Problem Details + Error Handling + .NET Core 3

Goals for this experiment:

  • HTTP calls that route to /api/... (the API) should return errors in the Problem Details JSON format.
    • We should be able to map specific exceptions to specific HTTP status codes, and control the data returned.
    • When in developer mode, errors include full exception details
    • When not in developer mode, exception details are excluded
  • HTTP calls that do not route to /api/... (*.html, *.css, *.png, etc…) should redirect to an error page
    • When in developer mode, errors are displayed with the “developer exception page”
    • When not in developer mode, errors are displayed as user- friendly pages

The scope of this experiment does not extend beyond the requirements of a SPA project I’m working on. The stack is: Aurelia, .NET Core 3, TypeScript, Entity Framework Core, MS SQL.

There’s a lot of conflicting information and overlap out there. Much has changed over the years, but my final solution seems simple and elegant.

To facilitate this experiment, I decided to use VS Code’s new remote container development feature (learn more here. You should be able to open the repo in VS Code and F5 to run it in a fully containerized development environment. It’s SO rad.

Testing the API

The repo contains a single controller used for testing various scenarios. I used curl to exercise the API and show my work. With each test below, you can see the curl result, and the code that produced that result. Pay particular attention to the input passed to the --data parameter, the HTTP response codes, and the HTTP response headers, and the response. You can copy/paste the curl commands below directly into the VS Code terminal if you’ve pulled the repo and are connected to the remote container.

Testing validation attributes in the API

This test returns the same result when hitting all of the actions in the TestController. MVC automatically turns those errors into problem details. Here, we’re passing null for the name.

curl --location --request GET "http://127.0.0.1:5000/api/A" --header "Content-Type: application/json" --data "{
    \"Name\": null
}" -k -i

HTTP/1.1 307 Temporary Redirect
Date: Fri, 30 Aug 2019 20:21:00 GMT
Server: Kestrel
Content-Length: 0
Location: https://127.0.0.1:5001/api/A

HTTP/2 400 
date: Fri, 30 Aug 2019 20:21:00 GMT
content-type: application/problem+json; charset=utf-8
server: Kestrel
{"title":"One or more validation errors occurred.","status":400,"traceId":"|ff211a43-456dc8484f1a0fb7.","errors":{"Name":["The Name field is required."]}}

This works because the Data.Name property has a [Required] attribute:

    public class Data {
        [Required]
        public string Name { get; set; }
    }

Testing thrown exceptions in the API

TestController.A is the only method that throws an exception.

    [HttpGet("A")]
    public ActionResult A(Data data) {
        throw new System.Exception("testoo");
    }

Here’s the result of calling that action. (I trimmed some of the exception details to make this easier to read):

curl --location --request GET "http://127.0.0.1:5000/api/A" --header "Content-Type: application/json" --data "{
    \"Name\": \"something\"
}" -k -i

HTTP/1.1 307 Temporary Redirect
Date: Fri, 30 Aug 2019 20:18:39 GMT
Server: Kestrel
Content-Length: 0
Location: https://127.0.0.1:5001/api/A

HTTP/2 500 
cache-control: no-cache, no-store, must-revalidate
date: Fri, 30 Aug 2019 20:18:42 GMT
pragma: no-cache
content-type: application/problem+json; charset=utf-8
expires: 0
server: Kestrel
{
	"errors": [{
		"message": "testoo",
		"type": "System.Exception",
		"raw": "System.Exception: testoo\n   at TestController.A(Data data) in /workspaces/netcore3problemdetails/Controllers/TestController.cs:line 13\n   at lambda_method(Closure , Object , Object[] )\n   at Microsoft.Extensions.Internal.ObjectMethodExecutor.Execute(Object target, Object[] parameters)\n   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.SyncActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)\n   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeActionMethodAsync()\n   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State\u0026 next, Scope\u0026 scope, Object\u0026 state, Boolean\u0026 isCompleted)\n   at...",
		"stackFrames": [{
			"filePath": "/workspaces/netcore3problemdetails/Controllers/TestController.cs",
			"fileName": "TestController.cs",
			"function": "TestController.A(Data data)",
			"line": 13,
			"preContextLine": 7,
			"preContextCode": ["[Route(\u0022api\u0022)]", "[ApiController]", "public class TestController : ControllerBase", "{", "    [HttpGet(\u0022A\u0022)]", "    public ActionResult A(Data data) {"],
			"contextCode": ["        throw new System.Exception(\u0022testoo\u0022);"],
			"postContextCode": ["    }", "", "    [HttpGet(\u0022B\u0022)]", "    public ActionResult B(Data data)", "    {", "        ModelState.AddModelError(\u0022\u0022, \u0022for reals\u0022);"]
		}...]
	}],
	"type": "https://httpstatuses.com/500",
	"title": "Internal Server Error",
	"status": 500,
	"detail": "testoo",
	"instance": null
}

Testing unhandled exceptions in the API

TestController.C throws an unhandled exception.

    [HttpGet("C")]
    public ActionResult C(Data data)
    {
        // force a div by zero exception
        var x = 0;
        x = 0 / x;
        return Ok();
    }

And the result…

curl --location --request GET "http://127.0.0.1:5000/api/C" --header "Content-Type: application/json" --data "{
    \"Name\": \"something\"
}" -k -i

HTTP/1.1 307 Temporary Redirect
Date: Fri, 30 Aug 2019 20:19:21 GMT
Server: Kestrel
Content-Length: 0
Location: https://127.0.0.1:5001/api/C

HTTP/2 500 
cache-control: no-cache, no-store, must-revalidate
date: Fri, 30 Aug 2019 20:19:23 GMT
pragma: no-cache
content-type: application/problem+json; charset=utf-8
expires: 0
server: Kestrel
{
	"errors": [{
		"message": "Attempted to divide by zero.",
		"type": "System.DivideByZeroException",
		"raw": "System.DivideByZeroException: Attempted to divide by zero.\n   at TestController.C(Data data) in...",
		"stackFrames": [{
			"filePath": "/workspaces/netcore3problemdetails/Controllers/TestController.cs",
			"fileName": "TestController.cs",
			"function": "TestController.C(Data data)",
			"line": 28,
			"preContextLine": 22,
			"preContextCode": ["", "    [HttpGet(\u0022C\u0022)]", "    public ActionResult C(Data data)", "    {", "        // force a div by zero exception", "        var x = 0;"],
			"contextCode": ["        x = 0 / x;"],
			"postContextCode": ["        return Ok();", "    }", "", "    [HttpGet(\u0022D\u0022)]", "    public ActionResult\u003Cbool\u003E D(Data data)", "    {"]
		}, ...]
	}],
	"type": "https://httpstatuses.com/500",
	"title": "Internal Server Error",
	"status": 500,
	"detail": "Attempted to divide by zero.",
	"instance": null
}

Testing manual model state errors in the API

TestController.B manually adds a model validation error. To return the error as problem details, you just need return this.ValidationProblem();.

    [HttpGet("B")]
    public ActionResult B(Data data)
    {
        ModelState.AddModelError("", "for reals");
        return this.ValidationProblem();
    }

And the result:

curl --location --request GET "http://127.0.0.1:5000/api/B" --header "Content-Type: application/json" --data "{
    \"Name\": \"something\"
}" -k -i

HTTP/1.1 307 Temporary Redirect
Date: Fri, 30 Aug 2019 20:19:43 GMT
Server: Kestrel
Content-Length: 0
Location: https://127.0.0.1:5001/api/B

HTTP/2 400 
date: Fri, 30 Aug 2019 20:19:43 GMT
content-type: application/problem+json; charset=utf-8
server: Kestrel
{
	"title": "One or more validation errors occurred.",
	"status": 400,
	"errors": {
		"": ["for reals"]
	}
}

Another way to return validation results the same result is to throw a ProblemDetailsException

[HttpGet("B2")]
public ActionResult B2(Data data)
{
    ModelState.AddModelError("", "for reals");
    var validation = new ValidationProblemDetails(ModelState);
    throw new ProblemDetailsException(validation);
}
There's one big difference between this result the previous result. `return this.ValidationProblem();` returns HTTP status code 400 (which I believe is correct), and `throw new ProblemDetailsException(validation);` returns HTTP status code 500. 
curl --location --request GET http://127.0.0.1:5000/api/B2 --header Content-Type: application/json --data {
    "Name": "something"
} -k -i
HTTP/1.1 307 Temporary Redirect
Date: Tue, 03 Sep 2019 12:34:59 GMT
Server: Kestrel
Content-Length: 0
Location: https://127.0.0.1:5001/api/B2

HTTP/2 500 
cache-control: no-cache, no-store, must-revalidate
date: Tue, 03 Sep 2019 12:35:00 GMT
pragma: no-cache
content-type: application/problem+json; charset=utf-8
expires: 0
server: Kestrel

{"title":"One or more validation errors occurred.","status":500,"errors":{"":["for reals"]}}

If you’re curious why throwing a ProblemDetailsException doesn’t include the exception details, follow this thread.

Mapping an exception to a specific HTTP status code

Here’s a fun one. I created a custom TeapotException and mapped it to HTTP status code 418. Here’s where we throw the exception:

[HttpGet("F")]
public ActionResult<bool> F(Data data)
{
    throw new TeapotException();
}

And here’s how we map the exception to the 418 status code (I’ll explain more below):

services.AddProblemDetails(options =>
{
    // This will map TeapotException to the 418 I'm a teapot status code
    options.Map<TeapotException>(ex => new ExceptionProblemDetails(ex, StatusCodes.Status418ImATeapot));
});

Here’s the truncated result:

curl --location --request GET http://127.0.0.1:5000/api/F --header Content-Type: application/json --data {
    "Name": "something"
} -k -i
HTTP/1.1 307 Temporary Redirect
Date: Mon, 02 Sep 2019 23:14:04 GMT
Server: Kestrel
Content-Length: 0
Location: https://127.0.0.1:5001/api/F

HTTP/2 418 
cache-control: no-cache, no-store, must-revalidate
date: Mon, 02 Sep 2019 23:14:06 GMT
pragma: no-cache
content-type: application/problem+json; charset=utf-8
expires: 0
server: Kestrel

{"errors":[{"message":"Exception of type \u0027TeapotException\u0027 was thrown.","type":"TeapotException","raw":"TeapotException: Exception of type \u0027TeapotException\u0027 was thrown.\n   at ApiController.F(Data data) in /workspaces/netcore3problemdetails/Controllers/TestController.cs:line 49\n   at lambda_method(Closure , Object , Object[] )\n   at Microsoft.Extensions.Internal.ObjectMethodExecutor.Execute(Object target, Object[] parameters)\n   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.SyncObjectResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)\n   at Micr

Exception details not included in production mode

When the application is not in developer mode, exception information is not included with the problem details JSON. Here’s the same call as last time, except we’re in production mode:

curl --location --request GET http://127.0.0.1:5000/api/F --header Content-Type: application/json --data {
    "Name": "something"
} -k -i
HTTP/1.1 307 Temporary Redirect
Date: Mon, 02 Sep 2019 23:48:20 GMT
Server: Kestrel
Content-Length: 0
Location: https://127.0.0.1:5001/api/F

HTTP/2 418 
cache-control: no-cache, no-store, must-revalidate
date: Mon, 02 Sep 2019 23:48:21 GMT
pragma: no-cache
content-type: application/problem+json; charset=utf-8
expires: 0
server: Kestrel

{"type":"https://httpstatuses.com/418","title":"I\u0027m a teapot","status":418,"detail":null,"instance":null}

What doesn’t work

The following code will not return problem details.

    [HttpGet("E")]
    public ActionResult<bool> E(Data data)
    {
        ModelState.AddModelError("", "for reals");
        return BadRequest(ModelState);
    }

It simply returns the ModelStateDictionary with a HTTP 400 result. Note the lack of a problem details response header.

curl --location --request GET "http://127.0.0.1:5000/api/E" --header "Content-Type: application/json" --data "{
    \"Name\": \"something\"
}" -k -i

HTTP/1.1 307 Temporary Redirect
Date: Fri, 30 Aug 2019 20:21:38 GMT
Server: Kestrel
Content-Length: 0
Location: https://127.0.0.1:5001/api/E

HTTP/2 400 
date: Fri, 30 Aug 2019 20:21:38 GMT
content-type: application/json; charset=utf-8
server: Kestrel
{
    "": [
        "for reals"
    ]
}

I believe that’s a common pitfall for people trying to return validation results as problem details. The fix is to return this.ValidationProblem(); instead of return BadRequest(ModelState);

And yet, if you return BadRequest(); (without passing in ModelState), you get problem details again. In other words, this:

[HttpGet("D")]
public ActionResult<bool> D(Data data)
{
    return BadRequest();
}

…returns:

curl --location --request GET "http://127.0.0.1:5000/api/D" --header "Content-Type: application/json" --data "{
>     \"Name\": \"something\"
> }" -k -i
HTTP/1.1 307 Temporary Redirect
Date: Wed, 04 Sep 2019 16:53:13 GMT
Server: Kestrel
Content-Length: 0
Location: https://127.0.0.1:5001/api/D

HTTP/2 400 
date: Wed, 04 Sep 2019 16:53:13 GMT
content-type: application/problem+json; charset=utf-8
server: Kestrel

{"type":"https://tools.ietf.org/html/rfc7231#section-6.5.1","title":"Bad Request","status":400,"traceId":"|b03ed286-4b7918f3c9895e87."}

If you want to return richer problem details when using return BadRequest(...);, you have to do a bit more work. You must create a class that derives from ProblemDetails, and pass that to BadRequest(...):

public class Deets : ProblemDetails
{
    public string Name { get; set; }
}

[HttpGet("D2")]
public ActionResult<bool> D2(Data data)
{
    return BadRequest(new Deets { Name = "Fred" });
}

And here’s the result:

curl --location --request GET "http://127.0.0.1:5000/api/D2" --header "Content-Type: application/json" --data "{
    \"Name\": \"something\"
}" -k -i
HTTP/1.1 307 Temporary Redirect
Date: Tue, 03 Sep 2019 14:04:08 GMT
Server: Kestrel
Content-Length: 0
Location: https://127.0.0.1:5001/api/D2

HTTP/2 400 
date: Tue, 03 Sep 2019 14:04:10 GMT
content-type: application/problem+json; charset=utf-8
server: Kestrel

{"name":"Fred","type":null,"title":null,"status":400,"detail":null,"instance":null}

That’s true for other built-in controller methods. For example, return NotFound(someIdentifier); also does not return results in problem details format.

I do not understand why Microsoft chose to do it that way, but I’m not the first to notice it. It makes it pretty painful to return problem details everywhere. Plus, the fact that they don’t automatically return problem details when passing data to those functions makes me wonder if I’m doing something wrong.

9/4/2019 12:56:20 PM EST UPDATE: I asked @khellang and he gave a perfect explanation as to why they’re different. Now I’m just curious if that’s by design, or if it’s a bug. :)

9/5/2019 1:12:10 PM EST UPDATE: .NET Core 3 Preview 9 was just released today, and along with it comes this new gem:

Helper methods for returning Problem Details from controllers
Problem Details is a standardized format for returning error information from an HTTP endpoint. We’ve added new Problem and ValidationProblem method overloads to controllers that use optional parameters to simplify returning Problem Detail responses.

I believe ValidationProblem was included with Preview 8, because I already demonstrated it before today. For that matter, return Problem(...); could have been there and I just didn’t see it.

In any case, it makes for a super method for returning problem details:

[HttpGet("G")]
public ActionResult<bool> G()
{
    // if some error condition...
    return Problem(title: "An error occurred while processing your request", statusCode: 400);
}

…returns:

curl --location --request GET "http://127.0.0.1:5000/api/G" -k -i
HTTP/1.1 307 Temporary Redirect
Date: Thu, 05 Sep 2019 17:10:50 GMT
Server: Kestrel
Content-Length: 0
Location: https://127.0.0.1:5001/api/G

HTTP/2 200 
date: Thu, 05 Sep 2019 17:10:50 GMT
content-type: application/problem+json; charset=utf-8
server: Kestrel

{"type":"https://tools.ietf.org/html/rfc7231#section-6.5.1","title":"An error occurred while processing your request","status":400,"traceId":"|7015daf6-4330b4b8851b85a7."}

The only down side I can see to this Problem(...) method is that you can only return a base problem details object. You cannot add additional metadata to the value returned.

I’ve updated the remote container in the repo to use .NET Core Preview 9, so you can safely and easily test this new Problem(...) method.

Testing non-API calls

This is basically the same as testing the API, except we’re calling a few razor actions on the HomeController. In general, I’m trying to solve the problem of “What is the user experience when an error occurs during a request to a page in the browser?”

Requesting the home page

Simple test first

404 shows 404 page

Just point the browser at a page that doesn’t exist:

Exception in developer mode

See this doc for configuring the current environment. For this test, we’re in developer mode. Here’s how we’re throwing an exception:

public IActionResult Boom()
{
    throw new ApplicationException("sweeet");
}

And here’s the result:

Exception in production mode

Here, we’re not in developer mode so the user should see a “friendly” message:

Additional tests?

Did I cover everything you can think of? Can you think of a test or requirement I haven’t met? Please let me know.

Making it work

The most interesting code is in the startup.cs Configure() method. Instead of the usual code that looks like this:

if (Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/home/error");
}

…we replaced that code that branches based on whether or not the request is trying to access the API:

app.UseIfElse(IsUIRequest, UIExceptionMiddleware, NonUIExceptionMiddleware);

Basically, if the request isn’t going to /api/..., use that old code we’re using to seeing, otherwise use this code:

app.UseProblemDetails();

That’s what enables the API to respond differently from the rest of the site. For the record, I got that code from this repo.

Beyond that, it’s just a matter of configuring Hellang.Middleware.ProblemDetails to map exceptions to HTTP status codes as necessary.

Interestingly…

I didn’t need to write a bunch of obscure code in UseExceptionHandler or write exception filters as demonstrated here, here, here, here, and elsewhere. I’m not throwing shade at those posts — I’m genuinely concerned that I’m missing something here. Can someone tell me where I’m going wrong?

I also didn’t have to use services.Configure<ApiBehaviorOptions>(...) as Hellang.Middleware.ProblemDetails, and proper, consistent use of ASP.NET Core. The latter requirement is a bit unfortunate because it’s easy to accidentally write code that doesn’t return problem details.

A C# nullable reference types policy

At work, we’re full steam ahead with .NET Core 3 and, thusly, nullable reference types. And we’ve come to realize that we need some kind of policy to dictate how nullable reference types will affect our code. It’s not just a matter of turning the feature on — you have to make careful decisions about where ? should be used in the codebase.

So, I wrote the first draft of such a policy and sent it to my teammates.

The policy

The most important rule, and why

Any time you know that an external system could pass a null reference object to a function, you should declare that parameter as nullable (?).

In the following example, the compiler will not warn you to check dto for null in ControllerAction because RepositoryMethod accepts a null dto. In other words, RepositoryMethod is telling the compiler it’s okay if it gets a null dto.

public void ControllerAction(Dto? dto) {
    RepositoryMethod(dto);
}

private void RepositoryMethod(Dto? dto) {

}

If RepositoryMethod does not accept a null dto, as in the following example, the compiler will warn when you pass the dto:

public void ControllerAction(Dto? dto) {
    RepositoryMethod(dto); // WARNING! Check for null first!!!
}

private void RepositoryMethod(Dto dto) {

}

Just to clarify a bit more, the compiler complains any time you access a nullable reference type before checking for null, so you’ll get a warning if you do something like this:

public void ControllerAction(Dto? dto) {
    dto.Something(); // WARNING! Check for null first!!!
}

Again, we can’t prevent external systems from passing null to ControllerAction so dto should be nullable. Since repositories are internal systems that are only called from code within our solution, we can rely on the repository to decide if null should be allowed.

That has the potential to push some null checks further down into the stack, but that’s a good thing, I think. We’re allowing null objects to be passed further down the stack where low-level components can decide if null is valid.

That being said, a repository method probably doesn’t have any reason to allow null objects. That leaves it up to the controller to check for null, and there’s no reason to check for null in the repository method (as long as you’re paying attention to the compiler warnings).

Kickback

Of course, I walked away thinking I had just written the epitome of null reference object policies. But not everyone felt the same.

Queue heated developer debate.

His feeling was that high-level methods should check for null regardless of what the compiler says. A high-level method called Create should immediately and always check for null regardless of what the code in the method allows. Logically, I get it. 99.9999% of the time, it doesn’t make sense to pass null anything to a method called Create.

My thought is that it doesn’t matter what the method is called. If a high-level method preemptively checks for null, you end up with conditions like the one below where a branch of code never gets called:

public void Create(Dto? dto) {
	if (dto == null) throw new ArgumentNullException(dto);

	RepositoryStuff(dto);
}

private void RepositoryStuff(Dto? dto) {
	if (dto == null) {
		// This code will never be called
	}
}

He doesn’t think it’s right to allow low-level code to make that determination, even if it says it will by allowing a nullable reference object.

It’s not so much that he doesn’t trust the low-level code to do the null check — although I suspect that’s the case — It’s that, fundamentally, it doesn’t make sense for a Create method to accept null at all.

As sometimes happens, we agreed to disagree.

What say you?

I’m kind of at a loss. I started writing this post before all the debate started, but now I’m finishing it to get some feedback. I’m curious what you think about this situation and what kind of policy you have in place for null reference objects.

Entity Framework Core Navigation Property vs Foreign Key Property — FIGHT!

9/30/2019 4:27:40 PM UPDATE: Apparently, things changed since .NET Core 3 Preview 5 which is when I wrote this post. I’ll update the post as soon as I make sense of all my unit tests that are failing since updating to .NET Core 3 RTM.

Suppose we have an entity A that has a direct, 1:1, relationship with entity B. So…

class A {
    public int Id { get; set; }
    public BId { get; set; }
    public B B { get; set; }
}

class B {
    public int Id { get; set; }
}

What happens if we try to assign two different Bs to an A like this?

var b1 = new B();
var b2 = new B();

var a = new A { 
    BId = b1,
    B = b2
};

context.A.Add(a);
context.SaveChanges();

b1 wins. A.B is set to b1 and A.BId is set to b1.Id.

How did I know that? I wrong a bunch of unit tests to find out. I couldn’t find it documented anywhere and my lack of understanding what seriously hindering my ability to use EF properly.

Here are the rules I came up with:

  1. In general, setting the foreign key property always trumps setting the navigation property. The only exception to the rule is when you set the foreign key property to null and the navigation property to a valid entity when the parent entity is already associated with a child entity. See ForeignKeySmarts11 below.
  2. When editing an entity, you only need to set the foreign key property or the navigation property. When you call SaveChanges, the property you didn’t set will be synchronized with the property that you did set. Setting both properties is fine, and in some cases may be the preference if you need to interrogate the properties before calling SaveChanges.

Given the strange quirk in rule #1, I recommend always setting the foreign key property and only the foreign key property if possible.

At this point, I’ll shut up and just show you the tests. Some details within the test have been removed to make them easier to read.

[Test]
public async Task ForeignKeySmarts1()
{
    var b = await AddB("Something");

    var a = await AddA(new A { Name = "Yum", B = b.Entity });

    Assert.That(a.Entity.B, Is.Not.Null);
    Assert.That(a.Entity.B?.Id, Is.EqualTo(b.Entity.Id));
}


[Test]
public async Task ForeignKeySmarts3()
{
    var b = await AddB("Something");

    var a = await AddA(new A { Name = "Yum", BId = b.Entity.Id });

    Assert.That(a.Entity.B, Is.Not.Null);
    Assert.That(a.Entity.B?.Id, Is.EqualTo(b.Entity.Id));
}

[Test]
public async Task ForeignKeySmarts4()
{
    var b1 = await AddB("Something");

    var a = await AddA(new A { Name = "Yum", BId = b1.Entity.Id });

    var b2 = await AddB("Another");

    a.Entity.B = b2.Entity;
    await _context.SaveChangesAsync();

    Assert.That(a.Entity.B.Id, Is.EqualTo(b2.Entity.Id));
    Assert.That(a.Entity.B.Name, Is.EqualTo(b2.Entity.Name));
}

[Test]
public async Task ForeignKeySmarts5()
{
    var b1 = await AddB("Something");

    var a = await AddA(new A { Name = "Yum", BId = b1.Entity.Id });

    var b2 = await AddB("Another");

    a.Entity.BId = b2.Entity.Id;
    await _context.SaveChangesAsync();

    Assert.That(a.Entity.B?.Id, Is.EqualTo(b2.Entity.Id));
    Assert.That(a.Entity.B?.Name, Is.EqualTo(b2.Entity.Name));
}

[Test]
public async Task ForeignKeySmarts10()
{
    var b1 = await AddB("Something");

    var a = await AddA(new A { Name = "Yum", BId = b1.Entity.Id });

    var b2 = await AddB("Another");

    a.Entity.BId = b2.Entity.Id;
    a.Entity.B = null;

    await _context.SaveChangesAsync();

    Assert.That(a.Entity.B?.Id, Is.EqualTo(b2.Entity.Id));
    Assert.That(a.Entity.B?.Name, Is.EqualTo(b2.Entity.Name));
}

[Test]
public async Task ForeignKeySmarts11()
{
    var b1 = await AddB("Something");

    var a = await AddA(new A { Name = "Yum", BId = b1.Entity.Id });

    var b2 = await AddB("Another");

    a.Entity.BId = null;
    a.Entity.B = b2.Entity;

    await _context.SaveChangesAsync();

    Assert.That(a.Entity.B?.Id, Is.EqualTo(b2.Entity.Id));
    Assert.That(a.Entity.B?.Name, Is.EqualTo(b2.Entity.Name));
}

[Test]
public async Task ForeignKeySmarts6()
{
    var b1 = await AddB("Something");

    var a = await AddA(new A { Name = "Yum", BId = b1.Entity.Id });

    var b2 = await AddB("Another");
    var b3 = await AddB("Dawg");

    a.Entity.BId = b2.Entity.Id;
    a.Entity.B = b3.Entity;

    await _context.SaveChangesAsync();

    Assert.That(a.Entity.B?.Id, Is.EqualTo(b2.Entity.Id));
    Assert.That(a.Entity.B?.Name, Is.EqualTo(b2.Entity.Name));
}

[Test]
public async Task ForeignKeySmarts7()
{
    var b1 = await AddB("Something");

    var a = await AddA(new A { Name = "Yum", BId = b1.Entity.Id });

    var b2 = await AddB("Another");
    var b3 = await AddB("Dawg");

    a.Entity.BId = b3.Entity.Id;
    a.Entity.B = b2.Entity;

    await _context.SaveChangesAsync();

    Assert.That(a.Entity.B?.Id, Is.EqualTo(b3.Entity.Id));
    Assert.That(a.Entity.B?.Name, Is.EqualTo(b3.Entity.Name));
}

[Test]
public async Task ForeignKeySmarts2()
{
    var b1 = await AddB("Something");
    var b2 = await AddB("Another");

    var a = await AddA(new A { Name = "Yum",
        B = b1.Entity,
        BId = b2.Entity.Id
    });

    Assert.That(a.Entity.B, Is.Not.Null);
    Assert.That(a.Entity.B?.Id, Is.GreaterThan(0));
    Assert.That(a.Entity.B?.Id, Is.EqualTo(b2.Entity.Id));
    Assert.That(a.Entity.B?.Name, Is.EqualTo(b2.Entity.Name));
}

[Test]
public async Task ForeignKeySmarts8()
{
    var b1 = await AddB("Something");
    var b2 = await AddB("Another");

    var a = await AddA(new A { Name = "Yum",
        B = b2.Entity,
        BId = b1.Entity.Id
    });

    Assert.That(a.Entity.B, Is.Not.Null);
    Assert.That(a.Entity.B?.Id, Is.GreaterThan(0));
    Assert.That(a.Entity.B?.Id, Is.EqualTo(b1.Entity.Id));
    Assert.That(a.Entity.B?.Name, Is.EqualTo(b1.Entity.Name));
}

[Test]
public async Task ForeignKeySmarts9()
{
    var b2 = await AddB("Another");
    var b1 = await AddB("Something");

    var a = await AddA(new A { Name = "Yum",
        B = b2.Entity,
        BId = b1.Entity.Id
    });

    Assert.That(a.Entity.B, Is.Not.Null);
    Assert.That(a.Entity.B?.Id, Is.GreaterThan(0));
    Assert.That(a.Entity.B?.Id, Is.EqualTo(b1.Entity.Id));
    Assert.That(a.Entity.B?.Name, Is.EqualTo(b1.Entity.Name));
}

/// <param name="intelligencePoint"></param>
private async Task<EntityEntry<A>> AddA(A intelligencePoint)
{
    var a = await _context.As.AddAsync(intelligencePoint);
    await _context.SaveChangesAsync();
    return a;
}

/// <param name="name"></param>
private async Task<EntityEntry<B>> AddB(string name)
{
    var b = await _context.Bs.AddAsync(new B { Name = name });
    await _context.SaveChangesAsync();
    return b;
}

[Test]
public async Task ForeignKeySmarts12()
{
    var b1 = await AddB("Something");
    var b2 = await AddB("Another");

    var a = await AddA(new A { Name = "Yum", BId = b1.Entity.Id, B = b2.Entity});

    Assert.That(a.Entity.B?.Id, Is.EqualTo(b1.Entity.Id));
    Assert.That(a.Entity.B?.Name, Is.EqualTo(b1.Entity.Name));
}

[Test]
public async Task ForeignKeySmarts13()
{
    var b1 = await AddB("Something");
    var b2 = await AddB("Another");

    var a = await AddA(new A { Name = "Yum", BId = b1.Entity.Id});

    a.Entity.B = b2.Entity;
    await _context.SaveChangesAsync();

    Assert.That(a.Entity.B?.Id, Is.EqualTo(b2.Entity.Id));
    Assert.That(a.Entity.B?.Name, Is.EqualTo(b2.Entity.Name));
}

[Test]
public async Task ForeignKeySmarts14()
{
    var b1 = await AddB("Something");

    var a = await AddA(new A { Name = "Yum", BId = b1.Entity.Id});

    a.Entity.B = new B
    {
        Name = "cow"
    };

    await _context.SaveChangesAsync();

    Assert.That(a.Entity.B?.Id, Is.Not.EqualTo(b1.Entity.Id));
    Assert.That(a.Entity.B?.Name, Is.Not.EqualTo(b1.Entity.Name));
}

[Test]
public async Task ForeignKeySmarts15()
{
    var b1 = await AddB("Something");

    var a = await AddA(new A { Name = "Yum", BId = b1.Entity.Id});

    a.Entity.BId = null;

    await _context.SaveChangesAsync();

    Assert.That(a.Entity.B?.Id, Is.Null);
    Assert.That(a.Entity.B?.Name, Is.Null);
}

[Test]
public async Task ForeignKeySmarts16()
{
    var b1 = await AddB("Something");

    var a = await AddA(new A { Name = "Yum", BId = b1.Entity.Id});

    a.Entity.B = null;

    await _context.SaveChangesAsync();

    Assert.That(a.Entity.B?.Id, Is.Null);
    Assert.That(a.Entity.B?.Name, Is.Null);
}

HtmlWebpackPlugin + ASP.NET Core 3

The idea

My goal with this experiment was to set up a Razor page that was generated from HtmlWebpackPlugin in ASP.NET Core 3.

In other words, I want webpack to generate my controller view (.cshtml file) because it has a better understanding of what the client code should look like. At the same time, I wanted the full power of ASP.NET Core in my Razor files.

I spent far too many hours trying to get this to work to not share my solution with the world.

The full code for this experiment is at https://github.com/alexdresko/htmlwebpackplugin-aspnetcore3

webpack config

The webpack.config.js file is simple enough. Note the template, inject, and filename options for HtmlWebpackPlugin. template is relative to webpack.config.js, and filename is relative to the output folder.

const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: { index: './src/index.js' },
    mode: 'development',
    output: {
        filename: '[name].[hash].bundle.js',
        path: path.resolve(__dirname, 'wwwroot/dist'),
        publicPath: '/dist'
    },
    plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
            template: './Views/Home/Index.ejs',
            filename: '../../Views/Home/Index.cshtml',
            inject: false,
            minify: false,
        })
    ]
};

The html-webpack-plugin template

The index.ejs file is pretty simple. Notice how I’m using both Razor functionality (setting the layout at the top of the page), and ejs functionality (definitely see the plugin documentation for more on that)

@using System.IO

@{
Layout = "";
}
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title><%= htmlWebpackPlugin.options.title || 'Webpack App'%></title>

    <% for (var css in htmlWebpackPlugin.files.css) { %>
    <link href="<%= htmlWebpackPlugin.files.css[css] %>" rel="stylesheet">
    <% } %>
</head>

<body>
    <h1>TADA!</h1>
	<p>If you didn't see a popup, it didn't work</p>
	<h2>Expected</h2>
	<ol>
	<% for (var chunk in htmlWebpackPlugin.files.chunks) { %>
	    <li><%= htmlWebpackPlugin.files.chunks[chunk].entry %></li>
    <% } %>
    </ol>

	<h2>Actual</h2>
	<ol>
	    @{
	        foreach (var file in Directory.GetFiles("./wwwroot/dist"))
	        {
	            <li>@file</li>
	        }
	    }
    </ol>

	<% for (var chunk in htmlWebpackPlugin.files.chunks) { %>
    <script src="<%= htmlWebpackPlugin.files.chunks[chunk].entry %>"></script>
    <% } %>

    <% if (htmlWebpackPlugin.options.devServer) { %>
    <script src="<%= htmlWebpackPlugin.options.devServer%>/webpack-dev-server.js"></script>
    <% } %>
</body>

</html>

On the server side

This page has some very important information. Namely:

Runtime compilation is enabled using the Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation package. To enable runtime compilation, apps must:

Install the Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation NuGet package.

Update the project’s Startup.ConfigureServices method to include a call to AddRazorRuntimeCompilation

With that information in hand, I added AddRazorRuntimeCompilation()to startup.cs:

services.AddRazorPages()
                .AddRazorRuntimeCompilation();

Justification

To prove why all of this is good, consider the typical approach for including webpack files into a Razor page:

<script type="text/javascript" asp-src-include="~/dist/app*.chunk.js"
                                  asp-src-exclude="~/dist/app*.async*.chunk.js"></script>

Then remember that webpack is going to put new files into the dist folder every time you build your application. By default, webpack will not clean your dist folder, and we’ve configured webpack to output our files as “[name].[hash].bundle.js”. If you’ve built your application 100 times, the <script> tag above is going to insert your javascript files 100 times!

Granted, you could just install and configure clean-webpack-plugin to clean your dist folder as part of the build, but that’s not the point. The point again is that html-webpack-plugin is smarter than ASP.NET about how the client code should look. There’s a rich ecosystem of plugins and options available for html-webpack-plugin, and many front end frameworks go to great lengths to ensure their compatibility.

It works!

Just for fun, the demo in the repo writes to the page the file(s) that webpack created, as well as the files that are in the dist folder.

Caveats

The only negative side effect I can think of with this approach is that it might cause an extra Razor compilation — Basically, when the page first loads, I think it gets compiled first. But before the page fully loads, the ASP.NET UseWebpackDevMiddleware... code kicks in and runs webpack. Our webpack process, however, rewrites the very Razor file that we were trying to load in the first place. My hunch is that this causes the Razor file to be recompiled before being displayed. I’m not sure if any of that is correct, but it’s worth a consideration. If it’s true, the performance hit is minimal.

Am I crazy?

I hope that’s helpful to someone. Please let me know what you think!

Publishing a Visual Studio extension via command line

Microsoft not so recently added support for publishing Visual Studio extensions to the marketplace via a command line interface called vsixpublisher

It’s no coincidence they tweeted me upon releasing the feature, as I’d been pestering them for a very long time to make it happen.

The walk-through I linked to will get you going, but might not get you all the way there. If you run into trouble trying to get it to work, here’s a reference implementation that might help:

I have an appveyor build that runs automatically, anytime someone pushes code to the master branch. Here’s how I configured the build to version, package, and publish the vsix to the marketplace. 

Last, but not least, the end result: https://marketplace.visualstudio.com/items?itemName=thealexdresko.HomeSeerTemplates2

I hope that helps someone who wants to get started using vsixpublisher. let me know if you have any questions, and I’ll try to help. 

What is ASP.NET Core 2.1 API “Problem Details” (RFC 7807)

When ASP.NET Core 2.1 came out, there’s was a relatively brief announcement about new support for “Problem Details” (RFC 7807)

In this release we added support for RFC 7808 – Problem Details for HTTP APIs as a standardized format for returning machine readable error responses from HTTP APIs.

If you just want a better explanation of problem details, fear not. I read the documentation for you.   …Continue reading “What is ASP.NET Core 2.1 API “Problem Details” (RFC 7807)” [What is ASP.NET Core 2.1 API “Problem Details” (RFC 7807)]

Thoughts: The Stack Overflow 2017 Developer Survey Results

I found it useful this year to take some notes as I skimmed through the always interesting Stack Overflow developer survey results. Here’s what I came out with this year:  …Continue reading “Thoughts: The Stack Overflow 2017 Developer Survey Results” [Thoughts: The Stack Overflow 2017 Developer Survey Results]

Scroll Up