Skip to content

Commit 2baad19

Browse files
author
Colin Wilmans
committed
DefaultSetter.cs - Enhanced member access logic to support properties and fields
Rule.cs - Improved member value retrieval and setting with better expression handling Customer.cs - Added new dictionary-based address properties for testing
1 parent 048b5b7 commit 2baad19

File tree

4 files changed

+158
-15
lines changed

4 files changed

+158
-15
lines changed

src/FluentDefaults/FluentDefaults.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<Nullable>enable</Nullable>
99
<PackageId>FluentDefaults</PackageId>
1010
<VersionSuffix Condition="'$(VersionSuffix)' == ''">0</VersionSuffix>
11-
<Version>1.1.$(VersionSuffix)</Version>
11+
<Version>1.2.$(VersionSuffix)</Version>
1212
<Authors>Colin Wilmans</Authors>
1313
<PackageDescription>A library for .NET that uses a fluent interface for setting default values, similar to how FluentValidation handles validations.</PackageDescription>
1414
<RepositoryUrl>https://github.com/crwsolutions/fluent-defaults.git</RepositoryUrl>
@@ -22,7 +22,7 @@
2222
<Description>A library for .NET that uses a fluent interface for setting defaults</Description>
2323
<PackageReadmeFile>README.md</PackageReadmeFile>
2424
<Title>Fluent Defaults</Title>
25-
<PackageReleaseNotes>Add check for collections with deferred execution. Added extension methods for chaining</PackageReleaseNotes>
25+
<PackageReleaseNotes>Improved member value retrieval and setting with better expression handling. This also results in support for Dictionaries</PackageReleaseNotes>
2626
<GenerateDocumentationFile>true</GenerateDocumentationFile>
2727
</PropertyGroup>
2828

src/FluentDefaults/Rule.cs

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,40 +21,89 @@ internal sealed class Rule<T>
2121

2222
private TElementProperty? GetChildValue<TProperty, TElementProperty>(TProperty element)
2323
{
24-
return (TElementProperty?)(ChildMemberExpression!.Member as PropertyInfo)?.GetGetMethod()?.Invoke(element, null);
24+
var (target, member) = GetTargetAndMember(ChildMemberExpression!, element);
25+
26+
return member switch
27+
{
28+
PropertyInfo prop => (TElementProperty?)prop.GetValue(target),
29+
FieldInfo field => (TElementProperty?)field.GetValue(target),
30+
_ => throw new Exception("Unsupported member type")
31+
};
2532
}
2633

27-
private void SetChildValue<TProperty, TElementProperty>(TProperty element, TElementProperty? defaultValue)
34+
private void SetChildValue<TProperty, TElementProperty>(TProperty element, TElementProperty? value)
2835
{
29-
(ChildMemberExpression!.Member as PropertyInfo)?.GetSetMethod()?.Invoke(element, [defaultValue]);
36+
var (target, member) = GetTargetAndMember(ChildMemberExpression!, element);
37+
38+
switch (member)
39+
{
40+
case PropertyInfo prop:
41+
prop.GetSetMethod()?.Invoke(target, [value]);
42+
break;
43+
case FieldInfo field:
44+
field.SetValue(target, value);
45+
break;
46+
default:
47+
throw new Exception("Unsupported member type");
48+
}
3049
}
3150

3251
internal TProperty? GetMemberValue<TProperty>(T instance)
3352
{
34-
return MemberExpression.Member switch
53+
var (target, member) = GetTargetAndMember(MemberExpression, instance);
54+
return member switch
3555
{
36-
PropertyInfo propertyInfo => (TProperty?)propertyInfo.GetGetMethod()?.Invoke(instance, null),
37-
FieldInfo fieldInfo => (TProperty?)fieldInfo.GetValue(instance),
56+
PropertyInfo prop => (TProperty?)prop.GetValue(target),
57+
FieldInfo field => (TProperty?)field.GetValue(target),
3858
_ => throw new Exception("Unsupported member type")
3959
};
4060
}
4161

42-
private void SetMemberValue<TProperty>(T instance, TProperty? defaultValue)
62+
private void SetMemberValue<TProperty>(T instance, TProperty? value)
4363
{
44-
if (MemberExpression.Member is PropertyInfo propertyInfo)
64+
var (target, member) = GetTargetAndMember(MemberExpression, instance);
65+
switch (member)
4566
{
46-
propertyInfo.GetSetMethod()?.Invoke(instance, [defaultValue]);
67+
case PropertyInfo prop:
68+
prop.GetSetMethod()?.Invoke(target, [value]);
69+
break;
70+
case FieldInfo field:
71+
field.SetValue(target, value);
72+
break;
73+
default:
74+
throw new Exception("Member is not a Property or Field");
4775
}
48-
else if (MemberExpression.Member is FieldInfo fieldInfo)
76+
}
77+
78+
79+
private static (object? target, MemberInfo member) GetTargetAndMember(Expression memberExpression, object? instance)
80+
{
81+
object? current = instance;
82+
var stack = new Stack<MemberExpression>();
83+
84+
Expression? expr = memberExpression;
85+
while (expr is MemberExpression m)
4986
{
50-
fieldInfo.SetValue(instance, defaultValue);
87+
stack.Push(m);
88+
expr = m.Expression;
5189
}
52-
else
90+
91+
while (stack.Count > 1)
5392
{
54-
throw new Exception("Member is not a Property or Field");
93+
var m = stack.Pop();
94+
current = m.Member switch
95+
{
96+
PropertyInfo prop => prop.GetValue(current),
97+
FieldInfo field => field.GetValue(current),
98+
_ => throw new Exception("Unsupported member type")
99+
};
55100
}
101+
102+
var lastMember = stack.Pop();
103+
return (current, lastMember.Member);
56104
}
57105

106+
58107
internal MemberExpression? ChildMemberExpression { get; set; }
59108

60109
internal Rule(MemberExpression memberExpression)
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
using FluentDefaults.Tests.Model;
2+
3+
namespace FluentDefaults.Tests;
4+
5+
public class DictionaryDefaultForTests
6+
{
7+
[Fact]
8+
public void DictionaryWithDefaulter_ShouldGetThatDefault()
9+
{
10+
var customer = new Customer();
11+
var defaulter = new DictionaryCustomerWithDefaulterDefaulter();
12+
13+
defaulter.Apply(customer);
14+
15+
Assert.Equal("Default Street", customer.Addresses4["key1"].Street);
16+
Assert.Equal("Default City", customer.Addresses4["key1"].City);
17+
Assert.Equal("Street 1", customer.Addresses5["key1"].Street);
18+
}
19+
20+
[Fact]
21+
public void DictionaryWithDefaultFor_ShouldGetThatDefault()
22+
{
23+
var customer = new Customer();
24+
var defaulter = new DictionaryCustomerWithDefaultForDefaulter();
25+
26+
defaulter.Apply(customer);
27+
var numberedAddresses = customer.Addresses5;
28+
29+
Assert.Equal("- unknown city -", customer.Addresses5.First().Value.City);
30+
Assert.Equal("- unknown street -", customer.Addresses5.First().Value.Street);
31+
Assert.Equal(1, numberedAddresses["key1"].Id);
32+
Assert.Equal(2, numberedAddresses["key2"].Id);
33+
Assert.Equal(3, numberedAddresses["key3"].Id);
34+
Assert.Equal("5 street", customer.Addresses4.First().Value.Street);
35+
}
36+
}
37+
38+
internal sealed class DictionaryAddressDefaulter : AbstractDefaulter<KeyValuePair<string, Address>>
39+
{
40+
internal DictionaryAddressDefaulter()
41+
{
42+
DefaultFor(x => x.Value.Street).Is("Default Street");
43+
DefaultFor(x => x.Value.City).Is("Default City");
44+
}
45+
}
46+
47+
internal sealed class DictionaryAddressDefaulterWithParameter : AbstractDefaulter<KeyValuePair<string, Address>>
48+
{
49+
internal DictionaryAddressDefaulterWithParameter(Customer customer)
50+
{
51+
DefaultFor(x => x.Value.Street).Is($"Street {customer.Number1}");
52+
}
53+
}
54+
55+
internal sealed class DictionaryCustomerWithDefaulterDefaulter : AbstractDefaulter<Customer>
56+
{
57+
internal DictionaryCustomerWithDefaulterDefaulter()
58+
{
59+
DefaultFor(x => x.Number1).Is(1);
60+
ForEach(x => x.Addresses4).SetDefaulter(new DictionaryAddressDefaulter());
61+
ForEach(x => x.Addresses5).SetDefaulter((x) => new DictionaryAddressDefaulterWithParameter(x));
62+
}
63+
}
64+
65+
internal sealed class DictionaryCustomerWithDefaultForDefaulter : AbstractDefaulter<Customer>
66+
{
67+
int _number = 0;
68+
69+
internal DictionaryCustomerWithDefaultForDefaulter()
70+
{
71+
ForEach(x => x.Addresses5).DefaultFor(x => x.Value.City).Is("- unknown city -");
72+
ForEach(x => x.Addresses5).DefaultFor(x => x.Value.Street).Is(() => "- unknown street -");
73+
ForEach(x => x.Addresses5).DefaultFor(x => x.Value.Id).Is(GetNumber);
74+
ForEach(x => x.Addresses4).DefaultFor(x => x.Value.Street).Is(GetSome);
75+
}
76+
77+
private string GetSome(Customer x) => $"{x.Number5} street";
78+
79+
private int GetNumber() => ++_number;
80+
}
81+

tests/FluentDefaults.Tests/Model/Customer.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,19 @@ internal sealed class Customer
1313
public Address[] Addresses1 { get; set; } = [new Address()];
1414
public List<Address> Addresses2 { get; set; } = [new Address(), new Address(), new Address()];
1515
public Address[] Addresses3 { get; set; } = [new Address()];
16+
public Dictionary<string, Address> Addresses4 { get; set; } = new Dictionary<string, Address>
17+
{
18+
{ "key1", new Address() },
19+
{ "key2", new Address() },
20+
{ "key3", new Address() }
21+
};
22+
23+
public Dictionary<string, Address> Addresses5 { get; set; } = new Dictionary<string, Address>
24+
{
25+
{ "key1", new Address() },
26+
{ "key2", new Address() },
27+
{ "key3", new Address() }
28+
};
1629

1730
public Address Address1 { get; set; } = new Address();
1831
public Address Address2 { get; set; } = default!;

0 commit comments

Comments
 (0)