Pregunta

I have an ASP.NET MVC 5 site.

I have several complex view models - usually subsets of a far larger model. Should I create items in the repository that take the ViewModels - then just use Dapper.net to map them to some SQL?

If not I would have to map the ViewModel to the Model somewhere. I can then use the standard Model Insert or Update. This causes two problems - how and where to map from ViewModel to Model (I don't want to create lots of work).

Also, where I am updating a subset of fields of a larger model - I want to avoid setting the fields that aren't in the ViewModel to NULL. If I just map a ViewModel to a Model - I will lose data.

It seems the simplest easiest solution is just to pass ViewModels in and out of Repositories. But is this bad practice? If so how can I make my other option (or a third option) work better?

¿Fue útil?

Solución

I have several complex view models - usually subsets of a far larger model. Should I create items in the repository that take the ViewModels - then just use Dapper.net to map them to some SQL?

This wouldn't necessarily be wrong for a very small, trivial app, or a quick'n'dirty KISS/YAGNI solution just to get something out of the door with minimal time or effort, but doesn't really fit with the spirit of the MVC pattern so I would caution against that as a general approach.

A few notes on MVC:

  • A ViewModel (A model of the view) belongs exclusively to your UI layer; not to your business layer or data layer.
  • The aim of the MVC pattern is to encapsulate all UI-specific concerns; decoupling the UI from the app. This allows the rest of the app to remain wholly agnostic to the UI; freed of any UI-specific constructs or concerns.
  • ViewModels can be reasonably expected to contain View-specific properties, and maybe custom logic describing how to display an item of data, as well as Data Annotations on top of the displayed properties.
  • Changes to a View are often facilitated by changes in the corresponding ViewModel; this indicates a very close dependency between the two.

By pushing ViewModels directly into your Repository, you risk introducing technical debt in a number of ways:

  1. If anything about your View changes, it can affect the ViewModel, which means it potentially affects the Repository too; this exposes you to the risk of future changes cascading and being "snowballed" across the app unnecessarily.

  2. Other views needing to re-use the same repository method would also need to know about the ViewModel used by the repository; even if those Views have their own separate ViewModels - that adds further danger to the cascading/snowballing effect of changes to the ViewModel.

  3. Using the ViewModel in this way implies direct interaction between the Controller and a Repository - that's all good in simple cases, but for anything nontrivial it involves additional logic which doesn't strictly belong in either class:

    • The responsibility of a Controller should ideally be limited to Creating a ViewModel from the service/business layer and injecting it into a View (or conversely, receiving an HTTP Request and using the request data to call into the service/business layer).
    • The responsibility of a Repository should ideally be limited to object persistence.
  4. Arguably, using a ViewModel in this way means it ceases to be a real ViewModel because you would need to move it into your Business layer or service layer to work; the result is having that layer effectively 'contaminated' by UI-specific concerns

When a simple call into your repository isn't enough and you need additional business logic to pull down the right kind of data into your controller, consider putting that logic into a separate Service class to ensure that neither your controllers nor repositories have that burden, for example:

public class MyLittleController : Controller
{
    // Service uses the Repositor(y/ies) internally and does a lot of work 
    // to fulfil a request
    private IMyLittleService _service;
    public MyLittleController(IMyLittleService s) { _service = s; }

    // By using a service for the veryComplexQueryString, the only logic 
    // in the controller is for deciding between a 404 or a ViewModel...
    public ActionResult MyLittleWebpage(string veryComplexQueryString)
    {
        var adventurer = _service.QueryAdventurer(veryComplexQueryString);
        var quest = _service.GetQuest(adventurer?.Id);

        if (quest == null)
        {
            return HttpNotFound();
        }

        var model = new MyLittleViewModel 
        { 
            Name = adventurer.Name,
            Quest = quest.Name,
            FavouriteColour = adventurer.FavouriteColour,
        };

        return View(model);
    }
}

If not I would have to map the ViewModel to the Model somewhere. I can then use the standard Model Insert or Update. This causes two problems - how and where to map from ViewModel to Model (I don't want to create lots of work).

Bridging the divide between your Business Layer and your Presentation Layer is the responsibility of your Controller, which includes mapping to/from your ViewModels. You could consider doing the actual work within Helper methods or use a library such as AutoMapper. (Related question on SO)

Also, where I am updating a subset of fields of a larger model - I want to avoid setting the fields that aren't in the ViewModel to NULL. If I just map a ViewModel to a Model - I will lose data.

Sometimes its easier to use a different model for your POST and PUT requests. A ViewModel is for your UI state, so it's only real purpose is to be used in an action which returns a View().

POST/PUT requests are about issuing commands, and a POST/PUT action usually isn't expected to return View(model); (usually a RedirectToAction instead). A POST/PUT action usually only needs to be a 'pass through' to some service class, in which case the domain models could be used for the request instead.


Update in response to the comment about Data Annotations:

Luckily, Dapper itself ignores Data Annotations so that shouldn't cause you any problems. Most of the time each framework uses it's own Data Annotations in fact; the real danger of using across many layers is not really a technical one and more of a 'code cleanliness' issue, and conceptually it starts to feel like a violation of SRP (a single model which serves different purpose depending where it's used).

As an aside, I once reviewed some code whereby somebody had managed to reuse the same Model for a whole bunch of different libraries and tools - it had various properties and attributes which were used in different frameworks in it. The reality of reusing Models in too many places can often look a bit like this:

[XmlElement("Album")]
public class Album
{
    [DisplayName("Album ID")]
    [ScaffoldColumn(false)]
    [Key("Id")]
    [XmlAttribute("id")]
    public int AlbumId { get; set; }

    [DisplayName("Genre ID")]
    [ForeignKey("Genre")]
    [XmlIgnore]
    public int GenreId { get; set; }

    [DisplayName("Artist ID")]
    [ForeignKey("Artist")]
    [XmlIgnore]
    public int ArtistId { get; set; }

    [DisplayName("Album Title")]
    [Required]
    [StringLength(80)]
    [XmlAttribute("albumTitle")]
    public string Title { get; set; }

    [NotMapped]
    [DisplayName("Album")]
    [XmlAttribute("AlbumName")]
    public string AlbumName { get; set; }

}

As somebody trying to read a model like this and figure out which attributes or properties are used at which layer it becomes a lot to think about; and all in the name of merely "saving" a few lines of code to map between models.

Furthermore, you do run the risk that some of the annotations could end up having an effect in the wrong place; again this is more of a risk with an ORM such as EntityFramework which also uses some of those same attributes as ASP.NET

Licenciado bajo: CC-BY-SA con atribución
scroll top