Skip to content
backend-csharp-patterns logo

backend-csharp-patterns

Use when editing C# backend files (.cs) in src/Backend/, src/Platform/, or src/PlatformExampleApp/. Provides CQRS patterns, repository patterns, validation patterns, entity patterns, background jobs, message bus consumers, data migrations, and fluent helpers for EasyPlatform .NET 9 development.

SKILL.md

Full skill instructions

Backend C# Code Patterns

When implementing backend C# code in EasyPlatform, follow these patterns exactly.

Full Pattern Reference

See the complete code patterns with examples: backend-code-patterns.md

Quick Reference

Pattern Index

#PatternKey Interface/​Contract
1Clean ArchitectureDomain → Application → Persistence → Api layers
2RepositoryIPlatformQueryableRootRepository<TEntity, TKey> + static expression extensions
3Repository APICreateAsync, GetByIdAsync, GetAllAsync, FirstOrDefaultAsync, CountAsync
4ValidationPlatformValidationResult.And().AndAsync() fluent chain, never throw
5Cross-ServicePlatformCqrsEntityEventBusMessageProducer + PlatformApplicationMessageBusConsumer
6Full-Text SearchsearchService.Search(q, text, Entity.SearchColumns()) in query builder
7CQRS CommandCommand + Result + Handler in ONE file, PlatformCqrsCommandApplicationHandler
8QueryPlatformCqrsPagedQuery + GetQueryBuilder() + parallel count/​items
9Side EffectsEntity Event Handlers in UseCaseEvents/, never in command handlers
10EntityRootEntity<T, TKey>, static expressions, [TrackFieldUpdatedDomainEvent], navigation properties
11DTOPlatformEntityDto<T, TKey>.MapToEntity(), DTO owns mapping, constructor from entity
12Fluent Helpers.With(), .Then(), .EnsureFound(), .EnsureValid(), .ParallelAsync()
13Background JobsPlatformApplicationPagedBackgroundJobExecutor, [PlatformRecurringJob("cron")]
14Message BusPlatformApplicationMessageBusConsumer<TMessage>, TryWaitUntilAsync() for deps
15Data MigrationPlatformDataMigrationExecutor<TDbContext>, OnlyForDbsCreatedBeforeDate
16Multi-DatabasePlatformEfCorePersistenceModule / PlatformMongoDbPersistenceModule

Critical Rules

  1. Repository: Use IPlatformQueryableRootRepository<TEntity, TKey> - NEVER generic IPlatformRootRepository
  2. Validation: Use PlatformValidationResult fluent API (.And(), .AndAsync()) - NEVER throw ValidationException
  3. Side Effects: Handle in Entity Event Handlers (UseCaseEvents/) - NEVER in command handlers
  4. DTO Mapping: DTOs own mapping via MapToEntity() or MapToObject() - NEVER map in handlers
  5. Command Structure: Command + Result + Handler in ONE file under UseCaseCommands/​{Feature}/
  6. Cross-Service: Use RabbitMQ message bus - NEVER direct database access

Anti-Patterns

// ❌ Direct cross-service DB access → ✅ Use message bus
// ❌ Custom repository interface → ✅ Use platform repo + extensions
// ❌ Manual validation throw → ✅ Use PlatformValidationResult fluent API
// ❌ Side effects in handler → ✅ Use entity event handlers
// ❌ DTO mapping in handler → ✅ DTO owns mapping via MapToObject()/​MapToEntity()

Templates

CQRS Command Template

public sealed class Save{Entity}Command : PlatformCqrsCommand<Save{Entity}CommandResult>
{
    public string Id { get; set; } = "";
    public string Name { get; set; } = "";

    public override PlatformValidationResult<IPlatformCqrsRequest> Validate()
        => base.Validate().And(_ => Name.IsNotNullOrEmpty(), "Name required");
}

public sealed class Save{Entity}CommandResult : PlatformCqrsCommandResult
{
    public {Entity}Dto Entity { get; set; } = null!;
}

internal sealed class Save{Entity}CommandHandler : PlatformCqrsCommandApplicationHandler<Save{Entity}Command, Save{Entity}CommandResult>
{
    protected override async Task<Save{Entity}CommandResult> HandleAsync(Save{Entity}Command req, CancellationToken ct)
    {
        var entity = req.Id.IsNullOrEmpty()
            ? req.MapToNewEntity().With(e => e.CreatedBy = RequestContext.UserId())
            : await repo.GetByIdAsync(req.Id, ct).Then(e => req.UpdateEntity(e));
        await entity.ValidateAsync(repo, ct).EnsureValidAsync();
        var saved = await repo.CreateOrUpdateAsync(entity, ct);
        return new Save{Entity}CommandResult { Entity = new {Entity}Dto(saved) };
    }
}

Detailed Instructions

For task-specific guidance, also reference: