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
toGridlab.PSSX.Odms.Application.Contracts
and then add the module dependency attribute to theOdmsApplicationContractsModule
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 toGridlab.PSSX.Odms.Application
and then add the module dependency toOdmsApplicationModule
.
[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 theGridlab.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 toappsettings.json
file of theGridlab.PSSX.Odms.HttpApi.Host
to configureClient Credential
access token request with client secret to theAuthServer
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 atGridlab.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
appsettingsIdentityClients
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 bothGridlab.PSSX.ModuleName.AuthServer
andGridlab.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 atGridlab.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
);
}
}
}
}