Skip to main content

csharp-patterns

C#/.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.

소스 정보

저장소
softspark/ai-toolkit
최근 소스 활동
2026년 5월 12일 13:02
감지된 SKILL.md 언어
영어
스타
181
포크
21

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
csharp-patterns
description
C#/.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.
effort
medium
user-invocable
false
allowed-tools
Read
# C# / .NET Patterns ## Project Structure ### Solution Layout ``` MyApp.sln Directory.Build.props # Shared build properties Directory.Packages.props # Central package management src/ MyApp.Api/ # ASP.NET Core host (Controllers, Middleware, Program.cs) MyApp.Application/ # Use cases, MediatR handlers, Behaviors MyApp.Domain/ # Entities, value objects, domain events MyApp.Infrastructure/ # EF Core, external services tests/ MyApp.UnitTests/ MyApp.IntegrationTests/ ``` ### Directory.Build.props ```xml <Project> <PropertyGroup> <TargetFramework>net9.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild> <AnalysisLevel>latest-recommended</AnalysisLevel> </PropertyGroup> </Project> ``` ### Central Package Management (Directory.Packages.props) ```xml <Project> <PropertyGroup> <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> </PropertyGroup> <ItemGroup> <PackageVersion Include="MediatR" Version="12.4.1" /> <PackageVersion Include="FluentValidation" Version="11.11.0" /> <PackageVersion Include="Microsoft.EntityFrameworkCore" Version="9.0.0" /> </ItemGroup> </Project> ``` --- ## Idioms / Code Style ### Nullable Reference Types ```csharp public class Order { public required string Id { get; init; } public string? Notes { get; set; } // explicitly nullable public required Customer Customer { get; init; } // never null public string Summary => $"Order {Id}: {Notes ?? "no notes"}"; } ``` ### Records ```csharp public record Money(decimal Amount, string Currency) { public Money Add(Money other) => Currency != other.Currency ? throw new InvalidOperationException("Currency mismatch") : this with { Amount = Amount + other.Amount }; } public record CreateOrderRequest(string CustomerId, List<OrderLineDto> Lines); public record OrderLineDto(string ProductId, int Quantity); ``` ### Pattern Matching ```csharp public decimal CalculateDiscount(Customer c) => c switch { { Tier: CustomerTier.Gold, TotalSpent: > 10_000m } => 0.20m, { Tier: CustomerTier.Gold } => 0.15m, { Tier: CustomerTier.Silver } => 0.10m, { IsNewCustomer: true } => 0.05m, _ => 0m }; // List patterns (.NET 8+) public string Describe(int[] v) => v switch { [] => "empty", [var x] => $"one: {x}", [var f, .., var l] => $"{f}..{l}", }; ``` ### LINQ ```csharp var activeUsers = users .Where(u => u.IsActive) .OrderByDescending(u => u.LastLogin) .Select(u => new UserDto(u.Id, u.Name)) .ToList(); ``` ### Async/Await ```csharp // Always: Async suffix, CancellationToken parameter, never .Result/.Wait() public async Task<Order?> GetOrderAsync(string id, CancellationToken ct = default) => await _db.Orders.Include(o => o.Lines).FirstOrDefaultAsync(o => o.Id == id, ct); // Parallel independent tasks var ordersTask = _orderRepo.GetRecentAsync(ct); var statsTask = _statsService.ComputeAsync(ct); await Task.WhenAll(ordersTask, statsTask); ``` ### Primary Constructors (C# 12) ```csharp public class OrderService(IOrderRepository repo, ILogger<OrderService> logger, IPublisher pub) { public async Task<Order> CreateAsync(CreateOrderRequest req, CancellationToken ct) { logger.LogInformation("Creating order for {CustomerId}", req.CustomerId); var order = Order.Create(req); await repo.AddAsync(order, ct); await pub.Publish(new OrderCreatedEvent(order.Id), ct); return order; } } ``` --- ## Error Handling ### Result Pattern ```csharp public sealed class Result<T> { public T? Value { get; } public Error? Error { get; } public bool IsSuccess => Error is null; private Result(T value) => Value = value; private Result(Error error) => Error = error; public static Result<T> Success(T value) => new(value); public static Result<T> Failure(Error error) => new(error); public TOut Match<TOut>(Func<T, TOut> ok, Func<Error, TOut> err) => IsSuccess ? ok(Value!) : err(Error!); } public record Error(string Code, string Message); ``` ### FluentValidation + MediatR Pipeline ```csharp public class CreateOrderValidator : AbstractValidator<CreateOrderRequest> { public CreateOrderValidator() { RuleFor(x => x.CustomerId).NotEmpty().MaximumLength(36); RuleForEach(x => x.Lines).ChildRules(line => { line.RuleFor(l => l.ProductId).NotEmpty(); line.RuleFor(l => l.Quantity).GreaterThan(0).LessThanOrEqualTo(1000); }); } } public class ValidationBehavior<TReq, TRes>(IEnumerable<IValidator<TReq>> validators) : IPipelineBehavior<TReq, TRes> where TReq : IRequest<TRes> { public async Task<TRes> Handle(TReq req, RequestHandlerDelegate<TRes> next, CancellationToken ct) { var failures = validators.Select(v => v.Validate(req)).SelectMany(r => r.Errors).ToList(); return failures.Count > 0 ? throw new ValidationException(failures) : await next(); } } ``` ### IAsyncDisposable ```csharp public sealed class TempFileHandle(string path) : IAsyncDisposable { public async ValueTask DisposeAsync() { if (File.Exists(path)) await Task.Run(() => File.Delete(path)); } } // Usage: await using var handle = new TempFileHandle("/tmp/export.csv"); ``` --- ## Testing Patterns ### xUnit + NSubstitute + FluentAssertions ```csharp public class OrderServiceTests { private readonly IOrderRepository _repo = Substitute.For<IOrderRepository>(); private readonly IPublisher _pub = Substitute.For<IPublisher>(); private readonly OrderService _sut; public OrderServiceTests() => _sut = new(_repo, Substitute.For<ILogger<OrderService>>(), _pub); [Fact] public async Task CreateAsync_ValidRequest_ReturnsOrder() { var order = await _sut.CreateAsync(new("cust-1", [new("prod-1", 2)]), CancellationToken.None); order.Should().NotBeNull(); order.Lines.Should().ContainSingle().Which.Quantity.Should().Be(2); await _repo.Received(1).AddAsync(Arg.Any<Order>(), Arg.Any<CancellationToken>()); } [Theory] [InlineData(0)] [InlineData(-1)] public async Task CreateAsync_InvalidQuantity_Throws(int qty) { var act = () => _sut.CreateAsync(new("cust-1", [new("prod-1", qty)]), CancellationToken.None); await act.Should().ThrowAsync<ValidationException>(); } } ``` ### WebApplicationFactory (Integration) ```csharp public class OrdersApiTests(WebApplicationFactory<Program> factory) : IClassFixture<WebApplicationFactory<Program>> { [Fact] public async Task PostOrder_Returns201() { var client = factory.WithWebHostBuilder(b => b.ConfigureServices(s => { s.RemoveAll<DbContext>(); s.AddDbContext<AppDbContext>(o => o.UseInMemoryDatabase("test")); })).CreateClient(); var response = await client.PostAsJsonAsync("/api/orders", new { CustomerId = "cust-1", Lines = new[] { new { ProductId = "prod-1", Quantity = 2 } } }); response.StatusCode.Should().Be(HttpStatusCode.Created); } } ``` ### Testcontainers ```csharp public class PostgresFixture : IAsyncLifetime { private readonly PostgreSqlContainer _pg = new PostgreSqlBuilder().WithDatabase("testdb").Build(); public string ConnectionString => _pg.GetConnectionString(); public Task InitializeAsync() => _pg.StartAsync(); public Task DisposeAsync() => _pg.DisposeAsync().AsTask(); } ``` --- ## Common Frameworks ### ASP.NET Core Minimal API ```csharp var builder = WebApplication.CreateBuilder(args); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly)); var app = builder.Build(); app.MapGet("/api/orders/{id}", async (string id, ISender sender, CancellationToken ct) => await sender.Send(new GetOrderQuery(id), ct) is { } order ? Results.Ok(order) : Results.NotFound()); app.MapPost("/api/orders", async (CreateOrderRequest req, ISender sender, CancellationToken ct) => (await sender.Send(new CreateOrderCommand(req), ct)).Match( o => Results.Created($"/api/orders/{o.Id}", o), e => Results.Problem(e.Message, statusCode: 400))); app.Run(); ``` ### Entity Framework Core ```csharp public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options) { public DbSet<Order> Orders => Set<Order>(); protected override void OnModelCreating(ModelBuilder mb) { mb.Entity<Order>(b => { b.HasKey(o => o.Id); b.Property(o => o.Id).HasMaxLength(36); b.HasMany(o => o.Lines).WithOne().HasForeignKey(l => l.OrderId); b.HasIndex(o => o.CustomerId); b.Property(o => o.Total).HasPrecision(18, 2); }); mb.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly); } } ``` ### MediatR (CQRS) ```csharp public record CreateOrderCommand(CreateOrderRequest Request) : IRequest<Result<Order>>; public class CreateOrderHandler(IOrderRepository repo, IPublisher pub) : IRequestHandler<CreateOrderCommand, Result<Order>> { public async Task<Result<Order>> Handle(CreateOrderCommand cmd, CancellationToken ct) { var order = Order.Create(cmd.Request); await repo.AddAsync(order, ct); await pub.Publish(new OrderCreatedEvent(order.Id), ct); return Result<Order>.Success(order); } } ``` ### Polly (Resilience) ```csharp builder.Services.AddHttpClient("Payments", c => c.BaseAddress = new Uri("https://api.payments.com")) .AddResilienceHandler("default", p => { p.AddRetry(new() { MaxRetryAttempts = 3, Delay = TimeSpan.FromMilliseconds(500), BackoffType = DelayBackoffType.Exponential, ShouldHandle = new PredicateBuilder<HttpResponseMessage>() .HandleResult(r => r.StatusCode >= HttpStatusCode.InternalServerError) }); p.AddCircuitBreaker(new() { FailureRatio = 0.5, SamplingDuration = TimeSpan.FromSeconds(30), BreakDuration = TimeSpan.FromSeconds(15) }); p.AddTimeout(TimeSpan.FromSeconds(5)); }); ``` ### MassTransit (Messaging) ```csharp builder.Services.AddMassTransit(x => { x.AddConsumer<OrderCreatedConsumer>(); x.UsingRabbitMq((ctx, cfg) => { cfg.Host("rabbitmq://localhost"); cfg.ConfigureEndpoints(ctx); }); }); ``` --- ## Performance Tips ### Span<T> -- Zero-Allocation Parsing ```csharp public static ReadOnlySpan<char> ExtractDomain(ReadOnlySpan<char> email) { int at = email.IndexOf('@'); return at >= 0 ? email[(at + 1)..] : ReadOnlySpan<char>.Empty; } // Stack-allocated buffer: Span<byte> buf = stackalloc byte[256]; ``` ### ValueTask -- Cache-Hit Fast Path ```csharp public ValueTask<Product?> GetProductAsync(string id, CancellationToken ct) {
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기