Skip to content

Microservice Context Mapping

Direct calls via network (Synchronous communication)

The Client Credentials flow is a server to server flow and there is no user authentication involved in the process. Instead of the user, the client ID (service itself) as the subject is authenticated with a predefined secret. The resulting access token contains client information instead of user information.

  • Do add the project reference of Gridlab.PSSX.DataManagement.Application.Contracts to Gridlab.PSSX.Odms.Application.Contracts and then add the module dependency attribute to the OdmsApplicationContractsModule class.
[DependsOn(
    typeof(DataManagementApplicationContractsModule),
    // ... other module layers will be here
)]
public class OdmsApplicationContractsModule : AbpModule
{

}

Now you can call File Descriptor Application Services from the Model Application Service:

[Authorize(OdmsPermissions.ModelManagement.Permission)]
[RequiresFeature(OdmsFeatures.ModelManagement)]
public class ModelAppService : OdmsAppService, IModelAppService
{
 protected IModelManager ModelManager { get; }

 protected IModelRepository ModelRepository { get; }

 /* Can use application services of others only if;
  * They are parts of another module / microservice.
  * The current module has only reference to the application contracts of the used module.
  */

 protected IFileDescriptorAppService FileDescriptorAppService { get; } //

 public ModelAppService(
     IModelManager modelManager,
     IModelRepository modelRepository,
     IFileDescriptorAppService fileDescriptorAppService)
 {
     ModelManager = modelManager;
     ModelRepository = modelRepository;
     FileDescriptorAppService = fileDescriptorAppService;
 }

Since there is no implementation of IFileDescriptorAppService in Gridlab.PSSX.Odms.Application, you need to configure it to make remote HTTP calls.

  • Do add the project reference of Gridlab.PSSX.DataManagement.HttpApi.Client project to Gridlab.PSSX.Odms.Application and then add the module dependency to OdmsApplicationModule.
[DependsOn(
    typeof(DataManagementHttpApiClientModule),
    // ... other module layers will be here
)]
public class OdmsApplicationModule : AbpModule
{
    public override void PreConfigureServices(ServiceConfigurationContext context)
    {
        PreConfigure<AbpHttpClientBuilderOptions>(options =>
        {
            options.ProxyClientBuildActions.Add((remoteServiceName, clientBuilder) =>
            {
                clientBuilder.AddTransientHttpErrorPolicy(policyBuilder =>
                    policyBuilder.WaitAndRetryAsync(
                        3,
                        i => TimeSpan.FromSeconds(Math.Pow(2, i))
                    )
                );
            });
        });
    }

If you want to add retry logic for the failing remote HTTP calls for the client proxies, you can configure the AbpHttpClientBuilderOptions in the PreConfigureServices method of your module class.

This example uses the Microsoft.Extensions.Http.Polly package. You also need to import the Polly namespace (using Polly;) to be able to use the WaitAndRetryAsync method.

Open the appsettings.json file in the Gridlab.PSSX.Odms.HttpApi.Host project and add RemoteServices section to route HTTP requests to the where Gridlab.PSSX.DataManagement.Application is hosted. In this case Gridlab.PSSX.Odms.AuthServer is hosting.

appsettings.json of Gridlab.PSSX.Odms.HttpApi.Host project should look like below:

"RemoteServices": {
  "FileManagement": {
    "BaseUrl": "https://localhost:44397", // Remote adress of Gridlab.PSSX.Odms.AuthServer
    "UseCurrentAccessToken": "false"
  }
}
  • Do sort module layers before common layers in single instance version of PSSX.

Example HttpApi:

[DependsOn(  
    // ... other module layers will be here
    // Module Packages
    typeof(OdmsHttpApiModule),
    // Base Packages
    typeof(DataManagementHttpApiModule),
    //
    typeof(PSSXApplicationContractsModule)
)]
public class PSSXHttpApiModule : AbpModule
{

}

Example Web Hosting:

[DependsOn(
   // ... other module layers will be here
   typeof(OdmsWebModule), // main module
   typeof(DataManagementWebModule), // base module
   //
   typeof(PSSXHttpApiClientModule),
   typeof(PSSXHttpApiModule)
)]
public class PSSXWebModule : AbpModule
{

}

Configuring Auto-Discovery Endpoint

  • Do to automate requesting access token and adding it as bearer to the request headers; add Volo.Abp.Http.Client.IdentityModel NuGet package to the Gridlab.PSSX.Odms.HttpApi.Host project.

  • Do open Gridlab.PSSX.Odms.HttpApi.Host and add the following line (update the version attribute according to your project!)

  <PackageReference Include="Volo.Abp.Http.Client.IdentityModel" Version="7.3.1" />
  • Do add DependsOn attribute, open OdmsHttpApiHostModule.cs class and add the following module dependency
[DependsOn(
    typeof(AbpHttpClientIdentityModelModule),
    // ... other module layers will be here
)]
public class OdmsHttpApiHostModule : AbpModule
{
    public override void ConfigureServices(ServiceConfigurationContext context)
    {

    }
}
  • Do add IdentityClients section to appsettings.json file of the Gridlab.PSSX.Odms.HttpApi.Host to configure Client Credential access token request with client secret to the AuthServer end point.
"IdentityClients": {
  "Default": {
    "GrantType": "client_credentials",
    "ClientId": "DataManagement_App",
    "ClientSecret": "1q2w3e*",
    "Authority": "https://localhost:44397",
    "Scope": "Odms"
  }
}

OpenIdDict Configuration

Create New Client:

DataManagement_App itself is a new client and you need to add the Odms as a client under CreateApplicationsAsync as well. Open IdentityServerDataSeeder.cs and add the below line in

  • Do open OdmsAuthServerDataSeeder.cs and add the below lines at Gridlab.PSSX.Odms.AuthServer project.
// Service Client
var serviceClientId = configurationSection["DataManagement_App:ClientId"];
if (!serviceClientId.IsNullOrWhiteSpace())
{
    await CreateApplicationAsync(
        name: serviceClientId,
        type: OpenIddictConstants.ClientTypes.Confidential,
        consentType: OpenIddictConstants.ConsentTypes.Implicit,
        displayName: "DataManagement Service",
        secret: configurationSection["DataManagement_App:ClientSecret"] ?? "1q2w3e*",
        grantTypes: new List<string>
        {
            OpenIddictConstants.GrantTypes.ClientCredentials,
        },
        scopes: commonScopes,
        permissions: FileManagementPermissions.GetAll().ToList() // Since Odms will use every possible method of the FileDescriptorAppService, we are adding all possible permission for this client.
    );
}

Gridlab.PSSX.Odms.HttpApi.Host appsettings IdentityClients section data must match with the client creation data.

FileDescriptorAppService has permission authorization and will return 401 unauthorized exception when a request has been made. To overcome this issue, add the required permissions of the service you are making to.

public static class FileManagementPermissions
{
    public const string GroupName = "FileManagement";

    public static class DirectoryDescriptor
    {
        public const string Default = GroupName + ".DirectoryDescriptor";

        public const string Create = Default + ".Create";
        public const string Update = Default + ".Update";
        public const string Delete = Default + ".Delete";
    }

    public static class FileDescriptor
    {
        public const string Default = GroupName + ".FileDescriptor";

        public const string Create = Default + ".Create";
        public const string Update = Default + ".Update";
        public const string Delete = Default + ".Delete";
    }

    public static string[] GetAll()
    {
        return ReflectionHelper.GetPublicConstantsRecursively(typeof(FileManagementPermissions));
    }
}

Events via message queues (Asynchronous communication)

  • Do publishing bounded context publishes an event, subscribing bounded context subscribes to it, this approach doesn't suffer from the temporal issues assoicated with REST communication.

  • Do install RabbitMQ message broker. See Installing RabbitMQ

  • Do install Volo.Abp.EventBus.RabbitMQ module both Gridlab.PSSX.ModuleName.AuthServer and Gridlab.PSSX.ModuleName.HttpApiHost hosting projects

use the DependsOn attribute to include the installed package services.

Example Publishing bounded context:

[DependsOn(
    // ... other module layers will be here
    typeof(AbpEventBusRabbitMqModule)
)]
public class ModuleNameAuthServerModule : AbpModule

Example Subscribing bounded context:

[DependsOn(
    // ... other module layers will be here
    typeof(AbpEventBusRabbitMqModule)
)]
public class ModuleNameHttpApiHostModule : AbpModule

configure rabbitmq parameters:

Configure<AbpRabbitMqOptions>(options =>
{
    options.Connections.Default.Ssl.Enabled = bool.Parse(configuration["RabbitMQ:IsSslEnabled"] ?? "false");
    options.Connections.Default.UserName = configuration["RabbitMQ:Connections:Default:UserName"] ?? "user";
    options.Connections.Default.Password = configuration["RabbitMQ:Connections:Default:Password"] ?? "bitnami";
    options.Connections.Default.HostName = configuration["RabbitMQ:Connections:Default:HostName"] ?? "localhost";
    options.Connections.Default.Port = int.Parse(configuration["RabbitMQ:Connections:Default:Port"] ?? (bool.Parse(configuration["RabbitMQ:IsSslEnabled"]) ? "5671" : "5672"));

    if (bool.Parse(configuration["RabbitMQ:IsSslEnabled"]))
        options.Connections.Default.Uri = new Uri(configuration["RabbitMQ:Connections:Default:Uri"]); // Uri can't be null
});

Configure<AbpRabbitMqEventBusOptions>(options =>
{
    options.ClientName = configuration["RabbitMQ:EventBus:ClientName"];
    options.ExchangeType = configuration["RabbitMQ:EventBus:ExchangeType"];
    options.ExchangeName = configuration["RabbitMQ:EventBus:ExchangeName"];
});

Example Publishing bounded context:

"RabbitMQ": {
  "IsSslEnabled": "false",
  "Connections": {
    "Default": {
      "HostName": "127.0.0.1",
      "Port": "5672",
      "UserName": "user",
      "Password": "bitnami",
      "Uri": "amqps://127.0.0.1:5671"
    }
  },
  "EventBus": {
    "ClientName": "AuthServer", // <== Client Name is the name of this application
    "ExchangeType": "topic", // Options: direct, topic, fanout, headers 
    "ExchangeName": "PSSX"
  }
}

Example Subscribing bounded context:

"RabbitMQ": {
  "IsSslEnabled": "false",
  "Connections": {
    "Default": {
      "HostName": "127.0.0.1",
      "Port": "5672",
      "UserName": "user",
      "Password": "bitnami",
      "Uri": "amqps://127.0.0.1:5671"
    }
  },
  "EventBus": {
    "ClientName": "Odms", // <== Client Name is the name of this application
    "ExchangeType": "topic", // Options: direct, topic, fanout, headers 
    "ExchangeName": "PSSX"
  }
}
  • Do define IDistributedEventHandler to hande domain or entity events

Example UserSynchronizer:

public class OdmsUserSynchronizer : 
    IDistributedEventHandler<EntityUpdatedEto<UserEto>>,
    ITransientDependency
{
    protected IOdmsUserRepository UserRepository;

    protected IOdmsUserLookupService UserLookupService;

    private readonly ILogger<OdmsUserSynchronizer> _logger;

    public OdmsUserSynchronizer(
        IOdmsUserRepository userRepository,
        IOdmsUserLookupService userLookupService,
        ILogger<OdmsUserSynchronizer> logger)
    {
        UserRepository = userRepository;
        UserLookupService = userLookupService;

        _logger = logger;
    }

    public virtual async Task HandleEventAsync(EntityUpdatedEto<UserEto> eventData)
    {
        if (eventData.Entity.TenantId.HasValue)
        {
            var user = await UserRepository.FindAsync(eventData.Entity.Id);
            if (user == null)
            {
                user = await UserLookupService.FindByIdAsync(eventData.Entity.Id);
                if (user == null)
                { 
                    return;
                }
            }

            _logger.LogInformation($"Syncing {eventData.Entity.UserName} user information.");

            if (user.Update(eventData.Entity))
            {
                await UserRepository.UpdateAsync(user);
            }
        }
    }
}
  • Do define IDistributedEventHandler to hande persona creation at Gridlab.PSSX.ModuleName.HttpApiHost project.
public class PersonaCreationHandler :
    IDistributedEventHandler<EntityCreatedEto<UserEto>>,
    ITransientDependency
{
    protected IOdmsUserRepository UserRepository;

    protected IOdmsUserLookupService UserLookupService;

    private readonly ILogger<OdmsUserSynchronizer> _logger;

    public PersonaCreationHandler(
        IOdmsUserRepository userRepository,
        IOdmsUserLookupService userLookupService,
        ILogger<OdmsUserSynchronizer> logger)
    {
        UserRepository = userRepository;
        UserLookupService = userLookupService;

        _logger = logger;
    }

    /* If you perform database operations and use the repositories inside the event handler
     * You may need to create a unit of work, because some repository methods need to work inside an active unit of work.
     * Make the handle method virtual and add a [UnitOfWork] attribute for the method
     */

    [UnitOfWork]
    public virtual async Task HandleEventAsync(EntityCreatedEto<UserEto> eventData)
    {
        if (eventData.Entity.TenantId.HasValue)
        {
            var user = await UserRepository.FindAsync(eventData.Entity.Id);
            if (user == null)
            {
                _logger.LogInformation($"Creating {eventData.Entity.UserName} user information.");

                await UserRepository.InsertAsync(
                    entity: new OdmsUser(eventData.Entity),
                    autoSave: false
                );
            }
        }
    }
}