I'm trying to bind a single parameter of a URL query to a single property in an action method.
The URL is along the lines of:
/user/validate?Some.Prefix.Username=MyUserName
And my controller and action method are along the lines of:
public class UserController : Controller
{
public IActionResult Validate([Bind(Prefix = "Some.Prefix")]string username)
{
return Ok(username);
}
}
However the username parameter is always null when this action method is called. It seems this is a result of using the BindAttribute with a string parameter, rather than a simple class parameter. If I replace the string username parameter with a simple class containing a Username property, I can capture the value as expected.
public class UserController : Controller
{
public IActionResult Validate([Bind(Prefix = "Some.Prefix")]ValidationModel model)
{
return Ok(model);
}
}
public class ValidationModel
{
public string Username { get; set; }
}
Is there any way to bind to a single parameter (rather than a class parameter) when the URL parameter name contains prefixes?
For simple types like int and string, the Bind Prefix overrides the model name for the parameters. Using your action method as an example, the DefaultModelBinder would be looking for the name "Some.Prefix" instead of "username".
So, if you want to work around this and not require a complex type, you need to specify the fully qualified name.
public IActionResult Validate([Bind(Prefix = "Some.Prefix.Username")]string username)
Related
Got a mismatch somewhere between my View and Controller which is causing the latter to receive a complex object, full of null values.
[HttpGet("find")]
[ProducesResponseType(typeof(PagableResults<UserDetails>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[SwaggerOperation("FindUsers")]
public async Task<IActionResult> FindUsers([FromQuery]FindUsersSearchFilter searchFilters)
And the searchFilters object is defined like this:
public class FindUsersSearchFilter
{
public int? Page { get; set; }
public string Username { get; set; }
public string Firstname { get; set; }
public string Surname { get; set; }
}
The View is sending the data in a querystring (because it's a get method) like so:
/find?SearchFilters.Page=1&SearchFilters.Firstname=foo&SearchFilters.Surname=bar&SearchFilters.Username=
However, if you debug the controller action the breakpoint is hit but the FindUsersSearchFilter received by the method has a null value for every property.
Things I've tried:
Binding(Prefix="SearchFilters") on the controller action.
Binding("Page,Firstname,Surname,Username") on the controller action
Manually changing the URL to remove the prefix and change the capitalisation
Removing [FromQuery]
At a loss as to where to go next. Any suggestions as to what I've got wrong?
The request is wrong. It should be:
/find?Page=1&Firstname=foo&Surname=bar&Username=
When you prefix all your properties with SearchFilters the binding engine is most likely looking for a nested property like searchFilters.SearchFilters.FirstName.
So removing the prefix should make it work.
If you really need to use that syntax in the query; then create another class like this:
public class SearchFilterContainer
{
public FindUsersSearchFilter SearchFilters { get; set; } = new FindUsersSearchFilter();
}
And pass that in the action as the parameter instead like this:
[HttpGet("find")]
[ProducesResponseType(typeof(PagableResults<UserDetails>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[SwaggerOperation("FindUsers")]
public async Task<IActionResult> FindUsers([FromQuery]SearchFilterContainer searchFilters)
Then inside your controller you can access the model like this searchFilters.SearchFilters.FirstName
I'm using ASP.NET Core, and attribute routing.
I have this strongly typed model
public Command
{
public int Id { get; set; }
}
And this action
[HttpPost]
[Route("my route here")]
public IActionResult Foo(Command command) { ... }
So route looks like this
.../whatever/foo?id=5
But I want
.../whatever/foo/5
I tried changing the property to int? but that doesn't help.
You can define your route with input parameters in it.
.../whatever/foo/{id}
If you want to access the supplied parameter, you can add it as a method parameter.
public IActionResult Foo(Command command, int id) { ... }
Is there any way to alter the passed object to an action-method before it actually enters/(gets passed to) the action-metod?.. For instance...
By default I want the Index()-action-method to always take an argument of MyClass..
So.. if a user visit mydomain.com/AController/
The triggerd action-method should be
public ActionResult Index(MyClass arg)
{
return View();
}
Im not really sure of how to explain this.. but hopefully you get it..
What I actually want to do is the same thing that Umbraco does in thier MVC-project.. where they always pass-along a RenderModel-object, except that I want to have my own type not the RenderModel..
Thanks in advance!
As I understand your question, you want your action being invoked with a default arg value (if none is provided)
Considering your MyClass as :
public class MyClass
{
public int Id { get; set; }
public string Name { get; set; }
}
You may define your route like :
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{arg}",
defaults: new { controller="Home",
action = "Index",
arg = new MyClass() { Id = 1, Name = "test" }
}
);
I guess another option would be to have a custom ModelBinder or, at lower level, a custom ValueProvider.
Provided your class is simple enough, there should be no need to intercept the call to the action.
For example, if the class has a properties of int, string and DateTime, even IEnumerable<int> etc, then MVC will map form post data or a querystring to the class automatically.
So, a URL like example.com/your-path/?id=1&categories=3,4,56&tags=this,that would automatically map to a class like:
public YourClass
{
public int Id { get; set; }
public IEnumerable<int> Categories { get; set; }
public IEnumerable<string> Tags { get; set; }
}
If you need to do additional work with the data, for example, if you are posting XML/JSON and need to deserialize it before populating your class, then #jbl's answer is the one for you.
From an Umbraco perspective, if you are hijacking your controllers, then you could simply add your class to the Index() action as an additional parameter, e.g. Index(RenderModel model, YourClass arg). This would pick up and populate any additional data pass to your page.
The problem with #jbl's approach with Umbraco is that Umbraco handles the routing for you, so you can not specify your own routing without bypassing Umbraco's controllers entirely - which defeats the purpose of using Umbraco :)
In my ASP.NET MVC app, I have an interface which acts as the template for several different view models:
public interface IMyViewModel
{
Client Client1 { get; set; }
Client Client2 { get; set; }
Validator Validate();
}
So, my view models are defined like this:
public interface MyViewModel1 : IMyViewModel
{
Client Client1 { get; set; }
Client Client2 { get; set; }
// Properties specific to MyViewModel1 here
public Validator Validate()
{
// Do ViewModel-specific validation here
}
}
public interface MyViewModel2 : IMyViewModel
{
Client Client1 { get; set; }
Client Client2 { get; set; }
// Properties specific to MyViewModel2 here
public Validator Validate()
{
// Do ViewModel-specific validation here
}
}
Then I currently have a separate controller action to do the validation for each different type, using model binding:
[HttpPost]
public ActionResult MyViewModel1Validator(MyViewModel1 model)
{
var validator = model.Validate();
var output = from Error e in validator.Errors
select new { Field = e.FieldName, Message = e.Message };
return Json(output);
}
[HttpPost]
public ActionResult MyViewModel2Validator(MyViewModel2 model)
{
var validator = model.Validate();
var output = from Error e in validator.Errors
select new { Field = e.FieldName, Message = e.Message };
return Json(output);
}
This works fineābut if I had 30 different view model types then there would have to be 30 separate controller actions, all with identical code apart from the method signature, which seems like bad practice.
My question is, how can I consolidate these validation actions so that I can pass any kind of view model in and call it's Validate() method, without caring about which type it is?
At first I tried using the interface itself as the action parameter:
public ActionResult MyViewModelValidator(IMyViewModel model)...
But this didn't work: I get a Cannot create an instance of an interface exception. I thought an instance of the model would be passed into the controller action, but apparently this is not the case.
I'm sure I'm missing something simple. Or perhaps I've just approached this all wrong. Can anyone help me out?
The reason why you cannot use the interface is because of serialization. When a request comes in it only contains string key/value pairs that represent the object:
"Client1.Name" = "John"
"Client2.Name" = "Susan"
When the action method gets invoked the MVC runtime tries to create values to populate the method's parameters (via a process called model binding). It uses the type of the parameter to infer how to create it. As you've noticed, the parameter cannot be an interface or any other abstract type because the runtime cannot create an instance of it. It needs a concrete type.
If you want to remove repeated code you could write a helper:
[HttpPost]
public ActionResult MyViewModel1Validator(MyViewModel1 model)
{
return ValidateHelper(model);
}
[HttpPost]
public ActionResult MyViewModel2Validator(MyViewModel2 model)
{
return ValidateHelper(model);
}
private ActionResult ValidateHelper(IMyViewModel model) {
var validator = model.Validate();
var output = from Error e in validator.Errors
select new { Field = e.FieldName, Message = e.Message };
return Json(output);
}
However, you will still need a different action method for each model type. Perhaps there are other ways you could refactor your code. It seems the only difference in your model classes is the validataion behavior. You could find a different way to encode the validation type in your model class.
You could check this: http://msdn.microsoft.com/en-us/magazine/hh781022.aspx.
This is caused because DefaultModelBinder has no way of knowing what concrete type of IMyViewModel should create.
For solution that, you create custom model binder and indicate how to create and bind an instance of interface.
I think I would create an abstract base class that implemented IMyViewModel. I would make Validate an abstract method and require overriding in my concrete view models that inherited from MyAbstractViewModel. Inside your controller, you can work with the IMyViewModel interface if you want, but binding and serialization really needs a concrete class to bind. My $.02.
You could consider using a base class instead of the interface.
I have a class MyController that inherits from Controller, so all my controllers inherit from MyController.
I have a property in MyController:
public class MyController : Controller
{
public string SomeProperty {get;set;}
}
if I set this property in MyController's OnExecuting method, my HtmlHelper extension method works fine:
public static string SomeExtension(this HtmlHelper htmlHelper)
{
StringBuilder sb = new StringBuilder();
string result = "";
var controller = htmlHelper.ViewContext.Controller as MyController;
if (controller != null)
{
result = controller.SomeProperty;
}
return result;
}
it doesn't work if I set 'SomeProperty' in my controllers action method.
I guess because I am doing 'as MyController' in the extension method?
is there a way for it to work in both situations?
I am using the value of SomeProperty to be outputted on my view pages.
How about using a common base class for all your ViewModels and then in the Html Helper, use reflection to get the values fom it?
Type type = htmlHelper.GetType();
PropertyInfo prop = type.GetProperty("YourPropertyName");
string value = prop.GetValue(htmlHelper.ViewData.Model, null).ToString();
And if you don't like the reflection part, why not just pass the property into the Html Helper?
Don't use properties on a base controller to pass data to the view. I would recommend you writing a custom action filter and store data inside ViewData which will be accessible everywhere.