diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml new file mode 100644 index 0000000..8dea50d --- /dev/null +++ b/.github/workflows/pr-build.yml @@ -0,0 +1,42 @@ +name: PR Build + +on: + pull_request: + branches: + - main + + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + name: Restore and Build + runs-on: windows-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup .NET SDKs + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 6.0.x + 7.0.x + 8.0.x + 9.0.x + 10.0.x + + - name: Show .NET info + run: dotnet --info + + - name: Show NuGet sources + run: dotnet nuget list source + + - name: Restore from nuget.org + run: dotnet restore --configfile nuget.config + + - name: Build + run: dotnet build --configuration Release --no-restore \ No newline at end of file diff --git a/DataArc.EntityFrameworkCore.Demo.Benchmark/ProcessEmployeeFinanceDataBenchmark.cs b/DataArc.EntityFrameworkCore.Demo.Benchmark/ProcessEmployeeFinanceDataBenchmark.cs index 513d424..7537f0d 100644 --- a/DataArc.EntityFrameworkCore.Demo.Benchmark/ProcessEmployeeFinanceDataBenchmark.cs +++ b/DataArc.EntityFrameworkCore.Demo.Benchmark/ProcessEmployeeFinanceDataBenchmark.cs @@ -20,18 +20,25 @@ namespace DataArc.EntityFrameworkCore.Demo.Benchmark [ThreadingDiagnoser] [SimpleJob] [WarmupCount(1)] - [IterationCount(4)] + [IterationCount(10)] + [InvocationCount(1)] [Config(typeof(BenchmarkConfig))] public class RawEmployeeBulkDataBenchmark { + private const int MaxRecordCount = 1_000_000; + private IServiceProvider? _serviceProvider; private IDatabaseCreator? _databaseCreator; private ICommandFactory? _commandFactory; - private IReadOnlyList? _employees; - [Params(10_000, 100_000, 250_000)] + private List _allEmployees = []; + private List _benchmarkEmployees = []; + + [Params(250_000, 500_000, 1_000_000)] public int RecordCount { get; set; } - public int BulkBatchSize => RecordCount; + + [Params(250_000)] + public int BulkBatchSize { get; set; } [GlobalSetup] public void GlobalSetup() @@ -42,6 +49,10 @@ public void GlobalSetup() _databaseCreator = _serviceProvider.GetRequiredService(); _commandFactory = _serviceProvider.GetRequiredService(); + + _allEmployees = SeedDataGenerator + .GenerateHrSeedData(MaxRecordCount) + .ToList(); } [IterationSetup] @@ -56,34 +67,40 @@ public void IterationSetup() if (!_databaseCreator.EnsureCreated()) throw new InvalidOperationException("Failed to create benchmark databases."); - _employees = SeedDataGenerator.GenerateHrSeedData(RecordCount); + _benchmarkEmployees = _allEmployees + .Take(RecordCount) + .ToList(); } [Benchmark] public async Task ExecuteParallelBulkInsertAsync() { - var commandBuilder = await _commandFactory!.CreateCommandBuilderAsync(); + if (_commandFactory == null) + throw new InvalidOperationException($"{nameof(ICommandFactory)} was not resolved."); + + if (_benchmarkEmployees.Count == 0) + throw new InvalidOperationException("Benchmark employee data was not generated."); + + var commandBuilder = await _commandFactory.CreateCommandBuilderAsync(); if (commandBuilder == null) throw new InvalidOperationException($"{nameof(commandBuilder)} was not resolved."); - if (_employees == null) - throw new InvalidOperationException("Benchmark employee data was not generated."); - commandBuilder .UseDbExecutionContext() - .AddBulk(_employees, BulkBatchSize); + .AddBulk(_benchmarkEmployees, BulkBatchSize); commandBuilder .UseDbExecutionContext() - .AddBulk(_employees, BulkBatchSize); + .AddBulk(_benchmarkEmployees, BulkBatchSize); - commandBuilder.UseDbExecutionContext() - .AddBulk(_employees, BulkBatchSize); + commandBuilder + .UseDbExecutionContext() + .AddBulk(_benchmarkEmployees, BulkBatchSize); commandBuilder .UseDbExecutionContext() - .AddBulk(_employees, BulkBatchSize); + .AddBulk(_benchmarkEmployees, BulkBatchSize); var command = await commandBuilder.BuildAsync(); var commandResult = await command.ExecuteParallelAsync(); @@ -100,8 +117,7 @@ public async Task ExecuteParallelBulkInsertAsync() [GlobalCleanup] public void GlobalCleanup() { - //_databaseCreator?.EnsureDeleted(); - // Cleanup if needed after all iterations are complete + //_databaseCreator?.EnsureDeleted(); } } @@ -144,7 +160,6 @@ public string GetValue(Summary summary, BenchmarkCase benchmarkCase) .Single(parameter => parameter.Name == "RecordCount"); var recordCount = Convert.ToInt32(recordCountParameter.Value); - var totalInsertedRecords = recordCount * _destinationContextCount; return totalInsertedRecords.ToString("N0"); diff --git a/DataArc.EntityFrameworkCore.Demo.Persistence/Seeding/DatabaseCreator.cs b/DataArc.EntityFrameworkCore.Demo.Persistence/Seeding/DatabaseCreator.cs index 61bcf7b..f1a9022 100644 --- a/DataArc.EntityFrameworkCore.Demo.Persistence/Seeding/DatabaseCreator.cs +++ b/DataArc.EntityFrameworkCore.Demo.Persistence/Seeding/DatabaseCreator.cs @@ -33,7 +33,7 @@ public bool EnsureCreated() .IncludeDbContext() .IncludeDbContext() .IncludeDbContext() - .Build(applyChanges: true, generateScripts: true); //Generates SQL scripts for the database changes and applies them to the databases. This is useful for debugging and auditing purposes. + .Build(applyChanges:true); // Drop and create databases db.ExecuteCreate(); diff --git a/DataArc.EntityFrameworkCore.Demo/Application/Modules/Finance/Features/SalaryAdjustments/Services/EmployeePerformanceService.cs b/DataArc.EntityFrameworkCore.Demo/Application/Modules/Finance/Features/SalaryAdjustments/Services/EmployeePerformanceService.cs new file mode 100644 index 0000000..927ebcb --- /dev/null +++ b/DataArc.EntityFrameworkCore.Demo/Application/Modules/Finance/Features/SalaryAdjustments/Services/EmployeePerformanceService.cs @@ -0,0 +1,40 @@ +using DataArc.Core; + +using DataArc.EntityFrameworkCore.Demo.Application.Modules.Finance.Features.SalaryAdjustments.Dtos; +using DataArc.EntityFrameworkCore.Demo.Persistence.Database.DBContexts; +using DataArc.EntityFrameworkCore.Demo.Persistence.Database.DBModels; + +namespace DataArc.EntityFrameworkCore.Demo.Application.Modules.Finance.Features.SalaryAdjustments.Services +{ + internal class EmployeePerformanceService : IEmployeePerformanceService + { + private readonly IQueryFactory _queryFactory; + public EmployeePerformanceService(IQueryFactory queryFactory) + { + _queryFactory = queryFactory; + } + + public async Task> GetTopRatedEmployeesAsync(double rating) + { + var topRatedEmployeesQuery = await _queryFactory.CreateQueryAsync(); + + var topRatedEmployees = await topRatedEmployeesQuery + .UseDbExecutionContext(e => e.Rating > rating) + .Join + (bag => bag.Get()!.Id, f => f.Id) + .Join + (bag => bag.Get()!.Id, i => i.Id) + .Join(bag => bag.Get()!.Id, o => o.Id) + .Select(bag => new EmployeeDto() + { + Id = bag.Get()!.Id, + Name = bag.Get()!.Name!, + Surname = bag.Get()!.Surname!, + Salary = bag.Get()!.Salary! + + }).ToListAsync(); + + return topRatedEmployees; + } + } +} \ No newline at end of file diff --git a/DataArc.EntityFrameworkCore.Demo/Application/Modules/Finance/Services/IFinanceService.cs b/DataArc.EntityFrameworkCore.Demo/Application/Modules/Finance/Features/SalaryAdjustments/Services/IEmployeePerformanceService.cs similarity index 59% rename from DataArc.EntityFrameworkCore.Demo/Application/Modules/Finance/Services/IFinanceService.cs rename to DataArc.EntityFrameworkCore.Demo/Application/Modules/Finance/Features/SalaryAdjustments/Services/IEmployeePerformanceService.cs index 128a221..66004f5 100644 --- a/DataArc.EntityFrameworkCore.Demo/Application/Modules/Finance/Services/IFinanceService.cs +++ b/DataArc.EntityFrameworkCore.Demo/Application/Modules/Finance/Features/SalaryAdjustments/Services/IEmployeePerformanceService.cs @@ -1,10 +1,9 @@ using DataArc.EntityFrameworkCore.Demo.Application.Modules.Finance.Features.SalaryAdjustments.Dtos; -namespace DataArc.EntityFrameworkCore.Demo.Application.Modules.Finance.Services +namespace DataArc.EntityFrameworkCore.Demo.Application.Modules.Finance.Features.SalaryAdjustments.Services { - public interface IFinanceService + public interface IEmployeePerformanceService { - Task ProcessEmployeeFinanceDataAsync(decimal salaryAdjustmentBaseRate, decimal salaryThreshold, int batchSize); Task> GetTopRatedEmployeesAsync(double rating); } } \ No newline at end of file diff --git a/DataArc.EntityFrameworkCore.Demo/Application/Modules/Finance/Features/SalaryAdjustments/Services/ISalaryAdjustmentService.cs b/DataArc.EntityFrameworkCore.Demo/Application/Modules/Finance/Features/SalaryAdjustments/Services/ISalaryAdjustmentService.cs new file mode 100644 index 0000000..1702bf6 --- /dev/null +++ b/DataArc.EntityFrameworkCore.Demo/Application/Modules/Finance/Features/SalaryAdjustments/Services/ISalaryAdjustmentService.cs @@ -0,0 +1,9 @@ +using DataArc.EntityFrameworkCore.Demo.Application.Modules.Finance.Features.SalaryAdjustments.Dtos; + +namespace DataArc.EntityFrameworkCore.Demo.Application.Modules.Finance.Features.SalaryAdjustments.Services +{ + public interface ISalaryAdjustmentService + { + Task ProcessEmployeeSalaryAdjustmentsAsync(decimal salaryAdjustmentBaseRate, decimal salaryThreshold, int batchSize); + } +} \ No newline at end of file diff --git a/DataArc.EntityFrameworkCore.Demo/Application/Modules/Finance/Services/FinanceService.cs b/DataArc.EntityFrameworkCore.Demo/Application/Modules/Finance/Features/SalaryAdjustments/Services/SalaryAdjustmentService.cs similarity index 59% rename from DataArc.EntityFrameworkCore.Demo/Application/Modules/Finance/Services/FinanceService.cs rename to DataArc.EntityFrameworkCore.Demo/Application/Modules/Finance/Features/SalaryAdjustments/Services/SalaryAdjustmentService.cs index a86cfd7..329fb4e 100644 --- a/DataArc.EntityFrameworkCore.Demo/Application/Modules/Finance/Services/FinanceService.cs +++ b/DataArc.EntityFrameworkCore.Demo/Application/Modules/Finance/Features/SalaryAdjustments/Services/SalaryAdjustmentService.cs @@ -1,45 +1,22 @@ using DataArc.Core; -using DataArc.EntityFrameworkCore.Demo.Application.Modules.Finance.Features.SalaryAdjustments.Dtos; + using DataArc.EntityFrameworkCore.Demo.Persistence.Database.DBContexts; using DataArc.EntityFrameworkCore.Demo.Persistence.Database.DBModels; -namespace DataArc.EntityFrameworkCore.Demo.Application.Modules.Finance.Services +namespace DataArc.EntityFrameworkCore.Demo.Application.Modules.Finance.Features.SalaryAdjustments.Services { - public class FinanceService : IFinanceService + internal class SalaryAdjustmentService : ISalaryAdjustmentService { private readonly ICommandFactory _commandFactory; private readonly IQueryFactory _queryFactory; - public FinanceService(IQueryFactory queryFactory, ICommandFactory commandFactory) + public SalaryAdjustmentService(ICommandFactory commandFactory, IQueryFactory queryFactory) { - _queryFactory = queryFactory; _commandFactory = commandFactory; + _queryFactory = queryFactory; } - public async Task> GetTopRatedEmployeesAsync(double rating) - { - var topRatedEmployeesQuery = await _queryFactory.CreateQueryAsync(); - - var topRatedEmployees = await topRatedEmployeesQuery - .UseDbExecutionContext(e => e.Rating > rating) - .Join - (bag => bag.Get()!.Id, f => f.Id) - .Join - (bag => bag.Get()!.Id, i => i.Id) - .Join(bag => bag.Get()!.Id, o => o.Id) - .Select(bag => new EmployeeDto() - { - Id = bag.Get()!.Id, - Name = bag.Get()!.Name!, - Surname = bag.Get()!.Surname!, - Salary = bag.Get()!.Salary! - - }).ToListAsync(); - - return topRatedEmployees; - } - - public async Task ProcessEmployeeFinanceDataAsync( + public async Task ProcessEmployeeSalaryAdjustmentsAsync( decimal salaryAdjustmentBaseRate, decimal salaryThreshold, int batchSize) @@ -56,7 +33,8 @@ public async Task ProcessEmployeeFinanceDataAsync( employee.Salary += employee.Salary * salaryAdjustmentBaseRate; } - var commandBuilder = await _commandFactory.CreateCommandBuilderAsync(); + var commandBuilder = await _commandFactory + .CreateCommandBuilderAsync(); commandBuilder .UseDbExecutionContext() @@ -72,7 +50,7 @@ public async Task ProcessEmployeeFinanceDataAsync( // Build the command builder pipeline and execute the command in parallel across the different contexts var command = await commandBuilder.BuildAsync(); - var commandResult = await command.ExecuteParallelAsync(); + var commandResult = await command.ExecuteAsync(); // Check the command result for success and handle any errors or exceptions if (!commandResult.Success) { diff --git a/DataArc.EntityFrameworkCore.Demo/Application/Modules/Finance/Registration/FinanceModuleRegistration.cs b/DataArc.EntityFrameworkCore.Demo/Application/Modules/Finance/Registration/FinanceModuleRegistration.cs index caea360..f48c428 100644 --- a/DataArc.EntityFrameworkCore.Demo/Application/Modules/Finance/Registration/FinanceModuleRegistration.cs +++ b/DataArc.EntityFrameworkCore.Demo/Application/Modules/Finance/Registration/FinanceModuleRegistration.cs @@ -1,4 +1,4 @@ -using DataArc.EntityFrameworkCore.Demo.Application.Modules.Finance.Services; +using DataArc.EntityFrameworkCore.Demo.Application.Modules.Finance.Features.SalaryAdjustments.Services; using DataArc.EntityFrameworkCore.Demo.Persistence; using Microsoft.Extensions.DependencyInjection; @@ -10,8 +10,9 @@ public static IServiceCollection AddFinanceModule(this IServiceCollection servic { // Register persistence layer for Finance module services.AddPersistence(); - // Register FinanceService and its dependencies - services.AddScoped(); + // Register Services + services.AddScoped(); + services.AddScoped(); return services; } } diff --git a/DataArc.EntityFrameworkCore.Demo/DataArc.EntityFrameworkCore.Demo.csproj b/DataArc.EntityFrameworkCore.Demo/DataArc.EntityFrameworkCore.Demo.csproj index 25af66d..1d0c591 100644 --- a/DataArc.EntityFrameworkCore.Demo/DataArc.EntityFrameworkCore.Demo.csproj +++ b/DataArc.EntityFrameworkCore.Demo/DataArc.EntityFrameworkCore.Demo.csproj @@ -11,7 +11,7 @@ - PreserveNewest + Always PreserveNewest diff --git a/DataArc.EntityFrameworkCore.Demo/Program.cs b/DataArc.EntityFrameworkCore.Demo/Program.cs index 8054cb8..2bd7c1e 100644 --- a/DataArc.EntityFrameworkCore.Demo/Program.cs +++ b/DataArc.EntityFrameworkCore.Demo/Program.cs @@ -1,155 +1,75 @@ -using Microsoft.EntityFrameworkCore.Storage; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - +using DataArc.EntityFrameworkCore.Demo.Application.Modules.Finance.Features.SalaryAdjustments.Services; using DataArc.EntityFrameworkCore.Demo.Application.Modules.Finance.Registration; -using DataArc.EntityFrameworkCore.Demo.Application.Modules.Finance.Services; using DataArc.EntityFrameworkCore.Demo.Persistence.Seeding; -internal class Program +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.Extensions.DependencyInjection; + +const int batchSize = 100_000; + +var serviceProvider = new ServiceCollection() + .AddFinanceModule() + .BuildServiceProvider(); + +var databaseCreator = serviceProvider.GetRequiredService(); +var databaseSeeder = serviceProvider.GetRequiredService(); + +var salaryAdjustmentService = serviceProvider.GetRequiredService(); +var employeePerformanceService = serviceProvider.GetRequiredService(); + +await ResetDatabasesAsync(databaseCreator, databaseSeeder, 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) { - static int batchSize = 100_000; - - //Infrastructure dependencies - static readonly IDatabaseCreator _databaseCreator; - static readonly IDatabaseSeeder _databaseSeeder; - - //Application dependencies - static readonly IFinanceService _financeService; - - /// - /// Initializes static resources for the Program type by configuring logging, building the service provider, and - /// resolving required infrastructure and application services. - /// - /// Runs once before the type is first used. Registers modules with the dependency-injection - /// container, configures the console logger, builds the service provider, and retrieves IDatabaseCreator, - /// IDatabaseSeeder, and IFinanceService instances. - static Program() - { - ILoggerFactory factory = LoggerFactory.Create(builder => - { - builder.AddConsole(); - }); - - // Register modules and build the service provider - var dataProvider = new ServiceCollection() - .AddFinanceModule() - .BuildServiceProvider(); - - //Infrastructure dependencies - _databaseCreator = dataProvider.GetRequiredService(); - _databaseSeeder = dataProvider.GetRequiredService(); - - //Application dependencies - _financeService = dataProvider.GetRequiredService(); - } - - /// - /// Sets up the database by creating it and seeding it with initial data. - /// - /// A task representing the asynchronous operation. - static async Task Setup() - { - try - { - if (_databaseCreator.EnsureCreated()) - { - Console.WriteLine("Database created successfully."); - if (await _databaseSeeder.SeedDatabaseAsync(batchSize)) - { - Console.WriteLine("Database seeded successfully."); - } - else - { - Console.WriteLine("Failed to seed the database."); - } - } - else - { - Console.WriteLine("Failed to create the database."); - } - } - catch (Exception) - { - throw; - } - } - - /// - /// Deletes the database using _databaseCreator.EnsureDeleted and writes the outcome to the console. - /// - /// Rethrows any exception encountered during deletion. Logs success or failure to the - /// console. - /// A task that represents the asynchronous teardown operation. - static async Task Teardown() - { - try - { - if (_databaseCreator.EnsureDeleted()) - { - Console.WriteLine("Database deleted successfully."); - } - else - { - Console.WriteLine("Failed to delete the database."); - } - } - catch (Exception) - { - throw; - } - } - - /// - /// Resets the database by performing teardown followed by setup asynchronously. - /// - /// Exceptions from Teardown or Setup propagate to the caller. Intended for use in test or - /// initialization scenarios. - /// A task that represents the asynchronous operation. - static async Task ResetDatabases() - { - await Teardown(); - await Setup(); - } - - /// - /// Performs the main execution flow of the application by resetting the database, processing employee finance data, - /// and retrieving top-rated employees. - /// - /// - static async Task ExecuteMain() - { - await ResetDatabases(); - - decimal salaryAdjustmentBaseRate = 0.05m; - decimal salaryThreshold = 10000m; - double ratingThreshold = 4.5; - - var processedCount = await _financeService.ProcessEmployeeFinanceDataAsync(salaryAdjustmentBaseRate, salaryThreshold, batchSize); - Console.WriteLine($"Processed {processedCount} employees."); - - if (processedCount > 0) { - var topRatedEmployees = await _financeService.GetTopRatedEmployeesAsync(ratingThreshold); - foreach (var employee in topRatedEmployees) - { - Console.WriteLine($"Top Rated Employee: {employee.Name} {employee.Surname}, Salary: {employee.Salary}"); - } - } - - Console.WriteLine(); - Console.WriteLine("Press any key to exit"); - Console.ReadKey(); - } - - private static void Main(string[] args) - { - try - { - ExecuteMain().GetAwaiter().GetResult(); - } - catch - { - throw; - } - } + 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("Press any key to exit."); +Console.ReadKey(); + +static async Task ResetDatabasesAsync( + IDatabaseCreator databaseCreator, + IDatabaseSeeder databaseSeeder, + int batchSize) +{ + if (!databaseCreator.EnsureDeleted()) + throw new InvalidOperationException("Failed to delete the demo databases."); + + Console.WriteLine("Databases deleted successfully."); + + if (!databaseCreator.EnsureCreated()) + throw new InvalidOperationException("Failed to create the demo databases."); + + Console.WriteLine("Databases created successfully."); + + if (!await databaseSeeder.SeedDatabaseAsync(batchSize)) + throw new InvalidOperationException("Failed to seed the demo databases."); + + Console.WriteLine("Databases seeded successfully."); } \ No newline at end of file diff --git a/DataArc.EntityFrameworkCore.Demo/appsettings.json b/DataArc.EntityFrameworkCore.Demo/appsettings.json index ef4bb1a..1f25904 100644 --- a/DataArc.EntityFrameworkCore.Demo/appsettings.json +++ b/DataArc.EntityFrameworkCore.Demo/appsettings.json @@ -2,7 +2,7 @@ "ConnectionStrings": { "FinanceDb": "Server=.;Database=FinanceDb;Integrated Security=true;TrustServerCertificate=True;", "HrDb": "Server=.;Database=HrDb;Integrated Security=true;TrustServerCertificate=True;", - "ItDb": "Server=MSI;Database=ItDb;Integrated Security=true;TrustServerCertificate=True;", - "OperationsDb": "Server=MSI;Database=OperationsDb;Integrated Security=true;TrustServerCertificate=True;" + "ItDb": "Server=.;Database=ItDb;Integrated Security=true;TrustServerCertificate=True;", + "OperationsDb": "Server=.;Database=OperationsDb;Integrated Security=true;TrustServerCertificate=True;" } } \ No newline at end of file diff --git a/README.md b/README.md index 3dafc22..74f300c 100644 --- a/README.md +++ b/README.md @@ -2,80 +2,86 @@ > A deterministic EF Core execution layer for modular, high-throughput, multi-context systems. -This repository demonstrates how **DataArc.EntityFrameworkCore** can be used inside a clean, modular .NET application without turning services into `DbContext` plumbing, repository-per-table boilerplate, or handler-heavy data access code. +This repository demonstrates **DataArc.EntityFrameworkCore** inside a small modular .NET application. -The demo is intentionally small. The point is not to show a complicated domain model. The point is to show a practical execution model for EF Core workflows that need more structure, more control, and better runtime behavior than direct `DbContext` calls alone. +The demo is intentionally focused. It does not try to model a large business domain. It shows how DataArc coordinates EF Core work across multiple persistence boundaries using explicit query and command pipelines. ---- +## Start Here -## What DataArc.EntityFrameworkCore Is +Recommended reading order: -**DataArc.EntityFrameworkCore** is an execution layer for EF Core-based systems. +1. `Program.cs` +2. `Application/Modules/Finance/Registration/FinanceModuleRegistration.cs` +3. `Application/Modules/Finance/Features/SalaryAdjustments/Services/SalaryAdjustmentService.cs` +4. `Application/Modules/Finance/Features/EmployeePerformance/Services/EmployeePerformanceService.cs` +5. `Persistence/PersistenceRegistration.cs` +6. `Persistence/Database/DbContexts` +7. `DataArc.EntityFrameworkCore.Demo.Benchmark` -It helps teams compose and execute database operations across EF Core context boundaries using explicit query and command pipelines. +## What DataArc.EntityFrameworkCore Is -```text -EF Core is excellent inside one DbContext. -Real systems often need workflows across multiple DbContext boundaries. -DataArc gives those workflows a deterministic execution layer. -``` +**DataArc.EntityFrameworkCore** is an execution layer for EF Core-based systems. -DataArc does not replace EF Core. +EF Core works well inside one `DbContext`. Real systems often need workflows that cross multiple `DbContext` boundaries. -It gives EF Core a stronger execution model for: +DataArc gives those workflows an explicit execution model for: - modular context boundaries - command/query separation - bulk operations - cross-context workflows - parallel execution -- transaction-safe command flows - structured execution results +- transaction-safe command flows - logging-friendly outcomes - high-volume scheduled workloads ---- +DataArc does not replace EF Core. It coordinates EF Core execution. ## What This Demo Shows The demo uses a simple finance workflow: -1. Read employees from an HR persistence boundary. -2. Apply a salary adjustment. -3. Bulk distribute the adjusted employee data into Finance, IT, and Operations persistence boundaries. -4. Execute the cross-context command pipeline in parallel. -5. Return structured execution results. +1. Reset and create demo databases. +2. Seed HR employee data. +3. Read employees from the HR persistence boundary. +4. Apply salary adjustment rules. +5. Bulk distribute adjusted employee data into Finance, IT, and Operations persistence boundaries. +6. Execute the command pipeline in parallel. +7. Query top-rated employee details. +8. Print a concise summary. ```mermaid flowchart LR - A[Application Module] --> B[Finance Service] - B --> C[DataArc Query Builder] - B --> D[DataArc Command Builder] + A[Program.cs] --> B[Finance Module] + B --> C[SalaryAdjustmentService] + B --> D[EmployeePerformanceService] + + C --> Q[DataArc Query Pipeline] + C --> W[DataArc Command Pipeline] + D --> R[DataArc Cross-Context Query] - C --> HR[HrDbContext] - D --> FIN[FinanceDbContext] - D --> IT[ItDbContext] - D --> OPS[OperationsDbContext] + Q --> HR[HrDbContext] + W --> FIN[FinanceDbContext] + W --> IT[ItDbContext] + W --> OPS[OperationsDbContext] - D --> E[Parallel Execution] - E --> R[Structured Result] + W --> X[Parallel Execution] + X --> Y[Structured Result] ``` -The demo focuses on the core DataArc value: +The central idea: ```text Define the workflow. -Build the command pipeline. +Choose the execution context. +Build the command or query pipeline. Execute across context boundaries. Receive a structured result. ``` ---- - ## Project Structure -The demo separates application and persistence concerns. - ```text DataArc.EntityFrameworkCore.Demo │ @@ -83,200 +89,195 @@ DataArc.EntityFrameworkCore.Demo │ └── Modules │ └── Finance │ ├── Features -│ │ └── SalaryAdjustments -│ │ └── Dtos -│ │ └── EmployeeDto.cs -│ ├── Registration -│ │ └── FinanceModuleRegistration.cs -│ └── Services -│ ├── FinanceService.cs -│ └── IFinanceService.cs +│ │ ├── SalaryAdjustments +│ │ │ └── Services +│ │ │ ├── ISalaryAdjustmentService.cs +│ │ │ └── SalaryAdjustmentService.cs +│ │ └── EmployeePerformance +│ │ ├── Dtos +│ │ │ └── EmployeeDto.cs +│ │ └── Services +│ │ ├── IEmployeePerformanceService.cs +│ │ └── EmployeePerformanceService.cs +│ └── Registration +│ └── FinanceModuleRegistration.cs │ ├── Persistence -│ └── Database -│ ├── DBContexts -│ │ ├── FinanceDbContext.cs -│ │ ├── HrDbContext.cs -│ │ ├── ItDbContext.cs -│ │ └── OperationsDbContext.cs -│ ├── DBModels -│ │ ├── Employee.cs -│ │ └── Employer.cs -│ └── Seeding -│ ├── DatabaseCreator.cs -│ ├── DatabaseSeeder.cs -│ └── SeedDataGenerator.cs +│ ├── Database +│ │ ├── DbContexts +│ │ │ ├── FinanceDbContext.cs +│ │ │ ├── HrDbContext.cs +│ │ │ ├── ItDbContext.cs +│ │ │ └── OperationsDbContext.cs +│ │ └── DbModels +│ │ ├── Employee.cs +│ │ └── Employer.cs +│ ├── Seeding +│ │ ├── DatabaseCreator.cs +│ │ ├── DatabaseSeeder.cs +│ │ └── SeedDataGenerator.cs +│ └── PersistenceRegistration.cs │ -├── PersistenceRegistration.cs └── Program.cs ``` -```mermaid -flowchart TB - A[Program.cs] --> B[Application Module Registration] - A --> C[Persistence Registration] - - B --> D[IFinanceService] - D --> E[FinanceService] - - C --> F[DataArc Registration] - C --> G[DbContext Registration] - C --> H[Database Creator] - C --> I[Database Seeder] - - E --> J[IAsyncQuery] - E --> K[ICommandFactory] - K --> L[Command Builder] -``` +The application module owns the feature flow. -The application module does not need to know how the database is created, seeded, or registered. +The persistence layer owns EF Core contexts, database models, seeding, and DataArc execution context registration. -The persistence layer owns EF Core contexts, database models, schema setup, seeding, and DataArc execution context registration. +## Demo Workflow ---- +### Salary Adjustment Use Case -# Demo Workflow - -## Salary Adjustment Scenario - -The finance service performs a practical multi-context workflow: - -```text -1. Query employees from HR. -2. Apply salary adjustment rules. -3. Bulk write adjusted employee data into Finance. -4. Bulk write adjusted employee data into IT. -5. Bulk write adjusted employee data into Operations. -6. Execute those writes in parallel. -7. Return updated employee DTOs. -``` +`SalaryAdjustmentService` reads employees from HR, adjusts salary data, and writes adjusted data into Finance, IT, and Operations. ```mermaid sequenceDiagram - participant App as Application - participant Service as FinanceService - participant Query as DataArc IAsyncQuery - participant Factory as DataArc ICommandFactory - participant Command as DataArc Command Builder + participant App as Program.cs + participant Salary as SalaryAdjustmentService + participant Query as DataArc Query Pipeline + participant Command as DataArc Command Pipeline participant HR as HrDbContext participant Finance as FinanceDbContext participant IT as ItDbContext participant Ops as OperationsDbContext - App->>Service: ProcessEmployeeFinanceDataAsync(rate, batchSize) - Service->>Query: UseExecutionContext() - Query->>HR: Read employees - HR-->>Service: Employee list + App->>Salary: ProcessEmployeeSalaryAdjustmentsAsync(...) + Salary->>Query: UseDbExecutionContext() + Query->>HR: Read employees above salary threshold + HR-->>Salary: Employee list - Service->>Service: Adjust salaries + Salary->>Salary: Apply salary adjustment - Service->>Factory: CreateCommandBuilderAsync() - Factory-->>Service: Command builder + Salary->>Command: AddBulk to Finance + Salary->>Command: AddBulk to IT + Salary->>Command: AddBulk to Operations + Salary->>Command: ExecuteParallelAsync() - Service->>Command: UseExecutionContext().AddBulk(...) - Service->>Command: UseExecutionContext().AddBulk(...) - Service->>Command: UseExecutionContext().AddBulk(...) - - Service->>Command: BuildAsync() - Service->>Command: ExecuteParallelAsync() - - Command->>Finance: Bulk operation - Command->>IT: Bulk operation - Command->>Ops: Bulk operation + Command->>Finance: Bulk insert + Command->>IT: Bulk insert + Command->>Ops: Bulk insert Finance-->>Command: Result IT-->>Command: Result Ops-->>Command: Result - - Command-->>Service: Structured execution result - Service-->>App: Employee DTOs + Command-->>Salary: Structured execution result ``` ---- - -## Core Code Shape - -Application services inject `ICommandFactory` as the command entry point. They create the required command or command builder inside the workflow instead of injecting a specific builder type at construction time. - -The demo reads employees from one context: +Core shape: ```csharp -var employeesQuery = await _asyncQuery - .UseExecutionContext() - .ReadWhereAsync(e => e.Salary > 10000m); -``` +var employeesQuery = await _queryFactory.CreateQueryAsync(); -It then builds a command pipeline across multiple contexts: +var employees = await employeesQuery + .UseDbExecutionContext() + .ReadWhereAsync(employee => employee.Salary > salaryThreshold); + +foreach (var employee in employees) +{ + employee.Salary += employee.Salary * salaryAdjustmentBaseRate; +} -```csharp var commandBuilder = await _commandFactory.CreateCommandBuilderAsync(); commandBuilder - .UseExecutionContext() - .AddBulk(employeesQuery, batchSize); + .UseDbExecutionContext() + .AddBulk(employees, batchSize); commandBuilder - .UseExecutionContext() - .AddBulk(employeesQuery, batchSize); + .UseDbExecutionContext() + .AddBulk(employees, batchSize); commandBuilder - .UseExecutionContext() - .AddBulk(employeesQuery, batchSize); -``` - -Then it builds and executes the pipeline in parallel: + .UseDbExecutionContext() + .AddBulk(employees, batchSize); -```csharp var command = await commandBuilder.BuildAsync(); - var commandResult = await command.ExecuteParallelAsync(); if (!commandResult.Success) { throw new InvalidOperationException( - $"Failed to process salary adjustments. {commandResult?.Exception?.Message}"); + $"Failed to process salary adjustments. {commandResult.Exception?.Message}"); } + +return commandResult.TotalAffected; ``` ---- +### Employee Performance Query -# DataArc Registration +`EmployeePerformanceService` demonstrates a cross-context read model. -Each EF Core context remains a normal `DbContext`. +It starts from HR employees above a rating threshold, joins related employees across Finance, IT, and Operations, then projects into a DTO. -DataArc treats each registered context as an execution boundary. +```csharp +var topRatedEmployeesQuery = await _queryFactory.CreateQueryAsync(); + +var topRatedEmployees = await topRatedEmployeesQuery + .UseDbExecutionContext(employee => employee.Rating > rating) + .Join( + bag => bag.Get()!.Id, + financeEmployee => financeEmployee.Id) + .Join( + bag => bag.Get()!.Id, + itEmployee => itEmployee.Id) + .Join( + bag => bag.Get()!.Id, + operationsEmployee => operationsEmployee.Id) + .Select(bag => new EmployeeDto + { + Id = bag.Get()!.Id, + Name = bag.Get()!.Name!, + Surname = bag.Get()!.Surname!, + Salary = bag.Get()!.Salary! + }) + .ToListAsync(); +``` + +This shows the query side of the same idea: + +```text +Multiple isolated contexts. +One explicit read model. +One application feature. +``` + +## DataArc Registration + +The demo keeps each EF Core context isolated. + +Each context is registered as a DataArc database execution context. ```csharp -services - .AddDataArcCore() - .UseEntityFrameworkCoreProviders(provider => +services.AddDataArcCore(); + +services.ConfigureDataArc(dataArc => +{ + dataArc.UseEntityFrameworkCore(ef => { - provider.ConfigureExecutionContexts(ctx => - { - ctx.AddDbContext(options => - options.UseSqlServer(financeConnectionString)); + ef.AddDbExecutionContext(options => + options.UseSqlServer(financeConnectionString)); - ctx.AddDbContext(options => - options.UseSqlServer(hrConnectionString)); + ef.AddDbExecutionContext(options => + options.UseSqlServer(hrConnectionString)); - ctx.AddDbContext(options => - options.UseSqlServer(itConnectionString)); + ef.AddDbExecutionContext(options => + options.UseSqlServer(itConnectionString)); - ctx.AddDbContext(options => - options.UseSqlServer(operationsConnectionString)); - }); + ef.AddDbExecutionContext(options => + options.UseSqlServer(operationsConnectionString)); }); +}); ``` -Each context implements the DataArc execution context contract: +A context interface marks the persistence boundary: ```csharp public interface IFinanceDbContext : IExecutionContext { } -public class FinanceDbContext - : DbContext, IFinanceDbContext +internal class FinanceDbContext : DbContext, IFinanceDbContext { public FinanceDbContext(DbContextOptions options) : base(options) @@ -284,74 +285,79 @@ public class FinanceDbContext } public DbSet Employer { get; set; } + public DbSet Employee { get; set; } } ``` ---- +## Database Setup -# Database Setup +The demo uses DataArc database setup to create the participating demo databases. -The demo also shows DataArc schema/database coordination through `IDatabaseFactory`. +The DDL/database builder is an advanced capability. In this demo, it is used only to prepare the databases required by the sample. ```csharp var databaseBuilder = _databaseFactory.CreateDatabaseBuilder(); -var db = databaseBuilder - .UseContext() - .UseContext() - .UseContext() - .UseContext() - .Build(applyChanges: true, generateScripts: true); +var databases = databaseBuilder + .IncludeDbContext() + .IncludeDbContext() + .IncludeDbContext() + .IncludeDbContext() + .Build(applyChanges: true, generateScripts: false); -db.ExecuteDrop(); -db.ExecuteCreate(); +databases.ExecuteDrop(); +databases.ExecuteCreate(); ``` -```mermaid -flowchart LR - A[IDatabaseFactory] --> X[CreateDatabaseBuilder] - X --> B[FinanceDbContext] - A --> C[HrDbContext] - A --> D[ItDbContext] - A --> E[OperationsDbContext] - - B --> F[Generate Scripts] - C --> F - D --> F - E --> F - - F --> G[Drop/Create Databases] -``` - ---- +`applyChanges: true` is explicit because database build operations can change or drop database structures. `generateScripts: false` keeps the demo setup focused on creating the databases rather than generating DDL scripts. -# Benchmark Snapshot +## Benchmark -The demo includes a BenchmarkDotNet benchmark that measures parallel bulk insert execution across four EF Core persistence boundaries: +The benchmark measures DataArc parallel bulk insert execution across four EF Core persistence boundaries: - `HrDbContext` - `FinanceDbContext` - `ItDbContext` - `OperationsDbContext` -Each benchmark run inserts the configured `RecordCount` into all four participating contexts. +Each benchmark case inserts `RecordCount` employees into each participating context. ```text -RecordCount = 250,000 -Participating DbContexts = 4 -Total inserted records = 1,000,000 +Total inserted records = RecordCount x 4 +Bulk batch size = 250,000 +``` + +Benchmark shape: + +```csharp +[Params(250_000, 500_000, 1_000_000)] +public int RecordCount { get; set; } + +[Params(250_000)] +public int BulkBatchSize { get; set; } ``` -The benchmark is intentionally focused on the execution layer. It builds one DataArc command pipeline and executes the work in parallel across all registered context boundaries. +Execution shape: ```csharp var commandBuilder = await _commandFactory.CreateCommandBuilderAsync(); -commandBuilder.UseExecutionContext().AddBulk(_employees, BulkBatchSize); -commandBuilder.UseExecutionContext().AddBulk(_employees, BulkBatchSize); -commandBuilder.UseExecutionContext().AddBulk(_employees, BulkBatchSize); -commandBuilder.UseExecutionContext().AddBulk(_employees, BulkBatchSize); +commandBuilder + .UseDbExecutionContext() + .AddBulk(_benchmarkEmployees, BulkBatchSize); + +commandBuilder + .UseDbExecutionContext() + .AddBulk(_benchmarkEmployees, BulkBatchSize); + +commandBuilder + .UseDbExecutionContext() + .AddBulk(_benchmarkEmployees, BulkBatchSize); + +commandBuilder + .UseDbExecutionContext() + .AddBulk(_benchmarkEmployees, BulkBatchSize); var command = await commandBuilder.BuildAsync(); var commandResult = await command.ExecuteParallelAsync(); @@ -359,7 +365,7 @@ var commandResult = await command.ExecuteParallelAsync(); ```mermaid flowchart LR - A[Generated Employee Data] --> B[DataArc Command Builder] + A[Benchmark Employee Data] --> B[DataArc Command Builder] B --> HR[HrDbContext Bulk Insert] B --> FIN[FinanceDbContext Bulk Insert] @@ -372,7 +378,7 @@ flowchart LR OPS --> R ``` -## Benchmark Environment +### Benchmark Environment ```text BenchmarkDotNet v0.15.8 @@ -383,45 +389,50 @@ Cores: 14 physical, 20 logical Runtime: .NET 8.0.27 JIT: RyuJIT x64 InvocationCount: 1 -IterationCount: 4 +IterationCount: 10 WarmupCount: 1 UnrollFactor: 1 Diagnosers: MemoryDiagnoser, ThreadingDiagnoser ``` -## Benchmark Results +### Benchmark Results -| Method | Record Count | Total Inserted Records | Mean | StdDev | Completed Work Items | Lock Contentions | Gen0 | Allocated | -|---|---:|---:|---:|---:|---:|---:|---:|---:| -| ExecuteParallelBulkInsertAsync | 10,000 | 40,000 | 193.7 ms | 4.16 ms | 2,952 | - | - | 10.04 MB | -| ExecuteParallelBulkInsertAsync | 100,000 | 400,000 | 657.2 ms | 77.40 ms | 30,844 | - | - | 97.02 MB | -| ExecuteParallelBulkInsertAsync | 250,000 | 1,000,000 | 2,350.7 ms | 52.27 ms | 78,184 | - | 10,000 | 242.4 MB | +| Method | Record Count | Bulk Batch Size | Total Inserted Records | Mean | StdDev | Completed Work Items | Lock Contentions | Gen0 | Allocated | +|---|---:|---:|---:|---:|---:|---:|---:|---:|---:| +| ExecuteParallelBulkInsertAsync | 250,000 | 250,000 | 1,000,000 | 1.520 s | 0.1270 s | 77,844 | - | 20,000 | 242.23 MB | +| ExecuteParallelBulkInsertAsync | 500,000 | 250,000 | 2,000,000 | 3.122 s | 0.4427 s | 156,401 | - | 40,000 | 484.37 MB | +| ExecuteParallelBulkInsertAsync | 1,000,000 | 250,000 | 4,000,000 | 5.580 s | 0.4736 s | 313,955 | - | 81,000 | 968.85 MB | -## Benchmark Trend +### Benchmark Trend ```mermaid xychart-beta title "Parallel bulk insert mean time" - x-axis ["40k", "400k", "1M"] - y-axis "Mean time in ms" 0 --> 2500 - bar [193.7, 657.2, 2350.7] + x-axis ["1M", "2M", "4M"] + y-axis "Mean time in seconds" 0 --> 6 + bar [1.520, 3.122, 5.580] ``` ```mermaid xychart-beta title "Managed memory allocated per operation" - x-axis ["40k", "400k", "1M"] - y-axis "Allocated MB" 0 --> 260 - bar [10.04, 97.02, 242.4] + x-axis ["1M", "2M", "4M"] + y-axis "Allocated MB" 0 --> 1000 + bar [242.23, 484.37, 968.85] ``` -## What This Shows +### Benchmark Validation -At the largest measured size in this demo benchmark, DataArc.EntityFrameworkCore inserted 1,000,000 total records across four participating EF Core contexts in approximately 2.35 seconds, allocating approximately 242 MB of managed memory for the operation. +After the 4,000,000-record benchmark case completed, each participating database contained the expected 1,000,000 records. -The useful engineering signal is not only the elapsed time. The stronger signal is that the workload is executed through a single explicit command pipeline, across multiple context boundaries, with structured success/failure reporting and no reported lock contentions in this run. +```text +HrDbContext database: 1,000,000 Employee records +FinanceDbContext database: 1,000,000 Employee records +ItDbContext database: 1,000,000 Employee records +OperationsDbContext database: 1,000,000 Employee records +``` -## Benchmark Notes +### Benchmark Notes These results are workload-specific evidence, not a universal performance guarantee. @@ -439,12 +450,11 @@ Results depend on: - database state before each iteration - transaction behavior -The benchmark resets the participating databases during iteration setup, generates fresh employee data for each `RecordCount`, builds the DataArc command pipeline, executes it in parallel, and returns the total affected record count. ---- +The useful engineering signal is not only elapsed time. The benchmark runs through a single explicit command pipeline, across four context boundaries, with structured success/failure reporting and no reported lock contentions in this run. -# Why Add DataArc.EntityFrameworkCore To Your Engineering Toolkit? +## Why Add DataArc.EntityFrameworkCore To Your Engineering Toolkit? -## 1. It Preserves Modular EF Core Boundaries +### 1. Preserve Modular EF Core Boundaries Many real systems use one normalized database while still needing separate logical EF Core boundaries. @@ -454,38 +464,12 @@ ProductsContext PackagingContext LicensingContext PricingContext -IntersectionContext AuditContext ``` -Normal EF Core works well inside one context. The pain starts when workflows must cross context boundaries. - -DataArc lets teams preserve modular context boundaries without collapsing everything into one giant `DbContext`. - -```mermaid -flowchart LR - DB[(One Normalized Database)] - - DB --> I[IdentityContext] - DB --> P[ProductsContext] - DB --> PK[PackagingContext] - DB --> L[LicensingContext] - DB --> X[IntersectionContext] - DB --> A[AuditContext] - - I --> DA[DataArc] - P --> DA - PK --> DA - L --> DA - X --> DA - A --> DA - - DA --> W[Composed Workflow] -``` - ---- +DataArc lets teams preserve modular context boundaries without collapsing every model into one giant `DbContext`. -## 2. It Gives Cross-Context Workflows A Real Execution Model +### 2. Give Cross-Context Workflows An Execution Model Real workflows often need data from multiple persistence boundaries: @@ -497,59 +481,38 @@ Real workflows often need data from multiple persistence boundaries: DataArc gives those workflows a C# execution model across contexts. -```mermaid -flowchart TB - A[Workflow] --> B[Query Context A] - A --> C[Query Context B] - A --> D[Command Context C] - A --> E[Command Context D] +### 3. Coordinate Parallel Operations - B --> F[Composed Read Model] - C --> F - D --> G[Command Pipeline] - E --> G -``` +EF Core can run multiple `DbContext` instances in parallel when each path has its own context instance and lifecycle. ---- +DataArc turns the coordination into a command pipeline with structured results: -## 3. It Supports Coordinated Parallel Operations - -Normal EF Core can run multiple `DbContext` instances in parallel when each path has its own context instance and lifecycle. - -The developer still has to manage: +```csharp +var commandBuilder = await _commandFactory.CreateCommandBuilderAsync(); -- context creation -- disposal -- transaction behavior -- `Task.WhenAll` -- exception handling -- rollback handling -- result aggregation +commandBuilder + .UseDbExecutionContext() + .AddBulk(financeEmployees, batchSize); -DataArc turns this into a command pipeline: +commandBuilder + .UseDbExecutionContext() + .AddBulk(itEmployees, batchSize); -```csharp -var commands = await commandBuilder - .UseExecutionContext() - .AddBulk(financeEmployees, batchSize) - .UseExecutionContext() - .AddBulk(itEmployees, batchSize) - .UseExecutionContext() - .AddBulk(operationsEmployees, batchSize) - .BuildAsync(); +commandBuilder + .UseDbExecutionContext() + .AddBulk(operationsEmployees, batchSize); -var result = await commands.ExecuteParallelAsync(); +var command = await commandBuilder.BuildAsync(); +var result = await command.ExecuteParallelAsync(); ``` ---- - -## 4. It Separates Reads From Writes At The Execution Layer +### 4. Separate Reads From Writes At The Execution Layer DataArc separates reads and writes through dedicated query and command APIs. ```mermaid flowchart LR - A[Application Service] --> Q[IAsyncQuery] + A[Application Service] --> Q[IQueryFactory] A --> C[ICommandFactory] Q --> R[Read Models] @@ -559,11 +522,9 @@ flowchart LR W --> X[Execution Result] ``` -This gives teams CQRS-style execution discipline at the persistence layer without forcing every operation into a request/handler class. +This gives teams CQRS-style execution discipline at the persistence layer without requiring every operation to become a request/handler class. ---- - -## 5. It Reduces Repository-Per-Table Sprawl +### 5. Reduce Repository Coordination Sprawl A repository-per-table design can grow quickly: @@ -578,7 +539,7 @@ ActivationRepository ProductPackageRepository ``` -The workflow still needs to stitch those repositories together. +The workflow still needs to coordinate those repositories. DataArc lets teams compose directly against known EF Core context boundaries through command/query builders. @@ -590,42 +551,20 @@ Orchestrators are workflow boundaries. Repositories can still be used where they add value. -The point is that DataArc gives teams another option when repository coordination becomes the problem. - ---- - -## 6. It Reduces Handler, Query, And Command Permutation Sprawl - -Handler-heavy designs can multiply for small data access variations: - -```text -GetUserByEmailQuery -GetUserByAccessFailedCountQuery -GetEmailByUserIdQuery -UpdateEmailConfirmedCommand -CreateUserCommand -DeleteUserCommand -``` - -DataArc reduces the need to create a separate handler, query, command, and repository method for every small data access variation. - -The operation can be expressed directly through the persistence execution layer. - ---- - -## 7. It Supports Cross-Context Read Models +### 6. Support Cross-Context Read Models DataArc supports cross-context read model composition using a bag pattern. Each join adds data to the bag. The final projection creates a workflow-specific read model. ```csharp -var rows = await asyncQuery - .UseExecutionContext(x => x.UserId == userId) - .Join( +var rows = await query + .UseDbExecutionContext( + userProduct => userProduct.UserId == userId) + .Join( bag => bag.Get()!.ProductId, product => product.Id) - .Join( + .Join( bag => bag.Get()!.PackageId, package => package.Id) .Select(bag => new ProductPackageRow @@ -636,25 +575,12 @@ var rows = await asyncQuery .ToListAsync(); ``` -This is useful when a read model spans multiple context boundaries. - ---- - -## 8. It Improves Observability Through Structured Results - -Normal EF Core workflows often repeat: - -- try/catch logic -- transaction handling -- logging -- timing -- failure reporting -- result mapping +### 7. Improve Observability Through Structured Results DataArc wraps execution in structured results: ```csharp -var result = await command.CommitTransactionAsync(); +var result = await command.ExecuteParallelAsync(); if (!result.Success) { @@ -673,21 +599,7 @@ DataArc results are designed to support: - transaction-safe command flows - performance visibility -```mermaid -flowchart LR - A[Command Pipeline] --> B[Execute] - B --> C{Success?} - C -->|Yes| D[Commit Result] - C -->|No| E[Failure Result] - E --> F[Exception Details] - E --> G[Context Metrics] - E --> H[Logging] - D --> I[Overall Metrics] -``` - ---- - -## 9. It Is Built For High-Volume Scheduled Workloads +### 8. Support High-Volume Scheduled Workloads Many systems run large data operations on a schedule: @@ -709,9 +621,7 @@ These jobs may not run every minute. When they run, they affect memory, CPU, dat DataArc is built for those moments. ---- - -## 10. It Gives EF Core A Path From Simple CRUD To Database Orchestration +## When To Use Direct DbContext Use `DbContext` directly when the operation is simple and stays inside one persistence boundary. @@ -736,115 +646,45 @@ DbContext is excellent inside one persistence boundary. DataArc coordinates execution across boundaries. ``` ---- - -# Why Not Just Use DbContext? - -Use `DbContext` directly when the operation is simple and stays inside one persistence boundary. - -DataArc becomes valuable when the workflow needs more than a single-context unit of work. - -```text -Direct DbContext for simple persistence. -DataArc for coordinated execution. -``` - ---- - -# Why Not Repositories? - -Repositories are still valid. - -DataArc does not require teams to abandon repositories. - -The pain DataArc addresses is repository coordination sprawl, where a service starts juggling many repositories, context boundaries, transactions, bulk operations, and results. - -DataArc can live under repositories, inside services, or inside orchestrators. - ---- - -# Why Not MediatR? - -MediatR dispatches messages to handlers. - -DataArc coordinates EF Core execution across persistence boundaries. - -DataArc gives command/query separation at the database execution level without forcing a request/handler class for every small operation. - ---- - -# Why Not Bulk Libraries? - -Bulk libraries are useful when the task is one isolated bulk operation. - -DataArc focuses on bulk operations inside broader execution workflows: - -- context boundaries -- command pipelines -- parallel execution -- transaction handling -- structured results -- performance visibility - ---- - -# Where Orchestrators Fit +## Where Orchestrators Fit DataArc.EntityFrameworkCore can be used directly in services, as this demo shows. -When workflows become larger, they need a dedicated home. +When workflows become larger, move the workflow into **DataArc.Orchestrator**. + +For post-execution outcomes, use **DataArc.Observer**. ```mermaid flowchart LR A[Controller / Endpoint] --> B[Service] B --> C[DataArc.EntityFrameworkCore] - B --> D[Orchestrator] + B --> D[DataArc.Orchestrator] D --> C - D --> E[Observer] + D --> E[DataArc.Observer] ``` -## Database Orchestration Needs A Home +## Running The Demo -For simple workflows, use DataArc.EntityFrameworkCore directly. - -For larger workflows, move the workflow into **DataArc.Orchestrator**. - -For post-execution outcomes, use **DataArc.Observer**. - -```text -DataArc.EntityFrameworkCore - ↓ -Database Orchestration Needs A Home - ↓ -DataArc.Orchestrator - ↓ -DataArc.Observer -``` - ---- - -# Running The Demo - -## 1. Configure Connection Strings +### 1. Configure Connection Strings Update the SQL Server connection strings in `PersistenceRegistration.cs`. ```csharp -ctx.AddDbContext(options => - options.UseSqlServer("Server=YOUR_SERVER;Database=FinanceDb;Integrated Security=true;TrustServerCertificate=True;")); +var financeConnectionString = + "Server=YOUR_SERVER;Database=FinanceDb;Integrated Security=true;TrustServerCertificate=True;"; -ctx.AddDbContext(options => - options.UseSqlServer("Server=YOUR_SERVER;Database=HrDb;Integrated Security=true;TrustServerCertificate=True;")); +var hrConnectionString = + "Server=YOUR_SERVER;Database=HrDb;Integrated Security=true;TrustServerCertificate=True;"; -ctx.AddDbContext(options => - options.UseSqlServer("Server=YOUR_SERVER;Database=ItDb;Integrated Security=true;TrustServerCertificate=True;")); +var itConnectionString = + "Server=YOUR_SERVER;Database=ItDb;Integrated Security=true;TrustServerCertificate=True;"; -ctx.AddDbContext(options => - options.UseSqlServer("Server=YOUR_SERVER;Database=OperationsDb;Integrated Security=true;TrustServerCertificate=True;")); +var operationsConnectionString = + "Server=YOUR_SERVER;Database=OperationsDb;Integrated Security=true;TrustServerCertificate=True;"; ``` -## 2. Run The Application +### 2. Run The Application ```bash dotnet run @@ -854,16 +694,36 @@ The application will: 1. Delete existing demo databases. 2. Create demo databases. -3. Seed HR data. -4. Run the finance salary adjustment workflow. +3. Seed HR employee data. +4. Run the salary adjustment workflow. 5. Bulk distribute adjusted employee data into Finance, IT, and Operations. -6. Print the number of processed employees. +6. Query top-rated employee details. +7. Print a summary. + +Expected summary shape: ---- +```text +Database deleted successfully. +Database created successfully. +Database seeded successfully. + +Top Rated Employee: Name12345 Surname12345, Salary: 151234.56, Number of top rated employees: 1234 +Press any key to exit. +``` + +## Running The Benchmark + +From the benchmark project: -# Trial Path +```bash +dotnet run -c Release +``` + +BenchmarkDotNet will generate detailed output under the benchmark artifacts folder. -DataArc.EntityFrameworkCore is intended to support a 7-day trial so teams can test real workloads before purchasing. +## Trial Path + +DataArc.EntityFrameworkCore supports a 14-day trial so teams can test real workloads before purchasing. Use the trial to validate: @@ -874,9 +734,7 @@ Use the trial to validate: - scheduled workload behavior - logging and execution reporting ---- - -# Summary +## Summary DataArc.EntityFrameworkCore gives EF Core a deterministic execution layer for modular, high-throughput, multi-context systems. @@ -886,9 +744,9 @@ It helps teams: - compose cross-context workflows - execute coordinated command pipelines - run parallel operations -- reduce repository and handler sprawl +- reduce repository and handler coordination sprawl - build cross-context read models -- run bulk workloads with lower memory pressure +- run bulk workloads with controlled memory behavior - return structured execution results - log and measure workflow execution diff --git a/nuget.config b/nuget.config new file mode 100644 index 0000000..b1de076 --- /dev/null +++ b/nuget.config @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file