首页 > 解决方案 > 将存储过程用于存储库模式和服务类

问题描述

背景

在我的 Web 应用程序中,我创建了存储过程,然后创建了一个 edmx 文件以使用存储过程来处理所有数据库交互。

但是我开始怀疑我这样做是否正确,因为正如您将在下面的示例中看到的那样,

  1. 即使被调用的方法不需要数据库工作,我也会在Context每次调用时实例化两个实例Controller

  2. Context在每个存储库中实例化一个实例,所以当一个请求需要从存储库 A 和 B 获取数据时,我有两个Context.

存储库 A

public class RepositoryA
{
            private readonly Context _context;

            public RepositoryA()
            {
                _context = new Context();
            }

            public List<CLASS> GetA(int id)
            {
                return _context.GetByID(id);
            }
}

存储库 B

public class RepositoryB
{
            private readonly Context _context;

            public RepositoryB()
            {
                _context = new Context();
            }

            public List<CLASS> GetB(int id)
            {
                return _context.GetByID(id);
            }
}

控制器

public class Controller
{
            private readonly IRepositoryA _reposA;
            private readonly IRepositoryB _reposB;

            public Controller() : this(new RepositoryA(), new RepositoryB())
            {}

            public Controller(IRepositoryA a, IRepositoryB b)
            {
                _respoA = a;
                _reposB = b;
            }

            public ActionResult METHOD()
            {
                //do something with both RepositoryA and RepositoryB 
                var dataFromA = _reposA.GetA(ID); 
                var dataFromB = _reposB.GetB(ID);

                return View(someData);
            }
}

现在的问题是:我不确定这是否应该是正常的实现,所以我一直试图弄清楚如何以更有效和可测试的方式进行设置,我尝试了类似的方法。

我相信这个设计解决了我的一些顾虑:

  1. 每次调用 Controller 时都会调用服务,但不会每次都实例化 Context(每个请求都会实例化 Context)。

  2. 当服务同时需要存储库 A 和 B 时,它使用相同的上下文

但是,通过Service设置方式,我无法Service使用测试数据进行单元测试,因为我无法使用我的模拟存储库。

public class Controller
{
        private Service _service;

        public Controller()
        {
            _service =  new Service();
        }

        public ActionResult METHOD()
        {
            _service.DoSomethingWithRepoAandB(); 

            return View();
        }
}

public class Service
{
            public void DoSomethingWithRepoAandB()
            {
                 using (var _context = new Context())
                 {
                     RepositoryA a = new RepositoryA(_context);
                     RepositoryB b = new RepositoryB(_context);
                     something = a.DoSomethingWithA();
                     otherThing = b.DoOtherThingWithB();
                 }                    
            }
}

所以,我想我应该这样设置Service

使用这种设计,Context每次Controller被调用时都会被实例化(除非我Service在一个Controller方法中实例化),但我可以通过传递模拟存储库来进行单元测试。

public class Service
{
    private readonly Context _context;
    private IRepositoryA _a;
    private IRepositoryB _b;

    public Service()
    {
         _context = new Context();
         _a = new RepositoryA(_context);
         _b = new RepositoryB(_context);
    }        

    // Pass Mock Repositories in unit tests
    public Service(RepositoryA a, RepositoryB b)
    {
        _a = a;
        _b = b;
    }

    public void DoSomethingWithRepoAandB()
    {
        something = _a.DoSomethingWithA();
        otherThing =_b.DoOtherThingWithB();  
    }
}

我这样做是完全错误的还是在正常的轨道上?我会很感激任何建议。

标签: c#asp.net-mvcentity-framework-6

解决方案


您的服务不应该有任何关于您的上下文的信息,这些信息应该只能由存储库访问,遵循 SOLID 原则。

您的图层的访问权限应如下所示:

控制器 -> 服务 -> 存储库 -> 上下文

在您的 IRepository 接口上,您将拥有要调用的方法,您可以手动实例化它们,但要正确实现它,您必须设置依赖注入。

此外,您的 Repositories 构造函数不会接收任何上下文作为参数,因此您不能执行以下操作:

RepositoryA a = new RepositoryA(_context);

另一方面,您的控制器也不应该访问存储库,因为它会破坏您的架构

public ActionResult METHOD()
    {
        //do something with both RepositoryA and RepositoryB 
        var dataFromA = _reposA.GetA(ID);
        var dataFromB = _reposB.GetB(ID);

        return View(someData);
    }

这是正确的代码,以供进一步理解:

public class RepositoryA : IRepositoryA
{
    private readonly Context _context;

    public RepositoryA(Context context)
    {
        _context = context;
    }

    public List<CLASS> GetA(int id)
    {
        return _context.GetByID(id);
    }
}

public interface IRepositoryA
{
    List<CLASS> GetA(int id);
}

public class RepositoryB : IRepositoryB
{
    private readonly Context _context;

    public RepositoryB(Context context)
    {
        _context = context;
    }

    public List<CLASS> GetB(int id)
    {
        return _context.GetByID(id);
    }
}

public interface IRepositoryB
{
    List<CLASS> GetB(int id);
}

public class Controller
{
    private IService _service;

    public Controller(IService service)
    {
        _service = service;
    }

    public ActionResult METHOD(int id)
    {
        //do something with both RepositoryA and RepositoryB THROUGH the service. The service needs to hold the business rules, and repositories should only care about querying data and handling contexts.
        var data = _service.DoSomethingWithRepoAandB(id)

        return View(data);
    }

}

public class Service : IService
{
    private IRepositoryA _a;
    private IRepositoryB _b;

    // Pass Mock Repositories in unit tests -> PS: You can't have 2 constructors if you're using dependency injection.
    public Service(RepositoryA a, RepositoryB b)
    {
        _a = a;
        _b = b;
    }

    public void DoSomethingWithRepoAandB(int id)
    {
        var something = _a.GetA(id);
        var otherThing = _b.GetB(id);
    }
}

public interface IService
{
    void DoSomethingWithRepoAandB(int id);
}

public class Bootstrapper
{
    //this class should be in a separated assembly, responsible for handling the dependency injection. Using Simple Injection syntax just as an example

    public static void RegisterServices(Container container) //IoC Container
    {
        container.Register<IService, Service>(Lifestyle.Scoped);
        container.Register<IRepositoryA, RepositoryA>(Lifestyle.Scoped);
        container.Register<IRepositoryB, RepositoryB>(Lifestyle.Scoped);
        container.Register<Context>(() => {
            var options = // Configure your ContextOptions here
            return new Context(options);
        });
    }
}

public class Startup
{
    //This is your startup configuration if you're using WebApi. If you're on MVC, you can do this on your Global.asax
    public void Configuration()
    {
        var container = new Container();
        container.Options.DefaultScopedLifestyle = new AsyncScopedLifestyle(); 
        BootStrapper.RegisterServices(container);
    }
}

推荐阅读