From 6b084b3287c9399e4d6b274329f89b6063a3bb31 Mon Sep 17 00:00:00 2001 From: mauroservienti Date: Fri, 11 Sep 2020 16:08:49 +0200 Subject: [PATCH 1/2] Test requirements --- ..._using_multiple_attributes_on_a_handler.cs | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 src/ServiceComposer.AspNetCore.Endpoints.Tests/When_using_multiple_attributes_on_a_handler.cs diff --git a/src/ServiceComposer.AspNetCore.Endpoints.Tests/When_using_multiple_attributes_on_a_handler.cs b/src/ServiceComposer.AspNetCore.Endpoints.Tests/When_using_multiple_attributes_on_a_handler.cs new file mode 100644 index 00000000..3c68eeef --- /dev/null +++ b/src/ServiceComposer.AspNetCore.Endpoints.Tests/When_using_multiple_attributes_on_a_handler.cs @@ -0,0 +1,118 @@ +using System.Dynamic; +using System.Net.Http; +using System.Net.Mime; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using ServiceComposer.AspNetCore.Testing; +using Xunit; + +namespace ServiceComposer.AspNetCore.Endpoints.Tests +{ + public class When_using_multiple_attributes_on_a_handler + { + public class MultipleAttributesOfDifferentTypesHandler : ICompositionRequestsHandler + { + [HttpPost("/multiple/attributes")] + [HttpGet("/multiple/attributes/{id}")] + public Task Handle(HttpRequest request) + { + var vm = request.GetComposedResponseModel(); + vm.RequestPath = request.Path; + + return Task.CompletedTask; + } + } + + public class MultipleGetAttributesHandler : ICompositionRequestsHandler + { + [HttpGet("/multiple/attributes")] + [HttpGet("/multiple/attributes/{id}")] + public Task Handle(HttpRequest request) + { + var vm = request.GetComposedResponseModel(); + vm.RequestPath = request.Path; + + return Task.CompletedTask; + } + } + + [Fact] + public async Task If_attribues_are_of_different_types_handler_should_be_invoked_for_all_routes() + { + // Arrange + var client = new SelfContainedWebApplicationFactoryWithWebHost + ( + configureServices: services => + { + services.AddViewModelComposition(options => + { + options.AssemblyScanner.Disable(); + options.RegisterCompositionHandler(); + }); + services.AddControllers(); + services.AddRouting(); + }, + configure: app => + { + app.UseRouting(); + app.UseEndpoints(builder => + { + builder.MapCompositionHandlers(); + builder.MapControllers(); + }); + } + ).CreateClient(); + + var json = "{}"; + var stringContent = new StringContent(json, Encoding.UTF8, MediaTypeNames.Application.Json); + stringContent.Headers.ContentLength = json.Length; + + // Act + var postResponse = await client.PostAsync("/multiple/attributes", stringContent); + var getResponse = await client.GetAsync("/multiple/attributes/2"); + + // Assert + //Assert.True(composedResponse.IsSuccessStatusCode); + } + + [Fact] + public async Task If_attribues_are_of_the_same_type_handler_should_be_invoked_for_all_routes() + { + // Arrange + var client = new SelfContainedWebApplicationFactoryWithWebHost + ( + configureServices: services => + { + services.AddViewModelComposition(options => + { + options.AssemblyScanner.Disable(); + options.RegisterCompositionHandler(); + }); + services.AddControllers(); + services.AddRouting(); + }, + configure: app => + { + app.UseRouting(); + app.UseEndpoints(builder => + { + builder.MapCompositionHandlers(); + builder.MapControllers(); + }); + } + ).CreateClient(); + + // Act + var composedResponse1 = await client.GetAsync("/multiple/attributes"); + var composedResponse2 = await client.GetAsync("/multiple/attributes/2"); + + // Assert + //Assert.True(composedResponse.IsSuccessStatusCode); + } + } +} \ No newline at end of file From c7b1551cc1dab4c01f5b9082bef312ef030c299e Mon Sep 17 00:00:00 2001 From: Mauro Servienti Date: Sat, 9 Oct 2021 12:09:46 +0200 Subject: [PATCH 2/2] Enable support for multiple attributes of the same type --- ..._using_multiple_attributes_on_a_handler.cs | 187 +++++++++++++----- .../EndpointsExtensions.cs | 110 +++++++---- 2 files changed, 210 insertions(+), 87 deletions(-) diff --git a/src/ServiceComposer.AspNetCore.Endpoints.Tests/When_using_multiple_attributes_on_a_handler.cs b/src/ServiceComposer.AspNetCore.Endpoints.Tests/When_using_multiple_attributes_on_a_handler.cs index 3c68eeef..d7549ffb 100644 --- a/src/ServiceComposer.AspNetCore.Endpoints.Tests/When_using_multiple_attributes_on_a_handler.cs +++ b/src/ServiceComposer.AspNetCore.Endpoints.Tests/When_using_multiple_attributes_on_a_handler.cs @@ -2,12 +2,14 @@ using System.Net.Http; using System.Net.Mime; using System.Text; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using ServiceComposer.AspNetCore.Testing; using Xunit; @@ -28,7 +30,47 @@ public Task Handle(HttpRequest request) } } - public class MultipleGetAttributesHandler : ICompositionRequestsHandler + [Fact] + public async Task If_attributes_are_of_different_types_handler_should_be_invoked_for_all_routes() + { + // Arrange + var client = + new SelfContainedWebApplicationFactoryWithWebHost + ( + configureServices: services => + { + services.AddViewModelComposition(options => + { + options.AssemblyScanner.Disable(); + options.RegisterCompositionHandler(); + }); + services.AddControllers(); + services.AddRouting(); + }, + configure: app => + { + app.UseRouting(); + app.UseEndpoints(builder => + { + builder.MapCompositionHandlers(); + builder.MapControllers(); + }); + } + ).CreateClient(); + + var json = "{}"; + var stringContent = new StringContent(json, Encoding.UTF8, MediaTypeNames.Application.Json); + stringContent.Headers.ContentLength = json.Length; + + // Act + var postResponse = await client.PostAsync("/multiple/attributes", stringContent); + var getResponse = await client.GetAsync("/multiple/attributes/2"); + + // Assert + //Assert.True(composedResponse.IsSuccessStatusCode); + } + + public class MultipleGetAttributesDifferentTemplatesHandler : ICompositionRequestsHandler { [HttpGet("/multiple/attributes")] [HttpGet("/multiple/attributes/{id}")] @@ -40,79 +82,116 @@ public Task Handle(HttpRequest request) return Task.CompletedTask; } } - + [Fact] - public async Task If_attribues_are_of_different_types_handler_should_be_invoked_for_all_routes() + public async Task If_attributes_are_of_the_same_type_handler_should_be_invoked_for_all_routes() { // Arrange - var client = new SelfContainedWebApplicationFactoryWithWebHost - ( - configureServices: services => - { - services.AddViewModelComposition(options => + var client = + new SelfContainedWebApplicationFactoryWithWebHost + ( + configureServices: services => { - options.AssemblyScanner.Disable(); - options.RegisterCompositionHandler(); - }); - services.AddControllers(); - services.AddRouting(); - }, - configure: app => - { - app.UseRouting(); - app.UseEndpoints(builder => + services.AddViewModelComposition(options => + { + options.AssemblyScanner.Disable(); + options.RegisterCompositionHandler(); + }); + services.AddControllers(); + services.AddRouting(); + }, + configure: app => { - builder.MapCompositionHandlers(); - builder.MapControllers(); - }); - } - ).CreateClient(); - - var json = "{}"; - var stringContent = new StringContent(json, Encoding.UTF8, MediaTypeNames.Application.Json); - stringContent.Headers.ContentLength = json.Length; + app.UseRouting(); + app.UseEndpoints(builder => + { + builder.MapCompositionHandlers(); + builder.MapControllers(); + }); + } + ).CreateClient(); // Act - var postResponse = await client.PostAsync("/multiple/attributes", stringContent); - var getResponse = await client.GetAsync("/multiple/attributes/2"); + var composedResponse1 = await client.GetAsync("/multiple/attributes"); + var composedResponse2 = await client.GetAsync("/multiple/attributes/2"); // Assert - //Assert.True(composedResponse.IsSuccessStatusCode); + Assert.True(composedResponse1.IsSuccessStatusCode); + Assert.True(composedResponse2.IsSuccessStatusCode); + } + + class InvocationCountViewModel + { + private int invocationCount = 0; + public int InvocationCount => invocationCount; + + public void IncrementInvocationCount() + { + Interlocked.Increment(ref invocationCount); + } + } + + class InvocationCountViewModelFactory : IViewModelFactory + { + public object CreateViewModel(HttpContext httpContext, ICompositionContext compositionContext) + { + return new InvocationCountViewModel(); + } + } + + public class MultipleGetAttributesSameTemplateHandler : ICompositionRequestsHandler + { + [HttpGet("/multiple/attributes")] + [HttpGet("/multiple/attributes")] + public Task Handle(HttpRequest request) + { + var vm = request.GetComposedResponseModel(); + vm.IncrementInvocationCount(); + + return Task.CompletedTask; + } } [Fact] - public async Task If_attribues_are_of_the_same_type_handler_should_be_invoked_for_all_routes() + public async Task If_attributes_are_of_the_same_type_and_same_template_handler_should_be_invoked_multiple_times() { // Arrange - var client = new SelfContainedWebApplicationFactoryWithWebHost - ( - configureServices: services => - { - services.AddViewModelComposition(options => + var client = + new SelfContainedWebApplicationFactoryWithWebHost + ( + configureServices: services => { - options.AssemblyScanner.Disable(); - options.RegisterCompositionHandler(); - }); - services.AddControllers(); - services.AddRouting(); - }, - configure: app => - { - app.UseRouting(); - app.UseEndpoints(builder => + services.AddViewModelComposition(options => + { + options.AssemblyScanner.Disable(); + options.RegisterGlobalViewModelFactory(); + options.RegisterCompositionHandler(); + }); + services.AddControllers(); + services.AddRouting(); + }, + configure: app => { - builder.MapCompositionHandlers(); - builder.MapControllers(); - }); - } - ).CreateClient(); + app.UseRouting(); + app.UseEndpoints(builder => + { + builder.MapCompositionHandlers(); + builder.MapControllers(); + }); + } + ).CreateClient(); // Act - var composedResponse1 = await client.GetAsync("/multiple/attributes"); - var composedResponse2 = await client.GetAsync("/multiple/attributes/2"); + var composedResponse = await client.GetAsync("/multiple/attributes"); // Assert - //Assert.True(composedResponse.IsSuccessStatusCode); + Assert.True(composedResponse.IsSuccessStatusCode); + + var responseString = await composedResponse.Content.ReadAsStringAsync(); + var responseObj = JObject.Parse(responseString); + var invocationCount = responseObj?.GetValue("invocationCount")?.Value(); + + Assert.Equal(2, invocationCount); } } } \ No newline at end of file diff --git a/src/ServiceComposer.AspNetCore/EndpointsExtensions.cs b/src/ServiceComposer.AspNetCore/EndpointsExtensions.cs index 36c488bc..15fc77a9 100644 --- a/src/ServiceComposer.AspNetCore/EndpointsExtensions.cs +++ b/src/ServiceComposer.AspNetCore/EndpointsExtensions.cs @@ -26,7 +26,8 @@ public static void MapCompositionHandlers(this IEndpointRouteBuilder endpoints) #pragma warning restore 618 } - [Obsolete("To enable write support use the EnableWriteSupport() method on the ViewModelCompositionOptions. This method will be treated as an error in v2 and removed in v3.")] + [Obsolete( + "To enable write support use the EnableWriteSupport() method on the ViewModelCompositionOptions. This method will be treated as an error in v2 and removed in v3.")] public static void MapCompositionHandlers(this IEndpointRouteBuilder endpoints, bool enableWriteSupport) { if (endpoints == null) @@ -35,16 +36,19 @@ public static void MapCompositionHandlers(this IEndpointRouteBuilder endpoints, } var options = endpoints.ServiceProvider.GetRequiredService(); - options.ResponseSerialization.ValidateConfiguration(endpoints.ServiceProvider.GetRequiredService>()); + options.ResponseSerialization.ValidateConfiguration(endpoints.ServiceProvider + .GetRequiredService>()); if (options.CompositionOverControllersOptions.IsEnabled) { - var compositionOverControllersRoutes = endpoints.ServiceProvider.GetRequiredService(); + var compositionOverControllersRoutes = + endpoints.ServiceProvider.GetRequiredService(); compositionOverControllersRoutes.AddGetComponentsSource(compositionOverControllerGetComponents); compositionOverControllersRoutes.AddPostComponentsSource(compositionOverControllerPostComponents); } - var compositionMetadataRegistry = endpoints.ServiceProvider.GetRequiredService(); + var compositionMetadataRegistry = + endpoints.ServiceProvider.GetRequiredService(); MapGetComponents( compositionMetadataRegistry, @@ -81,13 +85,18 @@ public static void MapCompositionHandlers(this IEndpointRouteBuilder endpoints, } } - private static void MapGetComponents(CompositionMetadataRegistry compositionMetadataRegistry, ICollection dataSources, CompositionOverControllersOptions compositionOverControllersOptions, ResponseCasing defaultCasing, bool useOutputFormatters) + private static void MapGetComponents(CompositionMetadataRegistry compositionMetadataRegistry, + ICollection dataSources, + CompositionOverControllersOptions compositionOverControllersOptions, ResponseCasing defaultCasing, + bool useOutputFormatters) { - var componentsGroupedByTemplate = SelectComponentsGroupedByTemplate(compositionMetadataRegistry, compositionOverControllersOptions.UseCaseInsensitiveRouteMatching); + var componentsGroupedByTemplate = SelectComponentsGroupedByTemplate( + compositionMetadataRegistry, compositionOverControllersOptions.UseCaseInsensitiveRouteMatching); foreach (var componentsGroup in componentsGroupedByTemplate) { - if (compositionOverControllersOptions.IsEnabled && ThereIsAlreadyAnEndpointForTheSameTemplate(componentsGroup, dataSources, + if (compositionOverControllersOptions.IsEnabled && ThereIsAlreadyAnEndpointForTheSameTemplate( + componentsGroup, dataSources, compositionOverControllersOptions.UseCaseInsensitiveRouteMatching, out var endpoint)) { var componentTypes = componentsGroup.Select(c => c.ComponentType).ToArray(); @@ -95,19 +104,25 @@ private static void MapGetComponents(CompositionMetadataRegistry compositionMeta } else { - var builder = CreateCompositionEndpointBuilder(componentsGroup, new HttpMethodMetadata(new[] {HttpMethods.Get}), defaultCasing, useOutputFormatters); + var builder = CreateCompositionEndpointBuilder(componentsGroup, + new HttpMethodMetadata(new[] {HttpMethods.Get}), defaultCasing, useOutputFormatters); AppendToDataSource(dataSources, builder); } } } - private static void MapPostComponents(CompositionMetadataRegistry compositionMetadataRegistry, ICollection dataSources, CompositionOverControllersOptions compositionOverControllersOptions, ResponseCasing defaultCasing, bool useOutputFormatters) + private static void MapPostComponents(CompositionMetadataRegistry compositionMetadataRegistry, + ICollection dataSources, + CompositionOverControllersOptions compositionOverControllersOptions, ResponseCasing defaultCasing, + bool useOutputFormatters) { - var componentsGroupedByTemplate = SelectComponentsGroupedByTemplate(compositionMetadataRegistry, compositionOverControllersOptions.UseCaseInsensitiveRouteMatching); + var componentsGroupedByTemplate = SelectComponentsGroupedByTemplate( + compositionMetadataRegistry, compositionOverControllersOptions.UseCaseInsensitiveRouteMatching); foreach (var componentsGroup in componentsGroupedByTemplate) { - if (compositionOverControllersOptions.IsEnabled && ThereIsAlreadyAnEndpointForTheSameTemplate(componentsGroup, dataSources, + if (compositionOverControllersOptions.IsEnabled && ThereIsAlreadyAnEndpointForTheSameTemplate( + componentsGroup, dataSources, compositionOverControllersOptions.UseCaseInsensitiveRouteMatching, out var endpoint)) { var componentTypes = componentsGroup.Select(c => c.ComponentType).ToArray(); @@ -115,49 +130,66 @@ private static void MapPostComponents(CompositionMetadataRegistry compositionMet } else { - var builder = CreateCompositionEndpointBuilder(componentsGroup, new HttpMethodMetadata(new[] {HttpMethods.Post}), defaultCasing, useOutputFormatters); + var builder = CreateCompositionEndpointBuilder(componentsGroup, + new HttpMethodMetadata(new[] {HttpMethods.Post}), defaultCasing, useOutputFormatters); AppendToDataSource(dataSources, builder); } } } - private static void MapPatchComponents(CompositionMetadataRegistry compositionMetadataRegistry, ICollection dataSources, CompositionOverControllersOptions compositionOverControllersOptions, ResponseCasing defaultCasing, bool useOutputFormatters) + private static void MapPatchComponents(CompositionMetadataRegistry compositionMetadataRegistry, + ICollection dataSources, + CompositionOverControllersOptions compositionOverControllersOptions, ResponseCasing defaultCasing, + bool useOutputFormatters) { - var componentsGroupedByTemplate = SelectComponentsGroupedByTemplate(compositionMetadataRegistry, compositionOverControllersOptions.UseCaseInsensitiveRouteMatching); + var componentsGroupedByTemplate = SelectComponentsGroupedByTemplate( + compositionMetadataRegistry, compositionOverControllersOptions.UseCaseInsensitiveRouteMatching); foreach (var componentsGroup in componentsGroupedByTemplate) { - var builder = CreateCompositionEndpointBuilder(componentsGroup, new HttpMethodMetadata(new[] {HttpMethods.Patch}), defaultCasing, useOutputFormatters); + var builder = CreateCompositionEndpointBuilder(componentsGroup, + new HttpMethodMetadata(new[] {HttpMethods.Patch}), defaultCasing, useOutputFormatters); AppendToDataSource(dataSources, builder); } } - private static void MapPutComponents(CompositionMetadataRegistry compositionMetadataRegistry, ICollection dataSources, CompositionOverControllersOptions compositionOverControllersOptions, ResponseCasing defaultCasing, bool useOutputFormatters) + private static void MapPutComponents(CompositionMetadataRegistry compositionMetadataRegistry, + ICollection dataSources, + CompositionOverControllersOptions compositionOverControllersOptions, ResponseCasing defaultCasing, + bool useOutputFormatters) { - var componentsGroupedByTemplate = SelectComponentsGroupedByTemplate(compositionMetadataRegistry, compositionOverControllersOptions.UseCaseInsensitiveRouteMatching); + var componentsGroupedByTemplate = SelectComponentsGroupedByTemplate( + compositionMetadataRegistry, compositionOverControllersOptions.UseCaseInsensitiveRouteMatching); foreach (var componentsGroup in componentsGroupedByTemplate) { - var builder = CreateCompositionEndpointBuilder(componentsGroup, new HttpMethodMetadata(new[] {HttpMethods.Put}), defaultCasing, useOutputFormatters); + var builder = CreateCompositionEndpointBuilder(componentsGroup, + new HttpMethodMetadata(new[] {HttpMethods.Put}), defaultCasing, useOutputFormatters); AppendToDataSource(dataSources, builder); } } - private static void MapDeleteComponents(CompositionMetadataRegistry compositionMetadataRegistry, ICollection dataSources, CompositionOverControllersOptions compositionOverControllersOptions, ResponseCasing defaultCasing, bool useOutputFormatters) + private static void MapDeleteComponents(CompositionMetadataRegistry compositionMetadataRegistry, + ICollection dataSources, + CompositionOverControllersOptions compositionOverControllersOptions, ResponseCasing defaultCasing, + bool useOutputFormatters) { - var componentsGroupedByTemplate = SelectComponentsGroupedByTemplate(compositionMetadataRegistry, compositionOverControllersOptions.UseCaseInsensitiveRouteMatching); + var componentsGroupedByTemplate = SelectComponentsGroupedByTemplate( + compositionMetadataRegistry, compositionOverControllersOptions.UseCaseInsensitiveRouteMatching); foreach (var componentsGroup in componentsGroupedByTemplate) { - var builder = CreateCompositionEndpointBuilder(componentsGroup, new HttpMethodMetadata(new[] {HttpMethods.Delete}), defaultCasing, useOutputFormatters); + var builder = CreateCompositionEndpointBuilder(componentsGroup, + new HttpMethodMetadata(new[] {HttpMethods.Delete}), defaultCasing, useOutputFormatters); AppendToDataSource(dataSources, builder); } } - private static void AppendToDataSource(ICollection dataSources, CompositionEndpointBuilder builder) + private static void AppendToDataSource(ICollection dataSources, + CompositionEndpointBuilder builder) { var dataSource = dataSources.OfType().FirstOrDefault(); if (dataSource == null) @@ -170,7 +202,8 @@ private static void AppendToDataSource(ICollection dataSourc } private static bool ThereIsAlreadyAnEndpointForTheSameTemplate( - IGrouping componentsGroup, ICollection dataSources, + IGrouping componentsGroup, + ICollection dataSources, bool useCaseInsensistiveRouteMatching, out Endpoint endpoint) { foreach (var dataSource in dataSources) @@ -222,20 +255,31 @@ private static CompositionEndpointBuilder CreateCompositionEndpointBuilder( return builder; } - static IEnumerable> SelectComponentsGroupedByTemplate( - CompositionMetadataRegistry compositionMetadataRegistry, bool useCaseInsensitiveRouteMatching) where TAttribute : HttpMethodAttribute + static IEnumerable> + SelectComponentsGroupedByTemplate( + CompositionMetadataRegistry compositionMetadataRegistry, bool useCaseInsensitiveRouteMatching) + where TAttribute : HttpMethodAttribute { var getComponentsGroupedByTemplate = compositionMetadataRegistry.Components - .Select(componentType => + .SelectMany(componentType => { var method = ExtractMethod(componentType); - var template = method.GetCustomAttribute()?.Template.TrimStart('/'); - if (template != null && useCaseInsensitiveRouteMatching) - { - template = template.ToLowerInvariant(); - } - - return (componentType, method, template); + var componentsAndTemplate = method.GetCustomAttributes()? + .Select(a => + { + var template = a.Template.TrimStart('/'); + return (componentType, method, useCaseInsensitiveRouteMatching + ? template.ToLowerInvariant() + : template); + }).ToArray(); + + // var groupsCount = componentsAndTemplate.GroupBy(c => c.Item3).Count(); + // if (groupsCount != componentsAndTemplate.Length) + // { + // throw new NotSupportedException("Multiple attributes of the same type with the same template are not supported."); + // } + + return componentsAndTemplate; }) .Where(component => component.Template != null) .GroupBy(component => component.Template);