Learn how to build an ASP.NET Core filter to automatically rewrite Link objects as absolute URLs at runtime.
- [Instructor] To generate an absolute URL for an instance of the Link class, we need to access an ASP.NET Core service called IUrlHelper. We can see this in one of the controllers. In the context of a controller, when you type URL to access the Url.Link method, the URL object is an IUrlHelper. It's also possible to use this object elsewhere, but it needs to be given the proper request context in order to work. Let's try a simple class that can use IUrlHelper to create an absolute URL.
In the Infrastructure folder, I'm going to create a class called LinkRewriter. Inside of LinkRewriter, I'm going to inject an IUrlHelper. This is from the AspNetCore MVC namespace. And I'll accept it in the constructor. The LinkRewriter will have one method called Rewrite. The Rewrite method will return a link from the models namespace, and accept a link as well.
We can do a quick sanity check at the beginning to make sure that we're not getting a null link as the original. If so, we'll just return null. If it's not null, we'll return a new Link whose Href is set to an absolute URL given to us by urlHelper.link. You can take the original.RouteName and the original.RouteValues, and pass those to urlHelper.link. We'll also include the original.Method and the original.Relations. So far, so good.
We need to call this Rewrite method right before a response is serialized and sent back to the client. After the controller and service code has created the response model, we can create a result filter for this and add it to the ASP.Net Core pipeline. Remember the JsonException filter we used to catch unhandled exceptions? A result filter is similar, but for normal responses. Result filters run before and after a result is processed and will let us intercept the response right before it's sent to the client. In the Filters folder here, I'll create a new filter called LinkRewritingFilter.
This class will implement the interface IAsyncResultFilter, which is in the Filters namespace. To implement this interface, we'll have to implement one method, which is OnResultExecutionAsync. We'll inject an IUrlHelperFactory in order to help us get an IUrlHelper, and we'll accept this in the constructor. Inside of the OnResultExecutionAsync method, we need to pull the result out of the context.
We'll access context.Result and cast it to ObjectResult. If the result from our controller is not an ObjectResult, or if the status code is not 200, we can skip this entirely, because those responses won't have any links inside of them. We can express this by saying, asObjectResult dot Value is null, or asObjectResult dot StatusCode is not HttpStatusCode, which we can import from system.net, dot okay.
Notice the null operator here. If asObjectResult is null, this entire line will short circuit, and shouldSkip will be true. If shouldSkip is true, we simply want to await the next method, which will allow execution to continue and skip our result filter entirely. We can't use the await keyword unless the method is Async. If we don't skip, we need to use the new LinkRewriter class we wrote in order to rewrite the links on this response. So we can declare a new rewriter.
We'll need to import this namespace because we put it in Infrastructure. And LinkRewriter takes an IUrlHelper. We can get one by taking the urlHelperFactory and calling GeturlHelper. This method takes an action context. Fortunately, the context that we're given in this method, satisfies that requirement. Now that we have a link rewriter, we'll call a new method called RewriteAllLinks. We'll pass the result and the rewriter. After the links have been rewritten, we'll await the next method to let execution continue.
Now we need to implement the RewriteAllLinks method. We'll declare this as a static void method that takes an object, which is the model, and a LinkRewriter called "rewriter." We'll use reflection to inspect the response and find any link objects that we can rewrite. I wrote some helper code that will make this a little bit simpler. I put it in TypeInfoMemberExtensions, which I added to the Begin state of this video in the Exercise Files. I won't take time to explain what this does, but you can check out this code if you're interested.
Inside of RewriteAllLinks, we can do a quick check to make sure the model is not null. This shouldn't be the case, but it's a useful sanity check. If the model is not null, we'll get all the properties on the model with reflection. We'll call GetType, and then GetTypeInfo to access the type info for this model. We'll have to import system.reflection to get access to that method. Then we'll call GetAllProperties, which will get both the properties on the model and any properties that it inherits from a base class.
And as a sanity check, we'll make sure to grab only the properties that we can read. So we'll filter on CanRead. We'll get these all and put them in an array so we can easily refer to them later in this method. Next, we'll pull all the link properties out of allProperties. So we'll say linkProperties equals allProperties where we can write to the property and the PropertyType is type of Link. We'll have to import this from models. Now we'll iterate over all those link properties with a for each.
We'll use our link rewriter on this link. We can say var rewritten is rewriter.Rewrite, and pass in the linkProperty, so we get the link property value from the model, and we'll need to cast it to Link. If it comes back as null, we don't need to do anything more, so we can simply continue and move on in the loop. If it's not null, we can take the new value and set it back on the property. So we can say linkProperty.SetValue, model, and the rewritten link.
Later on, we'll have more complicated resources that contain arrays and nested objects, which themselves can contain links. With just a little bit more code, we can recursively rewrite links and those properties too. After the for each, I can find all the array properties in the object model, by saying allProperties.Where, and where the PropertyType is an array. It'll call a method called RewriteLinksInArrays and pass those arrayProperties, and the model and the rewriter class.
I can do the same for object properties. I'll assume that any property that isn't already a link property or an array property, could be an object property. So we'll take those properties and call RewriteLinksInNestedObjects on those properties. To save some time, I've copied these methods to the clipboard, and I'll paste them here now. You can find this code in the Exercise Files for this video.
The last step is to add the new filter to the MVC pipeline in the startup class. In the ConfigureServices method, in the AddMvc block, we can add this filter by calling Filters.Add, type of, our new LinkRewritingFilter. Now I'm going to set a break point in the link rewriting filter so we can see this work. We'll go ahead and run the project, and test it in Postman. If I hit the root route, we'll fall into this link rewriting filter. The root route is returning an object result, so this should not skip, and then it should use our link rewriter to rewrite all the links in the response.
Alright, we've successfully used a result filter to automatically enrich the response object with these absolute URLs. The filter did miss one link, though. It missed the self-referential link contained in the "Href" property on the resource itself. We'll extend the filter to rewrite this link as well next.
- 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