In an ASP.NET core project, I've switched from using ResourceModel to using JObject as my [FromBody] parameter. I then pass JObject.ToObject<ResourceModel>() into a service, but want to maintain the JObject itself for the ContainsKey and similar functionality.
If JObject.ToObject<T> succeeds, I can use TryValidateModel() and, if that fails, return BadRequest(ModelState) simply enough. However, the problem I'm running into is when JObject.ToObject<T> throws an exception - I'm unsure how to capture the serialization errors in the ModelState.
Example:
public class Person {
public string FirstName {get;set;}
public string LastName {get;set;}
public uint Age {get;set;}
}
public class CommonParameters {
[FromQuery] public string? Fields {get;set;}
public string UserName {get;set;}
public IPAddress? RequestorIp {get;set;}
public JObject? JsonBody {get;set;}
}
public class PersonController : ControllerBase {
private readonly PersonService _personService;
// this is how I had been doing it
[HttpPatch("obsolete/{id}")] // this method doesn't actually exist, but is shown for the example
public async Task<IActionResult> UpdatePerson([FromRoute] int id, [FromBody] Person parameters, [FromHeader] CommonParameters commonParameters) {
SetCommonParameters(commonParameters, parameters);
// if parameters.Age in the json body was a string, BadRequest is returned before this method even starts
_personService.Set(parameters);
await _personService.SaveChangesAsync();
return NoContent();
}
// this is how I'm trying to do it now, so that I only update Age if JObject.ContainsKey("Age")
// instead of parameters.Age != default, etc.
[HttpPatch("{id}")]
public async Task<IActionResult> UpdatePerson([FromRoute] int Id, [FromBody] JObject parameters, [FromHeader] CommonParameters commonParameters) {
SetCommonParameters(commonParameters, parameters);
// if parameters.Age is a string in this version, a JsonReaderException
// or JsonSerializationException is thrown when I call JObject.ToObject<>()
// and I have to validate the model separately after that
// what this means is, a BadRequest with the ModelState errors is not
// returned if there's an issue with the json.
var model = parameters.ToObject<Person>();
if (!TryValidateModel(model)) return BadRequest(ModelState);
_personService.Set(person);
await _personService.SaveChangesAsync();
return NoContent();
}
private void SetCommonParameters(CommonParameters commonParameters, JObject? jsonBody = null) {
commonParameters.JsonBody = jsonBody;
commonParameters.UserName = User.Identity?.Name;
commonParameters.RequestorIp = Request.HttpContext.Connection.RemoteIpAddress;
}
}
EDIT: As Guru Stron has asked, here's an example of an invalid model/json object.
Model:
public class ContactInfo {
// PhoneNumber being a struct representing an 11-digit phone number
// PhoneType being an enum of Cell, Work, Home, etc.
public Dictionary<PhoneType, PhoneNumber[]> PhoneNumbers {get;set;} = new();
public string? Apartment {get;set;}
public int AddressNumber {get;set;}
public string StreetName {get;set;}
public string City {get;set;}
// State being an enum of the 50 United States of America
public State State {get;set;}
}
The invalid json
{
"phoneNumbers": {
"work": [ "1-111-111-1111" ],
"home": [ "2-222-222-2222" ],
"cell": [ "1-800-CALL-NOW" ]
},
"apartment": null,
"addressNumber": "3a",
"streetName": "Imagination St.",
"city": "Atlantis",
"state": "Atlantic"
}
In this case, passing the above json for ContactInfo to a controller's method that asks for a ContactInfo parameter will return a bad request indicating that the state was invalid, the address number was invalid, and a phone number was invalid - without testing this specifically, something like the following.
{
"errors": {
"phoneNumbers": [
"Could not parse phone number \"1-800-CALL-NOW\". The value must be an 11-digit numeric value."
],
"addressNumber": [
"The value could not be parsed as int."
],
"state": [
"\"Atlantic\" is an invalid value for State. The following values are valid: ..."
]
}
}
If instead the controller asks for JObject and I then call JObject.ToObject and catch an exception, I'll get one of those errors - such as Could not parse phone number "1-800-CALL-NOW". The value must be an 11-digit numeric value. The others will be ignored until that one is fixed and the user tries again.
I've at last figured it out... All that I need to do is provide a JsonSerializer to JObject.ToObject<> and include an error handler that marks errors as handled. For example:
var serializer = new JsonSerializer();
serializer.Error += (sender, args) =>
{
if (args.ErrorContext.Error is JsonException)
{
ModelState.AddModelError(args.ErrorContext.Path, args.ErrorContext.Error.Message);
args.ErrorContext.Handled = true;
}
};
var value = parameters.ToObject<Person>(serializer);
if (!TryValidateModel(value)) return ValidationProblem(ModelState);
It is my understanding that the default Microsoft.AspNetCore.Mvc.Formatters.NewtonsoftJsonInputFormater performs similar error handling; I'm still not sure how to use that handler directly, but what I have here is working for what I need.
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.
I am using .NET Core 3.1 to develop REST API. This is the controller that I am using (stripped down to basics just to demonstrate the issue, it returns the same data as it received):
OrdersController.cs
[Route("Payments/[controller]")]
[ApiController]
public class OrdersController : Controller
{
[HttpPost]
[Route("AddOrder")]
public IActionResult AddOrder(Order order)
{
return Json(order);
}
}
Order.cs
public class Product
{
[Required]
public string Manufacturer { get; set; }
[Required]
public string Code { get; set; }
}
public class Order
{
[Required]
public string Recipient { get; set; }
[Required]
public Product Product { get; set; }
}
When I call Payments/Orders/AddOrder with Postman with the following body (notice empty nested Code field):
{
"Recipient": "John Doe",
"Product": {
"Manufacturer": "Company Inc.",
"Code": ""
}
}
... I get the following error which is expected since Code is annotaded with [Required]:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "|7558e077-4f9147019767a0cf.",
"errors": {
"Product.Code": [
"The Code field is required."
]
}
}
However, if I try to validate the Order object with the same data in one of the services, it doesn't detect that field Code is empty.
// manually initialized order with same data as in Postman request
Order order = new Order()
{
Recipient = "John Doe",
Product = new Product()
{
Manufacturer = "Company Inc.",
Code = string.Empty
}
};
ValidationContext context = new ValidationContext(order, serviceProvider: null, items: null);
var validationResults = new List<ValidationResult>();
bool isValid = Validator.TryValidateObject(order, context, validationResults, true);
Here, isValid is true which means that object is valid. How can it be valid if Code is empty? Why does controller automatically detect that nested property is invalid but Validator.TryValidateObject doesn't? Does controller validation work recursively and Validator.TryValidateObject does not? Can I use the same recursive (nested fields) validation that controller uses somewhere else in the code?
EDIT: Why do we even want to validate the object on the service layer?
We developed a shared project to be used in other solutions. It calls REST API with correctly formatted payload and headers. We want to validate object inside shared project's code (service) before it is even sent to REST API server. We want to avoid situations where request is sent out by the shared client but is then rejected at REST API's server.
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'm attempting to receive a POSTed List of POCO but I'm unable to deserialize either via the Swagger API or via Postman when submitting over the wire. I've serialized the object back out to confirm how it supposed to be serialized, but when returning that back the body of the form, the request object parameter is null.
public class NameValuePair
{
public string Name { get; set; }
public string Value { get; set; }
}
public class NameValueList
{
private List<NameValuePair> valuesToProcess = null;
public List<NameValuePair> ValuesToProcess { get { return valuesToProcess; } set { valuesToProcess = value; } }
public NameValueList()
{
valuesToProcess = new List<NameValuePair>();
}
}
[Api("A description of what the API does")]
[Tag("Enriching Requests")]
[ApiResponse(HttpStatusCode.BadRequest, "Your request was not understood")]
[ApiResponse(HttpStatusCode.InternalServerError, "Oops, something broke")]
[Route("/EnrichValueList", "POST", Summary = "Summary of the Web Get method", Notes = "Longer information about what this service does")]
public class EnrichValueList : IPost, IReturn<EnrichValueListResponse>
{
[ApiMember(Name = "NameValues", Description = "A string that represents a value with which to understand more information about", ParameterType = "body", DataType = "NameValueList", IsRequired = true)]
public NameValueList NameValues
{
get;
set;
}
[ApiMember(Name = "AssociatedActivities", Description = "A comma seperated string that links Enrichment Activities with this value", ParameterType = "body", DataType = "string", IsRequired = false)]
public string AssociatedActivities
{
get;
set;
}
}
The request.NameValues in this case is null (no error is thrown):
public async Task<EnrichValueListResponse> Post(EnrichValueList request)
{
EnrichValueListResponse enrichValueListResponse = new EnrichValueListResponse();
return enrichValueListResponse;
}
I've already got other methods that receive a string of stringyfied object and then use the JsonSerializer.DeserializeFromString method from ServiceStack.Text so completely fine with that approach. I was attempting to use a more strongly typed object in the original request (which may not be possible or I'm doing it wrong).
Param: NameValues, Value {"valuesToProcess":[{"name":"bob","value":"Not Bob"}]}
and trying about every other permutation I can think of. Interestingly, when changing to plain string parameters and posting, the inbuilt swagger API returns a deserialization error, but Postman is fine.
Swagger Top Half
Swagger Bottom Half
Response Body as text
{
"responseStatus": {
"errorCode": "SerializationException",
"message": "Type definitions should start with a '{', expecting serialized type 'EnrichValueList', got string starting with: \"two\"",
"stackTrace": " at ServiceStack.Text.Common.DeserializeTypeRefJson.StringToType(ReadOnlySpan`1 strType, TypeConfig typeConfig, EmptyCtorDelegate ctorFn, KeyValuePair`2[] typeAccessors)\r\n at ServiceStack.Text.Common.DeserializeType`1.StringToTypeContext.DeserializeJson(ReadOnlySpan`1 value)\r\n at ServiceStack.Text.Json.JsonReader.<>c__DisplayClass3_0.<GetParseSpanFn>b__0(ReadOnlySpan`1 v)\r\n at ServiceStack.Text.JsonSerializer.DeserializeFromSpan(Type type, ReadOnlySpan`1 value)\r\n at ServiceStack.Memory.NetCoreMemory.Deserialize(MemoryStream memoryStream, Boolean fromPool, Type type, DeserializeStringSpanDelegate deserializer)\r\n at ServiceStack.Memory.NetCoreMemory.DeserializeAsync(Stream stream, Type type, DeserializeStringSpanDelegate deserializer)\r\n at ServiceStack.Host.RestHandler.CreateRequestAsync(IRequest httpReq, IRestPath restPath, Dictionary`2 requestParams)\r\n at ServiceStack.Host.RestHandler.CreateRequestAsync(IRequest httpReq, IRestPath restPath)\r\n at ServiceStack.Host.RestHandler.ProcessRequestAsync(IRequest req, IResponse httpRes, String operationName)"
}
}
Postman
Update
Following #mythz advice, I removed both the ParameterType and the DataType from the decorator and I was able to exhibit some slightly different behaviour.
Using the example classes:
public class NameValues
{
public string Name { get; set; }
public List<string> Values { get; set; }
public NameValues()
{
Values = new List<string>();
Name = string.Empty;
}
}
public class NameValuesList
{
public List<NameValues> ValuesToProcess { get; set; }
public NameValuesList()
{
ValuesToProcess = new List<NameValues>();
}
}
and setting the DTO parameter to this
[ApiMember(Name = "NameValues", Description = "A string that represents a value with which to understand more information about", IsRequired = true)]
public NameValuesList NameValues
{
get;
set;
}
causes the same deserialization error when I pass in a JSON structure that should deserialize. However, if I pass in some deformed string, it throws no error and just runs on through to the IPost handler method with a null for the property.
If I change the API parameter back to a string and use this example to show serialization and deserialization using the ServiceStack.Text library, works like a charm through both Swagger and Postman.
public async Task<EnrichValueListResponse> Post(EnrichValueList request)
{
EnrichValueListResponse enrichValueListResponse = new EnrichValueListResponse();
// Create a dummy object to serialize as JSON to return as an example back to the caller
NameValuesList stuff = new NameValuesList();
stuff.ValuesToProcess.Add(new NameValues { Name = "first", Values = { "second", "third" } });
enrichValueListResponse.BatchId = await Task.Run(() => stuff.ToJson().IndentJson());
// Deserialize the inbound string
NameValuesList betterStuff = JsonSerializer.DeserializeFromString<NameValuesList>(request.NameValues);
return enrichValueListResponse;
}
JSON submitted (although I did try many different variations of encoding and structure).
{
"valuesToProcess": [
{
"name": "first",
"values": [
"second",
"third"
]
}
]
}
I've got debug mode set, but not seeing any exceptions thrown to the SeriLog db during parsing. For now, I'll run with string parameter and derserialize after the fact. I can do some pre-checking in the Fluent Validator
I currently have a Web API that implements a RESTFul API. The model for my API looks like this:
public class Member
{
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime Created { get; set; }
public DateTime BirthDate { get; set; }
public bool IsDeleted { get; set; }
}
I've implemented a PUT method for updating a row similar to this (for brevity, I've omitted some non-relevant stuff):
[Route("{id}")]
[HttpPut]
public async System.Threading.Tasks.Task<HttpResponseMessage> UpdateRow(int id,
[FromBody]Models.Member model)
{
// Do some error checking
// ...
// ...
var myDatabaseEntity = new BusinessLayer.Member(id);
myDatabaseEntity.FirstName = model.FirstName;
myDatabaseEntity.LastName = model.LastName;
myDatabaseEntity.Created = model.Created;
myDatabaseEntity.BirthDate = model.BirthDate;
myDatabaseEntity.IsDeleted = model.IsDeleted;
await myDatabaseEntity.SaveAsync();
}
Using PostMan, I can send the following JSON and everything works fine:
{
firstName: "Sara",
lastName: "Smith",
created: "2018/05/10",
birthDate: "1977/09/12",
isDeleted: false
}
If I send this as my body to http://localhost:8311/api/v1/Member/12 as a PUT request, the record in my data with ID of 12 gets updated to what you see in the JSON.
What I would like to do though is implement a PATCH verb where I can do partial updates. If Sara gets married, I would like to be able to send this JSON:
{
lastName: "Jones"
}
I would like to be able to send just that JSON and update JUST the LastName field and leave all the other fields alone.
I tried this:
[Route("{id}")]
[HttpPatch]
public async System.Threading.Tasks.Task<HttpResponseMessage> UpdateRow(int id,
[FromBody]Models.Member model)
{
}
My problem is that this returns all the fields in the model object (all of them are nulls except the LastName field), which makes sense since I am saying I want a Models.Member object. What I would like to know is if there is a way to detect which properties have actually been sent in the JSON request so I can update just those fields?
I hope this helps using Microsoft JsonPatchDocument:
.Net Core 2.1 Patch Action into a Controller:
[HttpPatch("{id}")]
public IActionResult Patch(int id, [FromBody]JsonPatchDocument<Node> value)
{
try
{
//nodes collection is an in memory list of nodes for this example
var result = nodes.FirstOrDefault(n => n.Id == id);
if (result == null)
{
return BadRequest();
}
value.ApplyTo(result, ModelState);//result gets the values from the patch request
return NoContent();
}
catch (Exception ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, ex);
}
}
Node Model class:
[DataContract(Name ="Node")]
public class Node
{
[DataMember(Name = "id")]
public int Id { get; set; }
[DataMember(Name = "node_id")]
public int Node_id { get; set; }
[DataMember(Name = "name")]
public string Name { get; set; }
[DataMember(Name = "full_name")]
public string Full_name { get; set; }
}
A valid Patch JSon to update just the "full_name" and the "node_id" properties will be an array of operations like:
[
{ "op": "replace", "path": "full_name", "value": "NewNameWithPatch"},
{ "op": "replace", "path": "node_id", "value": 10}
]
As you can see "op" is the operation you would like to perform, the most common one is "replace" which will just set the existing value of that property for the new one, but there are others:
[
{ "op": "test", "path": "property_name", "value": "value" },
{ "op": "remove", "path": "property_name" },
{ "op": "add", "path": "property_name", "value": [ "value1", "value2" ] },
{ "op": "replace", "path": "property_name", "value": 12 },
{ "op": "move", "from": "property_name", "path": "other_property_name" },
{ "op": "copy", "from": "property_name", "path": "other_property_name" }
]
Here is an extensions method I built based on the Patch ("replace") specification in C# using reflection that you can use to serialize any object to perform a Patch ("replace") operation, you can also pass the desired Encoding and it will return the HttpContent (StringContent) ready to be sent to httpClient.PatchAsync(endPoint, httpContent):
public static StringContent ToPatchJsonContent(this object node, Encoding enc = null)
{
List<PatchObject> patchObjectsCollection = new List<PatchObject>();
foreach (var prop in node.GetType().GetProperties())
{
var patch = new PatchObject{ Op = "replace", Path = prop.Name , Value = prop.GetValue(node) };
patchObjectsCollection.Add(patch);
}
MemoryStream payloadStream = new MemoryStream();
DataContractJsonSerializer serializer = new DataContractJsonSerializer(patchObjectsCollection.GetType());
serializer.WriteObject(payloadStream, patchObjectsCollection);
Encoding encoding = enc ?? Encoding.UTF8;
var content = new StringContent(Encoding.UTF8.GetString(payloadStream.ToArray()), encoding, "application/json");
return content;
}
}
Noticed that tt also uses this class I created to serialize the PatchObject using DataContractJsonSerializer:
[DataContract(Name = "PatchObject")]
class PatchObject
{
[DataMember(Name = "op")]
public string Op { get; set; }
[DataMember(Name = "path")]
public string Path { get; set; }
[DataMember(Name = "value")]
public object Value { get; set; }
}
A C# example of how to use the extension method and invoking the Patch request using HttpClient:
var nodeToPatch = new { Name = "TestPatch", Private = true };//You can use anonymous type
HttpContent content = nodeToPatch.ToPatchJsonContent();//Invoke the extension method to serialize the object
HttpClient httpClient = new HttpClient();
string endPoint = "https://localhost:44320/api/nodes/1";
var response = httpClient.PatchAsync(endPoint, content).Result;
Thanks
PATCH operations aren't usually defined using the same model as the POST or PUT operations exactly for that reason: How do you differentiate between a null, and a don't change. From the IETF:
With PATCH, however, the enclosed entity contains a set of
instructions describing how a resource currently residing on the
origin server should be modified to produce a new version.
You can look here for their PATCH suggestion, but sumarilly is:
[
{ "op": "test", "path": "/a/b/c", "value": "foo" },
{ "op": "remove", "path": "/a/b/c" },
{ "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] },
{ "op": "replace", "path": "/a/b/c", "value": 42 },
{ "op": "move", "from": "/a/b/c", "path": "/a/b/d" },
{ "op": "copy", "from": "/a/b/d", "path": "/a/b/e" }
]
#Tipx's answer re using PATCH is spot on, but as you've probably already found, actually achieving that in a statically typed language like C# is a non-trivial exercise.
In the case where you're using a PATCH to represent a set of partial updates for a single domain entity (e.g. to update the first name and last name only for a contact with many more properties) you need to do something along the lines of looping each instruction in the 'PATCH' request and then applying that instruction to an instance of your class.
Applying an individual instruction will then comprise of
Finding the property of the instance that matches the name in the
instruction, or handling property names you weren't expecting
For an update: Trying to parse the value submitted in the patch into the instance property and handling the error if e.g. the instance property is a bool but the patch instruction contains a date
Deciding what to do with Add instructions as you can't add new properties to a statically typed C# class. One approach is to say that Add means "set the value of the instance's property only if property's existing value is null"
For Web API 2 on the full .NET Framework the JSONPatch github project looks to make a stab at providing this code, although it doesn't look like there's been a lot of development on that repo recently and the readme does state:
This is still very much an early project, don't use it in production
yet unless you understand the source and don't mind fixing a few bugs
;)
Things are simpler on .NET Core as that has a set of functionality to support this in the Microsoft.AspNetCore.JsonPatch namespace.
The rather useful jsonpatch.com site also lists out a few more options for Patch in .NET:
Asp.Net Core JsonPatch (Microsoft official implementation)
Ramone (a framework for consuming REST services, includes a JSON Patch implementation)
JsonPatch (Adds JSON Patch support to ASP.NET Web API)
Starcounter (In-memory Application Engine, uses JSON Patch with OT for client-server sync)
Nancy.JsonPatch (Adds JSON Patch support to NancyFX)
Manatee.Json (JSON-everything, including JSON Patch)
I need to add this functionality to an existing Web API 2 project of ours, so I'll update this answer if I find anything else that's useful while doing that.
I wanted to achieve exactly the same thing, but used a different method to others described here. I've created a working repo using this if you want to check it out:
https://github.com/emab/patch-example
If you have the following two models:
Database model
public class WeatherDBModel
{
[Key]
public int Id { get; set; }
public string City { get; set; }
public string Country { get; set; }
public double Temperature { get; set; }
public double WindSpeed { get; set; }
public double Rain { get; set; }
public Weather(int id, string city, string country, double temperature, double windSpeed, double rain)
{
Id = id;
City = city;
Country = country;
Temperature = temperature;
WindSpeed = windSpeed;
Rain = rain;
}
}
Update model
Containing exact names of database model properties. Includes properties which can be updated
public class WeatherUpdateModel
{
public string? City { get; set; }
public string? Country { get; set; }
public double Temperature { get; set; }
public double WindSpeed { get; set; }
public double Rain { get; set; }
}
This update model is sent to the service layer along with the id of the object you'd like to update.
You can then implement the following method in your repository layer which maps any non-null values from the updateModel into an existing entity if it has been found:
public Weather Update(int id, WeatherUpdate updateObject)
{
// find existing entity
var existingEntity = _context.Weather.Find(id);
// handle not found
if (existingEntity == null)
{
throw new EntityNotFoundException(id);
}
// iterate through all of the properties of the update object
// in this example it includes all properties apart from `id`
foreach (PropertyInfo prop in updateObject.GetType().GetProperties())
{
// check if the property has been set in the updateObject
// if it is null we ignore it. If you want to allow null values to be set, you could add a flag to the update object to allow specific nulls
if (prop.GetValue(updateObject) != null)
{
// if it has been set update the existing entity value
existingEntity.GetType().GetProperty(prop.Name)?.SetValue(existingEntity, prop.GetValue(updateObject));
}
}
_context.SaveChanges();
return existingEntity;
}
Using this method you can change your models without worrying about the update logic, as long as you ensure that the UpdateModel is kept up-to-date with the database model.
If a property of your object was omitted in your JSON, ASP.NET won't "set" that property on the object, the property will have its default value. In order to know which properties were sent with the JSON object you need to have a way to detect which properties of the object were set.
In order to detect which properties have "actually been sent" with the JSON object, you can modify your Member class to contain a collection of property names that were "set". Then, for all properties that you want to be able to know if they were sent in the JSON object make that when the property is set the name of the property should be added to the collection of set properties.
public class Member
{
private string _firstName;
private string _lastName;
...
private bool _isDeleted;
public string FirstName
{
get => _firstName;
set
{
_firstName = value;
_setProperties.Add(nameof(FirstName));
}
}
public string LastName
{
get => _lastName;
set
{
_lastName = value;
_setProperties.Add(nameof(LastName));
}
}
...
public bool IsDeleted
{
get => _isDeleted;
set
{
_isDeleted= value;
_setProperties.Add(nameof(IsDeleted));
}
}
private readonly HashSet<string> _setProperties = new HashSet<string>();
public HashSet<string> GetTheSetProperties()
{
return new HashSet<string>(_setProperties);
}
}
In the UpdateRow method you can now check whether a property was sent in the JSON by checking if it is in the _setProperties collection. So if you want to see if the LastName was sent in the JSON just do
bool lastNameWasInJson = model.Contains(nameof(model.LastName));
Following up to Avid Learners approach. I found this easy to add to an existing PUT method.
Alternatively to avoid loading twice you could apply update operations and then before saving apply the patch, but I'd rather load twice and have simple code.
public ResultModel Patch(UpdateModel model)
{
var record = LoadAsUpdateModel(model.Id);
if (record == null) return null;
foreach(var propertyName in model.SetProperties())
{
var property = model.GetType().GetProperty(propertyName);
property.SetValue(record, property.GetValue(model));
}
return Update(record);
}