Skip to content

Commit

Permalink
Added Set extension method that supports setting properties with inac…
Browse files Browse the repository at this point in the history
…cessible setters (#202)

* Support for setting properties with inaccessible setters
* Added null parameter unit tests

Co-authored-by: Andy <andrew.berman@rxpservices.com>
  • Loading branch information
bermo and Andy authored Aug 29, 2021
1 parent 812c1ff commit f1046a4
Show file tree
Hide file tree
Showing 5 changed files with 296 additions and 0 deletions.
181 changes: 181 additions & 0 deletions ModelBuilder.UnitTests/ExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,5 +100,186 @@ public void SetThrowsExceptionWithNullInstance()

action.Should().Throw<ArgumentNullException>();
}

[Fact]
public void SetExpressionThrowsExceptionWithNullAction()
{
var sut = new PropertySetters();

Action action = () => sut.Set(null!, true);

action.Should().Throw<ArgumentNullException>();
}

[Fact]
public void SetExpressionThrowsExceptionWithNullInstance()
{
var sut = new PropertySetters();

Action action = () => ((PropertySetters)null!).Set(x => x.AutoPublic, Guid.Empty);

action.Should().Throw<ArgumentNullException>();
}

[Fact]
public void SetExpressionAutoPublicSetter()
{
var sut = new PropertySetters();
var expected = Guid.NewGuid();

var actual = sut.Set(x => x.AutoPublic, expected);

actual.AutoPublic.Should().Be(expected);
sut.AutoPublic.Should().Be(expected);
}

[Fact]
public void SetExpressionAutoReadonlySetter()
{
var sut = new PropertySetters();

var actual = sut.Set(x => x.AutoReadonly, null);

actual.AutoReadonly.Should().BeNull();
sut.AutoReadonly.Should().BeNull();
}

[Fact]
public void SetExpressionAutoPrivateSetter()
{
var sut = new PropertySetters();
var expected = Model.Create<int>();

var actual = sut.Set(x => x.AutoPrivate, expected);

actual.AutoPrivate.Should().Be(expected);
sut.AutoPrivate.Should().Be(expected);
}

[Fact]
public void SetExpressionAutoProtectedSetter()
{
var sut = new PropertySetters();
var expected = Model.Create<decimal>();

var actual = sut.Set(x => x.AutoProtected, expected);

actual.AutoProtected.Should().Be(expected);
sut.AutoProtected.Should().Be(expected);
}

[Fact]
public void SetExpressionAutoProtectedInternalSetter()
{
var sut = new PropertySetters();
var expected = Model.Create<Uri>();

var actual = sut.Set(x => x.AutoProtectedInternal, expected);

actual.AutoProtectedInternal.Should().BeEquivalentTo(expected);
sut.AutoProtectedInternal.Should().BeEquivalentTo(expected);
}

[Fact]
public void SetExpressionAutoInternalSetter()
{
var sut = new PropertySetters();
var expected = Model.Create<DateTimeOffset>();

var actual = sut.Set(x => x.AutoInternal, expected);

actual.AutoInternal.Should().Be(expected);
sut.AutoInternal.Should().Be(expected);
}

[Fact]
public void SetExpressionAutoPrivateInternalSetter()
{
var sut = new PropertySetters();
var expected = Model.Create<PropertySetters>();

var actual = sut.Set(x => x.AutoPrivateInternal, expected);

actual.AutoPrivateInternal.Should().BeEquivalentTo(expected);
sut.AutoPrivateInternal.Should().BeEquivalentTo(expected);
}

[Fact]
public void SetExpressionAutoInitSetter()
{
var sut = new PropertySetters();
var expected = Model.Create<ConsoleColor>();

var actual = sut.Set(x => x.AutoInit, expected);

actual.AutoInit.Should().Be(expected);
sut.AutoInit.Should().Be(expected);
}

[Fact]
public void SetExpressionBackingField()
{
var sut = new PropertySetters();
var expected = Model.Create<float>();

var actual = sut.Set(x => x._backingField, expected);

actual._backingField.Should().Be(expected);
sut._backingField.Should().Be(expected);
}

[Fact]
public void SetExpressionPublicBackingFieldSetter()
{
var sut = new PropertySetters();
var expected = Model.Create<float>();

var actual = sut.Set(x => x.PublicBackingField, expected);

actual.PublicBackingField.Should().Be(expected);
sut.PublicBackingField.Should().Be(expected);
}

[Fact]
public void SetExpressionPrivateBackingFieldSetter()
{
var sut = new PropertySetters();
var expected = Model.Create<float>();

var actual = sut.Set(x => x.PrivateBackingField, expected);

actual.PrivateBackingField.Should().Be(expected);
sut.PrivateBackingField.Should().Be(expected);
}

[Fact]
public void SetExpressionReadonlySetterThrowsException()
{
var sut = new PropertySetters();

Action action = () => sut.Set(x => x.Readonly, null);

action.Should().Throw<NotSupportedException>();
}

[Fact]
public void SetExpressionMethodThrowsException()
{
var sut = new PropertySetters();

Action action = () => sut.Set(x => x.BackingFieldMethod(), default(float));

action.Should().Throw<NotSupportedException>();
}

[Fact]
public void SetExpressionComplexExpressionThrowsException()
{
var sut = new PropertySetters();

Action action = () => sut.Set(x => x.AutoPublic.ToString(), string.Empty);

action.Should().Throw<NotSupportedException>();
}
}
}
9 changes: 9 additions & 0 deletions ModelBuilder.UnitTests/IsExternalInit.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// the following namespace/class is required to be able to use init setters with framework versions lower than 5.0:
// https://www.mking.net/blog/error-cs0518-isexternalinit-not-defined
namespace System.Runtime.CompilerServices
{
using System.ComponentModel;

[EditorBrowsable(EditorBrowsableState.Never)]
internal static class IsExternalInit { }
}
1 change: 1 addition & 0 deletions ModelBuilder.UnitTests/ModelBuilder.UnitTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
<Compile Update="BuildConfigurationExtensionsTests.*.cs">
<DependentUpon>BuildConfigurationExtensionsTests.cs</DependentUpon>
</Compile>
<Compile Remove="IsExternalInit.cs" Condition="'$(TargetFramework)' == 'net5.0'" />
</ItemGroup>

<ItemGroup>
Expand Down
33 changes: 33 additions & 0 deletions ModelBuilder.UnitTests/Models/PropertySetters.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
namespace ModelBuilder.UnitTests.Models
{
using System;

public class PropertySetters
{
public Guid AutoPublic { get; set; }

public string? AutoReadonly { get; } = string.Empty;

public int AutoPrivate { get; private set; }

public decimal AutoProtected { get; protected set; }

public Uri? AutoProtectedInternal { get; protected internal set; }

public DateTimeOffset AutoInternal { get; internal set; }

public PropertySetters? AutoPrivateInternal { get; private protected set; }

public ConsoleColor AutoInit { get; init; }

internal float _backingField;
public float BackingFieldMethod() => _backingField;

public float PublicBackingField { get => _backingField; set => _backingField = value; }
public float BackingField { get => _backingField; set => _backingField = value; }

public float PrivateBackingField { get => _backingField; private set => _backingField = value; }

public char? Readonly => default;
}
}
72 changes: 72 additions & 0 deletions ModelBuilder/CommonExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
{
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;

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

return instance;
}

/// <summary>
/// Supports setting properties with inaccessible setters such as private or protected
/// Also limited support for setting of readonly auto-properties
/// </summary>
/// <typeparam name="T">The type of instance being changed.</typeparam>
/// <typeparam name="TVALUE">The value to set the expresison function to.</typeparam>
/// <param name="instance">The instance to update.</param>
/// <param name="expressionFunc">The expresion function to set against the instance.</param>
/// <param name="value"></param>
/// <returns>The updated instance.</returns>
/// <exception cref="ArgumentNullException">The <paramref name="instance" /> parameter is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException">The <paramref name="expressionFunc" /> parameter is <c>null</c>.</exception>
/// <exception cref="NotSupportedException">The <paramref name="expressionFunc" /> parameter is not supported - readonly and complex properties are not supported.</exception>
public static T Set<T, TVALUE>(this T instance, Expression<Func<T, TVALUE>> expressionFunc, TVALUE value)
{
instance = instance ?? throw new ArgumentNullException(nameof(instance));
expressionFunc = expressionFunc ?? throw new ArgumentNullException(nameof(expressionFunc));

var memberExpression = expressionFunc.Body as MemberExpression;
if (memberExpression == null)
{
throw new NotSupportedException("Only properties and fields are supported");
}

var member = memberExpression.Member;

var methodInfo = member as MethodInfo;
if (methodInfo != null)
{
throw new NotSupportedException("Methods are not supported");
}

var propertyInfo = member as PropertyInfo;
if (propertyInfo != null)
{
if (propertyInfo.GetSetMethod(true) != null)
{
propertyInfo.SetValue(instance, value);
return instance;
}

var declaringType = propertyInfo.DeclaringType;
if (declaringType == null)
{
throw new NotSupportedException("Could not find declaring type");
}

var backingField = declaringType.GetField($"<{propertyInfo.Name}>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance);
if (backingField != null && backingField.GetCustomAttribute<CompilerGeneratedAttribute>() != null)
{
member = backingField;
}
else
{
throw new NotSupportedException("Could not find a backing field - readonly properties are not supported");
}
}

var fieldInfo = member as FieldInfo;
if (fieldInfo == null)
{
throw new NotSupportedException("The member is not supported");
}

fieldInfo.SetValue(instance, value);

return instance;
}
}
}

0 comments on commit f1046a4

Please sign in to comment.