Use Attributes & Middleware in ASP.NET Core for Logging, Telemetry, and Anything Else

ASP.NET Core Attributes and Middleware

Every once in a while you need to add meta functionality without actually changing the business logic code. This might be reporting telemetry, logging, or adding metrics. While necessary, writing this code along with the business logic feels kind of wrong. There's no separation of concerns, it makes the business logic harder to read, and it's prone to bugs.

If you're using ASP.NET Core, you can use attributes and the middleware system to add this kind of logic. This makes the code looks great and separates concerns. But it's not a trivial problem. How do you pass data to the middleware for the telemetry or logging? What about dynamic data that you have during runtime? How to get values from the HTTP request? You're going to see how to do all that and more in this article.

Let's say that you want to add telemetry to your ASP.NET controllers. In other words, you want to report statistics of user actions. Suppose the SignUp and SignIn are two actions you're interested in. One way to go about it is to call a method from within the action, like this:

This is fine, I won't judge you, but wouldn't it be better to have code like this?

This way you separate the business logic concerns from telemetry concerns. And the code is more readable.

If you're using ASP.NET Core, this is pretty simple using a custom middleware.

Simple Custom Middleware

First, let's create our attribute:

Then, we need a middleware class, like this:

There are some creative choices to be made here. In the above code, the action in the controller will be executed first and the telemetry functionality after. You might want it the other way around. Or maybe in parallel. Then, what do you want to do if the action in the controller failed with an exception? Should we still execute the telemetry? Do nothing? Or report a telemetry event that indicates failure?

Anyway, the next step is to register the middleware into ASP.NET Core's pipeline. That's done in Startup.cs in Configure like this:

That's about it, the middleware should now work.

Using Request Data in the Middleware

Let's complicate the problem a little. Suppose that in SignUp you want to add the user's email to the telemetry. That's a reasonable request, right? So how exactly can you pass this kind of dynamic information through an attribute and a middleware? It's a bit tricky, but there are a few ways to do this. One way is to get the attribute object and change it in the business logic code.

In the Controller:

In the Attribute:

In the middleware:

I know what you're thinking—this isn't pretty. You have both the attribute and lines of code inside the business logic. Might as well remove the attribute and call the telemetry directly. And I'll be the first to agree with you. Except that maybe you have 20 such attributes in code already, and you want to add just one telemetry event that needs additional data.

You can use the above method, I won't judge you, but there are a couple of possibly better ways to do this.

Capturing data from the request body in the middleware

If you really want to separate the telemetry/logging functionality from the business logic, there's a way to get that Email field in the middleware itself. You're going to need to read and parse the request body in the middleware. Here's how to do it:

The above code reads the body directly from the HTTP request, deserializes it from JSON, and reads the email. The thing is, that in order to do that, you need to change the standard behavior of ASP.NET. By default, the request body stream will be disposed as soon as it's read. In our case, it's being read twice, so we have to call the EnableBuffering method and then rewind the stream to position 0.

This code potentially hurts performance. We're reading and deserializing the body's JSON twice, which is a waste. But there's also an impact on memory pressure. When you keep the request body in memory for a longer time, there's a higher chance it will be promoted to a higher garbage collection generation. This means more Gen 1 and Gen 2 collections, which means more execution time taken by the GC, and worse performance. The rule of thumb in healthy memory management is to have objects collected as fast as possible. Collections from higher generations are more expensive. Read more on that it in my article 8 Techniques to Avoid GC Pressure and Improve Performance in C# .NET.

Using dynamic properties from the request parameters

The above method works well enough (performance issues aside), but it's not very generic. For different telemetry actions, we might want different fields. For sign up it might be Email, for sign-in it might be FullName, and for BuyItem it might be ItemId. Here's a pretty neat way to turn your middleware into a generic mechanism.

In the controller (note the queryParam addition):

In the attribute:

In the middleware:

I really love this approach. This way, you don't have to have a giant switch/case in the middleware for each type of event. The middleware code never changes, and the only thing needed is to change the value of queryParam in the attribute.

Of course, this relies on having the items as query parameters and not in the request body. But you can implement the same kind of logic to the body as well using something like JSONPath (similar to XPath for JSON). I'll leave that part to you.

Summary

We saw how to use ASP.NET Core's middleware system to separate telemetry or logging code from the business logic code. One of the reasons why I love ASP.NET Core is that it's versatile that way. Although I admit that getting the body from the request was a bit of a pain. Still, the ability to easily create a mechanism that allows adding telemetry just by adding attributes is pretty cool. Let me know what you think in the comments. Cheers.

Share:

Enjoy the blog? I would love you to subscribe! Performance Optimizations in C#: 10 Best Practices (exclusive article)

Want to become an expert problem solver? Check out a chapter from my book Practical Debugging for .NET Developers

1 thought on “Use Attributes & Middleware in ASP.NET Core for Logging, Telemetry, and Anything Else”

  1. Hi Michael!
    While I appreciate the thought behind this approach but it’s very cumbersome to add these attributes on top of every method signature or at least Controller Actions. Can you please tell me the benefit that this approach provides in-comparison to writing these logs via OnActionExecuted\OnActionExecuting filter events?

    If you are looking for logging call chains then that still can be achieved via AOP without adding these attributes.

    Thanks,
    Mandeep

Comments are closed.