Short version
How to create API that can be shared between different parties that use different conventions for JSON properties in both places, request and response. In other words, if client #1 sends JSON request in snake case, it should receive JSON response in snake case. If client #2 sends camel case, they should receive camel case back in the response.
Long version with explanation and examples
1. snake_case { some_id: 1, some_name: "www" }
2. alllowercase { someid: 2, somename: "www" }
3. TitleCase { SomeId: 1, SomeName: "www" }
4. camelCase { someId: 1, someName: "www" }
Default model binder is case insensitive, and can handle options 2, 3, 4 naturally, but if input JSON contains "snake_case", default model binder will not extract it [FromUri] and will not map it to a model.
class SomeModel
{
int SomeId { get; set; }
string SomeName { get; set; }
}
A controller.
class SomeController()
{
[HttpGet]
public dynamic SomeGetAction([FromUri] SomeModel model) { ... }
[HttpPost]
public dynamic SomePostAction([FromBody] SomeModel model) { ... }
}
Approach #1: Attributes
In this case, we tell the controller to search for snake case properties in an incoming JSON. If the snake case attribute won't be found, it should try to extract properties without underscore.
class SomeModel
{
[JsonProperty(Name="some_id")]
int SomeId { get; set; }
[JsonProperty(Name="some_name")]
string SomeName { get; set; }
}
Approach #2: Get-Set wrappers
We can create both kinds of property in our model to cover all possible JSON variations.
class SomeModel
{
int some_id { get; set; }
string some_name { get; set; }
int SomeId { get { return some_id; } set { some_id = value; } }
string SomeName { get { return some_name; } set { some_name = value; } }
}
Approach #3: Custom model binder
In this case, create an intermediate layer that can parse request header and body and extract properties of any case.
class MyModelBinder<SomeModel>()
{
public bool BindModel(HttpActionContext actionCtx, ModelBindingContext modelCtx)
{
var queryGet = actionCtx
.Request
.GetQueryNameValuePairs()
.ToDictionary(o => o.Key, o => o.Value as object);
var queryPost = actionCtx
.Request
.Content
.ReadAsAsync<dynamic>
.Result
.ToObject<Dictionary<string, object>>);
modelContext.model.SomeId = // try to find property in the request
queryGet["some_id"] ??
queryGet["someid"] ??
queryGet["someId"] ??
queryGet["SomeId"];
return true;
}
}
class SomeController()
{
[HttpGet]
public dynamic SomeGetAction([MyModelBinder<typeof(SomeModel)>] SomeModel model) { ... }
[HttpPost]
public dynamic SomePostAction([MyModelBinder<typeof(SomeModel)>] SomeModel model) { ... }
}
Approach #4: Contract resolver
Approaches #1 and #2 seem too verbose and is not accepted by some team members. Approach #3 seems to cover all cases, but for some reason, it hides all model properties from Swagger, so we decided to take a look at a contract resolver that can be set up on the controller level and modify incoming and outgoing JSON before or after the action. We tried this approach https://stackoverflow.com/a/52766426/437393 and it works fine for the outgoing JSON in the response, but it doesn't transform incoming JSON in the request.
So, the alternate question is, how to make sure that the controller will receive JSON of a specific format using Contract Resolver, always convert JSON to snake case?
Related
I created a .NET Core API project as below. Everything works very well. But when I send a small JSON file, the null fields in the DTO are reflected in the database. So how can I update only the submitted fields?
When I send only the name field in the JSON object, it updates all the other fields, how can I do this in SaveChangesAsync in the DataContext?
When I send only the name field in the JSON object, it records all the fields as null. How can I prevent this? In other words, only the sent data should be updated, and the others should be recorded with their original values. Is there any way I can achieve this within the dbcontext object?
I am sending a JSON like here, but because the description field is empty inside the JSON object, it is changed to null in the database.
{
"id": 2,
"name": "test"
}
CompanyController, I am sending the JSON object via the body:
[HttpPut]
public async Task<IActionResult> Update([FromBody] CompanyUpdateDto updateCompany)
{
await _service.UpdateAsync(_mapper.Map<Company>(updateCompany));
return CreateActionResult(CustomResponseDto<CompanyUpdateDto>.Success(204));
}
I am sending my updatedDto object, sometimes name, and description fields, sometimes just the name field.
public class CompanyUpdateDto
{
public int Id { get; set; }
public string? Name { get; set; }
public string? Description { get; set; }
public DateTime? UpdatedDate { get; set; }
}
CompanyModel:
public class Company
{
public int Id { get; set; }
public string? Name { get; set; }
public string? Description { get; set; }
public DateTime? CreatedDate { get; set; }
public DateTime? UpdatedDate { get; set; }
}
DataContext:
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
foreach (var item in ChangeTracker.Entries())
{
if (item.Entity is BaseEntity entityReference)
{
switch (item.State)
{
case EntityState.Added:
{
entityReference.CreatedDate = DateTime.UtcNow;
break;
}
case EntityState.Modified:
{
Entry(entityReference).Property(x => x.CreatedDate).IsModified = false;
break;
}
}
}
}
return base.SaveChangesAsync(cancellationToken);
}
With AutoMapper, you can define a rule that only map from the source member to the destination member if the source member is not null via .Condition().
You may refer to the example in here.
CreateMap<CompanyUpdateDto, Company>()
.ForAllMembers(opt => opt.Condition((src, dest, value) => value != null));
Demo # .NET Fiddle
A concern is that you need to fetch the existing entity and map it with the received object to be updated as below:
[HttpPut]
public async Task<IActionResult> Update([FromBody] CompanyUpdateDto updateCompany)
{
// Get existing entity by id (Example)
var _company = await _service.GetAsync(updateCompany.Id);
// Map source to destination
_mapper.Map<CompanyUpdateDto, Company>(updateCompany, _company);
await _service.UpdateAsync(_company);
return CreateActionResult(CustomResponseDto<CompanyUpdateDto>.Success(204));
}
You can also ignore null values during serialization:
var company = new CompanyUpdateDto();
company.Description = "New description";
JsonSerializerOptions options = new()
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
var serialized = JsonSerializer.Serialize(company,options);
You will have to make design decisions here for your API update operation.
Get and Put the objects in full.
When retrieving an object, your Get operation must return the object in full detail. Then, when any fields change, the client will send the object back in full to the Put endpoint. In this way, you will keep the values for all fields.
However, in some cases, you only want to expose a subset of the fields and leave some of the fields untouched or updated by the system. In those cases, you will have to retrieve the object from the database by some identifier and then assign the fields from the incoming object.
Use JSON Patch
You will have a Patch endpoint for the resource. In the request body, you specify what operation for the object and which field has changed. When receiving the request, you will apply the changes based on the operation and fields in the request body.
The downside for the second option is that your client must follow the JSON Patch standards.
When I debug the code in VS, the cities list, which I am returning have 3 objects in it along with the properties. When I call this endpoint I am receiving a response of 3 list items of empty objects.
How to resolve this issue?
Model Class:
public class City
{
public string CityName;
public string AssociatedCities;
public string Province;
public int Status;
public City(string cityName, string associatedCities, string province, int status)
{
this.CityName = cityName;
this.AssociatedCities = associatedCities;
this.Province = province;
this.Status = status;
}
}
Endpoint:
[HttpGet]
[Route("cities")]
public ActionResult<IEnumerable<City>> GetCities()
{
return Ok(Cities);
}
This is how I am calling the endpoint
getCities() {
this.http.get<City[]>('/api/wizard/cities')
.subscribe(result => {
console.log(result);
this.cities = result;
}, error => console.error('Something went wrong : ' + error));
}
The response I get:
The response that is needed:
[
{
"SearchCity": "Toronto",
"AssociatedCities": "Ajax, Whitby, Toronto, Mississauga, Brampton",
"Province": "ON",
"Status": 1
},
{
"SearchCity": "Vancouver",
"AssociatedCities": "Vancouver, Vancouver City",
"Province": "BC",
"Status": 1
}
]
I have tried this already: Fresh ASP.NET Core API returns empty JSON objects
System.Text.Json currently does not support serialization/deserialization of fields and non-parameter-less, non-default constructors.
Your example model uses both fields and a non-default constructor. If you need to use a custom constructor for some reason, you would need to implement your own JsonConverter<T> to support that. This doc might be helpful for that:
https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-migrate-from-newtonsoft-how-to#deserialize-to-immutable-classes-and-structs
Only public properties with public getters/setters are supported along with the default, parameter-less constructor (what is referred to as Plain_old_CLR_object (POCO)). Note: If you are only serializing (i.e. writing), the setters generally don't have to be public.
Properties are different from fields (and contain getters/setters).
Here is the fix:
public class City
{
public string CityName { get; set; }
public string AssociatedCities { get; set; }
public string Province { get; set; }
public int Status { get; set; }
}
In my case, I just added this in my ConfigureServices method in Startup.cs (I am using Dot Net 5.0)
services.AddControllers().AddNewtonsoftJson();
Based on the fact that all your action does is return Cities, which presumably is a property or field defined on your controller, I'm going to take a shot in the dark and assume that you're setting that in another request and expecting it to still be there in this request. That's not how it works. The controller is instantiated and disposed with each request, so anything set to it during the lifetime of a request will not survive. As a result, Cities has nothing in this request, so you get an empty response.
If you need a list of cities in the action, then you should query those in that action. Also, for what it's worth, System.Text.Json does not currently support serializing fields, as others have mentioned in the comments, but you may still use JSON.NET instead, which does. See: https://learn.microsoft.com/en-us/aspnet/core/migration/22-to-30?view=aspnetcore-3.1&tabs=visual-studio#jsonnet-support
Currently I'm trying to create a web api based on asp.net core 2.0 and I'd like to create a nested route. In case of a put request it sends a part of the information in the route and another part in the body.
Requirements
The desired url to call would be
https://localhost/api/v1/master/42/details
If we'd like to create a new detail below our master 42 I would expect to send the data of the details in the body while the id of the master comes out of the route.
curl -X POST --header 'Content-Type: application/json' \
--header 'Accept: application/json' \
-d '{ \
"name": "My detail name", \
"description": "Just some kind of information" \
}' 'https://localhost/api/v1/master/42/details'
The outcoming response of the request would be
{
"name": "My detail name",
"description": "Just some kind of information",
"masterId": 42,
"id": 47
}
and a location url within the response header like
{
"location": "https://localhost/api/v1/master/42/details/47
}
Work done so far
To get this to work I created this controller:
[Produces("application/json")]
[Route("api/v1/master/{masterId:int}/details")]
public class MasterController : Controller
{
[HttpPost]
[Produces(typeof(DetailsResponse))]
public async Task<IActionResult> Post([FromBody, FromRoute]DetailCreateRequest request)
{
if(!ModelState.IsValid)
return BadRequest(ModelState);
var response = await Do.Process(request);
return CreatedAtAction(nameof(Get), new { id = response.Id }, response);
}
}
Which uses these classes:
public class DetailCreateRequest
{
public int MasterId { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}
public class DetailResponse
{
public int Id { get; set; }
public int MasterId { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}
The problem
So far most of the stuff works as expected. The only thing that really doesn't work is merging the MasterId from the route into the DetailCreateRequest that comes from the body.
First try: Use two attributes on the parameter
I tried to combine these two things by this action call:
public async Task<IActionResult> Post([FromBody, FromRoute]DetailCreateRequest request)
But the incoming object only had a MasterId of zero. If I change the order of the two attributes, then only the id from the route will be taken and all values within the body are ignored (so seems to be first attribute wins).
Second try: Use two different parameters in action
Another approach that I tried was this action call:
public async Task<IActionResult> Post([FromRoute]int masterId, [FromBody]DetailCreateRequest request)
In the first spot this looks okay, cause now I have both values within the controller action. But my big problem with this approach is the model validation. As you can see in the above code I check ModelState.IsValid which was filled through some checks from FluentValidation, but these checks can't be really done, cause the object wasn't build up correctly due to the missing master id.
(Not-working) Idea: Create own attribute with merge parameters
Tried to implement something like this:
public async Task<IActionResult> Post([FromMultiple(Merge.FromBody, Merge.FromRoute)]DetailCreateRequest request)
If we already would have something like this, that would be great. The order of the arguments within the attribute would give out the order in which the merge (and possible overwrites) would happen.
I already started with implementing this attribute and creating the skeleton for the needed IValueProvider and IValueProviderFactory. But it seems to be a quite lot of work. Especially finding all the nifty details to make this work seamlessly with the whole pipeline of asp.net core and other libraries I'm using (like swagger through swashbuckle).
So my question would be, if there already exists some mechanism within asp.net core to achieve such a merge or if anybody is aware about an already existing solution or about a good example on how to implement such a beast.
Solution so far: Custom ModelBinder
After getting the answer from Merchezatter I look into how to create a custom model binder and came up with this implementation:
public class MergeBodyAndValueProviderBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
var body = bindingContext.HttpContext.Request.Body;
var type = bindingContext.ModelMetadata.ModelType;
var instance = TryCreateInstanceFromBody(body, type, out bool instanceChanged);
var bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
var setters = type.GetProperties(bindingFlags).Where(property => property.CanWrite);
foreach (var setter in setters)
{
var result = bindingContext.ValueProvider.GetValue(setter.Name);
if (result != ValueProviderResult.None)
{
try
{
var value = Convert.ChangeType(result.FirstValue, setter.PropertyType);
setter.SetMethod.Invoke(instance, new[] { value });
instanceChanged = true;
}
catch
{ }
}
}
if (instanceChanged)
bindingContext.Result = ModelBindingResult.Success(instance);
return Task.CompletedTask;
}
private static object TryCreateInstanceFromBody(Stream body, Type type, out bool instanceChanged)
{
try
{
using (var reader = new StreamReader(body, Encoding.UTF8, false, 1024, true))
{
var data = reader.ReadToEnd();
var instance = JsonConvert.DeserializeObject(data, type);
instanceChanged = true;
return instance;
}
}
catch
{
instanceChanged = false;
return Activator.CreateInstance(type);
}
}
}
It tries to deserialize the body into the desired object type and afterwards tries to apply further values from the available value providers. To get this model binder to work I had to decorate the destination class with the ModelBinderAttribute and made the MasterId internal, so that swagger doesn't announce it and JsonConvert doesn't deserialize it:
[ModelBinder(BinderType = typeof(MergeBodyAndValueProviderBinder))]
public class DetailCreateRequest
{
internal int MasterId { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}
Within my controller the action method parameters are still containing the [FromBody] flag, cause it is used by swagger to announce on how the method can be called, but it never will be called, cause my model binder has a higher priority.
public async Task<IActionResult> Post([FromBody]DetailCreateRequest request)
So it is not perfect, but works good so far.
That is looks like a right choice:
[HttpPost]
[Produces(typeof(DetailsResponse))]
public async Task<IActionResult> Post([FromRoute]int masterId, [FromBody]DetailCreateRequest request) {
//...
}
But if you have some problems with domain model validation, create custom Dto object without master Id.
Otherwise you can use custom model binder, and then work with arguments from action and binding contexts.
I'm not sure if this works in Asp.Net-Core 2.0, but we use the following in 3.1 to have a single request object which gets its properties from multiple locations:
// Annotate the action parameter with all relevant attributes
public async Task<IActionResult> Post([FromBody][FromRoute][FromQuery]DetailCreateRequest request) { ... }
// Annotate each property separately, so the binder(s) don't overwrite
public class DetailCreateRequest
{
[FromRoute]
public int MasterId { get; set; }
[FromBody]
public string Name { get; set; }
[FromQuery]
public string Description { get; set; }
}
It works with .Net 6:
[HttpPost]
[Route("{id}")]
public async Task<ActionResult<CustomerResponse>> Post([FromRoute, FromBody] CustomerPostRequest request)
{
return Ok();
}
public class CustomerPostRequest
{
[FromRoute(Name = "id")]
public int Id { get; set; }
[FromBody]
public string Name { get; set; }
}
Set the your required "source" attributes on the single request object parameter, and inside this object add each property the relevant "source" attribute.
Make sure the FromBody is the last one (it didn't work when I switched them).
How to post a dynamic JSON property to a C# ASP.Net Core Web API using MongoDB?
It seems like one of the advantages of MongoDB is being able to store anything you need to in an Object type property but it doesn't seem clear how you can get the dynamic property info from the client ajax post through a C# Web API to MongoDB.
We want to allow an administrator to create an Event with Title and Start Date/Time but we also want to allow the user to add custom form fields using Reactive Forms for whatever they want such as t-shirt size or meal preference... Whatever the user may come up with in the future. Then when someone registers for the event, they post EventID and the custom fields to the Web API.
We can have an Event MongoDB collection with _id, event_id, reg_time, and form_fields where form_fields is an Object type where the dynamic data is stored.
So we want to POST variations of this JSON with custom FormsFields:
Variation 1:
{
"EventId": "595106234fccfc5fc88c40c2",
"RegTime":"2017-07-21T22:00:00Z",
"FormFields": {
"FirstName": "John",
"LastName": "Public",
"TShirtSize": "XL"
}
}
Variation 2:
{
"EventId": "d34f46234fccfc5fc88c40c2",
"RegTime":"2017-07-21T22:00:00Z",
"FormFields": {
"Email": "John.Public#email.com",
"MealPref": "Vegan"
}
}
I would like to have an EventController with Post action that takes a custom C# EventReg object that maps to the JSON above.
EventController:
[HttpPost]
public void Post([FromBody]EventReg value)
{
eventService.AddEventRegistration(value);
}
EventReg Class:
public class EventReg
{
public EventReg()
{
FormFields = new BsonDocument();
}
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string EventRegId { get; set; }
[BsonElement("EventId")]
[BsonRepresentation(BsonType.ObjectId)]
public string EventId { get; set; }
[BsonElement("reg_time")]
public DateTime RegTime
{
set; get;
}
[BsonElement("form_fields")]
public MongoDB.Bson.BsonDocument FormFields { get; set; }
}
EventService
public string AddEventRegistration(EventReg eventReg)
{
this.database.GetCollection<EventReg>("event_regs").InsertOne(eventReg);
return eventReg.EventRegId;
}
Right now, if I post to the controller, my EventReg is null because it must not know how to map my JSON FormFields properties to a BsonDocument.
What type can I use for FormFields?
Can I have the FormFields property be a BsonDocument and is there an easy way to map the Web API parameter to that?
Is there an example of how some custom serializer might work in this case?
We could maybe use a dynamic type and loop through the posted properties but that seems ugly. I have also seen the JToken solution from a post here but that looks ugly also.
If MongoDB is meant to be used dynamically like this, shouldn't there be a clean solution to pass dynamic data to MongoDB? Any ideas out there?
In ASP.NET Core 3.0+ Newtonsoft.Json is not the default JSON serializer anymore. Therefore I would use JsonElement:
[HttpPost("general")]
public IActionResult Post([FromBody] JsonElement elem)
{
var title = elem.GetProperty("title").GetString();
...
The JToken example works to get data in but upon retrieval it causes browsers and Postman to throw an error and show a warning indicating that content was read as a Document but it was in application/json format. I saw the FormFields property being returned as {{"TShirtSize":"XL"}} so maybe double braces was a problem during serialization.
I ended up using the .NET ExpandoObject in the System.Dynamic namespace. ExpandoObject is compatible with the MongoDB BsonDocument so the serialization is done automatically like you would expect. So no need for weird code to manually handle the properties like the JToken example in the question.
I still believe that a more strongly typed C# representation should be used if at all possible but if you must allow any JSON content to be sent to MongoDB through a Web API with a custom C# class as input, then the ExpandoObject should do the trick.
See how the FormFields property of EventReg class below is now ExpandoObject and there is no code to manually handle the property of the object being saved to MongoDB.
Here is the original problematic and overly complex JToken code to manually populate an object with standard type properties and a dynamic FormFields property:
[HttpPost]
public void Post([FromBody]JToken token)
{
if (token != null)
{
EventReg eventReg = new EventReg();
if (token.Type == Newtonsoft.Json.Linq.JTokenType.Object)
{
eventReg.RegTime = DateTime.Now;
foreach (var pair in token as Newtonsoft.Json.Linq.JObject)
{
if (pair.Key == "EventID")
{
eventReg.EventId = pair.Value.ToString();
}
else if (pair.Key == "UserEmail")
{
eventReg.UserEmail = pair.Value.ToString();
}
else
{
eventReg.FormFields.Add(new BsonElement(pair.Key.ToString(), pair.Value.ToString()));
}
}
}
//Add Registration:
eventService.AddEventRegistration(eventReg);
}
}
Using ExpandoObject removes the need for all of this code. See the final code below. The Web API controller is now 1 line instead of 30 lines of code. This code now can insert and return the JSON from the Question above without issue.
EventController:
[HttpPost]
public void Post([FromBody]EventReg value)
{
eventService.AddEventRegistration(value);
}
EventReg Class:
public class EventReg
{
public EventReg()
{
FormFields = new ExpandoObject();
}
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string EventRegId { get; set; }
[BsonElement("event_id")]
[BsonRepresentation(BsonType.ObjectId)]
public string EventId { get; set; }
[BsonElement("user_email")]
public string UserEmail { get; set; }
[BsonElement("reg_time")]
public DateTime RegTime{ get; set; }
[BsonElement("form_fields")]
public ExpandoObject FormFields { get; set; }
}
EventService:
public string AddEventRegistration(EventReg eventReg)
{
this.database.GetCollection<EventReg>("event_regs").InsertOne(eventReg);
return eventReg.EventRegId;
}
If you are not sure about the type of Json you are sending i.e. if you are dealing with dynamic json. then the below approach will work.
Api:
[HttpPost]
[Route("demoPath")]
public void DemoReportData([FromBody] JObject Jsondata)
Http client call from .net core app:
using (var httpClient = new HttpClient())
{
return await httpClient.PostAsJsonAsync(serviceUrl,
new demoClass()
{
id= "demoid",
name= "demo name",
place= "demo place"
};).ConfigureAwait(false);
}
You can define FormFields as a string and send data to the API in string format after converting JSON to string:
"{\"FirstName\":"John\",\"LastName\":\"Public\",\"TShirtSize\":\"XL\"}"
Then in your controller parse the string to BsonDocument
BsonDocument.Parse(FormFields);
I would use AutoMapper to automate the conversion between the dto and the document
I need to receive the next JSON in .NET
"currentData":
{
"Name": {"system": "wfdss", "canWrite": true },
"DiscoveryDateTime": { "system": "wfdss", "canWrite": true },
"Code": { "system": "code", "canWrite": false },
...
}
This elements are dynamics, it doesn't have default elements, so, how can I define a class doing that following next model:
public class currentData
{
//TODO
//<Data Element Name>: {
//data element system: <STRING of system>,
//the last system to update data element canWrite: <Boolean>
//true if requesting system may edit data element (based on ADS), otherwise false. }, ...
public List<Property> property { get; set; }
}
public class Property
{
public string system { get; set; }
public string canWrite { get; set; }
}
If you need to post dynamic structured Json to controller i have a bad news for you - you can't map it automattically in MVC. MVC model binding mechanism work only with stronly typed collecions - you must know structure.
One of the options that i can suggest you if use FormCollection and manually get values from it:
[HttpPost]
public JsonResult JsonAction(FormCollection collection)
{
string CurrentDataNameSystem = collection["currentData.Name.system"];
// and so on...
return Json(null);
}
Another option is to pass you dynamic json as string and then manually desirialize it:
[HttpPost]
public JsonResult JsonAction(string json)
{
//You probably want to try desirialize it to many different types you can wrap it with try catch
Newtonsoft.Json.JsonConvert.DeserializeObject<YourObjectType>(jsonString);
return Json(null);
}
Anyway my point is - you shouldn't mess with dynamic json unless you really need it in MVC.
I suggest you to creage object type that contain all the passible fields but make it all nullable so you can pass your Json and it will be mapped with Model binding MVC mechanism but some fields will be null.
I think the type format you are getting is an Object with a Dictionary.
So i think you need to Deserialize your Data into this.
public class ContainerObject
{
public Dictionary<String,Property> currentData { get; set; }
}