Preserving Query String Parameters in ASP.NET Core
I just recently had a need to preserve a query string parameters across all requests to an ASP.NET Core MVC site. Any links needed to have this parameter and redirects needed to preserve this parameter. While every call that generates a link or a URL could be changed to include this additional parameter, that would be very repetative and something that could be easily forgotten in future changes (and I think it would generally make the code uglier). So, I started on a quest to look for a better way of accomplishing this. I came across what I think is a rather neat and elegant solution and learned some interesting stuff along the way that I thought would be worth sharing.
The Naive Approach
To go into a few more details, the naive approach would be to add a route values to every method that generates a link. Most of the methods for creating urls take additional parameters and if they don’t match any of the tokens in the route template, will just be added onto as query string parameters. For example, there are overloads on the BaseController to take routeValues . There are also overloads on the IUrlHelper interface and the LinkGenerator class to take route values and the anchor tag helper can take additional values as well.
While this will work if you want to add one or a few parameters to every link, it doesn’t scale up very well when you have a large number of parameters. It also doesn’t work well when you simply want to include ALL parameters and not specific ones. While the case that I needed only required a single parameter to be preserved, I still didn’t like this due to the effort of maintaining every location that generates a link.
ASP.NET Core Routing
My initial look for a better way began with taking at how ASP.NET Core handles routing in the official documentation. Luckily, there are two pages of documentation provided regarding routing. The first is fundamental routing for ASP.NET Core that discusses routing in a general sense for ASP.NET Core. The second is routing to actions ASP.NET Core MVC .
I focused on the the fundamental routing of ASP.NET Core because I wanted the potential for this to be used in middleware and the LinkGenerator abstract class seemed the way to go.
Attempt One: Extend LinkGenerator
The LinkGenerator exposes four methods all of which accept RouteValueDictionary for route values and two of them accept an HttpContext. My initial thought was to extend the default LinkGenerator and override the two methods that take an HttpContext to copy all of the query string parmeters to the dictionary and then call the base method to maintain all the base functionality. Should be easy.
I wasn’t quite sure what the default LinkGenerator class to inherit from was, so I injected the LinkGenerator class into a controller, and set a breakpoint to get the class name. This ended up being a class creatively named “DefaultLinkGenerator”. I attempted to derive from the class and let the intellisense import the correct namespace, but it was unable to find it. This lead me to digging into the github repository for it to see where it was. The DefaultLinkGenerator class was found in the Microsoft.AspNetCore.Routing namespace, but it had a few issues that prevented this solution from working.
First, the class was internal and I couldn’t reference it to extend it. Second, the class was sealed and I couldn’t extend it even if I could reference it. Phooey, time to find another solution.
Attempt Two: A Custom Route Implementation
I knew I had to interact with the routing system and I needed to add route values. The next look was into how the routes are setup in ASP.NET Core MVC. The project this was originally done for is a ASP.NET Core 2.2 project. Because that version of the framework is now end of life, I’m not going to have a working code sample here, but I’ll explain the process that I got working.
The Route class has a method that generates a virtual path from a VirtualPathContext . The VirtualPathContext has a property for both the current HttpContext and a Values property for the route values to use. Again, my idea was to extend from this class, add the query string parameters from the HttpContext to the Values property, and then call the base functionality.
The class looks something like this:
public class CustomRoute : Route
{
public CustomRoute(IRouter target, string template, IInlineConstraintResolver resolver)
: base(target, template resolver)
{
}
public override VirtualPathData GetVirtualPath(VirtualPathContext context)
{
foreach (var queryValue in context.HttpContext.Request.Query){
context.Values[queryValue.Key] = queryValue.Value;
}
return base.GetVirtualPath(context);
}
}
This could then be installed in Startup.cs with the following code:
app.UseMvc(routes => {
var sp = routes.ServiceProvider;
routes.Routes.Add(new CustomRoute(
routes.DefaultHandler,
"{controller=Home}/{action=Index}/{id?}",
sp.GetRequiredService<IInlineConstraintResolver>()
));
});
Running this code will generate an exception with a message Cannot use 'CustomRoute' with Endpoint Routing. This error led me to this issue in the OData repository about the same error
. The workaround there was to disable endpoint routing when adding MVC to the services in Startup.cs like this:
services.AddMvc(options => {
options.EnableEndpointRouting = true;
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
Doing this got it working as I expected, but I wasn’t convinced this is the right way of doing it. First, a comment by David Fowler says “Subclassing route is no longer a thing we expect frameworks to do moving forward.” I’m not sure what I’m doing would be considered a framework, but this seems to be a pretty good indication that I probably shouldn’t do it this way. Second, I wasn’t sure I wanted to disable endpoint routing.
While this was technically working, I didn’t feel very good about doing it this way, so I kept looking.
Attempt Three: Back To LinkGenerator
Luckily, since the OData folks needed to solve this issue, I thought they might have a way of getting this to work. They have another issue to add endpoint routing that also includes some changesets for using endpoint routing . Digging through that changeset brought me to the solution that I like and which I think is kind of neat.
To generate custom links, they were using a custom LinkGenerator implementation . What is interesting here is that it takes an implementation of a LinkGenerator as a parameter to the constructor and then delegates a lot of functionality to the inner LinkGenerator. This is very similar to my initial idea, but using composition instead of inheritance.
I’m still a bit confused at this point because the DefaultLinkGenerator is still internal, so I’m not sure how it’s getting a reference to an instance of that class injected into it and I’m not aware of any other implementation of LinkGenerator that they are using.
The “magic”, so to speak, is in configuring the services of the application. The specific changes are the last few lines of changes to Startup.cs in the changeset. I’m copying it down below because I think the change is interesting enough to go through in detail here.
// LinkGenerator
var linkGenerator = services.First(s => s.ServiceType == typeof(LinkGenerator) && s.ImplementationType != null); // 1
services.Remove(linkGenerator); // 2
services.Add(new ServiceDescriptor(linkGenerator.ImplementationType, linkGenerator.ImplementationType, ServiceLifetime.Singleton)); // 3
services.AddSingleton<LinkGenerator>(s => // 4
{
return new ODataEndpointLinkGenerator((LinkGenerator)s.GetRequiredService(linkGenerator.ImplementationType));
});
Line one is finding the service descriptor for the LinkGenerator that is already installed in the service collection and line two removes it from the collection. Line three is an interesting part where it is added back into the services, but the service type is added as it’s implementation type instead of the original service type. That was kind of confusing even to me, so I think a better description with the concrete types is in order.
When we register a service with DI, there’s the service type, which is going to be the type that other classes are going to be request be provided. While the service type is frequently an interface, it is the abstract class LinkGenerator in this case. The other type is the implementation type, which is the type that will actually be provided when the service type is requested. This is going to be any class that implements or inherits from the service type. The implementation type is the default link generator mentioned above DefaultLinkGenerator. Basically, the service descriptor we get in line one says that whenever an instance of LinkGenerator needs to be injected, use the actual type of DefaultLinkGenerator.
That descriptor is then removed in line two, so that the LinkGenerator class is not being resolved to any concrete class. If you run the project at this point, you’ll get an exception because it doesn’t know how to generate any links, so we need to manually add that capability back.
Line three adds the DefaultLinkGenerator back into the service collection, but the service type is also the implementation type. This means that we will only get an instance of DefaultLinkGenerator if we explicitly request an instance of DefaultLinkGenerator. At this point, there is still no LinkGenerator services registered with the service collection.
Line four is where we add a new implementation to satisfy the LinkGenerator service. It uses the factory method to create a singleton of the new ODataEndpointLinkGenerator. To get the inner LinkGenerator instance, it uses the service provider to resolve an instance of DefaultLinkGenerator from the service provider.
We can use the same pattern to create our own LinkGenerator that preserves all the query string parameters across requests.
Creating a Custom LinkGenerator
The implementation is pretty similar to the changes that OData folks made. The code can be found in my Query String Preservation repository on Gitlab.
The implementation shows this working for redirection, anchor tag helpers, and using the UrlHelper.
Conclusion
Overall, I thought this was a pretty neat way of accomplishing what I wanted to accomplish. Controlling the behavior of an internal class that I could not reference directly by manipulating the service collection was something that I thought was pretty slick. This is the best way I’ve found of accomplishing this task that I found.
There may be some gaps that I’m not aware of with this implementation. For example, the LinkGenerator class has two methods that don’t take an HttpContext, so any links generated through those methods would not preserve the query string. I don’t know when those methods are called, so I’m not sure if there could be gaps that would lose the query string sometime.