formatting

This commit is contained in:
Aaron Po
2026-08-24 01:08:55 -04:00
parent c825d99b0a
commit b64c42ebde
27 changed files with 166 additions and 136 deletions

View File

@@ -1020,9 +1020,7 @@ public class AuthSteps(ScenarioContext scenario)
public async Task WhenISubmitAResendConfirmationRequestWithoutAnAccessToken() public async Task WhenISubmitAResendConfirmationRequestWithoutAnAccessToken()
{ {
HttpClient client = GetClient(); HttpClient client = GetClient();
Guid userId = scenario.TryGetValue(RegisteredUserIdKey, out Guid id) Guid userId = scenario.TryGetValue(RegisteredUserIdKey, out Guid id) ? id : Guid.NewGuid();
? id
: Guid.NewGuid();
HttpRequestMessage requestMessage = new( HttpRequestMessage requestMessage = new(
HttpMethod.Post, HttpMethod.Post,

View File

@@ -8,20 +8,23 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="idunno.Password.Generator" Version="2.0.0"/> <PackageReference Include="idunno.Password.Generator" Version="2.0.0" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.0.2"/> <PackageReference Include="Microsoft.Data.SqlClient" Version="7.0.2" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.11"/> <PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.11" />
<PackageReference Include="Spectre.Console" Version="0.57.2"/> <PackageReference Include="Spectre.Console" Version="0.57.2" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Domain\Domain.Entities\Domain.Entities.csproj"/> <ProjectReference Include="..\..\Domain\Domain.Entities\Domain.Entities.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Sql\Infrastructure.Sql.csproj"/> <ProjectReference Include="..\..\Infrastructure\Infrastructure.Sql\Infrastructure.Sql.csproj" />
<ProjectReference Include="..\..\Features\Features.Breweries\Features.Breweries.csproj"/> <ProjectReference Include="..\..\Features\Features.Breweries\Features.Breweries.csproj" />
<ProjectReference Include="..\..\Features\Features.Auth\Features.Auth.csproj"/> <ProjectReference Include="..\..\Features\Features.Auth\Features.Auth.csproj" />
<ProjectReference Include="..\..\Features\Features.Locations\Features.Locations.csproj"/> <ProjectReference Include="..\..\Features\Features.Locations\Features.Locations.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None CopyToOutputDirectory="PreserveNewest" Include="SeedData\biergarten_seed_2026-08-24T01-52-09.841693Z.sqlite"/> <None
CopyToOutputDirectory="PreserveNewest"
Include="SeedData\biergarten_seed_2026-08-24T01-52-09.841693Z.sqlite"
/>
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -12,5 +12,6 @@ public class ConnectionStrings
public static string GetSqlServerConnectionString(IConfiguration configuration) => public static string GetSqlServerConnectionString(IConfiguration configuration) =>
SqlConnectionStringHelper.BuildConnectionString(configuration); SqlConnectionStringHelper.BuildConnectionString(configuration);
public static string SqliteConnectionString => "Data Source=SeedData/biergarten_seed_2026-08-24T01-52-09.841693Z.sqlite"; public static string SqliteConnectionString =>
"Data Source=SeedData/biergarten_seed_2026-08-24T01-52-09.841693Z.sqlite";
} }

View File

@@ -79,8 +79,8 @@ public sealed class NominatimReverseGeocoder : IDisposable
string addressLine1 = string.Join( string addressLine1 = string.Join(
' ', ' ',
new[] { response.Address.HouseNumber, response.Address.Road }.Where( new[] { response.Address.HouseNumber, response.Address.Road }.Where(part =>
part => !string.IsNullOrWhiteSpace(part) !string.IsNullOrWhiteSpace(part)
) )
); );

View File

@@ -26,9 +26,7 @@ static async Task<int> RunAsync()
{ {
try try
{ {
IConfiguration configuration = new ConfigurationBuilder() IConfiguration configuration = new ConfigurationBuilder().AddEnvironmentVariables().Build();
.AddEnvironmentVariables()
.Build();
ServiceCollection services = []; ServiceCollection services = [];
services.AddSingleton(configuration); services.AddSingleton(configuration);
@@ -39,21 +37,17 @@ static async Task<int> RunAsync()
await using ServiceProvider provider = services.BuildServiceProvider(); await using ServiceProvider provider = services.BuildServiceProvider();
IBreweryRepository breweryRepository = IBreweryRepository breweryRepository = provider.GetRequiredService<IBreweryRepository>();
provider.GetRequiredService<IBreweryRepository>(); ILocationRepository locationRepository = provider.GetRequiredService<ILocationRepository>();
ILocationRepository locationRepository = UserManager<ApplicationUser> userManager = provider.GetRequiredService<
provider.GetRequiredService<ILocationRepository>(); UserManager<ApplicationUser>
UserManager<ApplicationUser> userManager = >();
provider.GetRequiredService<UserManager<ApplicationUser>>();
AnsiConsole.Write(new Rule("[bold green]Database Seeder[/]") AnsiConsole.Write(new Rule("[bold green]Database Seeder[/]").LeftJustified());
.LeftJustified()); AnsiConsole.MarkupLine("[grey]Connecting to SQLite source and loading seed data...[/]");
AnsiConsole.MarkupLine(
"[grey]Connecting to SQLite source and loading seed data...[/]");
AnsiConsole.WriteLine(); AnsiConsole.WriteLine();
PipelineSeedDataReader reader = PipelineSeedDataReader reader = new(ConnectionStrings.SqliteConnectionString);
new(ConnectionStrings.SqliteConnectionString);
SeedData seedData = null!; SeedData seedData = null!;
IReadOnlyList<Guid> postedByIds = []; IReadOnlyList<Guid> postedByIds = [];
@@ -79,16 +73,14 @@ static async Task<int> RunAsync()
AnsiConsole.Write(BuildBreweryTable(seedData.Breweries)); AnsiConsole.Write(BuildBreweryTable(seedData.Breweries));
AnsiConsole.WriteLine(); AnsiConsole.WriteLine();
AnsiConsole.MarkupLine( AnsiConsole.MarkupLine($"[green]✓[/] Loaded [bold]{seedData.Users.Count}[/] users.");
$"[green]✓[/] Loaded [bold]{seedData.Users.Count}[/] users.");
AnsiConsole.Write(BuildUserTable(seedData.Users)); AnsiConsole.Write(BuildUserTable(seedData.Users));
AnsiConsole.WriteLine(); AnsiConsole.WriteLine();
AnsiConsole.WriteLine("Seed data loaded successfully."); AnsiConsole.WriteLine("Seed data loaded successfully.");
AnsiConsole.Write( AnsiConsole.Write(
new Rule("[bold green]Loading seed data into target database.[/]") new Rule("[bold green]Loading seed data into target database.[/]").LeftJustified()
.LeftJustified()
); );
await AnsiConsole await AnsiConsole
@@ -193,8 +185,7 @@ static async Task LoadBreweriesIntoDatabaseAsync(
const string placeholderAddressLine1 = "Address unavailable"; const string placeholderAddressLine1 = "Address unavailable";
if (posterUserIds.Count == 0) if (posterUserIds.Count == 0)
throw new InvalidOperationException( throw new InvalidOperationException("Cannot load breweries without any registered users.");
"Cannot load breweries without any registered users.");
for (int i = 0; i < breweries.Count; i++) for (int i = 0; i < breweries.Count; i++)
{ {
@@ -294,4 +285,4 @@ static Table BuildUserTable(IReadOnlyList<UserRecord> users)
} }
return table; return table;
} }

View File

@@ -18,6 +18,5 @@ public sealed class UserAccount
public byte[]? RowVersion { get; set; } public byte[]? RowVersion { get; set; }
// Navigation properties // Navigation properties
public UserCredential? UserCredential { get; set; } public UserCredential? UserCredential { get; set; }
} }

View File

@@ -103,7 +103,11 @@ public class RegisterUserHandlerTests
.Setup(x => x.CreateAsync(It.IsAny<ApplicationUser>(), command.Password)) .Setup(x => x.CreateAsync(It.IsAny<ApplicationUser>(), command.Password))
.ReturnsAsync( .ReturnsAsync(
IdentityResult.Failed( IdentityResult.Failed(
new IdentityError { Code = "DuplicateUserName", Description = "Username taken." } new IdentityError
{
Code = "DuplicateUserName",
Description = "Username taken.",
}
) )
); );

View File

@@ -14,7 +14,8 @@ public class ResendConfirmationEmailHandlerTests
private readonly ResendConfirmationEmailHandler _handler; private readonly ResendConfirmationEmailHandler _handler;
private readonly Mock<IMediator> _mediatorMock = new(); private readonly Mock<IMediator> _mediatorMock = new();
private readonly Mock<ITokenService> _tokenServiceMock = new(); private readonly Mock<ITokenService> _tokenServiceMock = new();
private readonly Mock<UserManager<ApplicationUser>> _userManagerMock = UserManagerMockFactory.Create(); private readonly Mock<UserManager<ApplicationUser>> _userManagerMock =
UserManagerMockFactory.Create();
public ResendConfirmationEmailHandlerTests() public ResendConfirmationEmailHandlerTests()
{ {

View File

@@ -87,7 +87,9 @@ public class LoginHandlerTests
ApplicationUser user = new() { Id = Guid.NewGuid(), UserName = username }; ApplicationUser user = new() { Id = Guid.NewGuid(), UserName = username };
_userManagerMock.Setup(x => x.FindByNameAsync(username)).ReturnsAsync(user); _userManagerMock.Setup(x => x.FindByNameAsync(username)).ReturnsAsync(user);
_userManagerMock.Setup(x => x.CheckPasswordAsync(user, It.IsAny<string>())).ReturnsAsync(false); _userManagerMock
.Setup(x => x.CheckPasswordAsync(user, It.IsAny<string>()))
.ReturnsAsync(false);
Func<Task<LoginPayload>> act = async () => Func<Task<LoginPayload>> act = async () =>
await _handler.Handle(new LoginCommand(username, "password"), CancellationToken.None); await _handler.Handle(new LoginCommand(username, "password"), CancellationToken.None);
@@ -104,7 +106,9 @@ public class LoginHandlerTests
ApplicationUser user = new() { Id = Guid.NewGuid(), UserName = username }; ApplicationUser user = new() { Id = Guid.NewGuid(), UserName = username };
_userManagerMock.Setup(x => x.FindByNameAsync(username)).ReturnsAsync(user); _userManagerMock.Setup(x => x.FindByNameAsync(username)).ReturnsAsync(user);
_userManagerMock.Setup(x => x.CheckPasswordAsync(user, It.IsAny<string>())).ReturnsAsync(false); _userManagerMock
.Setup(x => x.CheckPasswordAsync(user, It.IsAny<string>()))
.ReturnsAsync(false);
Func<Task<LoginPayload>> act = async () => Func<Task<LoginPayload>> act = async () =>
await _handler.Handle( await _handler.Handle(

View File

@@ -29,9 +29,7 @@ public class ConfirmUserHandler(
request.Token request.Token
); );
ApplicationUser? user = await userManager.FindByIdAsync( ApplicationUser? user = await userManager.FindByIdAsync(validatedToken.UserId.ToString());
validatedToken.UserId.ToString()
);
if (user == null) if (user == null)
throw new UnauthorizedException("User account not found"); throw new UnauthorizedException("User account not found");

View File

@@ -27,7 +27,9 @@ public class DeleteAccountHandler(UserManager<ApplicationUser> userManager)
throw new ConflictException( throw new ConflictException(
"Account cannot be deleted while it still has associated posts, comments, photos, or follows." "Account cannot be deleted while it still has associated posts, comments, photos, or follows."
); );
throw new ConflictException(string.Join("; ", result.Errors.Select(e => e.Description))); throw new ConflictException(
string.Join("; ", result.Errors.Select(e => e.Description))
);
} }
} }
} }

View File

@@ -18,7 +18,10 @@ public class LoginHandler(UserManager<ApplicationUser> userManager, ITokenServic
/// Thrown when the username does not match any account or the supplied password does not match /// Thrown when the username does not match any account or the supplied password does not match
/// the stored hash. /// the stored hash.
/// </exception> /// </exception>
public async Task<LoginPayload> Handle(LoginCommand request, CancellationToken cancellationToken) public async Task<LoginPayload> Handle(
LoginCommand request,
CancellationToken cancellationToken
)
{ {
ApplicationUser user = ApplicationUser user =
await userManager.FindByNameAsync(request.Username) await userManager.FindByNameAsync(request.Username)

View File

@@ -43,7 +43,9 @@ public class RegisterUserHandler(
{ {
if (result.Errors.Any(e => e.Code is "DuplicateUserName" or "DuplicateEmail")) if (result.Errors.Any(e => e.Code is "DuplicateUserName" or "DuplicateEmail"))
throw new ConflictException("Username or email already exists"); throw new ConflictException("Username or email already exists");
throw new ConflictException(string.Join("; ", result.Errors.Select(e => e.Description))); throw new ConflictException(
string.Join("; ", result.Errors.Select(e => e.Description))
);
} }
string accessToken = tokenService.GenerateAccessToken(user.Id, user.UserName); string accessToken = tokenService.GenerateAccessToken(user.Id, user.UserName);
@@ -51,7 +53,13 @@ public class RegisterUserHandler(
string confirmationToken = tokenService.GenerateConfirmationToken(user.Id, user.UserName); string confirmationToken = tokenService.GenerateConfirmationToken(user.Id, user.UserName);
if (string.IsNullOrEmpty(accessToken) || string.IsNullOrEmpty(refreshToken)) if (string.IsNullOrEmpty(accessToken) || string.IsNullOrEmpty(refreshToken))
return new RegistrationPayload(user.Id, user.UserName, string.Empty, string.Empty, false); return new RegistrationPayload(
user.Id,
user.UserName,
string.Empty,
string.Empty,
false
);
bool emailSent = false; bool emailSent = false;
try try
@@ -68,6 +76,12 @@ public class RegisterUserHandler(
Console.WriteLine("Could not send email."); Console.WriteLine("Could not send email.");
} }
return new RegistrationPayload(user.Id, user.UserName, refreshToken, accessToken, emailSent); return new RegistrationPayload(
user.Id,
user.UserName,
refreshToken,
accessToken,
emailSent
);
} }
} }

View File

@@ -11,4 +11,5 @@ namespace Features.Auth.Commands.UpdateEmail;
/// The authenticated caller's own ID, extracted from the access token -- never bind this from /// The authenticated caller's own ID, extracted from the access token -- never bind this from
/// client-supplied input. /// client-supplied input.
/// </param> /// </param>
public record UpdateEmailCommand(Guid UserAccountId, string NewEmail) : IRequest<UpdateEmailPayload>; public record UpdateEmailCommand(Guid UserAccountId, string NewEmail)
: IRequest<UpdateEmailPayload>;

View File

@@ -31,7 +31,9 @@ public class UpdateEmailHandler(UserManager<ApplicationUser> userManager)
{ {
if (result.Errors.Any(e => e.Code == "DuplicateEmail")) if (result.Errors.Any(e => e.Code == "DuplicateEmail"))
throw new ConflictException("Email address already exists"); throw new ConflictException("Email address already exists");
throw new ConflictException(string.Join("; ", result.Errors.Select(e => e.Description))); throw new ConflictException(
string.Join("; ", result.Errors.Select(e => e.Description))
);
} }
return new UpdateEmailPayload(user.Id, user.Email, user.EmailConfirmed); return new UpdateEmailPayload(user.Id, user.Email, user.EmailConfirmed);

View File

@@ -34,7 +34,9 @@ public class UpdatePasswordHandler(UserManager<ApplicationUser> userManager)
{ {
if (result.Errors.Any(e => e.Code == "PasswordMismatch")) if (result.Errors.Any(e => e.Code == "PasswordMismatch"))
throw new UnauthorizedException("Current password is incorrect"); throw new UnauthorizedException("Current password is incorrect");
throw new ConflictException(string.Join("; ", result.Errors.Select(e => e.Description))); throw new ConflictException(
string.Join("; ", result.Errors.Select(e => e.Description))
);
} }
return new UpdatePasswordPayload(user.Id, DateTime.UtcNow); return new UpdatePasswordPayload(user.Id, DateTime.UtcNow);

View File

@@ -31,7 +31,9 @@ public class UpdateProfileHandler(UserManager<ApplicationUser> userManager)
IdentityResult result = await userManager.UpdateAsync(user); IdentityResult result = await userManager.UpdateAsync(user);
if (!result.Succeeded) if (!result.Succeeded)
throw new ConflictException(string.Join("; ", result.Errors.Select(e => e.Description))); throw new ConflictException(
string.Join("; ", result.Errors.Select(e => e.Description))
);
return new UpdateProfilePayload(user.Id, user.FirstName, user.LastName, user.DateOfBirth); return new UpdateProfilePayload(user.Id, user.FirstName, user.LastName, user.DateOfBirth);
} }

View File

@@ -26,7 +26,9 @@ public class UpdateUsernameHandler(UserManager<ApplicationUser> userManager)
{ {
if (result.Errors.Any(e => e.Code == "DuplicateUserName")) if (result.Errors.Any(e => e.Code == "DuplicateUserName"))
throw new ConflictException("Username already exists"); throw new ConflictException("Username already exists");
throw new ConflictException(string.Join("; ", result.Errors.Select(e => e.Description))); throw new ConflictException(
string.Join("; ", result.Errors.Select(e => e.Description))
);
} }
return new UpdateUsernamePayload(user.Id, user.UserName); return new UpdateUsernamePayload(user.Id, user.UserName);

View File

@@ -71,7 +71,9 @@ public class AuthController(IMediator mediator) : ControllerBase
/// </remarks> /// </remarks>
[AllowAnonymous] [AllowAnonymous]
[HttpPost("login")] [HttpPost("login")]
public async Task<ActionResult<ResponseBody<LoginPayload>>> Login([FromBody] LoginCommand command) public async Task<ActionResult<ResponseBody<LoginPayload>>> Login(
[FromBody] LoginCommand command
)
{ {
LoginPayload payload = await mediator.Send(command); LoginPayload payload = await mediator.Send(command);
return Ok( return Ok(

View File

@@ -22,6 +22,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Queries\" /> <Folder Include="Queries\" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -19,8 +19,7 @@ public sealed class DapperUserStore(ISqlConnectionFactory connectionFactory)
IUserPasswordStore<ApplicationUser>, IUserPasswordStore<ApplicationUser>,
IUserEmailStore<ApplicationUser> IUserEmailStore<ApplicationUser>
{ {
private const string SelectUserSql = private const string SelectUserSql = """
"""
SELECT UserAccountID AS Id, Username AS UserName, FirstName, LastName, Email, DateOfBirth SELECT UserAccountID AS Id, Username AS UserName, FirstName, LastName, Email, DateOfBirth
FROM dbo.UserAccount FROM dbo.UserAccount
"""; """;
@@ -33,8 +32,10 @@ public sealed class DapperUserStore(ISqlConnectionFactory connectionFactory)
Task.FromResult(user.Id.ToString()); Task.FromResult(user.Id.ToString());
/// <inheritdoc /> /// <inheritdoc />
public Task<string?> GetUserNameAsync(ApplicationUser user, CancellationToken cancellationToken) => public Task<string?> GetUserNameAsync(
Task.FromResult<string?>(user.UserName); ApplicationUser user,
CancellationToken cancellationToken
) => Task.FromResult<string?>(user.UserName);
/// <inheritdoc /> /// <inheritdoc />
public Task SetUserNameAsync( public Task SetUserNameAsync(

View File

@@ -21,7 +21,12 @@ public enum TokenType
public record ValidatedToken(Guid UserId, string Username, ClaimsPrincipal Principal); public record ValidatedToken(Guid UserId, string Username, ClaimsPrincipal Principal);
/// <summary>Represents the result of refreshing a user's session.</summary> /// <summary>Represents the result of refreshing a user's session.</summary>
public record RefreshTokenResult(Guid UserId, string Username, string RefreshToken, string AccessToken); public record RefreshTokenResult(
Guid UserId,
string Username,
string RefreshToken,
string AccessToken
);
/// <summary> /// <summary>
/// Defines the expiration windows, in hours, for each type of token issued by <see cref="ITokenService" />. /// Defines the expiration windows, in hours, for each type of token issued by <see cref="ITokenService" />.
@@ -61,7 +66,6 @@ public interface ITokenService
/// </summary> /// </summary>
string GenerateConfirmationToken(Guid userId, string username); string GenerateConfirmationToken(Guid userId, string username);
/// <summary>Validates an access token's signature and expiration and extracts the caller's identity.</summary> /// <summary>Validates an access token's signature and expiration and extracts the caller's identity.</summary>
/// <exception cref="Domain.Exceptions.UnauthorizedException"> /// <exception cref="Domain.Exceptions.UnauthorizedException">
/// Thrown when the token is missing required claims, has a malformed user ID, or otherwise fails validation. /// Thrown when the token is missing required claims, has a malformed user ID, or otherwise fails validation.

View File

@@ -23,9 +23,7 @@ public class CreateBreweryValidator : AbstractValidator<CreateBreweryCommand>
RuleFor(x => x.Location).NotNull().WithMessage("Location is required."); RuleFor(x => x.Location).NotNull().WithMessage("Location is required.");
RuleFor(x => x.Location.CityId) RuleFor(x => x.Location.CityId).NotEmpty().WithMessage("CityId is required.");
.NotEmpty()
.WithMessage("CityId is required.");
RuleFor(x => x.Location.AddressLine1) RuleFor(x => x.Location.AddressLine1)
.NotEmpty() .NotEmpty()

View File

@@ -70,8 +70,7 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
} }
/// <inheritdoc/> /// <inheritdoc/>
public async Task<IEnumerable<BreweryPost>> GetAllAsync(int? limit, public async Task<IEnumerable<BreweryPost>> GetAllAsync(int? limit, int? offset)
int? offset)
{ {
await using DbConnection connection = await CreateConnection(); await using DbConnection connection = await CreateConnection();
return await connection.QueryAsync< return await connection.QueryAsync<
@@ -122,25 +121,24 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
public async Task<BreweryPost> UpdateAsync(BreweryPost brewery) public async Task<BreweryPost> UpdateAsync(BreweryPost brewery)
{ {
await using DbConnection connection = await CreateConnection(); await using DbConnection connection = await CreateConnection();
await using DbTransaction transaction = await using DbTransaction transaction = await connection.BeginTransactionAsync();
await connection.BeginTransactionAsync();
try try
{ {
bool breweryExists = bool breweryExists =
await connection.ExecuteScalarAsync<int?>( await connection.ExecuteScalarAsync<int?>(
new CommandDefinition( new CommandDefinition(
""" """
SELECT 1 FROM SELECT 1 FROM
dbo.BreweryPost dbo.BreweryPost
WHERE WHERE
BreweryPostID = @BreweryPostId BreweryPostID = @BreweryPostId
""", """,
new { brewery.BreweryPostId }, new { brewery.BreweryPostId },
transaction transaction
)
) )
is not null; )
is not null;
if (!breweryExists) if (!breweryExists)
throw new NotFoundException("Brewery not found."); throw new NotFoundException("Brewery not found.");
@@ -149,13 +147,13 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
{ {
bool cityExists = bool cityExists =
await connection.ExecuteScalarAsync<int?>( await connection.ExecuteScalarAsync<int?>(
new CommandDefinition( new CommandDefinition(
"SELECT 1 FROM dbo.City WHERE CityID = @CityId", "SELECT 1 FROM dbo.City WHERE CityID = @CityId",
new { brewery.Location.CityId }, new { brewery.Location.CityId },
transaction transaction
)
) )
is not null; )
is not null;
if (!cityExists) if (!cityExists)
throw new NotFoundException("City not found."); throw new NotFoundException("City not found.");
@@ -207,17 +205,17 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
{ {
bool locationExists = bool locationExists =
await connection.ExecuteScalarAsync<int?>( await connection.ExecuteScalarAsync<int?>(
new CommandDefinition( new CommandDefinition(
""" """
SELECT 1 SELECT 1
FROM dbo.BreweryPostLocation FROM dbo.BreweryPostLocation
WHERE BreweryPostID = @BreweryPostId WHERE BreweryPostID = @BreweryPostId
""", """,
new { brewery.BreweryPostId }, new { brewery.BreweryPostId },
transaction transaction
)
) )
is not null; )
is not null;
if (locationExists) if (locationExists)
await connection.ExecuteAsync( await connection.ExecuteAsync(
@@ -288,9 +286,9 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
} }
return await GetByIdAsync(brewery.BreweryPostId) return await GetByIdAsync(brewery.BreweryPostId)
?? throw new InvalidOperationException( ?? throw new InvalidOperationException(
$"Brewery '{brewery.BreweryPostId}' was not found after a successful update." $"Brewery '{brewery.BreweryPostId}' was not found after a successful update."
); );
} }
/// <inheritdoc/> /// <inheritdoc/>
@@ -315,49 +313,47 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
public async Task CreateAsync(BreweryPost brewery) public async Task CreateAsync(BreweryPost brewery)
{ {
if (brewery.Location is null) if (brewery.Location is null)
throw new ArgumentException( throw new ArgumentException("Location must be provided when creating a brewery.");
"Location must be provided when creating a brewery.");
await using DbConnection connection = await CreateConnection(); await using DbConnection connection = await CreateConnection();
await using DbTransaction transaction = await using DbTransaction transaction = await connection.BeginTransactionAsync();
await connection.BeginTransactionAsync();
try try
{ {
bool userExists = bool userExists =
await connection.ExecuteScalarAsync<int?>( await connection.ExecuteScalarAsync<int?>(
new CommandDefinition( new CommandDefinition(
""" """
SELECT 1 SELECT 1
FROM FROM
dbo.UserAccount dbo.UserAccount
WHERE WHERE
UserAccountID = @PostedById UserAccountID = @PostedById
""", """,
new { brewery.PostedById }, new { brewery.PostedById },
transaction transaction
)
) )
is not null; )
is not null;
if (!userExists) if (!userExists)
throw new NotFoundException("User not found."); throw new NotFoundException("User not found.");
bool cityExists = bool cityExists =
await connection.ExecuteScalarAsync<int?>( await connection.ExecuteScalarAsync<int?>(
new CommandDefinition( new CommandDefinition(
""" """
SELECT 1 SELECT 1
FROM FROM
dbo.City dbo.City
WHERE WHERE
CityID = @CityId CityID = @CityId
""", """,
new { brewery.Location.CityId }, new { brewery.Location.CityId },
transaction transaction
)
) )
is not null; )
is not null;
if (!cityExists) if (!cityExists)
throw new NotFoundException("City not found."); throw new NotFoundException("City not found.");

View File

@@ -51,7 +51,8 @@ public class LocationRepository(ISqlConnectionFactory connectionFactory)
WHERE CityName = @CityName AND StateProvinceID = @StateProvinceId WHERE CityName = @CityName AND StateProvinceID = @StateProvinceId
""", """,
new { location.CityName, StateProvinceId = stateProvinceId } new { location.CityName, StateProvinceId = stateProvinceId }
) is not null; )
is not null;
if (!cityExists) if (!cityExists)
await connection.ExecuteAsync( await connection.ExecuteAsync(
@@ -83,7 +84,8 @@ public class LocationRepository(ISqlConnectionFactory connectionFactory)
WHERE ISO3166_1 = @ISO3166_1 WHERE ISO3166_1 = @ISO3166_1
""", """,
new { ISO3166_1 = isoCode } new { ISO3166_1 = isoCode }
) is not null; )
is not null;
if (!exists) if (!exists)
// A concurrent caller may create this country between the check above and here; that // A concurrent caller may create this country between the check above and here; that
@@ -128,7 +130,8 @@ public class LocationRepository(ISqlConnectionFactory connectionFactory)
WHERE ISO3166_2 = @ISO3166_2 WHERE ISO3166_2 = @ISO3166_2
""", """,
new { ISO3166_2 = isoCode } new { ISO3166_2 = isoCode }
) is not null; )
is not null;
if (!exists) if (!exists)
await connection.ExecuteAsync( await connection.ExecuteAsync(

View File

@@ -52,8 +52,7 @@ public static class SqlConnectionStringHelper
return !bool.TryParse( return !bool.TryParse(
configuration["DB_TRUST_SERVER_CERTIFICATE"], configuration["DB_TRUST_SERVER_CERTIFICATE"],
out bool trustServerCertificate out bool trustServerCertificate
) ) || trustServerCertificate;
|| trustServerCertificate;
} }
private static string Required(IConfiguration configuration, string key) => private static string Required(IConfiguration configuration, string key) =>

View File

@@ -1 +1 @@
namespace Infrastructure.Sql; namespace Infrastructure.Sql;