I am still (or again) on my student project and one part of the project is the ability to send and view (of course) messages between users, and we use HotChocalte (.Net 5.0) for queries (read) and mutations (send). Data are usually obtained from a MySQL database. We also applied [UseFiltering] and [UseSorting] to queries and that works find with the members of the DB model.
Along others, we added a custom field containing the number of messages within a "thread" (we called that a conversation), and it tells the correct number of queries, but it seems to be impossible to filter or sort by this special fiels - the message "The specified input object field '...' does not exist.".
I have no more ideas what can I do. Do you have any suggestions? Hope following code is good enough to understand what I'm doing here:
C# implementation (backend):
public class ConversationType : ObjectType<Model.Conversation>
protected override void Configure(IObjectTypeDescriptor<Model.Conversation> descriptor)
{
// [UseFiltering] // This doesn't help
// [UseSorting] // This doesn't help
descriptor.Field("messageCount")
.UseDbContext<ReviverDbContext>()
.Type<IntType>()
.ResolveWith<ConversationResolvers>(
product => product.GetMessageCount(default, default)
)
// .UseFiltering(); // This doesn't help
// ...
private class ConversationResolvers
{
/**
* #brief Get the number of message within a conversation
*/
public async Task<int>
GetMessageCount([Parent] Model.Conversation conversation,
[ScopedService] OurDbContext dbContext)
{
return await Task.FromResult(dbContext.Messages
.Where(message => message.Conversation == conversation)
.Count()
);
}
// ...
}
}
}
HotChocolate QueryType in C# (backend too):
[ExtendObjectType(OperationTypeNames.Query)]
public class ConversationQueries
{
[UseOurDbContext]
[UsePaging(IncludeTotalCount = true)]
[UseFiltering]
[UseSorting]
public async Task<IEnumerable<Model.Conversation>>
GetConversationsAsync([ScopedService] OurDbContext dbContext)
{
return dbContext.Conversations.ToListAsync();
}
}
Example of query that does not work:
query {
conversations
(
where: {
messageCount: {neq : 0} <- "The specified input object field does not exist"
}
)
nodes {
messageCount
...
}
}
Thanks for any advise.
TLDR: No, its not possible. And for good reasons.
Please note, that when you are configure() in Hot Chocolate, you are really setting a pipeline. Every piece (middleware) have input, process and is returning output. Thats why order of middleware really matter. This is important for further understanding.
[UseFiltering]
There is no magic in [UseFiltering] - it is just middleware which will generate gql parameter object translateable to Linq Expression and at execution time, it will take this parameter, make Linq Expression from it and call return pipeInput.Where(linq_expression_i_just_generated_from_useFiltering_parameter_object)
So, when your input is EF DbSet<Model.Conversation>, then it simply call conversationDbSet.Where(expression). Entity framework then take this expression and when someone read results, it translate it to SQL select and fetch results from server.
But your expression is not translatable to sql - it contains field unknown to sql server, hence the error.
This is the reason why it work, when you call .ToList() -- it fetched whole table from sql server and do everything locally, which is not good (maybe its doable right now, but this will bite you later)
Why this is actually good behaviour:
Ok, imagine yourself few years in future where your Conversation table have 100.000.000 rows. Your users ara really chatty or your app is smashing success, congratulations :)
Imagine you have (privacy nightmare, but this is only example) api endpoint returning all conversations. And because there is so many conversation, you did what any sane guy/girl do: you added filtering and paging. Lets say 100 items per page.
Now, you have this messageCount field, which is executed locally.
One of your user have no idea what he is doing and filter "where messageCount = -1" (or 123 or anything which have 0 result).
Now what... Your pipeline do select top 100 (paging) but when it try to evaluate filter locally, it found out that there is not a single result. And this is where problems start: what should it do? Fetch another 100 rows? And another, and another and (100.000.000 / 100 = 1.000.000x select query) another, until it iterate whole table only to find there is no result? Or, second options, when it found out that part of your Where must be evaluated locally, do paging locally too and fetch 100.000.000 items in one go?
Both cases are really bad for performance and RAM. You just DDoS yourself.
By the way, option B is what EF (before-core, no idea how it is called... EF Classic? :D ) did. And it was a source of hard to find bugs, when in one moment query take few ms but then it take minutes. Good folks from EF team dropped this in first version of EFCore.
What to do instead...
One, AFAIK future proof (but it take some work), solution for this is using DB Views. Actually, in this case it is pretty easy. Just create view like this:
create view ConversationDto
as
select Conversation.*,
isnull(c.[count], 0) as MessageCount
from Conversation
left join (
select ConversationId,
count(Id)
from Messages
group by ConversationId
) as c on c.ConversationId = Conversation.Id
(This is mssql query, no idea which db you are using. As a small bonus, you can optimalise hell out of your view (add custom index(ex), better joins, etc))
Then create new EF Entity:
public class ConversationDto
: Conversation
{
public int MessageCount { get; set; }
}
public class ConversationDtoMap
: IEntityTypeConfiguration<ConversationDto>
{
public void Configure(EntityTypeBuilder<SubjectOverviewDto> builder)
{
builder.ToView("ConversationDto"); // ToView! not ToTable!
}
}
And now you are pretty much ready to go.
Just throw out your custom resolver (you moved your work to db, which is (well... should be. MySql, I am looking at you :-| ) great in this kind of work - working with data sets.) and change (query) type to ConversationDtoType.
As side effect, your ConversationDto (gql query model) is no longer same as your input model (gql mutation model - but your (domain?) model Conversation still is), but thats ok - you dont want to let users set messageCount anyway.
What to do instead #2
Your second option is to use ComputedColumns. I am not sure if its doable in your case, not a big fan of ComputedColumns, but when your custom resolver do something stupid like var p = ctx.Parent<Foo>(); return p.x + p.y * 42; then this should be fine.
Again, you moved work to database and there is no need for custom resolver and you can throw it out.
What to do instead #3
Also, calling .ToList()/.ToArray() before returning your DbSet<> at the beginning of your pipe and simply emberacing that "this will fetch, materialize and filter whole table" is another possible solution.
But be aware, that this can come back later and bite you really hard, especially if your Conversation is not some sort of list and there is chance that this table will grow big. (I have a feeling that Conversation is exactly that).
Also, because your implementation is another query to database, be aware that you just created 1 + n problem.
Do it at your own risk.
Impossible cases:
Of course, there can be resolver which do things that are impossible to do in sql (for example, sending http request to REST api on another server).
Then you are out of luck for reason I showed you in example "Why this is actually good behaviour".
AFAIK you have two possible solutions: You can reimplement UseFiltering (I did that, its not THAT hard, but my case was relatively simple and HC is open source, so you have great starting point...) or you can just add custom argument + custom middleware when configuring your endpoint and implement (pre) filtering yourself.
Foot note:
please, dont do this:
public async Task<int> GetMessageCount([Parent] Model.Conversation conversation,
[ScopedService] OurDbContext dbContext)
{
return await Task.FromResult(dbContext.Messages
.Where(message => message.Conversation == conversation)
.Count()
);
}
at least, name it GetMessageCountAsync() but even better, its not async, so no need to wrap it in Task<> and await Task.FromResult() at all, just:
public int GetMessageCount([Parent] Model.Conversation conversation,
[ScopedService] OurDbContext dbContext)
{
return dbContext.Messages
.Where(message => message.Conversation == conversation)
.Count();
}
I'm using Hangfire v1.7.9 and I'm trying to configure a series of recurring background jobs within my MVC 5 application to automate the retrieval of external reference data into the application. I've tested this with one task and this works great, but I'd like administrators within the system to be able to configure the Attempts and DelayInSeconds attribute parameters associated with the method that is called in these background jobs.
The AutomaticRetryAttribute states that you have to use...
...a constant expression, typeof expression or an array creation expression of an attribute parameter type
... which from what I've read is typical of all Attributes. However, this means that I can't achieve my goal by setting a property value elsewhere and then referencing that in the class that contains the method I want to run.
Additionally, it doesn't look like there is any way to configure the automatic retry properties in the BackgroundJob.Enqueue or RecurringJob.AddOrUpdate methods. Lastly, I looked at whether you could utilise a specific retry count for each named Queue but alas the only properties about Hangfire queues you can set is their names in the BackgroundJobServerOptions class when the Hangfire server is initialised.
Have I exhausted every avenue here? The only other thing I can think of is to create my own implementation of the AutomaticRetryAttribute and set the values at compile time by using an int enum, though that in itself would create an issue in the sense that I would need to provide a defined list of each of the values that a user would need to select. Since I wanted the number of retries to be configurable from 5 minutes all the way up to 1440 minutes (24 hours), I really don't want a huge, lumbering enum : int with every available value. Has anyone ever encountered this issue or is this something I should submit as a request on the Hangfire GitHub?
I would take the approach of making a custom attribute that decorates AutomaticRetryAttribute:
public class MyCustomRetryAttribute : JobFilterAttribute, IElectStateFilter, IApplyStateFilter
{
public void OnStateElection(ElectStateContext context)
{
GetAutomaticRetryAttribute().OnStateElection(context);
}
public void OnStateApplied(ApplyStateContext context, IWriteOnlyTransaction transaction)
{
GetAutomaticRetryAttribute().OnStateApplied(context, transaction);
}
public void OnStateUnapplied(ApplyStateContext context, IWriteOnlyTransaction transaction)
{
GetAutomaticRetryAttribute().OnStateUnapplied(context, transaction);
}
private AutomaticRetryAttribute GetAutomaticRetryAttribute()
{
// Somehow instantiate AutomaticRetryAttribute with dynamically fetched/set `Attempts` value
return new AutomaticRetryAttribute { Attempts = /**/ };
}
}
Edit: To clarify, this method allows you to reuse AutomaticRetryAttribute's logic, without duplicating it. However, if you need to change more aspects on per-job basis, you may need to duplicate the logic inside your own attribute.
Also, you can use context.GetJobParameter<T> to store arbitrary data on per-job basis
I am using asp.net core 2.2 and trying to implement the pattern outlined at POST-REDIRECT-GET using TempData in ASP.NET Core. I have two actions as follows:
[ImportModelState]
public async Task<IActionResult> Upload(int id, CancellationToken ct)
{
var model = new MyModel();
// ...
return this.View(model);
}
[HttpPost]
[ExportModelState]
public async Task<IActionResult> Upload(int id, MyModel model, CancellationToken ct)
{
// ...
}
After accessing the get view, entering the form details and submitting, I set a break point in the post action to view the bound model data for the MyModel parameter and I see an array for a boolean Replace field which backs a checkbox in the views form: However the Replace field works and if the model state is valid, everything succeeds for both of the form checkbox states.
If the form is invalid and I serialize the modelstate to tempdata and merge it on a redirect to the get action, I get an exception System.InvalidOperationException: The parameter conversion from type 'Newtonsoft.Json.Linq.JArray' to type 'System.Boolean' failed because no type converter can convert between these types.
Why does the model binder create an array for the boolean field (is this due to the semantics for forms and check boxes?) which works on the post action, but the same data fails to bind against the same model type when repopulating the view?
The serialization to temp data uses the following logic:
var errorList = modelState
.Select(kvp => new ModelStateTransferValue
{
Key = kvp.Key,
AttemptedValue = kvp.Value.AttemptedValue,
RawValue = kvp.Value.RawValue,
ErrorMessages = kvp.Value.Errors
.Select(p => p.ErrorMessage)
.ToList(),
});
The deserialization and merging uses the following logic:
var errorList = JsonConvert.DeserializeObject<List<ModelStateTransferValue>>(serialized);
var modelState = new ModelStateDictionary();
foreach (var item in errorList)
{
modelState.SetModelValue(item.Key, item.RawValue, item.AttemptedValue);
foreach (var error in item.ErrorMessages)
{
modelState.AddModelError(item.Key, error);
}
}
filterContext.ModelState.Merge(modelState);
The issue is that you aren't actually following post-redirect-get. The redirect happens on success. If there's validation errors, you just return the view again. No need to persist anything in TempData. Even the author of the linked article points out:
As PRG is primarily intended to prevent double form submissions, it does not necessarily follow that you should REDIRECT a user if the form is invalid. In that case, the request should not be modifying state, and so it is valid to submit the form again.
Where he goes astray is in the whole use TempData for invalid submissions and still redirect thing. That's just patently wrong, and I actually find it a little surprising that that advice is coming from Andrew Lock. I've referenced his articles quite often, but apparently never noticed this one.
In software development, there's an unspoken golden rule: don't be a unicorn. Doing things differently than virtually every other developer on the planet doesn't make you unique and special; it makes you an idiot.
UPDATE
Wow. Just reading through the comments and came across one from Andrew where he says:
Having said that, I haven't actually personally used this approach - as you say it adds a lot of complexity
That's a done deal right there. I mean it's an interesting thought experiment, but if it's not good enough for the guy who wrote it, it's not good enough for anyone else either.
All this does is get rid of the "Confirm form resubmission" dialog when a user refreshes a page that was submitted with invalid data. The case where the post is successful is already covered by PRG in general. Given the complexity, and all the extra requests, it's just not even remotely worth it for something that might not even ever happen. Personally, I've found users to be afraid to refresh the page, fearing that they'll have to re-enter all the fields again (which ironically this solution would actually cause, whereas they'd have been just fine refreshing otherwise).
I'm still learning web API, so pardon me if my question sounds stupid.
I have this in my StudentController:
public HttpResponseMessage PostStudent([FromBody]Models.Student student)
{
if (DBManager.createStudent(student) != null)
return Request.CreateResponse(HttpStatusCode.Created, student);
else
return Request.CreateResponse(HttpStatusCode.BadRequest, student);
}
In order to test if this is working, I'm using Google Chrome's extension "Postman" to construct the HTTP POST request to test it out.
This is my raw POST request:
POST /api/Student HTTP/1.1
Host: localhost:1118
Content-Type: application/json
Cache-Control: no-cache
{"student": [{"name":"John Doe", "age":18, "country":"United States of America"}]}
student is supposed to be an object, but when I debug the application, the API receives the student object but the content is always null.
FromBody is a strange attribute in that the input POST values need to be in a specific format for the parameter to be non-null, when it is not a primitive type. (student here)
Try your request with {"name":"John Doe", "age":18, "country":"United States of America"} as the json.
Remove the [FromBody] attribute and try the solution. It should work for non-primitive types. (student)
With the [FromBody] attribute, the other option is to send the values in =Value format, rather than key=value format. This would mean your key value of student should be an empty string...
There are also other options to write a custom model binder for the student class and attribute the parameter with your custom binder.
I was looking for a solution to my problem for some minutes now, so I'll share my solution.
When you have a custom constructor within your model, your model also needs to have an empty/default constructor. Otherwise the model can't be created, obviously.
Be careful while refactoring.
I spend several hours with this issue... :( Getters and setters are REQUIRED in POST parameters object declaration. I do not recommend using simple data objects (string,int, ...) as they require special request format.
[HttpPost]
public HttpResponseMessage PostProcedure(EdiconLogFilter filter){
...
}
Does not work when:
public class EdiconLogFilter
{
public string fClientName;
public string fUserName;
public string fMinutes;
public string fLogDate;
}
Works fine when:
public class EdiconLogFilter
{
public string fClientName { get; set; }
public string fUserName { get; set; }
public string fMinutes { get; set; }
public string fLogDate { get; set; }
}
If the any of values of the request's JSON object are not the same type as expected by the service then the [FromBody] argument will be null.
For example, if the age property in the json had a float value:
"age":18.0
but the API service expects it to be an int
"age":18
then student will be null. (No error messages will be sent in the response unless no null reference check).
This is a little old one and my answer will go down to the last place but even so I would like to share my experience.
Tried every suggestion but still having the same "null" value in a PUT [FromBody].
Finally found it was all about Date format while JSON serializing the EndDate property of my Angular Object.
No error was thrown, just received an empty FromBody object....
If using Postman, make sure that:
You have set a "Content-Type" header to "application/json"
You are sending the body as "raw"
You don't need to specify the parameter name anywhere if you are using [FromBody]
I was stupidly trying to send my JSON as form data, duh...
UPDATE: A practical solution is writing a custom JSON formatter. For a general description of the problem (but no practical solution), read on here.
TL;DR: Don't use [FromBody], but roll your own version of it with better error handling. Reasons given below.
Other answers describe many possible causes of this problem. However, the root cause is that [FromBody] simply has terrible error handling, which makes it almost useless in production code.
For example, one of the most typical reasons for the parameter to be null is that the request body has invalid syntax (e.g., invalid JSON). In this case, a reasonable API would return 400 BAD REQUEST, and a reasonable web framework would do this automatically. However, ASP.NET Web API is not reasonable in this regard. It simply sets the parameter to null, and the request handler then needs "manual" code to check if the parameter is null.
Many of the answers given here are therefore incomplete with regards to error handling, and a buggy or malicious client may cause unexpected behavior on the server side by sending an invalid request, which will (in the best case) throw a NullReferenceException somewhere and return an incorrect status of 500 INTERNAL SERVER ERROR or, worse, do something unexpected or crash or expose a security vulnerability.
A proper solution would be to write a custom "[FromBody]" attribute which does proper error handling and returns proper status codes, ideally with some diagnostic information to aid client developers.
A solution that might help (not tested yet) is to make parameters required, as follows: https://stackoverflow.com/a/19322688/2279059
The following clumsy solution also works:
// BAD EXAMPLE, but may work reasonably well for "internal" APIs...
public HttpResponseMessage MyAction([FromBody] JObject json)
{
// Check if JSON from request body has been parsed correctly
if (json == null) {
var response = new HttpResponseMessage(HttpStatusCode.BadRequest) {
ReasonPhrase = "Invalid JSON"
};
throw new HttpResponseException(response);
}
MyParameterModel param;
try {
param = json.ToObject<MyParameterModel>();
}
catch (JsonException e) {
var response = new HttpResponseMessage(HttpStatusCode.BadRequest) {
ReasonPhrase = String.Format("Invalid parameter: {0}", e.Message)
};
throw new HttpResponseException(response);
}
// ... Request handling goes here ...
}
This does (hopefully) proper error handling, but is less declarative. If, for example, you use Swagger to document your API, it will not know the parameter type, which means you need to find some manual workaround to document your parameters. This is just to illustrate what [FromBody] should be doing.
EDIT: A less clumsy solution is to check ModelState: https://stackoverflow.com/a/38515689/2279059
EDIT: It appears that ModelState.IsValid is not, as one would expect, set to false if using JsonProperty with Required = Required.Always and a parameter is missing. So this is also useless.
However, in my opinion, any solution that requires writing additional code in every request handler is unacceptable. In a language like .NET, with powerful serialization capabilities, and in a framework like ASP.NET Web API, request validation should be automatic and built-in, and it is totally doable, even though Microsoft does not provide the necessary built-in tools.
It can be helpful to add TRACING to the json serializer so you can see what's up when things go wrong.
Define an ITraceWriter implementation to show their debug output like:
class TraceWriter : Newtonsoft.Json.Serialization.ITraceWriter
{
public TraceLevel LevelFilter {
get {
return TraceLevel.Error;
}
}
public void Trace(TraceLevel level, string message, Exception ex)
{
Console.WriteLine("JSON {0} {1}: {2}", level, message, ex);
}
}
Then in your WebApiConfig do:
config.Formatters.JsonFormatter.SerializerSettings.TraceWriter = new TraceWriter();
(maybe wrap it in an #if DEBUG)
I was also trying to use the [FromBody], however, I was trying to populate a string variable because the input will be changing and I just need to pass it along to a backend service but this was always null
Post([FromBody]string Input])
So I changed the method signature to use a dynamic class and then convert that to string
Post(dynamic DynamicClass)
{
string Input = JsonConvert.SerializeObject(DynamicClass);
This works well.
After Three days of searching and none of above solutions worked for me , I found another approach to this problem in this Link:
HttpRequestMessage
I used one of the solutions in this site
[HttpPost]
public async System.Threading.Tasks.Task<string> Post(HttpRequestMessage request)
{
string body = await request.Content.ReadAsStringAsync();
return body;
}
Just to add my history to this thread.
My model:
public class UserProfileResource
{
public Guid Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Phone { get; set; }
public UserProfileResource()
{
}
}
The above object couldn't be serialized in my API Controller and would always return null. The issue was with Id of type Guid: everytime I passed empty string as an Id (being naive that it will automatically be converted to Guid.Empty) from my frontend I received null object as [FromBody] paramether.
Solution was either to
pass valid Guid value
or change Guid to String
In my case the problem was the DateTime object I was sending. I created a DateTime with "yyyy-MM-dd", and the DateTime that was required by the object I was mapping to needed "HH-mm-ss" aswell. So appending "00-00" solved the problem (the full item was null because of this).
I've hit this problem so many times, but actually, it's quite straightforward to track down the cause.
Here's today's example. I was calling my POST service with an AccountRequest object, but when I put a breakpoint at the start of this function, the parameter value was always null. But why ?!
[ProducesResponseType(typeof(DocumentInfo[]), 201)]
[HttpPost]
public async Task<IActionResult> Post([FromBody] AccountRequest accountRequest)
{
// At this point... accountRequest is null... but why ?!
// ... other code ...
}
To identify the problem, change the parameter type to string, add a line to get JSON.Net to deserialize the object into the type you were expecting, and put a breakpoint on this line:
[ProducesResponseType(typeof(DocumentInfo[]), 201)]
[HttpPost]
public async Task<IActionResult> Post([FromBody] string ar)
{
// Put a breakpoint on the following line... what is the value of "ar" ?
AccountRequest accountRequest = JsonConvert.DeserializeObject<AccountRequest>(ar);
// ... other code ...
}
Now, when you try this, if the parameter is still blank or null, then you simply aren't calling the service properly.
However, if the string does contain a value, then the DeserializeObject should point you towards the cause of the problem, and should also fail to convert your string into your desired format. But with the raw (string) data which it's trying to deserialize, you should now be able to see what's wrong with your parameter value.
(In my case, we were calling the service with an AccountRequest object which had been accidentally serialized twice !)
This is another issue related to invalid property values in an Angular Typescript request.
This is was related to the conversion between a Typescript number to an int(Int32) in C#. I was using Ticks (UTC milliseconds) which is larger than the signed, Int32 range (int in C#). Changed the C# model from int to long and everything worked fine.
I had the same problem.
In my case, the problem was in public int? CreditLimitBasedOn { get; set; } property I had.
my JSON had the value "CreditLimitBasedOn":true when It should contain an integer. This property prevented the whole object being deserialized on my api method.
Maybe for someone it will be helpful: check the access modifiers for your DTO/Model class' properties, they should be public. In my case during refactoring domain object internals were moved to DTO like this:
// Domain object
public class MyDomainObject {
public string Name { get; internal set; }
public string Info { get; internal set; }
}
// DTO
public class MyDomainObjectDto {
public Name { get; internal set; } // <-- The problem is in setter access modifier (and carelessly performed refactoring).
public string Info { get; internal set; }
}
DTO is being finely passed to client, but when the time comes to pass the object back to the server it had only empty fields (null/default value). Removing "internal" puts things in order, allowing deserialization mechanizm to write object's properties.
public class MyDomainObjectDto {
public Name { get; set; }
public string Info { get; set; }
}
Check if JsonProperty attribute is set on the fields that come as null - it could be that they are mapped to different json property-names.
If this is because Web API 2 ran into a deserialization problem due to mismatched data types, it's possible to find out where it failed by inspecting the content stream. It will read up until it hits an error, so if you read the content as a string, you should have the back half of the data you posted:
string json = await Request.Content.ReadAsStringAsync();
Fix that parameter, and it should make it further next time (or succeed if you're lucky!)...
I used HttpRequestMessage and my problem got solved after doing so much research
[HttpPost]
public HttpResponseMessage PostProcedure(HttpRequestMessage req){
...
}
In my case, using postman I was sending a DateTime with invalid separators (%) so the parse failed silently.
Be sure you are passing valid params to your class constructor.
None of the above was my solution: in my case the issue is that [ApiController] was not added to the controller so it is giving Null value
[Produces("application/json")]
[Route("api/[controller]")]
[ApiController] // This was my problem, make sure that it is there!
public class OrderController : Controller
...
I just ran into this and was frustrating.
My setup:
The header was set to
Content-Type: application/JSON
and was passing the info from the body with JSON format, and was reading [FromBody] on the controller.
Everything was set up fine and I expect it to work, but the problem was with the JSON sent over. Since it was a complex structure, one of my classes which was defined 'Abstract' was not getting initialized and hence the values weren't assigned to the model properly. I removed the abstract keyword and it just worked..!!!
One tip, the way I could figure this out was to send data in parts to my controller and check when it becomes null... since it was a complex model I was appending one model at a time to my request params. Hope it helps someone who runs into this stupid issue.
Seems like there can be many different causes of this problem...
I found that adding an OnDeserialized callback to the model class caused the parameter to always be null. Exact reason unknown.
using System.Runtime.Serialization;
// Validate request
[OnDeserialized] // TODO: Causes parameter to be null
public void DoAdditionalValidatation() {...}
I had this problem in my .NET Framework Web API, because my model existed in a .NET Standard project that referenced a different version of data annotations.
Adding the ReadAsAsync line below highlighted the cause for me:
public async Task<HttpResponseMessage> Register(RegistrationDetails registrationDetails)
{
var regDetails = await Request.Content.ReadAsAsync<RegistrationDetails>();
In my case (.NET Core 3.0) I had to configure JSON serialization to resolve camelCase properties using AddNewtonsoftJson():
services.AddMvc(options =>
{
// (Irrelevant for the answer)
})
.AddNewtonsoftJson(options =>
{
options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
});
Do this in your Startup / Dependency Injection setup.
I was struggling with this for hours today. I could see that there was data in the response body because of the length, but any time I tried to read the data, I got an empty string, or the arguments in the parameter list for the method returned null. I compared this controller to another that was already working and found that I was missing the ApiController attribute for the class declaration. I also removed the FromBody attribute from my parameter declaration. I am not sure when that was added, but I am using .Net 5.0.
As detailed in my other answer, the problem is with error handling in the [FromBody] attribute, and you cannot do much about that without writing your own version of it.
However, a general solution that will improve error handling in your entire API without making changes in any controller or action is to write a custom JSON formatter (derived from FotoWareApiJsonFormatter) which handles serialization errors properly.
I will not present the entire solution here, but the important part is to catch JsonSerializationException and JsonReaderException in the formatter and make sure the endpoint will return 400 Bad Request as a result.
This ensures that if the request contains invalid JSON, or the JSON does not fulfill model constraints (such as missing required properties, type errors, etc.), the API will automatically return 400 Bad Request before your controller action is called, so you do not need to write extra error handling in the controller, and your parameter using [FromBody] will never be null.
// in JSON formatter class
private object Deserialize(Stream readStream, Type type)
{
try
{
var streamReader = new StreamReader(readStream);
return GetSerializer().Deserialize(streamReader, type);
}
catch (JsonSerializationException e)
{
// throw another exception which results in returning 400 Bad Request
}
catch (JsonReaderException e)
{
// throw another exception which results in returning 400 Bad Request
}
}
You also have to ensure that your custom JSON formatter is the only formatter, e.g., by adding this code to Application_Start():
var jsonFormatter = new MyJsonFormatter();
// Always return JSON, and accept JSON only
GlobalConfiguration.Configuration.Formatters.Clear();
GlobalConfiguration.Configuration.Formatters.Add(jsonFormatter);
This assumes that your API accepts and returns JSON only, as most modern APIs do. Offering XML or other formats as an alternative if you are not going to test or advertise it is unnecessary at best and a potential security risk at worst.
Be careful when introducing this to an existing API, as it can introduce some unexpected breaking changes, so good testing is advised. Consider it as a cleanup of the error handling in your API.
Just one more thing to look at... my model was marked as [Serializable] and that was causing the failure.