I'm currently trying to use the default ConfigurationBinder
from AspNetCore 6 especially IConfiguration.Get<T>()
Get<T>() is used to map a config file section to an object,
but it doesn't work correctly with collections like arrays and lists.
It works fine if there are at least 2 items in the array
but if there's only one item in the array then the mapping doesn't work. This seems to only affect XML files.
The array can be nested deep inside my sections.
The main problem seems to be that the config keys are generated like this:
collection1:item:myName for arrays with one element.
vs
collection2:item:0:myName for arrays with more than one element.
Has anyone a good idea how to accomplish the mapping to arrays
inside XML config sections that might have one or more elements?
// Nugets:
// Microsoft.Extensions.Configuration
// Microsoft.Extensions.Configuration.Xml
// Microsoft.Extensions.Configuration.Binder
public void Test()
{
var builder = new ConfigurationBuilder();
builder.AddXmlFile("MyConfig.config");
var config = builder.Build();
var section1 = config.GetSection("collection1");
var section2 = config.GetSection("collection2");
var bound1 = section1.Get<MyObject>(); // ERROR: Mapped to empty List
var bound2 = section2.Get<MyObject>(); // mapped correctly to 2 items
}
public class MyObject
{
public List<MyItem> Item { get; set; }
}
public class MyItem
{
public string MyName { get; set; }
public string MyVal { get; set; }
public override string ToString() => $"{MyName} = {MyVal}";
}
MyConfig.config
<configuration>
<collection1 >
<item myName="MyName1" myVal="MyString1" />
</collection1>
<collection2 >
<item myName="MyName1" myVal="MyString1" />
<item myName="MyName2" myVal="MyString2" />
</collection2>
</configuration>
Do not get objects yourself, configure a mapping:
services.Configure<MyObject>(configuration.GetSection("collection1"));
Then inject your settings where necessary as Options:
public class MyClass{
public MyClass(IOptions<MyObject> options){
...
}
}
This should handle the keys correctly.
That said, keep in mind that arrays in configurations can produce a lot of problems when using multiple stages, i.e. production and dev configurations files, see my answer here (as I see now from your question, the samples there are probably not 100% correct for an array with one element, but they definitely are for multiple elements).
On the other hand, is the number of elements really unbound or like 5 or 10 would be enough? You could add normal properties, e.g. 1 to 5, and collect them in an array in your MyObject, like:
public class MyObject{
public string P1 {get;set;}
public string P2 {get;set;}
public List<string> GetProperties(){...}
}
I think i found an easy solution to the Problem for now.
<configuration>
<collection1 >
<item name="MyName1" myVal="MyString1" />
</collection1>
<collection2 >
<item name="MyName1" myVal="MyString1" />
<item name="MyName2" myVal="MyString2" />
</collection2>
</configuration>
If we give each element of the collection the attribute name
then the mapping works fine as long as the name is unique.
Binding a single element to an array on ConfigurationBinder will be fixed in .NET 7.
https://github.com/dotnet/runtime/issues/57325
Related
I hope someone can help me. This is code I use to retrieve a bunch of data about a particular device with an api (in this case, a Twinkly light string).
Here's my code, which is partially functional.
HttpResponseMessage result = await httpClient.GetAsync(uri);
string response = await result.Content.ReadAsStringAsync();
JObject jObject = JObject.Parse(response);
Layout layout = new Layout();
layout = JsonConvert.DeserializeObject<Layout>(response);
I say it's "partially" functional because every property that is in the root level deserializes into the model just fine, but the json also returns a property called "coordinates" which consists of an array entry for each bulb, and each entry has three values for x,y,z.
I have tried a lot of stuff to get the data from the coordinates array and i can break-mode view that the data is in there.
However it doesn't deserialize properly. I have the correct number of elements in the coordinates array, but they are all x:0, y:0, z:0
Here is my model schema. I hope someone can help me with this. This is my first foray into api work, and the first time i've had a nested model like this.
internal class Layout
{
public int aspectXY { get; set; }
public int aspectXZ { get; set; }
public LedPosition[] coordinates { get; set; }
public string source { get; set; } //linear, 2d, 3d
public bool synthesized { get; set; }
public string uuid { get; set; }
}
internal class LedPosition
{
double x { get; set; }
double y { get; set; }
double z { get; set; }
}
Note: I've tried assigning the properties manually like this:
JToken dataToken = jObject.GetValue("coordinates");
and that indeed received the data but it didn't help me as it merely moved the issue.
you don' t need parse and deserialized in the same time, it would be enough
var response = await result.Content.ReadAsStringAsync();
var layout = JsonConvert.DeserializeObject<Layout>(response);
to make LedPosition properties visible make them public too
public class LedPosition
{
public double x { get; set; }
public double y { get; set; }
public double z { get; set; }
}
since it is used by another class this class should be public too
public class Layout
One thing I've learned recently from the big dog CTO at my work was you can actually copy the JSON you're expecting, and go to Edit -> Paste Special -> Paste JSON as Classes in Visual Studio and it'll paste it as the classes you need, with the proper names/properties. Really slick. Maybe try that and see if it comes out with a different model than what you have now.
This is my first foray into api work
Two things I want to point out, then..
does the api you're using publish a swagger/open api document?
No - see 2 below
Yes - take a look at tools like NSwag(Studio), Autorest and others. You feed the swagger.json into them and they crank out a few thousand lines of code that creates a client that does all the http calling, deserializing, the classes of data etc. if means your code would end up looking like:
var client = new TwinklyLightClient();
var spec = client.GetTwinklyLightSpec();
foreach(var coord in spec.Coords)
Console.Write(spec.X);
This is how APIs are supposed to be; the tools that create them operate to rules, the tools that describe them operate to rules so the consumption of them can be done by tools operating to rules - writing boilerplate json and http request bodies is a job for a computer because it's repetitive and always follows the same pattern
The API doesn't publish a spec we can use to get the computer to write the boring bits for us. Durn. Well, you can either make the spec yourself (not so hard) or go slightly more manual
Take your json (view it raw and copy it)
Go to any one of a number of websites that turn json into code - I like http://QuickType.io because it does a lot of languages, has a lot of customization and gives advanced examples of custom type deser, but there are others - and paste that json in
Instantly it's transformed into eg C# and can be pasted into your project
It gives an example of how to use it in the comments - a one liner something like:
var json = httpCallHereTo.GetTheResponseAsJsonString();
var twinklyLightSpec = TwinklyLightSpec.FromJson(json);
Yes, visual studio can make classes from json, but it's not very sophisticated - it does the job, but these sites that make json to c# go further in allowing you to choose arrays or lists, what the root object is called, decorating every property with a JsonProperty attribute that specifies the json name and keeps the c# property to c# naming conventions (or allows you to rename it to suit you)..
..and they work out of the box, which would resolve this problem you're having right now
I want to Deserialize two different kind of Xml elements into one List as I want to preserve the order
<Step name="Login to Account" >
<Action name="LoginToWebsite" type="Login" />
<NewAction name="EnterTextInTextBox" type="SendKeysToTextBox" extraAttribute="Testvalue" />
</Step>
currently I am using the following to get the two actions into one list but I have to change the element name to NewAction (like above) instead of Action for the second one
[XmlRoot(ElementName = "Step")]
public class WorkflowStep
{
[XmlAnyElement("Action")]
public XmlElement[] Actions
{
get;
set;
}
}
As the XmlAnyElement is bound to "Action" Element name, how can I change to support two different Element Names but need to be Deserialized into one array
What you want is
[XmlRoot(ElementName = "Step")]
public class WorkflowStep
{
[XmlAnyElement("Action")]
[XmlAnyElement("NewAction")]
public XmlElement[] Actions
{
get;
set;
}
}
I am trying to deserialize an xml to an object to use. We have created templates and would like to keep the xml the same standard if possible. The problem I am trying to figure out is how to look within a standard node in the xml and all subnodes are the same object type, just with different node names.
For example:
<Account>
<AccountNumber>12345</AccountNumber>
<Balance>12.52</Balance>
<LateFee>0</LateFee>
</Account>
The Account level is always within the template, but everything below that is variable. Is there a way to deserialize all nodes within the Account level to be the same object?
Public Class AccountNode
{
Public String Name { get; set; }
Public String Value { get; set; }
}
Based on my research, it appears, they have to have a standard naming schema and then you can have an attribute to assign to the Name value. I just haven't been able to confirm that. If someone has a link that I haven't been able to find, or is knowledgeable and can confirm whether or not this is a possibility, I would like to know.
EDIT:
I have a much larger xml than listed above, so I'm trying to see how I can deserialize it.
<AccountNumber>
<KeyWord Name="Customer Account" isRegex="False" errorAllowance="10" LookFor="Customer Account">
<Rectangle>
<Left>200</Left>
<Bottom>350</Bottom>
<Right>600</Right>
<Top>690</Top>
</Rectangle>
<Relations KwName="Charges">
<Relation>above.0</Relation>
</Relations>
</KeyWord>
<Capture DataType="String" FindIfKeywordMissing="false">
<Rectangle>
<Left>200</Left>
<Bottom>350</Bottom>
<Right>600</Right>
<Top>690</Top>
</Rectangle>
<Relations anchorName="ChargeSection">
<Relation>rightOf.0</Relation>
<Relation>below.-20</Relation>
<Relation>above.20</Relation>
<Relation>width.150</Relation>
</Relations>
<Regex>Customer account\s+(\S+)</Regex>
</Capture>
</AccountNumber>
So with this one, I assume it is similar, but basically the Account Number node is the variable one and everything above and below it is standard.
You could use a surrogate [XmlAnyElement] public XElement[] AccountNodesXml property in your Account class to manually convert your AccountNode objects from and to XML nodes. Marking the property with XmlAnyElement ensures that the elements will taken verbatim from the XML:
public class Account
{
public Account() { this.AccountNodes = new List<AccountNode>(); }
[XmlIgnore]
public List<AccountNode> AccountNodes { get; set; }
[Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
[XmlAnyElement]
public XElement[] AccountNodesXml
{
get
{
if (AccountNodes == null)
return null;
return AccountNodes.Select(a => new XElement((XName)a.Name, a.Value)).ToArray();
}
set
{
if (value != null)
AccountNodes = value.Select(e => new AccountNode { Name = e.Name.LocalName, Value = (string)e }).ToList();
}
}
}
Sample fiddle which successfully deserialized and re-serializes the following XML:
<Account>
<AccountNumber>12345</AccountNumber>
<Balance>12.52</Balance>
<LateFee>0</LateFee>
</Account>
** Brief Explanation **
In my C# WinForms program, I have 2 TreeView's (They work like a ListView so they are not nested) and 1 ListView. I populate the first TreeView with data from some XML files (name of file as TreeNode.Text property and TreeNode.Name will contain path to the XML file).
First TreeView:
When user clicks on any node, the second TreeView will populate with some data from inside of the XML file.
Second TreeView:
When user clicks on any node, again some data from the XML file will be shown in ListView.
ListView:
When user clicks on any item, again some data from the xml file will be shown (in my case, it is coordinates of a curve that I draw on a PictureBox).
** Visualation **
A very short example of my XML files:
<XML>
<Group Name="IO">
<PIN Name="IO1">
<PAIR>
<Voltage>-3</Voltage>
<Current>-3</Current>
</PAIR>
<PAIR>
<Voltage>3</Voltage>
<Current>-3</Current>
</PAIR>
</PIN>
<PIN Name="IO2">
<PAIR>
<Voltage>-3</Voltage>
<Current>-3</Current>
</PAIR>
<PAIR>
<Voltage>3</Voltage>
<Current>-3</Current>
</PAIR>
</PIN>
</Group>
<Group Name="PWR">
///and so on
</Group>
</XML>
So my idea is, Since my XML files are sometimes very large (More than 10MB) it is really slow to deal with them. I wonder if there is a way to convert them in Binary and work with similar commands like 'XPATH' and 'XmlDocument' with this binary file?
Serializing it as binary shouldn't be a problem - in fact, it would be an ideal scenario for protobuf-net, since this is a tree-serializer (like XmlSerializer), and can even work with the XmlElementAttributes if you really want (so you don't need any more decoration).
However, most serializations will not allow you to filter (etc) on the file while it is just a file - you would need to rehydrate the object model into a regular object model, and work with that. Fortunately, with a fast binary serializer like that, it should be faster to load anyway (and much smaller than 10MB).
However, another viable option is to use an embedded database, and write the data as records in an indexed table structure that supports the queries you need.
For info, a quick test with your sample data showed it taking your 512 byte file to 102 bytes:
using System.Collections.Generic;
using System.IO;
using System.Xml.Serialization;
using ProtoBuf;
class Program
{
static void Main()
{
// load your data
Model model;
using(var file = File.OpenRead("my.xml"))
{
model = (Model)new XmlSerializer(typeof(Model)).Deserialize(file);
}
// write as protobuf-net
using (var file = File.Create("my.bin"))
{
Serializer.Serialize(file, model);
}
}
}
[XmlRoot("XML"), ProtoContract]
public class Model
{
[XmlElement("Group"), ProtoMember(1)]
public List<Group> Groups { get; set; }
}
[ProtoContract]
public class Group
{
[XmlAttribute("Name"), ProtoMember(1)]
public string Name { get; set; }
[XmlElement("PIN"), ProtoMember(2)]
public List<Pin> Pins { get; set; }
}
[ProtoContract]
public class Pin
{
[XmlAttribute("Name"), ProtoMember(1)]
public string Name { get; set; }
[XmlElement("PAIR"), ProtoMember(2)]
public List<Pair> Pairs { get; set; }
}
[ProtoContract]
public class Pair
{
[ProtoMember(1)]
public int Voltage { get; set; }
[ProtoMember(2)]
public int Current { get; set; }
}
I'm learning about how to use config files and I ran into some problems that I'm hoping someone here can give me some advice. It doesn't matter if my files are XML or not but the majority of examples I have read are using them and Im all for anything that makes my life easier.
the problem Im running into is that the appSettings file seems to be setup to only accept one value for one key and I would like to have something similar to:
<key="Machine List" value="Server105" />
<key="Machine List" value="Server230" />
Ive found a hack here but it was written over 6 years ago and I didn't know if there was a better way.
Again, it doesnt matter if this is XML, a flat file, etc.... Im just trying to learn how to use config files instead of hard coding values directly into the app.
Thanks for your help.
if you really need to store multiple machines under the key, it would be more appropriate to do:
<key="Machine List" value="Server105,Server230" />
with the delimiter being a character of your choosing.
An alternative to entry attributes would be to add child nodes to your setting node:
<setting key="Machine List">
<value>Server105</value>
<value>Server230</value>
</setting>
This way you don't need string manipulations to extract the different values.
You can make use of configuration sections where you can define your own configuration. Just add
<configSections>
<sectionGroup name="MyConfiguration">
<section name="MyQuery" type="namespace.QueryConfigurationSection" allowLocation="true" allowDefinition="Everywhere"/>
</sectionGroup>
</configSections>
after the <configuration> and you can add your custom section just after the appsetting
</appSettings>
<!-- custom query configuration -->
<MyConfiguration>
<MyQuery>
<Query1> </Query1>
<Query2> </Query2>
To read you need to create few classes
/// <summary>
/// Creates a custom configuration section inside web.config
/// </summary>
public class QueryConfigurationSection : ConfigurationSection
{
//query 2
[ConfigurationProperty("Query1")]
public QueryElement1 Query1
{
get { return this["Query1"] as QueryElement1; }
}
//query 2
[ConfigurationProperty("Query2")]
public QueryElement2 Query2
{
get { return this["Query2"] as QueryElement2; }
}
}
public class QueryElement1 : ConfigurationElement
{
public string Value { get; private set; }
protected override void DeserializeElement(XmlReader reader, bool s)
{
Value = reader.ReadElementContentAs(typeof(string), null) as string;
}
}
public class QueryElement2 : ConfigurationElement
{
public string Value { get; private set; }
protected override void DeserializeElement(XmlReader reader, bool s)
{
Value = reader.ReadElementContentAs(typeof(string), null) as string;
}
}
The overridden DeserializedElement will deserialize the Xml(inside) the QueryElement1 & 2.
To read the values from the main application, you just need to call the following:
//calling my query config
QueryConfigurationSection wconfig = (QueryConfigurationSection)ConfigurationManager.GetSection("MyConfiguration/MyQuery");
string _query1 = wconfig.Query1.Value;
string _query2 = wconfig.Query2.Value;
Maybe you should rethink your design. I would just put the list you want in another file and not the config. You could do a delimited string but then if the list got long it would be hard to manage it. You could just put it in a text file or an XML/JSON file. Here is some code that might be a good place to start.
public static class MyClass
{
private static string _path = ConfigurationManager.AppSettings["FilePath"];
private static List<string> _list;
static MyClass()
{
_list = new List<string>();
foreach (string l in File.ReadAllLines(_path))
_list.Add(l);
}
public static List<string> GetList()
{
return _list;
}
}
I made it a static class so it would only read from the file once and not everytime you need to get information from it.
This might also be a good thing to put in a database if you need more functionality. But for a small read-only kind of thing, this will work better than a delimited string for longer values.