DDD Service Scaffolding
Generates a complete service following The Standard architecture for the arolariu.ro backend API.
When to Use
- •Creating a new CRUD service for a domain entity
- •Adding a new bounded context
- •Extending an existing domain with new capabilities
Architecture Layers
Generate artifacts in this order (bottom-up):
1. Broker (Data Access Layer)
Create interface and implementation for external data access.
Interface ([Domain]/Brokers/I[Entity][Storage]Broker.cs):
/// <summary>
/// Broker for [Entity] data access operations.
/// </summary>
public interface I[Entity]NoSqlBroker
{
/// <summary>Creates a new [entity] in the data store.</summary>
Task Create[Entity]Async([Entity] entity);
/// <summary>Reads an [entity] by identifier.</summary>
Task<[Entity]?> Read[Entity]Async(Guid identifier, Guid? partitionKey = null);
/// <summary>Updates an existing [entity].</summary>
Task Update[Entity]Async([Entity] entity, Guid? partitionKey = null);
/// <summary>Deletes an [entity] by identifier.</summary>
Task Delete[Entity]Async(Guid identifier, Guid? partitionKey = null);
}
Implementation ([Domain]/Brokers/[Entity]NoSqlBroker.cs):
public sealed class [Entity]NoSqlBroker(CosmosClient cosmosClient) : I[Entity]NoSqlBroker
{
private readonly Container _container = cosmosClient
.GetDatabase("arolariu")
.GetContainer("[entities]");
/// <inheritdoc/>
public async Task Create[Entity]Async([Entity] entity) =>
await _container.CreateItemAsync(entity,
new PartitionKey(entity.UserIdentifier.ToString()))
.ConfigureAwait(false);
// ... other CRUD operations with .ConfigureAwait(false)
}
Rules:
- •NO business logic in Brokers
- •Always use
.ConfigureAwait(false) - •Use primary constructors
- •Seal the class
2. Foundation Service (CRUD + Validation)
Create using partial class separation:
Main file ([Domain]/Services/Foundation/[Entity]StorageFoundationService.cs):
/// <summary>
/// Foundation service for [Entity] storage operations.
/// </summary>
/// <remarks>
/// <para>Handles CRUD with validation. Dependencies: 1 broker (Florance compliant).</para>
/// </remarks>
public partial class [Entity]StorageFoundationService(
I[Entity]NoSqlBroker broker,
ILoggerFactory loggerFactory) : I[Entity]StorageFoundationService
{
/// <summary>Creates a new [entity] after validation.</summary>
public async Task Create[Entity]Object([Entity] entity, Guid? userIdentifier = null) =>
await TryCatchAsync(async () =>
{
using var activity = [Domain]PackageTracing.StartActivity(nameof(Create[Entity]Object));
activity?.SetTag("[entity].id", entity.id.ToString());
Validate[Entity]InformationIsValid(entity);
await broker.Create[Entity]Async(entity).ConfigureAwait(false);
}).ConfigureAwait(false);
}
Exceptions partial ([Domain]/Services/Foundation/[Entity]StorageFoundationService.Exceptions.cs):
public partial class [Entity]StorageFoundationService
{
private async Task TryCatchAsync(Func<Task> returningFunction)
{
try { await returningFunction(); }
catch (CosmosException ex) { throw new [Entity]DependencyException(ex); }
catch (Exception ex) { throw new [Entity]ServiceException(ex); }
}
}
Validations partial ([Domain]/Services/Foundation/[Entity]StorageFoundationService.Validations.cs):
public partial class [Entity]StorageFoundationService
{
private static void Validate[Entity]InformationIsValid([Entity] entity)
{
if (entity is null) throw new [Entity]ValidationException("Entity cannot be null.");
if (entity.id == Guid.Empty) throw new [Entity]ValidationException("Entity ID is required.");
}
}
3. DI Registration
[Domain]/[Domain]Extensions.cs:
public static IServiceCollection Add[Domain]Services(this IServiceCollection services)
{
services.AddScoped<I[Entity]NoSqlBroker, [Entity]NoSqlBroker>();
services.AddScoped<I[Entity]StorageFoundationService, [Entity]StorageFoundationService>();
return services;
}
4. Tests
tests/[Domain]/Services/Foundation/[Entity]StorageFoundationServiceTests.cs:
public class [Entity]StorageFoundationServiceTests
{
private readonly Mock<I[Entity]NoSqlBroker> _mockBroker = new();
private readonly [Entity]StorageFoundationService _service;
public [Entity]StorageFoundationServiceTests()
{
_service = new [Entity]StorageFoundationService(
_mockBroker.Object,
new NullLoggerFactory());
}
[Fact]
public async Task Create[Entity]Object_ValidInput_CreatesSuccessfully()
{
// Arrange
var entity = [Entity]Builder.CreateRandom[Entity]();
// Act
await _service.Create[Entity]Object(entity);
// Assert
_mockBroker.Verify(b => b.Create[Entity]Async(entity), Times.Once);
}
[Fact]
public async Task Create[Entity]Object_NullInput_ThrowsValidationException()
{
// Act & Assert
await Assert.ThrowsAsync<[Entity]ValidationException>(
() => _service.Create[Entity]Object(null!));
}
}
Checklist
- • Broker interface with XML docs
- • Broker implementation with
.ConfigureAwait(false) - • Foundation Service main file with TryCatch pattern
- • Foundation Service exceptions partial
- • Foundation Service validations partial
- • OpenTelemetry activity spans on all methods
- • XML documentation on all public APIs
- • DI registration in Extensions class
- • xUnit tests with 85%+ coverage
- •
dotnet buildpasses with no warnings - • Max 2-3 dependencies (Florance Pattern)