Skip to content

Commit 138aabb

Browse files
authored
Introducing IProblemDetailsService.TryWriteAsync (#46680)
* Introducing IProblemDetailsService.TryWriteAsync * Change everywhere to use TryWriteAsync * API review updates * Update PublicAPI.Unshipped.txt * Update PublicAPI.Unshipped.txt
1 parent e48fb24 commit 138aabb

File tree

9 files changed

+101
-75
lines changed

9 files changed

+101
-75
lines changed

src/Http/Http.Abstractions/src/ProblemDetails/IProblemDetailsService.cs

+20-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Http;
1010
public interface IProblemDetailsService
1111
{
1212
/// <summary>
13-
/// Try to write a <see cref="Mvc.ProblemDetails"/> response to the current context,
13+
/// Write a <see cref="Mvc.ProblemDetails"/> response to the current context,
1414
/// using the registered <see cref="IProblemDetailsWriter"/> services.
1515
/// </summary>
1616
/// <param name="context">The <see cref="ProblemDetailsContext"/> associated with the current request/response.</param>
@@ -21,5 +21,24 @@ public interface IProblemDetailsService
2121
/// <item><description>All <see cref="IProblemDetailsWriter"/> were executed and none of them was able to write the response successfully.</description></item>
2222
/// </list>
2323
/// </remarks>
24+
/// <exception cref="InvalidOperationException">If no <see cref="IProblemDetailsWriter"/> can write to the given context.</exception>
2425
ValueTask WriteAsync(ProblemDetailsContext context);
26+
27+
/// <summary>
28+
/// Try to write a <see cref="Mvc.ProblemDetails"/> response to the current context,
29+
/// using the registered <see cref="IProblemDetailsWriter"/> services.
30+
/// </summary>
31+
/// <param name="context">The <see cref="ProblemDetailsContext"/> associated with the current request/response.</param>
32+
/// <remarks>The <see cref="IProblemDetailsWriter"/> registered services
33+
/// are processed in sequence and the processing is completed when:
34+
/// <list type="bullet">
35+
/// <item><description>One of them reports that the response was written successfully, or.</description></item>
36+
/// <item><description>All <see cref="IProblemDetailsWriter"/> were executed and none of them was able to write the response successfully.</description></item>
37+
/// </list>
38+
/// </remarks>
39+
async ValueTask<bool> TryWriteAsync(ProblemDetailsContext context)
40+
{
41+
await WriteAsync(context);
42+
return context.HttpContext.Response.HasStarted;
43+
}
2544
}

src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt

+1
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ Microsoft.AspNetCore.Http.DefaultEndpointFilterInvocationContext.DefaultEndpoint
55
Microsoft.AspNetCore.Http.HttpResults.EmptyHttpResult
66
Microsoft.AspNetCore.Http.HttpResults.EmptyHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task!
77
Microsoft.AspNetCore.Http.HttpValidationProblemDetails.Errors.set -> void
8+
Microsoft.AspNetCore.Http.IProblemDetailsService.TryWriteAsync(Microsoft.AspNetCore.Http.ProblemDetailsContext! context) -> System.Threading.Tasks.ValueTask<bool>
89
Microsoft.AspNetCore.Mvc.ProblemDetails.Extensions.set -> void
910
static Microsoft.AspNetCore.Http.HttpResults.EmptyHttpResult.Instance.get -> Microsoft.AspNetCore.Http.HttpResults.EmptyHttpResult!

src/Http/Http.Extensions/src/ProblemDetailsService.cs

+17-28
Original file line numberDiff line numberDiff line change
@@ -15,44 +15,33 @@ public ProblemDetailsService(
1515
_writers = writers.ToArray();
1616
}
1717

18-
public ValueTask WriteAsync(ProblemDetailsContext context)
18+
public async ValueTask WriteAsync(ProblemDetailsContext context)
1919
{
20-
ArgumentNullException.ThrowIfNull(context);
21-
ArgumentNullException.ThrowIfNull(context.ProblemDetails);
22-
ArgumentNullException.ThrowIfNull(context.HttpContext);
23-
24-
if (context.HttpContext.Response.HasStarted ||
25-
context.HttpContext.Response.StatusCode < 400 ||
26-
_writers.Length == 0)
20+
if (!await TryWriteAsync(context))
2721
{
28-
return ValueTask.CompletedTask;
22+
throw new InvalidOperationException("Unable to find a registered `IProblemDetailsWriter` that can write to the given context.");
2923
}
24+
}
3025

31-
IProblemDetailsWriter? selectedWriter = null;
32-
33-
if (_writers.Length == 1)
34-
{
35-
selectedWriter = _writers[0];
36-
37-
return selectedWriter.CanWrite(context) ?
38-
selectedWriter.WriteAsync(context) :
39-
ValueTask.CompletedTask;
40-
}
26+
public async ValueTask<bool> TryWriteAsync(ProblemDetailsContext context)
27+
{
28+
ArgumentNullException.ThrowIfNull(context);
29+
ArgumentNullException.ThrowIfNull(context.ProblemDetails);
30+
ArgumentNullException.ThrowIfNull(context.HttpContext);
4131

32+
// Try to write using all registered writers
33+
// sequentially and stop at the first one that
34+
// `canWrite`.
4235
for (var i = 0; i < _writers.Length; i++)
4336
{
44-
if (_writers[i].CanWrite(context))
37+
var selectedWriter = _writers[i];
38+
if (selectedWriter.CanWrite(context))
4539
{
46-
selectedWriter = _writers[i];
47-
break;
40+
await selectedWriter.WriteAsync(context);
41+
return true;
4842
}
4943
}
5044

51-
if (selectedWriter != null)
52-
{
53-
return selectedWriter.WriteAsync(context);
54-
}
55-
56-
return ValueTask.CompletedTask;
45+
return false;
5746
}
5847
}

src/Http/Http.Extensions/test/ProblemDetailsServiceTest.cs

+52-18
Original file line numberDiff line numberDiff line change
@@ -34,25 +34,49 @@ public async Task WriteAsync_Skip_NextWriters_WhenResponseAlreadyStarted()
3434
}
3535

3636
[Fact]
37-
public async Task WriteAsync_Skip_WhenNoWriterRegistered()
37+
public async Task TryWriteAsync_ReturnsTrue_WhenAtLeastOneWriterCanWrite()
3838
{
3939
// Arrange
40-
var service = CreateService();
40+
var service = CreateService(
41+
writers: new List<IProblemDetailsWriter>
42+
{
43+
new MetadataBasedWriter("FirstWriter", canWrite: false),
44+
new MetadataBasedWriter("SecondWriter"),
45+
new MetadataBasedWriter("FirstWriter"),
46+
});
47+
48+
var metadata = new EndpointMetadataCollection(new SampleMetadata() { ContentType = "application/problem+json" });
4149
var stream = new MemoryStream();
4250
var context = new DefaultHttpContext()
4351
{
4452
Response = { Body = stream, StatusCode = StatusCodes.Status400BadRequest },
4553
};
4654

4755
// Act
48-
await service.WriteAsync(new() { HttpContext = context });
56+
var result = await service.TryWriteAsync(new() { HttpContext = context, AdditionalMetadata = metadata });
4957

5058
// Assert
51-
Assert.Equal(string.Empty, Encoding.UTF8.GetString(stream.ToArray()));
59+
Assert.True(result);
60+
Assert.Equal("\"SecondWriter\"", Encoding.UTF8.GetString(stream.ToArray()));
61+
}
62+
63+
[Fact]
64+
public async Task WriteAsync_Throws_WhenNoWriterRegistered()
65+
{
66+
// Arrange
67+
var service = CreateService();
68+
var stream = new MemoryStream();
69+
var context = new DefaultHttpContext()
70+
{
71+
Response = { Body = stream, StatusCode = StatusCodes.Status400BadRequest },
72+
};
73+
74+
// Act
75+
await Assert.ThrowsAsync<InvalidOperationException>(async () => await service.WriteAsync(new() { HttpContext = context }));
5276
}
5377

5478
[Fact]
55-
public async Task WriteAsync_Skip_WhenNoWriterCanWrite()
79+
public async Task WriteAsync_Throws_WhenNoWriterCanWrite()
5680
{
5781
// Arrange
5882
var service = CreateService(
@@ -63,36 +87,46 @@ public async Task WriteAsync_Skip_WhenNoWriterCanWrite()
6387
Response = { Body = stream, StatusCode = StatusCodes.Status400BadRequest },
6488
};
6589

90+
// Act & Assert
91+
await Assert.ThrowsAsync<InvalidOperationException>(async () => await service.WriteAsync(new() { HttpContext = context }));
92+
}
93+
94+
[Fact]
95+
public async Task TryWriteAsync_ReturnsFalse_WhenNoWriterRegistered()
96+
{
97+
// Arrange
98+
var service = CreateService();
99+
var stream = new MemoryStream();
100+
var context = new DefaultHttpContext()
101+
{
102+
Response = { Body = stream, StatusCode = StatusCodes.Status400BadRequest },
103+
};
104+
66105
// Act
67-
await service.WriteAsync(new() { HttpContext = context });
106+
var result = await service.TryWriteAsync(new() { HttpContext = context });
68107

69108
// Assert
109+
Assert.False(result);
70110
Assert.Equal(string.Empty, Encoding.UTF8.GetString(stream.ToArray()));
71111
}
72112

73-
[Theory]
74-
[InlineData(StatusCodes.Status100Continue)]
75-
[InlineData(StatusCodes.Status200OK)]
76-
[InlineData(StatusCodes.Status300MultipleChoices)]
77-
[InlineData(399)]
78-
public async Task WriteAsync_Skip_WhenSuccessStatusCode(int statusCode)
113+
[Fact]
114+
public async Task TryWriteAsync_ReturnsFalse_WhenNoWriterCanWrite()
79115
{
80116
// Arrange
81117
var service = CreateService(
82118
writers: new List<IProblemDetailsWriter> { new MetadataBasedWriter() });
83119
var stream = new MemoryStream();
84120
var context = new DefaultHttpContext()
85121
{
86-
Response = { Body = stream, StatusCode = statusCode },
122+
Response = { Body = stream, StatusCode = StatusCodes.Status400BadRequest },
87123
};
88-
var metadata = new EndpointMetadataCollection(new SampleMetadata() { ContentType = "application/problem+json" });
89-
context.SetEndpoint(new Endpoint(context => Task.CompletedTask, metadata, null));
90124

91-
// Act
92-
await service.WriteAsync(new() { HttpContext = context });
125+
// Act & Assert
126+
var result = await service.TryWriteAsync(new() { HttpContext = context });
93127

94128
// Assert
95-
Assert.Equal(string.Empty, Encoding.UTF8.GetString(stream.ToArray()));
129+
Assert.False(result);
96130
}
97131

98132
private static ProblemDetailsService CreateService(

src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddlewareImpl.cs

+2-13
Original file line numberDiff line numberDiff line change
@@ -184,19 +184,8 @@ private async Task DisplayExceptionContent(ErrorContext errorContext)
184184
{
185185
var httpContext = errorContext.HttpContext;
186186

187-
if (_problemDetailsService != null)
188-
{
189-
var problemDetails = CreateProblemDetails(errorContext, httpContext);
190-
191-
await _problemDetailsService.WriteAsync(new()
192-
{
193-
HttpContext = httpContext,
194-
ProblemDetails = problemDetails
195-
});
196-
}
197-
198-
// If the response has not started, assume the problem details was not written.
199-
if (!httpContext.Response.HasStarted)
187+
if (_problemDetailsService == null ||
188+
!await _problemDetailsService.TryWriteAsync(new() { HttpContext = httpContext, ProblemDetails = CreateProblemDetails(errorContext, httpContext) }))
200189
{
201190
httpContext.Response.ContentType = "text/plain; charset=utf-8";
202191

src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddlewareImpl.cs

+3-2
Original file line numberDiff line numberDiff line change
@@ -154,13 +154,14 @@ private async Task HandleException(HttpContext context, ExceptionDispatchInfo ed
154154
context.Response.StatusCode = DefaultStatusCode;
155155
context.Response.OnStarting(_clearCacheHeadersDelegate, context.Response);
156156

157+
var problemDetailsWritten = false;
157158
if (_options.ExceptionHandler != null)
158159
{
159160
await _options.ExceptionHandler!(context);
160161
}
161162
else
162163
{
163-
await _problemDetailsService!.WriteAsync(new ()
164+
problemDetailsWritten = await _problemDetailsService!.TryWriteAsync(new()
164165
{
165166
HttpContext = context,
166167
AdditionalMetadata = exceptionHandlerFeature.Endpoint?.Metadata,
@@ -169,7 +170,7 @@ private async Task HandleException(HttpContext context, ExceptionDispatchInfo ed
169170
}
170171

171172
// If the response has already started, assume exception handler was successful.
172-
if (context.Response.HasStarted || context.Response.StatusCode != StatusCodes.Status404NotFound || _options.AllowStatusCode404Response)
173+
if (context.Response.HasStarted || problemDetailsWritten || context.Response.StatusCode != StatusCodes.Status404NotFound || _options.AllowStatusCode404Response)
173174
{
174175
const string eventName = "Microsoft.AspNetCore.Diagnostics.HandledException";
175176
if (_diagnosticListener.IsEnabled() && _diagnosticListener.IsEnabled(eventName))

src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesOptions.cs

+4-11
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,12 @@ public StatusCodePagesOptions()
2323
HandleAsync = async context =>
2424
{
2525
var statusCode = context.HttpContext.Response.StatusCode;
26+
var problemDetailsService = context.HttpContext.RequestServices.GetService<IProblemDetailsService>();
2627

27-
if (context.HttpContext.RequestServices.GetService<IProblemDetailsService>() is { } problemDetailsService)
28-
{
29-
await problemDetailsService.WriteAsync(new ()
30-
{
31-
HttpContext = context.HttpContext,
32-
ProblemDetails = { Status = statusCode }
33-
});
34-
}
35-
36-
// TODO: Render with a pre-compiled html razor view.
37-
if (!context.HttpContext.Response.HasStarted)
28+
if (problemDetailsService == null ||
29+
!await problemDetailsService.TryWriteAsync(new() { HttpContext = context.HttpContext, ProblemDetails = { Status = statusCode } }))
3830
{
31+
// TODO: Render with a pre-compiled html razor view
3932
var body = BuildResponseBody(statusCode);
4033

4134
context.HttpContext.Response.ContentType = "text/plain";

src/Mvc/Mvc.Core/src/Formatters/TextOutputFormatter.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ public override Task WriteAsync(OutputFormatterWriteContext context)
132132

133133
if (context.HttpContext.RequestServices.GetService<IProblemDetailsService>() is { } problemDetailsService)
134134
{
135-
return problemDetailsService.WriteAsync(new ()
135+
return problemDetailsService.TryWriteAsync(new ()
136136
{
137137
HttpContext = context.HttpContext,
138138
ProblemDetails = { Status = statusCode }

src/Mvc/Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ private Task ExecuteAsyncCore(ActionContext context, ObjectResult result, Type?
103103

104104
if (context.HttpContext.RequestServices.GetService<IProblemDetailsService>() is { } problemDetailsService)
105105
{
106-
return problemDetailsService.WriteAsync(new()
106+
return problemDetailsService.TryWriteAsync(new()
107107
{
108108
HttpContext = context.HttpContext,
109109
ProblemDetails = { Status = statusCode }

0 commit comments

Comments
 (0)