| name | csharp |
| description | C#, .NET, ASP.NET Core. Use when working on csharp tasks, related files, debugging, implementation, review, or verification workflows. |
Skill: C# / .NET
Loaded on-demand when working with .cs, .csproj, .sln files
Auto-Detect
Trigger this skill when:
- File extensions:
.cs, .csproj, .sln, .razor
- Project files contain:
<Project Sdk="Microsoft.NET.Sdk">
- Imports from:
Microsoft.AspNetCore, Microsoft.Extensions, System.Linq
- Tools:
dotnet CLI, NuGet, Entity Framework Core
Decision Tree: Project Type
What are you building?
├── Web API?
│ ├── Simple CRUD? → Minimal APIs (no controllers)
│ ├── Complex domain? → Controllers + MediatR/Vertical Slices
│ └── Microservices? → .NET Aspire orchestration
├── Background processing?
│ ├── Simple timer? → BackgroundService / IHostedService
│ ├── Queue-based? → Worker Service + message broker
│ └── Complex workflows? → Durable Functions / Temporal
├── Desktop/UI?
│ ├── Cross-platform? → MAUI / Avalonia
│ └── Windows only? → WPF / WinUI 3
└── Library/Package?
└── Class library + source generators for boilerplate
Decision Tree: Data Access
How to access data?
├── Simple queries, full control? → Dapper
├── Rich domain model, migrations? → EF Core
├── Document store? → MongoDB.Driver / CosmosDB SDK
└── Caching layer?
├── In-process? → IMemoryCache
└── Distributed? → IDistributedCache (Redis)
C# 13 / .NET 9 Patterns
public class UserService(IUserRepository repo, ILogger<UserService> logger)
{
public async Task<User?> GetByIdAsync(int id, CancellationToken ct = default)
{
logger.LogDebug("Fetching user {Id}", id);
return await repo.FindAsync(id, ct);
}
}
int[] numbers = [1, 2, 3, 4, 5];
List<string> names = ["Alice", "Bob", "Charlie"];
Span<byte> buffer = [0x00, 0xFF, 0xAB];
int[] combined = [..firstArray, ..secondArray, 42];
var json = """
{
"name": "Alice",
"email": "alice@example.com"
}
""";
string Classify(object obj) => obj switch
{
int n when n < 0 => "negative",
int n => $"positive: {n}",
string { Length: 0 } => "empty string",
string s => $"string: {s}",
null => "null",
_ => "unknown"
};
public class Config
{
public required string ConnectionString { get; init; }
public required int MaxRetries { get; init; }
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(30);
}
Minimal APIs (.NET 9)
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddDbContext<AppDbContext>(o =>
o.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
var app = builder.Build();
var users = app.MapGroup("/api/users")
.WithTags("Users")
.RequireAuthorization();
users.MapGet("/", async (IUserRepository repo, CancellationToken ct) =>
Results.Ok(await repo.GetAllAsync(ct)));
users.MapGet("/{id:int}", async (int id, IUserRepository repo, CancellationToken ct) =>
await repo.FindAsync(id, ct) is { } user
? Results.Ok(user)
: Results.NotFound());
users.MapPost("/", async (CreateUserRequest req, IUserRepository repo, CancellationToken ct) =>
{
var user = await repo.CreateAsync(req, ct);
return Results.Created($"/api/users/{user.Id}", user);
}).WithValidation<CreateUserRequest>();
app.Run();
users.MapGet("/{id:int}", async Task<Results<Ok<User>, NotFound>> (int id, IUserRepository repo, CancellationToken ct) =>
await repo.FindAsync(id, ct) is { } user
? TypedResults.Ok(user)
: TypedResults.NotFound());
.NET Aspire (Cloud-Native Orchestration)
var builder = DistributedApplication.CreateBuilder(args);
var postgres = builder.AddPostgres("pg")
.WithPgAdmin()
.AddDatabase("appdb");
var redis = builder.AddRedis("cache");
var api = builder.AddProject<Projects.MyApp_Api>("api")
.WithReference(postgres)
.WithReference(redis)
.WithExternalHttpEndpoints();
builder.AddProject<Projects.MyApp_Web>("web")
.WithReference(api)
.WithExternalHttpEndpoints();
builder.Build().Run();
builder.AddNpgsqlDbContext<AppDbContext>("appdb");
builder.AddRedisDistributedCache("cache");
builder.AddServiceDefaults();
Source Generators
[JsonSerializable(typeof(User))]
[JsonSerializable(typeof(List<User>))]
internal partial class AppJsonContext : JsonSerializerContext { }
app.MapGet("/users", () => Results.Json(users, AppJsonContext.Default.ListUser));
public static partial class Log
{
[LoggerMessage(Level = LogLevel.Information, Message = "User {UserId} created")]
public static partial void UserCreated(ILogger logger, int userId);
[LoggerMessage(Level = LogLevel.Error, Message = "Failed to process order {OrderId}")]
public static partial void OrderFailed(ILogger logger, string orderId, Exception ex);
}
public partial class Validators
{
[GeneratedRegex(@"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")]
private static partial Regex EmailRegex();
public static bool IsValidEmail(string email) => EmailRegex().IsMatch(email);
}
Error Handling & Result Pattern
public abstract record Result<T>
{
public record Success(T Value) : Result<T>;
public record Failure(Error Error) : Result<T>;
public TOut Match<TOut>(Func<T, TOut> onSuccess, Func<Error, TOut> onFailure) =>
this switch
{
Success s => onSuccess(s.Value),
Failure f => onFailure(f.Error),
_ => throw new InvalidOperationException()
};
}
public record Error(string Code, string Message);
app.UseExceptionHandler(error => error.Run(async context =>
{
var exception = context.Features.Get<IExceptionHandlerFeature>()?.Error;
var response = exception switch
{
NotFoundException e => (StatusCodes.Status404NotFound, e.Message),
ValidationException e => (StatusCodes.Status422UnprocessableEntity, e.Message),
_ => (StatusCodes.Status500InternalServerError, "An unexpected error occurred")
};
context.Response.StatusCode = response.Item1;
await context.Response.WriteAsJsonAsync(new { error = response.Item2 });
}));
Testing
public class UsersApiTests(WebApplicationFactory<Program> factory)
: IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client = factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
services.RemoveAll<DbContextOptions<AppDbContext>>();
services.AddDbContext<AppDbContext>(o => o.UseInMemoryDatabase("test"));
});
}).CreateClient();
[Fact]
public async Task GetUser_ReturnsNotFound_WhenMissing()
{
var response = await _client.GetAsync("/api/users/999");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
}
[Fact]
public async Task CreateUser_CallsRepository()
{
var repo = Substitute.For<IUserRepository>();
var service = new UserService(repo, NullLogger<UserService>.Instance);
await service.CreateAsync(new("alice@test.com", "Alice"), CancellationToken.None);
await repo.Received(1).CreateAsync(Arg.Any<CreateUserRequest>(), Arg.Any<CancellationToken>());
}
Anti-Patterns
| Anti-Pattern | Problem | Solution |
|---|
async void methods | Unobserved exceptions crash the process | Always return Task or Task<T> |
Catching Exception broadly | Swallows bugs, hides root cause | Catch specific exceptions, let others propagate |
Task.Result / .Wait() | Deadlocks in sync-over-async | Use await all the way up |
Service locator (GetService everywhere) | Hidden dependencies, untestable | Constructor injection via DI |
No CancellationToken propagation | Requests can't be cancelled, resource waste | Pass CancellationToken through all async chains |
| Mutable DTOs with public setters | Accidental mutation, thread-safety issues | Use record or required init properties |
| String concatenation for SQL | SQL injection vulnerability | Use parameterized queries / EF Core |
HttpClient created per request | Socket exhaustion | Use IHttpClientFactory |
Verification Checklist
Before considering .NET work done: