Initialize Microservice Project - Skills Guide¶
Skill Type: Project Scaffolding & Microservice Architecture
Technology Stack: .NET 10, ABP Framework, Entity Framework Core, PostgreSQL, Serilog
Complexity Level: Intermediate
Last Updated: 2025
📋 Overview¶
This guide walks through creating a new microservice project within the GMS² platform. Each microservice follows a no-layer ABP architecture with its own database, API endpoints, health checks, and test infrastructure.
What You'll Create¶
| Project | Purpose | Naming Convention |
|---|---|---|
| Service Host | API host with controllers, data access, configuration | GridLab.Gmss.MyService |
| Contracts | Shared DTOs, interfaces, localization, permissions | GridLab.Gmss.MyService.Contracts |
| Tests | Integration tests with SQLite in-memory | GridLab.Gmss.MyService.Tests |
Prerequisites¶
- .NET 10 SDK
- Visual Studio 2022+ or Rider
- PostgreSQL database server
- Redis server (for distributed caching)
- RabbitMQ (for distributed events)
- Git version control
Naming Conventions¶
- Use
-(hyphen) for spaces in folder names - Solution suffix:
GridLab.Gmss.{ServiceName} - Database prefix:
Gmss_{ServiceName} - Port assignment:
http://localhost:443XY(unique per service)
🚀 Phase 1: Solution Setup¶
Step 1: Create Solution Folder¶
Create a folder for your project under the services directory.
Step 2: Create Solution¶
Add a solution with the service name suffix like GridLab.Gmss.MyService.
Step 3: Create common.props¶
Create common.props in the solution root with shared project properties:
<Project>
<PropertyGroup>
<LangVersion>latest</LangVersion>
<Version>1.0.0</Version>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<NuGetAudit>false</NuGetAudit>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AbpProjectType>service-nolayer</AbpProjectType>
</PropertyGroup>
</Project>
Step 4: Create .editorconfig¶
[*.csproj]
indent_size = 2
Step 5: Create .gitignore¶
Create a .gitignore file. Below is the recommended template for .NET / Visual Studio projects:
📄 Click to expand full .gitignore template
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
*.ncb
*.aps
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
**/.idea/
*.sln.iml
# ABP Studio
**/.abpstudio/
Step 6: Add Projects to Solution¶
Create three projects within the solution:
| # | Project Type | Name | Purpose |
|---|---|---|---|
| 1 | ASP.NET Core Web | GridLab.Gmss.MyService | Service host (API, data, configuration) |
| 2 | Class Library | GridLab.Gmss.MyService.Contracts | Shared contracts, DTOs, localization |
| 3 | xUnit Test | GridLab.Gmss.MyService.Tests | Integration and unit tests |
🔧 Phase 2: Service Host Project¶
Step 1: Configure .csproj¶
Import common.props and add all required package references:
<Project Sdk="Microsoft.NET.Sdk.Web">
<Import Project="..\common.props" />
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<CopyLocalLockFileAssemblies>True</CopyLocalLockFileAssemblies>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.2" />
<PackageReference Include="Serilog.Sinks.Async" Version="2.1.0" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.ElasticSearch" Version="10.0.0" />
<PackageReference Include="Serilog.Sinks.OpenTelemetry" Version="4.2.0" />
<PackageReference Include="AspNetCore.HealthChecks.UI" Version="9.0.0" />
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="9.0.0" />
<PackageReference Include="AspNetCore.HealthChecks.UI.InMemory.Storage" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="10.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.5" />
<PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1" />
<PackageReference Include="DistributedLock.Redis" Version="1.1.0" />
<PackageReference Include="KubernetesClient" Version="18.0.5" />
<PackageReference Include="IdentityModel" Version="7.0.0" />
</ItemGroup>
<!-- Service Dependencies -->
<ItemGroup>
<PackageReference Include="Volo.Abp.EntityFrameworkCore.PostgreSql" Version="10.1.1" />
<PackageReference Include="Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy" Version="10.1.1" />
<PackageReference Include="Volo.Abp.AspNetCore.Authentication.JwtBearer" Version="10.1.1" />
<PackageReference Include="Volo.Abp.Autofac" Version="10.1.1" />
<PackageReference Include="Volo.Abp.AspNetCore.Serilog" Version="10.1.1" />
<PackageReference Include="Volo.Abp.Swashbuckle" Version="10.1.1" />
<PackageReference Include="Volo.Abp.EventBus.RabbitMQ" Version="10.1.1" />
<PackageReference Include="Volo.Abp.My.RabbitMQ" Version="10.1.1" />
<PackageReference Include="Volo.Abp.DistributedLocking" Version="10.1.1" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Volo.Abp.FeatureManagement.EntityFrameworkCore" Version="10.1.1" />
<PackageReference Include="Volo.Abp.PermissionManagement.EntityFrameworkCore" Version="10.1.1" />
<PackageReference Include="Volo.Abp.SettingManagement.EntityFrameworkCore" Version="10.1.1" />
<PackageReference Include="Volo.Abp.LanguageManagement.EntityFrameworkCore" Version="10.1.1" />
<PackageReference Include="Volo.Abp.BlobStoring.Database.EntityFrameworkCore" Version="10.1.1" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="GridLab.Gmss.TenantManagement.EntityFrameworkCore" Version="10.1.1.*" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="GridLab.Gmss.AuditLogging.EntityFrameworkCore" Version="10.1.1.*" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Volo.Abp.Studio.Client.AspNetCore" Version="2.2.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../GridLab.Gmss.MyService.Contracts/GridLab.Gmss.MyService.Contracts.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\aspire\GridLab.Gmss.ServiceDefaults\GridLab.Gmss.ServiceDefaults.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Remove="Logs\**" />
<Content Remove="Logs\**" />
<EmbeddedResource Remove="Logs\**" />
<None Remove="Logs\**" />
</ItemGroup>
<ItemGroup>
<Content Remove="ClientProxies\**\*.json" />
<EmbeddedResource Include="ClientProxies\**\*.json" />
</ItemGroup>
</Project>
````
### Step 2: Add `Program.cs`
Configure application startup with Serilog, Elasticsearch logging, and OpenTelemetry:
```cs
public class Program
{
public static async Task<int> Main(string[] args)
{
Log.Logger = new LoggerConfiguration()
.WriteTo.Async(c => c.File("Logs/logs.txt"))
.WriteTo.Async(c => c.Console())
.CreateBootstrapLogger();
try
{
Log.Information($"Starting {GetCurrentAssemblyName()}");
AbpStudioEnvironmentVariableLoader.Load();
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
builder.Host
.AddAppSettingsSecretsJson()
.UseAutofac()
.UseSerilog((context, services, loggerConfiguration) =>
{
var applicationName = services.GetRequiredService<IApplicationInfoAccessor>().ApplicationName;
loggerConfiguration
.ReadFrom.Configuration(context.Configuration)
.ReadFrom.Services(services)
.Enrich.WithProperty("Application", applicationName)
.If(context.Configuration.GetValue<bool>("ElasticSearch:IsLoggingEnabled"), c =>
c.WriteTo.Elasticsearch(
new ElasticsearchSinkOptions(new Uri(context.Configuration["ElasticSearch:Url"]!))
{
AutoRegisterTemplate = true,
AutoRegisterTemplateVersion = AutoRegisterTemplateVersion.ESv6,
IndexFormat = "Gmss-log-{0:yyyy.MM}"
})
)
.WriteTo.Async(c => c.OpenTelemetry())
.WriteTo.Async(c => c.AbpStudio(services));
});
await builder.AddApplicationAsync<GmssMyServiceModule>();
var app = builder.Build();
await app.InitializeApplicationAsync();
await app.RunAsync();
Log.Information($"Stopped {GetCurrentAssemblyName()}");
return 0;
}
catch (HostAbortedException)
{
/* Ignoring this exception because: https://github.com/dotnet/efcore/issues/29809#issuecomment-1345132260 */
return 2;
}
catch (Exception ex)
{
Console.WriteLine($"{GetCurrentAssemblyName()} terminated unexpectedly!");
Console.WriteLine(ex.ToString());
Console.WriteLine(ex.StackTrace ?? "");
Log.Fatal(ex, $"{GetCurrentAssemblyName()} terminated unexpectedly!");
Log.Fatal(ex.Message);
Log.Fatal(ex.StackTrace ?? "");
return 1;
}
finally
{
await Log.CloseAndFlushAsync();
}
}
private static string GetCurrentAssemblyName()
{
return typeof(Program).Assembly.GetName().Name!;
}
}
Step 3: Add Application Settings¶
Create two configuration files:
appsettings.Development.json¶
Development-specific overrides (enable PII for debugging):
{
"App": {
"EnablePII": true
}
}
appsettings.json¶
Full application configuration with connection strings, auth, caching, and messaging:
⚠️ Important: Replace passwords and connection strings with your actual values. Never commit secrets to source control.
{
"ConnectionStrings": {
"AdministrationService": "Host=localhost;Port=5432;Database=Gmss_Administration;User ID=postgres;Password=myPassword;Timeout=240;",
"AbpBlobStoring": "Host=localhost;Port=5432;Database=Gmss_BlobStoring;User ID=postgres;Password=myPassword;Timeout=240;",
"AuditLoggingService": "Host=localhost;Port=5432;Database=Gmss_AuditLogging;User ID=postgres;Password=myPassword;Timeout=240;",
"TenantManagementService": "Host=localhost;Port=5432;Database=Gmss_TenantManagement;User ID=postgres;Password=myPassword;Timeout=240;",
"MyService": "Host=localhost;Port=5432;Database=Gmss_My;User ID=postgres;Password=myPassword;Timeout=240;"
},
"App": {
"CorsOrigins": "http://localhost:44310",
"EnablePII": false,
"HealthCheckUrl": "/health-status"
},
"Swagger": {
"IsEnabled": true
},
"AuthServer": {
"Authority": "http://localhost:44346",
"MetaAddress": "http://localhost:44346",
"RequireHttpsMetadata": false,
"SwaggerClientId": "SwaggerTestUI",
"Audience": "MyService"
},
"Redis": {
"Configuration": "localhost:6379"
},
"RabbitMQ": {
"Connections": {
"Default": {
"HostName": "localhost"
}
},
"EventBus": {
"ClientName": "Gmss_MyService",
"ExchangeName": "Gmss"
}
},
"RemoteServices": {},
"AbpDistributedCache": {
"KeyPrefix": "Gmss:"
},
"DataProtection": {
"ApplicationName": "Gmss",
"Keys": "Gmss-Protection-Keys"
},
"ElasticSearch": {
"IsLoggingEnabled": true,
"Url": "http://localhost:9200"
},
"StringEncryption": {
"DefaultPassPhrase": "ENaTKK5o6BglpUjM"
}
}
Step 4: Add Data Layer¶
The data layer consists of several classes for Entity Framework Core integration.
4.1: Add DbContext¶
Create the main database context with event inbox/outbox support:
[ConnectionStringName(DatabaseName)]
public class MyServiceDbContext :
AbpDbContext<MyServiceDbContext>,
IHasEventInbox,
IHasEventOutbox
{
public const string DbTablePrefix = "";
public const string DbSchema = null;
public const string DatabaseName = "MyService";
public DbSet<IncomingEventRecord> IncomingEvents { get; set; }
public DbSet<OutgoingEventRecord> OutgoingEvents { get; set; }
public MyServiceDbContext(DbContextOptions<MyServiceDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.ConfigureEventInbox();
builder.ConfigureEventOutbox();
}
}
4.2: Add DatabaseMigrationEventHandler¶
Handles database migrations triggered by distributed events:
public class MyServiceDatabaseMigrationEventHandler : EfCoreDatabaseMigrationEventHandlerBase<MyServiceDbContext>
{
private readonly MyServiceDataSeeder _dataSeeder;
public MyServiceDatabaseMigrationEventHandler(
ILoggerFactory loggerFactory,
ICurrentTenant currentTenant,
IUnitOfWorkManager unitOfWorkManager,
ITenantStore tenantStore,
IAbpDistributedLock abpDistributedLock,
IDistributedEventBus distributedEventBus,
MyServiceDataSeeder dataSeeder
) : base(
MyServiceDbContext.DatabaseName,
currentTenant,
unitOfWorkManager,
tenantStore,
abpDistributedLock,
distributedEventBus,
loggerFactory)
{
_dataSeeder = dataSeeder;
}
protected override async Task SeedAsync(Guid? tenantId)
{
await _dataSeeder.SeedAsync(tenantId);
}
}
4.3: Add DataSeeder¶
Seed initial data when the service starts:
public class BackgroundJobsServiceDataSeeder : ITransientDependency
{
private readonly ILogger<BackgroundJobsServiceDataSeeder> _logger;
public BackgroundJobsServiceDataSeeder(
ILogger<BackgroundJobsServiceDataSeeder> logger)
{
_logger = logger;
}
public async Task SeedAsync(Guid? tenantId = null)
{
_logger.LogInformation("Seeding data...");
//...
}
}
4.4: Add DbContextFactory¶
Required for EF Core CLI commands (Add-Migration, Update-Database):
namespace GridLab.Gmss.MyService.Data;
/* This class is needed for EF Core console commands
* (like Add-Migration and Update-Database commands)
* */
public class MyServiceDbContextFactory : IDesignTimeDbContextFactory<MyServiceDbContext>
{
[ModuleInitializer]
public static void Initialize()
{
// https://www.npgsql.org/efcore/release-notes/6.0.html#opting-out-of-the-new-timestamp-mapping-logic
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
}
public MyServiceDbContext CreateDbContext(string[] args)
{
var builder = new DbContextOptionsBuilder<MyServiceDbContext>()
.UseNpgsql(GetConnectionStringFromConfiguration(), b =>
{
b.MigrationsHistoryTable("__MyService_Migrations");
});
return new MyServiceDbContext(builder.Options);
}
private static string GetConnectionStringFromConfiguration()
{
return BuildConfiguration().GetConnectionString(MyServiceDbContext.DatabaseName)
?? throw new ApplicationException($"Could not find a connection string named '{MyServiceDbContext.DatabaseName}'.");
}
private static IConfigurationRoot BuildConfiguration()
{
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false)
.AddEnvironmentVariables();
return builder.Build();
}
}
4.5: Add Runtime Database Migrator¶
Handles runtime migrations with distributed locking:
public class MyServiceRuntimeDatabaseMigrator : EfCoreRuntimeDatabaseMigratorBase<MyServiceDbContext>
{
private readonly MyServiceDataSeeder _dataSeeder;
public MyServiceRuntimeDatabaseMigrator(
ILoggerFactory loggerFactory,
IUnitOfWorkManager unitOfWorkManager,
IServiceProvider serviceProvider,
ICurrentTenant currentTenant,
IAbpDistributedLock abpDistributedLock,
IDistributedEventBus distributedEventBus,
MyServiceDataSeeder dataSeeder
) : base(
MyServiceDbContext.DatabaseName,
unitOfWorkManager,
serviceProvider,
currentTenant,
abpDistributedLock,
distributedEventBus,
loggerFactory)
{
_dataSeeder = dataSeeder;
}
protected override async Task SeedAsync()
{
await _dataSeeder.SeedAsync();
}
}
4.6: Add Health Checks¶
Add HealthChecksBuilderExtensions for service monitoring and health status UI:
public static class HealthChecksBuilderExtensions
{
public static void AddBackgroundJobsServiceHealthChecks(this IServiceCollection services)
{
// Add your health checks here
var healthChecksBuilder = services.AddHealthChecks();
healthChecksBuilder.AddCheck<BackgroundJobsCheck>("Background Jobs Service Check", tags: ["background-jobs"]);
var configuration = services.GetConfiguration();
var healthCheckUrl = configuration["App:HealthCheckUrl"];
if (string.IsNullOrEmpty(healthCheckUrl))
{
healthCheckUrl = "/health-status";
}
services.ConfigureHealthCheckEndpoint(healthCheckUrl);
var healthChecksUiBuilder = services.AddHealthChecksUI(settings =>
{
settings.AddHealthCheckEndpoint("Background Jobs Service Health Status", configuration["App:HealthUiCheckUrl"] ?? healthCheckUrl);
});
// Set your HealthCheck UI Storage here
healthChecksUiBuilder.AddInMemoryStorage();
services.MapHealthChecksUiEndpoints(options =>
{
options.UIPath = "/health-ui";
options.ApiPath = "/health-api";
});
}
private static IServiceCollection ConfigureHealthCheckEndpoint(this IServiceCollection services, string path)
{
services.Configure<AbpEndpointRouterOptions>(options =>
{
options.EndpointConfigureActions.Add(endpointContext =>
{
endpointContext.Endpoints.MapHealthChecks(
new PathString(path.EnsureStartsWith('/')),
new HealthCheckOptions
{
Predicate = _ => true,
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse,
AllowCachingResponses = false,
});
});
});
return services;
}
private static IServiceCollection MapHealthChecksUiEndpoints(this IServiceCollection services, Action<global::HealthChecks.UI.Configuration.Options>? setupOption = null)
{
services.Configure<AbpEndpointRouterOptions>(routerOptions =>
{
routerOptions.EndpointConfigureActions.Add(endpointContext =>
{
endpointContext.Endpoints.MapHealthChecksUI(setupOption);
});
});
return services;
}
}
Step 5: Configure Services¶
Configure Redis, distributed caching, and other infrastructure services. See the framework documentation for Redis and cache configuration details.
Step 6: Add Metrics Service¶
Add OpenTelemetry metrics for monitoring:
public class GmssMetrics : ISingletonDependency
{
public const string MeterName = "Gmss.Api";
private readonly Counter<long> _helloRequestCounter;
public GmssMetrics(IMeterFactory meterFactory)
{
var meter = meterFactory.Create(MeterName);
_helloRequestCounter = meter.CreateCounter<long>("hello_requests.count");
}
public void IncrementHelloCounter()
{
_helloRequestCounter.Add(1);
}
}
Step 7: Add Controllers¶
Create a Controllers folder and add your API and home controllers.
DemoController.cs¶
Sample API controller with metrics and authorization:
[Route("api/my/demo")]
[Area("my")]
[RemoteService(Name = "MyService")]
public class DemoController : AbpController
{
private readonly GmssMetrics _gmssMetrics;
public DemoController(GmssMetrics gmssMetrics)
{
_gmssMetrics = gmssMetrics;
}
[HttpGet]
[Route("hello")]
public async Task<string> HelloWorld()
{
_gmssMetrics.IncrementHelloCounter();
return await Task.FromResult("Hello World!");
}
[HttpGet]
[Route("hello-authorized")]
[Authorize]
public async Task<string> HelloWorldAuthorized()
{
return await Task.FromResult("Hello World (Authorized)!");
}
}
HomeController.cs¶
Redirect root URL to Swagger UI:
[RemoteService(false)]
public class HomeController : AbpController
{
public ActionResult Index()
{
return Redirect("~/swagger");
}
}
Step 8: Configure Launch Settings¶
Configure launchSettings.json with a unique port assignment.
⚠️ Important: The port value
443XYmust be unique across all services in the solution.
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:443XY"
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"GridLab.Gmss.CimService": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:443XY",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
Step 9: Run Initial Database Migration¶
After the service project compiles successfully, create the initial EF Core migration:
dotnet ef migrations add Initial --context BackgroundJobsServiceDbContext --output-dir Migrations
📦 Phase 3: Contracts Project¶
The Contracts project contains shared types consumed by other services and clients.
Step 1: Configure .csproj¶
Import common.props and add ABP framework base packages.
💡 Note: Include
GridLab.Gmss.My.Application.Contractsonly if it is published or available.
<Import Project="..\common.props" />
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace>GridLab.Gmss.MyService</RootNamespace>
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
<CopyLocalLockFileAssemblies>True</CopyLocalLockFileAssemblies>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="10.0.5" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Volo.Abp.UI" Version="10.1.1" />
<PackageReference Include="Volo.Abp.Validation" Version="10.1.1" />
<PackageReference Include="Volo.Abp.Authorization.Abstractions" Version="10.1.1" />
<PackageReference Include="Volo.Abp.Ddd.Application.Contracts" Version="10.1.1" />
<PackageReference Include="Volo.Abp.Ddd.Domain.Shared" Version="10.1.1" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="GridLab.Gmss.My.Application.Contracts" Version="10.1.1.*" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Localization\MyService\*.json" />
<Content Remove="Localization\MyService\*.json" />
</ItemGroup>
Step 2: Add Contracts Module¶
Create the GmssMyServiceContractsModule and configure localization:
using Localization.Resources.AbpUi;
using GridLab.Gmss.MyService.Localization;
using Volo.Abp.Application;
using Volo.Abp.Authorization;
using Volo.Abp.Domain;
using Volo.Abp.Localization;
using Volo.Abp.Localization.ExceptionHandling;
using Volo.Abp.Modularity;
using Volo.Abp.UI;
using Volo.Abp.Validation;
using Volo.Abp.Validation.Localization;
using Volo.Abp.VirtualFileSystem;
using GridLab.Gmss.My;
namespace GridLab.Gmss.MyService;
[DependsOn(
typeof(AbpValidationModule),
typeof(AbpUiModule),
typeof(AbpAuthorizationAbstractionsModule),
typeof(AbpDddApplicationContractsModule),
typeof(MyApplicationContractsModule),
typeof(AbpDddDomainSharedModule)
)]
public class GmssMyServiceContractsModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpVirtualFileSystemOptions>(options =>
{
options.FileSets.AddEmbedded<GmssMyServiceContractsModule>();
});
Configure<AbpLocalizationOptions>(options =>
{
options.Resources
.Add<MyServiceResource>("en")
.AddBaseTypes(typeof(AbpValidationResource), typeof(AbpUiResource))
.AddVirtualJson("/Localization/MyService");
});
Configure<AbpExceptionLocalizationOptions>(options =>
{
options.MapCodeNamespace("MyService", typeof(MyServiceResource));
});
}
}
Step 3: Add Localization Resources¶
Create the localization folder structure:
GridLab.Gmss.MyService.Contracts/
└── Localization/
└── MyService/
├── en.json
└── ... (other languages)
Example en.json:
{
"culture": "en",
"texts": {
"MyService": "My Service"
}
}
🧪 Phase 4: Tests Project¶
The Tests project uses SQLite in-memory database and ABP's test infrastructure.
Step 1: Configure .csproj¶
Import common.props and add xUnit, NSubstitute, and ABP test packages:
<Import Project="..\common.props" />
<PropertyGroup>
<Version>1.0.0</Version>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="NSubstitute.Analyzers.CSharp" Version="1.0.17">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Shouldly" Version="4.3.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.extensibility.execution" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.5" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Volo.Abp.AspNetCore.TestBase" Version="10.1.1" />
<PackageReference Include="Volo.Abp.EntityFrameworkCore.Sqlite" Version="10.1.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\GridLab.Gmss.MyService\GridLab.Gmss.MyService.csproj" />
</ItemGroup>
Step 2: Add Test Infrastructure¶
Create three files for the test infrastructure.
MyServiceTestsModule.cs¶
The test module carefully selects dependencies appropriate for testing (no RabbitMQ, no real Redis, etc.):
⚠️ Important: If you change
GmssMyServiceModule, review this class to keep test code compatible.
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using GridLab.Gmss.MyService.Data;
using Volo.Abp;
using Volo.Abp.AspNetCore.TestBase;
using Volo.Abp.My;
using Volo.Abp.BackgroundWorkers;
using Volo.Abp.Caching;
using Volo.Abp.DistributedLocking;
using Volo.Abp.EventBus;
using Volo.Abp.FeatureManagement;
using Volo.Abp.Modularity;
using Volo.Abp.PermissionManagement;
using Volo.Abp.SettingManagement;
using Volo.Abp.Uow;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore.Sqlite;
using Volo.Abp.SettingManagement.EntityFrameworkCore;
using Volo.Abp.PermissionManagement.EntityFrameworkCore;
using Volo.Abp.FeatureManagement.EntityFrameworkCore;
using Volo.Abp.BlobStoring.Database.EntityFrameworkCore;
namespace GridLab.Gmss.MyService.Tests;
/* This project has a dotnet project reference to the GridLab.Gmss.MyService project,
* but it does not have a module dependency to the GmssMyServiceModule module class.
* Because, GmssMyServiceModule has configurations proper for development and production
* environments, but not proper or necessary for tests.
*
* In this test project, we are carefully depending on the modules that we need in tests.
*
* For example, GmssMyServiceModule depends on AbpEventBusRabbitMqModule,
* but this module depends on AbpEventBusModule since we don't want to use RabbitMQ in tests.
* AbpEventBusModule has an in-process event bus instead of a real distributed event bus, and it is fine for tests.
*
* WARNING: If you change GmssMyServiceModule class, you may need to properly change this class to keep
* test code compatible with the application code.
*/
[DependsOn(
typeof(AbpAspNetCoreTestBaseModule),
typeof(AbpEntityFrameworkCoreSqliteModule),
typeof(BlobStoringDatabaseEntityFrameworkCoreModule),
typeof(AbpSettingManagementEntityFrameworkCoreModule),
typeof(AbpPermissionManagementEntityFrameworkCoreModule),
typeof(AbpFeatureManagementEntityFrameworkCoreModule),
typeof(AbpEventBusModule),
typeof(AbpCachingModule),
typeof(AbpDistributedLockingAbstractionsModule)
)]
[AdditionalAssembly(typeof(GmssMyServiceModule))]
public class MyServiceTestsModule : AbpModule
{
private SqliteConnection? _sqliteConnection;
public override void PreConfigureServices(ServiceConfigurationContext context)
{
PreConfigure<AbpSqliteOptions>(x => x.BusyTimeout = null);
}
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
ConfigureAuthorization(context);
ConfigureDatabase(context);
ConfigureDatabaseTransactions(context);
ConfigureDynamicStores();
ConfigureMy();
}
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
var app = context.GetApplicationBuilder();
app.UseCorrelationId();
app.UseAbpRequestLocalization();
app.UseStaticFiles();
app.UseRouting();
app.UseUnitOfWork();
app.UseConfiguredEndpoints();
}
public override async Task OnPostApplicationInitializationAsync(ApplicationInitializationContext context)
{
using var scope = context.ServiceProvider.CreateScope();
await scope.ServiceProvider
.GetRequiredService<MyServiceDataSeeder>()
.SeedAsync();
}
public override void OnApplicationShutdown(ApplicationShutdownContext context)
{
_sqliteConnection?.Dispose();
}
private static void ConfigureAuthorization(ServiceConfigurationContext context)
{
/* We don't need to authorization in tests */
context.Services.AddAlwaysAllowAuthorization();
}
private void ConfigureDatabase(ServiceConfigurationContext context)
{
_sqliteConnection = CreateDatabaseAndGetConnection();
context.Services.AddAbpDbContext<MyServiceDbContext>(options =>
{
options.AddDefaultRepositories();
});
Configure<AbpDbContextOptions>(options =>
{
options.Configure(opts =>
{
/* Use SQLite for all EF Core DbContexts in tests */
opts.UseSqlite(_sqliteConnection);
});
});
}
private void ConfigureDatabaseTransactions(ServiceConfigurationContext context)
{
context.Services.AddAlwaysDisableUnitOfWorkTransaction();
Configure<AbpUnitOfWorkDefaultOptions>(options =>
{
options.TransactionBehavior = UnitOfWorkTransactionBehavior.Disabled;
});
}
private void ConfigureDynamicStores()
{
Configure<FeatureManagementOptions>(options =>
{
options.SaveStaticFeaturesToDatabase = false;
options.IsDynamicFeatureStoreEnabled = false;
});
Configure<PermissionManagementOptions>(options =>
{
options.SaveStaticPermissionsToDatabase = false;
options.IsDynamicPermissionStoreEnabled = false;
});
Configure<SettingManagementOptions>(options =>
{
options.SaveStaticSettingsToDatabase = false;
options.IsDynamicSettingStoreEnabled = false;
});
}
private void ConfigureMy()
{
Configure<AbpBackgroundWorkerOptions>(options =>
{
options.IsEnabled = false;
});
Configure<AbpBackgroundJobOptions>(options =>
{
options.IsJobExecutionEnabled = false;
});
}
private static SqliteConnection CreateDatabaseAndGetConnection()
{
var connection = new SqliteConnection("Data Source=:memory:");
connection.Open();
// MyServiceDbContext ()
new MyServiceDbContext(
new DbContextOptionsBuilder<MyServiceDbContext>().UseSqlite(connection).Options
).GetService<IRelationalDatabaseCreator>().CreateTables();
// PermissionManagementDbContext
new PermissionManagementDbContext(
new DbContextOptionsBuilder<PermissionManagementDbContext>().UseSqlite(connection).Options
).GetService<IRelationalDatabaseCreator>().CreateTables();
// FeatureManagementDbContext
new FeatureManagementDbContext(
new DbContextOptionsBuilder<FeatureManagementDbContext>().UseSqlite(connection).Options
).GetService<IRelationalDatabaseCreator>().CreateTables();
// SettingManagementDbContext
new SettingManagementDbContext(
new DbContextOptionsBuilder<SettingManagementDbContext>().UseSqlite(connection).Options
).GetService<IRelationalDatabaseCreator>().CreateTables();
// BlobStoringDbContext
new BlobStoringDbContext(
new DbContextOptionsBuilder<BlobStoringDbContext>().UseSqlite(connection).Options
).GetService<IRelationalDatabaseCreator>().CreateTables();
return connection;
}
}
MyServiceIntegrationTestBase.cs¶
Base class for integration tests with HTTP client helpers and Unit of Work support:
```cs
namespace GridLab.Gmss.MyService.Tests;
/* Inherit your integration test classes from this class */ public abstract class MyServiceIntegrationTestBase : AbpWebApplicationFactoryIntegratedTest
protected virtual async Task<string> GetResponseAsStringAsync(
string url,
HttpStatusCode expectedStatusCode = HttpStatusCode.OK)
{
var response = await GetResponseAsync(url, expectedStatusCode);
return await response.Content.ReadAsStringAsync();
}
protected virtual async Task<HttpResponseMessage> GetResponseAsync(
string url,
HttpStatusCode expectedStatusCode = HttpStatusCode.OK)
{
var response = await Client.GetAsync(url);
response.StatusCode.ShouldBe(expectedStatusCode);
return response;
}
protected virtual Task WithUnitOfWorkAsync(Func<Task> func)
{
return WithUnitOfWorkAsync(new AbpUnitOfWorkOptions(), func);
}
protected virtual async Task WithUnitOfWorkAsync(AbpUnitOfWorkOptions options, Func<Task> action)
{
using (var scope = ServiceProvider.CreateScope())
{
var uowManager = scope.ServiceProvider.GetRequiredService<IUnitOfWorkManager>();
using (var uow = uowManager.Begin(options))
{
await action();
await uow.CompleteAsync();
}
}
}
protected virtual Task<TResult> WithUnitOfWorkAsync<TResult>(Func<Task<TResult>> func)
{
return WithUnitOfWorkAsync(new AbpUnitOfWorkOptions(), func);
}
protected virtual async Task<TResult> WithUnitOfWorkAsync<TResult>(AbpUnitOfWorkOptions options, Func<Task<TResult>> func)
{
using (var scope = ServiceProvider.CreateScope())
{
var uowManager = scope.ServiceProvider.GetRequiredService<IUnitOfWorkManager>();
using (var uow = uowManager.Begin(options))
{
var result = await func();
await uow.CompleteAsync();
return result;
}
}
}
private static string GetContentRootFolder()
{
var assemblyDirectoryPath = Path.GetDirectoryName(typeof(GmssMyServiceModule).Assembly.Location);
if (assemblyDirectoryPath == null)
{
throw new Exception($"Could not find location of {typeof(GmssMyServiceModule).Assembly.FullName} assembly!");
}
return assemblyDirectoryPath;
}
}
```
TestProgram.cs¶
Minimal program entry point for the test host:
```cs using GridLab.Gmss.MyService.Tests; using Microsoft.AspNetCore.Builder; using Volo.Abp.AspNetCore.TestBase;
var builder = WebApplication.CreateBuilder(); builder.Environment.ContentRootPath = GetWebProjectContentRootPathHelper.Get("GridLab.Gmss.MyService.csproj"); await builder.RunAbpModuleAsync
public partial class TestProgram { } ```
✅ Post-Setup Checklist¶
Use this checklist to verify your service is properly set up:
- Solution compiles without errors
-
common.propsimported in all.csprojfiles -
.gitignoreand.editorconfigcreated -
appsettings.jsonconfigured with correct connection strings -
DbContextcreated with event inbox/outbox -
DbContextFactorycreated for EF Core CLI - Initial migration created and applied
- Health checks configured and accessible
-
DemoControllerreturns "Hello World!" at/api/my/demo/hello - Swagger UI accessible at root URL
- Launch settings use unique port
- Contracts project has localization resources
- Tests project compiles and runs
- Redis and RabbitMQ connections verified
📚 Best Practices¶
✅ Do's¶
- Keep services small and focused on a single domain
- Use the Contracts project for all shared types
- Implement health checks for all external dependencies
- Use distributed events for inter-service communication
- Follow the naming conventions consistently
- Add integration tests for all API endpoints
- Use
appsettings.Development.jsonfor local overrides
❌ Don'ts¶
- Don't reference other service host projects directly
- Don't hardcode connection strings or secrets
- Don't share database contexts between services
- Don't skip the migration step
- Don't use the same port for multiple services
- Don't bypass the Contracts project for shared types
🔍 Troubleshooting¶
Common Issues¶
Issue: Migration fails with "connection string not found" - Solution: Verify appsettings.json has the correct connection string name matching DbContext.DatabaseName
Issue: Port already in use - Solution: Choose a unique 443XY port in launchSettings.json
Issue: Tests fail with "table not found" - Solution: Ensure all DbContexts are created in CreateDatabaseAndGetConnection() in the test module
Issue: RabbitMQ connection error in tests - Solution: Tests use AbpEventBusModule (in-process), not RabbitMQ. Verify test module dependencies