英文:
Implementing Specification pattern with a join in C# and Entity Framework Core
问题
我正在使用领域驱动设计方法来指定从“存储库”返回的项目的筛选条件 - 但现在我需要在一个相关表上进行筛选,而我没有导航属性。
我有以下表格:
Group
===
uniqueidentifier Id
nvarchar(32) Name
PersonGroupMembership
===
uniqueidentifier Id
uniqueidentifier PersonId
uniqueidentifier GroupId
Person
===
uniqueidentifier Id
nvarchar(32) Name
我的 C# 类如下,每个类在我的 DbContext
上都有一个 DbSet<T>
:
public class Group
{
public Guid Id { get; set; }
public string Name { get; set; }
}
public class PersonGroupMembership
{
public Guid Id { get; set; }
public Guid PersonId { get; set; }
public Guid GroupId { get; set; }
}
public class Person
{
public Guid Id { get; set; }
public string Name { get; set; }
}
我的当前 specification
接口如下:
public interface ISpecification<T>
{
IQueryable<T> Apply(IQueryable<T> query);
}
而我的存储库上的 SearchAsync
方法看起来(大致)如下:
public Task<T[]> SearchAsync(ISpecification<T>[] specifications)
{
if ((specifications?.Length ?? 0) == 0)
throw new ArgumentException(
paramName: nameof(specifications),
message: "至少需要一个规范");
IQueryable<T> query = DbSet.AsQueryable<T>();
foreach(var specification in specifications)
query = specification.Apply(query);
return query.ToArrayAsync();
}
一个示例规范如下:
public class FilterHasEmailAddress : ISpecification<Person>
{
public string EmailAddress { get; init; }
public IQueryable<Person> Apply(IQueryable<Person> query) =>
query.Where(x => x.EmailAddress == EmailAddress);
}
英文:
I am using a Domain Driven Design approach to specifying filters on items returned from a Repository
- but now I need to filter on a related table for which I do not have navigation properties.
I have the following tables
Group
===
uniqueidentifier Id
nvarchar(32) Name
PersonGroupMembership
===
uniqueidentifier Id
uniqueidentifier PersonId
uniqueidentifier GroupId
Person
===
uniqueidentifier Id
nvarchar(32) Name
My C# classes, which each have a DbSet<T>
on my DbContext
, are as follows
public class Group
{
public Guid Id { get; set; }
public string Name { get; set; }
}
public class PersonGroupMembership
{
public Guid Id { get; set; }
public Guid PersonId { get; set; }
public Guid GroupId { get; set; }
}
public class Person
{
public Guid Id { get; set; }
public string Name { get; set; }
}
My current specification
interface looks like this
public interface ISpecification<T>
{
IQueryable<T> Apply(IQueryable<T> query);
}
And my SearchAsync
method on a repository looks (something) like this
public Task<T[]> SearchAsync(ISpecification<T>[] specifications)
{
if ((specifications?.Length ?? 0) == 0)
throw new ArgumentException(
paramName: nameof(specifications),
message: "At least one specification is required");
IQueryable<T> query = DbSet.AsQueryable<T>();
foreach(var specification in specifications)
query = specification.Apply(query);
return query.ToArrayAsync();
}
An example specification
public class FilterHasEmailAddress : ISpecification<Person>
{
public required string EmailAddress { get; init; }
public IQueryable<Person> Apply(IQueryable<Person> query) =>
query.Where(x => x.EmailAddress == EmailAddress);
}
答案1
得分: 1
以下是翻译好的部分:
假设你想要查询 Person
根据 Group
名称来实现的一个示例,当然,正确的答案是你必须在模型中创建关系,但你可能有自己的原因不这样做(也许是因为这些是松散耦合的聚合,而这些对象并不属于同一个聚合中)。
这是仓储模式的永恒问题。你使用高级别实体模型来实现所有内容,包括查询,并完全抽象化数据库。规范模式甚至为非常特定的查询提供了高级别定制,所以你认为一切都很好。但这完全将你绑定在模型中存在的关系上,并禁止你使用特定于数据库的构造,比如仅适用于SQL服务器的许多 EF.Functions
。此外,当编写这些数据库不可知的构造时,你不断需要考虑你的高级别LINQ语句实际上是否可以翻译为你正在使用的数据库(对于某些数据库,如MongoDB,这种可能性非常低)。真正的数据库不可知性几乎是不可能实现的。
由于你无论如何都不能忘记数据库,我通常更喜欢在仓储和领域之间建立一个明确的边界,这样在你的领域中,你真的不必担心数据库细节。
在这种情况下,我认为我会建议创建一个单独的仓储,用于存放所有无法通过现有仓储和规范模式完成的事情。因此,你的大部分代码是友好的数据库不可知性(尽管仅仅是表面上的,出于上述原因),而你无法使用的肮脏的东西可以放在一个单独的仓储中,将其公开为领域服务,但在你的最深层数据库层中实现。
在足够大的项目中,你将不可避免地遇到需要特定于数据库的构造的情况,要么是因为查询需要非常特定的内容,这不能通过LINQ-to-SQL翻译来支持,要么是因为你需要进行一些重大优化。
英文:
Assuming that an example of what you want to achieve is to query Person
by the Group
name, the right answer is of course that you have to create the relationships in the model, but you probably have reasons not to do that (perhaps because these are loosely coupled aggregations and these objects are nowhere part of the same aggregate)
This is the eternal problem of the repository pattern. You implement everything, including the queries, using the high level entities model and abstract away the database completely. The specification pattern even gives you a high level customization for very specific queries so you think all is well.
But this ties you completely to relationships that exist in the models and it forbids you from using database specific constructs such as many of the EF.Functions
that only apply to SQL servers. Moreover, when writing these database agnostic constructs you constantly have to take into account that your high level LINQ statement can actually be translated to the database that you are using (probability of which is quite bad for some databases such as MongoDB). True database agnosticity is almost impossible to achieve.
As you cannot forget about the database anyway I usually prefer to have a hard and explicit boundary between the repositories and the domain so that in your domain you truly have no worries about database details.
In this case, I think I would recommend a separate repository where you would put everything that you cannot do with the existing repositories and the Specification Pattern. So most of your code is nice and database agnostic (albeit only superficially for the reasons mentioned above) and the dirty stuff you cannot do with that you can put in a separate repository that you expose as a domain service but implement in the deepest database layer you've got.
In a big enough project you will inevitably run into a situation where you need database specific constructs, either because a query requires something very specific which is not supported through the LINQ-to-SQL translation, or because you need to do some heavy optimization.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论