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.
abp new MainApp -t app -u mvc --tieredChange directory to src/MainApp.DbMigrator and run the migration project
dotnet runThis will apply the migrations to the db and we can run the MainApp.Web project. This will host the UI and API..
Now we will add a new module to our MainApp. Move back to the root folder of your project.
abp add-module ProjectService --newThis will create a ProjectService and it will be available in the modules folder.
We need to add a host for our module. first we have to navigate to the the src folder of the Module.
cd .\modules\ProjectService\src\Now lets create a Web Api project to host our module.
dotnet new webapi -n ProjectService.HttpApi.HostNow open the ProjectService solution and the newly created Host project to the solution.
Update the appsettings.json with the following
{
"App": {
"SelfUrl": "{Host Url}",
"CorsOrigins": "https://*.MainApp.com"
},
"AuthServer": {
"Authority": "{ Identity Server Url }",
"RequireHttpsMetadata": "true",
"SwaggerClientId": "ProjectService_Swagger",
"SwaggerClientSecret": "1q2w3e*"
},
"ConnectionStrings": {
"ProjectService": "Server=(LocalDb)\\MSSQLLocalDB;Database=ProjectService;Trusted_Connection=True"
},
"Redis": {
"Configuration": "127.0.0.1"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}Add the following packages to the ProjectService.HttpApi.Host project.
<ItemGroup>
<PackageReferenceInclude="Serilog.AspNetCore"Version="4.1.0" />
<PackageReferenceInclude="Volo.Abp.EntityFrameworkCore"Version="5.0.0-rc.1" />
<PackageReferenceInclude="Volo.Abp.AspNetCore.MultiTenancy"Version="5.0.0-rc.1" />
<PackageReferenceInclude="Volo.Abp.Autofac"Version="5.0.0-rc.1" />
<PackageReferenceInclude="Volo.Abp.Core"Version="5.0.0-rc.1" />
<PackageReferenceInclude="Volo.Abp.EntityFrameworkCore.SqlServer"Version="5.0.0-rc.1" />
<PackageReferenceInclude="Volo.Abp.Swashbuckle"Version="5.0.0-rc.1" />
<PackageReferenceInclude="Volo.Abp.AspNetCore.Authentication.JwtBearer"Version="5.0.0-rc.1" />
<PackageReferenceInclude="Volo.Abp.AspNetCore.Serilog"Version="5.0.0-rc.1" />
<PackageReferenceInclude="Serilog.Extensions.Logging"Version="3.0.1" />
<PackageReferenceInclude="Serilog.Sinks.Async"Version="1.4.0" />
<PackageReferenceInclude="Serilog.Sinks.File"Version="4.1.0" />
<PackageReferenceInclude="Serilog.Sinks.Console"Version="3.1.1" />
</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.
usingMicrosoft.AspNetCore.Authentication.JwtBearer;usingMicrosoft.AspNetCore.Builder;usingMicrosoft.AspNetCore.Cors;usingMicrosoft.Extensions.Configuration;usingMicrosoft.Extensions.DependencyInjection;usingMicrosoft.Extensions.Hosting;usingMicrosoft.OpenApi.Models;usingProjectService.EntityFrameworkCore;usingSystem;usingSystem.Collections.Generic;usingSystem.Linq;usingVolo.Abp;usingVolo.Abp.AspNetCore.MultiTenancy;usingVolo.Abp.AspNetCore.Mvc;usingVolo.Abp.AspNetCore.Serilog;usingVolo.Abp.Autofac;usingVolo.Abp.Modularity;usingVolo.Abp.Swashbuckle;namespaceProjectService.HttpApi.Host{[DependsOn(typeof(ProjectServiceHttpApiModule),typeof(ProjectServiceApplicationModule),typeof(ProjectServiceEntityFrameworkCoreModule),typeof(AbpAspNetCoreMultiTenancyModule),typeof(AbpAutofacModule),typeof(AbpAspNetCoreSerilogModule),typeof(AbpSwashbuckleModule))]publicclassProjectServiceHostModule:AbpModule{publicoverridevoidConfigureServices(ServiceConfigurationContextcontext){varconfiguration=context.Services.GetConfiguration();context.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>{options.Authority=configuration["AuthServer:Authority"];options.RequireHttpsMetadata=Convert.ToBoolean(configuration["AuthServer:RequireHttpsMetadata"]);options.Audience="ProjectService";});context.Services.AddAbpSwaggerGenWithOAuth(configuration["AuthServer:Authority"],newDictionary<string,string>{{"ProjectService","ProjectService API"}},
options =>{options.SwaggerDoc("v1",newOpenApiInfo{Title="ProjectService API",Version="v1"});options.DocInclusionPredicate((docName,description)=>true);options.CustomSchemaIds(type =>type.FullName);});Configure<AbpAspNetCoreMvcOptions>(options =>{options.ConventionalControllers.Create(typeof(ProjectServiceApplicationModule).Assembly);});context.Services.AddCors(options =>{options.AddDefaultPolicy(builder =>{builder.WithOrigins(configuration["App:CorsOrigins"].Split(",",StringSplitOptions.RemoveEmptyEntries).Select(o =>o.Trim().RemovePostFix("/")).ToArray()).WithAbpExposedHeaders().SetIsOriginAllowedToAllowWildcardSubdomains().AllowAnyHeader().AllowAnyMethod().AllowCredentials();});});}publicoverridevoidOnApplicationInitialization(ApplicationInitializationContextcontext){varapp=context.GetApplicationBuilder();varenv=context.GetEnvironment();if(env.IsDevelopment()){app.UseDeveloperExceptionPage();}app.UseCorrelationId();app.UseCors();app.UseAbpRequestLocalization();app.UseStaticFiles();app.UseRouting();app.UseAuthentication();app.UseAbpClaimsMap();app.UseMultiTenancy();app.UseAuthorization();app.UseSwagger();app.UseAbpSwaggerUI(options =>{options.SwaggerEndpoint("/swagger/v1/swagger.json","ProjectService Service API");varconfiguration=context.ServiceProvider.GetRequiredService<IConfiguration>();options.OAuthClientId(configuration["AuthServer:SwaggerClientId"]);options.OAuthClientSecret(configuration["AuthServer:SwaggerClientSecret"]);});app.UseAbpSerilogEnrichers();app.UseAuditing();app.UseUnitOfWork();app.UseConfiguredEndpoints();}}}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
usingSystem;usingSystem.IO;usingMicrosoft.AspNetCore.Hosting;usingMicrosoft.Extensions.Configuration;usingMicrosoft.Extensions.Hosting;usingSerilog;usingSerilog.Events;namespaceProjectService{publicclassProgram{publicstaticintMain(string[]args){Log.Logger=newLoggerConfiguration()
#if DEBUG.MinimumLevel.Debug()
#else
.MinimumLevel.Information()
#endif
.MinimumLevel.Override("Microsoft",LogEventLevel.Information).MinimumLevel.Override("Microsoft.EntityFrameworkCore",LogEventLevel.Warning).Enrich.FromLogContext().WriteTo.Async(c =>c.File("Logs/logs.txt"))
#if DEBUG
.WriteTo.Async(c =>c.Console())
#endif
.CreateLogger();try{Log.Information("Starting ProjectService.Host.");CreateHostBuilder(args).Build().Run();return0;}catch(Exceptionex){Log.Fatal(ex,"Host terminated unexpectedly!");return1;}finally{Log.CloseAndFlush();}}internalstaticIHostBuilderCreateHostBuilder(string[]args)=>Host.CreateDefaultBuilder(args).ConfigureAppConfiguration(build =>{build.AddJsonFile("appsettings.secrets.json",optional:true);}).ConfigureWebHostDefaults(webBuilder =>{webBuilder.UseStartup<Startup>();}).UseAutofac().UseSerilog();}}Program file is updated to use the serilog with enrichers.
Update the Startup.cs
usingMicrosoft.AspNetCore.Builder;usingMicrosoft.AspNetCore.Hosting;usingMicrosoft.Extensions.DependencyInjection;usingMicrosoft.Extensions.Logging;usingProjectService.HttpApi.Host;namespaceProjectService{publicclassStartup{publicvoidConfigureServices(IServiceCollectionservices){services.AddApplication<ProjectServiceHostModule>();}publicvoidConfigure(IApplicationBuilderapp,IWebHostEnvironmentenv,ILoggerFactoryloggerFactory){app.InitializeApplication();}}}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.
usingMicrosoft.AspNetCore.Mvc;usingVolo.Abp.AspNetCore.Mvc;namespaceProjectService.HttpApi.Host.Controllers{publicclassHomeController:AbpController{publicActionResultIndex(){returnRedirect("~/swagger");}}}Now our default route will be swagger.
We are mostly done with the host and now we can create the AppService.
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.
Learn more about the Entity in the abp docs.
First step is to create an Entity. Create the Entity in the ProjectService.Domain project.
publicclassProject:Entity<Guid>{publicstringName{get;set;}}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
publicDbSet<Project>Projects{get;set;}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
builder.Entity<Project>(b =>{//Configure table & schema nameb.ToTable(ProjectServiceDbProperties.DbTablePrefix+"Projects",ProjectServiceDbProperties.Schema);b.ConfigureByConvention();});dotnet tool update --global dotnet-efAdd necessary packages to the ProjectService.EntityFrameworkCore project.
<PackageReferenceInclude="Microsoft.EntityFrameworkCore.Design"Version="6.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReferenceInclude="Volo.Abp.EntityFrameworkCore"Version="5.0.0-rc.1" />
<PackageReferenceInclude="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
publicclassProjectServiceDbContextFactory:IDesignTimeDbContextFactory<ProjectServiceDbContext>{publicProjectServiceDbContextCreateDbContext(string[]args){varbuilder=newDbContextOptionsBuilder<ProjectServiceDbContext>().UseSqlServer(GetConnectionStringFromConfiguration());returnnewProjectServiceDbContext(builder.Options);}privatestaticstringGetConnectionStringFromConfiguration(){returnBuildConfiguration().GetConnectionString(ProjectServiceDbProperties.ConnectionStringName);}privatestaticIConfigurationRootBuildConfiguration(){varbuilder=newConfigurationBuilder().SetBasePath(Path.Combine(Directory.GetCurrentDirectory(),$"..{Path.DirectorySeparatorChar}ProjectService.HttpApi.Host")).AddJsonFile("appsettings.json",optional:false);returnbuilder.Build();}}This is needed for ef core migration to work. We are building the configuration by taking the appsetting.json from the ProjectService.HttpApi.Host.
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:
dotnet ef migrations add created_projectsVerify the migrations created in the migrations folder.
To update the database run this command
dotnet ef database updateDto are placed in ProjectService.Application.Contracts project
publicclassProjectDto:EntityDto<Guid>{publicstringName{get;set;}}create IProjectAppService interface in the ProjectService.Application.Contracts project
publicinterfaceIProjectAppService:IApplicationService{Task<List<ProjectDto>>GetListAsync();Task<ProjectDto>CreateAsync(stringtext);TaskDeleteAsync(Guidid);}Learn more about the Application Services in the abp docs.
Application service are created in the ProjectService.Application project
[Authorize]publicclassProjectAppService:ProjectServiceAppService,IProjectAppService{privatereadonlyIRepository<Project,Guid>projectRepository;publicProjectAppService(IRepository<Project,Guid>projectRepository){this.projectRepository=projectRepository;}publicasyncTask<ProjectDto>CreateAsync(stringtext){varprojectItem=awaitprojectRepository.InsertAsync(newProject{Name=text});returnnewProjectDto{Id=projectItem.Id,Name=projectItem.Name};}publicasyncTaskDeleteAsync(Guidid){awaitprojectRepository.DeleteAsync(id);}publicasyncTask<List<ProjectDto>>GetListAsync(){varitems=awaitprojectRepository.GetListAsync();returnitems.Select(item =>newProjectDto{Id=item.Id,Name=item.Name}).ToList();}}Update the ConfigureServices method in the ProjectServiceEntityFrameworkCoreModule file.
context.Services.AddAbpDbContext<ProjectServiceDbContext>(options =>{options.AddDefaultRepositories(includeAllEntities:true);});Configure<AbpDbContextOptions>(options =>{options.UseSqlServer();});We need to do this in the MainApp. We have to update the IdentityServerDataSeedContributor in the MainApp.Domain.
privateasyncTaskCreateApiScopesAsync(){awaitCreateApiScopeAsync("MainApp");awaitCreateApiScopeAsync("ProjectService");}privateasyncTaskCreateApiResourcesAsync(){varcommonApiUserClaims=new[]{"email","email_verified","name","phone_number","phone_number_verified","role"};awaitCreateApiResourceAsync("MainApp",commonApiUserClaims);awaitCreateApiResourceAsync("ProjectService",commonApiUserClaims);}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.
"ProjectService_Swagger":{"ClientId":"ProjectService_Swagger","ClientSecret":"1q2w3e*","RootUrl":"{ Your Service url }"}Update the commonScopes in the CreateClientsAsync method
varcommonScopes=new[]{"email","openid","profile","role","phone","address","MainApp","ProjectService"};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.
varswaggerClientIdProjectService=configurationSection["ProjectService_Swagger:ClientId"];if(!swaggerClientIdProjectService.IsNullOrWhiteSpace()){varswaggerRootUrl=configurationSection["ProjectService_Swagger:RootUrl"].TrimEnd('/');awaitCreateClientAsync(name:swaggerClientIdProjectService,scopes:commonScopes,grantTypes:new[]{"authorization_code"},secret:configurationSection["ProjectService_Swagger:ClientSecret"]?.Sha256(),requireClientSecret:false,redirectUri:$"{swaggerRootUrl}/swagger/oauth2-redirect.html",corsOrigins:new[]{swaggerRootUrl.RemovePostFix("/")});}Change directory to MainApp.DbMigrator and run the migration project
dotnet runThis 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.
"App": {
"SelfUrl": "https://localhost:44373",
"ClientUrl": "http://localhost:4200",
"CorsOrigins": "https://*.MainApp.com,http://localhost:4200,https://localhost:44307,https://localhost:44358,https://localhost:44372,{ Project Service url }", // update the entry here"RedirectAllowedUrls": "http://localhost:4200,https://localhost:44307"
},Now we have the application running lets see how we can communicate between services.
To communicate with the we need to use the Client ProxyCheck docs here
We need to add ProjectService.Application.Contracts project as project reference to MainApp.HttpApi.Client
<ProjectReferenceInclude="..\..\modules\ProjectService\src\ProjectService.Application.Contracts\ProjectService.Application.Contracts.csproj" />Update the MainAppHttpApiClientModule dependency and add the ProjectService as a client proxy.
typeof(ProjectServiceApplicationContractsModule)Update the ConfigureServices service in the MainAppHttpApiClientModule.cs file in the MainApp.HttpApi.Client project.
//Create dynamic client proxiescontext.Services.AddHttpClientProxies(typeof(ProjectServiceApplicationContractsModule).Assembly,"ProjectService");Now lets add Remote Service Endpoints to the appsettings.json in MainApp.Web
"RemoteServices": {
"Default": {
"BaseUrl": "https://localhost:44358/"
},
"ProjectService": {
"BaseUrl": "https://localhost:44372/"
}
}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
.AddAbpOpenIdConnect("oidc", options =>{options.Authority=configuration["AuthServer:Authority"];options.RequireHttpsMetadata=Convert.ToBoolean(configuration["AuthServer:RequireHttpsMetadata"]);options.ResponseType=OpenIdConnectResponseType.CodeIdToken;options.ClientId=configuration["AuthServer:ClientId"];options.ClientSecret=configuration["AuthServer:ClientSecret"];options.SaveTokens=true;options.GetClaimsFromUserInfoEndpoint=true;options.Scope.Add("role");options.Scope.Add("email");options.Scope.Add("phone");options.Scope.Add("MainApp");options.Scope.Add("ProjectService");// This is for the new project service});Create a project page to display the project list in the MainApp.Web.
Create a Projects.cshtml
@page
@model MainApp.Web.Pages.ProjectsModel
<divclass="container"><H1>List of projects</H1>
@if(Model.Projects != null && Model.Projects.Count > 0){
@foreach (var item in Model?.Projects) {
<divclass="card"><divclass="card-body"><h5class="card-title">@item.Name</h5></div></div>
} }
</div>Create a Projects.cshtml.cs
publicclassProjectsModel:MainAppPageModel{privatereadonlyILogger<ProjectsModel>logger;publicList<ProjectDto>Projects{get;set;}privateIProjectAppServiceprojectAppService{get;set;}publicProjectsModel(IProjectAppServiceprojectAppService,ILogger<ProjectsModel>logger){this.projectAppService=projectAppService;this.logger=logger;Projects=newList<ProjectDto>();}publicvoidOnGet(){try{varprojects=projectAppService.GetListAsync().Result;Projects=projects;}catch(Exceptione){logger.LogError(e.Message);}}}Lets add the newly created page to the Main menu.
Update the ConfigureMainMenuAsync method in the MainAppMenuContributor.cs file.
context.Menu.Items.Insert(3,newApplicationMenuItem(MainAppMenus.Home,l["Projects"],"~/Projects",icon:"fas fa-list",order:0));Now lets run the application.
Since we have run 4 services lets init tye so that it will be easier.
tye initThis 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.
name: mainappservices:
- name: mainapp-webproject: src/MainApp.Web/MainApp.Web.csprojbindings:
- port: 44343 //update your ports hereprotocol: https
- name: mainapp-identityserverproject: src/MainApp.IdentityServer/MainApp.IdentityServer.csprojbindings:
- port: 44373 //update your ports hereprotocol: https
- name: mainapp-httpapi-hostproject: src/MainApp.HttpApi.Host/MainApp.HttpApi.Host.csprojbindings:
- port: 44358 //update your ports hereprotocol: https
- name: project-serviceproject: modules/ProjectService/src/ProjectService.HttpApi.Host/ProjectService.HttpApi.Host.csprojbindings:
- port: 44372 //update your ports hereprotocol: httpsNow run the tye command.
tye runThis 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.