Service refactor (#153)

* remove email out of register service

* Update auth service, move JWT handling out of controller

* add docker config for service auth test

* Update mock email system

* Format: ./src/Core/Service

* Refactor authentication payloads and services for registration and login processes

* Format: src/Core/API, src/Core/Service
This commit is contained in:
Aaron Po
2026-02-16 15:12:59 -05:00
committed by GitHub
parent 0d52c937ce
commit 2cad88e3f6
31 changed files with 762 additions and 484 deletions

View File

@@ -0,0 +1,21 @@
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["Domain.Entities/Domain.Entities.csproj", "Domain.Entities/"]
COPY ["Domain.Exceptions/Domain.Exceptions.csproj", "Domain.Exceptions/"]
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.Jwt/Infrastructure.Jwt.csproj", "Infrastructure/Infrastructure.Jwt/"]
COPY ["Infrastructure/Infrastructure.Repository/Infrastructure.Repository.csproj", "Infrastructure/Infrastructure.Repository/"]
COPY ["Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj", "Infrastructure/Infrastructure.PasswordHashing/"]
COPY ["Service/Service.Auth/Service.Auth.csproj", "Service/Service.Auth/"]
COPY ["Service/Service.Auth.Tests/Service.Auth.Tests.csproj", "Service/Service.Auth.Tests/"]
RUN dotnet restore "Service/Service.Auth.Tests/Service.Auth.Tests.csproj"
COPY . .
WORKDIR "/src/Service/Service.Auth.Tests"
RUN dotnet build "./Service.Auth.Tests.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS final
RUN mkdir -p /app/test-results/service-auth-tests
WORKDIR /src/Service/Service.Auth.Tests
ENTRYPOINT ["dotnet", "test", "./Service.Auth.Tests.csproj", "-c", "Release", "--logger", "trx;LogFileName=/app/test-results/service-auth-tests/results.trx"]

View File

@@ -11,15 +11,18 @@ public class LoginServiceTest
{
private readonly Mock<IAuthRepository> _authRepoMock;
private readonly Mock<IPasswordInfrastructure> _passwordInfraMock;
private readonly Mock<ITokenService> _tokenServiceMock;
private readonly LoginService _loginService;
public LoginServiceTest()
{
_authRepoMock = new Mock<IAuthRepository>();
_passwordInfraMock = new Mock<IPasswordInfrastructure>();
_tokenServiceMock = new Mock<ITokenService>();
_loginService = new LoginService(
_authRepoMock.Object,
_passwordInfraMock.Object
_passwordInfraMock.Object,
_tokenServiceMock.Object
);
}
@@ -63,13 +66,26 @@ public class LoginServiceTest
.Setup(x => x.Verify(It.IsAny<string>(), It.IsAny<string>()))
.Returns(true);
_tokenServiceMock
.Setup(x => x.GenerateAccessToken(It.IsAny<UserAccount>()))
.Returns("access-token");
_tokenServiceMock
.Setup(x => x.GenerateRefreshToken(It.IsAny<UserAccount>()))
.Returns("refresh-token");
// Act
var result = await _loginService.LoginAsync(username, It.IsAny<string>());
var result = await _loginService.LoginAsync(
username,
It.IsAny<string>()
);
// Assert
result.Should().NotBeNull();
result.UserAccountId.Should().Be(userAccountId);
result.Username.Should().Be(username);
result.UserAccount.UserAccountId.Should().Be(userAccountId);
result.UserAccount.Username.Should().Be(username);
result.AccessToken.Should().Be("access-token");
result.RefreshToken.Should().Be("refresh-token");
_authRepoMock.Verify(
x => x.GetActiveCredentialByUserAccountIdAsync(userAccountId),

View File

@@ -1,11 +1,10 @@
using Domain.Entities;
using Domain.Exceptions;
using FluentAssertions;
using Infrastructure.Email;
using Infrastructure.Email.Templates.Rendering;
using Infrastructure.PasswordHashing;
using Infrastructure.Repository.Auth;
using Moq;
using Service.Emails;
namespace Service.Auth.Tests;
@@ -13,27 +12,27 @@ public class RegisterServiceTest
{
private readonly Mock<IAuthRepository> _authRepoMock;
private readonly Mock<IPasswordInfrastructure> _passwordInfraMock;
private readonly Mock<IEmailProvider> _emailProviderMock;
private readonly Mock<IEmailTemplateProvider> _emailTemplateProviderMock;
private readonly Mock<ITokenService> _tokenServiceMock;
private readonly Mock<IEmailService> _emailServiceMock; // todo handle email related test cases here
private readonly RegisterService _registerService;
public RegisterServiceTest()
{
_authRepoMock = new Mock<IAuthRepository>();
_passwordInfraMock = new Mock<IPasswordInfrastructure>();
_emailProviderMock = new Mock<IEmailProvider>();
_emailTemplateProviderMock = new Mock<IEmailTemplateProvider>();
_tokenServiceMock = new Mock<ITokenService>();
_emailServiceMock = new Mock<IEmailService>();
_registerService = new RegisterService(
_authRepoMock.Object,
_passwordInfraMock.Object,
_emailProviderMock.Object,
_emailTemplateProviderMock.Object
_tokenServiceMock.Object,
_emailServiceMock.Object
);
}
[Fact]
public async Task RegisterAsync_WithValidData_CreatesUserAndSendsEmail()
public async Task RegisterAsync_WithValidData_CreatesUserAndReturnsAuthServiceReturn()
{
// Arrange
var userAccount = new UserAccount
@@ -48,7 +47,6 @@ public class RegisterServiceTest
const string password = "SecurePassword123!";
const string hashedPassword = "hashed_password_value";
var expectedUserId = Guid.NewGuid();
const string expectedEmailHtml = "<html><body>Welcome!</body></html>";
// Mock: No existing user
_authRepoMock
@@ -89,36 +87,28 @@ public class RegisterServiceTest
}
);
// Mock: Email template rendering
_emailTemplateProviderMock
.Setup(x =>
x.RenderUserRegisteredEmailAsync(
userAccount.FirstName,
It.IsAny<string>()
)
)
.ReturnsAsync(expectedEmailHtml);
// Mock: Token generation
_tokenServiceMock
.Setup(x => x.GenerateAccessToken(It.IsAny<UserAccount>()))
.Returns("access-token");
// Mock: Email sending
_emailProviderMock
.Setup(x =>
x.SendAsync(
userAccount.Email,
"Welcome to The Biergarten App!",
expectedEmailHtml,
true
)
)
.Returns(Task.CompletedTask);
_tokenServiceMock
.Setup(x => x.GenerateRefreshToken(It.IsAny<UserAccount>()))
.Returns("refresh-token");
// Act
var result = await _registerService.RegisterAsync(userAccount, password);
var result = await _registerService.RegisterAsync(
userAccount,
password
);
// Assert
result.Should().NotBeNull();
result.UserAccountId.Should().Be(expectedUserId);
result.Username.Should().Be(userAccount.Username);
result.Email.Should().Be(userAccount.Email);
result.UserAccount.UserAccountId.Should().Be(expectedUserId);
result.UserAccount.Username.Should().Be(userAccount.Username);
result.UserAccount.Email.Should().Be(userAccount.Email);
result.AccessToken.Should().Be("access-token");
result.RefreshToken.Should().Be("refresh-token");
// Verify all mocks were called as expected
_authRepoMock.Verify(
@@ -142,24 +132,14 @@ public class RegisterServiceTest
),
Times.Once
);
_emailTemplateProviderMock.Verify(
_emailServiceMock.Verify(
x =>
x.RenderUserRegisteredEmailAsync(
userAccount.FirstName,
x.SendRegistrationEmailAsync(
It.IsAny<UserAccount>(),
It.IsAny<string>()
),
Times.Once
);
_emailProviderMock.Verify(
x =>
x.SendAsync(
userAccount.Email,
"Welcome to The Biergarten App!",
expectedEmailHtml,
true
),
Times.Once
);
}
[Fact]
@@ -195,7 +175,8 @@ public class RegisterServiceTest
.ReturnsAsync((UserAccount?)null);
// Act
var act = async () => await _registerService.RegisterAsync(userAccount, password);
var act = async () =>
await _registerService.RegisterAsync(userAccount, password);
// Assert
await act.Should()
@@ -215,18 +196,6 @@ public class RegisterServiceTest
),
Times.Never
);
// Verify email was never sent
_emailProviderMock.Verify(
x =>
x.SendAsync(
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<bool>()
),
Times.Never
);
}
[Fact]
@@ -262,7 +231,8 @@ public class RegisterServiceTest
.ReturnsAsync(existingUser);
// Act
var act = async () => await _registerService.RegisterAsync(userAccount, password);
var act = async () =>
await _registerService.RegisterAsync(userAccount, password);
// Assert
await act.Should()
@@ -323,14 +293,13 @@ public class RegisterServiceTest
)
.ReturnsAsync(new UserAccount { UserAccountId = Guid.NewGuid() });
_emailTemplateProviderMock
.Setup(x =>
x.RenderUserRegisteredEmailAsync(
It.IsAny<string>(),
It.IsAny<string>()
)
)
.ReturnsAsync("<html></html>");
_tokenServiceMock
.Setup(x => x.GenerateAccessToken(It.IsAny<UserAccount>()))
.Returns("access-token");
_tokenServiceMock
.Setup(x => x.GenerateRefreshToken(It.IsAny<UserAccount>()))
.Returns("refresh-token");
// Act
await _registerService.RegisterAsync(userAccount, plainPassword);
@@ -350,152 +319,4 @@ public class RegisterServiceTest
Times.Once
);
}
[Fact]
public async Task RegisterAsync_EmailConfirmationLink_ContainsUserEmail()
{
// Arrange
var userAccount = new UserAccount
{
Username = "testuser",
FirstName = "Test",
LastName = "User",
Email = "test@example.com",
DateOfBirth = new DateTime(1990, 1, 1),
};
var password = "Password123!";
string? capturedConfirmationLink = null;
_authRepoMock
.Setup(x => x.GetUserByUsernameAsync(It.IsAny<string>()))
.ReturnsAsync((UserAccount?)null);
_authRepoMock
.Setup(x => x.GetUserByEmailAsync(It.IsAny<string>()))
.ReturnsAsync((UserAccount?)null);
_passwordInfraMock
.Setup(x => x.Hash(It.IsAny<string>()))
.Returns("hashed");
_authRepoMock
.Setup(x =>
x.RegisterUserAsync(
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<DateTime>(),
It.IsAny<string>()
)
)
.ReturnsAsync(
new UserAccount
{
UserAccountId = Guid.NewGuid(),
Username = userAccount.Username,
FirstName = userAccount.FirstName,
LastName = userAccount.LastName,
Email = userAccount.Email,
DateOfBirth = userAccount.DateOfBirth,
}
);
_emailTemplateProviderMock
.Setup(x =>
x.RenderUserRegisteredEmailAsync(
It.IsAny<string>(),
It.IsAny<string>()
)
)
.Callback<string, string>(
(_, link) => capturedConfirmationLink = link
)
.ReturnsAsync("<html></html>");
// Act
await _registerService.RegisterAsync(userAccount, password);
// Assert
capturedConfirmationLink.Should().NotBeNull();
capturedConfirmationLink
.Should()
.Contain(Uri.EscapeDataString(userAccount.Email));
}
[Fact]
public async Task RegisterAsync_WhenEmailSendingFails_ExceptionPropagates()
{
// Arrange
var userAccount = new UserAccount
{
Username = "testuser",
FirstName = "Test",
LastName = "User",
Email = "test@example.com",
DateOfBirth = new DateTime(1990, 1, 1),
};
var password = "Password123!";
_authRepoMock
.Setup(x => x.GetUserByUsernameAsync(It.IsAny<string>()))
.ReturnsAsync((UserAccount?)null);
_authRepoMock
.Setup(x => x.GetUserByEmailAsync(It.IsAny<string>()))
.ReturnsAsync((UserAccount?)null);
_passwordInfraMock
.Setup(x => x.Hash(It.IsAny<string>()))
.Returns("hashed");
_authRepoMock
.Setup(x =>
x.RegisterUserAsync(
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<DateTime>(),
It.IsAny<string>()
)
)
.ReturnsAsync(
new UserAccount
{
UserAccountId = Guid.NewGuid(),
Email = userAccount.Email,
}
);
_emailTemplateProviderMock
.Setup(x =>
x.RenderUserRegisteredEmailAsync(
It.IsAny<string>(),
It.IsAny<string>()
)
)
.ReturnsAsync("<html></html>");
_emailProviderMock
.Setup(x =>
x.SendAsync(
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<bool>()
)
)
.ThrowsAsync(
new InvalidOperationException("SMTP server unavailable")
);
// Act
var act = async () => await _registerService.RegisterAsync(userAccount, password);
// Assert
await act.Should()
.ThrowAsync<InvalidOperationException>()
.WithMessage("SMTP server unavailable");
}
}

View File

@@ -4,5 +4,5 @@ namespace Service.Auth;
public interface ILoginService
{
Task<UserAccount> LoginAsync(string username, string password);
Task<LoginServiceReturn> LoginAsync(string username, string password);
}

View File

@@ -2,7 +2,38 @@ using Domain.Entities;
namespace Service.Auth;
public record RegisterServiceReturn
{
public bool IsAuthenticated { get; init; } = false;
public bool EmailSent { get; init; } = false;
public UserAccount UserAccount { get; init; }
public string AccessToken { get; init; } = string.Empty;
public string RefreshToken { get; init; } = string.Empty;
public RegisterServiceReturn(
UserAccount userAccount,
string accessToken,
string refreshToken,
bool emailSent
)
{
IsAuthenticated = true;
EmailSent = emailSent;
UserAccount = userAccount;
AccessToken = accessToken;
RefreshToken = refreshToken;
}
public RegisterServiceReturn(UserAccount userAccount)
{
UserAccount = userAccount;
}
}
public interface IRegisterService
{
Task<UserAccount> RegisterAsync(UserAccount userAccount, string password);
Task<RegisterServiceReturn> RegisterAsync(
UserAccount userAccount,
string password
);
}

View File

@@ -0,0 +1,34 @@
using Domain.Entities;
using Infrastructure.Jwt;
namespace Service.Auth;
public interface ITokenService
{
public string GenerateAccessToken(UserAccount user);
public string GenerateRefreshToken(UserAccount user);
}
public class TokenService(ITokenInfrastructure tokenInfrastructure)
: ITokenService
{
public string GenerateAccessToken(UserAccount userAccount)
{
var jwtExpiresAt = DateTime.UtcNow.AddHours(1);
return tokenInfrastructure.GenerateJwt(
userAccount.UserAccountId,
userAccount.Username,
jwtExpiresAt
);
}
public string GenerateRefreshToken(UserAccount userAccount)
{
var jwtExpiresAt = DateTime.UtcNow.AddDays(21);
return tokenInfrastructure.GenerateJwt(
userAccount.UserAccountId,
userAccount.Username,
jwtExpiresAt
);
}
}

View File

@@ -5,30 +5,42 @@ using Infrastructure.Repository.Auth;
namespace Service.Auth;
public record LoginServiceReturn(
UserAccount UserAccount,
string RefreshToken,
string AccessToken
);
public class LoginService(
IAuthRepository authRepo,
IPasswordInfrastructure passwordInfrastructure
IPasswordInfrastructure passwordInfrastructure,
ITokenService tokenService
) : ILoginService
{
public async Task<UserAccount> LoginAsync(string username, string password)
public async Task<LoginServiceReturn> LoginAsync(
string username,
string password
)
{
// Attempt lookup by username
var user = await authRepo.GetUserByUsernameAsync(username);
// the user was not found
if (user is null)
throw new UnauthorizedException("Invalid username or password.");
var user =
await authRepo.GetUserByUsernameAsync(username)
?? throw new UnauthorizedException("Invalid username or password.");
// @todo handle expired passwords
var activeCred = await authRepo.GetActiveCredentialByUserAccountIdAsync(user.UserAccountId);
if (activeCred is null)
throw new UnauthorizedException("Invalid username or password.");
var activeCred =
await authRepo.GetActiveCredentialByUserAccountIdAsync(
user.UserAccountId
)
?? throw new UnauthorizedException("Invalid username or password.");
if (!passwordInfrastructure.Verify(password, activeCred.Hash))
throw new UnauthorizedException("Invalid username or password.");
return user;
string accessToken = tokenService.GenerateAccessToken(user);
string refreshToken = tokenService.GenerateRefreshToken(user);
return new LoginServiceReturn(user, refreshToken, accessToken);
}
}

View File

@@ -4,28 +4,40 @@ using Infrastructure.Email;
using Infrastructure.Email.Templates.Rendering;
using Infrastructure.PasswordHashing;
using Infrastructure.Repository.Auth;
using Microsoft.Extensions.Logging;
using Service.Emails;
namespace Service.Auth;
public class RegisterService(
IAuthRepository authRepo,
IPasswordInfrastructure passwordInfrastructure,
IEmailProvider emailProvider,
IEmailTemplateProvider emailTemplateProvider
ITokenService tokenService,
IEmailService emailService
) : IRegisterService
{
public async Task<UserAccount> RegisterAsync(UserAccount userAccount, string password)
private async Task ValidateUserDoesNotExist(UserAccount userAccount)
{
// Check if user already exists
var existingUsername = await authRepo.GetUserByUsernameAsync(userAccount.Username);
var existingEmail = await authRepo.GetUserByEmailAsync(userAccount.Email);
var existingUsername = await authRepo.GetUserByUsernameAsync(
userAccount.Username
);
var existingEmail = await authRepo.GetUserByEmailAsync(
userAccount.Email
);
if (existingUsername != null || existingEmail != null)
{
throw new ConflictException("Username or email already exists");
}
}
public async Task<RegisterServiceReturn> RegisterAsync(
UserAccount userAccount,
string password
)
{
await ValidateUserDoesNotExist(userAccount);
// password hashing
var hashed = passwordInfrastructure.Hash(password);
@@ -36,26 +48,41 @@ public class RegisterService(
userAccount.LastName,
userAccount.Email,
userAccount.DateOfBirth,
hashed);
// Generate confirmation link (TODO: implement proper token-based confirmation)
var confirmationLink = $"https://thebiergarten.app/confirm?email={Uri.EscapeDataString(createdUser.Email)}";
// Render email template
var emailHtml = await emailTemplateProvider.RenderUserRegisteredEmailAsync(
createdUser.FirstName,
confirmationLink
hashed
);
// Send welcome email with rendered template
await emailProvider.SendAsync(
createdUser.Email,
"Welcome to The Biergarten App!",
emailHtml,
isHtml: true
);
var accessToken = tokenService.GenerateAccessToken(createdUser);
var refreshToken = tokenService.GenerateRefreshToken(createdUser);
return createdUser;
if (
string.IsNullOrEmpty(accessToken)
|| string.IsNullOrEmpty(refreshToken)
)
{
return new RegisterServiceReturn(createdUser);
}
bool emailSent = false;
try
{
// send confirmation email
await emailService.SendRegistrationEmailAsync(
createdUser,
"some-confirmation-token"
);
emailSent = true;
}
catch
{
// ignored
}
return new RegisterServiceReturn(
createdUser,
accessToken,
refreshToken,
emailSent
);
}
}

View File

@@ -5,20 +5,14 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Konscious.Security.Cryptography.Argon2" Version="1.3.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Domain.Entities\Domain.Entities.csproj" />
<ProjectReference Include="..\..\Domain.Exceptions\Domain.Exceptions.csproj" />
<ProjectReference
Include="..\..\Infrastructure\Infrastructure.Email\Infrastructure.Email.csproj" />
<ProjectReference
Include="..\..\Infrastructure\Infrastructure.Email.Templates\Infrastructure.Email.Templates.csproj" />
<ProjectReference
Include="..\..\Infrastructure\Infrastructure.Repository\Infrastructure.Repository.csproj" />
<ProjectReference
Include="..\..\Infrastructure\Infrastructure.PasswordHashing\Infrastructure.PasswordHashing.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Email\Infrastructure.Email.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Email.Templates\Infrastructure.Email.Templates.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Jwt\Infrastructure.Jwt.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Repository\Infrastructure.Repository.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.PasswordHashing\Infrastructure.PasswordHashing.csproj" />
<ProjectReference Include="..\Service.Emails\Service.Emails.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,41 @@
using Domain.Entities;
using Infrastructure.Email;
using Infrastructure.Email.Templates.Rendering;
namespace Service.Emails;
public interface IEmailService
{
public Task SendRegistrationEmailAsync(
UserAccount createdUser,
string confirmationToken
);
}
public class EmailService(
IEmailProvider emailProvider,
IEmailTemplateProvider emailTemplateProvider
) : IEmailService
{
public async Task SendRegistrationEmailAsync(
UserAccount createdUser,
string confirmationToken
)
{
var confirmationLink =
$"https://thebiergarten.app/confirm?token={confirmationToken}";
var emailHtml =
await emailTemplateProvider.RenderUserRegisteredEmailAsync(
createdUser.FirstName,
confirmationLink
);
await emailProvider.SendAsync(
createdUser.Email,
"Welcome to The Biergarten App!",
emailHtml,
isHtml: true
);
}
}

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Domain.Entities\Domain.Entities.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Email.Templates\Infrastructure.Email.Templates.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Email\Infrastructure.Email.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,15 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Domain.Exceptions\Domain.Exceptions.csproj" />
<ProjectReference
Include="..\..\Infrastructure\Infrastructure.Repository\Infrastructure.Repository.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Domain.Exceptions\Domain.Exceptions.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Repository\Infrastructure.Repository.csproj" />
</ItemGroup>
</Project>

View File

@@ -4,7 +4,10 @@ namespace Service.UserManagement.User;
public interface IUserService
{
Task<IEnumerable<UserAccount>> GetAllAsync(int? limit = null, int? offset = null);
Task<IEnumerable<UserAccount>> GetAllAsync(
int? limit = null,
int? offset = null
);
Task<UserAccount> GetByIdAsync(Guid id);
Task UpdateAsync(UserAccount userAccount);

View File

@@ -6,7 +6,10 @@ namespace Service.UserManagement.User;
public class UserService(IUserAccountRepository repository) : IUserService
{
public async Task<IEnumerable<UserAccount>> GetAllAsync(int? limit = null, int? offset = null)
public async Task<IEnumerable<UserAccount>> GetAllAsync(
int? limit = null,
int? offset = null
)
{
return await repository.GetAllAsync(limit, offset);
}