Learn how to calculate a hash and opt-in to returning the ETag header for specific resources.
- [Instructor] ASP.NET Core doesn't handle the etag header out of the box. However we can add etag support with just a few classes. Let's add an attribute that we can use to annotate methods similar to the response cache attribute. I'm going to add this new attribute in infrastructure and call it EtagAttribute. This attribute will derive from Attribute and also implement IFilterFactory in the Mvc.Filters namespace. Implementing IFilterFactory will let us tie this attribute to an ASP.NET Core filter, I'll explain why in a moment.
When we implement this interface we'll need to provide an implementation for the CreateInstance method. What we'll do here is create a new instance of EtagHeaderFilter, which exists in the Filters namespace. This filter is a new class that I added to the project and you can find it in the Exercise Files. If we take a look at what this filter does, it waits for the request to be processed and then inspects the response. If the result is an object and the object implements the Ietaggable interface, it delegates to the object to generate an etag and then returns it back to the client.
We also have to implement the IsReusable property. This will be true because we can reuse this EtagHeaderFilter across multiple requests. I'm going to add some attribute usage to this class and say that it targets methods, and can't be used more than once. In order to make this work in your own project you'll need to copy the new etag classes I added in the Infrastructure and Filters folders into your project. Or you can just keep it simple and open this solution in the Exercise Files. Let's return an etag from the Info route.
Over in the Info controller I'm going to add the new Etag attribute we created from the Infrastructure namespace. This controller returns a type hotelInfo, we need to go make sure the hotelInfo type implements Ietaggable, so we'll add IEtaggable here and import the namespace. Implementing this interface requires that we implement one method, GetEtag. Here we need to return some string representation of a hash or a fingerprint that uniquely identifies this version of the resource.
One way to do this is to serialize the entire object to JSON and then hash that resulting string. So we could say serialized = JsonConvert, and import JSON.NET, SerializeObject(this); and then to create a hash, I've added a new class called Md5Hash, you can return Md5Hash.ForString(serialized). You can see the implementation of Md5Hash in the Exercise Files. This is a pretty quick and dirty way of getting a hashed representation of the model.
You could also use binary serialization or manually construct some hashed representation of the state of the model. Putting the implementation behind an interface gives you the flexibility to handle this in whatever way makes sense for your objects. All right, let's test this API with Postman and see what we get back. Let's hit the Info route. If we look at the headers, we now have an EtagHeader with a hashed representation of the state of the resource. That's only half of the equation, though, we also need to look for an incoming If-None-Match header and compare the values.
Let's go back to the InfoController. I've added a few more classes to the Infrastructure folder to handle the If-None-Match scenario. If you're curious how it works you can see the implementation over in the Exercise Files. What we need to do is say if (Request.GetEtagHandler() .NoneMatch, and then pass in the object. This will check the If-None-Match header to see if it matches the current Etag of the object. If there is a match, we want to return status code 304 for not modified, and we'll also pass the object back as well so the filter will calculate the etag and include that on the response.
Since we want to return 304 if there is a match, we need to negate this expression, so this is really If-Not-None match. In other words, if there is a match, return 304, not modified. Okay, let's see what we get now. If we make another request to the Info route we'll see the etag there on the response. Let's copy that and add a header to the outgoing request. We want to add the If-None-Match header with the etag value, what we should get now is 304 Not Modified with an empty body.
And there we go. If we change the etag so it no longer matches, we'll get the full response back with a 200 ok. Since etags provide a stronger validation mechanism than the last modified header, I won't cover adding support for that header to the API. If you want to check for the last modified header in your project, it should be possible to support by copying the etag implementation and making a few changes.
- REST vs. RPC
- Using HTTP methods (aka verbs)
- Returning JSON
- Creating a new API project
- Building a root controller
- Routing to controllers with templates
- Requiring HTTPS for security
- Creating resources and data models
- Returning data and resources from a controller
- Representing links (HREFs)
- Representing collections
- Sorting and searching collections
- Creating forms
- Caching and compression
- Authentication and authorization for RESTful APIs