Let us suppose we have a document to store our client which has fixed and extra fields.
So here goes our sample class for the client:
public class Client
{
public string Name{ get; set; }
public string Address{ get; set; }
public List<ExtraField> ExtraFields{ get; set; } //these fields are extra ones
}
In extra field class we have something like this:
public class ExtraField
{
public string Key{ get; set; }
public string Type { get; set; }
public string Value { get; set; }
}
If I use standard driver's behaviour for serialization I would get smth like this:
{{Name:VName, Address:VAddress, ExtraFields:[{Key:VKey,Type:VType,
Value:VValue},...]}, document2,...,documentn}
While I would like to have something like this:
{{Name:VName, Address:VAddress, VKey:VValue,...}, document2,...,documentn}
This would improve the search performance and is generally the point of document orientation.
How can I customize the serialization to such a way?
Here is the way I solved it (it works fine) and solved the issue.
using System;
using System.Collections.Generic;
using System.Linq; using System.Text;
using MongoDB.Bson;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Serializers;
namespace TestDataGeneration {
public class FieldsWrapper : IBsonSerializable
{
public List<DataFieldValue> DataFieldValues { get; set; }
public object Deserialize(MongoDB.Bson.IO.BsonReader bsonReader, Type nominalType, IBsonSerializationOptions options)
{
if (nominalType != typeof(FieldsWrapper)) throw new ArgumentException("Cannot deserialize anything but self");
var doc = BsonDocument.ReadFrom(bsonReader);
var list = new List<DataFieldValue>();
foreach (var name in doc.Names)
{
var val = doc[name];
if (val.IsString)
list.Add(new DataFieldValue {LocalIdentifier = name, Values = new List<string> {val.AsString}});
else if (val.IsBsonArray)
{
DataFieldValue df = new DataFieldValue {LocalIdentifier = name};
foreach (var elem in val.AsBsonArray)
{
df.Values.Add(elem.AsString);
}
list.Add(df);
}
}
return new FieldsWrapper {DataFieldValues = list};
}
public void Serialize(MongoDB.Bson.IO.BsonWriter bsonWriter, Type nominalType, IBsonSerializationOptions options)
{
if (nominalType != typeof (FieldsWrapper))
throw new ArgumentException("Cannot serialize anything but self");
bsonWriter.WriteStartDocument();
foreach (var dataFieldValue in DataFieldValues)
{
bsonWriter.WriteName(dataFieldValue.LocalIdentifier);
if (dataFieldValue.Values.Count != 1)
{
var list = new string[dataFieldValue.Values.Count];
for (int i = 0; i < dataFieldValue.Values.Count; i++)
list[i] = dataFieldValue.Values[i];
BsonSerializer.Serialize(bsonWriter, list);
}
else
{
BsonSerializer.Serialize(bsonWriter, dataFieldValue.Values[0]);
}
}
bsonWriter.WriteEndDocument();
}
} }
Essentially you just need to implement two methods yourself. First one to serialize an object as you want and second to deserialize an object from db to your Client class back:
1 Seialize client class:
public static BsonValue ToBson(Client client)
{
if (client == null)
return null;
var doc = new BsonDocument();
doc["Name"] = client.Name;
doc["Address"] = client.Address;
foreach (var f in client.ExtraFields)
{
var fieldValue = new BsonDocument();
fieldValue["Type"] = f.Type;
fieldValue["Value"] = f.Value;
doc[f.Key] = fieldValue;
}
return doc;
}
2 Deserialize client object:
public static Client FromBson(BsonValue bson)
{
if (bson == null || !bson.IsBsonDocument)
return null;
var doc = bson.AsBsonDocument;
var client = new Client
{
ExtraFields = new List<ExtraField>(),
Address = doc["Address"].AsString,
Name = doc["Name"].AsString
};
foreach (var name in doc.Names)
{
var val = doc[name];
if (val is BsonDocument)
{
var fieldDoc = val as BsonDocument;
var field = new ExtraField
{
Key = name,
Value = fieldDoc["Value"].AsString,
Type = fieldDoc["Type"].AsString
};
client.ExtraFields.Add(field);
}
}
return client;
}
3 Complete test example:
I've added above two method to your client class.
var server = MongoServer.Create("mongodb://localhost:27020");
var database = server.GetDatabase("SO");
var clients = database.GetCollection<Type>("clients");
var client = new Client() {Id = ObjectId.GenerateNewId().ToString()};
client.Name = "Andrew";
client.Address = "Address";
client.ExtraFields = new List<ExtraField>();
client.ExtraFields.Add(new ExtraField()
{
Key = "key1",
Type = "type1",
Value = "value1"
});
client.ExtraFields.Add(new ExtraField()
{
Key = "key2",
Type = "type2",
Value = "value2"
});
//When inseting/saving use ToBson to serialize client
clients.Insert(Client.ToBson(client));
//When reading back from the database use FromBson method:
var fromDb = Client.FromBson(clients.FindOneAs<BsonDocument>());
4 Data structure in a database:
{
"_id" : ObjectId("4e3a66679c66673e9c1da660"),
"Name" : "Andrew",
"Address" : "Address",
"key1" : {
"Type" : "type1",
"Value" : "value1"
},
"key2" : {
"Type" : "type2",
"Value" : "value2"
}
}
BTW: Take a look into serialization tutorial as well.
Related
I have some huge classes and don't want to write them all out for testing, because it's a huge effort and I could forget some values what makes the test invalid.
Messages = new List<Request.Notif.NotifRuleMessages>
{
new Request.Notif.NotifRuleMessages
{
Code = 1234,
Message = new List<Request.Notif.NotifRuleMessagesMessage>
{
new Request.Notif.NotifMessagesMessage
{
Status = new Request.Notif.NotifMessagesMessageStatus
{
Code = 1,
Bool = true,
Test1 = "Test",
Test2 = "Test"
},
Rules = new List<Request.Notif.NotifMessagesMessageRule>
{
new Request.Notif.NotifMessagesMessageRule
{
Lengths = new Request.Notif.NotifMessagesMessageRuleLength
{
Lenght = 1,
Lengths = new List<Request.Notif.NotifMessagesMessageRuleLengthLength>
{
new Request.Notif.NotifMessagesMessageRuleLengthLength
{
Type = "Test",
Value = 1
}
}
},
Status = new List<Request.Notif.NotifMessagesMessageRuleStatus>
{
new Request.Notif.NotifMessagesMessageRuleStatus
{
Test1 = "Test",
Test2 = "Test"
Is there a way to automaticly fill all int values with 1 or 0 and all string values with Test and especially all objects with the right class without unit testing and external libs?
Using reflection you could populate your objects recursively and set whatever default values you choose. A small example of a helper function that could do that for you:
void SetDefaults(object testObj)
{
var props = testObj.GetType().GetProperties();
foreach (var prop in props)
{
if (prop.GetSetMethod() == null)
{
continue;
}
var propType = prop.PropertyType;
if (propType == typeof(int))
{
prop.SetValue(testObj, 1);
}
else if (propType == typeof(bool))
{
prop.SetValue(testObj, false);
}
// More conditions...
else
{
var ctor = propType.GetConstructor(Type.EmptyTypes);
var propertyObject = ctor.Invoke(new object[0]);
SetDefaults(propertyObject);
prop.SetValue(testObj, propertyObject);
}
}
}
As you can see, if your tree of objects use types that don't have default constructors (parameterless constructors) you need some more complicated logic in the else-condition. Basically the stuff going on here is a very simplified version of what happens in a dependency injection framework.
To use it, do something like:
void Main()
{
TestObject obj = new TestObject();
SetDefaults(obj);
Console.WriteLine(obj);
}
class TestObject {
public int MyInt { get; set; }
public SubTestObject SubObj { get; set; }
}
class SubTestObject {
public int MyOwnInt { get; set; }
public bool MyBoolGetter => 1 > 0;
}
I have a MongoDB collection that has documents that look like the following:
{
"Name" : "Name1",
"AA" : [
{
"B" : {
"Name" : "Name2"
},
"CC" : [
{
"Field" : "Value"
}
]
}
]
}
I am working from C# with the 2.7 MongoDB driver.
I want to push an element to the CC array of those elements of AA that match a certain condition. I was able to do that with following code:
[TestFixture]
class TestClass
{
[Test]
public void Test()
{
var b = new B { Name = "Name2" };
var c = new C { Field = "Value2" };
var field = new StringFieldDefinition<Document>("AA.$[element].CC");
var updateDefinition = Builders<Document>
.Update
.Push(field, c);
var arrayFilterBson = new BsonDocument("element.B",
new BsonDocument("$eq", b.ToBsonDocument()));
var arrayFilter = new BsonDocumentArrayFilterDefinition<AgentRecord>(arrayFilterBson);
var documentFilter = Builders<Document>.Filter.Eq(r => r.Name, "Name1");
var updateOptions = new UpdateOptions
{
IsUpsert = false,
ArrayFilters = new ArrayFilterDefinition[] { arrayFilter }
};
var settings = new MongoClientSettings
{
Server = new MongoServerAddress("localhost", 27000)
};
var mongoClient = new MongoClient(settings);
var database = mongoClient.GetDatabase("so-question");
var collection = database.GetCollection<Document>("collection");
collection.UpdateOne(documentFilter, updateDefinition, updateOptions);
}
}
// Mapping classes
public class Document
{
public string Name { get; set; }
public A[] AA { get; set; }
}
public class A
{
public B B { get; set; }
public C[] CC { get; set; }
}
public class B
{
public string Name { get; set; }
}
public class C
{
public string Field { get; set; }
}
Is there a way to achieve the same through the typed API to avoid having to hardcode the name of the fields in definitions such as AA.$[element].CC or element.B?
I am of course worried about having to change these hard-coded expressions when the name of the properties of the classes change or for example if I make use of the [BsonElement("name")] attribute to control the name of the mapped property.
I am doing a bot for VK on C# and faced to some problems. I have method which returns JSON like this
{
"ts": 1674111105,
"updates": [[4,
2262,
17,
61835649,
1534493714,
"",
{
"attach1_type": "doc",
"attach1": "61835649_472186415",
"title": " ... "
}
]]
}
This is object, as I see, but I cant get anything from the attach_type1 to title including. This is also an object, and it can't be transformed to string just like .ToString(), because in that case in the result I have System.Object. So, does anybody know how I can change this type or is it impossible?? I am in desperation.
I created a class for this object
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.Serialization;
namespace CSharpVKbot.VK.UserLongPoll
{
[DataContract()]
public class Attachment
{
[DataMember(Name = "attach1_type")]
public string AttachType;
[DataMember(Name = "attach1")]
public string Attach;
[DataMember(Name = "title")]
public string Title;
}
}
created an object of this class
public Attachment DocId = new Attachment();
and then tried to change type to attachment, but it doesnt work either
case UpdateCode.NewMessage:
u.MessageID = (int)item[1];
u.Flags = (int)item[2];
u.PeerID = (int)item[3];
u.TimeStamp = (int)item[4];
u.Text = (string)item[5];
u.DocId = (Attachment)item[6];
break;
You need to deserialise the JSON - It cannot be just converted to an object.
Try something like
Attachment deserializedAttachement = new Attachment();
MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(item[6]));
DataContractJsonSerializer ser = new DataContractJsonSerializer(deserializedAttachment.GetType());
deserializedAttachment = ser.ReadObject(ms) as Attachment;
ms.Close();
Where item[6] is the string that represents the attachment information.
See - https://learn.microsoft.com/en-us/dotnet/framework/wcf/feature-details/how-to-serialize-and-deserialize-json-data#example
I think that you will have to iterate through all json properties.
This code may help you
dynamic obj = Newtonsoft.Json.JsonConvert.DeserializeObject<dynamic>(YourJsonString);
foreach (var prop in obj)
{
if (prop is Newtonsoft.Json.Linq.JObject)
{
// Handle JObject
}
if (prop is Newtonsoft.Json.Linq.JProperty)
{
// Handle JProperty
}
}
There are many direction to resolve your problem.
I like it (using Newtonsoft):
JObject data = (JObject)JsonConvert.DeserializeObject(json);
var attach1_type = data.Descendants()
.OfType<JProperty>()
.FirstOrDefault(x => x.Name == "attach1_type")
?.Value;
numbers, strings, objects are in same array, which means they are boxed before returning to you. so your updates is List<List<object>> or object[][], whatever. your c# class, which match this json format, could be simply like this:
public class SomethingJsonResult
{
public int ts { get; set; }
public List<List<object>> updates { get; set; }
}
The 1st option is to use anonymous type:
public void ParseJsonResult(SomethingJsonResult result)
{
var definition = new
{
attach1_type = "",
attach1 = "",
title = ""
};
result?.updates?.ForEach(x =>
{
var update = JsonConvert.DeserializeAnonymousType(x[6], definition);
var attachment = new Attachment
{
AttachType = update.attach1_type,
Attach = update.attach1,
Title = update.title,
};
});
}
The 2nd option is a bit complex:
[DataContract()]
public class Attachment
{
[DataMember(Name = "attach1_type")]
[JsonProperty("attach1_type")] //Tell JsonConverter how to map your object
public string AttachType { get; set; }//Here is property, but not variable
[DataMember(Name = "attach1")]
[JsonProperty("attach1")]
public string Attach { get; set; }
[DataMember(Name = "title")]
[JsonProperty("title")]
public string Title { get; set; }
}
public void ParseJsonResult(SomethingJsonResult result)
{
result?.updates?.ForEach(update =>
{
//(Attachment)update[6] works only when your names of properties 100% match json objects
JsonConvert.DeserializeObject<Attachment>(update[6])
....
});
}
DataContractSerializer example:
https://learn.microsoft.com/en-us/dotnet/api/system.runtime.serialization.datamemberattribute?redirectedfrom=MSDN&view=netframework-4.7.2
you can implement everything inside your custom converter:
https://www.newtonsoft.com/json/help/html/CustomJsonConverter.htm
JsonConvert.DeserializeObject<AttachmentWrapper>(json, new AttachmentConverter(typeof(AttachmentWrapper)));
I'm using Linq/Lambda to write output to an XML file, just in the standard way:
new XElement("Employees",
from emp in empList
select new XElement("Employee",
new XAttribute("ID", emp.ID),
new XElement("FName", emp.FName),
new XElement("LName", emp.LName),
new XElement("DOB", emp.DOB),
new XElement("Sex", emp.Sex)
));
The issue I'm running into in effect is that my emp class contains fields that don't implement the IEnumerable interface, but which themselves also contain fields (imagine, for example, the emp included a "WorkHistory" field, which itself contained a set of fields related to complaints, commendations, etc). These latter fields are optional (and non-repeating) in the XML schema.
Is there any way of checking whether they have been set (i.e., whether they are null or not) given the Linq/Lambda framework? If they are not set, then the equivalent XML node needs to be absent.
Hope that made sense. I'm new to Linq/Lambda stuff, so sorry if it sounds confused.
I wrote a simple driver. In future, follow this guide when posting.
You can do this several ways. Out of curiosity, I tried out linq:
using System;
using System.Linq;
using System.Xml.Linq;
using System.Collections.Generic;
namespace Rextester
{
public class Program
{
public static void Main(string[] args)
{
var empList = new List<Employee>();
for (int i=0; i<10; i++) {
empList.Add(GenerateTestEmployee(i));
}
var xmlConverter = new XmlConverter();
var employeesNode = new XElement(
"Employees",
empList.Select(emp => xmlConverter.Convert(emp))
);
Console.WriteLine(employeesNode.ToString());
}
private static Employee GenerateTestEmployee(int seed) {
return new Employee() {
ID = Guid.NewGuid(),
FName = seed.ToString(),
LName = "Example",
DOB = DateTime.UtcNow.AddYears(-20).AddYears(-seed),
Sex = seed % 2 == 0 ? "Male" : "Female",
WorkHistory = GenerateTestWorkHistory(seed)
};
}
private static WorkHistory GenerateTestWorkHistory(int seed) {
if (seed % 7 == 0) {
return null;
}
return new WorkHistory() {
Complaints = Enumerable.Repeat("Complaint!", seed % 2).ToList(),
Commendations = Enumerable.Repeat("Commendation!", seed % 3).ToList()
};
}
}
public class Employee {
public Guid ID { get; set; }
public string FName { get; set; }
public string LName { get; set; }
public DateTime DOB { get; set; }
public string Sex { get; set; }
public WorkHistory WorkHistory { get; set; }
}
public class WorkHistory {
public List<string> Complaints { get; set; }
public List<string> Commendations { get; set; }
}
public class XmlConverter {
public XElement Convert(Employee emp) {
var attributes = new List<XAttribute> {
new XAttribute("ID", emp.ID)
};
var elements = new List<XElement> {
new XElement("FName", emp.FName),
new XElement("LName", emp.LName),
new XElement("DOB", emp.DOB),
new XElement("Sex", emp.Sex)
};
var workHistory = Convert(emp.WorkHistory);
if (workHistory != null) {
elements.Add(workHistory);
}
return new XElement("Employee", attributes, elements);
}
private XElement Convert(WorkHistory hist) {
if (hist == null) {
return null;
}
var elements = new List<XElement>();
if (hist.Complaints != null && hist.Complaints.Any()) {
var complaints = new XElement(
"Complaints",
hist.Complaints.Select(comp => new XElement("Complaint", comp))
);
elements.Add(complaints);
}
if (hist.Commendations != null && hist.Commendations.Any()) {
var commendations = new XElement(
"Commendations",
hist.Commendations.Select(comm => new XElement("Commendation",comm))
);
elements.Add(commendations);
}
return elements.Any() ? new XElement("WorkHistory", elements)
: null;
}
}
}
Good luck!
PS: I find this online IDE helpful for testing snippets.
Using method syntax, as opposed to query syntax, you can build Employee element gradually, adding optional child elements only when it should :
new XElement("Employees",
empList.Select(emp => {
var xe = new XElement("Employee",
new XAttribute("ID", emp.ID),
....
));
// do further checks and add optional child elements accordingly
if (emp.WorkHistory != null) xe.Add(new XElement(...));
// return the final result
return xe;
})
)
I have a class, which is created and populated from an xml string, I've simplified it for example purposes:
[XmlRoot("Person")]
public sealed class Person
{
[XmlElement("Name")]
public string Name { get; set; }
[XmlElement("Location")]
public string Location { get; set; }
[XmlElement("Emails", Type = typeof(PersonEmails)]
public PersonEmails Emails { get; set; }
}
public class PersonEmails
{
[XmlElement("Email", Type = typeof(PersonEmail))]
public PersonEmail[] Emails { get; set; }
}
public class PersonEmail
{
[XmlAttribute("Type")]
public string Type { get; set; }
[XmlText]
public string Value { get; set; }
}
To extract the information, I'm trying to load them into another class, which is simply:
public class TransferObject
{
public string Name { get; set; }
public ObjectField[] Fields { get; set; }
}
public class ObjectField
{
public string Name { get; set; }
public string Value { get; set; }
}
I'm only populating "Fields" from the other object, which would simply be (Name = "Location", Value = "London"), but for Emails, (Name = "Email"+Type, Value = jeff#here.com)
Currently I can populate all the other fields, but I'm stuck with Emails, and knowing how to dig deep enough to be able to use reflection (or not) to get the information I need. Currently I'm using:
Person person = Person.FromXmlString(xmlString);
List<ObjectField> fields = new List<ObjectField>();
foreach (PropertyInfo pinfo in person.getType().GetProperties()
{
fields.Add(new ObjectField { Name = pinfo.Name, Value = pinfo.getValue(person, null).ToString();
}
How can I expand on the above to add all my emails to the list?
You are trying to type cast a complex values type to string value so you lost the data. Instead use following code:
class Program
{
static void Main(string[] args)
{
Person person = new Person();
person.Name = "Person One";
person.Location = "India";
person.Emails = new PersonEmails();
person.Phones = new PersonPhones();
person.Emails.Emails = new PersonEmail[] { new PersonEmail() { Type = "Official", Value = "xyz#official.com" }, new PersonEmail() { Type = "Personal", Value = "xyz#personal.com" } };
person.Phones.Phones = new PersonPhone[] { new PersonPhone() { Type = "Official", Value = "789-456-1230" }, new PersonPhone() { Type = "Personal", Value = "123-456-7890" } };
List<ObjectField> fields = new List<ObjectField>();
fields = GetPropertyValues(person);
}
static List<ObjectField> GetPropertyValues(object obj)
{
List<ObjectField> propList = new List<ObjectField>();
foreach (PropertyInfo pinfo in obj.GetType().GetProperties())
{
var value = pinfo.GetValue(obj, null);
if (pinfo.PropertyType.IsArray)
{
var arr = value as object[];
for (var i = 0; i < arr.Length; i++)
{
if (arr[i].GetType().IsPrimitive)
{
propList.Add(new ObjectField() { Name = pinfo.Name + i.ToString(), Value = arr[i].ToString() });
}
else
{
var lst = GetPropertyValues(arr[i]);
if (lst != null && lst.Count > 0)
propList.AddRange(lst);
}
}
}
else
{
if (pinfo.PropertyType.IsPrimitive || value.GetType() == typeof(string))
{
propList.Add(new ObjectField() { Name = pinfo.Name, Value = value.ToString() });
}
else
{
var lst = GetPropertyValues(value);
if (lst != null && lst.Count > 0)
propList.AddRange(lst);
}
}
}
return propList;
}
}
Check this snippet out:
if(pinfo.PropertyType.IsArray)
{
// Grab the actual instance of the array.
// We'll have to use it in a few spots.
var array = pinfo.GetValue(personObject);
// Get the length of the array and build an indexArray.
int length = (int)pinfo.PropertyType.GetProperty("Length").GetValue(array);
// Get the "GetValue" method so we can extact the array values
var getValue = findGetValue(pinfo.PropertyType);
// Cycle through each index and use our "getValue" to fetch the value from the array.
for(int i=0; i<length; i++)
fields.Add(new ObjectField { Name = pinfo.Name, Value = getValue.Invoke(array, new object[]{i}).ToString();
}
// Looks for the "GetValue(int index)" MethodInfo.
private static System.Reflection.MethodInfo findGetValue(Type t)
{
return (from mi in t.GetMethods()
where mi.Name == "GetValue"
let parms = mi.GetParameters()
where parms.Length == 1
from p in parms
where p.ParameterType == typeof(int)
select mi).First();
}
You can definately do it with Reflection... You can take advantage of the fact that a Type can tell you if it's an array or not (IsArray)... and then take advantage of the fact that an Array has a method GetValue(int index) that will give you a value back.
Per your comment
Because Emails is a property within a different class, recursion should be used. However the trick is knowing when to go to the next level. Really that is up to you, but
if it were me, I would use some sort of Attribute:
static void fetchProperties(Object instance, List<ObjectField> fields)
{
foreach(var pinfo in instance.GetType().GetProperties())
{
if(pinfo.PropertyType.IsArray)
{
... // Code described above
}
else if(pinfo.PropertyType.GetCustomAttributes(typeof(SomeAttribute), false).Any())
// Go the next level
fetchProperties(pinfo.GetValue(instance), fields);
else
{
... // Do normal code
}
}
}