-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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
Enable binding to non-null IEnumerable properties. #52514
Closed
SteveDunn
wants to merge
1
commit into
dotnet:main
from
SteveDunn:issue-36390-binding-to-non-null-ienumerable
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought the idea was to actually create a new
List<T>
and call the theIEnumerable<T>
setter with that.It looks like this is effectively as-casting by checking if the
IEnumerable<T>
was really initialized with an object implementingICollection<T>
and adding the items to the existing object.I do not think we should be mutating objects declared to be
IEnumerable<T>
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @halter73 . Thank you for the feedback!
My interpretation of the original issue is that if the type of the property is an
IEnumerable<T>
and is non-null, and is a mutable collection (i.e. has anAdd
method, à laICollection<T>
), then it should be set. This is from the issue:Re. mutating:
Objectively, In this case, we're not mutating the object per se, rather, we're checking the runtime type to see if it's a mutable collection, and using what's already there; the
Add
method.But subjectively, this is clearly debatable. From reading the comments on the issue (assuming my interpretation is correct), this has previously been debated.
This is my first contribution to this repo, so I wanted to be sure I was fixing the right thing in the right place. To ensure the right thing aspect, I created a separate stand-alone exe and consumed the new DLL containing my fix. I used the exact DTO and config from the issue itself. But as I say, this is my first contribution, and it is entirely possible that I have misinterpreted the original issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at the issue, it's not very clear. I don't want to speak for everyone, but I thought the idea was to have ConfigurationBinder treat settable properties declared to be
IEnumerable<T>
similarly toT[]
. That is to say that if the property already has an initialIEnumerable<T>
value, ConfigurationBinder would allocate a newList<T>
copy over the items from the initialIEnumerable<T>
to the newList<T>
and then copy over the config items afterwards.This would be very similar to
BindArray
:runtime/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs
Lines 449 to 482 in 0ee10e9
I think that's what we agreed on. Is that right @maryamariyan @ericstj @safern @davidfowl?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
System.Text.Json and most of the deserializers out there overwrite the collection on the POCO, so IMHO, I think we should be consistent with what other deserializers do. However we at the moment append to a non-null (for Collections, for Array we overwrite) and we had a feature request to support this: #46988.
So I think that would be a breaking change, but I think it is worth making it?
Maybe we could leave the append behavior behind a flag?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this comment below relates to issue #46988; that issue and the issue that this PR addresses are intertwined so of course the comment is perfectly apposite:
I don't think that'd work as we can't assume that a property declared as
IEnumerable<T>
could be set with aList<T>
. Take for example this scenario:(yes, the setter for
Data
does allow another collection type, but I'd be miffed if something changed it after I'd explicitly set it, and it could potentially break anything that depended on the behaviour of the funky list)As mentioned above re. two similar issues: there does appear to be some disparity between them. Paraphrasing from #46988: 'if someone provides a list with some items, we should overwrite those items'. The issue fixed here is 'if someone provides an
IEnumerable
property which has a runtime type that is a mutable collection, then populate that collection'These two issues certainly seem to be at odds with each other and seem to be mutually exclusive; we can't fix one without breaking the other.
Just my $0.02: a corollary from the juxtaposition of items #36390 and #46988 is the concept of augmentation. Augmentation goes beyond basic binding of types (IMO). I want a type which contains data which is set at runtime (like in the constructor), but I don't want a straight 'blat whatever I set at runtime with whatever is in cofig', Instead, I'd like to augment with static data from config. #46988 touches on this by mentioning a flag specific to lists, but I don't think it goes far enough. Perhaps we need something like:
Anyway, that was just an idea as the term 'binding' seems rather vague (or more kindly, incomplete) given the complexities and choices that are highlighted by these two issues.
I suppose we are left needing to answer a few questions:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe something to cogitate is what this library offers. The mention of deserializers and their mostly common behaviours could hint at a leaky abstraction. From a users perspective, I provide the library some json and a
Type
and I'd like the library to populate that type with the contents of the json. I think the word populate here is ambiguous though as it could mean 'overwrite' or could mean (as mentioned in my comment above) 'augment'.From an API design perspective, the entry point is via this:
var val = config.GetSection("config").Get<Config>();
Perhaps, instead of
Get
we had eitherDeserializeTo
orAugmentTo
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Those are really valid points. Thanks, @SteveDunn for your thoughts. I think we should focus on fixing the real issue here which is being able to bind to non-null empty collection.
Then if there is real need to change the behavior and to override always, we could reopen: #46988 and build a real scenario to bring it back to discussion with the API Review crew. @halter73 does that make sense? It seems like we keep looping on what we should do, I think we should keep it "simple" now and fix the reported issue that is affecting customers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the real issue? Does fixing the real issue require mutating an object that's declared to be
IEnumerable
? I think that's a change for the worse not the better.I don't see what makes empty settable IEnumerables special, especially when empty arrays and empty ICollections are not. I'm fine taking this back to API review because it is a behavioral change. I thought what I suggested was what we agreed on in API review last time, but that clearly wasn't recorded.
I would argue that my suggested behavioral change is far less surprising and more consistent than the current behavior and could be treated as a simple bug fix. I'd be surprised to find code relying on settable IEnumerables not being set by config. This appears to be mutating non-settable IEnumerables which seems to me much more likely to be breaking.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The type is
IEnumerable
, but the object that the user provides is a mutable collection. That distinction is relevant as this is the convention used by binding when it comes across anull
enumerable (it mutates it to a list).I think this is a really important observation and really describes the issue! Being new to the party, I was on the fence about this issue and picked it up because it looked fairly easy and would be a nice entry point to what I hope will be many contributions. But by asking asking that question, I now believe that fixing this issue is the correct approach:
Why are empty enumerables special, but empty arrays and collections are not special...
Below are two examples, both, in the current implementation (net5.0): the first one showing that nothing is special and everything is consistent; the second one showing that empty enumerables are special and things are not consistent.
With this config:
... and this POCO:
.. running this (again, this is
net5.0
) produces:Empty arrays, empty collections, and empty enumerables are not special. So far so good, we're consistent.
But given this: POCO
... we get:
Now, empty arrays and collection are not special, but empty enumerables are special.
So, going back to the question, we can now ask it again with additional emphasis:
Clearly, they shouldn't be, but to answer that question...
In the first run
A convention was applied. That convention turned the enumerable into a list. That's fair enough as it's likely what the user wanted and is pretty safe from the immutability standpoint.
In the second run
The user manually applied that convention (of treating an enumerable as a list). The outcome is still the same: the enumerable property is backed by a list and is represented by an enumerable, and most importantly, it's still safe from the immutability standpoint.
So, if either way, in both example, we have an enumerable backed by a list and the only difference being one is set by convention and one is being set by the user. So the question is now why does functionality differ?
Perhaps if the issue behind this PR was worded something like:
... then it would be more clear-cut.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We discussed this issue offline today.
It was also discussed that a big difference between deserializing and configuration binding was there are multiple sources in binder.
There is a lack of clarity because there was not enough information upfront in the issue before setting this as
up-for-grabs
.It is best if we close this PR and do it ourselves.
Thanks for your contribution @SteveDunn.
cc: @HaoK