refactor: refactor analyzer configs and add formating to cli

This commit is contained in:
Meysam Hadeli 2024-09-16 17:57:37 +03:30
parent f9eb00c880
commit dda4a3f92b
18 changed files with 1066 additions and 607 deletions

File diff suppressed because it is too large Load Diff

View File

@ -50,6 +50,18 @@ runs:
# restore root solution
run: dotnet restore
# npm install, runs `prepare` script automatically in the initialize step
- name: Install NPM Dependencies
shell: bash
if: success()
run: npm install
- name: Format Service
shell: bash
if: ${{ success()}}
run: |
npm run ci-format
- name: Build Service
shell: bash
if: ${{ success()}}

View File

@ -39,7 +39,9 @@
<PropertyGroup>
<RunAnalyzersDuringBuild>true</RunAnalyzersDuringBuild>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<AnalysisMode>All</AnalysisMode>
<CodeAnalysisTreatWarningsAsErrors>false</CodeAnalysisTreatWarningsAsErrors>
<AnalysisLevel>latest-Recommended</AnalysisLevel>
<AnalysisMode>Recommended</AnalysisMode>
</PropertyGroup>
</Project>

View File

@ -26,6 +26,8 @@
- [Structure of Project](#structure-of-project)
- [Development Setup](#development-setup)
- [Dotnet Tools Packages](#dotnet-tools-packages)
- [Husky](#husky)
- [Upgrade Nuget Packages](#upgrade-nuget-packages)
- [How to Run](#how-to-run)
- [Config Certificate](#config-certificate)
- [Docker Compose](#docker-compose)
@ -47,7 +49,7 @@
- :sparkle: Using `Postgres` for `write side` of some microservices.
- :sparkle: Using `MongoDB` for `read side` of some microservices.
- :sparkle: Using `Event Store` for `write side` of Booking-Microservice to store all `historical state` of aggregate.
- :sparkle: Using `Inbox Pattern` for ensuring message idempotency for receiver and `Exactly once Delivery`.
- :sparkle: Using `Inbox Pattern` for ensuring message idempotency for receiver and `Exactly once Delivery`.
- :sparkle: Using `Outbox Pattern` for ensuring no message is lost and there is at `At Least One Delivery`.
- :sparkle: Using `Unit Testing` for testing small units and mocking our dependencies with `Nsubstitute`.
- :sparkle: Using `End-To-End Testing` and `Integration Testing` for testing `features` with all dependencies using `testcontainers`.
@ -159,7 +161,7 @@ Using the CQRS pattern, we cut each business functionality into vertical slices,
## Development Setup
### Dotnet Tools Packages
For installing our requirement package with .NET cli tools, we need to install `dotnet tool manifest`.
For installing our requirement packages with .NET cli tools, we need to install `dotnet tool manifest`.
```bash
dotnet new tool-manifest
```
@ -168,6 +170,24 @@ And after that we can restore our dotnet tools packages with .NET cli tools from
dotnet tool restore
```
### Husky
Here we use `husky` to handel some pre commit rules and we used `conventional commits` rules and `formatting` as pre commit rules, here in [package.json](./package.json). of course, we can add more rules for pre commit in future. (find more about husky in the [documentation](https://typicode.github.io/husky/get-started.html))
We need to install `husky` package for `manage` `pre commits hooks` and also I add two packages `@commitlint/cli` and `@commitlint/config-conventional` for handling conventional commits rules in [package.json](./package.json).
Run the command bellow in the root of project to install all npm dependencies related to husky:
```bash
npm install
```
> Note: In the root of project we have `.husky` folder and it has `commit-msg` file for handling conventional commits rules with provide user friendly message and `pre-commit` file that we can run our `scripts` as a `pre-commit` hooks. that here we call `format` script from [package.json](./package.json) for formatting purpose.
### Upgrade Nuget Packages
For upgrading our nuget packages to last version, we use the great package [dotnet-outdated](https://github.com/dotnet-outdated/dotnet-outdated).
Run the command below in the root of project to upgrade all of packages to last version:
```bash
dotnet outdated -u
```
## How to Run
> ### Config Certificate

View File

@ -8,6 +8,7 @@
"scripts": {
"prepare": "husky && dotnet tool restore",
"format": "dotnet format booking-microservices-sample.sln --severity error --verbosity detailed",
"ci-format": "dotnet format booking-microservices-sample.sln --verify-no-changes --severity error --verbosity detailed",
"upgrade-packages": "dotnet outdated --upgrade"
},
"devDependencies": {

View File

@ -18,7 +18,7 @@
<PackageReference Include="Grpc.Core.Testing" Version="2.46.6" />
<PackageReference Include="EasyCaching.Core" Version="1.9.2" />
<PackageReference Include="EasyCaching.InMemory" Version="1.9.2" />
<PackageReference Include="EasyNetQ.Management.Client" Version="2.0.0" />
<PackageReference Include="EasyNetQ.Management.Client" Version="3.0.0" />
<PackageReference Include="EFCore.NamingConventions" Version="8.0.3" />
<PackageReference Include="Figgle" Version="0.5.1" />
<PackageReference Include="FluentValidation" Version="11.10.0" />
@ -51,7 +51,7 @@
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="OpenTelemetry.Contrib.Instrumentation.MassTransit" Version="1.0.0-beta2" />
<PackageReference Include="Scrutor" Version="4.2.2" />
<PackageReference Include="Sentry.Serilog" Version="4.9.0" />
<PackageReference Include="Sentry.Serilog" Version="4.10.2" />
<PackageReference Include="Serilog" Version="4.0.1" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.2" />
<PackageReference Include="Serilog.Enrichers.Span" Version="3.1.0" />

View File

@ -54,13 +54,13 @@ public sealed class EventDispatcher : IEventDispatcher
switch (events)
{
case IReadOnlyList<IDomainEvent> domainEvents:
{
var integrationEvents = await MapDomainEventToIntegrationEventAsync(domainEvents)
{
var integrationEvents = await MapDomainEventToIntegrationEventAsync(domainEvents)
.ConfigureAwait(false);
await PublishIntegrationEvent(integrationEvents);
break;
}
await PublishIntegrationEvent(integrationEvents);
break;
}
case IReadOnlyList<IIntegrationEvent> integrationEvents:
await PublishIntegrationEvent(integrationEvents);

View File

@ -16,36 +16,40 @@ using Microsoft.EntityFrameworkCore.Metadata;
public static class Extensions
{
public static IServiceCollection AddCustomDbContext<TContext>(
this IServiceCollection services)
where TContext : DbContext, IDbContext
public static IServiceCollection AddCustomDbContext<TContext>(this IServiceCollection services)
where TContext : DbContext, IDbContext
{
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
services.AddValidateOptions<PostgresOptions>();
services.AddDbContext<TContext>((sp, options) =>
{
var postgresOptions = sp.GetRequiredService<PostgresOptions>();
services.AddDbContext<TContext>(
(sp, options) =>
{
var postgresOptions = sp.GetRequiredService<PostgresOptions>();
Guard.Against.Null(options, nameof(postgresOptions));
Guard.Against.Null(options, nameof(postgresOptions));
options.UseNpgsql(postgresOptions?.ConnectionString,
dbOptions =>
{
dbOptions.MigrationsAssembly(typeof(TContext).Assembly.GetName().Name);
})
// https://github.com/efcore/EFCore.NamingConventions
.UseSnakeCaseNamingConvention();
});
options.UseNpgsql(
postgresOptions?.ConnectionString,
dbOptions =>
{
dbOptions.MigrationsAssembly(typeof(TContext).Assembly.GetName().Name);
})
// https://github.com/efcore/EFCore.NamingConventions
.UseSnakeCaseNamingConvention();
});
services.AddScoped<IDbContext>(provider => provider.GetService<TContext>());
return services;
}
public static IApplicationBuilder UseMigration<TContext>(this IApplicationBuilder app, IWebHostEnvironment env)
where TContext : DbContext, IDbContext
public static IApplicationBuilder UseMigration<TContext>(
this IApplicationBuilder app,
IWebHostEnvironment env
)
where TContext : DbContext, IDbContext
{
MigrateDatabaseAsync<TContext>(app.ApplicationServices).GetAwaiter().GetResult();
@ -62,13 +66,16 @@ public static class Extensions
public static void FilterSoftDeletedProperties(this ModelBuilder modelBuilder)
{
Expression<Func<IAggregate, bool>> filterExpr = e => !e.IsDeleted;
foreach (var mutableEntityType in modelBuilder.Model.GetEntityTypes()
.Where(m => m.ClrType.IsAssignableTo(typeof(IEntity))))
{
// modify expression to handle correct child type
var parameter = Expression.Parameter(mutableEntityType.ClrType);
var body = ReplacingExpressionVisitor
.Replace(filterExpr.Parameters.First(), parameter, filterExpr.Body);
var lambdaExpression = Expression.Lambda(body, parameter);
// set filter
@ -76,8 +83,7 @@ public static class Extensions
}
}
//ref: https://andrewlock.net/customising-asp-net-core-identity-ef-core-naming-conventions-for-postgresql/
// ref: https://andrewlock.net/customising-asp-net-core-identity-ef-core-naming-conventions-for-postgresql/
public static void ToSnakeCaseTables(this ModelBuilder modelBuilder)
{
foreach (var entity in modelBuilder.Model.GetEntityTypes())
@ -86,7 +92,9 @@ public static class Extensions
entity.SetTableName(entity.GetTableName()?.Underscore());
var tableObjectIdentifier =
StoreObjectIdentifier.Table(entity.GetTableName()?.Underscore()!, entity.GetSchema());
StoreObjectIdentifier.Table(
entity.GetTableName()?.Underscore()!,
entity.GetSchema());
// Replace column names
foreach (var property in entity.GetProperties())
@ -107,7 +115,7 @@ public static class Extensions
}
private static async Task MigrateDatabaseAsync<TContext>(IServiceProvider serviceProvider)
where TContext : DbContext, IDbContext
where TContext : DbContext, IDbContext
{
using var scope = serviceProvider.CreateScope();
@ -119,6 +127,7 @@ public static class Extensions
{
using var scope = serviceProvider.CreateScope();
var seeders = scope.ServiceProvider.GetServices<IDataSeeder>();
foreach (var seeder in seeders)
{
await seeder.SeedAllAsync();

View File

@ -22,7 +22,7 @@ public class EventTypeMapper
public static string ToName(Type eventType) => Instance.typeNameMap.GetOrAdd(eventType, _ =>
{
var eventTypeName = eventType.FullName!.Replace(".", "_");
var eventTypeName = eventType.FullName!.Replace(".", "_", StringComparison.CurrentCulture);
Instance.typeMap.AddOrUpdate(eventTypeName, eventType, (_, _) => eventType);
@ -31,7 +31,7 @@ public class EventTypeMapper
public static Type? ToType(string eventTypeName) => Instance.typeMap.GetOrAdd(eventTypeName, _ =>
{
var type = TypeProvider.GetFirstMatchingTypeFromCurrentDomainAssembly(eventTypeName.Replace("_", "."));
var type = TypeProvider.GetFirstMatchingTypeFromCurrentDomainAssembly(eventTypeName.Replace("_", ".", StringComparison.CurrentCulture));
if (type == null)
return null;

View File

@ -17,7 +17,7 @@ public class AuthHeaderHandler : DelegatingHandler
{
var token = (_httpContext?.HttpContext?.Request.Headers["Authorization"])?.ToString();
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token?.Replace("Bearer ", ""));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token?.Replace("Bearer ", "", StringComparison.CurrentCulture));
return base.SendAsync(request, cancellationToken);
}

View File

@ -1,3 +1,4 @@
using System.Globalization;
using System.Text;
using BuildingBlocks.Web;
using Microsoft.AspNetCore.Builder;
@ -44,7 +45,7 @@ namespace BuildingBlocks.Logging
new ElasticsearchSinkOptions(new Uri(logOptions.Elastic.ElasticServiceUrl))
{
AutoRegisterTemplate = true,
IndexFormat = $"{appOptions.Name}-{environment?.ToLower()}"
IndexFormat = $"{appOptions.Name}-{environment?.ToLower(CultureInfo.CurrentCulture)}"
});
}

View File

@ -67,7 +67,7 @@ namespace BuildingBlocks.Mongo
private static bool ParameterMatchProperty(ParameterInfo parameter, PropertyInfo property)
{
return string.Equals(property.Name, parameter.Name, System.StringComparison.InvariantCultureIgnoreCase)
return string.Equals(property.Name, parameter.Name, StringComparison.OrdinalIgnoreCase)
&& parameter.ParameterType == property.PropertyType;
}

View File

@ -1,3 +1,4 @@
using System.Globalization;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Conventions;
@ -42,7 +43,7 @@ public class MongoDbContext : IMongoDbContext
public IMongoCollection<T> GetCollection<T>(string? name = null)
{
return Database.GetCollection<T>(name ?? typeof(T).Name.ToLower());
return Database.GetCollection<T>(name ?? typeof(T).Name.ToLower(CultureInfo.CurrentCulture));
}
public void Dispose()

View File

@ -37,7 +37,7 @@ using Testcontainers.PostgreSql;
using Testcontainers.RabbitMq;
public class TestFixture<TEntryPoint> : IAsyncLifetime
where TEntryPoint : class
where TEntryPoint : class
{
private readonly WebApplicationFactory<TEntryPoint> _factory;
private int Timeout => 120; // Second
@ -60,10 +60,11 @@ public class TestFixture<TEntryPoint> : IAsyncLifetime
var claims =
new Dictionary<string, object>
{
{ ClaimTypes.Name, "test@sample.com" },
{ ClaimTypes.Role, "admin" },
{ClaimTypes.Name, "test@sample.com"},
{ClaimTypes.Role, "admin"},
{"scope", "flight-api"}
};
var httpClient = _factory?.CreateClient();
httpClient.SetFakeBearerToken(claims);
return httpClient;
@ -71,7 +72,9 @@ public class TestFixture<TEntryPoint> : IAsyncLifetime
}
public GrpcChannel Channel =>
GrpcChannel.ForAddress(HttpClient.BaseAddress!, new GrpcChannelOptions { HttpClient = HttpClient });
GrpcChannel.ForAddress(
HttpClient.BaseAddress!,
new GrpcChannelOptions { HttpClient = HttpClient });
public IServiceProvider ServiceProvider => _factory?.Services;
public IConfiguration Configuration => _factory?.Services.GetRequiredService<IConfiguration>();
@ -80,28 +83,36 @@ public class TestFixture<TEntryPoint> : IAsyncLifetime
protected TestFixture()
{
_factory = new WebApplicationFactory<TEntryPoint>()
.WithWebHostBuilder(builder =>
{
builder.ConfigureAppConfiguration(AddCustomAppSettings);
builder.UseEnvironment("test");
builder.ConfigureServices(services =>
.WithWebHostBuilder(
builder =>
{
TestRegistrationServices?.Invoke(services);
services.ReplaceSingleton(AddHttpContextAccessorMock);
builder.ConfigureAppConfiguration(AddCustomAppSettings);
services.AddSingleton<PersistMessageBackgroundService>();
builder.UseEnvironment("test");
// add authentication using a fake jwt bearer - we can use SetAdminUser method to set authenticate user to existing HttContextAccessor
// https://github.com/webmotions/fake-authentication-jwtbearer
// https://github.com/webmotions/fake-authentication-jwtbearer/issues/14
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = FakeJwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = FakeJwtBearerDefaults.AuthenticationScheme;
}).AddFakeJwtBearer();
builder.ConfigureServices(
services =>
{
TestRegistrationServices?.Invoke(services);
services.ReplaceSingleton(AddHttpContextAccessorMock);
services.AddSingleton<PersistMessageBackgroundService>();
// add authentication using a fake jwt bearer - we can use SetAdminUser method to set authenticate user to existing HttContextAccessor
// https://github.com/webmotions/fake-authentication-jwtbearer
// https://github.com/webmotions/fake-authentication-jwtbearer/issues/14
services.AddAuthentication(
options =>
{
options.DefaultAuthenticateScheme =
FakeJwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme =
FakeJwtBearerDefaults.AuthenticationScheme;
})
.AddFakeJwtBearer();
});
});
});
}
public async Task InitializeAsync()
@ -114,7 +125,7 @@ public class TestFixture<TEntryPoint> : IAsyncLifetime
{
await StopTestContainerAsync();
await _factory.DisposeAsync();
CancellationTokenSource.Cancel();
await CancellationTokenSource.CancelAsync();
}
public virtual void RegisterServices(Action<IServiceCollection> services)
@ -153,87 +164,123 @@ public class TestFixture<TEntryPoint> : IAsyncLifetime
public Task<TResponse> SendAsync<TResponse>(IRequest<TResponse> request)
{
return ExecuteScopeAsync(sp =>
{
var mediator = sp.GetRequiredService<IMediator>();
return ExecuteScopeAsync(
sp =>
{
var mediator = sp.GetRequiredService<IMediator>();
return mediator.Send(request);
});
return mediator.Send(request);
});
}
public Task SendAsync(IRequest request)
{
return ExecuteScopeAsync(sp =>
{
var mediator = sp.GetRequiredService<IMediator>();
return mediator.Send(request);
});
return ExecuteScopeAsync(
sp =>
{
var mediator = sp.GetRequiredService<IMediator>();
return mediator.Send(request);
});
}
public async Task Publish<TMessage>(TMessage message, CancellationToken cancellationToken = default)
where TMessage : class, IEvent
public async Task Publish<TMessage>(
TMessage message,
CancellationToken cancellationToken = default
)
where TMessage : class, IEvent
{
await TestHarness.Bus.Publish(message, cancellationToken);
}
public async Task<bool> WaitForPublishing<TMessage>(CancellationToken cancellationToken = default)
where TMessage : class, IEvent
public async Task<bool> WaitForPublishing<TMessage>(
CancellationToken cancellationToken = default
)
where TMessage : class, IEvent
{
var result = await WaitUntilConditionMet(async () =>
{
var published = await TestHarness.Published.Any<TMessage>(cancellationToken);
var faulty = await TestHarness.Published.Any<Fault<TMessage>>(cancellationToken);
return published && faulty == false;
});
var result = await WaitUntilConditionMet(
async () =>
{
var published =
await TestHarness.Published.Any<TMessage>(cancellationToken);
var faulty =
await TestHarness.Published.Any<Fault<TMessage>>(
cancellationToken);
return published && faulty == false;
});
return result;
}
public async Task<bool> WaitForConsuming<TMessage>(CancellationToken cancellationToken = default)
where TMessage : class, IEvent
public async Task<bool> WaitForConsuming<TMessage>(
CancellationToken cancellationToken = default
)
where TMessage : class, IEvent
{
var result = await WaitUntilConditionMet(async () =>
{
var consumed = await TestHarness.Consumed.Any<TMessage>(cancellationToken);
var faulty = await TestHarness.Consumed.Any<Fault<TMessage>>(cancellationToken);
var result = await WaitUntilConditionMet(
async () =>
{
var consumed =
await TestHarness.Consumed.Any<TMessage>(cancellationToken);
return consumed && faulty == false;
});
var faulty =
await TestHarness.Consumed.Any<Fault<TMessage>>(cancellationToken);
return consumed && faulty == false;
});
return result;
}
public async Task<bool> ShouldProcessedPersistInternalCommand<TInternalCommand>(
CancellationToken cancellationToken = default)
where TInternalCommand : class, IInternalCommand
CancellationToken cancellationToken = default
)
where TInternalCommand : class, IInternalCommand
{
var result = await WaitUntilConditionMet(async () =>
{
return await ExecuteScopeAsync(async sp =>
{
var persistMessageProcessor = sp.GetService<IPersistMessageProcessor>();
Guard.Against.Null(persistMessageProcessor, nameof(persistMessageProcessor));
var result = await WaitUntilConditionMet(
async () =>
{
return await ExecuteScopeAsync(
async sp =>
{
var persistMessageProcessor =
sp.GetService<IPersistMessageProcessor>();
var filter = await persistMessageProcessor.GetByFilterAsync(x =>
x.DeliveryType == MessageDeliveryType.Internal &&
typeof(TInternalCommand).ToString() == x.DataType);
Guard.Against.Null(
persistMessageProcessor,
nameof(persistMessageProcessor));
var res = filter.Any(x => x.MessageStatus == MessageStatus.Processed);
var filter =
await persistMessageProcessor.GetByFilterAsync(
x =>
x.DeliveryType ==
MessageDeliveryType.Internal &&
typeof(TInternalCommand).ToString() ==
x.DataType);
return res;
});
});
var res = filter.Any(
x => x.MessageStatus == MessageStatus.Processed);
return res;
});
});
return result;
}
// Ref: https://tech.energyhelpline.com/in-memory-testing-with-masstransit/
private async Task<bool> WaitUntilConditionMet(Func<Task<bool>> conditionToMet, int? timeoutSecond = null)
private async Task<bool> WaitUntilConditionMet(
Func<Task<bool>> conditionToMet,
int? timeoutSecond = null
)
{
var time = timeoutSecond ?? Timeout;
var startTime = DateTime.Now;
var timeoutExpired = false;
var meet = await conditionToMet.Invoke();
while (!meet)
{
if (timeoutExpired)
@ -275,27 +322,42 @@ public class TestFixture<TEntryPoint> : IAsyncLifetime
private void AddCustomAppSettings(IConfigurationBuilder configuration)
{
configuration.AddInMemoryCollection(new KeyValuePair<string, string>[]
{
new("PostgresOptions:ConnectionString", PostgresTestcontainer.GetConnectionString()),
new("PersistMessageOptions:ConnectionString", PostgresPersistTestContainer.GetConnectionString()),
new("RabbitMqOptions:HostName", RabbitMqTestContainer.Hostname),
new("RabbitMqOptions:UserName", TestContainers.RabbitMqContainerConfiguration.UserName),
new("RabbitMqOptions:Password", TestContainers.RabbitMqContainerConfiguration.Password), new(
"RabbitMqOptions:Port",
RabbitMqTestContainer.GetMappedPublicPort(TestContainers.RabbitMqContainerConfiguration.Port)
.ToString(NumberFormatInfo.InvariantInfo)),
new("MongoOptions:ConnectionString", MongoDbTestContainer.GetConnectionString()),
new("MongoOptions:DatabaseName", TestContainers.MongoContainerConfiguration.Name),
new("EventStoreOptions:ConnectionString", EventStoreDbTestContainer.GetConnectionString())
});
configuration.AddInMemoryCollection(
new KeyValuePair<string, string>[]
{
new(
"PostgresOptions:ConnectionString",
PostgresTestcontainer.GetConnectionString()),
new(
"PersistMessageOptions:ConnectionString",
PostgresPersistTestContainer.GetConnectionString()),
new("RabbitMqOptions:HostName", RabbitMqTestContainer.Hostname),
new(
"RabbitMqOptions:UserName",
TestContainers.RabbitMqContainerConfiguration.UserName),
new(
"RabbitMqOptions:Password",
TestContainers.RabbitMqContainerConfiguration.Password),
new(
"RabbitMqOptions:Port",
RabbitMqTestContainer.GetMappedPublicPort(
TestContainers.RabbitMqContainerConfiguration.Port)
.ToString(NumberFormatInfo.InvariantInfo)),
new("MongoOptions:ConnectionString", MongoDbTestContainer.GetConnectionString()),
new("MongoOptions:DatabaseName", TestContainers.MongoContainerConfiguration.Name),
new(
"EventStoreOptions:ConnectionString",
EventStoreDbTestContainer.GetConnectionString())
});
}
private IHttpContextAccessor AddHttpContextAccessorMock(IServiceProvider serviceProvider)
{
var httpContextAccessorMock = Substitute.For<IHttpContextAccessor>();
using var scope = serviceProvider.CreateScope();
httpContextAccessorMock.HttpContext = new DefaultHttpContext { RequestServices = scope.ServiceProvider };
httpContextAccessorMock.HttpContext = new DefaultHttpContext
{ RequestServices = scope.ServiceProvider };
httpContextAccessorMock.HttpContext.Request.Host = new HostString("localhost", 6012);
httpContextAccessorMock.HttpContext.Request.Scheme = "http";
@ -305,8 +367,8 @@ public class TestFixture<TEntryPoint> : IAsyncLifetime
}
public class TestWriteFixture<TEntryPoint, TWContext> : TestFixture<TEntryPoint>
where TEntryPoint : class
where TWContext : DbContext
where TEntryPoint : class
where TWContext : DbContext
{
public Task ExecuteDbContextAsync(Func<TWContext, Task> action)
{
@ -320,7 +382,8 @@ public class TestWriteFixture<TEntryPoint, TWContext> : TestFixture<TEntryPoint>
public Task ExecuteDbContextAsync(Func<TWContext, IMediator, Task> action)
{
return ExecuteScopeAsync(sp => action(sp.GetService<TWContext>(), sp.GetService<IMediator>()));
return ExecuteScopeAsync(
sp => action(sp.GetService<TWContext>(), sp.GetService<IMediator>()));
}
public Task<T> ExecuteDbContextAsync<T>(Func<TWContext, Task<T>> action)
@ -335,94 +398,110 @@ public class TestWriteFixture<TEntryPoint, TWContext> : TestFixture<TEntryPoint>
public Task<T> ExecuteDbContextAsync<T>(Func<TWContext, IMediator, Task<T>> action)
{
return ExecuteScopeAsync(sp => action(sp.GetService<TWContext>(), sp.GetService<IMediator>()));
return ExecuteScopeAsync(
sp => action(sp.GetService<TWContext>(), sp.GetService<IMediator>()));
}
public Task InsertAsync<T>(params T[] entities) where T : class
public Task InsertAsync<T>(params T[] entities)
where T : class
{
return ExecuteDbContextAsync(db =>
{
foreach (var entity in entities)
return ExecuteDbContextAsync(
db =>
{
db.Set<T>().Add(entity);
}
foreach (var entity in entities)
{
db.Set<T>().Add(entity);
}
return db.SaveChangesAsync();
});
return db.SaveChangesAsync();
});
}
public async Task InsertAsync<TEntity>(TEntity entity) where TEntity : class
public async Task InsertAsync<TEntity>(TEntity entity)
where TEntity : class
{
await ExecuteDbContextAsync(db =>
{
db.Set<TEntity>().Add(entity);
await ExecuteDbContextAsync(
db =>
{
db.Set<TEntity>().Add(entity);
return db.SaveChangesAsync();
});
return db.SaveChangesAsync();
});
}
public Task InsertAsync<TEntity, TEntity2>(TEntity entity, TEntity2 entity2)
where TEntity : class
where TEntity2 : class
where TEntity : class
where TEntity2 : class
{
return ExecuteDbContextAsync(db =>
{
db.Set<TEntity>().Add(entity);
db.Set<TEntity2>().Add(entity2);
return ExecuteDbContextAsync(
db =>
{
db.Set<TEntity>().Add(entity);
db.Set<TEntity2>().Add(entity2);
return db.SaveChangesAsync();
});
return db.SaveChangesAsync();
});
}
public Task InsertAsync<TEntity, TEntity2, TEntity3>(TEntity entity, TEntity2 entity2, TEntity3 entity3)
where TEntity : class
where TEntity2 : class
where TEntity3 : class
public Task InsertAsync<TEntity, TEntity2, TEntity3>(
TEntity entity,
TEntity2 entity2,
TEntity3 entity3
)
where TEntity : class
where TEntity2 : class
where TEntity3 : class
{
return ExecuteDbContextAsync(db =>
{
db.Set<TEntity>().Add(entity);
db.Set<TEntity2>().Add(entity2);
db.Set<TEntity3>().Add(entity3);
return ExecuteDbContextAsync(
db =>
{
db.Set<TEntity>().Add(entity);
db.Set<TEntity2>().Add(entity2);
db.Set<TEntity3>().Add(entity3);
return db.SaveChangesAsync();
});
return db.SaveChangesAsync();
});
}
public Task InsertAsync<TEntity, TEntity2, TEntity3, TEntity4>(TEntity entity, TEntity2 entity2, TEntity3 entity3,
TEntity4 entity4)
where TEntity : class
where TEntity2 : class
where TEntity3 : class
where TEntity4 : class
public Task InsertAsync<TEntity, TEntity2, TEntity3, TEntity4>(
TEntity entity,
TEntity2 entity2,
TEntity3 entity3,
TEntity4 entity4
)
where TEntity : class
where TEntity2 : class
where TEntity3 : class
where TEntity4 : class
{
return ExecuteDbContextAsync(db =>
{
db.Set<TEntity>().Add(entity);
db.Set<TEntity2>().Add(entity2);
db.Set<TEntity3>().Add(entity3);
db.Set<TEntity4>().Add(entity4);
return ExecuteDbContextAsync(
db =>
{
db.Set<TEntity>().Add(entity);
db.Set<TEntity2>().Add(entity2);
db.Set<TEntity3>().Add(entity3);
db.Set<TEntity4>().Add(entity4);
return db.SaveChangesAsync();
});
return db.SaveChangesAsync();
});
}
public Task<T> FindAsync<T, TKey>(TKey id)
where T : class, IEntity
where T : class, IEntity
{
return ExecuteDbContextAsync(db => db.Set<T>().FindAsync(id).AsTask());
}
public Task<T> FirstOrDefaultAsync<T>()
where T : class, IEntity
where T : class, IEntity
{
return ExecuteDbContextAsync(db => db.Set<T>().FirstOrDefaultAsync());
}
}
public class TestReadFixture<TEntryPoint, TRContext> : TestFixture<TEntryPoint>
where TEntryPoint : class
where TRContext : MongoDbContext
where TEntryPoint : class
where TRContext : MongoDbContext
{
public Task ExecuteReadContextAsync(Func<TRContext, Task> action)
{
@ -434,19 +513,22 @@ public class TestReadFixture<TEntryPoint, TRContext> : TestFixture<TEntryPoint>
return ExecuteScopeAsync(sp => action(sp.GetRequiredService<TRContext>()));
}
public async Task InsertMongoDbContextAsync<T>(string collectionName, params T[] entities) where T : class
public async Task InsertMongoDbContextAsync<T>(string collectionName, params T[] entities)
where T : class
{
await ExecuteReadContextAsync(async db =>
{
await db.GetCollection<T>(collectionName).InsertManyAsync(entities.ToList());
});
await ExecuteReadContextAsync(
async db =>
{
await db.GetCollection<T>(collectionName).InsertManyAsync(entities.ToList());
});
}
}
public class TestFixture<TEntryPoint, TWContext, TRContext> : TestWriteFixture<TEntryPoint, TWContext>
where TEntryPoint : class
where TWContext : DbContext
where TRContext : MongoDbContext
public class TestFixture<TEntryPoint, TWContext, TRContext>
: TestWriteFixture<TEntryPoint, TWContext>
where TEntryPoint : class
where TWContext : DbContext
where TRContext : MongoDbContext
{
public Task ExecuteReadContextAsync(Func<TRContext, Task> action)
{
@ -458,17 +540,19 @@ public class TestFixture<TEntryPoint, TWContext, TRContext> : TestWriteFixture<T
return ExecuteScopeAsync(sp => action(sp.GetRequiredService<TRContext>()));
}
public async Task InsertMongoDbContextAsync<T>(string collectionName, params T[] entities) where T : class
public async Task InsertMongoDbContextAsync<T>(string collectionName, params T[] entities)
where T : class
{
await ExecuteReadContextAsync(async db =>
{
await db.GetCollection<T>(collectionName).InsertManyAsync(entities.ToList());
});
await ExecuteReadContextAsync(
async db =>
{
await db.GetCollection<T>(collectionName).InsertManyAsync(entities.ToList());
});
}
}
public class TestFixtureCore<TEntryPoint> : IAsyncLifetime
where TEntryPoint : class
where TEntryPoint : class
{
private Respawner _reSpawnerDefaultDb;
private Respawner _reSpawnerPersistDb;
@ -476,7 +560,10 @@ public class TestFixtureCore<TEntryPoint> : IAsyncLifetime
private NpgsqlConnection PersistDbConnection { get; set; }
public TestFixtureCore(TestFixture<TEntryPoint> integrationTestFixture, ITestOutputHelper outputHelper)
public TestFixtureCore(
TestFixture<TEntryPoint> integrationTestFixture,
ITestOutputHelper outputHelper
)
{
Fixture = integrationTestFixture;
integrationTestFixture.RegisterServices(RegisterTestsServices);
@ -505,13 +592,15 @@ public class TestFixtureCore<TEntryPoint> : IAsyncLifetime
if (!string.IsNullOrEmpty(persistOptions?.ConnectionString))
{
await Fixture.PersistMessageBackgroundService.StartAsync(Fixture.CancellationTokenSource.Token);
await Fixture.PersistMessageBackgroundService.StartAsync(
Fixture.CancellationTokenSource.Token);
PersistDbConnection = new NpgsqlConnection(persistOptions.ConnectionString);
await PersistDbConnection.OpenAsync();
_reSpawnerPersistDb = await Respawner.CreateAsync(PersistDbConnection,
new RespawnerOptions { DbAdapter = DbAdapter.Postgres });
_reSpawnerPersistDb = await Respawner.CreateAsync(
PersistDbConnection,
new RespawnerOptions { DbAdapter = DbAdapter.Postgres });
}
if (!string.IsNullOrEmpty(postgresOptions?.ConnectionString))
@ -519,8 +608,9 @@ public class TestFixtureCore<TEntryPoint> : IAsyncLifetime
DefaultDbConnection = new NpgsqlConnection(postgresOptions.ConnectionString);
await DefaultDbConnection.OpenAsync();
_reSpawnerDefaultDb = await Respawner.CreateAsync(DefaultDbConnection,
new RespawnerOptions { DbAdapter = DbAdapter.Postgres });
_reSpawnerDefaultDb = await Respawner.CreateAsync(
DefaultDbConnection,
new RespawnerOptions { DbAdapter = DbAdapter.Postgres });
await SeedDataAsync();
}
@ -532,7 +622,8 @@ public class TestFixtureCore<TEntryPoint> : IAsyncLifetime
{
await _reSpawnerPersistDb.ResetAsync(PersistDbConnection);
await Fixture.PersistMessageBackgroundService.StopAsync(Fixture.CancellationTokenSource.Token);
await Fixture.PersistMessageBackgroundService.StopAsync(
Fixture.CancellationTokenSource.Token);
}
if (DefaultDbConnection is not null)
@ -545,8 +636,10 @@ public class TestFixtureCore<TEntryPoint> : IAsyncLifetime
{
//https://stackoverflow.com/questions/3366397/delete-everything-in-a-mongodb-database
var dbClient = new MongoClient(Fixture.MongoDbTestContainer?.GetConnectionString());
var collections = await dbClient.GetDatabase(TestContainers.MongoContainerConfiguration.Name)
.ListCollectionsAsync(cancellationToken: cancellationToken);
var collections = await dbClient
.GetDatabase(TestContainers.MongoContainerConfiguration.Name)
.ListCollectionsAsync(cancellationToken: cancellationToken);
foreach (var collection in collections.ToList())
{
@ -557,27 +650,30 @@ public class TestFixtureCore<TEntryPoint> : IAsyncLifetime
private async Task ResetRabbitMqAsync(CancellationToken cancellationToken = default)
{
var port = Fixture.RabbitMqTestContainer?.GetMappedPublicPort(TestContainers.RabbitMqContainerConfiguration
.ApiPort)
?? TestContainers.RabbitMqContainerConfiguration.ApiPort;
var port = Fixture.RabbitMqTestContainer?.GetMappedPublicPort(
TestContainers.RabbitMqContainerConfiguration
.ApiPort) ??
TestContainers.RabbitMqContainerConfiguration.ApiPort;
var managementClient = new ManagementClient(Fixture.RabbitMqTestContainer?.Hostname,
TestContainers.RabbitMqContainerConfiguration?.UserName,
TestContainers.RabbitMqContainerConfiguration?.Password, port);
var bd = await managementClient.GetBindingsAsync(cancellationToken);
var bindings = bd.Where(x => !string.IsNullOrEmpty(x.Source) && !string.IsNullOrEmpty(x.Destination));
var bindings = bd.Where(
x => !string.IsNullOrEmpty(x.Source) && !string.IsNullOrEmpty(x.Destination));
foreach (var binding in bindings)
{
await managementClient.DeleteBindingAsync(binding, cancellationToken);
}
var queues = await managementClient.GetQueuesAsync(cancellationToken);
var queues = await managementClient.GetQueuesAsync(cancellationToken: cancellationToken);
foreach (var queue in queues)
{
await managementClient.DeleteQueueAsync(queue, cancellationToken);
await managementClient.PurgeAsync(queue, cancellationToken);
}
}
@ -590,6 +686,7 @@ public class TestFixtureCore<TEntryPoint> : IAsyncLifetime
using var scope = Fixture.ServiceProvider.CreateScope();
var seeders = scope.ServiceProvider.GetServices<IDataSeeder>();
foreach (var seeder in seeders)
{
await seeder.SeedAllAsync();
@ -598,13 +695,14 @@ public class TestFixtureCore<TEntryPoint> : IAsyncLifetime
}
public abstract class TestReadBase<TEntryPoint, TRContext> : TestFixtureCore<TEntryPoint>
//,IClassFixture<IntegrationTestFactory<TEntryPoint, TWContext>>
where TEntryPoint : class
where TRContext : MongoDbContext
// ,IClassFixture<IntegrationTestFactory<TEntryPoint, TWContext>>
where TEntryPoint : class
where TRContext : MongoDbContext
{
protected TestReadBase(
TestReadFixture<TEntryPoint, TRContext> integrationTestFixture, ITestOutputHelper outputHelper = null) : base(
integrationTestFixture, outputHelper)
TestReadFixture<TEntryPoint, TRContext> integrationTestFixture,
ITestOutputHelper outputHelper = null
) : base(integrationTestFixture, outputHelper)
{
Fixture = integrationTestFixture;
}
@ -613,13 +711,14 @@ public abstract class TestReadBase<TEntryPoint, TRContext> : TestFixtureCore<TEn
}
public abstract class TestWriteBase<TEntryPoint, TWContext> : TestFixtureCore<TEntryPoint>
//,IClassFixture<IntegrationTestFactory<TEntryPoint, TWContext>>
where TEntryPoint : class
where TWContext : DbContext
//,IClassFixture<IntegrationTestFactory<TEntryPoint, TWContext>>
where TEntryPoint : class
where TWContext : DbContext
{
protected TestWriteBase(
TestWriteFixture<TEntryPoint, TWContext> integrationTestFixture, ITestOutputHelper outputHelper = null) : base(
integrationTestFixture, outputHelper)
TestWriteFixture<TEntryPoint, TWContext> integrationTestFixture,
ITestOutputHelper outputHelper = null
) : base(integrationTestFixture, outputHelper)
{
Fixture = integrationTestFixture;
}
@ -628,13 +727,15 @@ public abstract class TestWriteBase<TEntryPoint, TWContext> : TestFixtureCore<TE
}
public abstract class TestBase<TEntryPoint, TWContext, TRContext> : TestFixtureCore<TEntryPoint>
//,IClassFixture<IntegrationTestFactory<TEntryPoint, TWContext, TRContext>>
where TEntryPoint : class
where TWContext : DbContext
where TRContext : MongoDbContext
//,IClassFixture<IntegrationTestFactory<TEntryPoint, TWContext, TRContext>>
where TEntryPoint : class
where TWContext : DbContext
where TRContext : MongoDbContext
{
protected TestBase(
TestFixture<TEntryPoint, TWContext, TRContext> integrationTestFixture, ITestOutputHelper outputHelper = null) :
TestFixture<TEntryPoint, TWContext, TRContext> integrationTestFixture,
ITestOutputHelper outputHelper = null
) :
base(integrationTestFixture, outputHelper)
{
Fixture = integrationTestFixture;

View File

@ -1,3 +1,4 @@
using System.Globalization;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Routing;
@ -10,6 +11,6 @@ public class SlugifyParameterTransformer : IOutboundParameterTransformer
// Slugify value
return value == null
? null
: Regex.Replace(value.ToString() ?? string.Empty, "([a-z])([A-Z])", "$1-$2").ToLower();
: Regex.Replace(value.ToString() ?? string.Empty, "([a-z])([A-Z])", "$1-$2").ToLower(CultureInfo.CurrentCulture);
}
}

View File

@ -45,7 +45,7 @@ public static class InitialData
ArriveDate.Of(new DateTime(2022, 1, 31, 14, 0, 0)),
AirportId.Of(Airports.Last().Id), DurationMinutes.Of(120m),
FlightDate.Of(new DateTime(2022, 1, 31, 13, 0, 0)), global::Flight.Flights.Enums.FlightStatus.Completed,
Price.Of((decimal)8000))
Price.Of(8000))
};
Seats = new List<Seat>

View File

@ -25,7 +25,7 @@ public class GetFlightByIdTests : FlightEndToEndTestBase
await Fixture.SendAsync(command);
// Act
var route = ApiRoutes.Flight.GetFlightById.Replace(ApiRoutes.Flight.Id, command.Id.ToString());
var route = ApiRoutes.Flight.GetFlightById.Replace(ApiRoutes.Flight.Id, command.Id.ToString(), StringComparison.CurrentCulture);
var result = await Fixture.HttpClient.GetAsync(route);
// Assert

View File

@ -65,7 +65,7 @@ public static class DbContextFactory
ArriveDate.Of( new DateTime(2022, 1, 31, 14, 0, 0)),
AirportId.Of( _airportId2), DurationMinutes.Of(120m),
FlightDate.Of( new DateTime(2022, 1, 31)), FlightStatus.Completed,
Price.Of((decimal)8000))
Price.Of(8000))
};
context.Flights.AddRange(flights);