Is is possible to flatten parts of object when auto mapping - c#

I'm new to using Elasticsearch and I am using search on a services where part of the result I get back is formatted like this(names translated from other language):
accounting: {
properties: {
accountingInterval: {
properties: {
endDate: {
type: "date",
format: "dateOptionalTime"
},
startDate: {
type: "date",
format: "dateOptionalTime"
}
}
}
}
}
I can auto map it to object like this without a problem:
class MyBaseObject
{
public Accounting Accounting { get; set; }
//...some other values on base object
}
class Accounting
{
public AccountingInterval AccountingInterval { get; set; }
}
class AccountingInterval
{
[Date(Format = "dateOptionalTime")]
public DateTime? StartDate { get; set; }
[Date(Format = "dateOptionalTime")]
public DateTime? EndDate { get; set; }
}
Is there an way to get it to map to a simple object like this:
class MyBaseObject
{
[Date(Format = "dateOptionalTime")]
public DateTime? AccountingStartDate { get; set; }
[Date(Format = "dateOptionalTime")]
public DateTime? AccountingEndDate { get; set; }
//...some other values on base object
}
I tried setting the name attribute but it did not seem to work
class MyBaseObject
{
[Date(Name ="accounting.accountingInterval.startDate", Format = "dateOptionalTime")]
public DateTime? AccountingStartDate { get; set; }
[Date(Name ="accounting.accountingInterval.endDate", Format = "dateOptionalTime")]
public DateTime? AccountingEndDate { get; set; }
//...some other values on base object
}

As panchicore said in the comments, it would be possible to perform this flattening at index time with Ingest node and pipelines, and the type mapping in the index would reflect this structure.
If you're not responsible for indexing, then this is trickier to do. The mapping in NEST is used for both input to and output of documents from Elasticsearch. It'd be possible to control how JSON is deserialized to MyBaseObject by hooking up the Nest.JsonSerializer nuget package, and using Json.NET as the serializer for the client, and defining a custom JsonConverter for the MyBaseObject type. If you'd only like to do it for type aesthetics though, the effort is probably more than the value!

Related

Is there a way to type check a generic function parameter using the shape of the object instead of inheritance?

I have a few EF model classes that I want to create. Each class has a few common properties that I want to set before inserting a new entity, for example:
public partial class BlogPost {
public DateTime CreatedTime { get; set; }
public string CreatorName { get; set; }
public string PostTitle { get; set; }
public string PostText { get; set; }
}
public partial class Comment {
public DateTime CreatedTime { get; set; }
public string CreatorName { get; set; }
public string CommentText { get; set; }
}
...
When I create these classes, I'm instantiating them like so:
var blogPost = new BlogPost {
CreatedTime = DateTime.UtcNow,
CreatorName = creatorName,
PostTitle = postTitle,
PostText = postText,
};
var comment = new Comment {
CreatedTime = DateTime.UtcNow,
CreatorName = creatorName,
...
};
...
I want to create a method to automatically set some of the common properties so I don't need to manually type them out for each class with the same properties. Since they don't extend the same class or implement the same interface, I'm wondering how this can be achieved. My first thought was to use a generic method; however, I don't know if there's a way to specify what properties the generic type should have without them extending the same class (similar to TypeScript's "duck typing"). My desired method looks something like this:
public void SetInitialProperties<T>(T dbEntity, DateTime createdTime, string creatorName) where T : ??? {
dbEntity.CreatedTime = createdTime;
dbEntity.CreatorName = creatorName;
}
...
var blogPost = new BlogPost { PostTitle = postTitle, PostText = postText };
SetInitialProperties(blogPost, createdTime, creatorName);
Worst case scenario if I can't use a generic, I could always use dynamic; however, I'd like to keep type checking if possible.
You can achieve what you want using reflection. You can pass in an object and resolve it's type, then get all the public properties of that given type and find if you have one called CreatedTime for example. Then you can set the value of the given property on the passed dbEntity object. However, I do not recommend this solution:
public void SetInitialProperties(object dbEntity, DateTime createdTime, string creatorName) {
// get the passed object's properties and find the one called CreatedTime
var createdTimePropertyInfo = dbEntity.GetType().GetProperties().Where(i => i.Name == "CreatedTime").FirstOrDefault();
// below line is equal to: dbEntity.CreatedTime = createdTime;
createdTimePropertyInfo.SetValue(dbEntity, createdTime);
var creatorNamePropertyInfo = dbEntity.GetType().GetProperties().Where(i => i.Name == "CreatorName").FirstOrDefault();
creatorNamePropertyInfo.SetValue(dbEntity, creatorName);
}
You would be better off on the long run by creating a common interface or even an abstract base class so you don't have to implement CreatedTime and CreatorName and other properties for every EF model. It would look like the following:
public interface IUserEntry
{
DateTime CreatedTime { get; set; }
string CreatorName { get; set; }
}
public abstract class UserEntryBase : IUserEntry
{
public DateTime CreatedTime { get; set; }
public string CreatorName { get; set; }
}
public partial class BlogPost : UserEntryBase
{
public string PostTitle { get; set; }
public string PostText { get; set; }
}
public partial class Comment : UserEntryBase
{
public string CommentText { get; set; }
}
And your SetInitialProperties would be pretty simple:
public void SetInitialProperties(IUserEntry dbEntity, DateTime createdTime, string creatorName)
{
dbEntity.CreatedTime = createdTime;
dbEntity.CreatorName = creatorName;
}
Once you develop onto an interface, you achieve much more flexibility than by using reflection or a dynamic type, since you get the compile-time checking that was mentioned before me and you can see the common properties of your models.
You can't do that in C# because C# uses a nominal type system and not a structural type system.
For your particular case you have to come up with an interface that contains the properties in common and which will be implemented by both entities, then use that new interface as you generic function parameter constraint.
If you're absolutely sure the properties will have the same name, you could pass a dynamic to set property values. However, this prevents any compile-time checking of the typing, so if you accidently pass an incompatible type it won't be caught until runtime.
public void SetInitialProperties(dynamic dbEntity, DateTime createdTime, string creatorName) {
dbEntity.CreatedTime = createdTime;
dbEntity.CreatorName = creatorName;
}

Deserializing from BsonDocument to string and serializing back to BsonDocument

I have a requirement where I need a property that is actually a JSON value from a MongoDB collection that needs to be deserialized into a string. This conversion is throwing a "Cannot deserialize a 'String' from a BsonType 'Document'" exception.
I tried implementing a JSON custom converter, but as the value is being treated as a BsonDocument, it is not helping and I am getting the same exception. I also need it in the original format as I need to cast it back into a BsonDocument down the line. I guess I would need a custom Bson serializer/deserializer.
Incoming sample document from MongoDB collection:
{
"name": "Jane Doe",
"dob": {
"month": "Sep",
"day": 09,
"year": 1987
}
}
Type it is expecting for deserialization:
public class Person
{
public string name { get; set; }
public Dob dob { get; set; }
public class Dob
{
public string month { get; set; }
public int day { get; set; }
public int year { get; set; }
}
}
Type I want it to deserialize into:
public class Person
{
public string name { get; set; }
public string dob { get; set; }
}
To summarize, you have a public-facing string property on your model that contains JSON which you would like to internally serialize to MongoDB by deserializing the JSON string to some intermediate DTO, then serializing the DTO itself to Mongo.
Here are a couple of approaches to solving your problem.
Firstly, you could introduce a private DTO-valued property Dob SerializedDOB { get; set; } into your data model, mark that property with [BsonElement("dob")] to force it to be serialized, then modify dob to be a non-serialized surrogate property that serializes from and to the underlying SerializedDOB within its getter and setter. The following code shows this approach:
public class Person
{
public string name { get; set; }
[BsonIgnore]
public string dob
{
get => BsonExtensionMethods.ToJson(SerializedDOB);
set => SerializedDOB = MyBsonExtensionMethods.FromJson<Dob>(value);
}
[BsonElement("dob")]
Dob SerializedDOB { get; set; }
class Dob // The DTO
{
public string month { get; set; }
public int day { get; set; }
public int year { get; set; }
}
}
The advantage of this approach is that, by making the JSON string a surrogate, the setter automatically ensures that it is well-formed.
Demo fiddle #1 here.
Secondly, you could create a custom SerializerBase<string> for dob that maps the string value to and from the DTO Dob during (de)serialization. The following code shows this approach:
public class Person
{
public string name { get; set; }
[BsonSerializer(typeof(JsonStringAsObjectSerializer<Dob>))]
public string dob { get; set; }
class Dob // The DTO
{
public string month { get; set; }
public int day { get; set; }
public int year { get; set; }
}
}
public class JsonStringAsObjectSerializer<TObject> : SerializerBase<string> where TObject : class
{
public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, string value)
{
if (value == null)
{
var bsonWriter = context.Writer;
bsonWriter.WriteNull();
}
else
{
var obj = MyBsonExtensionMethods.FromJson<TObject>(value);
var serializer = BsonSerializer.LookupSerializer(typeof(TObject));
serializer.Serialize(context, obj);
}
}
public override string Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
{
var bsonReader = context.Reader;
var serializer = BsonSerializer.LookupSerializer(typeof(TObject));
var obj = (TObject)serializer.Deserialize(context);
return (obj == null ? null : BsonExtensionMethods.ToJson(obj));
}
}
The advantage of this approach is that JsonStringAsObjectSerializer<TObject> can be reused whenever this requirement arises.
Demo fiddle #2 here.
The following extension method is used with both solutions to deserialize the JSON string to a specified type because, confusingly, BsonExtensionMethods has a ToJson() method but no FromJson() method:
public static partial class MyBsonExtensionMethods
{
// Not sure why but BsonExtensionMethods.cs seems to lack methods for deserializing from JSON, so I added some here.
// See https://github.com/mongodb/mongo-csharp-driver/blob/master/src/MongoDB.Bson/BsonExtensionMethods.cs
public static TNominalType FromJson<TNominalType>(
string json,
JsonReaderSettings readerSettings = null,
IBsonSerializer<TNominalType> serializer = null,
Action<BsonDeserializationContext.Builder> configurator = null)
{
return (TNominalType)FromJson(json, typeof(TNominalType), readerSettings, serializer, configurator);
}
public static object FromJson(
string json,
Type nominalType,
JsonReaderSettings readerSettings = null,
IBsonSerializer serializer = null,
Action<BsonDeserializationContext.Builder> configurator = null)
{
if (nominalType == null || json == null)
throw new ArgumentNullException();
serializer = serializer ?? BsonSerializer.LookupSerializer(nominalType);
if (serializer.ValueType != nominalType)
throw new ArgumentException(string.Format("serializer.ValueType {0} != nominalType {1}.", serializer.GetType().FullName, nominalType.FullName), "serializer");
using (var textReader = new StringReader(json))
using (var reader = new JsonReader(textReader, readerSettings ?? JsonReaderSettings.Defaults))
{
var context = BsonDeserializationContext.CreateRoot(reader, configurator);
return serializer.Deserialize(context);
}
}
}

Nested c# class not getting converted to TS class correctly by typewriter

I am trying to convert C# classes with the TsType attribute to TS classes to be used in my angular application. I have installed typewriter in my Visual Studio 2015 application and its been configured. I can see that it is generating the TS classes except for one.
I have a nested class and it doesn't seem to recognize the nested class hence gets omitted in the generated file. If you notice in the generated code, it hasn't generated the nested data class and hence complains saying data not found when I build my angular application.
Has anybody come across this?
C# class:
[TsType]
public class BoxPlotSeries
{
public string color { get; set; }
public string name { get; set; }
public Data data { get; set; }
[TsType]
public class Data
{
public decimal Low { get; set; }
public decimal Q1 { get; set; }
public decimal Median { get; set; }
public decimal Q3 { get; set; }
public decimal High { get; set; }
}
}
Generated file:
export interface BoxPlotSeries {
color: string;
name: string;
data: Data;
}
There's a $NestedClasses method you can use. So something like this would generate the parent and the first level of children...
$Classes(c => c.Attributes.Any(a => a.Name == "TsType"))[
export interface $Name$TypeParameters { $Properties[
$name: $Type;]
}]
$Classes()[$NestedClasses(c => c.Attributes.Any(a => a.Name == "TsType"))[
export class $Name {
$Properties[
public $Name: $Type;]
}]]
}

Add item in nested array (mongodb and C#)

I have the following document called Attendances
{
"_id" : ObjectId("5a4ffb00762caf6b54f61ebb"),
"AttnDate" : ISODate("2018-01-05T22:24:00.490Z"),
"AllAttendances" : [
{
"FullName" : "DOMAIN\Zack",
"Logged" : ISODate("2018-01-05T22:23:46.835Z"),
"Pauses" : [
{
PauseStartAt: ISODate("2018-01-05T22:30:46.835Z"),
PauseEndAt: ISODate("2018-01-05T22:35:46.835Z")
}
]
}
]
}
How can i add new items to Pauses. This is my attempt but i have this error "Cannot convert lambda expression to type 'fielddefinition because it is not a delegate type.
My attempt
var filter = Builders<Attendance>.Filter.Eq(a => a.Id, currentAttn.Id) & Builders<Attendance>.Filter.ElemMatch(s => s.AllAttendances, Builders<TimeRecord>.Filter.Eq(n => n.FullName, userName));
var update = Builders<Attendance>.Update.Push(e => e.AllAttendances[-1].Pauses, pauses);
context.Attendances.FindOneAndUpdate(filter, update);
I followed this guide
Attendance Class
public class Attendance
{
[JsonConverter(typeof(ObjectIdConverter))]
public ObjectId Id { get; set; }
[BsonDateTimeOptions(Kind = DateTimeKind.Local)]
public DateTime AttnDate { get; set; }
public List<TimeRecord> AllAttendances { get; set; }
}
TimeRecord Class (AllAttendances)
public class TimeRecord
{
public string FullName { get; set; }
[BsonDateTimeOptions(Kind = DateTimeKind.Local)]
public DateTime Logged { get; set; }
public List<Pause> Pauses { get; set; }
}
Pause Class
public class Pause
{
[BsonDateTimeOptions(Kind = DateTimeKind.Local)]
public DateTime PauseStartedAt { get; set; }
[BsonDateTimeOptions(Kind = DateTimeKind.Local)]
public DateTime PauseEndedAt { get; set; }
}
You need to update your filter to
var filter = Builders<Attendance>.Filter.Eq(a => a.Id, id) &
Builders<Attendance>.Filter.ElemMatch(s => s.AllAttendances, x => x.FullName == userName);
The first argument of ElemMatch is the field, the second argument is the filter.
Looking at it from a different angle, I would suggest you don't use ObjectIDs in c#. I always define ObjectIds as strings in my models and use the Bson attribute decorators to define them as ObjectId's in the database
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }
Purely for the pain it causes trying to use ObjectIds in C#. Strings are much easier to handle. Your document in mongodb will still look the same, and you will not need to cast as object id's in your code at all:
_id : ObjectId("xxxxxxx")
This should help you get around the issue of the compiler not knowing how to do the conversion

Returning an object derived from an interface with generic list

My application reads in JSON from disk and deserialising using JSON.net; which is working fine.
My JSON is laid out like this:
{
"driver": {
"driverTag": "blah_blah",
"driverName": "Blah Blah",
"driverTransport": "serial-device"
},
"devices": [
{
"deviceName": "Dev1",
"deviceTag": "DEV1",
"deviceStartMode": "Auto"
},
{
"deviceName": "Dev2",
"deviceTag": "DEV2",
"deviceStartMode": "Auto"
}
]
}
Based on the "driverTransport" value, I deserialise to either a SerialDriverConfig, TelnetDriverConfig, SNMPDriverConfig... etc class.
As the "driver" properties will be the same for every driver, no matter the transport type, I have a "DriverConfigTemplate" class. The "devices" will differ from JSON file to JSON file and have specific properties for that transport type (i.e. a serial device will have properties like "serialPortName", "serialBaudRate" etc.)
I have a "DriverConfig" interface, where T is "DeviceConfig".
public interface DriverConfig<T> where T : DeviceConfig
{
DriverConfigTemplate driver { get; set; }
List<T> devices { get; set; }
}
My device config is as follows:
public class DeviceConfig : IDeviceConfig
{
public string deviceTag { get; set; }
public string deviceName { get; set; }
public string deviceStartMode { get; set; }
}
Now; the problem part. When I am deserialising, I check the transport type before hand and determine the class to use; i.e for a serial driver I will use the "SerialDriverConfig" class and deserialise using the "SerialDeviceConfig":
public class SerialDeviceConfig : DeviceConfig
{
public int serialComPort { get; set; }
public int serialBaudRate { get; set; }
public int serialDataBits { get; set; }
public string serialParity { get; set; }
public string serialStopBits { get; set; }
public string serialHandshake { get; set; }
public int serialReadTimeout { get; set; }
public int serialWriteTimeout { get; set; }
public bool serialRtsEnable { get; set; }
public bool serialDtrEnable { get; set; }
}
My "SerialDriverConfig" class looks like this:
public class SerialDriverConfig : DriverConfig<SerialDeviceConfig>
{
public DriverConfigTemplate driver { get; set; }
public List<SerialDeviceConfig> devices { get; set; }
}
Again, this is fine and the JSON.net deserialiser does its job perfectly.
I have a function that gets called when the JSON config file has been loaded and validated against its respective schema, then passed on to a "DeserialiseDriverConfig" function where I am trying to return the derived driver object; which is where I am stuck :(
private DriverConfig<DeviceConfig> DeserialiseDriverConfig(string _json, string _driverTransport)
{
switch (_driverTransport)
{
case "serial-device":
try
{
SerialDriverConfig _serialDriverConfig = JsonConvert.DeserializeObject<SerialDriverConfig>(_json);
if (_serialDriverConfig != null)
{
return _serialDriverConfig;
}
}
catch (Exception e)
{
//Blah blah blah
}
break;
}
return null;
}
I have been stuck on this one for a few days, have tried many things and this is where I have ended up. I am getting "Cannot implicitly convert type "SerialDriverConfig" to "DriverConfig". An explicit conversion exists (are you missing a cast?)" So I understand why this error is occurring, but cannot get around it.
Hope my code makes sense and someone can help me out here?
You can change your DriverConfig class to be non-generic
public interface DriverConfig
{
DriverConfigTemplate driver { get; set; }
List<DeviceConfig> devices { get; set; }
}
and instead of using derived classes (SerialDriverConfig etc.) you can set Json.net to deserialize to the correct DeviceConfig type based on either having a $type attribute in your JSON like this or using a custom JsonConverter similar to this
I'm not sure if this solution fits your need but if you create your method and SerialDriverConfig with using generic type T you can use your interface as a returning type. Can you try the code below;
Your Method:
private static DriverConfig<T> DeserialiseDriverConfig<T>(string _json, string _driverTransport)
{
switch (_driverTransport)
{
case "serial-device":
try
{
SerialDriverConfig<T> _serialDriverConfig = JsonConvert.DeserializeObject<SerialDriverConfig<T>>(_json);
if (_serialDriverConfig != null)
{
return _serialDriverConfig;
}
}
catch (Exception e)
{
//Blah blah blah
}
break;
}
return null;
}
SerialDriverConfig Class:
public class SerialDriverConfig<T> : DriverConfig<T>
{
public DriverConfigTemplate driver { get; set; }
public List<T> devices { get; set; }
}
Also you should consider changing DriverConfig<T> interface approach because if you leave it as-is you will have boxing issue. If you do not need you may remove where T : DeviceConfig from your interface or modify it according to your current circumstances.
Hope this helps, please let me know if this works for you

Categories

Resources