- Published on
Add a new Module and convert it to a microservice in ABP
Table of Contents
- Intro
- Creating the abp application and run migrations
- Run Migrations
- Add a new Module
- Add and configure the host to the module
- Add new Entity to the ProjectService
- Create an Entity
- Add Entity to EfCore
- Configure Entity in EfCore
- Prepare for the migration
- Adding Migrations
- Create a Entity Dto
- Create a AppService interface
- Create an Application Services
- Update the EntityFrameworkCoreModule
- Create API Scope, API Resource and Swagger Client in IdentityServer
- Run Migrations again for the MainApp
- Communicating with the Microservice
- Create a UI to display the projects
- Running the application
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
abp new MainApp -t app -u mvc --tiered
Run Migrations
Change directory to src/MainApp.DbMigrator and run the migration project
dotnet 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.
abp 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.
cd .\modules\ProjectService\src\
Now lets create a Web Api project to host our module.
dotnet 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
{
"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>
<PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
<PackageReference Include="Volo.Abp.EntityFrameworkCore" Version="5.0.0-rc.1" />
<PackageReference Include="Volo.Abp.AspNetCore.MultiTenancy" Version="5.0.0-rc.1" />
<PackageReference Include="Volo.Abp.Autofac" Version="5.0.0-rc.1" />
<PackageReference Include="Volo.Abp.Core" Version="5.0.0-rc.1" />
<PackageReference Include="Volo.Abp.EntityFrameworkCore.SqlServer" Version="5.0.0-rc.1" />
<PackageReference Include="Volo.Abp.Swashbuckle" Version="5.0.0-rc.1" />
<PackageReference Include="Volo.Abp.AspNetCore.Authentication.JwtBearer" Version="5.0.0-rc.1" />
<PackageReference Include="Volo.Abp.AspNetCore.Serilog" Version="5.0.0-rc.1" />
<PackageReference Include="Serilog.Extensions.Logging" Version="3.0.1" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
<PackageReference Include="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
.
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Cors;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.OpenApi.Models;
using ProjectService.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using Volo.Abp;
using Volo.Abp.AspNetCore.MultiTenancy;
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Serilog;
using Volo.Abp.Autofac;
using Volo.Abp.Modularity;
using Volo.Abp.Swashbuckle;
namespace ProjectService.HttpApi.Host
{
[DependsOn(
typeof(ProjectServiceHttpApiModule),
typeof(ProjectServiceApplicationModule),
typeof(ProjectServiceEntityFrameworkCoreModule),
typeof(AbpAspNetCoreMultiTenancyModule),
typeof(AbpAutofacModule),
typeof(AbpAspNetCoreSerilogModule),
typeof(AbpSwashbuckleModule)
)]
public class ProjectServiceHostModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = 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"],
new Dictionary<string, string>
{
{"ProjectService", "ProjectService API"}
},
options =>
{
options.SwaggerDoc("v1", new OpenApiInfo { 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();
});
});
}
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
var app = context.GetApplicationBuilder();
var env = 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");
var configuration = 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
using System;
using System.IO;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Serilog;
using Serilog.Events;
namespace ProjectService
{
public class Program
{
public static int Main(string[] args)
{
Log.Logger = new LoggerConfiguration()
#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();
return 0;
}
catch (Exception ex)
{
Log.Fatal(ex, "Host terminated unexpectedly!");
return 1;
}
finally
{
Log.CloseAndFlush();
}
}
internal static IHostBuilder CreateHostBuilder(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
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using ProjectService.HttpApi.Host;
namespace ProjectService
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddApplication<ProjectServiceHostModule>();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
{
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.
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc;
namespace ProjectService.HttpApi.Host.Controllers
{
public class HomeController : AbpController
{
public ActionResult Index()
{
return Redirect("~/swagger");
}
}
}
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.
public class Project : Entity<Guid>
{
public string Name { get; set; }
}
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
public 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
builder.Entity<Project>(b =>
{
//Configure table & schema name
b.ToTable(ProjectServiceDbProperties.DbTablePrefix + "Projects", ProjectServiceDbProperties.Schema);
b.ConfigureByConvention();
});
Prepare for the migration
dotnet tool update --global dotnet-ef
Add necessary packages to the ProjectService.EntityFrameworkCore
project.
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Volo.Abp.EntityFrameworkCore" Version="5.0.0-rc.1" />
<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
public class ProjectServiceDbContextFactory : IDesignTimeDbContextFactory<ProjectServiceDbContext>
{
public ProjectServiceDbContext CreateDbContext(string[] args)
{
var builder = new DbContextOptionsBuilder<ProjectServiceDbContext>()
.UseSqlServer(GetConnectionStringFromConfiguration());
return new ProjectServiceDbContext(builder.Options);
}
private static string GetConnectionStringFromConfiguration()
{
return BuildConfiguration()
.GetConnectionString(ProjectServiceDbProperties.ConnectionStringName);
}
private static IConfigurationRoot BuildConfiguration()
{
var builder = new ConfigurationBuilder()
.SetBasePath(
Path.Combine(
Directory.GetCurrentDirectory(),
$"..{Path.DirectorySeparatorChar}ProjectService.HttpApi.Host"
)
)
.AddJsonFile("appsettings.json", optional: false);
return builder.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
.
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:
dotnet ef migrations add created_projects
Verify the migrations created in the migrations folder.
To update the database run this command
dotnet ef database update
Create a Entity Dto
Dto are placed in ProjectService.Application.Contracts
project
public class ProjectDto: EntityDto<Guid>
{
public string Name { get; set; }
}
Create a AppService interface
create IProjectAppService
interface in the ProjectService.Application.Contracts
project
public interface IProjectAppService: IApplicationService
{
Task<List<ProjectDto>> GetListAsync();
Task<ProjectDto> CreateAsync(string text);
Task DeleteAsync(Guid id);
}
Create an Application Services
Learn more about the Application Services in the abp docs.
Application service are created in the ProjectService.Application
project
[Authorize]
public class ProjectAppService : ProjectServiceAppService, IProjectAppService
{
private readonly IRepository<Project, Guid> projectRepository;
public ProjectAppService(IRepository<Project, Guid> projectRepository)
{
this.projectRepository = projectRepository;
}
public async Task<ProjectDto> CreateAsync(string text)
{
var projectItem = await projectRepository.InsertAsync(
new Project { Name = text }
);
return new ProjectDto
{
Id = projectItem.Id,
Name = projectItem.Name
};
}
public async Task DeleteAsync(Guid id)
{
await projectRepository.DeleteAsync(id);
}
public async Task<List<ProjectDto>> GetListAsync()
{
var items = await projectRepository.GetListAsync();
return items
.Select(item => new ProjectDto
{
Id = item.Id,
Name = item.Name
}).ToList();
}
}
Update the EntityFrameworkCoreModule
Update the ConfigureServices
method in the ProjectServiceEntityFrameworkCoreModule
file.
context.Services.AddAbpDbContext<ProjectServiceDbContext>(options =>
{
options.AddDefaultRepositories(includeAllEntities: true);
});
Configure<AbpDbContextOptions>(options =>
{
options.UseSqlServer();
});
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
.
private async Task CreateApiScopesAsync()
{
await CreateApiScopeAsync("MainApp");
await CreateApiScopeAsync("ProjectService");
}
private async Task CreateApiResourcesAsync()
{
var commonApiUserClaims = new[]
{
"email",
"email_verified",
"name",
"phone_number",
"phone_number_verified",
"role"
};
await CreateApiResourceAsync("MainApp", commonApiUserClaims);
await CreateApiResourceAsync("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
var commonScopes = 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
.
var swaggerClientIdProjectService = configurationSection["ProjectService_Swagger:ClientId"];
if (!swaggerClientIdProjectService.IsNullOrWhiteSpace())
{
var swaggerRootUrl = configurationSection["ProjectService_Swagger:RootUrl"].TrimEnd('/');
await CreateClientAsync(
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("/") }
);
}
Run Migrations again for the MainApp
Change directory to MainApp.DbMigrator
and run the migration project
dotnet 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.
"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"
},
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
<ProjectReference Include="..\..\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 proxies
context.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/"
}
}
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
.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 UI to display the projects
Create a project page to display the project list in the MainApp.Web
.
Create a Projects.cshtml
@page @model MainApp.Web.Pages.ProjectsModel
<div class="container">
<h1>List of projects</h1>
@if(Model.Projects != null && Model.Projects.Count > 0){ @foreach (var item in Model?.Projects) {
<div class="card">
<div class="card-body">
<h5 class="card-title">@item.Name</h5>
</div>
</div>
} }
</div>
Create a Projects.cshtml.cs
public class ProjectsModel : MainAppPageModel
{
private readonly ILogger<ProjectsModel> logger;
public List<ProjectDto> Projects { get; set; }
private IProjectAppService projectAppService { get; set; }
public ProjectsModel(IProjectAppService projectAppService, ILogger<ProjectsModel> logger)
{
this.projectAppService = projectAppService;
this.logger = logger;
Projects = new List<ProjectDto>();
}
public void OnGet()
{
try
{
var projects = projectAppService.GetListAsync().Result;
Projects = projects;
}
catch (Exception e)
{
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,
new ApplicationMenuItem(
MainAppMenus.Home,
l["Projects"],
"~/Projects",
icon: "fas fa-list",
order: 0
)
);
Now lets run the application.
Running the application
Since we have run 4 services lets init tye
so that it will be easier.
tye 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
.
name: mainapp
services:
- name: mainapp-web
project: src/MainApp.Web/MainApp.Web.csproj
bindings:
- port: 44343 //update your ports here
protocol: https
- name: mainapp-identityserver
project: src/MainApp.IdentityServer/MainApp.IdentityServer.csproj
bindings:
- port: 44373 //update your ports here
protocol: https
- name: mainapp-httpapi-host
project: src/MainApp.HttpApi.Host/MainApp.HttpApi.Host.csproj
bindings:
- port: 44358 //update your ports here
protocol: https
- name: project-service
project: modules/ProjectService/src/ProjectService.HttpApi.Host/ProjectService.HttpApi.Host.csproj
bindings:
- port: 44372 //update your ports here
protocol: https
Now run the tye command.
tye 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.
Related Posts
ABP-Powered Web App with Inertia.js, React, and Vite
Building a web application with ABP Framework, Inertia.js, React, and Vite.
Migrating Tye to Aspire
In this post we will see how to migrate the Tye to Aspire
ABP React Template V2
Version 2 of React Starter Template for ABP application with Next.js, Tailwind CSS, and shadcn-ui.