From 6e0a6941f505f84318b192b284e7b8b1e25844e5 Mon Sep 17 00:00:00 2001 From: MrDevRobot Date: Sun, 6 Sep 2026 18:28:58 +0200 Subject: [PATCH] fix(filetransfer): register a default no-op IFileProvider AddEntglDbFileTransfer() unconditionally registers FileQueryHandler and FileDownloadHandler as INetworkMessageHandler, and both require IFileProvider in their constructor with no default. A node that only ever downloads files (never serves any) had to hand-write and register its own no-op IFileProvider just to satisfy that dependency - otherwise resolving IEnumerable during IEntglDbNode.Start() throws. That failure is especially dangerous because Start() is commonly invoked as a fire-and-forget task (`_ = node.Start()`) during app startup: the exception goes unobserved, and the node never finishes starting - no TCP listener, no logs, no crash, nothing to grep for. Added NullFileProvider, registered via TryAddSingleton inside AddEntglDbFileTransfer() itself. A download-only consumer now needs nothing further. A node that does want to serve files still registers its own provider afterward via plain AddSingleton; since FileQueryHandler/FileDownloadHandler depend on a single (non- enumerable) IFileProvider, the last registration wins. Co-Authored-By: Claude Sonnet 5 --- .../FileTransferExtensions.cs | 113 ++++++++++-------- .../NullFileProvider.cs | 22 ++++ 2 files changed, 84 insertions(+), 51 deletions(-) create mode 100644 src/EntglDb.Services.FileTransfer/NullFileProvider.cs diff --git a/src/EntglDb.Services.FileTransfer/FileTransferExtensions.cs b/src/EntglDb.Services.FileTransfer/FileTransferExtensions.cs index b66b7ff..a3e50a4 100644 --- a/src/EntglDb.Services.FileTransfer/FileTransferExtensions.cs +++ b/src/EntglDb.Services.FileTransfer/FileTransferExtensions.cs @@ -1,51 +1,62 @@ -using EntglDb.Network; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace EntglDb.Services.FileTransfer; - -/// -/// Extension methods for registering EntglDb file transfer services. -/// -public static class FileTransferExtensions -{ - /// - /// Adds the EntglDb file transfer service to the DI container. - /// - /// - /// - /// Registers: - /// - /// as (server-side, wire type 1100) - /// as (server-side streaming, wire type 1102) - /// / (client-side download API) - /// - /// - /// - /// Call AddEntglDbNetwork<T>() before this method so that - /// and are already registered. - /// - /// - /// To make files available for remote download, also register an : - /// - /// services.AddEntglDbFileTransfer(); - /// services.AddSingleton<IFileProvider, MyFileProvider>(); - /// - /// Without an the server-side handlers are registered but will - /// return not-found for every request. - /// - /// - public static IServiceCollection AddEntglDbFileTransfer(this IServiceCollection services) - { - // Server side — file query (single response) - services.AddSingleton(); - - // Server side — file download (streaming response) - services.AddSingleton(); - - // Client side - services.TryAddSingleton(); - - return services; - } -} +using EntglDb.Network; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace EntglDb.Services.FileTransfer; + +/// +/// Extension methods for registering EntglDb file transfer services. +/// +public static class FileTransferExtensions +{ + /// + /// Adds the EntglDb file transfer service to the DI container. + /// + /// + /// + /// Registers: + /// + /// as (server-side, wire type 1100) + /// as (server-side streaming, wire type 1102) + /// / (client-side download API) + /// / (server-side default - see below) + /// + /// + /// + /// Call AddEntglDbNetwork<T>() before this method so that + /// and are already registered. + /// + /// + /// A node that only ever downloads files (never serves any) needs nothing further - it uses the + /// registered default, reporting every file as not found. To make files + /// available for remote download instead, register a real provider after this call: + /// + /// services.AddEntglDbFileTransfer(); + /// services.AddSingleton<IFileProvider, MyFileProvider>(); + /// + /// / take a single (non-enumerable) + /// , so the last registration wins - the call above's own provider overrides + /// the default even though both end up registered. + /// + /// + public static IServiceCollection AddEntglDbFileTransfer(this IServiceCollection services) + { + // Server side — file query (single response) + services.AddSingleton(); + + // Server side — file download (streaming response) + services.AddSingleton(); + + // Client side + services.TryAddSingleton(); + + // Server-side default so a download-only consumer doesn't have to implement IFileProvider itself + // just to satisfy FileQueryHandler/FileDownloadHandler's constructor dependency - see remarks + // above. Must be TryAddSingleton (not AddSingleton) so a real provider the caller registers + // afterward via plain AddSingleton becomes the winning "last registration" for the single-instance + // resolution FileQueryHandler/FileDownloadHandler actually use. + services.TryAddSingleton(); + + return services; + } +} diff --git a/src/EntglDb.Services.FileTransfer/NullFileProvider.cs b/src/EntglDb.Services.FileTransfer/NullFileProvider.cs new file mode 100644 index 0000000..71daa04 --- /dev/null +++ b/src/EntglDb.Services.FileTransfer/NullFileProvider.cs @@ -0,0 +1,22 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace EntglDb.Services.FileTransfer; + +/// +/// Default registered by - +/// reports every file as not found. Lets a node that only ever downloads files (never serves any) use +/// AddEntglDbFileTransfer() without also having to implement and register an +/// of its own just to satisfy /'s constructor +/// dependency. A node that does want to serve files registers its own provider after this call; DI resolves +/// a single (non-enumerable) service to the last registration, so the real provider wins. +/// +public sealed class NullFileProvider : IFileProvider +{ + public Task GetInfoAsync(string fileId, CancellationToken ct = default) => + Task.FromResult(null); + + public Task OpenReadAsync(string fileId, CancellationToken ct = default) => + Task.FromResult(null); +}