(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>
|
<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,12 +12,26 @@ $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
|
||||||
|
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..."
|
Write-Host "Building the project..."
|
||||||
dotnet build $projectDir -c Release
|
dotnet build $projectDir -c Release
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user