Fix 8 design majors: optional ILogger in libraries, IndexFactoryService singleton threading, SharedMediaTypeRegistry, required TrackDto fields, GetExtensionType dedup, PagedResult zero-guard, TrackService null-return on failure

This commit is contained in:
Daniel Harvey
2026-05-17 16:57:03 -04:00
parent fc5b8de81a
commit 4bd3be2eb8
11 changed files with 99 additions and 68 deletions
@@ -10,4 +10,8 @@
<ProjectReference Include="..\DeepDrftModels\DeepDrftModels.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
</ItemGroup>
</Project>
@@ -3,12 +3,20 @@ using DeepDrftContent.Services.FileDatabase.Services;
namespace DeepDrftContent.Services.FileDatabase.Models;
/// <summary>
/// Shared media type registry instance — one allocation for all factory classes in this file.
/// </summary>
file static class SharedMediaTypeRegistry
{
internal static readonly IMediaTypeRegistry Instance = new SimpleMediaTypeRegistry();
}
/// <summary>
/// Type mappings for media vault types - simple dictionary-based approach
/// </summary>
public static class MediaVaultTypeMap
{
private static readonly IMediaTypeRegistry _registry = new SimpleMediaTypeRegistry();
private static readonly IMediaTypeRegistry _registry = SharedMediaTypeRegistry.Instance;
public static Type GetBinaryType(MediaVaultType vaultType) => _registry.GetBinaryType(vaultType);
@@ -55,7 +63,7 @@ public static class MetaDataFactory
return new AudioMetaData(entryKey, extension, duration, bitrate);
}
private static readonly IMediaTypeRegistry _metaDataRegistry = new SimpleMediaTypeRegistry();
private static readonly IMediaTypeRegistry _metaDataRegistry = SharedMediaTypeRegistry.Instance;
public static MetaData CreateFromMedia(MediaVaultType type, string entryKey, string extension, object media)
{
@@ -75,7 +83,7 @@ public static class MetaDataFactory
/// </summary>
public static class MediaParamsFactory
{
private static readonly IMediaTypeRegistry _registry = new SimpleMediaTypeRegistry();
private static readonly IMediaTypeRegistry _registry = SharedMediaTypeRegistry.Instance;
public static object Create(MediaVaultType type, FileBinary fileBinary, MetaData metaData)
{
@@ -94,7 +102,7 @@ public static class MediaParamsFactory
/// </summary>
public static class FileBinaryFactory
{
private static readonly IMediaTypeRegistry _registry = new SimpleMediaTypeRegistry();
private static readonly IMediaTypeRegistry _registry = SharedMediaTypeRegistry.Instance;
public static object Create(MediaVaultType vaultType, object parameters)
{
@@ -124,7 +132,7 @@ public static class FileBinaryFactory
/// </summary>
public static class FileBinaryDtoFactory
{
private static readonly IMediaTypeRegistry _registry = new SimpleMediaTypeRegistry();
private static readonly IMediaTypeRegistry _registry = SharedMediaTypeRegistry.Instance;
public static object From(MediaVaultType type, object mediaBinary)
{
@@ -68,7 +68,7 @@ public class MediaBinary : FileBinary
return new MediaBinary(new MediaBinaryParams(buffer, dto.Size, extension));
}
private static string GetExtensionType(string mime)
protected static string GetExtensionType(string mime)
{
return MimeTypeExtensions.GetExtension(mime);
}
@@ -116,11 +116,6 @@ public class ImageBinary : MediaBinary
var extension = GetExtensionType(dto.Mime);
return new ImageBinary(new ImageBinaryParams(buffer, dto.Size, extension, dto.AspectRatio));
}
private static string GetExtensionType(string mime)
{
return MimeTypeExtensions.GetExtension(mime);
}
}
/// <summary>
@@ -171,11 +166,6 @@ public class AudioBinary : MediaBinary
var extension = GetExtensionType(dto.Mime);
return new AudioBinary(new AudioBinaryParams(buffer, dto.Size, extension, dto.Duration, dto.Bitrate));
}
private static string GetExtensionType(string mime)
{
return MimeTypeExtensions.GetExtension(mime);
}
}
/// <summary>
@@ -11,6 +11,7 @@ public class FileDatabase : DirectoryIndexDirectory, IDisposable
{
private readonly StructuralMap<string, MediaVault> _vaults;
private readonly IndexWatcher _indexWatcher;
private readonly IndexFactoryService _indexFactory;
private bool _disposed;
/// <summary>
@@ -23,7 +24,7 @@ public class FileDatabase : DirectoryIndexDirectory, IDisposable
if (rootIndex != null)
{
var db = new FileDatabase(rootPath, rootIndex);
var db = new FileDatabase(rootPath, rootIndex, factoryService);
await db.InitVaultsAsync();
return db;
}
@@ -31,10 +32,11 @@ public class FileDatabase : DirectoryIndexDirectory, IDisposable
return null;
}
private FileDatabase(string rootPath, IDirectoryIndex index) : base(rootPath, index)
private FileDatabase(string rootPath, IDirectoryIndex index, IndexFactoryService indexFactory) : base(rootPath, index)
{
_vaults = new StructuralMap<string, MediaVault>();
_indexWatcher = new IndexWatcher();
_indexFactory = indexFactory;
}
/// <summary>
@@ -58,7 +60,7 @@ public class FileDatabase : DirectoryIndexDirectory, IDisposable
private async Task InitVaultAsync(string vaultId, MediaVaultType vaultType)
{
var path = Path.Combine(RootPath, vaultId);
var directoryVault = await MediaVaultFactory.From(path, vaultType);
var directoryVault = await MediaVaultFactory.From(path, vaultType, _indexFactory);
if (directoryVault != null)
{
@@ -80,9 +82,8 @@ public class FileDatabase : DirectoryIndexDirectory, IDisposable
{
try
{
var factoryService = new IndexFactoryService();
var vaultPath = Path.Combine(RootPath, vaultId);
var index = await factoryService.LoadIndexAsync(IndexType.Vault, vaultPath);
var index = await _indexFactory.LoadIndexAsync(IndexType.Vault, vaultPath);
if (index is VaultIndex vaultIndex)
{
@@ -115,27 +116,20 @@ public class FileDatabase : DirectoryIndexDirectory, IDisposable
}
/// <summary>
/// Creates a new vault
/// Creates a new vault. Propagates exceptions to the caller — vault creation failure is not
/// silently swallowable because a partially-created vault would leave the index inconsistent.
/// </summary>
public async Task CreateVaultAsync(string vaultId, MediaVaultType vaultType)
{
try
{
var path = Path.Combine(RootPath, vaultId);
var directoryVault = await MediaVaultFactory.From(path, vaultType);
var directoryVault = await MediaVaultFactory.From(path, vaultType, _indexFactory);
if (directoryVault != null)
{
_vaults.Set(vaultId, directoryVault);
// Now using string-based index
await AddToIndexAsync(vaultId);
}
}
catch
{
throw;
}
}
/// <summary>
/// Loads a resource from a specific vault (MediaVaultType inferred from T)
@@ -1,6 +1,7 @@
using DeepDrftContent.Services.FileDatabase.Abstractions;
using DeepDrftContent.Services.FileDatabase.Models;
using DeepDrftContent.Services.FileDatabase.Utils;
using Microsoft.Extensions.Logging;
namespace DeepDrftContent.Services.FileDatabase.Services;
@@ -96,12 +97,15 @@ public class VaultIndexDirectory : IndexDirectory
{
private IVaultIndex _vaultIndex;
private readonly SemaphoreSlim _indexLock = new(1, 1);
private readonly IndexFactoryService _factoryService = new();
private readonly IndexFactoryService _factoryService;
private readonly ILogger<VaultIndexDirectory>? _logger;
public VaultIndexDirectory(string rootPath, IVaultIndex index, IIndexDataFactory? indexDataFactory = null)
: base(rootPath, IndexType.Vault, index, indexDataFactory)
public VaultIndexDirectory(string rootPath, IVaultIndex index, IIndexDataFactory? indexDataFactory = null, ILogger<VaultIndexDirectory>? logger = null, IndexFactoryService? factoryService = null)
: base(rootPath, IndexType.Vault, index, indexDataFactory ?? factoryService)
{
_vaultIndex = index;
_logger = logger;
_factoryService = factoryService ?? new IndexFactoryService();
}
protected async Task AddToIndexAsync(string entryId, MetaData metaData)
@@ -131,11 +135,17 @@ public class VaultIndexDirectory : IndexDirectory
{
_vaultIndex = vaultIndex;
Index = vaultIndex;
if (_logger != null)
_logger.LogDebug("VaultIndexDirectory: Reloaded index for {RootPath}, {EntryCount} entries", RootPath, vaultIndex.GetEntriesSize());
else
Console.WriteLine($"VaultIndexDirectory: Reloaded index for {RootPath}, {vaultIndex.GetEntriesSize()} entries");
}
}
catch (Exception ex)
{
if (_logger != null)
_logger.LogWarning(ex, "VaultIndexDirectory: Failed to reload index for {RootPath}", RootPath);
else
Console.WriteLine($"VaultIndexDirectory: Failed to reload index for {RootPath}: {ex.Message}");
}
finally
@@ -1,4 +1,5 @@
using DeepDrftContent.Services.FileDatabase.Models;
using Microsoft.Extensions.Logging;
namespace DeepDrftContent.Services.FileDatabase.Services;
@@ -11,8 +12,14 @@ public class IndexWatcher : IDisposable
private readonly Dictionary<string, FileSystemWatcher> _watchers = new();
private readonly Dictionary<string, Action> _reloadCallbacks = new();
private readonly object _lock = new();
private readonly ILogger<IndexWatcher>? _logger;
private bool _disposed;
public IndexWatcher(ILogger<IndexWatcher>? logger = null)
{
_logger = logger;
}
/// <summary>
/// Registers an index file to be watched for changes.
/// </summary>
@@ -46,10 +53,16 @@ public class IndexWatcher : IDisposable
_watchers[indexPath] = watcher;
_reloadCallbacks[indexPath] = onChanged;
if (_logger != null)
_logger.LogDebug("IndexWatcher: Watching {IndexPath}/index", indexPath);
else
Console.WriteLine($"IndexWatcher: Watching {indexPath}/index");
}
catch (Exception ex)
{
if (_logger != null)
_logger.LogWarning(ex, "IndexWatcher: Failed to watch {IndexPath}", indexPath);
else
Console.WriteLine($"IndexWatcher: Failed to watch {indexPath}: {ex.Message}");
}
}
@@ -83,6 +96,9 @@ public class IndexWatcher : IDisposable
{
if (_reloadCallbacks.TryGetValue(indexPath, out var callback))
{
if (_logger != null)
_logger.LogDebug("IndexWatcher: Index changed at {IndexPath}, triggering reload", indexPath);
else
Console.WriteLine($"IndexWatcher: Index changed at {indexPath}, triggering reload");
// Invoke callback on a background thread to avoid blocking the watcher
@@ -94,6 +110,9 @@ public class IndexWatcher : IDisposable
}
catch (Exception ex)
{
if (_logger != null)
_logger.LogWarning(ex, "IndexWatcher: Reload callback failed for {IndexPath}", indexPath);
else
Console.WriteLine($"IndexWatcher: Reload callback failed: {ex.Message}");
}
});
@@ -9,7 +9,8 @@ namespace DeepDrftContent.Services.FileDatabase.Services;
/// </summary>
public abstract class MediaVault : VaultIndexDirectory
{
protected MediaVault(string rootPath, VaultIndex index) : base(rootPath, index) { }
protected MediaVault(string rootPath, VaultIndex index, IndexFactoryService? factoryService = null)
: base(rootPath, index, factoryService: factoryService) { }
/// <summary>
/// Generates a media key from an entry key by sanitizing special characters
@@ -105,19 +106,20 @@ public abstract class MediaVault : VaultIndexDirectory
/// </summary>
public class ImageVault : MediaVault
{
private ImageVault(string rootPath, VaultIndex index) : base(rootPath, index) { }
private ImageVault(string rootPath, VaultIndex index, IndexFactoryService? factoryService = null)
: base(rootPath, index, factoryService) { }
/// <summary>
/// Factory method to create an ImageVault instance
/// </summary>
public static async Task<ImageVault?> FromAsync(string rootPath)
public static async Task<ImageVault?> FromAsync(string rootPath, IndexFactoryService? factoryService = null)
{
var factoryService = new IndexFactoryService();
var index = await factoryService.LoadOrCreateVaultIndexAsync(rootPath, MediaVaultType.Image);
var factory = factoryService ?? new IndexFactoryService();
var index = await factory.LoadOrCreateVaultIndexAsync(rootPath, MediaVaultType.Image);
if (index != null)
{
return new ImageVault(rootPath, (VaultIndex)index);
return new ImageVault(rootPath, (VaultIndex)index, factory);
}
return null;
@@ -126,16 +128,17 @@ public class ImageVault : MediaVault
public class AudioVault : MediaVault
{
private AudioVault(string rootPath, VaultIndex index) : base(rootPath, index) { }
private AudioVault(string rootPath, VaultIndex index, IndexFactoryService? factoryService = null)
: base(rootPath, index, factoryService) { }
public static async Task<AudioVault?> FromAsync(string rootPath)
public static async Task<AudioVault?> FromAsync(string rootPath, IndexFactoryService? factoryService = null)
{
var factoryService = new IndexFactoryService();
var index = await factoryService.LoadOrCreateVaultIndexAsync(rootPath, MediaVaultType.Audio);
var factory = factoryService ?? new IndexFactoryService();
var index = await factory.LoadOrCreateVaultIndexAsync(rootPath, MediaVaultType.Audio);
if (index != null)
{
return new AudioVault(rootPath, (VaultIndex)index);
return new AudioVault(rootPath, (VaultIndex)index, factory);
}
return null;
@@ -1,5 +1,4 @@
using DeepDrftContent.Services.FileDatabase.Abstractions;
using DeepDrftContent.Services.FileDatabase.Models;
using DeepDrftContent.Services.FileDatabase.Models;
namespace DeepDrftContent.Services.FileDatabase.Services;
@@ -8,10 +7,13 @@ namespace DeepDrftContent.Services.FileDatabase.Services;
/// </summary>
public static class MediaVaultFactory
{
private static readonly IMediaTypeRegistry _registry = new SimpleMediaTypeRegistry();
public static async Task<MediaVault?> From(string rootPath, MediaVaultType mediaType)
public static async Task<MediaVault?> From(string rootPath, MediaVaultType mediaType, IndexFactoryService? factoryService = null)
{
return await _registry.CreateVaultAsync(mediaType, rootPath);
return mediaType switch
{
MediaVaultType.Image => await ImageVault.FromAsync(rootPath, factoryService),
MediaVaultType.Audio => await AudioVault.FromAsync(rootPath, factoryService),
_ => null
};
}
}
+3 -2
View File
@@ -75,9 +75,10 @@ public class TrackService
return trackEntity;
}
catch (Exception ex)
catch (Exception ex) when (ex is not OperationCanceledException)
{
throw new InvalidOperationException($"Failed to add track: {ex.Message}", ex);
Console.WriteLine($"TrackService.AddTrackFromWavAsync failed: {ex.Message}");
return null;
}
}
+2 -2
View File
@@ -4,8 +4,8 @@ public class TrackDto
{
public long Id { get; set; }
public required string EntryKey { get; set; }
public string TrackName { get; set; }
public string Artist { get; set; }
public required string TrackName { get; set; }
public required string Artist { get; set; }
public string? Album { get; set; }
public string? Genre { get; set; }
public DateOnly? ReleaseDate { get; set; }
+2 -2
View File
@@ -6,7 +6,7 @@ public class PagedResult<T>
public int TotalCount { get; set; }
public int Page { get; set; }
public int PageSize { get; set; }
public int TotalPages => (int)Math.Ceiling((double)TotalCount / PageSize);
public int TotalPages => PageSize > 0 ? (int)Math.Ceiling((double)TotalCount / PageSize) : 0;
public bool HasNextPage => Page < TotalPages;
public bool HasPreviousPage => Page > 1;
@@ -27,7 +27,7 @@ public class PagedResult<T>
public PagedResult(IEnumerable<T> items, int totalCount, int page, int pageSize)
{
Items = items.ToList() ?? new List<T>();
Items = items.ToList();
TotalCount = totalCount;
Page = page;
PageSize = pageSize;