Tips on architecture for consuming/wrapping large webservice - c#

I am looking for good practices for implementing a smart architecture and way to handle integration against a system with many different wdsl webservices.
I have been hobby developing with C# for 2 years~, by that I am not always using the correct terminology, but I'll try to describe what I am looking for.
The main reason I'm posting this, is to get ideas of areas that I should read up on, design patterns to implement and how to manage API calls in a good way.
I'm integrating against a system that offers lots of different API's. Each API have 20-30 methods. Each API takes a number of parameters in XML format.
To give you an idea, I have taken the following API as an example, its the DeviceManager API and the Create method to create a device in the system.
The API takes two parameters, a string key and a XML parameter string.
Example
DeviceManager
public string Create(string sKey, string sXmlParameters)
**Parameters:**
Name: sKey Type: string
Description:
The API security key. A valid API security key is required for every API call.
Name: sXmlParameters Type: string
Description: Values needed to create a new device. See the Additional Information >section for XML format.
Return Value: Type: string
Description: Returns XML containing the status of the creation and if the status is
Constants.ExternalServiceReturnCodes.SUCCEEDED, the ID of the new device.
XMLParameters:
<PARAMETERS>
<NAME>string - up to 64 characters. Non-blank name of the device.
Must be unique within a gateway.
</NAME>
<DESCRIPTION>string - up to 1024 characters. The description of the new device.
</DESCRIPTION> (optional)
<TYPEID>string of DeviceType. The type of device.
</TYPEID>
<GATEWAYID>string - 32-character GUID. The ID of the gateway to associate with the
device. If this node is included, it must contain an ID of
a gateway that exists in the solution.
</GATEWAYID> (optional)
<INSTALLATIONDATETIME>
date time in UTC - greater than or equal to
1753-01-01 00:00:00.000 and less than or equal to
9999- 12-31 23:59:59.998. The date time that the device was installed.
</INSTALLATIONDATETIME> (optional - if not included, the installation
date time is set to the date time in UTC when the device is
created in the solution)
<SERIALNUMBER>string - up to 64 characters. The serial number of the device
</SERIALNUMBER>
<TIMEZONEID>string - time zone ID. The time zone ID of the device.
Call the TimeZoneManager.RetrieveList API to get a list of valid time zone IDs
</TIMEZONEID> (required for device type 'meter')
<HARDWAREVERSION>string - up to 32 characters. The hardware version of the device.
</HARDWAREVERSION> (optional)
<GEOGRAPHICCOORDINATES>
<LATITUDE>decimal - greater than or equal to -90 and less than or
equal to 90. The latitude geographical coordinate of the
device in decimal degrees. The value must be from the
WGS84 spatial reference system.
If more than 6 digits after the decimal point are included,
the value will be rounded to 6 digits.
</LATITUDE>
<LONGITUDE>
decimal - greater than or equal to -180 and less than or
equal to 180. The longitude geographical coordinate of the
device in decimal degrees. The value must be from the
WGS84 spatial reference system.
If more than 6 digits after the decimal point are included,
the value will be rounded to 6 digits.
</LONGITUDE>
</GEOGRAPHICCOORDINATES> (optional)
<METER>
<ID>string - 12 hexadecimal characters.</ID>
<TRANSFORMERID>string - up to 128 characters.</TRANSFORMERID>
<DOWNLIMIT>integer - greater than or equal to 0 and less than or
equal to 65535.
</DOWNLIMIT> (optional)
<METER>
</PARAMETERS>
Return API payload format:
<DEVICEID>string - 32-character GUID. The ID of the new device.</DEVICEID>
The API's always return the data in the following format:
<RETURNS>
<STATUS>
String from Constants.ExternalServiceReturnCodes class.
</STATUS>
<APIPAYLOAD>
Additional information from the API call. Node may be empty. For
APIs that return information, that information will be shown in
the Return section.
</APIPAYLOAD>
</RETURNS>
So in the example above, the payload would look something like:
<RETURNS>
<STATUS>
SUCCEEDED
</STATUS>
<APIPAYLOAD>
<DEVICEID>string - 32-character GUID. The ID of the new device.</DEVICEID>
</APIPAYLOAD>
</RETURNS>
As for now, I have been working on designing classes for all XMLparameters to be able to serialize and deserialize the parameters and payloads from the APIs, but it is a very time consuming job to define these. Example given below for the create API parameters and payload. (simple example using get set)
DeviceManager Create Parameters
[XmlRoot(ElementName = "PARAMETERS")]
public class Create
{
[XmlElement(ElementName = "NAME")]
public string Name { get; set; }
[XmlElement(ElementName = "DESCRIPTION")]
public string Description { get; set; }
[XmlElement(ElementName = "TYPEID")]
public string TypeId { get; set; }
[XmlElement(ElementName = "GATEWAYID")]
public string GatewayId { get; set; }
[XmlElement(ElementName = "INSTALLATIONDATETIME")]
public string InstallationDateTime { get; set; }
[XmlElement(ElementName = "SERIALNUMBER")]
public string SerialNumber { get; set; }
[XmlElement(ElementName = "TIMEZONEID")]
public string TimeZoneId { get; set; }
[XmlElement(ElementName = "HARDWAREVERSION")]
public string HardWareVersion { get; set; }
[XmlElement(ElementName = "GEOGRAPHICCOORDINATES")]
public CreateParametersGeoGraphicCoordinates GeographicCoordinates { get; set; }
[XmlElement(ElementName = "METER")]
public CreateMeter Meter { get; set; }
}
public class CreateMeter
{
[XmlElement(ElementName = "NEURONID")]
public string NeuronId { get; set; }
[XmlElement(ElementName = "TRANSFORMERID")]
public string TransformerId { get; set; }
[XmlElement(ElementName = "UTILITYID")]
public string UtilityId { get; set; }
[XmlElement(ElementName = "LONTALKKEY")]
public string LonTalkKey { get; set; }
[XmlElement(ElementName = "DOWNLIMIT")]
public string DownLimit { get; set; }
}
public class CreateParametersGeoGraphicCoordinates
{
[XmlElement(ElementName = "LATITUDE")]
public string Latitude { get; set; }
[XmlElement(ElementName = "LONGITUDE")]
public string Longitude { get; set; }
}
And for PayLoads I have the following generic class and DeviceManager.Create Payload specific class:
Create PayLoad
public class CreatePayLoad
{
[XmlElement(ElementName = "DEVICEID")]
public string DeviceId { get; set; }
}
PayLoad
[XmlRoot(ElementName = "RETURNS")]
public class PayLoad<T> where T : new()
{
public PayLoad()
{
ApiPayLoad = new T();
}
/// <summary>
/// Contains the payload from the command.
/// </summary>
[XmlElement(ElementName = "APIPAYLOAD")]
public T ApiPayLoad { get; set; }
/// <summary>
/// Status of the call
/// </summary>
[XmlElement(ElementName = "STATUS")]
public string Status { get; set; }
}
So in my code i can do the call in the following way:
Example use
//Create the parameters
var parameters = new Create
{
Description = "Description",
GatewayId = "GatewayId",
Name = "NameOfDevice",
GeographicCoordinates = new CreateParametersGeoGraphicCoordinates
{
Latitude = "Lat",
Longitude = "Long"
},
Meter = new CreateMeter
{
TransformerId = "ID",
DownLimit = "120"
}
};
//Serialize the parameters to xml
string sXmlParameters = Helper.SerializeToXml<Create>(parameters);
//API call
string apiPayLoad = DeviceManager.Create(apiKey, sXmlParameters);
//Deserialize payload
var payLoad = Helper.DeserializeFromXml<PayLoad<CreatePayLoad>>(apiPayLoad);
Can someone please contribute with ideas and better ways to manage this?
Please bear in mind that there are about 300 methods in the system, some with very complicated xml parameters where some properties are optional, some mandatory if property A, B or C are used and so on.
I have been looking at XSD.exe, but the generated code is not neat to the eye and it doesn't handle collections very well.
A friend also proposed T4 templates, where one could generate classes based on templates, but I'm not really finding any good examples out there.
I'm not sure if I have explained myself in a good way, If I'm being unclear - please let me know and I will try to elaborate.
Thank you,
amr-it

Stupid question time: so are you saying that none of the APIs are strongly typed? Meaning, all of the signatures are:
public string Create(string sKey, string sXmlParameters)
And the schema of the input "sXmlParameters" parameter is only defined by textual documentation? If that's the case, then I think you're overthinking things, and it will be just as efficient to write parsing methods for responses as it will be to create classes and use templating and casting techniques to convert responses into objects.
However, you mention XSD.exe, so maybe there are defined schemas for inputs and outputs? If that's the case, I would create those class definitions using the XSD tool, and not worry that they are not "neat to the eye" - you never look at those c# files, you just use the objects. In terms of their being weak with collections - if you're trying to filter/sort/inspect/traverse complex nested collection data, I would suggest you look at LINQ query expressions; they will let you quickly grab the objects you need. Of course, they don't help much with object instantiation, but I don't think there's a quick fix for that.

They are not strongly typed, all the API's take the sXmlParameters and are only defined by textual documentation. With XSD.exe, I've made examples of XML, then generated xsd's and from that I've generated some .cs files. However, it is a very time consuming task.
I was recommended to look into DSL fluent interfaces, that could help me in constructing the sXmlParameters.
var result = DeviceManager.Create(apiKey,
Parameters.
.Type(Parameters.Create)
.Name("Name of Device")
.Description("Description of Device")
.Coordinates("Lat","Long")
.Validate()
.asXML()
);
Using this, I can construct parameters from code, and have a base class validating the required fields before returning the parameters as XML.
However, mapping the PayLoads for each result will still be a time consuming task. I was thinking to use T4 templates to generate the classes based on XML or similair.

Related

Controller accepting different models

I want to accept different model type from body based on query param value.
Example:
[HttpGet]
[Route("GetSystemdetails")]
public string Getdeatils([FromBody] SystemDetail sysdetails, string type)
{
//some code here
string details = getdetails(sysdetails);
}
// abc model
public class abc
{
public int UserID { get; set; }
public string Email { get; set; }
}
//xyz model
public class xyz
{
public int xyzid { get; set; }
public string systemval { get; set; }
public string snum { get; set; }
}
type abc and xyz will have it's own model. So based on type I receive in query param I wanted to pick the model and proceed.
Sample url:
localhost/GetSystemdetails/type=abc
localhost/GetSystemdetails/type=xyz
I thought of creating a new model SystemDetail which holds these two models(xyz and abc) and based on system pick them.
I wanted to know what are possible ways to achieve this kind of requirements without creating multiple methods in controller(I don't want to change the format of the URL).
That's not something that's supported out of the box. Your linked solution is probably the closest you'll get to that.
ASP.NET Core is not supposed to take values of the parameters into account when routing, except for validation.
There are several possible ways to do so
Having multiple model objects
As in the link you provided, you can declare multiple model objects. The site has given the example of
public class PostUserGCM
{
public User User { get; set; }
public GCM GCM { get; set; }
}
but you can use your own examples.
Base model
Your models can inherit from some base model. If you only need a single model at a time and they share some similarities, then you could just create a base model which the other two are inheriting from, be agnostic at implementation time and your use cases will mainly differ on instantiation inside the controller, while some service methods could handle other differences.

Using a REST Api, how to include "any kind of json" in my typed request model?

I am using .NET Framework and ASP.NET Core to create a REST web Api.
This web api has a call that gets a request model to save data and some call that later retrieves the data.
Most of the data is structured information I need in the backend and it is saved into different fields and tables in the database. On retrieval it is loaded from those tables and returned.
This all works.
However, I now have a requirement where the caller wants to save and later retrieve arbitrary data (lets just say a random json) as one of those fields. I can save and load json from the database that is not a problem, my problem is to build the web api model for my request.
[HttpPost]
public IActionResult Save([FromBody] ApiCallRequestModel request)
{
// ...
}
public sealed class ApiCallRequestModel
{
// structured, well known information
public int? MaybeSomeNumber { get; set; }
[Required]
public string SomeText { get; set; }
[Required]
public SubModel SomeData { get; set; }
// one field of unknown json data
public ??? CustomData { get; set; }
}
I could think of dynamic or maybe even ExpandoObject or JObject to try and I might, but I would like a solution that works because it's best practice, not just because I tried and it didn't fail today with my simple tests.
If everything else fails, I could just make the field a string and tell the client to put serialized json into it. But that's a workaround I would see as a last resort if this question yields no answers.
It has proven to be extremly hard to google this topic, since all words I would use lead me to pages explaining Json serialization of my request model itself. I know how that works and it's not a problem. The mix of structured data and free json is what I cannot find out from a somewhat authorative source.
So what type would you use here, what is the best practice for receiving arbitrary json in one property of your model?
So to sum this up, as suggested I used a JToken from the Json.NET nuget package, since I already had that package in my project.
[HttpPost]
public IActionResult Save([FromBody] ApiCallRequestModel request)
{
// ...
}
public sealed class ApiCallRequestModel
{
// structured, well known information
public int? MaybeSomeNumber { get; set; }
[Required]
public string SomeText { get; set; }
[Required]
public SubModel SomeData { get; set; }
// one field of unknown json data
public JToken CustomData { get; set; }
}
Works like a charm.

Using Contains() in a Realm query

Let's say we have a realm results taken with
RealmDb.All<Entry>();
Then I want to do some search over those results using not yet supported techniques, like StartsWith on a function return or on a property which is not mapped in realm etc, so I get a subset
IEnumerable<Entry> subset = bgHaystack;
var results = subset.Where(entry => entry.Content.ToLower().StartsWith(needle));
To get somehow these as part of RealmResults, I extract the entry ids like this:
List<int> Ids = new List<int>();
foreach (Entry entry in entries)
{
Ids.Add(entry.Id);
}
return Ids;
and finally I want to return a subset of RealmResults (not IEnumerable) of only those Entries that contain those ids, how can I do that? IDE says the Contains method is not supported.
Can I use some kind of predicate or a comparer for that?
Entry is my model class
using System.ComponentModel.DataAnnotations.Schema;
using Realms;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System;
namespace Data.Models
{
[Table("entry")]
public class Entry : RealmObject
{
public class EntryType
{
public const byte Word = 1;
public const byte Phrase = 2;
public const byte Text = 3;
};
[Key]
[PrimaryKey]
[Column("entry_id")]
public int Id { get; set; }
[Column("user_id")]
public int UserId { get; set; }
[Column("source_id")]
public int SourceId { get; set; }
[Indexed]
[Column("type")]
public byte Type { get; set; }
[Column("rate")]
public int Rate { get; set; }
[Column("created_at")]
public string CreatedAt { get; set; }
[Column("updated_at")]
public string UpdatedAt { get; set; }
[NotMapped]
public Phrase Phrase { get; set; }
[NotMapped]
public Word Word { get; set; }
[NotMapped]
public Text Text { get; set; }
[NotMapped]
public IList<Translation> Translations { get; }
[NotMapped]
public string Content
{
get {
switch (Type)
{
case EntryType.Phrase:
return Phrase?.Content;
case EntryType.Word:
return Word?.Content;
case EntryType.Text:
return Text?.Content;
}
return "";
}
}
}
}
According to the documentation, Realm .NET supports LINQ, so that's promising. In your specific example, you indicate that StartsWith isn't supported, but I see that on the above page, specifically here.
Now, your example makes clear that Entry is a RealmObject, so it's not clear where you'd possibly get a RealmResult from (nor does their documentation on that page mention a RealmResult). Specifically, the home page indicates that you're really only going to ever work with Realm, RealmObject and Transaction, so I'm going to just assume that you meant that you'll need a resulting RealmObject per their examples.
The way you presently have your data object set up, you're rather stuck calling it like you are (though if I could make a recommendation to simplify it a little bit:
var entries = RealmDb.All<Entry>().ToList();
var results = entries.Where(entry => entry.Content.ToLower().StartsWith(needle));
var ids = results.Select(a => a.Id).ToList();
Now, your big issue with just combining the filter predicate in line 2 with the end of line 1: Content itself is marked with a [NotMapped] attribute. Per the documentation again:
As a general rule, you can only create predicates with conditions that
rely on data in Realm. Imagine a class
class Person : RealmObject
{
// Persisted properties
public string FirstName { get; set; }
public string LastName { get; set; }
// Non-persisted property
public string FullName => FirstName + " " + LastName;
}
Given this class, you can create queries with conditions that apply to
the FirstName and LastName properties but not to the FullName
property. Likewise, properties with the [Ignored] attribute cannot be
used.
Because you're using [NotMapped], I've got to believe that's going to behave similarly to [Ignored] and further, because it's just a computed value, it's not something that Realm is going to be able to process as part of the query - it simply doesn't know it because you didn't map it to the information Realm is storing. Rather, you'll have to compute the Content property when you've actually got the instances of your Entry objects to enumerate through.
Similarly, I expect you'll have issues pulling values from Phrase, Word and Text since they're also not mapped, and thus not stored in the record within Realm (unless you're populating those in code you didn't post before executing your Where filter).
As such, you might instead consider storing separate records as a PhraseEntry, WordEntry, and TextEntry so you can indeed perform exactly that filter and execute it on Realm. What if you instead used the following?
public class Entry : RealmObject
{
[Key]
[PrimaryKey]
[Column("entry_id")]
public int Id { get; set; }
[Column("user_id")]
public int UserId { get; set; }
[Column("source_id")]
public int SourceId { get; set; }
[Column("rate")]
public int Rate { get; set; }
[Column("created_at")]
public string CreatedAt { get; set; }
[Column("updated_at")]
public string UpdatedAt { get; set; }
[Column("content")]
public string Content { get; set; }
[NotMapped]
public IList<Translation> Translations { get; }
}
[Table("wordEntry")]
public class WordEntry : Entry
{
}
[Table("phraseEntry")]
public class PhraseEntry : Entry
{
}
[Table("textEntry")]
public class TextEntry : Entry
{
}
And now, you can offload the filtering to Realm:
var wordEntries = RealmDb.All<WordEntry>.Where(entry =>
entry.Content.StartsWith(needle, StringComparison.OrdinalIgnoreCase)).ToList();
var phraseEntries = RealmDb.All<PhraseEntry>.Where(entry => entry.Content.StartsWith(needle, StringComparison.OrdinalIgnoreCase)).ToList();
var textEntries = RealmDb.All<TextEntry>.Where(entry => entry.Content.StartsWith(needle, StringComparison.OrdinalIgnoreCase)).ToList();
var entries = new List<Entry>();
entries.AddRange(wordEntries);
entries.AddRange(phraseEntries);
entries.AddRange(textEntries);
var ids = entries.Select(entry => entry.Id).ToList();
It's not quite as brief as storing it all in one table, but I'm not immediately seeing any Realm documentation that indicates support for executing the same query against multiple tables simultaneously, so at least this would allow you to leave the filtering to the database and work against a more limited subset of values locally.
Finally, so we have all that and I missed your final question up top. You indicate that you want to return a subset of your entries based on some collection of ids you create. In the logic you provide, you're retrieving all the Id properties in all your results, so there's really no further subset to pull.
That said, let's assume you have a separate list of ids that for whatever complicated reason, you were only able to derive after retrieving the list of Entry types from above (themselves all PhraseEntry, WordEntry or TextEntry objects).
At this point, since you've already pulled all the values from Realm and have them locally, just execute another Where statement against them. Because a List implements IEnumerable, you can thus execute the LINQ locally without any of the Realm restrictions:
var myLimitedIdSet = new List<int>()
{
10, 15, 20, 25 //Really complicated logic to narrow these down locally
};
var resultingEntries = entries.Where(entry => myLimitedIdSet.Contains(entry.Id)).ToList();
And you're set. You'll have only those entries that match the IDs listed in myLimitedIdSet.
Edit to address comment
You see this error because of the detail provided at the top of this page in the documentation. Specifically (and adapting to your code):
The first statement gives you a new instance of Entry of a class that implements IQueryable... This is standard LINQ implementation - you get an object representing the query. The query doesn't do anything until you made a further call that needs to iterate or count the results.
Your error is then derived by taking the result from RealmDb.All<Entry>() and trying to cast it to an IEnumerable<Entry> to operate against it as though you have local data. Until you call ToList() onRealmDb.All` you simply have a LINQ representation of what the call will be, not the data itself. As such, when you further refine your results with a Where statement, you're actually adding that to a narrowed version of the IQueryable statement, which will also fail because you lack the appropriate mapping in the Realm dataset.
To skip the optimization I provided above, the following should resolve your issue here:
var bgHaystack = realm.All<Entry>().ToList(); //Now you have local data
var results = bgHaystack.Where(entry => entry.Content.ToLower().StartsWith(needle));
Unfortunately, given your provided code, I don't expect that you'll see any matches here unless needle is an empty string. Not only is your Content property not part of the Realm data and you thus cannot filter on it within Realm, but neither are your Phrase, Word or Text properties mapped either. As a result, you will only ever see an empty string when getting your Content value.
You can further refine the results variable above to yield only those instances with a provided ID as you see fit with normal LINQ (as again, you'll have pulled the data from Realm in the first line).
var limitedIds = new List<int>{10, 20, 30};
var resultsLimitedById = results.Select(a => limitedIds.Contains(a.Id)).ToList();
I've updated my examples above to reflect the use of ToList() in the appropriate places as well.

Issue with lambda expressions in c# data retrieval

i'm writing a system to track observation values from sensors (e.g. temperature, wind direction and speed) at different sites. I'm writing it in C# (within VS2015) using a code-first approach. Although i've a reasonable amount of programming experience, I'm relatively new to C# and the code-first approach.
I've defined my classes as below. I've built a REST api to accept observation reading through Post, which has driven my desire to have Sensor keyed by a string rather than an integer - Some sensors have their own unique identifier built in. Otherwise, i'm trying to follow the Microsoft Contoso university example (instructors - courses- enrolments).
What I am trying to achieve is a page for a specific site with a list of the sensors at the site, and their readings. Eventually this page will present the data in graphical form. But for now, i'm just after the raw data.
public class Site
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Sensor> Sensors { get; set; }
}
public class Sensor
{
[Key]
public string SensorName { get; set; }
public int SensorTypeId { get; set; }
public int SiteId { get; set; }
public ICollection<Observation> Observations { get; set; }
}
public class Observation
{
public int Id { get; set; }
public string SensorName { get; set; }
public float ObsValue { get; set; }
public DateTime ObsDateTime { get; set; }
}
and I've created a View Model for the page I'm going to use...
public class SiteDataViewModel
{
public Site Site { get; set; }
public IEnumerable<Sensor> Sensors { get; set;}
public IEnumerable<Observation> Observations { get; set; }
}
and then i try to join up the 3 classes into that View Model in the SiteController.cs...
public actionresult Details()
var viewModel.Site = _context.Sites
.Include(i => i.Sensors.select(c => c.Observations));
i used to get an error about "cannot convert lambda expression to type string", but then I included "using System.Data.Entity;" and the error has changed to two errors... on the 'include', I get "cannot resolve method 'include(lambda expression)'...". And on the 'select' i get "Icollection does not include a definition for select..."
There's probably all sorts of nastiness going on, but if someone could explain where the errors are (and more importantly why they are errors), then I'd be extremely grateful.
Simply you can you use like
viewModel.Site = _context.Sites
.Include("Sensors).Include("Sensors.Observations");
Hope this helps.
The way your ViewModel is setup, you're going to have 3 unrelated sets of data. Sites, sensors, and observations. Sites will have no inherent relation to sensors -- you'll have to manually match them on the foreign key. Realistically, your ViewModel should just be a list of Sites. You want to do
#Model.Sites[0].Sensors[0].Observations[0]
not something convoluted like
var site = #Model.Sites[0]; var sensor = #Model.Sensors.Where(s => SiteId == site.Id).Single(); etc...
Try doing
viewModel.Site = _context.Sites.Include("Sensors.Observations").ToList();
Eager-loading multiple levels of EF Relations can be accomplished in just one line.
One of the errors you reported receiving, by the way, is because you're using 'select' instead of 'Select'
And lastly, be aware that eager-loading like this can produce a huge amount of in-memory data. Consider splitting up your calls for each relation, such that you display a list of Sensors, and clicking, say, a dropdown will call an API that retrieves a list of Sites, etc. This is a bit more streamlined, and it prevents you from getting held up because your page is loading so much information.
Update
I've created a sample application for you that you can browse and look through. Data is populated in the Startup.Configure method, and retrieved in the About.cshtml.cs file and the About.cshtml page.. This produces this page, which is what you're looking for I believe.

WebAPI - Array of Objects not deserializing correctly on server side

In the client-side, I am using AngularJS and in the server-side I am using ASP.NET WebAPI.
I have two view models, ProductCriteriaViewModel and SimpleDisplayFieldViewModel:
public class ProductCriteriaViewModel
{
public int ID { get; set; }
public int? UserSearchID { get; set; }
public bool? Enabled { get; set; }
public SimpleDisplayFieldViewModel Property { get; set; }
public string Operator { get; set; }
public string CriteriaValue { get; set; }
}
public class SimpleDisplayFieldViewModel
{
public string Name { get; set; }
public string Value { get; set; }
public string PropertyType { get; set; }
}
In Angular, I submit a POST request to a WebAPI controller action with the following signature:
public IList<...> FindProducts(List<ProductCriteriaViewModel> criteriaVM, bool userFiltering)
{
...
}
In testing, I tried to send an array of Product Criterias, and checked Fiddler to see what the array looked like in the body of the POST request when it was being sent to the server. This is what the array looked like:
[
{"Enabled":true,
"Operator":"Less than",
"Property":
{"$id":"2",
"Name":"Copyright Year",
"Value":"Basic",
"PropertyType":null},
"CriteriaValue":"2013",
"IsNew":true},
{"Enabled":true,
"Operator":"Greater Than",
"Property":
{"$id":"2",
"Name":"Copyright Year",
"Value":"Basic",
"PropertyType":null},
"CriteriaValue":"1988",
"IsNew":true}
]
The above array has the correct values, however the result of deserialization on the server-side is incorrect. This is where it gets strange.
After the server deserializes the array and arrives in the controller action, the first element in criteriaVM is correct, all the values are set properly. However the second element is incorrect, CriteriaValue and Property are nulled out:
This issue only occurs whenever I choose the same search property for more than one criteria (i.e. Copyright < 2013 and Copyright > 1988). However, if I choose different properties (i.e. Copyright < 2013 and Price > 20), then all elements in the resulting criteriaVM are correctly initialized.
I do not understand what could be causing this issue. Why are only CriteriaValue and Property set to null in the second element of the List? Why does this issue only occur when I choose multiples of the same search properties?
Json.NET uses the keywords $id and $ref in order to preserve object references, so you are having troubles with your deserialization because your JSON has "$id" in the "Property" object. See this link for more information about object references.
In order to fix your deserialization issues, you can add the following line in the Register method of your WebApiConfig.cs class
config.Formatters.JsonFormatter.SerializerSettings.MetadataPropertyHandling = MetadataPropertyHandling.Ignore;
If your Web Api project does not include a WebApiConfig.cs class, simply add the configuration in your Global.asax:
GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings.MetadataPropertyHandling = MetadataPropertyHandling.Ignore;
Now your object in the web api method should look like this:

Categories