Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.NET Core Filters - ignore for defined methods

We implement a log information to our database. I will use a Filters (IActionFilter) functionality for it. I wrote the following class:

public class ActionFilter: Attribute, IActionFilter
{
    DateTime start;
    public void OnActionExecuting(ActionExecutingContext context)
    {
        start = DateTime.Now;
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
        DateTime end = DateTime.Now;
        double processTime = end.Subtract(start).TotalMilliseconds;
        ... some log actions
    }
}

Then I have added the following code to the Startup.cs:

services.AddMvc(options => {
            options.Filters.Add(typeof(ActionFilter));

        });

It works fine. I get the breakpoint in ActionFilter for each my method.

But I want to ignore for logging the most part of methods. As I understand, I can do it with my own attribute. I didn't work with own attributes before. Ok, I wrote the following attribute:

public class IgnoreAttribute : Attribute
{
    public IgnoreAttribute()
    { }
}

I added the attribute to method:

[Ignore]
    [HttpGet]
    [Route("api/AppovedTransactionAmountByDays/{daysCount}")]
    public JsonResult GetAppovedTransactionAmountByDays(int daysCount)
    {
        var result = daysCount;

        return new JsonResult(result);
    }

Of course, there simple actions don't work.

How I must change my attribute or my ActionFilter for ignore of method?

Thanks in advance.

like image 981
Natalija P Avatar asked Jan 31 '18 15:01

Natalija P


2 Answers

felix-b's note about naming is a good one.

And I want to make another note. You should not store state in the filter when you register it in this way. Since it is an attribute, it is instantiated only once! So you have a massive race condition there. One option would be to use:

services.AddMvc(o =>
{
    o.Filters.Add(new ServiceFilterAttribute(typeof(LoggingActionFilter)));
});

And register it as transient:

services.AddTransient<LoggingActionFilter>();

Now the attribute is instantiated every time it is needed, so you can safely store state.

Configuring it so that it ignores the action if the marker attribute is present is also possible:

public class LoggingActionFilter : Attribute, IActionFilter
{
    private DateTime start;
    private bool skipLogging = false;

    public void OnActionExecuting(ActionExecutingContext context)
    {
        var descriptor = (ControllerActionDescriptor)context.ActionDescriptor;
        var attributes = descriptor.MethodInfo.CustomAttributes;

        if (attributes.Any(a => a.AttributeType == typeof(SkipLoggingAttribute)))
        {
            skipLogging = true;
            return;
        }

        start = DateTime.Now;
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
        if (skipLogging)
        {
            return;
        }

        DateTime end = DateTime.Now;
        double processTime = end.Subtract(start).TotalMilliseconds;
    }
}

public class SkipLoggingAttribute : Attribute
{
}

Here we get the action descriptor available from the parameter and find if the method in question has the SkipLogging attribute. If it does, skip the logging code.

like image 174
juunas Avatar answered Sep 26 '22 15:09

juunas


First, I would strongly suggest renaming your ActionFilter to something more specific, like LogActionFilterAttribute (pay attention -- the action filter is an attribute). Now instead of applying it globally with options.Filters.Add(...), apply it only to the actions you want to log:

// an action that must be logged -- apply LogActionFilter attribute
[HttpGet]
[Route("api/....")]
[LogActionFilter]
public JsonResult FirstAction(...) 
{
    //...
}

// an action that should not be logged -- don't apply LogActionFilter
[HttpGet]
[Route("api/....")]
public JsonResult SecondAction(...)
{
    //...
}

UPDATED VERSION

If you have an explicit requirement for the "Ignore" attribute, you can implement it as follows.

Leave the global configuration the way you did:

services.AddMvc(options => {
    options.Filters.Add(typeof(LoggingActionFilter));
});

The implementation of LoggingActionFilter should change:

// this filter is applied globally during configuration of web application pipeline
public class LoggingActionFilter : IActionFilter
{
    // we use private class types as keys for HttpContext.Items dictionary
    // this is better than using strings as the keys, because 
    // it avoids accidental collisions with other code that uses HttpContext.Items
    private class StopwatchItemKey { }
    private class SuppressItemKey { }

    public void OnActionExecuting(ActionExecutingContext context)
    {
        // here we save timestamp at the beginning of the request
        // I use Stopwatch because it's handy in this case
        context.HttpContext.Items[typeof(StopwatchItemKey)] = Stopwatch.StartNew();
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
        // check whether SuppressLoggingAttribute was applied to current request
        // we check it here in the end of the request because we don't want to depend
        // on the order in which filters are configured in the pipeline
        if (!context.HttpContext.Items.ContainsKey(typeof(SuppressItemKey)))
        {
            // since SuppressItemKey was not set for the current request, 
            // we can do the logging stuff
            var clock = (Stopwatch) context.HttpContext.Items[typeof(StopwatchItemKey)];
            var elapsedMilliseconds = clock.ElapsedMilliseconds;
            DoMyLoggingStuff(context.HttpContext, elapsedMilliseconds);
        }
    }

    // SuppressLoggingAttribute calls this method to set SuppressItemKey indicator 
    // on the current request. In this way SuppressItemKey remains totally private
    // inside LoggingActionFilter, and no one else can use it against our intention
    public static void Suppress(HttpContext context)
    {
        context.Items[typeof(SuppressItemKey)] = null;
    }
}

The "Ignore" attribute (I named it SuppressLoggingAttribute) will look like this:

// this filter attribute is selectively applied to controllers or actions 
// in order to suppress LoggingActionFilter from logging the request
public class SuppressLoggingAttribute : Attribute, IActionFilter
{
    public void OnActionExecuting(ActionExecutingContext context)
    {
        // this will put "suppress" indicator on HttpContext of the current request
        LoggingActionFilter.Suppress(context.HttpContext);
    }
    public void OnActionExecuted(ActionExecutedContext context)
    {
    }
}

Now you only need to apply the "ignore" attribute wherever necessary:

[HttpGet("{id}")]
[SuppressLogging]
public string Get(int id)
{
    return "value";
}

Performance considerations

In contrast to @junnas' answer, my code doesn't use Reflection (MethodInfo.CustomAttributes), and thus it works faster.

If anyone questions the use of Stopwatch: yes, Stopwatch.StartNew() allocates a new Stopwatch object on the heap every request. But assigning DateTime to HttpContext.Items dictionary does the same because it implies boxing. Both DateTime and Stopwatch objects are of 64-bit size, so allocation-wise, both DateTime and Stopwatch options are equal.

like image 34
felix-b Avatar answered Sep 26 '22 15:09

felix-b