DEV Community

Cover image for Serilog IDestructuringPolicy
Karen Payne
Karen Payne

Posted on

Serilog IDestructuringPolicy

Introduction

The best logging package for logging details for C# is Serilog. Learn how to conditionally log specific properties of interest using Serilog IDestructuringPolicy.

Old technique

The former technique was to use [NotLogged] attribute Serilog.Extras.Attributed package on properties to ignore a property.

When possible, avoid adding attributes to a class, as this ties a class to a specific package and pollutes the class unnecessarily.

Source code

public interface ICustomer
{
    int Id { get; set; }
    string WorkTitle { get; set; }
    string FirstName { get; set; }
    string LastName { get; set; }
    DateOnly DateOfBirth { get; set; }
    string OfficeEmail { get; set; }
    string OfficePhoneNumber { get; set; }
}

public class Customer : ICustomer
{
    public int Id { get; set; }
    [NotLogged]
    public string WorkTitle { get; set; }
    public string FirstName { get; set; } 
    public string LastName { get; set; }
    [NotLogged]
    public DateOnly DateOfBirth { get; set; }
    [NotLogged]
    public string OfficeEmail { get; set; }
    [NotLogged]
    public string OfficePhoneNumber { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Note
Serilog.Extras.Attributed NuGet package is no longer maintained, do not use this package.

Preferred technique

The current technique is to implement IDestructuringPolicy. For example, given the following class only properties Id, FirstName and LastName should be logged.

Source code

public interface ICustomer
{
    int Id { get; set; }
    string WorkTitle { get; set; }
    string FirstName { get; set; }
    string LastName { get; set; }
    DateOnly DateOfBirth { get; set; }
    string OfficeEmail { get; set; }
    string OfficePhoneNumber { get; set; }
}

public class Customer : ICustomer
{
    public int Id { get; set; }
    public string WorkTitle { get; set; }
    public string FirstName { get; set; } 
    public string LastName { get; set; }
    public DateOnly DateOfBirth { get; set; }
    public string OfficeEmail { get; set; }
    public string OfficePhoneNumber { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

The class
First checks if the current object implements ICustomer, if so use propertyValueFactory.CreatePropertyValue to define which properties to log.

public class IdentifierFirstLastNamesWithPolicy : IDestructuringPolicy
{
    public bool TryDestructure(
        object value, 
        ILogEventPropertyValueFactory propertyValueFactory, 
        out LogEventPropertyValue result)
    {
        if (value is ICustomer c)
        {
            result = propertyValueFactory.CreatePropertyValue(new
            {
                c.Id, 
                c.FirstName, 
                c.LastName
            });
            return true;
        }

        result = null;
        return false;
    }
}
Enter fullscreen mode Exit fullscreen mode

Another example for ICustomer, only log FirstName, LastName and OfficePhoneNumber.

public class FirstLastNamesWithPhonePolicy : IDestructuringPolicy
{
    public bool TryDestructure(object value, ILogEventPropertyValueFactory propertyValueFactory, out LogEventPropertyValue result)
    {
        if (value is ICustomer c)
        {
            result = propertyValueFactory.CreatePropertyValue(new { c.FirstName, c.LastName, c.OfficePhoneNumber });
            return true;
        }

        result = null;
        return false;
    }
}
Enter fullscreen mode Exit fullscreen mode

Sample

In this sample output goes to the console and works the same for writing to a database or file.

internal partial class Program
{
    static void Main(string[] args)
    {
        AnsiConsole.MarkupLine("[hotpink]FirstName, LastName and office phone[/]");


        Log.Logger = new LoggerConfiguration()
            .Destructure.With(new FirstLastNamesWithPhonePolicy())
            .WriteTo.Console()
            .CreateLogger();

        foreach (var customer in MockedData.Customers())
        {
            Log.Information("Customers {@C}", customer);
        }

        Console.WriteLine();

        AnsiConsole.MarkupLine("[hotpink]Id,FirstName, LastName[/]");
        Log.Logger = new LoggerConfiguration()
            .Destructure.With(new IdentifierFirstLastNamesWithPolicy())
            .WriteTo.Console()
            .CreateLogger();


        foreach (var customer in MockedData.Customers())
        {
            Log.Information("Customers {@C}", customer);
        }

        AnsiConsole.MarkupLine("[yellow]Done[/]");
        Console.ReadLine();
    }
}
Enter fullscreen mode Exit fullscreen mode

Displays output from above code

Working with EF Core sample

In this example the model, Person includes masking a credit card property along with using IDataProtector for an ASP.NET Core project.

public partial class Person
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateOnly BirthDate { get; set; }
    public string CreditCard { get; set; }
    [NotMapped]
    public string MaskedCreditCard
    {
        get
        {
            if (string.IsNullOrWhiteSpace(CreditCard))
                return "XXXX-XXXX-XXXX-XXXX";

            var digitsOnly = new string(CreditCard.Where(char.IsDigit).ToArray());

            if (digitsOnly.Length < 4)
                return "XXXX-XXXX-XXXX-XXXX";

            return $"XXXX-XXXX-XXXX-{digitsOnly[^4..]}";
        }
    }
    public void EncryptCreditCard(EncryptionService encryptionService)
    {
        if (!string.IsNullOrEmpty(CreditCard))
        {
            CreditCard = encryptionService.Encrypt(CreditCard);
        }
    }
    public void DecryptCreditCard(EncryptionService encryptionService)
    {
        if (!string.IsNullOrEmpty(CreditCard))
        {
            CreditCard = encryptionService.Decrypt(CreditCard);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The IDestructuringPolicy follows the same way done in the example above.

public class PersonDestructuringPolicy : IDestructuringPolicy
{
    public bool TryDestructure(object value, ILogEventPropertyValueFactory propertyValueFactory, out LogEventPropertyValue result)
    {
        if (value is Person p)
        {
            result = propertyValueFactory.CreatePropertyValue(
                new
                {
                    p.FirstName, 
                    p.LastName, 
                    p.MaskedCreditCard
                });
            return true;
        }

        result = null!;
        return false;
    }
}
Enter fullscreen mode Exit fullscreen mode

Setup in Program.cs

public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        Log.Logger = new LoggerConfiguration()
                   .MinimumLevel.Override("Microsoft", Serilog.Events.LogEventLevel.Warning)
                   .MinimumLevel.Override("System", Serilog.Events.LogEventLevel.Warning)
                   .MinimumLevel.Information()
                   .Destructure.With(new PersonDestructuringPolicy())
                   .WriteTo.Console()
                   .CreateLogger();

        builder.Host.UseSerilog();


        builder.Services.AddDbContext<Context>(options =>
            options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))
                .LogTo(new DbContextToFileLogger().Log, [DbLoggerCategory.Database.Command.Name], LogLevel.Information));

        builder.Services.AddDataProtection();
        builder.Services.AddScoped<EncryptionService>();
        builder.Services.AddScoped<PersonService>();

        builder.Services.AddRazorPages();

        var app = builder.Build();

        if (!app.Environment.IsDevelopment())
        {
            app.UseExceptionHandler("/Error");
            app.UseHsts();
        }

        app.UseHttpsRedirection();

        app.UseRouting();

        app.UseAuthorization();

        app.MapStaticAssets();
        app.MapRazorPages()
           .WithStaticAssets();

        app.Run();
    }
}
Enter fullscreen mode Exit fullscreen mode

Source code

Summary

Using the Serilog interface, IDestructuringPolicy allows you to focus on logging specific properties rather than all a class's properties, which can assist, for instance, with diagnosing issues.

Top comments (0)