From ff65f5daeb1e91e34d12ae5d787f87de83caf1de Mon Sep 17 00:00:00 2001 From: meysamhadeli Date: Sun, 15 May 2022 02:17:31 +0430 Subject: [PATCH] AddMassTransitTestHarness with Rabbitmq --- src/BuildingBlocks/BuildingBlocks.csproj | 185 +++++++++--------- src/BuildingBlocks/CAP/Extensions.cs | 1 + .../Domain/Model/{Entity.cs => Auditable.cs} | 0 src/BuildingBlocks/Domain/Model/IAuditable.cs | 2 +- src/BuildingBlocks/Domain/Model/IEntity.cs | 1 - src/BuildingBlocks/EFCore/AppDbContextBase.cs | 2 +- src/BuildingBlocks/EFCore/Extensions.cs | 10 +- src/BuildingBlocks/Jwt/JwtExtensions.cs | 1 + src/BuildingBlocks/MassTransit/Extensions.cs | 1 + src/BuildingBlocks/Utils/EnumExtensions.cs | 28 --- src/BuildingBlocks/Utils/ObjectExtensions.cs | 18 -- src/BuildingBlocks/Utils/ReflectionHelpers.cs | 60 ------ .../SlugifyParameterTransformer.cs | 2 +- .../Web/ApiVersioningExtensions.cs | 34 ++++ .../{Utils => Web}/ConfigurationExtensions.cs | 2 +- .../{Utils => Web}/ConfigurationHelper.cs | 4 +- .../Web/CorrelationExtensions.cs | 26 +++ src/BuildingBlocks/Web/Extensions.cs | 62 ------ .../Web/ServiceCollectionExtensions.cs | 60 ++++++ .../Booking/src/Booking.Api/Program.cs | 2 +- src/Services/Flight/src/Flight.Api/Program.cs | 8 +- .../src/Flight.Api/appsettings.test.json | 12 +- .../Flight/src/Flight/Data/FlightDbContext.cs | 28 +-- ...ner.cs => 20220511215248_Init.Designer.cs} | 2 +- ...9213249_Init.cs => 20220511215248_Init.cs} | 0 src/Services/Flight/tests/DeleteTests.cs | 18 +- src/Services/Flight/tests/TestFixture.cs | 180 +++++++++-------- .../Passenger/src/Passenger.Api/Program.cs | 3 +- 28 files changed, 362 insertions(+), 390 deletions(-) rename src/BuildingBlocks/Domain/Model/{Entity.cs => Auditable.cs} (100%) delete mode 100644 src/BuildingBlocks/Utils/EnumExtensions.cs delete mode 100644 src/BuildingBlocks/Utils/ObjectExtensions.cs delete mode 100644 src/BuildingBlocks/Utils/ReflectionHelpers.cs rename src/BuildingBlocks/{Web => Utils}/SlugifyParameterTransformer.cs (92%) create mode 100644 src/BuildingBlocks/Web/ApiVersioningExtensions.cs rename src/BuildingBlocks/{Utils => Web}/ConfigurationExtensions.cs (96%) rename src/BuildingBlocks/{Utils => Web}/ConfigurationHelper.cs (95%) create mode 100644 src/BuildingBlocks/Web/CorrelationExtensions.cs delete mode 100644 src/BuildingBlocks/Web/Extensions.cs create mode 100644 src/BuildingBlocks/Web/ServiceCollectionExtensions.cs rename src/Services/Flight/src/Flight/Data/Migrations/{20220509213249_Init.Designer.cs => 20220511215248_Init.Designer.cs} (99%) rename src/Services/Flight/src/Flight/Data/Migrations/{20220509213249_Init.cs => 20220511215248_Init.cs} (100%) diff --git a/src/BuildingBlocks/BuildingBlocks.csproj b/src/BuildingBlocks/BuildingBlocks.csproj index 6d5cf38..e3c5308 100644 --- a/src/BuildingBlocks/BuildingBlocks.csproj +++ b/src/BuildingBlocks/BuildingBlocks.csproj @@ -7,114 +7,117 @@ - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + - - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + - - + + diff --git a/src/BuildingBlocks/CAP/Extensions.cs b/src/BuildingBlocks/CAP/Extensions.cs index cf1c6fa..db8c03f 100644 --- a/src/BuildingBlocks/CAP/Extensions.cs +++ b/src/BuildingBlocks/CAP/Extensions.cs @@ -1,6 +1,7 @@ using System.Text.Encodings.Web; using System.Text.Unicode; using BuildingBlocks.Utils; +using BuildingBlocks.Web; using DotNetCore.CAP; using DotNetCore.CAP.Messages; using Microsoft.EntityFrameworkCore; diff --git a/src/BuildingBlocks/Domain/Model/Entity.cs b/src/BuildingBlocks/Domain/Model/Auditable.cs similarity index 100% rename from src/BuildingBlocks/Domain/Model/Entity.cs rename to src/BuildingBlocks/Domain/Model/Auditable.cs diff --git a/src/BuildingBlocks/Domain/Model/IAuditable.cs b/src/BuildingBlocks/Domain/Model/IAuditable.cs index 15dadf9..348941b 100644 --- a/src/BuildingBlocks/Domain/Model/IAuditable.cs +++ b/src/BuildingBlocks/Domain/Model/IAuditable.cs @@ -1,6 +1,6 @@ namespace BuildingBlocks.Domain.Model; -public interface IAuditable +public interface IAuditable : IEntity { public DateTime? CreatedAt { get; set; } public long? CreatedBy { get; set; } diff --git a/src/BuildingBlocks/Domain/Model/IEntity.cs b/src/BuildingBlocks/Domain/Model/IEntity.cs index c7b88a5..7cc60fd 100644 --- a/src/BuildingBlocks/Domain/Model/IEntity.cs +++ b/src/BuildingBlocks/Domain/Model/IEntity.cs @@ -2,7 +2,6 @@ namespace BuildingBlocks.Domain.Model; public interface IEntity { - long Id { get; } } public interface IEntity diff --git a/src/BuildingBlocks/EFCore/AppDbContextBase.cs b/src/BuildingBlocks/EFCore/AppDbContextBase.cs index b43409a..3b5788b 100644 --- a/src/BuildingBlocks/EFCore/AppDbContextBase.cs +++ b/src/BuildingBlocks/EFCore/AppDbContextBase.cs @@ -44,7 +44,7 @@ public abstract class AppDbContextBase : DbContext, IDbContext await SaveChangesAsync(cancellationToken); await _currentTransaction?.CommitAsync(cancellationToken)!; } - catch(System.Exception ex) + catch { await RollbackTransactionAsync(cancellationToken); throw; diff --git a/src/BuildingBlocks/EFCore/Extensions.cs b/src/BuildingBlocks/EFCore/Extensions.cs index 31daaba..30ace5f 100644 --- a/src/BuildingBlocks/EFCore/Extensions.cs +++ b/src/BuildingBlocks/EFCore/Extensions.cs @@ -1,4 +1,3 @@ -using System.Reflection; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -9,16 +8,15 @@ public static class Extensions { public static IServiceCollection AddCustomDbContext( this IServiceCollection services, - IConfiguration configuration, - Assembly migrationAssembly) + IConfiguration configuration) where TContext : AppDbContextBase { - services.AddScoped(provider => provider.GetService()); - services.AddDbContext(options => options.UseSqlServer( configuration.GetConnectionString("DefaultConnection"), - x => x.MigrationsAssembly(migrationAssembly.GetName().Name))); + x => x.MigrationsAssembly(typeof(TContext).Assembly.GetName().Name))); + + services.AddScoped(provider => provider.GetService()); return services; } diff --git a/src/BuildingBlocks/Jwt/JwtExtensions.cs b/src/BuildingBlocks/Jwt/JwtExtensions.cs index f903cc7..e526541 100644 --- a/src/BuildingBlocks/Jwt/JwtExtensions.cs +++ b/src/BuildingBlocks/Jwt/JwtExtensions.cs @@ -1,4 +1,5 @@ using BuildingBlocks.Utils; +using BuildingBlocks.Web; using Duende.IdentityServer.Models; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.DependencyInjection; diff --git a/src/BuildingBlocks/MassTransit/Extensions.cs b/src/BuildingBlocks/MassTransit/Extensions.cs index 28c92a5..bce3f2e 100644 --- a/src/BuildingBlocks/MassTransit/Extensions.cs +++ b/src/BuildingBlocks/MassTransit/Extensions.cs @@ -1,6 +1,7 @@ using System.Reflection; using BuildingBlocks.Domain.Event; using BuildingBlocks.Utils; +using BuildingBlocks.Web; using Humanizer; using MassTransit; using Microsoft.AspNetCore.Hosting; diff --git a/src/BuildingBlocks/Utils/EnumExtensions.cs b/src/BuildingBlocks/Utils/EnumExtensions.cs deleted file mode 100644 index ec35658..0000000 --- a/src/BuildingBlocks/Utils/EnumExtensions.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.ComponentModel; - -namespace BuildingBlocks.Utils -{ - //https://stackoverflow.com/a/19621488/581476 - public static class EnumExtensions - { - // This extension method is broken out so you can use a similar pattern with - // other MetaData elements in the future. This is your base method for each. - public static T GetAttribute(this Enum value) where T : Attribute { - var type = value.GetType(); - var memberInfo = type.GetMember(value.ToString()); - var attributes = memberInfo[0].GetCustomAttributes(typeof(T), false); - return attributes.Length > 0 - ? (T)attributes[0] - : null; - } - - // This method creates a specific call to the above method, requesting the - // Description MetaData attribute. - public static string ToName(this Enum value) { - var attribute = value.GetAttribute(); - return attribute == null ? value.ToString() : attribute.Description; - } - - } -} \ No newline at end of file diff --git a/src/BuildingBlocks/Utils/ObjectExtensions.cs b/src/BuildingBlocks/Utils/ObjectExtensions.cs deleted file mode 100644 index 4c26baf..0000000 --- a/src/BuildingBlocks/Utils/ObjectExtensions.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Linq; -using System.Web; - -namespace BuildingBlocks.Utils -{ - public static class ObjectExtensions - { - public static string GetQueryString(this object obj) - { - var properties = from p in obj.GetType().GetProperties() - where p.GetValue(obj, null) != null - select p.Name + "=" + HttpUtility.UrlEncode(p.GetValue(obj, null).ToString()); - - return String.Join("&", properties.ToArray()); - } - } -} \ No newline at end of file diff --git a/src/BuildingBlocks/Utils/ReflectionHelpers.cs b/src/BuildingBlocks/Utils/ReflectionHelpers.cs deleted file mode 100644 index f4e8e16..0000000 --- a/src/BuildingBlocks/Utils/ReflectionHelpers.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Linq; -using System.Reflection; - -namespace BuildingBlocks.Utils -{ - public static class ReflectionHelpers - { - private static readonly ConcurrentDictionary TypeCacheKeys = new(); - private static readonly ConcurrentDictionary PrettyPrintCache = new(); - - public static string GetCacheKey(this Type type) - { - return TypeCacheKeys.GetOrAdd(type, t => $"{t.PrettyPrint()}"); - } - public static string PrettyPrint(this Type type) - { - return PrettyPrintCache.GetOrAdd( - type, - t => - { - try - { - return PrettyPrintRecursive(t, 0); - } - catch (System.Exception) - { - return t.Name; - } - }); - } - - public static bool IsActionDelegate(this Type sourceType) - { - if (sourceType.IsSubclassOf(typeof(MulticastDelegate)) && - sourceType.GetMethod("Invoke").ReturnType == typeof(void)) - return true; - return false; - } - private static string PrettyPrintRecursive(Type type, int depth) - { - if (depth > 3) - { - return type.Name; - } - - var nameParts = type.Name.Split('`'); - if (nameParts.Length == 1) - { - return nameParts[0]; - } - - var genericArguments = type.GetTypeInfo().GetGenericArguments(); - return !type.IsConstructedGenericType - ? $"{nameParts[0]}<{new string(',', genericArguments.Length - 1)}>" - : $"{nameParts[0]}<{string.Join(",", genericArguments.Select(t => PrettyPrintRecursive(t, depth + 1)))}>"; - } - } -} \ No newline at end of file diff --git a/src/BuildingBlocks/Web/SlugifyParameterTransformer.cs b/src/BuildingBlocks/Utils/SlugifyParameterTransformer.cs similarity index 92% rename from src/BuildingBlocks/Web/SlugifyParameterTransformer.cs rename to src/BuildingBlocks/Utils/SlugifyParameterTransformer.cs index 9aecf77..e0a7612 100644 --- a/src/BuildingBlocks/Web/SlugifyParameterTransformer.cs +++ b/src/BuildingBlocks/Utils/SlugifyParameterTransformer.cs @@ -1,7 +1,7 @@ using System.Text.RegularExpressions; using Microsoft.AspNetCore.Routing; -namespace BuildingBlocks.Web; +namespace BuildingBlocks.Utils; public class SlugifyParameterTransformer : IOutboundParameterTransformer { diff --git a/src/BuildingBlocks/Web/ApiVersioningExtensions.cs b/src/BuildingBlocks/Web/ApiVersioningExtensions.cs new file mode 100644 index 0000000..a893b44 --- /dev/null +++ b/src/BuildingBlocks/Web/ApiVersioningExtensions.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Versioning; +using Microsoft.Extensions.DependencyInjection; + +namespace BuildingBlocks.Web; + +public static class ApiVersioningExtensions +{ + public static void AddCustomVersioning(this IServiceCollection services, + Action configurator = null) + { + //https://www.meziantou.net/versioning-an-asp-net-core-api.htm + //https://exceptionnotfound.net/overview-of-api-versioning-in-asp-net-core-3-0/ + services.AddApiVersioning(options => + { + // Add the headers "api-supported-versions" and "api-deprecated-versions" + // This is better for discoverability + options.ReportApiVersions = true; + + // AssumeDefaultVersionWhenUnspecified should only be enabled when supporting legacy services that did not previously + // support API versioning. Forcing existing clients to specify an explicit API version for an + // existing service introduces a breaking change. Conceptually, clients in this situation are + // bound to some API version of a service, but they don't know what it is and never explicit request it. + options.AssumeDefaultVersionWhenUnspecified = true; + options.DefaultApiVersion = new ApiVersion(1, 0); + + // // Defines how an API version is read from the current HTTP request + options.ApiVersionReader = ApiVersionReader.Combine(new HeaderApiVersionReader("api-version"), + new UrlSegmentApiVersionReader()); + + configurator?.Invoke(options); + }); + } +} diff --git a/src/BuildingBlocks/Utils/ConfigurationExtensions.cs b/src/BuildingBlocks/Web/ConfigurationExtensions.cs similarity index 96% rename from src/BuildingBlocks/Utils/ConfigurationExtensions.cs rename to src/BuildingBlocks/Web/ConfigurationExtensions.cs index ace0c7b..76c437e 100644 --- a/src/BuildingBlocks/Utils/ConfigurationExtensions.cs +++ b/src/BuildingBlocks/Web/ConfigurationExtensions.cs @@ -2,7 +2,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -namespace BuildingBlocks.Utils; +namespace BuildingBlocks.Web; public static class ConfigurationExtensions { diff --git a/src/BuildingBlocks/Utils/ConfigurationHelper.cs b/src/BuildingBlocks/Web/ConfigurationHelper.cs similarity index 95% rename from src/BuildingBlocks/Utils/ConfigurationHelper.cs rename to src/BuildingBlocks/Web/ConfigurationHelper.cs index 908857c..87480fc 100644 --- a/src/BuildingBlocks/Utils/ConfigurationHelper.cs +++ b/src/BuildingBlocks/Web/ConfigurationHelper.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.Configuration; -namespace BuildingBlocks.Utils +namespace BuildingBlocks.Web { public static class ConfigurationHelper { @@ -17,4 +17,4 @@ namespace BuildingBlocks.Utils .Build(); } } -} \ No newline at end of file +} diff --git a/src/BuildingBlocks/Web/CorrelationExtensions.cs b/src/BuildingBlocks/Web/CorrelationExtensions.cs new file mode 100644 index 0000000..64c5347 --- /dev/null +++ b/src/BuildingBlocks/Web/CorrelationExtensions.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; + +namespace BuildingBlocks.Web; + +public static class CorrelationExtensions +{ + private const string CorrelationId = "correlationId"; + + public static IApplicationBuilder UseCorrelationId(this IApplicationBuilder app) + { + return app.Use(async (ctx, next) => + { + if (!ctx.Request.Headers.TryGetValue(CorrelationId, out var correlationId)) + correlationId = Guid.NewGuid().ToString("N"); + + ctx.Items[CorrelationId] = correlationId.ToString(); + await next(); + }); + } + + public static string GetCorrelationId(this HttpContext context) + { + return context.Items.TryGetValue(CorrelationId, out var correlationId) ? correlationId as string : null; + } +} diff --git a/src/BuildingBlocks/Web/Extensions.cs b/src/BuildingBlocks/Web/Extensions.cs deleted file mode 100644 index 88fc73d..0000000 --- a/src/BuildingBlocks/Web/Extensions.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Linq; -using System.Net; -using System.Net.Http.Headers; -using JetBrains.Annotations; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Versioning; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace BuildingBlocks.Web -{ - public static class Extensions - { - private const string CorrelationId = "correlationId"; - - public static void AddCustomVersioning(this IServiceCollection services, - Action configurator = null) - { - //https://www.meziantou.net/versioning-an-asp-net-core-api.htm - //https://exceptionnotfound.net/overview-of-api-versioning-in-asp-net-core-3-0/ - services.AddApiVersioning(options => - { - // Add the headers "api-supported-versions" and "api-deprecated-versions" - // This is better for discoverability - options.ReportApiVersions = true; - - // AssumeDefaultVersionWhenUnspecified should only be enabled when supporting legacy services that did not previously - // support API versioning. Forcing existing clients to specify an explicit API version for an - // existing service introduces a breaking change. Conceptually, clients in this situation are - // bound to some API version of a service, but they don't know what it is and never explicit request it. - options.AssumeDefaultVersionWhenUnspecified = true; - options.DefaultApiVersion = new ApiVersion(1, 0); - - // // Defines how an API version is read from the current HTTP request - options.ApiVersionReader = ApiVersionReader.Combine(new HeaderApiVersionReader("api-version"), - new UrlSegmentApiVersionReader()); - - configurator?.Invoke(options); - }); - } - - public static IApplicationBuilder UseCorrelationId(this IApplicationBuilder app) - => app.Use(async (ctx, next) => - { - if (!ctx.Request.Headers.TryGetValue(CorrelationId, out var correlationId)) - { - correlationId = Guid.NewGuid().ToString("N"); - } - - ctx.Items[CorrelationId] = correlationId.ToString(); - await next(); - }); - - public static string GetCorrelationId(this HttpContext context) - { - return context.Items.TryGetValue(CorrelationId, out var correlationId) ? correlationId as string : null; - } - } -} diff --git a/src/BuildingBlocks/Web/ServiceCollectionExtensions.cs b/src/BuildingBlocks/Web/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..74f723d --- /dev/null +++ b/src/BuildingBlocks/Web/ServiceCollectionExtensions.cs @@ -0,0 +1,60 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace BuildingBlocks.Web; + +public static class ServiceCollectionExtensions +{ + public static void ReplaceScoped(this IServiceCollection services) + where TService : class + where TImplementation : class, TService + { + services.Unregister(); + services.AddScoped(); + } + + public static void ReplaceScoped(this IServiceCollection services, + Func implementationFactory) + where TService : class + { + services.Unregister(); + services.AddScoped(implementationFactory); + } + + public static void ReplaceTransient(this IServiceCollection services) + where TService : class + where TImplementation : class, TService + { + services.Unregister(); + services.AddTransient(); + } + + public static void ReplaceTransient(this IServiceCollection services, + Func implementationFactory) + where TService : class + { + services.Unregister(); + services.AddTransient(implementationFactory); + } + + public static void ReplaceSingleton(this IServiceCollection services) + where TService : class + where TImplementation : class, TService + { + services.Unregister(); + services.AddSingleton(); + } + + public static void ReplaceSingleton(this IServiceCollection services, + Func implementationFactory) + where TService : class + { + services.Unregister(); + services.AddSingleton(implementationFactory); + } + + public static void Unregister(this IServiceCollection services) + { + var descriptor = services.FirstOrDefault(d => d.ServiceType == typeof(TService)); + services.Remove(descriptor); + } +} diff --git a/src/Services/Booking/src/Booking.Api/Program.cs b/src/Services/Booking/src/Booking.Api/Program.cs index a7b301d..2c8555e 100644 --- a/src/Services/Booking/src/Booking.Api/Program.cs +++ b/src/Services/Booking/src/Booking.Api/Program.cs @@ -31,7 +31,7 @@ builder.Services.Configure(options => configuration.GetSection("Grp Console.WriteLine(FiggleFonts.Standard.Render(appOptions.Name)); builder.Services.AddTransient(); -builder.Services.AddCustomDbContext(configuration, typeof(BookingRoot).Assembly); +builder.Services.AddCustomDbContext(configuration); builder.AddCustomSerilog(); builder.Services.AddJwt(); diff --git a/src/Services/Flight/src/Flight.Api/Program.cs b/src/Services/Flight/src/Flight.Api/Program.cs index 7379335..0891ca6 100644 --- a/src/Services/Flight/src/Flight.Api/Program.cs +++ b/src/Services/Flight/src/Flight.Api/Program.cs @@ -8,10 +8,8 @@ using BuildingBlocks.Jwt; using BuildingBlocks.Logging; using BuildingBlocks.Mapster; using BuildingBlocks.MassTransit; -using BuildingBlocks.Mongo; using BuildingBlocks.OpenTelemetry; using BuildingBlocks.Swagger; -using BuildingBlocks.Utils; using BuildingBlocks.Web; using Figgle; using Flight; @@ -32,12 +30,10 @@ var env = builder.Environment; var appOptions = builder.Services.GetOptions("AppOptions"); Console.WriteLine(FiggleFonts.Standard.Render(appOptions.Name)); - builder.Services.AddTransient(); -builder.Services.AddCustomDbContext(configuration, typeof(FlightRoot).Assembly); -builder.Services.AddMongoDbContext(configuration); - +builder.Services.AddCustomDbContext(configuration); builder.Services.AddScoped(); + builder.AddCustomSerilog(); builder.Services.AddJwt(); builder.Services.AddControllers(); diff --git a/src/Services/Flight/src/Flight.Api/appsettings.test.json b/src/Services/Flight/src/Flight.Api/appsettings.test.json index 4b190d6..09c3487 100644 --- a/src/Services/Flight/src/Flight.Api/appsettings.test.json +++ b/src/Services/Flight/src/Flight.Api/appsettings.test.json @@ -1,11 +1,19 @@ { "ConnectionStrings": { - "DefaultConnection": "Server=.\\sqlexpress;Database=FlightDB;Trusted_Connection=True;MultipleActiveResultSets=true" + "DefaultConnection": "Server=.\\sqlexpress;Database=FlightDB_Test;Trusted_Connection=True;MultipleActiveResultSets=true" }, "RabbitMq": { - "HostName": "rabbitmq", + "HostName": "localhost", "ExchangeName": "flight", "UserName": "guest", "Password": "guest" + }, + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft": "Debug", + "Microsoft.Hosting.Lifetime": "Debug", + "Microsoft.EntityFrameworkCore.Database.Command": "Debug" + } } } diff --git a/src/Services/Flight/src/Flight/Data/FlightDbContext.cs b/src/Services/Flight/src/Flight/Data/FlightDbContext.cs index dae7c09..8793eba 100644 --- a/src/Services/Flight/src/Flight/Data/FlightDbContext.cs +++ b/src/Services/Flight/src/Flight/Data/FlightDbContext.cs @@ -6,23 +6,23 @@ using Flight.Seats.Models; using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; -namespace Flight.Data +namespace Flight.Data; + +public sealed class FlightDbContext : AppDbContextBase { - public sealed class FlightDbContext : AppDbContextBase + public FlightDbContext(DbContextOptions options, IHttpContextAccessor httpContextAccessor) : base( + options, httpContextAccessor) { - public FlightDbContext(DbContextOptions options, IHttpContextAccessor httpContextAccessor) : base(options, httpContextAccessor) - { - } + } - public DbSet Flights => Set(); - public DbSet Airports => Set(); - public DbSet Aircraft => Set(); - public DbSet Seats => Set(); + public DbSet Flights => Set(); + public DbSet Airports => Set(); + public DbSet Aircraft => Set(); + public DbSet Seats => Set(); - protected override void OnModelCreating(ModelBuilder builder) - { - builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); - base.OnModelCreating(builder); - } + protected override void OnModelCreating(ModelBuilder builder) + { + builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); + base.OnModelCreating(builder); } } diff --git a/src/Services/Flight/src/Flight/Data/Migrations/20220509213249_Init.Designer.cs b/src/Services/Flight/src/Flight/Data/Migrations/20220511215248_Init.Designer.cs similarity index 99% rename from src/Services/Flight/src/Flight/Data/Migrations/20220509213249_Init.Designer.cs rename to src/Services/Flight/src/Flight/Data/Migrations/20220511215248_Init.Designer.cs index cd10e02..27ccb3a 100644 --- a/src/Services/Flight/src/Flight/Data/Migrations/20220509213249_Init.Designer.cs +++ b/src/Services/Flight/src/Flight/Data/Migrations/20220511215248_Init.Designer.cs @@ -12,7 +12,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Flight.Data.Migrations { [DbContext(typeof(FlightDbContext))] - [Migration("20220509213249_Init")] + [Migration("20220511215248_Init")] partial class Init { protected override void BuildTargetModel(ModelBuilder modelBuilder) diff --git a/src/Services/Flight/src/Flight/Data/Migrations/20220509213249_Init.cs b/src/Services/Flight/src/Flight/Data/Migrations/20220511215248_Init.cs similarity index 100% rename from src/Services/Flight/src/Flight/Data/Migrations/20220509213249_Init.cs rename to src/Services/Flight/src/Flight/Data/Migrations/20220511215248_Init.cs diff --git a/src/Services/Flight/tests/DeleteTests.cs b/src/Services/Flight/tests/DeleteTests.cs index 749f9ca..6ee4486 100644 --- a/src/Services/Flight/tests/DeleteTests.cs +++ b/src/Services/Flight/tests/DeleteTests.cs @@ -1,4 +1,8 @@ using System.Threading.Tasks; +using BuildingBlocks.Contracts.EventBus.Messages; +using Flight.Airports.Models; +using Flight.Flights.Features.GetFlightById; +using MassTransit.Testing; using Xunit; namespace Integration.Test; @@ -8,12 +12,16 @@ public class DeleteTests { private readonly TestFixture _fixture; - public DeleteTests(TestFixture fixture) => _fixture = fixture; - - [Fact] - public Task Should_delete_flight() + public DeleteTests(TestFixture fixture) { - return Task.CompletedTask; + _fixture = fixture; } + [Fact] + public async Task Should_get_flight() + { + var query = new GetFlightByIdQuery(1); + var flight = await _fixture.SendAsync(query); + var airport = await _fixture.FindAsync(1); + } } diff --git a/src/Services/Flight/tests/TestFixture.cs b/src/Services/Flight/tests/TestFixture.cs index 4a5ac72..7de9a4f 100644 --- a/src/Services/Flight/tests/TestFixture.cs +++ b/src/Services/Flight/tests/TestFixture.cs @@ -1,21 +1,24 @@ using System; -using System.Linq; +using System.Net.Http; using System.Threading.Tasks; using BuildingBlocks.Domain.Model; +using BuildingBlocks.EFCore; +using BuildingBlocks.MassTransit; +using BuildingBlocks.Web; using Flight.Data; +using Flight.Data.Seed; using MassTransit; using MassTransit.Testing; using MediatR; -using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.AspNetCore.TestHost; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Moq; +using NSubstitute; using Respawn; using Xunit; @@ -26,114 +29,84 @@ public class TestFixtureCollection : ICollectionFixture { } +// ref: https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-6.0 // ref: https://github.com/jbogard/ContosoUniversityDotNetCore-Pages/blob/master/ContosoUniversity.IntegrationTests/SliceFixture.cs -// ref: https://github.com/MassTransit/MassTransit/blob/00d6992286911a437b63b93c89a56e920b053c11/src/MassTransit.TestFramework/InMemoryTestFixture.cs -// ref: https://wrapt.dev/blog/building-an-event-driven-dotnet-application-integration-testing +// ref: https://github.com/jasontaylordev/CleanArchitecture/blob/main/tests/Application.IntegrationTests/Testing.cs public class TestFixture : IAsyncLifetime { - private readonly Checkpoint _checkpoint; - private readonly IConfiguration _configuration; - private readonly WebApplicationFactory _factory; - private readonly IServiceScopeFactory _scopeFactory; - private static InMemoryTestHarness _harness; + private Checkpoint _checkpoint; + private HttpClient _client; + private IConfiguration _configuration; + private WebApplicationFactory _factory; + private ITestHarness _harness; + private IServiceScopeFactory _scopeFactory; - public TestFixture() + public ILogger Logger => + _scopeFactory.CreateScope().ServiceProvider.GetRequiredService>(); + + + public async Task InitializeAsync() { - _factory = FlightTestApplicationFactory(); + Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "test"); + + _factory = new WebApplicationFactory() + .WithWebHostBuilder(builder => builder.ConfigureServices(services => + { + services.RemoveAll(typeof(IHostedService)); + services.ReplaceSingleton(AddHttpContextAccessorMock); + services.ReplaceScoped(); + services.AddMassTransitTestHarness(x => + { + x.UsingRabbitMq((context, cfg) => + { + var rabbitMqOptions = services.GetOptions("RabbitMq"); + var host = rabbitMqOptions.HostName; + + cfg.Host(host, h => + { + h.Username(rabbitMqOptions.UserName); + h.Password(rabbitMqOptions.Password); + }); + cfg.ConfigureEndpoints(context); + }); + }); + })); + + _harness = _factory.Services.GetTestHarness(); + + await _harness.Start(); _configuration = _factory.Services.GetRequiredService(); _scopeFactory = _factory.Services.GetRequiredService(); - _checkpoint = new Checkpoint(); - } + _client = _factory.CreateClient(new WebApplicationFactoryClientOptions {AllowAutoRedirect = false}); - public WebApplicationFactory FlightTestApplicationFactory() - { - Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "test"); + _checkpoint = new Checkpoint {TablesToIgnore = new[] {"__EFMigrationsHistory"}}; - return new WebApplicationFactory() - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices((services) => - { - services.RemoveAll(typeof(IHostedService)); - }); - - builder.ConfigureServices(services => - { - builder.ConfigureLogging(logging => - { - logging.ClearProviders(); // Remove other loggers - }); - - var httpContextAccessorService = services.FirstOrDefault(d => - d.ServiceType == typeof(IHttpContextAccessor)); - - services.Remove(httpContextAccessorService); - services.AddSingleton(_ => Mock.Of()); - - services.AddScoped(); - var provider = services.BuildServiceProvider(); - var serviceScopeFactory = provider.GetService(); - - // MassTransit Start Setup -- Do Not Delete Comment - _harness = serviceScopeFactory?.CreateScope().ServiceProvider.GetRequiredService(); - _harness?.Start().GetAwaiter().GetResult(); - }); - }); - } - - public Task InitializeAsync() - { - return _checkpoint.Reset(_configuration.GetConnectionString("DefaultConnection")); + await EnsureDatabaseAsync(); } public async Task DisposeAsync() { - await _harness.Stop(); + _harness.Cancel(); await _factory.DisposeAsync(); + await _checkpoint.Reset(_configuration.GetConnectionString("DefaultConnection")); } + public async Task ExecuteScopeAsync(Func action) { using var scope = _scopeFactory.CreateScope(); - var dbContext = scope.ServiceProvider.GetRequiredService(); - - try - { - await dbContext.BeginTransactionAsync(); - - await action(scope.ServiceProvider); - - await dbContext.CommitTransactionAsync(); - } - catch (Exception) - { - await dbContext.RollbackTransactionAsync(); - throw; - } + await action(scope.ServiceProvider); } public async Task ExecuteScopeAsync(Func> action) { using var scope = _scopeFactory.CreateScope(); - var dbContext = scope.ServiceProvider.GetRequiredService(); - try - { - await dbContext.BeginTransactionAsync(); + var result = await action(scope.ServiceProvider); - var result = await action(scope.ServiceProvider); - - await dbContext.CommitTransactionAsync(); - - return result; - } - catch (Exception) - { - await dbContext.RollbackTransactionAsync(); - throw; - } + return result; } public Task ExecuteDbContextAsync(Func action) @@ -171,6 +144,7 @@ public class TestFixture : IAsyncLifetime return ExecuteDbContextAsync(db => { foreach (var entity in entities) db.Set().Add(entity); + return db.SaveChangesAsync(); }); } @@ -231,7 +205,7 @@ public class TestFixture : IAsyncLifetime }); } - public Task FindAsync(int id) + public Task FindAsync(long id) where T : class, IEntity { return ExecuteDbContextAsync(db => db.Set().FindAsync(id).AsTask()); @@ -258,13 +232,15 @@ public class TestFixture : IAsyncLifetime } - // MassTransit Methods -- Do Not Delete Comment + // ref: https://github.com/MassTransit/MassTransit/blob/00d6992286911a437b63b93c89a56e920b053c11/src/MassTransit.TestFramework/InMemoryTestFixture.cs + // ref: https://wrapt.dev/blog/building-an-event-driven-dotnet-application-integration-testing + /// /// Publishes a message to the bus, and waits for the specified response. /// /// The message that should be published. /// The message that should be published. - public static async Task PublishMessage(object message) + public async Task PublishMessage(object message) where TMessage : class { await _harness.Bus.Publish(message); @@ -317,4 +293,34 @@ public class TestFixture : IAsyncLifetime var consumerHarness = scope.ServiceProvider.GetRequiredService>(); return consumerHarness.Consumed.Any(); } + + private async Task EnsureDatabaseAsync() + { + using var scope = _scopeFactory.CreateScope(); + + var context = scope.ServiceProvider.GetRequiredService(); + var seeders = scope.ServiceProvider.GetServices(); + + await context.Database.MigrateAsync(); + + foreach (var seeder in seeders) await seeder.SeedAllAsync(); + } + + // private async Task AddInMemoryHarnessAsync() + // { + // _harness = _factory.Services.GetRequiredService(); + // await _harness.Start(); + // } + + private IHttpContextAccessor AddHttpContextAccessorMock(IServiceProvider serviceProvider) + { + var httpContextAccessorMock = Substitute.For(); + using var scope = serviceProvider.CreateScope(); + httpContextAccessorMock.HttpContext = new DefaultHttpContext {RequestServices = scope.ServiceProvider}; + + httpContextAccessorMock.HttpContext.Request.Host = new HostString("localhost", 5000); + httpContextAccessorMock.HttpContext.Request.Scheme = "http"; + + return httpContextAccessorMock; + } } diff --git a/src/Services/Passenger/src/Passenger.Api/Program.cs b/src/Services/Passenger/src/Passenger.Api/Program.cs index dd4afc3..9bc1bc5 100644 --- a/src/Services/Passenger/src/Passenger.Api/Program.cs +++ b/src/Services/Passenger/src/Passenger.Api/Program.cs @@ -28,7 +28,7 @@ var appOptions = builder.Services.GetOptions("AppOptions"); Console.WriteLine(FiggleFonts.Standard.Render(appOptions.Name)); builder.Services.AddTransient(); -builder.Services.AddCustomDbContext(configuration, typeof(PassengerRoot).Assembly); +builder.Services.AddCustomDbContext(configuration); builder.AddCustomSerilog(); builder.Services.AddJwt(); builder.Services.AddControllers(); @@ -41,7 +41,6 @@ builder.Services.AddCustomMapster(typeof(PassengerRoot).Assembly); builder.Services.AddHttpContextAccessor(); builder.Services.AddTransient(); -builder.Services.AddTransient(); builder.Services.AddCustomMassTransit(typeof(PassengerRoot).Assembly, env); builder.Services.AddCustomOpenTelemetry();