Elasticsearch NEST partial update of anonymous object with an empty nested object - elasticsearch

Trying to "reset" a nested object in a document but it will not set it back to empty.
I have a POCO:
public class StreetAddress
{
public int HouseNumber { get; set; }
public string Street { get; set; }
}
public class FullAddress
{
public string City{ get; set; }
public string State { get; set; }
public StreetAddress StreetAddress { get; set; }
public int Zip { get; set; }
public List<string> Codes { get; set; }
}
So currently if I created this new document for FullAddress with the StreetAddress already set the document looks like this when queried:
"_source": {
"city": "Los Angeles",
"state": "California",
"zip": 90019,
"streetAddress": {
"houseNumber": 1,
"street": "Apple Street"
},
"codes" : [
{ "la-601" }
]
}
Now I want to call NEST client update to reset the StreetAddress nested object:
StreetAddress localStreetAddress = new StreetAddress();
var partialAddress = new
{
City = "NewCity",
Zip = 11111,
StreetAddress = localStreetAddress
};
this._client.Update<ElasticsearchProject, object>(update => update
.Id(1)
.Doc(partialAddress)
);
The final outcome I was hoping for when I query would be after the above update call:
"_source": {
"city": "NewCity",
"state": "California",
"zip": 11111,
"streetAddress": {}
}
However, the partial update does two things that is undesired:
It only updates the City and Zip fields and leaves the StreetAddress
as it was before and doesn't clear it out to empty or null.
It clears out the Codes list to empty since the partial update doesn't include the list.
I know I can set the StreetAddress to null and add the JSON property to include null like this:
[JsonProperty(NullValueHandling = NullValueHandling.Include)]
public StreetAddress StreetAddress { get; set; }
but all that would result is that the document update would set it to null and not empty and I am not sure if that is a desired result for the document:
"_source": {
"city": "NewCity",
"state": "California",
"zip": 11111,
"streetAddress": null
}
Not sure if there was a way to do a partial update without going down the script path to set the nested object back to empty.

Nested objects are actually mapped as separate hidden documents within Elasticsearch and as you have found, NEST by default is configured not to send through null values. the end result is that the nested type is not affected by your partial update to the top-level properties.
One way of handling this is to simply re-index the document again without the StreetAddress. An update is essentially a delete and insert behind the scenes anyway.
As an example
void Main()
{
var settings = new ConnectionSettings(new Uri("http://localhost:9200"), "address");
var client = new ElasticClient(settings);
// create the index
client.CreateIndex("address", c => c
.AddMapping<FullAddress>(m => m
.MapFromAttributes()
)
);
// index our document
client.Index(new FullAddress
{
City = "Los Angeles",
State = "California",
Zip = 90019,
Codes = new List<string>
{
"la-601"
},
StreetAddress = new StreetAddress
{
HouseNumber = 1,
Street = "Apple Street"
}
}, c => c.Id(1));
// Now at some later point, we need to update it,
// so get the current document
var response = client.Get<FullAddress>(1);
// make the changes to the current document
var address = response.Source;
address.StreetAddress = null;
// and re-index
client.Index<FullAddress>(address, c => c.Id(1));
}
public class StreetAddress
{
public int HouseNumber { get; set; }
public string Street { get; set; }
}
public class FullAddress
{
public string City { get; set; }
public string State { get; set; }
public StreetAddress StreetAddress { get; set; }
public int Zip { get; set; }
public List<string> Codes { get; set; }
}
The end result is as expected
{
"_index": "address",
"_type": "fulladdress",
"_id": "1",
"_version": 2,
"found": true,
"_source": {
"city": "Los Angeles",
"state": "California",
"zip": 90019,
"codes": [
"la-601"
]
}
}
You may also want to use optimistic concurrency control when doing this too, to handle any changes between the get and index.

Related

Want to display the text of the button clicked in adaptive cards

I have created a Teams bot and I have a few submit action buttons.
On clicking these buttons I would want to let the user know that the button has been clicked.
Im using adaptive cards and submit action.
card.Actions.Add(new AdaptiveSubmitAction()
{
Title = item.Key,
Data = item.Value,
DataJson = "{\"Type\": \"Sort\"}"
});
On clicking the "sort button I want the bot postback "sort".
This is how I see in teams
Thanks in advance
It's possible to do this using the special "msteams" payload, and specifying "messageBack", or "imBack" as the message type, similar to this:
{
"type": "Action.Submit",
"title": "Click me for messageBack",
"data": {
"msteams": {
"type": "messageBack",
"displayText": "I clicked this button",
"text": "text to bots",
"value": "{\"bfKey\": \"bfVal\", \"conflictKey\": \"from value\"}"
}
}
}
You can read more about it here.
Because you're using C#, you'd need an actual class to represent this, so you could do something like:
public class msteams
{
public string type {get; set;} = "messageBack";
public string displayText {get; set;}
public string text {get; set;}
public string value {get; set;}
}
Then you would set it like this:
card.Actions.Add(new AdaptiveSubmitAction()
{
Title = item.Key,
Data = new msTeams() { displayText = "...", ... }
});
Obviously you could use attributes to change the name of the class, property names, etc. For a more simple approach, you can just the "imBack" option, which I've wrapped below with attributes as well:
public class AdaptiveCardImBackButton
{
[JsonProperty("type")]
public string Type { get; set; } = "imBack";
[JsonProperty("value")]
public string Value { get; set; }
}
Then I wrap that again, to get it to serialize the outer "msteams" attribute, as follows:
public class AdaptiveCardImBackButtonContainer
{
[JsonProperty("msteams")]
public AdaptiveCardImBackButton AdaptiveCardImBackButton { get; private set; }
public AdaptiveCardImBackButtonContainer(string value)
{
AdaptiveCardImBackButton = new AdaptiveCardImBackButton() { Value = value };
}
}
The final usage in your code is really simple:
card.Actions.Add(new AdaptiveSubmitAction()
{
Title = "sort",
Data = new AdaptiveCardImBackButtonContainer("sort")
});

C# Mongo db driver updating an existing document

I have an existing document in mongo db with certain properties. When i am trying to update this document with a document with the same id but having more properties it is not updating it. I am using mongo db driver version 2.4.4
Following is the existing document in my db
{
"_id" : "1234",
"AccountNumber" : "3453535345354",
"Name" : "new1",
"PhoneNumber" : "34534535353534543",
"ETag" : "6ba32e6e-3808-41f5-9b28-0ea882d9c629",
"Id" : "1234"
}
Now when i am trying to update/ replace this document with the following document it does not work:
{
"AccountNumber": "3453535345354",
"Name":"new1",
"PhoneNumber":"34534535353534543",
"TempProperty":"something new",
"ETag" : "6ba32e6e-3808-41f5-9b28-0ea882d9c629",
"Id" : "1234",
"_id" : "1234"
}
Following is the C# code for my upsert method:
public async Task UpsertDocument(string collectionId, string documentId, BsonDocument item)
{
var collection = database.GetCollection<BsonDocument>(collectionId);
var initialCorrelationId = item["ETag"].AsString;
item["ETag"] = Guid.NewGuid().ToString();
var builders = Builders<BsonDocument>.Filter;
var filter = builders.Eq(x => x["Id"], documentId) & builders.Eq(x => x["ETag"], initialCorrelationId);
var options = new FindOneAndReplaceOptions<BsonDocument, BsonDocument>
{
ReturnDocument = ReturnDocument.Before,
IsUpsert = true
};
try
{
item["Id"] = documentId;
item["_id"] = documentId;
await collection.FindOneAndReplaceAsync(filter, item, options);
}
catch (MongoCommandException ex) when (ex.Code == 11000)
{
throw new ConcurrencyException(
$"Error upserting item with id {documentId} and etag {item["Etag"]}{Environment.NewLine}{item.ToJson()}");
}
}
Any help would be much appreciated.
Assuming you have a class as follows:
public class Thing
{
public ObjectId Id { get; set; }
public string AccountNumber { get; set; }
public string Name { get; set; }
public string PhoneNumber { get; set; }
public string ETag { get; set; }
public string SecondId { get; set; }
}
The following code does what you want:
var context = new Context();
var tempProperty = "newValue";
var id = "1234"; // This should be unique
var builder = Builders<Thing>.Filter;
var filter = builder.Eq(x => x.SecondId, id);
var update = Builders<Thing>.Update
.Set("TempProperty", tempProperty);
context.ThingCollection.UpdateOne(filter, update, new UpdateOptions() {IsUpsert = true});
I just renamed Id to SecondId.

How can I change the mapped field name of an entry in a dictionary to be indexed by Elasticsearch via NEST?

I need to index some dynamic data (the fields are not known at compile time) and I also need to index a GeoPoint.
I want to use the NEST api and index into Elasticsearch. I have the following code to handle this:
public class MyRow
{
public string Id { get; set; }
[ElasticProperty(Name = "GeometryHotspot", Type = FieldType.GeoPoint)]
public Coordinate GeometryHotspot { get; set; }
public Dictionary<string, object> DynamicValues { get; set; }
}
This works, but when I setup the dynamic values in the array like this:
row.DynamicValues["MyKey"] = "Some value";
Then Elasticsearch generates a Lucene field named "DynamicValues.MyKey" for this value. Due to some legacy issues (existing queries in the field), I need the Lucene field to be "MyKey" - and not prefixed with "DynamicValues."
Does anybody know how this can be achieved? I have looked at various mapping approaches, but no luck. I cannot figure out how to specify the Lucene field name - only whether it should be analyzed etc. I
There is a round about way to achieve what you want. Add the Id and GeometryHotspot key-values to DynamicValues dictionary and then index DynamicValues instead of MyRow object. You can use the power of dynamic templates here to achieve the mapping you desire. I've written a small program to illustrate this.
public class Coordinate
{
public float lat { get; set; }
public float lon { get; set; }
}
public class MyRow
{
public string Id { get; set; }
public Coordinate GeometryHotspot { get; set; }
public Dictionary<string, object> DynamicValues { get; set; }
}
class Program
{
static void Main(string[] args)
{
var row = new MyRow
{
Id = "randomid",
GeometryHotspot = new Coordinate
{
lat = 1.23f,
lon = 4.56f
},
DynamicValues = new Dictionary<string, object>
{
["numberField"] = 1,
["stringField"] = "TWO",
["dateField"] = DateTime.UtcNow,
["realNumberField"] = 25.6,
["booleanField"] = true
}
};
// Add the concrete fields of MyRow class to its DynamicValues property
row.DynamicValues["id"] = row.Id;
row.DynamicValues["geometryHotspot"] = row.GeometryHotspot;
var client = new ElasticClient(new ConnectionSettings(new Uri("http://localhost:9200")));
// Create a mapping called "myRow" in index "myindex". Make sure "myindex" exists already.
client.Map<object>(d => d
.Index("myindex")
.Type("myRow")
.DynamicTemplates(dtd => dtd
.Add(dd => dd
.Name("geopoint")
.Match("geometryHotspot")
.Mapping(fm => fm
.GeoPoint(f => f
.IndexLatLon())))));
client.Index(row.DynamicValues, d => d // Notice how we index row.DynamicValues and not row
.Index("myindex")
.Type("myRow"));
}
}
After this, simply search for all documents in "myindex" index and "myRow" type to see that the dynamic fields are no longer prefixed by "DynamicFields." string.
Below is the output of the _search API.
{
"took": 3,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"failed": 0
},
"hits": {
"total": 1,
"max_score": 1,
"hits": [
{
"_index": "myindex",
"_type": "myRow",
"_id": "AVIqRf4rqbcnf7z9goAa",
"_score": 1,
"_source": {
"numberField": 1,
"stringField": "TWO",
"dateField": "2016-01-10T06:42:55.7535106Z",
"realNumberField": 25.6,
"booleanField": true,
"id": "randomid",
"geometryHotspot": {
"lat": 1.23,
"lon": 4.56
}
}
}
]
}
}
Hope this helps.

Getting an Enum to display on client side

I'm having hard time understanding how to convert an Enum value to it's corresponding name. My model is as follows:
public class CatalogRule
{
public int ID { get; set; }
[Display(Name = "Catalog"), Required]
public int CatalogID { get; set; }
[Display(Name = "Item Rule"), Required]
public ItemType ItemRule { get; set; }
public string Items { get; set; }
[Display(Name = "Price Rule"), Required]
public PriceType PriceRule { get; set; }
[Display(Name = "Value"), Column(TypeName = "MONEY")]
public decimal PriceValue { get; set; }
[Display(Name = "Exclusive?")]
public bool Exclude { get; set; }
}
public enum ItemType
{
Catalog,
Category,
Group,
Item
}
public enum PriceType
{
Catalog,
Price_A,
Price_B,
Price_C
}
A sample result from .net API:
[
{
$id: "1",
$type: "XYZ.CMgr.Models.CatalogRule, XYZ.CMgr",
ID: 1,
CatalogID: 501981,
ItemRule: 0,
Items: "198",
PriceRule: 1,
PriceValue: 0.5,
Exclude: false
},
{
$id: "2",
$type: "XYZ.CMgr.Models.CatalogRule, XYZ.CMgr",
ID: 2,
CatalogID: 501981,
ItemRule: 2,
Items: "9899",
PriceRule: 2,
PriceValue: 10.45,
Exclude: false
}
]
So in this example, I need to get Catalog for results[0].ItemRule & Price A for results[0].PriceRule. How can I accomplish this in BreezeJS??
This is easy to do in ASP.NET Web API, because it is an out-of-box feature in the default JSON serializer (Json.NET).
To see strings instead of enum numbers in JSON, just add an instance of StringEnumConverter to JSON serializer settings during app init:
var jsonFormatter = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
jsonFormatter.SerializerSettings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter());
UPDATE: Yep, you right, this is not help with Breeze.js. Ok, you can anyway do a little magic to make enums work like strings (while new version with fix is not released).
Create a custom ContextProvider which updates all integer enum values in metadata to strings. Here it is:
public class StringEnumEFContextProvider<T> : EFContextProvider<T>
where T : class, new()
{
protected override string BuildJsonMetadata()
{
XDocument xDoc;
if (Context is DbContext)
{
xDoc = GetCsdlFromDbContext(Context);
}
else
{
xDoc = GetCsdlFromObjectContext(Context);
}
var schemaNs = "http://schemas.microsoft.com/ado/2009/11/edm";
foreach (var enumType in xDoc.Descendants(XName.Get("EnumType", schemaNs)))
{
foreach (var member in enumType.Elements(XName.Get("Member", schemaNs)))
{
member.Attribute("Value").Value = member.Attribute("Name").Value;
}
}
return CsdlToJson(xDoc);
}
}
And use it instead of EFContextProvider in your Web API controllers:
private EFContextProvider<BreezeSampleContext> _contextProvider =
new StringEnumEFContextProvider<BreezeSampleContext>();
This works well for me with current Breeze.js version (1.1.3), although I haven't checked other scenarios, like validation...
UPDATE: To fix validation, change data type for enums in breeze.[min|debug].js, manually (DataType.fromEdmDataType function, dt = DataType.String; for enum) or replace default function during app init:
breeze.DataType.fromEdmDataType = function (typeName) {
var dt = null;
var parts = typeName.split(".");
if (parts.length > 1) {
var simpleName = parts[1];
if (simpleName === "image") {
// hack
dt = DataType.Byte;
} else if (parts.length == 2) {
dt = DataType.fromName(simpleName);
if (!dt) {
if (simpleName === "DateTimeOffset") {
dt = DataType.DateTime;
} else {
dt = DataType.Undefined;
}
}
} else {
// enum
dt = DataType.String; // THIS IS A FIX!
}
}
return dt;
};
Dirty, dirty hacks, I know... But that's the solution I found
There will be a new release out in the next few days where we "change" breeze's enum behavior ( i.e. break existing code with regards to enums). In the new release enums are serialized and queried by their .NET names instead of as integers. I will post back here when the new release is out.

Access MongoDB Rest service with C#

I'm a beginner with mongoDb, I would like to access MongoDb rest service, the data I retrieved is json type. My question is, how do you parse this data ? I don't find any MongoDb api which allows me to query it easily. So what would you do ?
Here's an example of the data, I queried the key "Name" which returned me on row thanks to this url:
http://localhost:28017/MyDatabase/MyCollection/?filter_Key=Name
{
"offset" : 0,
"rows": [
{ "_id" : { "$binary" : "fXvnbtlMhU24EWg9NiY5QQ==", "$type" : "03" }, "Key" : "Name", "Value" : "John Smith" }
],
"total_rows" : 1 ,
"query" : { "Key" : "Name" } ,
"millis" : 0
}
And I would like to retrieve the Value "John Smith"
Thank's
[EDIT]
I've managed to get {"Value": "John Smith"} out of my json. oh!! See this ugly code:
var urlToFetch = "http://`localhost`:28017/MyDatabase/MyCollection/?filter_Key=Name";
var jsonData = GetDataFrom(urlToFetch);
var value = JsonConvert.DeserializeObject(jsonData);
foreach (var key in ((JObject)value)["rows"].Values())
{
key.Parent.Last;
}
It's not perfect, I still don't get my John Smith But there're must be a better way without manually parsing, aren't there ?
Here the solution guys:
public class yourclass
{
public void transformJsonToObject()
{
JsonSerializer serializer = new JsonSerializer();
var value = JsonConvert.DeserializeObject<ResultViewModel>(jsonData);
}
}
public class ResultViewModel
{
[JsonProperty("offset")]
public int Offset;
[JsonProperty("rows")]
public TestViewModel[] Rows;
}
public class TestViewModel
{
[JsonProperty("_id")]
public TestArray Id { get; set; }
public string Key { get; set; }
public string Value { get; set; }
}
public class TestArray
{
[JsonProperty("$binary")]
public string Binary { get; set; }
[JsonProperty("$type")]
public string Type { get; set; }
}

Resources