How do I map a property from an object to another object with a different property name?
I have a Product class that looks like this:
public class Product : IEntity
{
public int Id { get; set; }
public string Name { get; set; }
}
And the view model looks like:
public class ProductSpecificationAddViewModel
{
public int ProductId { get; set; }
public string ProductName { get; set; }
}
I need to do the following mapping:
Product.Id => ProductSpecificationAddViewModel.ProductId
Product.Name =>ProductSpecificationAddViewModel.ProductName
Here is my action method:
public ActionResult Add(int id)
{
Product product = productService.GetById(id);
// Mapping
//ProductSpecificationAddViewModel viewModel = new ProductSpecificationAddViewModel();
//viewModel.InjectFrom(product);
return View(viewModel);
}
How would I do this?
If you are using ValueInjecter then you would write a ConventionInjection. See the second sample here
public class PropToTypeProp : ConventionInjection
{
protected override bool Match(ConventionInfo c)
{
return c.TargetProp.Name == c.Source.Type.Name + c.TargetProp.Name;
}
}
this injection will do from all properties of TSource.* to TTarget.TSource+*, so you do:
vm.InjectFrom<PropToTypeProp>(product);
You can do this easily with AutoMapper. By default is uses convention (i.e. Id maps to Id and Name to Name), but you can also define custom mappings.
Mapper.CreateMap<Product, ProductSpecificationAddViewModel>()
.ForMember(destination => destination.ProductName,
options => options.MapFrom(
source => source.Name));
Your contoller mapping code will be then this simple :
Mapper.Map(product, viewModel);
Related
I have an action that will be called with optional querystring parameters. These parameters however are contained in different view models. When I try and add these models to my list of parameters, only a single one is filled and the others are always null. With the exception of an empty query string, where all models are instantiated with defaults.
It is not an option to nest these models for the reason that I don't want the nested property name to be visible in the querystring. So unless that can be circumvented somehow, that would also be a viable solution.
I noticed that, when creating a quick override of the DefaultModelBuilder, all models are parsed but the end result is still that only one model is actually assigned.
This is my scenario:
public ActionResult Index(ModelA ma, ModelB ba)
{
return Content("ok");
}
public class ModelA
{
public string Test { get; set; }
public string Name { get; set; }
}
public class ModelB
{
public int? SomeInteger { get; set; }
public int? TestInteger { get; set; }
}
Desired querystring:
index?Test=Hi&SomeInteger=7
What I want to avoid:
index?ModelA.Test=Hi&ModelB.SomeInteger=7
You can try to make a class combining those two:
public class ModelPair
{
public ModelA A { get; set; }
public ModelB B { get; set; }
}
And then with
public ActionResult Index(ModelPair mp)
{
return Content("ok");
}
You can do ?A.Test=blah&B.SomeInteger=42
I ended up delving in to creating my own custom model binder that does recursive binding. So long as property names are not re-used, which is not a thing that will happen in my models anyway, this fixes my issue of not exposing the property names of the nested model classes.
So now I have the following class structure:
public class ModelA
{
public string Test { get; set; }
public string Name { get; set; }
}
public class ModelB
{
public int? SomeInteger { get; set; }
public int? TestInteger { get; set; }
}
public class ViewModel
{
public ModelA ModelA { get; set; }
public ModelB ModelB { get; set; }
}
And the action now looks like this
public ActionResult Index(ViewModel model)
{
return Content("ok");
}
Which will let me use the following querystring without exposing the ugly property names:
index?Test=Hi&SomeInteger=7&Name=Yep&TestInteger=72
Of course I haven't tested this for an extensive period yet so I have no idea what problems lurk around the corner, but all of the nested models are now properly filled with the data from the querystring and the model classes can be easily re-used : )
public class RecursiveModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var model = base.BindModel(controllerContext, bindingContext);
if (model != null)
{
var properties = bindingContext.ModelType.GetProperties().Where(x => x.PropertyType.IsClass && !x.PropertyType.Equals(typeof(string)) );
foreach(var property in properties)
{
var resursiveBindingContext = new ModelBindingContext(bindingContext)
{
ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, property.PropertyType)
};
var recursiveModel = BindModel(controllerContext, resursiveBindingContext);
property.SetValue(model, recursiveModel);
}
}
return model;
}
}
As far as I know, the Default Model Binder cannot do that. we have to implement custom Model Binder as follow.
public class CustomModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var query = bindingContext.HttpContext.Request.Query;
var modelb = new ModelB();
if (query.TryGetValue($"{bindingContext.ModelName}.{nameof(modelb.SomeInteger)}", out var someInteger))
{
modelb.SomeInteger = Convert.ToInt32(JsonConvert.DeserializeObject(someInteger).ToString());
}
if (query.TryGetValue($"{bindingContext.ModelName}.{nameof(modelb.TestInteger)}", out var testInteger))
{
modelb.TestInteger = Convert.ToInt32(JsonConvert.DeserializeObject(testInteger).ToString());
}
bindingContext.Result = ModelBindingResult.Success(modelb);
return Task.FromResult(modelb);
}
}
In the controller Action we can use Binder as follow
public IActionResult Index(ModelA modelA, [ModelBinder(typeof(CustomModelBinder))]ModelB modelB)
{
return Json(new {modelA, modelB});
}
And in querystring we can have prefix to differentiate each model.
?modelA.test="MATests"&modelA.Name="modelANameValue"&modelB.SomeInteger="5"
Please find working sample here on github
I using Odata Webapi with EF6 and my models are as below
public class Company
{
Public Company()
{
Products = new List<Product>();
}
[Key]
public int Id { get; set; }
[Required]
public string Name { get; set; }
public List<Product> Products { get; set; }
}
public class Product
{
[Key]
public int Id { get; set; }
[Required]
public string Name { get; set; }
}
I need to expose an end point to add product/s to an existing Company.
the end point has to be something like below.
Post: ~odata/Company(1)/Products with body as List of products json.
body:[{"Name":"Product1"},{"Name":"Product2"}]
I would suggest using an bound ODataAction for this purpose.
You would need to pass in a DTO containing a list of Products.
An example for DTO would be:
public class CreateProductsDTO
{
List<Product> Products { get; set; }
}
Then you would need to register an bound action to Company controller:
var action = builder.EntityType<Company>().Action("AddProducts");
action.Parameter<CreateProductsDTO>("Value");
In the controller you need to define an action like:
[HttpPost]
public async Task<IHttpActionResult> AddProducts([FromODataUri] Guid key, ODataActionParameters parameters)
{
//read parameter from ODataActionParameters
var createProducts = parameters["Value"] as CreateProductsDTO;
//Process information
}
The request for this would look like this:
Post: ~odata/Company(1)/AddProducts
body:
{ "Value" : {
"Products" :
[{"Name":"Product1"},{"Name":"Product2"}]
}
}
Hope this helps.
Regards,
Mihai
My DTOs (simplified for demonstration purposes):
Item (the DTO mapped to my ViewModel in question):
public class Item {
public Item() { }
public virtual Guid ID { get; set; }
public virtual ItemType ItemType { get; set; }
public virtual string Title { get; set; }
}
ItemType (referenced by my Item class):
public class ItemType {
public ItemType() { }
public virtual Guid ID { get; set; }
public virtual IList<Item> Items { get; set; }
public virtual string Name { get; set; }
}
My ViewModel (for editing my Item class data):
public class ItemEditViewModel {
public ItemEditViewModel () { }
public Guid ID { get; set; }
public Guid ItemTypeID { get; set; }
public string Title { get; set; }
public SelectList ItemTypes { get; set; }
public IEnumerable<ItemType> ItemTypeEntities { get; set; }
public BuildItemTypesSelectList(Guid? itemTypeID)
{
ItemTypes = new SelectList(ItemTypeEntities, "ID", "Name", itemTypeID);
}
}
My AutoMapper mapping code:
Mapper.CreateMap<Item, ItemEditViewModel>()
.ForMember(dest => dest.ItemTypes, opt => opt.Ignore());
Mapper.CreateMap<ItemEditViewModel, Item>();
Controller code (again, simplified for demonstration):
public ActionResult Create()
{
var itemVM = new ItemEditViewModel();
// Populates the ItemTypeEntities and ItemTypes properties in the ViewModel:
PopulateEditViewModelWithItemTypes(itemVM, null);
return View(itemVM);
}
[HttpPost]
public ActionResult Create(ItemEditViewModel itemVM)
{
if (ModelState.IsValid) {
Item newItem = new Item();
AutoMapper.Mapper.Map(itemVM, newItem);
newItem.ID = Guid.NewGuid();
...
// Validation and saving code here...
...
return RedirectToAction("Index");
}
PopulateEditViewModelWithItemTypes(itemVM, null);
return View(itemVM);
}
Now, here's what's happening:
Within my HttpPost Create action result in my controller where I use Automapper to map my ItemEditViewModel to my Item DTO class, the ItemType ID value selected in the SelectList doesn't bind to the Item.ItemType.ID property. The Item.ItemType property is null.
I assume this is because, since I don't have an ItemTypeID Guid value in my Item DTO class, and I haven't created a new ItemType class for the property of the same name in my Item DTO, AutoMapper is unable to store the ItemType ID value.
I think it comes down to my Automapper mapping configuration.
I'm sure it's something simple that I'm overlooking.
Thanks in advance for any advice!
Pretty sure this is because automapper was designed as a Big Shape-> Smaller/Flat Shape mapping tool, not the other way around. This just isn't supported.
You should be able to say:
Mapper.CreateMap<ItemEditViewModel, Item>()
.ForMember(dest => dest.ItemType, opt => opt.MapFrom(src =>
{
return new ItemType()
{
ID = src.ItemTypeID
}
};
i'd like to know how can I get a property like an entity, for example:
My Model:
public class Product {
public int Id { get; set; }
public string Name { get; set; }
public Category Category { get; set; }
}
View:
Name: <%=Html.TextBoxFor(x => x.Name) %>
Category: <%= Html.DropDownList("Category", IEnumerable<SelectListItem>)ViewData["Categories"]) %>
Controller:
public ActionResult Save(Product product)
{
/// produtct.Category ???
}
and how is the category property ? It's fill by the view ? ASP.Net MVC know how to fill this object by ID ?
Thanks!
This is one of the reasons why it's bad to bind directly to entities. Consider
public class ProductForm {
public int Id { get; set; }
public string Name { get; set; }
public string CategoryId { get; set; }
}
public ActionResult Save(ProductForm form)
{
var product = new Product
{
Id = form.Id,
Name = form.Name,
Category = database.GetCategory(form.CategoryId)
};
}
In case of view models as above, it may be OK to use custom model binders to automatically get entities by Id from database. See here for sample implementation (in S#arp Architecture) that binds IDs to entities from database. But I think for now you better go with simpler implementation like above.
You can also use AutoMapper to simplify form->entity mapping.
Let's say you have an object called Person that looks like this:
class Person
{
public int ID { get; set; }
public string Name { get; set; }
public int NumberOfCatsNamedEnder { get; set; }
}
I have a simple HTML form that exposes the properties that gets posted to an ASP.NET MVC action inside of my PersonController class. The issue I have is that if someone puts in the letter 'A' for NumberOfCatsNamedEnder, I get a The model of type 'Person' was not successfully updated. error. Since this happens while trying to update the Model, I can't find any way to check to see if someone passed in a non-integer value without resorting to
if(!IsInteger(formCollection["NumberOfCatsNamedEnder"]))
{
ModelState.AddModelError(
"NumberOfCatsNamedEnder",
"Ender count should be a number");
}
Is there a better way to do this? I was able to find some information on custom ModelBinders; is that what is needed?
I really like the approach of using a presentation model. I'd create a class like this:
class PersonPresentation
{
public int ID { get; set; }
public string Name { get; set; }
public string NumberOfCatsNamedEnder { get; set; }
public void FromPerson(Person person){ /*Load data from person*/ }
}
Then your controller action can bind the view to a PersonPresentation:
public ActionResult Index()
{
Person person = GetPerson();
PersonPresentation presentation = new PersonPresentation();
ViewData.Model = presentation.FromPerson(person);
return View();
}
...and then accept one in your Update method and perform validation:
public ActionResult Update(PersonPresentation presentation)
{
if(!IsInteger(presentation.NumberOfCatsNamedEnder))
{
ModelState.AddModelError(
"NumberOfCatsNamedEnder",
"Ender count should be a number");
}
...
}