From 51e5a16d8a519085fe6c48d0bba283b7fbd92a18 Mon Sep 17 00:00:00 2001 From: Marc Wittke Date: Mon, 12 Jul 2021 09:19:30 -0400 Subject: [PATCH] Hotfix/7.0.4 (#110) * db conn aclaration * exception logging & builder * docs * wait for data generation * currentHolder with initial ctor * xmldocs * wait for default tenant * xmldocs * formatting * logging improved * xmldocs * xmldocs * method detection standardized * xmldocs * method detections * xmldocs * description and min version * fix logging of errors array * cherrypick-errors fixed * dependency fix * test fix * correlation holder is injected, must not contain more public ctors * build triggers adapted * master -> main --- README.md | 2 +- build/Build.cs | 2 +- .../Authentication/CurrentIdentityHolder.cs | 10 ++- .../MultiTenancy/CurrentTenantIdHolder.cs | 12 ++-- .../MultiTenancy/ITenantIdProvider.cs | 4 +- .../MultiTenancy/SingleTenantApplication.cs | 5 +- .../DbConnectionOperationDecorator.cs | 6 ++ .../Backend.Fx/Exceptions/ClientException.cs | 18 +++++ .../Exceptions/ConflictedException.cs | 7 +- .../Exceptions/ForbiddenException.cs | 7 +- .../Exceptions/NotFoundException.cs | 5 -- .../Exceptions/TooManyRequestsException.cs | 2 + .../Exceptions/UnauthorizedException.cs | 2 + .../Exceptions/UnprocessableException.cs | 9 ++- .../Authorization/IAggregateAuthorization.cs | 15 +++-- .../Patterns/DataGeneration/DataGenerated.cs | 3 + .../DataGeneration/GenerateDataOnBoot.cs | 12 +++- .../DependencyInjection/Correlation.cs | 4 ++ .../DependencyInjection/CurrentTHolder.cs | 8 +++ .../DependencyInjection/ICompositionRoot.cs | 7 ++ .../Patterns/DependencyInjection/IModule.cs | 3 + .../DependencyInjection/InjectionScope.cs | 4 ++ .../Patterns/DependencyInjection/Operation.cs | 5 ++ ...uentializingBackendFxApplicationInvoker.cs | 3 + .../Backend.Fx.AspNetCore/HttpRequestEx.cs | 67 ++++++++++++++++++- .../MultiTenancy/CreateTenantParams.cs | 4 +- .../MultiTenancy/HttpContextEx.cs | 2 +- .../MultiTenancy/MultiTenantMiddlewareBase.cs | 10 ++- .../MultiTenancy/SingleTenantMiddleware.cs | 3 + .../MultiTenancy/TenantAdminMiddlewareBase.cs | 2 +- .../Mvc/Execution/FlushFilter.cs | 4 ++ .../ExceptionThrottlingAttribute.cs | 4 ++ .../Mvc/Throttling/ThrottlingAttribute.cs | 4 ++ .../Mvc/Validation/ModelValidationFilter.cs | 9 ++- ...rnModelStateAsJsonModelValidationFilter.cs | 4 ++ ...x.SimpleInjectorDependencyInjection.csproj | 4 +- .../Modules/SimpleInjectorDomainModule.cs | 14 ++-- .../TheSingleTenantApplication.cs | 3 - .../TheGenerateDataOnBootDecorator.cs | 4 +- 39 files changed, 231 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index 6deeb34f..a8a52674 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Backend.Fx.AspNetCore.Mvc | 2.0 | [![NuGet](https://img.shields.io/nuget/v/Backe Backend.Fx.NetCore | 1.3 | [![NuGet](https://img.shields.io/nuget/v/Backend.Fx.NetCore.svg)](https://www.nuget.org/packages/Backend.Fx.NetCore) ## What does "opinionated" mean? -You get vendor locked to a set of abstractions, like my DDD building blocks and some architecture [patterns](https://github.com/marcwittke/Backend.Fx/tree/master/src/abstractions/Backend.Fx/Patterns) defined as interfaces +You get vendor locked to a set of abstractions, like my DDD building blocks and some architecture [patterns](https://github.com/marcwittke/Backend.Fx/tree/main/src/abstractions/Backend.Fx/Patterns) defined as interfaces ## Less opinonated, but already provided as pluggable implementations of patterns diff --git a/build/Build.cs b/build/Build.cs index ddeaaead..8777529b 100644 --- a/build/Build.cs +++ b/build/Build.cs @@ -92,7 +92,7 @@ class Build : NukeBuild .DependsOn(Pack) .Executes(() => { - bool pushToNuget = GitRepository.Branch == "master"; + bool pushToNuget = GitRepository.Branch == "main"; foreach (var nupkg in ArtifactsDirectory.GlobFiles("*.nupkg")) { diff --git a/src/abstractions/Backend.Fx/Environment/Authentication/CurrentIdentityHolder.cs b/src/abstractions/Backend.Fx/Environment/Authentication/CurrentIdentityHolder.cs index 075ef3cc..9799769d 100644 --- a/src/abstractions/Backend.Fx/Environment/Authentication/CurrentIdentityHolder.cs +++ b/src/abstractions/Backend.Fx/Environment/Authentication/CurrentIdentityHolder.cs @@ -5,6 +5,12 @@ namespace Backend.Fx.Environment.Authentication { public class CurrentIdentityHolder : CurrentTHolder { + public CurrentIdentityHolder() + { } + + private CurrentIdentityHolder(IIdentity initial) : base(initial) + { } + public override IIdentity ProvideInstance() { return new AnonymousIdentity(); @@ -28,9 +34,7 @@ public static ICurrentTHolder CreateSystem() public static ICurrentTHolder Create(IIdentity identity) { - var currentIdentityHolder = new CurrentIdentityHolder(); - currentIdentityHolder.ReplaceCurrent(identity); - return currentIdentityHolder; + return new CurrentIdentityHolder(identity); } } } \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/MultiTenancy/CurrentTenantIdHolder.cs b/src/abstractions/Backend.Fx/Environment/MultiTenancy/CurrentTenantIdHolder.cs index ede076ae..c5c4f1c2 100644 --- a/src/abstractions/Backend.Fx/Environment/MultiTenancy/CurrentTenantIdHolder.cs +++ b/src/abstractions/Backend.Fx/Environment/MultiTenancy/CurrentTenantIdHolder.cs @@ -4,17 +4,21 @@ namespace Backend.Fx.Environment.MultiTenancy { public class CurrentTenantIdHolder : CurrentTHolder { + public CurrentTenantIdHolder() + { } + + private CurrentTenantIdHolder(TenantId initial) : base(initial) + { } + public static CurrentTenantIdHolder Create(int tenantId) { - var instance = new CurrentTenantIdHolder(); - instance.ReplaceCurrent(new TenantId(tenantId)); + var instance = new CurrentTenantIdHolder((TenantId)tenantId); return instance; } public static CurrentTenantIdHolder Create(TenantId tenantId) { - var instance = new CurrentTenantIdHolder(); - instance.ReplaceCurrent(tenantId); + var instance = new CurrentTenantIdHolder(tenantId); return instance; } diff --git a/src/abstractions/Backend.Fx/Environment/MultiTenancy/ITenantIdProvider.cs b/src/abstractions/Backend.Fx/Environment/MultiTenancy/ITenantIdProvider.cs index ee81cc98..44873e50 100644 --- a/src/abstractions/Backend.Fx/Environment/MultiTenancy/ITenantIdProvider.cs +++ b/src/abstractions/Backend.Fx/Environment/MultiTenancy/ITenantIdProvider.cs @@ -1,8 +1,10 @@ namespace Backend.Fx.Environment.MultiTenancy { /// - /// By means of this instance, other services gain insight about all active tenants. This is useful, when for example a job + /// By means of this instance, the IBackendFxApplication gains insight about all active tenants. This is required, when for example a job /// should be executed for all tenants or data should be generated for all tenants during startup. + /// The can provide such implementation, but this can only be done in process. When the tenant service is + /// running in another process, the implementation must be done using a suitable remoting technology. /// public interface ITenantIdProvider { diff --git a/src/abstractions/Backend.Fx/Environment/MultiTenancy/SingleTenantApplication.cs b/src/abstractions/Backend.Fx/Environment/MultiTenancy/SingleTenantApplication.cs index b4ef0828..ba88af52 100644 --- a/src/abstractions/Backend.Fx/Environment/MultiTenancy/SingleTenantApplication.cs +++ b/src/abstractions/Backend.Fx/Environment/MultiTenancy/SingleTenantApplication.cs @@ -13,6 +13,7 @@ public class SingleTenantApplication : TenantApplication, IBackendFxApplication private readonly bool _isDemoTenant; private readonly ITenantService _tenantService; private readonly IBackendFxApplication _application; + private readonly ManualResetEventSlim _defaultTenantEnsured = new ManualResetEventSlim(false); public SingleTenantApplication(bool isDemoTenant, ITenantService tenantService, IBackendFxApplication application) : base(application) { @@ -38,7 +39,7 @@ public void Dispose() public bool WaitForBoot(int timeoutMilliSeconds = int.MaxValue, CancellationToken cancellationToken = default) { - return _application.WaitForBoot(timeoutMilliSeconds, cancellationToken); + return _defaultTenantEnsured.Wait(timeoutMilliSeconds, cancellationToken); } public async Task Boot(CancellationToken cancellationToken = default) @@ -50,6 +51,8 @@ public async Task Boot(CancellationToken cancellationToken = default) Logger.Info($"Ensuring existence of single tenant"); TenantId = _tenantService.GetActiveTenants().SingleOrDefault()?.GetTenantId() ?? _tenantService.CreateTenant("Single Tenant", "This application runs in single tenant mode", _isDemoTenant); + + _defaultTenantEnsured.Set(); } } } \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Environment/Persistence/DbConnectionOperationDecorator.cs b/src/abstractions/Backend.Fx/Environment/Persistence/DbConnectionOperationDecorator.cs index 6355e974..a29dbcc7 100644 --- a/src/abstractions/Backend.Fx/Environment/Persistence/DbConnectionOperationDecorator.cs +++ b/src/abstractions/Backend.Fx/Environment/Persistence/DbConnectionOperationDecorator.cs @@ -38,6 +38,12 @@ public void Complete() public void Cancel() { Operation.Cancel(); + Logger.Debug("Closing database connection"); + DbConnection.Close(); + _connectionLifetimeLogger?.Dispose(); + + // note: we do not dispose the DbConnection here, because we did not instantiate it. Disposing is always up to the creator of + // the instance, that is in this case the injection container. } } } \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Exceptions/ClientException.cs b/src/abstractions/Backend.Fx/Exceptions/ClientException.cs index 0278b926..dc32894d 100644 --- a/src/abstractions/Backend.Fx/Exceptions/ClientException.cs +++ b/src/abstractions/Backend.Fx/Exceptions/ClientException.cs @@ -12,11 +12,19 @@ public ClientException() { } + /// When using one of the middlewares in Backend.Fx.AspNetCore.ErrorHandling, the message is not sent + /// to the client to not provide internal details to an attacker. Write the exception message with a developer in mind, since + /// the application log will contain the message. To provide the user with functional feedback to correct their input, use + /// the AddError(s) overloads. public ClientException(string message) : base(message) { } + /// When using one of the middlewares in Backend.Fx.AspNetCore.ErrorHandling, the message is not sent + /// to the client to not provide internal details to an attacker. Write the exception message with a developer in mind, since + /// the application log will contain the message. To provide the user with functional feedback to correct their input, use + /// the AddError(s) overloads. public ClientException(string message, Exception innerException) : base(message, innerException) { @@ -48,6 +56,16 @@ public override string ToString() return string.Join(System.Environment.NewLine, new[] {message, Errors.ToString(), innerException, StackTrace}.Where(s => s != null)); } + + /// + /// Used to build an with multiple possible error messages. The builder will throw on disposal + /// when at least one error was added. Using the AddIf methods is quite comfortable when there are several criteria to be validated + /// before executing a business case. + /// + public static IExceptionBuilder UseBuilder() + { + return new ExceptionBuilder(); + } } public static class ClientExceptionEx diff --git a/src/abstractions/Backend.Fx/Exceptions/ConflictedException.cs b/src/abstractions/Backend.Fx/Exceptions/ConflictedException.cs index 471a3ea4..541d5ef9 100644 --- a/src/abstractions/Backend.Fx/Exceptions/ConflictedException.cs +++ b/src/abstractions/Backend.Fx/Exceptions/ConflictedException.cs @@ -9,19 +9,16 @@ public ConflictedException() { } + /// public ConflictedException(string message) : base(message) { } + /// public ConflictedException(string message, Exception innerException) : base(message, innerException) { } - - public static IExceptionBuilder UseBuilder() - { - return new ExceptionBuilder(); - } } } \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Exceptions/ForbiddenException.cs b/src/abstractions/Backend.Fx/Exceptions/ForbiddenException.cs index f02ecebe..e5db75a1 100644 --- a/src/abstractions/Backend.Fx/Exceptions/ForbiddenException.cs +++ b/src/abstractions/Backend.Fx/Exceptions/ForbiddenException.cs @@ -9,19 +9,16 @@ public ForbiddenException() { } + /// public ForbiddenException(string message) : base(message) { } + /// public ForbiddenException(string message, Exception innerException) : base(message, innerException) { } - - public static IExceptionBuilder UseBuilder() - { - return new ExceptionBuilder(); - } } } \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Exceptions/NotFoundException.cs b/src/abstractions/Backend.Fx/Exceptions/NotFoundException.cs index 74266c2c..cc3b2350 100644 --- a/src/abstractions/Backend.Fx/Exceptions/NotFoundException.cs +++ b/src/abstractions/Backend.Fx/Exceptions/NotFoundException.cs @@ -17,11 +17,6 @@ public NotFoundException(string entityName, object id) EntityName = entityName; Id = id; } - - public static IExceptionBuilder UseBuilder() - { - return new ExceptionBuilder(); - } } public class NotFoundException : NotFoundException diff --git a/src/abstractions/Backend.Fx/Exceptions/TooManyRequestsException.cs b/src/abstractions/Backend.Fx/Exceptions/TooManyRequestsException.cs index 77a6766b..42dd5f7c 100644 --- a/src/abstractions/Backend.Fx/Exceptions/TooManyRequestsException.cs +++ b/src/abstractions/Backend.Fx/Exceptions/TooManyRequestsException.cs @@ -10,11 +10,13 @@ public TooManyRequestsException(int retryAfter) RetryAfter = retryAfter; } + /// public TooManyRequestsException(int retryAfter, string message) : base(message) { RetryAfter = retryAfter; } + /// public TooManyRequestsException(int retryAfter, string message, Exception innerException) : base(message, innerException) { RetryAfter = retryAfter; diff --git a/src/abstractions/Backend.Fx/Exceptions/UnauthorizedException.cs b/src/abstractions/Backend.Fx/Exceptions/UnauthorizedException.cs index 28fe1829..f365a9f2 100644 --- a/src/abstractions/Backend.Fx/Exceptions/UnauthorizedException.cs +++ b/src/abstractions/Backend.Fx/Exceptions/UnauthorizedException.cs @@ -9,11 +9,13 @@ public UnauthorizedException() { } + /// public UnauthorizedException(string message) : base(message) { } + /// public UnauthorizedException(string message, Exception innerException) : base(message, innerException) { diff --git a/src/abstractions/Backend.Fx/Exceptions/UnprocessableException.cs b/src/abstractions/Backend.Fx/Exceptions/UnprocessableException.cs index ce231520..018108a6 100644 --- a/src/abstractions/Backend.Fx/Exceptions/UnprocessableException.cs +++ b/src/abstractions/Backend.Fx/Exceptions/UnprocessableException.cs @@ -9,17 +9,24 @@ public UnprocessableException() { } + /// public UnprocessableException(string message) : base(message) { } + /// public UnprocessableException(string message, Exception innerException) : base(message, innerException) { } - public static IExceptionBuilder UseBuilder() + /// + /// Used to build an with multiple possible error messages. The builder will throw on disposal + /// when at least one error was added. Using the AddIf methods is quite comfortable when there are several criteria to be validated + /// before executing a business case. + /// + public new static IExceptionBuilder UseBuilder() { return new ExceptionBuilder(); } diff --git a/src/abstractions/Backend.Fx/Patterns/Authorization/IAggregateAuthorization.cs b/src/abstractions/Backend.Fx/Patterns/Authorization/IAggregateAuthorization.cs index 990b8583..0eb6773b 100644 --- a/src/abstractions/Backend.Fx/Patterns/Authorization/IAggregateAuthorization.cs +++ b/src/abstractions/Backend.Fx/Patterns/Authorization/IAggregateAuthorization.cs @@ -1,9 +1,14 @@ -namespace Backend.Fx.Patterns.Authorization -{ - using System.Linq; - using System.Linq.Expressions; - using BuildingBlocks; +using System.Linq; +using System.Linq.Expressions; +using Backend.Fx.BuildingBlocks; +namespace Backend.Fx.Patterns.Authorization +{ + /// + /// Implements permissions on aggregate level. The respective instance is applied when creating an , + /// so that the repository never allows reading or writing of an aggregate without permissions. + /// + /// public interface IAggregateAuthorization where TAggregateRoot : AggregateRoot { /// diff --git a/src/abstractions/Backend.Fx/Patterns/DataGeneration/DataGenerated.cs b/src/abstractions/Backend.Fx/Patterns/DataGeneration/DataGenerated.cs index 49e36995..27f0100d 100644 --- a/src/abstractions/Backend.Fx/Patterns/DataGeneration/DataGenerated.cs +++ b/src/abstractions/Backend.Fx/Patterns/DataGeneration/DataGenerated.cs @@ -2,6 +2,9 @@ namespace Backend.Fx.Patterns.DataGeneration { + /// + /// Will appear on the message bus when the data generation process has been completed + /// public class DataGenerated : IntegrationEvent { public DataGenerated(int tenantId) : base(tenantId) diff --git a/src/abstractions/Backend.Fx/Patterns/DataGeneration/GenerateDataOnBoot.cs b/src/abstractions/Backend.Fx/Patterns/DataGeneration/GenerateDataOnBoot.cs index 9eb2dc93..4d2bb277 100644 --- a/src/abstractions/Backend.Fx/Patterns/DataGeneration/GenerateDataOnBoot.cs +++ b/src/abstractions/Backend.Fx/Patterns/DataGeneration/GenerateDataOnBoot.cs @@ -9,12 +9,16 @@ namespace Backend.Fx.Patterns.DataGeneration { + /// + /// Enriches the by calling all data generators for all tenants on application start. + /// public class GenerateDataOnBoot : IBackendFxApplication { private static readonly ILogger Logger = LogManager.Create(); private readonly ITenantIdProvider _tenantIdProvider; private readonly IBackendFxApplication _application; private readonly IModule _dataGenerationModule; + private readonly ManualResetEventSlim _dataGenerated = new ManualResetEventSlim(false); public IDataGenerationContext DataGenerationContext { get; [UsedImplicitly] private set; } public GenerateDataOnBoot(ITenantIdProvider tenantIdProvider, IModule dataGenerationModule, IBackendFxApplication application) @@ -39,7 +43,7 @@ public void Dispose() public bool WaitForBoot(int timeoutMilliSeconds = Int32.MaxValue, CancellationToken cancellationToken = default) { - return _application.WaitForBoot(timeoutMilliSeconds, cancellationToken); + return _dataGenerated.Wait(timeoutMilliSeconds, cancellationToken); } public async Task Boot(CancellationToken cancellationToken = default) @@ -48,9 +52,11 @@ public async Task Boot(CancellationToken cancellationToken = default) await _application.Boot(cancellationToken); SeedDataForAllActiveTenants(); + + _dataGenerated.Set(); } - - public void SeedDataForAllActiveTenants() + + private void SeedDataForAllActiveTenants() { using (Logger.InfoDuration("Seeding data")) { diff --git a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/Correlation.cs b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/Correlation.cs index eee99ba4..f278b237 100644 --- a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/Correlation.cs +++ b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/Correlation.cs @@ -3,6 +3,10 @@ namespace Backend.Fx.Patterns.DependencyInjection { + /// + /// A guid that is unique for an invocation. In case of an invocation as result of handling an integration event, the correlation + /// is stable, that is, the correlation can be used to track a logical action over different systems. + /// public class Correlation { private static readonly ILogger Logger = LogManager.Create(); diff --git a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/CurrentTHolder.cs b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/CurrentTHolder.cs index aa50a518..e572e178 100644 --- a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/CurrentTHolder.cs +++ b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/CurrentTHolder.cs @@ -20,6 +20,14 @@ public abstract class CurrentTHolder : ICurrentTHolder where T : class private static readonly ILogger Logger = LogManager.Create>(); private T _current; + protected CurrentTHolder() + { } + + protected CurrentTHolder(T initial) + { + _current = initial; + } + public T Current { get diff --git a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/ICompositionRoot.cs b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/ICompositionRoot.cs index f015d298..2dcfa214 100644 --- a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/ICompositionRoot.cs +++ b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/ICompositionRoot.cs @@ -16,8 +16,15 @@ public interface ICompositionRoot : IDisposable, IDomainEventHandlerProvider void RegisterModules(params IModule[] modules); IInjectionScope BeginScope(); + + /// + /// Access to the container's resolution functionality + /// IInstanceProvider InstanceProvider { get; } + /// + /// Access to the container's configuration functionality + /// IInfrastructureModule InfrastructureModule { get; } } } \ No newline at end of file diff --git a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/IModule.cs b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/IModule.cs index efec59f4..762f2042 100644 --- a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/IModule.cs +++ b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/IModule.cs @@ -1,5 +1,8 @@ namespace Backend.Fx.Patterns.DependencyInjection { + /// + /// A logically cohesive bunch of services + /// public interface IModule { void Register(ICompositionRoot compositionRoot); diff --git a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/InjectionScope.cs b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/InjectionScope.cs index 4174373d..0cf52b1c 100644 --- a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/InjectionScope.cs +++ b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/InjectionScope.cs @@ -2,6 +2,10 @@ namespace Backend.Fx.Patterns.DependencyInjection { + /// + /// During a scope, services by default are singletons. Scopes may exist in parallel, providing totally separate singleton + /// instances for every scope. + /// public interface IInjectionScope : IDisposable { int SequenceNumber { get; } diff --git a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/Operation.cs b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/Operation.cs index 384a74d3..52289819 100644 --- a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/Operation.cs +++ b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/Operation.cs @@ -3,6 +3,11 @@ namespace Backend.Fx.Patterns.DependencyInjection { + /// + /// The basic interface of an operation invoked by the (or its async counterpart). + /// Decorate this interface to provide operation specific infrastructure services (like a database connection, a database transaction + /// an entry-exit logging etc.) + /// public interface IOperation { void Begin(); diff --git a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/SequentializingBackendFxApplicationInvoker.cs b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/SequentializingBackendFxApplicationInvoker.cs index b145d0f9..893aac1b 100644 --- a/src/abstractions/Backend.Fx/Patterns/DependencyInjection/SequentializingBackendFxApplicationInvoker.cs +++ b/src/abstractions/Backend.Fx/Patterns/DependencyInjection/SequentializingBackendFxApplicationInvoker.cs @@ -4,6 +4,9 @@ namespace Backend.Fx.Patterns.DependencyInjection { + /// + /// Decorates the to prevent parallel invocation. + /// public class SequentializingBackendFxApplicationInvoker : IBackendFxApplicationInvoker { private readonly object _syncLock = new object(); diff --git a/src/environments/Backend.Fx.AspNetCore/HttpRequestEx.cs b/src/environments/Backend.Fx.AspNetCore/HttpRequestEx.cs index 1d9e01fb..d1c337dd 100644 --- a/src/environments/Backend.Fx.AspNetCore/HttpRequestEx.cs +++ b/src/environments/Backend.Fx.AspNetCore/HttpRequestEx.cs @@ -4,10 +4,71 @@ namespace Backend.Fx.AspNetCore { public static class HttpRequestEx { - public static bool IsSafe(this HttpRequest request) + /// + /// Is the request method considered as safe in sense of a RESTful API? + /// See https://restcookbook.com/HTTP%20Methods/idempotency/ + /// + /// + /// + public static bool IsRestfulSafe(this HttpRequest request) { - string method = request.Method.ToUpperInvariant(); - return method == "OPTIONS" || method == "GET" || method == "HEAD"; + return request.IsGet() || request.IsOptions() || request.IsHead(); + } + + /// + /// Is the request method considered as idempotent in sense of a RESTful API? + /// See https://restcookbook.com/HTTP%20Methods/idempotency/ + /// + /// + /// + public static bool IsRestfulIdempotent(this HttpRequest request) + { + return request.IsGet() || request.IsOptions() || request.IsHead() || request.IsDelete() || request.IsPut(); + } + + public static bool IsGet(this HttpRequest request) + { + return HttpMethods.IsGet(request.Method); + } + + public static bool IsConnect(this HttpRequest request) + { + return HttpMethods.IsConnect(request.Method); + } + + public static bool IsDelete(this HttpRequest request) + { + return HttpMethods.IsDelete(request.Method); + } + + public static bool IsHead(this HttpRequest request) + { + return HttpMethods.IsHead(request.Method); + } + + public static bool IsOptions(this HttpRequest request) + { + return HttpMethods.IsOptions(request.Method); + } + + public static bool IsPatch(this HttpRequest request) + { + return HttpMethods.IsPatch(request.Method); + } + + public static bool IsPost(this HttpRequest request) + { + return HttpMethods.IsPost(request.Method); + } + + public static bool IsPut(this HttpRequest request) + { + return HttpMethods.IsPut(request.Method); + } + + public static bool IsTrace(this HttpRequest request) + { + return HttpMethods.IsTrace(request.Method); } } } \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/MultiTenancy/CreateTenantParams.cs b/src/environments/Backend.Fx.AspNetCore/MultiTenancy/CreateTenantParams.cs index f61ac172..52957e3c 100644 --- a/src/environments/Backend.Fx.AspNetCore/MultiTenancy/CreateTenantParams.cs +++ b/src/environments/Backend.Fx.AspNetCore/MultiTenancy/CreateTenantParams.cs @@ -7,11 +7,11 @@ public class CreateTenantParams [JsonProperty(PropertyName = "isDemo")] public bool IsDemo { get; set; } - [JsonProperty(PropertyName = "name")] public string Name { get; set; } + [JsonProperty(PropertyName = "name")] + public string Name { get; set; } [JsonProperty(PropertyName = "description")] public string Description { get; set; } - [JsonProperty(PropertyName = "administratorEmail")] public string AdministratorEmail { get; set; } diff --git a/src/environments/Backend.Fx.AspNetCore/MultiTenancy/HttpContextEx.cs b/src/environments/Backend.Fx.AspNetCore/MultiTenancy/HttpContextEx.cs index 9249f631..09876ccb 100644 --- a/src/environments/Backend.Fx.AspNetCore/MultiTenancy/HttpContextEx.cs +++ b/src/environments/Backend.Fx.AspNetCore/MultiTenancy/HttpContextEx.cs @@ -12,7 +12,7 @@ public static void SetCurrentTenantId(this HttpContext httpContext, TenantId ten { if (httpContext.Items.TryGetValue(TenantId, out object untyped)) { - throw new InvalidOperationException("TenantId has been set already in this HttpContext"); + throw new InvalidOperationException($"TenantId has been set already in this HttpContext. Value: {(untyped ?? "null")}"); } httpContext.Items[TenantId] = tenantId; diff --git a/src/environments/Backend.Fx.AspNetCore/MultiTenancy/MultiTenantMiddlewareBase.cs b/src/environments/Backend.Fx.AspNetCore/MultiTenancy/MultiTenantMiddlewareBase.cs index 4ed7fa24..60c01c15 100644 --- a/src/environments/Backend.Fx.AspNetCore/MultiTenancy/MultiTenantMiddlewareBase.cs +++ b/src/environments/Backend.Fx.AspNetCore/MultiTenancy/MultiTenantMiddlewareBase.cs @@ -1,5 +1,4 @@ using System.Threading.Tasks; -using Backend.Fx.AspNetCore.Mvc; using Backend.Fx.Environment.MultiTenancy; using Microsoft.AspNetCore.Http; @@ -19,7 +18,14 @@ public async Task Invoke(HttpContext context) context.SetCurrentTenantId(FindMatchingTenantId(context)); await _next.Invoke(context); } - + + /// + /// Detects the for this request from the current HttpContext. Possible implementations might rely on + /// a dedicated header value, the (sub-) domain name, a query string parameter etc. This method is called for each request. If + /// the database is required for determination, some kind of caching is advised. + /// + /// + /// The TenantId for this request protected abstract TenantId FindMatchingTenantId(HttpContext context); } } \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/MultiTenancy/SingleTenantMiddleware.cs b/src/environments/Backend.Fx.AspNetCore/MultiTenancy/SingleTenantMiddleware.cs index 8b08bbed..92a18301 100644 --- a/src/environments/Backend.Fx.AspNetCore/MultiTenancy/SingleTenantMiddleware.cs +++ b/src/environments/Backend.Fx.AspNetCore/MultiTenancy/SingleTenantMiddleware.cs @@ -4,6 +4,9 @@ namespace Backend.Fx.AspNetCore.MultiTenancy { + /// + /// Always assumes TenantId: 1 for all requests. + /// public class SingleTenantMiddleware { private readonly RequestDelegate _next; diff --git a/src/environments/Backend.Fx.AspNetCore/MultiTenancy/TenantAdminMiddlewareBase.cs b/src/environments/Backend.Fx.AspNetCore/MultiTenancy/TenantAdminMiddlewareBase.cs index 0ebcb61c..d01051d0 100644 --- a/src/environments/Backend.Fx.AspNetCore/MultiTenancy/TenantAdminMiddlewareBase.cs +++ b/src/environments/Backend.Fx.AspNetCore/MultiTenancy/TenantAdminMiddlewareBase.cs @@ -65,7 +65,7 @@ public async Task Invoke(HttpContext context) return; } - if (context.Request.Method.ToLower() == "get") + if (HttpMethods.IsGet(context.Request.Method)) { var tenantIdStr = context.Request.Path.Value.Split('/').Last(); if (int.TryParse(tenantIdStr, out int tenantId)) diff --git a/src/environments/Backend.Fx.AspNetCore/Mvc/Execution/FlushFilter.cs b/src/environments/Backend.Fx.AspNetCore/Mvc/Execution/FlushFilter.cs index 79ab3f92..2691e7a2 100644 --- a/src/environments/Backend.Fx.AspNetCore/Mvc/Execution/FlushFilter.cs +++ b/src/environments/Backend.Fx.AspNetCore/Mvc/Execution/FlushFilter.cs @@ -3,6 +3,10 @@ namespace Backend.Fx.AspNetCore.Mvc.Execution { + /// + /// Makes sure that possible dirty objects are flushed to the persistence layer when the MVC action was executed. This will reveal + /// persistence related problems early and makes them easier to diagnose. + /// public class FlushFilter : IActionFilter { public void OnActionExecuting(ActionExecutingContext context) diff --git a/src/environments/Backend.Fx.AspNetCore/Mvc/Throttling/ExceptionThrottlingAttribute.cs b/src/environments/Backend.Fx.AspNetCore/Mvc/Throttling/ExceptionThrottlingAttribute.cs index 68c5ae34..4ed56721 100644 --- a/src/environments/Backend.Fx.AspNetCore/Mvc/Throttling/ExceptionThrottlingAttribute.cs +++ b/src/environments/Backend.Fx.AspNetCore/Mvc/Throttling/ExceptionThrottlingAttribute.cs @@ -6,6 +6,10 @@ namespace Backend.Fx.AspNetCore.Mvc.Throttling { + /// + /// returns HTTP 429 "Too many requests" when the attributed action get's called from the same IP address in less than + /// the configured interval and an exception was thrown. Useful to prevent brute force attacks.. + /// public class ExceptionThrottlingAttribute : ThrottlingBaseAttribute { public override void OnActionExecuted(ActionExecutedContext actionContext) diff --git a/src/environments/Backend.Fx.AspNetCore/Mvc/Throttling/ThrottlingAttribute.cs b/src/environments/Backend.Fx.AspNetCore/Mvc/Throttling/ThrottlingAttribute.cs index 0f1c8747..031a53f8 100644 --- a/src/environments/Backend.Fx.AspNetCore/Mvc/Throttling/ThrottlingAttribute.cs +++ b/src/environments/Backend.Fx.AspNetCore/Mvc/Throttling/ThrottlingAttribute.cs @@ -6,6 +6,10 @@ namespace Backend.Fx.AspNetCore.Mvc.Throttling { + /// + /// returns HTTP 429 "Too many requests" when the attributed action get's called from the same IP address in less than + /// the configured interval. Useful to prevent denial of service attacks. + /// public class ThrottlingAttribute : ThrottlingBaseAttribute { public override void OnActionExecuting(ActionExecutingContext actionContext) diff --git a/src/environments/Backend.Fx.AspNetCore/Mvc/Validation/ModelValidationFilter.cs b/src/environments/Backend.Fx.AspNetCore/Mvc/Validation/ModelValidationFilter.cs index ebba74b3..196512d6 100644 --- a/src/environments/Backend.Fx.AspNetCore/Mvc/Validation/ModelValidationFilter.cs +++ b/src/environments/Backend.Fx.AspNetCore/Mvc/Validation/ModelValidationFilter.cs @@ -16,11 +16,11 @@ public abstract class ModelValidationFilter : IActionFilter protected void LogErrors(FilterContext context, string controllerName, Errors errors) { - ILogger logger = TryGetControllerType(controllerName, out Type controllerType) + ILogger logger = TryGetControllerType(controllerName, out Type controllerType) ? LogManager.Create(controllerType) : LogManager.Create(); logger.Warn($"Model validation failed during {context.HttpContext.Request.Method} {context.HttpContext.Request.PathBase}: " + - string.Join(System.Environment.NewLine, errors)); + string.Join(System.Environment.NewLine, errors.Select(err => err.ToString()))); } protected bool AcceptsJson(FilterContext context) @@ -34,7 +34,7 @@ protected bool AcceptsHtml(FilterContext context) IList accept = context.HttpContext.Request.GetTypedHeaders().Accept; return accept?.Any(mth => mth.Type == "text" && mth.SubType == "html") == true; } - + private static bool TryGetControllerType(string controllerName, out Type type) { try @@ -45,9 +45,8 @@ private static bool TryGetControllerType(string controllerName, out Type type) { type = null; } - + return type != null; } - } } \ No newline at end of file diff --git a/src/environments/Backend.Fx.AspNetCore/Mvc/Validation/ReturnModelStateAsJsonModelValidationFilter.cs b/src/environments/Backend.Fx.AspNetCore/Mvc/Validation/ReturnModelStateAsJsonModelValidationFilter.cs index b7d0ea5b..43909a58 100644 --- a/src/environments/Backend.Fx.AspNetCore/Mvc/Validation/ReturnModelStateAsJsonModelValidationFilter.cs +++ b/src/environments/Backend.Fx.AspNetCore/Mvc/Validation/ReturnModelStateAsJsonModelValidationFilter.cs @@ -4,6 +4,10 @@ namespace Backend.Fx.AspNetCore.Mvc.Validation { + /// + /// Returns HTTP 400 "Bad Request" when model validation failed. In addition, the bad model state is converted into an instance of + /// gets serialized to the body as JSON. + /// public class ReturnModelStateAsJsonModelValidationFilter : ModelValidationFilter { public override void OnActionExecuting(ActionExecutingContext context) diff --git a/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Backend.Fx.SimpleInjectorDependencyInjection.csproj b/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Backend.Fx.SimpleInjectorDependencyInjection.csproj index 7c702a99..9329d9d0 100644 --- a/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Backend.Fx.SimpleInjectorDependencyInjection.csproj +++ b/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Backend.Fx.SimpleInjectorDependencyInjection.csproj @@ -14,7 +14,7 @@ Marc Wittke anic GmbH All rights reserved. Distributed under the terms of the MIT License. - Bootstrapping for Backend.Fx using Simple Injector 4 + Bootstrapping for Backend.Fx using Simple Injector 5 False MIT https://github.com/marcwittke/Backend.Fx @@ -24,7 +24,7 @@ - + diff --git a/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Modules/SimpleInjectorDomainModule.cs b/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Modules/SimpleInjectorDomainModule.cs index 48c0dfa5..04ce2d1c 100644 --- a/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Modules/SimpleInjectorDomainModule.cs +++ b/src/implementations/Backend.Fx.SimpleInjetorDependencyInjection/Modules/SimpleInjectorDomainModule.cs @@ -1,9 +1,7 @@ using System; using System.Linq; using System.Reflection; -using System.Security.Principal; using Backend.Fx.BuildingBlocks; -using Backend.Fx.Environment.MultiTenancy; using Backend.Fx.Logging; using Backend.Fx.Patterns.Authorization; using Backend.Fx.Patterns.DataGeneration; @@ -15,10 +13,14 @@ namespace Backend.Fx.SimpleInjectorDependencyInjection.Modules { /// - /// Wires all injected domain services: Current and current as set while - /// beginning the scope. All s, s, s - /// the collections of s, s and s - /// found in the given list of domain assemblies. + /// Wires all public domain services to be injected as scoped instances provided by the array of domain assemblies: + /// - s + /// - s + /// - s + /// - s + /// - s + /// - the collections of s + /// - 's /// public class SimpleInjectorDomainModule : SimpleInjectorModule { diff --git a/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheSingleTenantApplication.cs b/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheSingleTenantApplication.cs index 2faade10..469bd024 100644 --- a/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheSingleTenantApplication.cs +++ b/tests/Backend.Fx.Tests/Environment/MultiTenancy/TheSingleTenantApplication.cs @@ -69,9 +69,6 @@ public void DelegatesAllCalls() sut.Dispose(); A.CallTo(() => application.Dispose()).MustHaveHappenedOnceExactly(); - - sut.WaitForBoot(); - A.CallTo(() => application.WaitForBoot(A._, A._)).MustHaveHappenedOnceExactly(); } } } \ No newline at end of file diff --git a/tests/Backend.Fx.Tests/Patterns/DataGeneration/TheGenerateDataOnBootDecorator.cs b/tests/Backend.Fx.Tests/Patterns/DataGeneration/TheGenerateDataOnBootDecorator.cs index c68831b4..35e94ab0 100644 --- a/tests/Backend.Fx.Tests/Patterns/DataGeneration/TheGenerateDataOnBootDecorator.cs +++ b/tests/Backend.Fx.Tests/Patterns/DataGeneration/TheGenerateDataOnBootDecorator.cs @@ -56,8 +56,6 @@ public void DelegatesAllOtherCalls() IMessageBus mb = sut.MessageBus; A.CallTo(() => app.MessageBus).MustHaveHappenedOnceExactly(); - var b = sut.WaitForBoot(); - A.CallTo(() => app.WaitForBoot(A._, A._)).MustHaveHappenedOnceExactly(); // ReSharper restore UnusedVariable } @@ -83,6 +81,8 @@ public async Task RunsDataGeneratorsOnBoot() A.CallTo(() => _dataGenerationContext.SeedDataForTenant(A.That.IsEqualTo(tenantId2), A.That.IsEqualTo(true))).MustHaveHappenedOnceExactly(); A.CallTo(() => _dataGenerationContext.SeedDataForTenant(A.That.IsEqualTo(tenantId3), A.That.IsEqualTo(false))).MustHaveHappenedOnceExactly(); A.CallTo(() => _dataGenerationContext.SeedDataForTenant(A.That.IsEqualTo(tenantId4), A.That.IsEqualTo(false))).MustHaveHappenedOnceExactly(); + + Assert.True(_sut.WaitForBoot(1000)); } } } \ No newline at end of file