August 4, 2025
Discover the Arbiter project - a modern implementation of the Mediator pattern for .NET applications embracing clean architecture and CQRS principles.
What is the Arbiter Project?
The Arbiter project is a comprehensive suite of libraries that implements the Mediator pattern and Command Query Responsibility Segregation (CQRS) in .NET. At its core lies the Arbiter.Mediation library, which serves as the foundation for building loosely coupled, testable applications using clean architectural patterns like Vertical Slice Architecture and CQRS.
While libraries like MediatR have dominated the .NET mediator space, Arbiter.Mediation brings several distinctive features to the table:
- Lightweight and Extensible: Designed with performance and extensibility in mind
- Modern .NET Support: Built specifically for contemporary .NET applications
- Clean Architecture Focus: Explicitly designed for Vertical Slice Architecture and CQRS patterns
- Comprehensive Ecosystem: Part of a larger suite that includes Entity Framework, MongoDB, and communication libraries
Request/Response Pattern
The library supports the classic request/response pattern using IRequest<TResponse>
and IRequestHandler<TRequest, TResponse>
interfaces:
public class Ping : IRequest<Pong>
{
public string? Message { get; set; }
}
public class PingHandler : IRequestHandler<Ping, Pong>
{
public async ValueTask<Pong> Handle(
Ping request,
CancellationToken cancellationToken = default)
{
// Simulate some work
await Task.Delay(100, cancellationToken);
return new Pong { Message = $"{request.Message} Pong" };
}
}
Event Notifications
For scenarios requiring event-driven architecture, Arbiter.Mediation provides notification support through INotification
and INotificationHandler<TNotification>
:
public class OrderCreatedEvent : INotification
{
public int OrderId { get; set; }
public DateTime CreatedAt { get; set; }
}
public class OrderCreatedHandler : INotificationHandler<OrderCreatedEvent>
{
public async ValueTask Handle(
OrderCreatedEvent notification,
CancellationToken cancellationToken = default)
{
// Handle the order created event
// Send email, update inventory, etc.
}
}
Pipeline Behaviors
One of the most powerful features is the pipeline behavior system, which acts like middleware for your requests:
public class PingBehavior : IPipelineBehavior<Ping, Pong>
{
public async ValueTask<Pong> Handle(
Ping request,
RequestHandlerDelegate<Pong> next,
CancellationToken cancellationToken = default)
{
// Pre-processing logic
Console.WriteLine($"Before handling: {request.Message}");
var response = await next(cancellationToken);
// Post-processing logic
Console.WriteLine($"After handling: {response.Message}");
return response;
}
}
This pattern enables cross-cutting concerns like logging, validation, caching, and performance monitoring without cluttering your business logic.
Getting started is straightforward. Install the NuGet package:
dotnet add package Arbiter.Mediation
Register the services in your dependency injection container:
// Register Mediator services
services.AddMediator();
// Register handlers
services.TryAddTransient<IRequestHandler<Ping, Pong>, PingHandler>();
// Optionally register pipeline behaviors
services.AddTransient<IPipelineBehavior<Ping, Pong>, PingBehavior>();
Then inject and use the mediator in your controllers or services:
public class PingController : ControllerBase
{
private readonly IMediator _mediator;
public PingController(IMediator mediator)
{
_mediator = mediator;
}
[HttpGet]
public async Task<IActionResult> Get(
string? message = null,
CancellationToken cancellationToken = default)
{
var request = new Ping { Message = message };
var response = await _mediator.Send<Ping, Pong>(request, cancellationToken);
return Ok(response);
}
}
Observability with OpenTelemetry
Modern applications require comprehensive observability. Arbiter.Mediation addresses this with the Arbiter.Mediation.OpenTelemetry
package, providing built-in tracing and metrics:
// Install: dotnet add package Arbiter.Mediation.OpenTelemetry
services.AddMediatorDiagnostics();
services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddMediatorInstrumentation()
.AddConsoleExporter()
)
.WithMetrics(metrics => metrics
.AddMediatorInstrumentation()
.AddConsoleExporter()
);
This integration allows you to monitor request performance, track handler execution, and identify bottlenecks in your application.
The Broader Arbiter Ecosystem
While Arbiter.Mediation forms the core, the Arbiter project extends far beyond basic mediation:
- Arbiter.CommandQuery: CQRS framework with base commands and queries for CRUD operations
- Arbiter.CommandQuery.EntityFramework: Entity Framework Core handlers for database operations
- Arbiter.CommandQuery.MongoDB: MongoDB handlers for document-based storage
- Arbiter.CommandQuery.Endpoints: Minimal API endpoints for your commands and queries
- Arbiter.Communication: Message template communication for email and SMS services
This comprehensive ecosystem enables you to build complete applications using consistent patterns across different layers and technologies.
Arbiter.Mediation is particularly well-suited for:
- Clean Architecture Applications: When you’re implementing Vertical Slice Architecture or onion architecture patterns
- CQRS Systems: Applications that benefit from command-query separation
- Microservices: Services that need clear request/response boundaries and event handling
- Modern .NET Applications: Projects targeting recent .NET versions that want to leverage contemporary patterns
While specific benchmarks aren’t publicly available, Arbiter.Mediation is designed with performance in mind. The use of ValueTask<T>
instead of Task<T>
in handler interfaces suggests attention to allocation efficiency, particularly for synchronous operations that complete immediately.
The dependency injection-based resolution and pipeline behavior system provide flexibility without sacrificing performance, making it suitable for high-throughput applications.
Conclusion
The Arbiter project, with Arbiter.Mediation at its core, represents a modern, thoughtful approach to implementing the Mediator pattern in .NET applications. Its focus on clean architecture, comprehensive ecosystem, and built-in observability support make it a compelling choice for developers building maintainable, scalable applications.
Whether you’re starting a new project or looking to refactor existing code toward cleaner architecture patterns, Arbiter.Mediation provides the tools and structure to implement robust, loosely coupled systems that are easy to test and maintain.
For teams already familiar with MediatR, the transition to Arbiter.Mediation should be relatively smooth, while offering additional features and a more comprehensive ecosystem for building complete applications.
Learn more about the Arbiter project and explore the source code on GitHub.
August 1, 2025
A flexible and lightweight API key authentication library for ASP.NET Core applications that supports multiple authentication patterns and integrates seamlessly with ASP.NET Core’s authentication and authorization infrastructure.
Overview
AspNetCore.SecurityKey provides a complete API key authentication solution for ASP.NET Core applications with support for modern development patterns and best practices.
Key Features:
- Multiple Input Sources - API keys via headers, query parameters, or cookies
- Flexible Authentication - Works with ASP.NET Core’s built-in authentication or as standalone middleware
- Extensible Design - Custom validation and extraction logic support
- Rich Integration - Controller attributes, middleware, and minimal API support
- OpenAPI Support - Automatic Swagger/OpenAPI documentation generation (.NET 9+)
- High Performance - Minimal overhead with optional caching
- Multiple Deployment Patterns - Attribute-based, middleware, or endpoint filters
Quick Start
-
Install the package:
dotnet add package AspNetCore.SecurityKey
-
Configure your API key in appsettings.json
:
{
"SecurityKey": "your-secret-api-key-here"
}
-
Register services and secure endpoints:
builder.Services.AddSecurityKey();
app.UseSecurityKey(); // Secures all endpoints
-
Call your API with the key:
curl -H "X-API-KEY: your-secret-api-key-here" https://yourapi.com/endpoint
Installation
The library is available on nuget.org via package name AspNetCore.SecurityKey
.
Package Manager Console
Install-Package AspNetCore.SecurityKey
.NET CLI
dotnet add package AspNetCore.SecurityKey
PackageReference
<PackageReference Include="AspNetCore.SecurityKey" />
How to Pass API Keys
AspNetCore.SecurityKey supports multiple ways to pass API keys in requests, providing flexibility for different client scenarios:
The most common and secure approach for API-to-API communication:
GET https://api.example.com/users
Accept: application/json
X-API-KEY: 01HSGVBSF99SK6XMJQJYF0X3WQ
Query Parameters
Useful for simple integrations or when headers cannot be easily modified:
GET https://api.example.com/users?X-API-KEY=01HSGVBSF99SK6XMJQJYF0X3WQ
Accept: application/json
Security Note: When using query parameters, be aware that API keys may appear in server logs, browser history, and referrer headers. Headers are generally preferred for production use.
Cookies
Ideal for browser-based applications or when API keys need persistence:
GET https://api.example.com/users
Accept: application/json
Cookie: X-API-KEY=01HSGVBSF99SK6XMJQJYF0X3WQ
Configuration
Basic Setup
Configure your API keys in appsettings.json
:
{
"SecurityKey": "01HSGVBSF99SK6XMJQJYF0X3WQ"
}
Multiple API Keys
Support multiple valid API keys using semicolon separation:
{
"SecurityKey": "01HSGVBGWXWDWTFGTJSYFXXDXQ;01HSGVBSF99SK6XMJQJYF0X3WQ;01HSGVAH2M5WVQYG4YPT7FNK4K8"
}
Usage Patterns
AspNetCore.SecurityKey supports multiple integration patterns to fit different application architectures and security requirements.
1. Middleware Pattern (Global Protection)
Apply API key requirement to all endpoints in your application:
var builder = WebApplication.CreateBuilder(args);
// Register services
builder.Services.AddAuthorization();
builder.Services.AddSecurityKey();
var app = builder.Build();
// Apply security to ALL endpoints
app.UseSecurityKey();
app.UseAuthorization();
// All these endpoints require valid API keys
app.MapGet("/weather", () => WeatherService.GetForecast());
app.MapGet("/users", () => UserService.GetUsers());
app.MapGet("/products", () => ProductService.GetProducts());
app.Run();
2. Attribute Pattern (Selective Protection)
Apply API key requirement to specific controllers or actions:
[ApiController]
[Route("[controller]")]
public class UsersController : ControllerBase
{
// This action requires API key
[SecurityKey]
[HttpGet]
public IEnumerable<User> GetUsers()
{
return UserService.GetUsers();
}
// This action is public (no API key required)
[HttpGet("public")]
public IEnumerable<User> GetPublicUsers()
{
return UserService.GetPublicUsers();
}
}
// Or apply to entire controller
[SecurityKey]
[ApiController]
[Route("[controller]")]
public class SecureController : ControllerBase
{
// All actions in this controller require API key
[HttpGet]
public IActionResult Get() => Ok();
}
3. Endpoint Filter Pattern (Minimal APIs)
Secure specific minimal API endpoints:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization();
builder.Services.AddSecurityKey();
var app = builder.Build();
app.UseAuthorization();
// Public endpoint (no API key required)
app.MapGet("/health", () => "Healthy");
// Secured endpoint using filter
app.MapGet("/users", () => UserService.GetUsers())
.RequireSecurityKey();
// Multiple endpoints can be grouped
var securedGroup = app.MapGroup("/api/secure")
.RequireSecurityKey();
securedGroup.MapGet("/data", () => "Secured data");
securedGroup.MapPost("/action", () => "Secured action");
app.Run();
4. Authentication Scheme Pattern (Full Integration)
Integrate with ASP.NET Core’s authentication system:
var builder = WebApplication.CreateBuilder(args);
// Register authentication with SecurityKey scheme
builder.Services
.AddAuthentication()
.AddSecurityKey();
builder.Services.AddAuthorization();
builder.Services.AddSecurityKey();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
// Use standard authorization attributes
app.MapGet("/users", () => UserService.GetUsers())
.RequireAuthorization();
// Can also be combined with role-based authorization
app.MapGet("/admin", () => "Admin data")
.RequireAuthorization("AdminPolicy");
app.Run();
Advanced Customization
Custom Security Key Validation
Implement custom validation logic by creating a class that implements ISecurityKeyValidator
:
public class DatabaseSecurityKeyValidator : ISecurityKeyValidator
{
private readonly IApiKeyRepository _repository;
private readonly ILogger<DatabaseSecurityKeyValidator> _logger;
public DatabaseSecurityKeyValidator(
IApiKeyRepository repository,
ILogger<DatabaseSecurityKeyValidator> logger)
{
_repository = repository;
_logger = logger;
}
public async ValueTask<bool> Validate(string? value, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(value))
return false;
try
{
var apiKey = await _repository.GetApiKeyAsync(value, cancellationToken);
if (apiKey == null)
{
_logger.LogWarning("Invalid API key attempted: {Key}", value);
return false;
}
if (apiKey.IsExpired)
{
_logger.LogWarning("Expired API key used: {Key}", value);
return false;
}
// Update last used timestamp
await _repository.UpdateLastUsedAsync(value, DateTime.UtcNow, cancellationToken);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error validating API key");
return false;
}
}
public async ValueTask<ClaimsIdentity> Authenticate(string? value, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(value))
return new ClaimsIdentity();
var apiKey = await _repository.GetApiKeyAsync(value, cancellationToken);
if (apiKey?.User == null)
return new ClaimsIdentity();
var identity = new ClaimsIdentity(SecurityKeyAuthenticationDefaults.AuthenticationScheme);
identity.AddClaim(new Claim(ClaimTypes.Name, apiKey.User.Name));
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, apiKey.User.Id));
// Add role claims
foreach (var role in apiKey.User.Roles)
{
identity.AddClaim(new Claim(ClaimTypes.Role, role));
}
return identity;
}
}
// Register custom validator
builder.Services.AddScoped<IApiKeyRepository, ApiKeyRepository>();
builder.Services.AddSecurityKey<DatabaseSecurityKeyValidator>();
License
This project is licensed under the MIT License
September 7, 2024
Equatable.Generator
Source generator for Equals
and GetHashCode
Features
- Override
Equals
and GetHashCode
- Implement
IEquatable<T>
- Support
class
, record
and struct
types
- Support
EqualityComparer
per property
- Comparers supported: String, Sequence, Dictionary, HashSet, Reference, and Custom
- No dependencies
Usage
Add package
Add the nuget package to your projects.
dotnet add package Equatable.Generator
Prevent including Equatable.Generator as a dependency
<PackageReference Include="Equatable.Generator" PrivateAssets="all" />
Requirements
This library requires:
- Target framework .NET Standard 2.0 or greater
- Project C#
LangVersion
8.0 or higher
Equatable Attributes
Place equatable attribute on a class
, record
or struct
. The source generator will create a partial with overrides for Equals
and GetHashCode
.
[Equatable]
Marks the class to generate overrides for Equals
and GetHashCode
The default comparer used in the implementation of Equals
and GetHashCode
is EqualityComparer<T>.Default
. Customize the comparer used with the following attributes.
[IgnoreEquality]
Ignore property in Equals
and GetHashCode
implementations
[StringEquality]
Use specified StringComparer
when comparing strings
[SequenceEquality]
Use Enumerable.SequenceEqual
to determine whether enumerables are equal
[DictionaryEquality]
Use to determine if dictionaries are equal
[HashSetEquality]
Use ISet<T>.SetEquals
to determine whether enumerables are equal
[ReferenceEquality]
Use Object.ReferenceEquals
to determines whether instances are the same instance
[EqualityComparer]
Use the specified EqualityComparer
Example Usage
Example of using the attributes to customize the source generation of Equals
and GetHashCode
[Equatable]
public partial class UserImport
{
[StringEquality(StringComparison.OrdinalIgnoreCase)]
public string EmailAddress { get; set; } = null!;
public string? DisplayName { get; set; }
public string? FirstName { get; set; }
public string? LastName { get; set; }
public DateTimeOffset? LockoutEnd { get; set; }
public DateTimeOffset? LastLogin { get; set; }
[IgnoreEquality]
public string FullName => $"{FirstName} {LastName}";
[HashSetEquality]
public HashSet<string>? Roles { get; set; }
[DictionaryEquality]
public Dictionary<string, int>? Permissions { get; set; }
[SequenceEquality]
public List<DateTimeOffset>? History { get; set; }
}
November 1, 2022
Injectio
Source generator that helps register discovered services in the dependency injection container
Features
- Transient, Singleton, Scoped service registration
- Factory registration
- Module method registration
- Duplicate Strategy - Skip,Replace,Append
- Registration Strategy - Self, Implemented Interfaces, Self With Interfaces
Usage
Add package
Add the nuget package project to your projects.
dotnet add package Injectio
Prevent dependances from including Injectio
<PackageReference Include="Injectio" PrivateAssets="all" />
Registration Attributes
Place registration attribute on class. The class will be discovered and registered.
[RegisterSingleton]
Marks the class as a singleton service
[RegisterScoped]
Marks the class as a scoped service
[RegisterTransient]
Marks the class as a transient service
[RegisterServices]
Marks the method to be called to register services
Attribute Properties
Property |
Description |
ImplementationType |
The type that implements the service. If not set, the class the interface is on will be used. |
ServiceType |
The type of the service. If not set, the Registration property used to determine what is registered. |
Factory |
Name of a factory method to create new instances of the service implementation. |
Duplicate |
How the generator handles duplicate registrations. See Duplicate Strategy |
Registration |
How the generator determines what to register. See Registration Strategy |
Duplicate Strategy
Value |
Description |
Skip |
Skips registrations for services that already exists |
Replace |
Replaces existing service registrations |
Append |
Appends a new registration for existing services |
Registration Strategy
Value |
Description |
Self |
Registers each matching concrete type as itself |
ImplementedInterfaces |
Registers each matching concrete type as all of its implemented interfaces |
SelfWithInterfaces |
Registers each matching concrete type as all of its implemented interfaces and itself |
Singleton services
[RegisterSingleton]
public class SingletonService : IService { }
Explicit service type
[RegisterSingleton(ServiceType = typeof(IService))]
public class SingletonService : IService { }
Scoped Services
[RegisterScoped]
public class ScopedService : IService { }
Transient Services
[RegisterTransient]
public class TransientService : IService { }
Factories
[RegisterTransient(Factory = nameof(ServiceFactory))]
public class FactoryService : IFactoryService
{
private readonly IService _service;
public FactoryService(IService service)
{
_service = service;
}
public static IFactoryService ServiceFactory(IServiceProvider serviceProvider)
{
return new FactoryService(serviceProvider.GetService<IService>());
}
}
Open Generic
[RegisterSingleton(ImplementationType = typeof(OpenGeneric<>), ServiceType = typeof(IOpenGeneric<>))]
public class OpenGeneric<T> : IOpenGeneric<T> { }
Register Method
When the service registration is complex, use the RegisterServices
attribute on a method that has a parameter of IServiceCollection
or ServiceCollection
public class RegistrationModule
{
[RegisterServices]
public static void Register(IServiceCollection services)
{
services.TryAddTransient<IModuleService, ModuleService>();
}
}
Add to container
The source generator creates an extension method with all the discovered services registered. Call the generated extension method to add the services to the container. The method will be called Add[AssemblyName]
. The assembly name will have the dots removed.
var services = new ServiceCollection();
services.AddInjectioTestsConsole();
Override the extension method name by using the InjectioName
MSBuild property.
<PropertyGroup>
<InjectioName>Library</InjectioName>
</PropertyGroup>
var services = new ServiceCollection();
services.AddLibrary();
August 27, 2019
Overview
The LoreSoft Blazor Controls project contains a collection of Blazor user controls.
Installing
The LoreSoft.Blazor.Controls library is available on nuget.org via package name LoreSoft.Blazor.Controls
.
To install LoreSoft.Blazor.Controls, run the following command in the Package Manager Console
Install-Package LoreSoft.Blazor.Controls
Setup
To use, you need to include the following CSS and JS files in your index.html
(Blazor WebAssembly) or _Host.cshtml
(Blazor Server).
In the head tag add the following CSS.
<link rel="stylesheet" href="_content/LoreSoft.Blazor.Controls/BlazorControls.css" />
Then add the JS script at the bottom of the page using the following script tag.
<script src="_content/LoreSoft.Blazor.Controls/BlazorControls.js"></script>
Typeahead Features
- Searching data by supplying a search function
- Template for search result, selected value, and footer
- Debounce support for smoother search
- Character limit before searching
- Multiselect support
- Built in form validation support
Typeahead Properties
Required
- Value - Bind to
Value
in single selection mode. Mutually exclusive to Values
property.
- Values - Bind to
Values
in multiple selection mode. Mutually exclusive to Value
property.
- SearchMethod - The method used to return search result
Optional
- AllowClear - Allow the selected value to be cleared
- ConvertMethod - The method used to convert search result type to the value type
- Debounce - Time to wait, in milliseconds, after last key press before starting a search
- Items - The initial list of items to show when there isn’t any search text
- MinimumLength - Minimum number of characters before starting a search
- Placeholder - The placeholder text to show when nothing is selected
Templates
- ResultTemplate - User defined template for displaying a result in the results list
- SelectedTemplate - User defined template for displaying the selected item(s)
- NoRecordsTemplate - Template for when no items are found
- FooterTemplate - Template displayed at the end of the results list
- LoadingTemplate - Template displayed while searching
Typeahead Examples
Basic Example
State selection dropdown. Bind to Value
property for single selection mode.
<Typeahead SearchMethod="@SearchState"
Items="Data.StateList"
@bind-Value="@SelectedState"
Placeholder="State">
<SelectedTemplate Context="state">
@state.Name
</SelectedTemplate>
<ResultTemplate Context="state">
@state.Name
</ResultTemplate>
</Typeahead>
@code {
public StateLocation SelectedState { get; set; }
public Task<IEnumerable<StateLocation>> SearchState(string searchText)
{
var result = Data.StateList
.Where(x => x.Name.ToLower().Contains(searchText.ToLower()))
.ToList();
return Task.FromResult<IEnumerable<StateLocation>>(result);
}
}
Multiselect Example
When you want to be able to select multiple results. Bind to the Values
property. The target property must be type IList<T>
.
<Typeahead SearchMethod="@SearchPeople"
Items="Data.PersonList"
@bind-Values="@SelectedPeople"
Placeholder="Owners">
<SelectedTemplate Context="person">
@person.FullName
</SelectedTemplate>
<ResultTemplate Context="person">
@person.FullName
</ResultTemplate>
</Typeahead>
@code {
public IList<Person> SelectedPeople;
public Task<IEnumerable<Person>> SearchPeople(string searchText)
{
var result = Data.PersonList
.Where(x => x.FullName.ToLower().Contains(searchText.ToLower()))
.ToList();
return Task.FromResult<IEnumerable<Person>>(result);
}
}
GitHub Repository Search
Use Octokit to search for a GitHub repository.
<Typeahead SearchMethod="@SearchGithub"
@bind-Value="@SelectedRepository"
Placeholder="Repository"
MinimumLength="3">
<SelectedTemplate Context="repo">
@repo.FullName
</SelectedTemplate>
<ResultTemplate Context="repo">
<div class="github-repository clearfix">
<div class="github-avatar"><img src="@repo.Owner.AvatarUrl"></div>
<div class="github-meta">
<div class="github-title">@repo.FullName</div>
<div class="github-description">@repo.Description</div>
<div class="github-statistics">
<div class="github-forks"><i class="fa fa-flash"></i> @repo.ForksCount Forks</div>
<div class="github-stargazers"><i class="fa fa-star"></i> @repo.StargazersCount Stars</div>
<div class="github-watchers"><i class="fa fa-eye"></i> @repo.SubscribersCount Watchers</div>
</div>
</div>
</div>
</ResultTemplate>
</Typeahead>
@inject IGitHubClient GitHubClient;
@code {
public Repository SelectedRepository { get; set; }
public async Task<IEnumerable<Repository>> SearchGithub(string searchText)
{
var request = new SearchRepositoriesRequest(searchText);
var result = await GitHubClient.Search.SearchRepo(request);
return result.Items;
}
}