Designing an MVC repository using ViewModels

2019-03-11 15:46发布

I want to create a repository class to separate out my data logic from my controllers. I am using a ViewModel to represent some data that will be filled with data from different tables.

Here are some questions I have:

  1. For a method like GetAll(), do I return an IQueryable<MyViewModel> or IQueryable<Entity>? If I return viewmodels, how do I cope with a GetAll() that pulls thousands of records?
  2. Do I create a constructor for my custom ViewModel class that takes the Entity as a parameter to do the mapping? (I'm still unfamiliar with automapper so just need an understanding on how to do this from a design point of view)

Again, my main concern is a method like GetAll() which would pull many records. If I did a foreach loop to translate each Entity into a ViewModel seems like a lot of overhead. My thought was to put a reference inside the custom ViewModel class to the IQueryable<Entity> to access from the collection, and have the ListViewModel just have indexers or something like that which reference the collection property.

3条回答
成全新的幸福
2楼-- · 2019-03-11 16:04

There are many different ways to do this, but to start simply, I would return an IEnumerable<T> for your GetAll() method. However, you'll probably want to implement paging in some fashion. You might want to setup a generic repository that does your basic data access for most scenarios and returns an Enumerable. You could reserve a single method that should be reserved for more complicated queries and returns an IQueryable<T>. The basic stripped down implementation might look like below.

public class Repository<T> : IRepository<T> where T : class
{
    internal ObjectContext _objectContext;
    internal ObjectSet<T> _objectSet;

    public Repository(ObjectContext objectContext)
    {
        _objectContext = objectContext;
        _objectSet = objectContext.CreateObjectSet<T>();
    }

    public IQueryable<T> GetQuery()
    {
        return _objectSet;
    }

    public IEnumerable<T> GetAll()
    {
        return GetQuery().ToList();
    } 

    public IEnumerable<T> Find(Func<T, bool> where)
    {
        return _objectSet.Where<T>(where);
    }

    public T Single(Func<T, bool> where)
    {
        return _objectSet.SingleOrDefault<T>(where);
    }

    public List<T> Page<TKey>(Expression<Func<T, bool>> where, int page, int pagesize, Expression<Func<T, TKey>> orderBySelector, bool ascending)
    {
        return ascending
            ? GetQuery().Where(where).OrderBy(orderBySelector).Skip((page - 1) * pagesize).Take(pagesize).ToList()
            : GetQuery().Where(where).OrderByDescending(orderBySelector).Skip((page - 1) * pagesize).Take(pagesize).ToList();
    }


    public void Delete(T entity)
    {
        _objectSet.DeleteObject(entity);
    }

    public void Add(T entity)
    {
        _objectSet.AddObject(entity);
    }

}

And the Interface would look like

public interface IRepository<T> where T : class
{
    IQueryable<T> GetQuery();

    IEnumerable<T> GetAll();

    IEnumerable<T> Find(Func<T, bool> where);     

    T Single(Func<T, bool> where);

    List<T> Page<TKey>(Expression<Func<T, bool>> where, int page, int pagesize, Expression<Func<T, TKey>> orderBySelector, bool ascending);

    void Delete(T entity);

    void Add(T entity);

}

The above can function as the beginning of a simple generic repository. Once you have your entities, you don't need AutoMapper, it just makes life easier as many ViewModels have the same properties as your entities. You can simply define a new ViewModel or List of ViewModels and map the properties on your own.

List<ViewModel> vm = new List<ViewModel>();

foreach (var e in entities)
{
    ViewModel v = new ViewModel();
    v.something = e.something;
    // perform the rest
    vm.Add(v);
}

*That was quite a bit to type, sorry about any typos :)

查看更多
可以哭但决不认输i
3楼-- · 2019-03-11 16:05

1) For a method like GetAll(), do I return an IQueryable or IQueryable? If I return viewmodels, how do I cope with a GetAll() that pulls thousands of records?

IQueryable<Entity>. The repository doesn't deal with view models. Think of the repository as something that is defined in a separate class library that doesn't reference your ASP.NET MVC application which is where your view models live. It is the ASP.NET MVC application that references this library.

2) Do I create a constructor for my custom ViewModel class that takes the Entity as a parameter to do the mapping? (I'm still unfamiliar with automapper so just need an understanding on how to do this from a design point of view)

No. Don't create constructors in your view models especially if you want your controller actions to take those view models as action parameters (think of a POST action). The reason for this is that the default model binder will no longer know how to instantiate your view model and you will have to write custom model binders.

So AutoMapper or manually map.

Example with manual mapping which is what you could start with:

public ActionResult SomeAction()
{
    IEnumerable<Entity> entities = Repository.GetAll();
    IEnumerable<MyViewModel> model = entities.Select(x => new MyViewModel
    {
        Prop1 = x.Prop1,
        Prop2 = x.Prop2,
        ...
    }); 
    return View(model);
}

And once you get sick of writing this code move to AutoMapper:

public ActionResult SomeAction()
{
    IEnumerable<Entity> entities = Repository.GetAll();
    IEnumerable<MyViewModel> model = Mapper.Map<IEnumerable<Entity>, IEnumerable<MyViewModel>>(entities); 
    return View(model);
}

or if you write a custom action filter that uses the OnActionExecuted event to pull the domain model that was passed to the view, map it to the view model using AutoMapper and substitute the model with the view model for the view, you could further simplify the repetitive code:

[AutoMap(typeof(IEnumerable<Entity>), typeof(IEnumerable<MyViewModel>))]
public ActionResult SomeAction()
{
    IEnumerable<Entity> entities = Repository.GetAll();
    return View(entities);
}

Again, my main concern is a method like GetAll() which would pull many records. If I did a foreach loop to translate each Entity into a ViewModel seems like a lot of overhead.

Don't be concerned about that. Pulling your records will be a magnitude slower than looping and mapping to the view model.

查看更多
来,给爷笑一个
4楼-- · 2019-03-11 16:14

I think you may have a misunderstanding of the view model and it's purpose. You don't need to create a view model for every entity in your database, as it seems you want to do; you just create a view model for each view you want to render. Hence the term "view model"--it organizes the data in the form of a model that your view can be strongly typed to.

You wouldn't, for example, want to create a separate view model for every entity returned by a GetAll(). In a simple scenario of displaying a gridview of all records you would probably just need a single viewmodel with one property:

    public class MyViewModel
    {     
       public List<MyRecord> AllRecords {get;set;}
    }

You would populate this view model in the controller

public ActionResult SomeAction()
{
   var viewmodel = new MyViewModel{AllRecords = GetAll()};
   return View(viewModel);
}

Have a look at this blog post by Rachael Appel for a really concise discussion.

查看更多
登录 后发表回答