MediCore 1.0.2

Prefix Reserved
dotnet add package MediCore --version 1.0.2
                    
NuGet\Install-Package MediCore -Version 1.0.2
                    
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="MediCore" Version="1.0.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="MediCore" Version="1.0.2" />
                    
Directory.Packages.props
<PackageReference Include="MediCore" />
                    
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 MediCore --version 1.0.2
                    
#r "nuget: MediCore, 1.0.2"
                    
#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 MediCore@1.0.2
                    
#: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=MediCore&version=1.0.2
                    
Install as a Cake Addin
#tool nuget:?package=MediCore&version=1.0.2
                    
Install as a Cake Tool

MediCore

NuGet License: MIT .NET

A lightweight CQRS mediator for .NET 7, 8, and 9 with built-in request interceptors (logging, validation, transactions), notifications, and a startup service registry.

MediCore lets you structure application logic around Commands, Queries, and Notifications, keeping business logic independent of controllers and infrastructure, and keeping cross-cutting concerns (logging, validation, transactions) out of your handlers entirely.

dotnet add package MediCore

Why MediCore?

As a .NET application grows, its logic can become tightly coupled to controllers, infrastructure, validation, logging, and transaction handling. MediCore introduces a central dispatching model instead:

Caller → ISender → Mediator → RequestDispatcher → Interceptor Pipeline → Handler
                                                        │
                                                        ├── Logging
                                                        ├── Validation
                                                        ├── Transaction
                                                        └── Custom interceptors

Instead of depending directly on a business service, callers send a request and MediCore locates and executes the right handler:

var orderId = await sender.SendAsync(new CreateOrder("Acme"));

Key Features

Feature Description
CQRS-first requests ICommand<T> / IQuery<T> make write vs. read intent explicit at compile time
Interface segregation Inject ISender, IPublisher, or IMediator — only what a component actually needs
Interceptor pipeline A Chain-of-Responsibility pipeline wraps every request; interceptors can short-circuit
Built-in interceptors Logging, validation (IValidator<T>), and transaction handling ship out of the box
Notifications One event (INotification), many independent handlers, sequential or parallel publishing
Pluggable transactions ITransactionManager abstracts EF Core / Dapper / ADO.NET / TransactionScope
Service registry IServiceRegistry exposes every handler and interceptor discovered at startup
Fail-fast discovery Duplicate or missing handlers are detected at startup, not at request time

Available abstractions

Interface Responsibility
ISender Dispatch commands and queries
IPublisher Publish notifications
IMediator Both sending and publishing (ISender + IPublisher)
ICommand<T> / ICommand A write request (returns T, or nothing)
IQuery<T> A read request
IRequestHandler<TRequest, TResponse> Handles a request
ICommandHandler<T, TResponse> / IQueryHandler<T, TResponse> Semantic aliases of IRequestHandler<,> for commands/queries
INotification / INotificationHandler<T> An event and its handler(s)
IRequestInterceptor<TRequest, TResponse> Cross-cutting behavior around a request
IValidator<T> Validates a request before it reaches the handler
ITransactionManager Begins/commits/rolls back a transaction
INotificationPublisher Strategy for invoking notification handlers
IServiceRegistry Read-only catalogue of everything discovered at startup

Quick Start

1. Register MediCore

services.AddMediCore(options => options
    .RegisterHandlersFromAssemblyContaining<Program>()
    .AddDefaultInterceptors()
    .UseTransactionManager<MyEfTransactionManager>()); // optional — defaults to TransactionScopeManager

AddDefaultInterceptors() wires up, in order: Logging → Validation → Transaction → Handler.

2. Define a command

public sealed record CreateOrder(string Customer) : ICommand<Guid>;

3. Implement the handler

public sealed class CreateOrderHandler : ICommandHandler<CreateOrder, Guid>
{
    public Task<Guid> HandleAsync(CreateOrder command, CancellationToken cancellationToken)
    {
        var orderId = Guid.NewGuid();
        // application logic...
        return Task.FromResult(orderId);
    }
}

4. (Optional) Add a validator

public sealed class CreateOrderValidator : IValidator<CreateOrder>
{
    public Task<ValidationResult> ValidateAsync(CreateOrder instance, CancellationToken cancellationToken)
        => Task.FromResult(string.IsNullOrWhiteSpace(instance.Customer)
            ? ValidationResult.Failure(new ValidationError(nameof(instance.Customer), "Customer is required."))
            : ValidationResult.Success);
}
services.AddScoped<IValidator<CreateOrder>, CreateOrderValidator>();

The validation interceptor runs every registered IValidator<T> before the handler executes and throws ValidationException if any of them fail.

5. Send the command

public sealed class OrderService(ISender sender)
{
    public Task<Guid> CreateOrderAsync(string customer, CancellationToken ct)
        => sender.SendAsync(new CreateOrder(customer), ct);
}

The caller never needs to know which class handles the request.


Commands vs. Queries

public sealed record UpdateOrderStatus(Guid OrderId, string Status) : ICommand<bool>;
public sealed record GetOrder(Guid OrderId) : IQuery<OrderDto>;
  • Commands change state and are wrapped in a transaction by the default TransactionInterceptor.
  • Queries only read state and are never wrapped in a transaction — the interceptor's generic constraint (where TRequest : ICommand<TResponse>) means it simply doesn't apply to them.

Interceptors

Interceptors wrap request execution so cross-cutting concerns don't have to live inside handlers. Each one decides whether to call next():

public sealed class CachingInterceptor<TRequest, TResponse> : IRequestInterceptor<TRequest, TResponse>
    where TRequest : IQuery<TResponse>
{
    public async Task<TResponse> InterceptAsync(
        TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
    {
        // check cache...
        var response = await next();
        // store in cache...
        return response;
    }
}
options.AddInterceptor(typeof(CachingInterceptor<,>));

Because it's constrained to IQuery<TResponse>, it only ever applies to queries — the same mechanism the built-in TransactionInterceptor uses to target commands only.

Built-in interceptors

Interceptor Behavior
LoggingInterceptor<,> Logs start, duration, and failures around every request
ValidationInterceptor<,> Runs registered IValidator<T> implementations, throws ValidationException on failure
TransactionInterceptor<,> Wraps ICommand<T> execution in a transaction via ITransactionManager; commits on success, rolls back on exception

Execution order

Interceptors run in registration order — the first one registered is the outermost:

options
    .AddInterceptor(typeof(LoggingInterceptor<,>))
    .AddInterceptor(typeof(ValidationInterceptor<,>))
    .AddInterceptor(typeof(TransactionInterceptor<,>));
Logging → Validation → Transaction → Handler

AddInterceptor accepts an open generic (typeof(MyInterceptor<,>), applied to every matching request) or a closed type (applied to one specific request).


Notifications

One event, many independent handlers — useful when several unrelated things need to happen after something occurs:

public sealed record OrderPlaced(Guid OrderId) : INotification;

await publisher.PublishAsync(new OrderPlaced(orderId));
OrderPlaced → IPublisher → UpdateAnalyticsHandler
                          → SendEmailHandler
                          → AuditOrderHandler

Handlers are resolved by the notification's compile-time type, so publish the concrete type, not a variable typed as INotification.

Publishing strategies (INotificationPublisher)

Strategy Behavior
SequentialNotificationPublisher (default) Runs handlers one after another; a failing handler stops the rest
ParallelNotificationPublisher Starts all handlers at once via Task.WhenAll
options.UseParallelNotificationPublisher();
// or: options.UseNotificationPublisher<MyPublisher>();

Transactions

Applications depend on ITransactionManager, not directly on EF Core, Dapper, or ADO.NET:

public interface ITransactionManager
{
    Task<ITransaction> BeginAsync(CancellationToken cancellationToken);
}

MediCore ships TransactionScopeManager as the default implementation, built on System.Transactions.TransactionScope (ambient transaction, ReadCommitted, 30s timeout, async flow enabled). Swap in your own for explicit DbTransaction/EF Core transactions:

services.AddMediCore(options => options
    .RegisterHandlersFromAssemblyContaining<Program>()
    .AddDefaultInterceptors()
    .UseTransactionManager<MyEfTransactionManager>());

Service Registry

IServiceRegistry is a read-only, singleton catalogue of everything MediCore discovered at startup — useful for diagnostics, health checks, or startup assertions:

var registry = provider.GetRequiredService<IServiceRegistry>();

registry.RequestHandlers;      // every discovered request → handler mapping
registry.NotificationHandlers; // every discovered notification → handler(s) mapping
registry.Interceptors;         // interceptors, in execution order (outermost first)

Fail-fast handler discovery

A request must resolve to exactly one handler. If two handlers are found for the same request type, AddMediCore throws InvalidOperationException at startup rather than letting the ambiguity reach a live request. Sending a request with no registered handler throws HandlerNotFoundException at dispatch time.


Project Structure

MediCore-main/
├── src/MediCore/
│   ├── Abstractions/     ICommand, IQuery, IRequest, IRequestHandler, ISender, IPublisher,
│   │                     IMediator, INotification(Handler), IRequestInterceptor, IValidator,
│   │                     ITransactionManager, Unit
│   ├── Core/             Mediator (facade), RequestDispatcher (adapter), InterceptorPipeline
│   ├── Interceptors/     LoggingInterceptor, ValidationInterceptor, TransactionInterceptor
│   ├── Publishing/       SequentialNotificationPublisher, ParallelNotificationPublisher
│   ├── Transactions/     TransactionScopeManager (default ITransactionManager)
│   ├── Registry/         IServiceRegistry, ServiceRegistry, HandlerScanner, MediCoreOptions,
│   │                     MediCoreServiceCollectionExtensions (AddMediCore)
│   ├── Exceptions/       HandlerNotFoundException, ValidationException
│   └── MediCore.csproj
├── docs/                 GitHub Pages landing site (see docs/DEPLOY.md)
├── icon/                 NuGet package icon
├── LICENSE.txt           MIT
└── README.md

This repository currently contains only the library itself — there is no sample application or automated test project checked in yet. If you add samples/ or tests/ projects, list them here and update the build instructions below.


Design Patterns Used

Pattern Where
Mediator Decouples request senders from request handlers
Facade Mediator hides dispatching, pipeline construction, and publishing
Chain of Responsibility Interceptors execute as an ordered pipeline
Decorator Each interceptor wraps the next execution stage
Strategy Notification publishing and transaction management are swappable
Registry ServiceRegistry stores discovered handlers and interceptors
Builder / Options MediCoreOptions provides fluent configuration
Adapter RequestDispatcher adapts runtime requests to strongly typed handlers
Flyweight One dispatcher instance is cached and reused per (request, response) type pair
Dependency Inversion Core behavior (transactions, publishing) depends on abstractions
CQRS Commands and queries represent distinct application intentions

When to Use MediCore

Useful once an application has several distinct operations (CreateOrder, GetOrder, CancelOrder, SearchOrders, ...) that each deserve an isolated handler, and/or you need repeatable cross-cutting behavior (validation, logging, transactions) without duplicating it in every service. Works naturally with Clean/Onion/layered architectures, modular monoliths, ASP.NET Core APIs, and background workers.

For a small application with only a couple of operations, calling a service directly may be simpler — MediCore's value grows with the number of operations and cross-cutting concerns.


Building From Source

git clone https://github.com/VarunSalma/MediCore.git
cd MediCore
dotnet build

There is currently no test project in this repository — dotnet test will report "no tests to run" unless you've added one.


Important Notes

  • Publish concrete notification types. Handlers are resolved by the compile-time notification type, so await publisher.PublishAsync(new OrderPlaced(orderId)), not a variable typed as INotification.
  • Interceptor order matters. With Logging → Validation → Transaction, logging wraps the whole request, validation runs before any transaction begins, and the transaction stays as close to the handler as possible.
  • Replace TransactionScopeManager with your own ITransactionManager when you need explicit EF Core / Dapper / ADO.NET transactions instead of an ambient TransactionScope.

License

MediCore is licensed under the MIT License.

Product Compatible and additional computed target framework versions.
.NET net7.0 is compatible.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  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 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

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.2 99 9/21/2026
1.0.1 93 9/20/2026
1.0.0 97 9/19/2026

Added package icon, symbols and Source Link support.