Search code examples
asp.net-mvcmongodbseparation-of-concerns

MVC and NOSQL: Saving View Models directly to MongoDB?


I understand that the "proper" structure for separation-of-concerns in MVC is to have view-models for your structuring your views and separate data-models for persisting in your chosen repository. I started experimenting with MongoDB and I'm starting to think that this may not apply when using a schema-less, NO-SQL style database. I wanted to present this scenario to the stackoverflow community and see what everyone's thoughts are. I'm new to MVC, so this made sense to me, but maybe I am overlooking something...

Here is my example for this discussion: When a user wants to edit their profile, they would go to the UserEdit view, which uses the UserEdit model below.

public class UserEditModel
{
    public string Username
    {
        get { return Info.Username; }
        set { Info.Username = value; }
    }

    [Required]
    [MembershipPassword]
    [DataType(DataType.Password)]
    public string Password { get; set; }

    [DataType(DataType.Password)]
    [DisplayName("Confirm Password")]
    [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
    public string ConfirmPassword { get; set; }

    [Required]
    [Email]
    public string Email { get; set; }

    public UserInfo Info { get; set; }
    public Dictionary<string, bool> Roles { get; set; }
}

public class UserInfo : IRepoData
{
    [ScaffoldColumn(false)]
    public Guid _id { get; set; }

    [ScaffoldColumn(false)]
    public DateTime Timestamp { get; set; }

    [Required]
    [DisplayName("Username")]
    [ScaffoldColumn(false)]
    public string Username { get; set; }

    [Required]
    [DisplayName("First Name")]
    public string FirstName { get; set; }

    [Required]
    [DisplayName("Last Name")]
    public string LastName { get; set; }

    [ScaffoldColumn(false)]
    public string Theme { get; set; }

    [ScaffoldColumn(false)]
    public bool IsADUser { get; set; }
}

Notice that the UserEditModel class contains an instance of UserInfo that inherits from IRepoData? UserInfo is what gets saved to the database. I have a generic repository class that accepts any object that inherits form IRepoData and saves it; so I just call Repository.Save(myUserInfo) and its's done. IRepoData defines the _id (MongoDB naming convention) and a Timestamp, so the repository can upsert based on _id and check for conflicts based on the Timestamp, and whatever other properties the object has just get saved to MongoDB. The view, for the most part, just needs to use @Html.EditorFor and we are good to go! Basically, anything that just the view needs goes into the base-model, anything that only the repository needs just gets the [ScaffoldColumn(false)] annotation, and everything else is common between the two. (BTW - the username, password, roles, and email get saved to .NET providers, so that is why they are not in the UserInfo object.)

The big advantages of this scenario are two-fold...

  1. I can use less code, which is therefore more easily understood, faster to develop, and more maintainable (in my opinion).

  2. I can re-factor in seconds... If I need to add a second email address, I just add it to the UserInfo object - it gets added to the view and saved to the repository just by adding one property to the object. Because I am using MongoDB, I don't need to alter my db schema or mess with any existing data.

Given this setup, is there a need to make separate models for storing data? What do you all think the disadvantages of this approach are? I realize that the obvious answers are standards and separation-of-concerns, but are there any real world examples can you think of that would demonstrate some of the headaches this would cause?

Its also worth noting that I'm working on a team of two developers total, so it's easy to look at the benefits and overlook bending some standards. Do you think working on a smaller team makes a difference in that regard?


Solution

  • The advantages of view models in MVC exist regardless of database system used (hell even if you don't use one). In simple CRUD situations, your business model entities will very closely mimick what you show in the views, but in anything more than basic CRUD this will not be the case.

    One of the big things are business logic / data integrity concerns with using the same class for data modeling/persistence as what you use in views. Take the situation where you have a DateTime DateAdded property in your user class, to denote when a user was added. If you provide an form that hooks straight into your UserInfo class you end up with an action handler that looks like:

    [HttpPost]
    public ActionResult Edit(UserInfo model) { }
    

    Most likely you don't want the user to be able to change when they were added to the system, so your first thought is to not provide a field in the form.

    However, you can't rely on that for two reasons. First is that the value for DateAdded will be the same as what you would get if you did a new DateTime() or it will be null ( either way will be incorrect for this user).

    The second issue with this is that users can spoof this in the form request and add &DateAdded=<whatever date> to the POST data, and now your application will change the DateAdded field in the DB to whatever the user entered.

    This is by design, as MVC's model binding mechanism looks at the data sent via POST and tries to automatically connect them with any available properties in the model. It has no way to know that a property that was sent over wasn't in the originating form, and thus it will still bind it to that property.

    ViewModels do not have this issue because your view model should know how to convert itself to/from a data entity, and it does not have a DateAdded field to spoof, it only has the bare minimum fields it needs to display (or receive) it's data.

    In your exact scenario, I can reproduce this with ease with POST string manipulation, since your view model has access to your data entity directly.

    Another issue with using data classes straight in the views is when you are trying to present your view in a way that doesn't really fit how your data is modeled. As an example, let's say you have the following fields for users:

    public DateTime? BannedDate { get; set; }
    public DateTime? ActivationDate { get; set; } // Date the account was activated via email link
    

    Now let's say as an Admin you are interested on the status of all users, and you want to display a status message next to each user as well as give different actions the admin can do based on that user's status. If you use your data model, your view's code will look like:

    // In status column of the web page's data grid
    
    @if (user.BannedDate != null)
    {
        <span class="banned">Banned</span>
    }
    else if (user.ActivationDate != null)
    {
        <span class="Activated">Activated</span>
    }
    
    //.... Do some html to finish other columns in the table
    // In the Actions column of the web page's data grid
    @if (user.BannedDate != null)
    {
        // .. Add buttons for banned users
    }
    else if (user.ActivationDate != null)
    {
        // .. Add buttons for activated  users
    }
    

    This is bad because you have a lot of business logic in your views now (user status of banned always takes precedence over activated users, banned users are defined by users with a banned date, etc...). It is also much more complicated.

    Instead, a better (imho at least) solution is to wrap your users in a ViewModel that has an enumeration for their status, and when you convert your model to your view model (the view model's constructor is a good place to do this) you can insert your business logic once to look at all the dates and figure out what status the user should be.

    Then your code above is simplified as:

    // In status column of the web page's data grid
    
    @if (user.Status == UserStatuses.Banned)
    {
        <span class="banned">Banned</span>
    }
    else if (user.Status == UserStatuses.Activated)
    {
        <span class="Activated">Activated</span>
    }
    
    //.... Do some html to finish other columns in the table
    // In the Actions column of the web page's data grid
    @if (user.Status == UserStatuses.Banned)
    {
        // .. Add buttons for banned users
    }
    else if (user.Status == UserStatuses.Activated)
    {
        // .. Add buttons for activated  users
    }
    

    Which may not look like less code in this simple scenario, but it makes things a lot more maintainable when the logic for determining a status for a user becomes more complicated. You can now change the logic of how a user's status is determined without having to change your data model (you shouldn't have to change your data model because of how you are viewing data) and it keeps the status determination in one spot.