/
ObjectResultExecutor.cs
178 lines (148 loc) · 6.75 KB
/
ObjectResultExecutor.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
using System.Globalization;
using System.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.Infrastructure;
/// <summary>
/// Executes an <see cref="ObjectResult"/> to write to the response.
/// </summary>
public partial class ObjectResultExecutor : IActionResultExecutor<ObjectResult>
{
/// <summary>
/// Creates a new <see cref="ObjectResultExecutor"/>.
/// </summary>
/// <param name="formatterSelector">The <see cref="OutputFormatterSelector"/>.</param>
/// <param name="writerFactory">The <see cref="IHttpResponseStreamWriterFactory"/>.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
/// <param name="mvcOptions">Accessor to <see cref="MvcOptions"/>.</param>
public ObjectResultExecutor(
OutputFormatterSelector formatterSelector,
IHttpResponseStreamWriterFactory writerFactory,
ILoggerFactory loggerFactory,
IOptions<MvcOptions> mvcOptions)
{
ArgumentNullException.ThrowIfNull(formatterSelector);
ArgumentNullException.ThrowIfNull(writerFactory);
ArgumentNullException.ThrowIfNull(loggerFactory);
FormatterSelector = formatterSelector;
WriterFactory = writerFactory.CreateWriter;
Logger = loggerFactory.CreateLogger<ObjectResultExecutor>();
}
/// <summary>
/// Gets the <see cref="ILogger"/>.
/// </summary>
protected ILogger Logger { get; }
/// <summary>
/// Gets the <see cref="OutputFormatterSelector"/>.
/// </summary>
protected OutputFormatterSelector FormatterSelector { get; }
/// <summary>
/// Gets the writer factory delegate.
/// </summary>
protected Func<Stream, Encoding, TextWriter> WriterFactory { get; }
/// <summary>
/// Executes the <see cref="ObjectResult"/>.
/// </summary>
/// <param name="context">The <see cref="ActionContext"/> for the current request.</param>
/// <param name="result">The <see cref="ObjectResult"/>.</param>
/// <returns>
/// A <see cref="Task"/> which will complete once the <see cref="ObjectResult"/> is written to the response.
/// </returns>
public virtual Task ExecuteAsync(ActionContext context, ObjectResult result)
{
ArgumentNullException.ThrowIfNull(context);
ArgumentNullException.ThrowIfNull(result);
InferContentTypes(context, result);
var objectType = result.DeclaredType;
if (objectType == null || objectType == typeof(object))
{
objectType = result.Value?.GetType();
}
var value = result.Value;
return ExecuteAsyncCore(context, result, objectType, value);
}
private Task ExecuteAsyncCore(ActionContext context, ObjectResult result, Type? objectType, object? value)
{
var formatterContext = new OutputFormatterWriteContext(
context.HttpContext,
WriterFactory,
objectType,
value);
var selectedFormatter = FormatterSelector.SelectFormatter(
formatterContext,
(IList<IOutputFormatter>)result.Formatters ?? Array.Empty<IOutputFormatter>(),
result.ContentTypes);
if (selectedFormatter == null)
{
// No formatter supports this.
Log.NoFormatter(Logger, formatterContext, result.ContentTypes);
const int statusCode = StatusCodes.Status406NotAcceptable;
context.HttpContext.Response.StatusCode = statusCode;
if (context.HttpContext.RequestServices.GetService<IProblemDetailsService>() is { } problemDetailsService)
{
return problemDetailsService.TryWriteAsync(new()
{
HttpContext = context.HttpContext,
ProblemDetails = { Status = statusCode }
}).AsTask();
}
return Task.CompletedTask;
}
Log.ObjectResultExecuting(Logger, result, value);
result.OnFormatting(context);
return selectedFormatter.WriteAsync(formatterContext);
}
private static void InferContentTypes(ActionContext context, ObjectResult result)
{
Debug.Assert(result.ContentTypes != null);
// If the user sets the content type both on the ObjectResult (example: by Produces) and Response object,
// then the one set on ObjectResult takes precedence over the Response object
var responseContentType = context.HttpContext.Response.ContentType;
if (result.ContentTypes.Count == 0 && !string.IsNullOrEmpty(responseContentType))
{
result.ContentTypes.Add(responseContentType);
}
if (result.Value is ProblemDetails)
{
result.ContentTypes.Add("application/problem+json");
result.ContentTypes.Add("application/problem+xml");
}
}
// Internal for unit testing
internal static partial class Log
{
// Removed Log.
// new EventId(1, "BufferingAsyncEnumerable")
public static void ObjectResultExecuting(ILogger logger, ObjectResult result, object? value)
{
if (logger.IsEnabled(LogLevel.Information))
{
var objectResultType = result.GetType().Name;
var valueType = value == null ? "null" : value.GetType().FullName;
ObjectResultExecuting(logger, objectResultType, valueType);
}
}
[LoggerMessage(1, LogLevel.Information, "Executing {ObjectResultType}, writing value of type '{Type}'.", EventName = "ObjectResultExecuting", SkipEnabledCheck = true)]
private static partial void ObjectResultExecuting(ILogger logger, string objectResultType, string? type);
public static void NoFormatter(ILogger logger, OutputFormatterCanWriteContext context, MediaTypeCollection contentTypes)
{
if (logger.IsEnabled(LogLevel.Warning))
{
var considered = new List<string?>(contentTypes);
if (context.ContentType.HasValue)
{
considered.Add(Convert.ToString(context.ContentType, CultureInfo.InvariantCulture));
}
NoFormatter(logger, considered);
}
}
[LoggerMessage(2, LogLevel.Warning, "No output formatter was found for content types '{ContentTypes}' to write the response.", EventName = "NoFormatter", SkipEnabledCheck = true)]
private static partial void NoFormatter(ILogger logger, List<string?> contentTypes);
}
}