Anto Subash.

Table of contents

Intro

In this post we will see how to create a modular abp application and convert it to microservice. We will add a new module to tiered abp app and then use the separate database to store the modules data and then convert the module to a microservice.

In this sample we will create Tiered app which is called MainApp. Then we will add a module called ProjectService.

Creating the abp application and run migrations

1abp new MainApp -t app -u mvc --tiered

Run Migrations

Change directory to src/MainApp.DbMigrator and run the migration project

1dotnet run

This will apply the migrations to the db and we can run the MainApp.Web project. This will host the UI and API..

Add a new Module

Now we will add a new module to our MainApp. Move back to the root folder of your project.

1abp add-module ProjectService --new

This will create a ProjectService and it will be available in the modules folder.

Add and configure the host to the module

We need to add a host for our module. first we have to navigate to the the src folder of the Module.

1cd .\modules\ProjectService\src\

Now lets create a Web Api project to host our module.

1dotnet new webapi -n ProjectService.HttpApi.Host

Now open the ProjectService solution and the newly created Host project to the solution.

Update the appsettings.json with the following

1{
2  "App": {
3    "SelfUrl": "{Host Url}",
4    "CorsOrigins": "https://*.MainApp.com"
5  },
6  "AuthServer": {
7    "Authority": "{ Identity Server Url }",
8    "RequireHttpsMetadata": "true",
9    "SwaggerClientId": "ProjectService_Swagger",
10    "SwaggerClientSecret": "1q2w3e*"
11  },
12  "ConnectionStrings": {
13    "ProjectService": "Server=(LocalDb)\\MSSQLLocalDB;Database=ProjectService;Trusted_Connection=True"
14  },
15  "Redis": {
16    "Configuration": "127.0.0.1"
17  },
18  "Logging": {
19    "LogLevel": {
20      "Default": "Information",
21      "Microsoft": "Warning",
22      "Microsoft.Hosting.Lifetime": "Information"
23    }
24  }
25}

Add the following packages to the ProjectService.HttpApi.Host project.

1<ItemGroup>
2    <PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
3    <PackageReference Include="Volo.Abp.EntityFrameworkCore" Version="5.0.0-rc.1" />
4    <PackageReference Include="Volo.Abp.AspNetCore.MultiTenancy" Version="5.0.0-rc.1" />
5    <PackageReference Include="Volo.Abp.Autofac" Version="5.0.0-rc.1" />
6    <PackageReference Include="Volo.Abp.Core" Version="5.0.0-rc.1" />
7    <PackageReference Include="Volo.Abp.EntityFrameworkCore.SqlServer" Version="5.0.0-rc.1" />
8    <PackageReference Include="Volo.Abp.Swashbuckle" Version="5.0.0-rc.1" />
9    <PackageReference Include="Volo.Abp.AspNetCore.Authentication.JwtBearer" Version="5.0.0-rc.1" />
10    <PackageReference Include="Volo.Abp.AspNetCore.Serilog" Version="5.0.0-rc.1" />
11    <PackageReference Include="Serilog.Extensions.Logging" Version="3.0.1" />
12    <PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" />
13    <PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
14    <PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
15</ItemGroup>

These are the essential packages for our host.

Add ProjectService.Application, ProjectService.EntityFrameworkCore, ProjectService.HttpApi projects as a reference to your ProjectService.HttpApi.Host

We are adding the projects reference because the host project module will depend on the module from these projects.

Create a ProjectServiceHostModule in the newly created ProjectService.HttpApi.Host. This will be a abp module where we will setup the host.

Here is the sample for the ProductService.

1using Microsoft.AspNetCore.Authentication.JwtBearer;
2using Microsoft.AspNetCore.Builder;
3using Microsoft.AspNetCore.Cors;
4using Microsoft.Extensions.Configuration;
5using Microsoft.Extensions.DependencyInjection;
6using Microsoft.Extensions.Hosting;
7using Microsoft.OpenApi.Models;
8using ProjectService.EntityFrameworkCore;
9using System;
10using System.Collections.Generic;
11using System.Linq;
12using Volo.Abp;
13using Volo.Abp.AspNetCore.MultiTenancy;
14using Volo.Abp.AspNetCore.Mvc;
15using Volo.Abp.AspNetCore.Serilog;
16using Volo.Abp.Autofac;
17using Volo.Abp.Modularity;
18using Volo.Abp.Swashbuckle;
19
20namespace ProjectService.HttpApi.Host
21{
22
23    [DependsOn(
24    typeof(ProjectServiceHttpApiModule),
25    typeof(ProjectServiceApplicationModule),
26    typeof(ProjectServiceEntityFrameworkCoreModule),
27    typeof(AbpAspNetCoreMultiTenancyModule),
28    typeof(AbpAutofacModule),
29    typeof(AbpAspNetCoreSerilogModule),
30    typeof(AbpSwashbuckleModule)
31    )]
32    public class ProjectServiceHostModule : AbpModule
33    {
34        public override void ConfigureServices(ServiceConfigurationContext context)
35        {
36            var configuration = context.Services.GetConfiguration();
37            context.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
38                .AddJwtBearer(options =>
39                {
40                    options.Authority = configuration["AuthServer:Authority"];
41                    options.RequireHttpsMetadata = Convert.ToBoolean(configuration["AuthServer:RequireHttpsMetadata"]);
42                    options.Audience = "ProjectService";
43                });
44
45            context.Services.AddAbpSwaggerGenWithOAuth(
46                configuration["AuthServer:Authority"],
47                new Dictionary<string, string>
48                {
49                    {"ProjectService", "ProjectService API"}
50                },
51                options =>
52                {
53                    options.SwaggerDoc("v1", new OpenApiInfo { Title = "ProjectService API", Version = "v1" });
54                    options.DocInclusionPredicate((docName, description) => true);
55                    options.CustomSchemaIds(type => type.FullName);
56                });
57
58            Configure<AbpAspNetCoreMvcOptions>(options =>
59            {
60                options.ConventionalControllers.Create(typeof(ProjectServiceApplicationModule).Assembly);
61            });
62
63            context.Services.AddCors(options =>
64            {
65                options.AddDefaultPolicy(builder =>
66                {
67                    builder
68                        .WithOrigins(
69                            configuration["App:CorsOrigins"]
70                                .Split(",", StringSplitOptions.RemoveEmptyEntries)
71                                .Select(o => o.Trim().RemovePostFix("/"))
72                                .ToArray()
73                        )
74                        .WithAbpExposedHeaders()
75                        .SetIsOriginAllowedToAllowWildcardSubdomains()
76                        .AllowAnyHeader()
77                        .AllowAnyMethod()
78                        .AllowCredentials();
79                });
80            });
81        }
82
83        public override void OnApplicationInitialization(ApplicationInitializationContext context)
84        {
85            var app = context.GetApplicationBuilder();
86            var env = context.GetEnvironment();
87
88            if (env.IsDevelopment())
89            {
90                app.UseDeveloperExceptionPage();
91            }
92
93            app.UseCorrelationId();
94            app.UseCors();
95            app.UseAbpRequestLocalization();
96            app.UseStaticFiles();
97            app.UseRouting();
98            app.UseAuthentication();
99            app.UseAbpClaimsMap();
100            app.UseMultiTenancy();
101            app.UseAuthorization();
102            app.UseSwagger();
103            app.UseAbpSwaggerUI(options => {
104                options.SwaggerEndpoint("/swagger/v1/swagger.json", "ProjectService Service API");
105                var configuration = context.ServiceProvider.GetRequiredService<IConfiguration>();
106                options.OAuthClientId(configuration["AuthServer:SwaggerClientId"]); 
107                options.OAuthClientSecret(configuration["AuthServer:SwaggerClientSecret"]); 
108            });
109            app.UseAbpSerilogEnrichers();
110            app.UseAuditing();
111            app.UseUnitOfWork();
112            app.UseConfiguredEndpoints();
113        }
114    }
115}

In this module the depends on section configures the modules that it depends on in the DependsOn section and we will configure the JWT auth and the swagger UI for the host.

Update the Program.cs

1using System;
2using System.IO;
3using Microsoft.AspNetCore.Hosting;
4using Microsoft.Extensions.Configuration;
5using Microsoft.Extensions.Hosting;
6using Serilog;
7using Serilog.Events;
8
9namespace ProjectService
10{
11    public class Program
12    {
13        public static int Main(string[] args)
14        {
15            Log.Logger = new LoggerConfiguration()
16#if DEBUG
17                .MinimumLevel.Debug()
18#else
19                .MinimumLevel.Information()
20#endif
21                .MinimumLevel.Override("Microsoft", LogEventLevel.Information)
22                .MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Warning)
23                .Enrich.FromLogContext()
24                .WriteTo.Async(c => c.File("Logs/logs.txt"))
25#if DEBUG
26                .WriteTo.Async(c => c.Console())
27#endif
28                .CreateLogger();
29
30            try
31            {
32                Log.Information("Starting ProjectService.Host.");
33                CreateHostBuilder(args).Build().Run();
34                return 0;
35            }
36            catch (Exception ex)
37            {
38                Log.Fatal(ex, "Host terminated unexpectedly!");
39                return 1;
40            }
41            finally
42            {
43                Log.CloseAndFlush();
44            }
45        }
46
47        internal static IHostBuilder CreateHostBuilder(string[] args) =>
48            Host.CreateDefaultBuilder(args)
49                .ConfigureAppConfiguration(build =>
50                {
51                    build.AddJsonFile("appsettings.secrets.json", optional: true);
52                })
53                .ConfigureWebHostDefaults(webBuilder =>
54                {
55                    webBuilder.UseStartup<Startup>();
56                })
57                .UseAutofac()
58                .UseSerilog();
59    }
60
61}

Program file is updated to use the serilog with enrichers.

Update the Startup.cs

1using Microsoft.AspNetCore.Builder;
2using Microsoft.AspNetCore.Hosting;
3using Microsoft.Extensions.DependencyInjection;
4using Microsoft.Extensions.Logging;
5using ProjectService.HttpApi.Host;
6
7namespace ProjectService
8{
9    public class Startup
10    {
11        public void ConfigureServices(IServiceCollection services)
12        {
13            services.AddApplication<ProjectServiceHostModule>();
14        }
15
16        public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
17        {
18            app.InitializeApplication();
19        }
20    }
21}

In the startup file we are configuring our newly created Host module.

Create a HomeController.cs in the Controller folder and update wit the following text.

1using Microsoft.AspNetCore.Mvc;
2using Volo.Abp.AspNetCore.Mvc;
3
4namespace ProjectService.HttpApi.Host.Controllers
5{
6    public class HomeController : AbpController
7    {
8        public ActionResult Index()
9        {
10            return Redirect("~/swagger");
11        }
12    }
13}

Now our default route will be swagger.

We are mostly done with the host and now we can create the AppService.

Add new Entity to the ProjectService

We are going to create a simple Project entity and create a AppService for that project. This section of the tutorial is based on the Quick Start Guide

We will create a new Entity inside the ProjectService.Domain called Project.

Create an Entity

Learn more about the Entity in the abp docs.

First step is to create an Entity. Create the Entity in the ProjectService.Domain project.

1public class Project : Entity<Guid>
2{
3    public string Name { get; set; }
4}

Add Entity to EfCore

Learn more about the ef core in the abp docs.

Next is to add Entity to the EF Core. you will find the DbContext in the ProjectService.EntityFrameworkCore project. Add the DbSet to the DbContext

1public DbSet<Project> Projects { get; set; }

Configure Entity in EfCore

Make sure you have the updated the ef core global tool

Configuration is done in the DbContextModelCreatingExtensions class. This should be available in the ProjectService.EntityFrameworkCore project

1builder.Entity<Project>(b =>
2{
3    //Configure table & schema name
4    b.ToTable(ProjectServiceDbProperties.DbTablePrefix + "Projects", ProjectServiceDbProperties.Schema);
5
6    b.ConfigureByConvention();
7});

Prepare for the migration

1dotnet tool update --global dotnet-ef

Add necessary packages to the ProjectService.EntityFrameworkCore project.

1<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.0">
2    <PrivateAssets>all</PrivateAssets>
3    <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
4</PackageReference>
5<PackageReference Include="Volo.Abp.EntityFrameworkCore" Version="5.0.0-rc.1" />
6<PackageReference Include="Volo.Abp.EntityFrameworkCore.SqlServer" Version="5.0.0-rc.1" />

We need to create a ProjectServiceDbContextFactory to support migrations. Check here for more info about this

1public class ProjectServiceDbContextFactory : IDesignTimeDbContextFactory<ProjectServiceDbContext>
2{
3    public ProjectServiceDbContext CreateDbContext(string[] args)
4    {
5        var builder = new DbContextOptionsBuilder<ProjectServiceDbContext>()
6            .UseSqlServer(GetConnectionStringFromConfiguration());
7        return new ProjectServiceDbContext(builder.Options);
8    }
9
10    private static string GetConnectionStringFromConfiguration()
11    {
12        return BuildConfiguration()
13            .GetConnectionString(ProjectServiceDbProperties.ConnectionStringName);
14    }
15
16    private static IConfigurationRoot BuildConfiguration()
17    {
18        var builder = new ConfigurationBuilder()
19            .SetBasePath(
20                Path.Combine(
21                    Directory.GetCurrentDirectory(),
22                    $"..{Path.DirectorySeparatorChar}ProjectService.HttpApi.Host"
23                )
24            )
25            .AddJsonFile("appsettings.json", optional: false);
26
27        return builder.Build();
28    }
29}

This is needed for ef core migration to work. We are building the configuration by taking the appsetting.json from the ProjectService.HttpApi.Host.

Adding Migrations

Now the DbContextFactory is configured we can add the migrations.

Go the ProjectService.EntityFrameworkCore project in the terminal and create migrations.

To create migration run this command:

1dotnet ef migrations add created_projects

Verify the migrations created in the migrations folder.

To update the database run this command

1dotnet ef database update

Create a Entity Dto

Dto are placed in ProjectService.Application.Contracts project

1public class ProjectDto: EntityDto<Guid>
2{
3    public string Name { get; set; }
4}

Create a AppService interface

create IProjectAppService interface in the ProjectService.Application.Contracts project

1public interface IProjectAppService: IApplicationService
2{
3    Task<List<ProjectDto>> GetListAsync();
4    Task<ProjectDto> CreateAsync(string text);
5    Task DeleteAsync(Guid id);
6}

Create an Application Services

Learn more about the Application Services in the abp docs.

Application service are created in the ProjectService.Application project

1[Authorize]
2public class ProjectAppService : ProjectServiceAppService, IProjectAppService
3{
4    private readonly IRepository<Project, Guid> projectRepository;
5
6    public ProjectAppService(IRepository<Project, Guid> projectRepository)
7    {
8        this.projectRepository = projectRepository;
9    }
10
11    public async Task<ProjectDto> CreateAsync(string text)
12    {
13        var projectItem = await projectRepository.InsertAsync(
14                            new Project { Name = text }
15                            );
16
17        return new ProjectDto
18        {
19            Id = projectItem.Id,
20            Name = projectItem.Name
21        };
22    }
23
24    public async Task DeleteAsync(Guid id)
25    {
26        await projectRepository.DeleteAsync(id);
27    }
28
29    public async Task<List<ProjectDto>> GetListAsync()
30    {
31        var items = await projectRepository.GetListAsync();
32        return items
33            .Select(item => new ProjectDto
34            {
35                Id = item.Id,
36                Name = item.Name
37            }).ToList();
38    }
39}

Update the EntityFrameworkCoreModule

Update the ConfigureServices method in the ProjectServiceEntityFrameworkCoreModule file.

1context.Services.AddAbpDbContext<ProjectServiceDbContext>(options =>
2{
3    options.AddDefaultRepositories(includeAllEntities: true);
4});
5
6Configure<AbpDbContextOptions>(options =>
7{
8    options.UseSqlServer();
9});

Create API Scope, API Resource and Swagger Client in IdentityServer

We need to do this in the MainApp. We have to update the IdentityServerDataSeedContributor in the MainApp.Domain.

1private async Task CreateApiScopesAsync()
2{
3    await CreateApiScopeAsync("MainApp");
4    await CreateApiScopeAsync("ProjectService");
5}
6
7private async Task CreateApiResourcesAsync()
8{
9    var commonApiUserClaims = new[]
10    {
11        "email",
12        "email_verified",
13        "name",
14        "phone_number",
15        "phone_number_verified",
16        "role"
17    };
18
19    await CreateApiResourceAsync("MainApp", commonApiUserClaims);
20    await CreateApiResourceAsync("ProjectService", commonApiUserClaims);
21}

Now lets update the create the swagger client for the new service.

In the MainApp.DbMigrator project update the appsettings.json with the new swagger client.

1"ProjectService_Swagger": {
2    "ClientId": "ProjectService_Swagger",
3    "ClientSecret": "1q2w3e*",
4    "RootUrl": "{ Your Service url }"
5}

Update the commonScopes in the CreateClientsAsync method

1var commonScopes = new[]
2{
3    "email",
4    "openid",
5    "profile",
6    "role",
7    "phone",
8    "address",
9    "MainApp",
10    "ProjectService"
11};

This will add the newly created scope to all the clients.

Update the CreateClientsAsync method to create the swagger client in the IdentityServerDataSeedContributor in the MainApp.Domain.

1var swaggerClientIdProjectService = configurationSection["ProjectService_Swagger:ClientId"];
2if (!swaggerClientIdProjectService.IsNullOrWhiteSpace())
3{
4    var swaggerRootUrl = configurationSection["ProjectService_Swagger:RootUrl"].TrimEnd('/');
5
6    await CreateClientAsync(
7        name: swaggerClientIdProjectService,
8        scopes: commonScopes,
9        grantTypes: new[] { "authorization_code" },
10        secret: configurationSection["ProjectService_Swagger:ClientSecret"]?.Sha256(),
11        requireClientSecret: false,
12        redirectUri: $"{swaggerRootUrl}/swagger/oauth2-redirect.html",
13        corsOrigins: new[] { swaggerRootUrl.RemovePostFix("/") }
14    );
15}

Run Migrations again for the MainApp

Change directory to MainApp.DbMigrator and run the migration project

1dotnet run

This will run the DbMigrator project. The DbMigrator will seed the database with the New Scope, API and Client in our Identity Server.

Once the migration is done lets update the CorsOrigins in the IdentityServer.

1"App": {
2    "SelfUrl": "https://localhost:44373",
3    "ClientUrl": "http://localhost:4200",
4    "CorsOrigins": "https://*.MainApp.com,http://localhost:4200,https://localhost:44307,https://localhost:44358,https://localhost:44372,{ Project Service url }", // update the entry here
5    "RedirectAllowedUrls": "http://localhost:4200,https://localhost:44307"
6},

Communicating with the Microservice

Now we have the application running lets see how we can communicate between services.

To communicate with the we need to use the Client Proxy Check docs here

Add the Contract project reference

We need to add ProjectService.Application.Contracts project as project reference to MainApp.HttpApi.Client

1<ProjectReference Include="..\..\modules\ProjectService\src\ProjectService.Application.Contracts\ProjectService.Application.Contracts.csproj" />

Update the MainAppHttpApiClientModule dependency and add the ProjectService as a client proxy.

1typeof(ProjectServiceApplicationContractsModule)

Update the ConfigureServices service in the MainAppHttpApiClientModule.cs file in the MainApp.HttpApi.Client project.

1//Create dynamic client proxies
2context.Services.AddHttpClientProxies(
3    typeof(ProjectServiceApplicationContractsModule).Assembly,
4    "ProjectService"
5);

Now lets add Remote Service Endpoints to the appsettings.json in MainApp.Web

1"RemoteServices": {
2    "Default": {
3        "BaseUrl": "https://localhost:44358/"
4    },
5    "ProjectService": {
6        "BaseUrl": "https://localhost:44372/"
7    }
8}

Update the scope of the Web App

To connect to the project service we need to add the ProjectService scope as a scope to the AddAbpOpenIdConnect method inside the ConfigureAuthentication method in the MainAppWebModule

1.AddAbpOpenIdConnect("oidc", options =>
2{
3    options.Authority = configuration["AuthServer:Authority"];
4    options.RequireHttpsMetadata = Convert.ToBoolean(configuration["AuthServer:RequireHttpsMetadata"]);
5    options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
6
7    options.ClientId = configuration["AuthServer:ClientId"];
8    options.ClientSecret = configuration["AuthServer:ClientSecret"];
9
10    options.SaveTokens = true;
11    options.GetClaimsFromUserInfoEndpoint = true;
12
13    options.Scope.Add("role");
14    options.Scope.Add("email");
15    options.Scope.Add("phone");
16    options.Scope.Add("MainApp");
17    options.Scope.Add("ProjectService"); // This is for the new project service
18});

Create a UI to display the projects

Create a project page to display the project list in the MainApp.Web.

Create a Projects.cshtml

1@page
2@model MainApp.Web.Pages.ProjectsModel
3<div class="container">
4    <H1>List of projects</H1>
5    @if(Model.Projects != null && Model.Projects.Count > 0){
6        @foreach (var item in Model?.Projects) {
7            <div class="card">
8                <div class="card-body">
9                    <h5 class="card-title">@item.Name</h5>
10                </div>
11            </div>
12        }  
13    }
14</div>

Create a Projects.cshtml.cs

1public class ProjectsModel : MainAppPageModel
2    {
3        private readonly ILogger<ProjectsModel> logger;
4
5        public List<ProjectDto> Projects { get; set; }
6        private IProjectAppService projectAppService { get; set; }
7        public ProjectsModel(IProjectAppService projectAppService, ILogger<ProjectsModel> logger)
8        {
9            this.projectAppService = projectAppService;
10            this.logger = logger;
11            Projects = new List<ProjectDto>();
12        }
13
14        public void OnGet()
15        {
16            try
17            {
18                var projects = projectAppService.GetListAsync().Result;
19                Projects = projects;
20            }
21            catch (Exception e)
22            {
23                logger.LogError(e.Message);
24            }
25        }
26    }

Lets add the newly created page to the Main menu.

Update the ConfigureMainMenuAsync method in the MainAppMenuContributor.cs file.

1context.Menu.Items.Insert(
2    3,
3    new ApplicationMenuItem(
4        MainAppMenus.Home,
5        l["Projects"],
6        "~/Projects",
7        icon: "fas fa-list",
8        order: 0
9    )
10);

Now lets run the application.

Running the application

Since we have run 4 services lets init tye so that it will be easier.

1tye init

This command will create the tye.yaml file which will have the project without out ports binding. Update the ports from your solution. Here is the sample tye.yaml.

1name: mainapp
2services:
3- name: mainapp-web
4  project: src/MainApp.Web/MainApp.Web.csproj
5  bindings:
6  - port: 44343 //update your ports here
7    protocol: https
8- name: mainapp-identityserver
9  project: src/MainApp.IdentityServer/MainApp.IdentityServer.csproj
10  bindings:
11  - port: 44373 //update your ports here
12    protocol: https
13- name: mainapp-httpapi-host
14  project: src/MainApp.HttpApi.Host/MainApp.HttpApi.Host.csproj
15  bindings:
16  - port: 44358 //update your ports here
17    protocol: https
18- name: project-service
19  project: modules/ProjectService/src/ProjectService.HttpApi.Host/ProjectService.HttpApi.Host.csproj
20  bindings:
21  - port: 44372 //update your ports here
22    protocol: https

Now run the tye command.

1tye run

This will run all the projects. Navigate to the ProjectService and do the swagger authorization to login and call the project api to create new project.

To View the project visit the MainApp.Web project and click Projects menu to see the list of the project you just created.

Buy Me a Coffee at ko-fi.com