(feature): sagas builder, general improvements
This commit is contained in:
		
							parent
							
								
									ab763d7c8f
								
							
						
					
					
						commit
						ed5a333541
					
				
							
								
								
									
										42
									
								
								src/MaksIT.Core.Tests/LoggerHelper.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/MaksIT.Core.Tests/LoggerHelper.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,42 @@ | |||||||
|  | using Microsoft.Extensions.DependencyInjection; | ||||||
|  | using Microsoft.Extensions.Hosting; | ||||||
|  | using Microsoft.Extensions.Logging; | ||||||
|  | 
 | ||||||
|  | using MaksIT.Core.Logging; | ||||||
|  | 
 | ||||||
|  | namespace MaksIT.Core.Tests; | ||||||
|  | 
 | ||||||
|  | /// <summary> | ||||||
|  | /// Provides helper methods for creating loggers in tests. | ||||||
|  | /// </summary> | ||||||
|  | public static class LoggerHelper | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     /// Creates a console logger for testing purposes. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <returns>An instance of <see cref="ILogger"/> configured for console logging.</returns> | ||||||
|  |     public static ILogger CreateConsoleLogger() | ||||||
|  |     { | ||||||
|  |         var serviceCollection = new ServiceCollection(); | ||||||
|  | 
 | ||||||
|  |         // Use the reusable TestHostEnvironment for testing | ||||||
|  |         serviceCollection.AddSingleton<IHostEnvironment>(sp => | ||||||
|  |             new TestHostEnvironment | ||||||
|  |             { | ||||||
|  |                 EnvironmentName = Environments.Development, | ||||||
|  |                 ApplicationName = "TestApp", | ||||||
|  |                 ContentRootPath = Directory.GetCurrentDirectory() | ||||||
|  |             }); | ||||||
|  | 
 | ||||||
|  |         serviceCollection.AddLogging(builder => | ||||||
|  |         { | ||||||
|  |             var env = serviceCollection.BuildServiceProvider().GetRequiredService<IHostEnvironment>(); | ||||||
|  |             builder.ClearProviders(); | ||||||
|  |             builder.AddConsole(env); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         var provider = serviceCollection.BuildServiceProvider(); | ||||||
|  |         var factory = provider.GetRequiredService<ILoggerFactory>(); | ||||||
|  |         return factory.CreateLogger("TestLogger"); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -14,9 +14,10 @@ | |||||||
|       <PrivateAssets>all</PrivateAssets> |       <PrivateAssets>all</PrivateAssets> | ||||||
|       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> |       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||||||
|     </PackageReference> |     </PackageReference> | ||||||
|     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" /> |     <PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.3.0" /> | ||||||
|  |     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.0" /> | ||||||
|     <PackageReference Include="xunit" Version="2.9.3" /> |     <PackageReference Include="xunit" Version="2.9.3" /> | ||||||
|     <PackageReference Include="xunit.runner.visualstudio" Version="3.1.0"> |     <PackageReference Include="xunit.runner.visualstudio" Version="3.1.5"> | ||||||
|       <PrivateAssets>all</PrivateAssets> |       <PrivateAssets>all</PrivateAssets> | ||||||
|       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> |       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||||||
|     </PackageReference> |     </PackageReference> | ||||||
| @ -30,4 +31,8 @@ | |||||||
|     <Using Include="Xunit" /> |     <Using Include="Xunit" /> | ||||||
|   </ItemGroup> |   </ItemGroup> | ||||||
| 
 | 
 | ||||||
|  |   <ItemGroup> | ||||||
|  |     <Folder Include="Helpers\" /> | ||||||
|  |   </ItemGroup> | ||||||
|  | 
 | ||||||
| </Project> | </Project> | ||||||
|  | |||||||
							
								
								
									
										190
									
								
								src/MaksIT.Core.Tests/Sagas/LocalSagaTests.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										190
									
								
								src/MaksIT.Core.Tests/Sagas/LocalSagaTests.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,190 @@ | |||||||
|  | using MaksIT.Core.Logging; | ||||||
|  | using MaksIT.Core.Sagas; | ||||||
|  | using Microsoft.Extensions.Logging; | ||||||
|  | 
 | ||||||
|  | namespace MaksIT.Core.Tests.Sagas; | ||||||
|  | 
 | ||||||
|  | public class LocalSagaTests | ||||||
|  | { | ||||||
|  |     [Fact] | ||||||
|  |     public async Task LocalSagaBuilder_ShouldBuildSagaWithSteps() | ||||||
|  |     { | ||||||
|  |         // Arrange | ||||||
|  |         var logger = LoggerHelper.CreateConsoleLogger(); | ||||||
|  |         var builder = new LocalSagaBuilder().WithLogger(logger); | ||||||
|  |         var stepExecuted = false; | ||||||
|  | 
 | ||||||
|  |         builder.AddAction( | ||||||
|  |             "TestStep", | ||||||
|  |             async (ctx, ct) => | ||||||
|  |             { | ||||||
|  |                 stepExecuted = true; | ||||||
|  |                 await Task.CompletedTask; | ||||||
|  |             }); | ||||||
|  | 
 | ||||||
|  |         var saga = builder.Build(); | ||||||
|  | 
 | ||||||
|  |         // Act | ||||||
|  |         await saga.ExecuteAsync(); | ||||||
|  | 
 | ||||||
|  |         // Assert | ||||||
|  |         Assert.True(stepExecuted, "The step should have been executed."); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     [Fact] | ||||||
|  |     public async Task LocalSaga_ShouldCompensateOnFailure() | ||||||
|  |     { | ||||||
|  |         // Arrange | ||||||
|  |         var logger = LoggerHelper.CreateConsoleLogger(); | ||||||
|  |         var builder = new LocalSagaBuilder().WithLogger(logger); | ||||||
|  |         var compensationCalled = false; | ||||||
|  | 
 | ||||||
|  |         builder.AddAction( | ||||||
|  |             "FailingStep", | ||||||
|  |             async (ctx, ct) => | ||||||
|  |             { | ||||||
|  |                 throw new InvalidOperationException("Step failed"); | ||||||
|  |             }, | ||||||
|  |             async (ctx, ct) => | ||||||
|  |             { | ||||||
|  |                 compensationCalled = true; | ||||||
|  |                 await Task.CompletedTask; | ||||||
|  |             }); | ||||||
|  | 
 | ||||||
|  |         var saga = builder.Build(); | ||||||
|  | 
 | ||||||
|  |         // Act & Assert | ||||||
|  |         await Assert.ThrowsAsync<InvalidOperationException>(() => saga.ExecuteAsync()); | ||||||
|  |         Assert.True(compensationCalled, "Compensation should have been called."); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     [Fact] | ||||||
|  |     public async Task LocalSaga_ShouldSkipConditionalSteps() | ||||||
|  |     { | ||||||
|  |         // Arrange | ||||||
|  |         var logger = LoggerHelper.CreateConsoleLogger(); | ||||||
|  |         var builder = new LocalSagaBuilder().WithLogger(logger); | ||||||
|  |         var stepExecuted = false; | ||||||
|  | 
 | ||||||
|  |         builder.AddActionIf( | ||||||
|  |             ctx => false, | ||||||
|  |             "SkippedStep", | ||||||
|  |             async (ctx, ct) => | ||||||
|  |             { | ||||||
|  |                 stepExecuted = true; | ||||||
|  |                 await Task.CompletedTask; | ||||||
|  |             }); | ||||||
|  | 
 | ||||||
|  |         var saga = builder.Build(); | ||||||
|  | 
 | ||||||
|  |         // Act | ||||||
|  |         await saga.ExecuteAsync(); | ||||||
|  | 
 | ||||||
|  |         // Assert | ||||||
|  |         Assert.False(stepExecuted, "The step should have been skipped."); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     [Fact] | ||||||
|  |     public async Task LocalSaga_ShouldLogExecution() | ||||||
|  |     { | ||||||
|  |         // Arrange | ||||||
|  |         var logger = LoggerHelper.CreateConsoleLogger(); | ||||||
|  |         var builder = new LocalSagaBuilder().WithLogger(logger); | ||||||
|  | 
 | ||||||
|  |         builder.AddAction( | ||||||
|  |             "LoggingStep", | ||||||
|  |             async (ctx, ct) => await Task.CompletedTask); | ||||||
|  | 
 | ||||||
|  |         var saga = builder.Build(); | ||||||
|  | 
 | ||||||
|  |         // Act | ||||||
|  |         await saga.ExecuteAsync(); | ||||||
|  |         // No assertion on logs, but output will be visible in test runner console | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     [Fact] | ||||||
|  |     public async Task LocalSaga_ShouldRestorePreviousStateOnError() | ||||||
|  |     { | ||||||
|  |         // Arrange | ||||||
|  |         var logger = LoggerHelper.CreateConsoleLogger(); | ||||||
|  |         var builder = new LocalSagaBuilder().WithLogger(logger); | ||||||
|  |         var context = new LocalSagaContext(); | ||||||
|  |         context.Set("state", "initial"); | ||||||
|  | 
 | ||||||
|  |         builder.AddAction( | ||||||
|  |             "ModifyStateStep", | ||||||
|  |             async (ctx, ct) => | ||||||
|  |             { | ||||||
|  |                 ctx.Set("state", "modified"); | ||||||
|  |                 await Task.CompletedTask; | ||||||
|  |             }, | ||||||
|  |             async (ctx, ct) => | ||||||
|  |             { | ||||||
|  |                 ctx.Set("state", "initial"); | ||||||
|  |                 await Task.CompletedTask; | ||||||
|  |             }); | ||||||
|  | 
 | ||||||
|  |         builder.AddAction( | ||||||
|  |             "FailingStep", | ||||||
|  |             async (ctx, ct) => | ||||||
|  |             { | ||||||
|  |                 throw new InvalidOperationException("Step failed"); | ||||||
|  |             }); | ||||||
|  | 
 | ||||||
|  |         var saga = builder.Build(); | ||||||
|  | 
 | ||||||
|  |         // Act & Assert | ||||||
|  |         await Assert.ThrowsAsync<InvalidOperationException>(() => saga.ExecuteAsync(context)); | ||||||
|  |         Assert.Equal("initial", context.Get<string>("state")); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     [Fact] | ||||||
|  |     public async Task LocalSaga_ShouldHandleMultipleCompensations() | ||||||
|  |     { | ||||||
|  |         // Arrange | ||||||
|  |         var logger = LoggerHelper.CreateConsoleLogger(); | ||||||
|  |         var builder = new LocalSagaBuilder().WithLogger(logger); | ||||||
|  |         var context = new LocalSagaContext(); | ||||||
|  |         var compensationLog = new List<string>(); | ||||||
|  | 
 | ||||||
|  |         builder.AddAction( | ||||||
|  |             "Step1", | ||||||
|  |             async (ctx, ct) => | ||||||
|  |             { | ||||||
|  |                 ctx.Set("step1", true); | ||||||
|  |                 await Task.CompletedTask; | ||||||
|  |             }, | ||||||
|  |             async (ctx, ct) => | ||||||
|  |             { | ||||||
|  |                 compensationLog.Add("Step1 compensated"); | ||||||
|  |                 await Task.CompletedTask; | ||||||
|  |             }); | ||||||
|  | 
 | ||||||
|  |         builder.AddAction( | ||||||
|  |             "Step2", | ||||||
|  |             async (ctx, ct) => | ||||||
|  |             { | ||||||
|  |                 ctx.Set("step2", true); | ||||||
|  |                 await Task.CompletedTask; | ||||||
|  |             }, | ||||||
|  |             async (ctx, ct) => | ||||||
|  |             { | ||||||
|  |                 compensationLog.Add("Step2 compensated"); | ||||||
|  |                 await Task.CompletedTask; | ||||||
|  |             }); | ||||||
|  | 
 | ||||||
|  |         builder.AddAction( | ||||||
|  |             "FailingStep", | ||||||
|  |             async (ctx, ct) => | ||||||
|  |             { | ||||||
|  |                 throw new InvalidOperationException("Step failed"); | ||||||
|  |             }); | ||||||
|  | 
 | ||||||
|  |         var saga = builder.Build(); | ||||||
|  | 
 | ||||||
|  |         // Act & Assert | ||||||
|  |         await Assert.ThrowsAsync<InvalidOperationException>(() => saga.ExecuteAsync(context)); | ||||||
|  |         Assert.Contains("Step2 compensated", compensationLog); | ||||||
|  |         Assert.Contains("Step1 compensated", compensationLog); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										15
									
								
								src/MaksIT.Core.Tests/TestHostEnvironment.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/MaksIT.Core.Tests/TestHostEnvironment.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | |||||||
|  | using Microsoft.Extensions.FileProviders; | ||||||
|  | using Microsoft.Extensions.Hosting; | ||||||
|  | 
 | ||||||
|  | namespace MaksIT.Core.Tests; | ||||||
|  | 
 | ||||||
|  | /// <summary> | ||||||
|  | /// Simple implementation of IHostEnvironment for testing purposes. | ||||||
|  | /// </summary> | ||||||
|  | public class TestHostEnvironment : IHostEnvironment | ||||||
|  | { | ||||||
|  |     public string EnvironmentName { get; set; } = Environments.Production; | ||||||
|  |     public string ApplicationName { get; set; } = ""; | ||||||
|  |     public string ContentRootPath { get; set; } = ""; | ||||||
|  |     public IFileProvider ContentRootFileProvider { get; set; } = new NullFileProvider(); | ||||||
|  | } | ||||||
| @ -8,7 +8,7 @@ | |||||||
| 
 | 
 | ||||||
|     <!-- NuGet package metadata --> |     <!-- NuGet package metadata --> | ||||||
|     <PackageId>MaksIT.Core</PackageId> |     <PackageId>MaksIT.Core</PackageId> | ||||||
|     <Version>1.4.7</Version> |     <Version>1.4.8</Version> | ||||||
|     <Authors>Maksym Sadovnychyy</Authors> |     <Authors>Maksym Sadovnychyy</Authors> | ||||||
|     <Company>MAKS-IT</Company> |     <Company>MAKS-IT</Company> | ||||||
|     <Product>MaksIT.Core</Product> |     <Product>MaksIT.Core</Product> | ||||||
|  | |||||||
							
								
								
									
										88
									
								
								src/MaksIT.Core/Sagas/LocalSaga.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								src/MaksIT.Core/Sagas/LocalSaga.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,88 @@ | |||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using System.Linq; | ||||||
|  | using System.Text; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  | using Microsoft.Extensions.Logging; | ||||||
|  | 
 | ||||||
|  | namespace MaksIT.Core.Sagas; | ||||||
|  | /// <summary> | ||||||
|  | /// Executable local saga with LIFO compensation on failure. | ||||||
|  | /// </summary> | ||||||
|  | public sealed class LocalSaga { | ||||||
|  |   private readonly IReadOnlyList<ILocalSagaStep> _pipeline; | ||||||
|  |   private readonly ILogger _logger; | ||||||
|  | 
 | ||||||
|  |   internal LocalSaga( | ||||||
|  |     IReadOnlyList<ILocalSagaStep> pipeline, | ||||||
|  |     ILogger logger) { | ||||||
|  |     _pipeline = pipeline; | ||||||
|  |     _logger = logger; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   public async Task ExecuteAsync(LocalSagaContext? context = null, CancellationToken cancellationToken = default) | ||||||
|  |   { | ||||||
|  |       var ctx = context ?? new LocalSagaContext(); | ||||||
|  |       var executedStack = new Stack<ILocalSagaStep>(); | ||||||
|  | 
 | ||||||
|  |       for (int i = 0; i < _pipeline.Count; i++) | ||||||
|  |       { | ||||||
|  |           cancellationToken.ThrowIfCancellationRequested(); | ||||||
|  | 
 | ||||||
|  |           var step = _pipeline[i]; | ||||||
|  |           try | ||||||
|  |           { | ||||||
|  |               _logger.LogInformation($"LocalSaga: executing step [{i + 1}/{_pipeline.Count}] '{step.Name}'"); | ||||||
|  |               var ran = await step.ExecuteAsync(ctx, cancellationToken); | ||||||
|  |               if (ran) | ||||||
|  |                   executedStack.Push(step); // Ensure step is pushed if it ran successfully | ||||||
|  |               else | ||||||
|  |                   _logger.LogInformation($"LocalSaga: skipped step '{step.Name}'"); | ||||||
|  |           } | ||||||
|  |           catch (Exception ex) | ||||||
|  |           { | ||||||
|  |               _logger.LogError(ex, $"LocalSaga: step '{step.Name}' failed"); | ||||||
|  |               executedStack.Push(step); // Push the step to ensure compensation is triggered | ||||||
|  |               await CompensateAsync(executedStack, ctx, cancellationToken); | ||||||
|  |               throw; | ||||||
|  |           } | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       _logger.LogInformation("LocalSaga: completed successfully"); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private async Task CompensateAsync( | ||||||
|  |     Stack<ILocalSagaStep> executedStack, | ||||||
|  |     LocalSagaContext ctx, | ||||||
|  |     CancellationToken ct) { | ||||||
|  |     _logger.LogInformation("LocalSaga: starting compensation"); | ||||||
|  | 
 | ||||||
|  |     var compensationErrors = new List<Exception>(); | ||||||
|  |     int totalSteps = executedStack.Count; | ||||||
|  | 
 | ||||||
|  |     try | ||||||
|  |     { | ||||||
|  |         while (executedStack.Count > 0) | ||||||
|  |         { | ||||||
|  |             var step = executedStack.Pop(); | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 _logger.LogInformation($"LocalSaga: compensating step '{step.Name}' ({totalSteps - executedStack.Count}/{totalSteps})"); | ||||||
|  |                 await step.CompensateAsync(ctx, ct); | ||||||
|  |             } | ||||||
|  |             catch (Exception ex) | ||||||
|  |             { | ||||||
|  |                 _logger.LogError(ex, $"LocalSaga: compensation of step '{step.Name}' failed"); | ||||||
|  |                 compensationErrors.Add(ex); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     finally | ||||||
|  |     { | ||||||
|  |         _logger.LogInformation("LocalSaga: compensation finished"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (compensationErrors.Count > 0) | ||||||
|  |         throw new AggregateException("One or more compensation steps failed.", compensationErrors); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										73
									
								
								src/MaksIT.Core/Sagas/LocalSagaBuilder.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								src/MaksIT.Core/Sagas/LocalSagaBuilder.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,73 @@ | |||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using System.Linq; | ||||||
|  | using System.Text; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  | using Microsoft.Extensions.Logging; | ||||||
|  | 
 | ||||||
|  | namespace MaksIT.Core.Sagas; | ||||||
|  | 
 | ||||||
|  | /// <summary> | ||||||
|  | /// Fluent builder to compose a local saga (exception-based failures). | ||||||
|  | /// </summary> | ||||||
|  | public sealed class LocalSagaBuilder { | ||||||
|  |   private readonly List<ILocalSagaStep> _pipeline = new(); | ||||||
|  |   private ILogger? _logger; | ||||||
|  | 
 | ||||||
|  |   public LocalSagaBuilder WithLogger(ILogger logger) { | ||||||
|  |     _logger = logger; | ||||||
|  |     return this; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   public LocalSagaBuilder AddAction( | ||||||
|  |     string name, | ||||||
|  |     Func<LocalSagaContext, CancellationToken, Task> execute, | ||||||
|  |     Func<LocalSagaContext, CancellationToken, Task>? compensate = null) { | ||||||
|  |     _pipeline.Add(new LocalSagaStep<Unit>( | ||||||
|  |       name, | ||||||
|  |       async (c, ct) => { await execute(c, ct); return Unit.Value; }, | ||||||
|  |       compensate, | ||||||
|  |       predicate: null, | ||||||
|  |       outputKey: null)); | ||||||
|  |     return this; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   public LocalSagaBuilder AddActionIf( | ||||||
|  |     Func<LocalSagaContext, bool> predicate, | ||||||
|  |     string name, | ||||||
|  |     Func<LocalSagaContext, CancellationToken, Task> execute, | ||||||
|  |     Func<LocalSagaContext, CancellationToken, Task>? compensate = null) { | ||||||
|  |     _pipeline.Add(new LocalSagaStep<Unit>( | ||||||
|  |       $"[conditional] {name}", | ||||||
|  |       async (c, ct) => { await execute(c, ct); return Unit.Value; }, | ||||||
|  |       compensate, | ||||||
|  |       predicate, | ||||||
|  |       outputKey: null)); | ||||||
|  |     return this; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   public LocalSagaBuilder AddStep<T>( | ||||||
|  |     string name, | ||||||
|  |     Func<LocalSagaContext, CancellationToken, Task<T>> execute, | ||||||
|  |     string? outputKey = null, | ||||||
|  |     Func<LocalSagaContext, CancellationToken, Task>? compensate = null) { | ||||||
|  |     _pipeline.Add(new LocalSagaStep<T>(name, execute, compensate, predicate: null, outputKey: outputKey)); | ||||||
|  |     return this; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   public LocalSagaBuilder AddStepIf<T>( | ||||||
|  |     Func<LocalSagaContext, bool> predicate, | ||||||
|  |     string name, | ||||||
|  |     Func<LocalSagaContext, CancellationToken, Task<T>> execute, | ||||||
|  |     string? outputKey = null, | ||||||
|  |     Func<LocalSagaContext, CancellationToken, Task>? compensate = null) { | ||||||
|  |     _pipeline.Add(new LocalSagaStep<T>($"[conditional] {name}", execute, compensate, predicate, outputKey)); | ||||||
|  |     return this; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   public LocalSaga Build() { | ||||||
|  |     if (_logger == null) | ||||||
|  |       throw new InvalidOperationException("Logger must be provided via WithLogger()."); | ||||||
|  |     return new LocalSaga(_pipeline, _logger); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										25
									
								
								src/MaksIT.Core/Sagas/LocalSagaContext.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/MaksIT.Core/Sagas/LocalSagaContext.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,25 @@ | |||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using System.Linq; | ||||||
|  | using System.Text; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  | 
 | ||||||
|  | namespace MaksIT.Core.Sagas; | ||||||
|  | 
 | ||||||
|  | /// <summary> | ||||||
|  | /// Shared context to pass values between steps without tight coupling. | ||||||
|  | /// </summary> | ||||||
|  | public sealed class LocalSagaContext { | ||||||
|  |   private readonly Dictionary<string, object?> _bag = new(StringComparer.Ordinal); | ||||||
|  | 
 | ||||||
|  |   public T? Get<T>(string key) { | ||||||
|  |     return _bag.TryGetValue(key, out var v) && v is T t ? t : default; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   public LocalSagaContext Set<T>(string key, T value) { | ||||||
|  |     _bag[key] = value; | ||||||
|  |     return this; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   public bool Contains(string key) => _bag.ContainsKey(key); | ||||||
|  | } | ||||||
							
								
								
									
										58
									
								
								src/MaksIT.Core/Sagas/LocalSagaStep.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/MaksIT.Core/Sagas/LocalSagaStep.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,58 @@ | |||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using System.Linq; | ||||||
|  | using System.Text; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | namespace MaksIT.Core.Sagas; | ||||||
|  | 
 | ||||||
|  | /// <summary> | ||||||
|  | /// Internal non-generic step interface to unify generic steps. | ||||||
|  | /// </summary> | ||||||
|  | internal interface ILocalSagaStep { | ||||||
|  |   string Name { get; } | ||||||
|  |   Task<bool> ExecuteAsync(LocalSagaContext ctx, CancellationToken ct); | ||||||
|  |   Task CompensateAsync(LocalSagaContext ctx, CancellationToken ct); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | /// <summary> | ||||||
|  | /// Generic step with a result that can optionally be stored into the context. | ||||||
|  | /// Execution returns true if this step actually ran (useful for conditional steps). | ||||||
|  | /// </summary> | ||||||
|  | internal sealed class LocalSagaStep<T> : ILocalSagaStep { | ||||||
|  |   public string Name { get; } | ||||||
|  |   public Func<LocalSagaContext, CancellationToken, Task<T>> Execute { get; } | ||||||
|  |   public Func<LocalSagaContext, CancellationToken, Task>? Compensate { get; } | ||||||
|  |   public Func<LocalSagaContext, bool>? Predicate { get; } | ||||||
|  |   public string? OutputKey { get; } | ||||||
|  | 
 | ||||||
|  |   public LocalSagaStep( | ||||||
|  |     string name, | ||||||
|  |     Func<LocalSagaContext, CancellationToken, Task<T>> execute, | ||||||
|  |     Func<LocalSagaContext, CancellationToken, Task>? compensate, | ||||||
|  |     Func<LocalSagaContext, bool>? predicate, | ||||||
|  |     string? outputKey) { | ||||||
|  |     Name = name; | ||||||
|  |     Execute = execute; | ||||||
|  |     Compensate = compensate; | ||||||
|  |     Predicate = predicate; | ||||||
|  |     OutputKey = outputKey; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   public async Task<bool> ExecuteAsync(LocalSagaContext ctx, CancellationToken ct) { | ||||||
|  |     if (Predicate != null && !Predicate(ctx)) | ||||||
|  |       return false; | ||||||
|  | 
 | ||||||
|  |     var result = await Execute(ctx, ct); | ||||||
|  |     if (OutputKey != null) | ||||||
|  |       ctx.Set(OutputKey, result); | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   public async Task CompensateAsync(LocalSagaContext ctx, CancellationToken ct) { | ||||||
|  |     if (Compensate != null) | ||||||
|  |       await Compensate(ctx, ct); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										13
									
								
								src/MaksIT.Core/Sagas/Unit.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/MaksIT.Core/Sagas/Unit.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | |||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using System.Linq; | ||||||
|  | using System.Text; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  | 
 | ||||||
|  | namespace MaksIT.Core.Sagas; | ||||||
|  | /// <summary> | ||||||
|  | /// A simple unit type for steps that do not return a value. | ||||||
|  | /// </summary> | ||||||
|  | public readonly struct Unit { | ||||||
|  |   public static readonly Unit Value = new Unit(); | ||||||
|  | } | ||||||
							
								
								
									
										811
									
								
								src/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										811
									
								
								src/README.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,811 @@ | |||||||
|  | # MaksIT.Core Library Documentation | ||||||
|  | 
 | ||||||
|  | ## Table of Contents | ||||||
|  | 
 | ||||||
|  | - [Abstractions](#abstractions) | ||||||
|  |   - [Base Classes](#base-classes) | ||||||
|  |   - [Enumeration](#enumeration) | ||||||
|  | - [Extensions](#extensions) | ||||||
|  |   - [Expression Extensions](#expression-extensions) | ||||||
|  |   - [DateTime Extensions](#datetime-extensions) | ||||||
|  |   - [String Extensions](#string-extensions) | ||||||
|  |   - [Object Extensions](#object-extensions) | ||||||
|  |   - [DataTable Extensions](#datatable-extensions) | ||||||
|  |   - [Guid Extensions](#guid-extensions) | ||||||
|  | - [Logging](#logging) | ||||||
|  | - [Networking](#networking) | ||||||
|  |   - [Network Connection](#network-connection) | ||||||
|  |   - [Ping Port](#ping-port) | ||||||
|  | - [Security](#security) | ||||||
|  |   - [AES-GCM Utility](#aes-gcm-utility) | ||||||
|  |   - [Base32 Encoder](#base32-encoder) | ||||||
|  |   - [Checksum Utility](#checksum-utility) | ||||||
|  |   - [Password Hasher](#password-hasher) | ||||||
|  |   - [JWT Generator](#jwt-generator) | ||||||
|  |   - [TOTP Generator](#totp-generator) | ||||||
|  | - [Web API Models](#web-api-models) | ||||||
|  | - [Sagas](#sagas) | ||||||
|  | - [Others](#others) | ||||||
|  |   - [Culture](#culture) | ||||||
|  |   - [Environment Variables](#environment-variables) | ||||||
|  |   - [File System](#file-system) | ||||||
|  |   - [Processes](#processes) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## Abstractions | ||||||
|  | 
 | ||||||
|  | ### Base Classes | ||||||
|  | 
 | ||||||
|  | The following base classes in the `MaksIT.Core.Abstractions` namespace provide a foundation for implementing domain, DTO, and Web API models, ensuring consistency and maintainability in application design. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | ##### 1. **`DomainObjectBase`** | ||||||
|  | 
 | ||||||
|  | ###### Summary | ||||||
|  | Represents the base class for all domain objects in the application. | ||||||
|  | 
 | ||||||
|  | ###### Purpose | ||||||
|  | - Serves as the foundation for all domain objects. | ||||||
|  | - Provides a place to include shared logic or properties for domain-level entities in the future. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | ##### 2. **`DomainDocumentBase<T>`** | ||||||
|  | 
 | ||||||
|  | ###### Summary | ||||||
|  | Represents a base class for domain documents with a unique identifier. | ||||||
|  | 
 | ||||||
|  | ###### Purpose | ||||||
|  | - Extends `DomainObjectBase` to include an identifier. | ||||||
|  | - Provides a common structure for domain entities that need unique IDs. | ||||||
|  | 
 | ||||||
|  | ###### Example Usage | ||||||
|  | ```csharp | ||||||
|  | public class UserDomainDocument : DomainDocumentBase<Guid> { | ||||||
|  |     public UserDomainDocument(Guid id) : base(id) { | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | ##### 3. **`DtoObjectBase`** | ||||||
|  | 
 | ||||||
|  | ###### Summary | ||||||
|  | Represents the base class for all Data Transfer Objects (DTOs). | ||||||
|  | 
 | ||||||
|  | ###### Purpose | ||||||
|  | - Serves as the foundation for all DTOs. | ||||||
|  | - Provides a place to include shared logic or properties for DTOs in the future. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | ##### 4. **`DtoDocumentBase<T>`** | ||||||
|  | 
 | ||||||
|  | ###### Summary | ||||||
|  | Represents a base class for DTOs with a unique identifier. | ||||||
|  | 
 | ||||||
|  | ###### Purpose | ||||||
|  | - Extends `DtoObjectBase` to include an identifier. | ||||||
|  | - Provides a common structure for DTOs that need unique IDs. | ||||||
|  | 
 | ||||||
|  | ###### Example Usage | ||||||
|  | ```csharp | ||||||
|  | public class UserDto : DtoDocumentBase<Guid> { | ||||||
|  |     public required string Name { get; set; } | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | ##### 5. **`RequestModelBase`** | ||||||
|  | 
 | ||||||
|  | ###### Summary | ||||||
|  | Represents the base class for Web API request models. | ||||||
|  | 
 | ||||||
|  | ###### Purpose | ||||||
|  | - Serves as a foundation for request models used in Web API endpoints. | ||||||
|  | - Provides a common structure for request validation or shared properties. | ||||||
|  | 
 | ||||||
|  | ###### Example Usage | ||||||
|  | ```csharp | ||||||
|  | public class CreateUserRequest : RequestModelBase { | ||||||
|  |     public required string Name { get; set; } | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | ##### 6. **`ResponseModelBase`** | ||||||
|  | 
 | ||||||
|  | ###### Summary | ||||||
|  | Represents the base class for Web API response models. | ||||||
|  | 
 | ||||||
|  | ###### Purpose | ||||||
|  | - Serves as a foundation for response models returned by Web API endpoints. | ||||||
|  | - Provides a common structure for standardizing API responses. | ||||||
|  | 
 | ||||||
|  | ###### Example Usage | ||||||
|  | ```csharp | ||||||
|  | public class UserResponse : ResponseModelBase { | ||||||
|  |     public required Guid Id { get; set; } | ||||||
|  |     public required string Name { get; set; } | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Features and Benefits | ||||||
|  | 
 | ||||||
|  | 1. **Consistency**: | ||||||
|  |    - Ensures a uniform structure for domain, DTO, and Web API models. | ||||||
|  | 
 | ||||||
|  | 2. **Extensibility**: | ||||||
|  |    - Base classes can be extended to include shared properties or methods as needed. | ||||||
|  | 
 | ||||||
|  | 3. **Type Safety**: | ||||||
|  |    - Generic identifiers (`T`) ensure type safety for domain documents and DTOs. | ||||||
|  | 
 | ||||||
|  | 4. **Reusability**: | ||||||
|  |    - Common logic or properties can be added to base classes and reused across the application. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Example End-to-End Usage | ||||||
|  | 
 | ||||||
|  | ```csharp | ||||||
|  | // Domain Class | ||||||
|  | public class ProductDomain : DomainDocumentBase<int> { | ||||||
|  |     public ProductDomain(int id) : base(id) { } | ||||||
|  |     public string Name { get; set; } = string.Empty; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // DTO Class | ||||||
|  | public class ProductDto : DtoDocumentBase<int> { | ||||||
|  |     public required string Name { get; set; } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Web API Request Model | ||||||
|  | public class CreateProductRequest : RequestModelBase { | ||||||
|  |     public required string Name { get; set; } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Web API Response Model | ||||||
|  | public class ProductResponse : ResponseModelBase { | ||||||
|  |     public required int Id { get; set; } | ||||||
|  |     public required string Name { get; set; } | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Best Practices | ||||||
|  | 
 | ||||||
|  | 1. **Keep Base Classes Lightweight**: | ||||||
|  |    - Avoid adding unnecessary properties or methods to base classes. | ||||||
|  | 
 | ||||||
|  | 2. **Encapsulation**: | ||||||
|  |    - Use base classes to enforce encapsulation and shared behavior across entities. | ||||||
|  | 
 | ||||||
|  | 3. **Validation**: | ||||||
|  |    - Extend `RequestModelBase` or `ResponseModelBase` to include validation logic if needed. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | This structure promotes clean code principles, reducing redundancy and improving maintainability across the application layers. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | ### Enumeration | ||||||
|  | 
 | ||||||
|  | The `Enumeration` class in the `MaksIT.Core.Abstractions` namespace provides a base class for creating strongly-typed enumerations. It enables you to define enumerable constants with additional functionality, such as methods for querying, comparing, and parsing enumerations. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Features and Benefits | ||||||
|  | 
 | ||||||
|  | 1. **Strongly-Typed Enumerations**: | ||||||
|  |    - Combines the clarity of enums with the extensibility of classes. | ||||||
|  |    - Supports additional fields, methods, or logic as needed. | ||||||
|  | 
 | ||||||
|  | 2. **Reflection Support**: | ||||||
|  |    - Dynamically retrieve all enumeration values with `GetAll`. | ||||||
|  | 
 | ||||||
|  | 3. **Parsing Capabilities**: | ||||||
|  |    - Retrieve enumeration values by ID or display name. | ||||||
|  | 
 | ||||||
|  | 4. **Comparison and Equality**: | ||||||
|  |    - Fully implements equality and comparison operators for use in collections and sorting. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Example Usage | ||||||
|  | 
 | ||||||
|  | #### Defining an Enumeration | ||||||
|  | ```csharp | ||||||
|  | public class MyEnumeration : Enumeration { | ||||||
|  |     public static readonly MyEnumeration Value1 = new(1, "Value One"); | ||||||
|  |     public static readonly MyEnumeration Value2 = new(2, "Value Two"); | ||||||
|  | 
 | ||||||
|  |     private MyEnumeration(int id, string name) : base(id, name) { } | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | #### Retrieving All Values | ||||||
|  | ```csharp | ||||||
|  | var allValues = Enumeration.GetAll<MyEnumeration>(); | ||||||
|  | allValues.ToList().ForEach(Console.WriteLine); | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | #### Parsing by ID or Name | ||||||
|  | ```csharp | ||||||
|  | var valueById = Enumeration.FromValue<MyEnumeration>(1); | ||||||
|  | var valueByName = Enumeration.FromDisplayName<MyEnumeration>("Value One"); | ||||||
|  | 
 | ||||||
|  | Console.WriteLine(valueById); // Output: Value One | ||||||
|  | Console.WriteLine(valueByName); // Output: Value One | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | #### Comparing Enumeration Values | ||||||
|  | ```csharp | ||||||
|  | var difference = Enumeration.AbsoluteDifference(MyEnumeration.Value1, MyEnumeration.Value2); | ||||||
|  | Console.WriteLine($"Absolute Difference: {difference}"); // Output: 1 | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | #### Using in Collections | ||||||
|  | ```csharp | ||||||
|  | var values = new List<MyEnumeration> { MyEnumeration.Value2, MyEnumeration.Value1 }; | ||||||
|  | values.Sort(); // Orders by ID | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Best Practices | ||||||
|  | 
 | ||||||
|  | 1. **Extend for Specific Enums**: | ||||||
|  |    - Create specific subclasses for each enumeration type. | ||||||
|  | 
 | ||||||
|  | 2. **Avoid Duplicates**: | ||||||
|  |    - Ensure unique IDs and names for each enumeration value. | ||||||
|  | 
 | ||||||
|  | 3. **Use Reflection Sparingly**: | ||||||
|  |    - Avoid calling `GetAll` in performance-critical paths. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | The `Enumeration` class provides a powerful alternative to traditional enums, offering flexibility and functionality for scenarios requiring additional metadata or logic. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | ### Extensions | ||||||
|  | 
 | ||||||
|  | ### Guid Extensions | ||||||
|  | 
 | ||||||
|  | The `GuidExtensions` class provides methods for working with `Guid` values, including converting them to nullable types. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Features | ||||||
|  | 
 | ||||||
|  | 1. **Convert to Nullable**: | ||||||
|  |    - Convert a `Guid` to a nullable `Guid?`, returning `null` if the `Guid` is empty. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Example Usage | ||||||
|  | 
 | ||||||
|  | ##### Converting to Nullable | ||||||
|  | ```csharp | ||||||
|  | Guid id = Guid.NewGuid(); | ||||||
|  | Guid? nullableId = id.ToNullable(); | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | ### Expression Extensions | ||||||
|  | 
 | ||||||
|  | The `ExpressionExtensions` class provides utility methods for combining and manipulating LINQ expressions. These methods are particularly useful for building dynamic queries in a type-safe manner. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Features | ||||||
|  | 
 | ||||||
|  | 1. **Combine Expressions**: | ||||||
|  |    - Combine two expressions using logical operators like `AndAlso` and `OrElse`. | ||||||
|  | 
 | ||||||
|  | 2. **Negate Expressions**: | ||||||
|  |    - Negate an expression using the `Not` method. | ||||||
|  | 
 | ||||||
|  | 3. **Batch Processing**: | ||||||
|  |    - Divide a collection into smaller batches for processing. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Example Usage | ||||||
|  | 
 | ||||||
|  | ##### Combining Expressions | ||||||
|  | ```csharp | ||||||
|  | Expression<Func<int, bool>> isEven = x => x % 2 == 0; | ||||||
|  | Expression<Func<int, bool>> isPositive = x => x > 0; | ||||||
|  | 
 | ||||||
|  | var combined = isEven.AndAlso(isPositive); | ||||||
|  | var result = combined.Compile()(4); // True | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ##### Negating Expressions | ||||||
|  | ```csharp | ||||||
|  | Expression<Func<int, bool>> isEven = x => x % 2 == 0; | ||||||
|  | var notEven = isEven.Not(); | ||||||
|  | var result = notEven.Compile()(3); // True | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | ### DateTime Extensions | ||||||
|  | 
 | ||||||
|  | The `DateTimeExtensions` class provides methods for manipulating and querying `DateTime` objects. These methods simplify common date-related operations. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Features | ||||||
|  | 
 | ||||||
|  | 1. **Add Workdays**: | ||||||
|  |    - Add a specified number of workdays to a date, excluding weekends and holidays. | ||||||
|  | 
 | ||||||
|  | 2. **Find Specific Dates**: | ||||||
|  |    - Find the next occurrence of a specific day of the week. | ||||||
|  | 
 | ||||||
|  | 3. **Month and Year Boundaries**: | ||||||
|  |    - Get the start or end of the current month or year. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Example Usage | ||||||
|  | 
 | ||||||
|  | ##### Adding Workdays | ||||||
|  | ```csharp | ||||||
|  | DateTime today = DateTime.Today; | ||||||
|  | DateTime futureDate = today.AddWorkdays(5); | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ##### Finding the Next Monday | ||||||
|  | ```csharp | ||||||
|  | DateTime today = DateTime.Today; | ||||||
|  | DateTime nextMonday = today.NextWeekday(DayOfWeek.Monday); | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | ### String Extensions | ||||||
|  | 
 | ||||||
|  | The `StringExtensions` class provides a wide range of methods for string manipulation, validation, and conversion. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Features | ||||||
|  | 
 | ||||||
|  | 1. **Pattern Matching**: | ||||||
|  |    - Check if a string matches a pattern using SQL-like wildcards. | ||||||
|  | 
 | ||||||
|  | 2. **Substring Extraction**: | ||||||
|  |    - Extract substrings from the left, right, or middle of a string. | ||||||
|  | 
 | ||||||
|  | 3. **Type Conversion**: | ||||||
|  |    - Convert strings to various types, such as integers, booleans, and enums. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Example Usage | ||||||
|  | 
 | ||||||
|  | ##### Pattern Matching | ||||||
|  | ```csharp | ||||||
|  | bool matches = "example".Like("exa*e"); // True | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ##### Substring Extraction | ||||||
|  | ```csharp | ||||||
|  | string result = "example".Left(3); // "exa" | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | ### Object Extensions | ||||||
|  | 
 | ||||||
|  | The `ObjectExtensions` class provides methods for serializing objects to JSON strings and deserializing JSON strings back to objects. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Features | ||||||
|  | 
 | ||||||
|  | 1. **JSON Serialization**: | ||||||
|  |    - Convert objects to JSON strings. | ||||||
|  | 
 | ||||||
|  | 2. **JSON Deserialization**: | ||||||
|  |    - Convert JSON strings back to objects. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Example Usage | ||||||
|  | 
 | ||||||
|  | ##### Serialization | ||||||
|  | ```csharp | ||||||
|  | var person = new { Name = "John", Age = 30 }; | ||||||
|  | string json = person.ToJson(); | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ##### Deserialization | ||||||
|  | ```csharp | ||||||
|  | var person = json.ToObject<Person>(); | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | ### DataTable Extensions | ||||||
|  | 
 | ||||||
|  | The `DataTableExtensions` class provides methods for working with `DataTable` objects, such as counting duplicate rows and retrieving distinct records. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Features | ||||||
|  | 
 | ||||||
|  | 1. **Count Duplicates**: | ||||||
|  |    - Count duplicate rows between two `DataTable` instances. | ||||||
|  | 
 | ||||||
|  | 2. **Retrieve Distinct Records**: | ||||||
|  |    - Get distinct rows based on specified columns. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Example Usage | ||||||
|  | 
 | ||||||
|  | ##### Counting Duplicates | ||||||
|  | ```csharp | ||||||
|  | int duplicateCount = table1.DuplicatesCount(table2); | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ##### Retrieving Distinct Records | ||||||
|  | ```csharp | ||||||
|  | DataTable distinctTable = table.DistinctRecords(new[] { "Name", "Age" }); | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | ## Logging | ||||||
|  | 
 | ||||||
|  | The `Logging` namespace provides a custom file-based logging implementation that integrates with the `Microsoft.Extensions.Logging` framework. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Features | ||||||
|  | 
 | ||||||
|  | 1. **File-Based Logging**: | ||||||
|  |    - Log messages to a specified file. | ||||||
|  | 
 | ||||||
|  | 2. **Log Levels**: | ||||||
|  |    - Supports all standard log levels. | ||||||
|  | 
 | ||||||
|  | 3. **Thread Safety**: | ||||||
|  |    - Ensures thread-safe writes to the log file. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Example Usage | ||||||
|  | 
 | ||||||
|  | ```csharp | ||||||
|  | var services = new ServiceCollection(); | ||||||
|  | services.AddLogging(builder => builder.AddFile("logs.txt")); | ||||||
|  | 
 | ||||||
|  | var logger = services.BuildServiceProvider().GetRequiredService<ILogger<FileLogger>>(); | ||||||
|  | logger.LogInformation("Logging to file!"); | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | ## Networking | ||||||
|  | 
 | ||||||
|  | ### Network Connection | ||||||
|  | 
 | ||||||
|  | The `NetworkConnection` class provides methods for managing connections to network shares on Windows. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Features | ||||||
|  | 
 | ||||||
|  | 1. **Connect to Network Shares**: | ||||||
|  |    - Establish connections to shared network resources. | ||||||
|  | 
 | ||||||
|  | 2. **Error Handling**: | ||||||
|  |    - Provides detailed error messages for connection failures. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Example Usage | ||||||
|  | 
 | ||||||
|  | ```csharp | ||||||
|  | var credentials = new NetworkCredential("username", "password"); | ||||||
|  | if (NetworkConnection.TryCreate(logger, "\\server\share", credentials, out var connection, out var error)) { | ||||||
|  |     connection.Dispose(); | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | ### Ping Port | ||||||
|  | 
 | ||||||
|  | The `PingPort` class provides methods for checking the reachability of a host on specified TCP or UDP ports. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Features | ||||||
|  | 
 | ||||||
|  | 1. **TCP Port Checking**: | ||||||
|  |    - Check if a TCP port is reachable. | ||||||
|  | 
 | ||||||
|  | 2. **UDP Port Checking**: | ||||||
|  |    - Check if a UDP port is reachable. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Example Usage | ||||||
|  | 
 | ||||||
|  | ##### Checking a TCP Port | ||||||
|  | ```csharp | ||||||
|  | if (PingPort.TryHostPort("example.com", 80, out var error)) { | ||||||
|  |     Console.WriteLine("Port is reachable."); | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | ## Security | ||||||
|  | 
 | ||||||
|  | ### AES-GCM Utility | ||||||
|  | 
 | ||||||
|  | The `AESGCMUtility` class provides methods for encrypting and decrypting data using AES-GCM. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Features | ||||||
|  | 
 | ||||||
|  | 1. **Secure Encryption**: | ||||||
|  |    - Encrypt data with AES-GCM. | ||||||
|  | 
 | ||||||
|  | 2. **Data Integrity**: | ||||||
|  |    - Ensure data integrity with authentication tags. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Example Usage | ||||||
|  | 
 | ||||||
|  | ##### Encrypting Data | ||||||
|  | ```csharp | ||||||
|  | var key = AESGCMUtility.GenerateKeyBase64(); | ||||||
|  | AESGCMUtility.TryEncryptData(data, key, out var encryptedData, out var error); | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | ### Base32 Encoder | ||||||
|  | 
 | ||||||
|  | The `Base32Encoder` class provides methods for encoding and decoding data in Base32 format. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Features | ||||||
|  | 
 | ||||||
|  | 1. **Encoding**: | ||||||
|  |    - Encode binary data to Base32. | ||||||
|  | 
 | ||||||
|  | 2. **Decoding**: | ||||||
|  |    - Decode Base32 strings to binary data. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Example Usage | ||||||
|  | 
 | ||||||
|  | ##### Encoding Data | ||||||
|  | ```csharp | ||||||
|  | Base32Encoder.TryEncode(data, out var encoded, out var error); | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | ### Checksum Utility | ||||||
|  | 
 | ||||||
|  | The `ChecksumUtility` class provides methods for calculating and verifying CRC32 checksums. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Features | ||||||
|  | 
 | ||||||
|  | 1. **Checksum Calculation**: | ||||||
|  |    - Calculate CRC32 checksums for data. | ||||||
|  | 
 | ||||||
|  | 2. **Checksum Verification**: | ||||||
|  |    - Verify data integrity using CRC32 checksums. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Example Usage | ||||||
|  | 
 | ||||||
|  | ##### Calculating a Checksum | ||||||
|  | ```csharp | ||||||
|  | ChecksumUtility.TryCalculateCRC32Checksum(data, out var checksum, out var error); | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | ### Password Hasher | ||||||
|  | 
 | ||||||
|  | The `PasswordHasher` class provides methods for securely hashing and validating passwords. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Features | ||||||
|  | 
 | ||||||
|  | 1. **Salted Hashing**: | ||||||
|  |    - Hash passwords with a unique salt. | ||||||
|  | 
 | ||||||
|  | 2. **Validation**: | ||||||
|  |    - Validate passwords against stored hashes. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Example Usage | ||||||
|  | 
 | ||||||
|  | ##### Hashing a Password | ||||||
|  | ```csharp | ||||||
|  | PasswordHasher.TryCreateSaltedHash("password", out var hash, out var error); | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | ### JWT Generator | ||||||
|  | 
 | ||||||
|  | The `JwtGenerator` class provides methods for generating and validating JSON Web Tokens (JWTs). | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Features | ||||||
|  | 
 | ||||||
|  | 1. **Token Generation**: | ||||||
|  |    - Generate JWTs with claims and metadata. | ||||||
|  | 
 | ||||||
|  | 2. **Token Validation**: | ||||||
|  |    - Validate JWTs against a secret. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Example Usage | ||||||
|  | 
 | ||||||
|  | ##### Generating a Token | ||||||
|  | ```csharp | ||||||
|  | JwtGenerator.TryGenerateToken(secret, issuer, audience, 60, "user", roles, out var token, out var error); | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | ### TOTP Generator | ||||||
|  | 
 | ||||||
|  | The `TotpGenerator` class provides methods for generating and validating Time-Based One-Time Passwords (TOTP). | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Features | ||||||
|  | 
 | ||||||
|  | 1. **TOTP Generation**: | ||||||
|  |    - Generate TOTPs based on shared secrets. | ||||||
|  | 
 | ||||||
|  | 2. **TOTP Validation**: | ||||||
|  |    - Validate TOTPs with time tolerance. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Example Usage | ||||||
|  | 
 | ||||||
|  | ##### Generating a TOTP | ||||||
|  | ```csharp | ||||||
|  | TotpGenerator.TryGenerate(secret, TotpGenerator.GetCurrentTimeStepNumber(), out var totp, out var error); | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | ## Others | ||||||
|  | 
 | ||||||
|  | ### Culture | ||||||
|  | 
 | ||||||
|  | The `Culture` class provides methods for dynamically setting the culture for the current thread. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Features | ||||||
|  | 
 | ||||||
|  | 1. **Dynamic Culture Setting**: | ||||||
|  |    - Change the culture for the current thread. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Example Usage | ||||||
|  | 
 | ||||||
|  | ##### Setting the Culture | ||||||
|  | ```csharp | ||||||
|  | Culture.TrySet("fr-FR", out var error); | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | ### Environment Variables | ||||||
|  | 
 | ||||||
|  | The `EnvVar` class provides methods for managing environment variables. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Features | ||||||
|  | 
 | ||||||
|  | 1. **Add to PATH**: | ||||||
|  |    - Add directories to the `PATH` environment variable. | ||||||
|  | 
 | ||||||
|  | 2. **Set and Unset Variables**: | ||||||
|  |    - Manage environment variables at different scopes. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Example Usage | ||||||
|  | 
 | ||||||
|  | ##### Adding to PATH | ||||||
|  | ```csharp | ||||||
|  | EnvVar.TryAddToPath("/usr/local/bin", out var error); | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | ### File System | ||||||
|  | 
 | ||||||
|  | The `FileSystem` class provides methods for working with files and directories. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Features | ||||||
|  | 
 | ||||||
|  | 1. **Copy Files and Folders**: | ||||||
|  |    - Copy files or directories to a target location. | ||||||
|  | 
 | ||||||
|  | 2. **Delete Files and Folders**: | ||||||
|  |    - Delete files or directories. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Example Usage | ||||||
|  | 
 | ||||||
|  | ##### Copying Files | ||||||
|  | ```csharp | ||||||
|  | FileSystem.TryCopyToFolder("source", "destination", true, out var error); | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | ### Processes | ||||||
|  | 
 | ||||||
|  | The `Processes` class provides methods for managing system processes. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Features | ||||||
|  | 
 | ||||||
|  | 1. **Start Processes**: | ||||||
|  |    - Start new processes with optional arguments. | ||||||
|  | 
 | ||||||
|  | 2. **Kill Processes**: | ||||||
|  |    - Terminate processes by name. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | #### Example Usage | ||||||
|  | 
 | ||||||
|  | ##### Starting a Process | ||||||
|  | ```csharp | ||||||
|  | Processes.TryStart("notepad.exe", "", 0, false, out var error); | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | --- | ||||||
| @ -12,11 +12,20 @@ $nugetSource = "https://api.nuget.org/v3/index.json" | |||||||
| $solutionDir = Split-Path -Parent $MyInvocation.MyCommand.Path | $solutionDir = Split-Path -Parent $MyInvocation.MyCommand.Path | ||||||
| $projectDir = "$solutionDir\MaksIT.Core" | $projectDir = "$solutionDir\MaksIT.Core" | ||||||
| $outputDir = "$projectDir\bin\Release" | $outputDir = "$projectDir\bin\Release" | ||||||
|  | $testProjectDir = "$solutionDir\MaksIT.Core.Tests" | ||||||
| 
 | 
 | ||||||
| # Clean previous builds | # Clean previous builds | ||||||
| Write-Host "Cleaning previous builds..." | Write-Host "Cleaning previous builds..." | ||||||
| dotnet clean $projectDir -c Release | dotnet clean $projectDir -c Release | ||||||
| 
 | 
 | ||||||
|  | # Run tests | ||||||
|  | Write-Host "Running tests..." | ||||||
|  | dotnet test $testProjectDir -c Release --no-build | ||||||
|  | if ($LASTEXITCODE -ne 0) { | ||||||
|  |     Write-Host "Tests failed. Aborting release process." | ||||||
|  |     exit 1 | ||||||
|  | } | ||||||
|  | 
 | ||||||
| # Build the project | # Build the project | ||||||
| Write-Host "Building the project..." | Write-Host "Building the project..." | ||||||
| dotnet build $projectDir -c Release | dotnet build $projectDir -c Release | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 Maksym Sadovnychyy
						Maksym Sadovnychyy