-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Introduce opinionated API defaults. #6807
Conversation
Making sure we're on page with the design before I add tests etc |
samples/MvcSandbox/MvcSandbox.csproj
Outdated
@@ -15,6 +15,8 @@ | |||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" /> | |||
<PackageReference Include="Microsoft.Extensions.Logging.Console" /> | |||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" /> | |||
|
|||
<PackageReference Include="Swashbuckle.AspNetCore" Version="1.0.0" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
planning to check this in? that needs @Eilon to approve
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope. That's just to get an idea of what the API description looks like. I'll undo the sample changes before this goes in.
} | ||
} | ||
|
||
apiDescription.SupportedResponseTypes.Add(CreateProblemResponse(statusCode: 0)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
????
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was for returning a "default" error response.
@@ -11,6 +11,7 @@ | |||
using Microsoft.AspNetCore.Mvc.ApplicationModels; | |||
using Microsoft.AspNetCore.Mvc.ApplicationParts; | |||
using Microsoft.AspNetCore.Mvc.Controllers; | |||
using Microsoft.AspNetCore.Mvc.ErrorDescription; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we not have this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure. .Infrastructure
namespace? It doesn't seem top-level worthy.
var effectiveProblemDescriptionAttribute = (actionProblemDescriptionAttribute ?? controllerProblemDescriptionAttribute); | ||
if (effectiveProblemDescriptionAttribute != null) | ||
{ | ||
actionModel.Properties.Add(ProblemDescriptionAttributeKey, effectiveProblemDescriptionAttribute); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can make this attribute an IFilterMetadata
and then use that from the action descriptor. This will do the same thing with less complexity
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ProblemDescriptionAttribute
is already a filter, I guess I could use that instead.
|
||
namespace Microsoft.AspNetCore.Mvc.Internal | ||
{ | ||
public class ProblemDescriptionFilter : IResultFilter |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be an action filter. Result filters aren't called in some cases
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, missed updating it.
}; | ||
} | ||
|
||
bool BindsIdParameter() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't need this check. You already told us its a 404
{ | ||
apiDescription.SupportedResponseTypes.Add(CreateProblemResponse(StatusCodes.Status400BadRequest)); | ||
|
||
if (parameters.Any(p => string.Equals("id", p.Name, StringComparison.OrdinalIgnoreCase))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would we want to also look for things of the form blahId
, i.e.., EndsWith("Id", StringComparison.Ordinal)
? I personally always tend to name things customerId
, productId
, etc.
Also this is kind of English-language centric. How much precidence is there for giving special meaning to id
elsewhere? I seem to recall that EF has some conventions around this. But if I'm wrong then maybe the id
property name/suffix should be configurable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea on the EF bit. I'll take a gander at their code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, I think we can do ahead and do EndsWith("Id"...)
right now.
I'm not worried about extensibility for this class right now, we need to do it but we need to get to a working E2E so we can start on the other parts of this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 for EndsWith("Id"...)
. Extensibility, possibly in the form of an action parameter attribute, to indicate "this parameter can trigger a 404" would be useful.
|
||
if (parameters.Any(p => string.Equals("id", p.Name, StringComparison.OrdinalIgnoreCase))) | ||
{ | ||
apiDescription.SupportedResponseTypes.Add(CreateProblemResponse(StatusCodes.Status404NotFound)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't it also depend on the HTTP method? If I send a PUT request with an id
, I'm not sure what 404 would mean in that case. Does our ApiDescriptionProviderContext
have some way to say that certain ApiResponseType
values only apply to certain HTTP methods?
Also, what would be the design for how to say your action does not ever return a 404?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does our ApiDescriptionProviderContext have some way to say that certain ApiResponseType values only apply to certain HTTP methods?
Unfortunately not (and AFAIK neither does Swagger 2.0).
Also, what would be the design for how to say your action does not ever return a 404?
I intentionally left out any way to configure behavior at this point. We can do this in a follow up iteration. But I agree, ideally you want to be able to describe these in some way that affects both the Api description and the filter.
|
||
namespace Microsoft.AspNetCore.Mvc.ErrorDescription | ||
{ | ||
public class DefaultErrorDescriptorFactory : IErrorDescriptionFactory |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have two levels of extensibility here: both 'factory' and within the factory, some 'providers'. Could you clarify why the two-tier system is necessary? I'm missing why it's not enough to have a single collection of providers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have the pattern elsewhere in Mvc. Generally, you have a default provider and the user may append additional providers to run after to customize behavior. Doing so you let's you start from a default configured instance. The factory is meant to be a driver for the providers and we don't expect users to replace it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The factory is what our code calls. The providers are the extensibility
Title = Resources.ProblemDescription_400_Title, | ||
}); | ||
|
||
context.Result = new BadRequestObjectResult(problem) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't we risk losing valuable data here? If the incoming context.Result
is a subclass of StatusCodeResult
that contains extra information that you'd want to surface to the client, then is it possible that this code is discarding that information here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup. The check needs to be more stringent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, these probably need to be exact matches. We should be be looking for NotFoundResult
also
@@ -14,5 +14,9 @@ public IActionResult Index() | |||
{ | |||
return View(); | |||
} | |||
|
|||
[HttpGet("")] | |||
[ProblemDescription] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The spec (https://tools.ietf.org/html/rfc7807) uses the term "problem details" rather than "problem description". Might a different name like [ReturnsProblemDetails]
be more descriptive?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rynowak thoughts? We would change the type name too (it's currently named ProblemDescription
) if we were to change this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good point. We might want to rename that class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The attribute that's here is just an example.
My plan is for us to introduce an [ApiController]
attribute, and mostly have the 'problem filter' hidden from users
aada4da
to
58ed4c1
Compare
58ed4c1
to
1c531a6
Compare
1c531a6
to
25aefe0
Compare
🆙 📅 |
{ | ||
apiDescription.SupportedResponseTypes.Add(CreateProblemResponse(StatusCodes.Status400BadRequest)); | ||
|
||
if (parameters.Any(p => p.Name.EndsWith("id", StringComparison.OrdinalIgnoreCase))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
EF attempts to look for Id
and {ModelName}Id
. We wouldn't be able to do the latter, but I think it would be reasonable to look for the pattern {something}Id
(exact case)
} | ||
} | ||
|
||
apiDescription.SupportedResponseTypes.Add(CreateProblemResponse(statusCode: 0)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll file a bug for this. We need a way to represent default response types
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you please add a comment and explain what this is?
return new ProblemDetails | ||
{ | ||
Status = notFoundResult.StatusCode, | ||
Title = Resources.ProblemDetails_404_Title, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could look for the value of the id argument and include it as part of the detail.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is fine for now
case BadRequestResult badRequestResult: | ||
problemDetails = GetBadRequestProblemDetails(badRequestResult); | ||
break; | ||
case UnsupportedMediaTypeResult unsupportedMediaTypeResult: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the only other StatusCodeResult
subtype that's built in to the system. Seemed low cost to add it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure. Might want to comment that we don't really expect user code to return this
case StatusCodeResult result when isExactlyStatusCodeResult && result.StatusCode == StatusCodes.Status400BadRequest: | ||
problemDetails = GetBadRequestProblemDetails(result); | ||
break; | ||
case StatusCodeResult result when isExactlyStatusCodeResult && result.StatusCode == StatusCodes.Status404NotFound: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
overtly complicated code ftw. But I do get 💯 points
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is ugly, but I think it gets the job done. I want to go with something like this for now to unblock us, even if it's super inelegant
@@ -1503,7 +1503,7 @@ public virtual BadRequestObjectResult BadRequest(ModelStateDictionary modelState | |||
/// </summary> | |||
/// <returns>The created <see cref="BadRequestObjectResult"/> for the response.</returns> | |||
[NonAction] | |||
public virtual BadRequestObjectResult ValidationProblem(ValidationProblemDescription descriptor) | |||
public virtual BadRequestObjectResult ValidationProblem(ValidationProblemDetails descriptor) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we return ActionResult
here? You can override this but you can't change the result type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure.
|
||
void OnProvidersExecuting(ErrorDescriptionContext context); | ||
|
||
void OnProvidersExecuted(ErrorDescriptionContext context); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we simplify this down to one method? I don't see much value in spreading this project more
_providers = providers.OrderBy(p => p.Order).ToArray(); | ||
} | ||
|
||
public object CreateErrorDescription(ActionDescriptor actionDescriptor, object result) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
validate arguments
} | ||
|
||
var errorDetails = _factory.CreateErrorDescription(context.ActionDescriptor, problemDetails); | ||
context.Result = new BadRequestObjectResult(errorDetails) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be plain old object result
</data> | ||
<data name="ProblemDetails_415_Title" xml:space="preserve"> | ||
<value>The request entity is in a format that is not supported by the requested resource.</value> | ||
</data> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do these match the expectations from the spec about titles?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking a bit better. I have a crazy idea about the result executors that I want to prototype. I think the filter might have a few fatal flaws...
df39b2a
to
9782b08
Compare
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
`Extra line
/// Adds an <see cref="IFilterMetadata"/> that indicates to the framework that the current action conforms to well-known API behavior. | ||
/// </summary> | ||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)] | ||
public class ProblemDetailsAttribute : Attribute, IFilterMetadata |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is OK for now, I'm going to remove this when I merge API controller
Fixes #6785, Fixes #6786