How to stop .net core 6 webapp with Odata query from inlining dictionary? - odata

I have a rather simple(for now) Odata controller that does not work in the way I expect.
Backend store is a Mongo database, but I don't think that is relevant.
Model class:
public class EventModel
{
[Key]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }
public string EventName { get; set; }
public DateTime EventTime { get; set; }
// Fields omitted to shorten sample
public IDictionary<string,object?> Data { get; set; }
}
OData controller is added like so:
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Repository.Model.EventModel>("Events");
services.AddControllers()
.AddOData(options => options
.Select()
.Filter()
.OrderBy()
.Expand()
.Count()
.SetMaxTop(null)
.AddRouteComponents("eventservice/odata", builder.GetEdmModel()));
And this is the controller:
public class EventODataController : ODataController
{
private readonly IEventRepository _repository;
public EventODataController(IEventRepository repository)
{
_repository = repository;
}
[HttpGet("eventservice/odata/Events")]
[EnableQuery]
public ActionResult Get()
{
var data = _repository.GetAsQueryable();
return Ok(data);
}
Expected result when doing a HTTP get to eventservice/odata/Events$top=100 should be along the lines of this:
{
"#odata.context": "http://127.0.0.1:7125/eventservice/odata/$metadata#Events",
"#odata.count": 1080302,
"value": [
{
"Id": "63dbbcc9920829279f559025",
"EventName": "Asset",
"EventTime": "2022-09-27T09:14:15.398+02:00",
// Omitted field data here
"Data": {
"Data1":"Foo",
"Data2":"Bar",
"SomeMore":4
}
}
But it turns out that OData somehow flattens/inlines the dictionary, so the result is this:
"#odata.context": "http://127.0.0.1:7125/eventservice/odata/$metadata#Events",
"#odata.count": 1080302,
"value": [
{
"Id": "63dbbcc9920829279f559025",
"EventName": "Asset",
"EventTime": "2022-09-27T09:14:15.398+02:00",
"AssetId#odata.type": "#Int64",
"AssetId": 1258,
"UniqueCode": "9404120007",
"DbId#odata.type": "#Int64",
"DbId": 1038118,
"SomeData": "ABC",
"MoreData": "108",
"AreaName": "Area51,
...
},
Simple question: How do I stop OData from behaving like this, and do what I expect?

Related

MS Graph Create open extension call with Json object

I am trying to create events using MS Graph. We are using json objects with the call as in this example
using (HttpClient httpClient = new HttpClient())
{
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", msBearerToken);
var callJson = new
{
Subject = EventSummery.Title,
Body = new
{
ContentType = BodyType.Html.ToString(),
Content = EventSummery.Description
},
Start = new
{
DateTime = EventSummery.StartDateUTC.ToString("yyyy-MM-ddTHH:mm:ss"),
TimeZone = "GMT Standard Time"
},
End = new
{
DateTime = EventSummery.EndDateUTC.ToString("yyyy-MM-ddTHH:mm:ss"),
TimeZone = "GMT Standard Time"
}
};
I need to add an open extension however the documentation I need to add this attribute
"extensions": [
{
"#odata.type": "microsoft.graph.openTypeExtension",
"extensionName": "Com.Contoso.Referral",
"companyName": "Wingtip Toys",
"expirationDate": "2015-12-30T11:00:00.000Z",
"dealValue": 10000
}]
however #odata.type throws an error if I put it in this form:
Extensions = new
{
"#odata.type": "microsoft.graph.openTypeExtension",
"extensionName": "Com.Contoso.Referral",
"companyName": "Wingtip Toys",
"expirationDate": "2015-12-30T11:00:00.000Z",
"dealValue": 10000
}
What am I missing how can I make this call successfully?
I'm afraid that you cannot create anonymous object with property that has a special character at the beginning.
Only way is to create a class with properties that have JsonPropertyName attribute and then use json serializer.
public class OpenTypeExtension
{
[JsonPropertyName("#odata.type")]
public string ODataType { get; set; }
[JsonPropertyName("extensionName")]
public string ExtensionName { get; set; }
[JsonPropertyName("companyName")]
public string CompanyName { get; set; }
[JsonPropertyName("expirationDate")]
public string ExpirationDate{ get; set; }
[JsonPropertyName("dealValue")]
public string DealValue{ get; set; }
public OpenTypeExtension()
{
ODataType = "microsoft.graph.openTypeExtension";
}
}

.Net Core 3.0: Apply AddJsonOptions only to a specifc controller

I have two controller FooController and BooController (the last is for a backward compatibility), I want only the FooController will return its model with upper camel case notation ("UpperCamelCase").
For example:
public class MyData
{
public string Key {get;set;}
public string Value {get;set;}
}
public class BooController : ControllerBase
{
public ActionResult<MyData> GetData() { ... }
}
public class FooController : ControllerBase
{
public ActionResult<MyData> GetData() { ... }
}
The desired GET output:
GET {{domain}}/api/Boo/getData
[
{
"key": 1,
"value": "val"
}
]
GET {{domain}}/api/Foo/getData
[
{
"Key": 1,
"Value": "val"
}
]
If I'm using the AddJsonOptions extension with option.JsonSerializerOptions.PropertyNamingPolicy = null like:
services.AddMvc()
.AddJsonOptions(option =>
{
option.JsonSerializerOptions.PropertyNamingPolicy = null;
});
both BooController and FooController returns the data with upper camel case notation.
How to make only the FooController to return the data in upper camel case notation format?
Add [JsonProperty("name")] in the response class property.
stackoverflow.com/a/34071205/5952008
Solved (although kind of bad performance and a hacky solution - hoping for someone to suggest a better solution):
Inside the legacy BooController I'm serializing the response just before returning it using a custom formater of JsonSerializerSettings:
public class BooController : ControllerBase
{
public ActionResult<MyData> GetData()
{
var formatter = JsonConvert.DefaultSettings = () => new JsonSerializerSettings
{
Formatting = Formatting.Indented,
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
return Ok(JsonConvert.SerializeObject(response, Formatting.Indented, formatter()));
}
}
Result in:
GET {{domain}}/api/Boo/getData
[
{
"key": 1,
"value": "val"
}
]

DocumentDB LINQ Query - Select method not supported

The Application:
.Net Standard Class Library (Containing a series of Repositories)
Latest version https://www.nuget.org/packages/Microsoft.Azure.DocumentDB.Core
Azure Cosmos DB Emulator
Sample Document Structure:
{
"ProfileName": "User Profile 123",
"Country": "UK",
"Tags": [{
"Id": "686e4c9c-f1ab-40ce-8472-cc5d63597263",
"Name": "Tag 1"
},
{
"Id": "caa2c2a0-cc5b-42e3-9943-dcda776bdc20",
"Name": "Tag 2"
}],
"Topics": [{
"Id": "baa2c2a0-cc5b-42e3-9943-dcda776bdc20",
"Name": "Topic A"
},
{
"Id": "aaa2c2a0-cc5b-42e3-9943-dcda776bdc30",
"Name": "Topic B"
},
{
"Id": "eaa2c2a0-cc5b-42e3-9943-dcda776bdc40",
"Name": "Topic C"
}]
}
The Problem:
The issue I have is that any LINQ query I execute that contains a .Select, returns an error stating that the Select method is not supported.
What I Need?
I want to be able to use a LINQ Expression to return all documents WHERE:
Country = UK
Tags contains a specific GUID
Topics contain a specific GUID
What I Need? I want to be able to use a LINQ Expression to return all documents
In your case, the Tags and Topic are object array, if you use the following linq code it will get null.
var q = from d in client.CreateDocumentQuery<Profile>(
UriFactory.CreateDocumentCollectionUri("database", "coll"))
where d.Country == "UK" && d.Tags.Contains(new Tag
{
Id = "686e4c9c-f1ab-40ce-8472-cc5d63597264"
}) && d.Topics.Contains(new Topic
{
Id = "baa2c2a0-cc5b-42e3-9943-dcda776bdc22"
})
select d;
I also try to override IEqualityComparer for Tag and Topic
public class CompareTag: IEqualityComparer<Tag>
{
public bool Equals(Tag x, Tag y)
{
return x != null && x.Id.Equals(y?.Id);
}
public int GetHashCode(Tag obj)
{
throw new NotImplementedException();
}
}
And try it again then get Contains is not supported by Azure documentDb SDK
var q = from d in client.CreateDocumentQuery<Profile>(
UriFactory.CreateDocumentCollectionUri("database", "coll"))
where d.Country == "UK" && d.Tags.Contains(new Tag
{
Id = "686e4c9c-f1ab-40ce-8472-cc5d63597264"
},new CompareTag()) && d.Topics.Contains(new Topic
{
Id = "baa2c2a0-cc5b-42e3-9943-dcda776bdc22"
}, new CompareTopic())
select d;
My workaround is that we could use the SQL query directly. It works correctly on my side. We also could test it on the Azure portal.
SELECT * FROM root WHERE (ARRAY_CONTAINS(root.Tags, {"Id": "686e4c9c-f1ab-40ce-8472-cc5d63597263"}, true) And ARRAY_CONTAINS(root.Topics, {"Id": "baa2c2a0-cc5b-42e3-9943-dcda776bdc20"}, true) And root.Country = 'UK' )
Query with SDK
FeedOptions queryOptions = new FeedOptions { MaxItemCount = -1 };
var endpointUrl = "https://cosmosaccountName.documents.azure.com:443/";
var primaryKey = "VNMIT4ydeC.....";
var client = new DocumentClient(new Uri(endpointUrl), primaryKey);
var sql = "SELECT * FROM root WHERE (ARRAY_CONTAINS(root.Tags, {\"Id\":\"686e4c9c-f1ab-40ce-8472-cc5d63597263\"},true) AND ARRAY_CONTAINS(root.Topics, {\"Id\":\"baa2c2a0-cc5b-42e3-9943-dcda776bdc20\"},true) AND root.Country = \"UK\")";
var profileQuery = client.CreateDocumentQuery<Profile>(
UriFactory.CreateDocumentCollectionUri("dbname", "collectionName"),sql, queryOptions).AsDocumentQuery();
var profileList = new List<Profile>();
while (profileQuery.HasMoreResults)
{
profileList.AddRange(profileQuery.ExecuteNextAsync<Profile>().Result);
}
Profile.cs file
public class Profile
{
[JsonProperty(PropertyName = "id")]
public string Id { get; set; }
public string Country { get; set; }
public string ProfileName { get; set; }
public Tag[] Tags{ get; set; }
public Topic[] Topics { get; set; }
}
Topic.cs
public class Topic
{
public string Id { get; set; }
public string Name { get; set; }
}
Tag.cs
public class Tag
{
public string Id { get; set; }
public string Name { get; set; }
}

In MVC can I return the json data to the view which formate like a two-dimensional array?

In the view I want to get the json data like this:
[{"name":"NewWork",
"data":[{"\/Date(1398787200000)\/",196},
{"\/Date(1398009600000)\/",62},
{"\/Date(1397836800000)\/",65}]
},
{"name":"BeiJing",
"data":[{"\/Date(1398787200000)\/",106},
{"\/Date(1398700800000)\/",100},
{"\/Date(1398441600000)\/",61},
{"\/Date(1398355200000)\/",86}]
}]
So in the controller I define the class like these,and return List<ViewModelCityData> but the return data format is not what I want.How can I change the controller ViewModelCityData?And the other question,in the view my json data need to be order by X,if I sort them in the controller,why do they not order in the view?I have to sort them again in the view.
public class ViewModelCityData
{
public string name { get; set; }
public List<Point> data { get; set; }
}
public class Point
{
public DateTime X { get; set; }
public int Y { get; set; }
}
[{"name":"NewWork",
"data":[{"X":"\/Date(1398787200000)\/","Y":196},
{"X":"\/Date(1398009600000)\/","Y":62},
{"X":"\/Date(1397836800000)\/","Y":65}]
},
{"name":"BeiJing",
"data":[{"X":"\/Date(1398787200000)\/","Y":106},
{"X":"\/Date(1398700800000)\/","Y":100},
{"X":"\/Date(1398441600000)\/","Y":61},
{"X":"\/Date(1398355200000)\/","Y":86}]}]
Solution 1
If you can live with {Key:Value} format than you can accomplish this by implementing a custom converter.
public class PointsConverter : JsonConverter
{
private DateTime _epoch = new DateTime(1970, 1, 1);
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var point = (Point) value;
writer.WriteStartObject();
writer.WritePropertyName(string.Format("/Date({0})/", GetMilliseconds(point.X)));
writer.WriteRawValue(point.Y.ToString());
writer.WriteEnd();
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue,
JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof (Point);
}
private double GetMilliseconds(DateTime date)
{
var timeSpan = new TimeSpan(date.ToUniversalTime().Ticks - _epoch.Ticks);
return timeSpan.TotalMilliseconds;
}
}
and pass it to converter:
var serializeObject = JsonConvert.SerializeObject(models, Formatting.Indented, new PointsConverter());
Result:
[{"name": "Tiraspol",
"data": [{"/Date(1401188096166,27)/": 10},
{"/Date(1401191696166,27)/": 20}]
},
{"name": "Limassol",
"data": [{"/Date(1401195296166,27)/": 10},
{"/Date(1401198896166,27)/": 20}]
}
]
Solution 2
if you can live with [X,Y] format (instead of {X,Y}), than you can write them as an array. For this scenario use next code in converter:
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var point = (Point) value;
writer.WriteStartArray();
writer.WriteValue(string.Format("/Date({0})/", GetMilliseconds(point.X)));
writer.WriteValue(point.Y.ToString());
writer.WriteEnd();
}
Result:
[{"name": "Tiraspol",
"data": [["/Date(1401188096166,27)/": 10],
["/Date(1401191696166,27)/": 20]]
},
{"name": "Limassol",
"data": [["/Date(1401195296166,27)/": 10},
["/Date(1401198896166,27)/": 20]]
}
]
Solution 3
Pass X,Y as elements of array.
var jsonReady = models.Select(x => new
{
x.name,
data = x.data.Select(point => new object[] {point.X, point.Y}).ToArray()
});
var serializeObject = JsonConvert.SerializeObject(jsonReady, Formatting.Indented, new JavaScriptDateTimeConverter());
You will get the same result as in solution 2. Not pretty nice solution but you got the concept.
Instead of this:
public List<Point> data { get; set; }
Try this:
public Dictionary<DateTime, int> data { get; set; }
And the other question,in the view my json data need to be order by X,if I sort them in the controller,why do they not order in the view?I have to sort them again in the view.
You're not showing the sort code you use, but I find it likely that you're not actually changing the list. People often do this:
list.OrderBy(e => e.X);
... when they should do this:
list = list.OrderBy(e => e.X);
If Dictionary<DateTime, int> do not work,try to use int[][]
Instead of this:
public List<Point> data { get; set; }
Try this:
public int[][] data { get; set; }

A circular reference was detected while serializing an object of type?

DB MetersTree TABLE
id text parentId state
0 root 0 open
1 level 1 1 open
2 level 1 1 open
...
CONTROLLER
public ActionResult GetDemoTree()
{
OsosPlus2DbEntities entity = new OsosPlus2DbEntities();
MetersTree meterTree = entity.MetersTree.FirstOrDefault();
return Json(meterTree, JsonRequestBehavior.AllowGet);
}
DATA FORMAT THAT SHOULD BE (for example)
[{
"id": 1,
"text": "Node 1",
"state": "closed",
"children": [{
"id": 11,
"text": "Node 11"
},{
"id": 12,
"text": "Node 12"
}]
},{
"id": 2,
"text": "Node 2",
"state": "closed"
}]
How can I create tree Json Data? If I write MetersTree with its relationships I get the error that is defined in the title.
You need to break the circular reference that is being picked up because of the navigational property in your EF class.
You can map the results into an anonymous type like this, although this is untested:
public ActionResult GetDemoTree()
{
OsosPlus2DbEntities entity = new OsosPlus2DbEntities();
MetersTree meterTree = entity.MetersTree.FirstOrDefault();
var result = from x in meterTree
select new
{
x.id,
x.text,
x.state,
children = x.children.Select({
c => new {
c.id,
c.text
})
};
return Json(result, JsonRequestBehavior.AllowGet);
}
I solved it like this:
VIEW MODEL
public class MetersTreeViewModel
{
public int id { get; set; }
public string text { get; set; }
public string state { get; set; }
public bool #checked { get; set; }
public string attributes { get; set; }
public List<MetersTreeViewModel> children { get; set; }
}
CONTROLLER
public ActionResult GetMetersTree()
{
MetersTree meterTreeFromDb = entity.MetersTree.SingleOrDefault(x => x.sno == 5); //in my db this is the root.
List<MetersTreeViewModel> metersTreeToView = buildTree(meterTreeFromDb.Children).ToList();
return Json(metersTreeToView, JsonRequestBehavior.AllowGet);
}
BuildTree Method
private List<MetersTreeViewModel> BuildTree(IEnumerable<MetersTree> treeFromDb)
{
List<MetersTreeViewModel> metersTreeNodes = new List<MetersTreeViewModel>();
foreach (var node in treeFromDb)
{
if (node.Children.Any())
{
metersTreeNodes.Add(new MetersTreeViewModel
{
id = node.sno,
text = node.Text,
state = node.Text,
children = BuildTree(node.Children)
});
}
else {
metersTreeNodes.Add(new MetersTreeViewModel
{
id = node.sno,
text = node.Text,
state = node.Text
});
}
}
return metersTreeNodes;
}
Thanks to all who are interested in ...

Resources