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

Issue 36015 new BinderOption to throw on missing configuration #53852

Conversation

SteveDunn
Copy link
Contributor

@SteveDunn SteveDunn commented Jun 8, 2021

Fixes: #36015

@dotnet-issue-labeler
Copy link

Note regarding the new-api-needs-documentation label:

This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, to please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change.

@ghost
Copy link

ghost commented Jun 8, 2021

Tagging subscribers to this area: @maryamariyan, @safern
See info in area-owners.md if you want to be subscribed.

Issue Details
Author: SteveDunn
Assignees: -
Labels:

area-Extensions-Configuration, new-api-needs-documentation

Milestone: -

@SteveDunn SteveDunn force-pushed the issue-36015-binderoption-to-throw-on-missing-configuration branch 2 times, most recently from 8b53c47 to ae0ae28 Compare June 14, 2021 06:31
@SteveDunn SteveDunn force-pushed the issue-36015-binderoption-to-throw-on-missing-configuration branch from ae0ae28 to 89de0f3 Compare June 14, 2021 06:33
@@ -209,7 +209,27 @@ private static void BindNonScalar(this IConfiguration configuration, object inst
{
if (instance != null)
{
foreach (PropertyInfo property in GetAllProperties(instance.GetType()))
List<PropertyInfo> modelProperties = GetAllProperties(instance.GetType()).ToList();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need the ToList()?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see it is to enumerate the list twice. Maybe we can void it and check this as we bind the properties, aggregate the missing names on a list if the flag to throw is enabled and then throw?

Copy link
Contributor Author

@SteveDunn SteveDunn Jun 15, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If performance is the driving force behind this suggestion, then enumerating just once would be the best thing to do. I'm guessing though, that binding configuration won't be on a hot path. Hence, in my opinion, it is more beneficial this way as:

  • the list of properties has already been gathered (we're just enumerating twice, not building it twice)
  • it is logically self-contained; a distinct step before any binding is attempted
  • it fails fast: if we check as we bind we could end up with an object which has some properties that are set and some which aren't set
  • if we check as we bind, then the enumeration of property names in config would happen on BindProperty, which would result in more enumerations than we currently have

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those are valid points. Sounds good. I think since this is opt-in, I don't mind an extra enumeration, also Binding usually just happens once in a lifetime of the application.

What do you think of removing Linq usages? We have been trying to do that on these libraries.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy to remove Linq usages if that fits in with the plan for libraries. May I ask why though? I've certainly seen, in my day job, overuse of Linq especially in tight loops, which cause a lot of overhead for the GC with temporary objects. But outside of tight loops or hot paths, I think the declarative nature of Linq makes the code easier to understand.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just looked into doing this imperatively by creating an empty List to hold the missing property names and found that we could end up with a sparse array, which I estimate would counter any gains achieved by switching from Linq. One the one hand, we create a zero-sized List, in which case the size doubles for every addition potentially leading to a sparse array, or we could create the List with the same size as the amount of children and assume everything is missing, in which case it's likely that not everything is missing and we'll again end up with a sparse array.

I don't think these scenarios are tragic though; whether it's the overhead of a Linq enumerator, or a few dozen empty slots in an array, this usually happens just once.

Bearing this in mind, I think it's no less efficient to use Linq, but If you'd like me to switch, then I'm happy to do so.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't mind an extra enumeration, also Binding usually just happens once in a lifetime of the application.

I think if you use IOptionsSnapshot for example then binding could happen once per request

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, Microsoft.Extensions.Configuration has already used Linq to build the collection that we further 'compose'

The reason reducing Linq was brought up, has to do with this issue #44598, for example https://github.com/dotnet/runtime/pull/44825/files

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found this post by Andrew Lock:
https://andrewlock.net/the-dangers-and-gotchas-of-using-scoped-services-when-configuring-options-in-asp-net-core/

It says:

Note: As the strongly-typed settings are re-built with every request, and the binding relies on reflection under the hood, you should bear performance in mind. There is currently an open issue on GitHub to investigate performance.

It links to aspnet/Configuration#604

Seeing as Reflection is the slowest part of the process, I think the extra enumeration is fairly insignificant.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetAllProperties is a private method that's typed as returning IEnumerable<PropertyInfo>, but its implementation is actually constructing and returning a List<PropertyInfo>. The right change here then is to just change that private method to return a List<PropertyInfo> and remove the ToList call from the call site.

.Select(mp => $"'{mp.Key}'")
.ToList();

if (missingPropertyNames.Any())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are creating a List for these missing properties, you could use Length instead and avoid Linq.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've used the Count property. After about 15 years of .NET, I too still cannot remember whether it's .Length or .Count :) Luckily, ReSharper knows!

Copy link
Member

@safern safern left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@safern safern merged commit 33cf378 into dotnet:main Jun 17, 2021
@safern
Copy link
Member

safern commented Jun 17, 2021

Thank you very much @SteveDunn! Feel free to keep picking issues and we'll be happy to help and merge your contributions.

@ghost ghost locked as resolved and limited conversation to collaborators Jul 17, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
4 participants