Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/Carter/Attributes/TestNegotiatorAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Carter.Attributes;

using System;

[AttributeUsage(AttributeTargets.Class)]
public class TestNegotiatorAttribute : Attribute { }
7 changes: 6 additions & 1 deletion src/Carter/CarterExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@

foreach (var carterModuleInterface in builder.ServiceProvider.GetServices<ICarterModule>())
{
if (carterModuleInterface is CarterModule carterModule)

Check warning on line 33 in src/Carter/CarterExtensions.cs

View workflow job for this annotation

GitHub Actions / Github Actions Build

'CarterModule' is obsolete: 'CarterModule will be removed in the next version. Please migrate to ICarterModule.'

Check warning on line 33 in src/Carter/CarterExtensions.cs

View workflow job for this annotation

GitHub Actions / Github Actions Build

'CarterModule' is obsolete: 'CarterModule will be removed in the next version. Please migrate to ICarterModule.'
{
var group = builder.MapGroup(carterModule.basePath);

Expand Down Expand Up @@ -254,7 +254,12 @@
!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);
Expand Down
50 changes: 50 additions & 0 deletions src/Carter/Helpers/NegotiationHelper.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Selects the most appropriate <see cref="IResponseNegotiator"/> for content negotiation based on the current <see cref="HttpContext"/>'s "Accept" headers,
/// or defaults to <see cref="DefaultJsonResponseNegotiator"/> if none match.
/// </summary>
/// <param name="httpContext">Current <see cref="HttpContext"/></param>
/// <param name="negotiators">List of available <see cref="IResponseNegotiator"/> instances</param>
/// <returns>The selected <see cref="IResponseNegotiator"/> for the response.</returns>
public static IResponseNegotiator SelectNegotiator(HttpContext httpContext, List<IResponseNegotiator> 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);
}
38 changes: 13 additions & 25 deletions src/Carter/Response/ResponseExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -23,30 +24,14 @@ public static class ResponseExtensions
/// <returns><see cref="Task"/></returns>
public static Task Negotiate<T>(this HttpResponse response, T model, CancellationToken cancellationToken = default)
{
var negotiators = response.HttpContext.RequestServices.GetServices<IResponseNegotiator>().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<IResponseNegotiator>()
.Where(n => !NegotiationHelper.IsTestNegotiator(n))
.ToList();

var chosenNegotiator = NegotiationHelper.SelectNegotiator(response.HttpContext, negotiators);

return chosenNegotiator.Handle(response.HttpContext.Request, response, model, cancellationToken);
}

/// <summary>
Expand All @@ -58,7 +43,10 @@ public static Task Negotiate<T>(this HttpResponse response, T model, Cancellatio
/// <returns><see cref="Task"/></returns>
public static Task AsJson<T>(this HttpResponse response, T model, CancellationToken cancellationToken = default)
{
var negotiators = response.HttpContext.RequestServices.GetServices<IResponseNegotiator>();
var negotiators = response.HttpContext.RequestServices
.GetServices<IResponseNegotiator>()
.Where(n => !NegotiationHelper.IsTestNegotiator(n))
.ToList();

var negotiator = negotiators.First(x => x.CanHandle(new MediaTypeHeaderValue("application/json")));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
182 changes: 179 additions & 3 deletions test/Carter.Tests/CarterExtensionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -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]
Expand Down Expand Up @@ -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<InternalRoomModule>());

//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<InternalRoomModelValidator>());

//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<InternalResponseNegotiator>());

//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());
}
}
}
1 change: 0 additions & 1 deletion test/Carter.Tests/ContentNegotiation/NegotiatorModule.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
namespace Carter.Tests.ContentNegotiation
{
using Carter.Response;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -125,6 +126,7 @@ public async Task Should_pick_default_json_processor_last()
}
}

[TestNegotiator]
internal class TestResponseNegotiator : IResponseNegotiator
{
public bool CanHandle(MediaTypeHeaderValue accept) =>
Expand All @@ -137,6 +139,7 @@ public async Task Handle<T>(HttpRequest req, HttpResponse res, T model,
}
}

[TestNegotiator]
internal class TestHtmlResponseNegotiator : IResponseNegotiator
{
public bool CanHandle(MediaTypeHeaderValue accept) =>
Expand All @@ -149,6 +152,7 @@ public async Task Handle<T>(HttpRequest req, HttpResponse res, T model,
}
}

[TestNegotiator]
internal class TestXmlResponseNegotiator : IResponseNegotiator
{
public bool CanHandle(MediaTypeHeaderValue accept) =>
Expand All @@ -161,6 +165,7 @@ public async Task Handle<T>(HttpRequest req, HttpResponse res, T model,
}
}

[TestNegotiator]
internal class TestJsonResponseNegotiator : IResponseNegotiator
{
public bool CanHandle(MediaTypeHeaderValue accept) => accept
Expand Down
Loading
Loading