Yet another mtgjson.com inspired question; none of the other, similar questions are getting me where I need to be. First, a couple lines of sample JSON (from mtgjson's AllPrices.json):
"00028782-6ec2-54fe-8633-2c906d8f1076": {"prices": {"mtgo": {}, "mtgoFoil": {}, "paper": {"2019-12-01": 0.15}, "paperFoil": {}}},
"00040b50-3b84-5cea-b663-70038b87fa08": {"prices": {"mtgo": {"2019-12-02": 0.02}, "mtgoFoil": {"2019-12-02": 0.02}, "paper": {"2019-12-01": 0.15}, "paperFoil": {"2019-12-01": 0.53}}}
Each parent object is a GUID and the Price Info; the Price Info is the four types of prices offered, and for each of those four types, the price data is Last Updated Date and Price.
The classes I've created (after lots of other approaches, all of which have failed):
public class price_Class
{
public string Updated { get; set; }
public decimal Price { get; set; }
}
public class PriceInfo
{
[JsonProperty("mtgo")] public price_Class mtgo { get; set; }
[JsonProperty("mtgoFoil")] public price_Class mtgof { get; set; }
[JsonProperty("paper")] public price_Class RegPrice { get; set; }
[JsonProperty("paperFoil")] public price_Class FoilPrice { get; set; }
}
And how I'm using it:
dynamic prices = JsonConvert.DeserializeObject(sJSON);
IDictionary<string, JToken> pricelist = prices;
foreach (var priceline in pricelist)
{
sUUID = priceline.Key.ToString();
PriceInfo pi = JsonConvert.DeserializeObject<PriceInfo>(priceline.Value.ToString());
Stepping through in debug mode, I see that prices seems fine; pricelist, also. The foreach defines priceline as I'd expect, and sUUID is correctly defined - but pi shows up with all four sets of price data as null - not just those that are null, but those that should have data.
priceline.value looks fine, to me:
{{
"mtgo": {},
"mtgoFoil": {},
"paper": {
"2019-12-01": 0.53
},
"paperFoil": {
"2019-12-01": 4.53
}
}}
When I expand pi in the Locals window, it shows the four classes (FoilPrice, RegPrice, mtgo, mtgof), but the contents are null.
What I need, in case it's not obvious, is to have pi.RegPrice and pi.FoilPrice defined, with a Date and Price, when that data actually exists in the JSON.
I'll admit, nested classes and JSON in general is still outside my comfort zone; I appreciate all help!
The price_Class is not adequate for deserialize your JSON object.
try with this:
public class PriceInfo
{
[JsonProperty("mtgo")] public Dictionary<string, decimal> mtgo { get; set; }
[JsonProperty("mtgoFoil")] public Dictionary<string, decimal> mtgof { get; set; }
[JsonProperty("paper")] public Dictionary<string, decimal> RegPrice { get; set; }
[JsonProperty("paperFoil")] public Dictionary<string, decimal> FoilPrice { get; set; }
}
using this tool you can find exactly what are your DTO
the problem are on your json data as i believe date are not send like this in json it come as array of integers and with specific order like day month year .
as well as you need to create your DTO similar to json even in types you can't map it till you set the same type of json in your DTO
public class Prices
{
public Mtgo mtgo { get; set; }
public MtgoFoil mtgoFoil { get; set; }
public Paper paper { get; set; }
public PaperFoil paperFoil { get; set; }
}
public class RootObject
{
public Prices prices { get; set; }
}
don't forget to tag all of them with [JsonProperty("json prop name ")]
Parsing and formatting utilities for JSON.
A central concept in lift-json library is Json AST which models the structure of a JSON document as a syntax tree.
sealed abstract class JValue
case object JNothing extends JValue // 'zero' for JValue
case object JNull extends JValue
case class JString(s: String) extends JValue
case class JDouble(num: Double) extends JValue
case class JInt(num: BigInt) extends JValue
case class JBool(value: Boolean) extends JValue
case class JField(name: String, value: JValue) extends JValue
case class JObject(obj: List[JField]) extends JValue
case class JArray(arr: List[JValue]) extends JValue
It comes with Lift, but non-Lift users can add lift-json as a dependency in following ways. Note, replace XXX with correct Lift version.
SBT users
Add dependency to your project description:
val lift_json = "net.liftweb" %% "lift-json" % "XXX"
Maven users
Add dependency to your pom:
<dependency>
<groupId>net.liftweb</groupId>
<artifactId>lift-json</artifactId>
<version>XXX</version>
</dependency>
Summary of the features:
Fast JSON parser
LINQ style queries
Case classes can be used to extract values from parsed JSON
Diff & merge
DSL to produce valid JSON
XPath like expressions and HOFs to manipulate JSON
Pretty and compact printing
XML conversions
Serialization
Low level pull parser API
Try using this for deeply nested JSONs.
It seems that the odd sub-structure of {Prices:{label:{date:amount}}} just doesn't work well with Newtonsoft's (otherwise excellent) JSON tools.
I tried the various tools (some suggested here) to generate classes; they were getting confused by the dates, creating classes for each date. I even tried generating classes for just the substring of data (priceline.value, in the example) - nope, still wouldn't work.
I ended going with a brute-force, string manipulation approach; it's ugly, I'm not exactly proud of it - but I now have what I needed. Here's the relevant snippets, just in case anyone else stumbles on the same things as I did:
private static string RemoveNoise(string input)
{
input = Regex.Replace(input, #"\r\n?|\n", string.Empty); // no more NewLine stuff
return input.Replace(" ", string.Empty)
.Replace(#"""",string.Empty);
}
...
public class PriceData
{
public string UUID { get; set; }
public string Updated { get; set; }
public string Price { get; set; }
public string FoilUpd { get; set; }
public string FoilPrc { get; set; }
}
...
string sPaperTag = #"PAPER:{";
string sPprFlTag = #"PAPERFOIL:{";
...
dynamic prices = JsonConvert.DeserializeObject(sJSON);
IDictionary pricelist = prices;
foreach (var priceline in pricelist)
{
PriceData pData = new PriceData();
pData.UUID = priceline.Key.ToString();
bool bWeHavePrice = false;
string pi = RemoveNoise(priceline.Value.ToString().ToUpper());
// parse out paper, paperFoil dates & prices manually (unusual JSON format...)
iBeg = pi.IndexOf(sPaperTag);
if (iBeg >= 0)
{
sTemp = pi.Substring(iBeg, pi.Length - iBeg);
iBeg = sTemp.IndexOf(":") + 2;
iEnd = sTemp.IndexOf("}");
sTemp = sTemp.Substring(iBeg, iEnd - iBeg); // either YYYY-MM-DD:n.nn, or an empty string
iBeg = sTemp.IndexOf(":");
if (iBeg > 0)
{
if (DateTime.TryParse(sTemp.Substring(0, iBeg), out dtTemp)) { pData.Updated = dtTemp.ToString(); bWeHavePrice = true; }
if (Decimal.TryParse(sTemp.Substring(++iBeg, sTemp.Length - iBeg), out decTemp)) { pData.Price = decTemp.ToString(); bWeHavePrice = true; }
}
}
I do that string manipulation dance again for the foil prices; I'm not currently interested in the 'mtgo' or 'mtgoFoil' data.
I'm doing all that TryParse stuff to make sure I have a valid date or amount, but I'm using the results to populate parameters in a SQLCommand, so I have to have strings; seems like extra work, going from string to Date or Decimal, then back to string - but this way I don't get exceptions when executing the SQL Insert command.
My thanks to all who helped, or tried to help. And if someone figures out how to handle it via JSON.Net, I'd love to see it!
Related
I have a JSON string in below format for which I want to deserialize it into C# List. But the record number "1","2","3" (it can be upto 1,2,3...n depends on the json response each time) in JSON restricting me to deserialize it into C# object using Newtonsoft.Json
{
"1":{
"UID":"1",
"LICENCENO":"licenseno",
"NAME":"ABC"
},
"2":{
"UID":"2",
"LICENCENO":"licenseno",
"NAME":"PQR"
},
"3":{
"UID":"3",
"LICENCENO":"licenseno",
"NAME":"XYZ"
}
}
I am using below code for deserialization
var result = Newtonsoft.Json.JsonConvert.DeserializeObject<List<DriverMaster>>(json);
I have DriverMaster class created as-
public class DriverMaster
{
public string UID { get; set; }
public string LICENCENO { get; set; }
public string NAME { get; set; }
}
Deserialization line giving unhandled exception, I know I am doing it in wrong way, because DriverMaster json object cannot be extracted into c# directly without doing something to record number 1,2,3...n in c#. Can anyone please help me to sort it out? Thanks in advance.
You were close:
var result = JsonConvert.DeserializeObject<Dictionary<string, DriverMaster>>(json)
.Select(x => x.Value)
.ToList();
Solution.
Change your code to use...
var result = JsonConvert.DeserializeObject<Dictionary<int, DriverMaster>>(json);
Explaination
The type is not the same... The List<DriverMaster>type will convert to JSON like so...
{
"1":
{
"DriverMaster": {
"UID":"1",
"LICENCENO":"licenseno",
"NAME":"ABC"
}
}
}
This doesn't match what you showed in your question...
The type that you are looking for is actually Dictionary<int, DriverMaster>, which is a key/value pair which will output a JSON string like so
{
"1": { ... },
"2": { ... },
"3": { ... }
}
In order to fix that, you need to use the Dictionary<int, DriverMaster> type instead.
For these types of things I like to use the often overlooked feature of JToken.SelectTokens. This function allows you to select tokens within a json string and permits the use of wildcards.
Here's some code that will deserialize your sample by selecting past the 1,2,3...N in the json:
public static IEnumerable<DriverMaster> Deserialize(string json)
{
return JToken.Parse(json).SelectTokens("*")
.Select(jToken => jToken.ToObject<DriverMaster>());
}
The * basically says to select all tokens after the root, so it's selecting the values associated with 1, 2, 3.. etc... Here's another SO answer that shows a more complicated usage of the SelectTokens method.
You need to use
public class DriverMaster
{
public string UID { get; set; }
public string LICENCENO { get; set; }
public string NAME { get; set; }
}
public class Root
{
[JsonExtensionData]
public IDictionary<string,JToken> Data {get;set;}
}
and
var result = Newtonsoft.Json.JsonConvert.DeserializeObject<Root>(json);
If you want to have result as List, you can parse the result as.
var list = new List<DriverMaster>();
foreach(KeyValuePair<string, JToken> token in result.Data)
{
list.Add(token.Value.ToObject<DriverMaster>());
}
That would give you the desired result as
1 licenseno ABC
2 licenseno PQR
3 licenseno XYZ
I am integrating with a courier that requires me to pass box dimensions for each box in my consignment to their API in JSON format. I am able to set individual properties like RecipientName, but am not sure how to pass the box details for the varying number of boxes for each consignment.
The JSON needs to look like this (example is for a 2 box consignment):
{
"RecipientName": "Joe Bloggs",
"Packages" : [{
"boxNumber": "1",
"boxHeight": 1.55,
"boxLength": 1.55,
"boxWidth": 1.55
},
{
"boxNumber": "2",
"boxHeight": 2.55,
"boxLength": 2.55,
"boxWidth": 2.55
}]
}
I have built 2 classes, one that describes the structure of the JSON, and another that contains the method to serialize the JSON.
My JSON structure class looks like this (I have used a List because I have read that arrays are a fixed length, and because the number of boxes with vary I cannot use arrays):
public class API_JSON
{
public class Rootobject
{
public string RecipientName { get; set; }
public List<Package> Packages { get; set; }
}
public class Package
{
public string boxNumber { get; set; }
public double boxHeight { get; set; }
public double boxLength { get; set; }
public double boxWidth { get; set; }
}
}
And my API methods class looks like this:
public class API_Methods
{
public string recipientName;
public List<string> boxnumber;
public List<double> boxHeight;
public List<double> boxLength;
public List<double> boxWidth;
public Boolean SubmitConsignment(out string JSONData)
{
var NewRequestObject = new API_JSON.RootObject
{
Recipient = recipientName,
Packages = new API_JSON.Package
{
foreach (string item in ContainerNumber)
{
boxNumber=???,
boxHeight=???,
boxLength???=,
boxWidth=???
}
}
}
string JSONData = JsonConvert.SerializeObject(NewRequestObject);
return true;
}
}
I am then instantiating the object, setting its public variables, then running the method list this:
API_Methods myObject = new API_Methods();
myObject.recipientName;
myObject.boxnumber.Add(1);
myObject.boxnumber.Add(2);
myObject.boxHeight.Add(1.55);
myObject.boxHeight.Add(2.55);
myObject.boxLength.Add(1.55);
myObject.boxLength.Add(2.55);
myObject.boxWidth.Add(1.55);
myObject.boxWidth.Add(2.55);
bool test = API_Methods.SubmitConsignment(out JSON);
My problem is with the foreach loop - I know the code is incomplete - but I was hoping to iterate through the lists, but even with an empty foreach loop it appears to be the wrong place to put the loop as I start getting syntax errors about an expected "}"
You're actually overcomplicating this for yourself - create complete package objects, and add them to the List Packages, and then pass the rootobject to the serializer.
The error you are getting is because you are not correctly initializing / filling your Packages List. Your object is invalid, hence the serializer is throwing exceptions.
This will be a lot easier for you if you create some constructors for your objects, something like this:
public Package(number, height, length, width)
{
boxNumber = number;
boxHeight = height;
//rest of your properties here in same format
}
You can then also make your setters private in the class, if you wish.
You can then easily create your package objects:
var package1 = new Package(10, 10, 10, 10);
This should make it a lot easier to create your list of boxes to put in your rootObject.
You can add each package to the packages list (individually or within a foreach loop):
Packages.Add(package1)
Or you could even start getting more concise:
Packages.Add(new Package(10,10,10,10));
You want to separate your concerns more to help keep this clear - so I'd recommend you fully construct your rootObject, add the packages to the list in one class (your 3rd code snippet), and then serialize it another (your 2nd code snippet).
Edit:
I think you'd find it easier to refactor your code somewhat:
1) Have a public rootobject in your Json_Api class, with get; set;. Get rid of the box collections. Get rid of your foreach loop from here too.
public class API_Methods
{
public rootObject RootObject { get; set; }
public Boolean SubmitConsignment(out string JSONData)
{
string JSONData = JsonConvert.SerializeObject(NewRequestObject);
return true;
}
}
2) Set the properties of this rootobject outside this class (where you currently initialize your objects). Add the New Package()s to Packages list here too.
API_Methods myObject = new API_Methods();
myObject.RootObject.recipientName = "NAME";
myObject.RootObject.Packages.Add(new Package(10,10,10,10);
myObject.RootObject.Packages.Add(new Package(20,20,20,20);
bool test = API_Methods.SubmitConsignment(out JSON);
3) Call the API method next, it should return a serialized version of the wholerootobject, including your packages.
Just a side note, it would be more conventional to send the RootObject as a parameter to the API, and return the Json string object back.
How would I deserialize YAML to a immutable data structure?
e.g. I have this YAML:
Value: SomeString
Number: 99
And this data structure:
public class MyData
{
public MyData(string value, int number)
{
Value = value;
Number = number;
}
public string Value { get; }
public int Number { get; }
}
For this I'd to use the constructor. So somehow I'd need to first retrieve a Dictionary<string, object> parsed from the YAML respecting my class (so 99 would be int, not string), then scan my type for an appropriate constructor,
Although the question doesn't mention it, I'm assuming you are using YamlDotNet (or SharpYaml which is a fork of YamlDotNet)
YamlDotNet doesnt support deserializing into classes that do not have a default constructor - but one option to achieve what you want is to deserialize into an intermediate Builder type that is mutable which can produce the final type.
e.g.
public class MyDataBuilder
{
public string Value { get; set; }
public int Number { get; set; }
public MyData Build() => new MyData(Value, Number);
}
And then use something like:
deserializer.Deserialize<MyDataBuilder>(yaml).Build();
You would end up having to create a parallel set of builders for your whole model however, e.g. if MyData had a third parameter of type MyOtherData (I've changed the example to use records instead of classes to make it concise):
public record MyOtherData(string OtherValue);
public record MyData(string Value, int Number, MyOtherData otherData);
In which case we would need another Builder:
public class MyOtherDataBuilder
{
public string OtherValue { get; set; }
}
And MyDataBuilder would look like:
public class MyDataBuilder
{
public string Value { get; set; }
public int Number { get; set; }
public MyOtherDataBuilder MyOtherData { get; set; }
public MyData Build() => new MyData(Value, Number, MyOtherData.Build());
}
It's an old but surprisingly relevant question. Now, with records in C#, immutable collections in .net, lack of ability to deserialize immutable data is a blocker - there is no way we need to change all our data types just to be able to deserialize. One practical workaround that I found - is to convert yaml to json first, then deal with json your preferred way - System.Text.Json, Newtonsoft, etc.
Here is how to do is easiest way:
static string ConvertToJson(string yaml) {
object DeserializeYaml() =>
new DeserializerBuilder()
.Build()
.Deserialize(new StringReader(yaml))
?? throw new InvalidOperationException("Cannot deserialize yaml string:" + Environment.NewLine + yaml);
string SerializeYamlObjectToJson(object yamlObject) =>
new SerializerBuilder()
.JsonCompatible()
.Build()
.Serialize(yamlObject);
return SerializeYamlObjectToJson(DeserializeYaml());
}
The only disadvantage, potentially big, is performance. I feel, however, that it's rarely an important requirement for yaml.
use the FormatterServices.GetUninitializedObject API (this will NOT invoke any constructors at all) and then use reflection to set fields.
Code example:
var instance = FormatterServices.GetUninitializedObject(typeof(MyData));
var flags = BindingFlags.NonPublic | BindingFlags.Instance;
var type = typeof(MyData);
var stringField = type.GetField("_value", flags);
stringField.SetValue(instance, "SomeString");
var numberField = type.GetField("_number", flags);
numberField.SetValue(instance, 99);
MyData data = (MyData)instance;
I have a business class that contains many properties for various stock-exchange price types. This is a sample of the class:
public class Prices
{
public decimal Today {get; set;}
public decimal OneDay {get; set;}
public decimal SixDay {get; set;}
public decimal TenDay {get; set;}
public decimal TwelveDay {get; set;}
public decimal OneDayAdjusted {get; set;}
public decimal SixDayAdjusted {get; set;}
public decimal TenDayAdjusted {get; set;}
public decimal OneHundredDayAdjusted {get; set;}
}
I have a legacy system that supplies the prices using string ids to identify the price type.
E.g.
Today = "0D"
OneDay = "1D"
SixDay = "6D"
//..., etc.
Firstly, I load all the values to an IDictionary() collection so we have:
[KEY] VALUE
[0D] => 1.23456
[1D] => 1.23456
[6D] => 1.23456
...., etc.
Secondly, I set the properties of the Prices class using a method that takes the above collection as a parameter like so:
SetPricesValues(IDictionary<string, decimal> pricesDictionary)
{
// TODAY'S PRICE
string TODAY = "D0";
if (true == pricesDictionary.ContainsKey(TODAY))
{
this.Today = pricesDictionary[TODAY];
}
// OneDay PRICE
string ONE_DAY = "D1";
if (true == pricesDictionary.ContainsKey(ONE_DAY))
{
this.OneDay = pricesDictionary[ONE_DAY];
}
//..., ..., etc., for each other property
}
Is there a more elegant technique to set a large amount of properties?
Thanks,
j
Instead of using a string-to-decimal mapping and checking the dictionary repeatedly, use a delegate mapping/extension method:
public static class PriceConverter
{
private static readonly Dictionary<string, Action<Prices, decimal>> setters =
CreateSetterDictionary();
public static void SetPrice(this Prices p, string id, decimal newPrice)
{
Action<Prices, decimal> setter;
if (setters.TryGetValue(id, out setter))
setter(p, newPrice);
}
private static Dictionary<string, Action<Prices, decimal>>
CreateSetterDictionary()
{
var dic = new Dictionary<string, Action<Prices, decimal>>();
dic.Add("0D", (p, d) => p.Today = d);
dic.Add("1D", (p, d) => p.OneDay = d);
// etc.
return dic;
}
}
Then you can write prices.SetPrice("0D", 1.23456).
If you like, add a throw statement at the end of the SetPrice method to handle cases where the id doesn't match anything.
I would put the string variables into constants, rather than declare them every time you run the method:
private const string ONE_DAY = "D1";
If you expect the collection parameter to contain all or most of the possible values, then your code is probably cool. If you expect that the dictionary will have a small subset of the possible values, it might be more efficient to use a foreach loop and a switch statement to set values, rather then do a lookup for every possible value every time. It just depends on how many values you need to deal with and how many you get in each method call.
Define a dictionary of properties in the constructor e.g.
private Dictionary<int, PropertyInfo> propertyDictionary = new ...
MyClass()
{
this.propertyDictionary.Add(0, this.GetType().GetProperty("FirstProperty");
...
}
then access using an indexed property
decimal this[int index]
{
get
{
PropertyInfo property;
if (this.propertyDictionary.TryGetValue(index, out property))
{
// Not sure I remember the arguments right here:
property.SetValue(this, new object[] { value });
}
set
{
// Similar code
}
}
You could later on improve this code by automatically parsing the properties in the constructor using reflection,
adding all properties with an attribute that tells you what the id is.
(Instead of adding them manually in the constructor).
Just an idea:
interface IPrices_As_String{
string OD { get; set; }
// other properties here...
}
interface IPrices{
decimal Today{get; set;}
}
class Prices : IPrices, IPrices_As_String{
public decimal Today { get; set; }
public string IPrices_As_String.OD {
get { return this.Today.ToString(); }
set {
if(!String.IsNullOrEmpty(value)){
this.Today = decimal.Parse(value);
}
}
}
}
Then when I am setting the values from the legacy system, I will use the Prices class on the interface as IPrices_As_String like:
IPrices_As_String obj = new Prices();
// set values from the legacy system
IPrices obj2 = obj as IPrices; // will give me the correct object..
.
HTH.
The way I see it, you have a few options, depending on your skills, the way you are allowed to change the current POCO's or other classes:
If you must use a dictionary, create a similar dictionary which maps the "0D" etc to the OneDay names. Loop through the dictionary and assign using simple reflection.
If you can change the way the data is read, have the dictionary read with OneDay etc, instead of the "0D", which is only applicable to the external application.
Create an attribute, LegacyKeyAttribute, augment your POCO gettors/settors with this attribute. Now it becomes trivial: loop through the properties of the POCO to find the correct property for your current legacy key.
The last option requires a bit more understanding of C# than many average programmers know: writing and using attributes and reflection. However, in the end it's the cleanest and easiest solution (I'll try to come up with an example).
UPDATE: here's a little example. Meanwhile, many improvement suggestions have been posted, but none still uses attributes, while your case seems ideal. Why? It poses the least burden on existing code, I believe, and it makes reading and understanding your code even easier.
Usage:
// any price:
Prices prices = new Prices();
prices.SetPriceByLegacyName("0D", 1.2345M);
// or, your loop becomes a bit easier:
SetPricesValues(IDictionary<string, decimal> pricesDictionary)
{
foreach(string key in pricesDictionary.Keys)
{
// assuming "this" is of type Prices (you didn't specify)
this.SetPriceByLegacyName(key, pricesDictionary[key]);
}
}
The implementation:
// the simplest attribute class is enough for you:
[AttributeUsage(AttributeTargets.Property)]
public class LegacyNameAttribute : Attribute
{
public string Name { get; set; }
public LegacyNameAttribute(string name)
{
this.Name = name;
}
}
// your Prices POCO class becomes easier to read
public class Prices
{
[LegacyName("0D")] public decimal Today { get; set; }
[LegacyName("1D")] public decimal OneDay { get; set; }
[LegacyName("6D")] public decimal SixDay { get; set; }
[LegacyName("10D")] public decimal TenDay { get; set; }
[LegacyName("12D")] public decimal TwelveDay { get; set; }
[LegacyName("1DA")] public decimal OneDayAdjusted { get; set; }
[LegacyName("6DA")] public decimal SixDayAdjusted { get; set; }
[LegacyName("10DA")] public decimal TenDayAdjusted { get; set; }
[LegacyName("100DA")] public decimal OneHundredDayAdjusted { get; set; }
}
// an extension method to ease the implementation:
public static class PricesExtensions
{
public static void SetPriceByLegacyName(this Prices price, string name, decimal value)
{
if (price == null)
throw new ArgumentException("Price cannot be null");
foreach (PropertyInfo prop in price.GetType().GetProperties())
{
LegacyNameAttribute legNameAttribute = (LegacyNameAttribute)
Attribute.GetCustomAttribute(prop, typeof(LegacyNameAttribute));
// set the property if the attribute matches
if (legNameAttribute != null && legNameAttribute.Name == name)
{
prop.SetValue(price, value, null);
break; // nothing more to do
}
}
}
}
That's all there is to it. Even with all the added lines, it may well be that your total line count becomes less. But more importantly, it becomes easier to maintain and use.
This is a very complicated question concerning how to serialize data via a web service call, when the data is not-strongly typed. I'll try to lay it out as best possible.
Sample Storage Object:
[Serializable]
public class StorageObject {
public string Name { get; set; }
public string Birthday { get; set; }
public List<NameValuePairs> OtherInfo { get; set; }
}
[Serializable]
public class NameValuePairs {
public string Name { get; set; }
public string Value { get; set; }
}
Sample Use:
[WebMethod]
public List<StorageObject> GetStorageObjects() {
List<StorageObject> o = new List<StorageObject>() {
new StorageObject() {
Name = "Matthew",
Birthday = "Jan 1st, 2008",
OtherInfo = new List<NameValuePairs>() {
new NameValuePairs() { Name = "Hobbies", Value = "Programming" },
new NameValuePairs() { Name = "Website", Value = "Stackoverflow.com" }
}
},
new StorageObject() {
Name = "Joe",
Birthday = "Jan 10th, 2008",
OtherInfo = new List<NameValuePairs>() {
new NameValuePairs() { Name = "Hobbies", Value = "Programming" },
new NameValuePairs() { Name = "Website", Value = "Stackoverflow.com" }
}
}
};
return o;
}
Return Value from Web Service:
<StorageObject>
<Name>Matthew</Name>
<Birthday>Jan 1st, 2008</Birthday>
<OtherInfo>
<NameValuePairs>
<Name>Hobbies</Name>
<Value>Programming</Value>
</NameValuePairs>
<NameValuePairs>
<Name>Website</Name>
<Value>Stackoverflow.com</Value>
</NameValuePairs>
</OtherInfo>
</StorageObject>
What I want:
<OtherInfo>
<Hobbies>Programming</Hobbies>
<Website>Stackoverflow.com</Website>
</OtherInfo>
The Reason & Other Stuff:
First, I'm sorry for the length of the post, but I wanted to give reproducible code as well.
I want it in this format, because I'm consuming the web services from PHP. I want to easily go:
// THIS IS IMPORANT
In PHP => "$Result["StorageObject"]["OtherInfo"]["Hobbies"]".
If it's in the other format, then there would be no way for me to accomplish that, at all. Additionally, in C# if I am consuming the service, I would also like to be able to do the following:
// THIS IS IMPORANT
In C# => var m = ServiceResult[0].OtherInfo["Hobbies"];
Unfortunately, I'm not sure how to accomplish this. I was able to get it this way, by building a custom Dictionary that implemented IXmlSerializer (see StackOverflow: IXmlSerializer Dictionary), however, it blew the WSDL schema out of the water. It's also much too complicated, and produced horrible results in my WinFormsTester application!
Is there any way to accomplish this ? What type of objects do I need to create ? Is there any way to do this /other than by making a strongly typed collection/ ? Obviously, if I make it strongly typed like this:
public class OtherInfo {
public string Hobbies { get; set; }
public string FavoriteWebsite { get; set; }
}
Then it would work perfectly, I would have no WSDL issues, I would be able to easily access it from PHP, and C# (.OtherInfo.Hobbies).
However, I would completely lose the point of NVP's, in that I would have to know in advance what the list is, and it would be unchangeable.. say, from a Database.
Thanks everyone!! I hope we're able to come up with some sort of solution to this. Here's are the requirements again:
WSDL schema should not break
Name value pairs (NVP's) should be serialized into attribute format
Should be easy to access NVP's in PHP by name ["Hobbies"]
Should be easy to access in C# (and be compatible with it's Proxy generator)
Be easily serializable
Not require me to strongly type the data
Now, I am /completely/ open to input on a better/different way to do this. I'm storing some relatively "static" information (like Name), and a bunch of pieces of data. If there's a better way, I'd love to hear it.
This is like dynamic properties for a object.
C# is not quite a dynamic language unlike javascript or maybe PHP can parse the object properties on the fly. The following two methods are what I can think of. The second one might fit into your requirements.
The KISS Way
The Keep It Simple Stupid way
public class StorageObject {
public string Name { get; set; }
public string Birthday { get; set; }
public List<string> OtherInfo { get; set; }
}
You can have name value pairs which is separated by '|'
OtherInfo = {"Hobbies|Programming", "Website|Stackoverflow.com"}
Serialized forms
<StorageObject>
<Name>Matthew</Name>
<Birthday>Jan 1st, 2008</Birthday>
<OtherInfo>
<string>Hobbies|Programming</string>
<string>Website|Stackoverflow.com</string>
</OtherInfo>
</StorageObject>
The Dynamic Way in C#
Make the name value pair part become an XML element so that you can build it dynamically.
public class StorageObject {
public string Name { get; set; }
public string Birthday { get; set; }
public XElement OtherInfo { get; set; } // XmlElement for dot net 2
}
You can easily build up OtherInfo object as element centric
e.g.
XElement OtherInfo = new XElement("OtherInfo");
OtherInfo.Add( ..Hobbies xelement & text value..);
OtherInfo.Add( ..WebSite xelement & text value..);
The serialized form will be
<OtherInfo>
<Hobbies>Programming</Hobbies>
<Website>Stackoverflow.com</Website>
</OtherInfo>
or build it as attribute centric
XElement OtherInfo = new XElement("OtherInfo");
OtherInfo.Add( ..nvp xattribute Hobbies & value..);
OtherInfo.Add( ..nvp xattribute WebSite & value..);
<OtherInfo>
<nvp n="Hobbies" v="Programming" />
<nvp n="Website" v="Stackoverflow.com" />
</OtherInfo>
For any dynamic language, it can access to the properties directly.
For the rest, they can access the value by read the XML. Reading XML is well supported by most of framework.
This is what I've settled on.
Class Structure:
public class StorageObject {
public string Name { get; set; }
public string Birthday { get; set; }
[XmlAnyElement("Info")] // this prevents double-nodes in the XML
public XElement OtherInfo { get; set; }
}
Usage:
StorageObject o = new StorageObject();
o.OtherInfo.Add(new XElement("Hobbies","Programming");
o.OtherInfo.Add(new XElement("Website","Stackoverflow.com");
Output:
<Info>
<Hobbies>Programming</Hobbies>
<Website>Stackoverflow.com</Website>
</Info>
I would like to thank everyone for their assistance, I really appreciate the help and ideas.
As a completely different take on this, why not think about doing it completely differently. Have one web service method to return the serialized storage object, minus the OtherInfo and another method to return the list of properties (keys) for OtherInfo, and a third to return the list of values for any key. Granted, it will take more round trips to the web service if you want all of the data, but the solution will be much simpler and more flexible.
[Serializable]
public class StorageObject {
public string Name { get; set; }
public string Birthday { get; set; }
[Nonserializable]
public Dictionary<string,List<string>> OtherInfo { get; set; }
}
[WebMethod]
public List<StorageObject> GetStorageObjects() {
// returns list of storage objects from persistent storage or cache
}
[WebMethod]
public List<string> GetStorageObjectAttributes( string name )
{
// find storage object, sObj
return sObj.Keys.ToList();
}
[WebMethod]
public List<string> GetStorageObjectAtributeValues( sting name, string attribute )
{
// find storage object, sObj
return sObj[attribute];
}
Have a look into the System.Xml.Serialization.XmlSerializerAssemblyAttribute attribute. This lets you specify a custom class-level serializer. You'll be able to spit out whatever XML you like.
A quick way to get up to speed on these is to use sgen.exe to generate one and have a peek at it with Reflector.
-Oisin
I'm not sure this would solve your problem (it would in C#, but maybe not in PHP), but try using Dictionary<string,List<string>> OtherInfo instead of List<NameValuePairs>. Then "Hobbies" and "Websites" would be your keys and the values would be the list of hobbies or web sites. I'm not sure how it would serialize, though.
You would be able to reference the lists of hobbies as:
List<string> hobbies = storageObject.OtherInfo["Hobbies"];
[EDIT] See here for a generic XML serializable dictionary. This derived class is the one you would need to use instead of generic Dictionary.