Skip to content

Add helper methods on ControllerBase to return ProblemDetails #12298

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 5, 2019

Conversation

pranavkm
Copy link
Contributor

@pranavkm pranavkm commented Jul 18, 2019

  • Introduce ControllerBase.Problem and ValidationProblem overload
    that accepts optional parameters
  • Consistently use ClientErrorData when generating ProblemDetails
  • Clean-up InvalidModelStateResponseFactory initialization.

Fixes #8537


Description

Add APIs to return a ProblemDetails instance from a ControllerBase. Fixes bugs where instances of ProblemDetails created in different parts of MVC had different (missing) content.

Customer Impact

This is a new API so customers aren't impacted until they use it.

Regression?

No

Risk

Low.

Status = StatusCodes.Status400BadRequest,
};

if (options.ClientErrorMapping.TryGetValue(400, out var clientErrorData))
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 new bit. ValidationProblemDetails returned by the ModelStateInvalidFilter should now have a "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1" (or whatever Link the user configured).

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 realized I'm missing a test to verify we use the user configured value. I'll add one

Copy link
Member

Choose a reason for hiding this comment

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

oke

options.InvalidModelStateResponseFactory = context =>
{
var result = (BadRequestObjectResult)previous(context);
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 test was really janky. This broke because it expected a vanilla dictionary but now gets a ProblemDetails. We wouldn't recommend users to write something like this.

@pranavkm
Copy link
Contributor Author

/cc @khellang


if (problemDetails.Title is null || problemDetails.Type is null)
{
var options = HttpContext.RequestServices.GetRequiredService<IOptions<ApiBehaviorOptions>>().Value;
Copy link
Member

@rynowak rynowak Jul 18, 2019

Choose a reason for hiding this comment

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

I'm a little anxious about putting a service here without a way to mock it. I don't have a great solution other than doing things like making this a filter.

Most of the other places we interact with services, you can mock them easily. https://github.com/aspnet/AspNetCore/blob/f7e3eaa7e324b99aeb496a68414cd093acaae802/src/Mvc/Mvc.Core/src/ControllerBase.cs#L171

Does it seem reasonable to make a new service ProblemDetailsFactory, and then add that as a property, and then call it from 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.

Sure.

@@ -1849,6 +1898,8 @@ public virtual ActionResult ValidationProblem([ActionResultObjectValue] ModelSta
}

var validationProblem = new ValidationProblemDetails(modelStateDictionary);
ApplyProblemDetailsDefaults(validationProblem, statusCode: 400);
Copy link
Member

Choose a reason for hiding this comment

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

Is this broken today? We don't apply the defaults?

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

string instance = null,
string title = null,
string type = null,
[ActionResultObjectValue] ModelStateDictionary modelStateDictionary = null)
Copy link
Member

Choose a reason for hiding this comment

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

Do we need this as a parameter? is there really a use case for passing it in explicitly?

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 this for BadObjectResult and another ValidationProblem. Doesn't seem like it would hurt.

validationProblem.Title = title;
}

ApplyProblemDetailsDefaults(validationProblem, statusCode: 400);
Copy link
Member

Choose a reason for hiding this comment

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

For the people that really want this to be a 422 - what do they do?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ValidationProblem returns the non-specific ActionResult return type. We expect you to override and copy this block of code if you'd like it to get this behavior. It's a few lines of code to copy.

var controller = new TestableController
{
ControllerContext = context,
};
Copy link
Member

Choose a reason for hiding this comment

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

See I don't feel like this amount of setup is reasonable for someone who wants to unit test a controller.

@Eilon Eilon added the area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates label Jul 18, 2019
options.InvalidModelStateResponseFactory = ProblemDetailsFactory;
var problemDetails = new ValidationProblemDetails(context.ModelState)
{
Status = StatusCodes.Status400BadRequest,
Copy link
Member

Choose a reason for hiding this comment

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

This could be removed, ref. #8688

{
if (_problemDetailsFactory == null)
{
_problemDetailsFactory = HttpContext?.RequestServices?.GetRequiredService<ProblemDetailsFactory>();
Copy link
Member

Choose a reason for hiding this comment

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

This could end up being null which would lead to a NRE below.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's expected to only ever be null in test scenarios. Basically following the same pattern as https://github.com/aspnet/AspNetCore/pull/12298/files#diff-8aa2822b0a420b6f8a04153ca4312519R172

Copy link
Member

Choose a reason for hiding this comment

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

Wouldn't it be better to check if it's null and throw a better exception then? 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@khellang I'd rather keep the code comparable to the other properties for now. I did venture at using nullable types at one point and that forces some cleaning up some of these.

Copy link
Member

Choose a reason for hiding this comment

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

I'm not saying you should change this property, I'm saying you might want to guard against null at the call site. Haven't checked usage of the other properties, but I'd consider those potential bugs as well if not properly guarded against null.

/// <param name="detail">The value for <see cref="ProblemDetails.Detail" />.</param>
/// <param name="instance">The value for <see cref="ProblemDetails.Instance" />.</param>
/// <returns>The <see cref="ValidationProblemDetails"/> instance.</returns>
public abstract ValidationProblemDetails CreateValidationProblemDetails(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Going back to an earlier question about what would you do if you needed to consistently return 422 for ValidationProblem - this would be the API to override this. This is called by InvalidModelStateFilter and ControllerBase.ValidationProblem

pranavkm added 3 commits July 23, 2019 11:03
* Introduce ControllerBase.Problem and ValidationProblem overload
  that accepts optional parameters
* Consistently use ClientErrorData when generating ProblemDetails
* Clean-up InvalidModelStateResponseFactory initialization.

Fixes #8537
@pranavkm pranavkm force-pushed the prkrishn/problem-details-helper branch from 126150c to 8b1f527 Compare July 23, 2019 18:11
@mkArtakMSFT mkArtakMSFT added the tell-mode Indicates a PR which is being merged during tell-mode label Jul 25, 2019
@pranavkm pranavkm added ask-mode This issue / PR is a patch candidate which we will bar-check internally before patching it. and removed tell-mode Indicates a PR which is being merged during tell-mode labels Jul 30, 2019
@pranavkm pranavkm changed the base branch from master to release/3.0 August 4, 2019 02:09
@pranavkm pranavkm merged commit 709b390 into release/3.0 Aug 5, 2019
@ghost ghost deleted the prkrishn/problem-details-helper branch August 5, 2019 18:28
Copy link

@netcore-jroger netcore-jroger left a comment

Choose a reason for hiding this comment

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

useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates ask-mode This issue / PR is a patch candidate which we will bar-check internally before patching it.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants