Create a Custom ProblemDetailsFactory

on Monday, December 16, 2019

An Exception Handler is also needed for this to work. You can read more within the follow-up post, ExceptionHandler Needed.

In .NET Core 2.2/3.0 the ASP.NET Team made a move towards a validation and error reporting standard called Problem Details (RFC 7807). I don’t know much about the history of the standard except for what’s listed on it’s description. It became a proposed standard in March 2016 (which means it was probably being developed for years before that), and it was sponsored by M. Nottingham (W3C Technical Architecture Group), Akamai (they’re pretty famous) and E. Wilde (Siemens).

This standardization also lines up with something David Fowler has been talking about for a little while (1, 2, 3), Distributed Systems Tracing. From an outsiders perspective it really feels like many of the teams at Microsoft are trying their best to get more observability, metrics, and tracing into their tooling. And Microsoft seems to be using the “extensions” described in the Problem Details RFC to add a new property called “traceId”. I think this property will line up with a larger effort by Microsoft to support OpenTelemetery (Microsoft reference) and potential improvements to Application Insights.

So … Microsoft has these great baseline ProblemDetail objects which help standardize the way 500 and 400 errors are returned from Web APIs. But, how can you extend upon their work to add some customizations that are particular to your needs?

Well, when you read the Microsoft Handle errors in ASP.NET Core web APIs documentation, you feel like it must be pretty easy because they say you just need to “Implement ProblemDetailsFactory”. But, that’s all the documentation does. It just “says” you should implement it, there is no example code to work from. The example that is given shows how to replace the default factory with your custom factory (which is a great example, Thank You!), but there’s no example given on what your factory could look like.

This leads to the question of “How does Microsoft do it?”. Well … they use an internal (non-public) DefaultProblemDetailsFactory.

It would be great if DefaultProblemDetailsFactory could be made public.

One of the striking features of that default implementation it never references System.Exception. It’s job is to translate an uncaught exception into a 500 Internal Server Error response object. But, it never uses an exception object in it’s code?

Maybe that’s because it does the translation earlier on the process, like in ProblemDetailsClientErrorFactory. I really don’t know how it all connects. The original developers are some pretty smart people to get it all working.

Anyways … for this example, I’m going to:

  • Use the DefaultProblemDetailsFactory as the starting code to extend.
  • Create a custom class which the Factory will look for in order to alter the returned ProblemDetails object.
  • Use a Feature on the httpContext to pull in Exception information (I don’t know how else to get the exception object?)
  • Use the ProblemDetailsFactoryTest class from Microsoft to help build a unit test.
  • Update the unit test to inject the exception.

Let’s start with the custom class (YourBussException.cs) that will be used by our custom factory to extend the returned data. The class will:

  • Use the underlying Exception’s Message property to fill in the “Detail” property of the ProblemDetail object.
  • Add an ExtendedInfo object where your development teams can add extra information that can help inform API clients on how to resolve the issue.

Next we'll make some small updates to the factory in order to create one that will translate our exception into a 400 Bad Request response (YourBussProblemDetailsFactory.cs):

Alternatively, you can use a Decorator Pattern to wrap the default InvalidModelStateFactory as described in AspNetCore.Docs/issue/12157, How to log automatic 400 responses on model validation errors, option #2 (using PostConfigure<>). The concern I have with this approach is that you are no longer using the Dependency Injection system to create your factory. You are hand creating an instance of the factory and that instance is no longer easily referenceable to any code that wants to interact with it. This also makes the code more brittle to changes and less testable.

Finally, we can use the example code from Microsoft Handle errors in ASP.NET Core web APIs documentation, to swap in our new YourBussProblemDetailsFactory (Startup.cs):

Now, you should be able to throw your exception from anywhere in the code and have it translated back as a 400 error:

Some things to take note of:

  • The ProblemDetails classes were introduced in ASP.NET Core 3.0. So, you’ll have to update your target framework to ‘aspnetcore3.0’ or above to make this work.
  • You’ll also need to add in a FrameworkReference to ‘Microsoft.AspNetCore.App’ as the Microsoft.AspNetCore.Mvc.Infrastructure namespace only exists within it. And you can only get that reference through the FrameworkReference (as opposed to nuget packages). See Migrate from ASP.NET Core 2.2 to 3.0 for an example.
  • The null-coalescing operator (??=) only compiles in C# 8.0. So, if your project, or your referenced projects depend on ‘netstandard2.0’ or ‘netcoreapp2.X’ then you’ll need to update them to get the compiler to work (this took a while to figure out.) (<--That’s right, your referenced projects have to update too; it’s really non-intuitive.)

Finally, let’s take a look at a unit test. I’m going to make this code sample a bit short. To make the full example work, you will need to copy all of these internal classes into your testing code:

This is the code snippet needed just for the test (YourBussProblemDetailsFactoryTests.cs):

An Exception Handler is also needed for this to work. You can read more within the follow-up post, ExceptionHandler Needed.

9 comments:

matterai said...

Thank you! It helped me, actually. I was looking for some solution which helps me to return predefined response in case of any server error.

Anonymous said...

You can decorate the DefaultProblemDetailsFactory. Create your decorator and have it take a ProblemDetailsFactory as a constructor parameter. Delegate both methods to your inner factory and then modify the returned problem details. To register the decorator use `services.Decorate();`. IMPORTANT - this must be called after `services.AddControllers();` as that is where the default factory is registered.

smaglio81 said...

Oh wow! That's a really cool idea, I gotta try that.

Does services.Decorate() come with the framework? Or is there a third party library you would suggest? Or, maybe that's code we should write?

SAM said...

How can I access ProblemDetailsFactory in .NetStandard 2.0 class library. I want to try your code in a class library instead of in webapi project itself.
Thanks.

smaglio81 said...

SAM, I did the same thing. I moved the code to a class library. It looks like it's containing project is targeting `netcoreapp3.0`.

I also have some fuzzy memories of it requiring a netcoreapp target because the underlying Microsoft.AspNetCore.Mvc.Infrastructure.ProblemDetailsFactory class is only available in a library in netcoreapp; it's not available in any nuget package. So, you have to reference a framework.

Anonymous said...

Thanks for this useful article. When I try to throw an exception from controller I don't receive a ProblemDetail reponse, neither did get a breakpoint hit CustomProblemDetailsFactory. Is there a configuration that I am missing to set?

smaglio81 said...

Yeah, I missed a piece using an ExceptionHandler to tie the pieces together. Try also reading the follow-up article https://stevenmaglio.blogspot.com/2020/01/exceptionhandler-needed.html

hB said...

For some of the questions you raised above:
https://github.com/dotnet/AspNetCore.Docs/issues/21767

smaglio81 said...

Oh, that's cool. Thanks hB!

Post a Comment


Creative Commons License
This site uses Alex Gorbatchev's SyntaxHighlighter, and hosted by herdingcode.com's Jon Galloway.