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>
This commit is contained in:
2026-01-20 15:47:34 -05:00
commit befb8fbaeb
34 changed files with 12233 additions and 0 deletions

View File

@@ -0,0 +1,666 @@
---
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<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
```csharp
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)
```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<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
```csharp
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
```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<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
```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<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
```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<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
```csharp
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
```csharp
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
```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<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
```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
<!-- 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>
```