File: LogPropertiesTests.cs
Web Access
Project: src\test\Generators\Microsoft.Gen.Logging\Generated\Microsoft.Gen.Logging.Generated.Tests.csproj (Microsoft.Gen.Logging.Generated.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Numerics;
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing;
using Microsoft.Shared.Text;
using TestClasses;
using Xunit;
using static TestClasses.LogPropertiesExtensions;
using static TestClasses.LogPropertiesRecordExtensions;
 
namespace Microsoft.Gen.Logging.Test;
 
public class LogPropertiesTests
{
    private const string StringProperty = "microsoft.com";
    private readonly FakeLogger _logger = new();
 
    [Fact]
    public void LogPropertiesEnumerables()
    {
        var props = new LogPropertiesSimpleExtensions.MyProps
        {
            P5 = new[] { 1, 2, 3 },
            P6 = new[] { 4, 5, 6 },
            P7 = new Dictionary<string, int>
            {
                { "Seven", 7 },
                { "Eight", 8 },
                { "Nine", 9 }
            }
        };
 
        LogPropertiesSimpleExtensions.LogFunc(_logger, "Hello", props);
 
        Assert.Equal(1, _logger.Collector.Count);
 
        var ss = _logger.LatestRecord.StructuredState!.ToDictionary(x => x.Key, x => x.Value);
        Assert.Equal("[\"1\",\"2\",\"3\"]", ss["myProps.P5"]);
        Assert.Equal("[\"4\",\"5\",\"6\"]", ss["myProps.P6"]);
        Assert.Equal("{\"Seven\"=\"7\",\"Eight\"=\"8\",\"Nine\"=\"9\"}", ss["myProps.P7"]);
    }
 
    [Fact]
    public void LogPropertiesOmitParamName()
    {
        var props = new LogPropertiesOmitParameterNameExtensions.MyProps
        {
            P0 = 42,
            P1 = "foo"
        };
 
        LogPropertiesOmitParameterNameExtensions.M0(_logger, props);
 
        var state = _logger.LatestRecord.StructuredState!;
        Assert.Equal(2, state.Count);
        Assert.Equal(props.P0.ToString(CultureInfo.InvariantCulture), state!.GetValue("P0"));
        Assert.Equal(props.P1, state!.GetValue("P1"));
    }
 
    [Fact]
    public void LogPropertiesOmitParamNameDefaultAttrCtor()
    {
        var props = new LogPropertiesOmitParameterNameExtensions.MyProps
        {
            P0 = 42,
            P1 = "foo"
        };
 
        LogPropertiesOmitParameterNameExtensions.M2(_logger, LogLevel.Critical, props);
 
        var state = _logger.LatestRecord.StructuredState!;
        Assert.Equal(2, state.Count);
        Assert.Equal(props.P0.ToString(CultureInfo.InvariantCulture), state!.GetValue("P0"));
        Assert.Equal(props.P1, state!.GetValue("P1"));
    }
 
    [Fact]
    public void LogPropertiesSpecialTypes()
    {
        var props = new LogPropertiesSpecialTypesExtensions.MyProps
        {
            P0 = DateTime.Now,
            P1 = DateTimeOffset.Now,
            P2 = new TimeSpan(1234),
            P3 = Guid.NewGuid(),
            P4 = new Version(1, 2, 3, 4),
            P5 = new Uri("https://www.microsoft.com"),
            P6 = IPAddress.Parse("192.168.10.1"),
            P7 = new IPEndPoint(IPAddress.Parse("192.168.10.1"), 42),
            P8 = new IPEndPoint(IPAddress.Parse("192.168.10.1"), 42),
            P9 = new DnsEndPoint("microsoft.com", 42),
            P10 = new BigInteger(3.1415),
            P11 = new Complex(1.2, 3.4),
            P12 = new Matrix3x2(1, 2, 3, 4, 5, 6),
            P13 = new Matrix4x4(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16),
            P14 = new Plane(1, 2, 3, 4),
            P15 = new Quaternion(1, 2, 3, 4),
            P16 = new Vector2(1),
            P17 = new Vector3(1, 2, 3),
            P18 = new Vector4(1, 2, 3, 4),
 
#if NET6_0_OR_GREATER
            P19 = new TimeOnly(123),
            P20 = new DateOnly(2022, 6, 21),
#endif
        };
 
        LogPropertiesSpecialTypesExtensions.M0(_logger, props);
        var state = _logger.LatestRecord.StructuredState!;
 
#if NET6_0_OR_GREATER
        Assert.Equal(21, state.Count);
#else
        Assert.Equal(19, state.Count);
#endif
 
        Assert.Equal(props.P0.ToString(CultureInfo.InvariantCulture), state!.GetValue("p.P0"));
        Assert.Equal(props.P1.ToString(CultureInfo.InvariantCulture), state!.GetValue("p.P1"));
        Assert.Equal(props.P2.ToString(null, CultureInfo.InvariantCulture), state!.GetValue("p.P2"));
        Assert.Equal(props.P3.ToString(), state!.GetValue("p.P3"));
        Assert.Equal(props.P4.ToString(), state!.GetValue("p.P4"));
        Assert.Equal(props.P5.ToString(), state!.GetValue("p.P5"));
        Assert.Equal(props.P6.ToString(), state!.GetValue("p.P6"));
        Assert.Equal(props.P7.ToString(), state!.GetValue("p.P7"));
        Assert.Equal(props.P8.ToString(), state!.GetValue("p.P8"));
        Assert.Equal(props.P9.ToString(), state!.GetValue("p.P9"));
        Assert.Equal(props.P10.ToString(CultureInfo.InvariantCulture), state!.GetValue("p.P10"));
        Assert.Equal(props.P11.ToString(CultureInfo.InvariantCulture), state!.GetValue("p.P11"));
        Assert.Equal(props.P12.ToString(), state!.GetValue("p.P12"));
        Assert.Equal(props.P13.ToString(), state!.GetValue("p.P13"));
        Assert.Equal(props.P14.ToString(), state!.GetValue("p.P14"));
        Assert.Equal(props.P15.ToString(), state!.GetValue("p.P15"));
        Assert.Equal(props.P16.ToString(), state!.GetValue("p.P16"));
        Assert.Equal(props.P17.ToString(), state!.GetValue("p.P17"));
        Assert.Equal(props.P18.ToString(), state!.GetValue("p.P18"));
 
#if NET6_0_OR_GREATER
        Assert.Equal(props.P19.ToString(CultureInfo.InvariantCulture), state!.GetValue("p.P19"));
        Assert.Equal(props.P20.ToString(CultureInfo.InvariantCulture), state!.GetValue("p.P20"));
#endif
 
    }
 
    [Fact]
    public void LogPropertiesNullHandling()
    {
        using var logger = Utils.GetLogger();
        var collector = logger.FakeLogCollector;
 
        var props = new LogPropertiesNullHandlingExtensions.MyProps
        {
            P0 = null!,
            P1 = null,
            P2 = 2,
            P3 = null,
            P4 = default,
            P5 = null!,
            P6 = null,
            P7 = 2,
            P8 = null,
            P9 = default,
            P10 = default,
        };
 
        LogPropertiesNullHandlingExtensions.M0(logger, props);
        Assert.Equal(1, collector.Count);
 
        var ss = collector.LatestRecord.StructuredState!.ToDictionary(x => x.Key, x => x.Value);
        Assert.Equal(11, ss.Count);
        Assert.Null(ss["p.P0"]);
        Assert.Null(ss["p.P1"]);
        Assert.Equal(props.P2.ToString(null, CultureInfo.InvariantCulture), ss["p.P2"]);
        Assert.Null(ss["p.P3"]);
        Assert.Equal("I refuse to be formatted", ss["p.P4"]);
        Assert.Null(ss["p.P5"]);
        Assert.Null(ss["p.P6"]);
        Assert.Equal("-", ss["p.P7"]);
        Assert.Null(ss["p.P8"]);
        Assert.Equal("------------------------", ss["p.P9"]);
        Assert.Equal("null", ss["p.P10"]);
 
        collector.Clear();
        LogPropertiesNullHandlingExtensions.M1(logger, props);
        Assert.Equal(1, collector.Count);
 
        ss = collector.LatestRecord.StructuredState!.ToDictionary(x => x.Key, x => x.Value);
        Assert.Equal(4, ss.Count);
        Assert.Equal(props.P2.ToString(null, CultureInfo.InvariantCulture), ss["p.P2"]);
        Assert.Equal("I refuse to be formatted", ss["p.P4"]);
        Assert.Equal("-", ss["p.P7"]);
        Assert.Equal("------------------------", ss["p.P9"]);
    }
 
    [Fact]
    public void LogPropertiesTest()
    {
        var classToLog = new MyDerivedClass(double.Epsilon)
        {
            StringProperty = "test Abc",
            SimplifiedNullableIntProperty = null,
            ExplicitNullableIntProperty = 2,
            StringPropertyBase = "Base Abc",
            SetOnlyProperty = DateTime.MinValue,
            NonVirtualPropertyBase = "NonVirtualPropertyBase",
            TransitivePropertyArray = new[] { 1, 2, 3 },
            TransitiveProperty = new MyTransitiveDerivedClass
            {
                TransitiveStringProp = "Transitive string",
                TransitiveVirtualProp = int.MaxValue,
                InnerTransitiveProperty = new LeafTransitiveDerivedClass(),
                TransitiveDerivedProp = byte.MaxValue
            },
            AnotherTransitiveProperty = null,
            VirtualInterimProperty = -1,
            PropertyOfGenerics = new GenericClass<int> { GenericProp = 1 },
            InterimProperty = short.MaxValue
        };
 
        LogFunc(_logger, StringProperty, classToLog);
        Assert.Equal(1, _logger.Collector.Count);
        Assert.Equal(LogLevel.Debug, _logger.Collector.LatestRecord.Level);
 
        var expectedState = new Dictionary<string, string?>
        {
            ["classToLog_StringProperty_1"] = "microsoft.com",
            ["classToLog.StringProperty"] = classToLog.StringProperty,
            ["classToLog.SimplifiedNullableIntProperty"] = null,
            ["classToLog.ExplicitNullableIntProperty"] = classToLog.ExplicitNullableIntProperty.ToString(),
            ["classToLog.GetOnlyProperty"] = classToLog.GetOnlyProperty.ToString(CultureInfo.InvariantCulture),
            ["classToLog.VirtualPropertyBase"] = classToLog.VirtualPropertyBase,
            ["classToLog.NonVirtualPropertyBase"] = classToLog.NonVirtualPropertyBase,
            ["classToLog.TransitivePropertyArray"] = LoggerMessageHelper.Stringify(classToLog.TransitivePropertyArray),
            ["classToLog.TransitiveProperty.TransitiveNumberProp"]
                = classToLog.TransitiveProperty.TransitiveNumberProp.ToString(CultureInfo.InvariantCulture),
 
            ["classToLog.TransitiveProperty.TransitiveStringProp"] = classToLog.TransitiveProperty.TransitiveStringProp,
            ["classToLog.TransitiveProperty.InnerTransitiveProperty.IntegerProperty"]
                = classToLog.TransitiveProperty.InnerTransitiveProperty.IntegerProperty.ToString(CultureInfo.InvariantCulture),
            ["classToLog.TransitiveProperty.InnerTransitiveProperty.DateTimeProperty"]
                = classToLog.TransitiveProperty.InnerTransitiveProperty.DateTimeProperty.ToString(CultureInfo.InvariantCulture),
 
            ["classToLog.AnotherTransitiveProperty.IntegerProperty"] = null, // Since AnotherTransitiveProperty is null
            ["classToLog.StringPropertyBase"] = classToLog.StringPropertyBase,
            ["classToLog.VirtualInterimProperty"] = classToLog.VirtualInterimProperty.ToInvariantString(),
            ["classToLog.InterimProperty"] = classToLog.InterimProperty.ToString(CultureInfo.InvariantCulture),
            ["classToLog.TransitiveProperty.TransitiveDerivedProp"] = classToLog.TransitiveProperty.TransitiveDerivedProp.ToInvariantString(),
            ["classToLog.TransitiveProperty.TransitiveVirtualProp"] = classToLog.TransitiveProperty.TransitiveVirtualProp.ToInvariantString(),
            ["classToLog.TransitiveProperty.TransitiveGenericProp.GenericProp"]
                = classToLog.TransitiveProperty.TransitiveGenericProp.GenericProp,
 
            ["classToLog.PropertyOfGenerics.GenericProp"] = classToLog.PropertyOfGenerics.GenericProp.ToInvariantString(),
            ["classToLog.CustomStructProperty.LongProperty"] = classToLog.CustomStructProperty.LongProperty.ToInvariantString(),
            ["classToLog.CustomStructProperty.TransitiveStructProperty.DateTimeOffsetProperty"]
                = classToLog.CustomStructProperty.TransitiveStructProperty.DateTimeOffsetProperty.ToString(CultureInfo.InvariantCulture),
 
            ["classToLog.CustomStructProperty.NullableTransitiveStructProperty.DateTimeOffsetProperty"]
                = classToLog.CustomStructProperty.NullableTransitiveStructProperty?.DateTimeOffsetProperty.ToString(CultureInfo.InvariantCulture),
 
            ["classToLog.CustomStructProperty.NullableTransitiveStructProperty2.DateTimeOffsetProperty"]
                = classToLog.CustomStructProperty.NullableTransitiveStructProperty2?.DateTimeOffsetProperty.ToString(CultureInfo.InvariantCulture),
 
            ["classToLog.CustomStructNullableProperty.LongProperty"] = classToLog.CustomStructNullableProperty?.LongProperty.ToInvariantString(),
            ["classToLog.CustomStructNullableProperty.TransitiveStructProperty.DateTimeOffsetProperty"]
                = classToLog.CustomStructNullableProperty?.TransitiveStructProperty.DateTimeOffsetProperty.ToString(CultureInfo.InvariantCulture),
 
            ["classToLog.CustomStructNullableProperty.NullableTransitiveStructProperty.DateTimeOffsetProperty"]
                = classToLog.CustomStructNullableProperty?.NullableTransitiveStructProperty?.DateTimeOffsetProperty.ToString(CultureInfo.InvariantCulture),
 
            ["classToLog.CustomStructNullableProperty.NullableTransitiveStructProperty2.DateTimeOffsetProperty"]
                = classToLog.CustomStructNullableProperty?.NullableTransitiveStructProperty2?.DateTimeOffsetProperty.ToString(CultureInfo.InvariantCulture),
 
            ["classToLog.CustomStructNullableProperty2.LongProperty"] = classToLog.CustomStructNullableProperty2?.LongProperty.ToInvariantString(),
            ["classToLog.CustomStructNullableProperty2.TransitiveStructProperty.DateTimeOffsetProperty"]
                = classToLog.CustomStructNullableProperty2?.TransitiveStructProperty.DateTimeOffsetProperty.ToString(CultureInfo.InvariantCulture),
 
            ["classToLog.CustomStructNullableProperty2.NullableTransitiveStructProperty.DateTimeOffsetProperty"]
                = classToLog.CustomStructNullableProperty2?.NullableTransitiveStructProperty?.DateTimeOffsetProperty.ToString(CultureInfo.InvariantCulture),
 
            ["classToLog.CustomStructNullableProperty2.NullableTransitiveStructProperty2.DateTimeOffsetProperty"]
                = classToLog.CustomStructNullableProperty2?.NullableTransitiveStructProperty2?.DateTimeOffsetProperty.ToString(CultureInfo.InvariantCulture),
 
            ["{OriginalFormat}"] = "Only {classToLog_StringProperty_1} as param"
        };
 
        _logger.Collector.LatestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState);
    }
 
    [Fact]
    public void LogPropertiesInTemplateTest()
    {
        var classToLog = new ClassAsParam { MyProperty = 0 };
        LogMethodTwoParams(_logger, StringProperty, classToLog);
        Assert.Equal(1, _logger.Collector.Count);
        Assert.Equal(LogLevel.Information, _logger.Collector.LatestRecord.Level);
        Assert.Equal($"Both {StringProperty} and {classToLog} as params", _logger.Collector.LatestRecord.Message);
 
        var expectedState = new Dictionary<string, string?>
        {
            ["StringProperty"] = StringProperty,
            ["ComplexParam"] = classToLog.ToString(),
            ["complexParam.MyProperty"] = classToLog.MyProperty.ToInvariantString(),
            ["{OriginalFormat}"] = "Both {StringProperty} and {ComplexParam} as params"
        };
 
        _logger.Collector.LatestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState);
    }
 
    [Fact]
    public void LogPropertiesTestNullablePropertyInClass()
    {
        var now = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc);
        var classToLog = new MyClassWithNullableProperty { NullableDateTime = now, NonNullableDateTime = now };
        LogMethodNullablePropertyInClassMatchesNonNullable(_logger, classToLog);
        Assert.Equal(1, _logger.Collector.Count);
        Assert.Equal(LogLevel.Information, _logger.Collector.LatestRecord.Level);
        Assert.Equal($"Testing nullable property within class here...", _logger.Collector.LatestRecord.Message);
        var expectedState = new Dictionary<string, string?>
        {
            // Note that, regardless of nullability, the datetime fields SHOULD match given the same, non-null input.
            ["classWithNullablePropertyParam.NullableDateTime"] = "01/01/2024 00:00:00",
            ["classWithNullablePropertyParam.NonNullableDateTime"] = "01/01/2024 00:00:00",
            ["{OriginalFormat}"] = "Testing nullable property within class here..."
        };
 
        _logger.Collector.LatestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState);
    }
 
    [Fact]
    public void LogPropertiesTestNullablePropertyInClass_WhenNull()
    {
        var now = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc);
        var classToLog = new MyClassWithNullableProperty { NullableDateTime = null, NonNullableDateTime = now };
        LogMethodNullablePropertyInClassMatchesNonNullable(_logger, classToLog);
        Assert.Equal(1, _logger.Collector.Count);
        Assert.Equal(LogLevel.Information, _logger.Collector.LatestRecord.Level);
        Assert.Equal($"Testing nullable property within class here...", _logger.Collector.LatestRecord.Message);
        var expectedState = new Dictionary<string, string?>
        {
            ["classWithNullablePropertyParam.NullableDateTime"] = null,
            ["classWithNullablePropertyParam.NonNullableDateTime"] = "01/01/2024 00:00:00",
            ["{OriginalFormat}"] = "Testing nullable property within class here..."
        };
 
        _logger.Collector.LatestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState);
    }
 
    [Fact]
    public void LogPropertiesNonStaticClassTest()
    {
        const string StringParamValue = "Value for a string";
 
        var classToLog = new ClassToLog { MyIntProperty = 0 };
        new NonStaticTestClass(_logger).LogProperties(StringParamValue, classToLog);
 
        Assert.Equal(1, _logger.Collector.Count);
        var latestRecord = _logger.Collector.LatestRecord;
        Assert.Equal(LogLevel.Information, latestRecord.Level);
        Assert.Equal($"LogProperties: {StringParamValue}", latestRecord.Message);
 
        var expectedState = new Dictionary<string, string?>
        {
            ["P0"] = StringParamValue,
            ["p1.MyIntProperty"] = classToLog.MyIntProperty.ToInvariantString(),
            ["p1.MyStringProperty"] = classToLog.MyStringProperty,
            ["p1.AnotherStringProperty"] = classToLog.AnotherStringProperty,
            ["{OriginalFormat}"] = "LogProperties: {P0}"
        };
 
        latestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState);
    }
 
    [Fact]
    public void LogPropertyTestCustomStruct()
    {
        var transitiveStruct = new MyTransitiveStruct { DateTimeOffsetProperty = DateTimeOffset.MinValue };
        var structToLog = new MyCustomStruct { LongProperty = 1L, NullableTransitiveStructProperty2 = transitiveStruct };
        LogMethodStruct(_logger, structToLog);
 
        Assert.Equal(1, _logger.Collector.Count);
        var latestRecord = _logger.Collector.LatestRecord;
        Assert.Null(latestRecord.Exception);
        Assert.Equal("Testing non-nullable struct here...", latestRecord.Message);
 
        var expectedState = new Dictionary<string, string?>
        {
            ["structParam.LongProperty"] = structToLog.LongProperty.ToInvariantString(),
            ["structParam.TransitiveStructProperty.DateTimeOffsetProperty"]
                = structToLog.TransitiveStructProperty.DateTimeOffsetProperty.ToString(CultureInfo.InvariantCulture),
 
            ["structParam.NullableTransitiveStructProperty.DateTimeOffsetProperty"]
                = structToLog.NullableTransitiveStructProperty?.DateTimeOffsetProperty.ToString(CultureInfo.InvariantCulture),
 
            ["structParam.NullableTransitiveStructProperty2.DateTimeOffsetProperty"]
                = structToLog.NullableTransitiveStructProperty2.Value.DateTimeOffsetProperty.ToString(CultureInfo.InvariantCulture),
 
            ["{OriginalFormat}"] = "Testing non-nullable struct here..."
        };
 
        latestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState);
    }
 
    [Fact]
    public void LogPropertyTestCustomNullableStruct()
    {
        var transitiveStruct = new MyTransitiveStruct { DateTimeOffsetProperty = DateTimeOffset.MinValue };
        MyCustomStruct? structToLog = new MyCustomStruct { LongProperty = 1L, NullableTransitiveStructProperty = transitiveStruct };
        LogMethodNullableStruct(_logger, in structToLog);
 
        Assert.Equal(1, _logger.Collector.Count);
        var latestRecord = _logger.Collector.LatestRecord;
        Assert.Null(latestRecord.Exception);
        Assert.Equal("Testing nullable struct here...", latestRecord.Message);
 
        var expectedState = new Dictionary<string, string?>
        {
            ["structParam.LongProperty"] = structToLog.Value.LongProperty.ToInvariantString(),
            ["structParam.TransitiveStructProperty.DateTimeOffsetProperty"]
                = structToLog.Value.TransitiveStructProperty.DateTimeOffsetProperty.ToString(CultureInfo.InvariantCulture),
 
            ["structParam.NullableTransitiveStructProperty.DateTimeOffsetProperty"]
                = structToLog.Value.NullableTransitiveStructProperty.Value.DateTimeOffsetProperty.ToString(CultureInfo.InvariantCulture),
 
            ["structParam.NullableTransitiveStructProperty2.DateTimeOffsetProperty"]
                = structToLog.Value.NullableTransitiveStructProperty2?.DateTimeOffsetProperty.ToString(CultureInfo.InvariantCulture),
 
            ["{OriginalFormat}"] = "Testing nullable struct here..."
        };
 
        latestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState);
    }
 
    [Fact]
    public void LogPropertyTestCustomExplicitNullableStruct()
    {
        LogMethodExplicitNullableStruct(_logger, null);
 
        Assert.Equal(1, _logger.Collector.Count);
 
        var latestRecord = _logger.Collector.LatestRecord;
        Assert.Null(latestRecord.Exception);
        Assert.Equal("Testing explicit nullable struct here...", latestRecord.Message);
 
        var expectedState = new Dictionary<string, string?>
        {
            ["structParam.LongProperty"] = null,
            ["structParam.TransitiveStructProperty.DateTimeOffsetProperty"] = null,
            ["structParam.NullableTransitiveStructProperty.DateTimeOffsetProperty"] = null,
            ["structParam.NullableTransitiveStructProperty2.DateTimeOffsetProperty"] = null,
            ["{OriginalFormat}"] = "Testing explicit nullable struct here..."
        };
 
        latestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState);
    }
 
    [Fact]
    public void LogPropertiesDefaultAttrCtor()
    {
        var classToLog = new ClassAsParam { MyProperty = 0 };
 
        LogMethodDefaultAttrCtor(_logger, LogLevel.Critical, classToLog);
 
        Assert.Equal(1, _logger.Collector.Count);
        var latestRecord = _logger.Collector.LatestRecord;
        Assert.Null(latestRecord.Exception);
        Assert.NotEqual(0, latestRecord.Id.Id);
        Assert.Equal(LogLevel.Critical, latestRecord.Level);
        Assert.Equal(string.Empty, latestRecord.Message);
 
        var expectedState = new Dictionary<string, string?>
        {
            ["complexParam.MyProperty"] = classToLog.MyProperty.ToInvariantString()
        };
 
        latestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState);
    }
 
    [Fact]
    public void LogPropertiesInterfaceArgument()
    {
        var classToLog = new MyInterfaceImpl
        {
            IntProperty = 100,
            ClassStringProperty = "test string", // won't get logged
            TransitiveProp = new LeafTransitiveBaseClass { IntegerProperty = 500 }
        };
 
        LogMethodInterfaceArg(_logger, classToLog);
 
        Assert.Equal(1, _logger.Collector.Count);
        var latestRecord = _logger.Collector.LatestRecord;
        Assert.Null(latestRecord.Exception);
        Assert.Equal(6, latestRecord.Id.Id);
        Assert.Equal(LogLevel.Information, latestRecord.Level);
        Assert.Equal("Testing interface-typed argument here...", latestRecord.Message);
 
        var expectedState = new Dictionary<string, string?>
        {
            ["complexParam.IntProperty"] = classToLog.IntProperty.ToInvariantString(),
            ["complexParam.TransitiveProp.IntegerProperty"] = classToLog.TransitiveProp.IntegerProperty.ToInvariantString(),
            ["{OriginalFormat}"] = "Testing interface-typed argument here..."
        };
 
        latestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState);
    }
 
    [Fact]
    public void LogPropertiesRecordClassArgument()
    {
        var recordToLog = new MyRecordClass(100_500, "string_with_at_symbol - @");
 
        LogRecordClass(_logger, recordToLog);
 
        Assert.Equal(1, _logger.Collector.Count);
        var latestRecord = _logger.Collector.LatestRecord;
        Assert.Null(latestRecord.Exception);
        Assert.NotEqual(0, latestRecord.Id.Id);
        Assert.Equal(LogLevel.Debug, latestRecord.Level);
        Assert.Empty(latestRecord.Message);
 
        var expectedState = new Dictionary<string, string?>
        {
            ["p0.Value"] = recordToLog.Value.ToInvariantString(),
            ["p0.class"] = recordToLog.@class,
            ["p0.GetOnlyValue"] = recordToLog.GetOnlyValue.ToInvariantString(),
            ["p0.event"] = recordToLog.@event.ToString(CultureInfo.InvariantCulture)
        };
 
        latestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState);
    }
 
    [Fact]
    public void LogPropertiesRecordStructArgument()
    {
        var recordToLog = new MyRecordStruct(100_500, "string value");
 
        LogRecordStruct(_logger, recordToLog);
 
        Assert.Equal(1, _logger.Collector.Count);
        var latestRecord = _logger.Collector.LatestRecord;
        Assert.Null(latestRecord.Exception);
        Assert.NotEqual(0, latestRecord.Id.Id);
        Assert.Equal(LogLevel.Debug, latestRecord.Level);
        Assert.Equal($"Struct is: {recordToLog}", latestRecord.Message);
 
        var expectedState = new Dictionary<string, string?>
        {
            ["p0"] = recordToLog.ToString(),
            ["p0.IntValue"] = recordToLog.IntValue.ToInvariantString(),
            ["p0.StringValue"] = recordToLog.StringValue,
            ["p0.GetOnlyValue"] = recordToLog.GetOnlyValue.ToInvariantString(),
            ["{OriginalFormat}"] = "Struct is: {p0}"
        };
 
        latestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState);
    }
 
    [Fact]
    public void LogPropertiesReadonlyRecordStructArgument()
    {
        var recordToLog = new MyReadonlyRecordStruct(int.MaxValue, Guid.NewGuid().ToString());
 
        LogReadonlyRecordStruct(_logger, recordToLog);
 
        Assert.Equal(1, _logger.Collector.Count);
        var latestRecord = _logger.Collector.LatestRecord;
        Assert.Null(latestRecord.Exception);
        Assert.NotEqual(0, latestRecord.Id.Id);
        Assert.Equal(LogLevel.Debug, latestRecord.Level);
        Assert.Equal($"Readonly struct is: {recordToLog}", latestRecord.Message);
 
        var expectedState = new Dictionary<string, string?>
        {
            ["p0"] = recordToLog.ToString(),
            ["p0.IntValue"] = recordToLog.IntValue.ToInvariantString(),
            ["p0.StringValue"] = recordToLog.StringValue,
            ["p0.GetOnlyValue"] = recordToLog.GetOnlyValue.ToInvariantString(),
            ["{OriginalFormat}"] = "Readonly struct is: {p0}"
        };
 
        latestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState);
    }
}