user122222
user122222

Reputation: 2419

Inject different loggers for controllers in ASP.NET MVC

I have defined services in Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    string connectionString = Configuration.GetConnectionString("DbConnection");
    services.AddDbContext<AppDbContext>(options => options.UseSqlServer(connectionString));
    services.AddControllersWithViews();
    services.AddScoped<IAdsService, AdsService>();
    services.AddScoped<ILogger, ConsoleLogger>();
    services.AddScoped<ILogger, FileLogger>();
    services.AddScoped<IAdsRepository, AdsRepository>();
}

This is my implementation of Logger:

public class ConsoleLogger: ILogger
{
    public void Log(LogData data) => Console.WriteLine(data.ToString());
}

public class FileLogger : ILogger
{
    private string Path = "logs";

    public void Log(LogData data)
    {
        if (!Directory.Exists(Path))
        {
            DirectoryInfo di = Directory.CreateDirectory(Path);
        }

        File.AppendAllText(Path + "/logs.txt", data.ToString());
    }
}

I want to use different loggers for different controllers:

e.g.

private readonly ILogger _logger;
private readonly IAdsService _adsService;

public AdController(IAdsService adsService, ILogger logger)
{
    _adsService = adsService;
    _logger = logger;
}

However it only takes FileLogger - how to specify my controller to use ConsoleLogger in Startup?

Upvotes: 0

Views: 3302

Answers (1)

Steven
Steven

Reputation: 172646

What you are looking for is a feature called context-based injection, which is something that is not easily implemented with MS.DI. That's not to say it's impossible, but depending on your needs, it might require a lot of configuration.

One way to achieve this, though, it by configuring any component that requires an alternative logger explicitly using a lambda using the ActivatorUtilities class. Here's an example:

private static void AddServices(IServiceCollection services)
{
    // The default logger
    services.AddScoped<ILogger, FileLogger>();

    // Additional 'alternative' loggers
    services.AddScoped<ConsoleLogger>();

    // Configure a component that requires an alternative logger
    services.AddTransient<AdController>(c =>
        ActivatorUtilities.CreateInstance<AdController>(c,
            c.GetRequiredService<ConsoleLogger>()));
}

In this example:

  • FileLogger is registered as ILogger allowing any 'normal' component that depends on ILogger to get injected with FileLogger.
  • ConsoleLogger is registered as itself, allowing it to be requested als alternative logger
  • AdController is registered using the ActivatorUtilities so that ActivatorUtilities is responsible for creating a new AdController where a resolved ConsoleLogger is supplied to ActivatorUtilities. This allows ActivatorUtilities to supply ConsoleLogger to the first constructor parameter that is assignable from ConsoleLogger. This basically means that ConsoleLogger is supplied to the ILogger argument of AdController.

To test this code, try this:

public interface IAdsService { }
public class AdsService : IAdsService { }
public interface ILogger { }
public class ConsoleLogger : ILogger { }
public class FileLogger : ILogger { }

public class AdController
{
    public AdController(IAdsService adsService, ILogger logger) => this.Logger = logger;

    public ILogger Logger { get; }
}

class Program
{
    static void Main(string[] args)
    {
        var services = new ServiceCollection();

        services.AddTransient<IAdsService, AdsService>();

        AddServices(services);

        var provider = services.BuildServiceProvider();

        using (var scope = provider.CreateScope())
        {
            var controller =
                scope.ServiceProvider.GetRequiredService<AdController>();

            Console.WriteLine(controller.Logger.GetType().Name);
        }
    }
}

There are several downsides to this approach, the most important being that this solution might not scale well. There is no good way to take a more convention-based approach where you say "use ConsoleLogger for any component that follows the following definition X". You must specify each component that uses an alternative logger explicitly.

If these limitations cause maintainability issues, try using a different DI Container that natively supports this feature.

Upvotes: 1

Related Questions