ASP.NET MVC Core: Migrating ActionFilters Into Middleware Components

Transitioning your code to ASP.NET MVC Core can be difficult, but today, I show you how to take an existing ActionFilter and convert it into a Middleware component.

Written by Jonathan "JD" Danylko • Last Updated: • MVC •
An Adapter for A PS/2 Mouse and Keyboard to USB

I know I mentioned how much I love ActionFilters, but with ASP.NET Core, things are a little bit different now.

With the new Middleware in ASP.NET Core, we are injecting functionality into a pipeline. It's a different way to handle requests and responses.

While ActionFilters are specifically meant for MVC, I see Middleware as a site-wide feature similar to Http Modules.

Each Middleware component executes a piece of code and then moves on to the next one. Once each component is done processing the requests, it makes the return trip and executes any remaining code with the responses.

Middleware pipeline

While Middleware can replace HttpModules, but it can also replace ActionFilters.

ActionFilters are like Middleware components. ActionFilters have an OnExecuting (before execution) and an OnExecuted (after execution).

In Middleware, the equivalent to OnExecuting is code before the Next.Invoke() and the equivalent to OnExecuted is code after the Next.Invoke().

So why couldn't we convert ActionFilters into Middleware components?

A Quick Example

As an example of how to convert an ActionFilter into a Middleware component, I wrote a quick ActionFilter called EmoticonFilterAttribute.

The action happens in the response, not the request. So we need to use the OnActionExecuted for processing the HTML. When the HTML is returned, we simply want to enhance it with emoticons (or Emoji for you millennials).

ActionFilter\EmoticonFilterAttribute.cs

public class EmoticonFilterAttribute : ActionFilterAttribute
{
    public override void OnActionExecuted(ActionExecutedContext context)
    {
        var response = context.HttpContext.Response;

        response.Body = new EmoticonStream(response.Body);     } }

ActionFilter\EmoticonStream.cs

public class EmoticonStream : Stream
{
    private readonly Stream _base;
    StringBuilder _s = new StringBuilder();

    private List<KeyValuePair<string, string>> _emoticons = new List<KeyValuePair<string, string>>     {         new KeyValuePair<string, string>(@":-)", "smile.png"),         new KeyValuePair<string, string>(@":)", "smile.png"),         new KeyValuePair<string, string>(@";-)", "wink.png")     };
    public EmoticonStream(Stream responseStream)     {         if (responseStream == null)             throw new ArgumentNullException("responseStream");
        _base = responseStream;     }     public override void Flush()     {         _base.Flush();     }
    public override bool CanRead => _base.CanRead;     public override bool CanSeek => _base.CanSeek;     public override bool CanWrite => _base.CanWrite;     public override long Length => _base.Length;
    public override long Position     {         get { return _base.Position; }         set { _base.Position = value; }     }

    public override void Write(byte[] buffer, int offset, int count)     {         var html = Encoding.UTF8.GetString(buffer, offset, count);         foreach (var emoticon in _emoticons)         {             if (!html.Contains(emoticon.Key)) continue;
            var image = $"<img src=\"/images/{emoticon.Value}\" />";             html = html.Replace(emoticon.Key, image);         }         buffer = Encoding.UTF8.GetBytes(html);
        _base.Write(buffer, 0, buffer.Length);     }
    public override void SetLength(long value)     {         _base.SetLength(value);     }
    public override long Seek(long offset, SeekOrigin origin)     {         return _base.Seek(offset, origin);     }
    public override int Read(byte[] buffer, int offset, int count)     {         return _base.Read(buffer, offset, count);     } }

Streams, Streams, Streams. It's all about the streams. If you want optimized code, streams are always the better option for performance since it ties nicely into the ASP.NET pipeline. 

Notice how we isolated the EmoticonStream into it's own class instead of writing everything into the ActionFilter? Trust me, isolating this into a class makes this extremely easier to port over to a Middleware component.

Even though the Emoticon stream code is lengthy, you only need to focus on the Write method. We loop through our emoticons to see if we have any in our response. If we do, we simply replace the emoticon with an image.

It's a standard ActionFilter that can be applied to an Action or Controller like so.

    [EmoticonFilter]
    public IActionResult Index()
    {
        return View();
    }

For a simple test, I modified the Index.cshtml to look like this.

Screenshot of Before the Emoticon ActionFilter

After testing our ActionFilter, we can see it works.

Screenshot of After the Emoticon ActionFilter

Creating our Middleware component

Since we have a working ActionFilter, we can now focus on converting it into a Middleware component.

Middleware components are simple classes. Ours will contain a simple constructor and the Invoke method.

Middlware\EmoticonMiddleware.cs

public class EmoticonMiddleware
{
    private readonly RequestDelegate _next;

    public EmoticonMiddleware(RequestDelegate next)     {         _next = next;     }
    public async Task Invoke(HttpContext context)     {         using (var buffer = new MemoryStream())         {             // Replace the context response with our buffer             var stream = context.Response.Body;             context.Response.Body = buffer;
            // Invoke the rest of the pipeline              // if there are any other middleware components             await _next.Invoke(context);
            // Reset and read out the contents             buffer.Seek(0, SeekOrigin.Begin);
            // Adjust the response stream to include our images.             var emoticonStream = new EmoticonStream(stream);
            // Reset the stream again             buffer.Seek(0, SeekOrigin.Begin);
            // Copy our content to the original stream and put it back             await buffer.CopyToAsync(emoticonStream);             context.Response.Body = emoticonStream;         }
    } }

A very simple Middleware class.

For every Middleware class, we always need a way to access the RequestDelegate to know which Middleware component we are calling next (based on the _next.Invoke(context) line). This allows us to write code before and after the .Invoke() (similar to the OnActionExecuting and OnActionExecuted I mentioned above).

Also, remember when I mentioned it's better to have a separate class for our EmoticonStream?

The EmoticonStream class can be reused to latch onto the response and modify the returned HTML as you can see near the bottom of the file.

Once we've modified the HTML, we copy the emoticon response into our buffer and give it back to the Middleware pipeline. This returns it back to the client.

One Final Touch

Now that we wrote our Middleware piece, how do we use it?

We add it to our pipeline in our Startup.cs (in bold).

Startup.cs

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();

    if (env.IsDevelopment())     {         app.UseDeveloperExceptionPage();         app.UseBrowserLink();     }     else     {         app.UseExceptionHandler("/Home/Error");     }
    app.UseStaticFiles();
    app.UseMiddleware<EmoticonMiddleware>();
    app.UseMvc(routes =>     {         routes.MapRoute(             name: "default",             template: "{controller=Home}/{action=Index}/{id?}");     }); }

Once you have this in place, you can easily see that nothing has changed in our result.

Screenshot of After the Emoticon Middleware implementation

Conclusion

Today, I showed you how to convert a simple ActionFilter into Middleware components. While this is a simple example, you'll be able to use this to transition your library of ActionFilters into ready-to-use Middleware components.

Did I miss something? Post your comments below.

ASP.NET 8 Best Practices on Amazon

ASP.NET 8 Best Practices by Jonathan Danylko


Reviewed as a "comprehensive guide" and a "roadmap to excellence" with over 120 Best Practices for ASP.NET Core 8, Jonathan's first book by Packt Publishing explores proven techniques for every phase of the SDLC.

Learn industry-standard concepts to improve your coding, debugging, and deployment of ASP.NET Core websites.

Order now on Amazon.com button

Picture of Jonathan "JD" Danylko

Jonathan "JD" Danylko is an author, web architect, and entrepreneur who's been programming for over 30 years. He's developed websites for small, medium, and Fortune 500 companies since 1996.

He currently works at Insight Enterprises as an Architect.

When asked what he likes to do in his spare time, he replies, "I like to write and I like to code. I also like to write about code."

comments powered by Disqus