Skip to content

Commit f1046a4

Browse files
bermoAndy
andauthored
Added Set extension method that supports setting properties with inaccessible setters (#202)
* Support for setting properties with inaccessible setters * Added null parameter unit tests Co-authored-by: Andy <andrew.berman@rxpservices.com>
1 parent 812c1ff commit f1046a4

5 files changed

Lines changed: 296 additions & 0 deletions

File tree

ModelBuilder.UnitTests/ExtensionsTests.cs

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,5 +100,186 @@ public void SetThrowsExceptionWithNullInstance()
100100

101101
action.Should().Throw<ArgumentNullException>();
102102
}
103+
104+
[Fact]
105+
public void SetExpressionThrowsExceptionWithNullAction()
106+
{
107+
var sut = new PropertySetters();
108+
109+
Action action = () => sut.Set(null!, true);
110+
111+
action.Should().Throw<ArgumentNullException>();
112+
}
113+
114+
[Fact]
115+
public void SetExpressionThrowsExceptionWithNullInstance()
116+
{
117+
var sut = new PropertySetters();
118+
119+
Action action = () => ((PropertySetters)null!).Set(x => x.AutoPublic, Guid.Empty);
120+
121+
action.Should().Throw<ArgumentNullException>();
122+
}
123+
124+
[Fact]
125+
public void SetExpressionAutoPublicSetter()
126+
{
127+
var sut = new PropertySetters();
128+
var expected = Guid.NewGuid();
129+
130+
var actual = sut.Set(x => x.AutoPublic, expected);
131+
132+
actual.AutoPublic.Should().Be(expected);
133+
sut.AutoPublic.Should().Be(expected);
134+
}
135+
136+
[Fact]
137+
public void SetExpressionAutoReadonlySetter()
138+
{
139+
var sut = new PropertySetters();
140+
141+
var actual = sut.Set(x => x.AutoReadonly, null);
142+
143+
actual.AutoReadonly.Should().BeNull();
144+
sut.AutoReadonly.Should().BeNull();
145+
}
146+
147+
[Fact]
148+
public void SetExpressionAutoPrivateSetter()
149+
{
150+
var sut = new PropertySetters();
151+
var expected = Model.Create<int>();
152+
153+
var actual = sut.Set(x => x.AutoPrivate, expected);
154+
155+
actual.AutoPrivate.Should().Be(expected);
156+
sut.AutoPrivate.Should().Be(expected);
157+
}
158+
159+
[Fact]
160+
public void SetExpressionAutoProtectedSetter()
161+
{
162+
var sut = new PropertySetters();
163+
var expected = Model.Create<decimal>();
164+
165+
var actual = sut.Set(x => x.AutoProtected, expected);
166+
167+
actual.AutoProtected.Should().Be(expected);
168+
sut.AutoProtected.Should().Be(expected);
169+
}
170+
171+
[Fact]
172+
public void SetExpressionAutoProtectedInternalSetter()
173+
{
174+
var sut = new PropertySetters();
175+
var expected = Model.Create<Uri>();
176+
177+
var actual = sut.Set(x => x.AutoProtectedInternal, expected);
178+
179+
actual.AutoProtectedInternal.Should().BeEquivalentTo(expected);
180+
sut.AutoProtectedInternal.Should().BeEquivalentTo(expected);
181+
}
182+
183+
[Fact]
184+
public void SetExpressionAutoInternalSetter()
185+
{
186+
var sut = new PropertySetters();
187+
var expected = Model.Create<DateTimeOffset>();
188+
189+
var actual = sut.Set(x => x.AutoInternal, expected);
190+
191+
actual.AutoInternal.Should().Be(expected);
192+
sut.AutoInternal.Should().Be(expected);
193+
}
194+
195+
[Fact]
196+
public void SetExpressionAutoPrivateInternalSetter()
197+
{
198+
var sut = new PropertySetters();
199+
var expected = Model.Create<PropertySetters>();
200+
201+
var actual = sut.Set(x => x.AutoPrivateInternal, expected);
202+
203+
actual.AutoPrivateInternal.Should().BeEquivalentTo(expected);
204+
sut.AutoPrivateInternal.Should().BeEquivalentTo(expected);
205+
}
206+
207+
[Fact]
208+
public void SetExpressionAutoInitSetter()
209+
{
210+
var sut = new PropertySetters();
211+
var expected = Model.Create<ConsoleColor>();
212+
213+
var actual = sut.Set(x => x.AutoInit, expected);
214+
215+
actual.AutoInit.Should().Be(expected);
216+
sut.AutoInit.Should().Be(expected);
217+
}
218+
219+
[Fact]
220+
public void SetExpressionBackingField()
221+
{
222+
var sut = new PropertySetters();
223+
var expected = Model.Create<float>();
224+
225+
var actual = sut.Set(x => x._backingField, expected);
226+
227+
actual._backingField.Should().Be(expected);
228+
sut._backingField.Should().Be(expected);
229+
}
230+
231+
[Fact]
232+
public void SetExpressionPublicBackingFieldSetter()
233+
{
234+
var sut = new PropertySetters();
235+
var expected = Model.Create<float>();
236+
237+
var actual = sut.Set(x => x.PublicBackingField, expected);
238+
239+
actual.PublicBackingField.Should().Be(expected);
240+
sut.PublicBackingField.Should().Be(expected);
241+
}
242+
243+
[Fact]
244+
public void SetExpressionPrivateBackingFieldSetter()
245+
{
246+
var sut = new PropertySetters();
247+
var expected = Model.Create<float>();
248+
249+
var actual = sut.Set(x => x.PrivateBackingField, expected);
250+
251+
actual.PrivateBackingField.Should().Be(expected);
252+
sut.PrivateBackingField.Should().Be(expected);
253+
}
254+
255+
[Fact]
256+
public void SetExpressionReadonlySetterThrowsException()
257+
{
258+
var sut = new PropertySetters();
259+
260+
Action action = () => sut.Set(x => x.Readonly, null);
261+
262+
action.Should().Throw<NotSupportedException>();
263+
}
264+
265+
[Fact]
266+
public void SetExpressionMethodThrowsException()
267+
{
268+
var sut = new PropertySetters();
269+
270+
Action action = () => sut.Set(x => x.BackingFieldMethod(), default(float));
271+
272+
action.Should().Throw<NotSupportedException>();
273+
}
274+
275+
[Fact]
276+
public void SetExpressionComplexExpressionThrowsException()
277+
{
278+
var sut = new PropertySetters();
279+
280+
Action action = () => sut.Set(x => x.AutoPublic.ToString(), string.Empty);
281+
282+
action.Should().Throw<NotSupportedException>();
283+
}
103284
}
104285
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// the following namespace/class is required to be able to use init setters with framework versions lower than 5.0:
2+
// https://www.mking.net/blog/error-cs0518-isexternalinit-not-defined
3+
namespace System.Runtime.CompilerServices
4+
{
5+
using System.ComponentModel;
6+
7+
[EditorBrowsable(EditorBrowsableState.Never)]
8+
internal static class IsExternalInit { }
9+
}

ModelBuilder.UnitTests/ModelBuilder.UnitTests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
<Compile Update="BuildConfigurationExtensionsTests.*.cs">
3535
<DependentUpon>BuildConfigurationExtensionsTests.cs</DependentUpon>
3636
</Compile>
37+
<Compile Remove="IsExternalInit.cs" Condition="'$(TargetFramework)' == 'net5.0'" />
3738
</ItemGroup>
3839

3940
<ItemGroup>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
namespace ModelBuilder.UnitTests.Models
2+
{
3+
using System;
4+
5+
public class PropertySetters
6+
{
7+
public Guid AutoPublic { get; set; }
8+
9+
public string? AutoReadonly { get; } = string.Empty;
10+
11+
public int AutoPrivate { get; private set; }
12+
13+
public decimal AutoProtected { get; protected set; }
14+
15+
public Uri? AutoProtectedInternal { get; protected internal set; }
16+
17+
public DateTimeOffset AutoInternal { get; internal set; }
18+
19+
public PropertySetters? AutoPrivateInternal { get; private protected set; }
20+
21+
public ConsoleColor AutoInit { get; init; }
22+
23+
internal float _backingField;
24+
public float BackingFieldMethod() => _backingField;
25+
26+
public float PublicBackingField { get => _backingField; set => _backingField = value; }
27+
public float BackingField { get => _backingField; set => _backingField = value; }
28+
29+
public float PrivateBackingField { get => _backingField; private set => _backingField = value; }
30+
31+
public char? Readonly => default;
32+
}
33+
}

ModelBuilder/CommonExtensions.cs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
{
33
using System;
44
using System.Collections.Generic;
5+
using System.Linq.Expressions;
6+
using System.Reflection;
7+
using System.Runtime.CompilerServices;
58

69
/// <summary>
710
/// The <see cref="CommonExtensions" />
@@ -72,5 +75,74 @@ public static T Set<T>(this T instance, Action<T> action)
7275

7376
return instance;
7477
}
78+
79+
/// <summary>
80+
/// Supports setting properties with inaccessible setters such as private or protected
81+
/// Also limited support for setting of readonly auto-properties
82+
/// </summary>
83+
/// <typeparam name="T">The type of instance being changed.</typeparam>
84+
/// <typeparam name="TVALUE">The value to set the expresison function to.</typeparam>
85+
/// <param name="instance">The instance to update.</param>
86+
/// <param name="expressionFunc">The expresion function to set against the instance.</param>
87+
/// <param name="value"></param>
88+
/// <returns>The updated instance.</returns>
89+
/// <exception cref="ArgumentNullException">The <paramref name="instance" /> parameter is <c>null</c>.</exception>
90+
/// <exception cref="ArgumentNullException">The <paramref name="expressionFunc" /> parameter is <c>null</c>.</exception>
91+
/// <exception cref="NotSupportedException">The <paramref name="expressionFunc" /> parameter is not supported - readonly and complex properties are not supported.</exception>
92+
public static T Set<T, TVALUE>(this T instance, Expression<Func<T, TVALUE>> expressionFunc, TVALUE value)
93+
{
94+
instance = instance ?? throw new ArgumentNullException(nameof(instance));
95+
expressionFunc = expressionFunc ?? throw new ArgumentNullException(nameof(expressionFunc));
96+
97+
var memberExpression = expressionFunc.Body as MemberExpression;
98+
if (memberExpression == null)
99+
{
100+
throw new NotSupportedException("Only properties and fields are supported");
101+
}
102+
103+
var member = memberExpression.Member;
104+
105+
var methodInfo = member as MethodInfo;
106+
if (methodInfo != null)
107+
{
108+
throw new NotSupportedException("Methods are not supported");
109+
}
110+
111+
var propertyInfo = member as PropertyInfo;
112+
if (propertyInfo != null)
113+
{
114+
if (propertyInfo.GetSetMethod(true) != null)
115+
{
116+
propertyInfo.SetValue(instance, value);
117+
return instance;
118+
}
119+
120+
var declaringType = propertyInfo.DeclaringType;
121+
if (declaringType == null)
122+
{
123+
throw new NotSupportedException("Could not find declaring type");
124+
}
125+
126+
var backingField = declaringType.GetField($"<{propertyInfo.Name}>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance);
127+
if (backingField != null && backingField.GetCustomAttribute<CompilerGeneratedAttribute>() != null)
128+
{
129+
member = backingField;
130+
}
131+
else
132+
{
133+
throw new NotSupportedException("Could not find a backing field - readonly properties are not supported");
134+
}
135+
}
136+
137+
var fieldInfo = member as FieldInfo;
138+
if (fieldInfo == null)
139+
{
140+
throw new NotSupportedException("The member is not supported");
141+
}
142+
143+
fieldInfo.SetValue(instance, value);
144+
145+
return instance;
146+
}
75147
}
76148
}

0 commit comments

Comments
 (0)