1

I want to create a global action filter to audit all requests to my API. I want to use a globally registered ActionFilter to ensure all API Controller Actions are audited. I want to inject a scoped instance of AuditService, however, I get a System.ObjectDisposedException when calling _auditService.Create. What is the correct way to inject a scoped service into an ActionFilter so that it doesn't get disposed of before OnActionExecuted is called? The service is also disposed of before the OnActionExecuting event.

Startup code:

public void ConfigureServices(IServiceCollection services)
{
    // ..
    services.AddControllers(c => c.Filters.Add<AuditFilter>());
    services.AddDbContext<MyDbContext>(c => c.UseSqlServer("ConnectionString"));
    services.AddScoped<IAuditService, AuditService>();
    // ..
}

Action Filter:

public class AuditFilter : IActionFilter
{
    private readonly IAuditService _auditService;

    public AuditFilter(IAuditService auditService)
    {
        _auditService = auditService;
    }

    public void OnActionExecuting(ActionExecutingContext context)
    {
        
    }

    public async void OnActionExecuted(ActionExecutedContext context)
    {
        string username = ClaimsPrincipal.Current?.Identity?.Name;
        string remoteAddr = $"{context.HttpContext.Connection.RemoteIpAddress}:{context.HttpContext.Connection.RemotePort}";
        string queryString = context.HttpContext.Request.QueryString.HasValue ? context.HttpContext.Request.QueryString.Value : null;
        using StreamReader reader = new StreamReader(context.HttpContext.Request.Body);
        string body = await reader.ReadToEndAsync();
        body = body.Length > 0 ? body : null;
        // System.ObjectDisposedException: 'Cannot access a disposed context instance
        await _auditService.Create(username, remoteAddr, context.HttpContext.Request.Method,
            context.HttpContext.Request.Path, queryString, body, DateTime.Now);
    }
}

Audit Service:

public class AuditService : IAuditService
{
    private DRSContext Context { get; }

    public AuditService(DRSContext context)
    {
        Context = context;
    }

    public async Task<bool> Create(string username, string remoteAddr, string httpMethod, string path, string query,
        string body, DateTime timestamp)
    {
        await Context.AuditLogs.AddAsync(new AuditLog
        {
            Username = username,
            RemoteAddress = remoteAddr,
            HttpMethod = httpMethod,
            Path = path,
            Query = query,
            Body = body,
            Timestamp = timestamp
        });
        return await Context.SaveChangesAsync() > 0;
    }

    // ..
}
9
  • Most likely the injected IAuditService implementation is using the scoped DbContext when it gets disposed after the request. Use the HttpContext.RequestServices to create a local scope and perform your function. And there is also the async void being used. Commented Mar 28, 2021 at 23:07
  • firstly you should use IResourceFilter to ensure that all resource accesses are audited (page handlers will not invoke IActionFilter). Secondly I think this may not show the whole picture, the problem could be somewhere else, or something wrong inside the implementation of your IAuditService. Generally speaking, this could be caused by wrong caching, wrong way of running background tasks, explicitly disposing the DbContext ... you need to scan through all your project to find it. Commented Mar 29, 2021 at 0:41
  • @KingKing I have added my AuditService class to question. I will look into IResourceFilter. Commented Mar 29, 2021 at 0:45
  • your Dispose implementation may be the issue here, somehow that's called before the DbContext is being used again. You don't need to explicitly dispose the DbContext like that. Because it will be managed by the DI container. And Dispose is usually for unmanaged resources. Commented Mar 29, 2021 at 0:48
  • 1
    OK, I've summarized all the ideas commented here (as pointed out by @Nkosi as well) in my answer. Commented Mar 29, 2021 at 1:40

1 Answer 1

2

There are some things wrong in your code here:

  • You use async void on the OnActionExecuted. This should be avoided because of unpredictable results you may have. If you want to use async code, try implementing IAsyncActionFilter instead.
  • You implement Dispose for your implementation class of IAuditService in which you explicitly dispose the DbContext. You don't need to do that manually which can go out-of-sync with how the DI manages the DbContext (as scoped service) for you. Usually code inside Dispose is used to dispose unmanaged resources.

Finally I would suggest you to use IAsyncResourceFilter instead. It will be invoked by both controller actions & page handlers whereas the IAsyncActionFilter will be executed only by controller actions. You can examine the ResourceExecutingContext.ActionDescriptor to know about the action. It can be a ControllerActionDescriptor or a PageActionDescriptor.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.