4

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?

Community
  • 1
  • 1
Anders
  • 12,556
  • 24
  • 104
  • 151

1 Answers1

4

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;
}  
Yakimych
  • 17,612
  • 7
  • 52
  • 69
  • Ok, thanks. But just to play the devil's advocate (in order to understand): If I hadn't gotten the "EntityCollection is already initialized" error (which I still don't understand) for the Consultant object as model, that code would be so much simpler. And regarding the id, I could just leave the id field out of the View, right? (I have always done so when using an object in other contexts where it worked, sort of like the NerdDinner examples). – Anders Feb 24 '11 at 09:20
  • And as for adding extra properties, I have sometimes done so using a partial class (in cases where the property was only needed for the application logic, and not the database)... So I sort of get the idea of the ViewModels, but still not quite in this case. – Anders Feb 24 '11 at 09:22
  • @Anders - Regarding leaving the Id out of the view - that won't prevent an evil user posting it! And since the Model `DOES` have an Id, the modelbinder will be glad to pick it up, so you `WILL` have a potential security hole there. – Yakimych Feb 24 '11 at 10:15
  • @Anders - Regarding the other comment - yes, you can do it, and the code would be simpler, and it actually might be a good solution for RAD (Rapid Application Development). However, simpler code doesn't necessarily mean better code. Architecture-wise you are blending your domain objects with your presentation objects (viewmodels). And again - yes, you can add properties using partial classes, but you can't exclude those you want to really protect. – Yakimych Feb 24 '11 at 10:21
  • On further reflection, although I do accept the answer for the insights on security and so on, I still don't see how this sort of code is supposed to work. The NerdDinner tutorial uses entities as models, and it's that kind of simplicity I'm after. Just now I had to make a change in the application, and the complexity of the code due to the ViewModel made that change really hard to do... So the search for an answer goes on, but I'll have a new go at it and let this question go. Thanks anyway. – Anders Feb 24 '11 at 16:09
  • @Anders - NerdDinner is a demo, and the point is to make it as simple as possible in order to understand how ASP.NET MVC works. That's completely understandable. Furthermore, as I mentioned earlier, it is indeed acceptable to use your Entity as the model and is good for RAD. In small quick projects it just doesn't make sense to create layers of architecture. However, you have to deal with the consequences of using the same object (e.g. that contains lots of extra state tracking logic, etc) as a simple presentational model - such as the error you're encountering. – Yakimych Feb 24 '11 at 17:31
  • @Anders - As a sidenote (regarding the change you had to do), we use only viewmodels in our projects, and while it might seem like a but of an extra hustle at first, it does make the code much more understandable and maintainable in the long term (as the projects get larger). – Yakimych Feb 24 '11 at 17:34
  • Agreed w/ Yakimych re: viewmodels. think of them like data contracts in a web service; they are concerned with only that which the view needs; not all the plumbing that your domain model likely needs. That said, I have to correct one small incorrect statement, that you can't protect your model from a posted ID, the "best practice" when you decide you are going to use Entities is to use the whitelist of properties in the call to TryUpdateModel (there's an overload with a param called include where you give a whitelist of property names you want bound). – Paul Feb 25 '11 at 19:16
  • @Yakimych: As Paul said, I guess it is not possible to live without an ID. When posting a form, the controller needs to know what entity to update, and the ID is the very only thing it has to identify that entity. If a user is not supposed to update a certain entity, then this must be implemented by some sort of authorization, certainly not just by dropping the ID. Am I wrong? – chiccodoro Jun 28 '11 at 06:27
  • @chiccodoro - No, you are right. I referred to the Id purely for illustrational purposes and the discussion went on from there. Yes, of course you need to implement authentication and check what the user has rights to update. The concern would apply, however, to anything you have in your database and don't want the user to see or update. – Yakimych Jun 28 '11 at 06:57
  • 1
    @chiccodoro - If you want a real-world example, say you have a system with some sort of entries a user can add or update. When created, an entry has a `CreationDate` that is set automatically and is never meant to change. It wouldn't be present on the update form, but when updating an entry the user can post a new `CreationDate` along with the rest of the data, and by doing so falsify the logs. – Yakimych Jun 28 '11 at 06:58
  • @Yakimych: I see, that's a valid point indeed. Also, when implementing column-level authorization, part of that implementation would probably be to hide away these things in the ViewModel right away. – chiccodoro Jun 28 '11 at 08:54
  • @Yakimych: BTW, since the ID doesn't provide a good example for what you're saying, you may want to update your answer accordingly. – chiccodoro Jun 28 '11 at 08:55
  • @chiccodoro - thanks for the advice! This is a half-year old answer however, and it works fine as an illustration of my point anyway. We got a bit carried away in the comments though, but whoever reads them will straighten things up thanks to your input ;) – Yakimych Jun 28 '11 at 10:16