diff --git a/src/Carter/Attributes/TestNegotiatorAttribute.cs b/src/Carter/Attributes/TestNegotiatorAttribute.cs new file mode 100644 index 0000000..7ffc1f6 --- /dev/null +++ b/src/Carter/Attributes/TestNegotiatorAttribute.cs @@ -0,0 +1,6 @@ +namespace Carter.Attributes; + +using System; + +[AttributeUsage(AttributeTargets.Class)] +public class TestNegotiatorAttribute : Attribute { } diff --git a/src/Carter/CarterExtensions.cs b/src/Carter/CarterExtensions.cs index 476da22..5df7cdc 100644 --- a/src/Carter/CarterExtensions.cs +++ b/src/Carter/CarterExtensions.cs @@ -254,7 +254,12 @@ private static IEnumerable GetNewModules(CarterConfigurator carterConfigur !t.IsAbstract && typeof(ICarterModule).IsAssignableFrom(t) && t != typeof(ICarterModule) && - (t.IsPublic || t.IsNestedPublic) + ( + t.IsPublic || // public top-level class + (t.IsNotPublic && !t.IsNested) || // internal top-level class + t.IsNestedPublic || // public nested class + t.IsNestedAssembly // internal nested class + ) )); carterConfigurator.ModuleTypes.AddRange(modules); diff --git a/src/Carter/Helpers/NegotiationHelper.cs b/src/Carter/Helpers/NegotiationHelper.cs new file mode 100644 index 0000000..2543ca4 --- /dev/null +++ b/src/Carter/Helpers/NegotiationHelper.cs @@ -0,0 +1,50 @@ +namespace Carter.Helpers; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using Carter.Attributes; +using Carter.Response; +using Microsoft.AspNetCore.Http; +using Microsoft.Net.Http.Headers; + +public static class NegotiationHelper +{ + /// + /// Selects the most appropriate for content negotiation based on the current 's "Accept" headers, + /// or defaults to if none match. + /// + /// Current + /// List of available instances + /// The selected for the response. + public static IResponseNegotiator SelectNegotiator(HttpContext httpContext, List negotiators) + { + IResponseNegotiator negotiator = null; + + MediaTypeHeaderValue.TryParseList(httpContext.Request.Headers["Accept"], out var accept); + if (accept != null) + { + var ordered = accept.OrderByDescending(x => x.Quality ?? 1); + + foreach (var acceptHeader in ordered) + { + negotiator = negotiators.FirstOrDefault(x => x.CanHandle(acceptHeader)); + if (negotiator != null) + { + break; + } + } + } + + if (negotiator == null) + { + negotiator = negotiators.First(x => x.GetType() == typeof(DefaultJsonResponseNegotiator)); + } + + return negotiator; + } + + public static bool IsTestNegotiator(IResponseNegotiator negotiator) + => negotiator.GetType().IsDefined(typeof(TestNegotiatorAttribute), inherit: true); +} diff --git a/src/Carter/Response/ResponseExtensions.cs b/src/Carter/Response/ResponseExtensions.cs index 904f8a6..bff7787 100644 --- a/src/Carter/Response/ResponseExtensions.cs +++ b/src/Carter/Response/ResponseExtensions.cs @@ -7,6 +7,7 @@ namespace Carter.Response; using System.Net.Mime; using System.Threading; using System.Threading.Tasks; +using Carter.Helpers; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.DependencyInjection; @@ -23,30 +24,14 @@ public static class ResponseExtensions /// public static Task Negotiate(this HttpResponse response, T model, CancellationToken cancellationToken = default) { - var negotiators = response.HttpContext.RequestServices.GetServices().ToList(); - IResponseNegotiator negotiator = null; - - MediaTypeHeaderValue.TryParseList(response.HttpContext.Request.Headers["Accept"], out var accept); - if (accept != null) - { - var ordered = accept.OrderByDescending(x => x.Quality ?? 1); - - foreach (var acceptHeader in ordered) - { - negotiator = negotiators.FirstOrDefault(x => x.CanHandle(acceptHeader)); - if (negotiator != null) - { - break; - } - } - } - - if (negotiator == null) - { - negotiator = negotiators.First(x => x.CanHandle(new MediaTypeHeaderValue("application/json"))); - } - - return negotiator.Handle(response.HttpContext.Request, response, model, cancellationToken); + var negotiators = response.HttpContext.RequestServices + .GetServices() + .Where(n => !NegotiationHelper.IsTestNegotiator(n)) + .ToList(); + + var chosenNegotiator = NegotiationHelper.SelectNegotiator(response.HttpContext, negotiators); + + return chosenNegotiator.Handle(response.HttpContext.Request, response, model, cancellationToken); } /// @@ -58,7 +43,10 @@ public static Task Negotiate(this HttpResponse response, T model, Cancellatio /// public static Task AsJson(this HttpResponse response, T model, CancellationToken cancellationToken = default) { - var negotiators = response.HttpContext.RequestServices.GetServices(); + var negotiators = response.HttpContext.RequestServices + .GetServices() + .Where(n => !NegotiationHelper.IsTestNegotiator(n)) + .ToList(); var negotiator = negotiators.First(x => x.CanHandle(new MediaTypeHeaderValue("application/json"))); diff --git a/test/Carter.ResponseNegotiators.Newtonsoft.Tests/NewtonsoftJsonResponseNegotiatorTests.cs b/test/Carter.ResponseNegotiators.Newtonsoft.Tests/NewtonsoftJsonResponseNegotiatorTests.cs index a470139..2e19a9a 100644 --- a/test/Carter.ResponseNegotiators.Newtonsoft.Tests/NewtonsoftJsonResponseNegotiatorTests.cs +++ b/test/Carter.ResponseNegotiators.Newtonsoft.Tests/NewtonsoftJsonResponseNegotiatorTests.cs @@ -89,6 +89,7 @@ public async Task Should_pick_default_json_processor_last() } } + //TODO: Add [TestNegotiator] attribute when Carter supports it in this project internal class TestJsonResponseNegotiator : IResponseNegotiator { public bool CanHandle(MediaTypeHeaderValue accept) => accept diff --git a/test/Carter.Tests/CarterExtensionTests.cs b/test/Carter.Tests/CarterExtensionTests.cs index 77e8b22..a36e9ee 100644 --- a/test/Carter.Tests/CarterExtensionTests.cs +++ b/test/Carter.Tests/CarterExtensionTests.cs @@ -2,6 +2,7 @@ namespace Carter.Tests { using System.Linq; using Carter.Tests.ContentNegotiation; + using Carter.Tests.InternalRooms; using Carter.Tests.ModelBinding; using Carter.Tests.StreamTests; using FluentValidation; @@ -123,7 +124,7 @@ public void Should_register_responsenegotiators_passed_in_by_configurator_and_de } [Fact] - public void Should_register_multiple_responsenegotiators_passed_in_by_configurator_and_default_json_negotiator() + public void Should_register_multiple_response_negotiators_passed_in_by_configurator_and_default_json_negotiator() { //Given var serviceCollection = new ServiceCollection(); @@ -132,8 +133,8 @@ public void Should_register_multiple_responsenegotiators_passed_in_by_configurat serviceCollection.AddCarter(configurator: configurator => configurator.WithResponseNegotiators(typeof(TestResponseNegotiator), typeof(TestXmlResponseNegotiator))); //Then - var responsenegotiators = serviceCollection.Where(x => x.ServiceType == typeof(IResponseNegotiator)); - Assert.Equal(3,responsenegotiators.Count()); + var responseNegotiators = serviceCollection.Where(x => x.ServiceType == typeof(IResponseNegotiator)); + Assert.Equal(3, responseNegotiators.Count()); } [Fact] @@ -177,5 +178,180 @@ public void Should_register_no_response_negotiators_passed_in_by_configurator() var responseNegotiators = serviceCollection.Where(x => x.ServiceType == typeof(IResponseNegotiator)); Assert.Single(responseNegotiators); } + + [Fact] + public void Should_register_internal_modules_when_assembly_scanned() + { + //Given + var serviceCollection = new ServiceCollection(); + + //When + serviceCollection.AddCarter(); + + //Then + var modules = serviceCollection.Where(x => x.ServiceType == typeof(ICarterModule)); + var internalModule = modules.FirstOrDefault(x => x.ImplementationType == typeof(InternalRoomModule)); + Assert.NotNull(internalModule); + } + + [Fact] + public void Should_register_internal_module_passed_in_by_configurator() + { + //Given + var serviceCollection = new ServiceCollection(); + + //When + serviceCollection.AddCarter(configurator: configurator => configurator.WithModule()); + + //Then + var modules = serviceCollection.Where(x => x.ServiceType == typeof(ICarterModule)); + var internalModule = modules.FirstOrDefault(x => x.ImplementationType == typeof(InternalRoomModule)); + Assert.NotNull(internalModule); + } + + [Fact] + public void Should_register_multiple_internal_modules_passed_in_by_configurator() + { + //Given + var serviceCollection = new ServiceCollection(); + + //When + serviceCollection.AddCarter(configurator: configurator => + configurator.WithModules(typeof(InternalRoomModule), typeof(NestedInternalRoomModuleWrapper.NestedInternalRoomModule))); + + //Then + var modules = serviceCollection.Where(x => x.ServiceType == typeof(ICarterModule)).ToList(); + Assert.Contains(modules, m => m.ImplementationType == typeof(InternalRoomModule)); + Assert.Contains(modules, m => m.ImplementationType == typeof(NestedInternalRoomModuleWrapper.NestedInternalRoomModule)); + Assert.Equal(2, modules.Count()); + } + + [Fact] + public void Should_register_internal_validators_when_assembly_scanned() + { + //Given + var serviceCollection = new ServiceCollection(); + + //When + serviceCollection.AddCarter(); + + //Then + var validators = serviceCollection.Where(x => x.ServiceType == typeof(IValidator)); + var internalValidator = validators.FirstOrDefault(x => x.ImplementationType == typeof(InternalRoomModelValidator)); + Assert.NotNull(internalValidator); + } + + [Fact] + public void Should_register_internal_validator_passed_in_by_configurator() + { + //Given + var serviceCollection = new ServiceCollection(); + + //When + serviceCollection.AddCarter(configurator: configurator => configurator.WithValidator()); + + //Then + var validators = serviceCollection.Where(x => x.ServiceType == typeof(IValidator)).ToList(); + var internalValidator = validators.FirstOrDefault(x => x.ImplementationType == typeof(InternalRoomModelValidator)); + Assert.NotNull(internalValidator); + Assert.Single(validators); + } + + [Fact] + public void Should_register_multiple_internal_validators_passed_in_by_configurator() + { + //Given + var serviceCollection = new ServiceCollection(); + + //When + serviceCollection.AddCarter(configurator: configurator => + configurator.WithValidators(typeof(InternalRoomModelValidator), typeof(NestedInternalTestModelValidatorWrapper.InternalTestModelValidator))); + + //Then + var validators = serviceCollection.Where(x => x.ServiceType == typeof(IValidator)).ToList(); + Assert.Contains(validators, v => v.ImplementationType == typeof(InternalRoomModelValidator)); + Assert.Contains(validators, v => v.ImplementationType == typeof(NestedInternalTestModelValidatorWrapper.InternalTestModelValidator)); + Assert.Equal(2, validators.Count()); + } + + [Fact] + public void Should_register_internal_response_negotiators_when_assembly_scanned() + { + //Given + var serviceCollection = new ServiceCollection(); + + //When + serviceCollection.AddCarter(); + + //Then + var responseNegotiators = serviceCollection.Where(x => x.ServiceType == typeof(IResponseNegotiator)); + var internalNegotiator = responseNegotiators.FirstOrDefault(x => x.ImplementationType == typeof(InternalResponseNegotiator)); + Assert.NotNull(internalNegotiator); + } + + [Fact] + public void Should_register_internal_response_negotiator_passed_in_by_configurator() + { + //Given + var serviceCollection = new ServiceCollection(); + + //When + serviceCollection.AddCarter(configurator: configurator => configurator.WithResponseNegotiator()); + + //Then + var responseNegotiators = serviceCollection.Where(x => x.ServiceType == typeof(IResponseNegotiator)); + var internalNegotiator = responseNegotiators.FirstOrDefault(x => x.ImplementationType == typeof(InternalResponseNegotiator)); + Assert.NotNull(internalNegotiator); + } + + [Fact] + public void Should_register_multiple_internal_response_negotiators_passed_in_by_configurator() + { + //Given + var serviceCollection = new ServiceCollection(); + + //When + serviceCollection.AddCarter(configurator: configurator => + configurator.WithResponseNegotiators(typeof(InternalResponseNegotiator), typeof(NestedInternalResponseNegotiatorWrapper.NestedInternalResponseNegotiator))); + + //Then + var responseNegotiators = serviceCollection.Where(x => x.ServiceType == typeof(IResponseNegotiator)).ToList(); + Assert.Contains(responseNegotiators, r => r.ImplementationType == typeof(InternalResponseNegotiator)); + Assert.Contains(responseNegotiators, r => r.ImplementationType == typeof(NestedInternalResponseNegotiatorWrapper.NestedInternalResponseNegotiator)); + } + + [Fact] + public void Should_register_mix_of_public_and_internal_modules() + { + //Given + var serviceCollection = new ServiceCollection(); + + //When + serviceCollection.AddCarter(configurator: configurator => + configurator.WithModules(typeof(TestModule), typeof(InternalRoomModule))); + + //Then + var modules = serviceCollection.Where(x => x.ServiceType == typeof(ICarterModule)).ToList(); + Assert.Contains(modules, m => m.ImplementationType == typeof(TestModule)); + Assert.Contains(modules, m => m.ImplementationType == typeof(InternalRoomModule)); + Assert.Equal(2, modules.Count()); + } + + [Fact] + public void Should_register_mix_of_public_and_internal_validators() + { + //Given + var serviceCollection = new ServiceCollection(); + + //When + serviceCollection.AddCarter(configurator: configurator => + configurator.WithValidators(typeof(TestModelValidator), typeof(InternalRoomModelValidator))); + + //Then + var validators = serviceCollection.Where(x => x.ServiceType == typeof(IValidator)).ToList(); + Assert.Contains(validators, v => v.ImplementationType == typeof(TestModelValidator)); + Assert.Contains(validators, v => v.ImplementationType == typeof(InternalRoomModelValidator)); + Assert.Equal(2, validators.Count()); + } } } diff --git a/test/Carter.Tests/ContentNegotiation/NegotiatorModule.cs b/test/Carter.Tests/ContentNegotiation/NegotiatorModule.cs index 9b75848..3f2f91d 100644 --- a/test/Carter.Tests/ContentNegotiation/NegotiatorModule.cs +++ b/test/Carter.Tests/ContentNegotiation/NegotiatorModule.cs @@ -1,6 +1,5 @@ namespace Carter.Tests.ContentNegotiation { - using Carter.Response; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; diff --git a/test/Carter.Tests/ContentNegotiation/ResponseNegotiatorTests.cs b/test/Carter.Tests/ContentNegotiation/ResponseNegotiatorTests.cs index 2a93e16..ecc44df 100644 --- a/test/Carter.Tests/ContentNegotiation/ResponseNegotiatorTests.cs +++ b/test/Carter.Tests/ContentNegotiation/ResponseNegotiatorTests.cs @@ -5,6 +5,7 @@ namespace Carter.Tests.ContentNegotiation using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; + using Carter.Attributes; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; @@ -125,6 +126,7 @@ public async Task Should_pick_default_json_processor_last() } } + [TestNegotiator] internal class TestResponseNegotiator : IResponseNegotiator { public bool CanHandle(MediaTypeHeaderValue accept) => @@ -137,6 +139,7 @@ public async Task Handle(HttpRequest req, HttpResponse res, T model, } } + [TestNegotiator] internal class TestHtmlResponseNegotiator : IResponseNegotiator { public bool CanHandle(MediaTypeHeaderValue accept) => @@ -149,6 +152,7 @@ public async Task Handle(HttpRequest req, HttpResponse res, T model, } } + [TestNegotiator] internal class TestXmlResponseNegotiator : IResponseNegotiator { public bool CanHandle(MediaTypeHeaderValue accept) => @@ -161,6 +165,7 @@ public async Task Handle(HttpRequest req, HttpResponse res, T model, } } + [TestNegotiator] internal class TestJsonResponseNegotiator : IResponseNegotiator { public bool CanHandle(MediaTypeHeaderValue accept) => accept diff --git a/test/Carter.Tests/ContentNegotiation/TestResponseExtensions.cs b/test/Carter.Tests/ContentNegotiation/TestResponseExtensions.cs new file mode 100644 index 0000000..357d336 --- /dev/null +++ b/test/Carter.Tests/ContentNegotiation/TestResponseExtensions.cs @@ -0,0 +1,22 @@ +namespace Carter.Tests.ContentNegotiation; + +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Carter.Helpers; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +public static class TestResponseExtensions +{ + public static Task Negotiate(this HttpResponse response, T model, CancellationToken cancellationToken = default) + { + var negotiators = response.HttpContext.RequestServices + .GetServices() + .ToList(); + + var chosenNegotiator = NegotiationHelper.SelectNegotiator(response.HttpContext, negotiators); + + return chosenNegotiator.Handle(response.HttpContext.Request, response, model, cancellationToken); + } +} diff --git a/test/Carter.Tests/InternalRooms/InternalResponseNegotiator.cs b/test/Carter.Tests/InternalRooms/InternalResponseNegotiator.cs new file mode 100644 index 0000000..e32bc93 --- /dev/null +++ b/test/Carter.Tests/InternalRooms/InternalResponseNegotiator.cs @@ -0,0 +1,21 @@ +namespace Carter.Tests.InternalRooms; + +using System.Threading; +using System.Threading.Tasks; +using Carter.Attributes; +using Microsoft.AspNetCore.Http; +using Microsoft.Net.Http.Headers; + +[TestNegotiator] +internal class InternalResponseNegotiator: IResponseNegotiator +{ + public bool CanHandle(MediaTypeHeaderValue accept) + { + return true; + } + + public Task Handle(HttpRequest req, HttpResponse res, T model, CancellationToken cancellationToken) + { + return Task.CompletedTask; + } +} diff --git a/test/Carter.Tests/InternalRooms/InternalRoomModel.cs b/test/Carter.Tests/InternalRooms/InternalRoomModel.cs new file mode 100644 index 0000000..7a6931f --- /dev/null +++ b/test/Carter.Tests/InternalRooms/InternalRoomModel.cs @@ -0,0 +1,6 @@ +namespace Carter.Tests.InternalRooms; + +internal sealed class InternalRoomModel +{ + public string Name { get; set; } +} \ No newline at end of file diff --git a/test/Carter.Tests/InternalRooms/InternalRoomModelValidator.cs b/test/Carter.Tests/InternalRooms/InternalRoomModelValidator.cs new file mode 100644 index 0000000..8259d2f --- /dev/null +++ b/test/Carter.Tests/InternalRooms/InternalRoomModelValidator.cs @@ -0,0 +1,11 @@ +namespace Carter.Tests.InternalRooms; + +using FluentValidation; + +internal class InternalRoomModelValidator : AbstractValidator +{ + public InternalRoomModelValidator() + { + this.RuleFor(x => x.Name).NotEmpty(); + } +} diff --git a/test/Carter.Tests/InternalRooms/InternalRoomModule.cs b/test/Carter.Tests/InternalRooms/InternalRoomModule.cs new file mode 100644 index 0000000..ef8c045 --- /dev/null +++ b/test/Carter.Tests/InternalRooms/InternalRoomModule.cs @@ -0,0 +1,17 @@ +namespace Carter.Tests.InternalRooms; + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +internal class InternalRoomModule : ICarterModule +{ + public void AddRoutes(IEndpointRouteBuilder app) + { + app.MapGet("/", (HttpResponse res) => + { + res.StatusCode = 409; + return Results.Text("There's no place like 127.0.0.1"); + }); + } +} diff --git a/test/Carter.Tests/InternalRooms/NestedInternalResponseNegotiator.cs b/test/Carter.Tests/InternalRooms/NestedInternalResponseNegotiator.cs new file mode 100644 index 0000000..a4a978a --- /dev/null +++ b/test/Carter.Tests/InternalRooms/NestedInternalResponseNegotiator.cs @@ -0,0 +1,25 @@ +namespace Carter.Tests.InternalRooms; + +using System.Threading; +using System.Threading.Tasks; +using Carter.Attributes; +using Microsoft.AspNetCore.Http; +using Microsoft.Net.Http.Headers; + +internal static class NestedInternalResponseNegotiatorWrapper +{ + [TestNegotiator] + internal class NestedInternalResponseNegotiator: IResponseNegotiator + { + public bool CanHandle(MediaTypeHeaderValue accept) + { + return true; + } + + public Task Handle(HttpRequest req, HttpResponse res, T model, CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + } +} + diff --git a/test/Carter.Tests/InternalRooms/NestedInternalRoomModelValidator.cs b/test/Carter.Tests/InternalRooms/NestedInternalRoomModelValidator.cs new file mode 100644 index 0000000..da2bdf2 --- /dev/null +++ b/test/Carter.Tests/InternalRooms/NestedInternalRoomModelValidator.cs @@ -0,0 +1,15 @@ +namespace Carter.Tests.InternalRooms; + +using FluentValidation; + +internal static class NestedInternalTestModelValidatorWrapper +{ + internal class InternalTestModelValidator : AbstractValidator + { + public InternalTestModelValidator() + { + this.RuleFor(x => x.Name).NotEmpty(); + } + } +} + diff --git a/test/Carter.Tests/InternalRooms/NestedInternalRoomModule.cs b/test/Carter.Tests/InternalRooms/NestedInternalRoomModule.cs new file mode 100644 index 0000000..e65afed --- /dev/null +++ b/test/Carter.Tests/InternalRooms/NestedInternalRoomModule.cs @@ -0,0 +1,22 @@ +namespace Carter.Tests.InternalRooms; + +using Carter; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +internal static class NestedInternalRoomModuleWrapper +{ + internal class NestedInternalRoomModule : ICarterModule + { + public void AddRoutes(IEndpointRouteBuilder app) + { + app.MapGet("/nested-room", (HttpResponse res) => + { + res.StatusCode = 409; + return Results.Text("There's no place like 127.0.0.1"); + }); + } + } +} +