Json.NET does not recognize Data Annotations and allows the data through - c#

I have the following code:
public class EventController : ApiController
{
//public IHttpActionResult Post(List<Event> Events)
public IHttpActionResult Post(Newtonsoft.Json.Linq.JArray J)
{
//Debug.WriteLine(J.ToString());
List<Event> Events = Newtonsoft.Json.JsonConvert.DeserializeObject<List<Event>>(J.ToString(), new Newtonsoft.Json.JsonSerializerSettings {
Error = delegate(object sender, ErrorEventArgs args) {
Debug.WriteLine(args.ErrorContext.Error.Message);
args.ErrorContext.Handled = true;
},
Converters = { new IsoDateTimeConverter() }
}
);
foreach (Event Event in Events)
{
Debug.WriteLine(Event.Importance.ToString());
Debug.WriteLine(Event.Date.ToString());
Debug.WriteLine(Event.Description);
}
}
}
public class Event
{
[DataAnnotationsExtensions.Integer(ErrorMessage = "{0} must be a number.")]
[Range(0,10),Required]
public Int32 Importance { get; set; }
//[OnConversionError: "Please enter a valid date."]
[Required]
[DataAnnotationsExtensions.Date]
public object Date { get; set; }
[RegularExpression(#"^.{20,100}$", ErrorMessage="{0} must be between 20 and 100 characters.")]
[Required]
public string Description { get; set; }
}
I'm posting:
[{"Importancee":"adasdasd","Date":"2005-10-32","Descriptione":""},
{"Importance":"6.0","Date":"2015-10-02","Description":"a"}]
"Importance" is misspelled on purpose to simulate the scenario of missing data. When I post this I expect the delegate function to capture the invalid data and let me know the required fields are missing. I also expect the Regular Expression used for Description to cause an error for the 1 character description "a". Instead Json.net's Deserializer skips the missing fields and sets those properties to null and it sets the 2nd Description property to the "a" string. It's completely ignoring the Data Annotations. Is there any way to get Json.NET to recognize the Annotations?

You could generate a JSchema from the Data Annotation attributes:
http://www.newtonsoft.com/jsonschema/help/html/GenerateWithDataAnnotations.htm
And validate them them using:
http://www.newtonsoft.com/json/help/html/JsonSchema.htm
Data Annotations will not work directly, but with little effort, I believe you can get what you need.

Related

.NET Core API - Updating all fields while updating with DTO object

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.

.NET Core Razor event after Model Binding for modifications?

What I'd like to achive is to be able to modiy certain (string) values after they were binded to a property but they are being validated in .NET Core 3.1.
Example poco class:
public class MyPoco
{
[TrimContent]
[MinLength(2)]
public string? FirstName { get; set; }
[TrimContent]
[MinLength(2)]
public string? Surname { get; set; }
[TrimContent]
[LowerCase]
public string? EmailAddress { get; set; }
}
So let's say a form is posted to the MVC controller and the values entered are
" F " for the first name and " S " as the surname, " My.Email#Address.Com ".
They should be modified, i.e. trimmed to "F" and "S" and the MinLength=2 should be alerted i.e. Also: I can avoid all the Trim() statements in my code.
My idea is, that when using a "TrimContentAttribute" (and other attributes that "correct" the values in some way), all values that have been set by previous BindingSourceValueProviders and then are being processed, but before the validation kicks in.
Also attributes marked with LowerCase, should automatically be "ToLower()", so the email address would be "my.email#address.com".
So it the idea would be to declrative approch other than having all the Trim() und ToLowerCase() methods all over the code where the entity is used.
The only idea I came up with so far to write a custom source as described in
Model Binding in ASP.NET Core - Additional sources. But I actually would like to rely on all the default values providers.
Note: There are validators on client side in action as well, but I'd like to have a solution also on the server side.
a new attribute can be created
public class MinLengthWithTrim : MinLengthAttribute
{
public MinLengthWithTrim(int length) : base(length)
{
}
public override bool IsValid(object? value)
{
var str = value as string;
if (str == null)
{
return false;
}
return base.IsValid(str.Trim());
}
}
Using:
[MinLengthWithTrim(10)]
public string Name { get; set; }

How to validate business object in ASP.NET Core MVC

I want to create a small application for inserting Amount Date note Account_Name into table transaction.
I don't want to create an action method for this, and want to use from CLI.
My business object looks like this:
public class ExpenseBO : Controller
{
public void MakeExpense(MakeExpensePayload payload)
{
var transactionAccess = new TransactionAccessController(connection);
transactionAccess.MakeTransaction(payload);
}
}
In access layer:
public void MakeTransaction(MakeExpensePayload p)
{
connection.Insert(new { p.Amount, p.Date, p.Note });
}
Model MakeExpensePayload:
public class MakeExpensePayload
{
public int Amount { get; set; }
public string note { get; set; }
public DateTime Date { get; set; }
}
I want to validate Amount, Date, note, AccountName - so for instance, Amount cannot be negative, note should not be empty (i.e., make note field required). The Date is not mandatory to provide
As I am not using action methods here, I cannot validate using model validation and data annotations.
So, where should I add validations in all these structures and how can I validate these?
If you want server side validations, I consider you can do it individually. For example:
You can create a private method where you can do validations and throw an Exception to client side to inform that this field is required :
private void GeneralValidations(MakeExpensePayload payload)
{
if(payload.Amount <= 0)
//Throw new HttpException
if(string.IsNullorEmpty(payload.Note))
//Throw new HttpException
}
then, call it into your method:
public void MakeExpense(MakeExpensePayload payload)
{
GeneralValidations(payload);
var transactionAccess = new TransactionAccessController(connection);
transactionAccess.MakeTransaction(payload);
}
The rule of thumb is you should validate your data Once you receive it and before you start processing it, this will help neutralizing any possible threats
Client side validation is not enough as it can be bypassed, you should do validation also on server side
You can add Add validation conditions When you create model and use ModelState.IsValid to validate all models at once in service.
Model
public class MakeExpensePayload
{
[Range(0, int.MaxValue)]
[Required(ErrorMessage = "Amount cannot be negative")]
public int Amount { get; set; }
[Required]
public string note { get; set; }
public DateTime? Date { get; set; }
}
service
private void GeneralValidations(MakeExpensePayload payload)
{
if (ModelState.IsValid)
{
//do your logic
}
return View(model);
}
Then you can see If any properties in model validation fails, ModelState.IsValid will return false.

ViewModel with dynamic elements

I need to receive the next JSON in .NET
"currentData":
{
"Name": {"system": "wfdss", "canWrite": true },
"DiscoveryDateTime": { "system": "wfdss", "canWrite": true },
"Code": { "system": "code", "canWrite": false },
...
}
This elements are dynamics, it doesn't have default elements, so, how can I define a class doing that following next model:
public class currentData
{
//TODO
//<Data Element Name>: {
//data element system: <STRING of system>,
//the last system to update data element canWrite: <Boolean>
//true if requesting system may edit data element (based on ADS), otherwise false. }, ...
public List<Property> property { get; set; }
}
public class Property
{
public string system { get; set; }
public string canWrite { get; set; }
}
If you need to post dynamic structured Json to controller i have a bad news for you - you can't map it automattically in MVC. MVC model binding mechanism work only with stronly typed collecions - you must know structure.
One of the options that i can suggest you if use FormCollection and manually get values from it:
[HttpPost]
public JsonResult JsonAction(FormCollection collection)
{
string CurrentDataNameSystem = collection["currentData.Name.system"];
// and so on...
return Json(null);
}
Another option is to pass you dynamic json as string and then manually desirialize it:
[HttpPost]
public JsonResult JsonAction(string json)
{
//You probably want to try desirialize it to many different types you can wrap it with try catch
Newtonsoft.Json.JsonConvert.DeserializeObject<YourObjectType>(jsonString);
return Json(null);
}
Anyway my point is - you shouldn't mess with dynamic json unless you really need it in MVC.
I suggest you to creage object type that contain all the passible fields but make it all nullable so you can pass your Json and it will be mapped with Model binding MVC mechanism but some fields will be null.
I think the type format you are getting is an Object with a Dictionary.
So i think you need to Deserialize your Data into this.
public class ContainerObject
{
public Dictionary<String,Property> currentData { get; set; }
}

WebAPI - Array of Objects not deserializing correctly on server side

In the client-side, I am using AngularJS and in the server-side I am using ASP.NET WebAPI.
I have two view models, ProductCriteriaViewModel and SimpleDisplayFieldViewModel:
public class ProductCriteriaViewModel
{
public int ID { get; set; }
public int? UserSearchID { get; set; }
public bool? Enabled { get; set; }
public SimpleDisplayFieldViewModel Property { get; set; }
public string Operator { get; set; }
public string CriteriaValue { get; set; }
}
public class SimpleDisplayFieldViewModel
{
public string Name { get; set; }
public string Value { get; set; }
public string PropertyType { get; set; }
}
In Angular, I submit a POST request to a WebAPI controller action with the following signature:
public IList<...> FindProducts(List<ProductCriteriaViewModel> criteriaVM, bool userFiltering)
{
...
}
In testing, I tried to send an array of Product Criterias, and checked Fiddler to see what the array looked like in the body of the POST request when it was being sent to the server. This is what the array looked like:
[
{"Enabled":true,
"Operator":"Less than",
"Property":
{"$id":"2",
"Name":"Copyright Year",
"Value":"Basic",
"PropertyType":null},
"CriteriaValue":"2013",
"IsNew":true},
{"Enabled":true,
"Operator":"Greater Than",
"Property":
{"$id":"2",
"Name":"Copyright Year",
"Value":"Basic",
"PropertyType":null},
"CriteriaValue":"1988",
"IsNew":true}
]
The above array has the correct values, however the result of deserialization on the server-side is incorrect. This is where it gets strange.
After the server deserializes the array and arrives in the controller action, the first element in criteriaVM is correct, all the values are set properly. However the second element is incorrect, CriteriaValue and Property are nulled out:
This issue only occurs whenever I choose the same search property for more than one criteria (i.e. Copyright < 2013 and Copyright > 1988). However, if I choose different properties (i.e. Copyright < 2013 and Price > 20), then all elements in the resulting criteriaVM are correctly initialized.
I do not understand what could be causing this issue. Why are only CriteriaValue and Property set to null in the second element of the List? Why does this issue only occur when I choose multiples of the same search properties?
Json.NET uses the keywords $id and $ref in order to preserve object references, so you are having troubles with your deserialization because your JSON has "$id" in the "Property" object. See this link for more information about object references.
In order to fix your deserialization issues, you can add the following line in the Register method of your WebApiConfig.cs class
config.Formatters.JsonFormatter.SerializerSettings.MetadataPropertyHandling = MetadataPropertyHandling.Ignore;
If your Web Api project does not include a WebApiConfig.cs class, simply add the configuration in your Global.asax:
GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings.MetadataPropertyHandling = MetadataPropertyHandling.Ignore;
Now your object in the web api method should look like this:

Categories