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

ConfigurationBuilder - Configuration.Json: Binding a Dictionary<string,string> error when Key contains an string url like http://www.google.es #42643

Closed
Tracked by #77390
ghost opened this issue Sep 21, 2020 · 20 comments
Labels
api-needs-work API needs work before it is approved, it is NOT ready for implementation area-Extensions-Configuration enhancement Product code improvement that does NOT require public API changes/additions
Milestone

Comments

@ghost
Copy link

ghost commented Sep 21, 2020

Describe the bug

Im reading a json file to a dictionary<string,string> like:

{
  "auths": {
    "https://www.google.es": {
      "uri": "https://www.google.es"
    }
  }
}

having the POCO class

public class MyClass
{
        [ConfigurationProperty("auths", IsRequired = false)]
        public Dictionary<string, OtherObject> Auths { get; set; }
}

When i load it using ConfigurationRoot like

public MyClass Read()
        {
            IConfigurationRoot configuration =
                new ConfigurationBuilder()
                    .AddJsonFile("path_to_json_file", optional: true, reloadOnChange: true)
                .Build();

            // Try Parse and return (if will exception in case of binding error)
            var _settings= new MyClass();
            configuration.Bind(_settings);

            return _settings;
        }

The issue is that the Dictionary item contains https only on the Key property. Looks like it's ignoring or having issues with https://.....

Not sure if it's an intended feature or a bug.

To Reproduce

Steps to reproduce the behavior:

  1. Using version '3.1.8' of package 'Microsoft.Extensions.Configuration.Json'
  2. Run this code 'with files in description'
  3. See error in the screenshot attached

Expected behavior

Dictionary Key Item should contains https://www.google.es

Screenshots

image

@KalleOlaviNiemitalo
Copy link

KalleOlaviNiemitalo commented Sep 21, 2020

That seems deliberate to me. The configuration system flattens the key hierarchy to a string with colons as delimiters so it becomes "auths:https://www.google.es:uri", and binding then cannot know where the boundaries were originally.

It is documented in Hierarchical configuration data.

@ghost
Copy link
Author

ghost commented Sep 21, 2020

That makes sense, even if i don't agree on using : as hierarchy... but, i will try to look into the code to see if the separator char can be changed / configured somewhere.

Thanks for the reply i completely missed that point !

@KalleOlaviNiemitalo
Copy link

@KalleOlaviNiemitalo
Copy link

By the way, I don't think the Microsoft.Extensions.Configuration.* packages will care about [ConfigurationProperty("auths", IsRequired = false)] in your POCO class.

@ghost
Copy link
Author

ghost commented Sep 22, 2020

Im using the ConfigProperty due other reasons.
About the limiter... i will need to use JsonSerializer :\

@KalleOlaviNiemitalo
Copy link

Can you use some other strings as keys? For example:

{
  "auths": {
    "es": {
      "uri": "https://www.google.es"
    }
  }
}

Then register an IPostConfigureOptions<MyClass> implementation that reads the Dictionary<string, OtherObject> Auths property that was populated from configuration binding, and copies the objects to a separate dictionary with the keys that you really want. The "es" key would be used only in configuration files or environment variables, not for looking up OtherObject instances later.

@ghost
Copy link
Author

ghost commented Sep 23, 2020

Well, not in this case, i need to use the Uri as key. But anyway, i can do exactly the same using the JsonSerializer with a custom serializer, and works well.

Without the need of : in the key, the classical approach works pretty well.

Thank you for your help, i think we can close the issue ?

@BrennanConroy BrennanConroy transferred this issue from dotnet/extensions Sep 23, 2020
@Dotnet-GitSync-Bot Dotnet-GitSync-Bot added area-Extensions-Configuration untriaged New issue has not been triaged by the area owner labels Sep 23, 2020
@ghost
Copy link

ghost commented Sep 23, 2020

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

@BrennanConroy
Copy link
Member

Maybe we want to have an escape sequence for json config so this is allowed? Or a flag to disable the ':' behavior just for Json?

If any change is made, we should port it to the Newtonsoft.Json config provider in Extensions.

@JunTaoLuo
Copy link
Contributor

Looks like this issue has been encountered previously, e.g. aspnet/Configuration#792 dotnet/extensions#782. Sounds like something we should address instead of our usual answer that : is a reserved character and this issue is by design.

@maryamariyan maryamariyan added this to the Future milestone Sep 24, 2020
@maryamariyan maryamariyan removed the untriaged New issue has not been triaged by the area owner label Sep 24, 2020
@maryamariyan maryamariyan modified the milestones: Future, 6.0.0 Oct 22, 2020
@maryamariyan maryamariyan added this to Uncommitted in ML, Extensions, Globalization, etc, POD. via automation Oct 22, 2020
@maryamariyan maryamariyan added the enhancement Product code improvement that does NOT require public API changes/additions label Oct 22, 2020
@maryamariyan maryamariyan modified the milestones: 6.0.0, 7.0.0 Jul 26, 2021
@ghost ghost moved this from 6.0.0 to Untriaged in ML, Extensions, Globalization, etc, POD. Jul 26, 2021
@maryamariyan maryamariyan moved this from Untriaged to 7.0.0 in ML, Extensions, Globalization, etc, POD. Aug 4, 2021
@SteveDunn
Copy link
Contributor

SteveDunn commented Mar 19, 2022

I'm happy to look at this one. PR here: #66886

@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label Mar 21, 2022
@SteveDunn
Copy link
Contributor

@AraHaan
Copy link
Member

AraHaan commented Mar 22, 2022

I do not see the reason for :'s in json keys due to how everything gets messed up at runtime with them in the keys. Also it seems to make the json more verbose. I would think this should be by design.

@SteveDunn
Copy link
Contributor

@AraHaan - I personally wouldn't have been able to see the use of keys with colons in, but the examples here are quite compelling.

@tebeco
Copy link

tebeco commented Mar 23, 2022

you're describing an array, but you're writing object.
Here is a configuration with array:

{
  "auths": [
    {
      "name": "http://google",
      "uri": "https://www.google.es"
    },
    {
      "name": "http://microsoft",
      "uri": "https://www.microsoft.es"
    }
  ]
}
public class AuthEntry
{
  public string Name {get;set;}
  public Uri Uri {get;set;}
}

public class AuthsOptions
{
  public const string SectionName = "auths";
  public AuthEntry[] Auths {get;set;}
}

public static class AuthsServiceCollectionExtensions
{
  public static IServiceCollection AddAuths(this IServiceCollection services)
  {
    services
      .AddOptions<AuthsOptions>()
      .Configure<IConfiguration>((options, configuration) => configuration.Bind(AuthsOptions.SectionName, options))
      //.Validate(...............)
    ;

    return services;
  }

  public static IServiceCollection AddAuths(this IServiceCollection services, Action<AuthsOptions> 
  configure) =>
    services
      .AddAuths()
      .Configure(configure)
    ;
public class MyService
{
  private readonly AuthsOptions _options;
  private readonly HttpClient _httpClient;

  public MyService(IOptions<AuthsOptions> options, HttpClient httpClient)
  {
    _options = options.Value; //.Value here because there's no reload on change, you could adapt it
    // build a dictionary from key ?
  }
  
  public async Task<string> GetDataFrom(string key)
  {
    var authEntry = options.Auths.First(authEntry => authEntry == key); // use stringcomparison ?
    
    await _httpClient.GetStringAsync(new Uri(authEntry.Uri, otherRelativeUri);
  }
}

@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Apr 5, 2022
@maryamariyan maryamariyan self-assigned this Apr 6, 2022
@maryamariyan maryamariyan added the api-needs-work API needs work before it is approved, it is NOT ready for implementation label Apr 6, 2022
@eerhardt
Copy link
Member

We will consider this in a future release. Moving this issue out of the 7.0 milestone.

@eerhardt eerhardt modified the milestones: 7.0.0, Future Jul 14, 2022
@maryamariyan maryamariyan modified the milestones: Future, 8.0.0 Sep 28, 2022
@SteveDunn
Copy link
Contributor

This is the motivation behind #67616

@maryamariyan maryamariyan removed their assignment Jan 9, 2023
@rjgotten
Copy link

you're describing an array, but you're writing object.

Many times you have to use objects by necessity as arrays are completely useless for everything but the most basic configuration needs when you use environment-specific configuration overrides. This is because arrays use positional (index) based merging across environment-specific config files which is an absolute pain to manage and slip-ups in it are many times impossible to detect.

Basically; you're telling people: "You're doing it wrong. You're supposed to step into this minefield here."

@rjgotten
Copy link

rjgotten commented Apr 26, 2023

Looks like this issue has been encountered previously, e.g. aspnet/Configuration#792 dotnet/extensions#782. Sounds like something we should address instead of our usual answer that : is a reserved character and this issue is by design.

There was never any technical need for the : to be a reserved character to begin with.
That problem stems from the fact that the initial design for the internal storage of the configuration data in Microsoft.Extensions.Configuration was thought out poorly and stores everything in a flat dictionary by fully composed key, rather than use an appropriate tree-based data structure.

There are multiple other issues where this ill-thought out design is the root of the problem.
For instance; the performance problems with the ConfigurationBinder, when binding collection types. Because it has to loop over the entire collection of keys every time to determine which full keys are children of the collection key. (Which it does in GetChildKeys)

That was reported in #65885 as well, but ultimately wasn't acted upon (yet) because of fear of breaking existing implementations as well. At least that had a decent workaround. This one doesn't. You simply have to avoid using a dictionary keyed on anything that contains a : and that's it. No recourse.

Another problem with this internal storage of fully composed keys is that it's impossible to have a dictionary of empty objects.
That could be perfectly valid when bound to a Dictionary<string, Thing> where Thing is a POCO with default values assigned to properties. But because the config system stores fully composed keys only everything has to be a string->string key-value mapping. And as e.g. { "Items" : { "Foo" : {}, "Bar" : {}} does not have representable values in keys 'below' "Foo" or "Bar", that entire configuration is simply lost and you end up with an empty bound dictionary.

That would not be the case with a proper tree-based structure. Because there you'd have a string->node mapping where each node can optionally have a primitive value itself, and can have child nodes. Even if the collection of child nodes is empty; and even if the node has no primitive value itself; the node and its key are still representable in a sound way. And thus binding such empty objects would be feasible.

So...
Maybe, rather than trying to slap band-aid upon band-aid to try and fight symptoms of bad design, it's time to tackle the actual bad design?

@layomia
Copy link
Contributor

layomia commented Jul 21, 2023

Collapsing this into #67616 since that would be the fundamental change to enable this scenario.

@layomia layomia closed this as completed Jul 21, 2023
@ghost ghost locked as resolved and limited conversation to collaborators Aug 21, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-needs-work API needs work before it is approved, it is NOT ready for implementation area-Extensions-Configuration enhancement Product code improvement that does NOT require public API changes/additions
Projects
None yet
Development

Successfully merging a pull request may close this issue.