PouryaKit
Capabilities

Core helpers

Small helpers the Kit's own packages use and your code may too: predicate composition, the implementing-type lookup and a synchronization-context scope.

PouryaKit.Core holds a few small helpers and nothing else. AndAlso and OrElse compose two expression predicates into one lambda a LINQ provider such as EF Core can translate. ImplementingTypes.Of finds the concrete types of given assemblies that implement a contract, the same lookup the Kit's registrations use to find handlers, authorizers and endpoints. SynchronizationContextScope blocks on asynchronous code safely where a synchronous member has to.

You rarely reference it yourself: PouryaKit.Cqrs, PouryaKit.Web, PouryaKit.Messaging.Wolverine, PouryaKit.Persistence.Dapper, PouryaKit.Persistence.EFCore and PouryaKit.Security reference it, so a Module that uses any of them already has it.

Core stays this small

Core's public types are fixed (What Core never holds). A helper that belongs to one Capability lives in that Capability's package, and one that belongs to your application lives in your application.

When to use it

  • You build a query filter from optional parts and want one predicate EF Core can translate.
  • Your application registers its own implementations of a contract it finds in its assemblies.
  • A synchronous member has to wait for asynchronous code on a thread that may have a synchronization context.

How it works

Compose predicates

left.AndAlso(right) and left.OrElse(right) short-circuit as && and || do, and put both bodies on one new parameter, so the result is a single lambda with no invocation inside. Pass it to a specification's Query.Where or any IQueryable.

Find what implements a contract

ImplementingTypes.Of(contract, assemblies) returns each concrete type once, ordered by full name. An open generic contract finds every class that closes it, directly or through a base class.

Block on asynchronous code only inside a scope

SynchronizationContextScope.Enter() clears the current context and restores it when the scope is disposed. It is a ref struct, so the compiler refuses one that lives across an await. Code that is asynchronous all the way down needs no scope.

In the Kit's tests

Two predicates composed with AndAlso: AboveTen() decides first, and Even() runs only when it has not already decided the result.

PredicateBuilderTests.cs
var composed = AboveTen().AndAlso(Even()).Compile();

Assert.Equal(left && right, composed(value));
Assert.Equal(left ? 1 : 0, _rightEvaluations);

The lookup finds each concrete class that closes an open generic contract, directly or through a base class:

ImplementingTypesTests.cs
[Fact]
public void An_open_generic_definition_finds_each_concrete_class_that_closes_it_directly_or_through_a_base_class()
{
    // The assembly is given twice and read once. Open generic, abstract, interface and struct implementations are
    // left out.
    Assert.Equal(
        [typeof(DerivedHandler), typeof(NoteHandler)],
        ImplementingTypes.Of(typeof(IHandler<>), Fixtures, Fixtures)
    );
    Assert.Equal([typeof(Ledger)], ImplementingTypes.Of(typeof(Store<>), Fixtures));
}

Guarantees and failures

You want to knowRead
What the composed predicate's parameter is, and how a guard protects the right sidePredicate composition
What the lookup returns, and how to list the candidates a registration must validateImplementing-type lookup
What the scope sets, restores and refusesSynchronization-context scope
Why Core holds nothing moreWhat Core never holds

Next steps

Reference

PouryaKit.Core: three helpers and nothing else. One namespace, PouryaKit.Core; no dependencies. Cqrs, Web, Messaging.Wolverine, Persistence.Dapper, Persistence.EFCore and Security reference it, so a Module that references any of them already has it.

Predicate composition

AndAlso and OrElse are C# 14 extension members of Expression<Func<T, bool>>. They combine two predicates with Expression.AndAlso and Expression.OrElse, so the right predicate runs only when the left one has not decided the result, and they put both bodies on one new parameter, so a LINQ provider such as EF Core sees one lambda with no invocation in it, and a lambda nested in either predicate keeps the bindings it declares:

using System.Linq.Expressions;
using PouryaKit.Core;

internal sealed record ListedProduct(string Name, decimal Price, bool IsListed);

internal static class ListedProductFilters
{
    public static Expression<Func<ListedProduct, bool>> For(string? namePrefix, decimal? maxPrice)
    {
        Expression<Func<ListedProduct, bool>> filter = product => product.IsListed;
        if (namePrefix is not null)
        {
            filter = filter.AndAlso(product => product.Name.StartsWith(namePrefix));
        }

        if (maxPrice is not null)
        {
            filter = filter.AndAlso(product => product.Price <= maxPrice);
        }

        return filter;
    }
}

Pass the result to a specification's Query.Where or any IQueryable. Both predicates must be lambdas of one parameter; the composed one has a new parameter named as the left predicate's. A guard in the left predicate protects the right one, as && does in C#: product => product.Name != null followed by .AndAlso(product => product.Name.Length > 3) never reads the length of a null name.

Implementing-type lookup

ImplementingTypes.Of(contract, assemblies) returns the concrete types of the given assemblies that a contract describes, each once, ordered by full name. It is the only reflection Core offers, and the Kit's own registrations find their handlers, authorizers, Notification Handlers, typed IDs and endpoints through it too; use it for the application's own contracts:

using Microsoft.Extensions.DependencyInjection;
using PouryaKit.Core;

internal interface IReportExporter
{
    string Format { get; }
}

internal sealed class CsvReportExporter : IReportExporter
{
    public string Format => "csv";
}

internal static class ReportExporterRegistration
{
    public static IServiceCollection AddReportExporters(this IServiceCollection services)
    {
        foreach (var exporter in ImplementingTypes.Of(typeof(IReportExporter), typeof(IReportExporter).Assembly))
        {
            services.AddScoped(typeof(IReportExporter), exporter);
        }

        return services;
    }
}

The input contract:

  • A non-generic or closed contract, an interface or a class such as IReportExporter or IHandler<int>, matches every concrete type that can be assigned to it.
  • An open generic definition, an interface or a class such as typeof(IHandler<>) or typeof(Store<>), matches every concrete type that implements, or derives from, a closed form of it. Read each match's closed form from its interfaces or base types.
  • Only concrete classes come back: no interface, abstract or static class, struct or open generic class. Internal and nested classes are included, so a Module's internal implementations are found.
  • A type that fails to load throws ReflectionTypeLoadException rather than being skipped. A contract that is a generic parameter, or a generic type neither closed nor a definition, throws ArgumentException.

ImplementingTypes.Candidates(contract, assemblies) reads the contract the same way and returns every class it describes, abstract classes, open generic definitions and the classes the compiler generated included; interfaces and structs stay out, and the order is the same. Use it when a registration must reject a declaration Of would skip silently, as the Kit's own registrations reject a generic handler or a permission on an abstract type:

using PouryaKit.Core;

internal static class ReportExporterChecks
{
    public static void ThrowIfAnyIsUnusable()
    {
        var unusable = ImplementingTypes
            .Candidates(typeof(IReportExporter), typeof(IReportExporter).Assembly)
            .Where(type => type.IsAbstract || type.IsGenericTypeDefinition)
            .Select(type => $"{type.FullName} is an exporter that cannot be registered.")
            .ToList();
        if (unusable.Count > 0)
        {
            throw new InvalidOperationException(string.Join(Environment.NewLine, unusable));
        }
    }
}

Synchronization-context scope

SynchronizationContextScope.Enter(context) sets SynchronizationContext.Current to context, or to none when it is left out, and disposing the scope restores the context it found, after a normal exit or an exception. Its use is blocking on asynchronous code from a synchronous member that may run on a thread with a context, where the awaited continuations would otherwise wait for the blocked thread:

using PouryaKit.Core;

internal sealed class LegacyReportAdapter(Func<CancellationToken, Task<string>> renderAsync)
{
    public string Render()
    {
        using var scope = SynchronizationContextScope.Enter();
        return renderAsync(CancellationToken.None).GetAwaiter().GetResult();
    }
}

The context belongs to the thread, so the scope is a ref struct: the compiler refuses one that lives across an await, which could resume on another thread. Enter and dispose it on one thread, once, in a using statement or declaration. Asynchronous code all the way down needs no scope at all.

What Core never holds

Core's public types are exactly PredicateBuilder, ImplementingTypes and SynchronizationContextScope. A helper that belongs to one Capability lives in that Capability's package, and a helper that belongs to the application lives in the application.

On this page