Files
James Bland befb8fbaeb feat: initial Claude Code configuration scaffold
Comprehensive Claude Code guidance system with:

- 5 agents: tdd-guardian, code-reviewer, security-scanner, refactor-scan, dependency-audit
- 18 skills covering languages (Python, TypeScript, Rust, Go, Java, C#),
  infrastructure (AWS, Azure, GCP, Terraform, Ansible, Docker/K8s, Database, CI/CD),
  testing (TDD, UI, Browser), and patterns (Monorepo, API Design, Observability)
- 3 hooks: secret detection, auto-formatting, TDD git pre-commit
- Strict TDD enforcement with 80%+ coverage requirements
- Multi-model strategy: Opus for planning, Sonnet for execution (opusplan)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 15:47:34 -05:00

18 KiB

name, description
name description
csharp-development 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

using FluentAssertions;
using Moq;
using Xunit;

namespace Application.Tests.Features.Users;

public class CreateUserCommandHandlerTests
{
    private readonly Mock<IUserRepository> _userRepositoryMock;
    private readonly Mock<IUnitOfWork> _unitOfWorkMock;
    private readonly CreateUserCommandHandler _handler;

    public CreateUserCommandHandlerTests()
    {
        _userRepositoryMock = new Mock<IUserRepository>();
        _unitOfWorkMock = new Mock<IUnitOfWork>();
        _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<CancellationToken>()))
            .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<User>(), It.IsAny<CancellationToken>()),
            Times.Once);
        _unitOfWorkMock.Verify(
            x => x.SaveChangesAsync(It.IsAny<CancellationToken>()),
            Times.Once);
    }

    [Fact]
    public async Task Handle_WithDuplicateEmail_ShouldThrowDuplicateEmailException()
    {
        // Arrange
        var command = UserFixtures.CreateUserCommand();
        _userRepositoryMock
            .Setup(x => x.ExistsByEmailAsync(command.Email, It.IsAny<CancellationToken>()))
            .ReturnsAsync(true);

        // Act
        var act = () => _handler.Handle(command, CancellationToken.None);

        // Assert
        await act.Should()
            .ThrowAsync<DuplicateEmailException>()
            .WithMessage($"*{command.Email}*");
    }
}

Test Fixtures

namespace Application.Tests.Fixtures;

public static class UserFixtures
{
    public static User User(Action<User>? 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<CreateUserCommand>? 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<UserDto>? 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)

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<object[]> 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

using Microsoft.AspNetCore.Mvc.Testing;
using System.Net;
using System.Net.Http.Json;

namespace Api.Tests.Controllers;

public class UsersControllerTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;
    private readonly WebApplicationFactory<Program> _factory;

    public UsersControllerTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureServices(services =>
            {
                // Replace services with test doubles
                services.RemoveAll<IUserRepository>();
                services.AddScoped<IUserRepository, InMemoryUserRepository>();
            });
        });
        _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<UserDto>();
        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

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<User> Create(string email, string name)
    {
        var emailResult = Email.Create(email);
        if (emailResult.IsFailure)
        {
            return Result.Failure<User>(emailResult.Error);
        }

        if (string.IsNullOrWhiteSpace(name))
        {
            return Result.Failure<User>(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

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<Email> Create(string? email)
    {
        if (string.IsNullOrWhiteSpace(email))
        {
            return Result.Failure<Email>(DomainErrors.Email.Empty);
        }

        var normalizedEmail = email.Trim().ToLowerInvariant();

        if (!EmailRegex.IsMatch(normalizedEmail))
        {
            return Result.Failure<Email>(DomainErrors.Email.InvalidFormat);
        }

        return Result.Success(new Email(normalizedEmail));
    }

    public static bool IsValid(string? email) =>
        !string.IsNullOrWhiteSpace(email) && EmailRegex.IsMatch(email);

    protected override IEnumerable<object> GetEqualityComponents()
    {
        yield return Value;
    }

    public override string ToString() => Value;

    public static implicit operator string(Email email) => email.Value;
}

Result Pattern

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<T> Success<T>(T value) => new(value, true, Error.None);
    public static Result<T> Failure<T>(Error error) => new(default!, false, error);
}

public class Result<T> : 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

namespace Application.Features.Users.Commands;

public record CreateUserCommand : IRequest<Result<UserDto>>
{
    public required string Email { get; init; }
    public required string Name { get; init; }
    public required string Password { get; init; }
}

public class CreateUserCommandValidator : AbstractValidator<CreateUserCommand>
{
    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<CreateUserCommand, Result<UserDto>>
{
    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<Result<UserDto>> Handle(
        CreateUserCommand request,
        CancellationToken cancellationToken)
    {
        if (await _userRepository.ExistsByEmailAsync(request.Email, cancellationToken))
        {
            return Result.Failure<UserDto>(DomainErrors.User.DuplicateEmail);
        }

        var userResult = User.Create(request.Email, request.Name);
        if (userResult.IsFailure)
        {
            return Result.Failure<UserDto>(userResult.Error);
        }

        var user = userResult.Value;

        await _userRepository.AddAsync(user, cancellationToken);
        await _unitOfWork.SaveChangesAsync(cancellationToken);

        return Result.Success(user.ToDto());
    }
}

Query

namespace Application.Features.Users.Queries;

public record GetUserByIdQuery(Guid Id) : IRequest<Result<UserDto>>;

public class GetUserByIdQueryHandler : IRequestHandler<GetUserByIdQuery, Result<UserDto>>
{
    private readonly IUserRepository _userRepository;

    public GetUserByIdQueryHandler(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }

    public async Task<Result<UserDto>> Handle(
        GetUserByIdQuery request,
        CancellationToken cancellationToken)
    {
        var user = await _userRepository.GetByIdAsync(request.Id, cancellationToken);

        if (user is null)
        {
            return Result.Failure<UserDto>(DomainErrors.User.NotFound);
        }

        return Result.Success(user.ToDto());
    }
}

API Controllers

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<IActionResult> 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<IActionResult> 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

# 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

<!-- Domain.csproj - Minimal dependencies -->
<ItemGroup>
  <PackageReference Include="FluentResults" Version="3.15.2" />
</ItemGroup>

<!-- Application.csproj -->
<ItemGroup>
  <PackageReference Include="MediatR" Version="12.2.0" />
  <PackageReference Include="FluentValidation" Version="11.9.0" />
  <PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.0" />
</ItemGroup>

<!-- Infrastructure.csproj -->
<ItemGroup>
  <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0" />
</ItemGroup>

<!-- Test projects -->
<ItemGroup>
  <PackageReference Include="xunit" Version="2.6.6" />
  <PackageReference Include="xunit.runner.visualstudio" Version="2.5.6" />
  <PackageReference Include="Moq" Version="4.20.70" />
  <PackageReference Include="FluentAssertions" Version="6.12.0" />
  <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.0" />
  <PackageReference Include="coverlet.collector" Version="6.0.0" />
</ItemGroup>