Search / Filter / Specifier pattern for .NET, with an Entity Framework Core implementation.
Packages:
RoyalCode.SmartSearch.Abstractions— contracts (ICriteria<TEntity>,ISearch<…>,ISorting, …).RoyalCode.SmartSearch.Core— default implementations (Criteria,Search,CriteriaOptions).RoyalCode.SmartSearch.Linq— LINQ specifiers/selectors/order-by.RoyalCode.SmartSearch.EntityFramework— EF Core pipeline (ICriteriaover aDbContext).RoyalCode.SmartSearch.AspNetCore— ASP.NET Core helpers.
Register entities and resolve ICriteria<TEntity> (directly or via ISearchManager<TDbContext>):
services.AddEntityFrameworkSearches<MyDbContext>(cfg => cfg.Add<MyEntity>());
var criteria = provider.GetRequiredService<ICriteria<MyEntity>>();
var items = criteria.FilterBy(new MyFilter { Name = "abc" }).Collect();ICriteria<TEntity> exposes FilterBy, OrderBy, UseHints, Select<TDto>(), AsSearch(), and the terminals
Collect/CollectAsync, Exists/ExistsAsync, FirstOrDefault/Single (+ async).
ICriteria does not take a raw Include(...) expression. To load navigations (children, owned, 1:1) you declare
the graph once per (entity, hint) using RoyalCode.OperationHint,
then reference it by hint. This keeps the contract provider-neutral and centralizes include knowledge.
Rule of thumb: a read that returns a DTO uses
Select<TDto>(); loading the aggregate (to mutate or expose its graph) uses hints.
services.AddEntityFrameworkSearches<MyDbContext>(cfg => cfg.Add<Blog>());
services.ConfigureOperationHints(registry =>
registry.AddIncludesHandler<Blog, BlogHints>((hint, includes) =>
{
if (hint is BlogHints.WithPosts) includes.IncludeCollection(b => b.Posts);
if (hint is BlogHints.WithOwner) includes.IncludeReference(b => b.Owner);
}));
public enum BlogHints { WithPosts, WithOwner }var blog = criteria
.FilterBy(new BlogFilter { Id = 5 })
.UseHints(BlogHints.WithPosts, BlogHints.WithOwner)
.Single(); // blog.Posts and blog.Owner are loadedUseHints is local: a sibling criteria in the same scope is unaffected.
scope.ServiceProvider.GetRequiredService<IHintsContainer>().AddHint(BlogHints.WithPosts);
var blogs = criteria.Collect(); // every criteria in this scope includes PostsUseHints and ambient hints union.
- ✅ Entity terminals:
Collect/ToList,FirstOrDefault,Single, andAsSearch().ToList(). - ❌
Exists()(it is anAny()), and ❌ afterSelect<TDto>()(projection — the type changes). - 🔁 The same registration also drives the post-load
Findpath (IHintPerformer.Perform(entity, source)). - 🟢 Without
OperationHintregistered, criteria behave exactly as before (no include applied).