ScopeBus 1.1.23
dotnet add package ScopeBus --version 1.1.23
NuGet\Install-Package ScopeBus -Version 1.1.23
<PackageReference Include="ScopeBus" Version="1.1.23" />
<PackageVersion Include="ScopeBus" Version="1.1.23" />
<PackageReference Include="ScopeBus" />
paket add ScopeBus --version 1.1.23
#r "nuget: ScopeBus, 1.1.23"
#:package ScopeBus@1.1.23
#addin nuget:?package=ScopeBus&version=1.1.23
#tool nuget:?package=ScopeBus&version=1.1.23
ScopeBus
ScopeBus is a clean and extensible mediator framework for .NET, supporting CQRS, domain events, pipelines, and flexible handler behaviors — with automatic handler registration, minimal ceremony, and maximum control.
Features
- Modular Design: Easily extendable and customizable to fit your needs.
- Mediator Pattern: Implements the mediator pattern to decouple components and promote clean architecture.
- CQRS Support: Built-in support for Command Query Responsibility Segregation (CQRS) to separate read and write operations.
- Domain-Driven Design (DDD): Facilitates the implementation of DDD principles for better organization and maintainability of your code.
- Minimal Setup: Quick and easy to set up, allowing you to focus on building your application rather than configuring the framework.
- Flexible Pipeline: Create custom pipelines to handle various scenarios, including validation, logging, and error handling.
- Asynchronous Support: Built-in support for asynchronous operations, making it suitable for modern applications.
- Dependency Injection: Seamless integration with popular dependency injection frameworks, such as Microsoft.Extensions.DependencyInjection.
- Cross-Cutting Concerns: Easily manage cross-cutting concerns like logging, caching, and validation through middleware components.
- Testable: Designed with testability in mind, making it easy to write unit tests for your components.
- Open Source: Free to use and modify under the MIT License.
- Multi-Assembly Support: Automatically discovers and registers handlers across all referenced assemblies — no manual configuration needed.
- Performance: Optimized for performance, ensuring that your application runs smoothly even under heavy load.
Getting Started
To get started with ScopeBus, follow these steps:
Install the NuGet package:
dotnet add package ScopeBusRegister ScopeBus in your application:
using ScopeBus.Extensions; services.AddScopeBus();
Why ScopeBus?
- You're building a clean architecture application and need a flexible mediator
- You want CQRS without being locked into MediatR's conventions
- You need pre/post pipeline behaviors, auditing, and domain events
- You want automatic handler registration — no manual DI wiring
- You need to support streaming, retries, and granular tracing in one pipeline
Multi-Assembly / Multi-Project Support
ScopeBus automatically discovers handlers across all assemblies referenced by your startup project. This means you can keep your handlers in a dedicated SDK or class library project to keep your API lean and clean — no extra configuration needed.
For example, given this solution structure:
Edvance.Api ← startup project
Edvance.Api.Handlers.Sdk ← handlers, validators, processors live here
Edvance.Common ← shared models
As long as Edvance.Api has a project reference to Edvance.Api.Handlers.Sdk, ScopeBus will discover and register all handlers in that project automatically at startup.
Define Your Request Handler
ScopeBus will automatically register the handler for you.
public class MyRequest : IRequest<MyResponse>
{
public string Name { get; set; }
}
public class MyRequestHandler : IRequestHandler<MyRequest, MyResponse>
{
public Task<MyResponse> Handle(MyRequest request, CancellationToken cancellationToken)
{
return Task.FromResult(new MyResponse { Message = $"Hello, {request.Name}!" });
}
}
Send a request from your controller:
[ApiController]
[Route("api/[controller]")]
public class MyController : ControllerBase
{
private readonly IMediator _bus;
public MyController(IMediator bus)
{
_bus = bus;
}
[HttpPost]
public async Task<IActionResult> Post(MyRequest request)
{
var response = await _bus.Send(request);
return Ok(response);
}
}
Define Your Query Handler
ScopeBus will automatically register the handler for you.
Query handlers are intended for read operations. Any request that implements IQuery<T> will have caching automatically applied via the CachingQueryDecorator. By default, an in-memory cache is used. To provide a custom implementation, register your own ICacheProvider:
builder.Services.AddScoped<ICacheProvider, RedisCacheProvider>();
public class GetEmployeeQuery : IQuery<Employee>
{
public int EmployeeId { get; set; }
}
public class GetEmployeeQueryHandler : IRequestHandler<GetEmployeeQuery, Employee>
{
private readonly IEmployeeRepository _repository;
public GetEmployeeQueryHandler(IEmployeeRepository repository)
{
_repository = repository;
}
public async Task<Employee> Handle(GetEmployeeQuery request, CancellationToken cancellationToken)
{
return await _repository.GetByIdAsync(request.EmployeeId);
}
}
Validation
ScopeBus will automatically register the handler for you.
Implement IValidator<TRequest> to validate a request before it reaches the handler. If validation fails, the handler will not execute and the errors will be returned to the caller.
public class EmployeeRequestValidator : IValidator<EmployeeRequest>
{
public Task<IEnumerable<ErrorResponse>> ValidateAsync(EmployeeRequest request, CancellationToken cancellationToken)
{
var errors = new List<ErrorResponse>();
if (string.IsNullOrWhiteSpace(request.FirstName))
errors.Add(new ErrorResponse(nameof(request.FirstName), "First name is required"));
if (string.IsNullOrWhiteSpace(request.LastName))
errors.Add(new ErrorResponse(nameof(request.LastName), "Last name is required"));
return Task.FromResult<IEnumerable<ErrorResponse>>(errors);
}
}
Auditing
ScopeBus will automatically register the handler for you.
Implement IAuditableCommandHandler<TRequest, TResponse> to run logic before and after a command handler executes, without touching the handler itself.
PreAuditAsync— called before the handler executesPostAuditAsync— called after the handler completes
Your request must implement ICommand<TResponse> for the audit decorator to activate.
public class DeleteEmployeeRequest : ICommand<ApiResponse<string>>
{
public string? EmployeeId { get; set; }
}
public class DeleteEmployeeAuditHandler : IAuditableCommandHandler<DeleteEmployeeRequest, ApiResponse<string>>
{
private readonly ILogger<DeleteEmployeeAuditHandler> _logger;
private readonly IHttpContextAccessor _context;
public DeleteEmployeeAuditHandler(ILogger<DeleteEmployeeAuditHandler> logger, IHttpContextAccessor context)
{
_logger = logger;
_context = context;
}
public Task PreAuditAsync(DeleteEmployeeRequest request, CancellationToken cancellationToken)
{
var user = _context.HttpContext?.User?.Identity?.Name ?? "anonymous";
_logger.LogInformation($"[PRE] {user} requested to delete employee {request.EmployeeId}");
return Task.CompletedTask;
}
public Task PostAuditAsync(DeleteEmployeeRequest request, ApiResponse<string> response, CancellationToken cancellationToken)
{
var user = _context.HttpContext?.User?.Identity?.Name ?? "anonymous";
_logger.LogInformation($"[POST] {user} deleted employee {request.EmployeeId} with result: {response.Code}");
return Task.CompletedTask;
}
}
Roles And Permissions
Your request DTO should implement IAuthorizedRequest to enforce role or permission-based access control. ScopeBus will check the user's claims before the handler executes.
public class EmployeeRequest : IRequest<ApiResponse<Employee>>, IAuthorizedRequest
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
public string? Email { get; set; }
// User must have at least one of these roles to proceed
public string[] RequiredRoles => ["HR.Manager", "Admin"];
}
Roles are extracted from the JWT token claims. Authentication and authorization must be enabled in your application for this to work.
INotification
ScopeBus will automatically register the handler for you.
Implement INotificationHandler<TNotification> to react to events published through the mediator. Multiple handlers can be registered for the same notification.
public class Employee : EventEntity
{
public string? Id { get; set; } = Guid.NewGuid().ToString("N");
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public string? FirstName { get; set; }
public string? LastName { get; set; }
public string? Email { get; set; }
public void OnCreated()
{
AddDomainEvent(new EmployeeCreatedNotification
{
Employee = this
});
}
}
public class EmployeeCreatedNotification : INotification
{
public Employee? Employee { get; set; }
}
public class EmployeeCreatedNotificationHandler : INotificationHandler<EmployeeCreatedNotification>
{
private readonly ILogger<EmployeeCreatedNotificationHandler> _logger;
public EmployeeCreatedNotificationHandler(ILogger<EmployeeCreatedNotificationHandler> logger)
{
_logger = logger;
}
public Task Handle(EmployeeCreatedNotification notification, CancellationToken cancellationToken)
{
_logger.LogInformation($"Employee created: {notification.Employee?.FirstName} {notification.Employee?.LastName}");
return Task.CompletedTask;
}
}
Streams Request
ScopeBus will automatically register the handler for you.
Implement IStreamRequestHandler<TRequest, TResponse> to return a stream of items. Useful for large data sets or real-time feeds.
public class StreamEmployeesRequest : IStreamRequest<Employee>
{
public int Count { get; set; } = 5;
}
public class StreamEmployeesRequestHandler : IStreamRequestHandler<StreamEmployeesRequest, Employee>
{
public async IAsyncEnumerable<Employee> Handle(StreamEmployeesRequest request, [EnumeratorCancellation] CancellationToken cancellationToken)
{
for (int i = 0; i < request.Count; i++)
{
yield return new Employee
{
Id = Guid.NewGuid().ToString("N"),
FirstName = $"FirstName {i}",
LastName = $"LastName {i}",
Email = $"email{i}@example.com"
};
}
}
}
// Controller
[ApiController]
[Route("api/[controller]")]
public class EmployeeController : ControllerBase
{
private readonly IMediator _bus;
public EmployeeController(IMediator bus)
{
_bus = bus;
}
[HttpGet("stream")]
public async IAsyncEnumerable<Employee> StreamEmployees([FromQuery] StreamEmployeesRequest request)
{
await foreach (var employee in _bus.Stream(request))
{
yield return employee;
}
}
}
Retry Flaky Requests
To opt a request into retry and timeout behavior, implement the IResilientRequest interface. ScopeBus will automatically apply the configured retry policy via Polly.
public class RetryRequest : IRequest<ApiResponse<string>>, IResilientRequest
{
public string? Name { get; set; }
public int RetryCount => 3;
public int RetryDelayMilliseconds => 200;
public int TimeoutSeconds => 10;
}
public class RetryRequestHandler : IRequestHandler<RetryRequest, ApiResponse<string>>
{
private static int _attempt = 0;
public async Task<ApiResponse<string>> Handle(RetryRequest request, CancellationToken cancellationToken)
{
await Task.CompletedTask;
_attempt++;
Console.WriteLine($"Attempt #{_attempt} at handling RetryRequest");
if (_attempt < 3)
{
throw new InvalidOperationException("Simulated transient failure");
}
return new ApiResponse<string> { Data = $"Recovered after {_attempt} attempts for: {request.Name}" };
}
}
Requests that do not implement IResilientRequest pass through the resilience behavior unchanged.
PreProcessing and PostProcessing
ScopeBus will automatically register the handler for you.
Implement IRequestPreProcessor<TRequest> and IRequestPostProcessor<TRequest, TResponse> to run logic before and after any request, without modifying the handler.
// PreProcessing
public class EmployeePreProcessHandler : IRequestPreProcessor<EmployeeRequest>
{
private readonly ILogger<EmployeePreProcessHandler> _logger;
public EmployeePreProcessHandler(ILogger<EmployeePreProcessHandler> logger)
{
_logger = logger;
}
public Task Process(EmployeeRequest request, CancellationToken cancellationToken)
{
_logger.LogInformation($"[PRE] Processing EmployeeRequest at {DateTime.UtcNow}");
return Task.CompletedTask;
}
}
// PostProcessing
public class EmployeePostProcessHandler : IRequestPostProcessor<EmployeeRequest, ApiResponse<Employee>>
{
private readonly ILogger<EmployeePostProcessHandler> _logger;
public EmployeePostProcessHandler(ILogger<EmployeePostProcessHandler> logger)
{
_logger = logger;
}
public Task Process(EmployeeRequest request, ApiResponse<Employee> response, CancellationToken cancellationToken)
{
_logger.LogInformation($"[POST] EmployeeRequest returned status: {response.Code}");
return Task.CompletedTask;
}
}
Telemetry and Logging: CorrelationHandler
ScopeBus will automatically register the handler for you.
Implement ICorrelationHandler to track requests across the pipeline with a shared trace ID. OnStartAsync is called before the handler executes, OnCompleteAsync is called after.
Only one ICorrelationHandler implementation is registered. If multiple exist, the first discovered is used.
public class DefaultCorrelationHandler : ICorrelationHandler
{
private readonly ILogger<DefaultCorrelationHandler> _logger;
public DefaultCorrelationHandler(ILogger<DefaultCorrelationHandler> logger)
{
_logger = logger;
}
public Task OnStartAsync(string traceId, object request, CancellationToken cancellationToken)
{
_logger.LogInformation("[TRACE Start] {TraceId} for {RequestType}", traceId, request.GetType().Name);
return Task.CompletedTask;
}
public Task OnCompleteAsync(string traceId, object request, object response, long durationMs, CancellationToken cancellationToken)
{
_logger.LogInformation("[TRACE End] {TraceId} ({Duration} ms)", traceId, durationMs);
return Task.CompletedTask;
}
}
ScopeBus Logging
ScopeBus supports opt-in logging per request via ILoggingHook<TRequest, TResponse>. Logging only activates for requests that have a registered hook, giving you full control over what gets logged.
public interface ILoggingHook<TRequest, TResponse>
{
Task OnLogStartAsync(TRequest request, CancellationToken cancellationToken);
Task OnLogCompleteAsync(TRequest request, TResponse response, CancellationToken cancellationToken);
Task OnLogExceptionAsync(TRequest request, Exception ex, CancellationToken cancellationToken);
}
OnLogStartAsync— called before the request reaches the handlerOnLogCompleteAsync— called after successful completionOnLogExceptionAsync— called if an exception occurs
public class CreateEmployeeLoggingHook : ILoggingHook<CreateEmployeeRequest, ApiResponse<Employee>>
{
private readonly ILogger<CreateEmployeeLoggingHook> _logger;
public CreateEmployeeLoggingHook(ILogger<CreateEmployeeLoggingHook> logger)
{
_logger = logger;
}
public Task OnLogStartAsync(CreateEmployeeRequest request, CancellationToken cancellationToken)
{
_logger.LogInformation("[LOGGING] Starting CreateEmployeeRequest for: {Email}", request.Email);
return Task.CompletedTask;
}
public Task OnLogCompleteAsync(CreateEmployeeRequest request, ApiResponse<Employee> response, CancellationToken cancellationToken)
{
_logger.LogInformation("[LOGGING] Completed CreateEmployeeRequest with status {Status}", response.Code);
return Task.CompletedTask;
}
public Task OnLogExceptionAsync(CreateEmployeeRequest request, Exception ex, CancellationToken cancellationToken)
{
_logger.LogError(ex, "[LOGGING] Exception during CreateEmployeeRequest for: {Email}", request.Email);
return Task.CompletedTask;
}
}
Testing
ScopeBus is testable by design. You can mock IMediator, inject handlers directly, or use test doubles for pipeline behaviors.
We recommend creating a custom TestScopeBus helper for integration testing command/response flow.
License
This project is licensed under the MIT License.
Architecture
| Product | Versions 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. |
-
net8.0
- Microsoft.AspNetCore.Http.Abstractions (>= 2.2.0)
- Microsoft.Extensions.Caching.Memory (>= 6.0.2)
- Microsoft.Extensions.DependencyInjection (>= 8.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Logging (>= 8.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Options (>= 8.0.0)
- Newtonsoft.Json (>= 13.0.3)
- Polly (>= 8.5.2)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on ScopeBus:
| Package | Downloads |
|---|---|
|
DotnetCqrsPgTemplate.Api
Package Description |
GitHub repositories
This package is not used by any popular GitHub repositories.
Version 1.1.23 Fix assembly discovery to handle ReflectionTypeLoadException from analyzer assemblies; auto-discover all referenced assemblies without manual configuration