Table of Contents

Domain model

In this documentation, domain model means the part of your code that represents your business concepts and business rules, independent from UI, API, and infrastructure concerns.

For this template, we model the domain using DDD (Domain-Driven Design):

  • Entity: An object with identity (Id) and lifecycle.
  • Aggregate / Aggregate Root: A consistency boundary that protects business rules.
  • Domain validation: Rules that keep the model in a valid state.
  • Repository: A domain-facing contract for loading and saving aggregates.
  • Domain events: Business events raised by domain changes.

The DevKit already provides the base domain building blocks (for example Entity, AggregateRoot, IDomainEvent, IBaseAggregateRepository<TEntity>, and DomainException) so you can focus on your business behavior instead of rewriting infrastructure primitives.

For a concise catalog of those DevKit types and namespaces, see Domain building blocks.

DemoThing as the reference model

In this category, DemoThing is the sample feature used to teach the pattern. You can replace it with your own business concept, but keep the same structure.

The aggregate root is implemented in DemoThing.cs.

using System.ComponentModel.DataAnnotations;
using CanBeYours.Core.Resources;
using CodeBlock.DevKit.Core.Extensions;
using CodeBlock.DevKit.Domain.Entities;

namespace CanBeYours.Core.Domain.DemoThings;

/// <summary>
/// DemoThing is an example domain entity that demonstrates how to implement a complete domain model
/// using the CodeBlock.DevKit framework. This class serves as a learning example and should be
/// replaced with your actual business entities.
/// 
/// Key features demonstrated:
/// - Domain-driven design with aggregate root pattern
/// - Business rule validation through policies
/// - Domain event publishing
/// - Immutable properties with controlled modification
/// - Factory method pattern for creation
/// </summary>
public sealed class DemoThing : AggregateRoot
{
    /// <summary>
    /// Private constructor ensures DemoThing can only be created through the Create factory method
    /// and enforces business rules during instantiation.
    /// </summary>
    /// <param name="name">The name of the demo thing</param>
    /// <param name="description">Detailed description of the demo thing</param>
    /// <param name="type">Classification type of the demo thing</param>
    /// <param name="userId">Identifier of the user who owns this demo thing</param>
    private DemoThing(string name, string description, DemoThingType type, string userId)
    {
        Name = name;
        Description = description;
        Type = type;
        UserId = userId;

        CheckPolicies();

        AddDomainEvent(new DemoThingCreated(Id, Name));
        TrackChange(nameof(DemoThingCreated));
    }

    /// <summary>
    /// The display name of the demo thing. Required field that cannot be empty.
    /// </summary>
    public string Name { get; private set; }
    
    /// <summary>
    /// Detailed description providing additional context about the demo thing. Required field.
    /// </summary>
    public string Description { get; private set; }
    
    /// <summary>
    /// Classification type that categorizes the demo thing. Required field.
    /// </summary>
    public DemoThingType Type { get; private set; }
    
    /// <summary>
    /// Identifier of the user who owns this demo thing. Required field for ownership tracking.
    /// </summary>
    public string UserId { get; private set; }

    /// <summary>
    /// Factory method to create a new DemoThing instance. This method enforces business rules
    /// and ensures proper initialization of the domain entity.
    /// 
    /// Example usage:
    /// var demoThing = DemoThing.Create("My Demo", "A sample demo thing", DemoThingType.DemoType1, "user123");
    /// </summary>
    /// <param name="name">The name of the demo thing</param>
    /// <param name="description">Detailed description of the demo thing</param>
    /// <param name="type">Classification type of the demo thing</param>
    /// <param name="userId">Identifier of the user who owns this demo thing</param>
    /// <returns>A new DemoThing instance with validated business rules</returns>
    public static DemoThing Create(string name, string description, DemoThingType type, string userId)
    {
        return new DemoThing(name, description, type, userId);
    }

    /// <summary>
    /// Updates the properties of an existing DemoThing. Only modifies the entity if changes are detected,
    /// ensuring efficient domain event publishing and change tracking.
    /// 
    /// Example usage:
    /// demoThing.Update("Updated Name", "New description", DemoThingType.DemoType2);
    /// </summary>
    /// <param name="name">New name for the demo thing</param>
    /// <param name="description">New description for the demo thing</param>
    /// <param name="type">New classification type for the demo thing</param>
    public void Update(string name, string description, DemoThingType type)
    {
        if (Name == name && Description == description && Type == type)
            return;

        Name = name;
        Description = description;
        Type = type;

        CheckPolicies();

        AddDomainEvent(new DemoThingUpdated(Id, Name));
        TrackChange(nameof(DemoThingUpdated));
    }

    /// <summary>
    /// Override point for domain invariant validation. Currently empty as this example
    /// focuses on basic business rule validation through policies.
    /// </summary>
    protected override void CheckInvariants() { }

    /// <summary>
    /// Validates business rules and policies for the DemoThing entity. Throws domain exceptions
    /// if any required fields are missing or invalid.
    /// 
    /// Business rules enforced:
    /// - Name must not be null, empty, or whitespace
    /// - Description must not be null, empty, or whitespace  
    /// - UserId must not be null, empty, or whitespace
    /// </summary>
    private void CheckPolicies()
    {
        if (Name.IsNullOrEmptyOrWhiteSpace())
            throw DemoThingDomainExceptions.NameIsRequired();

        if (Description.IsNullOrEmptyOrWhiteSpace())
            throw DemoThingDomainExceptions.DescriptionIsRequired();

        if (UserId.IsNullOrEmptyOrWhiteSpace())
            throw DemoThingDomainExceptions.UserIdIsRequired();
    }
}

/// <summary>
/// Enumeration defining the available classification types for DemoThing entities.
/// This demonstrates how to use localized display names for enum values.
/// 
/// Note: This is an example implementation. Replace with your actual business classification types.
/// </summary>
public enum DemoThingType
{
    [Display(Name = nameof(SharedResource.DemoThingType_DemoType1), ResourceType = typeof(SharedResource))]
    DemoType1 = 0,

    [Display(Name = nameof(SharedResource.DemoThingType_DemoType2), ResourceType = typeof(SharedResource))]
    DemoType2 = 1,

    [Display(Name = nameof(SharedResource.DemoThingType_DemoType3), ResourceType = typeof(SharedResource))]
    DemoType3 = 2,
}

What this file demonstrates:

  • DemoThing inherits from AggregateRoot, so it is the aggregate boundary.
  • State changes happen through methods (Create, Update) rather than public setters.
  • Business checks run inside the domain model before changes are accepted.
  • Domain events are added from inside the aggregate when business-relevant changes happen.

Domain validation and domain exceptions

Domain validation should live in the domain itself (not only in UI/API validation), so invalid states are blocked everywhere.

The domain exception factory for this feature is in DemoThingDomainExceptions.cs.

// Copyright (c) CodeBlock.Dev. All rights reserved.
// For more information visit https://codeblock.dev

using CanBeYours.Core.Resources;
using CodeBlock.DevKit.Core.Exceptions;
using CodeBlock.DevKit.Core.Resources;
using CodeBlock.DevKit.Domain.Exceptions;

namespace CanBeYours.Core.Domain.DemoThings;

/// <summary>
/// Static factory class for creating domain-specific exceptions related to DemoThing entities.
/// This class demonstrates how to implement domain exception factories that provide
/// localized error messages with proper resource management.
/// 
/// This is an example implementation showing domain exception handling patterns. Replace with
/// your actual domain exception factories that handle your business rule violations.
/// 
/// Key features demonstrated:
/// - Centralized exception creation with consistent error messages
/// - Localized error messages using resource files
/// - Domain-specific exception types for different validation failures
/// - Proper use of message placeholders for dynamic content
/// </summary>
internal static class DemoThingDomainExceptions
{
    /// <summary>
    /// Creates a domain exception when the DemoThing name is missing or invalid.
    /// This exception is thrown when business rules require a valid name but none is provided.
    /// 
    /// Example usage:
    /// if (string.IsNullOrWhiteSpace(name))
    ///     throw DemoThingDomainExceptions.NameIsRequired();
    /// </summary>
    /// <returns>A domain exception with localized error message for missing name</returns>
    public static DomainException NameIsRequired()
    {
        return new DomainException(
            nameof(CoreResource.Required),
            typeof(CoreResource),
            new List<MessagePlaceholder> { MessagePlaceholder.CreateResource(SharedResource.DemoThing_Name, typeof(SharedResource)) }
        );
    }

    /// <summary>
    /// Creates a domain exception when the DemoThing description is missing or invalid.
    /// This exception is thrown when business rules require a valid description but none is provided.
    /// 
    /// Example usage:
    /// if (string.IsNullOrWhiteSpace(description))
    ///     throw DemoThingDomainExceptions.DescriptionIsRequired();
    /// </summary>
    /// <returns>A domain exception with localized error message for missing description</returns>
    public static DomainException DescriptionIsRequired()
    {
        return new DomainException(
            nameof(CoreResource.Required),
            typeof(CoreResource),
            new List<MessagePlaceholder> { MessagePlaceholder.CreateResource(SharedResource.DemoThing_Description, typeof(SharedResource)) }
        );
    }

    /// <summary>
    /// Creates a domain exception when the DemoThing user ID is missing or invalid.
    /// This exception is thrown when business rules require a valid user ID for ownership tracking.
    /// 
    /// Example usage:
    /// if (string.IsNullOrWhiteSpace(userId))
    ///     throw DemoThingDomainExceptions.UserIdIsRequired();
    /// </summary>
    /// <returns>A domain exception with localized error message for missing user ID</returns>
    public static DomainException UserIdIsRequired()
    {
        return new DomainException(
            nameof(CoreResource.Required),
            typeof(CoreResource),
            new List<MessagePlaceholder> { MessagePlaceholder.CreateResource(SharedResource.DemoThing_UserId, typeof(SharedResource)) }
        );
    }
}

Use this pattern to keep validation errors consistent and domain-specific.

When a domain rule fails and a DomainException is thrown, you usually do not need to add special handling in each feature method. In the template flow, these exceptions are handled by the application pipeline and are returned as validation/business errors in the application service Result.

For how managed exceptions flow into Result / Result<T> and related behavior, see Exception handling.

Domain events

Domain events describe something meaningful that happened in the domain (for example, created or updated).

The DemoThing events are defined in DemoThingDomainEvents.cs.

using CodeBlock.DevKit.Domain.Events;

namespace CanBeYours.Core.Domain.DemoThings;

/// <summary>
/// Domain events for DemoThing entities. These events are published when significant changes
/// occur to DemoThing entities, allowing other parts of the system to react to these changes.
/// 
/// This is an example implementation showing domain event patterns. Replace with your actual
/// domain events that represent important business occurrences in your domain.
/// 
/// Key features demonstrated:
/// - Domain event records for immutable event data
/// - Event publishing when entities are created or updated
/// - Integration with the domain event system for decoupled communication
/// 
/// Usage examples:
/// - Audit logging when entities are modified
/// - Notification systems for entity changes
/// - Integration with external systems
/// - CQRS read model updates
/// </summary>

/// <summary>
/// Event raised when a new DemoThing entity is created. This event contains the essential
/// information about the newly created entity that other parts of the system might need.
/// 
/// Example usage in event handlers:
/// public class DemoThingCreatedHandler : IDomainEventHandler<DemoThingCreated>
/// {
///     public async Task HandleAsync(DemoThingCreated @event)
///     {
///         // Handle the creation event (e.g., send notifications, update caches)
///     }
/// }
/// </summary>
/// <param name="Id">Unique identifier of the created DemoThing</param>
/// <param name="Name">Name of the created DemoThing</param>
public record DemoThingCreated(string Id, string Name) : IDomainEvent;

/// <summary>
/// Event raised when an existing DemoThing entity is updated. This event contains the essential
/// information about the updated entity that other parts of the system might need.
/// 
/// Example usage in event handlers:
/// public class DemoThingUpdatedHandler : IDomainEventHandler<DemoThingUpdated>
/// {
///     public async Task HandleAsync(DemoThingUpdated @event)
///     {
///         // Handle the update event (e.g., send notifications, update caches)
///     }
/// }
/// </summary>
/// <param name="Id">Unique identifier of the updated DemoThing</param>
/// <param name="Name">Updated name of the DemoThing</param>
public record DemoThingUpdated(string Id, string Name) : IDomainEvent;

Use events to keep your domain decoupled from side effects. The aggregate raises the event, and other layers can react to it.

In this template, these events are also important for the built-in change tracking flow. DemoThing publishes events and calls TrackChange(...) from the aggregate root methods, which enables the change-tracking module to record domain changes. For the full module overview, see Change Tracking Introduction.

Repository contract

In DDD, repositories are domain contracts. The implementation belongs to infrastructure, but the interface belongs to the domain boundary.

The DemoThing repository contract is in IDemoThingRepository.cs.

using CodeBlock.DevKit.Core.Helpers;
using CodeBlock.DevKit.Domain.Services;

namespace CanBeYours.Core.Domain.DemoThings;

/// <summary>
/// Repository interface for DemoThing domain entities. This interface demonstrates how to extend
/// the base repository with custom query methods specific to your business requirements.
/// 
/// This is an example implementation showing repository pattern usage. Replace with your actual
/// repository interfaces that define the data access contracts for your domain entities.
/// 
/// Key features demonstrated:
/// - Extending base repository interface for specific entity types
/// - Custom search and counting methods with filtering capabilities
/// - Pagination and sorting support
/// - Date range filtering for temporal queries
/// </summary>
public interface IDemoThingRepository : IBaseAggregateRepository<DemoThing>
{
    /// <summary>
    /// Counts the total number of DemoThing entities matching the specified search criteria.
    /// This method is useful for implementing pagination and providing total count information
    /// to clients.
    /// 
    /// Example usage:
    /// var totalCount = await repository.CountAsync("search term", DemoThingType.DemoType1, 
    ///     DateTime.Today.AddDays(-30), DateTime.Today);
    /// </summary>
    /// <param name="term">Search term to filter by name or description</param>
    /// <param name="type">Optional filter by DemoThing type</param>
    /// <param name="fromDateTime">Optional start date for temporal filtering</param>
    /// <param name="toDateTime">Optional end date for temporal filtering</param>
    /// <returns>Total count of matching entities</returns>
    Task<long> CountAsync(string term, DemoThingType? type, DateTime? fromDateTime, DateTime? toDateTime);
    
    /// <summary>
    /// Searches for DemoThing entities with advanced filtering, pagination, and sorting capabilities.
    /// This method demonstrates how to implement complex query operations that are common in
    /// real-world applications.
    /// 
    /// Example usage:
    /// var results = await repository.SearchAsync("search term", DemoThingType.DemoType1, 
    ///     1, 20, SortOrder.Ascending, DateTime.Today.AddDays(-30), DateTime.Today);
    /// </summary>
    /// <param name="term">Search term to filter by name or description</param>
    /// <param name="type">Optional filter by DemoThing type</param>
    /// <param name="pageNumber">Page number for pagination (1-based)</param>
    /// <param name="recordsPerPage">Number of records per page</param>
    /// <param name="sortOrder">Sorting order for the results</param>
    /// <param name="fromDateTime">Optional start date for temporal filtering</param>
    /// <param name="toDateTime">Optional end date for temporal filtering</param>
    /// <returns>Collection of DemoThing entities matching the criteria</returns>
    Task<IEnumerable<DemoThing>> SearchAsync(
        string term,
        DemoThingType? type,
        int pageNumber,
        int recordsPerPage,
        SortOrder sortOrder,
        DateTime? fromDateTime,
        DateTime? toDateTime
    );
}

IDemoThingRepository extends IBaseAggregateRepository<DemoThing>, which gives you a consistent base for aggregate persistence plus concurrency-safe updates.

Connecting the domain model to Infrastructure

After defining the domain model, connect it in Infrastructure so it can be persisted and queried.

1) Add your aggregate to MainDbContext

The MongoDB context is implemented in MainDbContext.cs.

using CanBeYours.Core.Domain.DemoThings;
using CodeBlock.DevKit.Infrastructure.Database;
using MongoDB.Driver;

namespace CanBeYours.Infrastructure.DbContext;

/// <summary>
/// Main database context for the application that extends MongoDbContext to provide
/// MongoDB-specific functionality. This class demonstrates how to set up a database context
/// with collections, indexes, and custom database operations.
/// 
/// IMPORTANT: This is an example implementation for learning purposes. Replace DemoThing
/// with your actual domain entities and collections.
/// 
/// Key features demonstrated:
/// - MongoDB collection management
/// - Database index creation
/// - Safe test database operations
/// - Collection access through properties
/// </summary>
internal class MainDbContext : MongoDbContext
{
    /// <summary>
    /// Initializes a new instance of MainDbContext with MongoDB settings.
    /// </summary>
    /// <param name="mongoDbSettings">MongoDB connection and configuration settings</param>
    public MainDbContext(MongoDbSettings mongoDbSettings)
        : base(mongoDbSettings) { }

    /// <summary>
    /// MongoDB collection for DemoThing entities.
    /// This property provides access to the DemoThings collection for CRUD operations.
    /// </summary>
    public IMongoCollection<DemoThing> DemoThings { get; private set; }

    /// <summary>
    /// Creates database indexes for optimal query performance.
    /// This method demonstrates how to set up indexes on commonly queried fields.
    /// 
    /// Example: Creates a non-unique index on the Name field for faster text searches.
    /// </summary>
    protected override void CreateIndexes()
    {
        DemoThings.Indexes.CreateOne(
            new CreateIndexModel<DemoThing>(
                Builders<DemoThing>.IndexKeys.Ascending(x => x.Name),
                new CreateIndexOptions() { Name = nameof(DemoThing.Name), Unique = false }
            )
        );
    }

    /// <summary>
    /// Safely drops test databases for testing purposes.
    /// Only drops databases that start with "Test_" prefix to prevent accidental
    /// deletion of production databases.
    /// 
    /// Example usage in test cleanup:
    /// dbContext.DropTestDatabase();
    /// </summary>
    public void DropTestDatabase()
    {
        // Only drop the database if it starts with "Test_" to avoid dropping production databases.
        if (!_mongoDbSettings.DatabaseName.StartsWith("Test_"))
            return;

        _client.DropDatabase(_mongoDbSettings.DatabaseName);
    }
}

For your own aggregate, follow the same pattern:

  • Add an IMongoCollection<YourAggregate> property.
  • Create indexes in CreateIndexes() for your common query paths.
  • Keep collection/index definitions in the context so repository code stays focused on business queries.

2) Configure database startup, migrations, and seed data

Infrastructure service wiring is in Startup.cs.

Use this focused placeholder to identify exactly what to update in Startup.cs:

// Add in AddDomainServices(...)
services.AddScoped<IYourAggregateRepository, YourAggregateRepository>();

// Keep/extend in AddMongoDbContext(...)
services.AddScoped<MainDbContext>();

// Ensure app startup runs infrastructure initialization
app.UseInfrastructureModule();

// Add your feature seed data in infrastructure startup flow
serviceProvider.SeedYourFeatureData();

This is where DB-related services are configured and initialized:

  • Register MainDbContext and repository services in DI.
  • Run module startup (UseInfrastructureModule) so database initialization is executed.
  • Add your own seed logic in this startup flow when your feature requires initial data.
  • Define indexes in MainDbContext.CreateIndexes() for performance.

3) Implement repository in Infrastructure

The DemoThing repository implementation is in DemoThingRepository.cs.

Focus on this repository shape:

internal class YourAggregateRepository
    : MongoDbBaseAggregateRepository<YourAggregate>, IYourAggregateRepository
{
    // 1) Inject MainDbContext and pass collection to base(...)
    // 2) Implement your domain contract methods
    // 3) Keep query filter/paging/sorting logic here
}

Use it as your reference:

  • Inherit from MongoDbBaseAggregateRepository<YourAggregate>.
  • Implement your domain repository interface from the Core layer.
  • Keep filtering/paging/sorting query logic in repository methods.
  • Register the interface-to-implementation mapping in Infrastructure Startup.

With this domain model in place, the next page shows how application use cases orchestrate these domain concepts to implement business operations.