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:
666
.claude/skills/languages/csharp/SKILL.md
Normal file
666
.claude/skills/languages/csharp/SKILL.md
Normal 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>
|
||||
```
|
||||
Reference in New Issue
Block a user