Core Concepts
This article introduces the core concepts of the MiCake framework to help you better understand and use it.
Module System
Section titled “Module System”What is a Module
Section titled “What is a Module”MiCake adopts a modular design; an application consists of multiple modules. Each module is an independent functional unit that can:
- Configure its own services
- Manage its own lifecycle
- Declare dependencies on other modules
Module Lifecycle
Section titled “Module Lifecycle”Each module has explicit lifecycle hooks:
public class MyModule : MiCakeModule{ // 1. Configure services phase public override void ConfigureServices(ModuleConfigServiceContext context) { // Register services into the DI container context.Services.AddScoped<IMyService, MyService>(); }
// 2. Application initialization phase public override void OnApplicationInitialization(ModuleInitializationContext context) { // Initialization logic when the application starts var logger = context.ServiceProvider.GetService<ILogger>(); logger.LogInformation("Module initialized"); }
// 3. Application shutdown phase public override void OnApplicationShutdown(ModuleShutdownContext context) { // Cleanup logic when the application shuts down }}Module Dependencies
Section titled “Module Dependencies”Use the [RelyOn] attribute to declare dependencies between modules:
[RelyOn(typeof(MiCakeAspNetCoreModule))][RelyOn(typeof(MiCakeEntityFrameworkCoreModule))]public class MyAppModule : MiCakeModule{ // The framework ensures dependent modules are initialized first}Domain-Driven Design (DDD)
Section titled “Domain-Driven Design (DDD)”Core Ideas of DDD
Section titled “Core Ideas of DDD”MiCake implements the core components of DDD tactical patterns:
- Entity: an object with a unique identity
- Value Object: an immutable object compared by property values
- Aggregate Root: the root entity of an aggregate
- Repository: provides persistence for aggregate roots
- Domain Event: captures business events
- Domain Service: encapsulates domain logic
Aggregate Boundaries
Section titled “Aggregate Boundaries”An aggregate is a collection of related objects, accessed through the aggregate root:
┌─────────────────────────────────┐│ Aggregate (Order) ││ ┌──────────────────────────┐ ││ │ Aggregate Root (Order) │◄──┼─── External access only through the aggregate root│ │ - OrderId │ ││ │ - Customer │ ││ │ - Status │ ││ └──────────────────────────┘ ││ │ manages ││ ▼ ││ ┌──────────────────────────┐ ││ │ Entity (OrderItem) │ ││ │ - ProductId │ ││ │ - Quantity │ ││ │ - Price │ ││ └──────────────────────────┘ │└─────────────────────────────────┘Entity vs Value Object
Section titled “Entity vs Value Object”Entity characteristics:
- Has a unique identity (Id)
- Mutable
- Equality is compared by Id
- Has a lifecycle
public class Order : AggregateRoot<int>{ public int Id { get; init; } // Unique identity public string OrderNumber { get; private set; } // ...}Value Object characteristics:
- No unique identity
- Immutable
- Equality is compared by all property values
- Can be replaced
public class Address : ValueObject{ public string Street { get; } public string City { get; } public string ZipCode { get; }
public Address(string street, string city, string zipCode) { Street = street; City = city; ZipCode = zipCode; }
protected override IEnumerable<object> GetEqualityComponents() { yield return Street; yield return City; yield return ZipCode; }}Repository Pattern
Section titled “Repository Pattern”Repository Responsibilities
Section titled “Repository Responsibilities”A repository encapsulates data access logic and provides a collection-like interface:
public interface IRepository<TAggregateRoot, TKey>{ Task<TAggregateRoot> FindAsync(TKey id); Task AddAsync(TAggregateRoot aggregateRoot); Task AddAndGetIdAsync(TAggregateRoot aggregateRoot); Task UpdateAsync(TAggregateRoot aggregateRoot); Task DeleteAsync(TAggregateRoot aggregateRoot); Task DeleteByIdAsync(TKey id);}Repositories no longer own persistence: repository methods only modify the tracking state of the Unit of Work; data is committed by
IUnitOfWork.CommitAsync().
Repositories Are Only for Aggregate Roots
Section titled “Repositories Are Only for Aggregate Roots”❌ Wrong approach:
// Do not create repositories for internal entitiespublic interface IOrderItemRepository : IRepository<OrderItem, int>{}✅ Correct approach:
// Create repositories only for aggregate rootspublic interface IOrderRepository : IRepository<Order, int>{}
// Access internal entities through the aggregate rootvar order = await orderRepository.FindAsync(orderId);var items = order.Items; // Access through the aggregate rootDomain Events
Section titled “Domain Events”Event-Driven Architecture
Section titled “Event-Driven Architecture”Domain events capture important business events that occur in the domain:
// 1. Define the eventpublic class OrderPlacedEvent : IDomainEvent{ public int OrderId { get; } public decimal TotalAmount { get; }
public OrderPlacedEvent(int orderId, decimal totalAmount) { OrderId = orderId; TotalAmount = totalAmount; }}
// 2. Raise the event in the aggregate rootpublic class Order : AggregateRoot<int>{ public void PlaceOrder() { // Business logic Status = OrderStatus.Placed;
// Raise the domain event RaiseDomainEvent(new OrderPlacedEvent(Id, TotalAmount)); }}
// 3. Handle the eventpublic class OrderPlacedEventHandler : IDomainEventHandler<OrderPlacedEvent>{ public Task HandleAsync(OrderPlacedEvent domainEvent, CancellationToken cancellationToken) { // Send email notification // Update inventory // Write logs return Task.CompletedTask; }}Automatic Event Dispatch
Section titled “Automatic Event Dispatch”Domain events are dispatched automatically when the unit of work commits (IUnitOfWork.CommitAsync()):
var order = Order.Create(customer);order.PlaceOrder(); // Raises the event, but does not dispatch immediately
await repository.AddAsync(order);await unitOfWork.CommitAsync(); // All events are dispatched hereUnit of Work
Section titled “Unit of Work”What is a Unit of Work
Section titled “What is a Unit of Work”The Unit of Work pattern is used to:
- Track all changes during a business operation
- Ensure changes are committed as a single transaction
- Guarantee data consistency
Unit of Work in MiCake
Section titled “Unit of Work in MiCake”MiCake provides several ways to start a unit of work:
using MiCake.AspNetCore.Uow;
[ApiController][Route("api/[controller]")]public class OrderController : ControllerBase{ private readonly IRepository<Order, int> _orderRepository; private readonly IRepository<Product, int> _productRepository;
[HttpPost] [UnitOfWork] // Starts a unit of work automatically public async Task<IActionResult> CreateOrder([FromBody] CreateOrderDto dto) { // 1. Create the order var order = Order.Create(dto.CustomerId, dto.ShippingAddress);
foreach (var item in dto.Items) { order.AddItem(item.ProductId, item.ProductName, item.Price, item.Quantity); }
await _orderRepository.AddAsync(order);
// 2. Update product inventory foreach (var item in dto.Items) { var product = await _productRepository.FindAsync(item.ProductId); product.DecreaseStock(item.Quantity); }
// When the method returns normally, the UoW commits the transaction automatically // If an exception is thrown, the transaction is rolled back automatically return Ok(order.Id); }}For concrete examples, read the Unit of Work documentation.
Dependency Injection
Section titled “Dependency Injection”Automatic Service Registration
Section titled “Automatic Service Registration”MiCake supports automatic service registration through interface markers:
// Marked as a transient servicepublic class MyService : ITransientService{ // Automatically registered with the Transient lifetime}
// Marked as a scoped servicepublic class OrderService : IScopedService{ // Automatically registered with the Scoped lifetime}
// Marked as a singleton servicepublic class CacheService : ISingletonService{ // Automatically registered with the Singleton lifetime}Manual Service Registration
Section titled “Manual Service Registration”Use the [InjectService] attribute to precisely control registration:
[InjectService(typeof(IMyService), ServiceLifetime.Scoped)]public class MyService : IMyService{ // ...}Best Practices Summary
Section titled “Best Practices Summary”1. Aggregate Design Principles
Section titled “1. Aggregate Design Principles”- Keep aggregates small and focused
- Access internal entities through the aggregate root
- Ensure consistency within aggregate boundaries
- Communicate across aggregates via domain events
2. Repository Usage Principles
Section titled “2. Repository Usage Principles”- Create repositories only for aggregate roots
- Repository operations should be atomic
- Complete related operations within a single unit of work
- Avoid exposing data access details via IQueryable, because every method in a repository should express a clear business intent
3. Service Layering
Section titled “3. Service Layering”- Domain services: core business logic
- Application services: coordinate operations across multiple aggregates
- Infrastructure services: technical services (caching, logging, etc.)
Next Steps
Section titled “Next Steps”Now that you understand the core concepts of MiCake, you can dive deeper into each specific component:
- Entity - learn the detailed usage of entities
- Value Object - learn how to design value objects
- Aggregate Root - master aggregate design principles
- Repository - understand the repository pattern in depth
- Domain Event - implement event-driven architecture
