Skip to content

Commit 38810c0

Browse files
authored
Introduce IgnoreOptions and allow to ignore specific values (#54)
1 parent bb8f28a commit 38810c0

6 files changed

Lines changed: 276 additions & 19 deletions

File tree

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
11
namespace Destructurama
22
{
3+
public sealed class IgnoreOptions<TDestructure>
4+
{
5+
public IgnoreOptions() { }
6+
public Destructurama.IgnoreOptions<TDestructure> DestructureAssignableTo() { }
7+
public Destructurama.IgnoreOptions<TDestructure> DestructureExactType() { }
8+
public Destructurama.IgnoreOptions<TDestructure> Ignore(System.Linq.Expressions.Expression<System.Func<TDestructure, object?>> ignoredProperty) { }
9+
public Destructurama.IgnoreOptions<TDestructure> IgnoreValue(System.Func<System.Reflection.PropertyInfo, object, bool> ignoreValuePredicate) { }
10+
}
311
public static class LoggerConfigurationIgnoreExtensions
412
{
13+
public static Serilog.LoggerConfiguration ByIgnoring<TDestructure>(this Serilog.Configuration.LoggerDestructuringConfiguration configuration, Destructurama.IgnoreOptions<TDestructure> options) { }
14+
public static Serilog.LoggerConfiguration ByIgnoring<TDestructure>(this Serilog.Configuration.LoggerDestructuringConfiguration configuration, System.Action<Destructurama.IgnoreOptions<TDestructure>> configure) { }
515
public static Serilog.LoggerConfiguration ByIgnoringProperties<TDestructure>(this Serilog.Configuration.LoggerDestructuringConfiguration configuration, params System.Linq.Expressions.Expression<System.Func<TDestructure, object?>>[] ignoredProperties) { }
616
public static Serilog.LoggerConfiguration ByIgnoringPropertiesOfTypeAssignableTo<TDestructure>(this Serilog.Configuration.LoggerDestructuringConfiguration configuration, params System.Linq.Expressions.Expression<System.Func<TDestructure, object?>>[] ignoredProperties) { }
717
public static Serilog.LoggerConfiguration ByIgnoringPropertiesWhere(this Serilog.Configuration.LoggerDestructuringConfiguration configuration, System.Func<object, bool> handleDestructuringPredicate, params System.Func<System.Reflection.PropertyInfo, bool>[] ignoredPropertyPredicates) { }
8-
public static Serilog.LoggerConfiguration ByIgnoringPropertiesWhere<TDestruture>(this Serilog.Configuration.LoggerDestructuringConfiguration configuration, System.Func<object, bool> handleDestructuringPredicate, params System.Linq.Expressions.Expression<System.Func<TDestruture, object?>>[] ignoredProperties) { }
18+
public static Serilog.LoggerConfiguration ByIgnoringPropertiesWhere<TDestructure>(this Serilog.Configuration.LoggerDestructuringConfiguration configuration, System.Func<object, bool> handleDestructuringPredicate, params System.Linq.Expressions.Expression<System.Func<TDestructure, object?>>[] ignoredProperties) { }
919
}
1020
}

src/Destructurama.ByIgnoring.Tests/TestCases/ByIgnoreWhereTestCases.cs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -101,13 +101,6 @@ public static IEnumerable<ByIgnoreWhereExceptionTestCase> ShouldThrowExceptionTe
101101
IgnoredPropertyPredicates = null!,
102102
ExceptionType = typeof(ArgumentNullException),
103103
};
104-
105-
yield return new ByIgnoreWhereExceptionTestCase("empty ignoredPropertyPredicates")
106-
{
107-
HandleDestructuringPredicate = obj => obj is IDestructureMe,
108-
IgnoredPropertyPredicates = Array.Empty<Func<PropertyInfo, bool>>(),
109-
ExceptionType = typeof(ArgumentOutOfRangeException),
110-
};
111104
}
112105

113106
private static IEnumerable<ByIgnoreWhereTestCase> Convert<T>(IEnumerable<ByIgnoringTestCase<T>> input)

src/Destructurama.ByIgnoring.Tests/Tests/DestructureByIgnoringTests.cs

Lines changed: 137 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public void Throwing_Accessor_Should_Be_Handled()
8686
[Test]
8787
public void TryDestructure_Should_Return_False_When_Called_With_Null()
8888
{
89-
var policy = new DestructureByIgnoringPolicy(_ => true, _ => true);
89+
var policy = new DestructureByIgnoringPolicy(_ => true, (_, _) => false, _ => true);
9090
policy.TryDestructure(null!, null!, out _).ShouldBeFalse();
9191
}
9292

@@ -112,14 +112,148 @@ public void TryDestructure_Should_Work_For_Structs()
112112
sv.Properties[0].Value.ShouldBeOfType<ScalarValue>().Value.ShouldBe("Tom");
113113
}
114114

115-
public struct DestructureMeStruct
115+
[Test]
116+
public void TryDestructure_Should_Ignore_Property_From_Options()
117+
{
118+
// Setup
119+
LogEvent evt = null!;
120+
121+
var log = new LoggerConfiguration()
122+
.Destructure.ByIgnoring<DestructureMeClass>(o => o.Ignore(x => x.Id))
123+
.WriteTo.Sink(new DelegatingSink(e => evt = e))
124+
.CreateLogger();
125+
var obj = new DestructureMeClass { Id = 42 };
126+
127+
// Execute
128+
log.Information("Here is {@Ignored}", obj);
129+
130+
// Verify
131+
var sv = (StructureValue)evt.Properties["Ignored"];
132+
sv.Properties.Count.ShouldBe(1);
133+
sv.Properties[0].Name.ShouldBe("Name");
134+
sv.Properties[0].Value.ShouldBeOfType<ScalarValue>().Value.ShouldBe("Tom");
135+
}
136+
137+
[Test]
138+
public void TryDestructure_Should_Ignore_Null_String()
139+
{
140+
// Setup
141+
LogEvent evt = null!;
142+
143+
var log = new LoggerConfiguration()
144+
.Destructure.ByIgnoring<DestructureMeClass>(o => o.IgnoreValue((_, v) => v is null))
145+
.WriteTo.Sink(new DelegatingSink(e => evt = e))
146+
.CreateLogger();
147+
var obj = new DestructureMeClass { Name = null };
148+
149+
// Execute
150+
log.Information("Here is {@Ignored}", obj);
151+
152+
// Verify
153+
var sv = (StructureValue)evt.Properties["Ignored"];
154+
sv.Properties.Count.ShouldBe(1);
155+
sv.Properties[0].Name.ShouldBe("Id");
156+
sv.Properties[0].Value.ShouldBeOfType<ScalarValue>().Value.ShouldBe(0);
157+
}
158+
159+
[Test]
160+
public void TryDestructure_Should_Ignore_Custom_Value()
161+
{
162+
// Setup
163+
LogEvent evt = null!;
164+
165+
var log = new LoggerConfiguration()
166+
.Destructure.ByIgnoring<DestructureMeClass>(o => o.IgnoreValue((p, v) => p.Name is "Id" && v is 42))
167+
.WriteTo.Sink(new DelegatingSink(e => evt = e))
168+
.CreateLogger();
169+
var obj = new DestructureMeClass { Id = 42 };
170+
171+
// Execute
172+
log.Information("Here is {@Ignored}", obj);
173+
174+
// Verify
175+
var sv = (StructureValue)evt.Properties["Ignored"];
176+
sv.Properties.Count.ShouldBe(1);
177+
sv.Properties[0].Name.ShouldBe("Name");
178+
sv.Properties[0].Value.ShouldBeOfType<ScalarValue>().Value.ShouldBe("Tom");
179+
}
180+
181+
[Test]
182+
public void TryDestructure_Should_Ignore_All_AssignableTo()
183+
{
184+
// Setup
185+
LogEvent evt = null!;
186+
187+
var log = new LoggerConfiguration()
188+
.Destructure.ByIgnoring<IDestructureMe>(o => o.IgnoreValue((_, v) => v is null).DestructureAssignableTo())
189+
.WriteTo.Sink(new DelegatingSink(e => evt = e))
190+
.CreateLogger();
191+
var obj1 = new DestructureMeStruct { Name = null };
192+
var obj2 = new DestructureMeClass { Name = null };
193+
194+
// Execute
195+
log.Information("Here is {@Ignored1} and {@Ignored2}", obj1, obj2);
196+
197+
// Verify
198+
var sv = (StructureValue)evt.Properties["Ignored1"];
199+
sv.Properties.Count.ShouldBe(1);
200+
sv.Properties[0].Name.ShouldBe("Id");
201+
sv.Properties[0].Value.ShouldBeOfType<ScalarValue>().Value.ShouldBe(0);
202+
sv = (StructureValue)evt.Properties["Ignored2"];
203+
sv.Properties.Count.ShouldBe(1);
204+
sv.Properties[0].Name.ShouldBe("Id");
205+
sv.Properties[0].Value.ShouldBeOfType<ScalarValue>().Value.ShouldBe(0);
206+
}
207+
208+
[Test]
209+
public void TryDestructure_Should_Ignore_Exact_Type()
210+
{
211+
// Setup
212+
LogEvent evt = null!;
213+
214+
var log = new LoggerConfiguration()
215+
.Destructure.ByIgnoring<DestructureMeClass>(o => o.IgnoreValue((_, v) => v is null).DestructureExactType())
216+
.WriteTo.Sink(new DelegatingSink(e => evt = e))
217+
.CreateLogger();
218+
var obj1 = new DestructureMeStruct { Name = null };
219+
var obj2 = new DestructureMeClass { Name = null };
220+
221+
// Execute
222+
log.Information("Here is {@Ignored1} and {@Ignored2}", obj1, obj2);
223+
224+
// Verify
225+
var sv = (StructureValue)evt.Properties["Ignored1"];
226+
sv.Properties.Count.ShouldBe(2);
227+
sv.Properties[0].Name.ShouldBe("Id");
228+
sv.Properties[0].Value.ShouldBeOfType<ScalarValue>().Value.ShouldBe(0);
229+
sv.Properties[1].Name.ShouldBe("Name");
230+
sv.Properties[1].Value.ShouldBeOfType<ScalarValue>().Value.ShouldBeNull();
231+
232+
sv = (StructureValue)evt.Properties["Ignored2"];
233+
sv.Properties.Count.ShouldBe(1);
234+
sv.Properties[0].Name.ShouldBe("Id");
235+
sv.Properties[0].Value.ShouldBeOfType<ScalarValue>().Value.ShouldBe(0);
236+
}
237+
238+
public struct DestructureMeStruct : IDestructureMe
116239
{
117240
public DestructureMeStruct()
118241
{
119242
}
120243

121244
public int Id { get; set; }
122245

123-
public string Name { get; set; } = "Tom";
246+
public string? Name { get; set; } = "Tom";
247+
}
248+
249+
public interface IDestructureMe
250+
{
251+
}
252+
253+
public class DestructureMeClass : IDestructureMe
254+
{
255+
public int Id { get; set; }
256+
257+
public string? Name { get; set; } = "Tom";
124258
}
125259
}

src/Destructurama.ByIgnoring/ByIgnoring/DestructureByIgnoringPolicy.cs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,19 @@ internal sealed class DestructureByIgnoringPolicy : IDestructuringPolicy
2626
{
2727
private readonly Func<object, bool> _handleDestructuringPredicate;
2828
private readonly Func<PropertyInfo, bool>[] _ignoredPropertyPredicates;
29+
private readonly Func<PropertyInfo, object, bool> _ignoreValuePredicate;
2930

3031
private readonly ConcurrentDictionary<Type, (PropertyInfo Property, Func<object, object> Accessor)[]> _cache = new();
3132

32-
public DestructureByIgnoringPolicy(Func<object, bool> handleDestructuringPredicate, params Func<PropertyInfo, bool>[] ignoredPropertyPredicates)
33+
public DestructureByIgnoringPolicy(Func<object, bool> handleDestructuringPredicate, Func<PropertyInfo, object, bool> ignoreValuePredicate, params Func<PropertyInfo, bool>[] ignoredPropertyPredicates)
3334
{
3435
_handleDestructuringPredicate = handleDestructuringPredicate ?? throw new ArgumentNullException(nameof(handleDestructuringPredicate));
36+
_ignoreValuePredicate = ignoreValuePredicate;
3537
_ignoredPropertyPredicates = ignoredPropertyPredicates ?? throw new ArgumentNullException(nameof(ignoredPropertyPredicates));
3638

37-
if (ignoredPropertyPredicates.Length == 0)
38-
throw new ArgumentOutOfRangeException(nameof(ignoredPropertyPredicates), "At least one ignore rule must be supplied");
39+
// After introducing ignoreValuePredicate caller now may not specify any value for ignoredPropertyPredicates.
40+
// if (ignoredPropertyPredicates.Length == 0)
41+
// throw new ArgumentOutOfRangeException(nameof(ignoredPropertyPredicates), "At least one ignore rule must be supplied");
3942
}
4043

4144
public bool TryDestructure(object value, ILogEventPropertyValueFactory propertyValueFactory, [NotNullWhen(true)] out LogEventPropertyValue? result)
@@ -76,25 +79,30 @@ static Func<object, object> Compile(PropertyInfo property)
7679
}
7780
}
7881

79-
private static StructureValue BuildStructure(object value, ILogEventPropertyValueFactory propertyValueFactory, (PropertyInfo Property, Func<object, object> Accessor)[] propertiesToInclude, Type destructureType)
82+
private StructureValue BuildStructure(object value, ILogEventPropertyValueFactory propertyValueFactory, (PropertyInfo Property, Func<object, object> Accessor)[] propertiesToInclude, Type destructureType)
8083
{
8184
var structureProperties = new List<LogEventProperty>();
8285
foreach (var (propertyInfo, accessor) in propertiesToInclude)
8386
{
8487
object propertyValue;
88+
bool ignoreValue = false;
8589
try
8690
{
8791
propertyValue = accessor(value);
92+
ignoreValue = _ignoreValuePredicate(propertyInfo, propertyValue);
8893
}
8994
catch (Exception ex)
9095
{
9196
SelfLog.WriteLine("The property accessor {0} threw exception {1}", propertyInfo, ex);
9297
propertyValue = "The property accessor threw an exception: " + ex.GetType().Name;
9398
}
9499

95-
var logEventPropertyValue = BuildLogEventProperty(propertyValue, propertyValueFactory);
100+
if (!ignoreValue)
101+
{
102+
var logEventPropertyValue = BuildLogEventProperty(propertyValue, propertyValueFactory);
96103

97-
structureProperties.Add(new LogEventProperty(propertyInfo.Name, logEventPropertyValue));
104+
structureProperties.Add(new LogEventProperty(propertyInfo.Name, logEventPropertyValue));
105+
}
98106
}
99107

100108
return new StructureValue(structureProperties, destructureType.Name);
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright 2017 Serilog Contributors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
using System.Linq.Expressions;
16+
using System.Reflection;
17+
using Destructurama.ByIgnoring;
18+
19+
namespace Destructurama;
20+
21+
/// <summary>
22+
/// Options for family of extension methods of <see cref="LoggerConfigurationIgnoreExtensions"/>.
23+
/// </summary>
24+
/// <typeparam name="TDestructure">Type of object to be destructured by Serilog.</typeparam>
25+
public sealed class IgnoreOptions<TDestructure>
26+
{
27+
/// <summary>
28+
/// Given an object to destructure, should this policy take effect?
29+
/// </summary>
30+
internal Func<object, bool> HandleDestructuringPredicate { get; set; } = obj => obj.GetType() == typeof(TDestructure);
31+
32+
/// <summary>
33+
/// When the predicate returns true for a provided property, said will be ignored when destructured by Serilog.
34+
/// </summary>
35+
internal List<Func<PropertyInfo, bool>> IgnoredPropertyPredicates { get; set; } = new();
36+
37+
/// <summary>
38+
/// Predicate to ignore properties with specific values.
39+
/// </summary>
40+
internal Func<PropertyInfo, object, bool> IgnoreValuePredicate { get; set; } = (_, _) => false;
41+
42+
/// <summary>
43+
/// Policy takes effect when object's type is <typeparamref name="TDestructure"/>.
44+
/// </summary>
45+
/// <returns>Reference to itself.</returns>
46+
public IgnoreOptions<TDestructure> DestructureExactType()
47+
{
48+
HandleDestructuringPredicate = obj => obj.GetType() == typeof(TDestructure);
49+
return this;
50+
}
51+
52+
/// <summary>
53+
/// Policy takes effect when object of type assignable to <typeparamref name="TDestructure"/>.
54+
/// </summary>
55+
/// <returns>Reference to itself.</returns>
56+
public IgnoreOptions<TDestructure> DestructureAssignableTo()
57+
{
58+
HandleDestructuringPredicate = obj => obj is TDestructure;
59+
return this;
60+
}
61+
62+
/// <summary>
63+
/// Takes expression that access a property, e.g. obj => obj.Property,
64+
/// and uses the property name to determine which property is ignored.
65+
/// </summary>
66+
/// <param name="ignoredProperty">The function expression that expose the property to ignore.</param>
67+
/// <returns>Reference to itself.</returns>
68+
public IgnoreOptions<TDestructure> Ignore(Expression<Func<TDestructure, object?>> ignoredProperty)
69+
{
70+
var name = ignoredProperty.GetPropertyNameFromExpression();
71+
IgnoredPropertyPredicates.Add(pi => pi.Name == name);
72+
return this;
73+
}
74+
75+
/// <summary>
76+
/// Takes predicate to ignore properties with specific values.
77+
/// For example, it allows you to ignore all properties that return null or empty strings:
78+
/// <code>
79+
/// IgnoreValue((_, v) => v is null || v is string s &amp;&amp; s.Length == 0);
80+
/// </code>
81+
/// </summary>
82+
/// <param name="ignoreValuePredicate">Predicate to ignore properties with specific values.</param>
83+
/// <returns>Reference to itself.</returns>
84+
public IgnoreOptions<TDestructure> IgnoreValue(Func<PropertyInfo, object, bool> ignoreValuePredicate)
85+
{
86+
IgnoreValuePredicate = ignoreValuePredicate;
87+
return this;
88+
}
89+
}
90+

src/Destructurama.ByIgnoring/LoggerConfigurationIgnoreExtensions.cs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,28 @@ namespace Destructurama;
2525
/// </summary>
2626
public static class LoggerConfigurationIgnoreExtensions
2727
{
28+
/// <summary>
29+
/// Takes options to determine which properties are ignored when an object is destructured by Serilog.
30+
/// </summary>
31+
/// <param name="configuration">The logger configuration to apply configuration to.</param>
32+
/// <param name="configure">Delegate to configure <see cref="IgnoreOptions{TDestructure}"/>.</param>
33+
/// <returns>An object allowing configuration to continue.</returns>
34+
public static LoggerConfiguration ByIgnoring<TDestructure>(this LoggerDestructuringConfiguration configuration, Action<IgnoreOptions<TDestructure>> configure)
35+
{
36+
IgnoreOptions<TDestructure> options = new();
37+
configure?.Invoke(options);
38+
return configuration.ByIgnoring(options);
39+
}
40+
41+
/// <summary>
42+
/// Takes options to determine which properties are ignored when an object is destructured by Serilog.
43+
/// </summary>
44+
/// <param name="configuration">The logger configuration to apply configuration to.</param>
45+
/// <param name="options"><see cref="IgnoreOptions{TDestructure}"/>.</param>
46+
/// <returns>An object allowing configuration to continue.</returns>
47+
public static LoggerConfiguration ByIgnoring<TDestructure>(this LoggerDestructuringConfiguration configuration, IgnoreOptions<TDestructure> options) =>
48+
configuration.With(new DestructureByIgnoringPolicy(options.HandleDestructuringPredicate, options.IgnoreValuePredicate, options.IgnoredPropertyPredicates.ToArray()));
49+
2850
/// <summary>
2951
/// Takes one or more expressions that access a property, e.g. obj => obj.Property,
3052
/// and uses the property names to determine which properties are ignored when an
@@ -56,7 +78,7 @@ public static LoggerConfiguration ByIgnoringPropertiesOfTypeAssignableTo<TDestru
5678
/// <param name="handleDestructuringPredicate">Given an object to destructure, should this policy take effect?</param>
5779
/// <param name="ignoredProperties">The function expressions that expose the properties to ignore.</param>
5880
/// <returns>An object allowing configuration to continue.</returns>
59-
public static LoggerConfiguration ByIgnoringPropertiesWhere<TDestruture>(this LoggerDestructuringConfiguration configuration, Func<object, bool> handleDestructuringPredicate, params Expression<Func<TDestruture, object?>>[] ignoredProperties)
81+
public static LoggerConfiguration ByIgnoringPropertiesWhere<TDestructure>(this LoggerDestructuringConfiguration configuration, Func<object, bool> handleDestructuringPredicate, params Expression<Func<TDestructure, object?>>[] ignoredProperties)
6082
{
6183
return configuration.ByIgnoringPropertiesWhere(
6284
handleDestructuringPredicate,
@@ -76,5 +98,5 @@ public static LoggerConfiguration ByIgnoringPropertiesWhere<TDestruture>(this Lo
7698
/// <param name="ignoredPropertyPredicates">When the predicate returns true for a provided property, said will be ignored when destructured by Serilog.</param>
7799
/// <returns>An object allowing configuration to continue.</returns>
78100
public static LoggerConfiguration ByIgnoringPropertiesWhere(this LoggerDestructuringConfiguration configuration, Func<object, bool> handleDestructuringPredicate, params Func<PropertyInfo, bool>[] ignoredPropertyPredicates) =>
79-
configuration.With(new DestructureByIgnoringPolicy(handleDestructuringPredicate, ignoredPropertyPredicates));
101+
configuration.With(new DestructureByIgnoringPolicy(handleDestructuringPredicate, (_, _) => false, ignoredPropertyPredicates));
80102
}

0 commit comments

Comments
 (0)