I'm trying to work out the best architecture for handling model type hierarchies within an MVC application.
Given the following hypothetical model -
public abstract class Person
{
public string Name { get; set; }
}
public class Teacher : Person
{
public string Department { get; set; }
}
public class Student : Person
{
public int Year { get; set; }
}
I could just create a controller for each type. Person would have just the index and detail action with the views making use of display templates, Teacher and Student would have just the Create/Edit actions. That would work but seems wasteful and wouldn't really scale because a new controller and views would be needed if another type was added to the hierarchy.
Is there a way to make a more generic Create/Edit action within the Person controller? I have searched for the answer for a while but can't seem to find exactly what I am looking for so any help or pointers would be appreciated :)
Sure, but it takes a little leg work.
First, in each of your edit/create views, you need to emit the type of model you are editing.
Second, you need add a new modelbinder for the person class. Here is a sample of why I would do for that:
public class PersonModelBinder :DefaultModelBinder
{
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
PersonType personType = GetValue<PersonType>(bindingContext, "PersonType");
Type model = Person.SelectFor(personType);
Person instance = (Person)base.CreateModel(controllerContext, bindingContext, model);
bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => instance, model);
return instance;
}
private T GetValue<T>(ModelBindingContext bindingContext, string key)
{
ValueProviderResult valueResult =bindingContext.ValueProvider.GetValue(key);
bindingContext.ModelState.SetModelValue(key, valueResult);
return (T)valueResult.ConvertTo(typeof(T));
}
}
Register it in your app start:
ModelBinders.Binders.Add(typeof(Person), new PersonModelBinder());
The PersonType is what I tend to use in each model and is an enum that says what each type is, I emit that in a HiddenFor so that it comes back in with the post data.
The SelectFor is a method that returns a type for the specified enum
public static Type SelectFor(PersonType type)
{
switch (type)
{
case PersonType.Student:
return typeof(Student);
case PersonType.Teacher:
return typeof(Teacher);
default:
throw new Exception();
}
}
You can now do something like this in your controller
public ActionResult Save(Person model)
{
// you have a teacher or student in here, save approriately
}
Ef is able to deal with this quite effectively with TPT style inheritance
Just to complete the example:
public enum PersonType
{
Teacher,
Student
}
public class Person
{
public PersonType PersonType {get;set;}
}
public class Teacher : Person
{
public Teacher()
{
PersonType = PersonType.Teacher;
}
}
Related
I have the following problem, I created an application to add game categories and the games themselves to the database. I created a relationship and unfortunately when I add to the database I get an error.
Model bound complex types must not be abstract or value types and must
have a parameterless constructor.
Game Category Model :
using System.Collections.Generic;
namespace relationship.Models
{
public class GameCategory
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Game> Game { get; set; }
}
}
Game Model :
namespace relationship.Models
{
public class Game
{
public int GameId { get; set; }
public string Name { get; set; }
public GameCategory Category { get; set; }
public int CategoryId { get; set; }
}
}
ViewModel :
using relationship.Models;
using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace relationship.ViewModels
{
public class AddGameViewModel
{
[Required]
public string GameName { get; set; }
public int CategoryID { get; set; }
public List<SelectListItem> Categories { get; set; }
public AddGameViewModel(IEnumerable<GameCategory> categories)
{
Categories = new List<SelectListItem>();
foreach (var catData in categories)
{
Categories.Add(new SelectListItem { Text = catData.Name.ToString(), Value = catData.Id.ToString() });
}
return;
}
}
}
GameRepository :
using System.Collections.Generic;
using System.Linq;
namespace relationship.Models
{
public class GameRepository : IGameRepository
{
private readonly AppDbContext appDbContext;
public GameRepository(AppDbContext dbContext)
{
appDbContext = dbContext;
}
public void AddGame(Game game)
{
appDbContext.Games.Add(game);
appDbContext.SaveChanges();
}
public IEnumerable<Game> Games()
{
return appDbContext.Games.ToList();
}
}
}
and last is GameController :
using Microsoft.AspNetCore.Mvc;
using relationship.Models;
using relationship.ViewModels;
namespace relationship.Controllers
{
public class GameController : Controller
{
private readonly IGameRepository gameRepository;
private readonly ICategoryRepository categoryRepository;
public GameController(IGameRepository gameRepo, ICategoryRepository catRepo)
{
gameRepository = gameRepo;
categoryRepository = catRepo;
}
public IActionResult Index()
{
return View();
}
[HttpGet]
public IActionResult Add()
{
var addGameViewModel = new AddGameViewModel(categoryRepository.GameCategory());
return View(addGameViewModel);
}
[HttpPost]
public IActionResult Add(AddGameViewModel addGameViewModel)
{
if (ModelState.IsValid)
{
GameCategory gameCategory = categoryRepository.GetDetails(addGameViewModel.CategoryID);
if(gameCategory == null)
{
return NotFound();
}
Game game = new Game
{
Name = addGameViewModel.GameName,
Category = gameCategory
};
gameRepository.AddGame(game);
return RedirectToAction("Index");
}
return View(addGameViewModel);
}
}
}
I don't have any idea what is wrong.
My error screen :
Could not create an instance of relationship.ViewModels.AddGameViewModel. Model bound complex types must not be abstract or value types and must have a parameterless constructor.
Let's try and break this error down.
Could not create an instance of relationship.ViewModels.AddGameViewModel.
Pretty self-explanatory: the model-binding components are trying to create an instance of your type, but failed.
Model bound complex types
"Model bound" refers to that they're being bound by the ASP.NET pipeline. "complex types" are basically any types which aren't "basic" like string or int. Your model classes are complex types.
must not be abstract
The model-binding system is going to want to be able to create instances of the class, so it cannot be abstract; it must be concrete. All of the types you've show are concrete so this isn't the problem.
or value types
You can't use struct types with model-binding; it's just one of its limitations. Fortunately your types are all classes, so you can ignore this.
and must have a parameterless constructor.
ASP.NET doesn't know how to supply parameters to model constructors. It can only do the equivalent of new T(), so all your model types must define a constructor which has zero parameters. This is the reason you're seeing the error; your AddGameViewModel class only defines this constructor:
public AddGameViewModel(IEnumerable<GameCategory> categories)
One of the C# language features is that when you don't specify a constructor manually, it adds a default one for you. When you define a constructor in your code, this default constructor is not added.
In all of your other models, you aren't defining any constructors so the compiler is adding the default one for you. In the case of AddGameViewModel you have added a constructor, so to fix the problem you must also add the default constructor:
public AddGameViewModel()
{
}
you need add [FromBody] to the parameter so that asp.net core know how to bind the model.
[HttpPost]
public IActionResult Add([FromBody] AddGameViewModel addGameViewModel)
As of this writing, I experienced this issue in an Asp.NET Core 2.2 Controller where the type was injected on one of the methods. Moving the type to the Controller's constructor worked around it. Since this wasn't really acceptable we eventually refactored the offending class out of the Controller and into the processing layer where singletons are already used extensively. Adding one more at that point cleared up our problem.
Note this is the OOB IoC container that is built-in to Asp.Net Core. Other IoC providers may be better able to handle injecting properties on methods.
Lamar might be an alternative.
Using a model binder might also have worked since the binder could probably use the singleton and/or support constructor injection more cleanly.
In my case, I was naively binding a complex object (a complex object without a no-args constructor):
Edit.cshtml.cs:
namespace MyNamespace.Pages.CSDA
{
public class EditModel : PageModel
{
...
[BindProperty]
public MyComplexClass WorkflowItem { get; set; }
...
I got this runtime error when I clicked "Save":
System.InvalidOperationException: Could not create an instance of type 'MyNamespace.MyComplexClass'.
Model bound complex types must not be abstract or value types and must have a parameterless constructor.
Alternatively, set the 'WorkflowItem' property to a
non-null value in the 'MyNamespace.EditModel' constructor. at
Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexTypeModelBinder.CreateModel(ModelBindingContext
bindingContext) at
Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexTypeModelBinder.BindModelCoreAsync(ModelBindingContext
bindingContext, Int32 propertyData)
I needed the object (it had information I wanted to display to the user), but I didn't need to "update" it (at least not in this edit menu).
SOLUTION:
Simply removing [BindProperty] eliminated the error.
I had this same error. Constructor was internal, I returned it back as public, and the model was passed normally.
Adding [ApiController] at the top of my Controller's class fixed it for me:
[Route("api/[controller]")]
[ApiController]
public class ProductController : Controller
{
...
}
In a POST call to a WebApi I am trying to return a Created(newobject) thing. But there is no signature for Created in ApiController that can only take the object and do the rest.
It works fine if I return something like:
return Created(newobject.blahid.ToString(), newobject);
or if I do a
return CreatedAtRoute("DefaultApi", new { controller = ControllerContext.ControllerDescriptor.ControllerName, id = newobject.blahid.ToString()}, newobject);
I want to simplify this to:
return Created(newobject);
I would need to implement a method in a BaseController
public class BaseController : ApiController
{
protected new CreatedNegotiatedContentResult<T> Created<T>(T content)
{
var id = GetId(content);//need help here
return base.Created(id, content);
}
}
I don't want to worry about the Unique Identifier for an object being called differently in different models e.g. myobjguid, someblahguid etc. I would just want to find it out and mark it as "id".
say if my model is
public class Model_A
{
public List<Model_A> ChildModels { get; set; }
[LookForThisAttribute]//I want something like this
public Guid Model_AGuid { set; get; }
public Guid ? ParentGuid { set; get; }
public List<SomeOtherObject> OtherObjects { set; get; }
}
Is there an attribute([LookForThisAttribute]) or something I can set on all my models to specify that this is the guy to be assumed as THE unique identifier if I ever look for it.
Just like the [Key] attribute in Entity Framework. No matter what you call it, Entity Framework know its going to be the primary key.
So the GetId(T content) method can take the object and return the value of the property that has a [LookForThisAttribute] set?
I ended up writing my own Attribute and then looking up for it in the BaseController.
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public sealed class UniqueIdAttribute: Attribute
{
}
And in the BaseController Created method:
protected CreatedNegotiatedContentResult<T> Created<T>(T content)
{
var props =typeof(T).GetProperties().Where(
prop => Attribute.IsDefined(prop, typeof(UniqueIdAttribute)));
if (props.Count() == 0)
{
//log this
return base.Created(Request.RequestUri.ToString(), content);
}
var id = props.FirstOrDefault().GetValue(content).ToString();
return base.Created(new Uri(Request.RequestUri + id), content);
}
Mark Gravell's post here helped me with getting the value of the property that has my custom attribute:
How to get a list of properties with a given attribute?
Along with a corresponding unit test for the controllers works fine for me.
Now I can just call Created(anyobject); from all ApiControllers without bothering about the different names people put for their IDs as long as they decorate it with my custom attribute.
I'm trying to implement this kind of mechanizm:
Model has bit field: is_active, so I'm using it to toggle visibility on the main form. The action in controler is like:
public ActionResult Toggle(int id)
{
Country country = _db.Countries.Find(id);
country.is_active = country.is_active == null ? true : !country.is_active;
_db.SaveChanges();
return RedirectToAction("Index");
}
I want my admin lookup controllers to derive from that generic class and use the implementation from it for common actions like toggle.
I've already created ITogglable interface that has ActionResult Toggle(int id) so I'm implementing it inside particular controler (CountryController : Controller, ITogglable), but I have to implement it every time. Instead I would more like my CountryController, RegionController, etc derive from a generic class (i.e. Togglable) that has these kind of methods already implemented like:
public virtual ActionResult Toggle(int id)
{
CurrentModelTypeUsedInController lookup = _db.CurrentModelTypeUsedInController.Find(id);
lookup .is_active = lookup .is_active == null ? true : !lookup.is_active;
_db.SaveChanges();
return RedirectToAction("Index");
}
Is this even possible? I have no idea how to make it lookup-independant so I do not have to provide type of CurrentModelTypeUsedInController each time.
You need a couple of abstractions to achieve this.
Firstly you will need an abstraction that defines is_active, such as
public interface IActive
{
bool is_active { get; set; }
}
And all of the types such as Country and CurrentModelTypeUsedInController will need to implement this abstraction
public class Country : IActive
{
public bool is_active { get; set; }
}
And with this in place we define the generic controller. The key point here is the generic constraint (IActive) that is placed on generic type (TModelType). By defining that all types of TModelType must implement IActive the code knows that the type is guaranteed to expose a property named is_active
public class AbstractController<TModelType> : Controller
where TModelType : class, IActive
{
private readonly DbContext _db;
public AbstractController()
{
_db = new DbContext("connection");
}
public ActionResult Toggle(int id)
{
TModelType instance = _db.Set<TModelType>().Find(id);
instance.is_active = instance.is_active == null ? true : !instance.is_active;
_db.SaveChanges();
return RedirectToAction("Index");
}
}
And each controller can derive from AbstractController<>
public sealed class CountryController : AbstractController<Country>
{
}
Currently I use private static methods in my controller file to map domain model to view model and vice-versa. Like below:
public ActionResult Details(int personID)
{
Person personDM = service.Get(personID);
PersonViewModel personVM = MapDmToVm(personDM);
return View(personVM);
}
private static PersonViewModel MapDmToVm(Person dm)
{
PersonViewModel vm;
// Map to VM
return vm;
}
Is there any other standard way to do this?
I prefer to put the mapping logic inside the view model (dto) class, because we want to keep the domain model as clean as possible and also the domain model might change overtime.
public class Person
{
public string Name {get; set;}
}
public class PersonViewModel
{
public string Text {get; set;}
public static implicit operator PersonViewModel(Person dm)
{
var vm = new PersonViewModel { Text = dm.Name };
return vm;
}
public static implicit operator Person(PersonViewModel vm)
{
var dm = new Person { Name = vm.Text };
return dm;
}
}
and use it in the controller without explicit casting.
Person dm = service.Get(id);
PersonViewModel vm = dm;
Since the mapping is not always trivial, I think that it might be better to separate it into a different class other than the viewmodel.
That way each class has its own single responsibility. You might want to add an extension method to your domain model, something like:
public static MyViewModel ToViewModel(this MyDomainModel model)
{
// mapping code goes here
}
You also might consider using automapper and call its Map method from your controller.
So I have a action method that takes a fancy Cart object:
[HttpPost]
public JsonResult BuildTransaction(Cart cart) { }
The Cart model:
public class Cart
{
public Guid UserId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public Address Address { get; set; }
}
I throw some JSON at the route, that looks like this:
object cart = new {
UserId = uid,
FirstName = "John",
LastName = "Travlota",
Address = new {
Line1 = "Ramsdean Grange",
Town = "boom town",
PostCode = "dd7 7sx"
}
};
var request = client.PostAsJsonAsync("BuildTransaction", cart);
The result, I have a cart of type Cart to play with in my controller. Fantastic!
My question is, how does .NET do this mapping? I imagine its someplace in the OnActionExecute but what/how does it do this.
If I wanted to mimic this functionality, what would I do? Do I really need an external tool like AutoMapper if MVC seems perfectly capable of doing it without it?
This is done by the Model Binder. (System.Web.Mvc.DefaultModelBinder)
You can implement a custom model binder like so:
Controller:
public ActionResult Create([ModelBinder(typeof(CreateModelBinder))] CreateViewModel vModel)
{
}
Model Binder:
public class CreateModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
//assign request parameters here, and return a CreateViewModel
//for example
CreateViewModel cVM = new CreateViewModel();
cVM.Name = controllerContext.HttpContext.Request.Params["Name"];
return cVM;
}
}
More info: http://www.dotnetcurry.com/ShowArticle.aspx?ID=584
https://stackoverflow.com/a/1249602/1324019
First MVC receives a post (for example) which contains your html input values and the name associated with each of them.
Then it checks the properties in the expected object (the model type) and tries to find a name which matches in the form received, and so on.
It all happens in the ModelBinder, you can even change the way it is done since this is an extension point.
So this is for the how, and about using AutoMapper, you don't need it here.
This is Mvc Model Binding. MVC has a set of a default model binders. It uses the DefaultModelBinder object as default for your post requests, but it also have the 3 following objects registered by default.
HttpPostedFiledModelBinder
ByteArrayModelBinder
LinqBinaryModelBinder
You can also create your own binders and associate them to a parameter with an attribute on a parameter as #Mansfield pointed out. But you can also register them globally for a specific type as follow (In Application_Start for example)
//Register FooModelBinder for all posted objects that are of type Foo
ModelBinders.Binders.Add(typeof(Foo),new FooModelBinder());
And if for some reason you want to reinvent the wheel you can also change de DefaultModelBinder
ModelBinders.Binders.DefaultBinder = new CustomDefaultModelBinder();
Here's a very simple implemention of a ModelBinder for Foo type
public class FooModelBinder: IModelBinder
{
#region IModelBinder Members
public object BindModel(ControllerContext controllerContext,ModelBindingContext bindingContext) {
IUnvalidatedValueProvider provider = (IUnvalidatedValueProvider)bindingContext.ValueProvider;
return new Foo {
Bar = provider.GetValue("Bar", true).AttemptedValue,
Banana= provider.GetValue("Banana", true).AttemptedValue
};
}
#endregion
}
The ASP.NET MVC model binder is what does this translation of request parameter to class instances. This great piece of functionality operates based on conventions, so as long as you follow default conventions (which means that your request parameters need to have the same name as the names of the properties of your class). So yes, in most cases the default model binder behavior is fine and you don't need a library like AutoMapper.
If you want to know more about when model binding occurs, this article goes into the MVC pipeline in great detail. If you just want to know more about model binding, I found this two page tutorial a great help: part 1 and part 2.