14

我正在设计我的 ASP.NET MVC 应用程序,我遇到了一些有趣的想法。

我见过的许多示例都描述并使用了存储库模式 ( IRepository),所以这就是我在学习 MVC 时所做的方式。

现在我知道它在做什么,我开始审视我目前的设计,想知道这是否是最好的方法。

目前我有一个 basic IUserRepository,它定义了诸如FindById(),SaveChanges()等方法。

目前,每当我想加载/查询数据库中的用户表时,我都会执行以下操作:

    private IUserRepository Repository;

    public UserController()
        : this(new UserRepository())
    { }

    [RequiresAuthentication]
    [AcceptVerbs(HttpVerbs.Get)]
    public ActionResult Edit(string ReturnUrl, string FirstRun)
    {
        var user = Repository.FindById(User.Identity.Name);

        var viewModel = Mapper.Map<User, UserEditViewModel>(user);
        viewModel.FirstRun = FirstRun == "1" ? true : false;

        return View("Edit", viewModel);
    }

    [AcceptVerbs(HttpVerbs.Post), ValidateAntiForgeryToken(Salt = "SaltAndPepper")]
    public ActionResult Edit(UserEditViewModel viewModel, string ReturnUrl)
    {
        //Map the ViewModel to the Model
        var user = Repository.FindById(User.Identity.Name);

        //Map changes to the user
        Mapper.Map<UserEditViewModel, User>(viewModel, user);

        //Save the DB changes
        Repository.SaveChanges();

        if (!string.IsNullOrEmpty(ReturnUrl))
            return Redirect(ReturnUrl);
        else
            return RedirectToAction("Index", "User");
    }

现在我不完全了解 MVC 在用户创建链接时如何创建控制器(不确定每个用户是否有 1 个控制器或每个应用程序有 1 个控制器),所以我不肯定最好的过程行动。

IRepository<T> 我在这里发现了一个关于使用通用存储库接口的好问题,并且RepositoryFactory在许多博客上似乎也有静态的想法。基本上只保留一个存储库实例,它是通过这个工厂获得的

所以我的问题围绕着人们如何在应用程序中做到这一点,以及什么被认为是好的做法。

人们是否有基于每个表 ( IUserRepository) 的单独存储库?
他们使用泛型IRepository<T>吗?
他们是否使用静态存储库工厂?
或者完全是别的什么?

编辑:我刚刚意识到我可能也应该问:

在每个控制器上都有一个私有IRepository的好方法吗?还是IRepository每次我想使用它时都应该实例化一个新的?

赏金编辑:我开始赏金以获得更多观点(不是蒂姆没有帮助)。

我更想知道人们在他们的 MVC 应用程序中做了什么,或者他们认为什么是好主意。

4

5 回答 5

23

泛型概念的一些非常明显的问题IRepository<T>

  • 它假设每个实体都使用相同类型的密钥,这在几乎任何重要的系统中都是不正确的。一些实体将使用 GUID,其他实体可能具有某种自然键和/或复合键。NHibernate 可以很好地支持这一点,但 Linq to SQL 在这方面做得很差——你必须编写大量的 hackish 代码来进行自动键映射。

  • 这意味着每个存储库只能处理一种实体类型,并且只支持最琐碎的操作。当存储库被降级为这样一个简单的 CRUD 包装器时,它几乎没有用处。您不妨只给客户一个IQueryable<T>or Table<T>

  • 它假定您对每个实体执行完全相同的操作。实际上,这与事实相去甚远。当然,也许您想Order通过它的 ID 来获取它,但更可能的是您想获取Order特定客户和某个日期范围内的对象列表。完全通用的概念IRepository<T>不允许您几乎肯定希望对不同类型的实体执行不同类型的查询。

存储库模式的全部意义在于创建通用数据访问模式的抽象。我认为有些程序员厌倦了创建存储库,所以他们说“嘿,我知道,我将创建一个可以处理任何实体类型的 über-repository!” 这很好,只是存储库对于您尝试做的 80% 几乎没有用处。作为基类/接口很好,但是如果这是您所做工作的全部范围,那么您只是懒惰(并保证将来会头疼)。


理想情况下,我可能会从一个看起来像这样的通用存储库开始:

public interface IRepository<TKey, TEntity>
{
    TEntity Get(TKey id);
    void Save(TEntity entity);
}

您会注意到它没有Listor函数 - 这是GetAll因为认为在代码中的任何位置一次从整个表中检索数据是可以接受的是荒谬的。这是您需要开始进入特定存储库的时候:

public interface IOrderRepository : IRepository<int, Order>
{
    IEnumerable<Order> GetOrdersByCustomer(Guid customerID);
    IPager<Order> GetOrdersByDate(DateTime fromDate, DateTime toDate);
    IPager<Order> GetOrdersByProduct(int productID);
}

等等 - 你明白了。这样我们就有了“通用”存储库,如果我们真的需要非常简单的按 id 检索语义,但一般来说,我们永远不会真正传递它,当然不会传递给控制器​​类。


现在,对于控制器,您必须正确地执行此操作,否则您几乎否定了您在将所有存储库放在一起所做的所有工作。

控制器需要从外界获取其存储库。您创建这些存储库的原因是您可以进行某种控制反转。您的最终目标是能够将一个存储库换成另一个存储库 - 例如,进行单元测试,或者如果您决定在未来某个时候从 Linq 切换到 SQL 再到 Entity Framework。

这个原则的一个例子是:

public class OrderController : Controller
{
    public OrderController(IOrderRepository orderRepository)
    {
        if (orderRepository == null)
            throw new ArgumentNullException("orderRepository");
        this.OrderRepository = orderRepository;
    }

    public ActionResult List(DateTime fromDate, DateTime toDate) { ... }
    // More actions

    public IOrderRepository OrderRepository { get; set; }
}

换句话说,控制器不知道如何创建存储库,也不应该这样做。如果您在那里进行任何存储库构建,那么它正在创建您真正不想要的耦合。ASP.NET MVC 示例控制器具有创建具体存储库的无参数构造函数的原因是,站点需要能够编译和运行,而无需强制您设置整个依赖注入框架。

但是在生产站点中,如果您没有通过构造函数或公共属性传递存储库依赖项,那么您就完全浪费了存储库的时间,因为控制器仍然与数据库层紧密耦合。您需要能够编写如下测试代码:

[TestMethod]
public void Can_add_order()
{
    OrderController controller = new OrderController();
    FakeOrderRepository fakeRepository = new FakeOrderRepository();
    controller.OrderRepository = fakeRepository; //<-- Important!
    controller.SubmitOrder(...);
    Assert.That(fakeRepository.ContainsOrder(...));
}

如果您OrderController要离开并创建自己的存储库,则无法执行此操作。此测试方法不应该进行任何数据访问,它只是确保控制器根据操作调用正确的存储库方法。


请注意,这还不是 DI,这只是伪装/嘲笑。DI 出现的地方是当您认为 Linq to SQL 对您来说还不够,并且您真的想要 NHibernate 中的 HQL,但是您需要 3 个月的时间来移植所有内容,并且您希望能够一次做一个存储库。因此,例如,使用像Ninject这样的 DI 框架,你所要做的就是改变这个:

Bind<ICustomerRepository>().To<LinqToSqlCustomerRepository>();
Bind<IOrderRepository>().To<LinqToSqlOrderRepository>();
Bind<IProductRepository>().To<LinqToSqlProductRepository>();

到:

Bind<ICustomerRepository>().To<LinqToSqlCustomerRepository>();
Bind<IOrderRepository>().To<NHibernateOrderRepository>();
Bind<IProductRepository>().To<NHibernateProductRepository>();

现在,您所依赖的一切都在IOrderRepository使用 NHibernate 版本,您只需要更改一行代码,而不是可能的数百行代码。我们正在并行运行 Linq to SQL 和 NHibernate 版本,逐个移植功能,而不会破坏中间的任何内容。


所以总结一下我提出的所有观点:

  1. 不要严格依赖通用IRepository<T>接口。您希望从存储库获得的大多数功能都是特定的,而不是通用的。如果您想IRepository<T>在类/接口层次结构的上层包含一个,那很好,但是控制器应该依赖于特定的存储库,因此当您发现通用存储库时,您最终不必在 5 个不同的地方更改代码缺少重要的方法。

  2. 控制器应该接受来自外部的存储库,而不是创建自己的存储库。这是消除耦合和提高可测试性的重要一步。

  3. 通常,您需要使用依赖注入框架连接控制器,其中许多可以与 ASP.NET MVC 无缝集成。如果这对你来说太多了,那么至少你应该使用某种静态服务提供者,这样你就可以集中所有的存储库创建逻辑。(从长远来看,您可能会发现学习和使用 DI 框架会更容易)。

于 2010-03-03T22:32:24.823 回答
3

人们是否有基于每个表 (IUserRepository) 的单独存储库? 我倾向于为每个聚合而不是每个表都有一个存储库。

他们使用通用的 IRepository 吗? 如果可能的话,是的

他们是否使用静态存储库工厂? 我更喜欢通过 IOC 容器注入 Repository 实例

于 2010-02-27T12:53:52.927 回答
1

这是我使用它的方式。我将 IRepository 用于我的所有存储库共有的所有操作。

public interface IRepository<T> where T : PersistentObject
{
    T GetById(object id);
    T[] GetAll();
    void Save(T entity);
}

我还为每个聚合使用了一个专用的 ITRepository,用于与该存储库不同的操作。例如对于用户,我将使用 IUserRepository 添加与 UserRepository 不同的方法:

public interface IUserRepository : IRepository<User>
{
    User GetByUserName(string username);
}

实现将如下所示:

public class UserRepository : RepositoryBase<User>, IUserRepository
{
    public User GetByUserName(string username)
    {
        ISession session = GetSession();
        IQuery query = session.CreateQuery("from User u where u.Username = :username");
        query.SetString("username", username);

        var matchingUser = query.UniqueResult<User>();

        return matchingUser;
    }
}


public class RepositoryBase<T> : IRepository<T> where T : PersistentObject
{
    public virtual T GetById(object id)
    {
        ISession session = GetSession();
        return session.Get<T>(id);
    }

    public virtual T[] GetAll()
    {
        ISession session = GetSession();
        ICriteria criteria = session.CreateCriteria(typeof (T));
        return criteria.List<T>().ToArray();
    }

    protected ISession GetSession()
    {
        return new SessionBuilder().GetSession();
    }

    public virtual void Save(T entity)
    {
        GetSession().SaveOrUpdate(entity);
    }
}

比在 UserController 中看起来像:

public class UserController : ConventionController
{
    private readonly IUserRepository _repository;
    private readonly ISecurityContext _securityContext;
    private readonly IUserSession _userSession;

    public UserController(IUserRepository repository, ISecurityContext securityContext, IUserSession userSession)
    {
        _repository = repository;
        _securityContext = securityContext;
        _userSession = userSession;
    }
}

比使用自定义控制器工厂使用依赖注入模式实例化存储库。我使用StructureMap作为我的依赖注入层。

数据库层是 NHibernate。ISession 是此会话中数据库的网关。

我建议你看看CodeCampServer结构,你可以从中学到很多东西。

您可以从中学习的另一个项目是Who Can Help Me。我还没有深入挖掘它。

于 2010-03-03T08:53:33.170 回答
0

人们是否有基于每个表 (IUserRepository) 的单独存储库?

是的,这是更好的选择,原因有两个:

  • 我的 DAL 基于Linq-to-Sql(但我的 DTO 是基于 LTS 实体的接口)
  • 执行的操作是原子的(添加是原子操作,保存是另一个,等等)

他们使用通用的 IRepository 吗?

是的,完全受 DDD 实体/值方案的启发,我为其他东西创建了 IRepositoryEntity / IRepositoryValue 和通用 IRepository。

他们是否使用静态存储库工厂?

是和否:我使用通过静态类调用的IOC 容器。嗯……我们可以说它是一种工厂。

注意:我自己设计了这个架构,我的一位同事发现它非常棒,以至于我们目前正在这个模型上创建我们的整个公司框架(是的,它是一家年轻的公司)。这绝对是值得尝试的事情,即使我觉得这种框架将由主要参与者发布。

于 2010-03-03T11:09:09.633 回答
0

您可以找到一个出色的Generic Repopsitory库,该库被编写为使其能够在 codeplex 上用作 WebForms asp:ObjectDataSource:MultiTierLinqToSql

我的每个控制器都有他们需要支持的操作的私有存储库。

于 2010-03-03T22:37:07.250 回答