mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-07-16 09:37:23 +00:00
Migrate UserManagement to a vertical slice (Features.UserManagement)
Same pattern as the Breweries migration: Service.UserManagement and the UserAccount repository fold into Features.UserManagement, with MediatR queries replacing UserService. UpdateUserCommand/Handler carry forward IUserService.UpdateAsync as-is (it has no HTTP route today, and adding one is a separate decision from this migration). Adds handler/repository test coverage that didn't exist before, since Service.UserManagement had no test project.
This commit is contained in:
@@ -33,8 +33,8 @@
|
|||||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Sql\Infrastructure.Sql.csproj" />
|
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Sql\Infrastructure.Sql.csproj" />
|
||||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Jwt\Infrastructure.Jwt.csproj" />
|
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Jwt\Infrastructure.Jwt.csproj" />
|
||||||
<ProjectReference Include="..\..\Service\Service.Auth\Service.Auth.csproj" />
|
<ProjectReference Include="..\..\Service\Service.Auth\Service.Auth.csproj" />
|
||||||
<ProjectReference Include="..\..\Service\Service.UserManagement\Service.UserManagement.csproj" />
|
|
||||||
<ProjectReference Include="..\..\Features\Features.Breweries\Features.Breweries.csproj" />
|
<ProjectReference Include="..\..\Features\Features.Breweries\Features.Breweries.csproj" />
|
||||||
|
<ProjectReference Include="..\..\Features\Features.UserManagement\Features.UserManagement.csproj" />
|
||||||
<ProjectReference Include="..\..\Shared\Shared.Contracts\Shared.Contracts.csproj" />
|
<ProjectReference Include="..\..\Shared\Shared.Contracts\Shared.Contracts.csproj" />
|
||||||
<ProjectReference Include="..\..\Shared\Shared.Application\Shared.Application.csproj" />
|
<ProjectReference Include="..\..\Shared\Shared.Application\Shared.Application.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ COPY ["Domain/Domain.Entities/Domain.Entities.csproj", "Domain/Domain.Entities/"
|
|||||||
COPY ["Domain/Domain.Exceptions/Domain.Exceptions.csproj", "Domain/Domain.Exceptions/"]
|
COPY ["Domain/Domain.Exceptions/Domain.Exceptions.csproj", "Domain/Domain.Exceptions/"]
|
||||||
COPY ["Features/Features.Breweries/Features.Breweries.csproj", "Features/Features.Breweries/"]
|
COPY ["Features/Features.Breweries/Features.Breweries.csproj", "Features/Features.Breweries/"]
|
||||||
COPY ["Features/Features.Breweries.Tests/Features.Breweries.Tests.csproj", "Features/Features.Breweries.Tests/"]
|
COPY ["Features/Features.Breweries.Tests/Features.Breweries.Tests.csproj", "Features/Features.Breweries.Tests/"]
|
||||||
|
COPY ["Features/Features.UserManagement/Features.UserManagement.csproj", "Features/Features.UserManagement/"]
|
||||||
|
COPY ["Features/Features.UserManagement.Tests/Features.UserManagement.Tests.csproj", "Features/Features.UserManagement.Tests/"]
|
||||||
COPY ["Infrastructure/Infrastructure.Email/Infrastructure.Email.csproj", "Infrastructure/Infrastructure.Email/"]
|
COPY ["Infrastructure/Infrastructure.Email/Infrastructure.Email.csproj", "Infrastructure/Infrastructure.Email/"]
|
||||||
COPY ["Infrastructure/Infrastructure.Email.Templates/Infrastructure.Email.Templates.csproj", "Infrastructure/Infrastructure.Email.Templates/"]
|
COPY ["Infrastructure/Infrastructure.Email.Templates/Infrastructure.Email.Templates.csproj", "Infrastructure/Infrastructure.Email.Templates/"]
|
||||||
COPY ["Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj", "Infrastructure/Infrastructure.Jwt/"]
|
COPY ["Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj", "Infrastructure/Infrastructure.Jwt/"]
|
||||||
@@ -33,7 +35,6 @@ COPY ["Infrastructure/Infrastructure.Sql/Infrastructure.Sql.csproj", "Infrastruc
|
|||||||
COPY ["Service/Service.Auth/Service.Auth.csproj", "Service/Service.Auth/"]
|
COPY ["Service/Service.Auth/Service.Auth.csproj", "Service/Service.Auth/"]
|
||||||
COPY ["Service/Service.Auth.Tests/Service.Auth.Tests.csproj", "Service/Service.Auth.Tests/"]
|
COPY ["Service/Service.Auth.Tests/Service.Auth.Tests.csproj", "Service/Service.Auth.Tests/"]
|
||||||
COPY ["Service/Service.Emails/Service.Emails.csproj", "Service/Service.Emails/"]
|
COPY ["Service/Service.Emails/Service.Emails.csproj", "Service/Service.Emails/"]
|
||||||
COPY ["Service/Service.UserManagement/Service.UserManagement.csproj", "Service/Service.UserManagement/"]
|
|
||||||
COPY ["Shared/Shared.Application/Shared.Application.csproj", "Shared/Shared.Application/"]
|
COPY ["Shared/Shared.Application/Shared.Application.csproj", "Shared/Shared.Application/"]
|
||||||
COPY ["Shared/Shared.Contracts/Shared.Contracts.csproj", "Shared/Shared.Contracts/"]
|
COPY ["Shared/Shared.Contracts/Shared.Contracts.csproj", "Shared/Shared.Contracts/"]
|
||||||
RUN dotnet restore "Core.slnx"
|
RUN dotnet restore "Core.slnx"
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ using API.Core;
|
|||||||
using API.Core.Authentication;
|
using API.Core.Authentication;
|
||||||
using Features.Breweries.Controllers;
|
using Features.Breweries.Controllers;
|
||||||
using Features.Breweries.DependencyInjection;
|
using Features.Breweries.DependencyInjection;
|
||||||
|
using Features.UserManagement.Controllers;
|
||||||
|
using Features.UserManagement.DependencyInjection;
|
||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
using FluentValidation.AspNetCore;
|
using FluentValidation.AspNetCore;
|
||||||
using Infrastructure.Email;
|
using Infrastructure.Email;
|
||||||
@@ -9,11 +11,9 @@ using Infrastructure.Email.Templates.Rendering;
|
|||||||
using Infrastructure.Jwt;
|
using Infrastructure.Jwt;
|
||||||
using Infrastructure.PasswordHashing;
|
using Infrastructure.PasswordHashing;
|
||||||
using Infrastructure.Repository.Auth;
|
using Infrastructure.Repository.Auth;
|
||||||
using Infrastructure.Repository.UserAccount;
|
|
||||||
using Infrastructure.Sql;
|
using Infrastructure.Sql;
|
||||||
using Service.Auth;
|
using Service.Auth;
|
||||||
using Service.Emails;
|
using Service.Emails;
|
||||||
using Service.UserManagement.User;
|
|
||||||
using Shared.Application.Behaviors;
|
using Shared.Application.Behaviors;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
@@ -23,7 +23,8 @@ builder.Services.AddControllers(options =>
|
|||||||
{
|
{
|
||||||
options.Filters.Add<GlobalExceptionFilter>();
|
options.Filters.Add<GlobalExceptionFilter>();
|
||||||
})
|
})
|
||||||
.AddApplicationPart(typeof(BreweryController).Assembly);
|
.AddApplicationPart(typeof(BreweryController).Assembly)
|
||||||
|
.AddApplicationPart(typeof(UserController).Assembly);
|
||||||
|
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
builder.Services.AddSwaggerGen();
|
builder.Services.AddSwaggerGen();
|
||||||
@@ -32,6 +33,7 @@ builder.Services.AddOpenApi();
|
|||||||
// Add FluentValidation
|
// Add FluentValidation
|
||||||
builder.Services.AddValidatorsFromAssemblyContaining<Program>();
|
builder.Services.AddValidatorsFromAssemblyContaining<Program>();
|
||||||
builder.Services.AddValidatorsFromAssembly(typeof(BreweryController).Assembly);
|
builder.Services.AddValidatorsFromAssembly(typeof(BreweryController).Assembly);
|
||||||
|
builder.Services.AddValidatorsFromAssembly(typeof(UserController).Assembly);
|
||||||
builder.Services.AddFluentValidationAutoValidation();
|
builder.Services.AddFluentValidationAutoValidation();
|
||||||
|
|
||||||
// Add MediatR. Each Features.* slice's assembly is registered here as it's introduced;
|
// Add MediatR. Each Features.* slice's assembly is registered here as it's introduced;
|
||||||
@@ -40,6 +42,7 @@ builder.Services.AddMediatR(cfg =>
|
|||||||
{
|
{
|
||||||
cfg.RegisterServicesFromAssemblyContaining<Program>();
|
cfg.RegisterServicesFromAssemblyContaining<Program>();
|
||||||
cfg.RegisterServicesFromAssembly(typeof(BreweryController).Assembly);
|
cfg.RegisterServicesFromAssembly(typeof(BreweryController).Assembly);
|
||||||
|
cfg.RegisterServicesFromAssembly(typeof(UserController).Assembly);
|
||||||
cfg.AddOpenBehavior(typeof(ValidationBehavior<,>));
|
cfg.AddOpenBehavior(typeof(ValidationBehavior<,>));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -61,11 +64,10 @@ builder.Services.AddSingleton<
|
|||||||
DefaultSqlConnectionFactory
|
DefaultSqlConnectionFactory
|
||||||
>();
|
>();
|
||||||
|
|
||||||
builder.Services.AddScoped<IUserAccountRepository, UserAccountRepository>();
|
|
||||||
builder.Services.AddScoped<IAuthRepository, AuthRepository>();
|
builder.Services.AddScoped<IAuthRepository, AuthRepository>();
|
||||||
builder.Services.AddFeaturesBreweries();
|
builder.Services.AddFeaturesBreweries();
|
||||||
|
builder.Services.AddFeaturesUserManagement();
|
||||||
|
|
||||||
builder.Services.AddScoped<IUserService, UserService>();
|
|
||||||
builder.Services.AddScoped<ILoginService, LoginService>();
|
builder.Services.AddScoped<ILoginService, LoginService>();
|
||||||
builder.Services.AddScoped<IRegisterService, RegisterService>();
|
builder.Services.AddScoped<IRegisterService, RegisterService>();
|
||||||
builder.Services.AddScoped<ITokenService, TokenService>();
|
builder.Services.AddScoped<ITokenService, TokenService>();
|
||||||
|
|||||||
@@ -26,11 +26,12 @@
|
|||||||
<Folder Name="/Features/">
|
<Folder Name="/Features/">
|
||||||
<Project Path="Features/Features.Breweries/Features.Breweries.csproj" />
|
<Project Path="Features/Features.Breweries/Features.Breweries.csproj" />
|
||||||
<Project Path="Features/Features.Breweries.Tests/Features.Breweries.Tests.csproj" />
|
<Project Path="Features/Features.Breweries.Tests/Features.Breweries.Tests.csproj" />
|
||||||
|
<Project Path="Features/Features.UserManagement/Features.UserManagement.csproj" />
|
||||||
|
<Project Path="Features/Features.UserManagement.Tests/Features.UserManagement.Tests.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/Service/">
|
<Folder Name="/Service/">
|
||||||
<Project Path="Service/Service.Auth.Tests/Service.Auth.Tests.csproj" />
|
<Project Path="Service/Service.Auth.Tests/Service.Auth.Tests.csproj" />
|
||||||
<Project Path="Service/Service.Emails/Service.Emails.csproj" />
|
<Project Path="Service/Service.Emails/Service.Emails.csproj" />
|
||||||
<Project Path="Service/Service.UserManagement/Service.UserManagement.csproj" />
|
|
||||||
<Project Path="Service/Service.Auth/Service.Auth.csproj" />
|
<Project Path="Service/Service.Auth/Service.Auth.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/Shared/">
|
<Folder Name="/Shared/">
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using Domain.Entities;
|
||||||
|
using Features.UserManagement.Commands.UpdateUser;
|
||||||
|
using Features.UserManagement.Repository;
|
||||||
|
using Moq;
|
||||||
|
|
||||||
|
namespace Features.UserManagement.Tests.Commands;
|
||||||
|
|
||||||
|
public class UpdateUserHandlerTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_DelegatesToRepository()
|
||||||
|
{
|
||||||
|
var repoMock = new Mock<IUserAccountRepository>();
|
||||||
|
var handler = new UpdateUserHandler(repoMock.Object);
|
||||||
|
var user = new UserAccount { UserAccountId = Guid.NewGuid() };
|
||||||
|
repoMock.Setup(r => r.UpdateAsync(user)).Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
await handler.Handle(new UpdateUserCommand(user), CancellationToken.None);
|
||||||
|
|
||||||
|
repoMock.Verify(r => r.UpdateAsync(user), Times.Once);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<RootNamespace>Features.UserManagement.Tests</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.2" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||||
|
<PackageReference Include="Moq" Version="4.20.72" />
|
||||||
|
<PackageReference Include="FluentAssertions" Version="6.9.0" />
|
||||||
|
<PackageReference Include="DbMocker" Version="1.26.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Features.UserManagement\Features.UserManagement.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using Domain.Entities;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Features.UserManagement.Queries.GetAllUsers;
|
||||||
|
using Features.UserManagement.Repository;
|
||||||
|
using Moq;
|
||||||
|
|
||||||
|
namespace Features.UserManagement.Tests.Queries;
|
||||||
|
|
||||||
|
public class GetAllUsersHandlerTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_PassesLimitAndOffset_ToRepository()
|
||||||
|
{
|
||||||
|
var repoMock = new Mock<IUserAccountRepository>();
|
||||||
|
var handler = new GetAllUsersHandler(repoMock.Object);
|
||||||
|
repoMock.Setup(r => r.GetAllAsync(10, 5)).ReturnsAsync(Array.Empty<UserAccount>());
|
||||||
|
|
||||||
|
var result = await handler.Handle(new GetAllUsersQuery(10, 5), CancellationToken.None);
|
||||||
|
|
||||||
|
result.Should().BeEmpty();
|
||||||
|
repoMock.Verify(r => r.GetAllAsync(10, 5), Times.Once);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using Domain.Entities;
|
||||||
|
using Domain.Exceptions;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Features.UserManagement.Queries.GetUserById;
|
||||||
|
using Features.UserManagement.Repository;
|
||||||
|
using Moq;
|
||||||
|
|
||||||
|
namespace Features.UserManagement.Tests.Queries;
|
||||||
|
|
||||||
|
public class GetUserByIdHandlerTests
|
||||||
|
{
|
||||||
|
private readonly Mock<IUserAccountRepository> _repoMock = new();
|
||||||
|
private readonly GetUserByIdHandler _handler;
|
||||||
|
|
||||||
|
public GetUserByIdHandlerTests()
|
||||||
|
{
|
||||||
|
_handler = new GetUserByIdHandler(_repoMock.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_ReturnsUser_WhenFound()
|
||||||
|
{
|
||||||
|
var user = new UserAccount { UserAccountId = Guid.NewGuid(), Username = "test" };
|
||||||
|
_repoMock.Setup(r => r.GetByIdAsync(user.UserAccountId)).ReturnsAsync(user);
|
||||||
|
|
||||||
|
var result = await _handler.Handle(new GetUserByIdQuery(user.UserAccountId), CancellationToken.None);
|
||||||
|
|
||||||
|
result.Should().Be(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_Throws_WhenNotFound()
|
||||||
|
{
|
||||||
|
var id = Guid.NewGuid();
|
||||||
|
_repoMock.Setup(r => r.GetByIdAsync(id)).ReturnsAsync((UserAccount?)null);
|
||||||
|
|
||||||
|
var act = async () => await _handler.Handle(new GetUserByIdQuery(id), CancellationToken.None);
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<NotFoundException>();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Data.Common;
|
||||||
|
using Infrastructure.Sql;
|
||||||
|
|
||||||
|
namespace Features.UserManagement.Tests.Repository;
|
||||||
|
|
||||||
|
internal class TestConnectionFactory(DbConnection conn) : ISqlConnectionFactory
|
||||||
|
{
|
||||||
|
private readonly DbConnection _conn = conn;
|
||||||
|
|
||||||
|
public DbConnection CreateConnection() => _conn;
|
||||||
|
}
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
using Apps72.Dev.Data.DbMocker;
|
using Apps72.Dev.Data.DbMocker;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Infrastructure.Repository.Tests.Database;
|
using Features.UserManagement.Repository;
|
||||||
using Infrastructure.Repository.UserAccount;
|
|
||||||
|
|
||||||
namespace Infrastructure.Repository.Tests.UserAccount;
|
namespace Features.UserManagement.Tests.Repository;
|
||||||
|
|
||||||
public class UserAccountRepositoryTest
|
public class UserAccountRepositoryTests
|
||||||
{
|
{
|
||||||
private static UserAccountRepository CreateRepo(MockDbConnection conn) =>
|
private static UserAccountRepository CreateRepo(MockDbConnection conn) =>
|
||||||
new(new TestConnectionFactory(conn));
|
new(new TestConnectionFactory(conn));
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace Features.UserManagement.Commands.UpdateUser;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates an existing user account. Not currently exposed via any HTTP route, carried forward
|
||||||
|
/// from <c>IUserService.UpdateAsync</c> as-is.
|
||||||
|
/// </summary>
|
||||||
|
public record UpdateUserCommand(UserAccount UserAccount) : IRequest;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using Features.UserManagement.Repository;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace Features.UserManagement.Commands.UpdateUser;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles <see cref="UpdateUserCommand"/> by persisting changes to an existing user account.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="repository">Repository used to persist the updated user account.</param>
|
||||||
|
public class UpdateUserHandler(IUserAccountRepository repository)
|
||||||
|
: IRequestHandler<UpdateUserCommand>
|
||||||
|
{
|
||||||
|
public Task Handle(UpdateUserCommand request, CancellationToken cancellationToken) =>
|
||||||
|
repository.UpdateAsync(request.UserAccount);
|
||||||
|
}
|
||||||
@@ -1,16 +1,18 @@
|
|||||||
using Domain.Entities;
|
using Domain.Entities;
|
||||||
|
using Features.UserManagement.Queries.GetAllUsers;
|
||||||
|
using Features.UserManagement.Queries.GetUserById;
|
||||||
|
using MediatR;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Service.UserManagement.User;
|
|
||||||
|
|
||||||
namespace API.Core.Controllers
|
namespace Features.UserManagement.Controllers
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides read-only endpoints for retrieving user accounts.
|
/// Provides read-only endpoints for retrieving user accounts.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="userService">Service used to query user account data.</param>
|
/// <param name="mediator">Used to dispatch user queries to their handlers.</param>
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
public class UserController(IUserService userService) : ControllerBase
|
public class UserController(IMediator mediator) : ControllerBase
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves a paginated list of user accounts.
|
/// Retrieves a paginated list of user accounts.
|
||||||
@@ -24,7 +26,7 @@ namespace API.Core.Controllers
|
|||||||
[FromQuery] int? offset
|
[FromQuery] int? offset
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var users = await userService.GetAllAsync(limit, offset);
|
var users = await mediator.Send(new GetAllUsersQuery(limit, offset));
|
||||||
return Ok(users);
|
return Ok(users);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +38,7 @@ namespace API.Core.Controllers
|
|||||||
[HttpGet("{id:guid}")]
|
[HttpGet("{id:guid}")]
|
||||||
public async Task<ActionResult<UserAccount>> GetById(Guid id)
|
public async Task<ActionResult<UserAccount>> GetById(Guid id)
|
||||||
{
|
{
|
||||||
var user = await userService.GetByIdAsync(id);
|
var user = await mediator.Send(new GetUserByIdQuery(id));
|
||||||
return Ok(user);
|
return Ok(user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using Features.UserManagement.Repository;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Features.UserManagement.DependencyInjection;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers the services owned by the UserManagement feature slice.
|
||||||
|
/// </summary>
|
||||||
|
public static class FeaturesUserManagementServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddFeaturesUserManagement(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddScoped<IUserAccountRepository, UserAccountRepository>();
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<RootNamespace>Features.UserManagement</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\Domain\Domain.Entities\Domain.Entities.csproj" />
|
||||||
|
<ProjectReference Include="..\..\Domain\Domain.Exceptions\Domain.Exceptions.csproj" />
|
||||||
|
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Sql\Infrastructure.Sql.csproj" />
|
||||||
|
<ProjectReference Include="..\..\Shared\Shared.Contracts\Shared.Contracts.csproj" />
|
||||||
|
<ProjectReference Include="..\..\Shared\Shared.Application\Shared.Application.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using Domain.Entities;
|
||||||
|
using Features.UserManagement.Repository;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace Features.UserManagement.Queries.GetAllUsers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles <see cref="GetAllUsersQuery"/> by retrieving a paginated list of user accounts.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="repository">Repository used to query user account data.</param>
|
||||||
|
public class GetAllUsersHandler(IUserAccountRepository repository)
|
||||||
|
: IRequestHandler<GetAllUsersQuery, IEnumerable<UserAccount>>
|
||||||
|
{
|
||||||
|
public Task<IEnumerable<UserAccount>> Handle(GetAllUsersQuery request, CancellationToken cancellationToken) =>
|
||||||
|
repository.GetAllAsync(request.Limit, request.Offset);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace Features.UserManagement.Queries.GetAllUsers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves a paginated list of user accounts.
|
||||||
|
/// </summary>
|
||||||
|
public record GetAllUsersQuery(int? Limit, int? Offset) : IRequest<IEnumerable<UserAccount>>;
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using Domain.Entities;
|
||||||
|
using Domain.Exceptions;
|
||||||
|
using Features.UserManagement.Repository;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace Features.UserManagement.Queries.GetUserById;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles <see cref="GetUserByIdQuery"/> by looking up the matching user account.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="repository">Repository used to query user account data.</param>
|
||||||
|
public class GetUserByIdHandler(IUserAccountRepository repository)
|
||||||
|
: IRequestHandler<GetUserByIdQuery, UserAccount>
|
||||||
|
{
|
||||||
|
/// <exception cref="NotFoundException">Thrown when no user account exists with the given ID.</exception>
|
||||||
|
public async Task<UserAccount> Handle(GetUserByIdQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var user = await repository.GetByIdAsync(request.UserAccountId);
|
||||||
|
if (user is null)
|
||||||
|
throw new NotFoundException($"User with ID {request.UserAccountId} not found");
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace Features.UserManagement.Queries.GetUserById;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves a single user account by its unique identifier.
|
||||||
|
/// </summary>
|
||||||
|
public record GetUserByIdQuery(Guid UserAccountId) : IRequest<UserAccount>;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Infrastructure.Repository.UserAccount;
|
namespace Features.UserManagement.Repository;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Repository for CRUD operations on user account records.
|
/// Repository for CRUD operations on user account records.
|
||||||
@@ -2,7 +2,7 @@ using System.Data;
|
|||||||
using System.Data.Common;
|
using System.Data.Common;
|
||||||
using Infrastructure.Sql;
|
using Infrastructure.Sql;
|
||||||
|
|
||||||
namespace Infrastructure.Repository.UserAccount;
|
namespace Features.UserManagement.Repository;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ADO.NET-based implementation of <see cref="IUserAccountRepository"/> backed by SQL Server
|
/// ADO.NET-based implementation of <see cref="IUserAccountRepository"/> backed by SQL Server
|
||||||
@@ -10,7 +10,7 @@ namespace Infrastructure.Repository.UserAccount;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="connectionFactory">The factory used to create database connections.</param>
|
/// <param name="connectionFactory">The factory used to create database connections.</param>
|
||||||
public class UserAccountRepository(ISqlConnectionFactory connectionFactory)
|
public class UserAccountRepository(ISqlConnectionFactory connectionFactory)
|
||||||
: Repository<Domain.Entities.UserAccount>(connectionFactory),
|
: Infrastructure.Sql.Repository<Domain.Entities.UserAccount>(connectionFactory),
|
||||||
IUserAccountRepository
|
IUserAccountRepository
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\..\Domain\Domain.Exceptions\Domain.Exceptions.csproj" />
|
|
||||||
<ProjectReference
|
|
||||||
Include="..\..\Infrastructure\Infrastructure.Repository\Infrastructure.Repository.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
</Project>
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
using Domain.Entities;
|
|
||||||
|
|
||||||
namespace Service.UserManagement.User;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Defines operations for retrieving and updating user accounts.
|
|
||||||
/// </summary>
|
|
||||||
public interface IUserService
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieves all user accounts, optionally paginated.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="limit">The maximum number of results to return, or <c>null</c> for no limit.</param>
|
|
||||||
/// <param name="offset">The number of results to skip, or <c>null</c> to start from the beginning.</param>
|
|
||||||
/// <returns>A collection of <see cref="UserAccount"/> entities.</returns>
|
|
||||||
Task<IEnumerable<UserAccount>> GetAllAsync(
|
|
||||||
int? limit = null,
|
|
||||||
int? offset = null
|
|
||||||
);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieves a user account by its unique identifier.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="id">The unique identifier of the user account.</param>
|
|
||||||
/// <returns>The matching <see cref="UserAccount"/>.</returns>
|
|
||||||
Task<UserAccount> GetByIdAsync(Guid id);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Updates an existing user account.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="userAccount">The user account containing the updated data.</param>
|
|
||||||
/// <returns>A task that completes once the update has finished.</returns>
|
|
||||||
Task UpdateAsync(UserAccount userAccount);
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
using Domain.Entities;
|
|
||||||
using Domain.Exceptions;
|
|
||||||
using Infrastructure.Repository.UserAccount;
|
|
||||||
|
|
||||||
namespace Service.UserManagement.User;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handles retrieval and update of user accounts.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="repository">Repository used to persist and query user account data.</param>
|
|
||||||
public class UserService(IUserAccountRepository repository) : IUserService
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieves all user accounts, optionally paginated.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="limit">The maximum number of results to return, or <c>null</c> for no limit.</param>
|
|
||||||
/// <param name="offset">The number of results to skip, or <c>null</c> to start from the beginning.</param>
|
|
||||||
/// <returns>A collection of <see cref="UserAccount"/> entities.</returns>
|
|
||||||
public async Task<IEnumerable<UserAccount>> GetAllAsync(
|
|
||||||
int? limit = null,
|
|
||||||
int? offset = null
|
|
||||||
)
|
|
||||||
{
|
|
||||||
return await repository.GetAllAsync(limit, offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieves a user account by its unique identifier.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="id">The unique identifier of the user account.</param>
|
|
||||||
/// <returns>The matching <see cref="UserAccount"/>.</returns>
|
|
||||||
/// <exception cref="NotFoundException">Thrown when no user account exists with the given <paramref name="id"/>.</exception>
|
|
||||||
public async Task<UserAccount> GetByIdAsync(Guid id)
|
|
||||||
{
|
|
||||||
var user = await repository.GetByIdAsync(id);
|
|
||||||
if (user is null)
|
|
||||||
throw new NotFoundException($"User with ID {id} not found");
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Updates an existing user account.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="userAccount">The user account containing the updated data.</param>
|
|
||||||
/// <returns>A task that completes once the update has finished.</returns>
|
|
||||||
public async Task UpdateAsync(UserAccount userAccount)
|
|
||||||
{
|
|
||||||
await repository.UpdateAsync(userAccount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user