<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:a10="http://www.w3.org/2005/Atom" version="2.0">
  <channel xmlns:media="http://search.yahoo.com/mrss/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <title>ABP.IO Stories</title>
    <link>https://abp.io/community/articles</link>
    <description>A hub for ABP Framework, .NET, and software development. Access articles, tutorials, news, and contribute to the ABP community.</description>
    <lastBuildDate>Fri, 09 Oct 2026 10:42:25 Z</lastBuildDate>
    <generator>Community - ABP.IO</generator>
    <image>
      <url>https://abp.io/assets/favicon.ico/favicon-32x32.png</url>
      <title>ABP.IO Stories</title>
      <link>https://abp.io/community/articles</link>
    </image>
    <a10:link rel="self" type="application/rss+xml" title="self" href="https://abp.io/community/rss?member=esozbek" />
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/zero-trust-microservice-architecture-with-abp-framework-xpiz9nvh</guid>
      <link>https://abp.io/community/posts/zero-trust-microservice-architecture-with-abp-framework-xpiz9nvh</link>
      <a10:author>
        <a10:name>esozbek</a10:name>
        <a10:uri>https://abp.io/community/members/esozbek</a10:uri>
      </a10:author>
      <category>microservices</category>
      <category>zero-trust</category>
      <title>Zero Trust Microservice Architecture with ABP Framework</title>
      <description>In this article we will explore how to integrate a zero trust microservice architecture with ABP Framework.</description>
      <pubDate>Wed, 16 Mar 2022 16:54:20 Z</pubDate>
      <a10:updated>2026-10-09T04:22:49Z</a10:updated>
      <content:encoded><![CDATA[<h1>Zero Trust Microservice Architecture with ABP Framework</h1>
<h2>Introduction</h2>
<p>In this article we will explore how to integrate a zero trust microservice architecture with ABP Framework. We will implement centralized permission management, tenant management and auditing. Each microservice will have it's own identity and permissions.</p>
<h3>What is zero trust?</h3>
<p><a href="https://en.wikipedia.org/wiki/Zero_trust_security_model">Zero trust</a> refers to a security model where you never implicitly trust a device or service and always verify instead. For example, many organizations trust all devices inside their network. In a zero trust environment, there is no difference between a device that is inside or outside a network.</p>
<h3>How does a zero trust microservice architecture work?</h3>
<p>In a zero trust microservice architecture, no service is trusted implicitly. All services must have an identity and explicit permissions to execute an action on other services. This way, if any service gets compromised, the attacker will only have as much authorization as the service itself, which should be limited to the service's dependencies.</p>
<h2>Getting started</h2>
<h3>Project Architecture</h3>
<p>The project will consist of 4 microservices:</p>
<ul>
<li>Two generic microservices where one depends on another for demonstration purposes</li>
<li>Identity microservice for authentication and authorization</li>
<li>Auditing microservice</li>
</ul>
<h4>Creating the Identity Service</h4>
<p>This service will host IdentityServer with the permission management and tenant management modules in the EntityFrameworkCore layer.</p>
<p>Create a new project using <code>abp new Sample.Identity -t app</code>. After that, remove all modules expect for permission management and tenant management.</p>
<p>After that create the following app service and DTOs in application contracts:</p>
<pre><code class="language-c#">namespace Sample.Identity;

public interface IPermissionCheckerAppService : IApplicationService
{
    Task&lt;bool&gt; CheckPermissionAsync(CheckPermissionInput input);

    Task&lt;MultiplePermissionGrantResultDto&gt; CheckPermissionsAsync(CheckPermissionsInput input);
}
</code></pre>
<pre><code class="language-c#">namespace Sample.Identity;

[Serializable]
public class CheckPermissionInput : EntityDto
{
    public string Id { get; set; }
    public string Type { get; set; }
    public string Name { get; set; }
}
</code></pre>
<pre><code class="language-c#">namespace Sample.Identity;

[Serializable]
public class CheckPermissionsInput : EntityDto
{
    public string Id { get; set; }
    public string Type { get; set; }
    public ICollection&lt;string&gt; Names { get; set; }
}
</code></pre>
<pre><code class="language-c#">namespace Sample.Identity;

[Serializable]
public class MultiplePermissionGrantResultDto
{
    public Dictionary&lt;string, PermissionGrantResult&gt; Result { get; set; }
}
</code></pre>
<p>and implement the service in the application layer to expose permission checking:</p>
<pre><code class="language-c#">namespace Sample.Identity;

[Authorize]
public class PermissionCheckerAppService : IPermissionCheckerAppService, ITransientDependency
{
    private readonly IPermissionStore _permissionStore;
    private readonly ILogger&lt;PermissionCheckerAppService&gt; _logger;
    private readonly IUserClaimsPrincipalFactory&lt;IdentityUser&gt; _userClaimsPrincipalFactory;
    private readonly IdentityUserManager _userManager;
    private readonly IPermissionChecker _permissionChecker;

    public PermissionCheckerAppService(
        IPermissionStore permissionStore,
        ILogger&lt;PermissionCheckerAppService&gt; logger,
        IUserClaimsPrincipalFactory&lt;IdentityUser&gt; userClaimsPrincipalFactory,
        IdentityUserManager userManager,
        IPermissionChecker permissionChecker)
    {
        _permissionStore = permissionStore;
        _logger = logger;
        _userClaimsPrincipalFactory = userClaimsPrincipalFactory;
        _userManager = userManager;
        _permissionChecker = permissionChecker;
    }

    [DisableAuditing]
    public virtual async Task&lt;bool&gt; CheckPermissionAsync(CheckPermissionInput input)
    {
        if (input.Type.Equals(&quot;Client&quot;, StringComparison.OrdinalIgnoreCase))
        {
            return await _permissionStore.IsGrantedAsync(input.Name, ClientPermissionValueProvider.ProviderName, input.Id);
        }

        var claimsPrincipal = await CreateUserClaimsPrincipal(input.Id, input.Type);
        var result = await _permissionChecker.IsGrantedAsync(claimsPrincipal, input.Name);

        _logger.LogInformation($&quot;Permission \&quot;{input.Name}\&quot; for {input.Id ?? &quot;&lt;anonymous&gt;&quot;} ({input.Type}): {(result ? &quot;Granted&quot; : &quot;Denied&quot;)}&quot;);
        return result;
    }

    [DisableAuditing]
    public virtual async Task&lt;MultiplePermissionGrantResultDto&gt; CheckPermissionsAsync(CheckPermissionsInput input)
    {
        var resultDto = new MultiplePermissionGrantResultDto();

        if (input.Type.Equals(&quot;Client&quot;, StringComparison.OrdinalIgnoreCase))
        {
            var result = await _permissionStore.IsGrantedAsync(input.Names.ToArray(), ClientPermissionValueProvider.ProviderName, input.Id);

            foreach (var r in result.Result)
            {
                resultDto.Result.Add(r.Key, r.Value);
            }
        }
        else
        {
            var claimsPrincipal = await CreateUserClaimsPrincipal(input.Id, input.Type);
            foreach (var name in input.Names)
            {
                var granted = await _permissionChecker.IsGrantedAsync(claimsPrincipal, name)
                    ? PermissionGrantResult.Granted
                    : PermissionGrantResult.Undefined;

                resultDto.Result.Add(name, granted);

                _logger.LogInformation(
                    $&quot;Permission \&quot;{name}\&quot; for {input.Id ?? &quot;&lt;anonymous&gt;&quot;} ({input.Type}): {(granted == PermissionGrantResult.Granted ? &quot;Granted&quot; : &quot;Denied&quot;)}&quot;);
            }
        }

        return resultDto;
    }

    [DisableAuditing]
    protected virtual async Task&lt;ClaimsPrincipal&gt; CreateUserClaimsPrincipal(string id, string type)
    {
        if (id == null)
        {
            return null;
        }

        var user = await _userManager.GetByIdAsync(Guid.Parse(id));
        if (user == null)
        {
            throw new EntityNotFoundException(typeof(IdentityUser), id);
        }

        return await _userClaimsPrincipalFactory.CreateAsync(user);
    }
}
</code></pre>
<h4>Creating the Products and Ordering Microservices</h4>
<p>These will be our sample generic microservices where one microservice will depend on another (in this case ordering will depend on products).</p>
<p>Create the services by using <code>abp new Sample.&lt;Name&gt; -t app --tiered</code> and by deleting the IdentityServer layer manually. Finally remove the EntityFrameworkCore modules for the feature-management, auditing and tenant-management modules as these will not be needed.</p>
<p>After that create a sample app service in products that orders will consume. Don't forget to add the permissions.</p>
<p><strong>Note:</strong> Each microservice's application contracts layer must be added to the identity host module so the permissions get registered.</p>
<h4>Creating the Auditing Microservice</h4>
<p>This service will host the full ABP auditing module.</p>
<p>Create a new project using <code>abp new Sample.Auditing -t app</code>. After that, remove all modules expect for auditing itself in the EntityFrameworkCore layer.</p>
<p>Similar to the identity service, we need to expose an app service for creating audit logs.</p>
<p>Start by creating the following service and DTOs in the application contracts layer:</p>
<pre><code class="language-c#">namespace Sample.Auditing;

public interface IAuditLogAppService : IApplicationService
{
    Task CreateAsync(AuditLogInfoDto logInfo);

    Task&lt;PagedResultDto&lt;AuditLogInfoDto&gt;&gt; GetListAsync(GetAuditLogsInput input);
}
</code></pre>
<pre><code class="language-c#">namespace Sample.Auditing;

[Serializable]
public class AuditLogInfoDto : EntityDto
{
    public string ApplicationName { get; set; }
    public Guid? UserId { get; set; }
    public string UserName { get; set; }
    public Guid? TenantId { get; set; }
    public string TenantName { get; set; }
    public Guid? ImpersonatorUserId { get; set; }
    public Guid? ImpersonatorTenantId { get; set; }
    public DateTime ExecutionTime { get; set; }
    public int ExecutionDuration { get; set; }
    public string ClientId { get; set; }
    public string CorrelationId { get; set; }
    public string ClientIpAddress { get; set; }
    public string ClientName { get; set; }
    public string BrowserInfo { get; set; }
    public string HttpMethod { get; set; }
    public int? HttpStatusCode { get; set; }
    public string Url { get; set; }
    public ICollection&lt;AuditLogActionInfoDto&gt; Actions { get; set; }
    public string Exceptions { get; set; }
    public string Comments { get; set; }
    public ICollection&lt;EntityChangeInfoDto&gt; EntityChanges { get; set; }
}
</code></pre>
<pre><code class="language-c#">namespace Sample.Auditing;

[Serializable]
public class AuditLogActionInfoDto
{
    public string ServiceName { get; set; }
    public string MethodName { get; set; }
    public string Parameters { get; set; }
    public DateTime ExecutionTime { get; set; }
    public int ExecutionDuration { get; set; }
}
</code></pre>
<pre><code class="language-c#">namespace Sample.Auditing;

[Serializable]
public class EntityChangeInfoDto
{
    public DateTime ChangeTime { get; set; }
    [JsonConverter(typeof(JsonStringEnumConverter))]
    public EntityChangeType ChangeType { get; set; }
    public Guid? EntityTenantId { get; set; }
    public string EntityId { get; set; }
    public string EntityTypeFullName { get; set; }
    public ICollection&lt;EntityPropertyChangeInfoDto&gt; PropertyChanges { get; set; }
}
</code></pre>
<pre><code class="language-c#">namespace Sample.Auditing;

[Serializable]
public class EntityPropertyChangeInfoDto
{
    public string NewValue { get; set; }
    public string OriginalValue { get; set; }
    public string PropertyName { get; set; }
    public string PropertyTypeFullName { get; set; }
}
</code></pre>
<pre><code class="language-c#">namespace Sample.Auditing;

[Serializable]
public class GetAuditLogsInput : PagedAndSortedResultRequestDto
{
    public DateTime? StartTime { get; set; }
    public DateTime? EndTime { get; set; }
    public string HttpMethod { get; set; }
    public string Url { get; set; }
    public Guid? UserId { get; set; }
    public string UserName { get; set; }
    public string ApplicationName { get; set; }
    public string ClientIpAddress { get; set; }
    public string CorrelationId { get; set; }
    public int? MaxExecutionDuration { get; set; }
    public int? MinExecutionDuration { get; set; }
    public bool? HasException { get; set; }
    [JsonConverter(typeof(JsonStringEnumConverter))]
    public HttpStatusCode? HttpStatusCode { get; set; }
    public string EntityTypeFullName { get; set; }
    public string EntityId { get; set; }
    public Guid? TenantId { get; set; }
    public Guid? EntityTenantId { get; set; }
}
</code></pre>
<p>and implement it in the application layer to expose auditing APIs:</p>
<pre><code class="language-c#">namespace Sample.Auditing;

public class AuditLogAppService : ApplicationService, IAuditLogAppService
{
    private readonly IAuditLogRepository _auditLogRepository;
    private readonly IGuidGenerator _guidGenerator;
    private readonly IRepository&lt;AuditLog&gt; _repository;

    public AuditLogAppService(
        IAuditLogRepository auditLogRepository,
        IGuidGenerator guidGenerator,
        IRepository&lt;AuditLog&gt; repository)
    {
        _auditLogRepository = auditLogRepository;
        _guidGenerator = guidGenerator;
        _repository = repository;
    }

    [Authorize]
    [DisableAuditing]
    [UnitOfWork(false, IsDisabled = true)]
    public Task CreateAsync(AuditLogInfoDto auditLogInfo)
    {
        // https://github.com/abpframework/abp/blob/dev/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain/Volo/Abp/AuditLogging/AuditLogInfoToAuditLogConverter.cs

        var auditLog = MapAuditLog(auditLogInfo);
        return _auditLogRepository.InsertAsync(auditLog, true);
    }

    protected virtual AuditLog MapAuditLog(AuditLogInfoDto auditLogInfo)
    {
        var auditLogId = _guidGenerator.Create();
        var constructor = typeof(AuditLog)
            .GetConstructor(BindingFlags.NonPublic | BindingFlags.CreateInstance | BindingFlags.Instance, null, new Type[0], null);

        if (constructor == null)
        {
            throw new Exception(&quot;Failed to find constructor in AuditLog&quot;);
        }

        var entityChanges = auditLogInfo
                                .EntityChanges?
                                .Select(entityChangeInfo =&gt; MapEntityChange(auditLogId, auditLogInfo.TenantId, entityChangeInfo))
                                .ToList()
                            ?? new List&lt;EntityChange&gt;();

        var actions = auditLogInfo
                          .Actions?
                          .Select(auditLogActionInfo =&gt; MapLogAction(auditLogId, auditLogInfo.TenantId, auditLogActionInfo))
                          .ToList()
                      ?? new List&lt;AuditLogAction&gt;();

        var auditLog = (AuditLog)constructor.Invoke(Array.Empty&lt;object&gt;());

        SetReflectionProperty(auditLog, &quot;Id&quot;, auditLogId);
        SetReflectionProperty(auditLog, &quot;ApplicationName&quot;, auditLogInfo.ApplicationName.Truncate(AuditLogConsts.MaxApplicationNameLength));
        SetReflectionProperty(auditLog, &quot;TenantId&quot;, auditLogInfo.TenantId);
        SetReflectionProperty(auditLog, &quot;TenantName&quot;, auditLogInfo.TenantName);
        SetReflectionProperty(auditLog, &quot;UserId&quot;, auditLogInfo.UserId);
        SetReflectionProperty(auditLog, &quot;UserName&quot;, auditLogInfo.UserName);
        SetReflectionProperty(auditLog, &quot;ExecutionTime&quot;, auditLogInfo.ExecutionTime);
        SetReflectionProperty(auditLog, &quot;ExecutionDuration&quot;, auditLogInfo.ExecutionDuration);
        SetReflectionProperty(auditLog, &quot;ClientIpAddress&quot;, auditLogInfo.ClientIpAddress.Truncate(AuditLogConsts.MaxClientIpAddressLength));
        SetReflectionProperty(auditLog, &quot;ClientName&quot;, auditLogInfo.ClientName.Truncate(AuditLogConsts.MaxClientNameLength));
        SetReflectionProperty(auditLog, &quot;ClientId&quot;, auditLogInfo.ClientId.Truncate(AuditLogConsts.MaxClientIdLength));
        SetReflectionProperty(auditLog, &quot;CorrelationId&quot;, auditLogInfo.CorrelationId.Truncate(AuditLogConsts.MaxCorrelationIdLength));
        SetReflectionProperty(auditLog, &quot;BrowserInfo&quot;, auditLogInfo.BrowserInfo.Truncate(AuditLogConsts.MaxBrowserInfoLength));
        SetReflectionProperty(auditLog, &quot;HttpMethod&quot;, auditLogInfo.HttpMethod.Truncate(AuditLogConsts.MaxHttpMethodLength));
        SetReflectionProperty(auditLog, &quot;Url&quot;, auditLogInfo.Url.Truncate(AuditLogConsts.MaxUrlLength));
        SetReflectionProperty(auditLog, &quot;HttpStatusCode&quot;, auditLogInfo.HttpStatusCode);
        SetReflectionProperty(auditLog, &quot;ImpersonatorUserId&quot;, auditLogInfo.ImpersonatorUserId);
        SetReflectionProperty(auditLog, &quot;ImpersonatorTenantId&quot;, auditLogInfo.ImpersonatorTenantId);
        SetReflectionProperty(auditLog, &quot;EntityChanges&quot;, entityChanges);
        SetReflectionProperty(auditLog, &quot;Actions&quot;, actions);
        SetReflectionProperty(auditLog, &quot;Exceptions&quot;, auditLogInfo.Exceptions);
        SetReflectionProperty(auditLog, &quot;Comments&quot;, auditLogInfo.Comments);

        return auditLog;
    }

    protected virtual AuditLogAction MapLogAction(Guid auditLogId, Guid? tenantId, AuditLogActionInfoDto auditLogActionInfo)
    {
        var constructor = typeof(AuditLogAction)
            .GetConstructor(BindingFlags.NonPublic | BindingFlags.CreateInstance | BindingFlags.Instance, null, new Type[0], null);

        if (constructor == null)
        {
            throw new Exception(&quot;Failed to find constructor in AuditLogAction&quot;);
        }

        var auditLogAction = (AuditLogAction)constructor.Invoke(new object[0]);
        SetReflectionProperty(auditLogAction, &quot;Id&quot;, _guidGenerator.Create());
        SetReflectionProperty(auditLogAction, &quot;TenantId&quot;, tenantId);
        SetReflectionProperty(auditLogAction, &quot;AuditLogId&quot;, auditLogId);
        SetReflectionProperty(auditLogAction, &quot;ExecutionTime&quot;, auditLogActionInfo.ExecutionTime);
        SetReflectionProperty(auditLogAction, &quot;ExecutionDuration&quot;, auditLogActionInfo.ExecutionDuration);
        SetReflectionProperty(auditLogAction, &quot;ServiceName&quot;, auditLogActionInfo.ServiceName.TruncateFromBeginning(AuditLogActionConsts.MaxServiceNameLength));
        SetReflectionProperty(auditLogAction, &quot;MethodName&quot;, auditLogActionInfo.MethodName.TruncateFromBeginning(AuditLogActionConsts.MaxMethodNameLength));
        SetReflectionProperty(auditLogAction, &quot;Parameters&quot;, auditLogActionInfo.Parameters.Length &gt; AuditLogActionConsts.MaxParametersLength ? &quot;&quot; : auditLogActionInfo.Parameters);

        return auditLogAction;
    }


    protected virtual EntityChange MapEntityChange(Guid auditLogId, Guid? tenantId, EntityChangeInfoDto entityChangeInfo)
    {
        var constructor = typeof(EntityChange)
            .GetConstructor(BindingFlags.NonPublic | BindingFlags.CreateInstance | BindingFlags.Instance, null, new Type[0], null);

        if (constructor == null)
        {
            throw new Exception(&quot;Failed to find constructor in EntityChange&quot;);
        }

        var id = _guidGenerator.Create();

        var propertyChanges = entityChangeInfo
                                  .PropertyChanges?
                                  .Select(p =&gt; MapEntityPropertyChange(id, tenantId, p))
                                  .ToList()
                              ?? new List&lt;EntityPropertyChange&gt;();

        var entityChange = (EntityChange)constructor.Invoke(Array.Empty&lt;object&gt;());
        SetReflectionProperty(entityChange, &quot;Id&quot;, id);
        SetReflectionProperty(entityChange, &quot;TenantId&quot;, tenantId);
        SetReflectionProperty(entityChange, &quot;AuditLogId&quot;, auditLogId);
        SetReflectionProperty(entityChange, &quot;ChangeTime&quot;, entityChangeInfo.ChangeTime);
        SetReflectionProperty(entityChange, &quot;ChangeType&quot;, entityChangeInfo.ChangeType);
        SetReflectionProperty(entityChange, &quot;EntityId&quot;, entityChangeInfo.EntityId.Truncate(EntityChangeConsts.MaxEntityTypeFullNameLength));
        SetReflectionProperty(entityChange, &quot;EntityTypeFullName&quot;, entityChangeInfo.EntityTypeFullName.TruncateFromBeginning(EntityChangeConsts.MaxEntityTypeFullNameLength));
        SetReflectionProperty(entityChange, &quot;PropertyChanges&quot;, propertyChanges);

        return entityChange;
    }

    protected virtual EntityPropertyChange MapEntityPropertyChange(Guid entityChangeId, Guid? tenantId, EntityPropertyChangeInfoDto entityPropertyChangeInfo)
    {
        var constructor = typeof(EntityPropertyChange)
            .GetConstructor(BindingFlags.NonPublic | BindingFlags.CreateInstance | BindingFlags.Instance, null, new Type[0], null);

        if (constructor == null)
        {
            throw new Exception(&quot;Failed to find constructor in EntityPropertyChange&quot;);
        }

        var entityChange = (EntityPropertyChange)constructor.Invoke(Array.Empty&lt;object&gt;());
        SetReflectionProperty(entityChange, &quot;Id&quot;, _guidGenerator.Create());
        SetReflectionProperty(entityChange, &quot;TenantId&quot;, tenantId);
        SetReflectionProperty(entityChange, &quot;EntityChangeId&quot;, entityChangeId);
        SetReflectionProperty(entityChange, &quot;NewValue&quot;, entityPropertyChangeInfo.NewValue.Truncate(EntityPropertyChangeConsts.MaxNewValueLength));
        SetReflectionProperty(entityChange, &quot;OriginalValue&quot;, entityPropertyChangeInfo.OriginalValue.Truncate(EntityPropertyChangeConsts.MaxOriginalValueLength));
        SetReflectionProperty(entityChange, &quot;PropertyName&quot;, entityPropertyChangeInfo.PropertyName.TruncateFromBeginning(EntityPropertyChangeConsts.MaxPropertyNameLength));
        SetReflectionProperty(entityChange, &quot;PropertyTypeFullName&quot;, entityPropertyChangeInfo.PropertyTypeFullName.TruncateFromBeginning(EntityPropertyChangeConsts.MaxPropertyTypeFullNameLength));

        return entityChange;
    }

    private void SetReflectionProperty(object o, string propertyName, object value)
    {
        var method = o.GetType()
            .GetProperty(propertyName, BindingFlags.Public
                               | BindingFlags.NonPublic
                               | BindingFlags.Static
                               | BindingFlags.Instance)?
            .GetSetMethod(true);

        if (method == null)
        {
            throw new Exception($&quot;Failed to find property setter for: {o.GetType()}.{propertyName}&quot;);
        }

        method.Invoke(o, new object[] { value });
    }
}
</code></pre>
<h3>Creating the Remoting Module</h3>
<p>The remoting module is responsible for centralizing permissions, tenants and auditing. It needs to be added to each microservice.</p>
<p>Create a new project called <code>Sample.Remoting</code> and add the following packages:</p>
<ul>
<li><code>Microsoft.AspNetCore.Http.Abstractions</code></li>
<li><code>Volo.Abp.AspNetCore.Mvc.Contracts</code></li>
<li><code>Volo.Abp.Auditing</code></li>
<li><code>Volo.Abp.Authorization.Abstractions</code></li>
<li><code>Volo.Abp.ExceptionHandling</code></li>
<li><code>Volo.Abp.Http.Client</code></li>
<li><code>Volo.Abp.ObjectMapping</code></li>
</ul>
<p>After that create the following options for configuring the module:</p>
<pre><code class="language-c#">namespace Sample.Remoting;

public class RemotingOptions
{
    public bool UseAuthentication { get; set; } = true;
    public bool UseRemoteAuditing { get; set; } = true;
    public bool UseRemotePermissionChecks { get; set; } = true;
}
</code></pre>
<h4>Authenticating with IdentityServer</h4>
<p>To implement authentication, we will create a <code>IRemotingTokenStore</code> service which will retrieve and cache the JWT token for the microservice. This service will also automatically renew the tokens before expiration.</p>
<p>First create the configuration for configuring the client:</p>
<pre><code class="language-c#">namespace Sample.Remoting;

public class RemotingClientOptions
{
    public string ClientName { get; set; }
    public string ClientSecret { get; set; }
    public string Scope { get; set; }
}
</code></pre>
<p>After that create and implement the service for retrieving the auth token from IdentityServer:</p>
<pre><code class="language-c#">namespace Sample.Remoting;

public interface IRemotingTokenStore
{
    Task&lt;string&gt; GetTokenAsync(CancellationToken cancellationToken);
}
</code></pre>
<pre><code class="language-c#">namespace Sample.Remoting;

public class RemotingTokenStore : IRemotingTokenStore, ISingletonDependency
{
    private readonly IOptions&lt;RemotingClientOptions&gt; _clientOptions;
    private readonly ILogger&lt;RemotingTokenStore&gt; _logger;
    private readonly IConfiguration _configuration;
    private readonly TimeSpan _refreshOffset = TimeSpan.FromMinutes(1);
    private readonly AsyncLocal&lt;Random&gt; _random;

    private string _token;

    public RemotingTokenStore(
        IOptions&lt;RemotingClientOptions&gt; clientOptions,
        ILogger&lt;RemotingTokenStore&gt; logger,
        IConfiguration configuration)
    {
        _clientOptions = clientOptions;
        _logger = logger;
        _configuration = configuration;

        _random = new AsyncLocal&lt;Random&gt;();
    }

    public virtual Task&lt;string&gt; GetTokenAsync(CancellationToken cancellationToken)
    {
        return GetTokenInternalAsync(cancellationToken);
    }

    protected virtual async Task&lt;string&gt; GetTokenInternalAsync(CancellationToken cancellationToken, int retryCount = 0)
    {
        if (!_token.IsNullOrEmpty())
        {
            return _token;
        }

        _logger.LogInformation($&quot;Logging in...&quot;);

        var clientId = _clientOptions.Value.ClientName;
        var clientSecret = _clientOptions.Value.ClientSecret;
        var scope = _clientOptions.Value.Scope;

        _random.Value ??= new Random();

        if (retryCount &gt; 0)
        {
            // Read https://dzone.com/articles/understanding-retry-pattern-with-exponential-back for more information on why we need a backoff delay
            var delay = CalculateBackoffDelay(retryCount);

            _logger.LogCritical($&quot;[#{retryCount}] Failed to log in, retrying in {delay} seconds...&quot;);

            await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken);
        }

        var authority = _configuration[&quot;AuthServer:Authority&quot;];
        var client = new HttpClient();
        var discovery = await client.GetDiscoveryDocumentAsync(authority, cancellationToken);
        if (discovery.IsError)
        {
            throw new Exception($&quot;Failed to get discovery document from {authority}: {discovery.Error}&quot;, discovery.Exception);
        }

        var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
        {
            Address = discovery.TokenEndpoint,
            ClientId = clientId,
            ClientSecret = clientSecret,
            Scope = scope
        }, cancellationToken);

        if (tokenResponse.IsError)
        {
            throw new Exception($&quot;Client login failed: {tokenResponse.Error}&quot;, tokenResponse.Exception);
        }

        _token = tokenResponse.AccessToken;

        StartRenewTask(client, discovery, tokenResponse, cancellationToken, retryCount);

        return _token;
    }

    protected virtual double CalculateBackoffDelay(int tryCount)
    {
        var delay = Math.Pow(2, tryCount) - 1;

        delay = Math.Max(delay, 180);

        var jitterPrcentage = 25;
        var lowerBoundary = delay * (100 - jitterPrcentage) / 100;
        var upperBoundary = delay * (100 + jitterPrcentage) / 100;

        delay += (upperBoundary - lowerBoundary) * _random.Value.NextDouble();

        return delay;
    }

    protected virtual void StartRenewTask(
        HttpClient client,
        DiscoveryDocumentResponse discovery,
        TokenResponse tokenResponse,
        CancellationToken cancellationToken,
        int retryCount)
    {
        var nextRefresh = tokenResponse.ExpiresIn;
        var refreshToken = tokenResponse.RefreshToken;

        Task.Factory.StartNew(async () =&gt;
        {
            renewDelay:
            await Task.Delay(TimeSpan.FromSeconds(nextRefresh) - _refreshOffset, cancellationToken);

            _logger.LogInformation($&quot;Refreshing client token...&quot;);

            var refreshResult = await client.RequestRefreshTokenAsync(new RefreshTokenRequest
            {
                Address = discovery.TokenEndpoint,
                RefreshToken = refreshToken
            }, cancellationToken: cancellationToken);

            if (refreshResult.IsError)
            {
                _logger.LogError($&quot;Client token refresh failed: &quot; + refreshResult.Error);

                await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
                await GetTokenInternalAsync(cancellationToken, ++retryCount);
                return;
            }

            retryCount = 0;
            nextRefresh = refreshResult.ExpiresIn;
            refreshToken = refreshResult.RefreshToken;
            _token = refreshResult.AccessToken;

            goto renewDelay;

        }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
    }
}
</code></pre>
<h4>Integrating Authentication for Dynamic Http Clients</h4>
<p>Implement a  <code>IRemoteServiceHttpClientAuthenticator</code> so that <a href="https://docs.abp.io/en/abp/4.4/API/Dynamic-CSharp-API-Clients">dynamic http clients</a> use the bearer token:</p>
<pre><code class="language-c#">namespace Sample.Remoting;

[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IRemoteServiceHttpClientAuthenticator))]
public class SampleServiceHttpClientAuthenticator : IRemoteServiceHttpClientAuthenticator, ISingletonDependency
{
    private readonly IOptions&lt;RemotingOptions&gt; _remotingOptions;
    private readonly IRemotingTokenStore _tokenStore;
    private readonly IHttpContextAccessor _httpContextAccessor;

    public SampleServiceHttpClientAuthenticator(
        IHttpContextAccessor httpContextAccessor,
        IOptions&lt;RemotingOptions&gt; remotingOptions,
        IRemotingTokenStore tokenStore)
    {
        _httpContextAccessor = httpContextAccessor;
        _remotingOptions = remotingOptions;
        _tokenStore = tokenStore;
    }

    public async Task Authenticate(RemoteServiceHttpClientAuthenticateContext context)
    {
        if (!_remotingOptions.Value.UseAuthentication)
        {
            return;
        }

        var cancellationToken = CancellationToken.None;
        if (_httpContextAccessor.HttpContext != null)
        {
            var httpContext = _httpContextAccessor.HttpContext;
            cancellationToken = httpContext.RequestAborted;
        }

        context.Request.Headers.Authorization = null;
        var authHeader = $&quot;Bearer {await _tokenStore.GetTokenAsync(cancellationToken)}&quot;;
        if (!string.IsNullOrEmpty(authHeader))
        {
            context.Request.Headers.Add(&quot;Authorization&quot;, authHeader);
        }
    }
}
</code></pre>
<h4>Audit Logging</h4>
<p>First add a reference to the auditing application contracts layer as we need to call the auditing API for storing audit logs. After that implement the following custom <code>IAuditingStore</code> which redirects all audit logs to the auditing service:</p>
<pre><code class="language-c#">namespace Sample.Remoting;

public class SampleAuditingStore : IAuditingStore
{
    private readonly IExceptionToErrorInfoConverter _exceptionToErrorInfoConverter;
    private readonly IAuditLogAppService _auditLogAppService;

    public SampleAuditingStore(
        IExceptionToErrorInfoConverter exceptionToErrorInfoConverter, 
        IAuditLogAppService auditLogAppService)
    {
        _exceptionToErrorInfoConverter = exceptionToErrorInfoConverter;
        _auditLogAppService = auditLogAppService;
    }

    public Task SaveAsync(AuditLogInfo auditInfo)
    {
        var auditInfoDto = MapAuditInfo(auditInfo);
        return _auditLogAppService.CreateAsync(auditInfoDto);
    }

    protected virtual AuditLogInfoDto MapAuditInfo(AuditLogInfo auditInfoDto)
    {
        var remoteServiceErrorInfos = auditInfoDto.Exceptions?
                                          .Select(exception =&gt;
                                              _exceptionToErrorInfoConverter.Convert(exception, true))
                                          .ToList()
                                      ?? new List&lt;RemoteServiceErrorInfo&gt;();


        return new()
        {
            Actions = auditInfoDto.Actions.Select(MapActions).ToList(),
            Comments = auditInfoDto.Comments.JoinAsString(Environment.NewLine),
            TenantId = auditInfoDto.TenantId,
            ExecutionTime = auditInfoDto.ExecutionTime,
            UserName = auditInfoDto.UserName,
            UserId = auditInfoDto.UserId,
            HttpStatusCode = auditInfoDto.HttpStatusCode,
            Url = auditInfoDto.Url,
            Exceptions = remoteServiceErrorInfos.Any()
                ? JsonSerializer.Serialize(remoteServiceErrorInfos, new JsonSerializerOptions
                {
                    WriteIndented = true
                })
                : null,
            ApplicationName = auditInfoDto.ApplicationName,
            BrowserInfo = auditInfoDto.BrowserInfo,
            ClientId = auditInfoDto.ClientId,
            ClientIpAddress = auditInfoDto.ClientIpAddress,
            ClientName = auditInfoDto.ClientName,
            CorrelationId = auditInfoDto.CorrelationId,
            EntityChanges = auditInfoDto.EntityChanges.Select(MapEntityChanges).ToList(),
            ExecutionDuration = auditInfoDto.ExecutionDuration,
            HttpMethod = auditInfoDto.HttpMethod,
            TenantName = auditInfoDto.TenantName,
            ImpersonatorTenantId = auditInfoDto.ImpersonatorTenantId,
            ImpersonatorUserId = auditInfoDto.ImpersonatorUserId,
        };
    }

    protected virtual AuditLogActionInfoDto MapActions(AuditLogActionInfo action)
    {
        return new()
        {
            ExecutionTime = action.ExecutionTime,
            ExecutionDuration = action.ExecutionDuration,
            MethodName = action.MethodName,
            Parameters = action.Parameters,
            ServiceName = action.ServiceName
        };
    }

    protected virtual EntityChangeInfoDto MapEntityChanges(EntityChangeInfo entityChange)
    {
        return new()
        {
            ChangeTime = entityChange.ChangeTime,
            ChangeType = entityChange.ChangeType,
            EntityId = entityChange.EntityId,
            EntityTenantId = entityChange.EntityTenantId,
            EntityTypeFullName = entityChange.EntityTypeFullName,
            PropertyChanges = entityChange.PropertyChanges.Select(MapPropertyChanges).ToList()
        };
    }

    private EntityPropertyChangeInfoDto MapPropertyChanges(EntityPropertyChangeInfo propertyChange)
    {
        return new()
        {
            NewValue = propertyChange.NewValue,
            OriginalValue = propertyChange.OriginalValue,
            PropertyName = propertyChange.PropertyName,
            PropertyTypeFullName = propertyChange.PropertyTypeFullName
        };
    }
}
</code></pre>
<h4>Resolving tenants</h4>
<p>Implement the following <code>ITenantStore</code> to resolve tenants from IdentityServer:</p>
<pre><code class="language-c#">namespace Sample.Remoting;

public class SampleTenantStore : ITenantStore
{
    private readonly IAbpTenantAppService _tenantAppService;

    public SampleTenantStore(
        IAbpTenantAppService tenantAppService)
    {
        _tenantAppService = tenantAppService;
    }

    public async Task&lt;TenantConfiguration&gt; FindAsync(string name)
    {
        var result = await _tenantAppService.FindTenantByNameAsync(name);
        if (!result.Success)
        {
            return null;
        }

        return new TenantConfiguration(result.TenantId!.Value, result.Name);
    }

    public async Task&lt;TenantConfiguration&gt; FindAsync(Guid id)
    {
        var result = await _tenantAppService.FindTenantByIdAsync(id);
        if (!result.Success)
        {
            return null;
        }

        return new TenantConfiguration(result.TenantId!.Value, result.Name);
    }

    public TenantConfiguration Find(string name)
    {
        return AsyncHelper.RunSync(() =&gt; FindAsync(name));
    }

    public TenantConfiguration Find(Guid id)
    {
        return AsyncHelper.RunSync(() =&gt; FindAsync(id));
    }
} 
</code></pre>
<h4>Checking Permissions</h4>
<p>Add a reference to the identity application contracts project. After that implement the following <code>IPermissionChecker</code> to redirect permission checks to IdentityServer:</p>
<pre><code class="language-c#">namespace Sample.Remoting;

public class SamplePermissionChecker : IPermissionChecker
{
    private readonly ICurrentUser _currentUser;
    private readonly ILogger&lt;SamplePermissionChecker&gt; _logger;
    private readonly IPermissionCheckerAppService _permissionAppService;
    private readonly IObjectMapper _mapper;

    public SamplePermissionChecker(
        ICurrentUser currentUser,
        ILogger&lt;SamplePermissionChecker&gt; logger,
        IPermissionCheckerAppService permissionAppService,
        IObjectMapper mapper)
    {
        _currentUser = currentUser;
        _logger = logger;
        _permissionAppService = permissionAppService;
        _mapper = mapper;
    }

    public async Task&lt;bool&gt; IsGrantedAsync(ClaimsPrincipal claimsPrincipal, string name)
    {
        var clientId = claimsPrincipal.FindClientId();
        var userId = claimsPrincipal.FindUserId()?.ToString();

        _logger.LogInformation($&quot;Checking permission {name} for principal {userId}/{clientId}&quot;);

        return await _permissionAppService.CheckPermissionAsync(new CheckPermissionInput
        {
            Name = name,
            Type = userId == null &amp;&amp; clientId != null ? &quot;Client&quot; : &quot;User&quot;,
            Id = claimsPrincipal.FindUserId()?.ToString() ?? clientId,
        });
    }

    public async Task&lt;bool&gt; IsGrantedAsync(string name)
    {
        _logger.LogInformation($&quot;Checking permission {name} for principal {_currentUser.Id}&quot;);

        return await _permissionAppService.CheckPermissionAsync(new CheckPermissionInput
        {
            Name = name,
            Type = &quot;User&quot;,
            Id = _currentUser.Id?.ToString(),
        });
    }

    public async Task&lt;MultiplePermissionGrantResult&gt; IsGrantedAsync(string[] names)
    {
        _logger.LogInformation($&quot;Checking permission {string.Join(&quot;, &quot;, names)} for {_currentUser.Id}&quot;);

        var result = await _permissionAppService.CheckPermissionsAsync(new CheckPermissionsInput
        {
            Names = names,
            Type = &quot;User&quot;,
            Id = _currentUser.Id?.ToString(),
        });

        return _mapper.Map&lt;MultiplePermissionGrantResultDto, MultiplePermissionGrantResult&gt;(result);
    }

    public async Task&lt;MultiplePermissionGrantResult&gt; IsGrantedAsync(ClaimsPrincipal claimsPrincipal, string[] names)
    {
        var clientId = claimsPrincipal.FindClientId();
        var userId = claimsPrincipal.FindUserId()?.ToString();

        _logger.LogInformation($&quot;Checking permission {string.Join(&quot;, &quot;, names)} for principal {userId}/{clientId}&quot;);

        var result = await _permissionAppService.CheckPermissionsAsync(new CheckPermissionsInput
        {
            Names = names,
            Type = userId == null &amp;&amp; clientId != null ? &quot;Client&quot; : &quot;User&quot;,
            Id = claimsPrincipal.FindUserId()?.ToString() ?? clientId
        });

        return _mapper.Map&lt;MultiplePermissionGrantResultDto, MultiplePermissionGrantResult&gt;(result);
    }
}
</code></pre>
<h4>Creating the Remoting ABP Module</h4>
<p>Finally create the remoting module and add
it to all microservices:</p>
<pre><code class="language-c#">namespace Sample.Remoting;

[DependsOn(
    typeof(IdentityApplicationContractsModule),
    typeof(AuditingApplicationContractsModule))]
public class RemotingModule : AbpModule
{
    public override void ConfigureServices(ServiceConfigurationContext context)
    {

        var configuration = context.Services.GetConfiguration();
        Configure&lt;RemotingClientOptions&gt;(configuration.GetSection(&quot;Remoting:Client&quot;));

        var options = context.Services.ExecutePreConfiguredActions&lt;RemotingOptions&gt;();

        if (options.UseRemoteAuditing)
        {
            context.Services.AddHttpClientProxies(
                typeof(AuditingApplicationContractsModule).Assembly,
                remoteServiceConfigurationName: &quot;Auditing&quot;
            );

            context.Services.Replace(ServiceDescriptor.Transient&lt;IAuditingStore, SampleAuditingStore&gt;());
        }

        if (options.UseRemotePermissionChecks)
        {
            context.Services.AddHttpClientProxies(
                typeof(IdentityApplicationContractsModule).Assembly,
                remoteServiceConfigurationName: &quot;Identity&quot;
            );

            context.Services.AddHttpClientProxies(
                typeof(AbpAspNetCoreMvcContractsModule).Assembly,
                remoteServiceConfigurationName: &quot;Identity&quot;
            );


            context.Services.Replace(ServiceDescriptor.Transient&lt;ITenantStore, SampleTenantStore&gt;());
            context.Services.Replace(ServiceDescriptor.Transient&lt;IPermissionChecker, SamplePermissionChecker&gt;());
        }
    }
}
</code></pre>
<h3>Configuring IdentityServer</h3>
<p>In Identity Server, create a client for each microservice. Make sure that the grant type is set to client credentials.</p>
<h3>Configuring the Microservices</h3>
<p>For each microservice add the following to the <code>appsettings.json</code>:</p>
<pre><code class="language-json">{
   ...
   &quot;AuthServer&quot;: {
       &quot;Authority&quot;: &quot;https://...&quot; # Set to identity service
   },
   &quot;RemoteServices&quot;: {
      &quot;Identity&quot;: { # Not needed on identity service
          &quot;BaseUrl&quot;: &quot;https://...&quot;
      }
      &quot;Auditing&quot;: { # Not needed on auditing service
          &quot;BaseUrl&quot;: &quot;https://...&quot; 
      }
   },
   &quot;Remoting&quot;: {
      &quot;Client&quot;: {
          &quot;ClientName&quot;: &quot;XXXXX&quot;,
          &quot;ClientSecret&quot;: &quot;XXXXX&quot;,
          &quot;Scope&quot;: &quot;XXXXX&quot;
      }
   }
}
</code></pre>
<p>For the identity microservice, disable remote permission checks in the host module:</p>
<pre><code class="language-c#">public override void PreConfigureServices(ServiceConfigurationContext context)
{
    PreConfigure&lt;RemotingOptions&gt;(o =&gt; o.UseRemotePermissionChecks = false);
}
</code></pre>
<p>Similarly, disable remote auditing for the auditing microservice:</p>
<pre><code class="language-c#">public override void PreConfigureServices(ServiceConfigurationContext context)
{
    PreConfigure&lt;RemotingOptions&gt;(o =&gt; o.UseRemoteAuditing = false);
}
</code></pre>
<h2>Conclusion</h2>
<p>We have created a zero trust microservice project where each microservice has it's own identity and permissions. All permissions and tenants are configured centralized in the identity service and all audit logs get saved in the auditing service's database.</p>
<h3>Caveats</h3>
<ul>
<li><strong>Lot's of dependencies</strong>: The identity service will have a dependency to all application contracts layers. You will have to recompile and redeploy the identity service every time a new microservice or permission is added. Otherwise, the permissions will not be registered and an exception will be thrown on permission check.
<ul>
<li>UPDATE: This is no longer required and solved with <a href="https://blog.abp.io/abp/ABP.IO-Platform-7.0-RC-Has-Been-Published">ABP Framework 7</a>.</li>
</ul>
</li>
<li><strong>Difficult debugging</strong>: On missing permissions, the remote exception is not forwarded correctly. You will only see a generic authorization exception but not which permission is missing.
<ul>
<li>This issue could be fixed by forwarding these exceptions correctly.</li>
</ul>
</li>
<li><strong>A new single point of failure</strong>: if the identity service goes down, nothing will work as authorization will not be possible.</li>
</ul>
<p><strong>Note</strong>: In this article we did not look into centralizing feature management and setting management.</p>
<ul>
<li>You can easily centralize them by implementing <code>IFeatureStore</code> and <code>ISettingStore</code> in the remoting module and redirecting them to the identity service.</li>
</ul>
<h2>Source Code</h2>
<p>The sample source code is available <a href="https://github.com/Trojaner/AbpZeroTrustArchitecture">here</a>.</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/c39992e6-1791-c17d-fe14-3a02a5d3b380" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/c39992e6-1791-c17d-fe14-3a02a5d3b380" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/creating-a-discord-bot-with-.net-and-the-abp-framework-usxl4e1k</guid>
      <link>https://abp.io/community/posts/creating-a-discord-bot-with-.net-and-the-abp-framework-usxl4e1k</link>
      <a10:author>
        <a10:name>esozbek</a10:name>
        <a10:uri>https://abp.io/community/members/esozbek</a10:uri>
      </a10:author>
      <category>discord-bot</category>
      <title>Creating a Discord Bot with .NET and the ABP Framework</title>
      <description>In this article we will explore how to integrate Discord .NET with ABP. </description>
      <pubDate>Mon, 14 Mar 2022 18:56:18 Z</pubDate>
      <a10:updated>2026-10-09T05:39:12Z</a10:updated>
      <content:encoded><![CDATA[<h1>What is Discord?</h1>
<p><a href="https://discord.com/">Discord</a> is a popular chat and VoIP platform. In this article we will write a Discord bot that integrates with ABP. This way we can use ABP features such as unit of work, authorization, users, etc.</p>
<p>Note: this article expects that you already have a running ABP website and that the bot will complement it.</p>
<p>ABP has also just created <a href="https://discord.gg/CrYrd5vcGh">it's own Discord server</a> too. Don't forget to join it!</p>
<h2>Discord Integration Libraries for .NET</h2>
<p>There are two popular unofficial Discord integration libraries for .NET: <a href="https://github.com/DSharpPlus/DSharpPlus">DSharpPlus</a> and <a href="https://github.com/discord-net/Discord.Net">Discord.NET</a>. We will use Discord .NET as it is modular and supports <a href="https://docs.microsoft.com/en-us/dotnet/core/extensions/generic-host">.NET Generic Host</a> integration via a <a href="https://github.com/Hawxy/Discord.Addons.Hosting">third party package</a>.</p>
<h1>Getting started</h1>
<p>We will create a Discord bot with .NET generic hosting support first.</p>
<p>Create a new .NET console project and name it Sample.DiscordBot.Host. After that, install <code>Discord.Net</code>, <code>Serilog.Extensions.Hosting</code>, <code>Serilog.Sinks.Console</code> and <code>Discord.Addons.Hosting</code> from NuGet.</p>
<h2>Setting up .NET Generic Host for Discord</h2>
<p>Replace the Main method with the following code:</p>
<pre><code class="language-c#">    
namespace Sample.DiscordBot
{
    public class Program
    {
        public static async Task&lt;int&gt; Main(string[] args)
        {
            Log.Logger = new LoggerConfiguration()
                .WriteTo.Console()
                .CreateLogger();

            try
            {
                var host = CreateHostBuilder(args).Build();
                using (host)
                {
                    await host.RunAsync();
                }

                return 0;
            }
            catch (Exception ex)
            {
                Log.Fatal(ex, &quot;Host terminated unexpectedly!&quot;);
                return 1;
            }
            finally
            {
                Log.CloseAndFlush();
            }
        }

        internal static IHostBuilder CreateHostBuilder(string[] args)
        {
            return Host
                .CreateDefaultBuilder(args)
                .UseCommandService((context, config) =&gt;
                {
                    config.LogLevel = LogSeverity.Verbose;
                    config.DefaultRunMode = RunMode.Async;
                })
                .ConfigureHostConfiguration(builder =&gt;
                {
                    ConfigureConfiguration(args, builder);
                })
                .ConfigureAppConfiguration(builder =&gt;
                {
                    ConfigureConfiguration(args, builder);
                })
                .UseSerilog()
                .UseConsoleLifetime();
        }

        internal static void ConfigureConfiguration(string[] args, IConfigurationBuilder builder)
        {
            builder
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile(&quot;appsettings.json&quot;, optional: false)
                .AddJsonFile($&quot;appsettings.{Environment.GetEnvironmentVariable(&quot;APP_ENVIRONMENT&quot;) ?? &quot;Production&quot;}.json&quot;, optional: true, reloadOnChange: true)
                .AddUserSecrets(typeof(Program).Assembly, true)
                .AddCommandLine(args)
                .AddEnvironmentVariables();
        }
    }
}
</code></pre>
<h2>Integrating ABP</h2>
<h3>Initializing ABP</h3>
<p>To initialze ABP, install the <code>Volo.Abp.Core</code> and <code>Volo.Abp.Autofac</code> packages from NuGet and call the ABP initialization service:</p>
<pre><code class="language-c#">...
using (host)
{
    // Initialize ABP
    var initializer = host.Services.GetRequiredService&lt;IAbpApplicationWithExternalServiceProvider&gt;();
    await initializer.InitializeAsync(host.Services);

    await host.RunAsync();
}
...
</code></pre>
<h3>Adding the Host module</h3>
<p>Create a new class called <code>DiscordBotHostModule</code>:</p>
<pre><code class="language-c#">namespace Sample.DiscordBot
{
    [DependsOn(typeof(AbpAutofacModule))]
    public class DiscordBotHostModule : AbpModule
    {
        public override void ConfigureServices(ServiceConfigurationContext context)
        {
            var configuration = context.Services.GetConfiguration();
            //...
        }
    }
}
</code></pre>
<p>Register the module in the host builder and add Autofac:</p>
<pre><code class="language-c#">return Host
    ...
    .ConfigureServices((_, services) =&gt;
    {
        services.AddApplication&lt;DiscordBotHostModule&gt;();
    })
    .UseAutofac()
    ...
</code></pre>
<h3>Linking Discord and ABP Users</h3>
<p>Create a new project, name it <code>Sample.DiscordBot.Application</code> and install <code>Discord.NET</code>, <code>Microsoft.AspNetCore.Identity</code> and <code>Volo.Abp.Ddd.Application</code>.</p>
<p>Create and implement the following interface:</p>
<pre><code class="language-c#">namespace Sample.DiscordBot.Authentication
{
    public interface IDiscordUserResolver
    {
        // Get's the ABP identity of a discord user
        // Return null if you fail to resolve a user (e.g. user is not registered or linked yet)
        Task&lt;ClaimsPrincipal&gt; ResolveAsync(IUser user);
    }
}
</code></pre>
<p>How you resolve users is up to you. For example, you could add a command like &quot;!link&quot; and redirect to your website with a token. Another alternative would be linking the discord account via the website with OpenID Connect. After that you could resolve the user with an API call to your identity backend.</p>
<p>Create and register a principal accessor and register it in the application module. The principal accessor will allow services and commands to resolve the current ABP user.</p>
<pre><code class="language-c#">namespace Sample.DiscordBot.Authentication
{
    [Dependency(ReplaceServices = true)]
    [ExposeServices(typeof(ICurrentPrincipalAccessor))]
    public class DiscordCurrentPrincipalAccessor : ICurrentPrincipalAccessor, IScopedDependency
    {
        public IDisposable Change(ClaimsPrincipal principal)
        {
            var previous = Principal;
            Principal = principal;
            return new DisposeAction(() =&gt; { Principal = previous; });
        }

        public ClaimsPrincipal Principal { get; set; }
    }
}
</code></pre>
<h3>Adding Permissions for Commands</h3>
<p>Create another new project, name it Sample.DiscordBot.Application.Contracts and install the <code>Volo.Abp.Ddd.Domain</code> and <code>Volo.Abp.Authorization.Abstractions</code> packages.</p>
<p>After that <a href="https://docs.abp.io/en/abp/4.4/Authorization#permission-system">create and register your permissions</a>:</p>
<pre><code class="language-c#">namespace Sample.DiscordBot.Permissions
{
   public class DiscordBotPermissions
   {
       public const string GroupName = &quot;SampleDiscordBot&quot;;

       public static class Commands
       {
           public const string Default = GroupName + &quot;.Commands&quot;;
           public const string Echo = Default + &quot;.Echo&quot;; 
       }
   }
}
</code></pre>
<pre><code class="language-c#">namespace Sample.DiscordBot.Permissions
{
    public class DiscordBotPermissionDefinitionProvider : PermissionDefinitionProvider
    {
        public override void Define(IPermissionDefinitionContext context)
        {
            if (context.GetGroupOrNull(DiscordBotPermissions.GroupName) != null)
            {
                return;
            }

            var discordPermisssionGroup = context.AddGroup(DiscordBotPermissions.GroupName);
            discordPermisssionGroup.AddPermission(DiscordBotPermissions.Commands.Echo);
        }
    }
}
</code></pre>
<p>Create the application contracts ABP module and register the permission definitions:</p>
<pre><code class="language-c#">namespace Sample.DiscordBot
{
    [DependsOn(
        typeof(AbpDddApplicationContractsModule)
    )]
    public class DiscordBotApplicationContractsModule : AbpModule
    {
        public override void ConfigureServices(ServiceConfigurationContext context)
        {
            Configure&lt;AbpPermissionOptions&gt;(options =&gt;
            {
               options.DefinitionProviders.Add&lt;DiscordBotPermissionDefinitionProvider&gt;();
            });	
        }
    }
}
</code></pre>
<p>Add the application contracts module as dependency to the host module.</p>
<h3>Handling Discord commands</h3>
<p>Similar to the principal accessor, create a command context accessor in the application layer so we can access the current command context from commands and services:</p>
<pre><code class="language-c#">namespace Sample.DiscordBot.Commands
{
    public interface IDiscordCommandContextAccessor
    {
        ICommandContext CommandContext { get; set; }
        int ArgsPos { get; set; }
    }
}
</code></pre>
<pre><code class="language-c#">namespace Sample.DiscordBot.Commands
{
    public class DiscordCommandContextAccessor : IDiscordCommandContextAccessor, IScopedDependency
    {
        public ICommandContext CommandContext { get; set; }
        public int ArgsPos { get; set; }
    }
}
</code></pre>
<p>Now we can implement the command handler. The command handler will</p>
<ul>
<li>create a unit of work scope for each command execution,</li>
<li>create a DI scope for each command execution for scoped dependencies,</li>
<li>set the current command context and</li>
<li>set the current user and principal.</li>
</ul>
<p>The command handler is the heart of the discord bot ABP integration. Add the following to the host module:</p>
<pre><code class="language-c#">namespace Sample.DiscordBot.Commands
{
    public class DiscordCommandHandler : ISingletonDependency, IDisposable
    {
        public const string CommandPrefix = &quot;!&quot;; // alternatively read it from the config
        
        private readonly ILogger&lt;DiscordCommandHandler&gt; _logger;
        private readonly IServiceProvider _serviceProvider;
        private readonly DiscordSocketClient _discordClient;
        private readonly IdentityOptions _identityOptions;
        private readonly CommandService _commandService;
        private readonly Dictionary&lt;ICommandContext, List&lt;IDisposable&gt;&gt; _disposables;

        private bool _isSubscribed;

        public DiscordCommandHandler(
            ILogger&lt;DiscordCommandHandler&gt; logger,
            IServiceProvider serviceProvider,
            DiscordSocketClient discordClient,
            IOptions&lt;IdentityOptions&gt; identityOptionsAccessor,
            CommandService commandService)
        {
            _disposables = new Dictionary&lt;ICommandContext, List&lt;IDisposable&gt;&gt;();
            _logger = logger;
            _serviceProvider = serviceProvider;
            _identityOptions = identityOptionsAccessor.Value;
            _discordClient = discordClient;
            _commandService = commandService;
        }

        public void Subscribe()
        {
            if (!_isSubscribed)
            {
                _discordClient.MessageReceived += HandleMessage;
                _commandService.CommandExecuted += CommandExecutedAsync;
                _isSubscribed = true;
            }
        }

        public void Unsubscribe()
        {
            if (_isSubscribed)
            {
                _discordClient.MessageReceived -= HandleMessage;
                _commandService.CommandExecuted -= CommandExecutedAsync;
                _isSubscribed = false;
            }
        }

        private async Task HandleMessage(SocketMessage incomingMessage)
        {
            if (!(incomingMessage is SocketUserMessage message 
                || message.Source != MessageSource.User)
            {
                // Message is not from a user
                return;
            }

            // Optionally log all messages
            // _logger.LogInformation($&quot;#{message.Channel.Name} &lt;{message.Author.Username}#{message.Author.Discriminator}&gt;: {message.Content}&quot;);
          
            var argPos = 0;
            if (!message.HasStringPrefix(CommandPrefix, ref argPos))
            {
                // message is not a command; ignore
                return;
            }
            
            var context = new SocketCommandContext(_discordClient, message);

            var scope = _serviceProvider.CreateScope();
            var uow = _serviceProvider.GetService&lt;IUnitOfWorkManager&gt;().Begin();

            var disposableContainer = new List&lt;IDisposable&gt; { uow, scope };
            _disposables.Add(context, disposableContainer);

            try
            {
                var contextAccessor = scope.ServiceProvider.GetRequiredService&lt;IDiscordCommandContextAccessor&gt;();
                contextAccessor.ArgsPos = argPos;
                contextAccessor.CommandContext = context;

                var userResolver= scope.ServiceProvider.GetRequiredService&lt;IDiscordUserResolver&gt;();
                var user = await userResolver.ResolveAsync(context.User);

                var discordPrincipalAccessor =
                    (DiscordCurrentPrincipalAccessor)scope.ServiceProvider
                        .GetRequiredService&lt;ICurrentPrincipalAccessor&gt;();
                discordPrincipalAccessor.Principal = user;

                await _commandService.ExecuteAsync(context, argPos, scope.ServiceProvider);
            }
            catch
            {
                DisposeContext(context);
                throw;
            }
        }

        public async Task CommandExecutedAsync(Optional&lt;CommandInfo&gt; command, ICommandContext context, IResult result)
        {
            DisposeContext(context);

            if (command.IsSpecified &amp;&amp; !result.IsSuccess)
            {
                // Error or exception occurred; notify user 
                var prefix = &quot;&quot;;
                if (context.Guild != null)
                {
                    // Not a private message; so ping user
                    prefix = $&quot;&lt;@!{context.User.Id}&gt;: &quot;;
                }

                await context.Channel.SendMessageAsync(prefix + result.ErrorReason);
            }
        }

        private void DisposeContext(ICommandContext context)
        {
            var disposables = _disposables[context];
            _disposables.Remove(context);

            foreach (var disposable in disposables)
            {
                disposable.Dispose();
            }
        }

        public void Dispose()
        {
            Unsubscribe();
        }
    }
} 
</code></pre>
<p>Update the host module to listen to Discord .NET events on application initialization:</p>
<pre><code class="language-c#">public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
    var commandHandler = context.ServiceProvider.GetRequiredService&lt;DiscordCommandHandler&gt;();
    commandHandler.Subscribe();
}
</code></pre>
<h3>Linking ABP Permissions</h3>
<p>Create a new attribute called RequireAuthorization:</p>
<pre><code class="language-c#">namespace Sample.DiscordBot.Authorization
{
    public class RequireAuthorizationAttribute : PreconditionAttribute
    {
        public string Permission { get; }

        public RequireAuthorizationAttribute(string permission = null)
        {
            Permission = permission;
        }

        public override async Task&lt;PreconditionResult&gt; CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)
        {
            var currentUser = services.GetRequiredService&lt;ICurrentUser&gt;();
            if (currentUser?.Id == null)
            {
                // Failed to resolve user
                return PreconditionResult.FromError(
                    &quot;You can not use this command because your discord account is not linked yet.\n&quot;);
            }

            if (string.IsNullOrEmpty(Permission))
            {
                return PreconditionResult.FromSuccess();
            }

            var permissionChecker = services.GetRequiredService&lt;IPermissionChecker&gt;();
            var isGranted = await permissionChecker.IsGrantedAsync(Permission);   

            if (!isGranted)
            {
                return PreconditionResult.FromError(
                    $&quot;You do not have access to this command (missing permission: {Permission}).&quot;);
            }

            return PreconditionResult.FromSuccess();
        }
    }
}
</code></pre>
<h3>Adding Commands</h3>
<p>Create the application module:</p>
<pre><code class="language-c#">namespace Sample.DiscordBot
{
    [DependsOn(
        typeof(AbpDddApplicationModule),
        typeof(DiscordBotApplicationContractsModule)
    )]
    public class DiscordBotApplicationModule : AbpModule
    {

    }
}
</code></pre>
<p>Now you can add Discord .NET command modules:</p>
<pre><code class="language-c#">public class PublicModule : ModuleBase&lt;SocketCommandContext&gt;
{
    private readonly ICurrentUser _currentUser;
    public PublicModule(
       ICurrentUser currentUser
    )
    {
        _currentUser = currentUser;
    }

    [Command(&quot;whoami&quot;)]
    public async Task WhoAmIAsync()
    {
        await ReplyAsync($&quot;You are user {_currentUser.UserName}/{_currentUser.Id}&quot;);
    }
    
    [Command(&quot;echo&quot;)]
    [RequireAuthorization(DiscordBotPermissions.Commands.Echo)]
    public async Task EchoAsync(string message) 
    {
        await ReplyAsync($&quot;You typed: {message}&quot;);
    }
}
</code></pre>
<p>Note: you don't have to register this module anywhere. Discord .NET will automatically register it.</p>
<h2>Adding the Discord Bot Token</h2>
<p>Create a Discord bot token as explained in <a href="https://www.writebots.com/discord-bot-token/">this article</a>.  After that add it to your appconfig.json like this:</p>
<pre><code class="language-json">{
   &quot;Discord&quot;: {
      &quot;Token&quot;: &quot;...&quot;
   }
}
</code></pre>
<p>Add the following to the host builder to read the token from the config:</p>
<pre><code class="language-c#">return Host
    ...
    .ConfigureDiscordHost((context, configurationBuilder) =&gt;
    {
        configurationBuilder.Token = context.Configuration[&quot;Discord:Token&quot;];
    })
</code></pre>
<h2>Conclusion</h2>
<p>We now have a working discord bot that integrates with ABP's  modularity, unit of work, principals/users and authorization. You can now easily use EntityFrameworkCore, auditing, local and distributed events, domain services,  i18n, etc.</p>
<h1>Source Code</h1>
<p>https://github.com/Trojaner/AbpDiscordBot</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/c2533108-90c2-b144-a578-3a029bf6a7be" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/c2533108-90c2-b144-a578-3a029bf6a7be" medium="image" />
    </item>
  </channel>
</rss>