BrandUp.Extensions.ObjectStorage.Testing
1.0.5
See the version list below for details.
dotnet add package BrandUp.Extensions.ObjectStorage.Testing --version 1.0.5
NuGet\Install-Package BrandUp.Extensions.ObjectStorage.Testing -Version 1.0.5
<PackageReference Include="BrandUp.Extensions.ObjectStorage.Testing" Version="1.0.5" />
<PackageVersion Include="BrandUp.Extensions.ObjectStorage.Testing" Version="1.0.5" />
<PackageReference Include="BrandUp.Extensions.ObjectStorage.Testing" />
paket add BrandUp.Extensions.ObjectStorage.Testing --version 1.0.5
#r "nuget: BrandUp.Extensions.ObjectStorage.Testing, 1.0.5"
#:package BrandUp.Extensions.ObjectStorage.Testing@1.0.5
#addin nuget:?package=BrandUp.Extensions.ObjectStorage.Testing&version=1.0.5
#tool nuget:?package=BrandUp.Extensions.ObjectStorage.Testing&version=1.0.5
BrandUp.Extensions.ObjectStorage
Библиотека для работы с S3-совместимыми объектными хранилищами (Yandex Cloud Object Storage, Amazon S3, MinIO и др.) через AWS SDK.
Пакеты
| Пакет | Описание |
|---|---|
BrandUp.Extensions.ObjectStorage.Abstraction |
Интерфейсы и модели. Зависимостей от AWS SDK нет. |
BrandUp.Extensions.ObjectStorage |
Реализация через AWSSDK.S3. |
BrandUp.Extensions.ObjectStorage.Testing |
Фейковая in-memory реализация для тестов. |
Быстрый старт
1. Описать метаданные объекта
public class UserPhotoMetadata : IObjectMetadata
{
public string FileName { get; set; }
public string ContentType { get; set; }
public DateTime UploadedAt { get; set; }
}
Любой класс с публичным конструктором без параметров и публичными свойствами с геттером и сеттером.
Поддерживаемые типы свойств: string, int, bool, Guid, DateTime, decimal, enum и любые типы с TypeConverter. Значения null при сериализации пропускаются.
2. Зарегистрировать в DI
services.AddObjectStorage(opts =>
{
opts.ServiceUrl = "https://storage.yandexcloud.net";
opts.AuthenticationRegion = "ru-central1";
opts.AccessKeyId = "...";
opts.SecretAccessKey = "...";
})
.AddMapping<UserPhotoMetadata>("my-bucket/photos");
Формат destination в AddMapping: bucketName или bucketName/prefix. Допустимые символы: буквы, цифры и /.
3. Использовать
// Через IObjectStorage (совместимый фасад)
public class PhotoService(IObjectStorage storage)
{
public Task UploadAsync(Guid id, Stream photo)
=> storage.UploadAsync(id, new UserPhotoMetadata { FileName = "photo.jpg" }, photo);
public Task<Stream?> DownloadAsync(Guid id)
=> storage.ReadAsync<UserPhotoMetadata>(id);
public async Task<UserPhotoMetadata?> GetMetadataAsync(Guid id)
=> (await storage.FindAsync<UserPhotoMetadata>(id))?.Metadata;
public Task<bool> DeleteAsync(Guid id)
=> storage.DeleteAsync<UserPhotoMetadata>(id);
}
// Через IObjectBucket<T> — типизированный бакет, инжектируется напрямую
public class PhotoService(IObjectBucket<UserPhotoMetadata> bucket)
{
public Task UploadAsync(Guid id, Stream photo)
=> bucket.UploadAsync(id, new UserPhotoMetadata { FileName = "photo.jpg" }, photo);
}
API
IObjectStorageClient
Точка входа, аналог IMongoClient. Навигация к бакетам синхронная (без I/O).
IObjectBucket bucket = client.GetBucket("my-bucket");
IObjectBucket<T> typed = client.GetBucket<UserPhotoMetadata>();
IReadOnlyList<BucketInfo> buckets = await client.ListBucketsAsync();
await client.CreateBucketAsync("new-bucket", s => s.Versioning = BucketVersioning.Enabled);
await client.DropBucketAsync("old-bucket");
IObjectBucket
Управление бакетом, аналог IMongoDatabase.
bool exists = await bucket.ExistsAsync();
BucketSettings settings = await bucket.GetSettingsAsync();
await bucket.UpdateSettingsAsync(s =>
{
s.Versioning = BucketVersioning.Enabled;
s.Access = BucketAccess.PublicRead;
s.LifecycleRules.Add(new LifecycleRule("cleanup", ExpirationDays: 30, Prefix: "temp/"));
});
IObjectBucket<TMetadata>
Типизированные CRUD-операции, аналог IMongoCollection<T>. Наследует IObjectBucket.
| Метод | Описание |
|---|---|
FindOneAsync(Guid, CancellationToken) |
Метаданные объекта. null если не найден. |
OpenReadAsync(Guid, CancellationToken) |
Поток содержимого. null если не найден. |
UploadAsync(Guid, TMetadata, Stream, CancellationToken) |
Загрузить объект. |
DeleteOneAsync(Guid, CancellationToken) |
Удалить объект. false если не существовал. |
IObjectStorage
Упрощённый фасад над IObjectStorageClient для обратной совместимости.
| Метод | Описание |
|---|---|
FindAsync<T>(Guid, CancellationToken) |
Метаданные объекта. null если не найден. |
ReadAsync<T>(Guid, CancellationToken) |
Поток содержимого. null если не найден. |
UploadAsync<T>(Guid, T, Stream, CancellationToken) |
Загрузить объект. |
DeleteAsync<T>(Guid, CancellationToken) |
Удалить объект. false если не существовал. |
ObjectStorageOptions
| Свойство | Описание |
|---|---|
ServiceUrl |
URL эндпоинта S3 |
AuthenticationRegion |
Регион авторизации |
AccessKeyId |
Идентификатор ключа доступа |
SecretAccessKey |
Секретный ключ доступа |
SessionToken |
Токен сессии для временных (STS) кред. Запросы подписываются заголовком X-Amz-Security-Token. См. Временные креды (STS). |
ForcePathStyle |
Path-style адресация ({serviceUrl}/{bucket}) вместо virtual-hosted ({bucket}.{serviceUrl}). Нужно для MinIO. По умолчанию false. |
При статическом доступе обязательны AccessKeyId + SecretAccessKey. Если зарегистрирован провайдер кред (UseCredentialsProvider), они становятся необязательными. ServiceUrl и AuthenticationRegion обязательны всегда.
BucketSettings
| Свойство | Тип | Описание |
|---|---|---|
Versioning |
BucketVersioning |
Disabled / Enabled / Suspended |
Access |
BucketAccess |
Private / PublicRead |
LifecycleRules |
List<LifecycleRule> |
Правила жизненного цикла |
LifecycleRule(string Id, int? ExpirationDays, string? Prefix, bool Enabled)
Работа с JSON
Extension-методы ReadJsonAsync / UploadJsonAsync работают с любым IObjectMetadata — никаких дополнительных интерфейсов реализовывать не нужно.
public class ReportContent
{
public string Title { get; set; }
public decimal Value { get; set; }
}
public class ReportMetadata : IObjectMetadata // обычный IObjectMetadata
{
public string Author { get; set; }
public DateTime CreatedAt { get; set; }
}
// Загрузка
await storage.UploadJsonAsync<ReportMetadata, ReportContent>(
id, new ReportMetadata { Author = "admin" }, reportContent);
// Чтение — возвращает null если объект не найден
ReportContent? content = await storage.ReadJsonAsync<ReportMetadata, ReportContent>(id);
// Опционально: настройки сериализации
var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
await storage.UploadJsonAsync<ReportMetadata, ReportContent>(id, metadata, content, options);
var result = await storage.ReadJsonAsync<ReportMetadata, ReportContent>(id, options);
// Через IObjectBucket<T>
await bucket.UploadJsonAsync<ReportMetadata, ReportContent>(id, metadata, content);
var result = await bucket.ReadJsonAsync<ReportMetadata, ReportContent>(id);
Исключения
При ошибках S3 бросается ObjectStorageException. Свойство StatusCode содержит HTTP-код ответа, InnerException — оригинальное AmazonS3Exception.
catch (ObjectStorageException ex) when (ex.StatusCode == HttpStatusCode.Forbidden) { }
catch (ObjectStorageException ex) { }
Временные креды (STS)
Поддерживаются три режима аутентификации. Приоритет при выборе: провайдер → SessionToken → статические AccessKeyId/SecretAccessKey.
1. Статические ключи
Режим по умолчанию — см. Быстрый старт.
2. Фиксированный токен сессии
Для коротких/одноразовых сценариев с уже полученными временными кредами без авто-обновления. SDK подписывает запросы заголовком X-Amz-Security-Token.
services.AddObjectStorage(opts =>
{
opts.ServiceUrl = "https://storage.yandexcloud.net";
opts.AuthenticationRegion = "ru-central1";
opts.AccessKeyId = "...";
opts.SecretAccessKey = "...";
opts.SessionToken = "...";
});
3. Провайдер с авто-обновлением
Основной режим для временных кред с ограниченным сроком жизни (например, Yandex STS, TTL ≤ 12 ч). AmazonS3Client создаётся один раз, а креды обновляются «на месте» — singleton-клиент не пересоздаётся.
public sealed record ObjectStorageCredentials(
string AccessKeyId, string SecretAccessKey, string? SessionToken, DateTimeOffset? ExpiresUtc);
public interface IObjectStorageCredentialsProvider
{
ObjectStorageCredentials GetCurrent(); // читается синхронно SDK при подписи
Task RefreshAsync(CancellationToken cancellationToken = default); // проактивное обновление кеша
}
services.AddObjectStorage(opts =>
{
opts.ServiceUrl = "https://storage.yandexcloud.net";
opts.AuthenticationRegion = "ru-central1";
// AccessKeyId / SecretAccessKey не нужны — креды отдаёт провайдер
})
.UseCredentialsProvider<MyCredentialsProvider>() // либо UseCredentialsProvider(sp => ...)
.AddMapping<UserPhotoMetadata>("my-bucket/photos");
Sync/async:
GetCurrent()обязан отдавать закешированные креды синхронно (вызывается SDK при подписи каждого запроса). Получение свежих кред — асинхронное и идёт вне SDK: потребитель проактивно обновляет кеш доExpiresUtc(например, по таймеру),RefreshAsync— точка такого обновления. Минтинг STS (вызов STS-эндпоинта) — на стороне потребителя, пакет только потребляет готовые креды.
MinIO
MinIO не поддерживает virtual-hosted адресацию, поэтому обязателен ForcePathStyle = true.
services.AddObjectStorage(opts =>
{
opts.ServiceUrl = "http://localhost:9000";
opts.AuthenticationRegion = "us-east-1";
opts.AccessKeyId = "minioadmin";
opts.SecretAccessKey = "minioadmin";
opts.ForcePathStyle = true;
})
.AddMapping<UserPhotoMetadata>("photos");
Тестирование
Пакет BrandUp.Extensions.ObjectStorage.Testing предоставляет in-memory реализацию всех интерфейсов без зависимостей от AWS SDK.
Настройка
services.AddFakeObjectStorage()
.AddMapping<UserPhotoMetadata>("photos/users")
.WithBucket("photos"); // предсоздать бакет (опционально)
Использование в тестах
// Стандартные интерфейсы работают как обычно
var storage = sp.GetRequiredService<IObjectStorage>();
var bucket = sp.GetRequiredService<IObjectBucket<UserPhotoMetadata>>();
var client = sp.GetRequiredService<IObjectStorageClient>();
// FakeObjectStore — инспекция и управление состоянием
var store = sp.GetRequiredService<FakeObjectStore>();
Assert.True(store.BucketExists("photos"));
Assert.Equal(1, store.GetObjectCount("photos"));
store.Clear(); // сброс между тестами
store.CreateBucket("extra"); // ручное создание бакета
store.PutObject("photos", "key", bytes, metadata); // предзаполнение данными
Yandex Cloud Object Storage
Конфигурация
services.AddObjectStorage(opts =>
{
opts.ServiceUrl = "https://storage.yandexcloud.net";
opts.AuthenticationRegion = "ru-central1";
opts.AccessKeyId = "<идентификатор статического ключа>";
opts.SecretAccessKey = "<секретный ключ>";
})
.AddMapping<UserPhotoMetadata>("my-bucket/photos");
Ключи доступа создаются в консоли Yandex Cloud: IAM → Сервисные аккаунты → Ключи доступа.
Совместимость
| Функция | YC |
|---|---|
| Загрузка / чтение / удаление объектов | ✅ |
| Метаданные объектов | ✅ |
| Создание / удаление бакетов | ✅ |
| Список бакетов | ✅ |
| Версионирование | ✅ |
| Lifecycle rules | ✅ |
| Временные креды (STS) | ✅ |
| Управление доступом (ACL) | ⚠️ |
ACL-операции работают через S3-совместимый API, однако публичный доступ может быть заблокирован политикой организации. Для управления публичным доступом рекомендуется консоль YC или CLI: yc storage bucket update --public-read.
Имена бакетов в Yandex Cloud уникальны глобально — не только в рамках вашего аккаунта.
Временные креды Yandex STS
Формат временного ключа Yandex STS (Key ID + Secret key + Session token, TTL ≤ 12 ч) совпадает с ObjectStorageCredentials, а адресация — virtual-hosted (ForcePathStyle оставить false). Используйте SessionToken или провайдер с авто-обновлением.
⚠️ Политика временного ключа Yandex STS привязана к одному бакету — одним ключом нельзя работать с несколькими бакетами. Если узлу нужны несколько бакетов под временными кредами, потребуется отдельный ключ/провайдер на каждый.
| Product | Versions 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. |
-
net10.0
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.