Skip to content

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 443XY must 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.Contracts only 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 GetResponseAsObjectAsync( string url, HttpStatusCode expectedStatusCode = HttpStatusCode.OK) { var strResponse = await GetResponseAsStringAsync(url, expectedStatusCode); return JsonSerializer.Deserialize(strResponse, new JsonSerializerOptions(JsonSerializerDefaults.Web))!; }

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(applicationName: "GridLab.Gmss.MyService");

public partial class TestProgram { } ```


✅ Post-Setup Checklist

Use this checklist to verify your service is properly set up:

  • Solution compiles without errors
  • common.props imported in all .csproj files
  • .gitignore and .editorconfig created
  • appsettings.json configured with correct connection strings
  • DbContext created with event inbox/outbox
  • DbContextFactory created for EF Core CLI
  • Initial migration created and applied
  • Health checks configured and accessible
  • DemoController returns "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.json for 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


📖 Additional Resources