Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Binding to non-null IEnumerable doesn't work #36390

Closed
Tracked by #64015
brockallen opened this issue Mar 4, 2017 · 24 comments
Closed
Tracked by #64015

Binding to non-null IEnumerable doesn't work #36390

brockallen opened this issue Mar 4, 2017 · 24 comments

Comments

@brockallen
Copy link

brockallen commented Mar 4, 2017

AB#1277574
When using the binding feature in the config system, for IEnumerable collections that have a default, non-null value, the collection is never assigned into the target model. For example:

For this json:

{
  "config": {
    "data": [ "a", "b" ]
  }
}

and this application code:

 public class Config
    {
        IEnumerable<string> _data = new List<string>();
        public IEnumerable<string> Data
        {
            get
            {
                return _data;
            }
            set
            {
                _data = value;
            }
        }
    }

    public class Program
    {
        public static void Main(string[] args)
        {
            var config = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("test.json")
                .Build();

            var val = config.GetSection("config").Get<Config>();
            Console.WriteLine(val?.Data?.Count());
        }
    }

The output is:

0
Press any key to continue . . .

I'd expect it to print "2".

I'd argue the issue is in BindInstance and explicit handling for IEnumerable should be made (much like the check for IsArray https://github.com/aspnet/Configuration/blob/dev/src/Microsoft.Extensions.Configuration.Binder/ConfigurationBinder.cs#L281).

@brockallen brockallen changed the title Binding to non-null IEnumerbale doesn't work Binding to non-null IEnumerable doesn't work Mar 4, 2017
@sei-jmattson
Copy link

I ran into this issue today and came to the same conclusion as @brockallen

@ajcvickers
Copy link
Member

We could treat a non-null but empty collection as something that we add to. This way an empty collection would be treated the same as a null collection.

@kevinlo
Copy link

kevinlo commented Aug 14, 2017

Can you handle the non-null and non empty collection case too?

e.g. IEnumerable _data = new[] { "c" };

The Config data can have default value if it does not exist in the JSON and can be overwritten if it exists in the JSON.

I find this problem when I try to bind the IdentityServer4 Client

For version 1.5.2, the allowedGrantTypes is IEnumerable and has default value (it is changed to ICollection in latest dev version). I try to put the client settings in the JSON file but the AllowedGrantTypes can't be overwritten because it is IEnumerable with default value. It works find if the IEnumberable is null or if it is ICollection

    private IEnumerable<string> _allowedGrantTypes = GrantTypes.Implicit; 

@ajcvickers ajcvickers transferred this issue from aspnet/Configuration Dec 15, 2018
@lbogdanov
Copy link

@ajcvickers - any updates on when this is going to be fixed? Thank you!

@ajcvickers
Copy link
Member

@lbogdanov It's currently in the backlog, which means it's not going to be fixed in an upcoming release. Beyond that, it will depend on prioritization against other work.

@speciesunknown
Copy link

The correct behaviour is to overwrite. Not append.
This is how other collections behave, and its generally how a json parser behaves regarding default values on a POCO.

@analogrelay analogrelay transferred this issue from dotnet/extensions May 13, 2020
@Dotnet-GitSync-Bot Dotnet-GitSync-Bot added area-Extensions-Configuration untriaged New issue has not been triaged by the area owner labels May 13, 2020
@analogrelay analogrelay added this to the Future milestone May 13, 2020
@maryamariyan maryamariyan removed the untriaged New issue has not been triaged by the area owner label Jul 1, 2020
@maryamariyan maryamariyan added the help wanted [up-for-grabs] Good issue for external contributors label Oct 8, 2020
@IanKemp
Copy link

IanKemp commented Oct 9, 2020

For .NET 5.0: https://github.com/dotnet/runtime/blob/master/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs#L327
For .NET Core 3.1.8: https://github.com/dotnet/extensions/blob/v3.1.8/src/Configuration/Config.Binder/src/ConfigurationBinder.cs#L328

@lbogdanov basically this is never going to be fixed because the team doesn't care enough to do it. Instead of fixing bugs they would rather just keep piling on new features, because bugs are boring and don't get blog posts written about them.

@reacheight
Copy link

Basically, why don't we check if the instance runtime type is some ICollection<T> instead of checking its declared type in https://github.com/dotnet/runtime/blob/master/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs#L334? If it is, then its declared type is either some ICollection<T> or IEnumerable<T>, and binding should be fine.

collectionInterface = FindOpenGenericInterface(typeof(ICollection<>), instance.GetType());
if (collectionInterface != null)
{
     BindCollection(instance, collectionInterface, config, options);
}

I've checked this on both empty and not empty initialized IEnumerable<T>s and it seems to work, all tests are also passed. If that's okay, I'd like to open PR with this change.

@ericstj
Copy link
Member

ericstj commented Feb 2, 2021

It seems reasonable to treat an empty enumerable in the same way as null. I'm less inclined to examine the runtime type unless we have some precedent for that. Additionally, it doesn't fix the cases where folks backed the property with Enumerable.Empty or an Array. cc @eerhardt @maryamariyan

@ericstj
Copy link
Member

ericstj commented Feb 2, 2021

Related: #46988 (which could also solve this problem)

@maryamariyan
Copy link
Member

As part of the closing discussions for issue #46988, it was noted that the current behavior for collections seems unexpected and rather than introducing an opt-in API for it, we could perhaps fix the default behavior. Using this issue to track the fix.

cc: @halter73 @safern

@maryamariyan maryamariyan modified the milestones: Future, 6.0.0 Feb 11, 2021
@SteveDunn
Copy link
Contributor

I'd like to look at this if it's still 'up-for-grabs'?

@safern
Copy link
Member

safern commented May 7, 2021

This is my first PR on this repo and I'm stumbling around like a newborn Giraffe!

Not a problem. I've pinged you in discord, also feel free to post your questions here if you rather keep them in the open.

SteveDunn added a commit to SteveDunn/runtime that referenced this issue May 9, 2021
Binding should work for an IEnumerable property instantiated with a collection type.
In the case of IEnumerable, rather than just checking the type of the property,
we also now check the instance type to see if it's `ICollection` based in order
to populate it.

Fix dotnet#36390
@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label May 9, 2021
@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label May 19, 2021
@maryamariyan maryamariyan removed the help wanted [up-for-grabs] Good issue for external contributors label May 19, 2021
@maryamariyan maryamariyan assigned halter73 and unassigned SteveDunn May 19, 2021
@danmoseley
Copy link
Member

@maryamariyan I'm going through 1st party asks. From what I can see, this is still open, we're still interested in doing this, but it can't fit this release any more?
If so please change milestone. Or close if appropriate.

@maryamariyan
Copy link
Member

maryamariyan commented Jul 21, 2021

For triage, will discuss with @halter73 on next steps for this.

@maryamariyan maryamariyan modified the milestones: 6.0.0, 7.0.0 Jul 21, 2021
@fakhrulhilal
Copy link

I have the same problem, and use this workaround to avoid analyzer complaint:

public class Config
{
    public IEnumerable<string> Data { get; set; } = null!;
}

But I agree to solve this. No one would understand to make it work without complain from analyzer.

@SteveDunn
Copy link
Contributor

I have the same problem, and use this workaround to avoid analyzer complaint:

public class Config
{
    public IEnumerable<string> Data { get; set; } = null!;
}

But I agree to solve this. No one would understand to make it work without complain from analyzer.

@maryamariyan - do you know if there's been any more thought on this one?

@maryamariyan
Copy link
Member

Here's a summary of where we're at with this issue:

When we set this issue up for grabs the first time, the desired fix was not clearly defined.

As part of closing the original PR we uncovered that there is a subtle difference between configuration binding and System.Text.Json deserialization. Because with configuration we have to append data from multiple sources but deserialization has a single source of truth and that is why we don't think of configuration binding as a deserializer. (In a way for the configuration case we are mapping what is already been de-serialized to a POCO.)

This comment covers where we left off from the original PR: #52514 (comment)

We decided since then that the proper fix here for this issue would be very similar to BindArray:

private static Array BindArray(Array source, IConfiguration config, BinderOptions options)
{
IConfigurationSection[] children = config.GetChildren().ToArray();
int arrayLength = source.Length;
Type elementType = source.GetType().GetElementType();
var newArray = Array.CreateInstance(elementType, arrayLength + children.Length);
// binding to array has to preserve already initialized arrays with values
if (arrayLength > 0)
{
Array.Copy(source, newArray, arrayLength);
}
for (int i = 0; i < children.Length; i++)
{
try
{
object item = BindInstance(
type: elementType,
instance: null,
config: children[i],
options: options);
if (item != null)
{
newArray.SetValue(item, arrayLength + i);
}
}
catch
{
}
}
return newArray;
}
.

@SteveDunn we could set it up for grabs if you're open to making the above fix. Thanks again for staying engaged on this.

/cc @halter73 @davidfowl

@SteveDunn
Copy link
Contributor

Hi @maryamariyan . Yes very happy to have another crack at this one. Will be great to get it done!

@maryamariyan
Copy link
Member

closed via @66131

@ghost ghost locked as resolved and limited conversation to collaborators Apr 16, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging a pull request may close this issue.