Skip to content

Commit b5d7adb

Browse files
committed
fix: implement negotiation helper and add TestNegotiator attribute
1 parent e2e6041 commit b5d7adb

9 files changed

Lines changed: 101 additions & 26 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace Carter.Attributes;
2+
3+
using System;
4+
5+
[AttributeUsage(AttributeTargets.Class)]
6+
public class TestNegotiatorAttribute : Attribute { }
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
namespace Carter.Helpers;
2+
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Linq.Expressions;
7+
using Carter.Attributes;
8+
using Carter.Response;
9+
using Microsoft.AspNetCore.Http;
10+
using Microsoft.Net.Http.Headers;
11+
12+
public static class NegotiationHelper
13+
{
14+
/// <summary>
15+
/// Selects the most appropriate <see cref="IResponseNegotiator"/> for content negotiation based on the current <see cref="HttpContext"/>'s "Accept" headers,
16+
/// or defaults to <see cref="DefaultJsonResponseNegotiator"/> if none match.
17+
/// </summary>
18+
/// <param name="httpContext">Current <see cref="HttpContext"/></param>
19+
/// <param name="negotiators">List of available <see cref="IResponseNegotiator"/> instances</param>
20+
/// <returns>The selected <see cref="IResponseNegotiator"/> for the response.</returns>
21+
public static IResponseNegotiator SelectNegotiator(HttpContext httpContext, List<IResponseNegotiator> negotiators)
22+
{
23+
IResponseNegotiator negotiator = null;
24+
25+
MediaTypeHeaderValue.TryParseList(httpContext.Request.Headers["Accept"], out var accept);
26+
if (accept != null)
27+
{
28+
var ordered = accept.OrderByDescending(x => x.Quality ?? 1);
29+
30+
foreach (var acceptHeader in ordered)
31+
{
32+
negotiator = negotiators.FirstOrDefault(x => x.CanHandle(acceptHeader));
33+
if (negotiator != null)
34+
{
35+
break;
36+
}
37+
}
38+
}
39+
40+
if (negotiator == null)
41+
{
42+
negotiator = negotiators.First(x => x.GetType() == typeof(DefaultJsonResponseNegotiator));
43+
}
44+
45+
return negotiator;
46+
}
47+
48+
public static bool IsTestNegotiator(IResponseNegotiator negotiator)
49+
=> negotiator.GetType().IsDefined(typeof(TestNegotiatorAttribute), inherit: true);
50+
}

src/Carter/Response/ResponseExtensions.cs

Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ namespace Carter.Response;
77
using System.Net.Mime;
88
using System.Threading;
99
using System.Threading.Tasks;
10+
using Carter.Helpers;
1011
using Microsoft.AspNetCore.Http;
1112
using Microsoft.AspNetCore.Http.Extensions;
1213
using Microsoft.Extensions.DependencyInjection;
@@ -23,30 +24,14 @@ public static class ResponseExtensions
2324
/// <returns><see cref="Task"/></returns>
2425
public static Task Negotiate<T>(this HttpResponse response, T model, CancellationToken cancellationToken = default)
2526
{
26-
var negotiators = response.HttpContext.RequestServices.GetServices<IResponseNegotiator>().ToList();
27-
IResponseNegotiator negotiator = null;
28-
29-
MediaTypeHeaderValue.TryParseList(response.HttpContext.Request.Headers["Accept"], out var accept);
30-
if (accept != null)
31-
{
32-
var ordered = accept.OrderByDescending(x => x.Quality ?? 1);
33-
34-
foreach (var acceptHeader in ordered)
35-
{
36-
negotiator = negotiators.FirstOrDefault(x => x.CanHandle(acceptHeader));
37-
if (negotiator != null)
38-
{
39-
break;
40-
}
41-
}
42-
}
43-
44-
if (negotiator == null)
45-
{
46-
negotiator = negotiators.First(x => x.CanHandle(new MediaTypeHeaderValue("application/json")));
47-
}
48-
49-
return negotiator.Handle(response.HttpContext.Request, response, model, cancellationToken);
27+
var negotiators = response.HttpContext.RequestServices
28+
.GetServices<IResponseNegotiator>()
29+
.Where(n => !NegotiationHelper.IsTestNegotiator(n))
30+
.ToList();
31+
32+
var chosenNegotiator = NegotiationHelper.SelectNegotiator(response.HttpContext, negotiators);
33+
34+
return chosenNegotiator.Handle(response.HttpContext.Request, response, model, cancellationToken);
5035
}
5136

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

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

test/Carter.ResponseNegotiators.Newtonsoft.Tests/NewtonsoftJsonResponseNegotiatorTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ public async Task Should_pick_default_json_processor_last()
8989
}
9090
}
9191

92+
//TODO: Add [TestNegotiator] attribute when Carter supports it in this project
9293
internal class TestJsonResponseNegotiator : IResponseNegotiator
9394
{
9495
public bool CanHandle(MediaTypeHeaderValue accept) => accept

test/Carter.Tests/ContentNegotiation/NegotiatorModule.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
namespace Carter.Tests.ContentNegotiation
22
{
3-
using Carter.Response;
43
using Microsoft.AspNetCore.Builder;
54
using Microsoft.AspNetCore.Http;
65
using Microsoft.AspNetCore.Routing;

test/Carter.Tests/ContentNegotiation/ResponseNegotiatorTests.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ namespace Carter.Tests.ContentNegotiation
55
using System.Net.Http.Headers;
66
using System.Threading;
77
using System.Threading.Tasks;
8+
using Carter.Attributes;
89
using Microsoft.AspNetCore.Builder;
910
using Microsoft.AspNetCore.Hosting;
1011
using Microsoft.AspNetCore.Http;
@@ -125,6 +126,7 @@ public async Task Should_pick_default_json_processor_last()
125126
}
126127
}
127128

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

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

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

168+
[TestNegotiator]
164169
internal class TestJsonResponseNegotiator : IResponseNegotiator
165170
{
166171
public bool CanHandle(MediaTypeHeaderValue accept) => accept
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
namespace Carter.Tests.ContentNegotiation;
2+
3+
using System.Linq;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using Carter.Helpers;
7+
using Microsoft.AspNetCore.Http;
8+
using Microsoft.Extensions.DependencyInjection;
9+
10+
public static class TestResponseExtensions
11+
{
12+
public static Task Negotiate<T>(this HttpResponse response, T model, CancellationToken cancellationToken = default)
13+
{
14+
var negotiators = response.HttpContext.RequestServices
15+
.GetServices<IResponseNegotiator>()
16+
.ToList();
17+
18+
var chosenNegotiator = NegotiationHelper.SelectNegotiator(response.HttpContext, negotiators);
19+
20+
return chosenNegotiator.Handle(response.HttpContext.Request, response, model, cancellationToken);
21+
}
22+
}

test/Carter.Tests/InternalRooms/InternalResponseNegotiator.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ namespace Carter.Tests.InternalRooms;
22

33
using System.Threading;
44
using System.Threading.Tasks;
5+
using Carter.Attributes;
56
using Microsoft.AspNetCore.Http;
67
using Microsoft.Net.Http.Headers;
78

9+
[TestNegotiator]
810
internal class InternalResponseNegotiator: IResponseNegotiator
911
{
1012
public bool CanHandle(MediaTypeHeaderValue accept)

test/Carter.Tests/InternalRooms/NestedInternalResponseNegotiator.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ namespace Carter.Tests.InternalRooms;
22

33
using System.Threading;
44
using System.Threading.Tasks;
5+
using Carter.Attributes;
56
using Microsoft.AspNetCore.Http;
67
using Microsoft.Net.Http.Headers;
78

89
internal static class NestedInternalResponseNegotiatorWrapper
910
{
11+
[TestNegotiator]
1012
internal class NestedInternalResponseNegotiator: IResponseNegotiator
1113
{
1214
public bool CanHandle(MediaTypeHeaderValue accept)

0 commit comments

Comments
 (0)