ASP.NET Core: Logging with Serilog to MongoDB Using Configuration

This is likely a bit esoteric, but I was setting up an ASP.NET Core 2.2 project recently where I wanted to store logs in MongoDB. I wanted to play with Serilog vs. some of the alternatives. I also wanted to use a configuration file, and I wasn’t able to find any good examples online, so I figured I’d create a quick example in this post. This example is applicable to ASP.NET Core 2.2, and may work for other versions, but isn’t tested with others.

First you’ll want to install the following NuGet packages:

Serilog
Serilog.AspNetCore
Serilog.Settings.Configuration
Serilog.Sinks.MongoDB

Next, go to your appsettings.json/appsettings.Development.json files. You can delete the Logging section in the default scaffolded files and add a Serilog section as below. Here is a basic example of my Development version with only the AllowedHosts section left from the scaffolded version:

{
  "Serilog": {
    "MinimumLevel": {
      "Default": "Debug",
      "Override": {
        "Microsoft": "Warning",
        "System": "Warning"
      }
    },
    "WriteTo": [
      {
        "Name": "MongoDBCapped",
        "Args": {
          "databaseUrl": "mongodb://localhost/logs",
          "collectionName": "log",
          "cappedMaxSizeMb": "50",
          "cappedMaxDocuments": "1000"
        }
      }
      // Add other sinks here if desired...
    ]
  },
  "AllowedHosts": "*"
}

A lot of this is straightforward, such as the url and connection name. In the example above, I’m using a capped collection with “Name”: “MongoDBCapped” in the WriteTo sinks section because in my application, I don’t care about keeping logs forever. If you want to use a normal collection, change the name to “MongoDB” and remove the capped… settings in “Args”.

For the “MinimumLevel”, you’ll likely want to change the values to higher levels for your production settings. Microsoft has a nice explanation of ASP.NET Core Log Levels.

Now you’re going to want to set Serilog up in code. There are a couple similar ways to do this, but I prefer adding the .UseSerilog call in Program.cs inside CreateWebHostBuilder. Right after .UseStartup, add the following code:

.UseSerilog((context, config) =>
{
    config.ReadFrom.Configuration(context.Configuration);
});
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
        .UseStartup<Startup>()
        .UseSerilog((context, config) =>
        {
            config.ReadFrom.Configuration(context.Configuration);
        });

The whole section in Program.cs might look like this, if you have no other builder calls:

And that’s really it from a setup perspective. At this point, you can add constructor parameters to get a working logger from dependency injection. For example:

public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;
    public HomeController(ILogger<HomeController> logger)
    {
        _logger = logger;
    }
    public IActionResult Index()
    {
        _logger.LogInformation("Hello from inside HomeController.Index!");
        return View();
    }
    // Rest of the class here...
}

MongoDB C# Aggregating and Grouping By Distinct Fields

This seems to be a difficult solution to search for at the moment. The MongoDB .NET driver documentation links to the unit test files on GitHub for examples on aggregation related items, but the links are broken. Looking at the repos on GitHub is useful to find the actual tests, but for the specific problem I was trying to solve, it wasn’t entirely helpful.

Basically what I was trying to do would be relatively easy with SQL — a SELECT DISTINCT from a table/collection based on multiple fields. I really just wanted to get the unique combinations of several fields at the database layer without having to retrieve all the documents from the collection, and then figuring out which were distinct.

Below is a bit of a contrived example using an IMongoCollection<Location> named Locations where the code is grouping and retrieving the distinct set of records for State, StateShortName, StateSlug, and CountrySlug into a List<State>.

var uniqueStates = await _context
        .Locations
        .Aggregate()
        .Group(
            i => new { i.State, i.StateShortName, i.StateSlug, i.CountrySlug },
            g => new State
            {
                Name = g.First().State,
                ShortName = g.First().StateShortName,
                Slug = g.First().StateSlug,
                CountrySlug = g.First().CountrySlug
            })
        .ToListAsync();

The first argument to the Group method is the new _id for the group, and the second is the actual group/output into the output object. The group output needs to correspond to accumulator objects, such as First, Distinct, Max, Min, Sum, etc. This is also possible using BsonDocument, but I wanted to use expressions and make it more strongly typed.

It’s more or less the equivalent of something like the below in SQL:

SELECT DISTINCT
	l.State, l.StateShortName, l.StateSlug, l.CountrySlug
	FROM Locations l

There may be a more straightforward way to do this, and if you know a better way, please feel free to let me know.