.claude/skills/design-patterns-standards/SKILL.md
Use when implementing or reviewing GoF design patterns (Factory, Strategy, Decorator, Observer, etc.) in .NET — provides canonical C# implementations, when-to-use, and when-NOT-to-use guidance.
npx skillsauth add klod68/littlerae design-patterns-standardsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
| Field | Value |
|---|---|
| Name | Design Patterns — Approved Pattern Registry |
| Domain | Design Patterns, GoF, Architecture |
| Level | Feature |
| Tags | patterns, factory, strategy, decorator, observer, adapter, builder |
Activate this skill when the task involves:
These rules layer on top of base architectural standards. On conflict, these win.
<!-- SHARED:rules/design-patterns.md -->Approved patterns for .NET projects. Agents MUST NOT introduce a pattern absent from this file without creating an ADR. Each entry defines: intent, canonical .NET/C# form, when to use, and when NOT to use.
Intent: Define an interface for creating an object, but let subclasses (or the domain itself) decide which class to instantiate. Use in: Domain entities — the only way to create a valid entity with invariants enforced.
// Domain/Entities/{Entity}.cs
public sealed class {Entity} : AggregateRoot
{
public {EntityId} Id { get; private set; }
public string Title { get; private set; }
public {EntityStatus} Status { get; private set; }
private {Entity}() { } // EF Core constructor — private
// Factory method: the ONLY valid entry point for creation
public static Result<{Entity}> Create(string title)
{
if (string.IsNullOrWhiteSpace(title))
return Result.Failure<{Entity}>(
{Entity}Errors.TitleRequired);
if (title.Length > 200)
return Result.Failure<{Entity}>(
{Entity}Errors.TitleTooLong);
return Result.Success(new {Entity}
{
Id = {EntityId}.New(),
Title = title.Trim(),
Status = {EntityStatus}.Draft
});
}
}
Do NOT use constructors with public access on aggregates. Force callers through the factory.
Intent: Separate the construction of a complex object from its representation. Use in: Complex test data setup (test fixtures), complex DTOs, report configuration objects.
// tests/Unit/Builders/{Entity}Builder.cs — test data builder
public sealed class {Entity}Builder
{
private string _title = "Default Title";
private {EntityStatus} _status = {EntityStatus}.Draft;
public {Entity}Builder WithTitle(string title)
{ _title = title; return this; }
public {Entity}Builder WithStatus({EntityStatus} status)
{ _status = status; return this; }
public {Entity} Build()
{
var result = {Entity}.Create(_title);
// apply _status via domain method if needed
return result.Value;
}
}
Do NOT use in production domain code — use Factory Methods there.
Intent: Encapsulate the logic required to access data sources behind a collection-like interface, decoupling the domain model from data access.
// Application/Interfaces/Repositories/I{Entity}Repository.cs
public interface I{Entity}Repository
{
Task<{Entity}?> GetByIdAsync({EntityId} id, CancellationToken ct);
Task<IReadOnlyList<{Entity}>> GetByStatusAsync(
{EntityStatus} status, CancellationToken ct);
Task AddAsync({Entity} entity, CancellationToken ct);
Task UpdateAsync({Entity} entity, CancellationToken ct);
}
// Infrastructure/Persistence/Repositories/EfCore{Entity}Repository.cs
public sealed class EfCore{Entity}Repository : I{Entity}Repository
{
private readonly {DbContextName} _db;
public EfCore{Entity}Repository({DbContextName} db) => _db = db;
public async Task<{Entity}?> GetByIdAsync(
{EntityId} id, CancellationToken ct)
=> await _db.{Entities}
.AsNoTracking()
.FirstOrDefaultAsync(e => e.Id == id, ct);
public async Task AddAsync({Entity} entity, CancellationToken ct)
=> await _db.AddAsync(entity, ct);
// SaveChangesAsync is called by the handler, NOT here
public Task UpdateAsync({Entity} entity, CancellationToken ct)
{ _db.Update(entity); return Task.CompletedTask; }
}
Rules:
SaveChangesAsync inside a repository method.AsNoTracking() for read operations.Intent: Convert the interface of a class into another interface clients expect. Use in: Wrapping external SDKs, third-party APIs, or legacy services into Application-defined interfaces.
// Application/Interfaces/Services/IExportService.cs
public interface IExportService
{
Task<byte[]> ExportAsync({Entity} entity, CancellationToken ct);
}
// Infrastructure/Services/{Technology}ExportAdapter.cs
// Adapts a third-party library to our IExportService contract
public sealed class {Technology}ExportAdapter : IExportService
{
public Task<byte[]> ExportAsync({Entity} entity, CancellationToken ct)
{
// Translate our domain model into the library's model
// Return raw bytes — caller decides what to do with them
}
}
Intent: Attach additional responsibilities to an object dynamically. Use in: Cross-cutting concerns layered onto interfaces (caching, logging, retry) without modifying the underlying implementation.
// Infrastructure/Persistence/Repositories/Cached{Entity}Repository.cs
public sealed class Cached{Entity}Repository : I{Entity}Repository
{
private readonly I{Entity}Repository _inner;
private readonly IMemoryCache _cache;
public Cached{Entity}Repository(
I{Entity}Repository inner, IMemoryCache cache)
{
_inner = inner;
_cache = cache;
}
public async Task<{Entity}?> GetByIdAsync(
{EntityId} id, CancellationToken ct)
{
var key = $"{entity-type}:{id}";
if (_cache.TryGetValue(key, out {Entity}? cached))
return cached;
var entity = await _inner.GetByIdAsync(id, ct);
if (entity is not null)
_cache.Set(key, entity, TimeSpan.FromMinutes(5));
return entity;
}
// Delegate write methods to inner
public Task AddAsync({Entity} entity, CancellationToken ct)
=> _inner.AddAsync(entity, ct);
public Task UpdateAsync({Entity} entity, CancellationToken ct)
=> _inner.UpdateAsync(entity, ct);
}
Register in DI:
services.AddScoped<EfCore{Entity}Repository>();
services.AddScoped<I{Entity}Repository>(sp =>
new Cached{Entity}Repository(
sp.GetRequiredService<EfCore{Entity}Repository>(),
sp.GetRequiredService<IMemoryCache>()));
Intent: Process a request through an ordered sequence of independent, composable processing stages. Each stage does one thing.
Pipeline order (enforced):
Request → [1. Logging] → [2. Validation] → [3. Transaction] → Handler → Response
// Application/Behaviors/LoggingBehavior.cs (order: 1)
public sealed class LoggingBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;
public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResponse>> logger)
=> _logger = logger;
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken ct)
{
_logger.LogInformation("Handling {Request}", typeof(TRequest).Name);
var response = await next();
_logger.LogInformation("Handled {Request}", typeof(TRequest).Name);
return response;
}
}
// Application/Behaviors/ValidationBehavior.cs (order: 2)
public sealed class ValidationBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
=> _validators = validators;
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken ct)
{
if (!_validators.Any()) return await next();
var context = new ValidationContext<TRequest>(request);
var failures = _validators
.Select(v => v.Validate(context))
.SelectMany(r => r.Errors)
.Where(f => f is not null)
.ToList();
if (failures.Count != 0)
throw new ValidationException(failures);
return await next();
}
}
DI registration order matters:
services.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(TransactionBehavior<,>));
});
Intent: Define a family of algorithms, encapsulate each one, and make them interchangeable. Let the algorithm vary independently from the clients that use it. Use in: Export formats, rule evaluation engines, report generation variants.
public interface I{Rule}Strategy
{
string RuleCode { get; }
RuleEvaluationResult Evaluate({WorkItem} workItem);
}
public sealed class {Specific}RuleStrategy : I{Rule}Strategy
{
public string RuleCode => "{RULE-CODE}";
public RuleEvaluationResult Evaluate({WorkItem} workItem)
{
if (string.IsNullOrWhiteSpace(workItem.{RequiredField}))
return RuleEvaluationResult.Fail(RuleCode,
"Work item has no {required field} defined.");
return RuleEvaluationResult.Pass(RuleCode);
}
}
// Resolved via DI: IEnumerable<I{Rule}Strategy>
// No switch statement needed — ever.
Intent: Encapsulate a business rule that can be combined with other rules using Boolean operators. Use for query predicates and domain validation.
// Domain/Specifications/Specification.cs
public abstract class Specification<T>
{
public abstract Expression<Func<T, bool>> ToExpression();
public bool IsSatisfiedBy(T entity)
=> ToExpression().Compile()(entity);
public Specification<T> And(Specification<T> other)
=> new AndSpecification<T>(this, other);
}
// Domain/Specifications/Active{Entity}Specification.cs
public sealed class Active{Entity}Specification : Specification<{Entity}>
{
public override Expression<Func<{Entity}, bool>> ToExpression()
=> e => e.Status != {EntityStatus}.Archived
&& e.Status != {EntityStatus}.Cancelled;
}
// Usage in repository:
var spec = new Active{Entity}Specification();
var results = await _db.{Entities}
.Where(spec.ToExpression())
.AsNoTracking()
.ToListAsync(ct);
Intent: Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified automatically.
// Domain/Events/{Entity}CreatedEvent.cs
public sealed record {Entity}CreatedEvent({EntityId} EntityId) : INotification;
// Raised inside the aggregate after state change:
public sealed class {Entity} : AggregateRoot
{
private readonly List<INotification> _domainEvents = [];
public IReadOnlyList<INotification> DomainEvents => _domainEvents;
public static Result<{Entity}> Create(string title)
{
var entity = new {Entity} { ... };
entity._domainEvents.Add(new {Entity}CreatedEvent(entity.Id));
return Result.Success(entity);
}
}
// Application/Handlers/{Entity}CreatedEventHandler.cs
public sealed class {Entity}CreatedEventHandler
: INotificationHandler<{Entity}CreatedEvent>
{
public Task Handle({Entity}CreatedEvent notification, CancellationToken ct)
{
// Send notification, update read model, etc.
return Task.CompletedTask;
}
}
Dispatch domain events after SaveChangesAsync in the handler or a TransactionBehavior.
| Anti-Pattern | Why Forbidden | Alternative |
|---|---|---|
| Service Locator | Hides dependencies, makes testing impossible | Constructor injection |
| Anemic Domain Model | Entities are data bags; logic scattered in services | Rich domain entities with behavior |
| God Class | Violates SRP; impossible to test | Decompose into focused classes |
| Shared Mutable State | Race conditions, unpredictable behavior | Immutable value objects, event sourcing |
| Magic Strings | Brittle, not refactorable | Named constants, strongly-typed IDs |
| static business methods | Untestable, violates DIP | Instance methods on injected interfaces |
| Introducing an unlisted pattern | No ADR approval trail | Create ADR before introducing new pattern |
cqrs.md — CQRS via MediatR, command/query separation, pipeline behaviorsapi-contracts.md — Interface-first design, return type abstractions, contract versioningef-core.md — EF Core entity design, migrations, DbContext, querying, repository patternnaming.md — Naming conventions for types, methods, properties, namespacestools
Use when cross-cutting concerns (logging, metrics, validation, authorization) are tangled into command handlers or service methods, when building database command pipelines with reorderable concerns, or when HTTP client pipelines or message handlers need composable, independently-replaceable processing stages. Covers ICommandInterceptor interface, InterceptorPipeline with reverse-chain construction, zero-cost Empty sentinel to skip overhead when no interceptors are registered, and ConfigureAwait(false) discipline for library code. Domain: Architecture, Cross-Cutting Concerns. Level: Intermediate. Tags: interceptor, pipeline, middleware, decorator, cross-cutting-concerns.
development
Use when writing integration tests for Razor Pages, MVC, or Minimal API applications to validate routing, middleware, page rendering, and HTTP behavior without a browser or live server, or when adding fast smoke tests to a CI pipeline. Covers WebApplicationFactory<Program> setup with public partial class Program, in-memory test server, AngleSharp HTML parsing, CSS selector assertions, redirect and status code testing, and a shared static fixture pattern for minimal per-test startup overhead. Domain: Testing, ASP.NET Core. Level: Intermediate. Tags: integration-testing, webapplicationfactory, razor-pages, anglesharp, http-testing.
development
Use when designing indexes for new tables, diagnosing slow queries that are not using indexes efficiently, reviewing index fragmentation and maintenance, or when the current indexing strategy results in key lookups, table scans, or missing index warnings. Covers clustered index key selection (narrow, unique, ever-increasing), non-clustered index design for query patterns, covering indexes with INCLUDE columns, filtered indexes for subset queries, composite index column ordering, DMV-based monitoring for missing and unused indexes, and rebuild vs reorganize maintenance thresholds. Domain: Database, Performance. Level: Intermediate. Tags: index, sql-server, covering-index, filtered-index, performance, dmv, maintenance.
development
Use when building a searchable in-memory catalog or registry for documentation sites, admin panels, or type/API browsers where you need keyword matching, fuzzy search, and ranked results without an external search engine or database. Covers RegistryService with weighted scoring across name, description, keywords, and method names; Levenshtein fuzzy matching; synonym expansion; category and subcategory filtering; and singleton DI registration for datasets of hundreds to low thousands of items. Domain: Search, Data Access Patterns. Level: Intermediate. Tags: search, registry, fuzzy-matching, in-memory, catalog, filtering.