I am interested in adding support for partial updates in my ASP.NET Core WebAPI where I only update the properties on a resource that the caller provided, leaving excluded properties unchanged.
For context, imagine I have a resource that can be described as follows:
GET /users/1
{
title: "Mister",
firstName: "Frederick",
middleName: "McFeely",
lastName: "Rodgers"
}
If I wanted to allow consumers to change the value stored in the firstName property from "Frederick" to "Fred" in isolation, I should be able to expose a PATCH endpoint that supports the JSON Merge Patch Content-Type, like so:
PATCH /users/1
Content-Type: application/merge-patch+json
{
firstName: "Fred"
}
However, I see no easy way for me to know that firstName is the only property being updated. For example, if I were to make a controller that accepted PATCH verbs, it could be scaffolded like this:
[Route("users")]
public class UsersController : Controller {
[HttpPatch("{userId:int}")]
public User Patch([FromRoute] int userId, [FromBody] User user) {
// How do I know which properties were set on User at this point?
}
}
public class User {
public String Title { get; set; }
public String FirstName { get; set; }
public String MiddleName { get; set; }
public String LastName { get; set; }
}
But I don't see how I can extract which properties' had keys defined on the JSON object before it was hydrated as a User and passed to my controller. I cannot assume a value of null to mean a property was excluded as the caller could be explicitly setting an optional property to null.
Edit
I am aware of the Microsoft.AspNetCore.JsonPatch library. This, unfortunately, expects the caller to use the "[description of changes]" to define a PATCH as described in RFC 5789, which I find unintuitive and verbose. I am referring to the "JSON Merge Patch" defined in RFC 7396.
I found a library that works: https://github.com/Morcatko/Morcatko.AspNetCore.JsonMergePatch
[HttpPatch]
[Consumes(JsonMergePatchDocument.ContentType)]
public void Patch([FromBody] JsonMergePatchDocument<Model> patch)
{
...
patch.ApplyTo(backendModel);
...
}
Or use patch.JsonPatchDocument.Operations to walk through patch request fields manually.
for simple types, I found a very simple solution using Newtonsoft.Json merge of JObjects:
public static T Patched<T>(T source, JObject patch) where T : class
{
var sourceObject = JObject.FromObject(source);
sourceObject.Merge(patch, new JsonMergeSettings() {MergeArrayHandling = MergeArrayHandling.Union});
return sourceObject.ToObject<T>();
}
public static T Patched<T>(T source, string patchCode) where T : class
{
return Patched<T>(source, JObject.Parse(patchCode));
}
Hope this helps someone searching for this topic and looking for a simple solution without external packages.
It appears like, for merge patch you will have to wait for odata support.
It is in beta at the moment and supports the merge semantics with the Delta<> class.
https://www.nuget.org/packages/Microsoft.AspNetCore.OData/
For doing a patch, you have to define PatchDocument.
More about it you can find PatchDocument
Example of method.
[HttpPatch("{userId:int}")]
public IActionResult UserPatch(int userId, [FromBody] JsonPatchDocument<User> patchDocument) {
var user = new User();
// Because it comes from url.
user.Id = userId;
patchDocument.ApplyTo(user);
// Here you call context or repository to save.
}
Example of document.
[
{ "op": "replace", "path": "/firstName", "value": "boo" },
]
That will update firstName field to 'boo' in user model.
What you might be looking for is ASP.Net Core JsonPatchDocument
https://github.com/aspnet/JsonPatch
https://learn.microsoft.com/en-us/aspnet/core/api/microsoft.aspnetcore.jsonpatch
Related
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
I have an action:
[HttpGet]
[Route("foo")]
public ActionResult Foo([FromQuery] MyClass request)
{
var image = ToImage(WidgetType.MedianSalesPriceSqft, request);
return File(image.ToByteArray(), "image/png");
}
below MyClass is defined:
public class MyClass {
[DefaultValue("90210")]
public string Zip { get; set; }
[DefaultValue("5361 Doverton Dr")]
public string StreetAddress { get; set; }
}
When I hit /swagger/index.html and want to try out this API, I always have to enter the StreetAddress and Zip values, even though I have default values defined.
Swagger currently provides a schema filter that let's you provide default values for object properties as long as they are not set to be [FromQuery]. Am I missing something simple?
It looks like the problem has been fixed on the beta version:
https://www.nuget.org/packages/Swashbuckle.AspNetCore/5.0.0-rc2
Lot of changes on the beta version, I had a couple of DocumentFilter and needed to be refactored,
I had to comment some stuff out that could not figure out how to do on the beta.
I added your same action and class, it looks like this:
Once you click on the try it out the values are populated
Just if you needed my code is here:
https://github.com/heldersepu/csharp-proj/tree/master/WebApi_NetCore
I'm using Web API with Microsoft's OData implementation to build a RESTful service. For a given resource (here, Employee) I would like to return one representation for GET requests (i.e. when retrieving the collection or a single record), but accept a different representation for POST and PUT/PATCH requests.
After building out my API project in this manner and attempting to create a new Employee via a POST request, I get this exception returned back:
{
"error": {
"code": "",
"message": "The request is invalid.",
"innererror": {
"message": "employee : The entity type 'Api.DataContracts.Manage.EmployeeCreate'
is not compatible with the base type 'Api.DataContracts.View.Employee'
of the provided entity set 'MyProject.MyContainer.Employees'. When an entity
type is specified for an OData resource set or resource reader, it has to be
the same or a subtype of the base type of the specified entity set.\r\n",
"type": "",
"stacktrace": ""
}
}
}
To further elaborate on my scenario, here's the model I use to return results for GET requests:
namespace Api.DataContracts.View
{
public class EmployeeView
{
public Guid Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string CompanyName { get; set; } // fetched from a related table
}
}
So far, so good. But when I want to create a new Employee I don't want an Id field (the database creates this). Nor do I want to accept a CompanyName field, as what I need at create time is a Company ID so I can properly set the relationship in the database. So that model looks like:
namespace Api.DataContracts.Manage
{
public class EmployeeCreate
{
public string FirstName { get; set; }
public string LastName { get; set; }
public Guid CompanyId { get; set; }
}
}
The relevant signatures on my EmployeeController OData controller are:
[HttpGet]
[ODataRoute("({key})")]
public IHttpActionResult GetEmployee([FromODataUri] Guid key)
// returns an Ok() with a EmployeeView model as the payload
[HttpPost]
[ODataRoute]
public IHttpActionResult AddEmployee(DataContracts.Manage.EmployeeCreate employee)
Lastly, I have this EDM model configuration:
var builder = new ODataConventionModelBuilder();
builder.EntitySet<DataContracts.View.EmployeeView>("Employees");
// Had to add this to avoid a Media Formatter error
builder.AddEntityType(typeof(DataContracts.Manage.EmployeeCreate));
In my research, both here on SO and on the Web at large, I've seen people ask similar (but not exactly the same) questions. The answers tend to point that this scenario should be possible, but it appears the use of Microsoft's OData libraries may make this impossible.
I have not been able to figure out a solution or workaround to this problem. I've considered making one of the models a sub-type of the other (since inheritance is supported in OData) but that feels like it will potentially raise a new set of issues.
Has anyone successfully used a similar architecture with Web API + OData?
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