BrandUp.Core 10.0.8

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

BrandUp.Core

Base framework for .NET development: a universal Result structure and lightweight CQRS infrastructure — queries, commands, items and domain events — on top of Microsoft.Extensions.DependencyInjection.

  • Results — success-or-errors outcome for every operation.
  • Queries — read operations returning a list of rows or a single value.
  • Commands — write operations, with or without result data, validated before execution.
  • Items — domain entities resolved by identifier, with commands targeting a loaded item.
  • Events — facts published by commands and routed to any number of handlers, immediately or after the command completes.

Getting started

services.AddDomain(options =>
    {
        options.AddQuery<UserByPhoneQueryHandler>();
        options.AddCommand<SignUpCommandHandler>();
        options.AddEvent<UserSignedUpNotificationHandler>();
    })
    .AddValidator<ComponentModelValidator>()
    .AddItemProvider<UserProvider>();

AddDomain registers IDomain — the entry point for dispatching queries and commands — as a scoped service. Handlers are constructed per dispatch with constructor injection, and disposed after execution if they implement IDisposable/IAsyncDisposable. Every dispatched query and command first runs through the registered validators; ComponentModelValidator applies the standard System.ComponentModel.DataAnnotations attributes.

using var scope = serviceProvider.CreateAsyncScope();
var domain = scope.ServiceProvider.GetRequiredService<IDomain>();

Results

Result (and Result<TData>) is the outcome of every operation: success, or a non-empty set of errors.

Result.Success();
Result.Success(data);                    // Result<TData>
Result.Error("code", "message");
Result.Error<TData>("code", "message");
Result.Error(errors);                    // IEnumerable<IError>

result.IsSuccess;
result.Errors;
result.Data;                             // Result<TData> only

ILogger extension logger.LogIfError(result) writes the errors of a failed result and reports whether there were any.

Queries

A list query implements IQuery<TRow>, its handler IQueryHandler<TQuery, TRow>:

public class UserByPhoneQuery : IQuery<User>
{
    [Required]
    public string Phone { get; set; }
}

public class UserByPhoneQueryHandler(IUserRepository userRepository) : IQueryHandler<UserByPhoneQuery, User>
{
    public async Task<IList<User>> HandleAsync(UserByPhoneQuery query, CancellationToken cancellationToken = default)
    {
        return await userRepository.FindByPhoneAsync(query.Phone, cancellationToken);
    }
}

Result<IList<User>> result = await domain.QueryAsync(new UserByPhoneQuery { Phone = "+79232229022" });

A single-value query implements ISingleQuery<TModel>, its handler ISingleQueryHandler<TQuery, TModel> and returns a Result<TModel>, so it can fail with domain errors:

public class UserCountQuery : ISingleQuery<int>
{
}

public class UserCountQueryHandler : ISingleQueryHandler<UserCountQuery, int>
{
    public Task<Result<int>> HandleAsync(UserCountQuery query, CancellationToken cancellationToken = default)
    {
        return Task.FromResult(Result.Success(5));
    }
}

Result<int> count = await domain.QueryAsync(new UserCountQuery());

Both kinds are registered the same way: options.AddQuery<THandler>().

Commands

A command implements ICommand (no result data) or ICommand<TResult>; the handler returns Result / Result<TResult>:

public class SignUpCommand : ICommand<SignUpResult>
{
    [Required]
    public string Phone { get; set; }
}

public class SignUpResult
{
    public User User { get; set; }
}

public class SignUpCommandHandler(IUserRepository userRepository) : ICommandHandler<SignUpCommand, SignUpResult>
{
    public async Task<Result<SignUpResult>> HandleAsync(SignUpCommand command, CancellationToken cancellationToken = default)
    {
        var user = new User
        {
            Id = Guid.NewGuid(),
            Phone = command.Phone
        };

        await userRepository.CreateAsync(user, cancellationToken);

        return Result.Success(new SignUpResult { User = user });
    }
}

Result<SignUpResult> signUpResult = await domain.SendAsync(new SignUpCommand { Phone = "+79231145449" });

Validation errors are returned as a failed Result without invoking the handler. Registration: options.AddCommand<THandler>() — one handler per command type.

Items

An item is a domain entity with an identifier (IItem<TId>), resolved through a registered IItemProvider<TId, TItem>:

public class User : IItem<Guid>
{
    public Guid Id { get; set; }
    public string Phone { get; set; }
}

public class UserProvider(IUserRepository userRepository) : IItemProvider<Guid, User>
{
    public Task<User?> FindByIdAsync(Guid itemId, CancellationToken cancellationToken = default)
    {
        return userRepository.FindByIdAsync(itemId, cancellationToken);
    }
}

An item command targets a loaded item — the handler receives the item together with the command:

public class VisitUserCommand : IItemCommand<User>
{
}

public class VisitUserCommandHandler : IItemCommandHandler<User, VisitUserCommand>
{
    public Task<Result> HandleAsync(User item, VisitUserCommand command, CancellationToken cancellationToken = default)
    {
        return Task.FromResult(Result.Success());
    }
}

Dispatch with an already-loaded item, or by identifier — the by-id overloads resolve the item through its provider first and turn a missing item into an error Result:

User? user = await domain.FindItemAsync<Guid, User>(userId);
if (user != null)
    await domain.SendItemAsync(user, new VisitUserCommand());

// or by id — a missing item becomes an error Result:
Result result = await domain.SendItemAsync(userId, new VisitUserCommand());

Item commands with result data (IItemCommand<TItem, TResult> / IItemCommandHandler<TItem, TCommand, TResult>) work the same way. Providers are also resolvable directly: domain.GetItemProvider<UserProvider>(), or IItemProvider<Guid, User> from the service provider.

Events

A domain event is a fact that occurred in the domain. Commands publish facts through IDomainEventPublisher; what each fact spawns is known by its registered handlers, not by the command that raised it. An event may have any number of handlers (zero is valid), executed sequentially in registration order; events are routed by their runtime type (exact match). One handler class may handle several event types.

public class UserSignedUp : IDomainEvent
{
    public required User User { get; init; }
}

public class UserSignedUpNotificationHandler(INotificationService notificationService) : IDomainEventHandler<UserSignedUp>
{
    public Task HandleAsync(UserSignedUp @event, CancellationToken cancellationToken = default)
    {
        return notificationService.WelcomeAsync(@event.User, cancellationToken);
    }
}

public class SignUpCommandHandler(IUserRepository userRepository, IDomainEventPublisher eventPublisher) : ICommandHandler<SignUpCommand, SignUpResult>
{
    public async Task<Result<SignUpResult>> HandleAsync(SignUpCommand command, CancellationToken cancellationToken = default)
    {
        // ...

        await eventPublisher.PublishAsync(new UserSignedUp { User = user }, cancellationToken);

        return Result.Success(result);
    }
}

services.AddDomain(options =>
{
    options.AddCommand<SignUpCommandHandler>();
    options.AddEvent<UserSignedUpNotificationHandler>();
    options.AddEvent<UserSignedUpAnalyticsHandler>();

    // Optionally, treat an event without handlers as a configuration error:
    // options.RequireEventHandlers = true;
});

Deferred handlers

A handler implementing IDeferredDomainEventHandler<TEvent> runs after the outermost domain command completes successfully — no manual flushing required. If the command (or any enclosing command) fails with an error Result or an exception, the deferred execution is discarded: the fact did not survive. Published outside of a command, a deferred handler runs immediately.

// Runs after the command completes successfully; discarded if it fails.
public class UserSignedUpAnalyticsHandler(IAnalyticsService analyticsService) : IDeferredDomainEventHandler<UserSignedUp>
{
    public Task HandleAsync(UserSignedUp @event, CancellationToken cancellationToken = default)
    {
        return analyticsService.TrackSignUpAsync(@event.User, cancellationToken);
    }
}

Error semantics differ by kind: an exception from an immediate handler propagates to the publishing command; an exception from a deferred handler is logged and neither fails the already-completed command nor stops the remaining deferred handlers. Deferred handlers run with CancellationToken.None — by then the command has succeeded and its caller may already be gone, so a handler needing a timeout manages its own. Parallel and nested command dispatches keep independent deferred queues, so concurrent commands never observe each other's events.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  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 (4)

Showing the top 4 NuGet packages that depend on BrandUp.Core:

Package Downloads
BrandUp.Core.Testing

Test helpers for BrandUp.Core: a domain test host, dispatch and Result assertions, event capture, fake transactions and a fake event outbox. Framework-agnostic — works with xUnit, NUnit and MSTest.

BrandUp.Core.AspNetCore

ASP.NET Core integration for BrandUp.Core: maps Result to HTTP responses and ProblemDetails, for both minimal APIs and MVC.

BrandUp.Core.FluentValidation

FluentValidation adapter for BrandUp.Core: runs the FluentValidation validators registered for a dispatched query or command inside the domain validation pipeline.

BrandUp.Core.Outbox.MongoDB

MongoDB transactional outbox for BrandUp.Core domain events: a store that persists deferred events inside the command's transaction and a background processor delivering them after commit with retries.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
10.2.1 172 8/23/2026
10.1.2 156 8/22/2026
10.1.1 156 8/22/2026
10.0.8 104 8/21/2026
10.0.7 171 7/4/2026
10.0.6 157 5/28/2026
10.0.5 157 4/27/2026
10.0.4 147 4/20/2026
10.0.3 220 3/16/2026
10.0.2 147 3/11/2026
10.0.1 228 1/3/2026
2.0.5 367 7/1/2025
2.0.4 479 6/10/2025
2.0.3 343 3/29/2025
2.0.2 368 3/9/2025
2.0.1 314 12/15/2024
1.2.1 336 7/9/2024
1.1.3 360 4/29/2024
1.1.1 286 4/29/2024
1.0.11 430 1/20/2024
Loading failed