Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Set extension method that supports setting properties with inaccessible setters #202

Merged
merged 2 commits into from
Aug 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}
}
}