为您的存储库添加流畅 API 风味





4.00/5 (1投票)
为 Entity Framework 存储库添加流畅 API 支持
引言
几天前,我的一个朋友问我如何看待在存储库中使用流畅 API 进行查询。起初,这个想法并没有引起我太大的兴趣,但在看了他的代码后,我意识到这是一个相当有趣的想法。本文将分享重构后的代码如何为我们的存储库类提供一个不错的扩展。
背景
本文假定读者对存储库模式、流畅 API 和 Entity Framework 有基本的了解。如果您需要回顾存储库模式和流畅 API,这里有一些 CodeProject 上的精彩文章:
- https://codeproject.org.cn/Articles/526874/Repositorypluspattern-cplusdoneplusright
- https://codeproject.org.cn/Articles/146394/A-Look-at-Fluent-APIs
Using the Code
在附带的演示解决方案中,我们创建了一个非常简单的控制台项目,用于处理汽车租赁域的模型。该模型仅包含一个实体 Car
以及 2 个支持的枚举 CarBrand
和 CarStatus
。
public class Car
{
public int Id { get; set; }
public CarBrand Brand { get; set; }
public string Model { get; set; }
public decimal RentalPricePerDay { get; set; }
public CarStatus Status { get; set; }
}
以下是一些用于初始化数据库的种子数据。
var cars = new List<Car>
{
new Car { Brand = CarBrand.BMW, Model = "M235i",
RentalPricePerDay = 90, Status = CarStatus.Available },
new Car { Brand = CarBrand.Cadillac, Model = "CTS",
RentalPricePerDay = 80, Status = CarStatus.Reserved },
new Car { Brand = CarBrand.Chevrolet, Model = "Corvette Stingray",
RentalPricePerDay = 85, Status = CarStatus.Available },
new Car { Brand = CarBrand.Ford, Model = "Mustang GT",
RentalPricePerDay = 70, Status = CarStatus.Available },
new Car { Brand = CarBrand.Honda, Model = "Accord",
RentalPricePerDay = 60, Status = CarStatus.Rented },
new Car { Brand = CarBrand.Mazda, Model = "3",
RentalPricePerDay = 65, Status = CarStatus.Rented },
new Car { Brand = CarBrand.BMW, Model = "i8",
RentalPricePerDay = 70, Status = CarStatus.Available },
new Car { Brand = CarBrand.Porsche, Model = "Boxster",
RentalPricePerDay = 90, Status = CarStatus.Available }
};
假设客户端 UI 需要显示 2 页内容,要求:
- 列出所有可用汽车,最低价格为 70。
- 列出所有宝马汽车,要求可用,最低价格为 60,最高价格为 80。
在正常的存储库实现中,您将在 CarRepository
中为这 2 个用例实现 2 个特定方法(例如 FindAvailableCarWithMinPrice()
、FindAvailableBMWWithPriceWithinRange()
等)。当项目越来越大时,您会发现自己创建了越来越多类似的方法,查询之间只有细微差别(例如,上面的两个查询都涉及可用汽车和某种最低价格)。
因此,一些存储库实现选择公开一个使用 Expression
的通用查询方法并返回 IEnumerable
,例如:
public IEnumerable<T> FindBy(Expression<Func<T, bool>> filter)
虽然这很灵活,并且在大多数情况下都有效,但它不再像第一种方法那样具有描述性。现在,要理解查询的作用,开发人员需要阅读和理解 lambda 过滤器,在复杂的情况下,这不是一项简单的任务。
通过使用流畅 API,我们发现它在上述两种方法之间提供了良好的权衡。它不是万能药,不能不加考虑地应用于所有存储库,而应权衡成本和收益(例如,现有存储库是否存在多个相似的查询?团队成员是否熟悉并乐于使用流畅 API?等等)。
我们为所有希望支持流畅 API 的存储库定义了一个通用接口:
public interface ISupportFluentQuery<TQueryBuilder>
where TQueryBuilder : IAmQueryBuilder
{
TQueryBuilder Query();
}
此接口只有一个方法,它返回一个 TQueryBuilder
类型的类。查询构建器是一个继承自通用基类的类。
public abstract class QueryBuilderBase<T> : IAmQueryBuilder where T : class
{
protected IQueryable<T> Query;
protected QueryBuilderBase(DbContext context)
{
Query = context.Set<T>();
}
public List<T> ToList()
{
return Query.ToList();
}
public T FirstOrDefault()
{
return Query.FirstOrDefault();
}
public static implicit operator List<T>(QueryBuilderBase<T> queryBuilder)
{
return queryBuilder.ToList();
}
public static implicit operator T(QueryBuilderBase<T> queryBuilder)
{
return queryBuilder.FirstOrDefault();
}
}
这个 QueryBuilderBase
又实现了接口:
public interface IAmQueryBuilder
{
}
这个接口只是一个标记接口。它用于约束 ISupportFluentQuery
可以接受的泛型类型。另一种替代方法是让 ISupportFluentQuery
定义接受第二个泛型参数,该参数是所有可查询域模型继承的某个通用基类/接口。
泛型基类 QueryBuilderBase<T>
被声明为 abstract
并且具有受保护的构造函数,因此只能由其子类使用。在构造函数中,它接受一个 DbContext
实例,创建并存储 IQueryable
以供模型类查询。此 Query
将可供其子类进行进一步过滤。此基类还包括两个隐式运算符,可将查询转换为结果列表或结果实例,而无需客户端显式调用 ToList()
或 FirstOrDefault()
。您将在下面看到一个使用示例。
让我们看看 CarQueryBuilder
。
public class CarQueryBuilder : QueryBuilderBase<Car>
{
public CarQueryBuilder(DbContext context)
: base(context)
{
}
public CarQueryBuilder IsBMW()
{
Query = Query.Where(car => car.Brand == CarBrand.BMW);
return this;
}
public CarQueryBuilder IsAvailable()
{
Query = Query.Where(car => car.Status == CarStatus.Available);
return this;
}
public CarQueryBuilder WithMinimumPriceOf(decimal minPrice)
{
Query = Query.Where(car => car.RentalPricePerDay >= minPrice);
return this;
}
public CarQueryBuilder WithMaximumPriceOf(decimal maxPrice)
{
Query = Query.Where(car => car.RentalPricePerDay <= maxPrice);
return this;
}
}
它继承自通用基类,并声明了自己特有的用于查询 car
的方法。每个查询方法都会修改基类中的 IQueryable
实例,并在完成后返回 CarQueryBuilder
本身。返回 CarQueryBuilder
本身的操作是创建 Fluent
API 的关键,使方法调用能够相互链接。请注意,方法名称现在非常具有描述性,您也不再需要处理 lambda 表达式。
此 CarQueryBuilder
仅使用 Where()
过滤,但没有任何内容阻止您使用其他 EF LINQ 方法,如 Include()
、OrderBy()
等。
现在最后一步是让 CarRepository
实现接口 ISupportFluentQuery
。
public class CarRepository : ISupportFluentQuery<CarQueryBuilder>, IDisposable
{
private readonly DbContext _context;
public CarRepository()
{
_context = new DemoDbContext();
}
public CarQueryBuilder Query()
{
return new CarQueryBuilder(_context);
}
public void Dispose()
{
_context.Dispose();
}
}
请注意,本文的重点不在于实现存储库模式,因此我们为了演示目的将此存储库尽可能简化,例如,DbContext
是直接在存储库内部创建和维护的。
在其 Query()
方法中,它创建一个新的 CarQueryBuilder
实例,并将当前的 DbContext
传递进去。因此,每次用户调用 Query()
时,他们都会得到一个不同的查询构建器实例。
最后,客户端代码中使用 Fluent
API。
using (var repository = new CarRepository())
{
List<Car> availableCarsWithMinPrice70
= repository.Query()
.IsAvailable()
.WithMinimumPriceOf(70);
PrintResult("Available cars with minimum price of 70", availableCarsWithMinPrice70);
List<Car> availableBMWWithPriceBetween60And80
= repository.Query()
.IsBMW()
.IsAvailable()
.WithMinimumPriceOf(60)
.WithMaximumPriceOf(80);
PrintResult("Available BMW cars with minimum price of 60 and maximum price of 80",
availableBMWWithPriceBetween60And80);
}
在这里,为了简单起见,我们直接创建了一个 CarRepository
实例。当然,也可以将存储库注册到您喜欢的 IoC 容器中,并获取 ISupportFluentQuery<CarQueryBuilder>
的实例。或者,如果您不希望客户端感知 ISupportFluentQuery
,您可以创建一个继承自 ISupportFluentQuery<CarQueryBuilder>
的接口 ICarRepository
,客户端所要做的就是获取 ICarRepository
的实例。
正如您所见,现在的查询非常清晰且有意义。新开发人员只需一瞥就应该能够理解这些查询。像 IsAvailable()
、WithMinimumPriceOf()
这样的常用过滤器在不同的查询中被重用。
另外,请注意,我们需要将结果分配给特定类型 List<Car>
,以便 QueryBuilderBase
中的隐式转换运算符能够工作。如果将 List<Car>
更改为 var
,则需要在方法链的末尾显式调用 ToList()
或 FirstOrDefault()
来获取结果,因为编译器无法猜测结果的类型。
存储库通过其 Query()
方法支持 Fluent
API,因此存储库可以自由地实现其他接口,拥有标准存储库的所有功能。客户仍然可以使用存储库中的标准方法,如果他们不喜欢使用 Fluent
API。因此,我们通常认为这个 Fluent
API 只是存储库实现的一个扩展。
这是在控制台中打印的结果。
关注点
代码的初始版本是让存储库类实现一个通用的存储库基类,该基类公开的方法类似于本文中 QueryBuilderBase
公开的方法。
然而,经过仔细考虑,我们发现这种继承关系存在许多局限性,因此我们决定改用组合(虽然不是严格意义上的组合,因为存储库类不持有查询构建器实例的引用)。在我们看来,这很快被证明是正确的决定:重构后,Fluent
API 解决了以前的局限性,成为现有存储库实现的自然扩展。我们相信这是另一个组合优于继承的例子。
贡献
特别感谢 Chu Trung Kien 提供关于在存储库中使用 Fluent
API 的初步想法和代码。