Skip to content

Latest commit

History

420 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

bITdevKit GettingStarted Example

bITDevKit

An application built using .NET 10 and following a Domain-Driven Design (DDD) approach by using the bITdevKit.

Table of Contents

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 (CoreModuleDbContext, migrations, configurations) and Generic Repositories with behaviors (tracing, logging, audit, outbox 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 (CoreModuleDomainSeederTask). StartupTasks
  • Job scheduling with Quartz (e.g., CustomerExportJob in Application layer). JobScheduling
  • Comprehensive testing: Unit tests (command/query handlers, architecture rules), Integration tests (endpoints, persistence), Architecture tests (boundary enforcement).
  • Build-time OpenAPI generation with Kiota client support for C#, TypeScript and Python.

Frameworks and Libraries


Getting Started

Running the Application

  1. Ensure you have .NET 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.

Access points:

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


Developer Guidelines

Commit Messages

Commit messages use this format:

<type>[optional scope]: <description>
[optional body]
[optional footer(s)]

Common types:

TypePurpose
featNew feature
fixBug fix
docsDocumentation only
styleFormatting/style (no logic)
refactorCode refactor (no feature/fix)
perfPerformance improvement
testAdd/update tests
buildBuild system/dependencies
ciCI/config changes
choreMaintenance/misc
revertRevert commit

Breaking changes are marked either with an exclamation mark after type/scope or with a BREAKING CHANGE: footer.

feat(core): add customer export endpoint
fix(core): handle missing email address
docs: describe branching strategy
feat!: remove deprecated endpoint
feat: allow config to extend other configs
BREAKING CHANGE: `extends` key behavior changed

Branching Strategy

Trunk-based development with short-lived feature branches. Changes merge into main through Pull Requests (PRs). Keep branches small, rebase frequently, and merge quickly to reduce drift.

A source-control branching model, where developers collaborate on code in a single branch called ‘trunk/main’ *, resist any pressure to create other long-lived development branches by employing documented techniques. They therefore avoid merge hell, do not break the build, and live happily ever after.

Key rules:

  • main is always releasable
  • Feature branches are short-lived and scoped to a single change
  • PRs are required for all merges to main
  • Commit messages follow the Conventional Commits standard described in Commit Messages

Features Development

gitGraph
commit id: "init"
branch feature/add-tasking
checkout feature/add-tasking
commit id: "implement"
commit id: "tests"
checkout main
merge feature/add-tasking tag: "PR merge"
commit id: "release"
Loading

PR Flow

flowchart LR
A[Create feature branch] --> B[Implement change]
B --> C[Open PR to main]
C --> D[Review and checks]
D -->|Approved| E[Merge to main]
D -->|Changes requested| B
Loading

EF Core Migrations

Use the tasks for migrations to keep the workflow consistent and repeatable:

  • Add a migration with the EF task for migration creation.
  • Apply migrations with the EF task for applying migrations or updating the database.
  • Keep migrations in the module infrastructure project and avoid direct edits unless a correction is required.
  • For the underlying dotnet ef command equivalents, see src/Modules/CoreModule/CoreModule.Infrastructure/EntityFramework/README.md.

Migrations are applied automatically on application startup in development mode:

services.AddSqlServerDbContext<CoreModuleDbContext>(o =>o.UseConnectionString(moduleConfiguration.ConnectionStrings["Default"])).WithDatabaseMigratorService(o =>o// create the database and apply existing migrations.Enabled(environment.IsLocalDevelopment()||environment.IsContainerized()));

Architecture

The bITdevKit GettingStarted project implements Clean/Onion Architecture principles combined with Domain-Driven Design (DDD) and a Modular Monolith approach. This section explains the architectural decisions, layer responsibilities and how components interact.

Architectural Decisions: For detailed rationale and alternatives considered for key architectural choices, see the Architectural Decision Records (ADRs) in the /docs/ADR directory. Key ADRs include Clean Architecture boundaries (ADR-0001), Result pattern (ADR-0002), Modular Monolith structure (ADR-0003), and implementation patterns for repositories (ADR-0004), jobs (ADR-0015), logging (ADR-0016), and testing (ADR-0013, ADR-0017).

Overview

Clean Architecture enforces strict dependency rules where inner layers never depend on outer layers. Dependencies flow inward toward the domain core, ensuring business logic remains independent of infrastructure concerns and delivery mechanisms.

graph TB
Client([HTTP Client]) --> Endpoints
subgraph Presentation["Presentation Layer (Outer)"]
Endpoints[Endpoints<br/>Minimal APIs]
DTOs[Request/Response DTOs]
end
subgraph Application["Application Layer"]
Requester[IRequester<br/>Mediator]
CMD[Commands & Queries<br/>CQRS]
BEHAV[Pipeline Behaviors<br/>Validation, Retry, Timeout]
HAND[Handlers<br/>Business Orchestration]
Jobs[Background Jobs<br/>CustomerExportJob]
end
subgraph Domain["Domain Layer (Inner Core)"]
AGG[Aggregates<br/>Customer]
VO[Value Objects<br/>EmailAddress, CustomerNumber]
EVENTS[Domain Events<br/>CustomerCreated]
RULES[Business Rules<br/>EmailShouldBeUnique]
SPECS[Specifications<br/>Query Expressions]
end
subgraph Infrastructure["Infrastructure Layer (Outer)"]
Repos[Repositories<br/>Generic Repository]
DB[(Entity Framework<br/>SQL Server)]
Scheduler[Job Scheduler<br/>Quartz]
end
%% Request Flow
Endpoints --> DTOs
DTOs --> Requester
Requester --> BEHAV
BEHAV --> CMD
CMD --> HAND
%% Handler to Domain
HAND --> AGG
HAND --> RULES
HAND --> SPECS
%% Jobs to Domain & Infrastructure
Jobs --> AGG
Jobs --> Repos
Jobs --> SPECS
%% Domain Internal
AGG --> VO
AGG --> EVENTS
%% Persistence Flow
HAND --> Repos
Repos --> DB
Scheduler -.triggers.-> Jobs
%% Styling
style Domain fill:#E8F5E9,stroke:#4CAF50,stroke-width:3px
style AGG fill:#66BB6A,color:#fff
style VO fill:#66BB6A,color:#fff
style EVENTS fill:#66BB6A,color:#fff
style RULES fill:#66BB6A,color:#fff
style SPECS fill:#66BB6A,color:#fff
style Application fill:#E3F2FD,stroke:#2196F3,stroke-width:2px
style Presentation fill:#F3E5F5,stroke:#9C27B0,stroke-width:2px
style Infrastructure fill:#FFF3E0,stroke:#FF9800,stroke-width:2px
Loading

Layer Responsibilities

Domain Layer (Core)

Location: src/Modules/CoreModule/CoreModule.Domain

Responsibilities:

  • Pure business logic and domain rules
  • Aggregates, Entities (e.g., Customer)
  • Value Objects (e.g., EmailAddress, CustomerNumber)
  • Domain Events (e.g., CustomerCreatedDomainEvent)
  • Business Rules (e.g., EmailShouldBeUniqueRule)
  • Enumerations (e.g., CustomerStatus)

Dependencies: None (only bITdevKit domain abstractions)

Key Principle: The domain layer is persistence-ignorant and framework-agnostic. It contains no references to databases, web frameworks, or external services.

Application Layer

Location: src/Modules/CoreModule/CoreModule.Application

Responsibilities:

  • Use cases orchestration via Commands and Queries
  • Request/Response DTOs (CustomerModel)
  • Handlers that coordinate domain operations
  • Validation logic (FluentValidation)
  • Mapping between domain and DTOs
  • Background Jobs (e.g., CustomerExportJob)

Dependencies: Domain layer only

Key Principle: Application defines what the system does, not how it's implemented (infrastructure) or how it's exposed (presentation).

Infrastructure Layer

Location: src/Modules/CoreModule/CoreModule.Infrastructure

Responsibilities:

  • Database context and EF Core configurations
  • Repository implementations
  • External service integrations
  • Startup tasks
  • Migrations

Dependencies: Domain and Application layers

Key Principle: Infrastructure provides implementations of abstractions defined by inner layers.

Presentation Layer

Location: src/Modules/CoreModule/CoreModule.Presentation

Responsibilities:

  • HTTP endpoints (Minimal APIs)
  • Module registration and configuration
  • DTO mapping (Mapster)
  • Request/Response transformations

Dependencies: Application layer (through IRequester)

Key Principle: Presentation is a thin adapter that translates HTTP requests into application commands/queries and responses back to HTTP.

Dependency Rules

The architecture enforces these strict dependency rules (validated by architecture tests):

  1. Domain → NONE: Domain has no dependencies on other layers
  2. Application → Domain: Application depends only on Domain
  3. Infrastructure → Domain + Application: Infrastructure implements abstractions
  4. Presentation → Application: Presentation uses Application through IRequester

Violations are automatically detected by architecture tests.

Request Processing Flow

Understanding how a request flows through the architecture is crucial. Here's a complete end-to-end flow for creating a customer:

sequenceDiagram
participant Client
participant Endpoint as CustomerEndpoints<br/>(Presentation)
participant Req as IRequester<br/>(Mediator)
participant Pipeline as Pipeline Behaviors
participant Handler as CustomerCreateCommandHandler<br/>(Application)
participant Domain as Customer Aggregate<br/>(Domain)
participant Repo as IGenericRepository<br/>(Abstraction)
participant RepoBehaviors as Repository Behaviors
participant DbCtx as CoreModuleDbContext<br/>(Infrastructure)
participant DB as SQL Server Database
Client->>Endpoint: POST /api/coremodule/customers<br/>{firstName, lastName, email}
Endpoint->>Req: SendAsync(CustomerCreateCommand)
Req->>Pipeline: Process request
Note over Pipeline: 1. Module Scope<br/>2. Validation<br/>3. Retry<br/>4. Timeout
Pipeline->>Handler: HandleAsync(command)
Handler->>Handler: Create context
Handler->>Handler: Validate rules
Note over Handler: EmailShouldBeUniqueRule<br/>FirstName not empty
Handler->>Domain: Customer.Create(...)
Domain->>Domain: Validate invariants
Domain->>Domain: Register CustomerCreatedDomainEvent
Domain-->>Handler: Result<Customer>
Handler->>Repo: InsertResultAsync(customer)
Repo->>RepoBehaviors: Execute behavior chain
Note over RepoBehaviors: 1. Tracing<br/>2. Logging<br/>3. Audit State<br/>4. Outbox Events
RepoBehaviors->>DbCtx: SaveChangesAsync()
DbCtx->>DB: INSERT INTO Customers
DB-->>DbCtx: Success
DbCtx-->>RepoBehaviors: Saved entity
RepoBehaviors-->>Repo: Result<Customer>
Repo-->>Handler: Result<Customer>
Handler->>Handler: Map to CustomerModel
Handler-->>Pipeline: Result<CustomerModel>
Pipeline-->>Req: Result<CustomerModel>
Req-->>Endpoint: Result<CustomerModel>
Endpoint->>Endpoint: MapHttpCreated()
Endpoint-->>Client: 201 Created<br/>Location: /api/coremodule/customers/{id}
Loading

Key Stages:

  1. HTTP Request: Client sends JSON payload to endpoint
  2. Command Creation: Endpoint creates CustomerCreateCommand with DTO
  3. Pipeline Processing: Request passes through cross-cutting behaviors
  4. Handler Execution: Handler orchestrates domain logic
  5. Domain Validation: Aggregate enforces business rules
  6. Repository Persistence: Entity saved with behavior chain
  7. Response Mapping: Result mapped to HTTP response

Modular Monolith Structure

The application follows a Modular Monolith pattern where each module is a vertical slice containing all layers:

src/Modules/CoreModule/
├── CoreModule.Domain/ (Business logic)
├── CoreModule.Application/ (Use cases)
├── CoreModule.Infrastructure/ (Persistence)
└── CoreModule.Presentation/ (HTTP endpoints)

Module Characteristics:

  • Self-contained: Each module has its own DbContext, endpoints and domain model
  • Loosely coupled: Modules communicate through contracts (sync) or integration events (async)
  • Independently deployable: Modules can be extracted into microservices if needed

Module Boundary Rules (enforced by architecture tests):

  • Modules cannot directly reference other modules' internal layers
  • Modules can reference other modules' .Contracts projects
  • Cross-module communication via integration events (async) or public APIs (sync)

See CoreModule README for module-specific implementation details.


Core Patterns

The bITdevKit GettingStarted application is built on several key design patterns that work together to create a robust, maintainable and testable architecture.

Result Pattern (Railway-Oriented Programming)

The Result Pattern replaces exception-based error handling with explicit success/failure types, enabling functional composition and railway-oriented programming.

Railway-Oriented Programming Diagram

graph LR
Start([Start]) --> Step1{Step 1<br/>Validation}
Step1 -->|Success| Step2{Step 2<br/>Business Rule}
Step1 -->|Failure| Failure([Failure Path])
Step2 -->|Success| Step3{Step 3<br/>Persistence}
Step2 -->|Failure| Failure
Step3 -->|Success| Step4[Step 4<br/>Mapping]
Step3 -->|Failure| Failure
Step4 --> Success([Success Path])
style Success fill:#4CAF50
style Failure fill:#f44336
Loading

Key Concept: Once a step fails, all subsequent steps are skipped and the failure flows directly to the end.

Result Type Structure

publicclassResult<T>{publicTValue{get;}publicboolIsSuccess{get;}publicboolIsFailure{get;}publicIEnumerable<IResultMessage>Messages{get;}publicIEnumerable<IResultError>Errors{get;}}

Result Pattern Methods

Transformation Methods:

  • Bind(): Transform success value
  • BindAsync(): Async transformation
  • BindResult(): Chain operations that return Results

Validation Methods:

  • Ensure(): Inline validation
  • Unless() / UnlessAsync(): Business rule checking

Mapping Methods:

  • Map(): Transform to different type

Side Effect Methods:

  • Tap(): Execute action without changing result
  • Log(): bITdevKit logging extension

See CoreModule README - Handler Deep Dive for detailed examples.

Requester/Notifier Pattern (Mediator)

The Requester/Notifier pattern is bITdevKit's implementation of the Mediator pattern, decoupling request senders from handlers and enabling cross-cutting concerns through pipeline behaviors.

Architecture Diagram

graph TB
subgraph "Client Code (Endpoint)"
Client[CustomerEndpoints]
end
subgraph "Mediator (IRequester)"
Req[IRequester.SendAsync]
Pipeline[Pipeline Behaviors]
end
subgraph "Handler"
Handler[CustomerCreateCommandHandler]
end
subgraph "Cross-Cutting Behaviors"
B1[ModuleScopeBehavior]
B2[ValidationBehavior]
B3[RetryBehavior]
B4[TimeoutBehavior]
end
Client -->|CustomerCreateCommand| Req
Req --> B1
B1 --> B2
B2 --> B3
B3 --> B4
B4 --> Handler
Handler -->|Result<CustomerModel>| B4
B4 --> B3
B3 --> B2
B2 --> B1
B1 --> Req
Req -->|Result<CustomerModel>| Client
style Handler fill:#4CAF50
style Pipeline fill:#2196F3
Loading

Pipeline Behaviors

Pipeline behaviors wrap handlers to provide cross-cutting concerns:

  1. ModuleScopeBehavior: Sets module context
  2. ValidationBehavior: Validates request (FluentValidation)
  3. RetryBehavior: Retries on transient failures
  4. TimeoutBehavior: Enforces execution timeout

Setup in Program.cs

builder.Services.AddRequester().AddHandlers().WithDefaultBehaviors();builder.Services.AddNotifier().AddHandlers().WithDefaultBehaviors();

Repository with Behaviors Pattern (Decorator)

The Repository pattern abstracts data access, while the Decorator pattern adds cross-cutting concerns through behavior chains.

Behavior Chain Diagram

graph LR
Handler[Handler] --> Tracing[TracingBehavior]
Tracing --> Logging[LoggingBehavior]
Logging --> Audit[AuditStateBehavior]
Audit --> Outbox[OutboxDomainEventBehavior]
Outbox --> Repo[EntityFrameworkRepository]
Repo --> DB[(Database)]
style Tracing fill:#2196F3
style Logging fill:#2196F3
style Audit fill:#2196F3
style Outbox fill:#2196F3
style Repo fill:#4CAF50
Loading

Behavior Implementations

  1. RepositoryTracingBehavior: OpenTelemetry spans for distributed tracing
  2. RepositoryLoggingBehavior: Structured logging with duration measurement
  3. RepositoryAuditStateBehavior: Automatic audit metadata (CreatedBy, UpdatedBy)
  4. RepositoryOutboxDomainEventBehavior: Outbox pattern for reliable event delivery

Configuration in Module

services.AddEntityFrameworkRepository<Customer,CoreModuleDbContext>().WithBehavior<RepositoryTracingBehavior<Customer>>().WithBehavior<RepositoryLoggingBehavior<Customer>>().WithBehavior<RepositoryAuditStateBehavior<Customer>>().WithBehavior<RepositoryOutboxDomainEventBehavior<Customer,CoreModuleDbContext>>();

See CoreModule README - Repository Behaviors for detailed explanation.

Module System (Vertical Slices)

The Modular Monolith pattern organizes code into self-contained vertical slices, each representing a business capability.

Module Structure

src/Modules/CoreModule/
├── CoreModule.Domain/ # Business logic layer
│ ├── Model/ # Aggregates, Value Objects
│ ├── Events/ # Domain Events
│ └── Rules/ # Business Rules
├── CoreModule.Application/ # Use cases layer
│ ├── Commands/ # Write operations
│ ├── Queries/ # Read operations
│ ├── Models/ # DTOs
│ ├── Jobs/ # Background jobs
│ └── Events/ # Event handlers
├── CoreModule.Infrastructure/ # Persistence layer
│ ├── EntityFramework/ # DbContext, Configurations
│ └── StartupTasks/ # Seeder tasks
└── CoreModule.Presentation/ # API layer
├── Web/Endpoints/ # HTTP endpoints
└── CoreModuleModule.cs # Module registration

Module Registration in Program.cs

builder.Services.AddModules(builder.Configuration,builder.Environment).WithModule<CoreModuleModule>().WithModuleContextAccessors().WithRequestModuleContextAccessors();

Application Bootstrap

The Program.cs file is the composition root where all services, middleware and modules are configured. Understanding this file is crucial for grasping how the application starts and how components wire together.

Configuration Stages

graph TD
A[Create WebApplication Builder] --> B[Configure Host & Logging]
B --> C[Register Modules]
C --> D[Register Requester/Notifier]
D --> E[Configure Job Scheduling]
E --> F[Register Endpoints]
F --> G[Configure OpenAPI]
G --> H[Configure CORS]
H --> I[Configure Authentication]
I --> J[Configure Health Checks]
J --> K[Configure Observability]
K --> L[Build Application]
L --> M[Configure Middleware Pipeline]
M --> N[Map Endpoints]
N --> O[Run Application]
style A fill:#4CAF50
style L fill:#4CAF50
style O fill:#4CAF50
Loading

Step-by-Step Breakdown

Step 1: Create Builder and Configure Logging

varbuilder=WebApplication.CreateBuilder(args);builder.Host.ConfigureLogging();builder.Services.AddConsoleCommandsInteractive();

What happens: Creates WebApplicationBuilder with configuration from appsettings.json, environment variables and command-line args. Configures Serilog for structured logging.

Step 2: Register Modules

builder.Services.AddModules(builder.Configuration,builder.Environment).WithModule<CoreModuleModule>().WithModuleContextAccessors().WithRequestModuleContextAccessors();

What happens: Each module's Register() method is called to register services (DbContext, repositories, handlers, endpoints, jobs).

Step 3: Register Requester and Notifier

builder.Services.AddRequester().AddHandlers().WithDefaultBehaviors();builder.Services.AddNotifier().AddHandlers().WithDefaultBehaviors();

What happens: Scans assemblies for handlers and registers pipeline behaviors (Module Scope, Validation, Retry, Timeout).

Step 4: Configure Job Scheduling

builder.Services.AddJobScheduling(o =>o.StartupDelay(builder.Configuration["JobScheduling:StartupDelay"]),builder.Configuration).WithSqlServerStore(builder.Configuration["JobScheduling:Quartz:..."]).WithBehavior<ModuleScopeJobSchedulingBehavior>();

What happens: Configures Quartz.NET with SQL Server persistence for background jobs.

Step 5. Register Application Endpoints

Step 6. Configure JSON Serialization

Step 7. Configure OpenAPI

Step 8. Configure CORS

Step 9. Configure Authentication/Authorization

Step 10. Configure Health Checks

Step 11. Configure Observability (OpenTelemetry)

Middleware Pipeline Configuration

The middleware pipeline processes HTTP requests in order:

graph TD
Request[HTTP Request] --> OpenAPI{Development?}
OpenAPI -->|Yes| MapOpenAPI[MapOpenApi/MapScalar]
OpenAPI -->|No| Rule
MapOpenAPI --> Rule[UseRuleLogger]
Rule --> Result[UseResultLogger]
Result --> Static[UseStaticFiles]
Static --> Correlation[UseRequestCorrelation]
Correlation --> ModuleCtx[UseRequestModuleContext]
ModuleCtx --> ReqLog[UseRequestLogging]
ReqLog --> CORS[UseCors]
CORS --> ProblemDetails[UseProblemDetails]
ProblemDetails --> HTTPS[UseHttpsRedirection]
HTTPS --> Modules[UseModules]
Modules --> Auth[UseAuthentication]
Auth --> Authz[UseAuthorization]
Authz --> UserLog[UseCurrentUserLogging]
UserLog --> HealthChecks[MapHealthChecks]
HealthChecks --> MapModules[MapModules]
MapModules --> Controllers[MapControllers]
Controllers --> Endpoints[MapEndpoints]
Endpoints --> Console[UseConsoleCommandsInteractive]
Console --> Response[HTTP Response]
style Request fill:#4CAF50
style Response fill:#4CAF50
Loading

Key middleware:

  • UseRequestCorrelation: Assigns unique correlation ID
  • UseRequestModuleContext: Determines handling module
  • UseProblemDetails: RFC 7807 error responses
  • UseAuthentication/UseAuthorization: Security layer

Complete Request Flow

1. HTTP Request: POST /api/coremodule/customers
2. UseHttpsRedirection → Ensure HTTPS
3. UseRequestCorrelation → Assign correlation ID
4. UseRequestModuleContext → Set context to CoreModule
5. UseRequestLogging → Log request start
6. UseCors → Validate CORS policy
7. UseAuthentication → Validate JWT token
8. UseAuthorization → Check authorization policy
9. Endpoint matched: CustomerEndpoints.MapPost
10. IRequester.SendAsync(CustomerCreateCommand)
11. Pipeline behaviors execute
12. CustomerCreateCommandHandler.HandleAsync
13. Result<CustomerModel> returned from Handler
14. MapHttpCreated() converts to HTTP 201
15. UseRequestLogging → Log completion
16. HTTP Response sent to client

Solution Structure

├── src
│ ├── Modules
│ │ └── CoreModule
│ │ ├── CoreModule.Application # Commands, Queries, Handlers, Jobs
│ │ ├── CoreModule.Contracts # Public interfaces for other modules
│ │ ├── CoreModule.Domain # Aggregates, Value Objects, Events
│ │ ├── CoreModule.Infrastructure # DbContext, Configurations, Migrations
│ │ └── CoreModule.Presentation # Endpoints, Module registration
│ └── Presentation.Web.Server # Host application (Program.cs)
├── tests
│ └── Modules
│ ├── CoreModule.UnitTests # Unit tests (handlers, domain)
│ ├── CoreModule.IntegrationTests # Integration tests (endpoints, DB)
│ └── CoreModule.Benchmarks # Performance benchmarks
├── bITdevKit.Examples.GettingStarted.slnx # Solution file
└── docker-compose.yml # Container definitions

Quick Code Examples

Commands

(CustomerCreateCommand.cs)

publicclassCustomerCreateCommand(CustomerModelmodel):RequestBase<CustomerModel>{publicCustomerModelModel{get;set;}=model;publicclassValidator:AbstractValidator<CustomerCreateCommand>{publicValidator(){this.RuleFor(c =>c.Model).NotNull();this.RuleFor(c =>c.Model.FirstName).NotNull().NotEmpty();this.RuleFor(c =>c.Model.LastName).NotNull().NotEmpty();this.RuleFor(c =>c.Model.Email).NotNull().NotEmpty().EmailAddress();}}}

Queries

(CustomerFindAllQuery.cs)

publicclassCustomerFindAllQuery:RequestBase<IEnumerable<CustomerModel>>{publicFilterModelFilter{get;set;}=null;}

Domain Aggregates

(Customer.cs)

[TypedEntityId<Guid>]publicclassCustomer:AuditableAggregateRoot<CustomerId>,IConcurrency{publicstringFirstName{get;privateset;}publicstringLastName{get;privateset;}publicEmailAddressEmail{get;privateset;}publicCustomerStatusStatus{get;privateset;}=CustomerStatus.Lead;publicstaticResult<Customer>Create(stringfirstName,stringlastName,stringemail,CustomerNumbernumber){varemailResult=EmailAddress.Create(email);if(emailResult.IsFailure)returnemailResult.Unwrap();varcustomer=newCustomer(firstName,lastName,emailResult.Value,number);customer.DomainEvents.Register(newCustomerCreatedDomainEvent(customer));returncustomer;}publicResult<Customer>ChangeEmail(stringemail){varemailResult=EmailAddress.Create(email);if(emailResult.IsFailure)returnemailResult.Unwrap();returnthis.ApplyChange(this.Email,emailResult.Value, v =>this.Email=v);}}

Value Objects

(EmailAddress.cs)

publicclassEmailAddress:ValueObject{publicstringValue{get;privateset;}publicstaticResult<EmailAddress>Create(stringvalue){value=value?.Trim()?.ToLowerInvariant();if(string.IsNullOrWhiteSpace(value))returnResult<EmailAddress>.Failure().WithError(Errors.Validation.Error("Email cannot be empty"));if(!value.Contains("@"))returnResult<EmailAddress>.Failure().WithError(Errors.Validation.Error("Invalid email format"));returnnewEmailAddress(value);}protectedoverrideIEnumerable<object>GetAtomicValues(){yieldreturnthis.Value;}}

Enumerations

(CustomerStatus.cs)

publicclassCustomerStatus:Enumeration{publicstaticreadonlyCustomerStatusLead=new(1,nameof(Lead),"Lead customer");publicstaticreadonlyCustomerStatusActive=new(2,nameof(Active),"Active customer");publicstaticreadonlyCustomerStatusRetired=new(3,nameof(Retired),"Retired customer");privateCustomerStatus(intid,stringname,stringdescription=null):base(id,name){this.Description=description;}publicstringDescription{get;privateset;}}

Domain Events

(CustomerCreatedDomainEvent.cs)

publicpartialclassCustomerCreatedDomainEvent(Customermodel):DomainEventBase{publicCustomerModel{get;privateset;}=model;}

Infrastructure

(CoreModuleDbContext.cs)

publicclassCoreModuleDbContext(DbContextOptions<CoreModuleDbContext>options):ModuleDbContextBase(options),IOutboxDomainEventContext{publicDbSet<Customer>Customers{get;set;}publicDbSet<OutboxDomainEvent>OutboxDomainEvents{get;set;}protectedoverridevoidOnModelCreating(ModelBuildermodelBuilder){modelBuilder.HasSequence<int>("CustomerNumbers").StartsAt(100000);base.OnModelCreating(modelBuilder);}}

Presentation

(CustomerEndpoints.cs)

publicclassCustomerEndpoints:EndpointsBase{publicoverridevoidMap(IEndpointRouteBuilderapp){vargroup=app.MapGroup("api/coremodule/customers").RequireAuthorization().WithTags("CoreModule.Customers");group.MapPost("",async(IRequesterrequester,CustomerModelmodel,CancellationTokenct)=>(awaitrequester.SendAsync(newCustomerCreateCommand(model),cancellationToken:ct)).MapHttpCreated(v =>$"/api/coremodule/customers/{v.Id}")).WithName("CoreModule.Customers.Create");}}

Testing

(CustomerCreateCommandHandlerTests.cs)

[Fact]publicasyncTaskProcess_ValidRequest_SuccessResult(){// Arrangevarrequester=this.ServiceProvider.GetService<IRequester>();varcommand=newCustomerCreateCommand(newCustomerModel{FirstName="John",LastName="Doe",Email="john@example.com"});// Actvarresponse=awaitrequester.SendAsync(command,null,CancellationToken.None);// Assertresponse.ShouldBeSuccess();response.Value.ShouldNotBeNull();response.Value.Id.ShouldNotBe(Guid.Empty.ToString());}

For detailed implementation guidance, see:


Appendix A: 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).

Prerequisites

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

Build Image

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

Tag For Local Registry

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

Push To Local Registry

docker push localhost:5500/bit_devkit_gettingstarted-web:latest

Run Container

docker run `-d `-p 8080:8080`--name bit_devkit_gettingstarted-web `--network bit_devkit_gettingstarted `-e ASPNETCORE_ENVIRONMENT=Development `-e "Modules__CoreModule__ConnectionStrings__Default=Server=mssql,1433;Initial Catalog=bit_devkit_gettingstarted;User Id=sa;Password=Abcd1234!;..."`
localhost:5500/bit_devkit_gettingstarted-web:latest

Test Running Container:

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

Appendix B: OpenAPI Specification and API Clients

The project uses build-time OpenAPI document generation with Kiota for client generation.

OpenAPI Document Generation

The OpenAPI specification is generated automatically during compilation:

  • On build: OpenAPI spec generated to wwwroot/openapi.json
  • At runtime: Served as static file at /openapi.json
  • UI: Scalar UI available at /scalar (Development/Container only)

Generating API Clients with Kiota

Kiota is Microsoft's OpenAPI-based API client generator that produces idiomatic, strongly-typed clients for multiple languages.

Installing Kiota

dotnet tool install --global Microsoft.OpenApi.Kiota

Generating C# Client

kiota generate \
--openapi src/Presentation.Web.Server/wwwroot/openapi.json \
--language CSharp \
--class-name GettingStartedApiClient \
--namespace BridgingIT.DevKit.Examples.GettingStarted.Client \
--output ./generated/csharp

Using the Generated Client

usingvarhttpClient=newHttpClient();httpClient.DefaultRequestHeaders.Add("Authorization","Bearer YOUR_JWT_TOKEN");varrequestAdapter=newHttpClientRequestAdapter(newAnonymousAuthenticationProvider(),httpClient:httpClient);varclient=newGettingStartedApiClient(requestAdapter);// Get all customersvarcustomers=awaitclient.Api.Coremodule.Customers.GetAsync();// Create new customervarnewCustomer=newCustomerModel{FirstName="Jane",LastName="Doe",Email="jane.doe@example.com"};varcreated=awaitclient.Api.Coremodule.Customers.PostAsync(newCustomer);Console.WriteLine($"Created customer: {created.Id}");

Generating TypeScript Client

kiota generate \
--openapi src/Presentation.Web.Server/wwwroot/openapi.json \
--language TypeScript \
--class-name GettingStartedApiClient \
--output ./generated/typescript

Resources

About

No description, website, or topics provided.

Resources

Code of conduct

Stars

4 stars

Watchers

3 watching

Forks

Releases

Packages

Used by

Contributors

Languages