asp.net core custom model binder just for one property - asp.net-core

I have a simple model for my asp.net core controller:
[HttpPost]
public async Task<DefaultResponse> AddCourse([FromBody]CourseDto dto)
{
var response = await _courseService.AddCourse(dto);
return response;
}
My model is :
public class CourseDto
{
public int Id { get; set; }
public string Name { get; set; }
public string Genre { get; set; }
public string Duration { get; set; }
public string Level { get; set; }
public string AgeRange { get; set; }
public string Notes { get; set; }
public bool Active { get; set; }
public string OrganisationCode { get; set; }
}
I'm trying to set value of "OrganisationCode" using a custom mode binder or action filter, but had no success.
I would be thnakful if you advise whats the right way to updat ethe model before executing the action.
Thanks.

I will show you here a very simple custom model binder I have just written (and tested in .Net Core 2.0):
My model binder:
public class CustomModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
var value = valueProviderResult.FirstValue; // get the value as string
var model = value.Split(",");
bindingContext.Result = ModelBindingResult.Success(model);
return Task.CompletedTask;
}
}
My model (and notice, only one property has my custom model binder annotation):
public class CreatePostViewModel
{
[Display(Name = nameof(ContentText))]
[MinLength(10, ErrorMessage = ValidationErrors.MinLength)]
public string ContentText { get; set; }
[BindProperty(BinderType = typeof(CustomModelBinder))]
public IEnumerable<string> Categories { get; set; } // <<<<<< THIS IS WHAT YOU ARE INTERESTER IN
#region View Data
public string PageTitle { get; set; }
public string TitlePlaceHolder { get; set; }
#endregion
}
What it does is: it receives some text like "aaa,bbb,ccc", and converts it into array, and return it to the ViewModel.
I hope that helps.
DISCLAIMER: I am not an expert in model binders writing, I have learnt that 15 minutes ago, and I found your question (with no helpful answer), so I tried to help. This is a very basic model binder, some improvements are surely required. I learned how to write it from the official documentation page.

The [FromBody] attribute you are using on the action parameter. means that you direct the default behavior of Model Binding to use the formatters instead. That is why your custom Model Binder does not work.
And [FromBody] is reading the content (request body). So you won't get the request body from your Action Filter, as the request body is a non-rewindable stream, so it suppose to be read only once (I'm assuming that you are trying to read the request body from Action Filter).
My suggestion is to use your custom model binder and remove the FromBody Attribute.

Related

how to send array to API which contains image and other data in .net core

When I am passing a single object like below then it is working as per below image
[HttpPost]
public async Task<ActionResult> Post([FromForm] MyModel Details)
{
}
but when I am passing the List of the object to API then it is not working. option to upload a file is not visible. and if I entered any values in the array then also I am getting count 0 for details.
[HttpPost]
public async Task<ActionResult> Post([FromForm] List<MyModel> Details)
{}
I want to pass the List of images and descriptions to API. How can I achieve it?
Thanks in advance!
You need custom model binding for the list model . Here is a similar demo:
custom model binding code:
public class MetadataValueModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
var values = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (values.Length == 0)
return Task.CompletedTask;
var options = new JsonSerializerOptions() { PropertyNameCaseInsensitive = true };
var deserialized = JsonSerializer.Deserialize(values.FirstValue, bindingContext.ModelType, options);
bindingContext.Result = ModelBindingResult.Success(deserialized);
return Task.CompletedTask;
}
}
Add the model binder to the model class:
public class MasterDTO
{
public string Comments { get; set; }
public IFormFile File { get; set; }
public List<DetailDTO> Details { get; set; }
public MasterDTO()
{
this.Details = new List<DetailDTO>();
}
}
[ModelBinder(BinderType = typeof(MetadataValueModelBinder))]
public class DetailDTO
{
public Int64 ElementId { get; set; }
public double LowerLimit { get; set; }
public double HigherLimit { get; set; }
public string Status { get; set; }
public string UserAuthorization { get; set; }
public DateTime? AutorizationDate { get; set; }
}
controller/action
[HttpPost]
public async Task<IActionResult> CreateProjectLimit([FromForm] MasterDTO masterDto)
{
//...
return Ok();
}
You can just use postman to pass the list of images and Descriptions to API
Below is the right answer. we can use Postman to pass images in the array as shown below.

ASP.NET Core Webapi (3.x) Custom ModelBinder Always Returns Empty Object

I've been attempting to create a custom model binder and am running into an issue where the object after going through the binder is ALWAYS empty.
I have a good reason for using a custom model binder. For the purposes of this question, assume I HAVE to use a custom model binder. In every case where i've used a custom binder provider with either stock or custom modelbinders I've run into this issue, so I've dumbed this down a LOT to demonstrate, but it happens in this specific example as well, and I really need to know why.
I have a simple controller action and DTO class:
[HttpPost]
[Route("{id}")]
public async Task<ActionResult<QueryServicesDto>> UpdateQueryService(int id,
[FromBody] QueryServicesDtoLight dto)
{
}
public class QueryServicesDtoLight
{
public long QueryServicesId { get; set; }
public DateTime? CreationDate { get; set; }
public long? ModifiedDate { get; set; }
public long? PropagationDate { get; set; }
public int? CabinetListNumber { get; set; }
public string GameCode { get; set; }
public string Status { get; set; }
}
Can anyone tell me why when posting valid JSON to this action with no custom binder providert I get a DTO with the proper values, but if I inject the custom modelBinderProvider below I get a newed up model with no values?
public class QueryServiceModelBinderBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType== typeof(QueryServicesDtoLight))
{
var propertyBinders = new Dictionary<ModelMetadata, IModelBinder>();
for (var i = 0; i < context.Metadata.Properties.Count; i++)
{
ModelMetadata theProp = context.Metadata.Properties[i];
var binder = context.CreateBinder(theProp);
propertyBinders.Add(theProp, binder);
}
var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
return new ComplexTypeModelBinder(propertyBinders, loggerFactory);
}
else
{
return null;
}
}
}
Binder provider is added like this::
services.AddControllersWithViews(o=>o.ModelBinderProviders.Insert(0, new QueryServiceModelBinderBinderProvider())).AddNewtonsoftJson();
Sample JSON Data
{"queryServicesId":14,"creationDate":"2021-03-08T17:06:36.053","modifiedDate":16176433093000000,"propagationDate":0,"cabinetListNumber":996,"gameCode":"PGA2006","status":"AC"}

Custom validation fires if in model class but not on page property

I have a contact Razor page implemented in ASP.NET Core 2.0. I am using model binding and custom validation.
If I use custom validation on a separate model class, the validation method is called. If I use custom validation on a property on the PageModel, the validation method is not called. However, all properties are successfully bound.
Here's the PageModel class and the separate model class:
public class ContactModel : PageModel
{
[BindProperty]
public ContactMessageModel ContactMessageModel { get; set; }
[BindProperty, CustomValidation]
public string SomeData { get; set; }
public IActionResult OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
return RedirectToPage("MessageSent");
}
}
public class ContactMessageModel
{
[Required]
public string Name { get; set; }
[Required]
public string Email { get; set; }
[Required, CustomValidation]
public string Message { get; set; }
}
A test validation attribute class is as follows:
public class CustomValidationAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
return ValidationResult.Success;
}
}
The validation attribute is called for the ContactMessageModel.Message property, but it isn't called for ContactModel.SomeData property.
Why is this and how do I fix it?
I was running into this too. In my case my issue was because I missed the addition of the [BindProperty] attribute when I moved the properties from a wrapper class model into the PageModel directly. I see that's not the case for you.
In your case I do see that you have the [Required] attribute on all properties except the ContactModel.SomeData. Maybe adding [Required] there would get things acting as you would expect?

Map GET request to Action

I'm trying to validate the progress quantity (and other fields once this works) that belongs to the BeginCollectionItems server side. The request is being sent but the parameter progressQty is not being read by the action.
This is the action I'm trying to map to:
[AllowAnonymous]
[AcceptVerbs("Get", "Post")]
public IActionResult CheckValidProgressQty(int progressQty)
{
int a =progressQty;
var result = false;
if (a > 0)
result = true;
return Json(result);
}
This is the request:
:method: GET
:path: /Components/CheckValidProgressQty?ProgressItems%5B16bad1f2-155c-4a29-844c-34e88da80b7c%5D.ProgressQty=-300
This is the Query String Parameters:
ProgressItems[16bad1f2-155c-4a29-844c-34e88da80b7c].ProgressQty: -300
Here is the remote validation in the View Model Class:
[Remote(action: "CheckValidProgressQty", controller: "Components", HttpMethod ="GET", ErrorMessage = "BAD QTY!")]
public int ProgressQty { get; set; }
Right now it goes into the CheckValidProgressQty method but I'm just not able to access the progressQty parameter. One way I can access is:
Request.QueryString.Value
?ProgressItems%5B16bad1f2-155c-4a29-844c-34e88da80b7c%5D.ProgressQty=-8
and parse it. But I think there should be something more simple available.
ProgressItems[16bad1f2-155c-4a29-844c-34e88da80b7c].ProgressQty: -300
This is posted form data when you do POST method not for GET method.
You could not get the query string on your action parameters using ?ProgressItems%5B16bad1f2-155c-4a29-844c-34e88da80b7c%5D.ProgressQty=-300since they are not match.
Refer to my below demo which introduces how to pass querystring to action, assume that I have models:
public class TestUser
{
[Key]
public int Id { set; get; }
public string Name { get; set; }
public IList<UserInterest> Interests
{
get; set;
}
}
public class UserInterest
{
[Key]
public int Id { set; get; }
[Required]
public string InterestText { set; get; }
public int Option { set; get; }
}
You need to use an object like
public ActionResult UserTest(TestUser model)
And the querystring is ?Interests[0].InterestText=hello

WCF with Entity Framework Code First relationship

I'm learning WCF, and tried to make a small service that exposes a Project and its tasks (the standard Entity Framework hello world).
The class structure is the following:
public class Project
{
public int ProjectId { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public DateTime CreationDate { get; set; }
public virtual ICollection<Task> Tasks { get; set; }
}
public class Task
{
public int TaskId { get; set; }
public string Title { get; set; }
public virtual Project RelatedProject { get; set; }
}
The DB context comes after:
public class ProjectContext : DbContext
{
public DbSet<Project> Projects { get; set; }
public DbSet<Task> Tasks { get; set; }
}
Finally, the service endpoint:
public IEnumerable<Project> getProjects()
{
ProjectContext p = new ProjectContext();
return p.Projects.AsEnumerable();
}
The problem is that this model will throw a System.ServiceModel.CommunicationException, but, If I remove the virtual properties from the model, It would work, but I would loose the entity framework links between Project and Task.
Anyone with a similar setup?
I banged my head against the wall several hours with this one. After extensive debugging, google gave the answer and I feel right to post it here since this was the first result I got in google.
Add this class on top of your [ServiceContract] interface declaration (typically IProjectService.cs
public class ApplyDataContractResolverAttribute : Attribute, IOperationBehavior
{
public void AddBindingParameters(OperationDescription description, BindingParameterCollection parameters)
{
}
public void ApplyClientBehavior(OperationDescription description, System.ServiceModel.Dispatcher.ClientOperation proxy)
{
var dataContractSerializerOperationBehavior =
description.Behaviors.Find<DataContractSerializerOperationBehavior>();
dataContractSerializerOperationBehavior.DataContractResolver =
new ProxyDataContractResolver();
}
public void ApplyDispatchBehavior(OperationDescription description, System.ServiceModel.Dispatcher.DispatchOperation dispatch)
{
var dataContractSerializerOperationBehavior =
description.Behaviors.Find<DataContractSerializerOperationBehavior>();
dataContractSerializerOperationBehavior.DataContractResolver =
new ProxyDataContractResolver();
}
public void Validate(OperationDescription description)
{
// Do validation.
}
}
Requirements are
using System.ServiceModel.Description;
using System.Data.Objects;
using System.ServiceModel.Channels;
Then under the [OperationContract] keyword add [ApplyDataContractResolver] keyword and you are set!
Big thanks to http://blog.rsuter.com/?p=286
For sending data trough WCF you should disable lazy loading (dataContext.ContextOptions.LazyLoadingEnabled = false;).
To be sure the data you want is loaded you need to use eager loading ( trough the Include method).
You need to change your function to:
public IEnumerable<Project> getProjects()
{
ProjectContext p = new ProjectContext();
p.ContextOptions.LazyLoadingEnabled = false;
return p.Projects.Include("Tasks").AsEnumerable();
}