Migrating web.config rewrite rules


Hey Everyone,

We're migrating from CMS 11 to 12.

We had a handful of rewrite rules in our web.release.config which I'm looking to reimplement. I understand from these MS docs that this should be done as middleware in Startup.cs now as part of the Configure method. We're running in DXP and my understanding is that this is now a linux powered platform and not an Windows one... which would mean no IIS.

So OK I just need to rework my current rules into something like in the example (given by the docs I've linked to above):

using (StreamReader iisUrlRewriteStreamReader =
    var options = new RewriteOptions()
        .AddRedirect("redirect-rule/(.*)", "redirected/$1")
        .AddRewrite(@"^rewrite-rule/(\d+)/(\d+)", "rewritten?var1=$1&var2=$2",
            skipRemainingRules: true)
        .Add(new RedirectImageRequests(".png", "/png-images"))
        .Add(new RedirectImageRequests(".jpg", "/jpg-images"));


I'm a bit confused about the seeming continued use of IIS. Am I misunderstanding the use of IIS or lack thereof when working with CMS 12 on DXP?

Has anyone else migrated this aspect of a project and point me in the right direction?



Jun 08, 2023 15:05

Hi Alex,

You might want to check out this blog post from Minesh on how to implement url rewrites in cms12.

The DXP enviornments use linux for running cms 12 solutions. Check out this post which details the various ways you can host an optimizely solution.


Edited, Jun 08, 2023 15:32
Minesh Shah (Netcel) - Jun 13, 2023 22:25
Cheers Paul for the mention :)

Yes, that's how I migrated mine. The format using .AddIISUrlRewrite(iisUrlRewriteStreamReader) is exactly the same as those in the web.config. The format is just a format for rewrite rules it's not directly tied to anything IIS it's just a regular expression format for rules but they named it AddIISUrlRewrite to make sense logically it's the same format as standard used when we we're in IIS.

We have 70k rewrites in our .NET 6 solution using this method and it's the highest performance.

Be aware if you want the rules to override Optimizely content place the rewrite code before

        app.UseEndpoints(endpoints =>
            endpoints.MapControllerRoute(name: "Default", pattern: "{controller}/{action}/{id?}");

And if you want them to only work when there's no Optimizely content place them after!

Edited, Jun 08, 2023 19:11

Hi Paul,

thank you for that. Minesh's blog post is very helpful -  contains the history aspect that the MS docs didn't have!

Scott, it's very reassuring that this is a path you've walked before. I've come to trust your judgement over the years from all of the contributions you make around here :)

thanks again,


Jun 09, 2023 8:00
Scott Reed - Jun 09, 2023 10:04
Thank you, that's very kind! :-)
Minesh Shah (Netcel) - Jun 13, 2023 22:25
Cheers if theirs anything additional you need let me know :)

I do actually have a follow up question with regards to the LowercaseUrls IRule if anyone can assist...

There are 3 main rules that I need to migrate. The first is http => https. Nice and simple we need to signal to search engines that this is a perm redirect so AddRedirectToHttpsPermanent() does that precisely.

The other two things we need to handle is forcing a trailing slash and lowercase urls (with the exception of some paths, such as /Static - that way contains files and we don't want slashes or lowercasing files names). But we don't want to redirect each of these individually and end up with a chain of redirects... we want to rewrite until the url has had it's slash added and it's been lowercased and THEN we redirect.

We did this in IIS by having a rule which rewrote the request path, for example "/MyExample" to something like "/_MyExample/", which then got handled by the lowercase rewrite rule we had in place, taking "/_MyExample/" to "_myexample/" and then a final  catch all rule to essentially remove the _ and issue a 301 redirect to the browser.

Because the rules in our web.config contains some path / directory specific negating we can't use AddIISUrlRewrite() as those are not compatible - so the Middleware needs to be used.

The RewriteOptions() looks a little something like this:

var rewriteOptions = new RewriteOptions()
                .Add(context =>
                    if (context.HttpContext.Request.Path.StartsWithSegments("/util") ||
                        context.HttpContext.Request.Path.StartsWithSegments("/episerver") ||
                        context.HttpContext.Request.Path.StartsWithSegments("/modules") ||
                        context.HttpContext.Request.Path.StartsWithSegments("/Login") ||
                       //...removed for brievity
                        context.Result = RuleResult.SkipRemainingRules;
                // Redirect to HTTPS => issues a 301 and the browser sends in another request, OK
                // Enforce trailing slash. => rewrites (no request) add _ and / and continues processing
                .AddRewrite("(.*[^/])$", "_$1/", skipRemainingRules: false)
                // Enforce lower case. => lowercases anything, but should only rewrite and continue processing
                .Add(new LowercaseUrlsRule())
                // Catch and Redirect => processing should now end, remove the _ and send a 301 to browser
                .AddRedirect("^(_+)(.*)", "$2", 301);

Where I'm stuck is that I can't seem to work out how to write the ApplyRule method of the IRule so that the path is lowercased and then carries on through the remaining rules. Here's what I've got:

public void ApplyRule(RewriteContext context)
            HttpRequest request = context.HttpContext.Request;
            PathString path = context.HttpContext.Request.Path;
            HostString host = context.HttpContext.Request.Host;

            if (path.HasValue && path.Value.Any(char.IsUpper) || host.HasValue && host.Value.Any(char.IsUpper))
                HttpResponse response = context.HttpContext.Response;
                //response.StatusCode = StatusCode;
                //path = (request.PathBase.Value + request.Path.Value).ToLower() + request.QueryString;
                response.Headers[HeaderNames.Location] = (request.Scheme + "://" + host.Value + request.PathBase.Value + request.Path.Value).ToLower() + request.QueryString;
                //request.Headers[HeaderNames.Location] = (request.Scheme + "://" + host.Value + request.PathBase.Value + request.Path.Value).ToLower() + request.QueryString;
                //context.Result = RuleResult.EndResponse;
            context.Result = RuleResult.ContinueRules;

and with that the url "/MyExample" ends up as "/MyExample/", even though when stepping through the code, the containing uppercase condition is met and the actual lowercasing code is executed, but that operation doesn't update the path and doesn't seem to be passing through to our catch all rule - so the _ gets removed but the url is still uppercase.

I've left the commented out lines in the code above to show that I've tried setting the path directly, setting both the response and request headers.

Am I misunderstanding how these rules are chained together? Am I not setting the correct property with the lowercased result?

Jun 15, 2023 10:23

Hi Alex,

Would you be able to clone a simple alloy site and fire this into it and share with the group?

I Imagine the situtation is occuring due to the chaining of redirects / rewrites / rules. Ideally as we are creating a rule for lowercasing, any logic for lowercasing should be within that rule.

There is an example on the MS Docs that shows how to do this:

public class RedirectImageRequests : IRule
    private readonly string _extension;
    private readonly PathString _newPath;

    public RedirectImageRequests(string extension, string newPath)
        if (string.IsNullOrEmpty(extension))
            throw new ArgumentException(nameof(extension));

        if (!Regex.IsMatch(extension, @"^\.(png|jpg|gif)$"))
            throw new ArgumentException("Invalid extension", nameof(extension));

        if (!Regex.IsMatch(newPath, @"(/[A-Za-z0-9]+)+?"))
            throw new ArgumentException("Invalid path", nameof(newPath));

        _extension = extension;
        _newPath = new PathString(newPath);

    public void ApplyRule(RewriteContext context)
        var request = context.HttpContext.Request;

        // Because we're redirecting back to the same app, stop 
        // processing if the request has already been redirected
        if (request.Path.StartsWithSegments(new PathString(_newPath)) ||
            request.Path.Value == null)

        if (request.Path.Value.EndsWith(_extension, StringComparison.OrdinalIgnoreCase))
            var response = context.HttpContext.Response;
            response.StatusCode = (int) HttpStatusCode.MovedPermanently;
            context.Result = RuleResult.EndResponse;
            response.Headers[HeaderNames.Location] = 
                _newPath + request.Path + request.QueryString;

Hopefully this gives some guidance.



Edited, Jun 15, 2023 13:23

Hi Paul,

Thanks for taking a look at this. It's the chaining as you say. I still don't really understand how the chaining is supposed to work. I assumed it was one synchronus flow per request just like rules in IIS, but it doesn't appear that way using the middleware, or at least not from what I am experiencing.

I totally agree with you that lowercase logic should be contained within the lowercase rule (separation of concerns and all that), however I've only managed to get it to work by combining all of my logic for both adding a trailing slash and lowercasing the url into one rule. 

With that done, I'm seeing just one redirect in the browser regardless of if the trailing slash logic needs to apply, the lowercase logic or both. And no redirect if none of it is needed.

As per the docs the middleware is thought to be less performant than IIS so I'm going to have to keep an eye on this, but for now and with the help of everyone in the thread (thank you all again!) I'm done with this which is my happy thought for this Friday!

Cheers all!

Jun 16, 2023 8:17

Hi Alex,

Glad we can can help.

I think the difference lies somewhere in the use of rewrite v redirect, and how this is processed. Would need to investigate further.

Enjoy your Friday and have a good weekend!


Jun 16, 2023 9:20
* You are NOT allowed to include any hyperlinks in the post because your account hasn't associated to your company. User profile should be updated.