Newtonsoft Json - Losing object reference from Child to Parent object - c#

I am trying to create an object structure like this
Universe - contains a collection of Worlds
Worlds contains a collection of areas.
All 3 of these types extend from a base class which has a property of 'ParentObject'.
Worlds will reference the Universe via this property.
Areas will reference the world via this property.
When running a test harness application.
Universe universe = Universe.GetInstance();
var world = universe.CreateWorld();
var area = world.CreateArea();
area.UpdateIpfs();
UpdateIpfs on the area object - then recurses upwards from area to world to universe - serializing each tier and then adding to the Ipfs network. (Thats just where I am storing the json data)
In order to get the objects back
I do
Universe universe = Universe.GetInstance("QmZnaSaDNnmqhUrE8kFHeu9PGGStAr2D4q3Vt88yHwFvzG");
var world = universe.Worlds[0];
var area = world.Areas[0];
Stepping through the code I can see the json content is this before deserialization:
{
"$id": "1",
"ParentObject": null,
"Worlds": [
{
"$id": "2",
"ParentObject": {
"$ref": "1"
},
"Time": "2019-10-06T23:13:56.6002056+01:00",
"Name": null,
"Description": null,
"Areas": [
{
"$id": "3",
"EventScripts": {
"$id": "4"
},
"ParentObject": {
"$ref": "2"
},
"Name": null,
"IsInterior": false,
"IsUnderground": false,
"IsArtificial": false,
"Description": null,
"X": 0,
"Y": 0,
"Z": 0,
"AreaObjects": [],
"ObjType": "BloodlinesLib.Engine.World.Area, BloodlinesLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
"Hash": "QmbM86GZn6w9JadBM143fDYwGisgNPGXke3bFxpXzrfgJh"
}
],
"ObjType": "BloodlinesLib.Engine.World.World, BloodlinesLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
"Hash": "QmRnEfndUDjCTifY694YuftPNWSRHQQJn9WwZmSpUBRJmv"
}
],
"ObjType": "BloodlinesLib.Engine.World.Universe, BloodlinesLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
"Hash": null
}
I can see that the ParentObject for the area object is $ref: 2
Which seems to match the Id of the World object.
But when I deserialize - the ParentObject on the 'Area' object is equal to Null.
However the ParentObject on World object is correctly assigned.
Anyone have any ideas why my ParentObject is not being deserialized properly for the Area's ParentObject.
My serialization and deserialize code is here:
public void UpdateIpfs()
{
JsonSerializerSettings sets = new JsonSerializerSettings
{
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
ContractResolver = new NonPublicPropertiesResolver()
};
var obj = Convert.ChangeType(this, ObjType);
if(ObjType == typeof(Universe))
{
Hash = null;
}
var strContent = JsonConvert.SerializeObject(obj, Formatting.Indented, sets);
var ipfs = new IpfsClient();
byte[] data = Encoding.UTF8.GetBytes(strContent);
data = CompressionHelper.Compress(data);
using (MemoryStream ms = new MemoryStream(data))
{
Hash = ipfs.FileSystem.AddAsync(ms).Result.Id.Hash.ToString();
}
if(ParentObject != null)
{
ParentObject.UpdateIpfs();
}
OnUpdate?.Invoke(this);
}
and
public static T LoadFromIpfs<T>(string hash)
{
JsonSerializerSettings sets = new JsonSerializerSettings
{
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
ContractResolver = new NonPublicPropertiesResolver()
};
var ipfs = new IpfsClient();
byte[] data;
using (MemoryStream ms = new MemoryStream())
{
ipfs.FileSystem.ReadFileAsync(hash).Result.CopyTo(ms);
data =ms.ToArray();
}
data = CompressionHelper.Decompress(data);
string content = Encoding.UTF8.GetString(data);
T obj = JsonConvert.DeserializeObject<T>(content, sets);
return obj;
}
Universe.cs
public class IpfsObject
{
public IpfsObject ParentObject;
public Type ObjType { get; set; }
public string Hash { get; set; }
public static T LoadFromIpfs<T>(string hash)
{
JsonSerializerSettings sets = new JsonSerializerSettings
{
PreserveReferencesHandling = PreserveReferencesHandling.Objects
};
var ipfs = new IpfsClient();
byte[] data;
using (MemoryStream ms = new MemoryStream())
{
ipfs.FileSystem.ReadFileAsync(hash).Result.CopyTo(ms);
data =ms.ToArray();
}
data = CompressionHelper.Decompress(data);
string content = Encoding.UTF8.GetString(data);
T obj = JsonConvert.DeserializeObject<T>(content, sets);
return obj;
}
public delegate void OnUpdateDelegate(object obj);
public event OnUpdateDelegate OnUpdate;
public void UpdateIpfs()
{
JsonSerializerSettings sets = new JsonSerializerSettings
{
PreserveReferencesHandling = PreserveReferencesHandling.Objects
};
var obj = Convert.ChangeType(this, ObjType);
if(ObjType == typeof(Universe))
{
Hash = null;
}
var strContent = JsonConvert.SerializeObject(obj, Formatting.Indented, sets);
var ipfs = new IpfsClient();
byte[] data = Encoding.UTF8.GetBytes(strContent);
data = CompressionHelper.Compress(data);
using (MemoryStream ms = new MemoryStream(data))
{
Hash = ipfs.FileSystem.AddAsync(ms).Result.Id.Hash.ToString();
}
if(ParentObject != null)
{
ParentObject.UpdateIpfs();
}
OnUpdate?.Invoke(this);
}
}
public class Universe : IpfsObject
{
public Universe()
{
Worlds = new List<World>();
this.ObjType = typeof(Universe);
OnUpdate += Universe_OnUpdate;
}
private void Universe_OnUpdate(object obj)
{
IpfsObject ipfsObj = obj as IpfsObject;
Console.WriteLine("Universe updated: "+ ipfsObj.Hash);
File.WriteAllText("UniverseHash.txt", ipfsObj.Hash);
}
public World CreateWorld()
{
World world = new World(this);
Worlds.Add(world);
return world;
}
public List<World> Worlds { get; set; }
public static Universe GetInstance(string hash = null)
{
if(_universe == null)
{
if(hash == null)
{
_universe = new Universe();
}
else
{
_universe = IpfsObject.LoadFromIpfs<Universe>(hash);
_universe.Hash = hash;
}
}
return _universe;
}
private static Universe _universe;
}
public class World : Ipfs.IpfsObject
{
public World(Universe universe)
{
Time = DateTime.Now;
ParentObject = universe;
this.ObjType = typeof(World);
Areas = new List<Area>();
}
public Area CreateArea(bool findFreespace = false)
{
Area area = new Area(this);
Areas.Add(area);
if (findFreespace)
{
bool b = false;
Random rand = new Random(Guid.NewGuid().GetHashCode());
while (!b)
{
b = area.SetPosition(rand.Next(-500, 500), rand.Next(-500, 500), 0);
}
}
area.UpdateIpfs();
return area;
}
public void SetName(string name)
{
Name = name;
UpdateIpfs();
}
public void SetDescription(string desc)
{
Description = desc;
UpdateIpfs();
}
public DateTime Time { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public List<Area> Areas { get; set; }
}
public class Area : IpfsObject
{
public Area(World worldParent)
{
this.ObjType = typeof(Area);
ParentObject = worldParent;
AreaObjects = new List<AreaObject>();
}
public string Name { get; private set; }
public bool IsInterior { get; private set; }
public bool IsUnderground { get; private set; }
public bool IsArtificial { get; private set; }
public string Description { get; private set; }
public void SetName(string name)
{
Name = name;
UpdateIpfs();
}
public void SetDescription(string desc)
{
Description = desc;
UpdateIpfs();
}
public void SetInterior(bool interior)
{
IsInterior = interior;
UpdateIpfs();
}
public void SetUnderground(bool underground)
{
IsUnderground = underground;
UpdateIpfs();
}
public void SetArtificial(bool artificial)
{
IsArtificial = artificial;
UpdateIpfs();
}
public int X { get; set; }
public int Y { get; set; }
public int Z { get; set; }
public bool SetPosition(int x,int y, int z)
{
World world = (World)ParentObject;
var area = world.Areas.FirstOrDefault(e => e.X == x && e.Y == y && e.Z == z);
if(area != null)
{
return false;
}
this.X = x;
this.Y = y;
this.Z = z;
UpdateIpfs();
return true;
}
}

I can confirm that when I take the parameters out of the constructor for World/Area - that the deserialisation then preserves the reference link between world and area.

Related

Custom JSON formatting based on number of properties

I am looking for a way to split a JSON string into multiple lines after serialization, inserting a newline after every Nth property.
For example, I have this class:
public class Obj
{
public string Property1 { get; set; }
public string Property2 { get; set; }
public string[] Property3 { get; set; }
public string Property4 { get; set; }
public string Property5 { get; set; }
public string Property6 { get; set; }
public TimeSpan Time { get; set; }
public string Property7 { get; set; }
public string Property8 { get; set; }
public string Property9 { get; set; }
public string Property10 { get; set; }
public string[] Property11 { get; set; }
}
Initialized to something like this:
var root = new Obj
{
Property1 = "value1",
Property2 = "value2",
Property3 = new[] {"test", "test1", "test3"},
Property4 = "value4",
Property5 = "value5",
Property6 = "value6",
Time = TimeSpan.FromSeconds(13),
Property7 = "value7",
Property8 = "value8",
Property9 = "value9",
Property10 = "value10",
Property11 = new string[] {"asdf", "basdf"}
};
When I call JsonConvert.SerializeObject(root), it prints out:
{"Property1":"value1","Property2":"value2","Property3":["test","test1","test3"],"Property4":"value4","Property5":"value5","Property6":"value6","Time":"00:00:13","Property7":"value7","Property8":"value8","Property9":"value9","Property10":"value10","Property11":["asdf","basdf"]}
I would like to include a new line once after every Nth property. Let's say every 3rd property.
I tried something like this:
public static string ReplaceEveryNth(string fullString, string pattern, int n)
{
if (n < 1) { return fullString; }
var index = 1;
return Regex.Replace(fullString, pattern, m =>
{
return (index++ % n == 0) ? m.Value + Environment.NewLine : m.Value;
});
}
and called it like this to match key-value pairs on the JSON string:
var replaced = ReplaceEveryNth(json, "(\".*?\":\".*?\"),", 3);
Now, this works for simple properties. But when I start introducing types like arrays, the Regex becomes more complex.
I am wondering if there is a simpler approach.
I'm not sure if this is exactly what you are looking for, but you could try implementing a custom JsonWriter to insert a newline before every Nth property name (assuming you are using Json.Net):
public class CustomJsonWriter : JsonTextWriter
{
public int N { get; set; }
private int propertyCount = 0;
public CustomJsonWriter(TextWriter textWriter, int n) : base(textWriter)
{
N = n;
}
public override void WritePropertyName(string name, bool escape)
{
if (propertyCount > 0 && propertyCount % N == 0)
WriteWhitespace(Environment.NewLine);
base.WritePropertyName(name, escape);
propertyCount++;
}
}
A helper method will make it easy to use:
public static string SerializeWithCustomFormatting(object obj, int n)
{
using (TextWriter sw = new StringWriter())
using (JsonWriter writer = new CustomJsonWriter(sw, n))
{
JsonSerializer ser = new JsonSerializer();
ser.Serialize(writer, obj);
return sw.ToString();
}
}
Then you can do:
string json = SerializeWithCustomFormatting(root, 3);
With your example it would produce output like this:
{"Property1":"value1","Property2":"value2","Property3":["test","test1","test3"]
,"Property4":"value4","Property5":"value5","Property6":"value6"
,"Time":"00:00:13","Property7":"value7","Property8":"value8"
,"Property9":"value9","Property10":"value10","Property11":["asdf","basdf"]}
Fiddle: https://dotnetfiddle.net/gG8az2
Would having a fully indented json work for you?
var json = JsonConvert.SerializeObject(o, new JsonSerializerSettings
{
Formatting = Formatting.Indented
});
Update
Since fully indented json is not good enough you could try a custom converter.
Result
{"Property1":"value1","Property2":"value2","Property3":["test","test1","test3"]
,"Property4":"value4","Property5":"value5","Property6":"value6"
,"Time":"00:00:13","Property7":"value7","Property8":"value8"
,"Property9":"value9","Property10":"value10","Property11":["asdf","basdf"]
}
Converter
public class CustomLineBreakerConverter : JsonConverter
{
private readonly uint n;
private uint i = 1;
public CustomLineBreakerConverter(uint n) { this.n = n; }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
// Scaffolding from https://www.newtonsoft.com/json/help/html/CustomJsonConverter.htm
// Please note this will not work recursively (only the top level will be have the new lines
JToken t = JToken.FromObject(value);
if (t.Type != JTokenType.Object)
{
t.WriteTo(writer);
}
else
{
JObject o = (JObject)t;
var properties = o.Properties();
writer.WriteStartObject();
foreach( var p in properties)
{
p.WriteTo(writer);
if (i++ % n == 0)
{
writer.WriteWhitespace("\r\n"); // This will write a new line after the property even if no more properties
}
}
writer.WriteEndObject();
}
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
=> throw new NotImplementedException("This converter is meant only for writing");
public override bool CanConvert(Type objectType) => true;
}
Test code
var o = new
{
Property1 = "value1",
Property2 = "value2",
Property3 = new[] { "test", "test1", "test3" },
Property4 = "value4",
Property5 = "value5",
Property6 = "value6",
Time = TimeSpan.FromSeconds(13),
Property7 = "value7",
Property8 = "value8",
Property9 = "value9",
Property10 = "value10",
Property11 = new string[] { "asdf", "basdf" }
};
var json = JsonConvert.SerializeObject(o, new JsonSerializerSettings
{
Formatting = Formatting.None,
Converters = new List<JsonConverter>() { new CustomLineBreakerConverter(3) }
});
Console.WriteLine(json);

How to convert Model List to Json

I am trying to create an json data similar to this https://cdn.jsdelivr.net/gh/highcharts/highcharts#v7.0.0/samples/data/world-mortality.json
The current output I am getting is like this. The model name is being displayed and child does not seems to work. is t possible to remove the model field name on the serialize?
[{Name:"ABC",
Firstchild:[{Name:"AIR IMPORT",
Secondchild:[{NAME:"SCMBOA00052997",VALUE:69.7500},
{NAME:"SH123",VALUE:-100.0000},
{NAME:"SH456",VALUE:50.0000},
{NAME:"SH789",VALUE:150.0000}]
}]
}{Name:"DEF",
Firstchild:[{Name:"AIR IMPORT",
Secondchild:[{NAME:"SCMBOA00052997",VALUE:69.7500},
{NAME:"SH111",VALUE:-10.0000},
{NAME:"SH222",VALUE:80.0000},
{NAME:"SH333",VALUE:160.0000}]
}]
}]
What I need is like this
{
"ABC": {
"AIR IMPORT":{
"SH123": -100.0000,
"SH456": 50.0000,
"SH789": 150.0000
}
},
"DEF": {
"AIR IMPORT":{
"SH111": -10.0000,
"SH222": 80.0000,
"SH333": 160.0000
}
}
}
MODEL
public class ParentTreemap
{
public string Name { get; set; }
public List<FirstChildTreemap> Firstchild { get; set; }
}
public class FirstChildTreemap
{
public string Name { get; set; }
public List<SecondChildTreemap> Secondchild { get; set; }
}
public class SecondChildTreemap
{
[JsonProperty]
public string Name { get; set; }
[JsonProperty]
public decimal Value { get; set; }
}
Controller
var parent = treemaplist.Where(s => s.Parent == 0);
List<ParentTreemap> plist = new List<ParentTreemap>();
foreach (var item in parent)
{
var firstchild = treemaplist.Where(s => s.Parent == item.Id && s.Value==0);
List<FirstChildTreemap> flist = new List<FirstChildTreemap>();
foreach (var fitem in firstchild)
{
var secondchild = treemaplist.Where(s => s.Parent == fitem.Id && s.Value!=0);
List<SecondChildTreemap> slist = new List<SecondChildTreemap>();
foreach (var sitem in secondchild)
{
SecondChildTreemap model = new SecondChildTreemap();
model.Name = sitem.Name;
model.Value = sitem.Value;
slist.Add(model);
}
FirstChildTreemap child = new FirstChildTreemap();
child.Name = fitem.Name;
child.Secondchild = slist;
flist.Add(child);
}
ParentTreemap pmodel = new ParentTreemap();
pmodel.Name = item.Name;
pmodel.Firstchild = flist;
plist.Add(pmodel);
}
var stringWriter = new StringWriter();
var serializer = new JsonSerializer();
using (var writer = new JsonTextWriter(stringWriter))
{
writer.QuoteName = false;
serializer.Serialize(writer, plist);
}
ViewData["result"] = stringWriter;

C# LINQ Group By multiple fields with Custom Properties

I'm trying to agregate a list of multiple propertys with Linq.
My Second field is a List of Strings + an other List of strings inside.
Here's a sample of my code :
using System;
using System.Collections.Generic;
using System.Linq;
public class RefValueData
{
public int ReferenceId { get; set; }
public int SiteId { get; set; }
public string SiteName { get; set; }
public string Code { get; set; }
public decimal UnitPoints { get; set; }
public List<TranslationData> Texts { get; set; }
}
public class TranslationData
{
public string Text { get; set; }
public List<TranslationValue> Translations { get; set; }
}
public class TranslationValue
{
public string Culture { get; set; }
public string TranslationText { get; set; }
}
public class Program
{
public static void Main()
{
var values = new List<RefValueData>
{
new RefValueData(){
ReferenceId = 4,
Code = "Code",
SiteId = 2,
SiteName = "Paris",
UnitPoints = 50,
Texts = new List<TranslationData>
{
new TranslationData(){
Text = "A",
Translations = new List<TranslationValue>
{
new TranslationValue() { Culture = "FR-fr", TranslationText = "Bonjour" },
new TranslationValue() { Culture = "ES-es", TranslationText = "Hola" },
}
}
}
},
new RefValueData()
{
ReferenceId = 5,
Code = "Code",
SiteId = 4,
SiteName = "Lyon",
UnitPoints = 50,
Texts = new List<TranslationData>
{
new TranslationData(){
Text = "A",
Translations = new List<TranslationValue>
{
new TranslationValue() { Culture = "FR-fr", TranslationText = "Bonjour" },
new TranslationValue() { Culture = "ES-es", TranslationText = "Hola" },
}
}
}
},
new RefValueData()
{
ReferenceId = 6,
Code = "Code",
SiteId = 3,
SiteName = "Paris",
UnitPoints = 52,
Texts = new List<TranslationData>
{
new TranslationData(){
Text = "B",
Translations = new List<TranslationValue>
{
new TranslationValue() { Culture = "FR-fr", TranslationText = "Salut" },
new TranslationValue() { Culture = "ES-es", TranslationText = "Ciao" },
}
}
}
}
};
var values2 = values
.Distinct()
.GroupBy(x => new
{
x.UnitPoints,
x.Texts
})
.Select(x => new
{
x.Key.UnitPoints,
Texts = x.Key.Texts,
Site = x.Select(y=>y.SiteName)
})
.ToList();
Console.WriteLine(values2.Count);
}
}
I want to have only two lines in my values2 list, but everytime it returns me the whole list.
When I only group by Unit Point, it's work great !
I tried to group the first two lines of my list with some custom Linq query but it doesn't work at all...
Any help / advice is much appreciated :) !
EDIT :
I also tried with an override of the Equals methods like this, but I can't make it work :
public class TranslationValue
{
public string Culture { get; set; }
public string TranslationText { get; set; }
public override bool Equals(object obj)
{
var other = obj as TranslationValue;
if (other == null)
{
return false;
}
return Culture == other.Culture && TranslationText == other.TranslationText;
}
public override int GetHashCode()
{
var hashCode = -2095322044;
hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(Culture);
hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(TranslationText);
return hashCode;
}
}
public class TranslationData
{
public string Text { get; set; }
public List<TranslationValue> Translations { get; set; }
public override bool Equals(object obj)
{
var other = obj as TranslationData;
if (other == null)
{
return false;
}
return Text == other.Text && Translations.SequenceEqual(other.Translations);
}
public override int GetHashCode()
{
var hashCode = -1551681861;
hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(Text);
hashCode = hashCode * -1521134295 + EqualityComparer<List<TranslationValue>>.Default.GetHashCode(Translations);
return hashCode;
}
}
EDIT2 : Here's my 'real' code :
var values = referenceValues.Select(value => new
{
ReferenceId = value.ReferenceId,
SiteId = value.Reference.SiteId ?? -1,
SiteName = value.Reference.Site.Name ?? allSitesName,
Code = value.Code,
UnitPoints = value.UnitPoints,
Texts = // Type: List<TranslationData> , but it will not use the TranslationDataList class that normally work thanks to your help
value.ReferenceValueTexts.Select(text =>
new TranslationData
{
Text = text.Text, // string
Translations = text.TranslationDataValues.Select(translation => // List<TranslationValue>
new TranslationValue {
Culture = translation.Language.StrCulture,
TranslationText = translation.Value
}).ToList()
}).ToList()
}
Julien.
Here's one solution. It works for the sample code that you wrote. But it needs a little work to be robust:
// and also change the declarations in the main method to: new TranslationDataList
public class TranslationDataList : List<TranslationData>
{
public override int GetHashCode()
{
int hash = 13;
// string.GetHashCode() is not reliable. This should be an algorithm that returns the same value for two different lists that contain the same data
foreach (var data in this)
hash = (hash * 7) + data.Text.GetHashCode();
return hash;
}
public override bool Equals(object obj)
{
var other = obj as TranslationDataList;
if (other == null) return false;
if (other.Count != Count) return false;
// write the equality logic here. I don't know if it's ok!
for (int i = 0; i < other.Count; i++)
if (other[i].Text != this[i].Text)
return false;
return true;
}
}
First of all you should add a constructor to the TranslationDataList:
public class TranslationDataList : List<TranslationData>
{
public TranslationDataList(IEnumerable<TranslationData> translationData)
: base(translationData)
{ }
// other members ...
}
Now you can use the TranslationDataList in your query:
var values = referenceValues.Select(value => new
{
ReferenceId = value.ReferenceId,
SiteId = value.Reference.SiteId ?? -1,
SiteName = value.Reference.Site.Name ?? allSitesName,
Code = value.Code,
UnitPoints = value.UnitPoints,
Texts = new TranslationDataList( value.ReferenceValueTexts.Select(text =>
new TranslationData
{
Text = text.Text, // string
Translations = text.TranslationDataValues.Select(translation => // List<TranslationValue>
new TranslationValue {
Culture = translation.Language.StrCulture,
TranslationText = translation.Value
}).ToList()
})); // don't ToList() here anymore
}
And here is another solution:
The GroupBy method takes an IEqualityComparer that can do the responsibility of comparing items for the grouping. But the problem is you used an anonymous type for the key in your grouping "GroupBy(x=>new{x.UnitPoints, x.Texts})". First we have to create a class to play the key role:
public class Key
{
public Key(decimal unitPoints, List<TranslationData> texts)
{
UnitPoints = unitPoints;
Texts = texts;
}
public decimal UnitPoints { get; set; }
public List<TranslationData> Texts { get; set; }
}
then we can implement the comparer:
public class Comparer : IEqualityComparer<Key>
{
public bool Equals(Key x, Key y)
{
if (x.UnitPoints != y.UnitPoints) return false;
if (!ListsAreEqual(x.Texts, y.Texts)) return false;
return true;
}
private bool ListsAreEqual(List<TranslationData> x, List<TranslationData> y)
{
if (x.Count != y.Count) return false;
for (int i = 0; i < x.Count; i++)
if (x[i].Text != y[i].Text)
return false;
return true;
}
public int GetHashCode(Key key)
{
int hash = 13;
hash = (hash * 7) + key.UnitPoints.GetHashCode();
foreach (var data in key.Texts)
hash = (hash * 7) + data.Text.GetHashCode();
return hash;
}
}
and finally this is what your query will look like:
var values2 = values
.Distinct()
.GroupBy(x => new Key(x.UnitPoints, x.Texts), new Comparer())
.Select(x => new
{
x.Key.UnitPoints,
Texts = x.Key.Texts,
Site = x.Select(y => y.SiteName)
}).ToList();
I think the first solution (creating the customized list class) is better, because you can also refactor your code and extract some logic to it.

C# printing LINQ-table to console without specifying the properties

I would like to make a method to print a LINQ (table) with all its properties and values to the console.
I'm currently trying to do this via the System.Reflection.GetType().GetProperties()
What I think is going wrong is the type of parameter I try to send to the method. If possible this should be a var so I can use this method with any class list. (not sure if this is possible)
The issue is with the printtabel() method.
I started from:
Print a table with LINQ
namespace LINQ
{
class Program
{
public class Bier
{
public int BierNr { get; set; }
public string Biernaam { get; set; }
public float Alcohol { get; set; }
public Brouwer Brouwer { get; set; } //associatie met een brouwer
public override string ToString() { return Biernaam + ": " + Alcohol + "% alcohol"; }
}
public class Brouwer
{
public int BrouwerNr { get; set; }
public string Brouwernaam { get; set; }
public bool Belgisch { get; set; }
public List<Bier> Bieren { get; set; }
public override string ToString() { return "Brouwerij " + Brouwernaam + " (" + (Belgisch ? "Belgisch" : "Niet Belgisch") + ")"; }
}
public class Brouwers
{
public List<Brouwer> GetBrouwers()
{
List<Brouwer> lijst = new List<Brouwer>();
Brouwer palm = new Brouwer { BrouwerNr = 1, Brouwernaam = "Palm", Belgisch = true };
palm.Bieren = new List<Bier> {
new Bier {BierNr=1,Biernaam="Palm Dobbel", Alcohol=6.2F, Brouwer=palm},
new Bier {BierNr=2, Biernaam="Palm Green", Alcohol=0.1F, Brouwer=palm},
new Bier {BierNr=3, Biernaam="Palm Royale", Alcohol=7.5F, Brouwer=palm}
};
lijst.Add(palm);
Brouwer hertogJan = new Brouwer { BrouwerNr = 2, Brouwernaam = "Hertog Jan", Belgisch = false };
hertogJan.Bieren = new List<Bier> {
new Bier{ BierNr=4, Biernaam="Hertog Jan Dubbel", Alcohol=7.0F, Brouwer=hertogJan},
new Bier{ BierNr=5, Biernaam="Hertog Jan Grand Prestige", Alcohol=10.0F, Brouwer=hertogJan} };
lijst.Add(hertogJan);
Brouwer inBev = new Brouwer { BrouwerNr = 3, Brouwernaam = "InBev", Belgisch = true };
inBev.Bieren = new List<Bier> {
new Bier { BierNr=6, Biernaam="Belle-vue kriek L.A", Alcohol=1.2F, Brouwer=inBev},
new Bier { BierNr=7, Biernaam="Belle-vue kriek", Alcohol=5.2F, Brouwer=inBev},
new Bier { BierNr=8, Biernaam="Leffe Radieuse", Alcohol=8.2F,Brouwer=inBev},
new Bier { BierNr=9, Biernaam="Leffe Triple", Alcohol=8.5F,Brouwer=inBev} };
lijst.Add(inBev);
//return new List<Brouwer> { palm, hertogJan, inBev };
return lijst;
}
}
static void Main(string[] args)
{
var brouwers = new Brouwers().GetBrouwers();
var belgischeBrouwerijenMet3Bieren =
from brouwer in brouwers
where brouwer.Belgisch && brouwer.Bieren.Count == 3
select brouwer;
foreach (var brouwer in belgischeBrouwerijenMet3Bieren)
Console.WriteLine(brouwer.Brouwernaam);
var bieren = from brouwer in brouwers
from bier in brouwer.Bieren
select bier;
string vorigeBrouwer = "";
foreach (var bier in bieren)
{
if (bier.Brouwer.ToString() != vorigeBrouwer)
{
Console.WriteLine(bier.Brouwer);
vorigeBrouwer = bier.Brouwer.ToString();
}
Console.WriteLine($"\t {bier.ToString()}");
}
Console.WriteLine(printtabel(belgischeBrouwerijenMet3Bieren));
Console.ReadLine();
}
public string printtabel(IEnumerable<Brouwer> response)
{
StringBuilder sb = new StringBuilder();
foreach (PropertyInfo prop in response.GetType().GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
{
object value = prop.GetValue(response, new object[] { });
sb.AppendLine($"{prop.Name} = {value}");
}
return sb.ToString();
}
}
}
Error CS0120 An object reference is required for the non-static field, method, or property 'Program.printtabel(IEnumerable)' LINQ 204 Active
You can fix it by writing:
public static string printtabel(IEnumerable<Brouwer> response)
You are calling a non static method from a static method. Set printtabel static and this error should disappear.

How to implement validation rules which are known only during runtime

I have the following situation.
I have a bunch of simple classes, for example this one
public class Student
{
public int Id { get; set; }
public int Age { get; set; }
public decimal AverageMark { get; set; }
public string Name { get; set; }
public string University { get; set; }
}
There is web page for every of them where user can create, edit and delete. When we create Student of update it, we need to validate it.
The problem is that we do not know validation rules during compilation !!!
We have separate web page for administrator where he set up validation criterias,
for example, that Student Age cannot be less then 15 or University have to be equal "SomeUniversity".
As result i have some list of criterias stored in my database
public class Criteria
{
public string PropertyName { get; set; }
public string OperationName { get; set; }
public string OperationValue { get; set; }
}
I have created simple console application for investigation purposes. Here is code
namespace DynamicValidation
{
class Program
{
static void Main(string[] args)
{
//set up students
var student1 = new Student() { Age = 20, AverageMark = 4, Name = "Ihor", University = "Lviv National University" };
var student2 = new Student() { Age = 20, AverageMark = 4, Name = "Taras", University = "Lviv National University" };
var student3 = new Student() { Age = 20, AverageMark = 5, Name = "Marko", University = "" };
var student4 = new Student() { Age = 20, AverageMark = 3, Name = "Tanya", University = "" };
var student5 = new Student() { Age = 22, AverageMark = 4, Name = "Ira", University = "" };
var students = new List<Student>() { student1, student2, student3, student4, student5 };
//set up validation rules
var criteria1 = new Criteria("Age", "Equal", "20");
var criteria2 = new Criteria("AverageMark", "NotLessThan", "4");
var criteria3 = new Criteria("University", "Contains", "Lviv");
var criterias = new List<Criteria>() { criteria1, criteria2, criteria3 };
var result = new List<Student>();
foreach (var currentStudent in students)
{
foreach (var currentCriteria in criterias)
{
object currentPropertyValue = typeof(Student).GetProperty(currentCriteria.PropertyName).GetValue(currentStudent);
//what is next ???!!!
}
}
}
}
public class Student
{
public int Id { get; set; }
public int Age { get; set; }
public decimal AverageMark { get; set; }
public string Name { get; set; }
public string University { get; set; }
}
public class Criteria
{
public string PropertyName { get; set; }
public string OperationName { get; set; }
public string OperationValue { get; set; }
}
}
How can i implement this piece of code ? (expression trees, dynamic ?)
I do not want that you do work for me but maybe there are some articles about this ? (i tried to found but without success)
Maybe some advices about approach ?
Maybe there is some similar open code ?
Or maybe it is already implemented in some libraries ?
Will be thankful for any help :)
You could write a student validator function, see IsValidStudent(Criteria criteria):
public class Student
{
public int Id { get; set; }
public int Age { get; set; }
public decimal AverageMark { get; set; }
public string Name { get; set; }
public string University { get; set; }
public bool IsValidStudent(Criteria criteria)
{
return IsValidByAge(criteria)
&& IsValidByMarks(criteria)
&& IsValidByUniversity(criteria);
}
private bool IsValidByAge(Criteria criteria)
{
switch (criteria.OperationType)
{
case Criteria.Operation.GreaterThan:
return Convert.ToInt32(criteria.OperationValue) > this.Age;
case Criteria.Operation.LessThan:
return Convert.ToInt32(criteria.OperationValue) < this.Age;
case Criteria.Operation.EqualTo:
return Convert.ToInt32(criteria.OperationValue) == this.Age;
default:
return false;
}
}
private bool IsValidByMarks(Criteria criteria)
{
// etc...
}
private bool IsValidByUniversity(Criteria criteria)
{
// etc...
}
}
Usage:
var result = new List<Student>();
foreach (var currentStudent in students)
{
foreach (var currentCriteria in criterias)
{
if (currentStudent.IsValidStudent(currentCriteria))
{
result.Add(currentStudent);
}
}
}
I also extended your Criteria class:
public class Criteria
{
public string PropertyName { get; set; }
public Operation OperationType { get; set; }
public string OperationValue { get; set; }
public enum Operation
{
EqualTo,
GreaterThan,
LessThan,
Contains
}
public Criteria(string propertyName, Operation operationType, string operationValue)
{
this.PropertyName = propertyName;
this.OperationType = operationType;
this.OperationValue = operationValue;
}
}
IMHO, I have found a little better solution then proposed
Now we do not need to change validation logic inside Student class if new property will be added. Also this code can be applied to any other class (not only for Student class as before)
Interface for validation
public interface IValidator
{
bool Validate(object value, object validateWith);
}
Set of implementations
public class ContainsValidator : IValidator
{
public bool Validate(object value, object validateWith)
{
string valueString = Convert.ToString(value);
string validateWithString = Convert.ToString(validateWith);
return valueString.Contains(validateWithString);
}
}
public class StartWithValidator : IValidator
{
public bool Validate(object value, object validateWith)
{
string valueString = Convert.ToString(value);
string validateWithString = Convert.ToString(validateWith);
return valueString.StartsWith(validateWithString);
}
}
public class LengthValidator : IValidator
{
public bool Validate(object value, object validateWith)
{
string valueString = Convert.ToString(value);
int valueLength = Convert.ToInt32(validateWith);
return (valueString.Length == valueLength);
}
}
public class LessThanValidator : IValidator
{
public bool Validate(object value, object validateWith)
{
decimal valueDecimal = Convert.ToDecimal(value);
decimal validateWithDecimal = Convert.ToDecimal(validateWith);
return (valueDecimal < validateWithDecimal);
}
}
public class MoreThanValidator : IValidator
{
public bool Validate(object value, object validateWith)
{
decimal valueDecimal = Convert.ToDecimal(value);
decimal validateWithDecimal = Convert.ToDecimal(validateWith);
return (valueDecimal > validateWithDecimal);
}
}
public class EqualValidator : IValidator
{
public bool Validate(object value, object validateWith)
{
string valueString = Convert.ToString(value);
string validateWithString = Convert.ToString(validateWith);
return (valueString == validateWithString);
}
}
And usages
class Program
{
static void Main(string[] args)
{
//set up students
var student1 = new Student() { Age = 20, AverageMark = 5, Name = "Ihor", University = "Lviv National University" };
var student2 = new Student() { Age = 20, AverageMark = 5, Name = "SomeLongName", University = "Lviv National University" };
var student3 = new Student() { Age = 20, AverageMark = 5, Name = "Taras", University = "Kyiv National University" };
var student4 = new Student() { Age = 20, AverageMark = 5, Name = "Marko", University = "Some University" };
var student5 = new Student() { Age = 20, AverageMark = 4, Name = "Tanya", University = "Lviv National University" };
var student6 = new Student() { Age = 22, AverageMark = 4, Name = "Ira", University = "" };
var students = new List<Student>() { student1, student2, student3, student4, student5, student6 };
//set up validation rules
var criteria1 = new Criteria("Age", "Equal", "20");
var criteria2 = new Criteria("AverageMark", "MoreThen", "4");
var criteria3 = new Criteria("University", "Contains", "National");
var criteria4 = new Criteria("University", "StartWith", "Lviv");
var criteria5 = new Criteria("Name", "Length", "4");
var criterias = new List<Criteria>() { criteria1, criteria2, criteria3, criteria4, criteria5 };
var result = new List<Student>();
foreach (var currentStudent in students)
{
var isValid = true;
foreach (var currentCriteria in criterias)
{
object currentPropertyValue = typeof(Student).GetProperty(currentCriteria.PropertyName).GetValue(currentStudent);
IValidator currentValidator = ValidatorFactory.GetValidator(currentCriteria.OperationName);
bool validationResult = currentValidator.Validate(currentPropertyValue, currentCriteria.OperationValue);
if (!validationResult)
{
isValid = false;
break;
}
}
if (isValid)
result.Add(currentStudent);
}
}
}
In the end the the code of ValidatorFactory
public class ValidatorFactory
{
public static IValidator GetValidator(string validatorName)
{
validatorName = validatorName.ToUpper();
switch (validatorName)
{
case "CONTAINS": return new ContainsValidator();
case "STARTWITH": return new StartWithValidator();
case "EQUAL": return new EqualValidator();
case "MORETHEN": return new MoreThanValidator();
case "LENGTH": return new LengthValidator();
default: throw new Exception("There are not appropriate validator.");
}
}
}
Maybe this will help someone in the future :)

Categories