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

Conversation

pranavkm
Copy link
Contributor

  • Introduce ProblemDescriptionAttribute to enhance some 4xx messages and produce better API description.
  • Introduce IErrorDescriptionProvider to modify the shape of error response.

Fixes #6785, Fixes #6786

@pranavkm
Copy link
Contributor Author

Making sure we're on page with the design before I add tests etc

@@ -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" />
Copy link
Member

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

Copy link
Contributor Author

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));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

????

Copy link
Contributor Author

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;
Copy link
Member

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?

Copy link
Contributor Author

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);
Copy link
Member

@rynowak rynowak Sep 11, 2017

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

Copy link
Contributor Author

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
Copy link
Member

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

Copy link
Contributor Author

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()
Copy link
Member

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)))
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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.

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));
Copy link
Member

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?

Copy link
Contributor Author

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
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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)
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Member

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]
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Member

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

* Introduce ProblemDescriptionAttribute to enhance some 4xx messages and produce better API description.
* Introduce IErrorDescriptionProvider to modify the shape of error response.

Fixes #6785, Fixes #6786
@pranavkm pranavkm changed the title [Design] Introduce opinionated API defaults. Introduce opinionated API defaults. Sep 13, 2017
@pranavkm
Copy link
Contributor Author

🆙 📅

{
apiDescription.SupportedResponseTypes.Add(CreateProblemResponse(StatusCodes.Status400BadRequest));

if (parameters.Any(p => p.Name.EndsWith("id", StringComparison.OrdinalIgnoreCase)))
Copy link
Contributor Author

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));
Copy link
Contributor Author

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

Copy link
Member

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,
Copy link
Contributor Author

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.

Copy link
Member

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:
Copy link
Contributor Author

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.

Copy link
Member

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:
Copy link
Contributor Author

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

Copy link
Member

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)
Copy link
Member

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.

Copy link
Contributor Author

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);
Copy link
Member

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)
Copy link
Member

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)
Copy link
Member

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>
Copy link
Member

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?

Copy link
Member

@rynowak rynowak left a 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...

@pranavkm
Copy link
Contributor Author

Expanding on @rynowak's mysterious comment, the plan is to use #6821 and #6822 to provide problem details rather than a single filter. We realized having the single filter was hard to scale. So this PR introduces the api descriptor. We'll retrofit the configuration once we have those pieces.

// 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.


Copy link
Member

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
Copy link
Member

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

@pranavkm pranavkm closed this Sep 15, 2017
@pranavkm pranavkm deleted the prkrishn/ead branch September 15, 2017 00:07
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants