Skip to content

Project Root Customization - Skills Guide

Skill Type: Project Setup & Configuration
Technology Stack: .NET, ABP Framework, Entity Framework Core
Complexity Level: Intermediate
Last Updated: 2025


📋 Overview

This guide covers customizing the root of a layered ABP application project, including solution cleanup, localization trimming, identity seeding, and database context separation.


🏗️ Solution Structure

Depending on development journey, you will have a solution like shown below:

Layered Solution Structure


🚀 Phase 1: Solution Cleanup

Step 1: Remove Unused Technologies

  • Remove unused technologies from .sln file and code repository.

You might consider removing unnecessary technologies.

Step 2: Verify Project Root Files

  • Ensure the following project-related files are available for reuse:
File Name Description
.dockerignore Files to be ignore when copying docker content.
.editorconfig File format and collection of text editor plugins for maintaining consistent coding styles between different editors and IDEs.
.gitattributes The .gitattributes file allows you to specify the files and paths attributes that should be used by git when performing git actions, such as git commit, etc. One of these attributes is the eol (end of line) and is used to configure the line endings for a file.
.trivyignore Vulnerability case number to be ignored when dockers are scanning.
CHANGELOG.md Changelog file.
CONTRIBUTING.md Guidelines for contributing to the project, including coding standards, commit message conventions, and the pull request process.
common.props MSBuild properties file used to define shared configuration settings, build properties, and variables that are commonly used across multiple projects in a solution.
COPYING.md Copyright file.
delete-bin-obj-folders.ps1 Script for cleaning bin and obj folders.
docker-compose.infrastructure.yml This file defines the core infrastructure services required for the project to function. It includes services such as databases, message brokers, caching systems, and other dependencies that are essential for the application to run.
docker-compose.infrastructure.override.yml This file is used to extend the base configuration of infrastructure services for specific environments or use cases.
docker-compose.migrations.yml Docker Compose configuration file specifically designed to manage database migrations for the application.
docker-compose.yml Docker Compose configuration file specifically for the application.
docker-compose-only.ps1 Only composes docker stack without building existing dotnet core projects.
docker-infrastructure-run.ps1 Starts all infrastructure services defined in the docker-compose.infrastructure file.
docker-infrastructure-stop.ps1 Stops all infrastructure services defined in the docker-compose.infrastructure file.
docker-run.ps1 Builds projects after that creates docker images by using Docker.Local file and last compose docker containers.
docker-stop.ps1 Stops composed stack with docker-run.ps1.
install-libs.ps1 Install NPM Packages for MVC / Razor Pages and Blazor Server UI types.
LICENSE.md Software license file.
NuGet.Config Nuget configuration for gitlab artifactory.
README.md ReadMe file.
renovate.json5 Renovate bot configuration for project.
sonarqube-analysis.xml This file is used to configure and provide details about the code analysis that SonarQube will perform on your project.
common.props Contains .csproj settings valid for all layers.
VERSION Release tag tracker, required for release-it git lab template.

Step 3: Update CI/CD Pipeline

  • Update basic GitLab CI template:
File Name Description
.gitlab-ci.yml Base ci/cd pipeline for gitlab projects.

Step 4: Install Local .NET Tools

  • Install dotnet tools locally (not globally) for reproducible builds.

A .NET tool is a special NuGet package that contains a console application. You can install a tool on your machine as a local tool. See more under .NET Tool

The .NET CLI uses manifest files to keep track of tools that are installed as local to a directory. When the manifest file is saved in the root directory of a source code repository, a contributor can clone the repository and invoke a single .NET CLI command to install all of the tools listed in the manifest file.

dotnet tool install volo.abp.studio.cli --version X.Y.Z
dotnet tool install dotnet-ef --version X.Y.Z
dotnet tool install cyclonedx --version X.Y.Z

Replace version number with latest available.

This creates a dotnet-tools.json file under the .config folder.


🔧 Phase 2: Domain Module Configuration

Step 1: Trim Unused Localizations

  • Keep only required languages (de, en, tr) at DomainModule:
public override void ConfigureServices(ServiceConfigurationContext context)
{
    Configure<AbpLocalizationOptions>(options =>
    {
        options.Languages.Add(new LanguageInfo("de", "de", "Deutsch"));
        options.Languages.Add(new LanguageInfo("en", "en", "English"));
        options.Languages.Add(new LanguageInfo("tr", "tr", "Türkçe"));
    });
}

Delete unused language .json files under Localization\GMSS folder in GridLab.Gmss.Domain.Shared layer.

Step 2: Remove Default Identity Data Seeders

  • Remove IdentityDataSeeder and IdentityDataSeedContributor from Volo.Abp.Identity at DomainModule:
public override void ConfigureServices(ServiceConfigurationContext context)
{
    IEnumerable<ServiceDescriptor> identityDataSeeders = context.Services.ToList().Where(service => service.ImplementationType == typeof(IdentityDataSeeder));
    if (identityDataSeeders.Any())
    {
        foreach (var dataSeeder in identityDataSeeders.ToArray())
        {
            bool result = context.Services.Remove(dataSeeder);
        }
    }

    IEnumerable<ServiceDescriptor> identityDataSeedContributors = context.Services.ToList().Where(service => service.ImplementationType == typeof(IdentityDataSeedContributor));
    if (identityDataSeedContributors.Any())
    {
        foreach (var contributor in identityDataSeedContributors.ToArray())
        {
            bool result = context.Services.Remove(contributor);
        }
    }
}

Step 3: Configure Emailing Module

  • Configure the Emailing module at DomainModule:
public override void ConfigureServices(ServiceConfigurationContext context)
{
    Configure<EmailOptions>(options =>
    {
        options.FromAddress = configuration["Email:FromAddress"];
        options.UserName = configuration["Email:UserName"];
    });
}

🗄️ Phase 3: Entity Framework Core Setup

Step 1: Add Hangfire Database Context

  • Add GMSSHangfireDbContext, GMSSHangfireDbContextFactory, GMSSHangfireDbContextSchemaMigrator and GMSSHangfireModelCacheKeyFactory to EntityFrameworkCore project.

Step 2: Add MiniProfiler Database Context

  • Add GMSSMiniProfilerDbContext and GMSSMiniprofilerDbContextSchemaMigrator to EntityFrameworkCore project.

Step 3: Separate Host and Tenant Database Contexts

  • Create GMSSTenantDbContext (separate from host context) at EntityFrameworkCore project.

Step 4: Add Custom Identity Data Seeder

  • Add custom GMSSIdentityDataSeeder for seeding platform admin and tenant admin:
public async virtual Task<IdentityDataSeedResult> SeedAdminAsync(string adminEmail, string adminPassword, string? adminUserName = null, Guid? tenantId = null)
{
    Check.NotNullOrWhiteSpace(adminEmail, nameof(adminEmail));
    Check.NotNullOrWhiteSpace(adminPassword, nameof(adminPassword));

    using (CurrentTenant.Change(tenantId))
    {
        await IdentityOptions.SetAsync();

        var result = new IdentityDataSeedResult();

        // default admin user name ise adrian
        if (adminUserName == null)
            adminUserName = "adrian";

        var adminUser = await UserRepository.FindByNormalizedUserNameAsync(
            LookupNormalizer.NormalizeName(adminUserName)
        );

        if (adminUser != null)
        {
            return result;
        }

        adminUser = new IdentityUser(
            GuidGenerator.Create(),
            adminUserName,
            adminEmail,
            tenantId
        )
        {
            Name = adminUserName
        };

        adminUser.SetShouldChangePasswordOnNextLogin(true); // Force user to change password on next login

        (await UserManager.CreateAsync(adminUser, adminPassword, validatePassword: false)).CheckErrors();
        result.CreatedAdminUser = true;

        //"admin" role
        const string adminRoleName = "admin";
        var adminRole = await RoleRepository.FindByNormalizedNameAsync(LookupNormalizer.NormalizeName(adminRoleName));
        if (adminRole == null)
        {
            adminRole = new IdentityRole(
                GuidGenerator.Create(),
                adminRoleName,
                tenantId
            )
            {
                IsStatic = true,
                IsPublic = true
            };

            (await RoleManager.CreateAsync(adminRole)).CheckErrors();
            result.CreatedAdminRole = true;
        }

        (await UserManager.AddToRoleAsync(adminUser, adminRoleName)).CheckErrors();

        return result;
    }
}