mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-10-15 10:04:44 +00:00
Compare commits
3 Commits
180ed41e68
...
fbede956e9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbede956e9 | ||
|
|
3793fca65e | ||
|
|
78d0eaa739 |
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
|||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
status=0
|
status=0
|
||||||
for name in test-env-api-specs test-env-unit-tests test-env-frontend-tests; do
|
for name in test-env-database-migrations test-env-database-seed test-env-api-specs test-env-unit-tests test-env-frontend-tests; do
|
||||||
code="$(docker inspect -f '{{.State.ExitCode}}' "$name" 2>/dev/null || echo 1)"
|
code="$(docker inspect -f '{{.State.ExitCode}}' "$name" 2>/dev/null || echo 1)"
|
||||||
echo "$name exited with $code"
|
echo "$name exited with $code"
|
||||||
if [ "$code" != "0" ]; then
|
if [ "$code" != "0" ]; then
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ see the notes for what else each of those foundation projects pulls in.
|
|||||||
- **Omitted from the diagram**:
|
- **Omitted from the diagram**:
|
||||||
- `Domain.Exceptions` (referenced by nearly every project directly, including
|
- `Domain.Exceptions` (referenced by nearly every project directly, including
|
||||||
all of `Shared.Contracts` and each feature slice)
|
all of `Shared.Contracts` and each feature slice)
|
||||||
- `Infrastructure.Configuration` (the shared options-binding project
|
- `Configuration` (the shared options-binding project
|
||||||
referenced by `Database.Connection`, `Features.PhotoUpload`,
|
referenced by `Database.Connection`, `Features.PhotoUpload`,
|
||||||
and `Features.Emails` directly)
|
and `Features.Emails` directly)
|
||||||
- both are plumbing-level dependencies of nearly everything above, and drawing
|
- both are plumbing-level dependencies of nearly everything above, and drawing
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ sqlserver # Test database
|
|||||||
database.migrations # Fresh schema
|
database.migrations # Fresh schema
|
||||||
database.seed # Test data
|
database.seed # Test data
|
||||||
api.specs # Reqnroll BDD tests
|
api.specs # Reqnroll BDD tests
|
||||||
unit.tests # All Features.*.Tests unit test projects
|
unit.tests # All Features.*.Tests and Shared.*.Tests unit test projects
|
||||||
frontend.tests # Storybook Vitest + Playwright suites (own Playwright image)
|
frontend.tests # Storybook Vitest + Playwright suites (own Playwright image)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -76,10 +76,11 @@ docker compose --env-file web/.env.test -f web/docker-compose.test.yaml down -v
|
|||||||
|
|
||||||
You can run individual test projects locally without Docker:
|
You can run individual test projects locally without Docker:
|
||||||
|
|
||||||
### Feature slice unit tests
|
### Feature slice and shared unit tests
|
||||||
|
|
||||||
Each feature slice has its own test project, covering its command/query
|
Each feature slice has its own test project, covering its command/query
|
||||||
handlers:
|
handlers. Shared cross-cutting code (e.g. MediatR pipeline behaviors) has
|
||||||
|
its own test project under `Shared/`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd web/backend
|
cd web/backend
|
||||||
@@ -87,9 +88,10 @@ dotnet test Features/Features.Users.Tests/Features.Users.Tests.csproj
|
|||||||
dotnet test Features/Features.Breweries.Tests/Features.Breweries.Tests.csproj
|
dotnet test Features/Features.Breweries.Tests/Features.Breweries.Tests.csproj
|
||||||
dotnet test Features/Features.Emails.Tests/Features.Emails.Tests.csproj
|
dotnet test Features/Features.Emails.Tests/Features.Emails.Tests.csproj
|
||||||
dotnet test Features/Features.PhotoUpload.Tests/Features.PhotoUpload.Tests.csproj
|
dotnet test Features/Features.PhotoUpload.Tests/Features.PhotoUpload.Tests.csproj
|
||||||
|
dotnet test Shared/Shared.Application.Tests/Shared.Application.Tests.csproj
|
||||||
|
|
||||||
# Or run all of them at once via the solution:
|
# Or run all of them at once via the solution:
|
||||||
for proj in Features/*.Tests; do dotnet test "$proj"; done
|
for proj in Features/*.Tests Shared/*.Tests; do dotnet test "$proj"; done
|
||||||
```
|
```
|
||||||
|
|
||||||
**Requirements**:
|
**Requirements**:
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
<ProjectReference Include="..\..\Domain\Domain.Entities\Domain.Entities.csproj" />
|
<ProjectReference Include="..\..\Domain\Domain.Entities\Domain.Entities.csproj" />
|
||||||
<ProjectReference Include="..\..\Domain\Domain.Exceptions\Domain.Exceptions.csproj" />
|
<ProjectReference Include="..\..\Domain\Domain.Exceptions\Domain.Exceptions.csproj" />
|
||||||
<ProjectReference Include="..\..\Database\Database.Connection\Database.Connection.csproj" />
|
<ProjectReference Include="..\..\Database\Database.Connection\Database.Connection.csproj" />
|
||||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Configuration\Infrastructure.Configuration.csproj" />
|
<ProjectReference Include="..\..\Configuration\Configuration.csproj" />
|
||||||
<ProjectReference Include="..\..\Features\Features.Breweries\Features.Breweries.csproj" />
|
<ProjectReference Include="..\..\Features\Features.Breweries\Features.Breweries.csproj" />
|
||||||
<ProjectReference Include="..\..\Features\Features.Users\Features.Users.csproj" />
|
<ProjectReference Include="..\..\Features\Features.Users\Features.Users.csproj" />
|
||||||
<ProjectReference Include="..\..\Features\Features.Emails\Features.Emails.csproj" />
|
<ProjectReference Include="..\..\Features\Features.Emails\Features.Emails.csproj" />
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
using Features.Users.Services;
|
using Features.Users.Services;
|
||||||
using Infrastructure.Configuration;
|
using Configuration;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Microsoft.Extensions.Primitives;
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|||||||
@@ -33,8 +33,9 @@ COPY ["Features/Features.PhotoUpload/Features.PhotoUpload.csproj", "Features/Fea
|
|||||||
COPY ["Features/Features.PhotoUpload.Tests/Features.PhotoUpload.Tests.csproj", "Features/Features.PhotoUpload.Tests/"]
|
COPY ["Features/Features.PhotoUpload.Tests/Features.PhotoUpload.Tests.csproj", "Features/Features.PhotoUpload.Tests/"]
|
||||||
COPY ["Features/Features.Users/Features.Users.csproj", "Features/Features.Users/"]
|
COPY ["Features/Features.Users/Features.Users.csproj", "Features/Features.Users/"]
|
||||||
COPY ["Features/Features.Users.Tests/Features.Users.Tests.csproj", "Features/Features.Users.Tests/"]
|
COPY ["Features/Features.Users.Tests/Features.Users.Tests.csproj", "Features/Features.Users.Tests/"]
|
||||||
COPY ["Infrastructure/Infrastructure.Configuration/Infrastructure.Configuration.csproj", "Infrastructure/Infrastructure.Configuration/"]
|
COPY ["Configuration/Configuration.csproj", "Configuration/"]
|
||||||
COPY ["Shared/Shared.Application/Shared.Application.csproj", "Shared/Shared.Application/"]
|
COPY ["Shared/Shared.Application/Shared.Application.csproj", "Shared/Shared.Application/"]
|
||||||
|
COPY ["Shared/Shared.Application.Tests/Shared.Application.Tests.csproj", "Shared/Shared.Application.Tests/"]
|
||||||
COPY ["Shared/Shared.Contracts/Shared.Contracts.csproj", "Shared/Shared.Contracts/"]
|
COPY ["Shared/Shared.Contracts/Shared.Contracts.csproj", "Shared/Shared.Contracts/"]
|
||||||
RUN dotnet restore "Biergarten.API.slnx"
|
RUN dotnet restore "Biergarten.API.slnx"
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ internal static class ServiceCollectionExtensions
|
|||||||
cfg.RegisterServicesFromAssembly(typeof(UploadPhotoCommand).Assembly);
|
cfg.RegisterServicesFromAssembly(typeof(UploadPhotoCommand).Assembly);
|
||||||
cfg.RegisterServicesFromAssembly(typeof(IEmailDispatcher).Assembly);
|
cfg.RegisterServicesFromAssembly(typeof(IEmailDispatcher).Assembly);
|
||||||
cfg.RegisterServicesFromAssembly(typeof(CityController).Assembly);
|
cfg.RegisterServicesFromAssembly(typeof(CityController).Assembly);
|
||||||
|
cfg.AddOpenBehavior(typeof(UnhandledExceptionLoggingBehavior<,>));
|
||||||
cfg.AddOpenBehavior(typeof(ValidationBehavior<,>));
|
cfg.AddOpenBehavior(typeof(ValidationBehavior<,>));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\API.Core\API.Core.csproj" />
|
<ProjectReference Include="..\API.Core\API.Core.csproj" />
|
||||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Configuration\Infrastructure.Configuration.csproj" />
|
<ProjectReference Include="..\..\Configuration\Configuration.csproj" />
|
||||||
<ProjectReference Include="..\..\Features\Features.Emails\Features.Emails.csproj" />
|
<ProjectReference Include="..\..\Features\Features.Emails\Features.Emails.csproj" />
|
||||||
<ProjectReference Include="..\..\Features\Features.Locations\Features.Locations.csproj" />
|
<ProjectReference Include="..\..\Features\Features.Locations\Features.Locations.csproj" />
|
||||||
<ProjectReference Include="..\..\Database\Database.Connection\Database.Connection.csproj" />
|
<ProjectReference Include="..\..\Database\Database.Connection\Database.Connection.csproj" />
|
||||||
|
|||||||
@@ -23,8 +23,9 @@ COPY ["Features/Features.PhotoUpload/Features.PhotoUpload.csproj", "Features/Fea
|
|||||||
COPY ["Features/Features.PhotoUpload.Tests/Features.PhotoUpload.Tests.csproj", "Features/Features.PhotoUpload.Tests/"]
|
COPY ["Features/Features.PhotoUpload.Tests/Features.PhotoUpload.Tests.csproj", "Features/Features.PhotoUpload.Tests/"]
|
||||||
COPY ["Features/Features.Users/Features.Users.csproj", "Features/Features.Users/"]
|
COPY ["Features/Features.Users/Features.Users.csproj", "Features/Features.Users/"]
|
||||||
COPY ["Features/Features.Users.Tests/Features.Users.Tests.csproj", "Features/Features.Users.Tests/"]
|
COPY ["Features/Features.Users.Tests/Features.Users.Tests.csproj", "Features/Features.Users.Tests/"]
|
||||||
COPY ["Infrastructure/Infrastructure.Configuration/Infrastructure.Configuration.csproj", "Infrastructure/Infrastructure.Configuration/"]
|
COPY ["Configuration/Configuration.csproj", "Configuration/"]
|
||||||
COPY ["Shared/Shared.Application/Shared.Application.csproj", "Shared/Shared.Application/"]
|
COPY ["Shared/Shared.Application/Shared.Application.csproj", "Shared/Shared.Application/"]
|
||||||
|
COPY ["Shared/Shared.Application.Tests/Shared.Application.Tests.csproj", "Shared/Shared.Application.Tests/"]
|
||||||
COPY ["Shared/Shared.Contracts/Shared.Contracts.csproj", "Shared/Shared.Contracts/"]
|
COPY ["Shared/Shared.Contracts/Shared.Contracts.csproj", "Shared/Shared.Contracts/"]
|
||||||
RUN dotnet restore "Biergarten.API.slnx"
|
RUN dotnet restore "Biergarten.API.slnx"
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ using System.Text;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Features.Users.Services;
|
using Features.Users.Services;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Infrastructure.Configuration;
|
using Configuration;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Reqnroll;
|
using Reqnroll;
|
||||||
|
|||||||
@@ -19,8 +19,8 @@
|
|||||||
<Project Path="Domain/Domain.Entities/Domain.Entities.csproj" />
|
<Project Path="Domain/Domain.Entities/Domain.Entities.csproj" />
|
||||||
<Project Path="Domain/Domain.Exceptions/Domain.Exceptions.csproj" />
|
<Project Path="Domain/Domain.Exceptions/Domain.Exceptions.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/Infrastructure/">
|
<Folder Name="/Configuration/">
|
||||||
<Project Path="Infrastructure/Infrastructure.Configuration/Infrastructure.Configuration.csproj" />
|
<Project Path="Configuration/Configuration.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/Features/" />
|
<Folder Name="/Features/" />
|
||||||
<Folder Name="/Features/Users/">
|
<Folder Name="/Features/Users/">
|
||||||
@@ -47,5 +47,6 @@
|
|||||||
<Folder Name="/Shared/">
|
<Folder Name="/Shared/">
|
||||||
<Project Path="Shared/Shared.Contracts/Shared.Contracts.csproj" />
|
<Project Path="Shared/Shared.Contracts/Shared.Contracts.csproj" />
|
||||||
<Project Path="Shared/Shared.Application/Shared.Application.csproj" />
|
<Project Path="Shared/Shared.Application/Shared.Application.csproj" />
|
||||||
|
<Project Path="Shared/Shared.Application.Tests/Shared.Application.Tests.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
</Solution>
|
</Solution>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<RootNamespace>Infrastructure.Configuration</RootNamespace>
|
<RootNamespace>Configuration</RootNamespace>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
namespace Infrastructure.Configuration;
|
namespace Configuration;
|
||||||
|
|
||||||
public static class ConfigurationHelpers
|
public static class ConfigurationHelpers
|
||||||
{
|
{
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Infrastructure.Configuration;
|
namespace Configuration;
|
||||||
|
|
||||||
public static class ConfigurationKeys
|
public static class ConfigurationKeys
|
||||||
{
|
{
|
||||||
@@ -21,6 +21,6 @@
|
|||||||
/>
|
/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Configuration\Infrastructure.Configuration.csproj" />
|
<ProjectReference Include="..\..\Configuration\Configuration.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Infrastructure.Configuration;
|
using Configuration;
|
||||||
using Microsoft.Data.SqlClient;
|
using Microsoft.Data.SqlClient;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Infrastructure.Configuration;
|
using Configuration;
|
||||||
using Microsoft.Data.SqlClient;
|
using Microsoft.Data.SqlClient;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\Database\Database.Connection\Database.Connection.csproj" />
|
<ProjectReference Include="..\..\Database\Database.Connection\Database.Connection.csproj" />
|
||||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Configuration\Infrastructure.Configuration.csproj" />
|
<ProjectReference Include="..\..\Configuration\Configuration.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Include="scripts/**/*.sql" />
|
<EmbeddedResource Include="scripts/**/*.sql" />
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using Database.Connection;
|
using Database.Connection;
|
||||||
using Infrastructure.Configuration;
|
using Configuration;
|
||||||
using Microsoft.Data.SqlClient;
|
using Microsoft.Data.SqlClient;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ namespace Database.Seed;
|
|||||||
|
|
||||||
internal class Program
|
internal class Program
|
||||||
{
|
{
|
||||||
private static IMediator CreateMediator()
|
private static ServiceProvider BuildServiceProvider()
|
||||||
{
|
{
|
||||||
IConfiguration configuration = new ConfigurationBuilder().AddEnvironmentVariables().Build();
|
IConfiguration configuration = new ConfigurationBuilder().AddEnvironmentVariables().Build();
|
||||||
|
|
||||||
@@ -38,11 +38,10 @@ internal class Program
|
|||||||
.RegisterServicesFromAssemblyContaining<UploadPhotoCommand>()
|
.RegisterServicesFromAssemblyContaining<UploadPhotoCommand>()
|
||||||
);
|
);
|
||||||
|
|
||||||
using ServiceProvider provider = services.BuildServiceProvider();
|
return services.BuildServiceProvider();
|
||||||
return provider.GetRequiredService<IMediator>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task Main()
|
public static async Task<int> Main()
|
||||||
{
|
{
|
||||||
using CancellationTokenSource cts = new();
|
using CancellationTokenSource cts = new();
|
||||||
Console.CancelKeyPress += (_, e) =>
|
Console.CancelKeyPress += (_, e) =>
|
||||||
@@ -54,20 +53,24 @@ internal class Program
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
IMediator mediator = CreateMediator();
|
await using ServiceProvider provider = BuildServiceProvider();
|
||||||
|
IMediator mediator = provider.GetRequiredService<IMediator>();
|
||||||
SeedRepository reader = new SeedRepository(
|
SeedRepository reader = new SeedRepository(
|
||||||
connectionString: "Data Source=SeedData/biergarten_seed_2026-08-25T20-45-50.697244Z.sqlite"
|
connectionString: "Data Source=SeedData/biergarten_seed_2026-08-25T20-45-50.697244Z.sqlite"
|
||||||
);
|
);
|
||||||
await new BiergartenDataSeeder(mediator, reader).Run(cts.Token);
|
await new BiergartenDataSeeder(mediator, reader).Run(cts.Token);
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
AnsiConsole.MarkupLine("[yellow]Seeding cancelled.[/]");
|
AnsiConsole.MarkupLine("[yellow]Seeding cancelled.[/]");
|
||||||
|
return 1;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
AnsiConsole.MarkupLine("[red]Seeding failed.[/]");
|
AnsiConsole.MarkupLine("[red]Seeding failed.[/]");
|
||||||
AnsiConsole.WriteException(ex);
|
AnsiConsole.WriteException(ex);
|
||||||
|
return 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,13 +58,13 @@ public sealed class SeedRepository : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
private sealed record BreweryRow(
|
private sealed record BreweryRow(
|
||||||
int Id,
|
long Id,
|
||||||
string NameEn,
|
string NameEn,
|
||||||
string DescriptionEn,
|
string DescriptionEn,
|
||||||
string NameLocal,
|
string NameLocal,
|
||||||
string DescriptionLocal,
|
string DescriptionLocal,
|
||||||
int AddressId,
|
long AddressId,
|
||||||
int CityId,
|
long CityId,
|
||||||
double Longitude,
|
double Longitude,
|
||||||
double Latitude,
|
double Latitude,
|
||||||
string AddressLine1,
|
string AddressLine1,
|
||||||
@@ -101,27 +101,27 @@ public sealed class SeedRepository : IDisposable
|
|||||||
|
|
||||||
return [.. rows.Select(row => new BreweryResult
|
return [.. rows.Select(row => new BreweryResult
|
||||||
{
|
{
|
||||||
Id = row.Id,
|
Id = (int)row.Id,
|
||||||
NameEn = row.NameEn,
|
NameEn = row.NameEn,
|
||||||
DescriptionEn = row.DescriptionEn,
|
DescriptionEn = row.DescriptionEn,
|
||||||
NameLocal = row.NameLocal,
|
NameLocal = row.NameLocal,
|
||||||
DescriptionLocal = row.DescriptionLocal,
|
DescriptionLocal = row.DescriptionLocal,
|
||||||
Address = new BreweryAddress
|
Address = new BreweryAddress
|
||||||
{
|
{
|
||||||
Id = row.AddressId,
|
Id = (int)row.AddressId,
|
||||||
CityId = row.CityId,
|
CityId = (int)row.CityId,
|
||||||
BreweryId = row.Id,
|
BreweryId = (int)row.Id,
|
||||||
Longitude = row.Longitude,
|
Longitude = row.Longitude,
|
||||||
Latitude = row.Latitude,
|
Latitude = row.Latitude,
|
||||||
AddressLine1 = row.AddressLine1,
|
AddressLine1 = row.AddressLine1,
|
||||||
PostalCode = row.PostalCode,
|
PostalCode = row.PostalCode,
|
||||||
City = cities[row.CityId],
|
City = cities[(int)row.CityId],
|
||||||
},
|
},
|
||||||
})];
|
})];
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed record UserRow(
|
private sealed record UserRow(
|
||||||
int Id,
|
long Id,
|
||||||
string FirstName,
|
string FirstName,
|
||||||
string LastName,
|
string LastName,
|
||||||
string Gender,
|
string Gender,
|
||||||
@@ -130,8 +130,8 @@ public sealed class SeedRepository : IDisposable
|
|||||||
double ActivityWeight,
|
double ActivityWeight,
|
||||||
string Email,
|
string Email,
|
||||||
string DateOfBirth,
|
string DateOfBirth,
|
||||||
int AddressId,
|
long AddressId,
|
||||||
int CityId,
|
long CityId,
|
||||||
double Longitude,
|
double Longitude,
|
||||||
double Latitude
|
double Latitude
|
||||||
);
|
);
|
||||||
@@ -172,16 +172,16 @@ public sealed class SeedRepository : IDisposable
|
|||||||
DateOfBirth = row.DateOfBirth,
|
DateOfBirth = row.DateOfBirth,
|
||||||
Address = new UserAddress
|
Address = new UserAddress
|
||||||
{
|
{
|
||||||
Id = row.AddressId,
|
Id = (int)row.AddressId,
|
||||||
CityId = row.CityId,
|
CityId = (int)row.CityId,
|
||||||
UserId = row.Id,
|
UserId = (int)row.Id,
|
||||||
Longitude = row.Longitude,
|
Longitude = row.Longitude,
|
||||||
Latitude = row.Latitude,
|
Latitude = row.Latitude,
|
||||||
City = cities[row.CityId],
|
City = cities[(int)row.CityId],
|
||||||
},
|
},
|
||||||
User = new UserResult
|
User = new UserResult
|
||||||
{
|
{
|
||||||
Id = row.Id,
|
Id = (int)row.Id,
|
||||||
FirstName = row.FirstName,
|
FirstName = row.FirstName,
|
||||||
LastName = row.LastName,
|
LastName = row.LastName,
|
||||||
Gender = row.Gender,
|
Gender = row.Gender,
|
||||||
@@ -193,7 +193,7 @@ public sealed class SeedRepository : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
private sealed record CityRow(
|
private sealed record CityRow(
|
||||||
int Id,
|
long Id,
|
||||||
string CityName,
|
string CityName,
|
||||||
string StateProvince,
|
string StateProvince,
|
||||||
string Iso31662,
|
string Iso31662,
|
||||||
@@ -231,10 +231,10 @@ public sealed class SeedRepository : IDisposable
|
|||||||
IEnumerable<CityRow> rows = await connection.QueryAsync<CityRow>(command);
|
IEnumerable<CityRow> rows = await connection.QueryAsync<CityRow>(command);
|
||||||
|
|
||||||
return rows.ToDictionary(
|
return rows.ToDictionary(
|
||||||
row => row.Id,
|
row => (int)row.Id,
|
||||||
row => new City
|
row => new City
|
||||||
{
|
{
|
||||||
Id = row.Id,
|
Id = (int)row.Id,
|
||||||
CityName = row.CityName,
|
CityName = row.CityName,
|
||||||
StateProvinceName = row.StateProvince,
|
StateProvinceName = row.StateProvince,
|
||||||
ISO_3166_2 = row.Iso31662,
|
ISO_3166_2 = row.Iso31662,
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ COPY . .
|
|||||||
RUN dotnet restore "Biergarten.API.slnx"
|
RUN dotnet restore "Biergarten.API.slnx"
|
||||||
RUN dotnet build "Biergarten.API.slnx" -c $BUILD_CONFIGURATION
|
RUN dotnet build "Biergarten.API.slnx" -c $BUILD_CONFIGURATION
|
||||||
|
|
||||||
# Runs every Features.*.Tests project (unit + DbMocker-backed repository tests). API.Specs is
|
# Runs every Features.*.Tests and Shared.*.Tests project (unit + DbMocker-backed repository
|
||||||
# excluded here since it's a separate BDD/integration suite, run by its own compose service
|
# tests). API.Specs is excluded here since it's a separate BDD/integration suite, run by its own
|
||||||
# against the live, seeded database.
|
# compose service against the live, seeded database.
|
||||||
FROM build AS final
|
FROM build AS final
|
||||||
ARG BUILD_CONFIGURATION=Release
|
ARG BUILD_CONFIGURATION=Release
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
RUN mkdir -p /app/test-results
|
RUN mkdir -p /app/test-results
|
||||||
ENTRYPOINT ["sh", "-c", "set -e; for proj in Features/*.Tests; do dotnet test \"$proj\" -c ${BUILD_CONFIGURATION:-Release} --logger \"trx;LogFileName=/app/test-results/$(basename \"$proj\").trx\"; done"]
|
ENTRYPOINT ["sh", "-c", "set -e; for proj in Features/*.Tests Shared/*.Tests; do dotnet test \"$proj\" -c ${BUILD_CONFIGURATION:-Release} --logger \"trx;LogFileName=/app/test-results/$(basename \"$proj\").trx\"; done"]
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Configuration\Infrastructure.Configuration.csproj" />
|
<ProjectReference Include="..\..\Configuration\Configuration.csproj" />
|
||||||
<ProjectReference Include="..\..\Shared\Shared.Application\Shared.Application.csproj" />
|
<ProjectReference Include="..\..\Shared\Shared.Application\Shared.Application.csproj" />
|
||||||
<ProjectReference Include="..\Features.Emails.Templates\Features.Emails.Templates.csproj" />
|
<ProjectReference Include="..\Features.Emails.Templates\Features.Emails.Templates.csproj" />
|
||||||
<ProjectReference Include="..\Features.Users\Features.Users.csproj" />
|
<ProjectReference Include="..\Features.Users\Features.Users.csproj" />
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using Features.Emails.Templates.Rendering;
|
using Features.Emails.Templates.Rendering;
|
||||||
using Infrastructure.Configuration;
|
using Configuration;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
namespace Features.Emails.Services;
|
namespace Features.Emails.Services;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Infrastructure.Configuration;
|
using Configuration;
|
||||||
using MailKit.Net.Smtp;
|
using MailKit.Net.Smtp;
|
||||||
using MailKit.Security;
|
using MailKit.Security;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
<ProjectReference Include="..\..\Domain\Domain.Entities\Domain.Entities.csproj" />
|
<ProjectReference Include="..\..\Domain\Domain.Entities\Domain.Entities.csproj" />
|
||||||
<ProjectReference Include="..\..\Domain\Domain.Exceptions\Domain.Exceptions.csproj" />
|
<ProjectReference Include="..\..\Domain\Domain.Exceptions\Domain.Exceptions.csproj" />
|
||||||
<ProjectReference Include="..\..\Database\Database.Connection\Database.Connection.csproj" />
|
<ProjectReference Include="..\..\Database\Database.Connection\Database.Connection.csproj" />
|
||||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Configuration\Infrastructure.Configuration.csproj" />
|
<ProjectReference Include="..\..\Configuration\Configuration.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>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using Amazon.S3;
|
using Amazon.S3;
|
||||||
using Amazon.S3.Model;
|
using Amazon.S3.Model;
|
||||||
using Infrastructure.Configuration;
|
using Configuration;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
namespace Features.ImageUploads.Services;
|
namespace Features.ImageUploads.Services;
|
||||||
|
|||||||
@@ -22,6 +22,6 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Features.Users\Features.Users.csproj" />
|
<ProjectReference Include="..\Features.Users\Features.Users.csproj" />
|
||||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Configuration\Infrastructure.Configuration.csproj" />
|
<ProjectReference Include="..\..\Configuration\Configuration.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ using Features.Users.Identity;
|
|||||||
using Features.Users.Services;
|
using Features.Users.Services;
|
||||||
using Features.Users.Tests.TestSupport;
|
using Features.Users.Tests.TestSupport;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Infrastructure.Configuration;
|
using Configuration;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Moq;
|
using Moq;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ using Domain.Exceptions;
|
|||||||
using Features.Users.Services;
|
using Features.Users.Services;
|
||||||
using Features.Users.Tests.TestSupport;
|
using Features.Users.Tests.TestSupport;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Infrastructure.Configuration;
|
using Configuration;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Moq;
|
using Moq;
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\Domain\Domain.Entities\Domain.Entities.csproj" />
|
<ProjectReference Include="..\..\Domain\Domain.Entities\Domain.Entities.csproj" />
|
||||||
<ProjectReference Include="..\..\Domain\Domain.Exceptions\Domain.Exceptions.csproj" />
|
<ProjectReference Include="..\..\Domain\Domain.Exceptions\Domain.Exceptions.csproj" />
|
||||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Configuration\Infrastructure.Configuration.csproj" />
|
<ProjectReference Include="..\..\Configuration\Configuration.csproj" />
|
||||||
<ProjectReference Include="..\..\Database\Database.Connection\Database.Connection.csproj" />
|
<ProjectReference Include="..\..\Database\Database.Connection\Database.Connection.csproj" />
|
||||||
<ProjectReference Include="..\Features.PhotoUpload\Features.PhotoUpload.csproj" />
|
<ProjectReference Include="..\Features.PhotoUpload\Features.PhotoUpload.csproj" />
|
||||||
<ProjectReference Include="..\..\Shared\Shared.Contracts\Shared.Contracts.csproj" />
|
<ProjectReference Include="..\..\Shared\Shared.Contracts\Shared.Contracts.csproj" />
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ using System.IdentityModel.Tokens.Jwt;
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using Domain.Exceptions;
|
using Domain.Exceptions;
|
||||||
using Features.Users.Identity;
|
using Features.Users.Identity;
|
||||||
using Infrastructure.Configuration;
|
using Configuration;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
using FluentAssertions;
|
||||||
|
using FluentValidation;
|
||||||
|
using FluentValidation.Results;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Moq;
|
||||||
|
using Shared.Application.Behaviors;
|
||||||
|
|
||||||
|
namespace Shared.Application.Tests.Behaviors;
|
||||||
|
|
||||||
|
public class UnhandledExceptionLoggingBehaviorTests
|
||||||
|
{
|
||||||
|
private readonly Mock<
|
||||||
|
ILogger<UnhandledExceptionLoggingBehavior<TestRequest, TestResponse>>
|
||||||
|
> _loggerMock = new();
|
||||||
|
|
||||||
|
private UnhandledExceptionLoggingBehavior<TestRequest, TestResponse> CreateBehavior() =>
|
||||||
|
new(_loggerMock.Object);
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_WhenHandlerThrowsGenericException_LogsOnceAndRethrows()
|
||||||
|
{
|
||||||
|
UnhandledExceptionLoggingBehavior<TestRequest, TestResponse> behavior = CreateBehavior();
|
||||||
|
TestRequest request = new("aaron", "hunter2");
|
||||||
|
InvalidOperationException thrown = new("boom");
|
||||||
|
RequestHandlerDelegate<TestResponse> next = () => throw thrown;
|
||||||
|
|
||||||
|
Func<Task> act = async () => await behavior.Handle(request, next, CancellationToken.None);
|
||||||
|
|
||||||
|
(await act.Should().ThrowAsync<InvalidOperationException>())
|
||||||
|
.Which.Should()
|
||||||
|
.BeSameAs(thrown);
|
||||||
|
VerifyLogError(Times.Once());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_WhenValidationExceptionThrown_DoesNotLogAndRethrows()
|
||||||
|
{
|
||||||
|
UnhandledExceptionLoggingBehavior<TestRequest, TestResponse> behavior = CreateBehavior();
|
||||||
|
TestRequest request = new("aaron", "hunter2");
|
||||||
|
ValidationException thrown = new([new ValidationFailure("Username", "is required")]);
|
||||||
|
RequestHandlerDelegate<TestResponse> next = () => throw thrown;
|
||||||
|
|
||||||
|
Func<Task> act = async () => await behavior.Handle(request, next, CancellationToken.None);
|
||||||
|
|
||||||
|
(await act.Should().ThrowAsync<ValidationException>()).Which.Should().BeSameAs(thrown);
|
||||||
|
VerifyLogError(Times.Never());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_WhenHandlerSucceeds_DoesNotLogAndReturnsResponse()
|
||||||
|
{
|
||||||
|
UnhandledExceptionLoggingBehavior<TestRequest, TestResponse> behavior = CreateBehavior();
|
||||||
|
TestRequest request = new("aaron", "hunter2");
|
||||||
|
TestResponse response = new("ok");
|
||||||
|
RequestHandlerDelegate<TestResponse> next = () => Task.FromResult(response);
|
||||||
|
|
||||||
|
TestResponse result = await behavior.Handle(request, next, CancellationToken.None);
|
||||||
|
|
||||||
|
result.Should().BeSameAs(response);
|
||||||
|
VerifyLogError(Times.Never());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_WhenHandlerThrows_RedactsSensitiveFieldsInLoggedPayload()
|
||||||
|
{
|
||||||
|
UnhandledExceptionLoggingBehavior<TestRequest, TestResponse> behavior = CreateBehavior();
|
||||||
|
TestRequest request = new("aaron", "hunter2");
|
||||||
|
RequestHandlerDelegate<TestResponse> next = () =>
|
||||||
|
throw new InvalidOperationException("boom");
|
||||||
|
|
||||||
|
Func<Task> act = async () => await behavior.Handle(request, next, CancellationToken.None);
|
||||||
|
await act.Should().ThrowAsync<InvalidOperationException>();
|
||||||
|
|
||||||
|
string loggedMessage = CaptureLoggedMessage();
|
||||||
|
loggedMessage.Should().Contain("aaron").And.NotContain("hunter2");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void VerifyLogError(Times times) =>
|
||||||
|
_loggerMock.Verify(
|
||||||
|
l =>
|
||||||
|
l.Log(
|
||||||
|
LogLevel.Error,
|
||||||
|
It.IsAny<EventId>(),
|
||||||
|
It.IsAny<It.IsAnyType>(),
|
||||||
|
It.IsAny<Exception>(),
|
||||||
|
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
|
||||||
|
),
|
||||||
|
times
|
||||||
|
);
|
||||||
|
|
||||||
|
private string CaptureLoggedMessage()
|
||||||
|
{
|
||||||
|
var invocation = _loggerMock
|
||||||
|
.Invocations.Where(i => i.Method.Name == nameof(ILogger.Log))
|
||||||
|
.Should()
|
||||||
|
.ContainSingle()
|
||||||
|
.Subject;
|
||||||
|
|
||||||
|
object state = invocation.Arguments[2];
|
||||||
|
object? exception = invocation.Arguments[3];
|
||||||
|
object formatter = invocation.Arguments[4];
|
||||||
|
System.Reflection.MethodInfo invokeMethod = formatter.GetType().GetMethod("Invoke")!;
|
||||||
|
|
||||||
|
return (string)invokeMethod.Invoke(formatter, [state, exception])!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record TestRequest(string Username, string Password) : IRequest<TestResponse>;
|
||||||
|
|
||||||
|
public record TestResponse(string Value);
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<RootNamespace>Shared.Application.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="7.2.0"/>
|
||||||
|
<PackageReference Include="FluentValidation" Version="11.3.0"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Shared.Application\Shared.Application.csproj"/>
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization.Metadata;
|
||||||
|
using FluentValidation;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Shared.Application.Behaviors;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MediatR pipeline behavior that logs unhandled exceptions together with the request type and
|
||||||
|
/// payload, then rethrows them unchanged for <c>GlobalExceptionFilter</c> to map to a status code.
|
||||||
|
/// </summary>
|
||||||
|
public class UnhandledExceptionLoggingBehavior<TRequest, TResponse>(
|
||||||
|
ILogger<UnhandledExceptionLoggingBehavior<TRequest, TResponse>> logger
|
||||||
|
) : IPipelineBehavior<TRequest, TResponse>
|
||||||
|
where TRequest : IRequest<TResponse>
|
||||||
|
{
|
||||||
|
private static readonly string[] SensitiveFieldNames = ["password", "token"];
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions SerializerOptions = new()
|
||||||
|
{
|
||||||
|
TypeInfoResolver = new DefaultJsonTypeInfoResolver
|
||||||
|
{
|
||||||
|
Modifiers = { RedactSensitiveProperties },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public async Task<TResponse> Handle(
|
||||||
|
TRequest request,
|
||||||
|
RequestHandlerDelegate<TResponse> next,
|
||||||
|
CancellationToken cancellationToken
|
||||||
|
)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await next();
|
||||||
|
}
|
||||||
|
catch (ValidationException)
|
||||||
|
{
|
||||||
|
// Expected: ValidationBehavior already reports these as request errors, not incidents.
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(
|
||||||
|
ex,
|
||||||
|
"Unhandled exception while processing {RequestName}: {RequestPayload}",
|
||||||
|
typeof(TRequest).Name,
|
||||||
|
Serialize(request)
|
||||||
|
);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Serialize(TRequest request)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return JsonSerializer.Serialize(request, SerializerOptions);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return "<unable to serialize request payload>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RedactSensitiveProperties(JsonTypeInfo typeInfo)
|
||||||
|
{
|
||||||
|
if (typeInfo.Kind != JsonTypeInfoKind.Object)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (JsonPropertyInfo property in typeInfo.Properties)
|
||||||
|
{
|
||||||
|
if (property.PropertyType != typeof(string))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (
|
||||||
|
SensitiveFieldNames.Any(name =>
|
||||||
|
property.Name.Contains(name, StringComparison.OrdinalIgnoreCase)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
property.Get = static _ => "***REDACTED***";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,5 +9,6 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="MediatR" Version="12.4.1" />
|
<PackageReference Include="MediatR" Version="12.4.1" />
|
||||||
<PackageReference Include="FluentValidation" Version="11.3.0" />
|
<PackageReference Include="FluentValidation" Version="11.3.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
Reference in New Issue
Block a user