From 6270d50405656ace34141c82c34063d86dd0a121 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 23 Apr 2025 20:08:13 +0200 Subject: [PATCH 1/2] Added marker interfaces and extension methods to register them --- Zooper.Bee.Example/BranchingExample.cs | 2 +- .../WorkflowActivitiesExtensions.cs | 194 ++++++++++++++++++ Zooper.Bee/Extensions/WorkflowExtensions.cs | 64 ++++++ .../WorkflowValidationExtensions.cs | 60 ++++++ Zooper.Bee/Features/IWorkflowFeature.cs | 2 +- Zooper.Bee/Interfaces/IWorkflowActivities.cs | 13 ++ Zooper.Bee/Interfaces/IWorkflowActivity.cs | 26 +++ Zooper.Bee/Interfaces/IWorkflowValidation.cs | 28 +++ Zooper.Bee/Interfaces/IWorkflowValidations.cs | 13 ++ Zooper.Bee/Zooper.Bee.csproj | 1 + 10 files changed, 401 insertions(+), 2 deletions(-) create mode 100644 Zooper.Bee/Extensions/WorkflowActivitiesExtensions.cs create mode 100644 Zooper.Bee/Extensions/WorkflowExtensions.cs create mode 100644 Zooper.Bee/Extensions/WorkflowValidationExtensions.cs create mode 100644 Zooper.Bee/Interfaces/IWorkflowActivities.cs create mode 100644 Zooper.Bee/Interfaces/IWorkflowActivity.cs create mode 100644 Zooper.Bee/Interfaces/IWorkflowValidation.cs create mode 100644 Zooper.Bee/Interfaces/IWorkflowValidations.cs diff --git a/Zooper.Bee.Example/BranchingExample.cs b/Zooper.Bee.Example/BranchingExample.cs index be76edc..321f7e7 100644 --- a/Zooper.Bee.Example/BranchingExample.cs +++ b/Zooper.Bee.Example/BranchingExample.cs @@ -92,7 +92,7 @@ private static Workflow new RegistrationSuccess( payload.UserId, payload.Email, diff --git a/Zooper.Bee/Extensions/WorkflowActivitiesExtensions.cs b/Zooper.Bee/Extensions/WorkflowActivitiesExtensions.cs new file mode 100644 index 0000000..e1cfa63 --- /dev/null +++ b/Zooper.Bee/Extensions/WorkflowActivitiesExtensions.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using Zooper.Bee.Interfaces; + +// ReSharper disable MemberCanBePrivate.Global + +namespace Zooper.Bee.Extensions; + +/// +/// Extension methods for registering workflow activities with dependency injection +/// +public static class WorkflowActivitiesExtensions +{ + /// + /// Adds all workflow activities from the specified assemblies, or from all loaded assemblies if none specified + /// + /// The service collection + /// Optional list of assemblies to scan. If null or empty, scans all loaded assemblies + /// The service lifetime (defaults to Scoped) + /// The service collection for chaining + public static IServiceCollection AddWorkflowActivities( + this IServiceCollection services, + IEnumerable? assemblies = null, + ServiceLifetime lifetime = ServiceLifetime.Scoped) + { + // If no assemblies are specified, use all loaded assemblies + assemblies ??= AppDomain.CurrentDomain.GetAssemblies() + .Where(a => !a.IsDynamic && !a.FullName.StartsWith("System") && !a.FullName.StartsWith("Microsoft")); + + // Register all IWorkflowActivity implementations + services.Scan(scan => scan + .FromAssemblies(assemblies) + .AddClasses(classes => classes.AssignableTo(typeof(IWorkflowActivity))) + .AsImplementedInterfaces() + .WithLifetime(lifetime) + ); + + // Register all IWorkflowActivities implementations + services.Scan(scan => scan + .FromAssemblies(assemblies) + .AddClasses(classes => classes.AssignableTo(typeof(IWorkflowActivities))) + .AsImplementedInterfaces() + .WithLifetime(lifetime) + ); + + return services; + } + + /// + /// Adds all workflow activities from the assemblies containing the specified marker types + /// + /// The service collection + /// Types whose assemblies will be scanned + /// The service lifetime (defaults to Scoped) + /// The service collection for chaining + public static IServiceCollection AddWorkflowActivitiesFromAssembliesContaining( + this IServiceCollection services, + IEnumerable markerTypes, + ServiceLifetime lifetime = ServiceLifetime.Scoped) + { + if (markerTypes == null) throw new ArgumentNullException(nameof(markerTypes)); + + var assemblies = markerTypes.Select(t => t.Assembly).Distinct(); + return services.AddWorkflowActivities(assemblies, lifetime); + } + + /// + /// Adds all workflow activities from the assemblies containing the specified marker types + /// + /// The service collection + /// The service lifetime (defaults to Scoped) + /// Types whose assemblies will be scanned + /// The service collection for chaining + public static IServiceCollection AddWorkflowActivitiesFromAssembliesContaining( + this IServiceCollection services, + ServiceLifetime lifetime, + params Type[] markerTypes) + { + return services.AddWorkflowActivitiesFromAssembliesContaining(markerTypes, lifetime); + } + + /// + /// Adds all workflow activities from the assemblies containing the specified marker types + /// + /// The service collection + /// Types whose assemblies will be scanned + /// The service collection for chaining + public static IServiceCollection AddWorkflowActivitiesFromAssembliesContaining( + this IServiceCollection services, + params Type[] markerTypes) + { + return services.AddWorkflowActivitiesFromAssembliesContaining(markerTypes, ServiceLifetime.Scoped); + } + + /// + /// Adds all workflow activities from the assemblies containing the specified marker types + /// + /// First marker type whose assembly will be scanned + /// The service collection + /// The service lifetime (defaults to Scoped) + /// The service collection for chaining + public static IServiceCollection AddWorkflowActivitiesFromAssembliesContaining( + this IServiceCollection services, + ServiceLifetime lifetime = ServiceLifetime.Scoped) + { + return services.AddWorkflowActivitiesFromAssembliesContaining( + [ + typeof(T1) + ], + lifetime + ); + } + + /// + /// Adds all workflow activities from the assemblies containing the specified marker types + /// + /// First marker type whose assembly will be scanned + /// Second marker type whose assembly will be scanned + /// The service collection + /// The service lifetime (defaults to Scoped) + /// The service collection for chaining + public static IServiceCollection AddWorkflowActivitiesFromAssembliesContaining( + this IServiceCollection services, + ServiceLifetime lifetime = ServiceLifetime.Scoped) + { + return services.AddWorkflowActivitiesFromAssembliesContaining( + [ + typeof(T1), typeof(T2) + ], + lifetime + ); + } + + /// + /// Adds all workflow activities from the assemblies containing the specified marker types + /// + /// First marker type whose assembly will be scanned + /// Second marker type whose assembly will be scanned + /// Third marker type whose assembly will be scanned + /// The service collection + /// The service lifetime (defaults to Scoped) + /// The service collection for chaining + public static IServiceCollection AddWorkflowActivitiesFromAssembliesContaining( + this IServiceCollection services, + ServiceLifetime lifetime = ServiceLifetime.Scoped) + { + return services.AddWorkflowActivitiesFromAssembliesContaining( + [ + typeof(T1), typeof(T2), typeof(T3) + ], + lifetime + ); + } + + /// + /// Adds all workflow activities of specific types from the specified assemblies, or from all loaded assemblies if none specified + /// + /// The type of payload the activities process + /// The type of error the activities might return + /// The service collection + /// Optional list of assemblies to scan. If null or empty, scans all loaded assemblies + /// The service lifetime (defaults to Scoped) + /// The service collection for chaining + public static IServiceCollection AddWorkflowActivities( + this IServiceCollection services, + IEnumerable? assemblies = null, + ServiceLifetime lifetime = ServiceLifetime.Scoped) + { + // If no assemblies are specified, use all loaded assemblies + assemblies ??= AppDomain.CurrentDomain.GetAssemblies() + .Where(a => !a.IsDynamic && !a.FullName.StartsWith("System") && !a.FullName.StartsWith("Microsoft")); + + // Register all IWorkflowActivity implementations + services.Scan(scan => scan + .FromAssemblies(assemblies) + .AddClasses(classes => classes.AssignableTo(typeof(IWorkflowActivity))) + .AsImplementedInterfaces() + .WithLifetime(lifetime) + ); + + // Register all IWorkflowActivities implementations + services.Scan(scan => scan + .FromAssemblies(assemblies) + .AddClasses(classes => classes.AssignableTo(typeof(IWorkflowActivities))) + .AsImplementedInterfaces() + .WithLifetime(lifetime) + ); + + return services; + } +} \ No newline at end of file diff --git a/Zooper.Bee/Extensions/WorkflowExtensions.cs b/Zooper.Bee/Extensions/WorkflowExtensions.cs new file mode 100644 index 0000000..7665d9d --- /dev/null +++ b/Zooper.Bee/Extensions/WorkflowExtensions.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; + +namespace Zooper.Bee.Extensions; + +/// +/// Provides extension methods for registering all workflow components (validations, activities, and workflow classes) +/// with dependency injection. These methods simplify the configuration process by centralizing the registration +/// of all workflow-related services. +/// +public static class WorkflowExtensions +{ + /// + /// Registers all workflow components from the specified assemblies into the service collection. + /// This includes workflow validations, workflow activities, and concrete workflow classes. + /// + /// The service collection to add the registrations to + /// Optional list of assemblies to scan for workflow components. If null or empty, all non-system + /// assemblies in the current AppDomain will be scanned + /// The service lifetime to use for the registered services (defaults to Scoped) + /// The service collection for chaining additional registrations + /// + /// This method provides a comprehensive registration of all workflow-related components: + /// - Workflow validations (via AddWorkflowValidations) + /// - Workflow activities (via AddWorkflowActivities) + /// - Concrete workflow classes (classes ending with "Workflow") + /// + /// Workflow classes are registered as themselves (not by interface) to support direct injection. + /// System and Microsoft assemblies are excluded by default when no specific assemblies are provided. + /// + public static IServiceCollection AddWorkflows( + this IServiceCollection services, + IEnumerable? assemblies = null, + ServiceLifetime lifetime = ServiceLifetime.Scoped) + { + // If no assemblies are specified, use all loaded assemblies + var assembliesToScan = assemblies != null + ? assemblies.ToArray() + : AppDomain.CurrentDomain.GetAssemblies() + .Where(a => !a.IsDynamic && !a.FullName.StartsWith("System") && !a.FullName.StartsWith("Microsoft")) + .ToArray(); + + // Register all workflow validations + services.AddWorkflowValidations(assembliesToScan, lifetime); + + // Register all workflow activities + services.AddWorkflowActivities(assembliesToScan, lifetime); + + // Then register all classes ending with Workflow + services.Scan(scan => scan + .FromAssemblies(assembliesToScan) + .AddClasses(classes => + classes.Where(type => type.Name.EndsWith("Workflow") && type is { IsAbstract: false, IsInterface: false }) + ) + .AsSelf() + .WithLifetime(lifetime) + ); + + return services; + } +} \ No newline at end of file diff --git a/Zooper.Bee/Extensions/WorkflowValidationExtensions.cs b/Zooper.Bee/Extensions/WorkflowValidationExtensions.cs new file mode 100644 index 0000000..d5a57a2 --- /dev/null +++ b/Zooper.Bee/Extensions/WorkflowValidationExtensions.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using Zooper.Bee.Interfaces; + +namespace Zooper.Bee.Extensions; + +/// +/// Provides extension methods for registering workflow validations with dependency injection. +/// These methods use assembly scanning to automatically discover and register all implementations +/// of workflow validation interfaces within specified assemblies. +/// +public static class WorkflowValidationExtensions +{ + /// + /// Registers all workflow validations from the specified assemblies into the service collection. + /// This includes both individual workflow validations (IWorkflowValidation) and validation collections (IWorkflowValidations). + /// + /// The service collection to add the registrations to + /// Optional list of assemblies to scan for workflow validations. If null or empty, all non-system + /// assemblies in the current AppDomain will be scanned + /// The service lifetime to use for the registered services (defaults to Scoped) + /// The service collection for chaining additional registrations + /// + /// This method uses Scrutor to scan assemblies and register classes based on their implemented interfaces. + /// System and Microsoft assemblies are excluded by default when no specific assemblies are provided. + /// + public static IServiceCollection AddWorkflowValidations( + this IServiceCollection services, + IEnumerable? assemblies = null, + ServiceLifetime lifetime = ServiceLifetime.Scoped) + { + // If no assemblies are specified, use all loaded assemblies + var assembliesToScan = assemblies != null + ? assemblies.ToArray() + : AppDomain.CurrentDomain.GetAssemblies() + .Where(a => !a.IsDynamic && !a.FullName.StartsWith("System") && !a.FullName.StartsWith("Microsoft")) + .ToArray(); + + // Register all IWorkflowValidation implementations + services.Scan(scan => scan + .FromAssemblies(assembliesToScan) + .AddClasses(classes => classes.AssignableTo(typeof(IWorkflowValidation))) + .AsImplementedInterfaces() + .WithLifetime(lifetime) + ); + + // Register all IWorkflowValidations implementations + services.Scan(scan => scan + .FromAssemblies(assembliesToScan) + .AddClasses(classes => classes.AssignableTo(typeof(IWorkflowValidations))) + .AsImplementedInterfaces() + .WithLifetime(lifetime) + ); + + return services; + } +} \ No newline at end of file diff --git a/Zooper.Bee/Features/IWorkflowFeature.cs b/Zooper.Bee/Features/IWorkflowFeature.cs index b6e6a8c..8e0a657 100644 --- a/Zooper.Bee/Features/IWorkflowFeature.cs +++ b/Zooper.Bee/Features/IWorkflowFeature.cs @@ -7,7 +7,7 @@ namespace Zooper.Bee.Features; /// /// The type of the main workflow payload /// The type of the error -public interface IWorkflowFeature +public interface IWorkflowFeature { /// /// Gets the condition that determines if this feature should execute. diff --git a/Zooper.Bee/Interfaces/IWorkflowActivities.cs b/Zooper.Bee/Interfaces/IWorkflowActivities.cs new file mode 100644 index 0000000..ca51539 --- /dev/null +++ b/Zooper.Bee/Interfaces/IWorkflowActivities.cs @@ -0,0 +1,13 @@ +namespace Zooper.Bee.Interfaces; + +/// +/// Base marker interface for workflow activity collections +/// +public interface IWorkflowActivities; + +/// +/// Interface for a collection of workflow activities operating on the same payload and error types +/// +/// The type of payload the activities process +/// The type of error the activities might return +public interface IWorkflowActivities : IWorkflowActivities; \ No newline at end of file diff --git a/Zooper.Bee/Interfaces/IWorkflowActivity.cs b/Zooper.Bee/Interfaces/IWorkflowActivity.cs new file mode 100644 index 0000000..8e359f9 --- /dev/null +++ b/Zooper.Bee/Interfaces/IWorkflowActivity.cs @@ -0,0 +1,26 @@ +using System.Threading; +using System.Threading.Tasks; +using Zooper.Fox; + +namespace Zooper.Bee.Interfaces; + +/// +/// Base marker interface for all workflow activities +/// +public interface IWorkflowActivity; + +/// +/// Interface for an activity that works with a specific payload and error type +/// +/// The type of payload the activity processes +/// The type of error the activity might return +public interface IWorkflowActivity : IWorkflowActivity +{ + /// + /// Executes the activity with the given payload + /// + /// The input payload + /// Cancellation token + /// Either the error or the updated payload + Task> Execute(TPayload payload, CancellationToken cancellationToken); +} diff --git a/Zooper.Bee/Interfaces/IWorkflowValidation.cs b/Zooper.Bee/Interfaces/IWorkflowValidation.cs new file mode 100644 index 0000000..502c765 --- /dev/null +++ b/Zooper.Bee/Interfaces/IWorkflowValidation.cs @@ -0,0 +1,28 @@ +using System.Threading; +using System.Threading.Tasks; +using Zooper.Fox; + +namespace Zooper.Bee.Interfaces; + +/// +/// Base marker interface for all workflow validations +/// +public interface IWorkflowValidation; + +/// +/// Interface for a validation that validates a request and potentially returns an error +/// +/// The type of request being validated +/// The type of error that might be returned +public interface IWorkflowValidation : IWorkflowValidation +{ + /// + /// Validates the request + /// + /// The request to validate + /// Cancellation token + /// An option containing an error if validation fails, or None if validation succeeds + Task> Validate( + TRequest request, + CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/Zooper.Bee/Interfaces/IWorkflowValidations.cs b/Zooper.Bee/Interfaces/IWorkflowValidations.cs new file mode 100644 index 0000000..189513e --- /dev/null +++ b/Zooper.Bee/Interfaces/IWorkflowValidations.cs @@ -0,0 +1,13 @@ +namespace Zooper.Bee.Interfaces; + +/// +/// Base marker interface for a collection of workflow validations +/// +public interface IWorkflowValidations; + +/// +/// Interface for a collection of workflow validations for a specific request and error type +/// +/// The type of request being validated +/// The type of error that might be returned +public interface IWorkflowValidations : IWorkflowValidations; \ No newline at end of file diff --git a/Zooper.Bee/Zooper.Bee.csproj b/Zooper.Bee/Zooper.Bee.csproj index dd2d2a5..e9993c9 100644 --- a/Zooper.Bee/Zooper.Bee.csproj +++ b/Zooper.Bee/Zooper.Bee.csproj @@ -10,6 +10,7 @@ + From a19c4f9cdbb87b5f878b8ee185fdc4b27b331f75 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 23 Apr 2025 20:18:15 +0200 Subject: [PATCH 2/2] Extended README --- CHANGELOG.md | 12 +++++++++- Directory.Build.props | 2 +- README.md | 55 ++++++++++++++++++++++++++++++++++++------- 3 files changed, 59 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc9d82b..34ec761 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.1.0] - 2025-04-23 +### Added +- Added new extension methods for dependency injection: + - `AddWorkflows()` - Registers all workflow components (validations, activities, and workflows) + - `AddWorkflowValidations()` - Registers workflow validations only + - `AddWorkflowActivities()` - Registers workflow activities only + +- Added support for automatic assembly scanning to discover workflow components +- Added the ability to specify service lifetime for workflow registrations + ## [3.0.0] - 2025-05-01 ### Added @@ -34,7 +44,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Compatibility -- All existing code using the deprecated methods will continue to work, but will show deprecation warnings +- All existing code using the deprecated methods will continue to work but will show deprecation warnings - To migrate, replace: ```csharp diff --git a/Directory.Build.props b/Directory.Build.props index a8250fe..30684bf 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -13,7 +13,7 @@ A .NET library for building robust, functional workflows and processing pipelines. - 3.0.0 + 3.1.0 true diff --git a/README.md b/README.md index 9108603..4150d24 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,14 @@ [![NuGet Version](https://img.shields.io/nuget/v/Zooper.Bee.svg)](https://www.nuget.org/packages/Zooper.Bee/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -A flexible and powerful workflow library for .NET that allows you to define complex business processes with a fluent API. +A flexible and powerful workflow library for .NET that allows you to define complex business processes with a fluent +API. ## Overview -Zooper.Bee lets you create workflows that process requests and produce either successful results or meaningful errors. The library uses a builder pattern to construct workflows with various execution patterns including sequential, conditional, parallel, and detached operations. +Zooper.Bee lets you create workflows that process requests and produce either successful results or meaningful errors. +The library uses a builder pattern to construct workflows with various execution patterns including sequential, +conditional, parallel, and detached operations. ## Key Concepts @@ -17,7 +20,7 @@ Zooper.Bee lets you create workflows that process requests and produce either su - **Request**: The input data to the workflow - **Payload**: Data that passes through and gets modified by workflow activities - **Success**: The successful result of the workflow -- **Error**: The error result if the workflow fails +- **Error**: The errors result if the workflow fails ## Installation @@ -90,7 +93,8 @@ Validates the incoming request before processing begins. #### Activities -Activities are the building blocks of a workflow. They process the payload and can produce either a success (with modified payload) or an error. +Activities are the building blocks of a workflow. They process the payload and can produce either a success (with +the modified payload) or an error. ```csharp // Asynchronous activity @@ -135,7 +139,8 @@ Activities that only execute if a condition is met. #### Groups -Organize related activities into logical groups. Groups can have conditions and always merge their results back to the main workflow. +Organize related activities into logical groups. Groups can have conditions and always merge their results back to the +main workflow. ```csharp .Group( @@ -149,7 +154,8 @@ Organize related activities into logical groups. Groups can have conditions and #### Contexts with Local State -Create a context with local state that is accessible to all activities within the context. This helps encapsulate related operations. +Create a context with the local state that is accessible to all activities within the context. This helps encapsulate +related operations. ```csharp .WithContext( @@ -188,7 +194,8 @@ Execute multiple groups of activities in parallel and merge the results. #### Detached Execution -Execute activities in the background without waiting for their completion. Results from detached activities are not merged back into the main workflow. +Execute activities in the background without waiting for their completion. Results from detached activities are not +merged back into the main workflow. ```csharp .Detach( @@ -269,6 +276,38 @@ Use conditions to determine which path to take in a workflow. ) ``` +## Dependency Injection Integration + +Zooper.Bee integrates seamlessly with .NET's dependency injection system. You can register all workflow components with +a single extension method: + +```csharp +// In Startup.cs or Program.cs +services.AddWorkflows(); +``` + +This will scan all assemblies and register: + +- All workflow validations +- All workflow activities +- All concrete workflow classes (classes ending with "Workflow") + +You can also register specific components: + +```csharp +// Register only validations +services.AddWorkflowValidations(); + +// Register only activities +services.AddWorkflowActivities(); + +// Specify which assemblies to scan +services.AddWorkflows(new[] { typeof(Program).Assembly }); + +// Specify service lifetime (Singleton, Scoped, Transient) +services.AddWorkflows(lifetime: ServiceLifetime.Singleton); +``` + ## Performance Considerations - Use `Parallel` for CPU-bound operations that can benefit from parallel execution @@ -288,4 +327,4 @@ Use conditions to determine which path to take in a workflow. ## License -MIT License (Copyright details here) +MIT License (Copyright details here) \ No newline at end of file