// -----------------------------------------------------------------------------
// ServiceCollectionExtensions.cs
// Sprint: SPRINT_20260118_017_Evidence_artifact_store_unification
// Tasks: AS-002, AS-003 - Service registration
// Description: DI registration for artifact store services
// -----------------------------------------------------------------------------
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using StellaOps.Artifact.Core;
using StellaOps.Infrastructure.Postgres.Options;
namespace StellaOps.Artifact.Infrastructure;
///
/// Extension methods for registering artifact store services.
///
public static class ServiceCollectionExtensions
{
///
/// Adds unified artifact store services with S3 backend.
///
/// Service collection.
/// Configuration root.
/// Configuration section for options.
/// Service collection for chaining.
public static IServiceCollection AddUnifiedArtifactStore(
this IServiceCollection services,
IConfiguration configuration,
string sectionName = "ArtifactStore")
{
// Configure S3 store options
services.Configure(configuration.GetSection($"{sectionName}:S3"));
// Configure PostgreSQL options for index
services.Configure("Artifact", configuration.GetSection($"{sectionName}:Postgres"));
// Register data source
services.AddSingleton(sp =>
{
var options = sp.GetRequiredService>().Get("Artifact");
var logger = sp.GetRequiredService>();
return new ArtifactDataSource(Options.Create(options), logger);
});
// Register core services
services.AddSingleton();
// Register index repository
services.AddScoped(sp =>
{
var dataSource = sp.GetRequiredService();
var logger = sp.GetRequiredService>();
// TODO: Get tenant ID from context
return new PostgresArtifactIndexRepository(dataSource, logger, "default");
});
// Register S3 artifact store
services.AddScoped();
return services;
}
///
/// Adds unified artifact store with in-memory backend (for testing).
///
/// Service collection.
/// Service collection for chaining.
public static IServiceCollection AddInMemoryArtifactStore(this IServiceCollection services)
{
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
return services;
}
///
/// Adds artifact migration services.
///
/// Service collection.
/// Options configuration.
/// Service collection for chaining.
public static IServiceCollection AddArtifactMigration(
this IServiceCollection services,
Action? configure = null)
{
var options = new ArtifactMigrationOptions();
configure?.Invoke(options);
services.AddSingleton(options);
services.AddScoped();
return services;
}
}
///
/// In-memory artifact store for testing.
///
public sealed class InMemoryArtifactStore : IArtifactStore
{
private readonly Dictionary _artifacts = new();
private readonly object _lock = new();
public Task StoreAsync(ArtifactStoreRequest request, CancellationToken ct = default)
{
var key = $"{request.BomRef}/{request.SerialNumber}/{request.ArtifactId}";
using var ms = new MemoryStream();
request.Content.CopyTo(ms);
var content = ms.ToArray();
using var sha = System.Security.Cryptography.SHA256.Create();
var hash = sha.ComputeHash(content);
var sha256 = Convert.ToHexStringLower(hash);
var metadata = new ArtifactMetadata
{
StorageKey = key,
BomRef = request.BomRef,
SerialNumber = request.SerialNumber,
ArtifactId = request.ArtifactId,
ContentType = request.ContentType,
SizeBytes = content.Length,
Sha256 = sha256,
CreatedAt = DateTimeOffset.UtcNow,
Type = request.Type,
TenantId = request.TenantId
};
lock (_lock)
{
var wasCreated = !_artifacts.ContainsKey(key);
_artifacts[key] = (content, metadata);
return Task.FromResult(ArtifactStoreResult.Succeeded(key, sha256, content.Length, wasCreated));
}
}
public Task ReadAsync(string bomRef, string? serialNumber, string? artifactId, CancellationToken ct = default)
{
lock (_lock)
{
var matching = _artifacts
.Where(kvp => kvp.Value.Metadata.BomRef == bomRef)
.Where(kvp => serialNumber == null || kvp.Value.Metadata.SerialNumber == serialNumber)
.Where(kvp => artifactId == null || kvp.Value.Metadata.ArtifactId == artifactId)
.FirstOrDefault();
if (matching.Value.Content == null)
{
return Task.FromResult(ArtifactReadResult.NotFound());
}
return Task.FromResult(ArtifactReadResult.Succeeded(
new MemoryStream(matching.Value.Content),
matching.Value.Metadata));
}
}
public Task> ListAsync(string bomRef, string? serialNumber = null, CancellationToken ct = default)
{
lock (_lock)
{
var result = _artifacts.Values
.Where(x => x.Metadata.BomRef == bomRef)
.Where(x => serialNumber == null || x.Metadata.SerialNumber == serialNumber)
.Select(x => x.Metadata)
.ToList();
return Task.FromResult>(result);
}
}
public Task ExistsAsync(string bomRef, string serialNumber, string artifactId, CancellationToken ct = default)
{
var key = $"{bomRef}/{serialNumber}/{artifactId}";
lock (_lock)
{
return Task.FromResult(_artifacts.ContainsKey(key));
}
}
public Task GetMetadataAsync(string bomRef, string serialNumber, string artifactId, CancellationToken ct = default)
{
var key = $"{bomRef}/{serialNumber}/{artifactId}";
lock (_lock)
{
return Task.FromResult(_artifacts.TryGetValue(key, out var entry) ? entry.Metadata : null);
}
}
public Task DeleteAsync(string bomRef, string serialNumber, string artifactId, CancellationToken ct = default)
{
var key = $"{bomRef}/{serialNumber}/{artifactId}";
lock (_lock)
{
return Task.FromResult(_artifacts.Remove(key));
}
}
}