Skip to content
This repository was archived by the owner on Dec 14, 2018. It is now read-only.

Commit 861d78f

Browse files
authored
Refactor content negotiation code into a service (#6998)
* Refactor content negotiation code into a service This is a refactor in anticipation of using this logic in some other places
1 parent 41efa40 commit 861d78f

21 files changed

+866
-799
lines changed

src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ internal static void AddMvcCoreServices(IServiceCollection services)
250250
services.TryAddSingleton<IHttpResponseStreamWriterFactory, MemoryPoolHttpResponseStreamWriterFactory>();
251251
services.TryAddSingleton(ArrayPool<byte>.Shared);
252252
services.TryAddSingleton(ArrayPool<char>.Shared);
253+
services.TryAddSingleton<OutputFormatterSelector, DefaultOutputFormatterSelector>();
253254
services.TryAddSingleton<IActionResultExecutor<ObjectResult>, ObjectResultExecutor>();
254255
services.TryAddSingleton<IActionResultExecutor<PhysicalFileResult>, PhysicalFileResultExecutor>();
255256
services.TryAddSingleton<IActionResultExecutor<VirtualFileResult>, VirtualFileResultExecutor>();
Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Collections.ObjectModel;
7+
using Microsoft.AspNetCore.Http;
8+
using Microsoft.AspNetCore.Mvc.Core;
9+
using Microsoft.AspNetCore.Mvc.Formatters;
10+
using Microsoft.AspNetCore.Mvc.Formatters.Internal;
11+
using Microsoft.AspNetCore.Mvc.Internal;
12+
using Microsoft.Extensions.Logging;
13+
using Microsoft.Extensions.Options;
14+
using Microsoft.Extensions.Primitives;
15+
using Microsoft.Net.Http.Headers;
16+
17+
namespace Microsoft.AspNetCore.Mvc.Infrastructure
18+
{
19+
public class DefaultOutputFormatterSelector : OutputFormatterSelector
20+
{
21+
private static readonly Comparison<MediaTypeSegmentWithQuality> _sortFunction = (left, right) =>
22+
{
23+
return left.Quality > right.Quality ? -1 : (left.Quality == right.Quality ? 0 : 1);
24+
};
25+
26+
private readonly ILogger _logger;
27+
private readonly IList<IOutputFormatter> _formatters;
28+
private readonly bool _respectBrowserAcceptHeader;
29+
private readonly bool _returnHttpNotAcceptable;
30+
31+
public DefaultOutputFormatterSelector(IOptions<MvcOptions> options, ILoggerFactory loggerFactory)
32+
{
33+
if (options == null)
34+
{
35+
throw new ArgumentNullException(nameof(options));
36+
}
37+
38+
if (loggerFactory == null)
39+
{
40+
throw new ArgumentNullException(nameof(loggerFactory));
41+
}
42+
43+
_logger = loggerFactory.CreateLogger<DefaultOutputFormatterSelector>();
44+
45+
_formatters = new ReadOnlyCollection<IOutputFormatter>(options.Value.OutputFormatters);
46+
_respectBrowserAcceptHeader = options.Value.RespectBrowserAcceptHeader;
47+
_returnHttpNotAcceptable = options.Value.ReturnHttpNotAcceptable;
48+
}
49+
50+
public override IOutputFormatter SelectFormatter(OutputFormatterCanWriteContext context, IList<IOutputFormatter> formatters, MediaTypeCollection contentTypes)
51+
{
52+
if (context == null)
53+
{
54+
throw new ArgumentNullException(nameof(context));
55+
}
56+
57+
if (formatters == null)
58+
{
59+
throw new ArgumentNullException(nameof(formatters));
60+
}
61+
62+
if (contentTypes == null)
63+
{
64+
throw new ArgumentNullException(nameof(contentTypes));
65+
}
66+
67+
ValidateContentTypes(contentTypes);
68+
69+
if (formatters.Count == 0)
70+
{
71+
formatters = _formatters;
72+
if (formatters.Count == 0)
73+
{
74+
throw new InvalidOperationException(Resources.FormatOutputFormattersAreRequired(
75+
typeof(MvcOptions).FullName,
76+
nameof(MvcOptions.OutputFormatters),
77+
typeof(IOutputFormatter).FullName));
78+
}
79+
}
80+
81+
var request = context.HttpContext.Request;
82+
var acceptableMediaTypes = GetAcceptableMediaTypes(request);
83+
var selectFormatterWithoutRegardingAcceptHeader = false;
84+
85+
IOutputFormatter selectedFormatter = null;
86+
if (acceptableMediaTypes.Count == 0)
87+
{
88+
// There is either no Accept header value, or it contained */* and we
89+
// are not currently respecting the 'browser accept header'.
90+
_logger.NoAcceptForNegotiation();
91+
92+
selectFormatterWithoutRegardingAcceptHeader = true;
93+
}
94+
else
95+
{
96+
if (contentTypes.Count == 0)
97+
{
98+
// Use whatever formatter can meet the client's request
99+
selectedFormatter = SelectFormatterUsingSortedAcceptHeaders(
100+
context,
101+
formatters,
102+
acceptableMediaTypes);
103+
}
104+
else
105+
{
106+
// Verify that a content type from the context is compatible with the client's request
107+
selectedFormatter = SelectFormatterUsingSortedAcceptHeadersAndContentTypes(
108+
context,
109+
formatters,
110+
acceptableMediaTypes,
111+
contentTypes);
112+
}
113+
114+
if (selectedFormatter == null && !_returnHttpNotAcceptable)
115+
{
116+
_logger.NoFormatterFromNegotiation(acceptableMediaTypes);
117+
118+
selectFormatterWithoutRegardingAcceptHeader = true;
119+
}
120+
}
121+
122+
if (selectFormatterWithoutRegardingAcceptHeader)
123+
{
124+
if (contentTypes.Count == 0)
125+
{
126+
selectedFormatter = SelectFormatterNotUsingContentType(
127+
context,
128+
formatters);
129+
}
130+
else
131+
{
132+
selectedFormatter = SelectFormatterUsingAnyAcceptableContentType(
133+
context,
134+
formatters,
135+
contentTypes);
136+
}
137+
}
138+
139+
if (selectedFormatter == null)
140+
{
141+
// No formatter supports this.
142+
_logger.NoFormatter(context);
143+
return null;
144+
}
145+
146+
_logger.FormatterSelected(selectedFormatter, context);
147+
return selectedFormatter;
148+
}
149+
150+
private List<MediaTypeSegmentWithQuality> GetAcceptableMediaTypes(HttpRequest request)
151+
{
152+
var result = new List<MediaTypeSegmentWithQuality>();
153+
AcceptHeaderParser.ParseAcceptHeader(request.Headers[HeaderNames.Accept], result);
154+
for (var i = 0; i < result.Count; i++)
155+
{
156+
var mediaType = new MediaType(result[i].MediaType);
157+
if (!_respectBrowserAcceptHeader && mediaType.MatchesAllSubTypes && mediaType.MatchesAllTypes)
158+
{
159+
result.Clear();
160+
return result;
161+
}
162+
}
163+
164+
result.Sort(_sortFunction);
165+
166+
return result;
167+
}
168+
169+
private IOutputFormatter SelectFormatterNotUsingContentType(
170+
OutputFormatterCanWriteContext formatterContext,
171+
IList<IOutputFormatter> formatters)
172+
{
173+
if (formatterContext == null)
174+
{
175+
throw new ArgumentNullException(nameof(formatterContext));
176+
}
177+
178+
if (formatters == null)
179+
{
180+
throw new ArgumentNullException(nameof(formatters));
181+
}
182+
183+
foreach (var formatter in formatters)
184+
{
185+
formatterContext.ContentType = new StringSegment();
186+
formatterContext.ContentTypeIsServerDefined = false;
187+
188+
if (formatter.CanWriteResult(formatterContext))
189+
{
190+
return formatter;
191+
}
192+
}
193+
194+
return null;
195+
}
196+
197+
private IOutputFormatter SelectFormatterUsingSortedAcceptHeaders(
198+
OutputFormatterCanWriteContext formatterContext,
199+
IList<IOutputFormatter> formatters,
200+
IList<MediaTypeSegmentWithQuality> sortedAcceptHeaders)
201+
{
202+
if (formatterContext == null)
203+
{
204+
throw new ArgumentNullException(nameof(formatterContext));
205+
}
206+
207+
if (formatters == null)
208+
{
209+
throw new ArgumentNullException(nameof(formatters));
210+
}
211+
212+
if (sortedAcceptHeaders == null)
213+
{
214+
throw new ArgumentNullException(nameof(sortedAcceptHeaders));
215+
}
216+
217+
for (var i = 0; i < sortedAcceptHeaders.Count; i++)
218+
{
219+
var mediaType = sortedAcceptHeaders[i];
220+
221+
formatterContext.ContentType = mediaType.MediaType;
222+
formatterContext.ContentTypeIsServerDefined = false;
223+
224+
for (var j = 0; j < formatters.Count; j++)
225+
{
226+
var formatter = formatters[j];
227+
if (formatter.CanWriteResult(formatterContext))
228+
{
229+
return formatter;
230+
}
231+
}
232+
}
233+
234+
return null;
235+
}
236+
237+
private IOutputFormatter SelectFormatterUsingAnyAcceptableContentType(
238+
OutputFormatterCanWriteContext formatterContext,
239+
IList<IOutputFormatter> formatters,
240+
MediaTypeCollection acceptableContentTypes)
241+
{
242+
if (formatterContext == null)
243+
{
244+
throw new ArgumentNullException(nameof(formatterContext));
245+
}
246+
247+
if (formatters == null)
248+
{
249+
throw new ArgumentNullException(nameof(formatters));
250+
}
251+
252+
if (acceptableContentTypes == null)
253+
{
254+
throw new ArgumentNullException(nameof(acceptableContentTypes));
255+
}
256+
257+
foreach (var formatter in formatters)
258+
{
259+
foreach (var contentType in acceptableContentTypes)
260+
{
261+
formatterContext.ContentType = new StringSegment(contentType);
262+
formatterContext.ContentTypeIsServerDefined = true;
263+
264+
if (formatter.CanWriteResult(formatterContext))
265+
{
266+
return formatter;
267+
}
268+
}
269+
}
270+
271+
return null;
272+
}
273+
274+
private IOutputFormatter SelectFormatterUsingSortedAcceptHeadersAndContentTypes(
275+
OutputFormatterCanWriteContext formatterContext,
276+
IList<IOutputFormatter> formatters,
277+
IList<MediaTypeSegmentWithQuality> sortedAcceptableContentTypes,
278+
MediaTypeCollection possibleOutputContentTypes)
279+
{
280+
for (var i = 0; i < sortedAcceptableContentTypes.Count; i++)
281+
{
282+
var acceptableContentType = new MediaType(sortedAcceptableContentTypes[i].MediaType);
283+
for (var j = 0; j < possibleOutputContentTypes.Count; j++)
284+
{
285+
var candidateContentType = new MediaType(possibleOutputContentTypes[j]);
286+
if (candidateContentType.IsSubsetOf(acceptableContentType))
287+
{
288+
for (var k = 0; k < formatters.Count; k++)
289+
{
290+
var formatter = formatters[k];
291+
formatterContext.ContentType = new StringSegment(possibleOutputContentTypes[j]);
292+
formatterContext.ContentTypeIsServerDefined = true;
293+
if (formatter.CanWriteResult(formatterContext))
294+
{
295+
return formatter;
296+
}
297+
}
298+
}
299+
}
300+
}
301+
302+
return null;
303+
}
304+
305+
private void ValidateContentTypes(MediaTypeCollection contentTypes)
306+
{
307+
for (var i = 0; i < contentTypes.Count; i++)
308+
{
309+
var contentType = contentTypes[i];
310+
311+
var parsedContentType = new MediaType(contentType);
312+
if (parsedContentType.HasWildcard)
313+
{
314+
var message = Resources.FormatObjectResult_MatchAllContentType(
315+
contentType,
316+
nameof(ObjectResult.ContentTypes));
317+
throw new InvalidOperationException(message);
318+
}
319+
}
320+
}
321+
}
322+
}

0 commit comments

Comments
 (0)