Skip to content

Commit b9d050a

Browse files
Fix Configuration.Binder for collection properties with no setters (#75723)
Binding to an IDictionary/ICollection/ISet in ConfigurationBinder with no setter was failing because we were returning too early. Only returning early now if we were able to set the property, or if the interface is read-only. Fix #75626 Co-authored-by: Eric Erhardt <eric.erhardt@microsoft.com>
1 parent e869f44 commit b9d050a

File tree

3 files changed

+72
-19
lines changed

3 files changed

+72
-19
lines changed

src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -299,43 +299,45 @@ private static void BindInstance(
299299

300300
if (config != null && config.GetChildren().Any())
301301
{
302-
// for arrays, collections, and read-only list-like interfaces, we concatenate on to what is already there
302+
// for arrays, collections, and read-only list-like interfaces, we concatenate on to what is already there, if we can
303303
if (type.IsArray || IsArrayCompatibleInterface(type))
304304
{
305305
if (!bindingPoint.IsReadOnly)
306306
{
307307
bindingPoint.SetValue(BindArray(type, (IEnumerable?)bindingPoint.Value, config, options));
308+
return;
309+
}
310+
311+
// for getter-only collection properties that we can't add to, nothing more we can do
312+
if (type.IsArray || IsImmutableArrayCompatibleInterface(type))
313+
{
314+
return;
308315
}
309-
return;
310316
}
311317

312-
// for sets and read-only set interfaces, we clone what's there into a new collection.
313-
if (TypeIsASetInterface(type))
318+
// for sets and read-only set interfaces, we clone what's there into a new collection, if we can
319+
if (TypeIsASetInterface(type) && !bindingPoint.IsReadOnly)
314320
{
315-
if (!bindingPoint.IsReadOnly)
321+
object? newValue = BindSet(type, (IEnumerable?)bindingPoint.Value, config, options);
322+
if (newValue != null)
316323
{
317-
object? newValue = BindSet(type, (IEnumerable?)bindingPoint.Value, config, options);
318-
if (newValue != null)
319-
{
320-
bindingPoint.SetValue(newValue);
321-
}
324+
bindingPoint.SetValue(newValue);
322325
}
326+
323327
return;
324328
}
325329

326330
// For other mutable interfaces like ICollection<>, IDictionary<,> and ISet<>, we prefer copying values and setting them
327331
// on a new instance of the interface over populating the existing instance implementing the interface.
328332
// This has already been done, so there's not need to check again.
329-
if (TypeIsADictionaryInterface(type))
333+
if (TypeIsADictionaryInterface(type) && !bindingPoint.IsReadOnly)
330334
{
331-
if (!bindingPoint.IsReadOnly)
335+
object? newValue = BindDictionaryInterface(bindingPoint.Value, type, config, options);
336+
if (newValue != null)
332337
{
333-
object? newValue = BindDictionaryInterface(bindingPoint.Value, type, config, options);
334-
if (newValue != null)
335-
{
336-
bindingPoint.SetValue(newValue);
337-
}
338+
bindingPoint.SetValue(newValue);
338339
}
340+
339341
return;
340342
}
341343

@@ -848,6 +850,16 @@ private static bool IsArrayCompatibleInterface(Type type)
848850
|| genericTypeDefinition == typeof(IReadOnlyList<>);
849851
}
850852

853+
private static bool IsImmutableArrayCompatibleInterface(Type type)
854+
{
855+
if (!type.IsInterface || !type.IsConstructedGenericType) { return false; }
856+
857+
Type genericTypeDefinition = type.GetGenericTypeDefinition();
858+
return genericTypeDefinition == typeof(IEnumerable<>)
859+
|| genericTypeDefinition == typeof(IReadOnlyCollection<>)
860+
|| genericTypeDefinition == typeof(IReadOnlyList<>);
861+
}
862+
851863
private static bool TypeIsASetInterface(Type type)
852864
{
853865
if (!type.IsInterface || !type.IsConstructedGenericType) { return false; }

src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ public string ReadOnly
7272

7373
public ISet<string> InstantiatedISet { get; set; } = new HashSet<string>();
7474

75+
public ISet<string> ISetNoSetter { get; } = new HashSet<string>();
76+
7577
public HashSet<string> InstantiatedHashSetWithSomeValues { get; set; } =
7678
new HashSet<string>(new[] {"existing1", "existing2"});
7779

@@ -662,6 +664,27 @@ public void CanBindNonInstantiatedISet()
662664
Assert.Equal("Yo2", options.NonInstantiatedISet.ElementAt(1));
663665
}
664666

667+
[Fact]
668+
public void CanBindISetNoSetter()
669+
{
670+
var dic = new Dictionary<string, string>
671+
{
672+
{"ISetNoSetter:0", "Yo1"},
673+
{"ISetNoSetter:1", "Yo2"},
674+
{"ISetNoSetter:2", "Yo2"},
675+
};
676+
var configurationBuilder = new ConfigurationBuilder();
677+
configurationBuilder.AddInMemoryCollection(dic);
678+
679+
var config = configurationBuilder.Build();
680+
681+
var options = config.Get<ComplexOptions>()!;
682+
683+
Assert.Equal(2, options.ISetNoSetter.Count);
684+
Assert.Equal("Yo1", options.ISetNoSetter.ElementAt(0));
685+
Assert.Equal("Yo2", options.ISetNoSetter.ElementAt(1));
686+
}
687+
665688
#if NETCOREAPP
666689
[Fact]
667690
public void CanBindInstantiatedIReadOnlySet()

src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationCollectionBindingTests.cs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -579,7 +579,10 @@ public void AlreadyInitializedStringDictionaryBinding()
579579
{
580580
{"AlreadyInitializedStringDictionaryInterface:abc", "val_1"},
581581
{"AlreadyInitializedStringDictionaryInterface:def", "val_2"},
582-
{"AlreadyInitializedStringDictionaryInterface:ghi", "val_3"}
582+
{"AlreadyInitializedStringDictionaryInterface:ghi", "val_3"},
583+
584+
{"IDictionaryNoSetter:Key1", "Value1"},
585+
{"IDictionaryNoSetter:Key2", "Value2"},
583586
};
584587

585588
var configurationBuilder = new ConfigurationBuilder();
@@ -596,6 +599,10 @@ public void AlreadyInitializedStringDictionaryBinding()
596599
Assert.Equal("val_1", options.AlreadyInitializedStringDictionaryInterface["abc"]);
597600
Assert.Equal("val_2", options.AlreadyInitializedStringDictionaryInterface["def"]);
598601
Assert.Equal("val_3", options.AlreadyInitializedStringDictionaryInterface["ghi"]);
602+
603+
Assert.Equal(2, options.IDictionaryNoSetter.Count);
604+
Assert.Equal("Value1", options.IDictionaryNoSetter["Key1"]);
605+
Assert.Equal("Value2", options.IDictionaryNoSetter["Key2"]);
599606
}
600607

601608
[Fact]
@@ -1059,7 +1066,10 @@ public void CanBindInitializedIEnumerableAndTheOriginalItemsAreNotMutated()
10591066
{"AlreadyInitializedIEnumerableInterface:0", "val0"},
10601067
{"AlreadyInitializedIEnumerableInterface:1", "val1"},
10611068
{"AlreadyInitializedIEnumerableInterface:2", "val2"},
1062-
{"AlreadyInitializedIEnumerableInterface:x", "valx"}
1069+
{"AlreadyInitializedIEnumerableInterface:x", "valx"},
1070+
1071+
{"ICollectionNoSetter:0", "val0"},
1072+
{"ICollectionNoSetter:1", "val1"},
10631073
};
10641074

10651075
var configurationBuilder = new ConfigurationBuilder();
@@ -1084,6 +1094,10 @@ public void CanBindInitializedIEnumerableAndTheOriginalItemsAreNotMutated()
10841094
Assert.Equal(2, options.ListUsedInIEnumerableFieldAndShouldNotBeTouched.Count);
10851095
Assert.Equal("This was here too", options.ListUsedInIEnumerableFieldAndShouldNotBeTouched.ElementAt(0));
10861096
Assert.Equal("Don't touch me!", options.ListUsedInIEnumerableFieldAndShouldNotBeTouched.ElementAt(1));
1097+
1098+
Assert.Equal(2, options.ICollectionNoSetter.Count);
1099+
Assert.Equal("val0", options.ICollectionNoSetter.ElementAt(0));
1100+
Assert.Equal("val1", options.ICollectionNoSetter.ElementAt(1));
10871101
}
10881102

10891103
[Fact]
@@ -1424,6 +1438,8 @@ public InitializedCollectionsOptions()
14241438
new CustomListIndirectlyDerivedFromIEnumerable();
14251439

14261440
public IReadOnlyDictionary<string, string> AlreadyInitializedDictionary { get; set; }
1441+
1442+
public ICollection<string> ICollectionNoSetter { get; } = new List<string>();
14271443
}
14281444

14291445
private class CustomList : List<string>
@@ -1564,6 +1580,8 @@ public OptionsWithDictionary()
15641580

15651581
public Dictionary<string, string> StringDictionary { get; set; }
15661582

1583+
public IDictionary<string, string> IDictionaryNoSetter { get; } = new Dictionary<string, string>();
1584+
15671585
public Dictionary<string, NestedOptions> ObjectDictionary { get; set; }
15681586

15691587
public Dictionary<string, ISet<string>> ISetDictionary { get; set; }

0 commit comments

Comments
 (0)