From 1400d93222ab984a984ca26042ac4823c5a74d19 Mon Sep 17 00:00:00 2001 From: solidarcsoftware Date: Tue, 30 Jun 2026 21:10:09 +0200 Subject: [PATCH] Added background worker --- ....EntityFrameworkCore.Demo.Benchmark.csproj | 2 +- .../Application/Workers/DemoWorkflowWorker.cs | 74 ++++++ .../DataArc.EntityFrameworkCore.Demo.csproj | 20 ++ DataArc.EntityFrameworkCore.Demo/Program.cs | 62 ++--- README.md | 221 +++++++++++++----- 5 files changed, 274 insertions(+), 105 deletions(-) create mode 100644 DataArc.EntityFrameworkCore.Demo/Application/Workers/DemoWorkflowWorker.cs diff --git a/DataArc.EntityFrameworkCore.Demo.Benchmark/DataArc.EntityFrameworkCore.Demo.Benchmark.csproj b/DataArc.EntityFrameworkCore.Demo.Benchmark/DataArc.EntityFrameworkCore.Demo.Benchmark.csproj index 876f96b..58af83b 100644 --- a/DataArc.EntityFrameworkCore.Demo.Benchmark/DataArc.EntityFrameworkCore.Demo.Benchmark.csproj +++ b/DataArc.EntityFrameworkCore.Demo.Benchmark/DataArc.EntityFrameworkCore.Demo.Benchmark.csproj @@ -19,7 +19,7 @@ - + \ No newline at end of file diff --git a/DataArc.EntityFrameworkCore.Demo/Application/Workers/DemoWorkflowWorker.cs b/DataArc.EntityFrameworkCore.Demo/Application/Workers/DemoWorkflowWorker.cs new file mode 100644 index 0000000..d6d42b6 --- /dev/null +++ b/DataArc.EntityFrameworkCore.Demo/Application/Workers/DemoWorkflowWorker.cs @@ -0,0 +1,74 @@ +using Microsoft.Extensions.Hosting; + +using DataArc.EntityFrameworkCore.Demo.Application.Modules.Finance.Features.SalaryAdjustments.Services; + +namespace DataArc.EntityFrameworkCore.Demo.Application.Workers +{ + internal sealed class DemoWorkflowWorker : BackgroundService + { + private const int BatchSize = 100_000; + + private readonly ISalaryAdjustmentService _salaryAdjustmentService; + private readonly IEmployeePerformanceService _employeePerformanceService; + private readonly IHostApplicationLifetime _hostApplicationLifetime; + + public DemoWorkflowWorker( + ISalaryAdjustmentService salaryAdjustmentService, + IEmployeePerformanceService employeePerformanceService, + IHostApplicationLifetime hostApplicationLifetime) + { + _salaryAdjustmentService = salaryAdjustmentService; + _employeePerformanceService = employeePerformanceService; + _hostApplicationLifetime = hostApplicationLifetime; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + try + { + decimal salaryAdjustmentBaseRate = 0.05m; + decimal salaryThreshold = 10_000m; + double ratingThreshold = 4.5; + + var processedCount = await _salaryAdjustmentService + .ProcessEmployeeSalaryAdjustmentsAsync( + salaryAdjustmentBaseRate, + salaryThreshold, + BatchSize); + + Console.WriteLine($"Processed salary adjustment records: {processedCount:N0}"); + + if (processedCount > 0) + { + var topRatedEmployees = await _employeePerformanceService + .GetTopRatedEmployeesAsync(ratingThreshold); + + var topRatedEmployee = topRatedEmployees + .OrderByDescending(employee => employee.Salary) + .FirstOrDefault(); + + Console.WriteLine(); + + Console.WriteLine(topRatedEmployee is null + ? "No top rated employees found." + : $"Top Rated Employee: {topRatedEmployee.Name} {topRatedEmployee.Surname}, " + + $"Salary: {topRatedEmployee.Salary:N2}, " + + $"Number of top rated employees: {topRatedEmployees.Count:N0}"); + } + + Console.WriteLine(); + Console.WriteLine("Demo workflow completed."); + } + catch (Exception exception) + { + Console.WriteLine(); + Console.WriteLine($"Demo workflow failed: {exception.Message}"); + throw; + } + finally + { + _hostApplicationLifetime.StopApplication(); + } + } + } +} \ No newline at end of file diff --git a/DataArc.EntityFrameworkCore.Demo/DataArc.EntityFrameworkCore.Demo.csproj b/DataArc.EntityFrameworkCore.Demo/DataArc.EntityFrameworkCore.Demo.csproj index 1d0c591..c8e4155 100644 --- a/DataArc.EntityFrameworkCore.Demo/DataArc.EntityFrameworkCore.Demo.csproj +++ b/DataArc.EntityFrameworkCore.Demo/DataArc.EntityFrameworkCore.Demo.csproj @@ -16,6 +16,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/DataArc.EntityFrameworkCore.Demo/Program.cs b/DataArc.EntityFrameworkCore.Demo/Program.cs index 82ab01c..edf016f 100644 --- a/DataArc.EntityFrameworkCore.Demo/Program.cs +++ b/DataArc.EntityFrameworkCore.Demo/Program.cs @@ -1,64 +1,42 @@ using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; -using DataArc.EntityFrameworkCore.Demo.Application.Modules.Finance.Features.SalaryAdjustments.Services; using DataArc.EntityFrameworkCore.Demo.Application.Modules.Finance.Registration; +using DataArc.EntityFrameworkCore.Demo.Application.Workers; using DataArc.EntityFrameworkCore.Demo.Persistence.Database.Creator; using DataArc.EntityFrameworkCore.Demo.Persistence.Database.Seeder; const int batchSize = 100_000; -var serviceProvider = new ServiceCollection() - .AddFinanceModule() - .BuildServiceProvider(); +var host = Host + .CreateDefaultBuilder(args) + .ConfigureServices(services => + { + services + .AddFinanceModule() + .AddHostedService(); + }) + .Build(); + +using var scope = host.Services.CreateScope(); var databaseCreators = new IDatabaseCreator[] { - serviceProvider.GetRequiredService(), - serviceProvider.GetRequiredService(), - serviceProvider.GetRequiredService(), - serviceProvider.GetRequiredService() + scope.ServiceProvider.GetRequiredService(), + scope.ServiceProvider.GetRequiredService(), + scope.ServiceProvider.GetRequiredService(), + scope.ServiceProvider.GetRequiredService() }; var databaseSeeders = new IDatabaseSeeder[] { - serviceProvider.GetRequiredService(), + scope.ServiceProvider.GetRequiredService() }; - -var salaryAdjustmentService = serviceProvider.GetRequiredService(); -var employeePerformanceService = serviceProvider.GetRequiredService(); await ResetDatabasesAsync(databaseCreators, databaseSeeders, batchSize); -decimal salaryAdjustmentBaseRate = 0.05m; -decimal salaryThreshold = 10_000m; -double ratingThreshold = 4.5; - -var processedCount = await salaryAdjustmentService - .ProcessEmployeeSalaryAdjustmentsAsync( - salaryAdjustmentBaseRate, - salaryThreshold, - batchSize); - -Console.WriteLine($"Processed salary adjustment records: {processedCount:N0}"); - -if (processedCount > 0) -{ - var topRatedEmployees = await employeePerformanceService - .GetTopRatedEmployeesAsync(ratingThreshold); - - var topRatedEmployee = topRatedEmployees - .OrderByDescending(employee => employee.Salary) - .FirstOrDefault(); - - Console.WriteLine(); - - Console.WriteLine(topRatedEmployee is null - ? "No top rated employees found." - : $"Top Rated Employee: {topRatedEmployee.Name} {topRatedEmployee.Surname}, " + - $"Salary: {topRatedEmployee.Salary:N2}, " + - $"Number of top rated employees: {topRatedEmployees.Count:N0}"); -} +await host.RunAsync(); Console.WriteLine(); Console.WriteLine("Press any key to exit."); @@ -87,7 +65,7 @@ static async Task ResetDatabasesAsync( foreach (var databaseSeeder in databaseSeeders) { - if(!await databaseSeeder.SeedDatabaseAsync(batchSize)) + if (!await databaseSeeder.SeedDatabaseAsync(batchSize)) throw new InvalidOperationException("Failed to seed the demo databases."); } diff --git a/README.md b/README.md index 2433742..6a614de 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,14 @@ # DataArc.EntityFrameworkCore -> EF Core execution for workflows that need isolated `DbContext` boundaries, bulk operations, cross-context reads, parallel writes, and structured results. +> EF Core execution for background workers, batch jobs, bulk operations, cross-context reads, parallel writes, and structured results. -**DataArc.EntityFrameworkCore** utilises EF Core. +**DataArc.EntityFrameworkCore** uses EF Core. -It providates a dedicated execution layer that allows application code to coordinate data work across multiple `DbContext` boundaries without turning the workflow into a pile of repository calls, handler classes, or manual `DbContext` plumbing. +It provides a dedicated execution layer that helps application code coordinate serious EF Core data workflows across multiple `DbContext` boundaries without turning the workflow into scattered repository calls, handler classes, or manual `DbContext` plumbing. -This demo shows the product in a small finance-style workflow: +This demo runs as a small .NET background worker. + +The worker executes a finance-style data workflow: ```text Read employees from HR. @@ -32,7 +34,7 @@ The demo proves four practical things: One query pipeline can compose a read model across multiple contexts. 4. **Structured results** - Command execution returns success/failure, affected records, and exception detail in one result shape. + Command execution returns success or failure, affected records, and exception detail in one result shape. The central idea: @@ -128,21 +130,23 @@ The application flow: 2. Creates the demo databases. 3. Generates SQL scripts for the database operations. 4. Seeds employee data into HR. -5. Reads employees from HR. -6. Applies salary adjustment rules. -7. Bulk writes adjusted data into Finance, IT, and Operations. -8. Executes the command pipeline in parallel. -9. Queries top-rated employees across all four contexts. -10. Prints a short summary. +5. Starts a .NET hosted worker. +6. Reads employees from HR. +7. Applies salary adjustment rules. +8. Bulk writes adjusted data into Finance, IT, and Operations. +9. Executes the command pipeline in parallel. +10. Queries top-rated employees across all four contexts. +11. Prints a short summary. ```mermaid flowchart LR - A[Program.cs] --> B[SalaryAdjustmentService] - A --> C[EmployeePerformanceService] + A[Program.cs] --> B[DemoWorkflowWorker] + B --> C[SalaryAdjustmentService] + B --> D[EmployeePerformanceService] - B --> Q[DataArc Query Pipeline] - B --> W[DataArc Command Pipeline] - C --> R[DataArc Cross-Context Query] + C --> Q[DataArc Query Pipeline] + C --> W[DataArc Command Pipeline] + D --> R[DataArc Cross-Context Query] Q --> HR[HrDbContext] W --> FIN[FinanceDbContext] @@ -157,51 +161,122 @@ flowchart LR ## Application Shape -`Program.cs` stays small. +The demo uses the .NET generic host. + +`Program.cs` configures services, prepares the demo databases, and starts the host. The hosted worker runs the actual data workflow. + +```text +Program.cs = host setup + demo database preparation +DemoWorkflowWorker = background data workflow runner +SalaryAdjustmentService = read-transform-write workflow +EmployeePerformanceService = cross-context read model +Persistence = EF Core contexts, registration, seeding, and database creators +``` -It prepares the demo databases, runs the salary adjustment workflow, then queries a concise result. +This shape is familiar to .NET developers because many real data workflows run as background jobs, scheduled workers, import/export processors, reconciliation jobs, reporting projection builders, or internal batch processes. ```csharp const int batchSize = 100_000; -var serviceProvider = new ServiceCollection() - .AddFinanceModule() - .BuildServiceProvider(); +var host = Host + .CreateDefaultBuilder(args) + .ConfigureServices(services => + { + services + .AddFinanceModule() + .AddHostedService(); + }) + .Build(); + +using var scope = host.Services.CreateScope(); var databaseCreators = new IDatabaseCreator[] { - serviceProvider.GetRequiredService(), - serviceProvider.GetRequiredService(), - serviceProvider.GetRequiredService(), - serviceProvider.GetRequiredService() + scope.ServiceProvider.GetRequiredService(), + scope.ServiceProvider.GetRequiredService(), + scope.ServiceProvider.GetRequiredService(), + scope.ServiceProvider.GetRequiredService() }; var databaseSeeders = new IDatabaseSeeder[] { - serviceProvider.GetRequiredService() + scope.ServiceProvider.GetRequiredService() }; -var salaryAdjustmentService = - serviceProvider.GetRequiredService(); +await ResetDatabasesAsync(databaseCreators, databaseSeeders, batchSize); + +await host.RunAsync(); +``` -var employeePerformanceService = - serviceProvider.GetRequiredService(); +The repetitive bulk-operation detail belongs inside the use-case service, not in `Program.cs`. -await ResetDatabasesAsync(databaseCreators, databaseSeeders, batchSize); +--- -var processedCount = await salaryAdjustmentService - .ProcessEmployeeSalaryAdjustmentsAsync( - salaryAdjustmentBaseRate: 0.05m, - salaryThreshold: 10_000m, - batchSize); +## Background Worker + +`DemoWorkflowWorker` calls the application services that use DataArc.EntityFrameworkCore. + +```csharp +internal sealed class DemoWorkflowWorker : BackgroundService +{ + private const int BatchSize = 100_000; -Console.WriteLine($"Processed salary adjustment records: {processedCount:N0}"); + private readonly ISalaryAdjustmentService _salaryAdjustmentService; + private readonly IEmployeePerformanceService _employeePerformanceService; + private readonly IHostApplicationLifetime _hostApplicationLifetime; -var topRatedEmployees = await employeePerformanceService - .GetTopRatedEmployeesAsync(rating: 4.5); + public DemoWorkflowWorker( + ISalaryAdjustmentService salaryAdjustmentService, + IEmployeePerformanceService employeePerformanceService, + IHostApplicationLifetime hostApplicationLifetime) + { + _salaryAdjustmentService = salaryAdjustmentService; + _employeePerformanceService = employeePerformanceService; + _hostApplicationLifetime = hostApplicationLifetime; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + try + { + var processedCount = await _salaryAdjustmentService + .ProcessEmployeeSalaryAdjustmentsAsync( + salaryAdjustmentBaseRate: 0.05m, + salaryThreshold: 10_000m, + batchSize: BatchSize); + + Console.WriteLine($"Processed salary adjustment records: {processedCount:N0}"); + + if (processedCount > 0) + { + var topRatedEmployees = await _employeePerformanceService + .GetTopRatedEmployeesAsync(rating: 4.5); + + var topRatedEmployee = topRatedEmployees + .OrderByDescending(employee => employee.Salary) + .FirstOrDefault(); + + Console.WriteLine(); + + Console.WriteLine(topRatedEmployee is null + ? "No top rated employees found." + : $"Top Rated Employee: {topRatedEmployee.Name} {topRatedEmployee.Surname}, " + + $"Salary: {topRatedEmployee.Salary:N2}, " + + $"Number of top rated employees: {topRatedEmployees.Count:N0}"); + } + + Console.WriteLine(); + Console.WriteLine("Demo workflow completed."); + } + finally + { + _hostApplicationLifetime.StopApplication(); + } + } +} ``` -The repetitive bulk-operation detail belongs inside the use-case service, not in `Program.cs`. +The worker is intentionally thin. It runs the workflow and leaves EF Core execution details inside the application services. --- @@ -211,6 +286,7 @@ The repetitive bulk-operation detail belongs inside the use-case service, not in ```mermaid sequenceDiagram + participant Worker as DemoWorkflowWorker participant Salary as SalaryAdjustmentService participant Query as DataArc Query Pipeline participant Command as DataArc Command Pipeline @@ -219,6 +295,7 @@ sequenceDiagram participant IT as ItDbContext participant Ops as OperationsDbContext + Worker->>Salary: ProcessEmployeeSalaryAdjustmentsAsync(...) Salary->>Query: UseDbExecutionContext() Query->>HR: Read employees above salary threshold HR-->>Salary: Employee list @@ -276,7 +353,7 @@ if (!commandResult.Success) return commandResult.TotalAffected; ``` -The repeated `UseDbExecutionContext()` calls are intentional. Each write target is explicit, visible, and independently routed. +The repeated `UseDbExecutionContext()` calls are intentional. Each target is explicit, visible, and independently routed. --- @@ -498,21 +575,23 @@ GO DataArc.EntityFrameworkCore.Demo │ ├── Application -│ └── Modules -│ └── Finance -│ ├── Features -│ │ ├── SalaryAdjustments -│ │ │ ├── Dtos -│ │ │ │ └── EmployeeDto.cs -│ │ │ └── Services -│ │ │ ├── ISalaryAdjustmentService.cs -│ │ │ └── SalaryAdjustmentService.cs -│ │ └── EmployeePerformance -│ │ └── Services -│ │ ├── IEmployeePerformanceService.cs -│ │ └── EmployeePerformanceService.cs -│ └── Registration -│ └── FinanceModuleRegistration.cs +│ ├── Modules +│ │ └── Finance +│ │ ├── Features +│ │ │ ├── SalaryAdjustments +│ │ │ │ ├── Dtos +│ │ │ │ │ └── EmployeeDto.cs +│ │ │ │ └── Services +│ │ │ │ ├── ISalaryAdjustmentService.cs +│ │ │ │ └── SalaryAdjustmentService.cs +│ │ │ └── EmployeePerformance +│ │ │ └── Services +│ │ │ ├── IEmployeePerformanceService.cs +│ │ │ └── EmployeePerformanceService.cs +│ │ └── Registration +│ │ └── FinanceModuleRegistration.cs +│ └── Workers +│ └── DemoWorkflowWorker.cs │ ├── Persistence │ ├── Database @@ -537,7 +616,7 @@ DataArc.EntityFrameworkCore.Demo └── Program.cs ``` -The application project owns the use cases. +The application project owns the worker and use-case services. The persistence project owns EF Core contexts, database models, database creation, seeding, and DataArc registration. @@ -579,10 +658,12 @@ The application will: 2. Create demo databases. 3. Generate SQL scripts under the app output `Scripts` folder. 4. Seed HR employee data. -5. Run the salary adjustment workflow. -6. Bulk distribute adjusted employee data into Finance, IT, and Operations. -7. Query top-rated employee details. -8. Print a summary. +5. Start the hosted worker. +6. Run the salary adjustment workflow. +7. Bulk distribute adjusted employee data into Finance, IT, and Operations. +8. Query top-rated employee details. +9. Print a summary. +10. Stop the host after the workflow completes. Expected summary shape: @@ -594,6 +675,8 @@ Processed salary adjustment records: 300,000 Top Rated Employee: Name12345 Surname12345, Salary: 151,234.56, Number of top rated employees: 1,234 +Demo workflow completed. + Press any key to exit. ``` @@ -670,9 +753,19 @@ Use the trial to validate: --- +## Boundary Note + +DataArc.EntityFrameworkCore is designed for controlled EF Core execution where an application is allowed to coordinate the participating contexts. + +It is not intended to bypass service ownership rules or encourage unrelated services to read and write each other's private databases directly. + +The goal is explicit execution, not hidden coupling. + +--- + ## Related DataArc Packages -DataArc.EntityFrameworkCore can be used directly in services, as this demo shows. +DataArc.EntityFrameworkCore can be used directly in background workers and application services, as this demo shows. For larger workflow coordination, use **DataArc.Orchestrator**. @@ -701,3 +794,7 @@ It helps teams: **EF Core owns data access.** **DataArc controls execution.** + +--- + +Learn more, start a trial, or purchase a license: [www.dataarc.dev](https://www.dataarc.dev)