diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Emitter.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Emitter.cs index 4231c227003757..9edc39feb44bfe 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Emitter.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Emitter.cs @@ -27,6 +27,7 @@ private static class GlobalName public const string Enum = "global::System.Enum"; public const string FromBase64String = "global::System.Convert.FromBase64String"; public const string IConfiguration = "global::Microsoft.Extensions.Configuration.IConfiguration"; + public const string IConfigurationSection = "global::Microsoft.Extensions.Configuration.IConfigurationSection"; public const string Int32 = "int"; public const string IServiceCollection = "global::Microsoft.Extensions.DependencyInjection.IServiceCollection"; public const string Object = "object"; @@ -75,6 +76,8 @@ public void Emit() EmitBindMethods(); + EmitIConfigurationHasChildrenHelperMethod(); + _writer.WriteBlockEnd(); SourceText source = SourceText.From(_writer.GetSource(), Encoding.UTF8); @@ -363,7 +366,7 @@ private void EmitBindCoreImplForProperty(PropertySpec property, TypeSpec propert string propertyParentReference = property.IsStatic ? parentType.DisplayString : Literal.obj; string expressionForPropertyAccess = $"{propertyParentReference}.{property.Name}"; - string expressionForConfigGetSection = $@"{Literal.configuration}.{Literal.GetSection}(""{configurationKeyName}"")"; + string expressionForConfigSectionAccess = $@"{Literal.configuration}.{Literal.GetSection}(""{configurationKeyName}"")"; string expressionForConfigValueIndexer = $@"{Literal.configuration}[""{configurationKeyName}""]"; bool canGet = property.CanGet; @@ -394,12 +397,12 @@ private void EmitBindCoreImplForProperty(PropertySpec property, TypeSpec propert property, propertyType, expressionForPropertyAccess, - expressionForConfigArg: expressionForConfigGetSection); + expressionForConfigSectionAccess); } break; case TypeSpecKind.IConfigurationSection: { - EmitAssignment(expressionForPropertyAccess, expressionForConfigGetSection); + EmitAssignment(expressionForPropertyAccess, expressionForConfigSectionAccess); } break; case TypeSpecKind.Nullable: @@ -414,7 +417,7 @@ private void EmitBindCoreImplForProperty(PropertySpec property, TypeSpec propert property, propertyType, expressionForPropertyAccess, - expressionForConfigArg: expressionForConfigGetSection); + expressionForConfigSectionAccess); } break; } @@ -491,8 +494,12 @@ private void EmitBindCoreCallForProperty( PropertySpec property, TypeSpec effectivePropertyType, string expressionForPropertyAccess, - string expressionForConfigArg) + string expressionForConfigSectionAccess) { + string bindCoreConfigArg = GetIncrementalVarName(Literal.section); + EmitAssignment($"{GlobalName.IConfigurationSection} {bindCoreConfigArg}", expressionForConfigSectionAccess); + _writer.WriteBlockStart($"if ({Literal.HasChildren}({bindCoreConfigArg}))"); + bool canGet = property.CanGet; bool canSet = property.CanSet; @@ -523,7 +530,7 @@ private void EmitBindCoreCallForProperty( EmitObjectInit(effectivePropertyType, tempVarName, InitializationKind.Declaration); } - _writer.WriteLine($@"{Literal.BindCore}({expressionForConfigArg}, ref {tempVarName});"); + _writer.WriteLine($@"{Literal.BindCore}({bindCoreConfigArg}, ref {tempVarName});"); EmitAssignment(expressionForPropertyAccess, tempVarName); _privateBindCoreMethodGen_QueuedTypes.Enqueue(effectivePropertyType); } @@ -532,7 +539,7 @@ private void EmitBindCoreCallForProperty( { EmitAssignment($"{effectivePropertyType.DisplayString} {tempVarName}", $"{expressionForPropertyAccess}"); EmitObjectInit(effectivePropertyType, tempVarName, InitializationKind.AssignmentWithNullCheck); - _writer.WriteLine($@"{Literal.BindCore}({expressionForConfigArg}, ref {tempVarName});"); + _writer.WriteLine($@"{Literal.BindCore}({bindCoreConfigArg}, ref {tempVarName});"); if (canSet) { @@ -543,10 +550,12 @@ private void EmitBindCoreCallForProperty( { Debug.Assert(canSet); EmitObjectInit(effectivePropertyType, tempVarName, InitializationKind.Declaration); - _writer.WriteLine($@"{Literal.BindCore}({expressionForConfigArg}, ref {tempVarName});"); + _writer.WriteLine($@"{Literal.BindCore}({bindCoreConfigArg}, ref {tempVarName});"); EmitAssignment(expressionForPropertyAccess, tempVarName); } + _writer.WriteBlockEnd(); + _privateBindCoreMethodGen_QueuedTypes.Enqueue(effectivePropertyType); } @@ -609,7 +618,7 @@ private void EmitObjectInit(TypeSpec type, string expressionForMemberAccess, Ini { return; } - else if (type is CollectionSpec { ConcreteType: { } concreteType}) + else if (type is CollectionSpec { ConcreteType: { } concreteType }) { displayString = concreteType.DisplayString; } @@ -639,6 +648,16 @@ private void EmitCastToIConfigurationSection() _writer.WriteBlockEnd(); } + private void EmitIConfigurationHasChildrenHelperMethod() + { + _writer.WriteBlockStart($"public static bool {Literal.HasChildren}({GlobalName.IConfiguration} {Literal.configuration})"); + _writer.WriteBlockStart($"foreach ({GlobalName.IConfigurationSection} {Literal.section} in {Literal.configuration}.{Literal.GetChildren}())"); + _writer.WriteLine($"return true;"); + _writer.WriteBlockEnd(); + _writer.WriteLine($"return false;"); + _writer.WriteBlockEnd(); + } + private void EmitVarDeclaration(TypeSpec type, string varName) => _writer.WriteLine($"{type.DisplayString} {varName};"); private void EmitAssignment(string lhsSource, string rhsSource) => _writer.WriteLine($"{lhsSource} = {rhsSource};"); diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Helpers.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Helpers.cs index 6264a7612b0054..d4b3bc3a661b77 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Helpers.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Helpers.cs @@ -3,7 +3,6 @@ using System; using System.Diagnostics; -using System.Linq; using System.Text; using Microsoft.CodeAnalysis; @@ -61,7 +60,10 @@ private static class Literal public const string Get = nameof(Get); public const string GetChildren = nameof(GetChildren); public const string GetSection = nameof(GetSection); + public const string HasChildren = nameof(HasChildren); public const string HasValue = nameof(HasValue); + public const string IConfiguration = nameof(IConfiguration); + public const string IConfigurationSection = nameof(IConfigurationSection); public const string Length = nameof(Length); public const string Parse = nameof(Parse); public const string Resize = nameof(Resize); diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Parser.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Parser.cs index 78db3a5aac9129..da8474e54a6c0a 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Parser.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Parser.cs @@ -355,12 +355,18 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, Location? loc private ObjectSpec? CreateObjectSpec(INamedTypeSymbol type, Location? location) { + Debug.Assert(!_createdSpecs.ContainsKey(type)); + + // Add spec to cache before traversing properties to avoid stack overflow. + if (!CanConstructObject(type, location)) { + _createdSpecs.Add(type, null); return null; } + ObjectSpec objectSpec = new(type) { Location = location, ConstructionStrategy = ConstructionStrategy.ParameterlessConstructor }; + _createdSpecs.Add(type, objectSpec); - List properties = new(); INamedTypeSymbol current = type; while (current != null) { @@ -385,7 +391,7 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, Location? loc PropertySpec spec = new PropertySpec(property) { Type = propertyTypeSpec, ConfigurationKeyName = configKeyName }; if (spec.CanGet || spec.CanSet) { - properties.Add(spec); + objectSpec.Properties.Add(spec); } } } @@ -394,7 +400,7 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, Location? loc current = current.BaseType; } - return new ObjectSpec(type) { Location = location, Properties = properties, ConstructionStrategy = ConstructionStrategy.ParameterlessConstructor }; + return objectSpec; } private bool IsCandidateEnumerable(INamedTypeSymbol type, out ITypeSymbol? elementType) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ObjectSpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ObjectSpec.cs index 752654f25e9ecd..63f6f38709ba1f 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ObjectSpec.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ObjectSpec.cs @@ -10,6 +10,6 @@ internal sealed record ObjectSpec : TypeSpec { public ObjectSpec(INamedTypeSymbol type) : base(type) { } public override TypeSpecKind SpecKind => TypeSpecKind.Object; - public required List Properties { get; init; } + public List Properties { get; } = new(); } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs index 4518348cbf540c..6d5e1fbeb13338 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs @@ -1434,5 +1434,75 @@ public void EnsureCallingThePropertySetter() Assert.Equal(401, options.HttpStatusCode); // exists in configuration and properly sets the property Assert.Equal(2, options.OtherCode); // doesn't exist in configuration. the setter sets default value '2' } + + [Fact] + public void RecursiveTypeGraphs_DirectRef() + { + var data = @"{ + ""MyString"":""Hello"", + ""MyClass"": { + ""MyString"": ""World"", + ""MyClass"": { + ""MyString"": ""World"", + ""MyClass"": null + } + } + }"; + + var configuration = new ConfigurationBuilder() + .AddJsonStream(TestStreamHelpers.StringToStream(data)) + .Build(); + + var obj = configuration.Get(); + Assert.Equal("Hello", obj.MyString); + + var nested = obj.MyClass; + Assert.Equal("World", nested.MyString); + + var deeplyNested = nested.MyClass; + Assert.Equal("World", deeplyNested.MyString); + Assert.Null(deeplyNested.MyClass); + } + + public class ClassWithDirectSelfReference + { + public string MyString { get; set; } + public ClassWithDirectSelfReference MyClass { get; set; } + } + + [Fact] + public void RecursiveTypeGraphs_IndirectRef() + { + var data = @"{ + ""MyString"":""Hello"", + ""MyList"": [{ + ""MyString"": ""World"", + ""MyList"": [{ + ""MyString"": ""World"", + ""MyClass"": null + }] + }] + }"; + + var configuration = new ConfigurationBuilder() + .AddJsonStream(TestStreamHelpers.StringToStream(data)) + .Build(); + + var obj = configuration.Get(); + Assert.Equal("Hello", obj.MyString); + + var nested = obj.MyList[0]; + Assert.Equal("World", nested.MyString); + + var deeplyNested = nested.MyList[0]; + Assert.Equal("World", deeplyNested.MyString); + Assert.Null(deeplyNested.MyList); + } + + public class ClassWithIndirectSelfReference + { + public string MyString { get; set; } + public List MyList { get; set; } + } } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestBindCallGen.generated.txt b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestBindCallGen.generated.txt index 6420c215b3d23f..5a0abd17224edf 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestBindCallGen.generated.txt +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestBindCallGen.generated.txt @@ -24,20 +24,32 @@ internal static class GeneratedConfigurationBinder obj.MyInt = int.Parse(stringValue1); } - System.Collections.Generic.List temp2 = obj.MyList; - temp2 ??= new System.Collections.Generic.List(); - BindCore(configuration.GetSection("MyList"), ref temp2); - obj.MyList = temp2; + global::Microsoft.Extensions.Configuration.IConfigurationSection section2 = configuration.GetSection("MyList"); + if (HasChildren(section2)) + { + System.Collections.Generic.List temp3 = obj.MyList; + temp3 ??= new System.Collections.Generic.List(); + BindCore(section2, ref temp3); + obj.MyList = temp3; + } - System.Collections.Generic.Dictionary temp3 = obj.MyDictionary; - temp3 ??= new System.Collections.Generic.Dictionary(); - BindCore(configuration.GetSection("MyDictionary"), ref temp3); - obj.MyDictionary = temp3; + global::Microsoft.Extensions.Configuration.IConfigurationSection section4 = configuration.GetSection("MyDictionary"); + if (HasChildren(section4)) + { + System.Collections.Generic.Dictionary temp5 = obj.MyDictionary; + temp5 ??= new System.Collections.Generic.Dictionary(); + BindCore(section4, ref temp5); + obj.MyDictionary = temp5; + } - System.Collections.Generic.Dictionary temp4 = obj.MyComplexDictionary; - temp4 ??= new System.Collections.Generic.Dictionary(); - BindCore(configuration.GetSection("MyComplexDictionary"), ref temp4); - obj.MyComplexDictionary = temp4; + global::Microsoft.Extensions.Configuration.IConfigurationSection section6 = configuration.GetSection("MyComplexDictionary"); + if (HasChildren(section6)) + { + System.Collections.Generic.Dictionary temp7 = obj.MyComplexDictionary; + temp7 ??= new System.Collections.Generic.Dictionary(); + BindCore(section6, ref temp7); + obj.MyComplexDictionary = temp7; + } } @@ -51,9 +63,9 @@ internal static class GeneratedConfigurationBinder int element; foreach (Microsoft.Extensions.Configuration.IConfigurationSection section in configuration.GetChildren()) { - if (section.Value is string stringValue5) + if (section.Value is string stringValue8) { - element = int.Parse(stringValue5); + element = int.Parse(stringValue8); obj.Add(element); } } @@ -69,13 +81,13 @@ internal static class GeneratedConfigurationBinder string key; foreach (Microsoft.Extensions.Configuration.IConfigurationSection section in configuration.GetChildren()) { - if (section.Key is string stringValue6) + if (section.Key is string stringValue9) { - key = stringValue6; + key = stringValue9; string element; - if (section.Value is string stringValue7) + if (section.Value is string stringValue10) { - element = stringValue7; + element = stringValue10; obj[key] = element; } } @@ -92,9 +104,9 @@ internal static class GeneratedConfigurationBinder string key; foreach (Microsoft.Extensions.Configuration.IConfigurationSection section in configuration.GetChildren()) { - if (section.Key is string stringValue8) + if (section.Key is string stringValue11) { - key = stringValue8; + key = stringValue11; if (obj.TryGetValue(key, out Program.MyClass2? element) && element is not null) { BindCore(section, ref element); @@ -119,4 +131,12 @@ internal static class GeneratedConfigurationBinder } + public static bool HasChildren(global::Microsoft.Extensions.Configuration.IConfiguration configuration) + { + foreach (global::Microsoft.Extensions.Configuration.IConfigurationSection section in configuration.GetChildren()) + { + return true; + } + return false; + } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestConfigureCallGen.generated.txt b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestConfigureCallGen.generated.txt index cd255ce893cd8e..f03c9097c33a9f 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestConfigureCallGen.generated.txt +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestConfigureCallGen.generated.txt @@ -35,15 +35,23 @@ internal static class GeneratedConfigurationBinder obj.MyInt = int.Parse(stringValue2); } - System.Collections.Generic.List temp3 = obj.MyList; - temp3 ??= new System.Collections.Generic.List(); - BindCore(configuration.GetSection("MyList"), ref temp3); - obj.MyList = temp3; + global::Microsoft.Extensions.Configuration.IConfigurationSection section3 = configuration.GetSection("MyList"); + if (HasChildren(section3)) + { + System.Collections.Generic.List temp4 = obj.MyList; + temp4 ??= new System.Collections.Generic.List(); + BindCore(section3, ref temp4); + obj.MyList = temp4; + } - System.Collections.Generic.Dictionary temp4 = obj.MyDictionary; - temp4 ??= new System.Collections.Generic.Dictionary(); - BindCore(configuration.GetSection("MyDictionary"), ref temp4); - obj.MyDictionary = temp4; + global::Microsoft.Extensions.Configuration.IConfigurationSection section5 = configuration.GetSection("MyDictionary"); + if (HasChildren(section5)) + { + System.Collections.Generic.Dictionary temp6 = obj.MyDictionary; + temp6 ??= new System.Collections.Generic.Dictionary(); + BindCore(section5, ref temp6); + obj.MyDictionary = temp6; + } } @@ -57,9 +65,9 @@ internal static class GeneratedConfigurationBinder int element; foreach (Microsoft.Extensions.Configuration.IConfigurationSection section in configuration.GetChildren()) { - if (section.Value is string stringValue5) + if (section.Value is string stringValue7) { - element = int.Parse(stringValue5); + element = int.Parse(stringValue7); obj.Add(element); } } @@ -75,17 +83,25 @@ internal static class GeneratedConfigurationBinder string key; foreach (Microsoft.Extensions.Configuration.IConfigurationSection section in configuration.GetChildren()) { - if (section.Key is string stringValue6) + if (section.Key is string stringValue8) { - key = stringValue6; + key = stringValue8; string element; - if (section.Value is string stringValue7) + if (section.Value is string stringValue9) { - element = stringValue7; + element = stringValue9; obj[key] = element; } } } } + public static bool HasChildren(global::Microsoft.Extensions.Configuration.IConfiguration configuration) + { + foreach (global::Microsoft.Extensions.Configuration.IConfigurationSection section in configuration.GetChildren()) + { + return true; + } + return false; + } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestGetCallGen.generated.txt b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestGetCallGen.generated.txt index bddaf65a9bd8e6..b0ff688d005e27 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestGetCallGen.generated.txt +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestGetCallGen.generated.txt @@ -45,15 +45,23 @@ internal static class GeneratedConfigurationBinder obj.MyInt = int.Parse(stringValue2); } - System.Collections.Generic.List temp3 = obj.MyList; - temp3 ??= new System.Collections.Generic.List(); - BindCore(configuration.GetSection("MyList"), ref temp3); - obj.MyList = temp3; + global::Microsoft.Extensions.Configuration.IConfigurationSection section3 = configuration.GetSection("MyList"); + if (HasChildren(section3)) + { + System.Collections.Generic.List temp4 = obj.MyList; + temp4 ??= new System.Collections.Generic.List(); + BindCore(section3, ref temp4); + obj.MyList = temp4; + } - System.Collections.Generic.Dictionary temp4 = obj.MyDictionary; - temp4 ??= new System.Collections.Generic.Dictionary(); - BindCore(configuration.GetSection("MyDictionary"), ref temp4); - obj.MyDictionary = temp4; + global::Microsoft.Extensions.Configuration.IConfigurationSection section5 = configuration.GetSection("MyDictionary"); + if (HasChildren(section5)) + { + System.Collections.Generic.Dictionary temp6 = obj.MyDictionary; + temp6 ??= new System.Collections.Generic.Dictionary(); + BindCore(section5, ref temp6); + obj.MyDictionary = temp6; + } } @@ -67,9 +75,9 @@ internal static class GeneratedConfigurationBinder int element; foreach (Microsoft.Extensions.Configuration.IConfigurationSection section in configuration.GetChildren()) { - if (section.Value is string stringValue5) + if (section.Value is string stringValue7) { - element = int.Parse(stringValue5); + element = int.Parse(stringValue7); obj.Add(element); } } @@ -85,17 +93,25 @@ internal static class GeneratedConfigurationBinder string key; foreach (Microsoft.Extensions.Configuration.IConfigurationSection section in configuration.GetChildren()) { - if (section.Key is string stringValue6) + if (section.Key is string stringValue8) { - key = stringValue6; + key = stringValue8; string element; - if (section.Value is string stringValue7) + if (section.Value is string stringValue9) { - element = stringValue7; + element = stringValue9; obj[key] = element; } } } } + public static bool HasChildren(global::Microsoft.Extensions.Configuration.IConfiguration configuration) + { + foreach (global::Microsoft.Extensions.Configuration.IConfigurationSection section in configuration.GetChildren()) + { + return true; + } + return false; + } }