使用 Entity Framework 6 重构代码以遵循 TDD

Refactoring code using Entity Framework 6 to follow TDD

现在,我正在执行重构使用 EF6 的 ASP.Net MVC 应用程序的任务。目前,代码使用 EF Designer 生成实体(edmx 文件),所有逻辑都使控制器膨胀。

我读过一些关于 TDD 和 EF6 的文章,我知道这个问题与其他问题类似,例如

但大部分是指"Code First"开发。我还知道 EF6 在内部实现了 UnitOfWork,所以我的问题是如何构建我的代码,以便它:

但仍然使用"Database first"开发类型。我应该遵循什么模式?非常感谢所有反馈。

[已编辑] 这是当前控制器中的一个动作(现在它包含 500 多行代码)

public ActionResult ToggleProductPromoCodeIsActive(string promoCode, string productID, string countryCode)
    {
        var isActive = ToggleProductPromoCodeIsActive(promoCode, productID, countryCode);
        return Json(new { isActive = isActive }, JsonRequestBehavior.AllowGet);
    }

public ActionResult AddPromoCodeProperties(string promoCode, DateTime beginningDateTime, DateTime endDateTime, bool isActive, int? length, string countryCode, short? maximumRenewals)
    {
        int id = AddPromoCodeProperties(promoCode, beginningDateTime, endDateTime, isActive, length, countryCode, maximumRenewals);
        if (id != 0)
        {
            return Json(new { message = "Promo code properties have been succesfully added", id = id, promoCode = promoCode }, JsonRequestBehavior.AllowGet);
        }
        else
        {
            return Json(new { message = "Failed: The same promo code with the same country code has existed", promoCode = promoCode }, JsonRequestBehavior.AllowGet);
        }
    }


    // ----- Another line of code

    private int AddProductPromoCode(string promoCode, double discountPercentage, string productID, bool isActive, string countryCode, string paymentPageText, string finalProductID)
    {
        using (var provisioningContext = new ProvisioningEntities())
        {
            var productPromoCode = provisioningContext.ProductPromoCodes.SingleOrDefault(x => x.code == promoCode && x.productID == productID && x.countryCode == countryCode);
            if (productPromoCode == null)
            {
                try
                {
                    productPromoCode = new ProductPromoCode();
                    productPromoCode.code = promoCode;
                    productPromoCode.discountPercentage = discountPercentage;
                    productPromoCode.productID = productID;
                    productPromoCode.isActive = isActive;
                    productPromoCode.countryCode = countryCode;
                    productPromoCode.paymentPageText = paymentPageText;
                    productPromoCode.finalProductID = finalProductID;
                    provisioningContext.ProductPromoCodes.Add(productPromoCode);
                    provisioningContext.SaveChanges();
                    return productPromoCode.ID;
                }
                catch
                {
                    return 0;
                }
            }
            else
            {
                return 0;
            }
        }
    }

    private bool ToggleProductPromoCodeIsActive(string promoCode, string productID, string countryCode)
    {
        using (var provisioningContext = new ProvisioningEntities())
        {
            var productPromoCode = provisioningContext.ProductPromoCodes.SingleOrDefault(x => x.code == promoCode && x.productID == productID && x.countryCode == countryCode);
            productPromoCode.isActive = !productPromoCode.isActive;
            provisioningContext.Entry(productPromoCode).State = EntityState.Modified;
            provisioningContext.SaveChanges();
            return productPromoCode.isActive;
        }
    }

这就是我要做的:

为上下文中的每个实体实施一个存储库。如果您有一组对所有实体执行的类似操作(例如,GetAll、GetByID、创建、更新、删除),您可以定义一个通用接口作为您的起点。然后,所有存储库都将实现自己的接口,该接口将从该接口继承。您将使用泛型来完成此操作:

public interface IBaseRepository<T>
{
    T GetByID(int id);
    IEnumerable<T> GetAll();
    void Create(T element);
    // and so on...
}

这将为您提供一些优势:

1) 您将更重要的操作集中在一个基本接口中,每个存储库接口都将继承自该接口。

2) 由于所有存储库都实现了自己的接口,因此您可以创建有助于 TDD 的模拟实现。

3) 您现在可以将接口与实际实现分离并实现依赖注入。有大量非常易于使用的 IoC 容器。可以使用微软的Unity,非常好用(比如在Unity中可以通过web.config或者app.config来解决哪个类型实现哪个接口)。

因此此时您可以将数据访问层划分为以下组件:

  • 数据库上下文(设置数据库集、配置、映射、种子的地方)
  • 存储库(使用 dbcontext 的实际实现)
  • 存储库接口(您将其视为操作数据库的实际合同)。
  • 模拟存储库

对于第二部分,您将不得不详细说明控制器中臃肿的逻辑,因为操作和方法可能在他们手中承担了太多责任。根据您提供的信息,我建议您遵循 S.O.L.I.D。原则。

编辑:

现在我看到你粘贴的代码了。

在您提供的这个特定示例中,为 ProductPromoCodes 实体实现存储库会将与处理实体相关的逻辑从控制器转移到存储库和数据提供程序层。

首先,像这样定义 repo 接口(因为我看到这些是您需要在 provisioningContext

中执行的操作
public interface IProductPromoCodeRepository
{
    // You can see that these are similar to the methods that the IBaseRepository interface defines, so you can actually make this one inherit from it.
    ProductPromoCode Get(string code, string id, string country);
    void Create(ProductPromoCode item);
    void Update(ProductPromoCode item);
}

然后,编写实现(一个class调用ProductPromoCodeRepository,它继承了存储库接口)。这些方法中的每一个的代码将或多或少像现在控制器的私有方法中一样,并且 provisioningContext 将成为实现它们的 class 的成员,因此控制器只需要处理 ProductPromoCodeRepository.

你甚至可以封装这个逻辑

productPromoCode.isActive = !productPromoCode.isActive;
provisioningContext.Entry(productPromoCode).State = EntityState.Modified;
provisioningContext.SaveChanges();

如果需要,可以在存储库中使用它自己的方法,因此在控制器中,您需要做的是使用此方法 returns.

的值

毕竟,这两个动作将如下所示:

public ActionResult ToggleProductPromoCodeIsActive(string promoCode, string productID, string countryCode)
{
    var isActive = _productPromoCodeRepository.ToogleActive(promoCode, productID, countryCode);// This encapsulates the three lines of code from above...
    return Json(new { isActive = isActive }, JsonRequestBehavior.AllowGet);
}

public ActionResult AddPromoCodeProperties(string promoCode, DateTime beginningDateTime, DateTime endDateTime, bool isActive, int? length, string countryCode, short? maximumRenewals)
{
    var productPromoCode = _productPromoCodeRepository.Get(promoCode, productID/*I don't know where do you get this value*/, countryCode);
    if(productPromoCode != null)
        return Json(new { message = "Failed: The same promo code with the same country code has existed", promoCode = promoCode }, JsonRequestBehavior.AllowGet);

    productPromoCode = new ProductPromoCode();
    productPromoCode.code = promoCode;
    productPromoCode.discountPercentage = discountPercentage;
    productPromoCode.productID = productID;
    productPromoCode.isActive = isActive;
    productPromoCode.countryCode = countryCode;
    productPromoCode.paymentPageText = paymentPageText;
    productPromoCode.finalProductID = finalProductID;
    _productPromoCodeRepository.Create(productPromoCode);
    // you can actually move all these lines above to the repo with another overload for the Create method that takes all the parameters.

    // Here you can be sure the item didn't exist before in the database, but you would have to deal with errors in the creation and return the proper error message.
    return Json(new { message = "Promo code properties have been succesfully added", id = productPromoCode.id, promoCode = productPromoCode.code }, JsonRequestBehavior.AllowGet);

}