From a954f1418f7bc54f6197e6769d07d7233db6fd4e Mon Sep 17 00:00:00 2001 From: NotCoffee418 <9306304+NotCoffee418@users.noreply.github.com> Date: Sat, 23 Mar 2024 16:50:49 +0100 Subject: [PATCH 1/2] BREAKING: Remove dependencies Objective is to not force needless third party dependencies. But a lot of functionality would break for applications using this library without DI at all. - Removing Autofac in favor of Microsoft DI since Autofac depends on it anyway. - Move Application.RunAsync to OperationManager --- CSharpScriptOperations.sln | 4 +- CSharpScriptOperations/Application.cs | 50 ------------ .../CSharpScriptOperations.csproj | 7 +- CSharpScriptOperations/OperationManager.cs | 81 +++++++++++++------ CSharpScriptOperations/Usings.cs | 5 +- DemoApp/DemoApp.csproj | 10 +-- DemoApp/Program.cs | 23 +++++- 7 files changed, 88 insertions(+), 92 deletions(-) delete mode 100644 CSharpScriptOperations/Application.cs diff --git a/CSharpScriptOperations.sln b/CSharpScriptOperations.sln index 18abc16..14d016d 100644 --- a/CSharpScriptOperations.sln +++ b/CSharpScriptOperations.sln @@ -1,11 +1,11 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31912.275 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CSharpScriptOperations", "CSharpScriptOperations\CSharpScriptOperations.csproj", "{8D54816D-2826-4287-AFF8-7722775207AC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DemoApp", "DemoApp\DemoApp.csproj", "{EF66B8B7-4344-458D-AECE-5537AD1D197F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DemoApp", "DemoApp\DemoApp.csproj", "{EF66B8B7-4344-458D-AECE-5537AD1D197F}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/CSharpScriptOperations/Application.cs b/CSharpScriptOperations/Application.cs deleted file mode 100644 index 73a2434..0000000 --- a/CSharpScriptOperations/Application.cs +++ /dev/null @@ -1,50 +0,0 @@ -namespace CSharpScriptOperations; - -internal class Application -{ - internal async Task RunAsync() - { - // List all operations - Console.Write(OperationManager.GetOperationsDisplay()); - - // Request user - while (true) // Breakout is calling Exit operation - { - Console.WriteLine(); // empty - Console.WriteLine("Select an operation ('help' for list of operations)"); - string userInput = Console.ReadLine(); - - // Handle "help" - if (userInput.ToLower() == "help") - { - Console.Write(OperationManager.GetOperationsDisplay()); - continue; - } - - // Parse input, report and repeat on invalid - int reqOperationId = -1; - if (!int.TryParse(userInput, out reqOperationId)) - { - Console.WriteLine("Input must be the numeric identifier of a registered operation. Try again."); - continue; - } - - // Handle invalid operation id - if (!OperationManager.OperationIdExists(reqOperationId)) - { - Console.WriteLine("Invalid operation number. Try again."); - continue; - } - - // Create instance - Type operationType = OperationManager.GetOperationTypeById(reqOperationId); - IOperation operationInstance = (IOperation)OperationManager.Container.Resolve(operationType); - - // Valid registered operation, report and run it run it. - Console.WriteLine(); - Console.WriteLine($"Running operation {reqOperationId}..."); - await operationInstance.RunAsync(); - Console.WriteLine(); - } - } -} diff --git a/CSharpScriptOperations/CSharpScriptOperations.csproj b/CSharpScriptOperations/CSharpScriptOperations.csproj index b4a3ea3..de36029 100644 --- a/CSharpScriptOperations/CSharpScriptOperations.csproj +++ b/CSharpScriptOperations/CSharpScriptOperations.csproj @@ -26,10 +26,9 @@ - - - - + + + diff --git a/CSharpScriptOperations/OperationManager.cs b/CSharpScriptOperations/OperationManager.cs index ae78591..f80097f 100644 --- a/CSharpScriptOperations/OperationManager.cs +++ b/CSharpScriptOperations/OperationManager.cs @@ -16,12 +16,10 @@ private static Dictionary _registeredOperations /// /// Use to register additional dependencies /// - public static ContainerBuilder ContainerBuilder = new ContainerBuilder(); + public static IServiceCollection Services = new ServiceCollection(); + + internal static IServiceProvider EffectiveServiceProvider = null; - /// - /// Built container for internal access - /// - internal static Autofac.IContainer Container = null; /// /// Read-only access to RegisteredOperations. @@ -52,7 +50,7 @@ public static void RegisterOperation(Type operation, int overrideIndex = 0) _registeredOperations.Add(newIndex, operation); // Register as dependency - ContainerBuilder.RegisterType(operation); + Services.AddTransient(operation); } /// @@ -70,7 +68,8 @@ public static void AutoRegisterOperations() // Get all applicable types List operationTypes = AppDomain.CurrentDomain.GetAssemblies() .SelectMany(x => x.GetTypes()) - .UniqueBy( x => x.FullName) + .GroupBy(x => x.FullName) + .Select(g => g.First()) .Where(t => !t.IsInterface && !t.IsAbstract && t.GetInterfaces().Any(t => t.FullName == typeof(IOperation).FullName)) // Don't include any internal operations .Where(a => !a.FullName.StartsWith(nameof(CSharpScriptOperations))) @@ -81,26 +80,61 @@ public static void AutoRegisterOperations() /// /// Starts the listener loop which displays the registered operations, - /// requests user input and runs the requested operation. + /// requests user input, and runs the requested operation. /// This will keep looping until the Exit Application operation is called. /// /// Register any dependencies before calling this function. /// - /// - public static async Task StartListeningAsync() + /// The IServiceProvider to use for resolving dependencies. + /// A Task representing the asynchronous operation. + public static async Task StartListeningAsync(IServiceProvider serviceProvider = null) { - // Register application dependencies - ContainerBuilder.RegisterType(); + // Use the provided serviceProvider, or fallback to the Services property. + EffectiveServiceProvider = serviceProvider + ?? Services.BuildServiceProvider(); + + // List all operations + Console.Write(OperationManager.GetOperationsDisplay()); + + // Request user + while (true) // Breakout is calling Exit operation + { + Console.WriteLine(); // empty + Console.WriteLine("Select an operation ('help' for list of operations)"); + string userInput = Console.ReadLine(); + + // Handle "help" + if (userInput.ToLower() == "help") + { + Console.Write(GetOperationsDisplay()); + continue; + } - // Prevents IServiceProvider related issues - // See: https://stackoverflow.com/questions/61779868/injecting-iserviceprovider-into-factory-class-with-autofac - ContainerBuilder.Populate(Enumerable.Empty()); + // Parse input, report and repeat on invalid + int reqOperationId = -1; + if (!int.TryParse(userInput, out reqOperationId)) + { + Console.WriteLine("Input must be the numeric identifier of a registered operation. Try again."); + continue; + } - // Create container with all dependencies - Container = ContainerBuilder.Build(); + // Handle invalid operation id + if (!OperationIdExists(reqOperationId)) + { + Console.WriteLine("Invalid operation number. Try again."); + continue; + } - // Start listening in container - await Container.Resolve().RunAsync(); + // Create instance + Type operationType = GetOperationTypeById(reqOperationId); + IOperation operationInstance = (IOperation)EffectiveServiceProvider.GetService(operationType); + + // Valid registered operation, report and run it run it. + Console.WriteLine(); + Console.WriteLine($"Running operation {reqOperationId}..."); + await operationInstance.RunAsync(); + Console.WriteLine(); + } } /// @@ -110,20 +144,18 @@ public static async Task StartListeningAsync() public static string GetOperationsDisplay() { // Handle incorrect usage - if (Container is null) + if (EffectiveServiceProvider is null) return "Cannot call GetOperationsDisplay() manually before calling StartListeningAsync(), which will print it automatically."; /// Display operations string result = "Available Operations: " + Environment.NewLine; foreach (var opKvp in _registeredOperations) { - - /// Find the description // Default string description = "Add an [OperationDescription(\"goes here\")] attribute to the operation class."; // Try via description attribute - OperationDescriptionAttribute? descAttr = opKvp.Value.GetCustomAttributes(typeof(OperationDescriptionAttribute), false) + OperationDescriptionAttribute descAttr = opKvp.Value.GetCustomAttributes(typeof(OperationDescriptionAttribute), false) .FirstOrDefault() as OperationDescriptionAttribute; if (descAttr is not null) // via attribute description = descAttr.Description; @@ -134,7 +166,7 @@ public static string GetOperationsDisplay() IOperation operationInstance = null; try { - operationInstance = (IOperation)Container.Resolve(opKvp.Value); + operationInstance = (IOperation)EffectiveServiceProvider.GetService(opKvp.Value); description = opKvp.Value.GetProperty("Description").GetValue(operationInstance) as string; } catch {/* Use default description */} @@ -162,4 +194,5 @@ public static Type GetOperationTypeById(int operationId) => OperationIdExists(operationId) ? _registeredOperations[operationId] : throw new ArgumentException("GetOperationTypeById failed because the input id has no registered operation"); + } diff --git a/CSharpScriptOperations/Usings.cs b/CSharpScriptOperations/Usings.cs index f33bc6f..a5f35dc 100644 --- a/CSharpScriptOperations/Usings.cs +++ b/CSharpScriptOperations/Usings.cs @@ -4,9 +4,6 @@ global using System.Reflection; global using System.Linq; global using System.Threading.Tasks; -global using Autofac; -global using Autofac.Extensions.DependencyInjection; global using CSharpScriptOperations.InteralOperations; global using System.Collections.ObjectModel; -global using Microsoft.Extensions.DependencyInjection; -global using CoffeeToolkit.Linq; \ No newline at end of file +global using Microsoft.Extensions.DependencyInjection; \ No newline at end of file diff --git a/DemoApp/DemoApp.csproj b/DemoApp/DemoApp.csproj index 0e5c84f..bcd293f 100644 --- a/DemoApp/DemoApp.csproj +++ b/DemoApp/DemoApp.csproj @@ -1,18 +1,16 @@ - Exe - net6.0 + net8.0 enable enable - + + - - - + \ No newline at end of file diff --git a/DemoApp/Program.cs b/DemoApp/Program.cs index 26f7052..f1b288a 100644 --- a/DemoApp/Program.cs +++ b/DemoApp/Program.cs @@ -1,9 +1,11 @@ using Autofac; // Import Autofac if you want DI +using Autofac.Extensions.DependencyInjection; using CSharpScriptOperations; using DemoApp.Logic; using DemoApp.Operations; +/* --- REGISTER OPERATIONS --- */ // You can automatically register all operations OperationManager.AutoRegisterOperations(); @@ -22,14 +24,31 @@ // } //); + +/* --- OPTIONAL: REGISTER DEPENDENCIES --- */ // Optionally register any custom dependencies through "OperationManager.ContainerBuilder" if needed -OperationManager.ContainerBuilder +// This example uses Autofac to register our operations. You need the following nuget dependencies: +// - Autofac +// - Autofac.Extensions.DependencyInjection +ContainerBuilder AutofacContainerBuilder = new ContainerBuilder(); + +// Include application dependencies +AutofacContainerBuilder .RegisterType() .As(); +// Include the services registered by the OperationManager +AutofacContainerBuilder.Populate(OperationManager.Services); + +// Build the container +var serviceProvider = new AutofacServiceProvider(AutofacContainerBuilder.Build()); + + + +/* --- START LISTENING --- */ // Start the listener loop // This will display our options and interpret user input to run the approperiate operation -await OperationManager.StartListeningAsync(); +await OperationManager.StartListeningAsync(serviceProvider); // optional service provider // Alternatively, you can implement your own approach // using the OperationManager.RegisteredOperations object \ No newline at end of file From a246feed263d80ee52f5020016cdb749c22f9048 Mon Sep 17 00:00:00 2001 From: NotCoffee418 <9306304+NotCoffee418@users.noreply.github.com> Date: Sat, 23 Mar 2024 17:12:13 +0100 Subject: [PATCH 2/2] Change README for DI --- CSharpScriptOperations/OperationManager.cs | 2 +- README.md | 18 ++++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/CSharpScriptOperations/OperationManager.cs b/CSharpScriptOperations/OperationManager.cs index f80097f..b62fc3e 100644 --- a/CSharpScriptOperations/OperationManager.cs +++ b/CSharpScriptOperations/OperationManager.cs @@ -94,7 +94,7 @@ public static async Task StartListeningAsync(IServiceProvider serviceProvider = ?? Services.BuildServiceProvider(); // List all operations - Console.Write(OperationManager.GetOperationsDisplay()); + Console.Write(GetOperationsDisplay()); // Request user while (true) // Breakout is calling Exit operation diff --git a/README.md b/README.md index ac73d3e..1c9376f 100644 --- a/README.md +++ b/README.md @@ -92,22 +92,32 @@ OperationManager.RegisterOperation(typeof(HelloWorld)); ``` ### 4. Register dependencies (optional) -If you'd like to use dependency injection with autofac, make sure to install the [autofac nuget package](https://www.nuget.org/packages/Autofac) and add the using statement. +You can optionally use dependency injection with Autofac or Microsoft Dependency Injection. + -```csharp -using Autofac; -``` Register any dependencies to `OperationManager.ContainerBuilder` before starting the listener. ```csharp +ContainerBuilder AutofacContainerBuilder = new ContainerBuilder(); + +// Include application dependencies OperationManager.ContainerBuilder .RegisterType() .As(); + +// Include the services registered by the OperationManager +AutofacContainerBuilder.Populate(OperationManager.Services); + +// Build the container +var serviceProvider = new AutofacServiceProvider(AutofacContainerBuilder.Build()); ``` ### 5. Start the listener This will display our options and interpret user input to run the approperiate operation. ```csharp await OperationManager.StartListeningAsync(); + +// or if you use dependency injection +await OperationManager.StartListeningAsync(serviceProvider); ``` Alternatively you can implement your own version of `StartListening()`. You can access the registered operations and it's classes through the