Tisa.Domain 2025.9.10.1120

dotnet add package Tisa.Domain --version 2025.9.10.1120
                    
NuGet\Install-Package Tisa.Domain -Version 2025.9.10.1120
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Tisa.Domain" Version="2025.9.10.1120" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Tisa.Domain" Version="2025.9.10.1120" />
                    
Directory.Packages.props
<PackageReference Include="Tisa.Domain" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Tisa.Domain --version 2025.9.10.1120
                    
#r "nuget: Tisa.Domain, 2025.9.10.1120"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Tisa.Domain@2025.9.10.1120
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Tisa.Domain&version=2025.9.10.1120
                    
Install as a Cake Addin
#tool nuget:?package=Tisa.Domain&version=2025.9.10.1120
                    
Install as a Cake Tool

Tisa.Domain

NuGet Version License

Библиотека базовых компонентов доменной модели для .NET приложений, разработанная компанией ТИСА. Предоставляет фундаментальные абстракции и примитивы для построения доменной модели в соответствии с принципами Domain-Driven Design (DDD).

Возможности

  • Поддержка .NET 8.0, .NET 9.0 и .NET 10.0
  • Базовые классы для DDD: AggregateRoot, XEntity
  • Система доменных событий (Domain Events)
  • Коллекции сущностей с типизированным доступом
  • Ссылки на сущности (EntityLink)
  • Интерфейсы для аудита и валидации
  • Интеграция с FluentValidation
  • Доменные исключения и ошибки
  • Интеграция с Tisa.Common

Установка

dotnet add package Tisa.Domain

Основные компоненты

XEntity - Базовая сущность

Базовый класс для всех доменных сущностей с идентификатором типа Guid и поддержкой доменных событий.

Определение сущности
using Tisa.Domain.Primitives;

public class User : XEntity
{
    public string Name { get; private set; }
    public string Email { get; private set; }

    // Защищенный конструктор для EF Core
    protected User() { }

    public User(Guid id, string name, string email)
        : base(id)
    {
        Name = name;
        Email = email;
        
        // Регистрация доменного события
        RegisterEvent(new UserCreatedEvent(id, name, email));
    }

    public void UpdateEmail(string newEmail)
    {
        Email = newEmail;
        RegisterEvent(new UserEmailUpdatedEvent(Id, newEmail));
    }
}
Работа с доменными событиями
// Получение всех событий сущности
var events = user.DomainEvents;

// Очистка событий (обычно после сохранения)
user.ClearEvents();

AggregateRoot - Агрегирующий корень

Базовый класс для агрегирующих корней в DDD. Все агрегаты должны наследоваться от этого класса.

Определение агрегата
using Tisa.Domain.Primitives;

public class Order : AggregateRoot
{
    private readonly EntityList<OrderItem> _items = new();
    
    public string OrderNumber { get; private set; }
    public DateTime CreatedAt { get; private set; }
    public IReadOnlyCollection<OrderItem> Items => _items.Items;

    protected Order() { }

    public Order(Guid id, string orderNumber)
        : base(id)
    {
        OrderNumber = orderNumber;
        CreatedAt = DateTime.UtcNow;
        RegisterEvent(new OrderCreatedEvent(id, orderNumber));
    }

    public void AddItem(OrderItem item)
    {
        _items.Add(item);
        RegisterEvent(new OrderItemAddedEvent(Id, item.Id));
    }

    public void RemoveItem(Guid itemId)
    {
        _items.Delete(itemId);
        RegisterEvent(new OrderItemRemovedEvent(Id, itemId));
    }
}

EntityList - Коллекция сущностей

Типизированная коллекция для работы с коллекциями сущностей внутри агрегатов.

Использование EntityList
using Tisa.Domain.Primitives;
using Tisa.Common.Primitives;

public class Order : AggregateRoot
{
    private readonly EntityList<OrderItem> _items = new();

    public IReadOnlyCollection<OrderItem> Items => _items.Items;

    public void AddItem(OrderItem item)
    {
        // Проверка существования
        var existingItem = _items.Get(item.Id);
        if (existingItem != null)
        {
            throw new DomainException("Товар уже добавлен в заказ");
        }

        _items.Add(item);
    }

    public void RemoveItem(Guid itemId)
    {
        // Использование Maybe для безопасного получения
        var item = _items.Maybe(itemId);
        item.Match(
            onValue: _ => _items.Delete(itemId),
            onNone: () => throw new NotFoundException("Товар не найден в заказе")
        );
    }

    public OrderItem? GetItem(Guid itemId)
    {
        return _items.Get(itemId);
    }

    public bool HasItems()
    {
        return !_items.IsEmpty;
    }
}

Класс для представления ссылок на другие сущности без полной загрузки.

using Tisa.Domain.Primitives;

public class OrderItem : XEntity
{
    public EntityLink Product { get; private set; }
    public int Quantity { get; private set; }

    public OrderItem(Guid id, Guid productId, string productName, int quantity)
        : base(id)
    {
        Product = new EntityLink("Product", productId, productName);
        Quantity = quantity;
    }
}

// Использование
var orderItem = new OrderItem(
    Guid.NewGuid(),
    productId,
    "Product Name",
    10
);

// Доступ к ссылке
var productId = orderItem.Product.Id;
var productType = orderItem.Product.Type;
var productTitle = orderItem.Product.Title;

Domain Events - Доменные события

Система доменных событий для реализации паттерна Domain Events в DDD.

Определение доменного события
using Tisa.Domain.Messaging;

public class UserCreatedEvent : DomainEvent
{
    public Guid UserId { get; }
    public string UserName { get; }
    public string UserEmail { get; }

    public UserCreatedEvent(Guid userId, string userName, string userEmail)
    {
        UserId = userId;
        UserName = userName;
        UserEmail = userEmail;
    }
}
Обработчик доменного события
using Tisa.Domain.Messaging;

public class UserCreatedEventHandler : IDomainEventHandler<UserCreatedEvent>
{
    private readonly IEmailService _emailService;

    public UserCreatedEventHandler(IEmailService emailService)
    {
        _emailService = emailService;
    }

    public async Task Handle(UserCreatedEvent domainEvent, CancellationToken cancellationToken = default)
    {
        // Отправка приветственного email
        await _emailService.SendWelcomeEmailAsync(
            domainEvent.UserEmail,
            domainEvent.UserName,
            cancellationToken
        );
    }
}
Регистрация обработчиков событий
using Microsoft.Extensions.DependencyInjection;
using Tisa.Domain.Messaging;

public void ConfigureServices(IServiceCollection services)
{
    // Регистрация обработчиков событий
    services.AddScoped<IDomainEventHandler<UserCreatedEvent>, UserCreatedEventHandler>();
    services.AddScoped<IDomainEventHandler<OrderCreatedEvent>, OrderCreatedEventHandler>();
}
Публикация событий
public class UnitOfWork : IUnitOfWork
{
    private readonly IServiceProvider _serviceProvider;

    public async Task SaveChangesAsync(CancellationToken cancellationToken = default)
    {
        // Сохранение изменений в БД
        await _dbContext.SaveChangesAsync(cancellationToken);

        // Публикация доменных событий
        var entities = _dbContext.ChangeTracker
            .Entries<XEntity>()
            .Select(e => e.Entity)
            .ToList();

        foreach (var entity in entities)
        {
            foreach (var domainEvent in entity.DomainEvents)
            {
                await PublishEventAsync(domainEvent, cancellationToken);
            }
            entity.ClearEvents();
        }
    }

    private async Task PublishEventAsync(IDomainEvent domainEvent, CancellationToken cancellationToken)
    {
        var handlerType = typeof(IDomainEventHandler<>).MakeGenericType(domainEvent.GetType());
        var handlers = _serviceProvider.GetServices(handlerType);

        foreach (var handler in handlers)
        {
            var method = handlerType.GetMethod(nameof(IDomainEventHandler<IDomainEvent>.Handle));
            await (Task)method.Invoke(handler, new object[] { domainEvent, cancellationToken });
        }
    }
}

IAuditableEntity - Аудит сущностей

Интерфейс для отслеживания дат создания и изменения сущностей.

Реализация аудита
using Tisa.Domain.Abstractions;

public class User : XEntity, IAuditableEntity
{
    public DateTimeOffset? CreatedOn { get; private set; }
    public DateTimeOffset? ModifiedOn { get; private set; }

    public User(Guid id, string name, string email)
        : base(id)
    {
        Name = name;
        Email = email;
        CreatedOn = DateTimeOffset.UtcNow;
    }

    public void UpdateEmail(string newEmail)
    {
        Email = newEmail;
        ModifiedOn = DateTimeOffset.UtcNow;
    }
}

IValidatableObject - Валидация объектов

Интерфейс для интеграции с FluentValidation.

Использование валидации
using Tisa.Domain.Abstractions;
using FluentValidation;

public class CreateUserCommand : IValidatableObject
{
    public string Name { get; set; }
    public string Email { get; set; }

    private ValidationException _validationException;

    public void SetValidationError(ValidationException validationException)
    {
        _validationException = validationException;
    }
}

public class CreateUserCommandValidator : AbstractValidator<CreateUserCommand>
{
    public CreateUserCommandValidator()
    {
        RuleFor(x => x.Name)
            .NotEmpty().WithMessage("Имя обязательно")
            .MaximumLength(100).WithMessage("Имя не должно превышать 100 символов");

        RuleFor(x => x.Email)
            .NotEmpty().WithMessage("Email обязателен")
            .EmailAddress().WithMessage("Неверный формат email");
    }
}

Исключения домена

Библиотека предоставляет специализированные исключения для доменной логики.

DomainException
using Tisa.Domain.Exceptions;
using Tisa.Common.Errors;

public class UserService
{
    public void ActivateUser(User user)
    {
        if (user.IsActive)
        {
            throw new DomainException(
                Error.Conflict("User.AlreadyActive", "Пользователь уже активирован")
            );
        }

        user.Activate();
    }
}
NotFoundException
using Tisa.Domain.Exceptions;

public class UserRepository
{
    public User GetById(Guid id)
    {
        var user = _dbContext.Users.Find(id);
        if (user == null)
        {
            throw new NotFoundException($"Пользователь с ID {id} не найден");
        }
        return user;
    }
}
ValidateException
using Tisa.Domain.Exceptions;
using FluentValidation;

public class UserService
{
    private readonly IValidator<CreateUserCommand> _validator;

    public void CreateUser(CreateUserCommand command)
    {
        var validationResult = _validator.Validate(command);
        if (!validationResult.IsValid)
        {
            throw new ValidateException(validationResult.Errors);
        }

        // Создание пользователя
    }
}

DomainErrors - Статические ошибки домена

Централизованное хранение ошибок домена.

Использование
using Tisa.Domain.Errors;

public static partial class DomainErrors
{
    public static Error UserNotFound(Guid userId) =>
        Error.NotFound("User.NotFound", $"Пользователь с ID {userId} не найден");

    public static Error UserEmailAlreadyExists(string email) =>
        Error.Conflict("User.EmailExists", $"Email {email} уже используется");

    public static Error InvalidUserStatus =>
        Error.BadRequest("User.InvalidStatus", "Неверный статус пользователя");
}

// Использование
throw new DomainException(DomainErrors.UserNotFound(userId));

Структура проекта

  • Abstractions/ - Базовые интерфейсы
    • IAuditableEntity - Интерфейс для аудита сущностей
    • IValidatableObject - Интерфейс для валидации объектов
  • Primitives/ - Базовые примитивы домена
    • AggregateRoot - Агрегирующий корень
    • XEntity - Базовая сущность с Guid
    • EntityLink - Ссылка на сущность
    • EntityList<T> - Типизированная коллекция сущностей
  • Messaging/ - Система доменных событий
    • DomainEvent - Базовый класс доменного события
    • IDomainEvent - Интерфейс доменного события
    • IDomainEventHandler - Интерфейс обработчика событий
  • Errors/ - Ошибки домена
    • DomainErrors - Статические ошибки домена
  • Exceptions/ - Исключения домена
    • DomainException - Базовое исключение домена
    • NotFoundException - Исключение для не найденных объектов
    • ValidateException - Исключение валидации

Примеры использования

Полный пример агрегата

using Tisa.Domain.Primitives;
using Tisa.Domain.Messaging;
using Tisa.Domain.Abstractions;

public class Order : AggregateRoot, IAuditableEntity
{
    private readonly EntityList<OrderItem> _items = new();

    public string OrderNumber { get; private set; }
    public OrderStatus Status { get; private set; }
    public DateTimeOffset? CreatedOn { get; private set; }
    public DateTimeOffset? ModifiedOn { get; private set; }
    public IReadOnlyCollection<OrderItem> Items => _items.Items;

    protected Order() { }

    public Order(Guid id, string orderNumber)
        : base(id)
    {
        OrderNumber = orderNumber;
        Status = OrderStatus.Draft;
        CreatedOn = DateTimeOffset.UtcNow;
        RegisterEvent(new OrderCreatedEvent(id, orderNumber));
    }

    public void AddItem(Guid productId, string productName, int quantity, decimal price)
    {
        if (Status != OrderStatus.Draft)
        {
            throw new DomainException(
                Error.BadRequest("Order.InvalidStatus", "Нельзя добавлять товары в заказ не в статусе Draft")
            );
        }

        var item = new OrderItem(Guid.NewGuid(), productId, productName, quantity, price);
        _items.Add(item);
        ModifiedOn = DateTimeOffset.UtcNow;
        RegisterEvent(new OrderItemAddedEvent(Id, item.Id));
    }

    public void Confirm()
    {
        if (Status != OrderStatus.Draft)
        {
            throw new DomainException(
                Error.BadRequest("Order.InvalidStatus", "Можно подтверждать только заказы в статусе Draft")
            );
        }

        if (_items.IsEmpty)
        {
            throw new DomainException(
                Error.BadRequest("Order.Empty", "Нельзя подтвердить пустой заказ")
            );
        }

        Status = OrderStatus.Confirmed;
        ModifiedOn = DateTimeOffset.UtcNow;
        RegisterEvent(new OrderConfirmedEvent(Id));
    }
}

Требования

  • .NET 8.0, .NET 9.0 или .NET 10.0
  • Tisa.Common
  • FluentValidation 12.1.0

Интеграция с Entity Framework Core

public class ApplicationDbContext : DbContext
{
    public DbSet<Order> Orders { get; set; }
    public DbSet<User> Users { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Настройка XEntity
        modelBuilder.Entity<Order>(entity =>
        {
            entity.HasKey(e => e.Id);
            entity.Ignore(e => e.DomainEvents);
        });

        // Настройка Value Objects через OwnsOne
        // ...
    }

    public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
    {
        // Сохранение изменений
        var result = await base.SaveChangesAsync(cancellationToken);

        // Публикация доменных событий
        await PublishDomainEventsAsync(cancellationToken);

        return result;
    }

    private async Task PublishDomainEventsAsync(CancellationToken cancellationToken)
    {
        var entities = ChangeTracker
            .Entries<XEntity>()
            .Select(e => e.Entity)
            .Where(e => e.DomainEvents.Any())
            .ToList();

        foreach (var entity in entities)
        {
            foreach (var domainEvent in entity.DomainEvents)
            {
                // Публикация события через медиатор или сервис
                await _eventPublisher.PublishAsync(domainEvent, cancellationToken);
            }
            entity.ClearEvents();
        }
    }
}

Авторы

Команда разработчиков TISA

Лицензия

MIT License

Поддержка

Для получения поддержки или сообщения об ошибках, пожалуйста, напишите нам на support@tisn.ru

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.