Microservice Demo Solution

“Microservices are a software development technique—a variant of the service-oriented architecture (SOA) architectural style that structures an application as a collection of loosely coupled services. In a microservices architecture, services are fine-grained and the protocols are lightweight. The benefit of decomposing an application into different smaller services is that it improves modularity. This makes the application easier to understand, develop, test, and become more resilient to architecture erosion. It parallelizes development by enabling small autonomous teams to develop, deploy and scale their respective services independently. It also allows the architecture of an individual service to emerge through continuous refactoring. Microservices-based architectures enable continuous delivery and deployment.”

Wikipedia

Introduction

One of the major goals of the ABP framework is to provide a convenient infrastructure to create microservice solutions.

This sample aims to demonstrate a simple yet complete microservice solution;

  • Has multiple, independent, self-deployable microservices.
  • Multiple web applications, each uses a different API gateway.
  • Has multiple gateways / BFFs (Backend for Frontends) developed using the Ocelot library.
  • Has an authentication service developed using the IdentityServer framework. It’s also a SSO (Single Sign On) application with necessary UIs.
  • Has multiple databases. Some microservices has their own database while some services/applications shares a database (to demonstrate different use cases).
  • Has different types of databases: SQL Server (with Entity Framework Core ORM) and MongoDB.
  • Has a console application to show the simplest way of using a service by authenticating.
  • Uses Redis for distributed caching.
  • Uses RabbitMQ for service-to-service messaging.
  • Uses Docker & Kubernates to deploy & run all services and applications.
  • Uses Elasticsearch & Kibana to store and visualize the logs (written using Serilog).

The diagram below shows the system:

microservice-sample-diagram-2

Source Code

You can get the source code from the GitHub repository.

Running the Solution

Pre Requirements

To be able to run the solution from source code, following tools should be installed and running on your computer:

Running Infrastructure

  • Docker-compose is used to run the pre requirements with ease as default. If you don’t have it, you can download and start using Docker for Windows from here on windows environment.
  • Run the command docker-compose -f docker-compose.infrastructure.yml -f docker-compose.infrastructure.override.yml up -d at MicroserviceDemo directory or run the powershell script __Run_Infrastructure.ps1 located at MicroserviceDemo/_run directory.
  • If you don’t want to use docker for pre required services and install them on your local development, you need to update appsettings.json files of the projects in the MicroserviceDemo solution accordingly.

Open & Build the Visual Studio Solution

  • Open the samples\MicroserviceDemo\MicroserviceDemo.sln in Visual Studio 2017 (15.9.0+).
  • Run dotnet restore from the command line inside the samples\MicroserviceDemo folder.
  • Build the solution in Visual Studio.

Create Databases

MongoDB database is created dynamically, however you need to create database schemas for SQL server databases. The solution is configured to use Entity Core Code First migrations, so you can easily create databases.

There are two SQL server databases in this solution.

MsDemo_Identity Database

  • Right click to the AuthServer.Host project and click to the Set as startup project.
  • Open the Package Manager Console (Tools -> Nuget Package Manager -> Package Manager Console)
  • Select AuthServer.Host as the Default project.
  • Run Update-Database command.

microservice-sample-update-database-authserver

MsDemo_ProductManagement

  • Right click to the ProductService.Host project and click to the Set as startup project.
  • Open the Package Manager Console (Tools -> Nuget Package Manager -> Package Manager Console)
  • Select ProductService.Host as the Default project.
  • Run Update-Database command.

microservice-sample-update-database-products

Run Projects

Run the projects with the following order (right click to each project, set as startup project an press Ctrl+F5 to run without debug):

  • AuthServer.Host
  • IdentityService.Host
  • TenantManagementService.Host
  • BloggingService.Host
  • ProductService.Host
  • InternalGateway.Host
  • BackendAdminAppGateway.Host
  • PublicWebSiteGateway.Host
  • BackendAdminApp.Host
  • PublicWebSite.Host

When you run projects, they will add some initial demo data to their databases.

A Brief Overview of the Solution

The Visual Studio solution consists of multiple projects each have different roles in the system:

microservice-sample-solution

Applications

These are the actual applications those have user interfaces to interact to the users and use the system.

  • AuthServer.Host: Host the IdentityServer4 to provide an authentication service to other services and applications. It is a single-sign server and contains the login page.
  • BackendAdminApp.Host: This is a backend admin application that host UI for Identity and Product management modules.
  • PublicWebSite.Host: As public web site that contains a simple product list page and blog module UI.
  • ConsoleClientDemo: A simple console application to demonstrate the usage of services from a C# application.

Gateways / BFFs (Backend for Frontend)

Gateways are used to provide a single entry point to the applications. It can also used for rate limiting, load balancing… etc. Used the Ocelot library.

  • BackendAdminAppGateway.Host: Used by the BackendAdminApp.Host application as backend.
  • PublicWebSiteGateway.Host: Used by the PublicWebSite.Host application as backend.
  • InternalGateway.Host: Used for inter-service communication (the communication between microservices).

Microservices

Microservices have no UI, but exposes some REST APIs.

  • IdentityService.Host: Hosts the ABP Identity module which is used to manage users & roles. It has no additional service, but only hosts the Identity module’s API.
  • TenantManagementService.Host: Hosts the ABP Tenant Management module which is used to manage roles. It has no additional service, but only hosts the Tenant Management module’s API.
  • BloggingService.Host: Hosts the ABP Blogging module which is used to manage blog & posts (a typical blog application). It has no additional service, but only hosts the Blogging module’s API.
  • ProductService.Host: Hosts the Product module (that is inside the solution) which is used to manage products. It also contains the EF Core migrations to create/update the Product Management database schema.

Modules

  • Product: A layered module that is developed with the module development best practices. It can be embedded into a monolithic application or can be hosted as a microservice by separately deploying API and UI (as done in this demo solution).

Databases

This solution is using multiple databases:

  • MsDemo_Identity: An SQL database. Used SQL Server by default, but can be any DBMS supported by the EF Core. Shared by AuthServer, IdentityService and the TenantManagementService. Also audit logs, permissions and settings are stored in this database (while they could have their own databases, shared the same database to keep it simple).
  • MsDemo_ProductManagement: An SQL database. Again, used SQL Server by default, but can be any DBMS supported by the EF Core. Used by the ProductService as a dedicated database.
  • MsDemo_Blogging: A MongoDB database. Used by the BloggingService.
  • Elasticsearch: Used to write logs over Serilog.

Applications

Authentication Server (AuthServer.Host)

This project is used by all other services and applications for authentication & single sign on. Mainly, uses IdentityServer4 to provide these services. It uses some of the pre-build ABP modules like Identity, Audit Logging and Permission Management.

Database & EF Core Configuration

This application uses a SQL database (named it as MsDemo_Identity) and maintains its schema via Entity Framework Core migrations.

It has a DbContext named AuthServerDbContext and defined as shown below:

  1. public class AuthServerDbContext : AbpDbContext<AuthServerDbContext>
  2. {
  3. public AuthServerDbContext(DbContextOptions<AuthServerDbContext> options)
  4. : base(options)
  5. {
  6. }
  7. protected override void OnModelCreating(ModelBuilder modelBuilder)
  8. {
  9. base.OnModelCreating(modelBuilder);
  10. modelBuilder.ConfigureIdentity();
  11. modelBuilder.ConfigureIdentityServer();
  12. modelBuilder.ConfigureAuditLogging();
  13. modelBuilder.ConfigurePermissionManagement();
  14. modelBuilder.ConfigureSettingManagement();
  15. }
  16. }

In the OnModelCreating, you see ConfigureX() method calls. A module with a database schema generally declares such an extension method to configure EF Core mappings for its own entities. This is a flexible approach where you can arrange your databases and modules inside them; You can use a different database for each module, or combine some of them in a shared database. In the AuthServer project, we decided to combine multiple module schemas in a single EF Core DbContext, in a single physical database. These modules are Identity, IdentityServer, AuditLogging, PermissionManagement and SettingManagement modules.

Notice that this DbContext is only for database migrations. All modules have their own DbContext classes those are used in the runtime by the modules.

User Interface

AuthServer has a simple home page that shows the current user info if the current user has logged in:

microservice-sample-authserver-home

It also provides Login & Register pages:

microservice-sample-authserver-login

These pages are not included in the project itself. Instead, AuthServer project uses the prebuilt ABP account module with IdentityServer extension. That means it can also act as an OpenId Connect server with necessary UI and logic.

Dependencies

  • RabbitMQ for messaging to other services.
  • Redis for distributed/shared caching.
  • Elasticsearch for storing logs.

Backend Admin Application (BackendAdminApp.Host)

This is a web application that is used to manage users, roles, permissions and products in the system.

Authentication

BackendAdminApp redirects to the AuthServer for authentication. Once the user enters a correct username & password, the page is redirected to the backend application again. Authentication configuration is setup in the BackendAdminAppHostModule class:

  1. context.Services.AddAuthentication(options =>
  2. {
  3. options.DefaultScheme = "Cookies";
  4. options.DefaultChallengeScheme = "oidc";
  5. })
  6. .AddCookie("Cookies", options =>
  7. {
  8. options.Cookie.Expiration = TimeSpan.FromDays(365);
  9. options.ExpireTimeSpan = TimeSpan.FromDays(365);
  10. })
  11. .AddOpenIdConnect("oidc", options =>
  12. {
  13. options.Authority = configuration["AuthServer:Authority"];
  14. options.ClientId = configuration["AuthServer:ClientId"];
  15. options.ClientSecret = configuration["AuthServer:ClientSecret"];
  16. options.RequireHttpsMetadata = false;
  17. options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
  18. options.SaveTokens = true;
  19. options.GetClaimsFromUserInfoEndpoint = true;
  20. options.Scope.Add("role");
  21. options.Scope.Add("email");
  22. options.Scope.Add("phone");
  23. options.Scope.Add("BackendAdminAppGateway");
  24. options.Scope.Add("IdentityService");
  25. options.Scope.Add("ProductService");
  26. options.ClaimActions.MapAbpClaimTypes();
  27. });
  • It adds “Cookies” authentication as the primary authentication type.
  • “oidc” authentication is configured to use the AuthServer application as the authentication server.
  • It requires the additional identity scopes role, email and phone.
  • It requires the API resource scopes BackendAdminAppGateway, IdentityService and ProductService because it will use these services as APIs.

IdentityServer client settings are stored inside the appsettings.json file:

  1. "AuthServer": {
  2. "Authority": "http://localhost:64999",
  3. "ClientId": "backend-admin-app-client",
  4. "ClientSecret": "1q2w3e*"
  5. }

User Interface

The BackendAdminApp.Host project itself has not a single UI element/page. It is only used to serve UI pages of the Identity and Product Management modules. BackendAdminAppHostModule adds dependencies to AbpIdentityWebModule (Volo.Abp.Identity.Web package) and ProductManagementWebModule (ProductManagement.Web project) for that purpose.

A screenshot from the user management page:

microservice-sample-backend-ui

A screenshot from the permission management modal for a role:

microservice-sample-backend-ui-permissions

Using Microservices

Backend admin application uses the Identity and Product microservices for all operations, over the Backend Admin Gateway (BackendAdminAppGateway.Host).

Remote End Point

appsettings.json file contains the RemoteServices section to declare the remote service endpoint(s). Each microservice will normally have different endpoints. However, this solution uses the API Gateway pattern to provide a single endpoint for the applications:

  1. "RemoteServices": {
  2. "Default": {
  3. "BaseUrl": "http://localhost:65115/"
  4. }
  5. }

http://localhost:65115/ is the URL of the BackendAdminAppGateway.Host project. It knows where are Identity and Product services are located.

HTTP Clients

ABP application modules generally provides C# client libraries to consume services (APIs) easily (they generally uses the Dynamic C# API Clients feature of the ABP framework). That means if you need to consume Identity service API, you can reference to its client package and easily use the APIs by provided interfaces.

For that purpose, BackendAdminAppHostModule class declares dependencies for AbpIdentityHttpApiClientModule and ProductManagementHttpApiClientModule.

Once you refer these client packages, you can directly inject an application service interface (e.g. IIdentityUserAppService) and use its methods like a local method call. It actually invokes remote service calls over HTTP to the related service endpoint.

Passing the Access Token

Since microservices requires authentication & authorization, each remote service call should contain an Authentication header. This header is obtained from the access_token inside the current HttpContext for the current user. This is automatically done when you use the Volo.Abp.Http.Client.IdentityModel package. BackendAdminAppHostModule declares dependencies to this package and to the related AbpHttpClientIdentityModelModule class. It is integrated to the HTTP Clients explained above.

Dependencies

  • Redis for distributed/shared caching.
  • Elasticsearch for storing logs.

Public Web Site (PublicWebSite.Host)

This is a public web site project that has a web blog and product list page.

Authentication

PublicWebSite can show blog posts and product list without login. If you login, you can also manage blogs. It redirects to the AuthServer for authentication. Once the user enters a correct username & password, the page is redirected to the public web site application again. Authentication configuration is setup in the PublicWebSiteHostModule class:

  1. context.Services.AddAuthentication(options =>
  2. {
  3. options.DefaultScheme = "Cookies";
  4. options.DefaultChallengeScheme = "oidc";
  5. })
  6. .AddCookie("Cookies", options =>
  7. {
  8. options.Cookie.Expiration = TimeSpan.FromDays(365);
  9. options.ExpireTimeSpan = TimeSpan.FromDays(365);
  10. })
  11. .AddOpenIdConnect("oidc", options =>
  12. {
  13. options.Authority = configuration["AuthServer:Authority"];
  14. options.ClientId = configuration["AuthServer:ClientId"];
  15. options.ClientSecret = configuration["AuthServer:ClientSecret"];
  16. options.RequireHttpsMetadata = false;
  17. options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
  18. options.SaveTokens = true;
  19. options.GetClaimsFromUserInfoEndpoint = true;
  20. options.Scope.Add("role");
  21. options.Scope.Add("email");
  22. options.Scope.Add("phone");
  23. options.Scope.Add("PublicWebSiteGateway");
  24. options.Scope.Add("ProductService");
  25. options.Scope.Add("BloggingService");
  26. options.ClaimActions.MapAbpClaimTypes();
  27. });
  • It adds “Cookies” authentication as the primary authentication type.
  • “oidc” authentication is configured to use the AuthServer application as the authentication server.
  • It requires the additional identity scopes role, email and phone.
  • It requires the API resource scopes PublicWebSiteGateway, BloggingService and ProductService because it will use these services as APIs.

IdentityServer client settings are stored inside the appsettings.json file:

  1. "AuthServer": {
  2. "Authority": "http://localhost:64999",
  3. "ClientId": "public-website-client",
  4. "ClientSecret": "1q2w3e*"
  5. }

User Interface

The PublicWebSite.Host project has a page to list products (Pages/Products.cshtml). It also uses the UI from the blogging module. PublicWebSiteHostModule adds dependencies to BloggingWebModule (Volo.Blogging.Web package) for that purpose.

A screenshot from the Products page:

microservice-sample-public-product-list

Using Microservices

Public web site application uses the Blogging and Product microservices for all operations, over the Public Web Site Gateway (PublicWebSiteGateway.Host).

Remote End Point

appsettings.json file contains the RemoteServices section to declare the remote service endpoint(s). Each microservice will normally have different endpoints. However, this solution uses the API Gateway pattern to provide a single endpoint for the applications:

  1. "RemoteServices": {
  2. "Default": {
  3. "BaseUrl": "http://localhost:64897/"
  4. }
  5. }

http://localhost:64897/ is the URL of the PublicWebSiteGateway.Host project. It knows where are Blogging and Product services are located.

HTTP Clients

PublicWebSiteHostModule class declares dependencies for BloggingHttpApiClientModule and ProductManagementHttpApiClientModule to be able to use remote HTTP APIs for these services.

Passing the Access Token

Just like explained in the Backend Admin Application section, Public Web Site project also uses the AbpHttpClientIdentityModelModule to pass access_token to the calling services for authentication.

Dependencies

  • Redis for distributed/shared caching.
  • Elasticsearch for storing logs.

Console Client Demo

Finally, the solution includes a very simple console application, named ConsoleClientDemo, that uses Identity and Product services by authenticating through the AuthServer. It uses the Internal Gateway (InternalGateway.Host) to perform HTTP API calls.

Remote Service Configuration

RemoteService configuration in the appsettings.json file is simple:

  1. "RemoteServices": {
  2. "Default": {
  3. "BaseUrl": "http://localhost:65129/"
  4. }
  5. }

http://localhost:65129/ is the URL of the Internal Gateway. All API calls to the services are performed over this URL.

Authentication (IdentityServer Client) Configuration

appsettings.json also has a configuration for the IdentityServer authentication:

  1. "IdentityClients": {
  2. "Default": {
  3. "GrantType": "client_credentials",
  4. "ClientId": "console-client-demo",
  5. "ClientSecret": "1q2w3e*",
  6. "Authority": "http://localhost:64999",
  7. "Scope": "InternalGateway IdentityService ProductService"
  8. }
  9. }

This sample uses the client_credentials grant type which requires a ClientId and ClientSecret for the authentication process. There are also other grant types. For example, you can use the following configuration to swith to the password (Resource Owner Password) grant type:

  1. "IdentityClients": {
  2. "Default": {
  3. "GrantType": "password",
  4. "ClientId": "console-client-demo",
  5. "ClientSecret": "1q2w3e*",
  6. "UserName": "admin",
  7. "UserPassword": "1q2w3E*",
  8. "Authority": "http://localhost:64999",
  9. "Scope": "InternalGateway IdentityService ProductService"
  10. }
  11. }

Resource Owner Password requires a UserName & UserPassword in addition to client credentials. This grant type is useful to call remote services on behalf of a user.

Scope declares the APIs (and the gateway) to grant access. This application uses the Internal Gateway.

HTTP Client Dependencies

ConsoleClientDemoModule has dependencies to AbpIdentityHttpApiClientModule and ProductManagementHttpApiClientModule in order to use Identity and Product APIs. It also has AbpHttpClientIdentityModelModule dependency to authenticate via IdentityServer.

Using the Services

Using the services is straightforward. See the ClientDemoService class which simply injects IIdentityUserAppService and IProductAppService and uses them. This class also shows a manual HTTP call using an HttpClient object. See source code of the ClientDemoService for details.

API Gateways / BFFs (Backend for Frontend)

Gateways are used to provide a single entry point to the applications. In this way, an application only deal with a single service address (API endpoint) instead of a different addresses for each services. Gateways are also used for rate limiting, security, authentication, load balancing and many more requirements.

Backend for Frontend“ (BFF) is a common architectural pattern which offers to build a dedicated and specialized gateway for each different application / client type. This solution uses this pattern and has multiple gateways.

This solution uses the Ocelot library to build API Gateways. It’s a widely accepted API Gateway library for ASP.NET Core.

Backend Admin Application Gateway (BackendAdminAppGateway.Host)

This is backend (server side API) for the “Backend Admin Application” (don’t confuse about the naming; Backend Admin Application is a frontend web application actually, but used by system admins rather than regular users).

Authentication

This gateway uses IdentityServer Bearer authentication and configured like that:

  1. context.Services.AddAuthentication("Bearer")
  2. .AddIdentityServerAuthentication(options =>
  3. {
  4. options.Authority = configuration["AuthServer:Authority"];
  5. options.ApiName = configuration["AuthServer:ApiName"];
  6. options.RequireHttpsMetadata = false;
  7. options.InboundJwtClaimTypeMap["sub"] = AbpClaimTypes.UserId;
  8. options.InboundJwtClaimTypeMap["role"] = AbpClaimTypes.Role;
  9. options.InboundJwtClaimTypeMap["email"] = AbpClaimTypes.Email;
  10. options.InboundJwtClaimTypeMap["email_verified"] = AbpClaimTypes.EmailVerified;
  11. options.InboundJwtClaimTypeMap["phone_number"] = AbpClaimTypes.PhoneNumber;
  12. options.InboundJwtClaimTypeMap["phone_number_verified"] =
  13. AbpClaimTypes.PhoneNumberVerified;
  14. options.InboundJwtClaimTypeMap["name"] = AbpClaimTypes.UserName;
  15. });

AddIdentityServerAuthentication extension method comes from the IdentityServer4.AccessTokenValidation package, part of the IdentityServer4 project (see its documentation).

ApiName is the API which is being protected, BackendAdminAppGateway in this case. So, this solution defines gateways as API resources. Rest of the configuration is related to claims mapping (which is planned to be automated in next ABP versions). The configuration related to authentication in the appsettings.json is simple:

  1. "AuthServer": {
  2. "Authority": "http://localhost:64999",
  3. "ApiName": "BackendAdminAppGateway"
  4. }

Ocelot Configuration

Ocelot needs to know the real URLs of the microservices to be able to redirect HTTP requests. The configuration for this gateway is like below:

  1. "ReRoutes": [
  2. {
  3. "DownstreamPathTemplate": "/api/identity/{everything}",
  4. "DownstreamScheme": "http",
  5. "DownstreamHostAndPorts": [
  6. {
  7. "Host": "localhost",
  8. "Port": 63568
  9. }
  10. ],
  11. "UpstreamPathTemplate": "/api/identity/{everything}",
  12. "UpstreamHttpMethod": [ "Put", "Delete", "Get", "Post" ]
  13. },
  14. {
  15. "DownstreamPathTemplate": "/api/productManagement/{everything}",
  16. "DownstreamScheme": "http",
  17. "DownstreamHostAndPorts": [
  18. {
  19. "Host": "localhost",
  20. "Port": 60244
  21. }
  22. ],
  23. "UpstreamPathTemplate": "/api/productManagement/{everything}",
  24. "UpstreamHttpMethod": [ "Put", "Delete", "Get", "Post" ]
  25. }
  26. ],
  27. "GlobalConfiguration": {
  28. "BaseUrl": "http://localhost:65115"
  29. }

ReRoutes is an array of URL mappings. BaseUrl in the GlobalConfiguration section is the URL of this gateway (Ocelot needs to know its own URL). See its own documentation to better understand the configuration.

Ocelot is a finalizer ASP.NET Core middleware and should be written as the last item in the pipeline:

  1. app.UseOcelot().Wait();

It handles and redirects requests based on the configuration above.

ABP Configuration Endpoints

ABP provides some built-in APIs to get some configuration and information from the server. Examples:

These endpoints should be served by the gateway service, not by microservices. A microservice can only know permissions related to that microservice. But, once properly configured, gateway can aggregate permission values for multiple services as a single list which is more suitable for clients.

For this purpose, the ASP.NET Core pipeline was configured to handle some specific routes via MVC, instead of Ocelot. To make this possible, MapWhen extension method is used like that:

  1. app.MapWhen(ctx => ctx.Request.Path.ToString().StartsWith("/api/abp/") ||
  2. ctx.Request.Path.ToString().StartsWith("/Abp/"),
  3. app2 =>
  4. {
  5. app2.UseConfiguredEndpoints();
  6. });
  7. app.UseOcelot().Wait();

This configuration uses standard MVC middleware when request path starts with /api/abp/ or /Abp/.

Swagger

This gateway is configured to use the swagger UI, a popular tool to discover & test HTTP APIs. Normally, Ocelot does not support to show APIs on the swagger, because it can not know details of each microservice API. But it is possible when you follow ABP layered module architecture best practices.

BackendAdminAppGatewayHostModule adds dependency to AbpIdentityHttpApiModule (Volo.Abp.Identity.HttpApi package) and ProductManagementHttpApiModule (ProductManagement.HttpApi project) to include their HTTP API Controllers. In this way, swagger can discover them. While it references to the API layer, it does not reference to the implementation of application services, because they will be running in the related microservice endpoints and redirected by the Ocelot based on the request URL.

Anyway, when you open the URL http://localhost:65115/swagger/index.html, you will see APIs of all configured microservices.

Permission Management

Backend Admin Application provides a permission management UI (seen before) and uses this gateway to get/set permissions. Permission management API is hosted inside the gateway, instead of a separate service. This is a design decision, but it could be hosted as another microservice if you would like.

Dependencies

  • RabbitMQ for messaging to other services.
  • Redis for distributed/shared caching.
  • Elasticsearch for storing logs.

Public Web Site Gateway (PublicWebSiteGateway.Host)

This is backend (server side API gateway) for the “Public Web Site” application.

Authentication

This gateway uses IdentityServer Bearer authentication and configured like that:

  1. context.Services.AddAuthentication("Bearer")
  2. .AddIdentityServerAuthentication(options =>
  3. {
  4. options.Authority = configuration["AuthServer:Authority"];
  5. options.ApiName = configuration["AuthServer:ApiName"];
  6. options.RequireHttpsMetadata = false;
  7. options.InboundJwtClaimTypeMap["sub"] = AbpClaimTypes.UserId;
  8. options.InboundJwtClaimTypeMap["role"] = AbpClaimTypes.Role;
  9. options.InboundJwtClaimTypeMap["email"] = AbpClaimTypes.Email;
  10. options.InboundJwtClaimTypeMap["email_verified"] = AbpClaimTypes.EmailVerified;
  11. options.InboundJwtClaimTypeMap["phone_number"] = AbpClaimTypes.PhoneNumber;
  12. options.InboundJwtClaimTypeMap["phone_number_verified"] =
  13. AbpClaimTypes.PhoneNumberVerified;
  14. options.InboundJwtClaimTypeMap["name"] = AbpClaimTypes.UserName;
  15. });

AddIdentityServerAuthentication extension method comes from the IdentityServer4.AccessTokenValidation package, part of the IdentityServer4 project (see its documentation).

ApiName is the API which is being protected, PublicWebSiteGateway in this case. Rest of the configuration is related to claims mapping (which is planned to be automated in next ABP versions). The configuration related to authentication in the appsettings.json is simple:

  1. "AuthServer": {
  2. "Authority": "http://localhost:64999",
  3. "ApiName": "PublicWebSiteGateway"
  4. }

Ocelot Configuration

Ocelot needs to know the real URLs of the microservices to be able to redirect HTTP requests. The configuration for this gateway is like below:

  1. "ReRoutes": [
  2. {
  3. "DownstreamPathTemplate": "/api/productManagement/{everything}",
  4. "DownstreamScheme": "http",
  5. "DownstreamHostAndPorts": [
  6. {
  7. "Host": "localhost",
  8. "Port": 60244
  9. }
  10. ],
  11. "UpstreamPathTemplate": "/api/productManagement/{everything}",
  12. "UpstreamHttpMethod": [ "Put", "Delete", "Get", "Post" ]
  13. },
  14. {
  15. "DownstreamPathTemplate": "/api/blogging/{everything}",
  16. "DownstreamScheme": "http",
  17. "DownstreamHostAndPorts": [
  18. {
  19. "Host": "localhost",
  20. "Port": 62157
  21. }
  22. ],
  23. "UpstreamPathTemplate": "/api/blogging/{everything}",
  24. "UpstreamHttpMethod": [ "Put", "Delete", "Get", "Post" ]
  25. }
  26. ],
  27. "GlobalConfiguration": {
  28. "BaseUrl": "http://localhost:64897"
  29. }

See its own documentation to better understand the Ocelot configuration.

Other

See the “ABP Configuration Endpoints” and “Swagger” topics inside the “Backend Admin Application Gateway” section which are very similar for this gateway.

Dependencies

  • RabbitMQ for messaging to other services.
  • Redis for distributed/shared caching.
  • Elasticsearch for storing logs.

Internal Gateway (InternalGateway.Host)

This gateway is not a BFF. It is designed for inter-microservice communication and is not exposed publicly.

Authentication

This gateway uses IdentityServer Bearer authentication and configured like that:

  1. context.Services.AddAuthentication("Bearer")
  2. .AddIdentityServerAuthentication(options =>
  3. {
  4. options.Authority = configuration["AuthServer:Authority"];
  5. options.ApiName = configuration["AuthServer:ApiName"];
  6. options.RequireHttpsMetadata = false;
  7. options.InboundJwtClaimTypeMap["sub"] = AbpClaimTypes.UserId;
  8. options.InboundJwtClaimTypeMap["role"] = AbpClaimTypes.Role;
  9. options.InboundJwtClaimTypeMap["email"] = AbpClaimTypes.Email;
  10. options.InboundJwtClaimTypeMap["email_verified"] = AbpClaimTypes.EmailVerified;
  11. options.InboundJwtClaimTypeMap["phone_number"] = AbpClaimTypes.PhoneNumber;
  12. options.InboundJwtClaimTypeMap["phone_number_verified"] = AbpClaimTypes.PhoneNumberVerified;
  13. options.InboundJwtClaimTypeMap["name"] = AbpClaimTypes.UserName;
  14. });

AddIdentityServerAuthentication extension method comes from the IdentityServer4.AccessTokenValidation package, part of the IdentityServer4 project (see its documentation).

ApiName is the API which is being protected, InternalGateway in this case. Rest of the configuration is related to claims mapping (which is planned to be automated in next ABP versions). The configuration related to authentication in the appsettings.json is simple:

  1. "AuthServer": {
  2. "Authority": "http://localhost:64999",
  3. "ApiName": "InternalGateway"
  4. }

Ocelot Configuration

Ocelot needs to know the real URLs of the microservices to be able to redirect HTTP requests. The configuration for this gateway is like below:

  1. "ReRoutes": [
  2. {
  3. "DownstreamPathTemplate": "/api/identity/{everything}",
  4. "DownstreamScheme": "http",
  5. "DownstreamHostAndPorts": [
  6. {
  7. "Host": "localhost",
  8. "Port": 63568
  9. }
  10. ],
  11. "UpstreamPathTemplate": "/api/identity/{everything}",
  12. "UpstreamHttpMethod": [ "Put", "Delete", "Get", "Post" ]
  13. },
  14. {
  15. "DownstreamPathTemplate": "/api/productManagement/{everything}",
  16. "DownstreamScheme": "http",
  17. "DownstreamHostAndPorts": [
  18. {
  19. "Host": "localhost",
  20. "Port": 60244
  21. }
  22. ],
  23. "UpstreamPathTemplate": "/api/productManagement/{everything}",
  24. "UpstreamHttpMethod": [ "Put", "Delete", "Get", "Post" ]
  25. },
  26. {
  27. "DownstreamPathTemplate": "/api/blogging/{everything}",
  28. "DownstreamScheme": "http",
  29. "DownstreamHostAndPorts": [
  30. {
  31. "Host": "localhost",
  32. "Port": 62157
  33. }
  34. ],
  35. "UpstreamPathTemplate": "/api/blogging/{everything}",
  36. "UpstreamHttpMethod": [ "Put", "Delete", "Get", "Post" ]
  37. }
  38. ],
  39. "GlobalConfiguration": {
  40. "BaseUrl": "http://localhost:65129"
  41. }

ReRoutes configuration covers all microservices in the system. See its own documentation to better understand the Ocelot configuration.

Other

See the “ABP Configuration Endpoints” and “Swagger” topics inside the “Backend Admin Application Gateway” section which are very similar for this gateway.

Dependencies

  • RabbitMQ for messaging to other services.
  • Redis for distributed/shared caching.
  • Elasticsearch for storing logs.

Microservices

Microservices are standalone HTTP APIs those implement the business of the system in a distributed manner.

  • They are used by applications and other microservices through the gateways and HTTP APIs.
  • They can raise or register to events in the system.
  • They can communicate to each other via asynchronous messaging.

Identity Service (IdentityService.Host)

This service provides user and role management APIs.

Database

Shares the same database (MsDemo_Identity) with the AuthServer application.

Identity Module

This service actually just hosts the ABP Identity package/module. Does not include any API itself. In order to host it, adds the following dependencies:

See the module architecture best practice guide to understand the layering better.

Authentication

This microservice uses IdentityServer Bearer authentication and configured like that:

  1. context.Services.AddAuthentication("Bearer")
  2. .AddIdentityServerAuthentication(options =>
  3. {
  4. options.Authority = configuration["AuthServer:Authority"];
  5. options.ApiName = configuration["AuthServer:ApiName"];
  6. options.RequireHttpsMetadata = false;
  7. options.InboundJwtClaimTypeMap["sub"] = AbpClaimTypes.UserId;
  8. options.InboundJwtClaimTypeMap["role"] = AbpClaimTypes.Role;
  9. options.InboundJwtClaimTypeMap["email"] = AbpClaimTypes.Email;
  10. options.InboundJwtClaimTypeMap["email_verified"] = AbpClaimTypes.EmailVerified;
  11. options.InboundJwtClaimTypeMap["phone_number"] = AbpClaimTypes.PhoneNumber;
  12. options.InboundJwtClaimTypeMap["phone_number_verified"] =
  13. AbpClaimTypes.PhoneNumberVerified;
  14. options.InboundJwtClaimTypeMap["name"] = AbpClaimTypes.UserName;
  15. });

ApiName is the API which is being protected, IdentityService in this case. Rest of the configuration is related to claims mapping (which is planned to be automated in next ABP versions). The configuration related to authentication in the appsettings.json is simple:

  1. "AuthServer": {
  2. "Authority": "http://localhost:64999",
  3. "ApiName": "IdentityService"
  4. }

Swagger

Swagger UI is configured and is the default page for this service. If you navigate to the URL http://localhost:63568/, you are redirected to the swagger page to see and test the API.

Dependencies

  • RabbitMQ for messaging to other services.
  • Redis for distributed/shared caching.
  • Elasticsearch for storing logs.

Blogging Service (BloggingService.Host)

This service hosts the blogging API.

Database

It has a dedicated MongoDB database (MsDemo_Blogging) to store blog and posts. It also uses the MsDemo_Identity SQL database for audit logs, permissions and settings. So, there are two connection strings in the appsettings.json file:

  1. "ConnectionStrings": {
  2. "Default": "Server=localhost;Database=MsDemo_Identity;Trusted_Connection=True;MultipleActiveResultSets=true",
  3. "Blogging": "mongodb://localhost/MsDemo_Blogging"
  4. }

Blogging Module

This service actually just hosts the ABP Blogging package/module. Does not include any API itself. In order to host it, adds the following dependencies:

See the module architecture best practice guide to understand the layering better.

Authentication

This microservice uses IdentityServer Bearer authentication and configured like that:

  1. context.Services.AddAuthentication("Bearer")
  2. .AddIdentityServerAuthentication(options =>
  3. {
  4. options.Authority = configuration["AuthServer:Authority"];
  5. options.ApiName = configuration["AuthServer:ApiName"];
  6. options.RequireHttpsMetadata = false;
  7. options.InboundJwtClaimTypeMap["sub"] = AbpClaimTypes.UserId;
  8. options.InboundJwtClaimTypeMap["role"] = AbpClaimTypes.Role;
  9. options.InboundJwtClaimTypeMap["email"] = AbpClaimTypes.Email;
  10. options.InboundJwtClaimTypeMap["email_verified"] = AbpClaimTypes.EmailVerified;
  11. options.InboundJwtClaimTypeMap["phone_number"] = AbpClaimTypes.PhoneNumber;
  12. options.InboundJwtClaimTypeMap["phone_number_verified"] =
  13. AbpClaimTypes.PhoneNumberVerified;
  14. options.InboundJwtClaimTypeMap["name"] = AbpClaimTypes.UserName;
  15. });

ApiName is the API which is being protected, BloggingService in this case. Rest of the configuration is related to claims mapping (which is planned to be automated in next ABP versions). The configuration related to authentication in the appsettings.json is simple:

  1. "AuthServer": {
  2. "Authority": "http://localhost:64999",
  3. "ApiName": "BloggingService"
  4. }

IdentityServer Client

This microservice also uses the Identity microservice API through the Internal Gateway, because it needs to query user details (username, email, phone, name and surname) in some cases. So, it is also a client for the IdentityServer and defines a section in the appsettings.json file for that:

  1. "IdentityClients": {
  2. "Default": {
  3. "GrantType": "client_credentials",
  4. "ClientId": "blogging-service-client",
  5. "ClientSecret": "1q2w3e*",
  6. "Authority": "http://localhost:64999",
  7. "Scope": "InternalGateway IdentityService"
  8. }
  9. }

Since it uses the Internal Gateway, it should also configure the remote endpoint of the gateway:

  1. "RemoteServices": {
  2. "Default": {
  3. "BaseUrl": "http://localhost:65129/",
  4. "UseCurrentAccessToken": "false"
  5. }
  6. }

When you set UseCurrentAccessToken to false, ABP ignores the current access_token in the current HttpContext and authenticates to the AuthServer with the credentials defined above.

Why not using the token of the current user in the current request? Because, the user may not have required permissions on the Identity module, so it can not just pass the current authentication token directly to the Identity service. In addition, some of the blog service APIs are anonymous (not requires authenticated user), so in some cases there is no “current user” in the HTTP request. For these reasons, Blogging service should be defined as a client for the Identity service with its own credentials and permissions.

If you check the AbpPermissionGrants table in the MsDemo_Identity database, you can see the related permission for the blogging-service-client.

microservice-sample-blogservice-permission-in-database

Swagger

Swagger UI is configured and is the default page for this service. If you navigate to the URL http://localhost:62157/, you are redirected to the swagger page to see and test the API.

Dependencies

  • RabbitMQ for messaging to other services.
  • Redis for distributed/shared caching.
  • Elasticsearch for storing logs.

Product Service (ProductService.Host)

This service hosts the Product Management API.

Database & EF Core Migrations

It has a separated SQL database, named MsDemo_ProductManagement, for the product management module. It uses EF Core as the database provider and has a DbContext named ProductServiceMigrationDbContext:

  1. public class ProductServiceMigrationDbContext : AbpDbContext<ProductServiceMigrationDbContext>
  2. {
  3. public ProductServiceMigrationDbContext(
  4. DbContextOptions<ProductServiceMigrationDbContext> options
  5. ) : base(options)
  6. {
  7. }
  8. protected override void OnModelCreating(ModelBuilder modelBuilder)
  9. {
  10. base.OnModelCreating(modelBuilder);
  11. modelBuilder.ConfigureProductManagement();
  12. }
  13. }

Actual model configuration is done inside the modelBuilder.ConfigureProductManagement() extension method. This project maintains the database schema using EF Core migrations.

Notice that this DbContext is only for database migrations. Product Management module has its own DbContext class that is used in the runtime (See ProductManagementDbContext class in the ProductManagement.EntityFrameworkCore project).

There are two connection strings in the appsettings.json file:

  1. "ConnectionStrings": {
  2. "Default": "Server=localhost;Database=MsDemo_Identity;Trusted_Connection=True;MultipleActiveResultSets=true",
  3. "ProductManagement": "Server=localhost;Database=MsDemo_ProductManagement;Trusted_Connection=True;MultipleActiveResultSets=true"
  4. }

Default connection strings points to the MsDemo_Identity database that is used for audit logging, permission and setting stores. ProductManagement connection string is used by the product module.

Product Module

This service actually just hosts the Product Management module. Does not include any API itself. In order to host it, adds the following dependencies:

  • ProductManagementHttpApiModule to provide product management APIs.
  • ProductManagementApplicationModule to host the implementation of the application and domain layers of the module.
  • ProductManagementEntityFrameworkCoreModule to use EF Core as database API.

See the module architecture best practice guide to understand the layering better. See the Product Management module section below for more information about this module.

Authentication

This microservice uses IdentityServer Bearer authentication and configured like that:

  1. context.Services.AddAuthentication("Bearer")
  2. .AddIdentityServerAuthentication(options =>
  3. {
  4. options.Authority = configuration["AuthServer:Authority"];
  5. options.ApiName = configuration["AuthServer:ApiName"];
  6. options.RequireHttpsMetadata = false;
  7. options.InboundJwtClaimTypeMap["sub"] = AbpClaimTypes.UserId;
  8. options.InboundJwtClaimTypeMap["role"] = AbpClaimTypes.Role;
  9. options.InboundJwtClaimTypeMap["email"] = AbpClaimTypes.Email;
  10. options.InboundJwtClaimTypeMap["email_verified"] = AbpClaimTypes.EmailVerified;
  11. options.InboundJwtClaimTypeMap["phone_number"] = AbpClaimTypes.PhoneNumber;
  12. options.InboundJwtClaimTypeMap["phone_number_verified"] =
  13. AbpClaimTypes.PhoneNumberVerified;
  14. options.InboundJwtClaimTypeMap["name"] = AbpClaimTypes.UserName;
  15. });

ApiName is the API which is being protected, ProductService in this case. Rest of the configuration is related to claims mapping (which is planned to be automated in next ABP versions). The configuration related to authentication in the appsettings.json is simple:

  1. "AuthServer": {
  2. "Authority": "http://localhost:64999",
  3. "ApiName": "ProductService"
  4. }

Swagger

Swagger UI is configured and is the default page for this service. If you navigate to the URL http://localhost:60244/, you are redirected to the swagger page to see and test the API.

Dependencies

  • RabbitMQ for messaging to other services.
  • Redis for distributed/shared caching.
  • Elasticsearch for storing logs.

Modules

ABP provides a strong infrastructure to make modular application development easier by providing services and architecture (see the module development best practices guide).

This solution demonstrate how to use prebuilt application modules in a distributed architecture. The solution also includes a simple “Product Management” module to show the implementation of a well layered module example.

Product Management

Product Management is a module that consists of several layers and packages/projects:

microservice-sample-product-module-in-solution

  • ProductManagement.Domain.Shared contains constants and types shared among all layers.
  • ProductManagement.Domain contains the domain logic and defines entities, domain services, domain events, business/domain exceptions.
  • ProductManagement.Application.Contracts contains application service interfaces and DTOs.
  • ProductManagement.Application contains the implementation of application services.
  • ProductManagement.EntityFrameworkCore contains DbConext and other EF Core related classes and configuration.
  • ProductManagement.HttpApi contains API Controllers.
  • ProductManagement.HttpApi.Client contains C# proxies to directly use the HTTP API remotely. Uses Dynamic C# API Clients feature of the ABP framework.
  • ProductManagement.Web contains the UI elements (pages, scripts, styles… etc).

By the help of this layering, it is possible to use the same module as a package reference in a monolithic application or use as a service that runs in another server. It is possible to separate UI (Web) and API layers, so they run in different servers.

In this solution, Web layer runs in the Backend Admin Application while API layer is hosted by the Product microservice.

This tutorial will highlight some important aspects of the module. But, it’s suggested to see the source code for a better understanding.

Domain Layer

Product is the main Aggregate Root of this module:

  1. public class Product : AuditedAggregateRoot<Guid>
  2. {
  3. /// <summary>
  4. /// A unique value for this product.
  5. /// ProductManager ensures the uniqueness of it.
  6. /// It can not be changed after creation of the product.
  7. /// </summary>
  8. [NotNull]
  9. public string Code { get; private set; }
  10. [NotNull]
  11. public string Name { get; private set; }
  12. public float Price { get; private set; }
  13. public int StockCount { get; private set; }
  14. //...
  15. }

All of its properties have private setters which prevents any direct change of the properties from out of the class. Product class ensures its own integrity and validity by its own constructors and methods.

It has two constructors:

  1. private Product()
  2. {
  3. //Default constructor is needed for ORMs.
  4. }
  5. internal Product(
  6. Guid id,
  7. [NotNull] string code,
  8. [NotNull] string name,
  9. float price = 0.0f,
  10. int stockCount = 0)
  11. {
  12. Check.NotNullOrWhiteSpace(code, nameof(code));
  13. if (code.Length >= ProductConsts.MaxCodeLength)
  14. {
  15. throw new ArgumentException(
  16. $"Product code can not be longer than {ProductConsts.MaxCodeLength}"
  17. );
  18. }
  19. Id = id;
  20. Code = code;
  21. SetName(Check.NotNullOrWhiteSpace(name, nameof(name)));
  22. SetPrice(price);
  23. SetStockCountInternal(stockCount, triggerEvent: false);
  24. }

Default (parameterless) constructor is private and is not used in the application code. It is needed because most ORMs requires a parameterless constructor on deserializing entities while getting from the database.

Second constructor is internal that means it can only be used inside the domain layer. This enforces to use the ProductManager while creating a new Product. Because, ProductManager should implement a business rule on a new product creation. This constructor only requires the minimal required arguments to create a new product with some optional arguments. It checks some simple business rules to ensure that the entity is created as a valid product.

Rest of the class has methods to manipulate properties of the entity. Example:

  1. public Product SetPrice(float price)
  2. {
  3. if (price < 0.0f)
  4. {
  5. throw new ArgumentException($"{nameof(price)} can not be less than 0.0!");
  6. }
  7. Price = price;
  8. return this;
  9. }

SetPrice method is used to change the price of the product in a safe manner (by checking a validation rule).

SetStockCount is another method that is used to change stock count of a product:

  1. public Product SetStockCount(int stockCount)
  2. {
  3. return SetStockCountInternal(stockCount);
  4. }
  5. private Product SetStockCountInternal(int stockCount, bool triggerEvent = true)
  6. {
  7. if (StockCount < 0)
  8. {
  9. throw new ArgumentException($"{nameof(stockCount)} can not be less than 0!");
  10. }
  11. if (StockCount == stockCount)
  12. {
  13. return this;
  14. }
  15. if (triggerEvent)
  16. {
  17. AddDistributedEvent(
  18. new ProductStockCountChangedEto(
  19. Id, StockCount, stockCount
  20. )
  21. );
  22. }
  23. StockCount = stockCount;
  24. return this;
  25. }

This method also triggers a distributed event with the ProductStockCountChangedEto parameter (Eto is a conventional postfix stands for Event Transfer Object, but not required) to notify listeners that stock count of a product has changed. Any subscriber can receive this event and perform an action based on that knowledge.

Events are distributed by RabbitMQ for this solution. But ABP is message broker independent by providing necessary abstractions (see the Event Bus document).

As said before, this module forces to always use the ProductManager to create a new Product. ProductManager is a simple domain service defined as shown:

  1. public class ProductManager : DomainService
  2. {
  3. private readonly IRepository<Product, Guid> _productRepository;
  4. public ProductManager(IRepository<Product, Guid> productRepository)
  5. {
  6. _productRepository = productRepository;
  7. }
  8. public async Task<Product> CreateAsync(
  9. [NotNull] string code,
  10. [NotNull] string name,
  11. float price = 0.0f,
  12. int stockCount = 0)
  13. {
  14. var existingProduct =
  15. await _productRepository.FirstOrDefaultAsync(p => p.Code == code);
  16. if (existingProduct != null)
  17. {
  18. throw new ProductCodeAlreadyExistsException(code);
  19. }
  20. return await _productRepository.InsertAsync(
  21. new Product(
  22. GuidGenerator.Create(),
  23. code,
  24. name,
  25. price,
  26. stockCount
  27. )
  28. );
  29. }
  30. }
  • It checks if given code is used before. Throws ProductCodeAlreadyExistsException so.
  • If uses the GuidGenerator (IGuidGenerator) service to create a new Guid.
  • It inserts the entity to the repository.

So, with this design, uniqueness of the product code is guaranteed.

ProductCodeAlreadyExistsException is a domain/business exception defined as like below:

  1. public class ProductCodeAlreadyExistsException : BusinessException
  2. {
  3. public ProductCodeAlreadyExistsException(string productCode)
  4. : base("PM:000001", $"A product with code {productCode} has already exists!")
  5. {
  6. }
  7. }

PM:000001 is a code for the exception type that is sent to the clients, so they can understand the error type. Not implemented for this case, but it is also possible to localize business exceptions. See the exception handling documentation.

Application Layer

Application layer of this module has two services:

  • ProductAppService is mainly used by the Backend Admin Application to manage (create, update, delete…) products. It requires permission to perform any operation.
  • PublicProductAppService is used by the Public Web Site to show list of products to the visitors. It does not require any permission since most of the visitors are not logged in to the application.

Notice that; instead of putting two application service into the same project, it might be a better principle to have separated application layers per application. But we unified them for simplicity in this solution.

As an example, ProductAppService has the following method to update a product:

  1. [Authorize(ProductManagementPermissions.Products.Update)]
  2. public async Task<ProductDto> UpdateAsync(Guid id, UpdateProductDto input)
  3. {
  4. var product = await _productRepository.GetAsync(id);
  5. product.SetName(input.Name);
  6. product.SetPrice(input.Price);
  7. product.SetStockCount(input.StockCount);
  8. return ObjectMapper.Map<Product, ProductDto>(product);
  9. }
  • It defines the required permission (ProductManagementPermissions.Products.Update is a constant with value ProductManagement.Update) to perform this operation.
  • Gets the id of the product and a DTO contains the values to update.
  • Gets the related product entity from the repository.
  • Uses the related methods (like SetName) of the Product class to change properties, because they are with private setters and the only way to change a value is to use an entity method.
  • Returns an updated ProductDto to the client (client may need it for some reason) by using the ObjectMapper.

The implementation may vary based on the requirements. This implementation follows the best practices offered here.

Other Layers

See other layers from the source code.

Infrastructure

Messaging and RabbitMQ

Asynchronous Messaging is a key concept in distributed systems. It makes possible to communicate as a loosely coupled manner with fault tolerance. It does not require both sides to be online at the moment of messaging. So, it is a widely used communication pattern in microservice architecture.

Distributed Event Bus

Distributed Events (Event Bus) is a way of messaging where a service raise/trigger events while other services registers/listens to these events to be notified when an important event occurs. ABP makes distributed events easier to use by providing conventions, services and integrations.

You have seen that the Product class publishing an event using the following code line:

  1. AddDistributedEvent(new ProductStockCountChangedEto(Id, StockCount, stockCount));

ProductStockCountChangedEto was defined as shown below:

  1. [Serializable]
  2. public class ProductStockCountChangedEto : EtoBase
  3. {
  4. public Guid Id { get; }
  5. public int OldCount { get; set; }
  6. public int CurrentCount { get; set; }
  7. private ProductStockCountChangedEto()
  8. {
  9. //Default constructor is needed for deserialization.
  10. }
  11. public ProductStockCountChangedEto(Guid id, int oldCount, int currentCount)
  12. {
  13. Id = id;
  14. OldCount = oldCount;
  15. CurrentCount = currentCount;
  16. }
  17. }

This object stores necessary information about the event. Another service can easily register to this event by implementing the IDistributedEventHandler interface with the generic ProductStockCountChangedEto parameter:

  1. public class MyHandler : IDistributedEventHandler<ProductStockCountChangedEto>
  2. {
  3. public async Task HandleEventAsync(ProductStockCountChangedEto eventData)
  4. {
  5. var productId = eventData.Id;
  6. //...
  7. }
  8. }

All the integration and communication are done by the ABP framework when you use the Volo.Abp.EventBus.RabbitMQ package. If you need to publish events out of an entity, just inject the IDistributedEventBus and use the PublishAsync method.

See the Event Bus documentation for more information about the distributed event system.

RabbitMQ Configuration

In this solution, RabbitMQ is used for messaging & distributed events.

Volo.Abp.EventBus.RabbitMQ package is required to integrate to the RabbitMQ for distributed event system. Then you need to add dependency to the AbpEventBusRabbitMqModule for your module. For example, ProductServiceHostModule declares this dependency.

AbpEventBusRabbitMqModule gets configuration from the appsettings.json by default. For example, the Product Service has such a configuration:

  1. "RabbitMQ": {
  2. "Connections": {
  3. "Default": {
  4. "HostName": "localhost"
  5. }
  6. },
  7. "EventBus": {
  8. "ClientName": "MsDemo_ProductService",
  9. "ExchangeName": "MsDemo"
  10. }
  11. }

Caching and Redis

A distributed system obviously needs to a distributed and shared cache, instead of isolated in-memory caches for each service.

Redis is used as a distributed cache in this solution. The solution uses Microsoft’s standard Microsoft.Extensions.Caching.Redis package for integration. All applications and services uses Redis cache when you use and configure this package. See Microsoft’s documentation for more.

The solution also uses the Microsoft.AspNetCore.DataProtection.StackExchangeRedis package to share data protection keys between applications and services over Redis cache.

Logging, Serilog, Elasticsearch and Kibana

This solution uses Serilog as a logging library. It is a widely used library which has many data source integrations including Elasticsearch.

Logging configurations are done in Program.cs files using a code block similar to the given below:

  1. Log.Logger = new LoggerConfiguration()
  2. .MinimumLevel.Debug()
  3. .MinimumLevel.Override("Microsoft", LogEventLevel.Information)
  4. .Enrich.WithProperty("Application", "ProductService")
  5. .Enrich.FromLogContext()
  6. .WriteTo.File("Logs/logs.txt")
  7. .WriteTo.Elasticsearch(
  8. new ElasticsearchSinkOptions(new Uri(configuration["ElasticSearch:Url"]))
  9. {
  10. AutoRegisterTemplate = true,
  11. AutoRegisterTemplateVersion = AutoRegisterTemplateVersion.ESv6,
  12. IndexFormat = "msdemo-log-{0:yyyy.MM}"
  13. })
  14. .CreateLogger();

This configures multiple log target: File and Elasticsearch. Application property is set to ProductService for this example. This is a way of distinguishing the logs of multiple services in a single database. You can then query logs by the Application name.

Elasticsearch URL is read from the appsettings.json configuration file:

  1. "ElasticSearch": {
  2. "Url": "http://localhost:9200"
  3. }

If you use Kibana, which is a Visualization tool that is well integrated to Elasticsearch, you can see some fancy UI about your logs:

microservice-sample-kibana-2

Figure - A dashboard that shows log and error counts by service/application.

microservice-sample-kibana-1

Figure - A list of log entries

Kibana URL is http://localhost:5601/ by default.

Audit Logging

ABP provides automatic audit logging which saves every request in detail (who is the current user, what is the browser/client, what actions performed, which entities changed, even which properties of entities has been updated). See the audit logging document for details.

All of the services and applications are configured to write audit logs. Audit logs are saved to the MsDemo_Identity SQL database. So, you can query all audit logs of all applications from a single point.

An Audit Log record has a CorrelationId property that can be used to track a request. When a service calls another service in a single web request, they both save audit logs with the same CorrelationId. See the AbpAuditLogs table in the database.

Multi-Tenancy

The solution has been configured to provide a multi-tenant system, where each tenant can have their isolated users, roles, permissions and other data.