ViewModels and one-to-many relationships with Enti

2019-04-10 13:38发布

问题:

I have an application for storing information about consultants in a database. The model is an Entity Framework model, and the database tables are Consultant with one-to-many relationships to a number of other tables (WorkExperiences, Programs, CompetenceAreas, etc). Now, when I want to create a new Consultant object in a View, I would really just want to pass a Consultant object as the model to the View. But for one, it has been suggested to me (Collection of complex child objects in Asp.Net MVC 3 application?) that I shouldn't do this, but use ViewModels instead. Secondly, and maybe this is the reason, I get an error saying "The EntityCollection has already been initialized" when I try to post the Consultant object if using it as a model in the View, and the cause of the error seems to be the collections of objects such as WorkExperiences.

So my first question is why I'm getting this error.

But more importantly, if I should instead use a ViewModel, how would I do that properly? Because I have in fact tried something, and got it working. But...the code is awful. Can anyone please tell me what I should be doing instead to get this working more cleanly?

Let me show you what I have (that again works, but is a nightmare codewise):

The GET Create method:

    public ActionResult Create()
    {
        Consultant consultant = new Consultant();
        ConsultantViewModel vm = GetViewModel(consultant);

        return View(vm);
    }

Helper method to create the "ViewModel" (if this is in fact what a ViewModel is supposed to be like):

    private ConsultantViewModel GetViewModel(Consultant consultant)
    {
        ConsultantViewModel vm = new ConsultantViewModel();
        vm.FirstName = consultant.FirstName;
        vm.LastName = consultant.LastName;
        vm.UserName = consultant.UserName;
        vm.Description = consultant.Description;

        vm.Programs = consultant.Programs.ToList();
        vm.Languages = consultant.Languages.ToList();
        vm.Educations = consultant.Educations.ToList();
        vm.CompetenceAreas = consultant.CompetenceAreas.ToList();
        vm.WorkExperiences = consultant.WorkExperiences.ToList();
        return vm;
    }

The POST Create method:

    [HttpPost]
    [ValidateInput(false)] //To allow HTML in description box
    public ActionResult Create(ConsultantViewModel vm, FormCollection collection)
    {
        try
        {
            Consultant consultant = CreateConsultant(vm);
            _repository.AddConsultant(consultant);
            _repository.Save();
            return RedirectToAction("Index");
        }
        catch
        {
            return View();
        }
    }

Helper method to create a Consultant object (this one is particularly awful, where I have to check that collections are not null, in case the user decides not to add anything in those lists...):

    private Consultant CreateConsultant(ConsultantViewModel vm)
    {
        Consultant consultant = new Consultant();
        consultant.Description = vm.Description;
        consultant.FirstName = vm.FirstName;
        consultant.LastName = vm.LastName;
        consultant.UserName = vm.UserName;

        if (vm.Programs != null)
            foreach (var program in vm.Programs)
                consultant.Programs.Add(program);
        if (vm.Languages != null)
            foreach (var language in vm.Languages)
                consultant.Languages.Add(language);
        if (vm.Educations != null)
            foreach (var education in vm.Educations)
                consultant.Educations.Add(education);
        if (vm.WorkExperiences != null)
            foreach (var workExperience in vm.WorkExperiences)
                consultant.WorkExperiences.Add(workExperience);
        if (vm.CompetenceAreas != null)
            foreach (var competenceArea in vm.CompetenceAreas)
                consultant.CompetenceAreas.Add(competenceArea);

        return consultant;
    }

So, again it works, but is nowhere near as clean as if I could have used a Consultant object directly (if not for that "EntityCollection is already initialized" error"...). So how should I do it instead?

回答1:

First of all, you shouldn't use your entity object as the viewmodel because (and I can think of at least two reasons right now, but there are more):

  1. You don't want to expose sensitive data, such as 'Id' or 'Password'. Imagine your consultant has an Id and an evil user opens the edit consultant page and posts back a different Id. As a result, the evil user will succeed in updating different Consultant.

  2. Currently whatever you show in the View corresponds to what your Consultant object looks like. But in case you want to add extra info that is not part of the Consultant object (as simple as a checkbox field). In that case, you have to rewrite quite a bit of code, create the ViewModel, map it, etc. While if you follow the ViewModel pattern from the start, you can just make this simple change whenever you need it.

Regarding your code - you can try to use AutoMapper with Nested Mappings for this type of conversion. Even if you don't, your code can be made a bit cleaner by using projections.

private ConsultantViewModel GetViewModel(Consultant consultant)
{
    return new ConsultantViewModel
               {
                   FirstName = consultant.FirstName,
                   LastName = consultant.LastName,
                   ...
                   vm.Programs = consultant.Programs.ToList(),
                   ...
               };
 }

 private Consultant CreateConsultant(ConsultantViewModel vm)
 {
     var consultant = new Consultant
                      {
                          Description = vm.Description,
                          FirstName = vm.FirstName,
                          ...
                       };

     if (vm.Programs != null)
     {
         vm.Programs.ForEach(consultant.Programs.Add);
     }
     ...

     return consultant;
}