From 224b38b40843efb711769cb4caf8ec26e8b8e608 Mon Sep 17 00:00:00 2001 From: Maksym Sadovnychyy Date: Thu, 30 Oct 2025 22:39:26 +0100 Subject: [PATCH] (feature): sagas builder, general improvements --- src/MaksIT.Core.Tests/LoggerHelper.cs | 42 + .../MaksIT.Core.Tests.csproj | 9 +- src/MaksIT.Core.Tests/Sagas/LocalSagaTests.cs | 190 ++++ src/MaksIT.Core.Tests/TestHostEnvironment.cs | 15 + src/MaksIT.Core/MaksIT.Core.csproj | 2 +- src/MaksIT.Core/Sagas/LocalSaga.cs | 88 ++ src/MaksIT.Core/Sagas/LocalSagaBuilder.cs | 73 ++ src/MaksIT.Core/Sagas/LocalSagaContext.cs | 25 + src/MaksIT.Core/Sagas/LocalSagaStep.cs | 58 ++ src/MaksIT.Core/Sagas/Unit.cs | 13 + src/README.md | 811 ++++++++++++++++++ src/Release-NuGetPackage.ps1 | 16 +- 12 files changed, 1338 insertions(+), 4 deletions(-) create mode 100644 src/MaksIT.Core.Tests/LoggerHelper.cs create mode 100644 src/MaksIT.Core.Tests/Sagas/LocalSagaTests.cs create mode 100644 src/MaksIT.Core.Tests/TestHostEnvironment.cs create mode 100644 src/MaksIT.Core/Sagas/LocalSaga.cs create mode 100644 src/MaksIT.Core/Sagas/LocalSagaBuilder.cs create mode 100644 src/MaksIT.Core/Sagas/LocalSagaContext.cs create mode 100644 src/MaksIT.Core/Sagas/LocalSagaStep.cs create mode 100644 src/MaksIT.Core/Sagas/Unit.cs create mode 100644 src/README.md diff --git a/src/MaksIT.Core.Tests/LoggerHelper.cs b/src/MaksIT.Core.Tests/LoggerHelper.cs new file mode 100644 index 0000000..ab0991e --- /dev/null +++ b/src/MaksIT.Core.Tests/LoggerHelper.cs @@ -0,0 +1,42 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +using MaksIT.Core.Logging; + +namespace MaksIT.Core.Tests; + +/// +/// Provides helper methods for creating loggers in tests. +/// +public static class LoggerHelper +{ + /// + /// Creates a console logger for testing purposes. + /// + /// An instance of configured for console logging. + public static ILogger CreateConsoleLogger() + { + var serviceCollection = new ServiceCollection(); + + // Use the reusable TestHostEnvironment for testing + serviceCollection.AddSingleton(sp => + new TestHostEnvironment + { + EnvironmentName = Environments.Development, + ApplicationName = "TestApp", + ContentRootPath = Directory.GetCurrentDirectory() + }); + + serviceCollection.AddLogging(builder => + { + var env = serviceCollection.BuildServiceProvider().GetRequiredService(); + builder.ClearProviders(); + builder.AddConsole(env); + }); + + var provider = serviceCollection.BuildServiceProvider(); + var factory = provider.GetRequiredService(); + return factory.CreateLogger("TestLogger"); + } +} \ No newline at end of file diff --git a/src/MaksIT.Core.Tests/MaksIT.Core.Tests.csproj b/src/MaksIT.Core.Tests/MaksIT.Core.Tests.csproj index 77f9d3c..24d3a2f 100644 --- a/src/MaksIT.Core.Tests/MaksIT.Core.Tests.csproj +++ b/src/MaksIT.Core.Tests/MaksIT.Core.Tests.csproj @@ -14,9 +14,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -30,4 +31,8 @@ + + + + diff --git a/src/MaksIT.Core.Tests/Sagas/LocalSagaTests.cs b/src/MaksIT.Core.Tests/Sagas/LocalSagaTests.cs new file mode 100644 index 0000000..6e3230d --- /dev/null +++ b/src/MaksIT.Core.Tests/Sagas/LocalSagaTests.cs @@ -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(() => 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(() => saga.ExecuteAsync(context)); + Assert.Equal("initial", context.Get("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(); + + 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(() => saga.ExecuteAsync(context)); + Assert.Contains("Step2 compensated", compensationLog); + Assert.Contains("Step1 compensated", compensationLog); + } +} diff --git a/src/MaksIT.Core.Tests/TestHostEnvironment.cs b/src/MaksIT.Core.Tests/TestHostEnvironment.cs new file mode 100644 index 0000000..a35f8f9 --- /dev/null +++ b/src/MaksIT.Core.Tests/TestHostEnvironment.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Hosting; + +namespace MaksIT.Core.Tests; + +/// +/// Simple implementation of IHostEnvironment for testing purposes. +/// +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(); +} \ No newline at end of file diff --git a/src/MaksIT.Core/MaksIT.Core.csproj b/src/MaksIT.Core/MaksIT.Core.csproj index afcced1..d2c2576 100644 --- a/src/MaksIT.Core/MaksIT.Core.csproj +++ b/src/MaksIT.Core/MaksIT.Core.csproj @@ -8,7 +8,7 @@ MaksIT.Core - 1.4.7 + 1.4.8 Maksym Sadovnychyy MAKS-IT MaksIT.Core diff --git a/src/MaksIT.Core/Sagas/LocalSaga.cs b/src/MaksIT.Core/Sagas/LocalSaga.cs new file mode 100644 index 0000000..84c254c --- /dev/null +++ b/src/MaksIT.Core/Sagas/LocalSaga.cs @@ -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; +/// +/// Executable local saga with LIFO compensation on failure. +/// +public sealed class LocalSaga { + private readonly IReadOnlyList _pipeline; + private readonly ILogger _logger; + + internal LocalSaga( + IReadOnlyList 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(); + + 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 executedStack, + LocalSagaContext ctx, + CancellationToken ct) { + _logger.LogInformation("LocalSaga: starting compensation"); + + var compensationErrors = new List(); + 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); + } +} \ No newline at end of file diff --git a/src/MaksIT.Core/Sagas/LocalSagaBuilder.cs b/src/MaksIT.Core/Sagas/LocalSagaBuilder.cs new file mode 100644 index 0000000..4083d54 --- /dev/null +++ b/src/MaksIT.Core/Sagas/LocalSagaBuilder.cs @@ -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; + +/// +/// Fluent builder to compose a local saga (exception-based failures). +/// +public sealed class LocalSagaBuilder { + private readonly List _pipeline = new(); + private ILogger? _logger; + + public LocalSagaBuilder WithLogger(ILogger logger) { + _logger = logger; + return this; + } + + public LocalSagaBuilder AddAction( + string name, + Func execute, + Func? compensate = null) { + _pipeline.Add(new LocalSagaStep( + name, + async (c, ct) => { await execute(c, ct); return Unit.Value; }, + compensate, + predicate: null, + outputKey: null)); + return this; + } + + public LocalSagaBuilder AddActionIf( + Func predicate, + string name, + Func execute, + Func? compensate = null) { + _pipeline.Add(new LocalSagaStep( + $"[conditional] {name}", + async (c, ct) => { await execute(c, ct); return Unit.Value; }, + compensate, + predicate, + outputKey: null)); + return this; + } + + public LocalSagaBuilder AddStep( + string name, + Func> execute, + string? outputKey = null, + Func? compensate = null) { + _pipeline.Add(new LocalSagaStep(name, execute, compensate, predicate: null, outputKey: outputKey)); + return this; + } + + public LocalSagaBuilder AddStepIf( + Func predicate, + string name, + Func> execute, + string? outputKey = null, + Func? compensate = null) { + _pipeline.Add(new LocalSagaStep($"[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); + } +} \ No newline at end of file diff --git a/src/MaksIT.Core/Sagas/LocalSagaContext.cs b/src/MaksIT.Core/Sagas/LocalSagaContext.cs new file mode 100644 index 0000000..c8b0645 --- /dev/null +++ b/src/MaksIT.Core/Sagas/LocalSagaContext.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MaksIT.Core.Sagas; + +/// +/// Shared context to pass values between steps without tight coupling. +/// +public sealed class LocalSagaContext { + private readonly Dictionary _bag = new(StringComparer.Ordinal); + + public T? Get(string key) { + return _bag.TryGetValue(key, out var v) && v is T t ? t : default; + } + + public LocalSagaContext Set(string key, T value) { + _bag[key] = value; + return this; + } + + public bool Contains(string key) => _bag.ContainsKey(key); +} \ No newline at end of file diff --git a/src/MaksIT.Core/Sagas/LocalSagaStep.cs b/src/MaksIT.Core/Sagas/LocalSagaStep.cs new file mode 100644 index 0000000..1af047a --- /dev/null +++ b/src/MaksIT.Core/Sagas/LocalSagaStep.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + + +namespace MaksIT.Core.Sagas; + +/// +/// Internal non-generic step interface to unify generic steps. +/// +internal interface ILocalSagaStep { + string Name { get; } + Task ExecuteAsync(LocalSagaContext ctx, CancellationToken ct); + Task CompensateAsync(LocalSagaContext ctx, CancellationToken ct); +} + + +/// +/// 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). +/// +internal sealed class LocalSagaStep : ILocalSagaStep { + public string Name { get; } + public Func> Execute { get; } + public Func? Compensate { get; } + public Func? Predicate { get; } + public string? OutputKey { get; } + + public LocalSagaStep( + string name, + Func> execute, + Func? compensate, + Func? predicate, + string? outputKey) { + Name = name; + Execute = execute; + Compensate = compensate; + Predicate = predicate; + OutputKey = outputKey; + } + + public async Task 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); + } +} \ No newline at end of file diff --git a/src/MaksIT.Core/Sagas/Unit.cs b/src/MaksIT.Core/Sagas/Unit.cs new file mode 100644 index 0000000..c5f276b --- /dev/null +++ b/src/MaksIT.Core/Sagas/Unit.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MaksIT.Core.Sagas; +/// +/// A simple unit type for steps that do not return a value. +/// +public readonly struct Unit { + public static readonly Unit Value = new Unit(); +} diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..5da7709 --- /dev/null +++ b/src/README.md @@ -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`** + +###### 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 { + 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`** + +###### 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 { + 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 { + public ProductDomain(int id) : base(id) { } + public string Name { get; set; } = string.Empty; +} + +// DTO Class +public class ProductDto : DtoDocumentBase { + 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(); +allValues.ToList().ForEach(Console.WriteLine); +``` + +#### Parsing by ID or Name +```csharp +var valueById = Enumeration.FromValue(1); +var valueByName = Enumeration.FromDisplayName("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.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> isEven = x => x % 2 == 0; +Expression> isPositive = x => x > 0; + +var combined = isEven.AndAlso(isPositive); +var result = combined.Compile()(4); // True +``` + +##### Negating Expressions +```csharp +Expression> 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(); +``` + +--- + +### 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>(); +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); +``` + +--- \ No newline at end of file diff --git a/src/Release-NuGetPackage.ps1 b/src/Release-NuGetPackage.ps1 index 8e69465..631dea8 100644 --- a/src/Release-NuGetPackage.ps1 +++ b/src/Release-NuGetPackage.ps1 @@ -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