A generic, domain-independent strategy pattern framework for .NET designed for building complex, maintainable, and extensible business logic pipelines. It provides robust support for sequential and parallel execution, dynamic scripting with JavaScript and Lua, automatic dependency injection registration via source generators, and built-in monitoring.
- Flexible Pipeline Execution: Execute strategies sequentially for dependent tasks or in parallel for independent, performance-critical operations.
- Domain-Agnostic Design: Built with generics (
IStrategyStep<TContext>) to work with any business domain or data context. - Dynamic Scripting: Define and modify business rules at runtime using JavaScript (via Jint) and Lua (via NLua) without recompiling your application.
- Source Generators: Use the
[StrategyStep]attribute on your strategy classes for automatic registration in the .NET dependency injection container. - Fluent Builder API: A
StrategyBuilderprovides a clean, fluent interface for composing ad-hoc workflows and simple strategy sequences. - Built-in Monitoring: Enable metrics to automatically track strategy execution counts, duration (min, max, average), success/failure rates, and more.
- Dependency Injection Friendly: Deep integration with
Microsoft.Extensions.DependencyInjectionfor easy setup and management of strategies.
Install the main package from NuGet to get started.
dotnet add package FlexibleStrategyFrameworkFor specific features, you can install the extension packages:
FlexibleStrategyFramework.Extensions.DynamicScripting: For JavaScript and Lua support.FlexibleStrategyFramework.Extensions.Monitoring: For performance metrics collection.FlexibleStrategyFramework.SourceGenerators: For automatic strategy registration (used as an analyzer).
Here is a quick guide to setting up and using the framework.
Your context class holds the state that is passed through the pipeline. It must implement IContext, but you can inherit from the provided BaseContext for convenience.
// src/FlexibleStrategyFramework.Core/BaseContext.cs
public abstract class BaseContext : IContext
{
public string ContextId { get; set; } = Guid.NewGuid().ToString();
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public Dictionary<string, object> Data { get; set; } = new();
// ...
}
// samples/PaymentProcessor/Contexts/PaymentContext.cs
public class PaymentContext : BaseContext
{
public string PaymentId { get; set; }
public decimal Amount { get; set; }
public string CustomerEmail { get; set; }
public PaymentStatus Status { get; set; } = PaymentStatus.Pending;
}A strategy step is a single, atomic piece of work in your pipeline. Mark it with the [StrategyStep] attribute for auto-registration.
// samples/PaymentProcessor/Strategies/PaymentValidationStrategy.cs
using FlexibleStrategyFramework.Abstractions;
using FlexibleStrategyFramework.SourceGenerators; // For StrategyStepAttribute
using PaymentProcessor.Contexts;
[StrategyStep(typeof(PaymentContext), Priority = 100)]
public class PaymentValidationStrategy : IStrategyStep<PaymentContext>
{
// The generator will use the class name if StepName is not provided.
public string StepName => "Validation";
public async Task<StepResult> ExecuteAsync(PaymentContext context)
{
Console.WriteLine($"Validating payment {context.PaymentId}...");
await Task.Delay(100);
if (context.Amount <= 0)
return StepResult.Failure("Payment amount must be greater than zero");
if (string.IsNullOrEmpty(context.CustomerEmail))
return StepResult.Failure("Customer email is required");
context.Status = PaymentStatus.Validated;
return StepResult.Success("Payment validated successfully");
}
}In your application's startup, add the framework and register your strategies. The source generator automatically finds classes marked with [StrategyStep].
// samples/PaymentProcessor/Program.cs
var services = new ServiceCollection();
// Add logging, etc.
services.AddLogging(builder => builder.AddConsole());
// Add the framework
services.AddFlexibleStrategyFramework(options =>
{
options.EnableMetrics = true; // Optional: enable monitoring
options.DefaultTimeout = TimeSpan.FromSeconds(60);
});
// Register strategies (manually or via source generators)
// Manual registration:
services.AddStrategy<PaymentValidationStrategy, PaymentContext>();
services.AddStrategy<FraudDetectionStrategy, PaymentContext>();
services.AddStrategy<GatewayCommunicationStrategy, PaymentContext>();
// Register a use case that will use the pipeline
services.AddScoped<ProcessPaymentUseCase>();Inject IStrategyPipeline<TContext> into your services and call ExecuteAsync to run the pipeline.
// samples/PaymentProcessor/UseCases/ProcessPaymentUseCase.cs
public class ProcessPaymentUseCase
{
private readonly IStrategyPipeline<PaymentContext> _pipeline;
public ProcessPaymentUseCase(IStrategyPipeline<PaymentContext> pipeline)
{
_pipeline = pipeline;
}
public async Task<PaymentResult> ExecuteAsync(PaymentRequest request)
{
var context = new PaymentContext
{
Amount = request.Amount,
CustomerEmail = request.CustomerEmail,
// ...
};
// Execute all registered strategies for PaymentContext
var pipelineResult = await _pipeline.ExecuteAsync(context);
if (pipelineResult.IsSuccess)
{
Console.WriteLine("Pipeline executed successfully!");
}
else
{
Console.WriteLine($"Pipeline failed: {string.Join(", ", pipelineResult.Errors)}");
}
// Map to business result...
return new PaymentResult { /* ... */ };
}
}Execute business logic from scripts, allowing rules to be changed without redeploying your application.
Create a JavaScriptStrategy and pass it your script content. The script can access and modify the context.
// samples/DynamicStrategies/Program.cs
var jsScript = File.ReadAllText("Scripts/discount.js");
var jsStrategy = new JavaScriptStrategy<DynamicStrategyContext>(
"DiscountCalculation",
jsScript);
var jsResult = await jsStrategy.ExecuteAsync(jsContext);discount.js
// samples/DynamicStrategies/Scripts/discount.js
function calculateDiscount(context) {
var amount = context.Data.amount || 0;
var customerType = context.Data.customerType || 'regular';
var discount = 0;
if (amount > 1000) {
discount += 0.1; // 10% for large orders
}
if (customerType === 'premium') {
discount += 0.15; // 15% for premium customers
}
var finalAmount = amount * (1 - discount);
// The returned object is automatically converted to a StepResult
return {
isSuccess: true,
message: 'Discount calculated successfully',
outputData: {
originalAmount: amount,
discountPercentage: discount * 100,
finalAmount: finalAmount
}
};
}
// Execute the function. The last evaluation is the return value.
calculateDiscount(context);For simpler, ad-hoc workflows, the StrategyBuilder offers a fluent API to chain steps together without needing separate classes for each strategy.
// samples/InvoiceValidator/Program.cs
var builder = new StrategyBuilder<InvoiceValidationContext>(serviceProvider);
var result = await builder
.AddStep("BasicValidation", async ctx =>
{
if (ctx.Amount <= 0)
return StepResult.Failure("Invoice amount must be positive");
return StepResult.Success("Basic validation passed");
})
.AddStep("AmountCalculation", async ctx =>
{
var calculatedTotal = ctx.Lines.Sum(line => line.TotalPrice);
if (Math.Abs(calculatedTotal - ctx.Amount) > 0.01m)
return StepResult.Failure("Amount mismatch");
return StepResult.Success("Amount calculation verified");
})
.AddStep<ApplyBusinessRulesStrategy>() // Can mix with existing strategy classes
.ExecuteAsync(context);Customize pipeline execution by passing a PipelineConfiguration object.
// samples/PaymentProcessor/UseCases/ProcessPaymentUseCase.cs
var config = new PipelineConfiguration
{
// Only execute these specific steps
RequiredSteps = new List<string> { "Validation", "GatewayProcessing" },
// Stop if any step returns a failure
StopOnFirstFailure = true,
// Run independent strategies in parallel
EnableParallelExecution = false,
// Set an overall timeout for the pipeline
Timeout = TimeSpan.FromSeconds(30),
};
var pipelineResult = await _pipeline.ExecuteAsync(context, config);The repository includes several sample projects to demonstrate various features:
- PaymentProcessor: A realistic example showing a multi-step payment processing workflow with validation, fraud detection, and gateway communication.
- ReportGenerator: Demonstrates the difference between sequential and parallel pipeline execution for a report generation task.
- WorkflowEngine: A complex example that orchestrates a stateful business process, including initialization, validation, approvals, and notifications.
- DynamicStrategies: A focused sample showcasing how to use JavaScript and Lua to implement dynamic business rules.
- InvoiceValidator: A simple example using the
StrategyBuilderfor an ad-hoc validation workflow.
To build the project, run the build script from the root of the repository.
# In PowerShell
# Clean, restore, build, and test
./build.ps1 -Clean -Test
# To also create NuGet packages
./build.ps1 -PackThe CI build is defined in .github/workflows/ci.yml.
Contributions are welcome! Please feel free to open an issue to discuss a bug or feature, or submit a pull request with your improvements.
This project is licensed under the MIT License. See the LICENSE file for details.
