$npx -y skills add softspark/ai-toolkit --skill csharp-patternsC#/.NET: LINQ, async/await, DI, records, nullable refs, ASP.NET Core, EF Core, MediatR. Triggers: C#, .NET, dotnet, ASP.NET, EF Core, LINQ, record type, IServiceCollection.
| 1 | # C# / .NET Patterns |
| 2 | |
| 3 | ## Project Structure |
| 4 | |
| 5 | ### Solution Layout |
| 6 | ``` |
| 7 | MyApp.sln |
| 8 | Directory.Build.props # Shared build properties |
| 9 | Directory.Packages.props # Central package management |
| 10 | src/ |
| 11 | MyApp.Api/ # ASP.NET Core host (Controllers, Middleware, Program.cs) |
| 12 | MyApp.Application/ # Use cases, MediatR handlers, Behaviors |
| 13 | MyApp.Domain/ # Entities, value objects, domain events |
| 14 | MyApp.Infrastructure/ # EF Core, external services |
| 15 | tests/ |
| 16 | MyApp.UnitTests/ |
| 17 | MyApp.IntegrationTests/ |
| 18 | ``` |
| 19 | |
| 20 | ### Directory.Build.props |
| 21 | ```xml |
| 22 | <Project> |
| 23 | <PropertyGroup> |
| 24 | <TargetFramework>net9.0</TargetFramework> |
| 25 | <Nullable>enable</Nullable> |
| 26 | <ImplicitUsings>enable</ImplicitUsings> |
| 27 | <TreatWarningsAsErrors>true</TreatWarningsAsErrors> |
| 28 | <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild> |
| 29 | <AnalysisLevel>latest-recommended</AnalysisLevel> |
| 30 | </PropertyGroup> |
| 31 | </Project> |
| 32 | ``` |
| 33 | |
| 34 | ### Central Package Management (Directory.Packages.props) |
| 35 | ```xml |
| 36 | <Project> |
| 37 | <PropertyGroup> |
| 38 | <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> |
| 39 | </PropertyGroup> |
| 40 | <ItemGroup> |
| 41 | <PackageVersion Include="MediatR" Version="12.4.1" /> |
| 42 | <PackageVersion Include="FluentValidation" Version="11.11.0" /> |
| 43 | <PackageVersion Include="Microsoft.EntityFrameworkCore" Version="9.0.0" /> |
| 44 | </ItemGroup> |
| 45 | </Project> |
| 46 | ``` |
| 47 | |
| 48 | --- |
| 49 | |
| 50 | ## Idioms / Code Style |
| 51 | |
| 52 | ### Nullable Reference Types |
| 53 | ```csharp |
| 54 | public class Order |
| 55 | { |
| 56 | public required string Id { get; init; } |
| 57 | public string? Notes { get; set; } // explicitly nullable |
| 58 | public required Customer Customer { get; init; } // never null |
| 59 | public string Summary => $"Order {Id}: {Notes ?? "no notes"}"; |
| 60 | } |
| 61 | ``` |
| 62 | |
| 63 | ### Records |
| 64 | ```csharp |
| 65 | public record Money(decimal Amount, string Currency) |
| 66 | { |
| 67 | public Money Add(Money other) => Currency != other.Currency |
| 68 | ? throw new InvalidOperationException("Currency mismatch") |
| 69 | : this with { Amount = Amount + other.Amount }; |
| 70 | } |
| 71 | |
| 72 | public record CreateOrderRequest(string CustomerId, List<OrderLineDto> Lines); |
| 73 | public record OrderLineDto(string ProductId, int Quantity); |
| 74 | ``` |
| 75 | |
| 76 | ### Pattern Matching |
| 77 | ```csharp |
| 78 | public decimal CalculateDiscount(Customer c) => c switch |
| 79 | { |
| 80 | { Tier: CustomerTier.Gold, TotalSpent: > 10_000m } => 0.20m, |
| 81 | { Tier: CustomerTier.Gold } => 0.15m, |
| 82 | { Tier: CustomerTier.Silver } => 0.10m, |
| 83 | { IsNewCustomer: true } => 0.05m, |
| 84 | _ => 0m |
| 85 | }; |
| 86 | |
| 87 | // List patterns (.NET 8+) |
| 88 | public string Describe(int[] v) => v switch |
| 89 | { |
| 90 | [] => "empty", [var x] => $"one: {x}", [var f, .., var l] => $"{f}..{l}", |
| 91 | }; |
| 92 | ``` |
| 93 | |
| 94 | ### LINQ |
| 95 | ```csharp |
| 96 | var activeUsers = users |
| 97 | .Where(u => u.IsActive) |
| 98 | .OrderByDescending(u => u.LastLogin) |
| 99 | .Select(u => new UserDto(u.Id, u.Name)) |
| 100 | .ToList(); |
| 101 | ``` |
| 102 | |
| 103 | ### Async/Await |
| 104 | ```csharp |
| 105 | // Always: Async suffix, CancellationToken parameter, never .Result/.Wait() |
| 106 | public async Task<Order?> GetOrderAsync(string id, CancellationToken ct = default) |
| 107 | => await _db.Orders.Include(o => o.Lines).FirstOrDefaultAsync(o => o.Id == id, ct); |
| 108 | |
| 109 | // Parallel independent tasks |
| 110 | var ordersTask = _orderRepo.GetRecentAsync(ct); |
| 111 | var statsTask = _statsService.ComputeAsync(ct); |
| 112 | await Task.WhenAll(ordersTask, statsTask); |
| 113 | ``` |
| 114 | |
| 115 | ### Primary Constructors (C# 12) |
| 116 | ```csharp |
| 117 | public class OrderService(IOrderRepository repo, ILogger<OrderService> logger, IPublisher pub) |
| 118 | { |
| 119 | public async Task<Order> CreateAsync(CreateOrderRequest req, CancellationToken ct) |
| 120 | { |
| 121 | logger.LogInformation("Creating order for {CustomerId}", req.CustomerId); |
| 122 | var order = Order.Create(req); |
| 123 | await repo.AddAsync(order, ct); |
| 124 | await pub.Publish(new OrderCreatedEvent(order.Id), ct); |
| 125 | return order; |
| 126 | } |
| 127 | } |
| 128 | ``` |
| 129 | |
| 130 | --- |
| 131 | |
| 132 | ## Error Handling |
| 133 | |
| 134 | ### Result Pattern |
| 135 | ```csharp |
| 136 | public sealed class Result<T> |
| 137 | { |
| 138 | public T? Value { get; } |
| 139 | public Error? Error { get; } |
| 140 | public bool IsSuccess => Error is null; |
| 141 | private Result(T value) => Value = value; |
| 142 | private Result(Error error) => Error = error; |
| 143 | public static Result<T> Success(T value) => new(value); |
| 144 | public static Result<T> Failure(Error error) => new(error); |
| 145 | public TOut Match<TOut>(Func<T, TOut> ok, Func<Error, TOut> err) => |
| 146 | IsSuccess ? ok(Value!) : err(Error!); |
| 147 | } |
| 148 | public record Error(string Code, string Message); |
| 149 | ``` |
| 150 | |
| 151 | ### FluentValidation + MediatR Pipeline |
| 152 | ```csharp |
| 153 | public class CreateOrderValidator : AbstractValidator<CreateOrderRequest> |
| 154 | { |
| 155 | public CreateOrderValidator() |
| 156 | { |
| 157 | RuleFor(x => x.CustomerId).NotEmpty().MaximumLength(36); |
| 158 | RuleForEach(x => x.Lines).ChildRules(line => |