Aspnet Core Url Rewrite and Redirect with Umbraco/Cms and Database Sourced Rules

Aspnet Core Url Rewrite and Redirect with Umbraco/Cms and Database Sourced Rules

Storing ASPNET Core Url Rewrite/Redirect rules in a database

I recently migrated an older Umbraco CMS website to version 9 which is a much cleaner architecture that runs on top of aspnet core. One of the items that I left for the end was the URL rewriting piece which mostly existed for some redirects from over 6 years ago so I didn't prioritize it for the launch. The old Umbraco site had these url rewrite/redirect rules in the webconfig and the new aspnet core middleware can read them from a file however let's be honest, we don't want to manage these in a file we have to check-in and out of source control. Let's keep it simple and do the following:

  • Create a simple table in the database that contains the rules in a varchar(max) column
  • Update the table to have the old ruleset
  • Read the one row on startup and configure the middleware

Of course, we could overcomplicate the hell out of this and add rules to Umbraco and let the user configure them but not a customer requirement at this time therefore let's just get it done and working and iterate over it later if needed.

How do we do this? First, Umbraco ships with an ORM called NPoco so we can leverage that to access the database. Next, we create a table and class to read the information, and last we configure the middleware. The process is very straightforward and simple.

SQL Table:

-- Create table [dbo].[RewriteRules]
--
PRINT (N'Create table [dbo].[RewriteRules]')
GO
CREATE TABLE dbo.RewriteRules (
  Id int IDENTITY,
  Xml varchar(max) NULL,
  CONSTRAINT PK_RewriteRules_Id PRIMARY KEY CLUSTERED (Id)
)
GO

C# Class for NPoco:

[PrimaryKey("Id")]
public class RewriteRules
{
    public int Id { get; set; }
    public string Xml { get; set; }
}

Now for the fun part, we just read from the table in the Startup:

RewriteRules rewrites = null;
try {
  using(var db = new Database(_config.GetConnectionString("umbracoDbDSN")) {
    rewrites = db.SingleById <RewriteRules>(1);  // we only have one row
  }
  if (rewrites != null) {
    using(var m = new StringReader(rewrites.Xml)) {
      var options = new RewriteOptions().AddIISUrlRewrite(m);
      if (!env.IsDevelopment()) {
        options.AddRedirectToHttps();
      }
      app.UseRewriter(options);
    }
  }
} catch (Exception ex) {}

Quick tip. The middleware should probably be configured toward the top prior to the static files as you want this to always run. Also, not all rules are supported like the IsFile and IsDirectory contraints. The URL Rewriting Middleware is provided by the Microsoft.AspNetCore.Rewrite package, which is implicitly included in ASP.NET Core apps.

Here is an example of the ruleset:

<rewrite>
  <rules>
     <rule name="Testing" stopProcessing="true">
          <match url="(.*)"/>
          <conditions logicalGrouping="MatchAny" trackAllCaptures="false">
            <add input="{HTTP_HOST}" pattern="^dev1"/>
            <add input="{HTTP_HOST}" pattern="^local"/>
            <add input="{HTTP_HOST}" pattern="staging"/>
          </conditions>
          <action type="None"/>
        </rule>
          <rule name="Prevent wp-login.php" stopProcessing="true">
          <match url="wp-login.php"/>
          <conditions logicalGrouping="MatchAll" trackAllCaptures="false"/>
          <action type="AbortRequest"/>
        </rule>
         <rule name="Shop Redirect" stopProcessing="true">
          <match url="(.*)"/>
          <conditions logicalGrouping="MatchAll" trackAllCaptures="false">
            <add input="{HTTP_HOST}" pattern="^shop.domain2.com"/>
          </conditions>
          <action type="Redirect" url="https://shop.domain.com"/>
        </rule>
         <rule name="redirect store" stopProcessing="true">
          <match url="store/"/>
          <conditions logicalGrouping="MatchAll" trackAllCaptures="false"/>
          <action type="Redirect" url="https://shop.domain.com"/>
        </rule>
        <rule name="redirect menu" stopProcessing="true">
          <match url="^menu.html"/>
          <conditions logicalGrouping="MatchAll" trackAllCaptures="false"/>
          <action type="Redirect" url="https://www.domain.com/menu"/>
        </rule>
        <rule name="domain.com base" stopProcessing="true">
          <match url="(.*)"/>
          <conditions logicalGrouping="MatchAll" trackAllCaptures="false">
            <add input="{HTTP_HOST}" pattern="^domain\.com$" negate="true"/>
            <add input="{R:0}" pattern="umbraco" negate="true"/>
          </conditions>
          <action type="Redirect" url="https://domain.com/{R:1}"/>
        </rule>
        <rule name="Force SSl" enabled="true" stopProcessing="true">
          <match url="(.*)"/>
          <conditions logicalGrouping="MatchAll" trackAllCaptures="false">
            <add input="{HTTPS}" pattern="^OFF$"/>
          </conditions>
          <action type="Redirect" url="https://{HTTP_HOST}/{R:1}"/>
        </rule>
  </rules>
</rewrite>

In future iterations of this we may want to create some IRule's that read and cache the database rules so that we can do this on the fly, or add them to Umbraco to manage in the backend. These rules change very infrequently so we also have to consider whether that work is necessary (hint, it is not).