Logging with .NET Core

Home / Logging with .NET Core

In most of my projects over the past two years, I’ve used Log4Net for my logging needs. Log4Net does not work, currently, with .NET Core. However, it’s pretty easy to take advantage of the new built-in logging features to wrap the Log4Net database schema using Entity Framework.


The .NET Core logging expects an ILoggerProvider and ILogger to be defined. In our StartUp.cs, the ILoggerProvider must be attached in the Configure method. I called my provider AppLoggerProvider. This provider must be added to the available list of providers.

loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();

loggerFactory.AddDebug(LogLevel.Information);
loggerFactory.AddDebug(LogLevel.Debug);
loggerFactory.AddDebug(LogLevel.Error);

Func<IRepository<AppLogModel>> appLogRepoFactory = () => ServiceProviderFactory.ServiceProvider.GetService(typeof(IRepository<AppLogModel>)) as IRepository<AppLogModel>;

loggerFactory.AddProvider(new AppLoggerProvider(appLogRepoFactory));

Note that I’m passing a “factory” to get an instance of to get a handle to the repository that I use. The repository relies on an Entity Framework context. I’m still using EF 6.1.3 with .NET Core, so my injection looks like this:

// Logging
services.AddScoped<AppLogContext>(provider =>
{
    var connectionString = Configuration.GetConnectionString("Logging");
    var currentUserService = provider.GetService(typeof(ICurrentUserService)) as ICurrentUserService;
    return new AppLogContext(connectionString, currentUserService);
});

services.AddTransient<IRepository<Queue>, QueueRepository>();

I should also mention that the “ServiceProviderFactory” is simply a static reference to the IServiceProvider:

public static class ServiceProviderFactory
{
    public static IServiceProvider ServiceProvider { get; set; }
}

That’s all that’s required for setup/injection. The actual logger and provider are relatively simple.

First, creating the model, mapping, and repository that wraps the Log4Net schema is straight forward.

public class AppLogModelConfiguration : EntityTypeConfiguration<AppLogModel>
{
    public AppLogModelConfiguration()
    {
        ToTable("Log4Net", "ApplicationLogging");

        HasKey(x => x.Id);
        Property(p => p.Id).HasColumnName("Id").HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity).HasColumnOrder(0);

        Property(p => p.Date).HasColumnName("Date");
        Property(p => p.Thread).HasColumnName("Thread").HasMaxLength(AppLogModel.MaxThreadLength);
        Property(p => p.Level).HasColumnName("Level").HasMaxLength(AppLogModel.MaxLevelLength);
        Property(p => p.Logger).HasColumnName("Logger").HasMaxLength(AppLogModel.MaxLoggerLength);
        Property(p => p.Message).HasColumnName("Message").HasMaxLength(AppLogModel.MaximumMessageLength);
        Property(p => p.Exception).HasColumnName("Exception").HasMaxLength(AppLogModel.MaximumExceptionLength);
        Property(p => p.Application).HasColumnName("Application").HasMaxLength(AppLogModel.MaxApplicationLength);
        Property(p => p.HostAddress).HasColumnName("Host").HasMaxLength(AppLogModel.MaxHostLength);
        Property(p => p.IpAddress).HasColumnName("IpAddress").HasMaxLength(AppLogModel.MaxIpAddressLength);

        // These aren't actually mapped
        Ignore(p => p.CreateDate);
        Ignore(p => p.CreateUserId);
        Ignore(p => p.ModifyDate);
        Ignore(p => p.ModifyUserId);
        Ignore(p => p.IsActive);
        Ignore(p => p.IsDeleted);
    }
}

public class AppLogModel : BaseEntity
{
    public const int MaxThreadLength = 255;
    public const int MaxLoggerLength = 255;
    public const int MaxLevelLength = 50;
    public const int MaxApplicationLength = 255;
    public const int MaxHostLength = 50;
    public const int MaxIpAddressLength = 50;
    public const int MaximumExceptionLength = 4000;
    public const int MaximumMessageLength = 8000;
    public int Id { get; set; }
    public DateTime Date { get; set; }
    public string Thread { get; set; }
    public string Level { get; set; }
    public string Logger { get; set; }
    public string Message { get; set; }
    public string Exception { get; set; }
    public string HostAddress { get; set; }
    public string Application { get; set; }
    public string IpAddress { get; set; }
}

public class AppLogRepository : BaseRepository<AppLogModel>
{
    public AppLogRepository(AppLogContext dbContext)
        : base(dbContext)
    { }
}

My repository wraps the DbContext and providers extra functionality. If you don’t want to use a repository, the DbContext could simply be accessed directly.

The DbContext code is pretty stock/boiler plate. The only thing specific to my use case is that I do inject a service that lets me access the current user and their claims.

[DbConfigurationType(typeof(DbConfig))]
public class AppLogContext : BaseContext
{
    private int _commandTimeout = 240;
    public string ConnectionStringName { get; set; }
    public DbSet<AppLogModel> AppLogModels { get; set; }

    public AppLogContext(string connectionStringName, ICurrentUserService currentUserService)
        : base(connectionStringName, currentUserService)
    {
        ConnectionStringName = connectionStringName;
        this.Database.CommandTimeout = _commandTimeout;
    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Properties()
        .Where(x =>
            x.PropertyType.FullName.Equals("System.String") &&
            !x.GetCustomAttributes(false).OfType<ColumnAttribute>().Where(q => q.TypeName != null && q.TypeName.Equals("varchar", StringComparison.InvariantCultureIgnoreCase)).Any())
            .Configure(c =>
                c.HasColumnType("varchar").HasMaxLength(255));

        var assembly = System.Reflection.Assembly.GetAssembly(typeof(AppLogContext));
        modelBuilder.Configurations.AddFromAssembly(assembly);
    }
}

The ILoggerProvider interface only, really, has a single method called CreateLogger that must be implemented.

public class AppLoggerProvider : ILoggerProvider
{
    Func<IRepository<AppLogModel>> _repoFactory;

    public AppLoggerProvider(Func<IRepository<AppLogModel>> repoFactory)
    {
        _repoFactory = repoFactory;
    }

    public ILogger CreateLogger(string categoryName)
    {
        return new AppLogger(_repoFactory, categoryName);
    }

    public void Dispose()
    {

    }
}

Finally, the ILogger interface that does all of the grunt work is below.

public class AppLogger : ILogger
{
    private readonly string _name;
    Func<IRepository<AppLogModel>> _repoFactory;

    public AppLogger(Func<IRepository<AppLogModel>> repoFactory, string categoryName)
    {
        _repoFactory = repoFactory;
        _name = categoryName;
    }

    public IDisposable BeginScope<TState>(TState state)
    {
        if (state == null)
        {
            throw new ArgumentNullException(nameof(state));
        }

        return null;
    }

    public bool IsEnabled(LogLevel logLevel)
    {
        return true;
    }

    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
    {
        try
        {
            if (!IsEnabled(logLevel))
            {
                return;
            }

            if (formatter == null)
            {
                throw new ArgumentNullException(nameof(formatter));
            }

            var message = exception == null ?
                formatter(state, exception) :
                $"{formatter(state, exception)}{Environment.NewLine}{Environment.NewLine}{exception}";

            if (string.IsNullOrWhiteSpace(message))
            {
                return;
            }

            var hostAddress = Dns.GetHostName();
            var ipAddress = Dns
                .GetHostEntry(hostAddress)
                .AddressList.Where(x => x.AddressFamily == AddressFamily.InterNetwork)
                .FirstOrDefault();

            var model = new AppLogModel()
            {
                Date = DateTime.UtcNow,
                Application = Trim($"{_name}", AppLogModel.MaxApplicationLength),
                Message = Trim(message, AppLogModel.MaximumMessageLength),
                Exception = Trim($"{exception}", AppLogModel.MaximumExceptionLength),
                Level = Trim($"{logLevel}", AppLogModel.MaxLoggerLength),
                Logger = Trim($"{_name}", AppLogModel.MaxLoggerLength),
                Thread = Trim($"{eventId}", AppLogModel.MaxThreadLength),
                IpAddress = Trim($"{ipAddress}", AppLogModel.MaxIpAddressLength),
                HostAddress = Trim($"{hostAddress}", AppLogModel.MaxHostLength)
            };

            var repo = _repoFactory();
            repo.Add(model);
            repo.Save();
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);
        }
    }

    private static string Trim(string value, int maximumLength)
    {
        return value.Length > maximumLength ? value.Substring(0, maximumLength) : value;
    }
}

The primary method that performs the the logging is the Log. You can see the parameters above are rather esoteric, but the breakdown is like so:

  • logLevel – Loglevel as indicated by how ILogger is called.
  • eventId – This is the thread upon which the log was generated
  • state – This is the actual log message. You can see in the code above that I format it with the exception, if an exception is present.
  • exception – Well, this one is pretty self explanatory
  • formatted – This one is self explanatory (sort of) as well. It’s a function that can format whatever text is passed in. Think of it like Log4Net’s patterning.

The other point of note is the “categoryName.” This is actually derived from the injection of the ILogger. When injecting ILogger, we actually inject it was ILogger>T<.

Injection and usage, then, looks like this:

public RandomController(ILogger<RandomController> log) {
    _log = log;

    _log.LogCritical("Critical");
    _log.LogDebug("Debug");
    _log.LogInformation("Info");
    _log.LogWarning("Warning");
    _log.LogError("Error", new Exception("Exception!"));
}

I like the new intefaces. I would prefer not have to define a provider. I don’t see much value in having a Provider rather than just injecting ILogger. Other than that, this OOTB interface/mechanism can be used to wrap whatever logging mechanism (or 3rd party logger) you want. It doesn’t even have to even be database driven – it could be email, mobile notifications, or whatever mechanism is appropriate for your use case.

One caveat that I did notice in my brief testing revealed that, on occasion, .NET Core would dispose my DbContext. This is part of the reason I used a Func provider to get a repository, but it didn’t fully eliminate the problem. It could be that creating a new instance of the DbContext when the logger is called could be necessary.

2 thoughts on “Logging with .NET Core”

    1. I really haven’t seen any type of performance degradation. Behind the scenes, I am tempted to think that .NET Core already spawns a task with the logger. The reason I think this is that if the DbContext is scoped to the request, then sometimes, on save, the DbContext is already disposed.

      This code is a little bit out of date. I have a newer logger that is utilizing EF Core and has extension methods to simply enable logging if you want a more up-to-date example. Here are a few snippets.

      https://long2know.com/2017/07/net-core-logging-levels-and-categories/
      https://long2know.com/2017/05/migrating-from-net-core-1-1-to-2-0/

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.