diff --git a/Directory.Build.props b/Directory.Build.props index 250ad39..944379b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -18,6 +18,6 @@ MIT True - 0.6.3 + 0.6.4 \ No newline at end of file diff --git a/README.md b/README.md index 2ab446a..44e26c5 100644 --- a/README.md +++ b/README.md @@ -308,6 +308,33 @@ internal class CreateBook(ServiceDbContext db, ILocationStore location) } ``` +### Disabling components + +DoNotRegister attribute can be used to prevent targeted route group (including its children) or endpoint to be registered during server application startup. + +Also prevents targeted service endpoint request to be registered during service endpoint client application startup. + +```csharp +[MapToGroup()] +[DoNotRegister] +internal class DisabledCustomerFeature + : MinimalEndpoint +{ + protected override void Configure( + IServiceProvider serviceProvider, + IRouteGroupConfigurator? parentRouteGroup) + { + MapGet("/disabled/"); + } + + protected override Task HandleAsync( + CancellationToken ct) + { + throw new NotImplementedException(); + } +} +``` + ## Samples [ShowcaseWebApi](https://github.com/modabas/ModEndpoints/tree/main/samples/ShowcaseWebApi) project demonstrates various kinds of endpoint implementations and configurations: diff --git a/samples/ShowcaseWebApi/Features/Customers/CreateCustomer.cs b/samples/ShowcaseWebApi/Features/Customers/CreateCustomer.cs index 4f9eded..9ef5f88 100644 --- a/samples/ShowcaseWebApi/Features/Customers/CreateCustomer.cs +++ b/samples/ShowcaseWebApi/Features/Customers/CreateCustomer.cs @@ -24,7 +24,7 @@ public CreateCustomerRequestValidator() [MapToGroup()] internal class CreateCustomer(ServiceDbContext db) - : MinimalEndpoint, ValidationProblem>> + : MinimalEndpoint, ValidationProblem, ProblemHttpResult>> { protected override void Configure( IServiceProvider serviceProvider, @@ -33,7 +33,7 @@ protected override void Configure( MapPost("/"); } - protected override async Task, ValidationProblem>> HandleAsync( + protected override async Task, ValidationProblem, ProblemHttpResult>> HandleAsync( CreateCustomerRequest req, CancellationToken ct) { diff --git a/samples/ShowcaseWebApi/Features/Customers/DeleteBook.cs b/samples/ShowcaseWebApi/Features/Customers/DeleteCustomer.cs similarity index 100% rename from samples/ShowcaseWebApi/Features/Customers/DeleteBook.cs rename to samples/ShowcaseWebApi/Features/Customers/DeleteCustomer.cs diff --git a/samples/ShowcaseWebApi/Features/Customers/DisabledCustomerFeature.cs b/samples/ShowcaseWebApi/Features/Customers/DisabledCustomerFeature.cs new file mode 100644 index 0000000..a5efb15 --- /dev/null +++ b/samples/ShowcaseWebApi/Features/Customers/DisabledCustomerFeature.cs @@ -0,0 +1,24 @@ +using ModEndpoints.Core; +using ModEndpoints.RemoteServices.Core; +using ShowcaseWebApi.Features.Customers.Configuration; + +namespace ShowcaseWebApi.Features.Customers; + +[MapToGroup()] +[DoNotRegister] +internal class DisabledCustomerFeature + : MinimalEndpoint +{ + protected override void Configure( + IServiceProvider serviceProvider, + IRouteGroupConfigurator? parentRouteGroup) + { + MapGet("/disabled/"); + } + + protected override Task HandleAsync( + CancellationToken ct) + { + throw new NotImplementedException(); + } +} diff --git a/samples/ShowcaseWebApi/Features/Customers/GetCustomerById.cs b/samples/ShowcaseWebApi/Features/Customers/GetCustomerById.cs index ee9eb15..1dfe79c 100644 --- a/samples/ShowcaseWebApi/Features/Customers/GetCustomerById.cs +++ b/samples/ShowcaseWebApi/Features/Customers/GetCustomerById.cs @@ -21,7 +21,7 @@ public GetCustomerByIdRequestValidator() [MapToGroup()] internal class GetCustomerById(ServiceDbContext db) - : MinimalEndpoint, NotFound, ValidationProblem>> + : MinimalEndpoint, NotFound, ValidationProblem, ProblemHttpResult>> { protected override void Configure( IServiceProvider serviceProvider, @@ -30,7 +30,7 @@ protected override void Configure( MapGet("/{Id}"); } - protected override async Task, NotFound, ValidationProblem>> HandleAsync( + protected override async Task, NotFound, ValidationProblem, ProblemHttpResult>> HandleAsync( GetCustomerByIdRequest req, CancellationToken ct) { diff --git a/src/ModEndpoints.Core/DependencyInjectionExtensions.cs b/src/ModEndpoints.Core/DependencyInjectionExtensions.cs index 12d9d5d..10f908d 100644 --- a/src/ModEndpoints.Core/DependencyInjectionExtensions.cs +++ b/src/ModEndpoints.Core/DependencyInjectionExtensions.cs @@ -38,7 +38,8 @@ private static IServiceCollection AddRouteGroupsCoreFromAssembly( .DefinedTypes .Where(type => type is { IsAbstract: false, IsInterface: false } && type.IsAssignableTo(typeof(IRouteGroupConfigurator)) && - type != typeof(RootRouteGroup)) + type != typeof(RootRouteGroup) && + !type.GetCustomAttributes(typeof(DoNotRegisterAttribute)).Any()) .Select(type => ServiceDescriptor.DescribeKeyed(typeof(IRouteGroupConfigurator), type, type, lifetime)) .ToArray(); @@ -55,7 +56,8 @@ public static IServiceCollection AddEndpointsCoreFromAssembly( var endpointTypes = assembly .DefinedTypes .Where(type => type is { IsAbstract: false, IsInterface: false } && - type.IsAssignableTo(typeof(IEndpointConfigurator))); + type.IsAssignableTo(typeof(IEndpointConfigurator)) && + !type.GetCustomAttributes(typeof(DoNotRegisterAttribute)).Any()); CheckServiceEndpointRegistrations(endpointTypes); diff --git a/src/ModEndpoints.Core/[Endpoints]/MinimalEndpoint.cs b/src/ModEndpoints.Core/[Endpoints]/MinimalEndpoint.cs index 32ee4c0..62ddb0d 100644 --- a/src/ModEndpoints.Core/[Endpoints]/MinimalEndpoint.cs +++ b/src/ModEndpoints.Core/[Endpoints]/MinimalEndpoint.cs @@ -70,6 +70,14 @@ protected virtual ValueTask HandleInvalidValidationResultAsync( responseType.Name.StartsWith("Results`") && (responseType.Namespace?.Equals("Microsoft.AspNetCore.Http.HttpResults") ?? false)) { + if (TryUseImplicitOperatorFor( + responseType, + validationResult, + vr => vr.ToTypedProblem(), + out var problem)) + { + return new ValueTask(problem); + } if (TryUseImplicitOperatorFor( responseType, validationResult, @@ -82,25 +90,17 @@ protected virtual ValueTask HandleInvalidValidationResultAsync( responseType, validationResult, vr => vr.ToTypedBadRequestWithValidationProblem(), - out var badRequestWithValidationProblems)) + out var badRequestWithValidationProblem)) { - return new ValueTask(badRequestWithValidationProblems); + return new ValueTask(badRequestWithValidationProblem); } if (TryUseImplicitOperatorFor>( responseType, validationResult, vr => vr.ToTypedBadRequestWithProblem(), - out var badRequestWithProblems)) + out var badRequestWithProblem)) { - return new ValueTask(badRequestWithProblems); - } - if (TryUseImplicitOperatorFor( - responseType, - validationResult, - vr => vr.ToTypedProblem(), - out var problem)) - { - return new ValueTask(problem); + return new ValueTask(badRequestWithProblem); } if (TryUseImplicitOperatorFor( responseType, diff --git a/src/ModEndpoints.RemoteServices.Core/DoNotRegisterAttribute.cs b/src/ModEndpoints.RemoteServices.Core/DoNotRegisterAttribute.cs new file mode 100644 index 0000000..ec02681 --- /dev/null +++ b/src/ModEndpoints.RemoteServices.Core/DoNotRegisterAttribute.cs @@ -0,0 +1,10 @@ +namespace ModEndpoints.RemoteServices.Core; + +/// +/// Attribute to prevent targeted route group (including its children) or endpoint to be registered during server application startup. +/// Also prevents targeted service endpoint request to be registered during service endpoint client application startup. +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] +public class DoNotRegisterAttribute : Attribute +{ +} diff --git a/src/ModEndpoints.RemoteServices/DependencyInjectionExtensions.cs b/src/ModEndpoints.RemoteServices/DependencyInjectionExtensions.cs index 2594edb..bae2650 100644 --- a/src/ModEndpoints.RemoteServices/DependencyInjectionExtensions.cs +++ b/src/ModEndpoints.RemoteServices/DependencyInjectionExtensions.cs @@ -10,6 +10,7 @@ public static class DependencyInjectionExtensions private const string ClientDoesNotExist = "A client with name {0} does not exist."; private const string ChannelAlreadyRegistered = "A channel for request type {0} is already registered."; private const string ChannelCannotBeRegistered = "Channel couldn't be registered for request type {0} and client name {1}."; + private const string RequestTypeFlaggedAsDoNotRegister = "Request type {0} is flagged as 'DoNotRegister'."; /// /// Adds and configures a new client for specified ServiceEndpoint request. @@ -163,7 +164,8 @@ public static IServiceCollection AddRemoteServicesWithNewClient( var requestTypes = fromAssembly .DefinedTypes .Where(type => type is { IsAbstract: false, IsInterface: false } && - type.IsAssignableTo(typeof(IServiceRequestMarker))); + type.IsAssignableTo(typeof(IServiceRequestMarker)) && + !type.GetCustomAttributes(typeof(DoNotRegisterAttribute)).Any()); if (requestFilterPredicate is not null) { @@ -206,7 +208,8 @@ public static IServiceCollection AddRemoteServicesToExistingClient( var requestTypes = fromAssembly .DefinedTypes .Where(type => type is { IsAbstract: false, IsInterface: false } && - type.IsAssignableTo(typeof(IServiceRequestMarker))); + type.IsAssignableTo(typeof(IServiceRequestMarker)) && + !type.GetCustomAttributes(typeof(DoNotRegisterAttribute)).Any()); if (requestFilterPredicate is not null) { @@ -229,6 +232,10 @@ private static IServiceCollection AddRemoteServiceWithNewClientInternal( Action configureClient, Action? configureClientBuilder) { + if (requestType.GetCustomAttributes(typeof(DoNotRegisterAttribute)).Any()) + { + throw new InvalidOperationException(string.Format(RequestTypeFlaggedAsDoNotRegister, requestType)); + } if (ServiceChannelRegistry.Instance.IsRequestRegistered(requestType)) { throw new InvalidOperationException(string.Format(ChannelAlreadyRegistered, requestType)); @@ -250,6 +257,10 @@ private static IServiceCollection AddRemoteServiceToExistingClientInternal( Type requestType, string clientName) { + if (requestType.GetCustomAttributes(typeof(DoNotRegisterAttribute)).Any()) + { + throw new InvalidOperationException(string.Format(RequestTypeFlaggedAsDoNotRegister, requestType)); + } if (ServiceChannelRegistry.Instance.IsRequestRegistered(requestType)) { throw new InvalidOperationException(string.Format(ChannelAlreadyRegistered, requestType));