| name | csharp-developer |
| description | Senior C#/.NET developer with mastery of .NET 9+, C# 13, ASP.NET Core Minimal APIs, Entity Framework Core, Blazor, gRPC, AOT compilation, Span<T>/Memory<T> performance, dependency injection, and clean architecture. Covers modern language features, async patterns, testing with xUnit, and production deployment. Use when building .NET applications, APIs, or any C# code. |
| allowed-tools | Read, Write, Edit, Glob, Grep |
| version | 2.0.0 |
| last-updated | "2026-04-01T00:00:00.000Z" |
| applies-to-model | gemini-2.5-pro, claude-3-7-sonnet |
| routing | {"domain":"general","tier":"basic"} |
C# / .NET Pro — .NET 9+ & C# 13 Mastery
Modern C# Language Features
Records & Primary Constructors
public record UserDto(string Name, string Email, string Role = "user");
public record CreateUserRequest(string Name, string Email)
{
public string Name { get; init; } = !string.IsNullOrWhiteSpace(Name)
? Name.Trim()
: throw new ArgumentException("Name is required", nameof(Name));
}
public class UserService(IUserRepository repo, ILogger<UserService> logger)
{
public async Task<User?> GetUserAsync(int id, CancellationToken ct = default)
{
logger.LogInformation("Fetching user {UserId}", id);
return await repo.GetByIdAsync(id, ct);
}
}
Pattern Matching (C# 12+)
public static string ClassifyTemperature(double temp) => temp switch
{
< 0 => "Freezing",
>= 0 and < 15 => "Cold",
>= 15 and < 25 => "Comfortable",
>= 25 and < 35 => "Warm",
>= 35 => "Hot",
};
public static decimal CalculateDiscount(Order order) => order switch
{
{ Total: > 1000, Customer.IsPremium: true } => 0.20m,
{ Total: > 500 } => 0.10m,
{ Customer.IsPremium: true } => 0.05m,
_ => 0m,
};
public static string DescribeArray(int[] arr) => arr switch
{
[] => "Empty",
[var single] => $"Single: {single}",
[var first, .., var last] => $"First: {first}, Last: {last}",
};
Collection Expressions & Ranges
List<int> numbers = [1, 2, 3, 4, 5];
int[] array = [10, 20, 30];
Span<byte> bytes = [0xFF, 0x00, 0xAB];
int[] combined = [..numbers, ..array, 99];
var last = array[^1];
var slice = array[1..^1];
var firstThree = array[..3];
Nullable Reference Types
public class UserService
{
public string GetDisplayName(User user)
{
return user.DisplayName ?? user.Email;
}
public async Task<User?> FindUserAsync(string email, CancellationToken ct)
{
return await _db.Users.FirstOrDefaultAsync(u => u.Email == email, ct);
}
}
ASP.NET Core Minimal APIs
Route Structure
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
var api = app.MapGroup("/api").RequireAuthorization();
var users = api.MapGroup("/users").WithTags("Users");
users.MapGet("/", GetUsersAsync);
users.MapGet("/{id:int}", GetUserByIdAsync);
users.MapPost("/", CreateUserAsync).AllowAnonymous();
users.MapPut("/{id:int}", UpdateUserAsync);
users.MapDelete("/{id:int}", DeleteUserAsync);
app.Run();
Handler Methods
static async Task<Results<Ok<UserDto>, NotFound>> GetUserByIdAsync(
int id,
IUserService userService,
CancellationToken ct)
{
var user = await userService.GetByIdAsync(id, ct);
return user is not null
? TypedResults.Ok(user.ToDto())
: TypedResults.NotFound();
}
static async Task<Results<Created<UserDto>, ValidationProblem>> CreateUserAsync(
CreateUserRequest request,
IUserService userService,
IValidator<CreateUserRequest> validator,
CancellationToken ct)
{
var validation = await validator.ValidateAsync(request, ct);
if (!validation.IsValid)
return TypedResults.ValidationProblem(validation.ToDictionary());
var user = await userService.CreateAsync(request, ct);
return TypedResults.Created($"/api/users/{user.Id}", user.ToDto());
}
Endpoint Filters (Middleware for Endpoints)
public class ValidationFilter<T> : IEndpointFilter where T : class
{
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext ctx,
EndpointFilterDelegate next)
{
var validator = ctx.HttpContext.RequestServices.GetService<IValidator<T>>();
var argument = ctx.Arguments.OfType<T>().FirstOrDefault();
if (validator is not null && argument is not null)
{
var result = await validator.ValidateAsync(argument);
if (!result.IsValid)
return TypedResults.ValidationProblem(result.ToDictionary());
}
return await next(ctx);
}
}
users.MapPost("/", CreateUserAsync)
.AddEndpointFilter<ValidationFilter<CreateUserRequest>>();
Entity Framework Core
DbContext & Configuration
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
{
public DbSet<User> Users => Set<User>();
public DbSet<Post> Posts => Set<Post>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
}
public override async Task<int> SaveChangesAsync(CancellationToken ct = default)
{
foreach (var entry in ChangeTracker.Entries<BaseEntity>())
{
if (entry.State == EntityState.Added)
entry.Entity.CreatedAt = DateTime.UtcNow;
if (entry.State is EntityState.Added or EntityState.Modified)
entry.Entity.UpdatedAt = DateTime.UtcNow;
}
return await base.SaveChangesAsync(ct);
}
}
public class UserConfiguration : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> builder)
{
builder.HasIndex(u => u.Email).IsUnique();
builder.Property(u => u.Name).HasMaxLength(100).IsRequired();
builder.Property(u => u.Email).HasMaxLength(255).IsRequired();
builder.HasMany(u => u.Posts).WithOne(p => p.Author).HasForeignKey(p => p.AuthorId);
}
}
Query Patterns
public async Task<List<UserDto>> GetActiveUsersAsync(CancellationToken ct)
{
return await _db.Users
.AsNoTracking()
.Where(u => u.IsActive)
.OrderByDescending(u => u.CreatedAt)
.Select(u => new UserDto(u.Name, u.Email, u.Role))
.ToListAsync(ct);
}
public async Task<PagedResult<UserDto>> GetUsersPagedAsync(int page, int pageSize, CancellationToken ct)
{
var query = _db.Users.AsNoTracking().Where(u => u.IsActive);
var totalCount = await query.CountAsync(ct);
var items = await query
.OrderBy(u => u.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(u => new UserDto(u.Name, u.Email, u.Role))
.ToListAsync(ct);
return new PagedResult<UserDto>(items, totalCount, page, pageSize);
}
private static readonly Func<AppDbContext, string, CancellationToken, Task<User?>> _getUserByEmail =
EF.CompileAsyncQuery((AppDbContext db, string email, CancellationToken ct) =>
db.Users.FirstOrDefault(u => u.Email == email));
Async Patterns
public async Task<Result<User>> ProcessUserAsync(int id, CancellationToken ct)
{
var (user, permissions) = await (
_userRepo.GetByIdAsync(id, ct),
_permissionService.GetPermissionsAsync(id, ct)
);
await foreach (var notification in GetNotificationsAsync(id, ct))
{
await SendNotificationAsync(notification, ct);
}
return Result.Ok(user);
}
var channel = Channel.CreateBounded<WorkItem>(100);
async Task ProduceAsync(ChannelWriter<WorkItem> writer, CancellationToken ct)
{
await foreach (var item in GetWorkItemsAsync(ct))
{
await writer.WriteAsync(item, ct);
}
writer.Complete();
}
async Task ConsumeAsync(ChannelReader<WorkItem> reader, CancellationToken ct)
{
await foreach (var item in reader.ReadAllAsync(ct))
{
await ProcessAsync(item, ct);
}
}
Performance Patterns
public static ReadOnlySpan<char> ExtractDomain(ReadOnlySpan<char> email)
{
var atIndex = email.IndexOf('@');
return atIndex >= 0 ? email[(atIndex + 1)..] : ReadOnlySpan<char>.Empty;
}
public static void ProcessLargeData()
{
var buffer = ArrayPool<byte>.Shared.Rent(8192);
try
{
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
FrozenDictionary<string, int> lookup = new Dictionary<string, int>
{
["admin"] = 1,
["user"] = 2,
["moderator"] = 3,
}.ToFrozenDictionary();
Testing with xUnit
public class UserServiceTests
{
private readonly Mock<IUserRepository> _repoMock = new();
private readonly Mock<ILogger<UserService>> _loggerMock = new();
private readonly UserService _sut;
public UserServiceTests()
{
_sut = new UserService(_repoMock.Object, _loggerMock.Object);
}
[Fact]
public async Task GetUserAsync_ReturnsUser_WhenFound()
{
var expected = new User { Id = 1, Name = "Alice", Email = "alice@test.com" };
_repoMock.Setup(r => r.GetByIdAsync(1, It.IsAny<CancellationToken>()))
.ReturnsAsync(expected);
var result = await _sut.GetUserAsync(1);
Assert.NotNull(result);
Assert.Equal("Alice", result.Name);
}
[Fact]
public async Task GetUserAsync_ReturnsNull_WhenNotFound()
{
_repoMock.Setup(r => r.GetByIdAsync(999, It.IsAny<CancellationToken>()))
.ReturnsAsync((User?)null);
var result = await _sut.GetUserAsync(999);
Assert.Null(result);
}
[Theory]
[InlineData("", "required")]
[InlineData("ab", "too short")]
public async Task CreateUser_Fails_WithInvalidName(string name, string expectedError)
{
var request = new CreateUserRequest(name, "test@test.com");
var ex = await Assert.ThrowsAsync<ValidationException>(
() => _sut.CreateAsync(request));
Assert.Contains(expectedError, ex.Message, StringComparison.OrdinalIgnoreCase);
}
}
public class UsersApiTests(WebApplicationFactory<Program> factory)
: IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client = factory.CreateClient();
[Fact]
public async Task GetUsers_Returns200()
{
var response = await _client.GetAsync("/api/users");
response.EnsureSuccessStatusCode();
var users = await response.Content.ReadFromJsonAsync<List<UserDto>>();
Assert.NotNull(users);
}
}
AI coding assistants often fall into specific bad habits when dealing with this domain. These are strictly forbidden:
- Over-engineering: Proposing complex abstractions or distributed systems when a simpler approach suffices.
- Hallucinated Libraries/Methods: Using non-existent methods or packages. Always
// VERIFY or check package.json / requirements.txt.
- Skipping Edge Cases: Writing the "happy path" and ignoring error handling, timeouts, or data validation.
- Context Amnesia: Forgetting the user's constraints and offering generic advice instead of tailored solutions.
- Silent Degradation: Catching and suppressing errors without logging or re-raising.
Slash command: /review or /tribunal-full
Active reviewers: logic-reviewer · security-auditor
❌ Forbidden AI Tropes
- Blind Assumptions: Never make an assumption without documenting it clearly with
// VERIFY: [reason].
- Silent Degradation: Catching and suppressing errors without logging or handling.
- Context Amnesia: Forgetting the user's constraints and offering generic advice instead of tailored solutions.
Review these questions before confirming output:
✅ Did I rely ONLY on real, verified tools and methods?
✅ Is this solution appropriately scoped to the user's constraints?
✅ Did I handle potential failure modes and edge cases?
✅ Have I avoided generic boilerplate that doesn't add value?
🛑 Verification-Before-Completion (VBC) Protocol
CRITICAL: You must follow a strict "evidence-based closeout" state machine.
- ❌ Forbidden: Declaring a task complete because the output "looks correct."
- ✅ Required: You are explicitly forbidden from finalizing any task without providing concrete evidence (terminal output, passing tests, compile success, or equivalent proof) that your output works as intended.
Pre-Flight Checklist
VBC Protocol (Verification-Before-Completion)
You MUST verify existing code signatures and variables before attempting to modify or call them. No hallucination is permitted.
🤖 LLM-Specific Traps
AI coding assistants often fall into specific bad habits when dealing with this domain. These are strictly forbidden:
- Over-engineering: Proposing complex abstractions or distributed systems when a simpler approach suffices.
- Hallucinated Libraries/Methods: Using non-existent methods or packages. Always
// VERIFY or check package.json / requirements.txt.
- Skipping Edge Cases: Writing the "happy path" and ignoring error handling, timeouts, or data validation.
- Context Amnesia: Forgetting the user's constraints and offering generic advice instead of tailored solutions.
- Silent Degradation: Catching and suppressing errors without logging or re-raising.
🏛️ Tribunal Integration (Anti-Hallucination)
Slash command: /review or /tribunal-full
Active reviewers: logic-reviewer · security-auditor
❌ Forbidden AI Tropes
- Blind Assumptions: Never make an assumption without documenting it clearly with
// VERIFY: [reason].
- Silent Degradation: Catching and suppressing errors without logging or handling.
- Context Amnesia: Forgetting the user's constraints and offering generic advice instead of tailored solutions.
✅ Pre-Flight Self-Audit
Review these questions before confirming output:
✅ Did I rely ONLY on real, verified tools and methods?
✅ Is this solution appropriately scoped to the user's constraints?
✅ Did I handle potential failure modes and edge cases?
✅ Have I avoided generic boilerplate that doesn't add value?
🛑 Verification-Before-Completion (VBC) Protocol
CRITICAL: You must follow a strict "evidence-based closeout" state machine.
- ❌ Forbidden: Declaring a task complete because the output "looks correct."
- ✅ Required: You are explicitly forbidden from finalizing any task without providing concrete evidence (terminal output, passing tests, compile success, or equivalent proof) that your output works as intended.