I think I might be misunderstanding how the TryUpdateModelAsync method works, I have a process where multipart form data is passed into mt controller in a PUT.
I have a requirement to trigger the model binding, use a custom model binder to pull the form data out and manipulate it as required and make it available for further processing.
I have tried to simplify the process for the purpose of this example and this is the simplest I can get :
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
[HttpPut]
public async Task<IActionResult> Upload()
{
var mod = new TestModel();
await base.TryUpdateModelAsync(mod, "Model");
return new OkResult();
}
}
[ModelBinder(BinderType = typeof(TestModelBinder))]
public class TestModel
{
public string Name;
}
public class TestModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
bindingContext.Result = ModelBindingResult.Success(new TestModel()
{
Name = "Some Value"
});
return Task.CompletedTask;
}
}
So the process is
Client does a PUT to the Upload action.
upload action creates a new instance of a class (in this example the "TestModel")
we call TryUpdateModelAsync passing in the new (empty) model.
What I expected was for the name on the mod variable to be set to "Some Value" as that's what I have hard-coded the model binder to do, but it doesn't seem to work.
What am I doing wrong?
As far as I know, the TryUpdateModelAsync method is used for Manual model binding, it will not trigger custom model binder. Since the custom model binder has happened before you called TryUpdateModelAsync method. The TryUpdateModelAsync method will not recalled the custom model binder BindModelAsync method.
In my opinion, if you want to use Manual model binding, you could directly set the binder value to Some Value.
Related
One of the most popular books on ASP.NET Core is "Pro ASP.NET Core 3" by Adam Freeman.
In chapters 7-11, he builds an example application, SportsStore.
The Index method of the HomeController shows a list of products:
Here's the Index method:
public ViewResult Index(int productPage = 1)
=> View(new ProductsListViewModel {
Products = repository.Products
.OrderBy(p => p.ProductID)
.Skip((productPage - 1) * PageSize)
.Take(PageSize),
PagingInfo = new PagingInfo {
CurrentPage = productPage,
ItemsPerPage = PageSize,
TotalItems = repository.Products.Count()
}
});
The view file that corresponds to this method is Views\Home\Index.cshtml. This file has the following line at the top:
#model ProductsListViewModel
So the view is expecting an object of type ProductsListViewModel. However, in Visual Studio, IntelliSense shows View as expecting an argument of type object:
I'm surprised that View here isn't shown to expect an object of type ProductsListViewModel. Since it's setup to accept an argument of type object, we can actually pass in some nonsensical value:
public ViewResult Index(string category, int productPage = 1) =>
View(10);
and the project will still compile!
Is there a way to set things up so that View actually only accepts the model type specified in the view file? I.e. in this case, set things up so that View only accepts ProductsListViewModel?
Thanks!
By using generic, I did the following.
//This is the Base class for every model must inherit.
public class EntityModel
{
public int ID { get; set; }
}
//Model / view model must inherit from EntityModel
public class EmployeeModel : EntityModel
{
public string Name { get; set; }
}
Create a new ViewController which accepts only the EntityModel or its derived class.
Mark the View and its overloaded method obsolete so that only the entity Model calls as a parameter can only be used. Also if anyone uses it show restrict them for using it. Therefore throw error.
public class ViewController<T> : Controller where T: EntityModel
{
[Obsolete]
public override ViewResult View()
{
return base.View();
}
[Obsolete]
public override ViewResult View(object model)
{
throw new Exception("Use view method which accepts EntityModel");
}
[Obsolete]
public override ViewResult View(string viewName)
{
throw new Exception("Use view method which accepts EntityModel");
}
[Obsolete]
public override ViewResult View(string viewName, object model)
{
throw new Exception("Use view method which accepts EntityModel");
}
public new ViewResult View(T model)
{
return base.View(model);
}
}
Use the newly created ViewController in your Home Controller.
public class HomeController : ViewController<EmployeeModel>
{
public IActionResult Index()
{
EmployeeModel emp = new EmployeeModel();
emp.ID = 1;
emp.Name = "Satish Pai";
return View(emp);
}
}
I don't think there is a way to catch any wrong view models got passed into views at compile time!
The problem
The view is strongly typed with the view model you declare on the top so it knows what model is coming in, but the controller doesn't know which view you want it to return to...
By default, yes the controller is going to return to a view that has the same name as the method, but you can change that default, and you can even pass the name of the view as string parameter to one of the View() overloads:
public IActionResult Index(string category, int page = 1)
{
...
return View("OutOfStock", vm);
}
Now Visual Studio doesn't know which view model you want the controller to build and pass to the view. In fact, Visual Studio doesn't even know what view I want to return. Even after I put "OutOfStock" as the view name, Visual Studio doesn't know whether the view even exists or not...
Using Generic
#Satish's solution is indeed interesting but it assumes you are only working with 1 single view model for a single controller, which normally isn't the case. Usually you will have different view models for different actions.
If Generic were the way to go, I would suggest to put it on the action, rather on the controller:
public abstract class BaseController : Controller
{
public ViewResult View<T>(T viewModel) where T : new()
{
return View(viewModel);
}
}
Then you can use it like this in the controller:
public class ProductController : BaseController
{
public IActionResult Index(string category, int page = 1)
{
var vm = new ProductListViewModel
{
...
};
return View<ProductListViewModel>(vm);
// This would give you the compile time error!
// return View<ProductListViewModel>(10);
}
}
But why? What's the point of doing this? You, as the developer, have to know ProductListViewModel is the right view model to pass anyway. Putting something like this in place would be only helpful if there is a junior or new hire who's working on your code and doesn't bother to check the view model the returned view is asking for?
Maybe?
Now I know a tool like Resharper might be able to help and catch the mismatch at compile time.
Also writing unit tests on what the methods in the controller return might be helpful?
I am creating an asp.net core 2.2 api. I have several model classes, most of which contains a string property CreatorId:
public class ModelOne
{
public string CreatorId { get; set; }
//other boring properties
}
My controllers' actions accept these models with [FromBody] attribute
[HttpPost]
public IActionResult Create([FromBody] ModelOne model) { }
I don't want my web client to set the CreatorId property. This CreatorId field appears in tens of classes and I don't want to set it in the controlers actions manually like this:
model.CreatorId = User.Claims.First(claim => claim.Type == "id").Value;
I can't pollute the model classes with any custom model-binding related attributes. I don't want to pollute the controllers or actions with any model binding attributes.
Now, the question is: is there a way to add some custom logic after model is bound to check (maybe with the use of reflection) if model class contains CreatorId field and then to update this value. If it is not possible from ControllerBase User property, than maybe by looking at the jwt token. The process should be transparent for the developer, no attributes - maybe some custom model binder registered at the application level or some middleware working transparently.
Ok, I've invented my own - action filter based solution. It seems to work good. I've created a custom action filter:
public class CreatorIdActionFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ActionArguments.ContainsKey("request")) return;
var request = context.ActionArguments["request"];
var creatorIdProperty = request?.GetType().GetProperties().FirstOrDefault(x => x.Name == "CreatorId");
if (creatorIdProperty == null) return;
var user = ((ControllerBase)context.Controller).User;
creatorIdProperty.SetValue(request, user.Claims.First(claim => claim.Type == "id").Value);
}
public void OnActionExecuted(ActionExecutedContext context) { }
}
which was just registered in ConfigureServices
services.AddMvc(options => { options.Filters.Add(typeof(CreatorIdActionFilter)); })
no other boilerplate code is needed. Parameter name is always "request", that's why I take this from ActionArguments, but of course this can be implemented in more universal way if needed.
The solution works. I have only one question to some experts - can we call this solution elegant and efficient? Maybe I used some bad practices? I will be grateful for any comments on this.
I'm having a problem trying to get custom model binders to work as a query parameter like I have gotten to work previously in .net framework 4.7.
To ensure this wasn't a scenario where my object was too complex, I reduced the model to a simple string but even then I cannot get this to work.
I have a simple model I would like to be binded from query parameters.
public class SearchModel {
public string SearchTerms { get; set; }
}
And I have configured the ModelBinder and ModelBinderProvider as shown here like so.
public class TestModelBinder : IModelBinder {
public Task BindModelAsync(ModelBindingContext bindingContext) {
if (bindingContext.ModelType != typeof(SearchModel)) {
throw new ArgumentException($"Invalid binding context supplied {bindingContext.ModelType}");
}
var model = (SearchModel)bindingContext.Model ?? new SearchModel();
var properties = model.GetType().GetProperties();
foreach(var p in properties) {
var value = this.GetValue(bindingContext, p.Name);
p.SetValue(model, Convert.ChangeType(value, p.PropertyType), null);
}
return Task.CompletedTask;
}
protected string GetValue(ModelBindingContext context, string key) {
var result = context.ValueProvider.GetValue(key);
return result.FirstValue;
}
}
public class TestModelBinderProvider : IModelBinderProvider {
public IModelBinder GetBinder(ModelBinderProviderContext context) {
if (context == null) {
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.ModelType == typeof(SearchModel)) {
var returnType = new BinderTypeModelBinder(typeof(TestModelBinder));
return returnType;
}
return null;
}
}
As stated in the last step in Microsoft documentation I updated my ConfigureServices method in Startup.cs to include the BinderProvider.
services.AddMvc(options => {
options.ModelBinderProviders.Insert(0, new TestModelBinderProvider());
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
But when I call my Search endpoint with a url such as "https://localhost:44387/api/testbinding?searchTerms=newSearch" I am always seeing a return of "request == null True" even though I see it properly hit the custom binding and bind correctly if I step through debugging, can anyone please point me in the right direction as to what I am doing wrong?
[Route("api/[controller]")]
[ApiController]
public class TestBindingController : ControllerBase {
[HttpGet()]
public IActionResult GetResult([FromQuery] SearchModel request) {
return Ok($"request == null {request == null}");
}
}
I think what you're missing if the statement that sets the result of the model binding operation, as you can see in the AuthorEntityBinder code sample in this section of the docs:
bindingContext.Result = ModelBindingResult.Success(model);
Your implementation of the model binder does create an instance of SearchModel, but doesn't feed it back to the model binding context.
As a separate note, I don't think you need to add a custom model binder is the query string segments match the properties names of the model you're trying to bind.
I want to log the each action method parameter name and its
corresponding values in the database as key value pair. As part of
this, I am using OnActionExecuting ActionFilterAttribute, since it
will be the right place (OnActionExecuting method will get invoke for
all controller action methods call) to get Action Executing context.
I am getting the value for .Net types (string, int, bool). But I am
unable to get the value of the User defined types (custom types).
(ex: Login model). My model might have some other nested user
defined types as well.
I was trying to get the values of the user defined types but I am
getting the only class name as string. I hope we can do in
reflection.
Could you please anyone assist to resolve the issue. since I am new
to reflection. It will helpful to me. Thanks in Advance.
I need to get the name and value of these types in OnActionExecuting.
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
ActionParameter = new SerializableDictionary<string,string>();
if(filterContext.ActionParameter != null)
{
foreach(var paramter in filterContext.ActionParameter)
{
//able to get returnUrl value
//unable to get model values
ActionParameter.Add(paramter.Key, paramter.Value);
}
}
}
public ActionResult Login(LoginModel model, string returnUrl)
{
return View(model);
}
User defined type
public class LoginModel
{
public string UserName {get;set;}
public string Password {get;set;}
//User defined type
public UserRequestBase Request {get;set;}
}
//User defined type
public class UserRequestBase
{
public string ApplicationName {get;set;}
}
I am able to get the value of the returnUrl (login method param) in OnActionExecuting but not for model (login method param). I am able to see the values, but don't know how to access it, I used typeof even though I am unable to get it, but I need generic because i have 20 methods in controller so I could not only for LoginModel.
This answer isn't exactly what you want - based on your question - but I think it will work better for what want to accomplish. Quick aside...
Playing around with reflection and nested classes in this instance, lead to some SO (a propos?) errors for me...
So, a better path, maybe? Rather than trying to get/cast the property names, values (types?) from 'context.ActionParameters,` I found it was much easier to let a Json serialization do the work for me. You can then persist the Json object, then deserialize... pretty easy.
Anyway, here's the code:
using Newtonsoft.Json; // <-- or some other serialization entity
//...
public class LogActions : ActionFilterAttribute, IActionFilter
{
// Using the example -- LoginModel, UserRequestBase objects and Login controller...
void IActionFilter.OnActionExecuting(ActionExecutingContext context)
{
var param = (Dictionary<String, Object>)context.ActionParameters;
foreach (var item in param.Values)
{
string itemName = item.GetType().Name.ToString();
string itemToJson = JsonConvert.SerializeObject(item);
// Save JsonObject along with whatever other values you need (route, etc)
}
}
}
Then when you retrieve the Json object from the database you just have to deserialize / cast it.
LoginModel model = (LoginModel)JsonConvert.DeserializeObject(itemToJson, typeof(LoginModel));
From example:
public class LoginModel
{
public string UserName {get;set;}
public string Password {get;set;}
//User defined type
public UserRequestBase Request {get;set;}
}
//User defined type
public class UserRequestBase
{
public string ApplicationName {get;set;}
}
Controller used in example:
public ActionResult Login(LoginModel model, string returnUrl)
{
return View(model);
}
Hope this helps. If there are further issues with this please let me know and I will try to help.
I am curious why the ApiController handles default parameter values on actions differently than a 'regular' Controller.
This code works just fine, request to /Test means page gets value 1
public class TestController : Controller
{
public ActionResult Index(int page = 1)
{
return View(page);
}
}
This code doesn't work when a request is made to /api/Values. It fails with:
"The parameters dictionary contains a null entry for parameter 'page' of non-nullable type 'System.Int32' for method 'System.Collections.Generic.IEnumerable`1[System.String] Get(Int32)' in 'MvcApplication1.Controllers.Controllers.ValuesController'. An optional parameter must be a reference type, a nullable type, or be declared as an optional parameter."
public class ValuesController : ApiController
{
public IEnumerable<string> Get(int page = 1)
{
return new string[] { page.ToString() };
}
}
Any hints on why this is?
Try adding the [FromUri] or [FromForm] parameter attribute.
public class ValuesController : ApiController
{
public IEnumerable<string> Get([FromUri]int page = 1)
{
return new string[] { page.ToString() };
}
}
Mike Stall has two good posts about parameter binding in Webapi which does not work as it does in ASP MVC. The big problem to get used to is that you can only read the request body once in your pipeline. So if you need to read more than 1 complex object as a parameter, you probably need to resort to ModelBinding by parameter. I had a problem similar to yours when I was reading the content body earlier in the pipeline for logging purposes and did not realize about the read once restriction above and had to solve with my own custom model binder.
Explains model binding at http://blogs.msdn.com/b/jmstall/archive/2012/04/16/how-webapi-does-parameter-binding.aspx and then suggests a way to make WebAPI model binding more like ASP MVC http://blogs.msdn.com/b/jmstall/archive/2012/04/18/mvc-style-parameter-binding-for-webapi.aspx
Try defining as Nullable<T>:
public class ValuesController : ApiController
{
public IEnumerable<string> Get(int? page = 1)
{
return new string[] { page.ToString() };
}
}