{
switch (type.SpecKind)
{
- case TypeSpecKind.Array:
- {
- EmitBindCoreImplForArray((ArraySpec)type);
- }
- break;
case TypeSpecKind.Enumerable:
{
EmitBindCoreImplForEnumerable((EnumerableSpec)type);
}
}
- private void EmitBindCoreImplForArray(ArraySpec type)
+ private void EmitBindCoreImplForEnumerable(EnumerableSpec type)
{
- EnumerableSpec concreteType = (EnumerableSpec)type.ConcreteType;
+ EmitCheckForNullArgument_WithBlankLine_IfRequired(type.IsValueType);
+
+ if (type.PopulationStrategy is CollectionPopulationStrategy.Array)
+ {
+ EmitPopulationImplForArray(type);
+ }
+ else
+ {
+ EmitPopulationImplForEnumerableWithAdd(type);
+ }
+ }
- EmitCheckForNullArgument_WithBlankLine_IfRequired(isValueType: false);
+ private void EmitPopulationImplForArray(EnumerableSpec type)
+ {
+ EnumerableSpec concreteType = (EnumerableSpec)type.ConcreteType;
// Create, bind, and add elements to temp list.
string tempVarName = GetIncrementalVarName(Identifier.temp);
""");
}
- private void EmitBindCoreImplForEnumerable(EnumerableSpec type)
+ private void EmitPopulationImplForEnumerableWithAdd(EnumerableSpec type)
{
- EmitCheckForNullArgument_WithBlankLine_IfRequired(type.IsValueType);
-
TypeSpec elementType = type.ElementType;
+ EmitCollectionCastIfRequired(type, out string objIdentifier);
+
_writer.WriteBlockStart($"foreach ({Identifier.IConfigurationSection} {Identifier.section} in {Identifier.configuration}.{Identifier.GetChildren}())");
- string addStatement = $"{Identifier.obj}.{Identifier.Add}({Identifier.element})";
+ string addExpression = $"{objIdentifier}.{Identifier.Add}({Identifier.element})";
if (elementType.SpecKind is TypeSpecKind.ParsableFromString)
{
{
string tempVarName = GetIncrementalVarName(Identifier.stringValue);
_writer.WriteBlockStart($"if ({Expression.sectionValue} is string {tempVarName})");
- _writer.WriteLine($"{Identifier.obj}.{Identifier.Add}({tempVarName});");
+ _writer.WriteLine($"{objIdentifier}.{Identifier.Add}({tempVarName});");
_writer.WriteBlockEnd();
}
else
{
EmitVarDeclaration(elementType, Identifier.element);
- EmitBindLogicFromString(stringParsableType, Identifier.element, Expression.sectionValue, Expression.sectionPath, () => _writer.WriteLine($"{addStatement};"));
+ EmitBindLogicFromString(stringParsableType, Identifier.element, Expression.sectionValue, Expression.sectionPath, () => _writer.WriteLine($"{addExpression};"));
}
}
else
{
EmitBindCoreCall(elementType, Identifier.element, Identifier.section, InitializationKind.Declaration);
- _writer.WriteLine($"{addStatement};");
+ _writer.WriteLine($"{addExpression};");
}
_writer.WriteBlockEnd();
{
EmitCheckForNullArgument_WithBlankLine_IfRequired(type.IsValueType);
+ EmitCollectionCastIfRequired(type, out string objIdentifier);
+
_writer.WriteBlockStart($"foreach ({Identifier.IConfigurationSection} {Identifier.section} in {Identifier.configuration}.{Identifier.GetChildren}())");
- // Parse key
ParsableFromStringTypeSpec keyType = type.KeyType;
+ TypeSpec elementType = type.ElementType;
+ // Parse key
if (keyType.StringParsableTypeKind is StringParsableTypeKind.ConfigValue)
{
_writer.WriteLine($"{keyType.MinimalDisplayString} {Identifier.key} = {Expression.sectionKey};");
void Emit_BindAndAddLogic_ForElement()
{
- TypeSpec elementType = type.ElementType;
-
if (elementType.SpecKind == TypeSpecKind.ParsableFromString)
{
ParsableFromStringTypeSpec stringParsableType = (ParsableFromStringTypeSpec)elementType;
{
string tempVarName = GetIncrementalVarName(Identifier.stringValue);
_writer.WriteBlockStart($"if ({Expression.sectionValue} is string {tempVarName})");
- _writer.WriteLine($"{Identifier.obj}[{Identifier.key}] = {tempVarName};");
+ _writer.WriteLine($"{objIdentifier}[{Identifier.key}] = {tempVarName};");
_writer.WriteBlockEnd();
}
else
Identifier.element,
Expression.sectionValue,
Expression.sectionPath,
- () => _writer.WriteLine($"{Identifier.obj}[{Identifier.key}] = {Identifier.element};"));
+ () => _writer.WriteLine($"{objIdentifier}[{Identifier.key}] = {Identifier.element};"));
}
}
else // For complex types:
{
+ bool isValueType = elementType.IsValueType;
+ string expressionForElementIsNotNull = $"{Identifier.element} is not null";
string elementTypeDisplayString = elementType.MinimalDisplayString + (elementType.IsValueType ? string.Empty : "?");
- // If key already exists, bind to value to existing element instance if not null (for ref types).
- string conditionToUseExistingElement = $"{Identifier.obj}.{Identifier.TryGetValue}({Identifier.key}, out {elementTypeDisplayString} {Identifier.element})";
- if (!elementType.IsValueType)
+ string expressionForElementExists = $"{objIdentifier}.{Identifier.TryGetValue}({Identifier.key}, out {elementTypeDisplayString} {Identifier.element})";
+ string conditionToUseExistingElement = expressionForElementExists;
+
+ // If key already exists, bind to existing element instance if not null (for ref types).
+ if (!isValueType)
{
- conditionToUseExistingElement += $" && {Identifier.element} is not null";
+ conditionToUseExistingElement += $" && {expressionForElementIsNotNull}";
}
+
_writer.WriteBlockStart($"if (!({conditionToUseExistingElement}))");
EmitObjectInit(elementType, Identifier.element, InitializationKind.SimpleAssignment);
_writer.WriteBlockEnd();
+ if (elementType is CollectionSpec
+ {
+ ConstructionStrategy: ConstructionStrategy.ParameterizedConstructor or ConstructionStrategy.ToEnumerableMethod
+ } collectionSpec)
+ {
+ // This is a read-only collection. If the element exists and is not null,
+ // we need to copy its contents into a new instance & then append/bind to that.
+
+ string initExpression = collectionSpec.ConstructionStrategy is ConstructionStrategy.ParameterizedConstructor
+ ? $"new {collectionSpec.ConcreteType.MinimalDisplayString}({Identifier.element})"
+ : $"{Identifier.element}.{collectionSpec.ToEnumerableMethodCall!}";
+
+ _writer.WriteBlock($$"""
+ else
+ {
+ {{Identifier.element}} = {{initExpression}};
+ }
+ """);
+ }
+
EmitBindCoreCall(elementType, $"{Identifier.element}!", Identifier.section, InitializationKind.None);
- _writer.WriteLine($"{Identifier.obj}[{Identifier.key}] = {Identifier.element};");
+ _writer.WriteLine($"{objIdentifier}[{Identifier.key}] = {Identifier.element};");
}
}
{
_writer.WriteBlockStart($@"case ""{property.ConfigurationKeyName}"":");
- TypeSpec propertyType = property.Type;
+ if (property.ShouldBind())
+ {
+ EmitBindCoreImplForProperty(property, property.Type!, parentType: type);
+ }
- EmitBindCoreImplForProperty(property, propertyType, parentType: type);
_writer.WriteBlockEnd();
_writer.WriteLine("break;");
}
break;
default:
{
- EmitBindCoreCallForProperty(
- property,
- propertyType,
- expressionForPropertyAccess);
+ EmitBindCoreCallForProperty(property, propertyType, expressionForPropertyAccess);
}
break;
}
return;
}
- string displayString = GetTypeDisplayString(type);
+ string expressionForInit;
+ CollectionSpec? collectionType = type as CollectionSpec;
- string expressionForInit = null;
- if (type is ArraySpec)
+ string displayString;
+ if (collectionType is not null)
{
- expressionForInit = $"new {_arrayBracketsRegex.Replace(displayString, "[0]", 1)}";
+ if (collectionType is EnumerableSpec { PopulationStrategy: CollectionPopulationStrategy.Array })
+ {
+ displayString = GetTypeDisplayString(type);
+ expressionForInit = $"new {_arrayBracketsRegex.Replace(displayString, "[0]", 1)}";
+ }
+ else
+ {
+ displayString = GetTypeDisplayString(collectionType.ConcreteType ?? collectionType);
+ expressionForInit = $"new {displayString}()";
+ }
}
- else if (type.ConstructionStrategy != ConstructionStrategy.ParameterlessConstructor)
+ else if (type.ConstructionStrategy is ConstructionStrategy.ParameterlessConstructor)
{
- return;
+ displayString = GetTypeDisplayString(type);
+ expressionForInit = $"new {displayString}()";
}
- else if (type is CollectionSpec { ConcreteType: { } concreteType })
+ else
{
- displayString = GetTypeDisplayString(concreteType);
+ return;
}
- // Not an array.
- expressionForInit ??= $"new {displayString}()";
-
if (initKind == InitializationKind.Declaration)
{
Debug.Assert(!expressionForMemberAccess.Contains("."));
}
else if (initKind == InitializationKind.AssignmentWithNullCheck)
{
- _writer.WriteLine($"{expressionForMemberAccess} ??= {expressionForInit};");
+ ConstructionStrategy? collectionConstructionStratey = collectionType?.ConstructionStrategy;
+ if (collectionConstructionStratey is ConstructionStrategy.ParameterizedConstructor)
+ {
+ _writer.WriteLine($"{expressionForMemberAccess} = {expressionForMemberAccess} is null ? {expressionForInit} : new {displayString}({expressionForMemberAccess});");
+ }
+ else if (collectionConstructionStratey is ConstructionStrategy.ToEnumerableMethod)
+ {
+ _writer.WriteLine($"{expressionForMemberAccess} = {expressionForMemberAccess} is null ? {expressionForInit} : {expressionForMemberAccess}.{collectionType.ToEnumerableMethodCall!};");
+ }
+ else
+ {
+ _writer.WriteLine($"{expressionForMemberAccess} ??= {expressionForInit};");
+ }
}
else
{
}
}
+ private void EmitCollectionCastIfRequired(CollectionSpec type, out string objIdentifier)
+ {
+ objIdentifier = Identifier.obj;
+ if (type.PopulationStrategy is CollectionPopulationStrategy.Cast_Then_Add)
+ {
+ objIdentifier = Identifier.temp;
+ _writer.WriteBlock($$"""
+ if ({{Identifier.obj}} is not {{type.PopulationCastType!.MinimalDisplayString}} {{objIdentifier}})
+ {
+ return;
+ }
+ """);
+ _writer.WriteBlankLine();
+ }
+ }
+
private void EmitCastToIConfigurationSection()
{
string sectionTypeDisplayString;
internal sealed class Helpers
{
public static DiagnosticDescriptor TypeNotSupported { get; } = CreateTypeNotSupportedDescriptor(nameof(SR.TypeNotSupported));
- public static DiagnosticDescriptor AbstractOrInterfaceNotSupported { get; } = CreateTypeNotSupportedDescriptor(nameof(SR.AbstractOrInterfaceNotSupported));
public static DiagnosticDescriptor NeedPublicParameterlessConstructor { get; } = CreateTypeNotSupportedDescriptor(nameof(SR.NeedPublicParameterlessConstructor));
public static DiagnosticDescriptor CollectionNotSupported { get; } = CreateTypeNotSupportedDescriptor(nameof(SR.CollectionNotSupported));
public static DiagnosticDescriptor DictionaryKeyNotSupported { get; } = CreateTypeNotSupportedDescriptor(nameof(SR.DictionaryKeyNotSupported));
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
+using System.Reflection.Metadata;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Operations;
TypeSpec? spec = GetOrCreateTypeSpec(type, location);
if (spec != null)
{
- GetRootConfigTypeCache(method).Add(spec);
- GetRootConfigTypeCache(methodGroup).Add(spec);
+ AddToRootConfigTypeCache(method, spec);
+ AddToRootConfigTypeCache(methodGroup, spec);
_methodsToGen |= method;
}
{
if (spec is not null)
{
- GetRootConfigTypeCache(MethodSpecifier.BindCore).Add(spec);
+ AddToRootConfigTypeCache(MethodSpecifier.BindCore, spec);
_methodsToGen |= MethodSpecifier.BindCore;
}
}
}
- private HashSet<TypeSpec> GetRootConfigTypeCache(MethodSpecifier method)
+ private void AddToRootConfigTypeCache(MethodSpecifier method, TypeSpec spec)
{
+ Debug.Assert(spec is not null);
+
if (!_rootConfigTypes.TryGetValue(method, out HashSet<TypeSpec> types))
{
_rootConfigTypes[method] = types = new HashSet<TypeSpec>();
}
- return types;
+ types.Add(spec);
}
private static bool IsNullable(ITypeSymbol type, [NotNullWhen(true)] out ITypeSymbol? underlyingType)
{
spec = GetOrCreateTypeSpec(type);
- if (spec == null)
+ if (spec is null)
{
ReportUnsupportedType(type, descriptor);
return false;
return true;
}
- private ArraySpec? CreateArraySpec(IArrayTypeSymbol arrayType, Location? location)
+ private EnumerableSpec? CreateArraySpec(IArrayTypeSymbol arrayType, Location? location)
{
if (!TryGetTypeSpec(arrayType.ElementType, Helpers.ElementTypeNotSupported, out TypeSpec elementSpec))
{
}
// We want a BindCore method for List<TElement> as a temp holder for the array values.
- EnumerableSpec? listSpec = ConstructAndCacheGenericTypeForBindCore(_typeSymbols.List, arrayType.ElementType) as EnumerableSpec;
+ EnumerableSpec? listSpec = GetOrCreateTypeSpec(_typeSymbols.List.Construct(arrayType.ElementType)) as EnumerableSpec;
// We know the element type is supported.
Debug.Assert(listSpec != null);
+ if (listSpec is not null)
+ {
+ AddToRootConfigTypeCache(MethodSpecifier.BindCore, listSpec);
+ }
- return new ArraySpec(arrayType)
+ return new EnumerableSpec(arrayType)
{
Location = location,
ElementType = elementSpec,
ConcreteType = listSpec,
+ PopulationStrategy = CollectionPopulationStrategy.Array,
+ ToEnumerableMethodCall = null,
};
}
{
return CreateDictionarySpec(type, location, keyType, elementType);
}
- else if (IsCandidateEnumerable(type, out elementType))
- {
- return CreateEnumerableSpec(type, location, elementType);
- }
- return null;
+ return CreateEnumerableSpec(type, location);
}
private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, Location? location, ITypeSymbol keyType, ITypeSymbol elementType)
return null;
}
- DictionarySpec? concreteType = null;
- if (IsInterfaceMatch(type, _typeSymbols.GenericIDictionary) || IsInterfaceMatch(type, _typeSymbols.IDictionary))
+ ConstructionStrategy constructionStrategy;
+ CollectionPopulationStrategy populationStrategy;
+ INamedTypeSymbol? concreteType = null;
+ INamedTypeSymbol? populationCastType = null;
+ string? toEnumerableMethodCall = null;
+
+ if (HasPublicParameterlessCtor(type))
+ {
+ constructionStrategy = ConstructionStrategy.ParameterlessConstructor;
+
+ if (HasAddMethod(type, keyType, elementType))
+ {
+ populationStrategy = CollectionPopulationStrategy.Add;
+ }
+ else if (GetInterface(type, _typeSymbols.GenericIDictionary_Unbound) is not null)
+ {
+ populationCastType = _typeSymbols.GenericIDictionary;
+ populationStrategy = CollectionPopulationStrategy.Cast_Then_Add;
+ }
+ else
+ {
+ ReportUnsupportedType(type, Helpers.CollectionNotSupported, location);
+ return null;
+ }
+ }
+ else if (IsInterfaceMatch(type, _typeSymbols.GenericIDictionary_Unbound) || IsInterfaceMatch(type, _typeSymbols.IDictionary))
+ {
+ concreteType = _typeSymbols.Dictionary;
+ constructionStrategy = ConstructionStrategy.ParameterlessConstructor;
+ populationStrategy = CollectionPopulationStrategy.Add;
+ }
+ else if (IsInterfaceMatch(type, _typeSymbols.IReadOnlyDictionary_Unbound))
{
- // We know the key and element types are supported.
- concreteType = ConstructAndCacheGenericTypeForBindCore(_typeSymbols.Dictionary, keyType, elementType) as DictionarySpec;
- Debug.Assert(concreteType != null);
+ concreteType = _typeSymbols.Dictionary;
+ populationCastType = _typeSymbols.GenericIDictionary;
+ constructionStrategy = ConstructionStrategy.ToEnumerableMethod;
+ populationStrategy = CollectionPopulationStrategy.Cast_Then_Add;
+ toEnumerableMethodCall = "ToDictionary(pair => pair.Key, pair => pair.Value)";
+ _namespaces.Add("System.Linq");
}
- else if (!CanConstructObject(type, location) || !HasAddMethod(type, elementType, keyType))
+ else
{
ReportUnsupportedType(type, Helpers.CollectionNotSupported, location);
return null;
}
- return new DictionarySpec(type)
+ DictionarySpec spec = new(type)
{
Location = location,
KeyType = (ParsableFromStringTypeSpec)keySpec,
ElementType = elementSpec,
- ConstructionStrategy = ConstructionStrategy.ParameterlessConstructor,
- ConcreteType = concreteType
+ ConstructionStrategy = constructionStrategy,
+ PopulationStrategy = populationStrategy,
+ ToEnumerableMethodCall = toEnumerableMethodCall,
};
- }
- private TypeSpec? ConstructAndCacheGenericTypeForBindCore(INamedTypeSymbol type, params ITypeSymbol[] parameters)
- {
- Debug.Assert(type.IsGenericType);
- TypeSpec spec = GetOrCreateTypeSpec(type.Construct(parameters));
- GetRootConfigTypeCache(MethodSpecifier.BindCore).Add(spec);
+ Debug.Assert(!(populationStrategy is CollectionPopulationStrategy.Cast_Then_Add && populationCastType is null));
+ spec.ConcreteType = ConstructGenericCollectionTypeSpec(concreteType, keyType, elementType);
+ spec.PopulationCastType = ConstructGenericCollectionTypeSpec(populationCastType, keyType, elementType);
+
return spec;
}
- private EnumerableSpec? CreateEnumerableSpec(INamedTypeSymbol type, Location? location, ITypeSymbol elementType)
+ private EnumerableSpec? CreateEnumerableSpec(INamedTypeSymbol type, Location? location)
{
- if (!TryGetTypeSpec(elementType, Helpers.ElementTypeNotSupported, out TypeSpec elementSpec))
+ if (!TryGetElementType(type, out ITypeSymbol? elementType) ||
+ !TryGetTypeSpec(elementType, Helpers.ElementTypeNotSupported, out TypeSpec elementSpec))
{
return null;
}
- EnumerableSpec? concreteType = null;
- if (IsInterfaceMatch(type, _typeSymbols.ISet))
+
+ ConstructionStrategy constructionStrategy;
+ CollectionPopulationStrategy populationStrategy;
+ INamedTypeSymbol? concreteType = null;
+ INamedTypeSymbol? populationCastType = null;
+
+ if (HasPublicParameterlessCtor(type))
+ {
+ constructionStrategy = ConstructionStrategy.ParameterlessConstructor;
+
+ if (HasAddMethod(type, elementType))
+ {
+ populationStrategy = CollectionPopulationStrategy.Add;
+ }
+ else if (GetInterface(type, _typeSymbols.GenericICollection_Unbound) is not null)
+ {
+ populationCastType = _typeSymbols.GenericICollection;
+ populationStrategy = CollectionPopulationStrategy.Cast_Then_Add;
+ }
+ else
+ {
+ ReportUnsupportedType(type, Helpers.CollectionNotSupported, location);
+ return null;
+ }
+ }
+ else if (IsInterfaceMatch(type, _typeSymbols.GenericICollection_Unbound) ||
+ IsInterfaceMatch(type, _typeSymbols.GenericIList_Unbound))
+ {
+ concreteType = _typeSymbols.List;
+ constructionStrategy = ConstructionStrategy.ParameterlessConstructor;
+ populationStrategy = CollectionPopulationStrategy.Add;
+ }
+ else if (IsInterfaceMatch(type, _typeSymbols.GenericIEnumerable_Unbound))
+ {
+ concreteType = _typeSymbols.List;
+ populationCastType = _typeSymbols.GenericICollection;
+ constructionStrategy = ConstructionStrategy.ParameterizedConstructor;
+ populationStrategy = CollectionPopulationStrategy.Cast_Then_Add;
+ }
+ else if (IsInterfaceMatch(type, _typeSymbols.ISet_Unbound))
{
- concreteType = ConstructAndCacheGenericTypeForBindCore(_typeSymbols.HashSet, elementType) as EnumerableSpec;
+ concreteType = _typeSymbols.HashSet;
+ constructionStrategy = ConstructionStrategy.ParameterlessConstructor;
+ populationStrategy = CollectionPopulationStrategy.Add;
}
- else if (IsInterfaceMatch(type, _typeSymbols.ICollection) ||
- IsInterfaceMatch(type, _typeSymbols.GenericIList))
+ else if (IsInterfaceMatch(type, _typeSymbols.IReadOnlySet_Unbound))
{
- concreteType = ConstructAndCacheGenericTypeForBindCore(_typeSymbols.List, elementType) as EnumerableSpec;
+ concreteType = _typeSymbols.HashSet;
+ populationCastType = _typeSymbols.ISet;
+ constructionStrategy = ConstructionStrategy.ParameterizedConstructor;
+ populationStrategy = CollectionPopulationStrategy.Cast_Then_Add;
}
- else if (!CanConstructObject(type, location) || !HasAddMethod(type, elementType))
+ else if (IsInterfaceMatch(type, _typeSymbols.IReadOnlyList_Unbound) || IsInterfaceMatch(type, _typeSymbols.IReadOnlyCollection_Unbound))
+ {
+ concreteType = _typeSymbols.List;
+ populationCastType = _typeSymbols.GenericICollection;
+ constructionStrategy = ConstructionStrategy.ParameterizedConstructor;
+ populationStrategy = CollectionPopulationStrategy.Cast_Then_Add;
+ }
+ else
{
ReportUnsupportedType(type, Helpers.CollectionNotSupported, location);
return null;
RegisterHasChildrenHelperForGenIfRequired(elementSpec);
- return new EnumerableSpec(type)
+ EnumerableSpec spec = new(type)
{
Location = location,
ElementType = elementSpec,
- ConstructionStrategy = ConstructionStrategy.ParameterlessConstructor,
- ConcreteType = concreteType
+ ConstructionStrategy = constructionStrategy,
+ PopulationStrategy = populationStrategy,
+ ToEnumerableMethodCall = null,
};
+
+ Debug.Assert(!(populationStrategy is CollectionPopulationStrategy.Cast_Then_Add && populationCastType is null));
+ spec.ConcreteType = ConstructGenericCollectionTypeSpec(concreteType, elementType);
+ spec.PopulationCastType = ConstructGenericCollectionTypeSpec(populationCastType, elementType);
+
+ return spec;
}
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))
+ if (!HasPublicParameterlessCtor(type))
{
+ ReportUnsupportedType(type, Helpers.NeedPublicParameterlessConstructor, location);
_createdSpecs.Add(type, null);
return null;
}
{
if (property.Type is ITypeSymbol { } propertyType)
{
- TypeSpec? propertyTypeSpec = GetOrCreateTypeSpec(propertyType);
+ AttributeData? attributeData = property.GetAttributes().FirstOrDefault(a => Helpers.TypesAreEqual(a.AttributeClass, _typeSymbols.ConfigurationKeyNameAttribute));
string propertyName = property.Name;
+ string configKeyName = attributeData?.ConstructorArguments.FirstOrDefault().Value as string ?? propertyName;
+
+ TypeSpec? propertyTypeSpec = GetOrCreateTypeSpec(propertyType);
+ PropertySpec spec;
if (propertyTypeSpec is null)
{
}
else
{
- AttributeData? attributeData = property.GetAttributes().FirstOrDefault(a => Helpers.TypesAreEqual(a.AttributeClass, _typeSymbols.ConfigurationKeyNameAttribute));
- string configKeyName = attributeData?.ConstructorArguments.FirstOrDefault().Value as string ?? propertyName;
-
- PropertySpec spec = new PropertySpec(property) { Type = propertyTypeSpec, ConfigurationKeyName = configKeyName };
- if (spec.CanGet || spec.CanSet)
- {
- objectSpec.Properties[configKeyName] = (spec);
- }
-
RegisterHasChildrenHelperForGenIfRequired(propertyTypeSpec);
}
+
+
+ spec = new PropertySpec(property) { Type = propertyTypeSpec, ConfigurationKeyName = configKeyName };
+ objectSpec.Properties[configKeyName] = spec;
}
}
}
}
}
- private bool IsCandidateEnumerable(INamedTypeSymbol type, out ITypeSymbol? elementType)
+ private bool TryGetElementType(INamedTypeSymbol type, out ITypeSymbol? elementType)
{
- INamedTypeSymbol? @interface = GetInterface(type, _typeSymbols.ICollection);
+ INamedTypeSymbol? candidate = GetInterface(type, _typeSymbols.GenericIEnumerable_Unbound);
- if (@interface is not null)
+ if (candidate is not null)
{
- elementType = @interface.TypeArguments[0];
+ elementType = candidate.TypeArguments[0];
return true;
}
private bool IsCandidateDictionary(INamedTypeSymbol type, out ITypeSymbol? keyType, out ITypeSymbol? elementType)
{
- INamedTypeSymbol? @interface = GetInterface(type, _typeSymbols.GenericIDictionary);
- if (@interface is not null)
+ INamedTypeSymbol? candidate = GetInterface(type, _typeSymbols.GenericIDictionary_Unbound) ?? GetInterface(type, _typeSymbols.IReadOnlyDictionary_Unbound);
+
+ if (candidate is not null)
{
- keyType = @interface.TypeArguments[0];
- elementType = @interface.TypeArguments[1];
+ keyType = candidate.TypeArguments[0];
+ elementType = candidate.TypeArguments[1];
return true;
}
return false;
}
- private bool CanConstructObject(INamedTypeSymbol type, Location? location)
+ private static bool HasPublicParameterlessCtor(INamedTypeSymbol type)
{
if (type.IsAbstract || type.TypeKind == TypeKind.Interface)
{
- ReportUnsupportedType(type, Helpers.AbstractOrInterfaceNotSupported, location);
- return false;
- }
- else if (!HasPublicParameterlessCtor(type))
- {
- ReportUnsupportedType(type, Helpers.NeedPublicParameterlessConstructor, location);
return false;
}
- return true;
- }
-
- private static bool HasPublicParameterlessCtor(ITypeSymbol type)
- {
if (type is not INamedTypeSymbol namedType)
{
return false;
return false;
}
- private static bool HasAddMethod(INamedTypeSymbol type, ITypeSymbol element, ITypeSymbol key)
+ private static bool HasAddMethod(INamedTypeSymbol type, ITypeSymbol key, ITypeSymbol element)
{
INamedTypeSymbol current = type;
while (current != null)
private static bool IsEnum(ITypeSymbol type) => type is INamedTypeSymbol { EnumUnderlyingType: INamedTypeSymbol { } };
+ private CollectionSpec? ConstructGenericCollectionTypeSpec(INamedTypeSymbol? collectionType, params ITypeSymbol[] parameters) =>
+ (collectionType is not null ? ConstructGenericCollectionSpec(collectionType, parameters) : null);
+
+ private CollectionSpec? ConstructGenericCollectionSpec(INamedTypeSymbol type, params ITypeSymbol[] parameters)
+ {
+ Debug.Assert(type.IsGenericType);
+ INamedTypeSymbol constructedType = type.Construct(parameters);
+ return CreateCollectionSpec(constructedType, location: null);
+ }
+
private void ReportUnsupportedType(ITypeSymbol type, DiagnosticDescriptor descriptor, Location? location = null)
{
if (!_unsupportedTypes.Contains(type))
{
internal sealed record KnownTypeSymbols
{
- public INamedTypeSymbol GenericIList { get; }
- public INamedTypeSymbol ICollection { get; }
- public INamedTypeSymbol IEnumerable { get; }
public INamedTypeSymbol String { get; }
-
public INamedTypeSymbol? CultureInfo { get; }
public INamedTypeSymbol? DateOnly { get; }
public INamedTypeSymbol? DateTimeOffset { get; }
public INamedTypeSymbol? Uri { get; }
public INamedTypeSymbol? Version { get; }
- public INamedTypeSymbol? Action { get; }
public INamedTypeSymbol? ActionOfBinderOptions { get; }
- public INamedTypeSymbol? BinderOptions { get; }
public INamedTypeSymbol? ConfigurationKeyNameAttribute { get; }
+
+ public INamedTypeSymbol GenericIList_Unbound { get; }
+ public INamedTypeSymbol GenericICollection_Unbound { get; }
+ public INamedTypeSymbol GenericICollection { get; }
+ public INamedTypeSymbol GenericIEnumerable_Unbound { get; }
+ public INamedTypeSymbol IEnumerable { get; }
public INamedTypeSymbol? Dictionary { get; }
+ public INamedTypeSymbol? GenericIDictionary_Unbound { get; }
public INamedTypeSymbol? GenericIDictionary { get; }
public INamedTypeSymbol? HashSet { get; }
public INamedTypeSymbol? IConfiguration { get; }
public INamedTypeSymbol? IConfigurationSection { get; }
public INamedTypeSymbol? IDictionary { get; }
+ public INamedTypeSymbol? IReadOnlyCollection_Unbound { get; }
+ public INamedTypeSymbol? IReadOnlyDictionary_Unbound { get; }
+ public INamedTypeSymbol? IReadOnlyList_Unbound { get; }
+ public INamedTypeSymbol? IReadOnlySet_Unbound { get; }
public INamedTypeSymbol? IServiceCollection { get; }
+ public INamedTypeSymbol? ISet_Unbound { get; }
public INamedTypeSymbol? ISet { get; }
public INamedTypeSymbol? List { get; }
Version = compilation.GetBestTypeByMetadataName(TypeFullName.Version);
// Used to verify input configuation binding API calls.
- Action = compilation.GetBestTypeByMetadataName(TypeFullName.Action);
- BinderOptions = compilation.GetBestTypeByMetadataName(TypeFullName.BinderOptions);
- ActionOfBinderOptions = Action?.Construct(BinderOptions);
+ INamedTypeSymbol? binderOptions = compilation.GetBestTypeByMetadataName(TypeFullName.BinderOptions);
+ ActionOfBinderOptions = binderOptions is null ? null : compilation.GetBestTypeByMetadataName(TypeFullName.Action)?.Construct(binderOptions);
ConfigurationKeyNameAttribute = compilation.GetBestTypeByMetadataName(TypeFullName.ConfigurationKeyNameAttribute);
IConfiguration = compilation.GetBestTypeByMetadataName(TypeFullName.IConfiguration);
IConfigurationSection = compilation.GetBestTypeByMetadataName(TypeFullName.IConfigurationSection);
IServiceCollection = compilation.GetBestTypeByMetadataName(TypeFullName.IServiceCollection);
- // Collections.
+ // Used to test what kind of collection a type is.
IEnumerable = compilation.GetSpecialType(SpecialType.System_Collections_IEnumerable);
IDictionary = compilation.GetBestTypeByMetadataName(TypeFullName.IDictionary);
- // Used for type equivalency checks for unbounded generics.
- ICollection = compilation.GetSpecialType(SpecialType.System_Collections_Generic_ICollection_T).ConstructUnboundGenericType();
- GenericIDictionary = compilation.GetBestTypeByMetadataName(TypeFullName.GenericIDictionary)?.ConstructUnboundGenericType();
- GenericIList = compilation.GetSpecialType(SpecialType.System_Collections_Generic_IList_T).ConstructUnboundGenericType();
- ISet = compilation.GetBestTypeByMetadataName(TypeFullName.ISet)?.ConstructUnboundGenericType();
-
- // Used to construct concrete types at runtime; cannot also be constructed.
+ // Used to construct concrete type symbols for generic types, given their type parameters.
+ // These concrete types are used to generating instantiation and casting logic in the emitted binding code.
Dictionary = compilation.GetBestTypeByMetadataName(TypeFullName.Dictionary);
+ GenericICollection = compilation.GetSpecialType(SpecialType.System_Collections_Generic_ICollection_T);
+ GenericIDictionary = compilation.GetBestTypeByMetadataName(TypeFullName.GenericIDictionary);
HashSet = compilation.GetBestTypeByMetadataName(TypeFullName.HashSet);
List = compilation.GetBestTypeByMetadataName(TypeFullName.List);
+ ISet = compilation.GetBestTypeByMetadataName(TypeFullName.ISet);
+
+ // Used for type equivalency checks for unbound generics. The parameters of the types
+ // retured by the Roslyn Get*Type* APIs are not unbound, so we construct unbound
+ // generics to equal those corresponding to generic types in the input type graphs.
+ GenericICollection_Unbound = GenericICollection?.ConstructUnboundGenericType();
+ GenericIDictionary_Unbound = GenericIDictionary?.ConstructUnboundGenericType();
+ GenericIEnumerable_Unbound = compilation.GetSpecialType(SpecialType.System_Collections_Generic_IEnumerable_T).ConstructUnboundGenericType();
+ GenericIList_Unbound = compilation.GetSpecialType(SpecialType.System_Collections_Generic_IList_T).ConstructUnboundGenericType();
+ IReadOnlyDictionary_Unbound = compilation.GetBestTypeByMetadataName(TypeFullName.IReadOnlyDictionary)?.ConstructUnboundGenericType();
+ IReadOnlyCollection_Unbound = compilation.GetBestTypeByMetadataName(TypeFullName.IReadOnlyCollection)?.ConstructUnboundGenericType();
+ IReadOnlyList_Unbound = compilation.GetBestTypeByMetadataName(TypeFullName.IReadOnlyList)?.ConstructUnboundGenericType();
+ IReadOnlySet_Unbound = compilation.GetBestTypeByMetadataName(TypeFullName.IReadOnlySet)?.ConstructUnboundGenericType();
+ ISet_Unbound = ISet?.ConstructUnboundGenericType();
}
private static class TypeFullName
public const string IConfigurationSection = "Microsoft.Extensions.Configuration.IConfigurationSection";
public const string IDictionary = "System.Collections.Generic.IDictionary";
public const string Int128 = "System.Int128";
+ public const string IReadOnlyCollection = "System.Collections.Generic.IReadOnlyCollection`1";
+ public const string IReadOnlyDictionary = "System.Collections.Generic.IReadOnlyDictionary`2";
+ public const string IReadOnlyList = "System.Collections.Generic.IReadOnlyList`1";
+ public const string IReadOnlySet = "System.Collections.Generic.IReadOnlySet`1";
public const string ISet = "System.Collections.Generic.ISet`1";
public const string IServiceCollection = "Microsoft.Extensions.DependencyInjection.IServiceCollection";
public const string List = "System.Collections.Generic.List`1";
public bool IsInterface { get; }
- public CollectionSpec? ConcreteType { get; init; }
- }
+ public CollectionSpec? ConcreteType { get; set; }
- internal sealed record ArraySpec : CollectionSpec
- {
- public ArraySpec(ITypeSymbol type) : base(type) { }
+ public CollectionSpec? PopulationCastType { get; set; }
+
+ public required CollectionPopulationStrategy PopulationStrategy { get; init; }
- public override TypeSpecKind SpecKind => TypeSpecKind.Array;
+ public required string? ToEnumerableMethodCall { get; init; }
}
internal sealed record EnumerableSpec : CollectionSpec
public required ParsableFromStringTypeSpec KeyType { get; init; }
}
+
+ internal enum CollectionPopulationStrategy
+ {
+ Unknown,
+ Array,
+ Add,
+ Cast_Then_Add,
+ }
}
{
internal enum ConstructionStrategy
{
- NotApplicable = 0,
+ None = 0,
ParameterlessConstructor = 1,
+ ParameterizedConstructor = 2,
+ ToEnumerableMethod = 3,
}
}
{
public ObjectSpec(INamedTypeSymbol type) : base(type) { }
public override TypeSpecKind SpecKind => TypeSpecKind.Object;
- public Dictionary<string, PropertySpec> Properties { get; } = new();
+ public Dictionary<string, PropertySpec?> Properties { get; } = new();
}
}
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System.Linq;
using Microsoft.CodeAnalysis;
namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
{
internal sealed record PropertySpec
{
+ public string Name { get; }
+ public bool IsStatic { get; }
+ public bool CanGet { get; }
+ public bool CanSet { get; }
+ public required TypeSpec? Type { get; init; }
+ public required string ConfigurationKeyName { get; init; }
+
public PropertySpec(IPropertySymbol property)
{
Name = property.Name;
CanSet = property.SetMethod is IMethodSymbol { DeclaredAccessibility: Accessibility.Public, IsInitOnly: false };
}
- public string Name { get; }
- public bool IsStatic { get; }
- public bool CanGet { get; }
- public bool CanSet { get; }
- public required TypeSpec Type { get; init; }
- public required string ConfigurationKeyName { get; init; }
+ public bool ShouldBind() =>
+ (CanGet || CanSet) &&
+ Type is not null &&
+ !(!CanSet && (Type as CollectionSpec)?.ConstructionStrategy is ConstructionStrategy.ParameterizedConstructor);
}
}
Assert.Equal("val_3", options.AlreadyInitializedHashSetDictionary["123"].ElementAt(3));
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [Fact]
public void CanOverrideExistingDictionaryKey()
{
var input = new Dictionary<string, string>
#endif
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [Fact]
public void GetStringArray()
{
var input = new Dictionary<string, string>
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [Fact]
public void BindStringArray()
{
var input = new Dictionary<string, string>
Assert.Equal("valx", array[3]);
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [Fact]
public void GetAlreadyInitializedArray()
{
var input = new Dictionary<string, string>
Assert.Equal("valx", array[6]);
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [Fact]
public void BindAlreadyInitializedArray()
{
var input = new Dictionary<string, string>
exception.Message);
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [Fact]
public void JaggedArrayBinding()
{
var input = new Dictionary<string, string>
Assert.Equal("12", options.JaggedArray[1][2]);
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [Fact]
public void ReadOnlyArrayIsIgnored()
{
var input = new Dictionary<string, string>
Assert.Equal(new OptionsWithArrays().ReadOnlyArray, options.ReadOnlyArray);
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [Fact]
public void CanBindUninitializedIEnumerable()
{
var input = new Dictionary<string, string>
Assert.Equal("valx", array[3]);
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [Fact]
public void CanBindInitializedIEnumerableAndTheOriginalItemsAreNotMutated()
{
var input = new Dictionary<string, string>
Assert.Equal("ExtraItem", options.ICollectionNoSetter.ElementAt(2));
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [Fact]
public void CanBindInitializedCustomIEnumerableBasedList()
{
// A field declared as IEnumerable<T> that is instantiated with a class
Assert.Equal("val1", array[3]);
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [Fact]
public void CanBindInitializedCustomIndirectlyDerivedIEnumerableList()
{
// A field declared as IEnumerable<T> that is instantiated with a class
Assert.Equal("val1", array[3]);
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [Fact]
public void CanBindInitializedIReadOnlyDictionaryAndDoesNotModifyTheOriginal()
{
// A field declared as IEnumerable<T> that is instantiated with a class
Assert.Equal("ExtraItem", options.ICollection.ElementAt(4));
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [Fact]
public void CanBindUninitializedIList()
{
var input = new Dictionary<string, string>
Assert.Equal("val_3", options.IDictionary["ghi"]);
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [Fact]
public void CanBindUninitializedIReadOnlyDictionary()
{
var input = new Dictionary<string, string>
/// <summary>
/// Replicates scenario from https://github.com/dotnet/runtime/issues/63479
/// </summary>
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [Fact]
public void TestCanBindListPropertyWithoutSetter()
{
var input = new Dictionary<string, string>
Assert.Equal(new[] { "a", "b" }, options.ListPropertyWithoutSetter);
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [Fact]
public void CanBindNonInstantiatedIEnumerableWithItems()
{
var dic = new Dictionary<string, string>
}
#if NETCOREAPP
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [Fact]
public void CanBindInstantiatedIReadOnlySet()
{
var dic = new Dictionary<string, string>
Assert.Equal("Yo2", options.InstantiatedIReadOnlySetWithSomeValues.ElementAt(3));
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [Fact]
public void CanBindNonInstantiatedIReadOnlySet()
{
var dic = new Dictionary<string, string>
Assert.Equal("Yo2", options.NonInstantiatedIReadOnlySet.ElementAt(1));
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [Fact]
public void CanBindInstantiatedDictionaryOfIReadOnlySetWithSomeExistingValues()
{
var dic = new Dictionary<string, string>
}
#endif
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [Fact]
public void CanBindInstantiatedReadOnlyDictionary2()
{
var dic = new Dictionary<string, string>
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [Fact]
public void BindInstantiatedIReadOnlyDictionary_CreatesCopyOfOriginal()
{
var dic = new Dictionary<string, string>
Assert.Equal(3, options.Dictionary["item3"]);
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [Fact]
public void BindNonInstantiatedIReadOnlyDictionary()
{
var dic = new Dictionary<string, string>
Assert.Equal(2, options.Dictionary["item2"]);
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [Fact]
public void BindInstantiatedConcreteDictionary_OverwritesOriginal()
{
var dic = new Dictionary<string, string>
Assert.Equal(3, options.Dictionary["item3"]);
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [Fact]
public void CanBindInstantiatedReadOnlyDictionary()
{
var dic = new Dictionary<string, string>
Assert.Equal(4, resultingDictionary["item4"]);
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [Fact]
public void CanBindNonInstantiatedReadOnlyDictionary()
{
var dic = new Dictionary<string, string>
Assert.Equal(4, options.NonInstantiatedReadOnlyDictionary["item4"]);
}
-
[ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
public void CanBindNonInstantiatedDictionaryOfISet()
{
Assert.Equal("bar-2", options.NonInstantiatedDictionaryWithISet["bar"].ElementAt(1));
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [Fact]
public void CanBindInstantiatedDictionaryOfISet()
{
var dic = new Dictionary<string, string>
Assert.Equal("bar-2", options.InstantiatedDictionaryWithHashSet["bar"].ElementAt(1));
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [Fact]
public void CanBindInstantiatedDictionaryOfISetWithSomeExistingValues()
{
var dic = new Dictionary<string, string>
Assert.Equal("bar-2", options.InstantiatedDictionaryWithHashSetWithSomeValues["bar"].ElementAt(1));
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Dropped members for binding: diagnostic warning issued instead.
public void ThrowsForCustomIEnumerableCollection()
{
var configurationBuilder = new ConfigurationBuilder();
exception.Message);
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Dropped members for binding: diagnostic warning issued instead.
public void ThrowsForCustomICollection()
{
var configurationBuilder = new ConfigurationBuilder();
exception.Message);
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Dropped members for binding: diagnostic warning issued instead.
public void ThrowsForCustomDictionary()
{
var configurationBuilder = new ConfigurationBuilder();
exception.Message);
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Dropped members for binding: diagnostic warning issued instead.
public void ThrowsForCustomSet()
{
var configurationBuilder = new ConfigurationBuilder();
exception.Message);
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [Fact]
public void CanBindInstantiatedISet()
{
var dic = new Dictionary<string, string>
Assert.Equal("Yo2", options.InstantiatedISet.ElementAt(1));
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [Fact]
public void CanBindInstantiatedISetWithSomeValues()
{
var dic = new Dictionary<string, string>
Assert.Equal("Yo2", options.InstantiatedISetWithSomeValues.ElementAt(3));
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [Fact]
public void CanBindInstantiatedHashSetWithSomeValues()
{
var dic = new Dictionary<string, string>
Assert.Equal("Yo2", options.InstantiatedHashSetWithSomeValues.ElementAt(3));
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [Fact]
public void CanBindNonInstantiatedHashSet()
{
var dic = new Dictionary<string, string>
Assert.Equal("Yo2", options.NonInstantiatedHashSet.ElementAt(1));
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [Fact]
public void CanBindInstantiatedSortedSetWithSomeValues()
{
var dic = new Dictionary<string, string>
Assert.Equal("Yo2", options.InstantiatedSortedSetWithSomeValues.ElementAt(3));
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [Fact]
public void CanBindNonInstantiatedSortedSetWithSomeValues()
{
var dic = new Dictionary<string, string>
Assert.Equal("Yo2", options.NonInstantiatedSortedSetWithSomeValues.ElementAt(1));
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Ensure exception messages are in sync
public void DoesNotBindInstantiatedISetWithUnsupportedKeys()
{
var dic = new Dictionary<string, string>
Assert.Equal(0, options.HashSetWithUnsupportedKey.Count);
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Ensure exception messages are in sync
public void DoesNotBindUninstantiatedISetWithUnsupportedKeys()
{
var dic = new Dictionary<string, string>
Assert.Null(options.UninstantiatedHashSetWithUnsupportedKey);
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [Fact]
public void CanBindInstantiatedIEnumerableWithItems()
{
var dic = new Dictionary<string, string>
Assert.Equal("Yo2", options.InstantiatedIEnumerable.ElementAt(1));
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [Fact]
public void CanBindInstantiatedCustomICollectionWithoutAnAddMethodWithItems()
{
var dic = new Dictionary<string, string>
Assert.Equal("Yo2", options.InstantiatedCustomICollectionWithoutAnAddMethod.ElementAt(1));
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [Fact]
public void CanBindNonInstantiatedCustomICollectionWithoutAnAddMethodWithItems()
{
var dic = new Dictionary<string, string>
Assert.Equal("Yo2", options.NonInstantiatedCustomICollectionWithoutAnAddMethod.ElementAt(1));
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [Fact]
public void CanBindInstantiatedICollectionWithItems()
{
var dic = new Dictionary<string, string>
Assert.Equal("Yo2", options.InstantiatedICollection.ElementAt(1));
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [Fact]
public void CanBindInstantiatedIReadOnlyCollectionWithItems()
{
var dic = new Dictionary<string, string>
Assert.Equal("Yo2", options.InstantiatedIReadOnlyCollection.ElementAt(1));
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [Fact]
public void CanBindInstantiatedIEnumerableWithNullItems()
{
var dic = new Dictionary<string, string>
Assert.Equal("Yo2", options.InstantiatedIEnumerable.ElementAt(1));
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [Fact]
public void DifferentDictionaryBindingCasesTest()
{
var dic = new Dictionary<string, string>() { { "key", "value" } };
Assert.Single(config.Get<Dictionary<string, string>>());
Assert.Single(config.Get<IDictionary<string, string>>());
Assert.Single(config.Get<ExtendedDictionary<string, string>>());
+ // The System.Reflection.AmbiguousMatchException scenario that
+ // this test validates is not applicable. Source generator will
+ // statically bind to best-fit dictionary value indexer.
+#if !BUILDING_SOURCE_GENERATOR_TESTS
Assert.Single(config.Get<ImplementerOfIDictionaryClass<string, string>>());
- }
-
- public class OptionsWithDifferentCollectionInterfaces
- {
- private static IEnumerable<string> s_instantiatedIEnumerable = new List<string> { "value1", "value2" };
- public bool IsSameInstantiatedIEnumerable() => object.ReferenceEquals(s_instantiatedIEnumerable, InstantiatedIEnumerable);
- public IEnumerable<string> InstantiatedIEnumerable { get; set; } = s_instantiatedIEnumerable;
-
- private static IList<string> s_instantiatedIList = new List<string> { "value1", "value2" };
- public bool IsSameInstantiatedIList() => object.ReferenceEquals(s_instantiatedIList, InstantiatedIList);
- public IList<string> InstantiatedIList { get; set; } = s_instantiatedIList;
-
- private static IReadOnlyList<string> s_instantiatedIReadOnlyList = new List<string> { "value1", "value2" };
- public bool IsSameInstantiatedIReadOnlyList() => object.ReferenceEquals(s_instantiatedIReadOnlyList, InstantiatedIReadOnlyList);
- public IReadOnlyList<string> InstantiatedIReadOnlyList { get; set; } = s_instantiatedIReadOnlyList;
-
- private static IDictionary<string, string> s_instantiatedIDictionary = new Dictionary<string, string> { ["Key1"] = "value1", ["Key2"] = "value2" };
- public IDictionary<string, string> InstantiatedIDictionary { get; set; } = s_instantiatedIDictionary;
- public bool IsSameInstantiatedIDictionary() => object.ReferenceEquals(s_instantiatedIDictionary, InstantiatedIDictionary);
-
- private static IReadOnlyDictionary<string, string> s_instantiatedIReadOnlyDictionary = new Dictionary<string, string> { ["Key1"] = "value1", ["Key2"] = "value2" };
- public IReadOnlyDictionary<string, string> InstantiatedIReadOnlyDictionary { get; set; } = s_instantiatedIReadOnlyDictionary;
- public bool IsSameInstantiatedIReadOnlyDictionary() => object.ReferenceEquals(s_instantiatedIReadOnlyDictionary, InstantiatedIReadOnlyDictionary);
-
- private static ISet<string> s_instantiatedISet = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "a", "A", "b" };
- public ISet<string> InstantiatedISet { get; set; } = s_instantiatedISet;
- public bool IsSameInstantiatedISet() => object.ReferenceEquals(s_instantiatedISet, InstantiatedISet);
-
-#if NETCOREAPP
- private static IReadOnlySet<string> s_instantiatedIReadOnlySet = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "a", "A", "b" };
- public IReadOnlySet<string> InstantiatedIReadOnlySet { get; set; } = s_instantiatedIReadOnlySet;
- public bool IsSameInstantiatedIReadOnlySet() => object.ReferenceEquals(s_instantiatedIReadOnlySet, InstantiatedIReadOnlySet);
-
- public IReadOnlySet<string> UnInstantiatedIReadOnlySet { get; set; }
#endif
- private static ICollection<string> s_instantiatedICollection = new List<string> { "a", "b", "c" };
- public ICollection<string> InstantiatedICollection { get; set; } = s_instantiatedICollection;
- public bool IsSameInstantiatedICollection() => object.ReferenceEquals(s_instantiatedICollection, InstantiatedICollection);
-
- private static IReadOnlyCollection<string> s_instantiatedIReadOnlyCollection = new List<string> { "a", "b", "c" };
- public IReadOnlyCollection<string> InstantiatedIReadOnlyCollection { get; set; } = s_instantiatedIReadOnlyCollection;
- public bool IsSameInstantiatedIReadOnlyCollection() => object.ReferenceEquals(s_instantiatedIReadOnlyCollection, InstantiatedIReadOnlyCollection);
-
- public IReadOnlyCollection<string> UnInstantiatedIReadOnlyCollection { get; set; }
- public ICollection<string> UnInstantiatedICollection { get; set; }
- public ISet<string> UnInstantiatedISet { get; set; }
- public IReadOnlyDictionary<string, string> UnInstantiatedIReadOnlyDictionary { get; set; }
- public IEnumerable<string> UnInstantiatedIEnumerable { get; set; }
- public IList<string> UnInstantiatedIList { get; set; }
- public IReadOnlyList<string> UnInstantiatedIReadOnlyList { get; set; }
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [Fact]
public void TestOptionsWithDifferentCollectionInterfaces()
{
var input = new Dictionary<string, string>
Assert.Equal(new string[] { "r", "e" }, options.UnInstantiatedIReadOnlyCollection);
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+ [Fact]
public void TestMutatingDictionaryValues()
{
IConfiguration config = new ConfigurationBuilder()
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
- public class CustomDictionary<T> : Dictionary<string, T>
- {
- }
-
public class NestedOptions
{
public int Integer { get; set; }
public interface ICustomDictionary<T, T1> : IDictionary<T, T1>
{
}
+
+ public class OptionsWithDifferentCollectionInterfaces
+ {
+ private static IEnumerable<string> s_instantiatedIEnumerable = new List<string> { "value1", "value2" };
+ public bool IsSameInstantiatedIEnumerable() => object.ReferenceEquals(s_instantiatedIEnumerable, InstantiatedIEnumerable);
+ public IEnumerable<string> InstantiatedIEnumerable { get; set; } = s_instantiatedIEnumerable;
+
+ private static IList<string> s_instantiatedIList = new List<string> { "value1", "value2" };
+ public bool IsSameInstantiatedIList() => object.ReferenceEquals(s_instantiatedIList, InstantiatedIList);
+ public IList<string> InstantiatedIList { get; set; } = s_instantiatedIList;
+
+ private static IReadOnlyList<string> s_instantiatedIReadOnlyList = new List<string> { "value1", "value2" };
+ public bool IsSameInstantiatedIReadOnlyList() => object.ReferenceEquals(s_instantiatedIReadOnlyList, InstantiatedIReadOnlyList);
+ public IReadOnlyList<string> InstantiatedIReadOnlyList { get; set; } = s_instantiatedIReadOnlyList;
+
+ private static IDictionary<string, string> s_instantiatedIDictionary = new Dictionary<string, string> { ["Key1"] = "value1", ["Key2"] = "value2" };
+ public IDictionary<string, string> InstantiatedIDictionary { get; set; } = s_instantiatedIDictionary;
+ public bool IsSameInstantiatedIDictionary() => object.ReferenceEquals(s_instantiatedIDictionary, InstantiatedIDictionary);
+
+ private static IReadOnlyDictionary<string, string> s_instantiatedIReadOnlyDictionary = new Dictionary<string, string> { ["Key1"] = "value1", ["Key2"] = "value2" };
+ public IReadOnlyDictionary<string, string> InstantiatedIReadOnlyDictionary { get; set; } = s_instantiatedIReadOnlyDictionary;
+ public bool IsSameInstantiatedIReadOnlyDictionary() => object.ReferenceEquals(s_instantiatedIReadOnlyDictionary, InstantiatedIReadOnlyDictionary);
+
+ private static ISet<string> s_instantiatedISet = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "a", "A", "b" };
+ public ISet<string> InstantiatedISet { get; set; } = s_instantiatedISet;
+ public bool IsSameInstantiatedISet() => object.ReferenceEquals(s_instantiatedISet, InstantiatedISet);
+
+#if NETCOREAPP
+ private static IReadOnlySet<string> s_instantiatedIReadOnlySet = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "a", "A", "b" };
+ public IReadOnlySet<string> InstantiatedIReadOnlySet { get; set; } = s_instantiatedIReadOnlySet;
+ public bool IsSameInstantiatedIReadOnlySet() => object.ReferenceEquals(s_instantiatedIReadOnlySet, InstantiatedIReadOnlySet);
+
+ public IReadOnlySet<string> UnInstantiatedIReadOnlySet { get; set; }
+#endif
+ private static ICollection<string> s_instantiatedICollection = new List<string> { "a", "b", "c" };
+ public ICollection<string> InstantiatedICollection { get; set; } = s_instantiatedICollection;
+ public bool IsSameInstantiatedICollection() => object.ReferenceEquals(s_instantiatedICollection, InstantiatedICollection);
+
+ private static IReadOnlyCollection<string> s_instantiatedIReadOnlyCollection = new List<string> { "a", "b", "c" };
+ public IReadOnlyCollection<string> InstantiatedIReadOnlyCollection { get; set; } = s_instantiatedIReadOnlyCollection;
+ public bool IsSameInstantiatedIReadOnlyCollection() => object.ReferenceEquals(s_instantiatedIReadOnlyCollection, InstantiatedIReadOnlyCollection);
+
+ public IReadOnlyCollection<string> UnInstantiatedIReadOnlyCollection { get; set; }
+ public ICollection<string> UnInstantiatedICollection { get; set; }
+ public ISet<string> UnInstantiatedISet { get; set; }
+ public IReadOnlyDictionary<string, string> UnInstantiatedIReadOnlyDictionary { get; set; }
+ public IEnumerable<string> UnInstantiatedIEnumerable { get; set; }
+ public IList<string> UnInstantiatedIList { get; set; }
+ public IReadOnlyList<string> UnInstantiatedIReadOnlyList { get; set; }
+ }
}
}
exception.Message);
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
- public void ExceptionWhenTryingToBindToConstructorWithMissingConfig() // Need support for parameterized ctors.
+ [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors.
+ public void ExceptionWhenTryingToBindToConstructorWithMissingConfig()
{
var input = new Dictionary<string, string>
{
exception.Message);
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
- public void ExceptionWhenTryingToBindConfigToClassWhereNoMatchingParameterIsFoundInConstructor() // Need support for parameterized ctors.
+ [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors.
+ public void ExceptionWhenTryingToBindConfigToClassWhereNoMatchingParameterIsFoundInConstructor()
{
var input = new Dictionary<string, string>
{
exception.Message);
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
- public void BindsToClassConstructorParametersWithDefaultValues() // Need support for parameterized ctors.
+ [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors.
+ public void BindsToClassConstructorParametersWithDefaultValues()
{
var input = new Dictionary<string, string>
{
Assert.True(bound.NullableNestedStruct.Value.DeeplyNested.Boolean);
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need collection support.
+ [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need property selection in sync with reflection.
public void CanBindVirtualProperties()
{
ConfigurationBuilder configurationBuilder = new();
#endif
}
- [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need collection support.
+ [Fact]
public void EnsureCallingThePropertySetter()
{
var json = @"{
Assert.Equal(2, options.ParsedBlacklist.Count); // should be initialized when calling the options.Blacklist setter.
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'
+#if BUILDING_SOURCE_GENERATOR_TESTS
+ // Setter not called if there's no matching configuration value.
+ Assert.Equal(0, options.OtherCode);
+#else
+ // doesn't exist in configuration. the setter sets default value '2'
+ Assert.Equal(2, options.OtherCode);
+#endif
}
[Fact]
--- /dev/null
+// <auto-generated/>
+#nullable enable
+
+internal static class GeneratedConfigurationBinder
+{
+ public static T? Get<T>(this global::Microsoft.Extensions.Configuration.IConfiguration configuration) => (T?)(global::Microsoft.Extensions.Configuration.Binder.SourceGeneration.Helpers.GetCore(configuration, typeof(T), configureActions: null) ?? default(T));
+}
+
+namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
+{
+ using System;
+ using System.Globalization;
+ using Microsoft.Extensions.Configuration;
+ using System.Collections.Generic;
+ using System.Linq;
+
+ internal static class Helpers
+ {
+ public static object? GetCore(this IConfiguration configuration, Type type, Action<BinderOptions>? configureActions)
+ {
+ if (configuration is null)
+ {
+ throw new ArgumentNullException(nameof(configuration));
+ }
+
+ BinderOptions? binderOptions = GetBinderOptions(configureActions);
+
+ if (!HasValueOrChildren(configuration))
+ {
+ return null;
+ }
+
+ if (type == typeof(Program.MyClassWithCustomCollections))
+ {
+ var obj = new Program.MyClassWithCustomCollections();
+ BindCore(configuration, ref obj, binderOptions);
+ return obj;
+ }
+
+ throw new global::System.NotSupportedException($"Unable to bind to type '{type}': generator did not detect the type as input.");
+ }
+
+ public static void BindCore(IConfiguration configuration, ref Program.CustomDictionary<string, int> obj, BinderOptions? binderOptions)
+ {
+ if (obj is null)
+ {
+ throw new ArgumentNullException(nameof(obj));
+ }
+
+ foreach (IConfigurationSection section in configuration.GetChildren())
+ {
+ string key = section.Key;
+ int element;
+ if (section.Value is string stringValue1)
+ {
+ element = ParseInt(stringValue1, () => section.Path);
+ obj[key] = element;
+ }
+ }
+ }
+
+ public static void BindCore(IConfiguration configuration, ref Program.CustomList obj, BinderOptions? binderOptions)
+ {
+ if (obj is null)
+ {
+ throw new ArgumentNullException(nameof(obj));
+ }
+
+ foreach (IConfigurationSection section in configuration.GetChildren())
+ {
+ if (section.Value is string stringValue2)
+ {
+ obj.Add(stringValue2);
+ }
+ }
+ }
+
+ public static void BindCore(IConfiguration configuration, ref IReadOnlyList<int> obj, BinderOptions? binderOptions)
+ {
+ if (obj is null)
+ {
+ throw new ArgumentNullException(nameof(obj));
+ }
+
+ if (obj is not ICollection<int> temp)
+ {
+ return;
+ }
+
+ foreach (IConfigurationSection section in configuration.GetChildren())
+ {
+ int element;
+ if (section.Value is string stringValue3)
+ {
+ element = ParseInt(stringValue3, () => section.Path);
+ temp.Add(element);
+ }
+ }
+ }
+
+ public static void BindCore(IConfiguration configuration, ref IReadOnlyDictionary<string, int> obj, BinderOptions? binderOptions)
+ {
+ if (obj is null)
+ {
+ throw new ArgumentNullException(nameof(obj));
+ }
+
+ if (obj is not IDictionary<string, int> temp)
+ {
+ return;
+ }
+
+ foreach (IConfigurationSection section in configuration.GetChildren())
+ {
+ string key = section.Key;
+ int element;
+ if (section.Value is string stringValue4)
+ {
+ element = ParseInt(stringValue4, () => section.Path);
+ temp[key] = element;
+ }
+ }
+ }
+
+ public static void BindCore(IConfiguration configuration, ref Program.MyClassWithCustomCollections obj, BinderOptions? binderOptions)
+ {
+ if (obj is null)
+ {
+ throw new ArgumentNullException(nameof(obj));
+ }
+
+ List<string>? temp = null;
+ foreach (IConfigurationSection section in configuration.GetChildren())
+ {
+ switch (section.Key)
+ {
+ case "CustomDictionary":
+ {
+ if (HasChildren(section))
+ {
+ Program.CustomDictionary<string, int> temp5 = obj.CustomDictionary;
+ temp5 ??= new Program.CustomDictionary<string, int>();
+ BindCore(section, ref temp5, binderOptions);
+ obj.CustomDictionary = temp5;
+ }
+ }
+ break;
+ case "CustomList":
+ {
+ if (HasChildren(section))
+ {
+ Program.CustomList temp6 = obj.CustomList;
+ temp6 ??= new Program.CustomList();
+ BindCore(section, ref temp6, binderOptions);
+ obj.CustomList = temp6;
+ }
+ }
+ break;
+ case "ICustomDictionary":
+ {
+ }
+ break;
+ case "ICustomCollection":
+ {
+ }
+ break;
+ case "IReadOnlyList":
+ {
+ if (HasChildren(section))
+ {
+ IReadOnlyList<int> temp7 = obj.IReadOnlyList;
+ temp7 = temp7 is null ? new List<int>() : new List<int>(temp7);
+ BindCore(section, ref temp7, binderOptions);
+ obj.IReadOnlyList = temp7;
+ }
+ }
+ break;
+ case "UnsupportedIReadOnlyDictionaryUnsupported":
+ {
+ }
+ break;
+ case "IReadOnlyDictionary":
+ {
+ if (HasChildren(section))
+ {
+ IReadOnlyDictionary<string, int> temp8 = obj.IReadOnlyDictionary;
+ temp8 = temp8 is null ? new Dictionary<string, int>() : temp8.ToDictionary(pair => pair.Key, pair => pair.Value);
+ BindCore(section, ref temp8, binderOptions);
+ obj.IReadOnlyDictionary = temp8;
+ }
+ }
+ break;
+ default:
+ {
+ if (binderOptions?.ErrorOnUnknownConfiguration == true)
+ {
+ (temp ??= new List<string>()).Add($"'{section.Key}'");
+ }
+ }
+ break;
+ }
+ }
+
+ if (temp is not null)
+ {
+ throw new InvalidOperationException($"'ErrorOnUnknownConfiguration' was set on the provided BinderOptions, but the following properties were not found on the instance of {typeof(Program.MyClassWithCustomCollections)}: {string.Join(", ", temp)}");
+ }
+ }
+
+ public static bool HasValueOrChildren(IConfiguration configuration)
+ {
+ if ((configuration as IConfigurationSection)?.Value is not null)
+ {
+ return true;
+ }
+ return HasChildren(configuration);
+ }
+
+ public static bool HasChildren(IConfiguration configuration)
+ {
+ foreach (IConfigurationSection section in configuration.GetChildren())
+ {
+ return true;
+ }
+ return false;
+ }
+
+ public static int ParseInt(string stringValue, Func<string?> getPath)
+ {
+ try
+ {
+ return int.Parse(stringValue, NumberStyles.Integer, CultureInfo.InvariantCulture);
+ }
+ catch (Exception exception)
+ {
+ throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(int)}'.", exception);
+ }
+ }
+ }
+}
using System;
using System.Collections;
+using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.IO;
+using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
config.Bind(configObj, options => { })
config.Bind(""key"", configObj);
}
-
+
public class MyClass
{
public string MyString { get; set; }
configObj = config.Get<MyClass>(binderOptions => { });
configObj = config.Get(typeof(MyClass2), binderOptions => { });
}
-
+
public class MyClass
{
public string MyString { get; set; }
ServiceCollection services = new();
services.Configure<MyClass>(section);
}
-
+
public class MyClass
{
public string MyString { get; set; }
Assert.Equal(DiagnosticSeverity.Error, diagnostic.Severity);
}
+ [Fact]
+ public async Task TestCollectionsGen()
+ {
+ string testSourceCode = """
+ using System.Collections.Generic;
+ using Microsoft.Extensions.Configuration;
+
+ public class Program
+ {
+ public static void Main()
+ {
+ ConfigurationBuilder configurationBuilder = new();
+ IConfiguration config = configurationBuilder.Build();
+ IConfigurationSection section = config.GetSection(""MySection"");
+
+ section.Get<MyClassWithCustomCollections>();
+ }
+
+ public class MyClassWithCustomCollections
+ {
+ public CustomDictionary<string, int> CustomDictionary { get; set; }
+ public CustomList CustomList { get; set; }
+ public ICustomDictionary<string> ICustomDictionary { get; set; }
+ public ICustomSet<MyClassWithCustomCollections> ICustomCollection { get; set; }
+ public IReadOnlyList<int> IReadOnlyList { get; set; }
+ public IReadOnlyDictionary<MyClassWithCustomCollections, int> UnsupportedIReadOnlyDictionaryUnsupported { get; set; }
+ public IReadOnlyDictionary<string, int> IReadOnlyDictionary { get; set; }
+ }
+
+ public class CustomDictionary<TKey, TValue> : Dictionary<TKey, TValue>
+ {
+ }
+
+ public class CustomList : List<string>
+ {
+ }
+
+ public interface ICustomDictionary<T> : IDictionary<T, string>
+ {
+ }
+
+ public interface ICustomSet<T> : ISet<T>
+ {
+ }
+ }
+ """;
+
+ await VerifyAgainstBaselineUsingFile("TestCollectionsGen.generated.txt", testSourceCode, assessDiagnostics: (d) =>
+ {
+ Assert.Equal(6, d.Length);
+ Test(d.Where(diagnostic => diagnostic.Id is "SYSLIB1100"), "Did not generate binding logic for a type");
+ Test(d.Where(diagnostic => diagnostic.Id is "SYSLIB1101"), "Did not generate binding logic for a property on a type");
+
+ static void Test(IEnumerable<Diagnostic> d, string expectedTitle)
+ {
+ Assert.Equal(3, d.Count());
+ foreach (Diagnostic diagnostic in d)
+ {
+ Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity);
+ Assert.Contains(expectedTitle, diagnostic.Descriptor.Title.ToString(CultureInfo.InvariantCulture));
+ }
+ }
+ });
+ }
+
private async Task VerifyAgainstBaselineUsingFile(
string filename,
string testSourceCode,
- LanguageVersion languageVersion = LanguageVersion.Preview)
+ LanguageVersion languageVersion = LanguageVersion.Preview,
+ Action<ImmutableArray<Diagnostic>>? assessDiagnostics = null)
{
string baseline = LineEndingsHelper.Normalize(await File.ReadAllTextAsync(Path.Combine("Baselines", filename)).ConfigureAwait(false));
string[] expectedLines = baseline.Replace("%VERSION%", typeof(ConfigurationBindingSourceGenerator).Assembly.GetName().Version?.ToString())
.Split(Environment.NewLine);
var (d, r) = await RunGenerator(testSourceCode, languageVersion);
-
- Assert.Empty(d);
Assert.Single(r);
+ (assessDiagnostics ?? ((d) => Assert.Empty(d))).Invoke(d);
Assert.True(RoslynTestUtils.CompareLines(expectedLines, r[0].SourceText,
out string errorMessage), errorMessage);