brainstorming
brainstorming
You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or planning anything new, in software or out of it (a talk, a business, a renovation).
dotnet-architectureUse this skill when structuring .NET applications — clean architecture, vertical slices, CQRS, MediatR, dependency injection, and project organization. This skill enforces: solution structure conventions, dependency flow direction, DI registration patterns, and architecture testing. Requires .NET SDK (dotnet new). Do NOT use for: Go, Node.js, Java, or non-.…
Independent GitHub snapshot · Automated classification · Original documentation governs
Structure .NET applications with clean architecture, vertical slices, or CQRS — dependency inversion, MediatR pipelines, DI registration, and solution organization.
User request includes: dotnet clean architecture, dotnet vertical slice, dotnet CQRS, dotnet solution structure, .NET project layout, dotnet DI, dotnet MediatR.
Solution structure, project references, DI registration, MediatR pipeline, EF Core setup.
Produce artifact directly. No preamble, no postamble, no explanations. No filler, no hedging, no transitions.
4096 tokens
| Criterion | Clean Architecture | Vertical Slices | N-tier |
|---|---|---|---|
| Team size | 5+ (many devs on same codebase) | 2-5 (feature teams) | 1-3 (simple apps) |
| Domain complexity | High (many business rules) | Medium (CRUD-heavy) | Low (simple CRUD) |
| Change frequency | Core domain changes rarely | Features change independently | Everything changes together |
| Testing strategy | Unit test domain, integration infra | Feature-level integration tests | End-to-end, horizontal |
| Learning curve | Steep (many abstractions) | Moderate (vertical boundaries) | Low (traditional layers) |
Decision: Complex domain with many rules → Clean Architecture. CRUD-heavy medium app → Vertical Slices. Simple API with few rules → N-tier.
| Aspect | Full CQRS (separate models) | Simple CQRS (same model, separate methods) |
|---|---|---|
| Read/write models | Separate DbContext, tables | Same DbContext, different queries |
| Complexity | High (eventual consistency) | Low (immediate consistency) |
| Performance | Optimized for each workload | Compromise |
| When to use | High read/write asymmetry | Simple CRUD with queries |
Decision: Reports/analytics workload separate from transactional → Full CQRS. Simple list/detail views → Simple CQRS.
src/
Domain/
Entities/
ValueObjects/
Aggregates/
DomainEvents/
Exceptions/
Interfaces/
Application/
Common/
Interfaces/
Behaviors/ // MediatR pipelines
Mappings/
Features/
Users/
Commands/
Queries/
DTOs/
Validators/
DependencyInjection.cs
Infrastructure/
Persistence/
Context/
Configurations/
Repositories/
Services/
DependencyInjection.cs
Presentation/
Controllers/
Middleware/
Program.cs
tests/
Domain.Tests/
Application.Tests/
Integration.Tests/
Architecture.Tests/
// Domain/Entities/Order.cs
public class Order : IAggregateRoot
{
public Guid Id { get; private set; }
public string OrderNumber { get; private set; }
public OrderStatus Status { get; private set; }
private readonly List<OrderItem> _items = new();
public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();
private Order() { } // EF Core
public Order(string orderNumber)
{
Id = Guid.NewGuid();
OrderNumber = orderNumber;
Status = OrderStatus.Pending;
AddDomainEvent(new OrderCreatedDomainEvent(Id));
}
public void AddItem(string product, decimal price, int quantity)
{
if (Status != OrderStatus.Pending)
throw new DomainException("Cannot modify confirmed order");
_items.Add(new OrderItem(product, price, quantity));
}
public void Confirm()
{
if (_items.Count == 0)
throw new DomainException("Cannot confirm empty order");
Status = OrderStatus.Confirmed;
AddDomainEvent(new OrderConfirmedDomainEvent(Id));
}
}
// Domain/ValueObjects/Money.cs
public record Money
{
public decimal Amount { get; init; }
public string Currency { get; init; }
public Money(decimal amount, string currency)
{
if (amount < 0) throw new DomainException("Amount cannot be negative");
if (string.IsNullOrWhiteSpace(currency)) throw new DomainException("Currency required");
Amount = amount;
Currency = currency.ToUpperInvariant();
}
}
// Application/Features/Orders/Commands/CreateOrder/CreateOrderCommand.cs
public record CreateOrderCommand(string OrderNumber, List<OrderItemDto> Items) : IRequest<OrderDto>;
// Application/Features/Orders/Commands/CreateOrder/CreateOrderCommandHandler.cs
public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, OrderDto>
{
private readonly IOrderRepository _repository;
private readonly IMapper _mapper;
public CreateOrderCommandHandler(IOrderRepository repository, IMapper mapper)
{
_repository = repository;
_mapper = mapper;
}
public async Task<OrderDto> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
{
var order = new Order(request.OrderNumber);
request.Items.ForEach(i => order.AddItem(i.Product, i.Price, i.Quantity));
await _repository.AddAsync(order);
await _repository.SaveChangesAsync(cancellationToken);
return _mapper.Map<OrderDto>(order);
}
}
// Application/Common/Behaviors/ValidationBehavior.cs
public 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 != null)
.ToList();
if (failures.Count != 0)
throw new ValidationException(failures);
return await next();
}
}
// Application/Common/Behaviors/LoggingBehavior.cs
public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct)
{
var requestName = typeof(TRequest).Name;
_logger.LogInformation("Processing {Request}", requestName);
var stopwatch = Stopwatch.StartNew();
var response = await next();
stopwatch.Stop();
_logger.LogInformation("Completed {Request} in {Elapsed}ms", requestName, stopwatch.ElapsedMilliseconds);
return response;
}
}
// Application/DependencyInjection.cs
public static class DependencyInjection
{
public static IServiceCollection AddApplication(this IServiceCollection services)
{
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()));
services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
services.AddAutoMapper(Assembly.GetExecutingAssembly());
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
return services;
}
}
// Infrastructure/DependencyInjection.cs
public static class DependencyInjection
{
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration config)
{
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(config.GetConnectionString("Default")));
services.AddScoped<IOrderRepository, OrderRepository>();
services.AddScoped<IUnitOfWork>(sp => sp.GetRequiredService<AppDbContext>());
return services;
}
}
// Presentation/Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddApplication();
builder.Services.AddInfrastructure(builder.Configuration);
builder.Services.AddControllers();
// Infrastructure/Persistence/Configurations/OrderConfiguration.cs
public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
public void Configure(EntityTypeBuilder<Order> builder)
{
builder.ToTable("Orders");
builder.HasKey(o => o.Id);
builder.Property(o => o.OrderNumber).IsRequired().HasMaxLength(50);
builder.Property(o => o.Status).HasConversion<string>().HasMaxLength(20);
builder.OwnsMany(o => o.Items, item =>
{
item.WithOwner().HasForeignKey("OrderId");
item.Property(i => i.Product).IsRequired().HasMaxLength(100);
item.Property(i => i.Price).HasColumnType("decimal(18,2)");
});
builder.Ignore(o => o.DomainEvents);
}
}
// tests/Architecture.Tests/ArchitectureTests.cs
public class ArchitectureTests
{
[Fact]
public void Domain_ShouldNotDependOnInfrastructure()
{
var domainAssembly = typeof(Order).Assembly;
var infrastructureAssembly = typeof(OrderRepository).Assembly;
var result = domainAssembly.GetReferencedAssemblies()
.Any(a => a.FullName == infrastructureAssembly.FullName);
Assert.False(result);
}
[Fact]
public void Application_ShouldNotDependOnInfrastructure()
{
var appAssembly = typeof(CreateOrderCommand).Assembly;
var infraAssembly = typeof(OrderRepository).Assembly;
var result = appAssembly.GetReferencedAssemblies()
.Any(a => a.FullName == infraAssembly.FullName);
Assert.False(result);
}
}
AsNoTracking() for read-only queriesAddRange() not individual Add()ExecuteUpdate/ExecuteDelete for bulk operations (EF Core 7+)UseLoggerFactory only in developmentEnableRetryOnFailure() for transient faultsEF.CompileQuery()services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
options.ApiVersionReader = new UrlSegmentApiVersionReader();
});
builder.Services.AddOpenTelemetry()
.WithTracing(t => t
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddEntityFrameworkCoreInstrumentation()
.AddOtlpExporter());
| Anti-Pattern | Why | Fix |
|---|---|---|
| Domain depending on EF Core | Couples business rules to ORM | Keep Domain clean — use interfaces |
Task.Run() in ASP.NET Core | Steals thread pool threads | Use async I/O natively |
| Fat controllers | Business logic in UI layer | Thin controller → MediatR handler |
DbContext as singleton | Concurrency issues, stale data | Scoped per request |
| AutoMapper in Domain | Domain shouldn't know about mapping | Mapping only in Application layer |
[ApiController] without validation attribute | Model state may be invalid | Always add [ApiController] for auto-400 |
[Authorize] at controller or endpoint level, never globally skipAspNetCoreRateLimit or YARP[LogProperties] selectively[Fact]
public void Order_WithNoItems_CannotBeConfirmed()
{
var order = new Order("ORD-001");
Assert.Throws<DomainException>(() => order.Confirm());
}
public class OrderRepositoryTests : IClassFixture<TestContainerFixture>
{
[Fact]
public async Task AddOrder_PersistsToDatabase()
{
var order = new Order("ORD-001");
order.AddItem("Widget", 10m, 2);
await _repository.AddAsync(order);
await _repository.SaveChangesAsync();
var saved = await _context.Orders.FindAsync(order.Id);
Assert.NotNull(saved);
Assert.Equal("ORD-001", saved.OrderNumber);
}
}
Use WebApplicationFactory<T> for integration tests. Use Testcontainers for SQL Server/PostgreSQL. Use Verify for snapshot testing.
Hand off to backend/dotnet/patterns/SKILL.md for MediatR pipelines, result pattern, and EF Core patterns.
function createModule<T>(config: ModuleConfig): T { const dependencies = initializeDependencies(config); const module = new Module(dependencies); module.hooks.onInit(); return module as T; }
class ConfigBuilder { private config: AppConfig = new AppConfig(); withDatabase(url: string): ConfigBuilder { ... } withCache(ttl: number): ConfigBuilder { ... } withLogging(level: string): ConfigBuilder { ... } build(): AppConfig { return this.config; } }
| Metric | Threshold | Severity | Action |
|---|---|---|---|
| Error rate | > 1% | Critical | Rollback or fix |
| p95 latency | > 500ms | Warning | Profile and optimize |
| Uptime | < 99.9% | Critical | Investigate infrastructure |
| Memory usage | > 80% | Warning | Check for leaks |
| CPU usage | > 80% | Warning | Scale up or optimize |
public class GetOrderQuery : IRequest<OrderDto>
{
public Guid OrderId { get; init; }
}
public class GetOrderHandler : IRequestHandler<GetOrderQuery, OrderDto>
{
private readonly IOrderRepository _repository;
private readonly IMapper _mapper;
public GetOrderHandler(IOrderRepository repository, IMapper mapper)
{
_repository = repository;
_mapper = mapper;
}
public async Task<OrderDto> Handle(GetOrderQuery request, CancellationToken ct)
{
var order = await _repository.GetByIdAsync(request.OrderId, ct);
return _mapper.Map<OrderDto>(order);
}
}
public 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 != null)
.ToList();
if (failures.Count != 0)
throw new ValidationException(failures);
return await next();
}
}
Directory.Packages.props.ReadyToRun images. Assembly trimming for self-contained deployments.# Gen 0/1/2 collections.Task.Run only for CPU-bound work.AddHealthChecks with liveness (light) and readiness (deep) probes.| Anti-Pattern | Why It Hurts | Fix |
|---|---|---|
| Async void | Crash on exception. Uncatchable. | Always return Task or Task<T>. |
| Blocking in async code | Thread pool starvation. Deadlocks. | Use await all the way. No .Result or .Wait(). |
| Giant startup class | SRP violation. Hard to test. | Modular startup with StartupFilter or IHostingStartup. |
| Stringly-typed configuration | No compile-time safety. | Strongly-typed options with IOptions<T>. |
| Ignoring cancellation tokens | Hung requests on shutdown. | Pass CancellationToken to all async methods. |
Catching Exception broadly | Swallows critical failures. | Catch specific exceptions. Re-throw if not recoverable. |
AsNoTracking() for read-only queries. Skip EF change tracker overhead.EF.Functions.Like for dynamic filtering.ValueTask for synchronous completion paths. Avoid Task allocation.System.Buffers.ArrayPool<T>) for buffer-heavy operations.Lazy<T> and LazyInitializer.ConcurrentDictionary over locks for read-heavy caches.IAsyncEnumerable<T> for streaming large result sets.IDataProtector for encrypting sensitive data. Key management with Azure Key Vault.[Authorize(Policy = "RequireAdmin")].[AutoValidateAntiforgeryToken] on all POST endpoints.[FromBody] with [Required] attributes. FluentValidation for complex rules.Cache hierarchy: L1 (in-memory local) → L2 (distributed Redis/Memcached) → L3 (CDN/Edge). Cache invalidation: TTL-based (simple, stale), event-based (complex, fresh), write-through (consistent, higher write latency), write-behind (fast writes, eventual consistency).