From b3755f4ceb9e0e1f6af2ee305776a6bb481f0af1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81d=C3=A1m=20Kov=C3=A1cs?= Date: Sat, 28 Jan 2023 14:34:23 +0100 Subject: [PATCH] Delete command --- ...emManipulationUserCommandHandlerService.cs | 17 ++--- .../DependencyInjection.cs | 4 +- .../ContentAccess/IContentAccessorFactory.cs | 2 + .../ContentAccess/IItemDeleter.cs | 13 ++++ .../FileTime.Core.Abstraction/Models/IItem.cs | 3 +- .../Delete/DeleteCommand.cs | 69 +++++++++++++++-- .../Delete/DeleteStrategy.cs | 12 +++ .../Delete/IDeleteStrategy.cs | 9 +++ .../ContentAccessorFactory.cs | 14 ++++ .../Configuration/MainConfiguration.cs | 74 +++++++++---------- .../ViewModels/MessageBoxViewModel.cs | 2 +- .../LocalItemDeleter.cs | 35 +++++++++ .../FileTime.Providers.Local/Startup.cs | 2 + 13 files changed, 200 insertions(+), 56 deletions(-) create mode 100644 src/Core/FileTime.Core.Abstraction/ContentAccess/IItemDeleter.cs create mode 100644 src/Core/FileTime.Core.Command/Delete/DeleteStrategy.cs create mode 100644 src/Core/FileTime.Core.Command/Delete/IDeleteStrategy.cs create mode 100644 src/Providers/FileTime.Providers.Local/LocalItemDeleter.cs diff --git a/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/ItemManipulationUserCommandHandlerService.cs b/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/ItemManipulationUserCommandHandlerService.cs index e6f58fa..b8c035d 100644 --- a/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/ItemManipulationUserCommandHandlerService.cs +++ b/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/ItemManipulationUserCommandHandlerService.cs @@ -90,9 +90,10 @@ public class ItemManipulationUserCommandHandlerService : UserCommandHandlerServi else if (_currentSelectedItem?.BaseItem != null) { var item = _currentSelectedItem.BaseItem; - _clipboardService.AddContent(item.FullName ?? - throw new ArgumentException($"{nameof(item.FullName)} can not be null.", - nameof(item))); + _clipboardService.AddContent( + item.FullName + ?? throw new ArgumentException($"{nameof(item.FullName)} can not be null.", nameof(item)) + ); } return Task.CompletedTask; @@ -233,14 +234,12 @@ public class ItemManipulationUserCommandHandlerService : UserCommandHandlerServi return; } - var deleteCommand = new FileTime.Core.Command.Delete.DeleteCommand() - { - HardDelete = command.IsHardDelete - }; - + + var deleteCommand = _serviceProvider.GetRequiredService(); + deleteCommand.HardDelete = command.IsHardDelete; deleteCommand.ItemsToDelete.AddRange(itemsToDelete!); await AddCommand(deleteCommand); - + _selectedTab?.ClearMarkedItems(); } diff --git a/src/AppCommon/FileTime.App.DependencyInjection/DependencyInjection.cs b/src/AppCommon/FileTime.App.DependencyInjection/DependencyInjection.cs index 11712e8..87387c7 100644 --- a/src/AppCommon/FileTime.App.DependencyInjection/DependencyInjection.cs +++ b/src/AppCommon/FileTime.App.DependencyInjection/DependencyInjection.cs @@ -6,6 +6,7 @@ using FileTime.Core.Command; using FileTime.Core.Command.Copy; using FileTime.Core.Command.CreateContainer; using FileTime.Core.Command.CreateElement; +using FileTime.Core.Command.Delete; using FileTime.Core.CommandHandlers; using FileTime.Core.ContentAccess; using FileTime.Core.Services; @@ -49,6 +50,7 @@ public static class DependencyInjection return serviceCollection .AddTransient() .AddTransient() - .AddTransient(); + .AddTransient() + .AddTransient(); } } \ No newline at end of file diff --git a/src/Core/FileTime.Core.Abstraction/ContentAccess/IContentAccessorFactory.cs b/src/Core/FileTime.Core.Abstraction/ContentAccess/IContentAccessorFactory.cs index be6ba41..8653c35 100644 --- a/src/Core/FileTime.Core.Abstraction/ContentAccess/IContentAccessorFactory.cs +++ b/src/Core/FileTime.Core.Abstraction/ContentAccess/IContentAccessorFactory.cs @@ -8,4 +8,6 @@ public interface IContentAccessorFactory IContentReaderFactory GetContentReaderFactory(IContentProvider provider); IContentWriterFactory GetContentWriterFactory() where TContentProvider : IContentProvider; IContentWriterFactory GetContentWriterFactory(IContentProvider provider); + IItemDeleter GetItemDeleter(IContentProvider provider); + IItemDeleter GetItemDeleter() where TContentProvider : IContentProvider; } \ No newline at end of file diff --git a/src/Core/FileTime.Core.Abstraction/ContentAccess/IItemDeleter.cs b/src/Core/FileTime.Core.Abstraction/ContentAccess/IItemDeleter.cs new file mode 100644 index 0000000..c244838 --- /dev/null +++ b/src/Core/FileTime.Core.Abstraction/ContentAccess/IItemDeleter.cs @@ -0,0 +1,13 @@ +using FileTime.Core.Models; + +namespace FileTime.Core.ContentAccess; + +public interface IItemDeleter +{ + Task DeleteAsync(IContentProvider contentProvider, FullName fullName); +} + +public interface IItemDeleter : IItemDeleter where TContentProvider : IContentProvider +{ + Task DeleteAsync(TContentProvider contentProvider, FullName fullName); +} \ No newline at end of file diff --git a/src/Core/FileTime.Core.Abstraction/Models/IItem.cs b/src/Core/FileTime.Core.Abstraction/Models/IItem.cs index 32b19bf..3bff277 100644 --- a/src/Core/FileTime.Core.Abstraction/Models/IItem.cs +++ b/src/Core/FileTime.Core.Abstraction/Models/IItem.cs @@ -1,7 +1,6 @@ using System.Reactive.Linq; using FileTime.Core.ContentAccess; using FileTime.Core.Enums; -using FileTime.Core.Services; using FileTime.Core.Timeline; namespace FileTime.Core.Models; @@ -26,4 +25,4 @@ public interface IItem ReadOnlyExtensionCollection Extensions { get; } T? GetExtension() => (T?)Extensions.FirstOrDefault(i => i is T); -} \ No newline at end of file +} diff --git a/src/Core/FileTime.Core.Command/Delete/DeleteCommand.cs b/src/Core/FileTime.Core.Command/Delete/DeleteCommand.cs index ca1e8f2..d20572e 100644 --- a/src/Core/FileTime.Core.Command/Delete/DeleteCommand.cs +++ b/src/Core/FileTime.Core.Command/Delete/DeleteCommand.cs @@ -1,3 +1,5 @@ +using FileTime.Core.ContentAccess; +using FileTime.Core.Extensions; using FileTime.Core.Models; using FileTime.Core.Timeline; @@ -5,21 +7,76 @@ namespace FileTime.Core.Command.Delete; public class DeleteCommand : IExecutableCommand { - public bool HardDelete { get; init; } - public List ItemsToDelete { get; } = new List(); + private readonly IContentAccessorFactory _contentAccessorFactory; + private readonly ITimelessContentProvider _timelessContentProvider; + public bool HardDelete { get; set; } + public List ItemsToDelete { get; } = new(); + + public DeleteCommand( + IContentAccessorFactory contentAccessorFactory, + ITimelessContentProvider timelessContentProvider) + { + _contentAccessorFactory = contentAccessorFactory; + _timelessContentProvider = timelessContentProvider; + } public Task CanRun(PointInTime currentTime) { - throw new NotImplementedException(); + //TODO + return Task.FromResult(CanCommandRun.True); } public Task SimulateCommand(PointInTime currentTime) { - throw new NotImplementedException(); + //TODO + return Task.FromResult(currentTime); } - public Task Execute() + public async Task Execute() { - throw new NotImplementedException(); + //Calculate + + //Delete + await TraverseTree( + PointInTime.Present, + ItemsToDelete, + new Dictionary(), + new DeleteStrategy() + ); + } + + private async Task TraverseTree( + PointInTime currentTime, + IEnumerable itemsToDelete, + Dictionary itemDeleters, + IDeleteStrategy deleteStrategy) + { + foreach (var itemToDeleteName in itemsToDelete) + { + var itemToDelete = await _timelessContentProvider.GetItemByFullNameAsync(itemToDeleteName, currentTime); + IItemDeleter itemDeleter; + + if (itemDeleters.ContainsKey(itemToDelete.Provider.Name)) + { + itemDeleter = itemDeleters[itemToDelete.Provider.Name]; + } + else + { + itemDeleter = _contentAccessorFactory.GetItemDeleter(itemToDelete.Provider); + itemDeleters.Add(itemToDelete.Provider.Name, itemDeleter); + } + + if (itemToDelete is IContainer container) + { + await TraverseTree( + currentTime, + (await container.Items.GetItemsAsync())?.Select(i => i.Path) ?? Enumerable.Empty(), + itemDeleters, + deleteStrategy + ); + } + + await itemDeleter.DeleteAsync(itemToDelete.Provider, itemToDelete.FullName!); + } } } \ No newline at end of file diff --git a/src/Core/FileTime.Core.Command/Delete/DeleteStrategy.cs b/src/Core/FileTime.Core.Command/Delete/DeleteStrategy.cs new file mode 100644 index 0000000..3fe1897 --- /dev/null +++ b/src/Core/FileTime.Core.Command/Delete/DeleteStrategy.cs @@ -0,0 +1,12 @@ +using FileTime.Core.ContentAccess; +using FileTime.Core.Models; + +namespace FileTime.Core.Command.Delete; + +public class DeleteStrategy : IDeleteStrategy +{ + public async Task DeleteItem(IItem item, IItemDeleter deleter) + { + await deleter.DeleteAsync(item.Provider, item.FullName!); + } +} \ No newline at end of file diff --git a/src/Core/FileTime.Core.Command/Delete/IDeleteStrategy.cs b/src/Core/FileTime.Core.Command/Delete/IDeleteStrategy.cs new file mode 100644 index 0000000..77ab53f --- /dev/null +++ b/src/Core/FileTime.Core.Command/Delete/IDeleteStrategy.cs @@ -0,0 +1,9 @@ +using FileTime.Core.ContentAccess; +using FileTime.Core.Models; + +namespace FileTime.Core.Command.Delete; + +public interface IDeleteStrategy +{ + Task DeleteItem(IItem item, IItemDeleter deleter); +} \ No newline at end of file diff --git a/src/Core/FileTime.Core.ContentAccess/ContentAccessorFactory.cs b/src/Core/FileTime.Core.ContentAccess/ContentAccessorFactory.cs index b3c4aca..87145bc 100644 --- a/src/Core/FileTime.Core.ContentAccess/ContentAccessorFactory.cs +++ b/src/Core/FileTime.Core.ContentAccess/ContentAccessorFactory.cs @@ -52,4 +52,18 @@ public class ContentAccessorFactory : IContentAccessorFactory return (IContentWriterFactory)_serviceProvider.GetRequiredService(genericType); } + + public IItemDeleter GetItemDeleter() where TContentProvider : IContentProvider + { + var genericType = typeof(IItemDeleter<>).MakeGenericType(typeof(TContentProvider)); + + return (IItemDeleter)_serviceProvider.GetRequiredService(genericType); + } + + public IItemDeleter GetItemDeleter(IContentProvider provider) + { + var genericType = typeof(IItemDeleter<>).MakeGenericType(provider.GetType()); + + return (IItemDeleter)_serviceProvider.GetRequiredService(genericType); + } } \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Configuration/MainConfiguration.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Configuration/MainConfiguration.cs index b99a206..9d4236a 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Configuration/MainConfiguration.cs +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Configuration/MainConfiguration.cs @@ -45,64 +45,64 @@ public static class MainConfiguration { //new CommandBindingConfiguration(ConfigCommand.AutoRefresh, new KeyConfig(Key.R, shift: true)), //new CommandBindingConfiguration(ConfigCommand.ChangeTimelineMode, new[] { Key.T, Key.M }), - new CommandBindingConfiguration(CloseTabCommand.CommandName, Key.Q), + new(CloseTabCommand.CommandName, Key.Q), //new CommandBindingConfiguration(ConfigCommand.Compress, new[] { Key.Y, Key.C }), - new CommandBindingConfiguration(CopyCommand.CommandName, new[] { Key.Y, Key.Y }), + new(CopyCommand.CommandName, new[] { Key.Y, Key.Y }), //new CommandBindingConfiguration(ConfigCommand.CopyHash, new[] { Key.C, Key.H }), - new CommandBindingConfiguration(CopyNativePathCommand.CommandName, new[] { Key.C, Key.P }), - new CommandBindingConfiguration(CreateContainer.CommandName, Key.F7), - new CommandBindingConfiguration(CreateContainer.CommandName, new[] { Key.C, Key.C }), - new CommandBindingConfiguration(CreateElement.CommandName, new[] { Key.C, Key.E }), + new(CopyNativePathCommand.CommandName, new[] { Key.C, Key.P }), + new(CreateContainer.CommandName, Key.F7), + new(CreateContainer.CommandName, new[] { Key.C, Key.C }), + new(CreateElement.CommandName, new[] { Key.C, Key.E }), //new CommandBindingConfiguration(ConfigCommand.Cut, new[] { Key.D, Key.D }), //new CommandBindingConfiguration(ConfigCommand.Edit, new KeyConfig(Key.F4)), - new CommandBindingConfiguration(EnterRapidTravelCommand.CommandName,new KeyConfig(Key.OemComma, shift: true)), + new(EnterRapidTravelCommand.CommandName,new KeyConfig(Key.OemComma, shift: true)), //new CommandBindingConfiguration(ConfigCommand.FindByName, new[] { Key.F, Key.N }), //new CommandBindingConfiguration(ConfigCommand.FindByNameRegex, new[] { Key.F, Key.R }), - new CommandBindingConfiguration(GoToHomeCommand.CommandName, new[] { Key.G, Key.H }), - new CommandBindingConfiguration(GoToPathCommand.CommandName, new KeyConfig(Key.L, ctrl: true)), - new CommandBindingConfiguration(GoToPathCommand.CommandName, new[] { Key.G, Key.P }), - new CommandBindingConfiguration(GoToProviderCommand.CommandName, new[] { Key.G, Key.T }), - new CommandBindingConfiguration(GoToRootCommand.CommandName, new[] { Key.G, Key.R }), - new CommandBindingConfiguration(DeleteCommand.HardDeleteCommandName, new[] { new KeyConfig(Key.D,shift: true), new KeyConfig(Key.D, shift: true) }), - new CommandBindingConfiguration(MarkCommand.CommandName, Key.Space), - new CommandBindingConfiguration(MoveCursorToLastCommand.CommandName, new KeyConfig(Key.G, shift: true)), - new CommandBindingConfiguration(MoveCursorToFirstCommand.CommandName, new[] { Key.G, Key.G }), + new(GoToHomeCommand.CommandName, new[] { Key.G, Key.H }), + new(GoToPathCommand.CommandName, new KeyConfig(Key.L, ctrl: true)), + new(GoToPathCommand.CommandName, new[] { Key.G, Key.P }), + new(GoToProviderCommand.CommandName, new[] { Key.G, Key.T }), + new(GoToRootCommand.CommandName, new[] { Key.G, Key.R }), + new(DeleteCommand.HardDeleteCommandName, new[] { new KeyConfig(Key.D,shift: true), new KeyConfig(Key.D, shift: true) }), + new(MarkCommand.CommandName, Key.Space), + new(MoveCursorToLastCommand.CommandName, new KeyConfig(Key.G, shift: true)), + new(MoveCursorToFirstCommand.CommandName, new[] { Key.G, Key.G }), //new CommandBindingConfiguration(ConfigCommand.NextTimelineBlock, Key.L ), //new CommandBindingConfiguration(ConfigCommand.NextTimelineCommand, Key.J ), - new CommandBindingConfiguration(OpenInDefaultFileExplorerCommand.CommandName, new[] { Key.O, Key.E }), - new CommandBindingConfiguration(PasteCommand.PasteMergeCommandName, new[] { Key.P, Key.P }), - new CommandBindingConfiguration(PasteCommand.PasteOverwriteCommandName, new[] { Key.P, Key.O }), - new CommandBindingConfiguration(PasteCommand.PasteSkipCommandName, new[] { Key.P, Key.S }), + new(OpenInDefaultFileExplorerCommand.CommandName, new[] { Key.O, Key.E }), + new(PasteCommand.PasteMergeCommandName, new[] { Key.P, Key.P }), + new(PasteCommand.PasteOverwriteCommandName, new[] { Key.P, Key.O }), + new(PasteCommand.PasteSkipCommandName, new[] { Key.P, Key.S }), //new CommandBindingConfiguration(ConfigCommand.PinFavorite, new[] { Key.F, Key.P }), //new CommandBindingConfiguration(ConfigCommand.PreviousTimelineBlock, Key.H ), //new CommandBindingConfiguration(ConfigCommand.PreviousTimelineCommand, Key.K ), - new CommandBindingConfiguration(RefreshCommand.CommandName, Key.R), + new(RefreshCommand.CommandName, Key.R), //new CommandBindingConfiguration(ConfigCommand.Rename, Key.F2), //new CommandBindingConfiguration(ConfigCommand.Rename, new[] { Key.C, Key.W }), //new CommandBindingConfiguration(ConfigCommand.RunCommand, new KeyConfig(Key.D4, shift: true)), //new CommandBindingConfiguration(ConfigCommand.ScanContainerSize, new[] { Key.C, Key.S }), //new CommandBindingConfiguration(ConfigCommand.ShowAllShortcut, Key.F1), - new CommandBindingConfiguration(DeleteCommand.SoftDeleteCommandName, new[] { new KeyConfig(Key.D), new KeyConfig(Key.D, shift: true) }), - new CommandBindingConfiguration(SwitchToTabCommand.SwitchToLastTabCommandName, Key.D9), - new CommandBindingConfiguration(SwitchToTabCommand.SwitchToTab1CommandName, Key.D1), - new CommandBindingConfiguration(SwitchToTabCommand.SwitchToTab2CommandName, Key.D2), - new CommandBindingConfiguration(SwitchToTabCommand.SwitchToTab3CommandName, Key.D3), - new CommandBindingConfiguration(SwitchToTabCommand.SwitchToTab4CommandName, Key.D4), - new CommandBindingConfiguration(SwitchToTabCommand.SwitchToTab5CommandName, Key.D5), - new CommandBindingConfiguration(SwitchToTabCommand.SwitchToTab6CommandName, Key.D6), - new CommandBindingConfiguration(SwitchToTabCommand.SwitchToTab7CommandName, Key.D7), - new CommandBindingConfiguration(SwitchToTabCommand.SwitchToTab8CommandName, Key.D8), + new(DeleteCommand.SoftDeleteCommandName, new[] { new KeyConfig(Key.D), new KeyConfig(Key.D, shift: true) }), + new(SwitchToTabCommand.SwitchToLastTabCommandName, Key.D9), + new(SwitchToTabCommand.SwitchToTab1CommandName, Key.D1), + new(SwitchToTabCommand.SwitchToTab2CommandName, Key.D2), + new(SwitchToTabCommand.SwitchToTab3CommandName, Key.D3), + new(SwitchToTabCommand.SwitchToTab4CommandName, Key.D4), + new(SwitchToTabCommand.SwitchToTab5CommandName, Key.D5), + new(SwitchToTabCommand.SwitchToTab6CommandName, Key.D6), + new(SwitchToTabCommand.SwitchToTab7CommandName, Key.D7), + new(SwitchToTabCommand.SwitchToTab8CommandName, Key.D8), //new CommandBindingConfiguration(ConfigCommand.TimelinePause, new[] { Key.T, Key.P }), //new CommandBindingConfiguration(ConfigCommand.TimelineRefresh, new[] { Key.T, Key.R }), //new CommandBindingConfiguration(ConfigCommand.TimelineStart, new[] { Key.T, Key.S }), //new CommandBindingConfiguration(ConfigCommand.ToggleAdvancedIcons, new[] { Key.Z, Key.I }), - new CommandBindingConfiguration(GoUpCommand.CommandName, Key.Left), - new CommandBindingConfiguration(OpenSelectedCommand.CommandName, Key.Right), + new(GoUpCommand.CommandName, Key.Left), + new(OpenSelectedCommand.CommandName, Key.Right), //new CommandBindingConfiguration(ConfigCommand.OpenOrRun, Key.Enter), - new CommandBindingConfiguration(MoveCursorUpCommand.CommandName, Key.Up), - new CommandBindingConfiguration(MoveCursorDownCommand.CommandName, Key.Down), - new CommandBindingConfiguration(MoveCursorUpPageCommand.CommandName, Key.PageUp), - new CommandBindingConfiguration(MoveCursorDownPageCommand.CommandName, Key.PageDown), + new(MoveCursorUpCommand.CommandName, Key.Up), + new(MoveCursorDownCommand.CommandName, Key.Down), + new(MoveCursorUpPageCommand.CommandName, Key.PageUp), + new(MoveCursorDownPageCommand.CommandName, Key.PageDown), }; } diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/ViewModels/MessageBoxViewModel.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/ViewModels/MessageBoxViewModel.cs index a718d5a..9d928ad 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/ViewModels/MessageBoxViewModel.cs +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/ViewModels/MessageBoxViewModel.cs @@ -11,7 +11,7 @@ public partial class MessageBoxViewModel : IModalViewModel public string Text { get; } public string Name => "MessageBoxViewModel"; - public MessageBoxViewModel(string text, Action handler) + public MessageBoxViewModel(string text, Action handler) : this() { _handler = handler; Text = text; diff --git a/src/Providers/FileTime.Providers.Local/LocalItemDeleter.cs b/src/Providers/FileTime.Providers.Local/LocalItemDeleter.cs new file mode 100644 index 0000000..ad9d893 --- /dev/null +++ b/src/Providers/FileTime.Providers.Local/LocalItemDeleter.cs @@ -0,0 +1,35 @@ +using FileTime.Core.ContentAccess; +using FileTime.Core.Models; + +namespace FileTime.Providers.Local; + +public class LocalItemDeleter : IItemDeleter +{ + public Task DeleteAsync(ILocalContentProvider contentProvider, FullName fullName) + { + var nativePath = contentProvider.GetNativePath(fullName).Path; + + if (File.Exists(nativePath)) + { + File.Delete(nativePath); + } + else if (Directory.Exists(nativePath)) + { + Directory.Delete(nativePath, true); + } + else + { + throw new FileNotFoundException(nativePath); + } + + return Task.CompletedTask; + } + + public async Task DeleteAsync(IContentProvider contentProvider, FullName fullName) + { + var localContentProvider = contentProvider as ILocalContentProvider; + if (localContentProvider is null) throw new ArgumentException("Content provider is not a local content provider", nameof(contentProvider)); + + await DeleteAsync(localContentProvider, fullName); + } +} \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.Local/Startup.cs b/src/Providers/FileTime.Providers.Local/Startup.cs index 8270427..a973066 100644 --- a/src/Providers/FileTime.Providers.Local/Startup.cs +++ b/src/Providers/FileTime.Providers.Local/Startup.cs @@ -12,6 +12,8 @@ public static class Startup serviceCollection.TryAddSingleton(sp => sp.GetRequiredService()); serviceCollection.TryAddSingleton, LocalItemCreator>(); serviceCollection.TryAddSingleton>(sp => sp.GetRequiredService>()); + serviceCollection.TryAddSingleton, LocalItemDeleter>(); + serviceCollection.TryAddSingleton>(sp => sp.GetRequiredService>()); serviceCollection.TryAddSingleton, LocalContentReaderFactory>(); serviceCollection.TryAddSingleton>(sp => sp.GetRequiredService>()); serviceCollection.TryAddSingleton, LocalContentWriterFactory>();