csharp-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.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "csharp-expert" with this command: npx skills add personamanagmentlayer/pcl/personamanagmentlayer-pcl-csharp-expert

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&#x3C;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&#x3C;UsersController> logger)
{
    _userService = userService;
    _logger = logger;
}

[HttpGet]
[ProducesResponseType(typeof(IEnumerable&#x3C;UserDto>), StatusCodes.Status200OK)]
public async Task&#x3C;ActionResult&#x3C;IEnumerable&#x3C;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&#x3C;ActionResult&#x3C;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&#x3C;ActionResult&#x3C;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&#x3C;ActionResult&#x3C;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&#x3C;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&#x3C;UserService> logger,
    IOptions&#x3C;AppSettings> settings)
{
    _repository = repository;
    _logger = logger;
    _settings = settings;
}

public async Task&#x3C;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&#x3C;User> Users => Set&#x3C;User>();
public DbSet&#x3C;Order> Orders => Set&#x3C;Order>();
public DbSet&#x3C;Product> Products => Set&#x3C;Product>();

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);

    // User configuration
    modelBuilder.Entity&#x3C;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&#x3C;Order>(entity =>
    {
        entity.HasKey(e => e.Id);
        entity.Property(e => e.Total).HasColumnType("decimal(18,2)");
        entity.Property(e => e.Status).HasConversion&#x3C;string>();

        entity.HasMany(e => e.Items)
            .WithOne(e => e.Order)
            .HasForeignKey(e => e.OrderId);
    });

    // Seed data
    modelBuilder.Entity&#x3C;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&#x3C;IUserRepository>();
    _loggerMock = new Mock&#x3C;ILogger&#x3C;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&#x3C;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&#x3C;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>();

  1. 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! }

  1. 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(); }

  1. 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");

  1. 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&#x3C;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&#x3C;T>();
}

public async Task&#x3C;T?> GetByIdAsync(int id) => await _dbSet.FindAsync(id);
public async Task&#x3C;IEnumerable&#x3C;T>> GetAllAsync() => await _dbSet.ToListAsync();

public async Task&#x3C;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&#x3C;T> Success(T value) => new() { IsSuccess = true, Value = value };
public static Result&#x3C;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.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

python-expert

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

devops-expert

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

code-review-expert

No summary provided by upstream source.

Repository SourceNeeds Review