Myth.Specification 3.0.4

There is a newer prerelease version of this package available.
See the version list below for details.
dotnet add package Myth.Specification --version 3.0.4
                    
NuGet\Install-Package Myth.Specification -Version 3.0.4
                    
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="Myth.Specification" Version="3.0.4" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Myth.Specification" Version="3.0.4" />
                    
Directory.Packages.props
<PackageReference Include="Myth.Specification" />
                    
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 Myth.Specification --version 3.0.4
                    
#r "nuget: Myth.Specification, 3.0.4"
                    
#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 Myth.Specification@3.0.4
                    
#: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=Myth.Specification&version=3.0.4
                    
Install as a Cake Addin
#tool nuget:?package=Myth.Specification&version=3.0.4
                    
Install as a Cake Tool

Myth.Specification

NuGet Version NuGet Version

License

pt-br en

A fluent, type-safe .NET library implementing the Specification Pattern for building complex, composable, and testable queries. Keep your business logic readable, maintainable, and firmly rooted in domain concepts.

Why Myth.Specification?

Traditional query building often leads to:

  • Scattered business rules across services and repositories
  • Difficult-to-test query logic
  • Duplicate filtering logic in multiple places
  • Poor readability and maintainability

Myth.Specification solves these problems by:

  • Encapsulating query logic in reusable, composable specifications
  • Making business rules explicit and self-documenting
  • Providing a fluent, chainable API for building complex queries
  • Supporting filtering, sorting, pagination, and validation in one cohesive interface

Installation

dotnet add package Myth.Specification

Core Concepts

The library is built around the ISpec<T> interface and SpecBuilder<T> abstract class, providing:

  • Filtering: Combine predicates with And, Or, Not, AndIf, OrIf
  • Sorting: Chain multiple sort criteria with Order and OrderDescending
  • Pagination: Apply skip/take logic with Skip, Take, or WithPagination
  • Post-processing: Apply distinct operations with DistinctBy
  • Validation: Check if entities satisfy specifications with IsSatisfiedBy
  • Execution: Apply specifications to queryables and enumerate results

Quick Start

Basic Specification Building

using Myth.Interfaces;
using Myth.Specifications;
using Myth.Extensions;

// Build a specification
var spec = SpecBuilder<Person>
    .Create()
    .And( x => x.IsActive )
    .And( x => x.Age >= 18 )
    .Order( x => x.Name )
    .Skip( 10 )
    .Take( 20 );

// Apply to a queryable
var results = dbContext.People
    .Specify( spec )
    .ToList();

Encapsulating Business Rules

The real power comes from creating reusable specification extensions that represent business concepts:

public static class PersonSpecifications {
    public static ISpec<Person> IsAdult( this ISpec<Person> spec ) {
        return spec.And( person => person.Age >= 18 );
    }

    public static ISpec<Person> IsActive( this ISpec<Person> spec ) {
        return spec.And( person => person.Status == PersonStatus.Active );
    }

    public static ISpec<Person> HasEmail( this ISpec<Person> spec ) {
        return spec.And( person => !string.IsNullOrEmpty( person.Email ) );
    }

    public static ISpec<Person> InCity( this ISpec<Person> spec, string city ) {
        return spec.And( person => person.Address.City == city );
    }

    public static ISpec<Person> RegisteredAfter( this ISpec<Person> spec, DateTime date ) {
        return spec.And( person => person.RegistrationDate >= date );
    }
}

Now your queries read like business requirements:

var spec = SpecBuilder<Person>
    .Create()
    .IsActive()
    .IsAdult()
    .HasEmail()
    .InCity( "New York" )
    .RegisteredAfter( DateTime.UtcNow.AddYears( -1 ) )
    .Order( x => x.Name )
    .WithPagination( pagination );

var activeAdultSubscribers = await repository.GetAsync( spec, cancellationToken );

API Reference

Building Specifications

Creating Specifications
// Start with empty specification
var spec = SpecBuilder<Person>.Create();
Logical Operators
// And - adds a filter condition
spec.And( x => x.Age >= 18 );
spec.And( otherSpec );

// AndIf - conditionally adds a filter
spec.AndIf( includeInactive, x => x.IsActive == false );

// Or - adds an alternative filter condition
spec.Or( x => x.IsVip );
spec.Or( vipSpec );

// OrIf - conditionally adds an alternative filter
spec.OrIf( allowGuests, x => x.Role == "Guest" );

// Not - negates the current specification
spec.And( x => x.Status == "Banned" ).Not();
Sorting
// Sort ascending
spec.Order( x => x.LastName )
    .Order( x => x.FirstName );

// Sort descending
spec.OrderDescending( x => x.RegistrationDate )
    .OrderDescending( x => x.Score );

// Combine ascending and descending
spec.Order( x => x.Category )
    .OrderDescending( x => x.Priority );
Pagination
// Manual pagination
spec.Skip( 20 ).Take( 10 );

// Using Pagination value object
using Myth.ValueObjects;

var pagination = new Pagination( pageNumber: 3, pageSize: 20 );
spec.WithPagination( pagination );

// Special cases
Pagination.Default;  // Page 1, 10 items
Pagination.All;      // No pagination (returns all)

// Combining pagination methods
spec.WithPagination( pagination )
    .Skip( 5 )  // Additional skip
    .Take( 3 ); // Further limit
Post-Processing
// Remove duplicates based on property
spec.DistinctBy( x => x.Email );

Applying Specifications

Extension Methods
using Myth.Extensions;

IQueryable<Person> people = dbContext.People;

// Apply all transformations (filter + sort + paginate)
var results = people.Specify( spec );

// Apply only filtering
var filtered = people.Filter( spec );

// Apply only sorting
var sorted = people.Sort( spec );

// Apply only pagination/post-processing
var paginated = people.Paginate( spec );
Direct Methods
// Apply all transformations
IQueryable<Person> result = spec.Prepare( queryable );

// Apply individual transformations
IQueryable<Person> filtered = spec.Filtered( queryable );
IQueryable<Person> sorted = spec.Sorted( queryable );
IQueryable<Person> processed = spec.Processed( queryable );

// Get results directly
IQueryable<Person> items = spec.SatisfyingItemsFrom( queryable );
Person? singleItem = spec.SatisfyingItemFrom( queryable );
Validation
// Check if an entity satisfies the specification
Person person = GetPerson();
bool isValid = spec.IsSatisfiedBy( person );

Specification Properties

// Access the underlying expression
Expression<Func<T, bool>> predicate = spec.Predicate;
Func<T, bool> query = spec.Query;

// Access transformation functions
Func<IQueryable<T>, IOrderedQueryable<T>> sortFunc = spec.Sort;
Func<IQueryable<T>, IQueryable<T>> postProcessFunc = spec.PostProcess;

// Access pagination tracking
int itemsSkipped = spec.ItemsSkiped;
int itemsTaken = spec.ItemsTaked;

Integration with Repository Pattern

Myth.Specification works seamlessly with repository patterns:

public interface IPersonRepository {
    Task<IEnumerable<Person>> GetAsync( ISpec<Person> specification, CancellationToken cancellationToken = default );
    Task<Person?> GetSingleAsync( ISpec<Person> specification, CancellationToken cancellationToken = default );
    Task<int> CountAsync( ISpec<Person> specification, CancellationToken cancellationToken = default );
}

public class PersonRepository : IPersonRepository {
    private readonly DbContext _context;

    public async Task<IEnumerable<Person>> GetAsync( ISpec<Person> specification, CancellationToken cancellationToken = default ) {
        return await _context.People
            .Specify( specification )
            .ToListAsync( cancellationToken );
    }

    public async Task<Person?> GetSingleAsync( ISpec<Person> specification, CancellationToken cancellationToken = default ) {
        return specification.SatisfyingItemFrom( _context.People.AsQueryable() );
    }

    public async Task<int> CountAsync( ISpec<Person> specification, CancellationToken cancellationToken = default ) {
        return await _context.People
            .Filter( specification )
            .CountAsync( cancellationToken );
    }
}

Real-World Examples

public static class ProductSpecifications {
    public static ISpec<Product> InStock( this ISpec<Product> spec ) {
        return spec.And( p => p.StockQuantity > 0 );
    }

    public static ISpec<Product> InCategory( this ISpec<Product> spec, string category ) {
        return spec.And( p => p.Category == category );
    }

    public static ISpec<Product> InPriceRange( this ISpec<Product> spec, decimal min, decimal max ) {
        return spec.And( p => p.Price >= min && p.Price <= max );
    }

    public static ISpec<Product> WithRatingAbove( this ISpec<Product> spec, double rating ) {
        return spec.And( p => p.AverageRating >= rating );
    }

    public static ISpec<Product> OnSale( this ISpec<Product> spec ) {
        return spec.And( p => p.DiscountPercentage > 0 );
    }
}

// In a service
public async Task<ProductSearchResult> SearchProductsAsync( ProductSearchRequest request ) {
    var spec = SpecBuilder<Product>
        .Create()
        .InStock()
        .AndIf( !string.IsNullOrEmpty( request.Category ), s => s.InCategory( request.Category ) )
        .AndIf( request.MinPrice.HasValue && request.MaxPrice.HasValue,
                s => s.InPriceRange( request.MinPrice.Value, request.MaxPrice.Value ) )
        .AndIf( request.MinRating.HasValue, s => s.WithRatingAbove( request.MinRating.Value ) )
        .AndIf( request.OnSaleOnly, s => s.OnSale() )
        .OrderDescending( p => p.AverageRating )
        .WithPagination( request.Pagination );

    var products = await _productRepository.GetAsync( spec );

    return new ProductSearchResult { Products = products };
}

User Management

public static class UserSpecifications {
    public static ISpec<User> IsVerified( this ISpec<User> spec ) {
        return spec.And( u => u.EmailVerified );
    }

    public static ISpec<User> HasRole( this ISpec<User> spec, string role ) {
        return spec.And( u => u.Roles.Contains( role ) );
    }

    public static ISpec<User> LastLoginAfter( this ISpec<User> spec, DateTime date ) {
        return spec.And( u => u.LastLoginDate >= date );
    }

    public static ISpec<User> IsActive( this ISpec<User> spec ) {
        return spec.And( u => !u.IsDeleted && !u.IsSuspended );
    }
}

// Find inactive users for cleanup
var inactiveUsers = SpecBuilder<User>
    .Create()
    .IsActive()
    .LastLoginAfter( DateTime.UtcNow.AddMonths( -6 ) )
    .Not();  // Negate to find inactive users

Audit Log Filtering

public static class AuditLogSpecifications {
    public static ISpec<AuditLog> ByUser( this ISpec<AuditLog> spec, Guid userId ) {
        return spec.And( log => log.UserId == userId );
    }

    public static ISpec<AuditLog> ByAction( this ISpec<AuditLog> spec, string action ) {
        return spec.And( log => log.Action == action );
    }

    public static ISpec<AuditLog> InDateRange( this ISpec<AuditLog> spec, DateTime start, DateTime end ) {
        return spec.And( log => log.Timestamp >= start && log.Timestamp <= end );
    }

    public static ISpec<AuditLog> WithErrors( this ISpec<AuditLog> spec ) {
        return spec.And( log => !log.Success );
    }
}

// Query audit logs
var spec = SpecBuilder<AuditLog>
    .Create()
    .ByUser( userId )
    .InDateRange( startDate, endDate )
    .OrIf( includeErrors, s => s.WithErrors() )
    .OrderDescending( log => log.Timestamp )
    .WithPagination( pagination );

Advanced Usage

Specification Composition

// Create base specifications
var activeUsersSpec = SpecBuilder<User>
    .Create()
    .IsActive()
    .IsVerified();

var adminSpec = SpecBuilder<User>
    .Create()
    .HasRole( "Admin" );

// Compose specifications
var activeAdmins = activeUsersSpec.And( adminSpec );

// Or combine with additional filters
var recentActiveAdmins = activeAdmins
    .LastLoginAfter( DateTime.UtcNow.AddDays( -7 ) )
    .Order( u => u.LastLoginDate );

Dynamic Specification Building

public ISpec<Product> BuildProductSpec( ProductFilter filter ) {
    var spec = SpecBuilder<Product>.Create();

    if ( !string.IsNullOrEmpty( filter.SearchTerm ) ) {
        spec = spec.And( p => p.Name.Contains( filter.SearchTerm ) ||
                              p.Description.Contains( filter.SearchTerm ) );
    }

    if ( filter.Categories?.Any() == true ) {
        spec = spec.And( p => filter.Categories.Contains( p.Category ) );
    }

    if ( filter.MinPrice.HasValue ) {
        spec = spec.And( p => p.Price >= filter.MinPrice.Value );
    }

    if ( filter.MaxPrice.HasValue ) {
        spec = spec.And( p => p.Price <= filter.MaxPrice.Value );
    }

    return spec
        .Order( p => p.Name )
        .WithPagination( filter.Pagination );
}

Testing Specifications

[Fact]
public void ActiveAdultSpec_Should_Filter_Correctly() {
    // Arrange
    var person1 = new Person { Age = 25, IsActive = true };
    var person2 = new Person { Age = 17, IsActive = true };
    var person3 = new Person { Age = 30, IsActive = false };

    var spec = SpecBuilder<Person>
        .Create()
        .IsActive()
        .IsAdult();

    // Assert
    spec.IsSatisfiedBy( person1 ).Should().BeTrue();
    spec.IsSatisfiedBy( person2 ).Should().BeFalse();  // Not adult
    spec.IsSatisfiedBy( person3 ).Should().BeFalse();  // Not active
}

Best Practices

1. Create Specification Extension Methods

Always encapsulate business logic in extension methods:

// Good
public static ISpec<Order> IsPending( this ISpec<Order> spec ) {
    return spec.And( o => o.Status == OrderStatus.Pending );
}

// Avoid
var spec = SpecBuilder<Order>.Create().And( o => o.Status == OrderStatus.Pending );

2. Use Descriptive Names

Name specifications after business concepts, not technical operations:

// Good
.IsEligibleForDiscount()
.RequiresApproval()
.HasExpired()

// Avoid
.CheckStatus()
.FilterByDate()
.ValidateField()

3. Keep Specifications Focused

Each specification method should represent one business rule:

// Good
.IsActive()
.IsVerified()
.HasCompletedProfile()

// Avoid
.IsActiveAndVerifiedWithProfile()

4. Use Conditional Specifications

Leverage AndIf and OrIf for optional filters:

var spec = SpecBuilder<Product>
    .Create()
    .InStock()
    .AndIf( !string.IsNullOrEmpty( category ), s => s.InCategory( category ) )
    .AndIf( minPrice.HasValue, s => s.MinimumPrice( minPrice.Value ) );

5. Compose Specifications for Reusability

Build complex specifications from simpler ones:

public static ISpec<User> IsActiveSubscriber( this ISpec<User> spec ) {
    return spec
        .IsActive()
        .IsVerified()
        .HasActiveSubscription();
}

Performance Considerations

  • Specifications generate Expression Trees: All filter operations compile to SQL when used with Entity Framework
  • Lazy Evaluation: Specifications are not executed until enumerated
  • Efficient Sorting: Multiple sort operations are chained efficiently
  • Pagination Support: Skip/Take operations translate directly to SQL OFFSET/FETCH

Integration with Other Myth Libraries

Myth.Repository

public interface IRepository<T> {
    Task<IEnumerable<T>> FindAsync( ISpec<T> specification, CancellationToken cancellationToken = default );
}

Myth.Flow

var result = await Pipeline.Start( searchRequest )
    .Step( ctx => BuildSpecification( ctx ) )
    .StepAsync( async ( ctx, spec ) => await repository.GetAsync( spec ) )
    .ExecuteAsync();

Myth.Guard

builder.For( request.Pagination, x => x
    .Respect( p => p.PageNumber > 0 )
    .WithMessage( "Page number must be positive" ) );

Contributing

Contributions are welcome! Please read the contribution guidelines before submitting pull requests.

License

This project is licensed under the Apache License 2.0 - see the LICENSE file for details.

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 was computed.  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 was computed.  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.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on Myth.Specification:

Package Downloads
Myth.Repository

Generic repository pattern interfaces with async support, specification integration, and pagination. Provides read/write separation, CRUD operations, and extensible repository contracts for clean data access architecture.

Myth.Repository.EntityFramework

Entity Framework Core implementations of repository pattern with Unit of Work, specification support, expression handling, and transaction management for robust data access with EF Core.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
3.0.5-preview.3 38 11/4/2025
3.0.5-preview.2 38 11/4/2025
3.0.5-preview.1 37 11/4/2025
3.0.4 130 11/3/2025
3.0.4-preview.19 51 11/2/2025
3.0.4-preview.17 51 11/1/2025
3.0.4-preview.16 51 11/1/2025
3.0.4-preview.15 59 10/31/2025
3.0.4-preview.14 124 10/31/2025
3.0.4-preview.13 125 10/30/2025
3.0.4-preview.12 118 10/23/2025
3.0.4-preview.11 116 10/23/2025
3.0.4-preview.10 113 10/23/2025
3.0.4-preview.9 112 10/23/2025
3.0.4-preview.8 120 10/22/2025
3.0.4-preview.6 112 10/21/2025
3.0.4-preview.5 112 10/21/2025
3.0.4-preview.4 114 10/20/2025
3.0.4-preview.3 117 10/20/2025
3.0.4-preview.2 35 10/18/2025
3.0.4-preview.1 118 10/7/2025
3.0.3 235 8/30/2025
3.0.2 140 8/23/2025
3.0.2-preview.4 122 8/21/2025
3.0.2-preview.3 97 8/16/2025
3.0.2-preview.1 79 5/23/2025
3.0.1 3,361 3/12/2025
3.0.1-preview.2 156 3/11/2025
3.0.1-preview.1 88 2/5/2025
3.0.0.2-preview 144 12/10/2024
3.0.0.1-preview 183 6/29/2024
3.0.0 4,608 12/10/2024
3.0.0-preview 176 6/28/2024
2.0.0.17 2,328 12/15/2023
2.0.0.16 14,785 12/15/2023
2.0.0.15 294 12/15/2023
2.0.0.11 5,629 8/11/2022
2.0.0.10 2,976 7/20/2022
2.0.0.9 3,080 7/15/2022
2.0.0.8 3,097 7/12/2022
2.0.0.7 3,019 6/20/2022
2.0.0.6 3,138 5/23/2022
2.0.0.5 3,107 5/18/2022
2.0.0 3,188 2/17/2022