Практичні Настанови з Архітектури Witstally Backend
Цей документ описує Уніфіковану Архітектуру (Unified Generic CRUD Architecture) для проекту Witstally. Архітектура натхненна найкращими практиками паттернів, які використовуються у проектах екосистеми MyCodeBot, що дозволяє значно пришвидшити розробку та уникнути дублювання коду.
Основними концептами є:
IEntity— для всіх доменних сутностей БД уWitstally.Core.IModelableDto<T>таIUpdatableDto<T>— для прямого мапінгу у DTO моделях.ServiceBase<T>— узагальнений CRUD сервіс.BaseCrudController<TModel, TCreateDto, TUpdateDto, TReadDto>— узагальнений REST контролер.BaseUnitтаITransactionalExecutor— для багатокрокових, складних бізнес-транзакцій.
1. Сутності (Entities)
Усі сутності, що мапляться на таблиці в базі даних, тепер знаходяться в проекті Witstally.Core і обов’язково мають реалізовувати інтерфейс IEntity (щоб гарантувати наявність Id).
namespace Witstally.Core.Entities;
public class Employee : IEntity
{
public Guid Id { get; set; } = Guid.NewGuid();
public string Name { get; set; } = string.Empty;
}
2. DTO Мапінг (Без AutoMapper)
Всі DTO винесені в окремий проект Witstally.DtoModels. Замість створення зовнішніх маперів ми використовуємо інтерфейси, які примушують саму DTO містити логіку своєї конвертації:
Створення (Create)
Для DTO створення використовується IModelableDto<TEntity>. Метод ToModel() перетворює DTO у нову сутність для запису.
public class CreateEmployeeDto : IModelableDto<Employee>
{
public string Name { get; set; } = string.Empty;
public Employee ToModel()
{
return new Employee { Name = Name };
}
}
Оновлення (Update)
Для оновлення використовується IUpdatableDto<TEntity>. Метод ApplyToModel(model) модифікує існуючу в БД сутність.
public class UpdateEmployeeDto : IUpdatableDto<Employee>
{
public string Name { get; set; } = string.Empty;
public void ApplyToModel(Employee model)
{
model.Name = Name;
}
}
3. Базові Сервіси (ServiceBase)
Усі сервіси, що відповідають за прості CRUD операції над таблицею, знаходяться в проекті Witstally.Services і наслідують ServiceBase<TEntity>.
using Witstally.Infrastructure.Data;
namespace Witstally.Services;
public class EmployeeService : ServiceBase<Employee>
{
// ApplicationDbContext підключається автоматично в ServiceBase
public EmployeeService(ApplicationDbContext context) : base(context) { }
// Конкретні для співробітників методи...
}
ServiceBase вже вміє робити GetById, GetAll, AddAsync, UpdateAsync, DeleteById. Ти просто створюєш свій сервіс та додаєш його у Program.cs (services.AddScoped<EmployeeService>()).
4. Базові Контролери (BaseCrudController)
Щоб не писати однотипні POST, GET, PUT, DELETE ендпоінти, контролери у Witstally.WebApi наслідують BaseCrudController.
using Witstally.WebApi.Controllers.BaseControllers;
using Witstally.DtoModels;
using Witstally.Services;
using Witstally.Core.Entities;
using Microsoft.AspNetCore.Mvc;
namespace Witstally.WebApi.Controllers;
[ApiController]
[Route("api/[controller]")]
public class EmployeeController : BaseCrudController<Employee, CreateEmployeeDto, UpdateEmployeeDto, GetEmployeeDto>
{
public EmployeeController(EmployeeService service) : base(service)
{
// CRUD (GetAll, Get{id}, Post, Put, Delete) генерується автоматично!
}
}
5. Units та Транзакції (Складні процеси)
Для процесів, що потребують участі кількох сервісів одночасно (наприклад, затвердження документу Document, що має автоматично згенерувати проводки JournalEntry через TransactionService), категорично заборонено викликати сервіс із контролера без транзакції.
Для цього використовується патерн Unit:
Всі юніти наслідують BaseUnit (знаходиться в Witstally.Services.Units) і отримують ITransactionalExecutor.
Приклад DocumentApprovalUnit:
public class DocumentApprovalUnit : BaseUnit
{
private readonly DocumentService _documentService;
private readonly TransactionService _transactionService;
public DocumentApprovalUnit(ITransactionalExecutor tx, DocumentService ds, TransactionService ts) : base(tx)
{
_documentService = ds;
_transactionService = ts;
}
public async Task ExecuteAsync(Guid documentId)
{
// Уся ця логіка буде надійно огорнута в одну транзакцію БД. При помилці - Rollback.
await _tx.StartEffect(async (ct) =>
{
var document = await _documentService.ApproveDocumentAsync(documentId);
await _transactionService.CreateJournalEntriesForDocumentAsync(document);
}, IsolationLevel.ReadCommitted);
}
}
Юніти також треба реєструвати у DI (builder.Services.AddScoped<DocumentApprovalUnit>()).
Реєстрація DI Container
Пам'ятайте завжди додавати нові сервіси у ваш Program.cs:
// Infrastructure
builder.Services.AddScoped<ApplicationDbContext>();
// Transactions Utility
builder.Services.AddScoped(typeof(ITransactionalExecutor), typeof(TransactionalExecutor<ApplicationDbContext>));
// Basic CRUD Services
builder.Services.AddScoped<EmployeeService>();
builder.Services.AddScoped<CounterpartyService>();
// Orchestration Units
builder.Services.AddScoped<DocumentApprovalUnit>();