Loading...

Offline route matching

I many situations, I would like to use the route matching of AspNetCore for validating a url. Specially in a background task. For example, within the application I run a scheduled task that will check an Imap mailbox, and validates the bounces of a previous send mailing. In most cases, the undelivered message ID in the bounce mail, contains my ID. But in a particular situation, with a Domino email server in this case, the undelivered message ID is the ID of the reply email (should I post a bug report somewhere). I found that the included original email still contains the headers, including my listUnsubscribe header. This url contains a key, which is an encrypted object, containing a reference ID from my mailing.

The problem then, is that I have a full url. I could just hard code something to extract the ID, but I rather match it with the routeData. This has been on my wishlist for quite some time. The issue is running in a background task. Today I did find the solution.

The problem is, there is no valid HttpContext in the background solution. I then realized I had the simular situation with rendering Razor views in the background. The solution is the ActionContext. We need a IServiceProvider, but we don't have one, because of running in a background thread. We can create a new one, but creating a new IServiceScope. This requires the IServiceScopeFactory, which isn't available. Therefor, in the startup, the instance of the IServiceScopeFactory is stored in a static property (do this in the Configure method). This is the only static that is required for this proces:

public class ServiceProviderHelper
{
    public static IServiceScopeFactory ServiceScopeFactory { get; set; }

    public static IServiceScope CreateNewScope()
    {
        return ServiceProviderHelper
            .ServiceScopeFactory
            .CreateScope();
    }
}

With this helper, we can create a new instance of the IServiceScope, which creates an instance of the IServiceProvider. For now I'm using an extension on Uri, but you could put it in a separate class:

public static RouteValueDictionary MatchRouting(this Uri uri)
{
    if (uri == null)
        throw new NullReferenceException();

    using (var serviceScope = ServiceProviderHelper.CreateNewScope())
    {
        return uri.MatchRouting(serviceScope.ServiceProvider);
    }
}

From this serviceProvider, the ActionContext can be fetched, which has a valid RouteData property. In routeData, a routeCollection can be found, which contains the default Route object. In this case, I use the default route name to fetch it. Mine is named after a constant in my startup baseclass (CmsStartupBase.DefaultRouteName). This route then contains a ParsedTemplate which can be used with the TemplateMatcher. The matcher has a TryMatch method, which uses the path & query, to fill a routeValues dictionary. And then we have found what we are looking for:

public static RouteValueDictionary MatchRouting(this Uri uri, IServiceProvider serviceProvider)
{
    var actionContext = serviceProvider.GetService<ActionContext>();
    var routeData = actionContext.RouteData;
    var routeCollection = routeData.Routers.OfType<RouteCollection>().FirstOrDefault() ??
                          throw new Exception("RouteCollection not available");

    Route route = null;
    for (int i = 0; i < routeCollection.Count; i++)
    {
        var tmpRoute = routeCollection[i] as Route;
        if (tmpRoute == null || tmpRoute.Name != CmsStartupBase.DefaultRouteName)
            continue;

        route = tmpRoute;
        break;
    }

    if (route == null)
        throw new Exception("Default rout not found");

    var template = route.ParsedTemplate;
    var matcher = new TemplateMatcher(template, route.Defaults);
    var routeValues = new RouteValueDictionary();

    if (!matcher.TryMatch(uri.PathAndQuery, routeValues))
        throw new Exception("Could not identity controller and action");

    // routeValues
    return routeValues;
}

This method matched my url with the default route, which leeds to the action Unsubscribe (public IActionResult Unsubscribe(string id)), and the routeValues contain the key "id", with my key in it! So, decrypting the key would provide me with the necessary ID's to validate the email message.

I found the required information in the following thread: How to Get Route Data from URL #350. The solutions works for me on AspNetCore 2.2, with 2.1 compatibility. Switching to the new 2.2 compatibility might not give the same result. The solution is added to WillowMedia.Cms 1.8.155.