A comprehensive .NET library providing utilities for logging (file/JSON with folder organization), security (JWT, JWK, JWS, TOTP, AES-GCM, password hashing), extensions (string, object, LINQ expressions, DateTime), saga orchestration, COMB GUIDs, Web API pagination, and more.
$ dotnet add package MaksIT.Core
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.
DomainObjectBaseRepresents the base class for all domain objects in the application.
DomainDocumentBase<T>Represents a base class for domain documents with a unique identifier.
DomainObjectBase to include an identifier.public class UserDomainDocument : DomainDocumentBase<Guid> {
public UserDomainDocument(Guid id) : base(id) {
}
}
DtoObjectBaseRepresents the base class for all Data Transfer Objects (DTOs).
DtoDocumentBase<T>Represents a base class for DTOs with a unique identifier.
DtoObjectBase to include an identifier.public class UserDto : DtoDocumentBase<Guid> {
public required string Name { get; set; }
}
RequestModelBaseRepresents the base class for Web API request models.
public class CreateUserRequest : RequestModelBase {
public required string Name { get; set; }
}
ResponseModelBaseRepresents the base class for Web API response models.
public class UserResponse : ResponseModelBase {
public required Guid Id { get; set; }
public required string Name { get; set; }
}
Consistency:
Extensibility:
Type Safety:
T) ensure type safety for domain documents and DTOs.Reusability:
// 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; }
}
Keep Base Classes Lightweight:
Encapsulation:
Validation:
RequestModelBase or ResponseModelBase to include validation logic if needed.This structure promotes clean code principles, reducing redundancy and improving maintainability across the application layers.
The CombGuidGenerator class in the MaksIT.Core.Comb namespace provides methods for generating and extracting COMB GUIDs (GUIDs with embedded timestamps). COMB GUIDs improve index locality by combining randomness with a sortable timestamp.
Generate COMB GUIDs:
Extract Timestamps:
Support for Multiple Formats:
var baseGuid = Guid.NewGuid();
var timestamp = DateTime.UtcNow;
// Generate a COMB GUID for SQL Server
var combGuid = CombGuidGenerator.CreateCombGuid(baseGuid, timestamp, CombGuidType.SqlServer);
// Generate a COMB GUID for PostgreSQL
var combGuidPostgres = CombGuidGenerator.CreateCombGuid(baseGuid, timestamp, CombGuidType.PostgreSql);
var extractedTimestamp = CombGuidGenerator.ExtractTimestamp(combGuid, CombGuidType.SqlServer);
Console.WriteLine($"Extracted Timestamp: {extractedTimestamp}");
var combGuidWithCurrentTimestamp = CombGuidGenerator.CreateCombGuid(Guid.NewGuid(), CombGuidType.SqlServer);
Use COMB GUIDs for Indexing:
Choose the Correct Format:
CombGuidType.SqlServer for SQL Server and CombGuidType.PostgreSql for PostgreSQL.Ensure UTC Timestamps:
The CombGuidGenerator class simplifies the creation and management of COMB GUIDs, making it easier to work with GUIDs in database applications.
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.
Strongly-Typed Enumerations:
Reflection Support:
GetAll.Parsing Capabilities:
Comparison and Equality:
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) { }
}
var allValues = Enumeration.GetAll<MyEnumeration>();
allValues.ToList().ForEach(Console.WriteLine);
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
var difference = Enumeration.AbsoluteDifference(MyEnumeration.Value1, MyEnumeration.Value2);
Console.WriteLine($"Absolute Difference: {difference}"); // Output: 1
var values = new List<MyEnumeration> { MyEnumeration.Value2, MyEnumeration.Value1 };
values.Sort(); // Orders by ID
Extend for Specific Enums:
Avoid Duplicates:
Use Reflection Sparingly:
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.
The Sagas namespace in the MaksIT.Core project provides a framework for managing distributed transactions or workflows. It includes classes for defining saga steps, contexts, and builders.
Saga Context:
Saga Steps:
Saga Builder:
public class MySagaStep : LocalSagaStep {
public override Task ExecuteAsync(LocalSagaContext context) {
// Perform step logic here
return Task.CompletedTask;
}
}
var saga = new LocalSagaBuilder()
.AddStep(new MySagaStep())
.Build();
await saga.ExecuteAsync();
Idempotency:
Error Handling:
State Management:
The Sagas namespace simplifies the implementation of distributed workflows, making it easier to manage complex transactions and processes.
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.
Combine Expressions:
AndAlso and OrElse.Negate Expressions:
Not method.Batch Processing:
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
Expression<Func<int, bool>> isEven = x => x % 2 == 0;
var notEven = isEven.Not();
var result = notEven.Compile()(3); // True
The DateTimeExtensions class provides methods for manipulating and querying DateTime objects. These methods simplify common date-related operations.
Add Workdays:
Find Specific Dates:
Month and Year Boundaries:
DateTime today = DateTime.Today;
DateTime futureDate = today.AddWorkdays(5);
DateTime today = DateTime.Today;
DateTime nextMonday = today.NextWeekday(DayOfWeek.Monday);
The StringExtensions class provides a wide range of methods for string manipulation, validation, and conversion.
Pattern Matching:
Substring Extraction:
Type Conversion:
bool matches = "example".Like("exa*e"); // True
string result = "example".Left(3); // "exa"
The ObjectExtensions class provides advanced methods for working with objects, including serialization, deep cloning, and structural equality comparison.
JSON Serialization:
Deep Cloning:
Structural Equality:
Snapshot Reversion:
var person = new { Name = "John", Age = 30 };
string json = person.ToJson();
// With custom converters
var converters = new List<JsonConverter> { new CustomConverter() };
string jsonWithConverters = person.ToJson(converters);
var original = new Person { Name = "John", Age = 30 };
var clone = original.DeepClone();
var person1 = new Person { Name = "John", Age = 30 };
var person2 = new Person { Name = "John", Age = 30 };
bool areEqual = person1.DeepEqual(person2); // True
var snapshot = new Person { Name = "John", Age = 30 };
var current = new Person { Name = "Doe", Age = 25 };
current.RevertFrom(snapshot);
// current.Name is now "John"
// current.Age is now 30
Use Deep Cloning for Complex Objects:
Validate Structural Equality:
DeepEqual for scenarios requiring precise object comparisons.Revert State Safely:
RevertFrom to safely restore object states in tracked entities.The DataTableExtensions class provides methods for working with DataTable objects, such as counting duplicate rows and retrieving distinct records.
Count Duplicates:
DataTable instances.Retrieve Distinct Records:
int duplicateCount = table1.DuplicatesCount(table2);
DataTable distinctTable = table.DistinctRecords(new[] { "Name", "Age" });
The GuidExtensions class provides methods for working with Guid values, including converting them to nullable types.
Guid to a nullable Guid?, returning null if the Guid is empty.Guid id = Guid.NewGuid();
Guid? nullableId = id.ToNullable();
The Logging namespace provides a custom file-based logging implementation that integrates with the Microsoft.Extensions.Logging framework.
The FileLogger class in the MaksIT.Core.Logging namespace provides a simple and efficient way to log messages to plain text files. It supports log retention policies and ensures thread-safe writes using the LockManager.
Plain Text Logging:
Log Retention:
Thread Safety:
LockManager.Folder-Based Logging:
LoggerPrefix feature.var services = new ServiceCollection();
services.AddLogging(builder => builder.AddFileLogger("logs", TimeSpan.FromDays(7)));
var logger = services.BuildServiceProvider().GetRequiredService<ILogger<FileLogger>>();
logger.LogInformation("Logging to file!");
The JsonFileLogger class in the MaksIT.Core.Logging namespace provides structured logging in JSON format. It is ideal for machine-readable logs and integrates seamlessly with log aggregation tools.
JSON Logging:
Log Retention:
Thread Safety:
LockManager.Folder-Based Logging:
LoggerPrefix feature.var services = new ServiceCollection();
services.AddLogging(builder => builder.AddJsonFileLogger("logs", TimeSpan.FromDays(7)));
var logger = services.BuildServiceProvider().GetRequiredService<ILogger<JsonFileLogger>>();
logger.LogInformation("Logging to JSON file!");
The LoggerPrefix class in the MaksIT.Core.Logging namespace provides a type-safe way to specify logger categories with special prefixes. It extends the Enumeration base class and enables organizing logs into subfolders or applying custom categorization without using magic strings.
Type-Safe Prefixes:
Folder-Based Organization:
LoggerPrefix.Folder to write logs to specific subfolders.Extensible Categories:
LoggerPrefix.Category and LoggerPrefix.Tag are available for future use.Automatic Parsing:
LoggerPrefix.Parse().Backward Compatible:
ILogger<T> usage remains unchanged; prefixes are only applied when explicitly used.| Prefix | Purpose |
|---|---|
LoggerPrefix.Folder | Writes logs to a subfolder with the specified name |
LoggerPrefix.Category | Reserved for categorization (future use) |
LoggerPrefix.Tag | Reserved for tagging (future use) |
var services = new ServiceCollection();
services.AddLogging(builder => builder.AddFileLogger("logs", TimeSpan.FromDays(7)));
var provider = services.BuildServiceProvider();
var loggerFactory = provider.GetRequiredService<ILoggerFactory>();
// Create a logger that writes to logs/Audit/log_yyyy-MM-dd.txt
var auditLogger = loggerFactory.CreateLogger(LoggerPrefix.Folder.WithValue("Audit"));
auditLogger.LogInformation("Audit event occurred");
// Create a logger that writes to logs/Orders/log_yyyy-MM-dd.txt
var ordersLogger = loggerFactory.CreateLogger(LoggerPrefix.Folder.WithValue("Orders"));
ordersLogger.LogInformation("Order processed");
// Standard usage - logs go to the default folder (logs/log_yyyy-MM-dd.txt)
var logger = provider.GetRequiredService<ILogger<MyService>>();
logger.LogInformation("Standard log message");
var categoryName = "Folder:Audit";
var (prefix, value) = LoggerPrefix.Parse(categoryName);
if (prefix == LoggerPrefix.Folder) {
Console.WriteLine($"Folder: {value}"); // Output: Folder: Audit
}
| Logger Creation | Log File Location |
|---|---|
ILogger<MyService> | logs/log_2026-01-30.txt |
CreateLogger(LoggerPrefix.Folder.WithValue("Audit")) | logs/Audit/log_2026-01-30.txt |
CreateLogger(LoggerPrefix.Folder.WithValue("Orders")) | logs/Orders/log_2026-01-30.txt |
Use Type-Safe Prefixes:
LoggerPrefix.Folder.WithValue() instead of raw strings like "Folder:Audit".Organize by Domain:
Keep Default Logging Simple:
ILogger<T> for general application logging and folder prefixes for specialized logs.The LockManager class in the MaksIT.Core.Threading namespace provides a robust solution for managing concurrency and rate limiting. It ensures safe access to shared resources in multi-threaded or multi-process environments.
Thread Safety:
Rate Limiting:
Reentrant Locks:
var lockManager = new LockManager();
await lockManager.ExecuteWithLockAsync(async () => {
// Critical section
Console.WriteLine("Executing safely");
});
lockManager.Dispose();
The NetworkConnection class provides methods for managing connections to network shares on Windows.
Connect to Network Shares:
Error Handling:
var credentials = new NetworkCredential("username", "password");
if (NetworkConnection.TryCreate(logger, "\\server\share", credentials, out var connection, out var error)) {
connection.Dispose();
}
The PingPort class provides methods for checking the reachability of a host on specified TCP or UDP ports.
TCP Port Checking:
UDP Port Checking:
if (PingPort.TryHostPort("example.com", 80, out var error)) {
Console.WriteLine("Port is reachable.");
}
The AESGCMUtility class provides methods for encrypting and decrypting data using AES-GCM.
Secure Encryption:
Data Integrity:
var key = AESGCMUtility.GenerateKeyBase64();
AESGCMUtility.TryEncryptData(data, key, out var encryptedData, out var error);
The Base32Encoder class provides methods for encoding and decoding data in Base32 format.
Encoding:
Decoding:
Base32Encoder.TryEncode(data, out var encoded, out var error);
The ChecksumUtility class provides methods for calculating and verifying CRC32 checksums.
Checksum Calculation:
Checksum Verification:
ChecksumUtility.TryCalculateCRC32Checksum(data, out var checksum, out var error);
The PasswordHasher class provides methods for securely hashing and validating passwords using salt and pepper.
const string pepper = "YourAppSecretPepper";
PasswordHasher.TryCreateSaltedHash("password", pepper, out var hashResult, out var error);
// hashResult.Salt and hashResult.Hash are Base64 strings
const string pepper = "YourAppSecretPepper";
PasswordHasher.TryValidateHash("password", hashResult.Salt, hashResult.Hash, pepper, out var isValid, out var error);
public static bool TryCreateSaltedHash(
string value,
string pepper,
out (string Salt, string Hash)? saltedHash,
out string? errorMessage)
value: The password to hash.pepper: Application-level secret (not stored with the hash).saltedHash: Tuple containing the generated salt and hash (Base64 strings).errorMessage: Error message if hashing fails.public static bool TryValidateHash(
string value,
string salt,
string hash,
string pepper,
out bool isValid,
out string? errorMessage)
value: The password to validate.salt: The Base64-encoded salt used for hashing.hash: The Base64-encoded hash to validate against.pepper: Application-level secret (must match the one used for hashing).isValid: True if the password is valid.errorMessage: Error message if validation fails.The JwtGenerator class provides methods for generating and validating JSON Web Tokens (JWTs).
Token Generation:
Token Validation:
JwtGenerator.TryGenerateToken(secret, issuer, audience, 60, "user", roles, out var token, out var error);
The JwkGenerator class in the MaksIT.Core.Security.JWK namespace provides a utility method for generating a minimal RSA public JWK (JSON Web Key) from a given RSA instance.
RSA object and encodes them as a JWK.using System.Security.Cryptography;
using MaksIT.Core.Security.JWK;
using var rsa = RSA.Create(2048);
var result = JwkGenerator.TryGenerateFromRSA(rsa, out var jwk, out var errorMessage);
if (result)
{
// jwk contains KeyType, RsaExponent, RsaModulus
Console.WriteLine($"Exponent: {jwk!.RsaExponent}, Modulus: {jwk.RsaModulus}");
}
else
{
Console.WriteLine($"Error: {errorMessage}");
}
public static bool TryGenerateFromRSA(
RSA rsa,
[NotNullWhen(true)] out Jwk? jwk,
[NotNullWhen(false)] out string? errorMessage
)
rsa: The RSA instance to extract public parameters from.jwk: The resulting JWK object (with KeyType, RsaExponent, and RsaModulus).errorMessage: Error message if generation fails.false and an error message if the RSA parameters are missing or invalid.The JwsGenerator class in the MaksIT.Core.Security.JWS namespace provides methods for creating JSON Web Signatures (JWS) using RSA keys and JWKs. It supports signing string or object payloads and produces JWS objects with protected headers, payload, and signature.
Algorithm property to RS256 in the protected header.KeyId or the full Jwk in the protected header, depending on the presence of KeyId.using System.Security.Cryptography;
using MaksIT.Core.Security.JWK;
using MaksIT.Core.Security.JWS;
using var rsa = RSA.Create(2048);
JwkGenerator.TryGenerateFromRSA(rsa, out var jwk, out var errorMessage);
var header = new JwsHeader();
var payload = "my-payload";
var result = JwsGenerator.TryEncode(rsa, jwk!, header, payload, out var jwsMessage, out var error);
if (result)
{
Console.WriteLine($"Signature: {jwsMessage!.Signature}");
}
else
{
Console.WriteLine($"Error: {error}");
}
public static bool TryEncode<THeader>(
RSA rsa,
Jwk jwk,
THeader protectedHeader,
[NotNullWhen(true)] out JwsMessage? message,
[NotNullWhen(false)] out string? errorMessage
) where THeader : JwsHeader
public static bool TryEncode<THeader, TPayload>(
RSA rsa,
Jwk jwk,
THeader protectedHeader,
TPayload? payload,
[NotNullWhen(true)] out JwsMessage? message,
[NotNullWhen(false)] out string? errorMessage
) where THeader : JwsHeader
KeyId, it is set in the header; otherwise, the full JWK is included.false and an error message if signing fails.The JwkThumbprintUtility class in the MaksIT.Core.Security.JWK namespace provides methods for computing RFC7638 JWK SHA-256 thumbprints and generating key authorization strings for ACME challenges.
using System.Security.Cryptography;
using MaksIT.Core.Security.JWK;
using var rsa = RSA.Create(2048);
JwkGenerator.TryGenerateFromRSA(rsa, out var jwk, out var errorMessage);
var result = JwkThumbprintUtility.TryGetSha256Thumbprint(jwk!, out var thumbprint, out var error);
if (result)
{
Console.WriteLine($"Thumbprint: {thumbprint}");
}
else
{
Console.WriteLine($"Error: {error}");
}
var token = "acme-token";
var result = JwkThumbprintUtility.TryGetKeyAuthorization(jwk!, token, out var keyAuth, out var error);
if (result)
{
Console.WriteLine($"Key Authorization: {keyAuth}");
}
else
{
Console.WriteLine($"Error: {error}");
}
public static bool TryGetSha256Thumbprint(
Jwk jwk,
[NotNullWhen(true)] out string? thumbprint,
[NotNullWhen(false)] out string? errorMessage
)
public static bool TryGetKeyAuthorization(
Jwk jwk,
string token,
[NotNullWhen(true)] out string? keyAuthorization,
[NotNullWhen(false)] out string? errorMessage
)
{token}.{thumbprint}.false and an error message if required JWK fields are missing or invalid.The TotpGenerator class in the MaksIT.Core.Security namespace provides methods for generating and validating Time-based One-Time Passwords (TOTP) for two-factor authentication.
TOTP Validation:
TOTP Generation:
Secret Generation:
Recovery Codes:
Auth Link Generation:
otpauth:// URIs for QR code scanning in authenticator apps.TotpGenerator.TryGenerateSecret(out var secret, out var error);
// secret is a Base32-encoded string for use with authenticator apps
var timeTolerance = 1; // Allow 1 time step before/after current
TotpGenerator.TryValidate(totpCode, secret, timeTolerance, out var isValid, out var error);
if (isValid) {
Console.WriteLine("TOTP is valid");
}
TotpGenerator.TryGenerateRecoveryCodes(10, out var recoveryCodes, out var error);
// recoveryCodes contains 10 codes in format "XXXX-XXXX"
TotpGenerator.TryGenerateTotpAuthLink(
"MyApp",
"user@example.com",
secret,
"MyApp",
null, // algorithm (default SHA1)
null, // digits (default 6)
null, // period (default 30)
out var authLink,
out var error
);
// authLink = "otpauth://totp/MyApp:user@example.com?secret=...&issuer=MyApp"
The Webapi namespace provides models and utilities for building Web APIs, including pagination support and patch operations.
The PagedRequest class in the MaksIT.Core.Webapi.Models namespace provides a base class for paginated API requests with filtering and sorting capabilities.
Pagination:
Dynamic Filtering:
Dynamic Sorting:
| Property | Type | Default | Description |
|---|---|---|---|
PageSize | int | 100 | Number of items per page |
PageNumber | int | 1 | Current page number |
Filters | string? | null | Filter expression string |
SortBy | string? | null | Property name to sort by |
IsAscending | bool | true | Sort direction |
var request = new PagedRequest {
PageSize = 20,
PageNumber = 1,
Filters = "Name.Contains(\"John\") && Age > 18",
SortBy = "Name",
IsAscending = true
};
var filterExpression = request.BuildFilterExpression<User>();
var sortExpression = request.BuildSortExpression<User>();
var results = dbContext.Users
.Where(filterExpression)
.OrderBy(sortExpression)
.Skip((request.PageNumber - 1) * request.PageSize)
.Take(request.PageSize)
.ToList();
The PagedResponse<T> class in the MaksIT.Core.Webapi.Models namespace provides a generic wrapper for paginated API responses.
| Property | Type | Description |
|---|---|---|
Items | IEnumerable<T> | The items for the current page |
PageNumber | int | Current page number |
PageSize | int | Number of items per page |
TotalCount | int | Total number of items across all pages |
TotalPages | int | Calculated total number of pages |
HasPreviousPage | bool | Whether a previous page exists |
HasNextPage | bool | Whether a next page exists |
var items = await dbContext.Users
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
var totalCount = await dbContext.Users.CountAsync();
var response = new PagedResponse<UserDto>(items, totalCount, pageNumber, pageSize);
// response.TotalPages, response.HasNextPage, etc. are automatically calculated
The PatchOperation enum in the MaksIT.Core.Webapi.Models namespace defines operations for partial updates (PATCH requests).
| Value | Description |
|---|---|
SetField | Set or replace a normal field value |
RemoveField | Set a field to null |
AddToCollection | Add an item to a collection property |
RemoveFromCollection | Remove an item from a collection property |
public class UserPatchRequest : PatchRequestModelBase {
public PatchOperation Operation { get; set; }
public string PropertyName { get; set; }
public object? Value { get; set; }
}
// Example: Set a field
var patch = new UserPatchRequest {
Operation = PatchOperation.SetField,
PropertyName = "Name",
Value = "New Name"
};
// Example: Add to collection
var patch = new UserPatchRequest {
Operation = PatchOperation.AddToCollection,
PropertyName = "Roles",
Value = "Admin"
};
The Culture class provides methods for dynamically setting the culture for the current thread.
Culture.TrySet("fr-FR", out var error);
The EnvVar class provides methods for managing environment variables.
Add to PATH:
PATH environment variable.Set and Unset Variables:
EnvVar.TryAddToPath("/usr/local/bin", out var error);
The FileSystem class provides methods for working with files and directories.
Copy Files and Folders:
Delete Files and Folders:
FileSystem.TryCopyToFolder("source", "destination", true, out var error);
The Processes class provides methods for managing system processes.
Start Processes:
Kill Processes:
Processes.TryStart("notepad.exe", "", 0, false, out var error);
The EnumExtensions class provides utility methods for working with enum types, specifically for retrieving display names defined via the DisplayAttribute.
DisplayAttribute.Name property for an enum value, or fall back to the enum's name if the attribute is not present.using System.ComponentModel.DataAnnotations;
using MaksIT.Core.Extensions;
public enum Status {
[Display(Name = "In Progress")]
InProgress,
Completed
}
var status = Status.InProgress;
string displayName = status.GetDisplayName(); // "In Progress"
var completed = Status.Completed;
string completedName = completed.GetDisplayName(); // "Completed"
Display attribute on enum members to provide user-friendly names for UI or logging.GetDisplayName() to consistently retrieve display names for enums throughout your application.If you have any questions or need further assistance, feel free to reach out:
See LICENSE.md.