RoyalCode.SmartSearch.Abstractions 0.10.0

dotnet add package RoyalCode.SmartSearch.Abstractions --version 0.10.0
                    
NuGet\Install-Package RoyalCode.SmartSearch.Abstractions -Version 0.10.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="RoyalCode.SmartSearch.Abstractions" Version="0.10.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="RoyalCode.SmartSearch.Abstractions" Version="0.10.0" />
                    
Directory.Packages.props
<PackageReference Include="RoyalCode.SmartSearch.Abstractions" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add RoyalCode.SmartSearch.Abstractions --version 0.10.0
                    
#r "nuget: RoyalCode.SmartSearch.Abstractions, 0.10.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package RoyalCode.SmartSearch.Abstractions@0.10.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=RoyalCode.SmartSearch.Abstractions&version=0.10.0
                    
Install as a Cake Addin
#tool nuget:?package=RoyalCode.SmartSearch.Abstractions&version=0.10.0
                    
Install as a Cake Tool

RoyalCode SmartSearch

Overview

RoyalCode SmartSearch is a set of .NET libraries for implementing advanced search, filtering, sorting, and data projection in enterprise applications, following the Specification/Filter-Specifier-Pattern. The goal is to facilitate the creation of reusable, decoupled, and extensible search components, integrating with ORMs such as Entity Framework.

Applied Pattern

The core pattern is the Filter-Specifier-Pattern (Specification Pattern), which allows you to define filters and search criteria declaratively, compose LINQ expressions, and simplify testing and maintenance. The main components are:

  • Filter: Object representing search criteria.
  • Specifier: Function or component that applies the filter to a LINQ query.
  • Selector: Projection from entities to DTOs.
  • Sorting: Sorting of results.
  • Disjunction (OR groups): Grouped OR conditions across multiple filter properties.
  • OR by property name/path: Automatically split properties containing "Or" into OR conditions.
  • ComplexFilter: Filter complex/owned types and structs by mapping scalar or nested filters to target paths.
  • FilterExpressionGenerator: Plug custom expression builders for complex filter scenarios via ISpecifierExpressionGenerator.

Libraries

RoyalCode.SmartSearch.Core

Abstract components for search and filtering using LINQ and the Specification Pattern.

RoyalCode.SmartSearch.Linq

Implements property resolution, expression generation, selector mapping, and dynamic sorting.

RoyalCode.SmartSearch.EntityFramework

Integration with Entity Framework Core, adding services and extensions to register entities and perform persistent searches.

RoyalCode.SmartSearch.Abstractions

Interfaces and contracts for results, sorting, projection, and search criteria.

Main Components

ICriteria<TEntity>

Interface for defining search criteria, applying filters, sorting, and collecting results:

var criteria = provider.GetRequiredService<ICriteria<SimpleModel>>();
criteria.FilterBy(new SimpleFilter { Name = "B" });
var results = criteria.Collect(); // Returns only records with Name = "B"

ISearchManager<TDbContext>

Search manager for a DbContext, allowing you to create criteria for entities:

var manager = provider.GetRequiredService<ISearchManager<MyDbContext>>();
var criteria = manager.Criteria<MyEntity>();

Sorting and ResultList

Allows sorting and paging of results, as well as projection to DTOs:

var sorting = new Sorting { OrderBy = "Name", Direction = ListSortDirection.Ascending };
criteria.OrderBy(sorting);
ResultList<MyEntity> result = ...;
var items = result.Items;

Search Configuration

Use ISearchConfigurations to configure filters, sorting, and selectors:

services.AddEntityFrameworkSearches<MyDbContext>(cfg =>
{
    cfg.Add<MyEntity>();
    cfg.AddOrderBy<MyEntity, string>("Name", x => x.Name);
    cfg.AddSelector<MyEntity, MyDto>(x => new MyDto { Id = x.Id, Name = x.Name });
});

6. Disjunction filters (grouped OR)

Use [Disjuction("alias")] to group multiple filter properties into an OR clause:

public class DisjunctionFilter
{
    [Disjuction("g1")] public string? P1 { get; set; }
    [Disjuction("g1")] public string? P2 { get; set; }
}

// When both are empty: no WHERE is applied.
// When one has a value: single condition.
// When multiple have values: OR across P1 and P2.

7. OR by property name or TargetPropertyPath

If a filter property name or Criterion.TargetPropertyPath contains Or, it is split and applied as OR across the parts:

public class OrFilterNameProperty
{
    [Criterion] // default operator for string is Like
    public string? FirstNameOrMiddleNameOrLastName { get; set; }
}

public class OrFilterTargetPath
{
    [Criterion(TargetPropertyPath = "FirstNameOrLastName")]
    public string? Query { get; set; }
}

8. ComplexFilter for complex/owned types

Use [ComplexFilter] on filter properties or complex types to apply nested filtering with automatic resolution of target properties.

Scalar-to-complex mapping (do not require [ComplexFilter]):

public readonly record struct Email(string Value);

public class UserFilter
{
    [Criterion("Email.Value")]
    public string? Email { get; set; } // maps to User.Email.Value
}

Nested complex type filtering:

public readonly record struct PersonName(string FirstName, string MiddleName, string LastName);

public class UserFilter
{
    [ComplexFilter]
    public PersonName? Name { get; set; } // filters FirstName/MiddleName/LastName, ignores empty fields
}

Owned/complex entity filtering via target path:

[ComplexFilter]
public class Address
{
    public string Street { get; set; } = null!;
    public string City { get; set; } = null!;
    public string State { get; set; } = null!;
    public string PostalCode { get; set; } = null!;
}

public class User
{
    public Address? MainAddress { get; set; }
}

public class UserFilter
{
    [Criterion("MainAddress")]
    public Address? Address { get; set; } // applies nested filters to MainAddress
}

Behavior notes:

  • Empty/null values are ignored when IgnoreIfIsEmpty applies (strings blank, nullables not set, empty collections).
  • Complex filters support AND across provided subfields; only non-empty subfields are applied.
  • OR semantics can be declared via [Disjuction] groups or inferred from Or in names/paths.

9. FilterExpressionGenerator for complex filter logic

Use [FilterExpressionGenerator<TGenerator>] to delegate expression creation to a custom generator that implements ISpecifierExpressionGenerator.

Attribute:

public class OrderByDateFilter
{
    [Criterion(nameof(Order.OrderDate))]
    [FilterExpressionGenerator<PeriodSpecifierExpressionGenerator>]
    public Period Period { get; set; }
}

Generator:

public class PeriodSpecifierExpressionGenerator : ISpecifierExpressionGenerator
{
    public static Expression GenerateExpression(ExpressionGeneratorContext ctx)
    {
        // Compute range (outside expression) and then apply via expression
        var getRange = typeof(PeriodSpecifierExpressionGenerator).GetMethod("GetRange")!;
        var rangeCall = Expression.Call(getRange, ctx.FilterMember);

        var start = Expression.Property(rangeCall, nameof(PeriodRange.Start));
        var end   = Expression.Property(rangeCall, nameof(PeriodRange.End));

        var ge = Expression.GreaterThanOrEqual(ctx.ModelMember, start);
        var lt = Expression.LessThan(ctx.ModelMember, end);
        var body = Expression.AndAlso(ge, lt);

        var predType = typeof(Func<,>).MakeGenericType(ctx.Model.Type, typeof(bool));
        var lambda = Expression.Lambda(predType, body, ctx.Model);

        var where = Expression.Call(typeof(Queryable), nameof(Queryable.Where), new[] { ctx.Model.Type }, ctx.Query, lambda);
        return Expression.Assign(ctx.Query, where);
    }
}

This allows encapsulating advanced logic (date ranges, business rules) and reusing it across filters while keeping the generated LINQ expression EF-translatable.

Usage Examples

1. Simple Search with Filter

public class SimpleModel { public int Id; public string Name; }
public class SimpleFilter { public string Name; }

var criteria = provider.GetRequiredService<ICriteria<SimpleModel>>();
criteria.FilterBy(new SimpleFilter { Name = "B" });
var results = criteria.Collect(); // Returns only records with Name = "B"
var results = await criteria.FilterBy(new SimpleFilter { Name = "A" }).CollectAsync();

3. Dynamic Sorting

criteria.OrderBy(new Sorting { OrderBy = "Name", Direction = ListSortDirection.Descending });
var results = criteria.Collect();

4. Projection to DTO

criteria.Select<MyDto>(); // Projects to the configured DTO

5. Advanced Filter Configuration

cfg.ConfigureSpecifierGenerator<MyEntity, MyFilter>(opt =>
{
    opt.For(f => f.SomeProperty).Predicate(val => e => e.Collection.Any(x => x.Id == val));
});

Tests and Examples

Tests in RoyalCode.SmartSearch.Tests demonstrate usage scenarios such as:

  • Filtering by simple and complex properties
  • Sorting and paging
  • Projection to DTOs
  • Custom filter configuration
  • Disjunction and OR splitting by name/path
  • Complex filter on structs/owned entities (Email, PersonName, Address)

References


For more examples, see the test files in the RoyalCode.SmartSearch.Tests folder.

Documentation Status

  • Libraries and patterns are described above with examples of usage.
  • More API-level docs and extension points can be expanded (e.g., selector resolvers, specifier generator options).
  • Tests illustrate typical usage; a dedicated samples directory could be added in the future.

Test Coverage

The test projects already reference coverlet.collector and Microsoft.CodeCoverage (see src/tests.targets).

Local coverage report

  1. Run tests and collect coverage:
    • dotnet test ./src --collect:"XPlat Code Coverage" --results-directory ./TestResults
  2. Generate HTML report:
    • Install tool once: dotnet tool install --global dotnet-reportgenerator-globaltool
    • Generate: reportgenerator -reports:./TestResults/**/coverage.cobertura.xml -targetdir:./TestResults/Report -reporttypes:Html
  3. Open ./TestResults/Report/index.html in a browser.

GitHub Actions coverage

The workflow at .github/workflows/smart-search.yml runs tests with coverage and publishes an artifact coverage-report containing the HTML output. After the run completes:

  • Download the coverage-report artifact from the job summary.
  • Open index.html to view coverage.
Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.
  • net8.0

    • No dependencies.
  • net9.0

    • No dependencies.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on RoyalCode.SmartSearch.Abstractions:

Package Downloads
RoyalCode.SmartSearch.Core

Persistence abstract components for search entities by filters using linq specifiers and queryables.

RoyalCode.SmartSearch.AspNetCore

Package Description

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.10.0 0 1/26/2026
0.9.1 301 1/19/2026
0.9.0 237 1/16/2026
0.8.6 224 10/23/2025
0.8.5 218 10/9/2025
0.8.4 212 10/9/2025
0.8.3 139 10/4/2025
0.8.2 577 7/25/2025
0.8.1 538 7/20/2025
0.8.0 297 7/19/2025
0.8.0-preview-3 206 7/18/2025
0.8.0-preview-2 224 6/26/2025
0.8.0-preview-1 203 6/25/2025
0.7.0 264 4/21/2025