--- name: csharp-development description: C# and .NET development patterns with xUnit, Clean Architecture, and modern C# practices. Use when writing C# code. --- # C# Development Skill ## Project Structure (Clean Architecture) ``` Solution/ ├── src/ │ ├── Domain/ # Enterprise business rules │ │ ├── Entities/ │ │ ├── ValueObjects/ │ │ ├── Exceptions/ │ │ └── Domain.csproj │ ├── Application/ # Application business rules │ │ ├── Common/ │ │ │ ├── Interfaces/ │ │ │ └── Behaviours/ │ │ ├── Features/ │ │ │ └── Users/ │ │ │ ├── Commands/ │ │ │ └── Queries/ │ │ └── Application.csproj │ ├── Infrastructure/ # External concerns │ │ ├── Persistence/ │ │ ├── Identity/ │ │ └── Infrastructure.csproj │ └── Api/ # Presentation layer │ ├── Controllers/ │ ├── Middleware/ │ └── Api.csproj ├── tests/ │ ├── Domain.Tests/ │ ├── Application.Tests/ │ ├── Infrastructure.Tests/ │ └── Api.Tests/ └── Solution.sln ``` ## Testing with xUnit ### Unit Tests with Fluent Assertions ```csharp using FluentAssertions; using Moq; using Xunit; namespace Application.Tests.Features.Users; public class CreateUserCommandHandlerTests { private readonly Mock _userRepositoryMock; private readonly Mock _unitOfWorkMock; private readonly CreateUserCommandHandler _handler; public CreateUserCommandHandlerTests() { _userRepositoryMock = new Mock(); _unitOfWorkMock = new Mock(); _handler = new CreateUserCommandHandler( _userRepositoryMock.Object, _unitOfWorkMock.Object); } [Fact] public async Task Handle_WithValidCommand_ShouldCreateUser() { // Arrange var command = UserFixtures.CreateUserCommand(); _userRepositoryMock .Setup(x => x.ExistsByEmailAsync(command.Email, It.IsAny())) .ReturnsAsync(false); // Act var result = await _handler.Handle(command, CancellationToken.None); // Assert result.Should().NotBeNull(); result.Id.Should().NotBeEmpty(); result.Email.Should().Be(command.Email); _userRepositoryMock.Verify( x => x.AddAsync(It.IsAny(), It.IsAny()), Times.Once); _unitOfWorkMock.Verify( x => x.SaveChangesAsync(It.IsAny()), Times.Once); } [Fact] public async Task Handle_WithDuplicateEmail_ShouldThrowDuplicateEmailException() { // Arrange var command = UserFixtures.CreateUserCommand(); _userRepositoryMock .Setup(x => x.ExistsByEmailAsync(command.Email, It.IsAny())) .ReturnsAsync(true); // Act var act = () => _handler.Handle(command, CancellationToken.None); // Assert await act.Should() .ThrowAsync() .WithMessage($"*{command.Email}*"); } } ``` ### Test Fixtures ```csharp namespace Application.Tests.Fixtures; public static class UserFixtures { public static User User(Action? customize = null) { var user = new User { Id = Guid.NewGuid(), Email = "test@example.com", Name = "Test User", Role = Role.User, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }; customize?.Invoke(user); return user; } public static CreateUserCommand CreateUserCommand( Action? customize = null) { var command = new CreateUserCommand { Email = "newuser@example.com", Name = "New User", Password = "SecurePass123!" }; customize?.Invoke(command); return command; } public static UserDto UserDto(Action? customize = null) { var dto = new UserDto { Id = Guid.NewGuid(), Email = "test@example.com", Name = "Test User", Role = Role.User.ToString() }; customize?.Invoke(dto); return dto; } } // Usage var adminUser = UserFixtures.User(u => u.Role = Role.Admin); var customCommand = UserFixtures.CreateUserCommand(c => c.Email = "custom@example.com"); ``` ### Theory Tests (Parameterized) ```csharp namespace Domain.Tests.ValueObjects; public class EmailTests { [Theory] [InlineData("user@example.com", true)] [InlineData("user.name@example.co.uk", true)] [InlineData("invalid", false)] [InlineData("missing@domain", false)] [InlineData("", false)] [InlineData(null, false)] public void IsValid_ShouldReturnExpectedResult(string? email, bool expected) { // Act var result = Email.IsValid(email); // Assert result.Should().Be(expected); } [Theory] [MemberData(nameof(ValidEmailTestData))] public void Create_WithValidEmail_ShouldSucceed(string email) { // Act var result = Email.Create(email); // Assert result.IsSuccess.Should().BeTrue(); result.Value.Value.Should().Be(email.ToLowerInvariant()); } public static IEnumerable ValidEmailTestData() { yield return new object[] { "user@example.com" }; yield return new object[] { "USER@EXAMPLE.COM" }; yield return new object[] { "user.name+tag@example.co.uk" }; } } ``` ### Integration Tests with WebApplicationFactory ```csharp using Microsoft.AspNetCore.Mvc.Testing; using System.Net; using System.Net.Http.Json; namespace Api.Tests.Controllers; public class UsersControllerTests : IClassFixture> { private readonly HttpClient _client; private readonly WebApplicationFactory _factory; public UsersControllerTests(WebApplicationFactory factory) { _factory = factory.WithWebHostBuilder(builder => { builder.ConfigureServices(services => { // Replace services with test doubles services.RemoveAll(); services.AddScoped(); }); }); _client = _factory.CreateClient(); } [Fact] public async Task CreateUser_WithValidRequest_ReturnsCreated() { // Arrange var request = new CreateUserRequest { Email = "new@example.com", Name = "New User", Password = "SecurePass123!" }; // Act var response = await _client.PostAsJsonAsync("/api/users", request); // Assert response.StatusCode.Should().Be(HttpStatusCode.Created); var user = await response.Content.ReadFromJsonAsync(); user.Should().NotBeNull(); user!.Email.Should().Be(request.Email); } [Fact] public async Task GetUser_WhenNotFound_ReturnsNotFound() { // Act var response = await _client.GetAsync($"/api/users/{Guid.NewGuid()}"); // Assert response.StatusCode.Should().Be(HttpStatusCode.NotFound); } } ``` ## Domain Models ### Entity with Value Objects ```csharp namespace Domain.Entities; public class User : BaseEntity { private User() { } // EF Core constructor public Guid Id { get; private set; } public Email Email { get; private set; } = null!; public string Name { get; private set; } = null!; public Role Role { get; private set; } public DateTime CreatedAt { get; private set; } public DateTime UpdatedAt { get; private set; } public static Result Create(string email, string name) { var emailResult = Email.Create(email); if (emailResult.IsFailure) { return Result.Failure(emailResult.Error); } if (string.IsNullOrWhiteSpace(name)) { return Result.Failure(DomainErrors.User.NameRequired); } var user = new User { Id = Guid.NewGuid(), Email = emailResult.Value, Name = name.Trim(), Role = Role.User, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }; user.RaiseDomainEvent(new UserCreatedEvent(user.Id, user.Email.Value)); return Result.Success(user); } public Result UpdateName(string name) { if (string.IsNullOrWhiteSpace(name)) { return Result.Failure(DomainErrors.User.NameRequired); } Name = name.Trim(); UpdatedAt = DateTime.UtcNow; return Result.Success(); } public void PromoteToAdmin() { Role = Role.Admin; UpdatedAt = DateTime.UtcNow; RaiseDomainEvent(new UserPromotedEvent(Id)); } } ``` ### Value Object ```csharp namespace Domain.ValueObjects; public sealed class Email : ValueObject { private static readonly Regex EmailRegex = new( @"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$", RegexOptions.Compiled); public string Value { get; } private Email(string value) => Value = value; public static Result Create(string? email) { if (string.IsNullOrWhiteSpace(email)) { return Result.Failure(DomainErrors.Email.Empty); } var normalizedEmail = email.Trim().ToLowerInvariant(); if (!EmailRegex.IsMatch(normalizedEmail)) { return Result.Failure(DomainErrors.Email.InvalidFormat); } return Result.Success(new Email(normalizedEmail)); } public static bool IsValid(string? email) => !string.IsNullOrWhiteSpace(email) && EmailRegex.IsMatch(email); protected override IEnumerable GetEqualityComponents() { yield return Value; } public override string ToString() => Value; public static implicit operator string(Email email) => email.Value; } ``` ### Result Pattern ```csharp namespace Domain.Common; public class Result { protected Result(bool isSuccess, Error error) { if (isSuccess && error != Error.None || !isSuccess && error == Error.None) { throw new ArgumentException("Invalid error", nameof(error)); } IsSuccess = isSuccess; Error = error; } public bool IsSuccess { get; } public bool IsFailure => !IsSuccess; public Error Error { get; } public static Result Success() => new(true, Error.None); public static Result Failure(Error error) => new(false, error); public static Result Success(T value) => new(value, true, Error.None); public static Result Failure(Error error) => new(default!, false, error); } public class Result : Result { private readonly T _value; protected internal Result(T value, bool isSuccess, Error error) : base(isSuccess, error) { _value = value; } public T Value => IsSuccess ? _value : throw new InvalidOperationException("Cannot access value of failed result"); } public record Error(string Code, string Message) { public static readonly Error None = new(string.Empty, string.Empty); } ``` ## CQRS with MediatR ### Command ```csharp namespace Application.Features.Users.Commands; public record CreateUserCommand : IRequest> { public required string Email { get; init; } public required string Name { get; init; } public required string Password { get; init; } } public class CreateUserCommandValidator : AbstractValidator { public CreateUserCommandValidator() { RuleFor(x => x.Email) .NotEmpty() .EmailAddress() .MaximumLength(255); RuleFor(x => x.Name) .NotEmpty() .MaximumLength(100); RuleFor(x => x.Password) .NotEmpty() .MinimumLength(8) .Matches("[A-Z]").WithMessage("Password must contain uppercase letter") .Matches("[a-z]").WithMessage("Password must contain lowercase letter") .Matches("[0-9]").WithMessage("Password must contain digit") .Matches("[^a-zA-Z0-9]").WithMessage("Password must contain special character"); } } public class CreateUserCommandHandler : IRequestHandler> { private readonly IUserRepository _userRepository; private readonly IUnitOfWork _unitOfWork; private readonly IPasswordHasher _passwordHasher; public CreateUserCommandHandler( IUserRepository userRepository, IUnitOfWork unitOfWork, IPasswordHasher passwordHasher) { _userRepository = userRepository; _unitOfWork = unitOfWork; _passwordHasher = passwordHasher; } public async Task> Handle( CreateUserCommand request, CancellationToken cancellationToken) { if (await _userRepository.ExistsByEmailAsync(request.Email, cancellationToken)) { return Result.Failure(DomainErrors.User.DuplicateEmail); } var userResult = User.Create(request.Email, request.Name); if (userResult.IsFailure) { return Result.Failure(userResult.Error); } var user = userResult.Value; await _userRepository.AddAsync(user, cancellationToken); await _unitOfWork.SaveChangesAsync(cancellationToken); return Result.Success(user.ToDto()); } } ``` ### Query ```csharp namespace Application.Features.Users.Queries; public record GetUserByIdQuery(Guid Id) : IRequest>; public class GetUserByIdQueryHandler : IRequestHandler> { private readonly IUserRepository _userRepository; public GetUserByIdQueryHandler(IUserRepository userRepository) { _userRepository = userRepository; } public async Task> Handle( GetUserByIdQuery request, CancellationToken cancellationToken) { var user = await _userRepository.GetByIdAsync(request.Id, cancellationToken); if (user is null) { return Result.Failure(DomainErrors.User.NotFound); } return Result.Success(user.ToDto()); } } ``` ## API Controllers ```csharp namespace Api.Controllers; [ApiController] [Route("api/[controller]")] public class UsersController : ControllerBase { private readonly ISender _sender; public UsersController(ISender sender) { _sender = sender; } [HttpGet("{id:guid}")] [ProducesResponseType(typeof(UserDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task GetUser(Guid id, CancellationToken cancellationToken) { var result = await _sender.Send(new GetUserByIdQuery(id), cancellationToken); return result.IsSuccess ? Ok(result.Value) : NotFound(CreateProblemDetails(result.Error)); } [HttpPost] [ProducesResponseType(typeof(UserDto), StatusCodes.Status201Created)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)] public async Task CreateUser( [FromBody] CreateUserRequest request, CancellationToken cancellationToken) { var command = new CreateUserCommand { Email = request.Email, Name = request.Name, Password = request.Password }; var result = await _sender.Send(command, cancellationToken); if (result.IsFailure) { return result.Error.Code == "User.DuplicateEmail" ? Conflict(CreateProblemDetails(result.Error)) : BadRequest(CreateProblemDetails(result.Error)); } return CreatedAtAction( nameof(GetUser), new { id = result.Value.Id }, result.Value); } private static ProblemDetails CreateProblemDetails(Error error) => new() { Title = error.Code, Detail = error.Message, Status = StatusCodes.Status400BadRequest }; } ``` ## Commands ```bash # Build and test dotnet build dotnet test dotnet test --collect:"XPlat Code Coverage" # Run specific test project dotnet test tests/Application.Tests # Run with filter dotnet test --filter "FullyQualifiedName~UserService" # Generate coverage report (requires reportgenerator) dotnet tool install -g dotnet-reportgenerator-globaltool reportgenerator -reports:**/coverage.cobertura.xml -targetdir:coveragereport # Run application dotnet run --project src/Api # EF Core migrations dotnet ef migrations add InitialCreate --project src/Infrastructure --startup-project src/Api dotnet ef database update --project src/Infrastructure --startup-project src/Api ``` ## NuGet Dependencies ```xml ```