S3Lab.Box.Result 2.2.0

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

S3Lab.Box

Решение для ASP.NET Core приложений, включающее библиотеки для аутентификации, работы с данными и обработки результатов.

Проекты в решении

S3Lab.Box.Result

Библиотека для обработки результатов операций в стиле Railway Oriented Programming.

Описание

S3Lab.Box.Result предоставляет минималистичную реализацию паттерна Result/Result<T> для обработки успешных и неуспешных операций. Библиотека идеально подходит для CQRS архитектуры и функционального программирования.

Возможности
  • ✅ Простой и понятный API для обработки результатов
  • 🚫 Типобезопасная обработка ошибок
  • 🔄 Поддержка Railway Oriented Programming
  • ⚡ Минимальные накладные расходы
  • 🛠️ Расширения для удобной работы с результатами
Установка
dotnet add package S3Lab.Box.Result
Быстрый старт
using S3Lab.Box.Result;

// Успешный результат
var successResult = Result<int>.Success(42);

// Неуспешный результат
var error = new Error("Validation failed", "INVALID_INPUT");
var failureResult = Result<int>.Failure(error);

// Обработка результатов
var result = await SomeOperationAsync();
if (result.IsSuccess)
{
    Console.WriteLine($"Success: {result.Value}");
}
else
{
    Console.WriteLine($"Error: {result.Error.Message}");
}

// Использование с расширениями
var mappedResult = result
    .Map(value => value * 2)
    .OnSuccess(value => Console.WriteLine($"Doubled: {value}"))
    .OnFailure(error => Console.WriteLine($"Failed: {error.Message}"));
Основные типы

Result<T> - результат операции с типизированным значением:

public class Result<T>
{
    public bool IsSuccess { get; }
    public bool IsFailure => !IsSuccess;
    public T Value { get; } // Доступно только при IsSuccess
    public Error Error { get; } // Доступно только при IsFailure
    
    public static Result<T> Success(T value);
    public static Result<T> Failure(Error error);
}

Error - информация об ошибке:

public record Error(string Message, string Code);
Расширения

Библиотека предоставляет набор расширений для удобной работы:

// Map - преобразование значения
var result = Result<int>.Success(42);
var stringResult = result.Map(x => x.ToString()); // Result<string>

// Bind - цепочка операций
var result = await GetUserAsync(1)
    .Bind(user => GetUserOrdersAsync(user.Id))
    .Bind(orders => CalculateTotalAsync(orders));

// OnSuccess/OnFailure - побочные эффекты
var result = await SaveUserAsync(user)
    .OnSuccess(savedUser => LogSuccess(savedUser))
    .OnFailure(error => LogError(error));

S3Lab.Box.Auth

Библиотека для аутентификации ASP.NET Core приложений с использованием API ключей.

Описание

S3Lab.Box.Auth предоставляет простое и эффективное решение для защиты API endpoints с помощью API ключей. Библиотека включает в себя middleware для проверки заголовка X-API-Key и расширения для удобной интеграции в ASP.NET Core приложения.

Возможности
  • 🔐 Аутентификация по API ключу через заголовок X-API-Key
  • 🚫 Автоматическое отклонение запросов без валидного ключа
  • 🔓 Исключения для определенных маршрутов (Scalar, OpenAPI)
  • ⚡ Поддержка preflight запросов (OPTIONS)
  • 🛠️ Простая интеграция через extension методы
Установка
dotnet add package S3Lab.Box.Auth
Быстрый старт
  1. Настройка конфигурации

Добавьте API ключ в ваш appsettings.json:

{
  "ApiKey": "your-secret-api-key-here"
}
  1. Регистрация сервисов

В Program.cs или Startup.cs:

using S3Lab.Box.Auth.Extensions;

var builder = WebApplication.CreateBuilder(args);

// Добавление сервисов
builder.Services.AddApplicationReferences(builder.Configuration);

var app = builder.Build();

// Регистрация middleware
app.UseApiKeyMiddleware();

app.Run();
  1. Использование

После настройки все запросы к вашему API должны включать заголовок X-API-Key с валидным ключом:

curl -H "X-API-Key: your-secret-api-key-here" https://your-api.com/endpoint
Исключения

Middleware автоматически пропускает следующие типы запросов:

  • OPTIONS запросы (preflight для CORS)
  • Scalar endpoints (/scalar/*)
  • OpenAPI endpoints (/openApi/*)
Конфигурация

API ключ должен быть указан в конфигурации приложения:

{
  "ApiKey": "your-secret-api-key-here"
}

Для production окружения рекомендуется использовать переменные окружения:

export ApiKey="your-production-api-key"
Обработка ошибок

При отсутствии или неверном API ключе middleware возвращает:

  • HTTP Status Code: 401 Unauthorized
  • Response Body: "Unauthorized: Invalid or missing X-API-Key"
Безопасность
  • API ключ должен быть достаточно длинным и случайным
  • Используйте HTTPS в production окружении
  • Не храните API ключи в исходном коде
  • Регулярно ротируйте API ключи

S3Lab.Box.Data

Библиотека для работы с данными в ASP.NET Core приложениях, предоставляющая базовые сущности, интерфейсы и репозиторий для Entity Framework Core.

Описание

S3Lab.Box.Data предоставляет набор базовых классов и интерфейсов для упрощения работы с данными в .NET приложениях. Библиотека включает в себя готовые сущности с аудитом и архивированием, универсальный репозиторий и расширения для Entity Framework Core.

Возможности
  • 🏗️ Базовые сущности с поддержкой различных типов ключей
  • 📝 Аудит сущностей (создание, изменение, пользователи)
  • 🗄️ Архивирование сущностей (мягкое удаление)
  • 📚 Универсальный репозиторий с полным набором CRUD операций
  • 🔍 Поддержка Include для связанных сущностей
  • ⚡ Асинхронные операции с поддержкой CancellationToken
  • 🛠️ Расширения для Entity Framework Core
  • 🔄 Интеграция с S3Lab.Box.Result для обработки результатов
Установка
dotnet add package S3Lab.Box.Data
Требования
  • .NET 9.0 или выше
  • Entity Framework Core 9.0.6 или выше

Структура библиотеки

Сущности (Entities)

Entity<TKey>

Базовая сущность с идентификатором:

public abstract class Entity<TKey> : IEntity<TKey>, IEntity
{
    public required TKey Id { get; set; }
}
AuditableEntity<TKey, TUserKey>

Сущность с аудитом (отслеживание создания и изменения):

public abstract class AuditableEntity<TKey, TUserKey> : Entity<TKey>, IAuditableEntity<TUserKey>
{
    public TUserKey CreatedBy { get; set; }
    public TUserKey UpdatedBy { get; set; }
    public DateTime Created { get; set; }
    public DateTime? Updated { get; set; }
}
ArchivableEntity<TKey, TUserKey>

Архивируемая сущность с мягким удалением:

public class ArchivableEntity<TKey, TUserKey> : AuditableEntity<TKey, TUserKey>, IArchivableEntity
{
    public bool IsDeleted { get; set; }
}
DictionaryEntity<TKey>

Справочная сущность с именем:

public abstract class DictionaryEntity<TKey> : IDictionaryEntity<TKey>
{
    public TKey Id { get; set; }
    public required string Name { get; set; }
}

Интерфейсы (Interfaces)

IEntity<TKey>

Базовый интерфейс для сущностей с идентификатором:

public interface IEntity<TKey>
{
    TKey Id { get; set; }
}
IAuditableEntity<TUserKey>

Интерфейс для аудируемых сущностей:

public interface IAuditableEntity<TUserKey>
{
    TUserKey CreatedBy { get; set; }
    TUserKey UpdatedBy { get; set; }
    DateTime Created { get; set; }
    DateTime? Updated { get; set; }
}
IArchivableEntity

Интерфейс для архивируемых сущностей:

public interface IArchivableEntity
{
    bool IsDeleted { get; set; }
}
IRepository<TEntity, TKey>

Универсальный интерфейс репозитория с полным набором CRUD операций:

public interface IRepository<TEntity, TKey>
{
    // Получение по ID
    Task<TEntity?> GetByIdAsync(TKey id);
    Task<TEntity?> GetByIdAsync(TKey id, IEnumerable<string> includePaths);
    Task<TEntity?> GetByIdAsync(TKey id, bool asNoTracking);
    
    // Получение списка по ID
    Task<List<TEntity>> GetByIdListAsync(IEnumerable<TKey> idList);
    
    // Добавление
    Task<TEntity> AddAsync(TEntity entity);
    Task AddRangeAsync(IEnumerable<TEntity> entities);
    
    // Обновление
    Task UpdateAsync(TEntity entity);
    
    // Удаление
    Task DeleteAsync(TEntity entity);
    Task DeleteRangeAsync(IEnumerable<TEntity> entities);
    
    // Сохранение
    Task SaveAsync();
    
    // Проверка существования
    Task<bool> IsExistAsync(TKey id);
}

Репозиторий (Repository)

Repository<TEntity, TKey>

Универсальная реализация репозитория с поддержкой:

  • Асинхронных операций
  • CancellationToken
  • Include для связанных сущностей
  • AsNoTracking для оптимизации производительности
  • Пакетных операций
UnitOfWork

Реализация паттерна Unit of Work для управления транзакциями:

public interface IUnitOfWork : IDisposable
{
    IRepository<TEntity, TKey> Repository<TEntity, TKey>();
    Task<int> SaveChangesAsync();
    Task<IDbContextTransaction> BeginTransactionAsync();
    Task<T> ExecuteInTransactionAsync<T>(Func<Task<T>> operation);
}

Возможности UnitOfWork:

  • 🏗️ Централизованное управление репозиториями
  • 💾 Автоматическое управление транзакциями
  • 🔄 Поддержка отката изменений при ошибках
  • ⚡ Кэширование репозиториев для оптимизации
  • 🛡️ Безопасное освобождение ресурсов

Расширения (Extensions)

QueryableExtensions

Расширения для работы с IQueryable:

public static IQueryable<TEntity> AddIncludes<TEntity, TKey>(
    this IQueryable<TEntity> entities, 
    IEnumerable<string>? includePaths)
ServiceCollectionExtensions

Расширения для регистрации сервисов в DI:

// Регистрация UnitOfWork
services.AddS3LabBoxData();

// Регистрация конкретного репозитория
services.AddRepository<User, int, UserRepository>();

// Регистрация базового репозитория
services.AddRepository<Product, int>();
ResultExtensions

Расширения для работы с результатами операций:

public static class ResultExtensions
{
    public static Result<TResult> Map<T, TResult>(this Result<T> result, Func<T, TResult> mapper);
    public static Result<TResult> Bind<T, TResult>(this Result<T> result, Func<T, Result<TResult>> binder);
    public static Result<T> OnSuccess<T>(this Result<T> result, Action<T> action);
    public static Result<T> OnFailure<T>(this Result<T> result, Action<Error> action);
}

Быстрый старт

1. Создание сущности

public class User : AuditableEntity<int, int>
{
    public string Name { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
}

public class Category : DictionaryEntity<int>
{
    public Category(int id, string name) : base(id, name)
    {
    }
}

2. Создание репозитория

public class UserRepository : Repository<User, int>
{
    public UserRepository(DbContext context) : base(context)
    {
    }
    
    // Дополнительные методы специфичные для User
    public async Task<User?> GetByEmailAsync(string email)
    {
        return await DbSet.FirstOrDefaultAsync(u => u.Email == email);
    }
}

3. Регистрация в DI

// Program.cs
builder.Services.AddScoped<UserRepository>();
builder.Services.AddScoped<IRepository<User, int>, UserRepository>();

4. Использование с Result

public class UserService
{
    private readonly IRepository<User, int> _userRepository;
    
    public UserService(IRepository<User, int> userRepository)
    {
        _userRepository = userRepository;
    }
    
    public async Task<Result<User>> CreateUserAsync(User user)
    {
        try
        {
            user.Created = DateTime.UtcNow;
            user.CreatedBy = 1; // ID текущего пользователя
            
            var createdUser = await _userRepository.AddAsync(user);
            return Result<User>.Success(createdUser);
        }
        catch (Exception ex)
        {
            var error = new Error($"Failed to create user: {ex.Message}", "USER_CREATION_FAILED");
            return Result<User>.Failure(error);
        }
    }
    
    public async Task<Result<User?>> GetUserWithDetailsAsync(int id)
    {
        try
        {
            var user = await _userRepository.GetByIdAsync(id, new[] { "Orders", "Profile" });
            return Result<User?>.Success(user);
        }
        catch (Exception ex)
        {
            var error = new Error($"Failed to get user: {ex.Message}", "USER_RETRIEVAL_FAILED");
            return Result<User?>.Failure(error);
        }
    }
    
    public async Task<Result<List<User>>> GetUsersByIdsAsync(IEnumerable<int> ids)
    {
        try
        {
            var users = await _userRepository.GetByIdListAsync(ids);
            return Result<List<User>>.Success(users);
        }
        catch (Exception ex)
        {
            var error = new Error($"Failed to get users: {ex.Message}", "USERS_RETRIEVAL_FAILED");
            return Result<List<User>>.Failure(error);
        }
    }
}

5. Использование UnitOfWork с Result

public class OrderService
{
    private readonly IUnitOfWork _unitOfWork;
    
    public OrderService(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }
    
    public async Task<Result<Order>> CreateOrderWithItemsAsync(Order order, List<OrderItem> items)
    {
        try
        {
            // Использование транзакции для атомарной операции
            var result = await _unitOfWork.ExecuteInTransactionAsync(async () =>
            {
                var orderRepo = _unitOfWork.Repository<Order, int>();
                var itemRepo = _unitOfWork.Repository<OrderItem, int>();
                
                // Создаем заказ
                var createdOrder = await orderRepo.AddAsync(order);
                
                // Добавляем товары к заказу
                foreach (var item in items)
                {
                    item.OrderId = createdOrder.Id;
                    await itemRepo.AddAsync(item);
                }
                
                // Сохраняем все изменения
                await _unitOfWork.SaveChangesAsync();
                
                return createdOrder;
            });
            
            return Result<Order>.Success(result);
        }
        catch (Exception ex)
        {
            var error = new Error($"Failed to create order: {ex.Message}", "ORDER_CREATION_FAILED");
            return Result<Order>.Failure(error);
        }
    }
    
    public async Task<Result<bool>> TransferBetweenOrdersAsync(int fromOrderId, int toOrderId, int itemId)
    {
        try
        {
            using var transaction = await _unitOfWork.BeginTransactionAsync();
            
            var itemRepo = _unitOfWork.Repository<OrderItem, int>();
            
            // Получаем товар
            var item = await itemRepo.GetByIdAsync(itemId);
            if (item == null)
            {
                var error = new Error("Item not found", "ITEM_NOT_FOUND");
                return Result<bool>.Failure(error);
            }
            
            // Обновляем заказ
            item.OrderId = toOrderId;
            await itemRepo.UpdateAsync(item);
            
            // Сохраняем изменения
            await _unitOfWork.SaveChangesAsync();
            
            // Подтверждаем транзакцию
            await transaction.CommitAsync();
            
            return Result<bool>.Success(true);
        }
        catch (Exception ex)
        {
            // Транзакция автоматически откатится при исключении
            var error = new Error($"Failed to transfer item: {ex.Message}", "ITEM_TRANSFER_FAILED");
            return Result<bool>.Failure(error);
        }
    }
}

Примеры использования

Работа с аудируемыми сущностями

public class Product : AuditableEntity<int, int>
{
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }
}

// Создание
var product = new Product
{
    Name = "Sample Product",
    Price = 99.99m,
    Created = DateTime.UtcNow,
    CreatedBy = currentUserId
};

await _repository.AddAsync(product);

// Обновление
product.Price = 89.99m;
product.Updated = DateTime.UtcNow;
product.UpdatedBy = currentUserId;

await _repository.UpdateAsync(product);

Работа с архивируемыми сущностями

public class Order : ArchivableEntity<int, int>
{
    public string OrderNumber { get; set; } = string.Empty;
    public decimal Total { get; set; }
}

// Мягкое удаление
var order = await _repository.GetByIdAsync(orderId);
if (order != null)
{
    order.IsDeleted = true;
    order.Updated = DateTime.UtcNow;
    order.UpdatedBy = currentUserId;
    
    await _repository.UpdateAsync(order);
}

Использование Include

// Получение пользователя с заказами и профилем
var user = await _userRepository.GetByIdAsync(
    userId, 
    new[] { "Orders", "Profile", "Orders.Items" }
);

// Получение без отслеживания изменений
var user = await _userRepository.GetByIdAsync(
    userId, 
    new[] { "Orders" }, 
    asNoTracking: true
);

Пакетные операции

// Добавление нескольких сущностей
var users = new List<User>
{
    new User { Name = "User1", Email = "user1@example.com" },
    new User { Name = "User2", Email = "user2@example.com" }
};

await _userRepository.AddRangeAsync(users);
await _userRepository.SaveAsync();

// Удаление нескольких сущностей
var usersToDelete = await _userRepository.GetByIdListAsync(userIds);
await _userRepository.DeleteRangeAsync(usersToDelete);
await _userRepository.SaveAsync();

Работа с результатами операций

// Цепочка операций с обработкой ошибок
var result = await userService.CreateUserAsync(user)
    .Bind(createdUser => orderService.CreateOrderAsync(createdUser.Id, order))
    .Map(order => $"Order {order.Id} created successfully")
    .OnSuccess(message => Console.WriteLine(message))
    .OnFailure(error => Console.WriteLine($"Error: {error.Message}"));

// Обработка результатов в контроллере
[HttpPost]
public async Task<IActionResult> CreateUser([FromBody] CreateUserRequest request)
{
    var user = new User { Name = request.Name, Email = request.Email };
    var result = await _userService.CreateUserAsync(user);
    
    return result.Match(
        success => Ok(success),
        error => BadRequest(new { error = error.Message, code = error.Code })
    );
}

Конфигурация Entity Framework

DbContext

public class ApplicationDbContext : DbContext
{
    public DbSet<User> Users { get; set; }
    public DbSet<Category> Categories { get; set; }
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        
        // Конфигурация для аудируемых сущностей
        modelBuilder.Entity<User>()
            .Property(u => u.Created)
            .HasDefaultValueSql("GETUTCDATE()");
            
        // Конфигурация для архивируемых сущностей
        modelBuilder.Entity<Order>()
            .HasQueryFilter(o => !o.IsDeleted);
    }
}

Производительность

Рекомендации

  1. Используйте AsNoTracking для операций только чтения:

    var users = await _repository.GetByIdAsync(id, asNoTracking: true);
    
  2. Применяйте Include только когда необходимо:

    // Только при необходимости связанных данных
    var user = await _repository.GetByIdAsync(id, new[] { "Orders" });
    
  3. Используйте пакетные операции для множественных изменений:

    await _repository.AddRangeAsync(entities);
    await _repository.SaveAsync(); // Одно сохранение для всех
    
  4. Применяйте CancellationToken для длительных операций:

    var users = await _repository.GetByIdListAsync(ids, cancellationToken);
    
  5. Используйте Result для обработки ошибок:

    var result = await _repository.GetByIdAsync(id);
    return result.Match(
        success => Ok(success),
        error => NotFound(error.Message)
    );
    

Зависимости

S3Lab.Box.Auth

  • Microsoft.Extensions.Configuration 9.0.6
  • Microsoft.Extensions.DependencyInjection 9.0.6
  • Scalar.AspNetCore 2.5.3

S3Lab.Box.Data

  • Microsoft.EntityFrameworkCore 9.0.6
  • S3Lab.Box.Result 2.0.0

S3Lab.Box.Result

  • Нет внешних зависимостей

Лицензия

MIT License

Автор

Sazonov Andrei

Версия

2.0.0

Product Compatible and additional computed target framework versions.
.NET 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.
  • net9.0

    • No dependencies.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on S3Lab.Box.Result:

Package Downloads
S3Lab.Box.Data

Библиотека для работы с данными в ASP.NET Core приложениях, предоставляющая базовые сущности, интерфейсы и репозиторий для Entity Framework Core.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.2.0 140 6/27/2025
2.1.0 83 6/27/2025
2.0.0 146 6/26/2025
1.0.0 118 2/12/2025