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);
}
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'm trying to make an API using .NET Core 3.1 with C# programming language which utilizes Firebase real-time database from Google through FireSharp Nu-Get package. When I ran it, I got this exception: System.Text.Json.JsonException: A possible object cycle was detected which is not supported. This can either be due to a cycle or if the object depth is larger than the maximum allowed depth of 32. I read here that I can just install Microsoft.AspNetCore.Mvc.NewtonsoftJson package and ignore the reference loop handling, which works fine. But the data format looks unreadable, very hard to deserialize, and way too long:
{
"stateMachine": {
"<>1__state": 0,
"<>t__builder": {},
"bpjs": "12345678",
"reservationTime": "24-11-2020",
"<>4__this": {}
},
"context": {},
"moveNextAction": {
"method": {
"name": "MoveNext",
"declaringType": ...
...
((lots of irrelevant things))
...
}
},
"result": {
"queueNumber": "1",
"bpjs": "12345678",
"name": "Addi",
"poli": "Umum",
"reservationTime": "24-11-2020",
"status": "Queueing"
},
"id": 2,
"exception": null,
"status": 5,
"isCanceled": false,
"isCompleted": true,
"isCompletedSuccessfully": true,
"creationOptions": 0,
"asyncState": null,
"isFaulted": false}
Before this, I already tried to do some mapping and not directly use my data model by following this video's tutorial on doing DTOs, but it didn't work most likely because I don't use SQL Database.
Now I'm trying to use this API on a Flutter app. Is there anyway for me to maybe do some minor changes on my API code (anything that doesn't involve completely changing the database to SQL) to reformat my response? Or maybe a way to just partly deserialize my response, since I only need the "result" part and not anything else?
Edit:
This is my main code to get the data:
public async Task<QueueData> GetQueueData(string bpjs, string reservationTime)
{
//set up connection
IFirebaseConfig config = new FirebaseConfig
{
AuthSecret = myAuthSecret,
BasePath = myBasePath
};
IFirebaseClient client = new FireSharp.FirebaseClient(config);
//checkqueuenumber
string queueCounter = await QueueDbConnection.CheckQueueNumber(reservationTime, client);
//getresult
QueueData result = await QueueDbConnection.GetResult(bpjs, reservationTime, queueCounter, client);
return result;
}
this is my controller to call the function above:
[HttpGet("{bpjs}/{reservationTime}")]
public ActionResult<QueueData> GetQueueData(string bpjs, string reservationTime)
{
var queueData = _repository.GetQueueData(bpjs, reservationTime);
return Ok(queueData);
}
This is my model for the QueueData, which is the type I'm trying to return as response:
public class QueueData
{
[Required]
public string QueueNumber { get; set; }
[Required]
public string BPJS { get; set; }
[Required]
public string Name { get; set; }
[Required]
public string Poli { get; set; }
[Required]
public string ReservationTime { get; set; }
[Required]
public string Status { get; set; }
}
this is another model class called control to monitor the number of people queueing each day:
public class QueueCounter
{
[Required]
public string q { get; set; }
}
So, I solved this. I went back to here and apparently someone said the error I got in the first place was because not all async functions had been waited or handled properly. So I went through my codes once again and finally found the problem in my controller. After I made my controller async then changed the return type into Task<ActionResult>, everything went perfectly fine!
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
In RavenDB 4.2, I want to create an Index/Map based on a dynamic object. Better put, on dynamic properties which are not known at compile-time.
Here is an example of the raw JSON I'm ingesting:
{
"id": "A",
"detections":
[
{
"steps": [
{
"object": {
"id": "A1",
"target": {
"domain_name": "foobar.com"
}
},
"object": {
"id": "A2",
"target": {
"ipv4": "127.0.0.1"
}
}
}
]
}
]
}
The above sample is ingested from a 3rd party and stored in a RavenDB collection. Roughly translated, the following model has the challenge:
public class Step
{
public string Id { get; set; }
public DateTime When {get; set;}
public dynamic Object { get; set; } // aware that it's not handy naming
}
The pickle in this is that the object.target.X property name is dynamic. They cannot be strong-typed and can be a lot of things, like: domain_name, ipv4, ipv6, dns, shoe_size, hair_colour etc. This is why the entire steps.object is ingested and stored as either System.Object or dynamic.
My objective is to basically do a SelectMany() on each object.target and extract the property name (key) and value. This would make my RavenDB Index something like this:
public class StepsIndex : AbstractIndexCreationTask<Models.Step, StepsIndex.Result>
{
public class Result
{
public DateTime When { get; set; }
public string TargetKey { get; set; }
public string TargetValue { get; set; }
// ... removed other properties for brevity
}
public StepsIndex()
{
Map = steps =>
from block in blocks
from detection in blocks.Detections
from step in detection.Steps
select new Result
{
// extract property name (key), like 'domain_name'
TargetKey = step.Object.target.GetType().GetProperties()[0].Name,
// extract property value, like 'foobar.com'
TargetValue = step.Object.target.GetType().GetProperty(s.Object.target.GetType().GetProperties()[0].Name).GetValue(s.Object.target, null)
};
}
}
Unfortunately this doesn't work due to step.Object being dynamic and resulting in the following error during compile-time:
Error [CS1963] An expression tree may not contain a dynamic operation
Second option I've tried is to cast it to JSON in the expression, which also fails because Raven's projection is not aware of Newtonsoft.Json during runtime:
// Error CS0103: The name 'JObject' does not exist in the current context
// Error CS0103: The name 'JsonConvert' does not exist in the current context
TargetKey = JObject.Parse(JsonConvert.SerializeObject(ass.Object))["target"][0].Value<string>(),
A third option I thought of was perhaps changing the dynamic Object to System.Object Object, but haven't found a neat way to extract the property key/values without knowning the property.
The question: how can I extract these dynamic property keys and values and Map them to a RavenDB index?
RavenDB allows to index dynamic fields:
See:
https://ravendb.net/docs/article-page/4.2/Csharp/indexes/using-dynamic-fields
https://github.com/ravendb/book/blob/v4.0/Ch10/Ch10.md#dynamic-data
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