(feature): sagas builder, general improvements
This commit is contained in:
		
							parent
							
								
									ab763d7c8f
								
							
						
					
					
						commit
						224b38b408
					
				
							
								
								
									
										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> | ||||
|       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||||
|     </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.runner.visualstudio" Version="3.1.0"> | ||||
|     <PackageReference Include="xunit.runner.visualstudio" Version="3.1.5"> | ||||
|       <PrivateAssets>all</PrivateAssets> | ||||
|       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||||
|     </PackageReference> | ||||
| @ -30,4 +31,8 @@ | ||||
|     <Using Include="Xunit" /> | ||||
|   </ItemGroup> | ||||
| 
 | ||||
|   <ItemGroup> | ||||
|     <Folder Include="Helpers\" /> | ||||
|   </ItemGroup> | ||||
| 
 | ||||
| </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 --> | ||||
|     <PackageId>MaksIT.Core</PackageId> | ||||
|     <Version>1.4.7</Version> | ||||
|     <Version>1.4.8</Version> | ||||
|     <Authors>Maksym Sadovnychyy</Authors> | ||||
|     <Company>MAKS-IT</Company> | ||||
|     <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,12 +12,26 @@ $nugetSource = "https://api.nuget.org/v3/index.json" | ||||
| $solutionDir = Split-Path -Parent $MyInvocation.MyCommand.Path | ||||
| $projectDir = "$solutionDir\MaksIT.Core" | ||||
| $outputDir = "$projectDir\bin\Release" | ||||
| $testProjectDir = "$solutionDir\MaksIT.Core.Tests" | ||||
| 
 | ||||
| # Clean previous builds | ||||
| Write-Host "Cleaning previous builds..." | ||||
| dotnet clean $projectDir -c Release | ||||
| dotnet clean $testProjectDir -c Release | ||||
| 
 | ||||
| # Build the project | ||||
| # Build the test project | ||||
| Write-Host "Building the test project..." | ||||
| dotnet build $testProjectDir -c Release | ||||
| 
 | ||||
| # Run tests | ||||
| Write-Host "Running tests..." | ||||
| dotnet test $testProjectDir -c Release | ||||
| if ($LASTEXITCODE -ne 0) { | ||||
|     Write-Host "Tests failed. Aborting release process." | ||||
|     exit 1 | ||||
| } | ||||
| 
 | ||||
| # Build the main project | ||||
| Write-Host "Building the project..." | ||||
| dotnet build $projectDir -c Release | ||||
| 
 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 Maksym Sadovnychyy
						Maksym Sadovnychyy