diff --git a/ModelBuilder.UnitTests/ExtensionsTests.cs b/ModelBuilder.UnitTests/ExtensionsTests.cs index 607ef9e..5f6f4e0 100644 --- a/ModelBuilder.UnitTests/ExtensionsTests.cs +++ b/ModelBuilder.UnitTests/ExtensionsTests.cs @@ -100,5 +100,186 @@ public void SetThrowsExceptionWithNullInstance() action.Should().Throw(); } + + [Fact] + public void SetExpressionThrowsExceptionWithNullAction() + { + var sut = new PropertySetters(); + + Action action = () => sut.Set(null!, true); + + action.Should().Throw(); + } + + [Fact] + public void SetExpressionThrowsExceptionWithNullInstance() + { + var sut = new PropertySetters(); + + Action action = () => ((PropertySetters)null!).Set(x => x.AutoPublic, Guid.Empty); + + action.Should().Throw(); + } + + [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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + } + + [Fact] + public void SetExpressionMethodThrowsException() + { + var sut = new PropertySetters(); + + Action action = () => sut.Set(x => x.BackingFieldMethod(), default(float)); + + action.Should().Throw(); + } + + [Fact] + public void SetExpressionComplexExpressionThrowsException() + { + var sut = new PropertySetters(); + + Action action = () => sut.Set(x => x.AutoPublic.ToString(), string.Empty); + + action.Should().Throw(); + } } } \ No newline at end of file diff --git a/ModelBuilder.UnitTests/IsExternalInit.cs b/ModelBuilder.UnitTests/IsExternalInit.cs new file mode 100644 index 0000000..d86f352 --- /dev/null +++ b/ModelBuilder.UnitTests/IsExternalInit.cs @@ -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 { } +} \ No newline at end of file diff --git a/ModelBuilder.UnitTests/ModelBuilder.UnitTests.csproj b/ModelBuilder.UnitTests/ModelBuilder.UnitTests.csproj index 56ece6f..955eb9e 100644 --- a/ModelBuilder.UnitTests/ModelBuilder.UnitTests.csproj +++ b/ModelBuilder.UnitTests/ModelBuilder.UnitTests.csproj @@ -34,6 +34,7 @@ BuildConfigurationExtensionsTests.cs + diff --git a/ModelBuilder.UnitTests/Models/PropertySetters.cs b/ModelBuilder.UnitTests/Models/PropertySetters.cs new file mode 100644 index 0000000..e6dd41e --- /dev/null +++ b/ModelBuilder.UnitTests/Models/PropertySetters.cs @@ -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; + } +} \ No newline at end of file diff --git a/ModelBuilder/CommonExtensions.cs b/ModelBuilder/CommonExtensions.cs index dd74da3..c015c75 100644 --- a/ModelBuilder/CommonExtensions.cs +++ b/ModelBuilder/CommonExtensions.cs @@ -2,6 +2,9 @@ { using System; using System.Collections.Generic; + using System.Linq.Expressions; + using System.Reflection; + using System.Runtime.CompilerServices; /// /// The @@ -72,5 +75,74 @@ public static T Set(this T instance, Action action) return instance; } + + /// + /// Supports setting properties with inaccessible setters such as private or protected + /// Also limited support for setting of readonly auto-properties + /// + /// The type of instance being changed. + /// The value to set the expresison function to. + /// The instance to update. + /// The expresion function to set against the instance. + /// + /// The updated instance. + /// The parameter is null. + /// The parameter is null. + /// The parameter is not supported - readonly and complex properties are not supported. + public static T Set(this T instance, Expression> 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() != 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; + } } } \ No newline at end of file