JsonExtensionData attribute with default DotNetCore binder - c#

I am trying to pass JSON with some dynamic fields to a controller action method in DotNetCore 3.1 Web API project. The class I am using when sending the payload looks something like this:
public class MyDynamicJsonPayload
{
public string Id { get; set; }
[JsonExtensionData]
public IDictionary<string, object> CustomProps { get; set; }
}
I can see that object serialized correctly with props added to the body of JSON. So I send it from one service to another:
using var response = await _httpClient.PostAsync($"/createpayload", new StringContent(JsonConvert.SerializeObject(payload), Encoding.UTF8, "application/json"));
On the receiving end, however, when using same class in the controller action:
public async Task<ActionResult> CreatePayload([FromBody] MyDynamicJsonPayload payload)
{
var payload = JsonConvert.SerializeObject(payload);
return Ok(payload);
}
The object is parsed as something different where customProps is an actual field with JSON object containing my properties, plus instead of a simple value I get a JSON object {"valueKind":"string"} for string properties for example. I tried with both Newtonsoft.Json and System.Text.Json.Serialization nothing works as expected. Anyone has any ideas?

Thank you dbc for pointing me in the right direction, the problem was Newtownsoft vs System.Text.Json serialization/deserialization. I could not change the serializer in the Startup class because the service had many other methods and I didn't want to break existing contracts. However, I managed to write a custom model binder that did the trick:
public class NewtonsoftModelBinder : IModelBinder
{
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
string body = string.Empty;
using (var reader = new StreamReader(bindingContext.HttpContext.Request.Body))
{
body = await reader.ReadToEndAsync();
}
var result = JsonConvert.DeserializeObject(body, bindingContext.ModelType);
bindingContext.Result = ModelBindingResult.Success(result);
}
}
And usage:
public async Task<ActionResult> CreatePayload([ModelBinder(typeof(NewtonsoftModelBinder))] MyDynamicJsonPayload payload)
{
// Process payload...
return Ok();
}

First of all welcome to the StackOverflow.
You can try this solution;
Fist create a class like you did but without dictionary;
public class History
{
public int Id { get; set; }
public string DeviceName { get; set; }
public int DeviceId { get; set; }
public string AssetName { get; set; }
}
After that please add this attribute to your controller class;
[Produces("application/json")]
Your method should be like this;
[Produces("application/json")]
public class ExampleController: Controller
{
[HttpGet]
public Task<IEnumerable<History>> Get()
{
List<History> historyList = new List<History>()
{
new History()
{
...
},
new History()
{
...
}
}
return historyList;
}
}

Related

How can I send JSON response to client from ASP.NET Core Web API?

I am new to .NET Core. I am creating an API which returns some data in response. I have created a utility function successResponse to return a generic success response with data to client.
Here is the model class of that successResponse
public class SuccessResponse
{
public object? payload { get; set; } = null;
public bool success { get; set; } = false;
public SuccessResponse(object data, bool isSuccess)
{
payload = data;
success = isSuccess;
}
}
I have also created a helper class that have a function to return successResponse like this
public static class ResponseHandler
{
public static SuccessResponse successResponse(object data)
{
return new SuccessResponse(data, true);
}
public static ErrorResponse errorResponse(string error)
{
return new ErrorResponse(error);
}
}
In my controller, I have code like this:
[HttpPost]
public async Task<IActionResult> GetIncomingFile(IFormFile file)
{
try
{
var options = new JsonSerializerOptions { IncludeFields = true };
List<ImportedFileData> importedExcelFileData = await ExcelMapperFileReader.getFileData(file);
BalanceSheet balanceSheetData = BalanceSheetReport.createBalanceSheet(importedExcelFileData);
return Ok(ResponseHandler.successResponse(JsonSerializer.Serialize(balanceSheetData, options)));
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
return BadRequest(ResponseHandler.errorResponse(ex.Message));
}
}
My model class BalanceSheet is like this
public class BalanceSheet
{
public BalanceSheetAssets assets = null!;
public BalanceSheetLiabilities liabilities = null!;
public BalanceSheetEquity equity = null!;
public BalanceSheet(BalanceSheetAssets incomingAssets, BalanceSheetLiabilities incomingLiabilities, BalanceSheetEquity incomingEquity)
{
assets = incomingAssets;
liabilities = incomingLiabilities;
equity = incomingEquity;
}
}
The problem is that I get this response on the client:
{
"payload": "{\"assets\":{\"currentAssets\":[{\"title\":\"Inventory\",\"amount\":85300,\"code\":\"CA\"},{\"title\":\"Accounts Receivable\",\"amount\":4700,\"code\":\"CA\"},{\"title\":\"Cash\",\"amount\":5000,\"code\":\"CA\"}],\"nonCurrentAssets\":[{\"title\":\"Furniture\",\"amount\":200000,\"code\":\"NCA\"}],\"totalAssets\":255000},\"laibilities\":{\"currentLiabilities\":[{\"title\":\"Inventory\",\"amount\":85300,\"code\":\"CA\"},{\"title\":\"Accounts Receivable\",\"amount\":4700,\"code\":\"CA\"},{\"title\":\"Cash\",\"amount\":5000,\"code\":\"CA\"}],\"nonCurrentLiabilities\":[{\"title\":\"Furniture\",\"amount\":200000,\"code\":\"NCA\"}],\"totalLiabilities\":45000},\"equity\":{\"equityList\":[{\"title\":\"Equity\",\"amount\":150000,\"code\":\"EQ\"},{\"title\":\"Retained Earnings\",\"amount\":60000,\"code\":\"EQ\"}],\"totalEquity\":210000}}",
"success": true
}
But I want to receive correctly formatted JSON response at the client.
How can I do that?
You are getting an empty object because System.Text.Json, by default, ignores fields as documented here.
The BalanceSheet class is designed to have public fields, so you get an empty object in the response.
You have a couple of options:
you can instruct System.Text.Json to include fields by adding the JsonIncludeAttribute to all the public fields of the BalanceSheet class. See here for more details.
you can chage the design of the BalanceSheet class and use properties instead of fields.
In options 1 you need to change the code this way:
public class BalanceSheet
{
[JsonInclude]
public BalanceSheetAssets assets = null!;
[JsonInclude]
public BalanceSheetLiabilities laibilities = null!;
[JsonInclude]
public BalanceSheetEquity equity = null!;
public BalanceSheet(
BalanceSheetAssets incomingAssets,
BalanceSheetLiabilities incomingLaibilities,
BalanceSheetEquity incomingEquity) {
assets = incomingAssets;
laibilities = incomingLaibilities;
equity = incomingEquity;
}
}
In options 2 you need to change the code this way:
public class BalanceSheet
{
public BalanceSheetAssets Assets { get; } = null!;
public BalanceSheetLiabilities Laibilities { get; } = null!;
public BalanceSheetEquity Equity { get; } = null!;
public BalanceSheet(
BalanceSheetAssets incomingAssets,
BalanceSheetLiabilities incomingLaibilities,
BalanceSheetEquity incomingEquity) {
Assets = incomingAssets;
Laibilities = incomingLaibilities;
Equity = incomingEquity;
}
}
In general using public fields is a bad design, so I would go with option 2. See here for more details.
Either way you need to let ASP.NET core to do the JSON serialization for you, so change the return statement in the action method this way:
return Ok(ResponseHandler.succesResponse(balanceSheetData));
In my comment I suggested you to manually serialize to JSON just as a debugging step, to help us understanding the nature of your issue. You should never manually serialize the response object to JSON: this is done automatically for you by the ASP.NET core framework, when the OkObjectResult is executed.

How to return IEnumerable json array in a Http Post controller using .Net core

I have the following dotnet core code and I'm trying to return a TestResponse JSON object that has a few nodes under it. However, using the return Enumerable.Range(1, 4).Select(index => new Entities.TestResponse call in the post return for some reason all the attributes of response are not found in the enclosure when clearly Entities.TestResponse has the response definition. I'm probably not configuring the Enumerable enclosure correctly. Does anyone know how to resolve this, so I can set the response.result & response.exception and return response JSON from my REST POST method?
namespace TestApi.Entities
{
public class TestResponse
{
public TestResponseNodes response { get; set; }
}
public class TestResponseNodes
{
public string result { get; set; }
public string exception { get; set; }
}
}
[HttpPost]
public Task<IEnumerable<Entities.TestResponse>> Post([FromBody] String input)
{
return Enumerable.Range(1, 4).Select(index => new Entities.TestResponse
{
response.result = "No Error",
response.exception = "None"
}).ToArray();
}
Your syntax is wrong, you need to also new up the inner object, for example:
new Entities.TestResponse
{
response = new Entities.TestResponseNodes
{
result = "No Error",
exception = "None"
}
}
As an aside, you should follow common C# conventions and capitalise your property names, for example:
public class TestResponse
{
public TestResponseNodes Response;
}
public class TestResponseNodes
{
public string Result { get; set; }
public string Exception { get; set; }
}

ASP.NET Core FromBody Model Binding: Bind a Class with Interafece Field

I've been struggling a lot with that, I found some questions but none could answer my needs. I will try to post a better question and some of the things I tried.
Here is the situation:
I have an APIGateway and a WebApp. The WebApp sends POST requests to the APIGateway, so far so good. I use the FromBody attribute to send larger objects, and that was fine too until I introduced interfaces :))
Here's some code:
WebApp:
public interface ICommand
{
Guid CorrelationId { get; set; }
Guid SocketId { get; set; }
}
public class Command : ICommand
{
public Command(Guid CorrelationId, Guid SocketId)
{
this.CorrelationId = CorrelationId;
this.SocketId = SocketId;
}
public Guid CorrelationId { get; set; } = new Guid();
public Guid SocketId { get; set; } = new Guid();
}
public interface IDocument
{
Guid Id { get; set; }
ulong Number { get; set; }
}
public class Document : IDocument
{
public Guid Id { get; set; } = new Guid();
public ulong Number { get; set; } = 0;
}
public interface ICreateDocumentCommand : ICommand
{
IDocument Document { get; set; }
}
public class CreateDocumentCommand : Command, ICreateDocumentCommand
{
public CreateDocumentCommand(IDocument Document, ICommand Command) : base(Command.CorrelationId, Command.SocketId)
{
this.Document = Document;
}
public IDocument Document { get; set; }
}
APIGateway:
[HttpPost]
public async Task<IActionResult> Create([FromBody]CreateDocumentCommand documentCommand)
{
if (documentCommand == null)
{
return StatusCode(403);
}
return Json(documentCommand.Document.Id);
}
Use case:
public class InventoryList : Document
{
public Guid WarehouseId { get; set; } = new Guid();
}
// Example document class
////////////////////////////////////////
// Example POST Request
ICommand command = new Command(messageId, socketId);
switch (item.GetType().Name)
{
case "InventoryList":
command = new CreateDocumentCommand((InventoryList)item, command);
break;
}
string result = await PostAsync($"{apiGatewayAddress}{item.GetType().BaseType.Name}/Create", command, accessToken);
My POST sending function:
public async Task<string> PostAsync<T>(string uri, T item, string authorizationToken = null, string authorizationMethod = "Bearer")
{
JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All };
HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Post, uri);
requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
requestMessage.Content = new StringContent(JsonConvert.SerializeObject(item, typeof(T), jsonSerializerSettings), System.Text.Encoding.UTF8, "application/json");
return await _client.SendAsync(requestMessage).Result.Content.ReadAsStringAsync();
}
As you can see I have included TypeNameHandling.All in the JSON serialization settings, the request is sent and the Create in the APIGateway gets called. However the parameter documentCommand is NULL.
I've read this: Asp.Net Core Post FromBody Always Null
This: ASP.NET Core MVC - Model Binding : Bind an interface model using the attribute [FromBody] (BodyModelBinder)
This: Casting interfaces for deserialization in JSON.NET
Tried all kind of magic tricks, created new constructors, marked them with [JSONConstructor], still no success. Also I tried changing the APIGateway Cerate method parameter type to ICreateDocumentCommand and again I got a null. I've been searching some model binding tricks online however I couldn't find anything for binding with FromBody. I also found some solution including DI but I am looking for a simple solution. I hope that we will be able to find one :)
Turns out, passing interfaces or classes with interfaces inside as JSON is not that easy. I added a custom JSONConverter and it works now!

Strict mapping in web Web API (Over-Posting)

I just realized that the mapping between the JSON send from a query and my API is not strict.
I give you more explanations:
Here is my C# POCO
public partial class AddressDto
{
public string AddrId { get; set; }
public string Addr1 { get; set; }
public string Addr2 { get; set; }
public string PostalCode { get; set; }
public string City { get; set; }
public string Country { get; set; }
}
And the REST JSON query
PUT http://Localhost:55328/api/ClientAddr/ADD-2059-S002 HTTP/1.1
content-type: application/json
{
"AddrID": "ADD-2059-S002",
"addr1": "B-1/327",
"addr2": "1ST FLOOR",
"city": "Paris",
"Zip_Code": "78956",
"country": "France",
}
The web client send a PUT with Zip_Code in place of PostalCode. PostalCode is not madatory/required. But Zip_Code does not exist in my DTO.
So in my C# code testing the model state won't help.
public HttpResponseMessage Put(string id, AddressDto address)
{
if (!ModelState.IsValid)
return BadRequest(ModelState); // This wont help
}
How can I raise exception when the client is using something in the JSON that is not existing in my DTO (model) ?
if you need to identify extra columns and handle that as an error you have to extend IModelBinder interface and tell json deserializer to treat extra column as an error and add that error to ModelState. By that way you can check in controller for ModelState.IsValid. Checkout the below Code
CustomModelBinder
public class CustomModelBinder : IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
JsonSerializerSettings settings = new JsonSerializerSettings();
settings.MissingMemberHandling = MissingMemberHandling.Error;
ObjToPass obj = new ObjToPass();
;
try
{
ObjToPass s =
JsonConvert.DeserializeObject<ObjToPass>(actionContext.Request.Content.ReadAsStringAsync().Result,
settings);
bindingContext.Model = obj;
}
catch (Exception ex)
{
bindingContext.ModelState.AddModelError("extraColumn", ex.Message);
}
return true;
}
}
public class CustomerOrderModelBinderProvider : ModelBinderProvider
{
public override IModelBinder GetBinder(System.Web.Http.HttpConfiguration configuration, Type modelType)
{
return new CustomModelBinder();
}
}
Object Class that is passed to webapi
public class ObjToPass
{
public int Id { get; set; }
public string Name { get; set; }
}
Controller
[HttpPost]
public void PostValues([ModelBinder(typeof(CustomerOrderModelBinderProvider))] ObjToPass obj)
{
if(!ModelState.IsValid)
{ }
else
{
}
}
This sample holds good for HttpPut as well.
"Over-Posting": A client can also send more data than you expected. For example:
Here, the JSON includes a property ("Zip_Code") that does not exist in the Address model. In this case, the JSON formatter simply ignores this value. (The XML formatter does the same.)
https://learn.microsoft.com/en-us/aspnet/web-api/overview/formats-and-model-binding/model-validation-in-aspnet-web-api

Posting an XML to WebAPI and return a result

So I have been struggling to get this work for some time now.
Let me explain the requirement.
I have to write a WebAPI that will accept an XML, do some look up, and return a response.
I am new to this so sought some help. The suggestion was to create a custom object that represents the incoming XML
and use that object as the param for the method exposed by the WebAPI.
The result will be a class that's not a problem.
Here are the steps done.
Created an empty WebAPI project.
Added a class that represents the incoming XML.
Incoming XML:
<InComingStudent>
<StudentID>10</StudentID>
<Batch>56</Batch>
</InComingStudent>
The class:
public class InComingStudent
{
public string StudentID { get; set; }
public string Batch { get; set; }
}
Return object of this type:
public class StudentResult
{
public string StudentID { get; set; }
public string Batch { get; set; }
public string Score { get; set; }
}
Added a Students controller with this method:
public class StudentsController : ApiController
{
[HttpPost]
public StudentResult StudentStatus(InComingStudent inComingStudent)
{
ProcessStudent po = new ProcessStudent();
StudentResult studentResult = po.ProcessStudent();
return StudentResult;
}
}
Ran the service. It opened in a new browser with 404. That's ok as I don't have a starting page.
Wrote a console app to test:
private static async void PostToStudentService()
{
using (var client = new HttpClient())
{
var studentToPost = new InComingStudent() { StudentID = "847", Batch="56"};
client.BaseAddress = new Uri("http://localhost:53247/");
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml"));
HttpResponseMessage response = await client.PostAsXmlAsync("api/Students/StudentStatus", studentToPost);
if (response.IsSuccessStatusCode)
{
// Print the response
}
}
}
MediaTypeWithQualityHeaderValue is set to application/xml.
So far so good.
When the service is running and I run the console app, the response is 404 "Not Found".
What am I missing?
Any help is greatly appreciated.
Regards.
Have you tried the [FromBody] Attribute?
public class StudentsController : ApiController
{
[HttpPost]
public StudentResult StudentStatus([FromBody]InComingStudent inComingStudent)
{
ProcessStudent po = new ProcessStudent();
StudentResult studentResult = po.ProcessStudent();
return StudentResult;
}
}
And as a suggestion, I would use Attribute-Routing.
public class StudentsController : ApiController
{
[HttpPost, Route("api/stutents/studentsstatus")]
public StudentResult StudentStatus([FromBody]InComingStudent inComingStudent)
{
// ...
}
}

Categories