Using interceptors with dependency injection in Entity Framework Core
Image from Unsplash

Using interceptors with dependency injection in Entity Framework Core

EF Core interceptors via dependency injection in ASP.NET 5

I while ago I wrote an article on how to Access multiple databases from the same DbContext in EF Core which relies on interceptors to mutate the SQL query diring execution. This code works just fine and it updates the command prior to it's execution, but it lacks in option to inject registered services to it's constructor as i used new keyword to initialize class instances.

Adding interceptor without dependency injection

As you can see from the article mentioned before and from the snippet below, our GlobalListener and GlobalCommandInterceptor classes are instantiated using new keyword and parameterless constructor. 

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<Database1Context>(options =>
        {
            options.UseSqlServer(Configuration.GetConnectionString("Database1"));
        });

    DiagnosticListener.AllListeners.Subscribe(new GlobalListener());
    ...
}
    

Same situation as with GlobalListener class is situation for GlobalCommandInterceptor which is instantiated from GobalListener constructor.

    public class GlobalListener : IObserver<DiagnosticListener>
    {
        private readonly CommandInterceptor _commandInterceptor = new CommandInterceptor();
        public void OnCompleted()
        {
            
        }

        public void OnError(Exception error)
        {
            
        }

        public void OnNext(DiagnosticListener value)
        {
            if (value.Name == DbLoggerCategory.Name)
                value.Subscribe(_commandInterceptor);
        }
    }
    

However, if you want to introduce some of the common services to listener or interceptor instance like logging or configuration, you cannot keep using parameterless constructor as you would need to inject these service instances via the constructor of the listener of interceptor class.

The way it was initially done is not the best way of doing it since you may quite often need access to common plumbing things like configuration and logging to be accessed within the interceptor.

Using dependency injection for the listener registration

For the difference from previous code, I am going to expand the constructor of the interceptor to take IConfiguration, ILogger and IWebHostingEnvironment services which are part of the plumbing for any application pretty much and use them to build some simple logic.

As a simple sample logic I am going to write logs only if the application is running in development environment.

    public class GlobalCommandInterceptor : IObserver<KeyValuePair<string, object>>
    {
        readonly IConfiguration _configuration;
        readonly ILogger<GlobalCommandInterceptor> _logger;
        readonly IWebHostEnvironment _hostEnvironment;
        public GlobalCommandInterceptor(
            IConfiguration configuration, 
            ILogger<GlobalCommandInterceptor> logger, 
            IWebHostEnvironment hostEnvironment)
        {
            _configuration = configuration;
            _logger = logger;
            _hostEnvironment = hostEnvironment;
        }
        public void OnCompleted()
        {
            
        }

        public void OnError(Exception error)
        {
            
        }

        public void OnNext(KeyValuePair<string, object> value)
        {
            if (value.Key == RelationalEventId.CommandExecuting.Name)
            {
                var command = ((CommandEventData)value.Value).Command;

                if(_hostEnvironment.IsDevelopment())
                    _logger.LogInformation(@$"Intercepted command: {command.CommandText}");

                command.CommandText = command.CommandText.Replace(
                    "[Database2.dbo].",
                    "[Database2].[dbo].");

                if (_hostEnvironment.IsDevelopment())
                    _logger.LogInformation(@$"Intercepted command altered: {command.CommandText}");
            }
        }
    }
    

For the GlobalListener class, we do not want to instantiate interceptor the way we did, plus it would me much more difficult now when we got rid of parameterless constructor or resolve the services prior to calling constructor from the GlobalListener class.

We'll just add the GlobalInterceptor class registration to our DI container which will make it resolved in the constructor of GlobalListener and we can use it to add our interceptor to subscription

    public class GlobalListener : IObserver<DiagnosticListener>
    {
        private readonly GlobalCommandInterceptor _commandInterceptor;

        public GlobalListener(GlobalCommandInterceptor commandInterceptor)
        {
            _commandInterceptor = commandInterceptor;
        }

        public void OnCompleted()
        {
            
        }

        public void OnError(Exception error)
        {
            
        }

        public void OnNext(DiagnosticListener value)
        {
            if (value.Name == DbLoggerCategory.Name)
                value.Subscribe(_commandInterceptor);
        }
    }
    

So far so good, we have our services injected to constrictor, but the problem appears when we want to add the listener. Method System.DiagnosticListener.AllListeners.Subscribe expect the instance of listener class. This means we have to resolve the GlobalListener instance to be able to call this method and put our interceptor into action.

Luckily for us, extension method AddDbContext has an overload which allows access to dependency injection provider instance so we can easily ask it to resolve the GlobalListener instance from the DI container which we previously need to register to DI container.

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddLogging(c => c.AddSerilog());
            services.AddScoped<GlobalListener>();
            services.AddScoped<GlobalCommandInterceptor>();

            services.AddDbContext<Database1Context>((provider, options) =>
            {
                options.UseSqlServer(Configuration.GetConnectionString("Database1"));
                DiagnosticListener.AllListeners.Subscribe(provider.GetRequiredService<GlobalListener>());
            });
            ...
        }
    

We are now fully away from manual instantiation of the services via new keyword and we are making our interceptor being able to access other services registered to DI container which gives are more option to expand on the logic of the interceptor.

The whole project from which these snippets are pulled from can be found on github https://github.com/dejanstojanovic/efcore-multiple-db

References

Disclaimer

Purpose of the code contained in snippets or available for download in this article is solely for learning and demo purposes. Author will not be held responsible for any failure or damages caused due to any other usage.


About the author

DEJAN STOJANOVIC

Dejan is a passionate Software Architect/Developer. He is highly experienced in .NET programming platform including ASP.NET MVC and WebApi. He likes working on new technologies and exciting challenging projects

CONNECT WITH DEJAN  Loginlinkedin Logintwitter Logingoogleplus Logingoogleplus

JavaScript

read more

SQL/T-SQL

read more

Umbraco CMS

read more

PowerShell

read more

Comments for this article