diff --git a/README.md b/README.md index 85a49e0..89ef423 100644 --- a/README.md +++ b/README.md @@ -372,6 +372,60 @@ public class WithRegex snippet source | anchor +## 8. Working with MetadataTypeAttribute + +You can apply [`MetadataTypeAttribute`](https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.metadatatypeattribute) to your class providing another type with annotated properties. It may help you in the case when source type with its properties is defined in a code generated file so you don't want to put the attributes in there as they would get overwritten by the generator. Note that you have to set `AttributedDestructuringPolicyOptions.RespectMetadataTypeAttribute` to `true`. + +```csharp +var log = new LoggerConfiguration() + .Destructure.UsingAttributes(x => x.UseMetadataTypeAttribute = true) + ... +``` + +### Examples + +```cs + /// + /// Simple Metadata Sample + /// + [MetadataType(typeof(DtoMetadata))] + public partial class Dto + { + public string Private { get; set; } + + public string Public { get; set; } + } + + internal class DtoMetadata + { + [NotLogged] + public object Private { get; set; } + } + + /// + /// Metadata Sample with derived subclass + /// + [MetadataType(typeof(DtoMetadataDerived))] + public partial class DtoWithDerived + { + public string Private { get; set; } + + public string Public { get; set; } + } + + internal class DtoMetadataBase + { + public object Public { get; set; } + } + + internal class DtoMetadataDerived : DtoMetadataBase + { + [NotLogged] + public object Private { get; set; } + } + +``` + # Benchmarks The results are available [here](https://destructurama.github.io/attributed/dev/bench/). diff --git a/src/Destructurama.Attributed.Tests/MetadataTypeTests.cs b/src/Destructurama.Attributed.Tests/MetadataTypeTests.cs index 46fa84f..3861e4b 100644 --- a/src/Destructurama.Attributed.Tests/MetadataTypeTests.cs +++ b/src/Destructurama.Attributed.Tests/MetadataTypeTests.cs @@ -9,8 +9,21 @@ namespace Destructurama.Attributed.Tests; [TestFixture] public class MetadataTypeTests { +#if NET5_0_OR_GREATER + [SetUp] + public void SetUp() + { + AttributedDestructuringPolicy.Clear(); + } + + [TearDown] + public void TearDown() + { + AttributedDestructuringPolicy.Clear(); + } + [Test] - public void MetadataType_Should_Be_Respected() + public void MetadataType_Should_Not_Be_Respected() { var customized = new Dto { @@ -23,10 +36,49 @@ public void MetadataType_Should_Be_Respected() var sv = (StructureValue)evt.Properties["Customized"]; var props = sv.Properties.ToDictionary(p => p.Name, p => p.Value); + props.Count.ShouldBe(2); + props["Public"].LiteralValue().ShouldBe("not_Secret"); + props["Private"].LiteralValue().ShouldBe("secret"); + } + [Test] + public void MetadataType_Should_Be_Respected() + { + var customized = new Dto + { + Private = "secret", + Public = "not_Secret" + }; + + var evt = DelegatingSink.Execute(customized, configure: opt => opt.UseMetadataTypeAttribute = true); + + var sv = (StructureValue)evt.Properties["Customized"]; + var props = sv.Properties.ToDictionary(p => p.Name, p => p.Value); + props.Count.ShouldBe(1); props["Public"].LiteralValue().ShouldBe("not_Secret"); } + [Test] + public void MetadataTypeWithDerived_Should_Be_Respected() + { + var customized = new DtoWithDerived + { + Private = "secret", + Public = "not_Secret" + }; + + var evt = DelegatingSink.Execute(customized, configure: opt => opt.UseMetadataTypeAttribute = true); + + var sv = (StructureValue)evt.Properties["Customized"]; + var props = sv.Properties.ToDictionary(p => p.Name, p => p.Value); + + props.Count.ShouldBe(1); + props["Public"].LiteralValue().ShouldBe("not_Secret"); + } +#endif + /// + /// Simple Metadata Sample + /// [MetadataType(typeof(DtoMetadata))] public partial class Dto { @@ -40,4 +92,27 @@ internal class DtoMetadata [NotLogged] public object Private { get; set; } } + + /// + /// Metadata Sample with derived subclass + /// + [MetadataType(typeof(DtoMetadataDerived))] + public partial class DtoWithDerived + { + public string Private { get; set; } + + public string Public { get; set; } + } + + internal class DtoMetadataBase + { + public object Public { get; set; } + } + + internal class DtoMetadataDerived : DtoMetadataBase + { + [NotLogged] + public object Private { get; set; } + } + } diff --git a/src/Destructurama.Attributed/Attributed/AttributedDestructuringPolicy.cs b/src/Destructurama.Attributed/Attributed/AttributedDestructuringPolicy.cs index 7298a61..0db91cf 100644 --- a/src/Destructurama.Attributed/Attributed/AttributedDestructuringPolicy.cs +++ b/src/Destructurama.Attributed/Attributed/AttributedDestructuringPolicy.cs @@ -21,6 +21,9 @@ using Serilog.Core; using Serilog.Debugging; using Serilog.Events; +#if NETSTANDARD2_1_OR_GREATER +using System.ComponentModel.DataAnnotations; +#endif namespace Destructurama.Attributed; @@ -47,7 +50,7 @@ public bool TryDestructure(object value, ILogEventPropertyValueFactory propertyV return cached.CanDestructure; } - private static IEnumerable GetPropertiesRecursive(Type type) + private IEnumerable GetPropertiesRecursive(Type type) { var seenNames = new HashSet(); @@ -56,6 +59,27 @@ private static IEnumerable GetPropertiesRecursive(Type type) var unseenProperties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) .Where(p => p.CanRead && p.GetMethod.IsPublic && p.GetIndexParameters().Length == 0 && !seenNames.Contains(p.Name)); +#if NETSTANDARD2_1_OR_GREATER + + if (_options.UseMetadataTypeAttribute) + { + var metaProp = new List(); + // find Metadata Class + // Take only first Entry, metadatatypeAttribute definition specifies AllowMultiple=false + // see https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.metadatatypeattribute?view=net-9.0#definition + var metaDataType = type.GetCustomAttributes(true).ToList().FirstOrDefault(); + if (metaDataType != null) + { + var metaClass = metaDataType.MetadataClassType; + // find all properties with Custom Attributes which are in referenced class + metaProp = metaClass.GetProperties().Where(mp => mp.CustomAttributes.Count() > 0 && unseenProperties.Any(up => up.Name == mp.Name)).ToList(); + // replace all found properties in unseenProperties with those from Metadataclass + var removedAttr = unseenProperties.Where(up => metaProp.Any(mp => mp.Name != up.Name)).ToList(); + removedAttr.AddRange(metaProp); + unseenProperties = removedAttr; + } + } +#endif foreach (var propertyInfo in unseenProperties) { seenNames.Add(propertyInfo.Name); diff --git a/src/Destructurama.Attributed/Attributed/AttributedDestructuringPolicyOptions.cs b/src/Destructurama.Attributed/Attributed/AttributedDestructuringPolicyOptions.cs index 4d00e9a..03ddfb9 100644 --- a/src/Destructurama.Attributed/Attributed/AttributedDestructuringPolicyOptions.cs +++ b/src/Destructurama.Attributed/Attributed/AttributedDestructuringPolicyOptions.cs @@ -19,4 +19,9 @@ public class AttributedDestructuringPolicyOptions /// This works the same as when applying to the property but may help if you have no access to it's source code. /// public bool RespectLogPropertyIgnoreAttribute { get; set; } + + /// + /// Respect MetadataTypeAttribute pointing to another class with annotated properties. + /// + public bool RespectMetadataTypeAttribute { get; set; } }