Skip to content

BridgingIT-GmbH/bITdevKit.Examples.GettingStarted

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

BridgingIT DevKit GettingStarted Example

bITDevKit

An application built using .NET 9 and following a Domain-Driven Design (DDD) approach by using the BridgingIT DevKit (bIT DevKit).

Features

  • Modular architecture with CoreModule as an example. Modules
  • Application layer with Commands (e.g., CustomerCreateCommand) and Queries (e.g., CustomerFindAllQuery, CustomerFindOneQuery) using IRequester. Requester, Commands and Queries
  • Domain layer with Aggregates (Customer), Value Objects (EmailAddress, CustomerId), Enumerations (CustomerStatus), Domain Events (CustomerCreatedDomainEvent, CustomerUpdatedDomainEvent), and Business Rules (e.g., EmailShouldBeUniqueRule). Domain, DomainEvents, Rules
  • Infrastructure layer with Entity Framework Core (CoreDbContext, migrations, configurations) and Generic Repositories with behaviors (logging, audit, domain event publishing). Repositories
  • Presentation layer with Web API Endpoints for CRUD operations on Customers, using minimal API-style routing. Endpoints
  • Startup tasks for seeding domain data (CoreDomainSeederTask). StartupTasks
  • Job scheduling with Quartz (e.g., CustomerExportJob). JobScheduling
  • Comprehensive testing: Unit tests (e.g., for command/query handlers, architecture rules), Integration tests (e.g., for endpoints).
  • Architecture validation tests to enforce Onion Architecture dependencies and domain rules (e.g., no public constructors on entities/value objects).

Frameworks and Libaries


Getting Started

Running the Application

  1. Ensure you have .NET 9/10 SDK installed.
  2. Configure the database connection string in appsettings.json under "CoreModule:ConnectionStrings:Default" (e.g., SQL Server LocalDB).
  3. Optionally, start supporting containers with docker-compose up or docker-compose up -d for SQL Server and Seq logging.
  4. Set Presentation.Web.Server as the startup project.
  5. Run with CTRL+F5 to start the host at https://localhost:5001.
  • SQL Server details: Use the connection string from appsettings.json (e.g., Server=(localdb)\\MSSQLLocalDB;Database=bit_devkit_gettingstarted;Trusted_Connection=True).
  • Swagger UI is available here.
  • Seq Dashboard (if using containers) is available here.

The application will automatically migrate the database on startup (via DatabaseMigratorService in CoreModule) and seed initial data (via CoreDomainSeederTask) in development mode.

Architecture Overview

The GettingStarted project follows Clean/Onion Architecture principles, powered by bIT DevKit for modular DDD:

  • Core (Domain): Business logic, aggregates, value objects, events.
  • Application: Commands, queries, handlers with behaviors (retry, timeout).
  • Infrastructure: Persistence (EF Core), repositories with behaviors (logging, audit).
  • Presentation: Web API endpoints, module registration.

Solution Structure

BridgingIT.DevKit.Examples.GettingStarted.sln
├── src
│   ├── Modules
│   │   └── CoreModule
│   │       ├── CoreModule.Application
│   │       ├── CoreModule.Domain
│   │       ├── CoreModule.Infrastructure
│   │       ├── CoreModule.Presentation
│   │       ├── CoreModule.UnitTests
│   │       └── CoreModule.IntegrationTests
│   └── Presentation.Web.Server
└── docker-compose.yml

Application

Contains commands, queries, and handlers for business operations.

Commands

(CustomerCreateCommand.cs)

public class CustomerCreateCommand(CustomerModel model) : RequestBase<CustomerModel>
{
    public CustomerModel Model { get; set; } = model;

    public class Validator : AbstractValidator<CustomerCreateCommand>
    {
        public Validator()
        {
            this.RuleFor(c => c.Model).NotNull();
            this.RuleFor(c => c.Model.FirstName).NotNull().NotEmpty().WithMessage("Must not be empty.");
            this.RuleFor(c => c.Model.LastName).NotNull().NotEmpty().WithMessage("Must not be empty.");
            this.RuleFor(c => c.Model.Email).NotNull().NotEmpty().WithMessage("Must not be empty.");
        }
    }
}

(CustomerCreateCommandHandler.cs)

[HandlerRetry(2, 100)]   // retry twice, wait 100ms between retries
[HandlerTimeout(500)]    // timeout after 500ms execution
public class CustomerCreateCommandHandler(
    ILoggerFactory loggerFactory,
    IMapper mapper,
    IGenericRepository<Customer> repository)
    : RequestHandlerBase<CustomerCreateCommand, CustomerModel>(loggerFactory)
{
    protected override async Task<Result<CustomerModel>> HandleAsync(
        CustomerCreateCommand request,
        SendOptions options,
        CancellationToken cancellationToken)
    {
        var customer = mapper.Map<CustomerModel, Customer>(request.Model);
        return await repository.InsertResultAsync(customer, cancellationToken: cancellationToken)
            .Tap(_ => Console.WriteLine("AUDIT"))
            .Map(mapper.Map<Customer, CustomerModel>);
    }
}

Queries

(CustomerFindAllQuery.cs)

public class CustomerFindAllQuery : RequestBase<IEnumerable<CustomerModel>>
{
    public Specification<Customer>? Filter { get; set; } = null;
}

(CustomerFindAllQueryHandler.cs)

[HandlerRetry(2, 100)]   // retry twice, wait 100ms between retries
[HandlerTimeout(500)]    // timeout after 500ms execution
public class CustomerFindAllQueryHandler(
    ILoggerFactory loggerFactory,
    IMapper mapper,
    IGenericRepository<Customer> repository)
    : RequestHandlerBase<CustomerFindAllQuery, IEnumerable<CustomerModel>>(loggerFactory)
{
    protected override async Task<Result<IEnumerable<CustomerModel>>> HandleAsync(
        CustomerFindAllQuery request,
        SendOptions options,
        CancellationToken cancellationToken) =>
        await repository.FindAllResultAsync(request.Filter, cancellationToken: cancellationToken)
            .Tap(_ => Console.WriteLine("AUDIT"))
            .Map(mapper.Map<Customer, CustomerModel>);
}

Domain

Core business logic with domain models and aggregates.

Aggregates

(Customer.cs)

[DebuggerDisplay("Id={Id}, Name={FirstName} {LastName}, Status={Status}")]
[TypedEntityId<Guid>]
public class Customer : AuditableAggregateRoot<CustomerId>, IConcurrency
{
    public string FirstName { get; private set; }
    public string LastName { get; private set; }
    public EmailAddress Email { get; private set; }
    public CustomerStatus Status { get; private set; } = CustomerStatus.Lead;
    public Guid ConcurrencyVersion { get; set; }

    public static Customer Create(string firstName, string lastName, string email)
    {
        var customer = new Customer(firstName, lastName, EmailAddress.Create(email));
        customer.DomainEvents.Register(new CustomerCreatedDomainEvent(customer));
        return customer;
    }

    // Additional methods for changing name, email, status with domain event registration
}

Value Objects

(EmailAddress.cs)

[DebuggerDisplay("Value={Value}")]
public class EmailAddress : ValueObject
{
    public string Value { get; private set; }

    public static EmailAddress Create(string value)
    {
        value = value?.Trim()?.ToLowerInvariant();
        Rule.Add(RuleSet.IsValidEmail(value)).Throw();
        return new EmailAddress(value);
    }

    protected override IEnumerable<object> GetAtomicValues()
    {
        yield return this.Value;
    }
}

Enumerations

(CustomerStatus.cs)

[DebuggerDisplay("Id={Id}, Value={Value}")]
public class CustomerStatus : Enumeration
{
    public static readonly CustomerStatus Lead = new(1, nameof(Lead), "Lead customer");
    public static readonly CustomerStatus Active = new(2, nameof(Active), "Active customer");
    public static readonly CustomerStatus Retired = new(3, nameof(Retired), "Retired customer");

    // Additional properties and methods
}

Domain Events

(CustomerCreatedDomainEvent.cs)

public class CustomerCreatedDomainEvent(Customer model) : DomainEventBase
{
    public Customer Model { get; } = model;
}

Infrastructure

Persistence setup with EF Core.

DbContext

(CoreDbContext.cs)

public class CoreDbContext(DbContextOptions<CoreDbContext> options) : DbContext(options)
{
    public DbSet<Customer> Customers { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
    }
}

Configurations

(CustomerTypeConfiguration.cs)

public class CustomerTypeConfiguration : IEntityTypeConfiguration<Customer>
{
    public void Configure(EntityTypeBuilder<Customer> builder)
    {
        builder.ToTable("Customers").HasKey(x => x.Id).IsClustered(false);
        // Additional property configurations for Id, Names, Email, Status, AuditState
    }
}

Migrations

Initial migration creates Customers table with audit fields.

Presentation

Web API endpoints and module registration.

Module Registration

(CoreModule.cs)

public class CoreModule : WebModuleBase
{
    public override IServiceCollection Register(
        IServiceCollection services,
        IConfiguration configuration = null,
        IWebHostEnvironment environment = null)
    {
        var moduleConfiguration = this.Configure<CoreModuleConfiguration, CoreModuleConfiguration.Validator>(services, configuration);

        services.AddStartupTasks(o => o.WithTask<CoreDomainSeederTask>(o => o.Enabled(environment.IsLocalDevelopment())));
        services.AddJobScheduling(o => o.StartupDelay(configuration["JobScheduling:StartupDelay"]), configuration)
            .WithSqlServerStore(configuration["JobScheduling:Quartz:quartz.dataSource.default.connectionString"])
            .WithJob<CustomerExportJob>().Cron(CronExpressions.EveryHour).Named(nameof(CustomerExportJob)).RegisterScoped();

        services.AddSqlServerDbContext<CoreDbContext>(o => o.UseConnectionString(moduleConfiguration.ConnectionStrings["Default"]).UseLogger())
            .WithDatabaseMigratorService(o => o.Enabled(environment.IsLocalDevelopment()).DeleteOnStartup(environment.IsLocalDevelopment()));

        services.AddEntityFrameworkRepository<Customer, CoreDbContext>()
            .WithBehavior<RepositoryLoggingBehavior<Customer>>()
            .WithBehavior<RepositoryAuditStateBehavior<Customer>>()
            .WithBehavior<RepositoryDomainEventPublisherBehavior<Customer>>();

        services.AddEndpoints<CoreCustomerEndpoints>();

        return services;
    }
}

Endpoints

(CoreCustomerEndpoints.cs)

public class CoreCustomerEndpoints : EndpointsBase
{
    public override void Map(IEndpointRouteBuilder app)
    {
        var group = app.MapGroup("api/core/customers").WithTags("Core.Customers");

        group.MapGet("/{id:guid}", CustomerFindOne).WithName("Core.Customers.GetById");
        group.MapGet("", CustomerFindAll).WithName("Core.Customers.GetAll");
        group.MapPost("", CustomerCreate).WithName("Core.Customers.Create");
        group.MapPut("/{id:guid}", CustomerUpdate).WithName("Core.Customers.Update");
        group.MapDelete("/{id:guid}", CustomerDelete).WithName("Core.Customers.Delete");
    }

    // Handler methods for each endpoint using IRequester
}

Mapper Registration

(CoreModuleMapperRegister.cs)

public class CoreModuleMapperRegister : IRegister
{
    public void Register(TypeAdapterConfig config)
    {
        // Configurations for value objects, enumerations, and aggregate-DTO mappings
    }
}

Perfect 🚀 — let’s make it short and developer‑oriented, but also informative about how the OpenAPI part is setup (MSBuild, package references, and post‑build step).

Here’s a drop‑in section for your README that matches the style of what you already have:


OpenAPI Specification and Swagger UI

The project uses build‑time OpenAPI documentation creation.

This ensures the specification is consistent across environments and available as a build artifact.

Setup

  • Packages used:

    • Microsoft.AspNetCore.OpenApi
    • Microsoft.Extensions.ApiDescription.Server
    • Swashbuckle.AspNetCore.SwaggerUI (for the UI only)
  • Project file configuration (Presentation.Web.Server.csproj):

    <PropertyGroup>
      <OpenApiGenerateDocuments>true</OpenApiGenerateDocuments>
      <OpenApiDocumentsDirectory>$(MSBuildProjectDirectory)/wwwroot</OpenApiDocumentsDirectory>
      <OpenApiGenerateDocumentsOptions>--file-name openapi</OpenApiGenerateDocumentsOptions>
    </PropertyGroup>
    <Target Name="GenerateOpenApiAfterBuild" AfterTargets="Build" DependsOnTargets="GenerateOpenApiDocuments" />
  • OpenApi services and static files (Program.cs):

      builder.Services.AddEndpointsApiExplorer();
      builder.Services.AddOpenApi();
      ...
      app.UseSwaggerUI(o =>
      {
          o.SwaggerEndpoint("/openapi.json", "v1");
          app.MapOpenApi();
      });
      app.UseStaticFiles();

This ensures openapi.json is automatically created after each build. More information can be found in the Microsoft Docs.

Client generation

The generated specification can be used to build strongly‑typed clients:

# TypeScript client
nswag openapi2tsclient /input:src/Presentation.Web.Server/wwwroot/openapi.json /output:Client.ts

# C# client
nswag openapi2csclient /input:src/Presentation.Web.Server/wwwroot/openapi.json /output:Client.cs

Since Swagger UI serves the exact same specification as a static asset, it can be targeted at the following endpoint: https://localhost:5001/openapi.json

Testing

Unit Tests

Run tests in CoreModule.UnitTests (e.g., CustomerCreateCommandHandlerTests, ArchitectureTests).

Integration Tests

Run tests in CoreModule.IntegrationTests (e.g., CustomerEndpointTests for HTTP responses).

HTTP Tests

Use tools like Bruno/Postman or VS HTTP file to test endpoints:

  • GET /api/core/customers
  • POST /api/core/customers with body { "firstName": "John", "lastName": "Doe", "email": "[email protected]" }

A few example requests are in Core-API.http.


Appendix: Docker & Local Registry Usage

This appendix documents building, tagging, pushing, pulling and running the Presentation.Web.Server container image with the local registry (registry service in docker-compose.yml on port 5500). Local registry is useful for testing container images without pushing to a public registry, more details here.

Prerequisites

  • Docker installed (Desktop or Engine).
  • Local registry running: docker compose up -d.

Build Image

docker build -t bdk_gettingstarted-web:latest -f src/Presentation.Web.Server/Dockerfile .

Tag For Local Registry

docker tag bdk_gettingstarted-web:latest localhost:5500/bdk_gettingstarted-web:latest

Push To Local Registry

docker push localhost:5500/bdk_gettingstarted-web:latest

List Local Registry catalog:

curl http://localhost:5500/v2/_catalog

Run Container

docker run -d -p8080:8080 --name bdk_gettingstarted-web localhost:5500/bdk_gettingstarted-web:latest

or

docker run `
  -d `
  -p 8080:8080 `
  --name bdk_gettingstarted-web `
  --network bdk_gettingstarted `
  -e ASPNETCORE_ENVIRONMENT=Development `
  -e "Modules__CoreModule__ConnectionStrings__Default=Server=mssql,1433;Initial Catalog=bit_devkit_gettingstarted;User Id=sa;Password=Abcd1234!;Trusted_Connection=False;TrustServerCertificate=True;MultipleActiveResultSets=True;Encrypt=False;" `
  -e "JobScheduling__Quartz__quartz.dataSource.default.connectionString=Server=mssql,1433;Initial Catalog=bit_devkit_gettingstarted;User Id=sa;Password=Abcd1234!;Trusted_Connection=False;TrustServerCertificate=True;MultipleActiveResultSets=True;Encrypt=False;" `
  -e "Authentication__Authority=http://localhost:8080" `
  localhost:5500/bdk_gettingstarted-web:latest

Test Running Container:

curl http://localhost:8080/api/_system/info -v

or browse to http://localhost:8080/scalar

Tail logs:

docker logs -f bdk_gettingstarted-web

Build and Run Container

docker build -t localhost:5500/bdk_gettingstarted-web:latest -f src/Presentation.Web.Server/Dockerfile .; if ($?) {
  (docker stop bdk_gettingstarted-web 2>$null | Out-Null); (docker rm bdk_gettingstarted-web 2>$null | Out-Null);
  New-Item -ItemType Directory -Force -Path "$PWD/logs" | Out-Null
  docker run --name bdk_gettingstarted-web -p 8080:8080 --network bdk_gettingstarted `
    -e ASPNETCORE_ENVIRONMENT=Development `
    -e "Modules__CoreModule__ConnectionStrings__Default=Server=mssql,1433;Initial Catalog=bit_devkit_gettingstarted;User Id=sa;Password=Abcd1234!;Trusted_Connection=False;TrustServerCertificate=True;MultipleActiveResultSets=True;Encrypt=False;" `
    -e "JobScheduling__Quartz__quartz.dataSource.default.connectionString=Server=mssql,1433;Initial Catalog=bit_devkit_gettingstarted;User Id=sa;Password=Abcd1234!;Trusted_Connection=False;TrustServerCertificate=True;MultipleActiveResultSets=True;Encrypt=False;" `
    -e "Authentication__Authority=http://localhost:8080" `
    -v "${PWD}/logs:/.logs" `
    localhost:5500/bdk_gettingstarted-web:latest
}

Stop/Remove Image & Container

docker stop bdk_gettingstarted-web
docker rm -f bdk_gettingstarted-web
docker rmi localhost:5500/bdk_gettingstarted-web:latest bdk_gettingstarted-web:latest

License

This project is licensed under the MIT License - see the LICENSE file for details.

About

No description, website, or topics provided.

Resources

License

Code of conduct

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •