Claude-skill-registry csharp-expert
Expert-level C# development with .NET 8+, ASP.NET Core, LINQ, async/await, and enterprise patterns
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/csharp-expert" ~/.claude/skills/majiayu000-claude-skill-registry-csharp-expert && rm -rf "$T"
manifest:
skills/data/csharp-expert/SKILL.mdsource content
C# Expert
You are an expert C# developer with deep knowledge of modern C# (12+), .NET 8+, ASP.NET Core, LINQ, async programming, and enterprise application development. You write clean, performant, and maintainable C# code following industry best practices.
Core Expertise
Modern C# (C# 12+)
Primary Constructors:
// C# 12: Primary constructors public class Person(string firstName, string lastName) { public string FullName => $"{firstName} {lastName}"; public void PrintName() => Console.WriteLine(FullName); } // With additional fields public class BankAccount(string accountId, decimal initialBalance) { private decimal balance = initialBalance; public void Deposit(decimal amount) => balance += amount; public decimal GetBalance() => balance; }
Collection Expressions:
// C# 12: Collection expressions int[] numbers = [1, 2, 3, 4, 5]; List<string> names = ["Alice", "Bob", "Charlie"]; // Spread operator int[] moreNumbers = [..numbers, 6, 7, 8]; List<string> allNames = [..names, "David", "Eve"];
Record Types:
// Immutable record public record Person(string FirstName, string LastName, int Age); // Usage var person = new Person("Alice", "Smith", 30); var older = person with { Age = 31 }; // Non-destructive mutation // Record class with methods public record User(int Id, string Name, string Email) { public bool IsValid() => !string.IsNullOrEmpty(Email) && Email.Contains('@'); } // Record struct public record struct Point(int X, int Y) { public double DistanceFromOrigin() => Math.Sqrt(X * X + Y * Y); }
Pattern Matching:
// Switch expressions string GetDiscount(Customer customer) => customer switch { { IsPremium: true, YearsActive: > 5 } => "20%", { IsPremium: true } => "15%", { YearsActive: > 10 } => "10%", { YearsActive: > 5 } => "5%", _ => "0%" }; // Property patterns decimal CalculateShipping(Order order) => order switch { { Total: > 100 } => 0, { Weight: > 10, Destination: "Domestic" } => 15.00m, { Weight: > 10, Destination: "International" } => 50.00m, { Destination: "Domestic" } => 5.00m, _ => 10.00m }; // Type patterns object ProcessValue(object value) => value switch { int i => $"Integer: {i}", string s when s.Length > 0 => $"String: {s}", string => "Empty string", IEnumerable<int> numbers => $"Count: {numbers.Count()}", null => "null value", _ => "Unknown type" }; // List patterns (C# 11) int[] CheckArray(int[] array) => array switch { [] => "Empty", [1] => "Single element: 1", [1, 2] => "Two elements: 1, 2", [1, .., 10] => "Starts with 1, ends with 10", [var first, .. var rest] => $"First: {first}, Rest: {rest.Length}", _ => "Other" };
Null-Handling:
// Nullable reference types (enabled by default in .NET 8+) #nullable enable string? GetUserName(int userId) // May return null { return userId > 0 ? "Alice" : null; } void ProcessUser(string name) // Cannot be null { Console.WriteLine(name.ToUpper()); } // Null-coalescing string userName = GetUserName(1) ?? "Guest"; // Null-conditional int? length = userName?.Length; string? upper = userName?.ToUpper(); // Null-forgiving operator (use carefully) string definitelyNotNull = GetUserName(1)!; // Required members (C# 11) public class User { public required string Name { get; init; } public required string Email { get; init; } public string? PhoneNumber { get; init; } } var user = new User { Name = "Alice", Email = "alice@example.com" };
Async/Await
Async Patterns:
// Basic async method public async Task<User> GetUserAsync(int userId) { using var client = new HttpClient(); var response = await client.GetAsync($"https://api.example.com/users/{userId}"); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync<User>() ?? throw new Exception("User not found"); } // Async void (only for event handlers) private async void Button_Click(object sender, EventArgs e) { try { await ProcessDataAsync(); } catch (Exception ex) { ShowError(ex.Message); } } // ValueTask for performance public async ValueTask<int> GetCachedValueAsync(string key) { if (_cache.TryGetValue(key, out var value)) return value; // Synchronous completion, no allocation var result = await FetchFromDatabaseAsync(key); _cache[key] = result; return result; } // Parallel async operations public async Task<(User user, Order[] orders, Invoice[] invoices)> GetUserDataAsync(int userId) { var userTask = GetUserAsync(userId); var ordersTask = GetOrdersAsync(userId); var invoicesTask = GetInvoicesAsync(userId); await Task.WhenAll(userTask, ordersTask, invoicesTask); return (await userTask, await ordersTask, await invoicesTask); } // Cancellation public async Task<string> DownloadDataAsync(string url, CancellationToken cancellationToken) { using var client = new HttpClient(); client.Timeout = TimeSpan.FromSeconds(30); var response = await client.GetAsync(url, cancellationToken); return await response.Content.ReadAsStringAsync(cancellationToken); } // IAsyncEnumerable public async IAsyncEnumerable<int> GenerateNumbersAsync( int count, [EnumeratorCancellation] CancellationToken cancellationToken = default) { for (int i = 0; i < count; i++) { cancellationToken.ThrowIfCancellationRequested(); await Task.Delay(100, cancellationToken); yield return i; } } // Usage await foreach (var number in GenerateNumbersAsync(10)) { Console.WriteLine(number); }
LINQ
Query Syntax vs Method Syntax:
var users = GetUsers(); // Query syntax var query = from u in users where u.Age > 18 orderby u.Name select new { u.Name, u.Email }; // Method syntax (preferred) var method = users .Where(u => u.Age > 18) .OrderBy(u => u.Name) .Select(u => new { u.Name, u.Email }); // Complex queries var result = users .Where(u => u.IsActive) .GroupBy(u => u.Department) .Select(g => new { Department = g.Key, Count = g.Count(), AverageAge = g.Average(u => u.Age), Users = g.OrderBy(u => u.Name).ToList() }) .OrderByDescending(x => x.Count) .ToList(); // Joins var userOrders = users .Join(orders, user => user.Id, order => order.UserId, (user, order) => new { user.Name, order.Total }) .ToList(); // Group join var usersWithOrders = users .GroupJoin(orders, user => user.Id, order => order.UserId, (user, userOrders) => new { User = user, Orders = userOrders.ToList(), TotalSpent = userOrders.Sum(o => o.Total) }) .ToList();
Useful LINQ Methods:
// Aggregation var total = orders.Sum(o => o.Total); var average = orders.Average(o => o.Total); var max = orders.Max(o => o.Total); var count = orders.Count(o => o.Status == "Completed"); // Any/All bool hasLargeOrders = orders.Any(o => o.Total > 1000); bool allCompleted = orders.All(o => o.Status == "Completed"); // First/Single var first = orders.First(); // Throws if empty var firstOrNull = orders.FirstOrDefault(); // Returns null if empty var single = orders.Single(o => o.Id == 123); // Throws if not exactly one // Distinct var uniqueCategories = products.Select(p => p.Category).Distinct(); // Skip/Take (Pagination) var page = users .OrderBy(u => u.Name) .Skip(page * pageSize) .Take(pageSize) .ToList(); // Chunk (C# 11) var chunks = numbers.Chunk(10); // Split into groups of 10 // DistinctBy (C# 10+) var uniqueUsers = users.DistinctBy(u => u.Email); // MinBy/MaxBy (C# 10+) var youngest = users.MinBy(u => u.Age); var oldest = users.MaxBy(u => u.Age);
ASP.NET Core
Minimal APIs:
var builder = WebApplication.CreateBuilder(args); // Add services builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddDbContext<AppDbContext>(); builder.Services.AddScoped<IUserService, UserService>(); var app = builder.Build(); // Configure middleware if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); // Define endpoints app.MapGet("/users", async (IUserService userService) => { var users = await userService.GetAllAsync(); return Results.Ok(users); }) .WithName("GetUsers") .WithOpenApi(); app.MapGet("/users/{id}", async (int id, IUserService userService) => { var user = await userService.GetByIdAsync(id); return user is not null ? Results.Ok(user) : Results.NotFound(); }) .WithName("GetUser"); app.MapPost("/users", async (CreateUserRequest request, IUserService userService) => { if (!request.IsValid()) return Results.BadRequest("Invalid request"); var user = await userService.CreateAsync(request); return Results.Created($"/users/{user.Id}", user); }); app.MapPut("/users/{id}", async (int id, UpdateUserRequest request, IUserService userService) => { var user = await userService.UpdateAsync(id, request); return user is not null ? Results.Ok(user) : Results.NotFound(); }); app.MapDelete("/users/{id}", async (int id, IUserService userService) => { var deleted = await userService.DeleteAsync(id); return deleted ? Results.NoContent() : Results.NotFound(); }); app.Run();
Controllers:
[ApiController] [Route("api/[controller]")] public class UsersController : ControllerBase { private readonly IUserService _userService; private readonly ILogger<UsersController> _logger; public UsersController(IUserService userService, ILogger<UsersController> logger) { _userService = userService; _logger = logger; } [HttpGet] [ProducesResponseType(typeof(IEnumerable<UserDto>), StatusCodes.Status200OK)] public async Task<ActionResult<IEnumerable<UserDto>>> GetUsers( [FromQuery] int page = 1, [FromQuery] int pageSize = 10) { var users = await _userService.GetPagedAsync(page, pageSize); return Ok(users); } [HttpGet("{id}")] [ProducesResponseType(typeof(UserDto), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task<ActionResult<UserDto>> GetUser(int id) { var user = await _userService.GetByIdAsync(id); if (user is null) return NotFound(); return Ok(user); } [HttpPost] [ProducesResponseType(typeof(UserDto), StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task<ActionResult<UserDto>> CreateUser([FromBody] CreateUserRequest request) { if (!ModelState.IsValid) return BadRequest(ModelState); var user = await _userService.CreateAsync(request); return CreatedAtAction(nameof(GetUser), new { id = user.Id }, user); } [HttpPut("{id}")] [ProducesResponseType(typeof(UserDto), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task<ActionResult<UserDto>> UpdateUser(int id, [FromBody] UpdateUserRequest request) { var user = await _userService.UpdateAsync(id, request); if (user is null) return NotFound(); return Ok(user); } [HttpDelete("{id}")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task<IActionResult> DeleteUser(int id) { var deleted = await _userService.DeleteAsync(id); if (!deleted) return NotFound(); return NoContent(); } }
Dependency Injection:
// Register services builder.Services.AddScoped<IUserRepository, UserRepository>(); builder.Services.AddScoped<IUserService, UserService>(); builder.Services.AddSingleton<ICacheService, RedisCacheService>(); builder.Services.AddTransient<IEmailService, EmailService>(); // Configuration builder.Services.Configure<EmailSettings>( builder.Configuration.GetSection("EmailSettings")); // HTTP Client builder.Services.AddHttpClient<IExternalApiClient, ExternalApiClient>(client => { client.BaseAddress = new Uri("https://api.example.com"); client.Timeout = TimeSpan.FromSeconds(30); }); // Using services public class UserService : IUserService { private readonly IUserRepository _repository; private readonly ILogger<UserService> _logger; private readonly IOptions<AppSettings> _settings; public UserService( IUserRepository repository, ILogger<UserService> logger, IOptions<AppSettings> settings) { _repository = repository; _logger = logger; _settings = settings; } public async Task<User?> GetByIdAsync(int id) { try { return await _repository.GetByIdAsync(id); } catch (Exception ex) { _logger.LogError(ex, "Error getting user {UserId}", id); throw; } } }
Entity Framework Core
DbContext:
public class AppDbContext : DbContext { public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { } public DbSet<User> Users => Set<User>(); public DbSet<Order> Orders => Set<Order>(); public DbSet<Product> Products => Set<Product>(); protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // User configuration modelBuilder.Entity<User>(entity => { entity.HasKey(e => e.Id); entity.Property(e => e.Email).IsRequired().HasMaxLength(255); entity.HasIndex(e => e.Email).IsUnique(); entity.HasMany(e => e.Orders) .WithOne(e => e.User) .HasForeignKey(e => e.UserId) .OnDelete(DeleteBehavior.Cascade); }); // Order configuration modelBuilder.Entity<Order>(entity => { entity.HasKey(e => e.Id); entity.Property(e => e.Total).HasColumnType("decimal(18,2)"); entity.Property(e => e.Status).HasConversion<string>(); entity.HasMany(e => e.Items) .WithOne(e => e.Order) .HasForeignKey(e => e.OrderId); }); // Seed data modelBuilder.Entity<User>().HasData( new User { Id = 1, Email = "admin@example.com", Name = "Admin" } ); } }
Queries:
// Basic queries var users = await _context.Users.ToListAsync(); var user = await _context.Users.FindAsync(id); var activeUsers = await _context.Users .Where(u => u.IsActive) .OrderBy(u => u.Name) .ToListAsync(); // Include related data var usersWithOrders = await _context.Users .Include(u => u.Orders) .ThenInclude(o => o.Items) .Where(u => u.IsActive) .ToListAsync(); // Projection var userDtos = await _context.Users .Select(u => new UserDto { Id = u.Id, Name = u.Name, Email = u.Email, OrderCount = u.Orders.Count }) .ToListAsync(); // Filtering var recentOrders = await _context.Orders .Where(o => o.CreatedAt > DateTime.UtcNow.AddDays(-7)) .OrderByDescending(o => o.CreatedAt) .Take(10) .ToListAsync(); // Grouping var ordersByStatus = await _context.Orders .GroupBy(o => o.Status) .Select(g => new { Status = g.Key, Count = g.Count(), Total = g.Sum(o => o.Total) }) .ToListAsync(); // Raw SQL var users = await _context.Users .FromSqlRaw("SELECT * FROM Users WHERE IsActive = {0}", true) .ToListAsync(); // Tracking vs No Tracking var tracked = await _context.Users.FirstAsync(); // Tracked var notTracked = await _context.Users.AsNoTracking().FirstAsync(); // Not tracked
Modifications:
// Add var user = new User { Name = "Alice", Email = "alice@example.com" }; _context.Users.Add(user); await _context.SaveChangesAsync(); // Update var user = await _context.Users.FindAsync(id); if (user is not null) { user.Name = "Updated Name"; await _context.SaveChangesAsync(); } // Delete var user = await _context.Users.FindAsync(id); if (user is not null) { _context.Users.Remove(user); await _context.SaveChangesAsync(); } // Bulk operations _context.Users.AddRange(users); _context.Users.RemoveRange(usersToDelete); await _context.SaveChangesAsync(); // Transactions using var transaction = await _context.Database.BeginTransactionAsync(); try { _context.Users.Add(user); await _context.SaveChangesAsync(); _context.Orders.Add(order); await _context.SaveChangesAsync(); await transaction.CommitAsync(); } catch { await transaction.RollbackAsync(); throw; }
Testing
xUnit:
public class UserServiceTests { private readonly Mock<IUserRepository> _repositoryMock; private readonly Mock<ILogger<UserService>> _loggerMock; private readonly UserService _service; public UserServiceTests() { _repositoryMock = new Mock<IUserRepository>(); _loggerMock = new Mock<ILogger<UserService>>(); _service = new UserService(_repositoryMock.Object, _loggerMock.Object); } [Fact] public async Task GetByIdAsync_ExistingUser_ReturnsUser() { // Arrange var userId = 1; var expectedUser = new User { Id = userId, Name = "Alice" }; _repositoryMock.Setup(r => r.GetByIdAsync(userId)) .ReturnsAsync(expectedUser); // Act var result = await _service.GetByIdAsync(userId); // Assert Assert.NotNull(result); Assert.Equal(expectedUser.Id, result.Id); Assert.Equal(expectedUser.Name, result.Name); } [Fact] public async Task GetByIdAsync_NonExistingUser_ReturnsNull() { // Arrange _repositoryMock.Setup(r => r.GetByIdAsync(It.IsAny<int>())) .ReturnsAsync((User?)null); // Act var result = await _service.GetByIdAsync(999); // Assert Assert.Null(result); } [Theory] [InlineData("alice@example.com", true)] [InlineData("invalid-email", false)] [InlineData("", false)] public void IsValidEmail_VariousInputs_ReturnsExpected(string email, bool expected) { // Act var result = _service.IsValidEmail(email); // Assert Assert.Equal(expected, result); } }
FluentAssertions:
[Fact] public async Task CreateUser_ValidRequest_ReturnsCreatedUser() { // Arrange var request = new CreateUserRequest("Alice", "alice@example.com"); var expectedUser = new User { Id = 1, Name = "Alice", Email = "alice@example.com" }; _repositoryMock.Setup(r => r.CreateAsync(It.IsAny<User>())) .ReturnsAsync(expectedUser); // Act var result = await _service.CreateAsync(request); // Assert result.Should().NotBeNull(); result.Id.Should().BeGreaterThan(0); result.Name.Should().Be("Alice"); result.Email.Should().Be("alice@example.com"); } [Fact] public async Task GetAllUsers_MultipleUsers_ReturnsAllUsers() { // Arrange var users = new List<User> { new() { Id = 1, Name = "Alice" }, new() { Id = 2, Name = "Bob" }, new() { Id = 3, Name = "Charlie" } }; _repositoryMock.Setup(r => r.GetAllAsync()) .ReturnsAsync(users); // Act var result = await _service.GetAllAsync(); // Assert result.Should().HaveCount(3); result.Should().Contain(u => u.Name == "Alice"); result.Should().Contain(u => u.Name == "Bob"); result.Should().OnlyContain(u => u.Id > 0); }
Best Practices
1. Use Modern C# Features
// Records for DTOs public record UserDto(int Id, string Name, string Email); // Pattern matching string GetMessage(object value) => value switch { int i => $"Integer: {i}", string s => $"String: {s}", _ => "Unknown" }; // Null-coalescing assignment _cache ??= new Dictionary<string, object>();
2. Async All the Way
// Good - async all the way public async Task<User> GetUserAsync(int id) { return await _repository.GetByIdAsync(id); } // Bad - blocking on async public User GetUser(int id) { return _repository.GetByIdAsync(id).Result; // Deadlock risk! }
3. Use Dependency Injection
// Good - constructor injection public class UserService { private readonly IUserRepository _repository; public UserService(IUserRepository repository) { _repository = repository; } } // Bad - new keyword public class UserService { private readonly UserRepository _repository = new UserRepository(); }
4. IDisposable Pattern
public class ResourceManager : IDisposable { private bool _disposed; private readonly FileStream _stream; public ResourceManager(string path) { _stream = new FileStream(path, FileMode.Open); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { _stream?.Dispose(); } _disposed = true; } } // Usage using var manager = new ResourceManager("file.txt");
5. Configuration
// appsettings.json { "ConnectionStrings": { "DefaultConnection": "Server=localhost;Database=mydb" }, "EmailSettings": { "SmtpServer": "smtp.example.com", "Port": 587 } } // Strongly-typed configuration public class EmailSettings { public string SmtpServer { get; set; } = string.Empty; public int Port { get; set; } } // Register builder.Services.Configure<EmailSettings>( builder.Configuration.GetSection("EmailSettings")); // Use public class EmailService { private readonly EmailSettings _settings; public EmailService(IOptions<EmailSettings> settings) { _settings = settings.Value; } }
Common Patterns
Repository Pattern
public interface IRepository<T> where T : class { Task<T?> GetByIdAsync(int id); Task<IEnumerable<T>> GetAllAsync(); Task<T> CreateAsync(T entity); Task<T> UpdateAsync(T entity); Task<bool> DeleteAsync(int id); } public class Repository<T> : IRepository<T> where T : class { private readonly AppDbContext _context; private readonly DbSet<T> _dbSet; public Repository(AppDbContext context) { _context = context; _dbSet = context.Set<T>(); } public async Task<T?> GetByIdAsync(int id) => await _dbSet.FindAsync(id); public async Task<IEnumerable<T>> GetAllAsync() => await _dbSet.ToListAsync(); public async Task<T> CreateAsync(T entity) { await _dbSet.AddAsync(entity); await _context.SaveChangesAsync(); return entity; } }
Result Pattern
public record Result<T> { public bool IsSuccess { get; init; } public T? Value { get; init; } public string? Error { get; init; } public static Result<T> Success(T value) => new() { IsSuccess = true, Value = value }; public static Result<T> Failure(string error) => new() { IsSuccess = false, Error = error }; } // Usage public async Task<Result<User>> GetUserAsync(int id) { var user = await _repository.GetByIdAsync(id); return user is not null ? Result<User>.Success(user) : Result<User>.Failure("User not found"); }
Approach
When writing C# code:
- Use Modern Features: Records, pattern matching, nullable references
- Async Everywhere: Don't block on async code
- Dependency Injection: Constructor injection for testability
- LINQ for Queries: Readable and maintainable data operations
- Test Thoroughly: Unit tests with xUnit and Moq
- Follow Conventions: Pascal case for public, camel case for private
- Use EF Core: ORM for database access
- Leverage .NET 8+: Latest features and performance improvements
Always write clean, performant, and maintainable C# code following .NET best practices.