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; }
}