存储库方法与扩展 IQueryable

发布于 2024-08-04 12:24:30 字数 997 浏览 8 评论 0原文

我有存储库(例如 ContactRepository、UserRepository 等),它们封装了对域模型的数据访问。

当我搜索数据时,例如

  • 查找名字的联系人 以 XYZ

    开头,

  • 生日晚于联系人 1960年

    (等),

我开始实现诸如 FirstNameStartsWith(string prefix)YoungerThanBirthYear(intyear) 之类的存储库方法,基本上遵循了许多示例。

然后我遇到了一个问题 - 如果我必须组合多个搜索怎么办?我的每个存储库搜索方法(例如上面的)仅返回一组有限的实际域对象。为了寻找更好的方法,我开始在 IQueryable上编写扩展方法,例如:

public static IQueryable<Contact> FirstNameStartsWith(
               this IQueryable<Contact> contacts, String prefix)
{
    return contacts.Where(
        contact => contact.FirstName.StartsWith(prefix));
}        

现在我可以做诸如此类的事情

ContactRepository.GetAll().FirstNameStartsWith("tex").YoungerThanBirthYear(1960);

但是,我发现自己正在编写扩展方法(并发明了疯狂的类,例如ContactsQueryableExtensions 全部结束,并且我通过将所有内容都放在适当的存储库中而失去了“良好的分组”,

这真的是这样做的方法,还是有更好的方法来实现相同的目标?

I have repositories (e.g. ContactRepository, UserRepository and so forth) which encapsulate data access to the domain model.

When I was looking at searching for data, e.g.

  • finding a contact whose first name
    starts with XYZ
  • a contact whose birthday is after
    1960

    (etc),

I started implementing repository methods such as FirstNameStartsWith(string prefix) and YoungerThanBirthYear(int year), basically following the many examples out there.

Then I hit a problem - what if I have to combine multiple searches? Each of my repository search methods, such as above, only return a finite set of actual domain objects. In search for a better way, I started writing extension methods on IQueryable<T>, e.g. this:

public static IQueryable<Contact> FirstNameStartsWith(
               this IQueryable<Contact> contacts, String prefix)
{
    return contacts.Where(
        contact => contact.FirstName.StartsWith(prefix));
}        

Now I can do things such as

ContactRepository.GetAll().FirstNameStartsWith("tex").YoungerThanBirthYear(1960);

However, I found myself writing extension methods (and inventing crazy classes such as ContactsQueryableExtensions all over, and I lose the "nice grouping" by having everything in the appropriate repository.

Is this really the way to do it, or is there a better way to achieve the same goal?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

往事随风而去 2024-08-11 12:24:30

自从开始现在的工作后,我最近一直在思考这个问题。我习惯了存储库,它们按照您的建议仅使用简单的存储库即可走完整的 IQueryable 路径。

我认为存储库模式是合理的,并且在描述您希望如何处理应用程序域中的数据方面做了半有效的工作。不过你所描述的问题肯定会发生。它变得混乱、快速,超出了简单的应用程序的范围。

也许有什么方法可以重新思考为什么你要以如此多的方式索取数据?如果没有,我真的认为混合方法是最好的方法。为您重用的内容创建存储库方法。实际上有意义的东西。干燥等等。但那些一次性的事情呢?为什么不利用 IQueryable 以及你可以用它做的有趣的事情呢?正如您所说,为此创建一种方法是愚蠢的,但这并不意味着您不需要数据。 DRY 在那里并不适用,不是吗?

做好这件事需要纪律,但我确实认为这是一条合适的道路。

I have been thinking about this a lot lately, after starting at my current job. I am used to Repositories, they go the full IQueryable path using just bare bones repositories as you suggest.

I feel the repo pattern is sound and does a semi-effective job at describing how you want to work with the data in the application domain. However the issue you are describing definitely occurs. It gets messy, fast, beyond a simple application.

Are there, perhaps, ways to rethink why you are asking for the data in so many ways? If not, I really feel that a hybrid approach is the best way to go. Create repo methods for the stuff you reuse. Stuff that actually it makes sense for. DRY and all that. But those one-offs? Why not take advantage of IQueryable and the sexy things you can do with it? It is silly, as you said, to create a method for that, but it doesn't mean you don't need the data. DRY doesn't really apply there does it?

It would take discipline to do this well, but I really think it's an appropriate path.

坏尐絯 2024-08-11 12:24:30

@Alex - 我知道这是一个老问题,但我要做的就是让存储库只做非常简单的事情。这意味着,获取表或视图的所有记录。

然后,在服务层(您使用的是 n 层解决方案,对吧?:))我将在那里处理所有“特殊”查询内容。

好的,示例时间。

存储库层

ContactRepository.cs

public IQueryable<Contact> GetContacts()
{
    return (from q in SqlContext.Contacts
            select q).AsQueryable();
}

漂亮又简单。 SqlContextEF Context 的实例。它有一个名为 ContactsEntity 。基本上是你的 sql Contacts 类。

这意味着,该方法基本上正在执行:SELECT * FROM CONTACTS ...但它没有使用该查询访问数据库......它现在只是一个查询。

好的..下一层..KICK ...我们继续(Inception有人吗?)

服务层

ContactService.cs

public  ICollection<Contact> FindContacts(string name)
{
    return FindContacts(name, null)
}

public ICollection<Contact> FindContacts(string name, int? year)
{
   IQueryable<Contact> query = _contactRepository.GetContacts();
   
   if (!string.IsNullOrEmpty(name))
   {
       query = from q in query
               where q.FirstName.StartsWith(name)
               select q;
   }

   if (int.HasValue)
   {
       query = from q in query
               where q.Birthday.Year <= year.Value
               select q);
    }

    return (from q in query
            select q).ToList();
}

完成。

让我们回顾一下。首先,我们从一个简单的“从联系人中获取所有内容”查询开始。现在,如果我们提供了姓名,那么让我们添加一个过滤器来按姓名过滤所有联系人。接下来,如果我们提供了年份,那么我们将按年份过滤生日。等等。最后,我们访问数据库(使用这个修改后的查询)并查看返回的结果。

注意:-

  • 为了简单起见,我省略了任何依赖注入。强烈推荐。
  • 这都是伪代码。未经测试(针对编译器),但您明白了......

要点

  • 服务层处理所有智能。您可以在此处决定需要哪些数据。
  • 存储库是一个简单的 SELECT * FROM TABLE 或简单的 INSERT/UPDATE 到表中。

祝你好运 :)

@Alex - i know this is an old question, but what I would be doing would be letting the Repository do really simple stuff only. This means, get all records for a table or view.

Then, in the SERVICES layer (you are using an n-tiered solution, right? :) ) i would be handling all the 'special' query stuff there.

Ok, example time.

Repository Layer

ContactRepository.cs

public IQueryable<Contact> GetContacts()
{
    return (from q in SqlContext.Contacts
            select q).AsQueryable();
}

Nice and simple. SqlContext is the instance of your EF Context .. which has an Entity on it called Contacts .. which is basically your sql Contacts class.

This means, that method basically is doing: SELECT * FROM CONTACTS ... but it's not hitting the database with that query .. it's only a query right now.

Ok .. next layer.. KICK ... up we go (Inception anyone?)

Services Layer

ContactService.cs

public  ICollection<Contact> FindContacts(string name)
{
    return FindContacts(name, null)
}

public ICollection<Contact> FindContacts(string name, int? year)
{
   IQueryable<Contact> query = _contactRepository.GetContacts();
   
   if (!string.IsNullOrEmpty(name))
   {
       query = from q in query
               where q.FirstName.StartsWith(name)
               select q;
   }

   if (int.HasValue)
   {
       query = from q in query
               where q.Birthday.Year <= year.Value
               select q);
    }

    return (from q in query
            select q).ToList();
}

Done.

So lets recap. First, we start our with a simple 'Get everything from contacts' query. Now, if we have a name provided, lets add a filter to filter all contacts by name. Next, if we have a year provided, then we filter the birthday by Year. Etc. Finally, we then hit the DB (with this modified query) and see what results we get back.

NOTES:-

  • I've omitted any Dependency Injection for simplicity. It's more than highly recommended.
  • This is all pseduo-code. Untested (against a compiler) but you get the idea ....

Takeaway points

  • The Services layer handles all the smarts. That is where you decide what data you require.
  • The Repository is a simple SELECT * FROM TABLE or a simple INSERT/UPDATE into TABLE.

Good luck :)

遇见了你 2024-08-11 12:24:30

我意识到这已经很旧了,但我最近一直在处理同样的问题,并且得出了与 Chad 相同的结论:只要有一点纪律,扩展方法和存储库方法的混合似乎效果最好。

我在(实体框架)应用程序中遵循的一些一般规则:

排序查询

如果该方法仅用于排序,我更喜欢编写在 IQueryable上操作的扩展方法>IOrderedQueryable(利用底层提供程序。)eg

public static IOrderedQueryable<TermRegistration> ThenByStudentName(
    this IOrderedQueryable<TermRegistration> query)
{
    return query
        .ThenBy(reg => reg.Student.FamilyName)
        .ThenBy(reg => reg.Student.GivenName);
}

现在我可以根据需要在我的存储库类中使用 ThenByStudentName()

返回单个实例的查询

如果方法涉及通过原始参数进行查询,则通常需要 ObjectContext 并且不能轻易地使其静态。我将这些方法留在我的存储库中,例如

public Student GetById(int id)
{
    // Calls context.ObjectSet<T>().SingleOrDefault(predicate) 
    // on my generic EntityRepository<T> class 
    return SingleOrDefault(student => student.Active && student.Id == id);
}

但是,如果该方法涉及使用其 导航属性,通常可以很容易地将其设为静态,并作为扩展方法实现。 eg

public static TermRegistration GetLatestRegistration(this Student student)
{
    return student.TermRegistrations.AsQueryable()
        .OrderByTerm()
        .FirstOrDefault();
}

现在我可以方便地编写 someStudent.GetLatestRegistration() 而无需当前范围内的存储库实例。

返回集合的查询

如果该方法返回一些 IEnumerableICollectionIList,那么我喜欢将其设为 static 如果可能,并将其保留在存储库中即使它使用导航属性。例如

public static IList<TermRegistration> GetByTerm(Term term, bool ordered)
{
    var termReg = term.TermRegistrations;
    return (ordered)
        ? termReg.AsQueryable().OrderByStudentName().ToList()
        : termReg.ToList();
}

这是因为我的GetAll()方法已经存在于存储库中,并且它有助于避免混乱的扩展方法。

不将这些“集合 getter”实现为扩展方法的另一个原因是,它们需要更详细的命名才有意义,因为没有隐含返回类型。例如,最后一个示例将变为 GetTermRegistrationsByTerm(this Term term)

我希望这有帮助!

I realize this is old, but I've been dealing with this same issue lately, and I came to the same conclusion as Chad: with a little discipline, a hybrid of extension methods and repository methods seems to work best.

Some general rules I've been following in my (Entity Framework) application:

Ordering queries

If the method is used only for ordering, I prefer to write extension methods that operate on IQueryable<T> or IOrderedQueryable<T> (to leverage the underlying provider.) e.g.

public static IOrderedQueryable<TermRegistration> ThenByStudentName(
    this IOrderedQueryable<TermRegistration> query)
{
    return query
        .ThenBy(reg => reg.Student.FamilyName)
        .ThenBy(reg => reg.Student.GivenName);
}

Now I can use ThenByStudentName() as needed within my repository class.

Queries returning single instances

If the method involves querying by primitive parameters, it usually requires an ObjectContext and can't be easily made static. These methods I leave on my repository, e.g.

public Student GetById(int id)
{
    // Calls context.ObjectSet<T>().SingleOrDefault(predicate) 
    // on my generic EntityRepository<T> class 
    return SingleOrDefault(student => student.Active && student.Id == id);
}

However, if the method instead involves querying an EntityObject using its navigation properties, it can usually be made static quite easily, and implemented as an extension method. e.g.

public static TermRegistration GetLatestRegistration(this Student student)
{
    return student.TermRegistrations.AsQueryable()
        .OrderByTerm()
        .FirstOrDefault();
}

Now I can conveniently write someStudent.GetLatestRegistration() without needing a repository instance in the current scope.

Queries returning collections

If the method returns some IEnumerable, ICollection or IList, then I like to make it static if possible, and leave it on the repository even if it uses navigation properties. e.g.

public static IList<TermRegistration> GetByTerm(Term term, bool ordered)
{
    var termReg = term.TermRegistrations;
    return (ordered)
        ? termReg.AsQueryable().OrderByStudentName().ToList()
        : termReg.ToList();
}

This is because my GetAll() methods already live on the repository, and it helps to avoid a cluttered mess of extension methods.

Another reason for not implementing these "collection getters" as extension methods is that they would require more verbose naming to be meaningful, since the return type isn't implied. For example, the last example would become GetTermRegistrationsByTerm(this Term term).

I hope this helps!

﹉夏雨初晴づ 2024-08-11 12:24:30

六年后,我确信@Alex 已经解决了他的问题,但在阅读了已接受的答案后,我想添加我的两分钱。

扩展存储库中的 IQueryable 集合的一般目的是为了提供灵活性并使其使用者能够自定义数据检索。亚历克斯已经做了很好的工作。

服务层的主要作用是遵守关注点分离原则并处理与业务功能相关的命令逻辑

在现实世界的应用程序中,查询逻辑通常不需要超出存储库本身提供的检索机制(例如值更改、类型转换)的扩展。

考虑以下两种场景:

IQueryable<Vehicle> Vehicles { get; }

// raw data
public static IQueryable<Vehicle> OwnedBy(this IQueryable<Vehicle> query, int ownerId)
{
    return query.Where(v => v.OwnerId == ownerId);
}

// business purpose
public static IQueryable<Vehicle> UsedThisYear(this IQueryable<Vehicle> query)
{
    return query.Where(v => v.LastUsed.Year == DateTime.Now.Year);
}

两种方法都是简单的查询扩展,但无论多么微妙,它们都有不同的作用。第一个是简单的过滤器,而第二个则意味着业务需求(例如维护或计费)。在一个简单的应用程序中,人们可以在存储库中实现它们。在更理想的系统中,UsedThisYear 最适合服务层(甚至可以作为普通实例方法实现),它还可以更好地促进分离 CQRS 策略。 em>命令和查询

关键考虑因素是 (a) 存储库的主要目的以及 (b) 您是否愿意遵守 CQRS 和 DDD 理念。

Six years later, I am certain @Alex has solved his problem, but after reading the accepted answer I wanted to add my two cents.

The general purpose of extending IQueryable collections in a repository to provide flexibility and empower its consumers to customize data retrieval. What Alex has already done is good work.

The primary role of a service layer is to adhere to the separation of concerns principle and address command logic associated with business function.

In real world applications, query logic often needs no extension beyond the retrieval mechanics provided by the repository itself (ex. value alterations, type conversions).

Consider the two following scenarios:

IQueryable<Vehicle> Vehicles { get; }

// raw data
public static IQueryable<Vehicle> OwnedBy(this IQueryable<Vehicle> query, int ownerId)
{
    return query.Where(v => v.OwnerId == ownerId);
}

// business purpose
public static IQueryable<Vehicle> UsedThisYear(this IQueryable<Vehicle> query)
{
    return query.Where(v => v.LastUsed.Year == DateTime.Now.Year);
}

Both methods are simple query extensions, but however subtle they have different roles. The first is a simple filter, while the second implies business need (ex. maintenance or billing). In a simple application one might implement them both in a repository. In a more idealistic system UsedThisYear is best suited for the service layer (and may even be implemented as a normal instance method) where it may also better facilitate CQRS strategy of separating commands and queries.

Key considerations are (a) the primary purpose of your repository and (b) how much do you like to adhere to CQRS and DDD philosophies.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文