Essential utilities, value objects, JSON extensions, and global service provider management for building clean architecture applications. Includes type-safe constants, value objects, string utilities, and centralized dependency injection.
$ dotnet add package Myth.CommonsMyth.Commons is a foundational .NET library providing essential utilities and patterns for building robust, maintainable enterprise applications. It offers JSON serialization, string manipulation, DDD building blocks, and centralized service provider management for cross-library dependency resolution.
dotnet add package Myth.CommonsPowerful JSON serialization and deserialization with System.Text.Json, featuring global configuration, custom converters, and flexible naming strategies.
using Myth.Extensions;
// Serialize to JSON
var user = new User { Id = 1, Name = "John Doe", Email = "john@example.com" };
var json = user.ToJson();
// {"id":1,"name":"John Doe","email":"john@example.com"}
// Deserialize from JSON
var userObj = json.FromJson<User>();Configure JSON settings globally for your entire application:
JsonExtensions.Configure( settings => settings
.UseCaseStrategy( CaseStrategy.SnakeCase )
.IgnoreNull()
.Minify()
);
var json = user.ToJson();
// {"id":1,"name":"John Doe","email":"john@example.com"}Override global settings for specific operations:
// Use snake_case for this operation only
var json = user.ToJson( settings => settings
.UseCaseStrategy( CaseStrategy.SnakeCase )
);
// {"id":1,"name":"john doe","email":"john@example.com"}
// Minify JSON output
var compactJson = user.ToJson( settings => settings.Minify() );
// Ignore null values
var jsonWithoutNulls = user.ToJson( settings => settings.IgnoreNull() );Handle interfaces and abstract types during serialization/deserialization:
// Using generic converter
var json = user.ToJson( settings => settings
.UseInterfaceConverter<IAddress, Address>()
);
// Using non-generic converter
var json = user.ToJson( settings => settings
.UseInterfaceConverter( typeof( IAddress ), typeof( Address ) )
);Add custom System.Text.Json converters:
var json = user.ToJson( settings => settings
.UseCustomConverter( new CustomDateTimeConverter() )
);Access underlying JsonSerializerOptions for fine-grained control:
var json = user.ToJson( settings => {
settings.IgnoreNull().Minify();
settings.OtherSettings = options => {
options.MaxDepth = 64;
options.NumberHandling = JsonNumberHandling.AllowReadingFromString;
};
} );Two naming conventions are supported:
public enum CaseStrategy {
CamelCase, // myAwesomeProperty
SnakeCase // my_awesome_property
}Deserialize to dynamic objects:
var json = "{\"name\":\"John\",\"age\":30}";
dynamic obj = json.FromJson<object>();
Console.WriteLine( obj.name ); // JohnAll JSON operations throw JsonParsingException on failure:
try {
var obj = invalidJson.FromJson<User>();
} catch ( JsonParsingException ex ) {
Console.WriteLine( $"JSON parsing failed: {ex.Message}" );
Console.WriteLine( $"Inner exception: {ex.InnerException?.Message}" );
}Rich set of utilities for string manipulation and analysis.
using Myth.Extensions;
// Remove text
var result = "Hello World".Remove( "World" ); // "Hello "
// Minify (remove all whitespace)
var minified = "Hello World\n\t".Minify(); // "HelloWorld"
// Change case of first letter
var lower = "Hello".ToFirstLower(); // "hello"
var upper = "hello".ToFirstUpper(); // "Hello"
// Extract text between characters
var text = "The 'quick' brown fox";
var extracted = text.GetStringBetween( '\'' ); // "quick"
// Find words
var sentence = "The quick brown fox";
var word = sentence.GetWordThatContains( "qui" ); // "quick"
var before = sentence.GetWordBefore( "brown" ); // "quick"
var after = sentence.GetWordAfter( "quick" ); // "brown"
// Search operations
var hasAny = "Hello World".ContainsAnyOf( "Hi", "Hello", "Hey" ); // true
var startsWithAny = "Hello World".StartsWithAnyOf( "Hi", "Hello" ); // trueEncode objects for URL usage:
using Myth.Extensions;
var text = "Hello World";
var encoded = text.EncodeAsUrl(); // "Hello+World"
var flag = true;
var encodedFlag = flag.EncodeAsUrl(); // true (as boolean)Base class for implementing Domain-Driven Design value objects with structural equality.
using Myth.ValueObjects;
public class Address : ValueObject {
public string Street { get; }
public string City { get; }
public string ZipCode { get; }
public Address( string street, string city, string zipCode ) {
Street = street;
City = city;
ZipCode = zipCode;
}
protected override IEnumerable<object> GetAtomicValues() {
yield return Street;
yield return City;
yield return ZipCode;
}
}
// Value objects are compared by their values, not reference
var address1 = new Address( "123 Main St", "Springfield", "12345" );
var address2 = new Address( "123 Main St", "Springfield", "12345" );
var address3 = new Address( "456 Oak Ave", "Springfield", "12345" );
Console.WriteLine( address1 == address2 ); // true (same values)
Console.WriteLine( address1 == address3 ); // false (different values)
// Clone value objects
var clone = address1.Clone();Type-safe constants using Ardalis.SmartEnum pattern.
using Myth.ValueObjects;
public class OrderStatus : Constant<OrderStatus, string> {
public static readonly OrderStatus Pending = new( nameof( Pending ), "PENDING" );
public static readonly OrderStatus Processing = new( nameof( Processing ), "PROCESSING" );
public static readonly OrderStatus Completed = new( nameof( Completed ), "COMPLETED" );
public static readonly OrderStatus Cancelled = new( nameof( Cancelled ), "CANCELLED" );
private OrderStatus( string name, string value ) : base( name, value ) { }
}
// Usage
var status = OrderStatus.Pending;
string statusValue = status; // Implicit conversion to "PENDING"
// Get from value
var status2 = OrderStatus.FromValue( "PROCESSING" ); // OrderStatus.Processing
// Get from name
var status3 = OrderStatus.FromName( "Completed" ); // OrderStatus.Completed
// List all options
var options = OrderStatus.GetOptions();
// "(Pending): PENDING | (Processing): PROCESSING | (Completed): COMPLETED | (Cancelled): CANCELLED"
// Switch on constants (exhaustive)
var message = status switch {
var s when s == OrderStatus.Pending => "Order is pending",
var s when s == OrderStatus.Processing => "Order is being processed",
var s when s == OrderStatus.Completed => "Order is completed",
var s when s == OrderStatus.Cancelled => "Order was cancelled",
_ => throw new InvalidOperationException()
};Global service provider management enables cross-library dependency resolution without coupling libraries together.
Use BuildApp() instead of Build() to automatically initialize the global service provider:
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddFlow();
builder.Services.AddGuard();
builder.Services.AddFlowActions( config => { ... } );
var app = builder.BuildApp(); // Instead of builder.Build()
app.UseGuard();
app.Run();Use the global service provider for non-web applications:
var services = new ServiceCollection();
services.AddFlow();
services.AddGuard();
services.AddMyServices();
var serviceProvider = services.BuildServiceProvider();
MythServiceProvider.Initialize( serviceProvider );
// Now all libraries can resolve dependencies
var pipeline = Pipeline.Start( context );using Myth.ServiceProvider;
// Check if initialized
if ( MythServiceProvider.IsInitialized ) {
var provider = MythServiceProvider.Current;
}
// Get or throw if not initialized
var requiredProvider = MythServiceProvider.GetRequired();
// Get with fallback
var provider = MythServiceProvider.GetOrFallback( localServiceProvider );
// Try initialize (first-wins pattern)
var initialized = MythServiceProvider.TryInitialize( serviceProvider );
// Force initialize (overwrites existing)
MythServiceProvider.Initialize( serviceProvider );External libraries can access registered services:
public class ThirdPartyLibrary {
public void DoSomething() {
var provider = ServiceCollectionExtensions.GetGlobalProvider();
var validator = provider?.GetService<IValidator>();
if ( validator != null ) {
// Use Myth libraries without direct coupling
}
}
}Reset the global provider for isolated unit tests:
[Fact]
public void TestWithCleanProvider() {
MythServiceProvider.Reset();
var services = new ServiceCollection();
// ... configure test services
var provider = services.BuildServiceProvider();
MythServiceProvider.Initialize( provider );
// Run test
}Pattern for executing operations within automatic service scopes, perfect for transient handlers accessing scoped dependencies like repositories with DbContext.
Register the scoped service provider pattern once in your application:
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddScopedServiceProvider();
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddDbContext<AppDbContext>();
var app = builder.BuildApp();public class CreateOrderHandler : ICommandHandler<CreateOrderCommand> {
private readonly IScopedService<IOrderRepository> _repository;
private readonly IScopedService<IEmailService> _emailService;
public CreateOrderHandler(
IScopedService<IOrderRepository> repository,
IScopedService<IEmailService> emailService ) {
_repository = repository;
_emailService = emailService;
}
public async Task<CommandResult> HandleAsync(
CreateOrderCommand command,
CancellationToken ct ) {
// Execute with automatic scope management
var order = await _repository.ExecuteAsync( repo =>
repo.CreateAsync( command.OrderData, ct )
);
// Execute void operations
await _emailService.ExecuteAsync( email =>
email.SendOrderConfirmationAsync( order.Id, ct )
);
return CommandResult.Success();
}
}// With return value
var result = _scopedService.Execute( service =>
service.GetData()
);
// Void operation
_scopedService.Execute( service =>
service.ProcessData()
);// With return value
var result = await _scopedService.ExecuteAsync( service =>
service.GetDataAsync()
);
// Void operation
await _scopedService.ExecuteAsync( service =>
service.ProcessDataAsync()
);Value objects and interfaces for implementing paginated results.
using Myth.ValueObjects;
// Default pagination (page 1, size 10)
var pagination = Pagination.Default;
// Custom pagination
var customPagination = new Pagination( pageNumber: 2, pageSize: 20 );
// Get all items (single page)
var allItems = Pagination.All;
// ASP.NET Core automatic binding
[HttpGet]
public IActionResult GetOrders( [FromQuery] Pagination pagination ) {
// Automatically binds from query string: ?$pagenumber=2&$pagesize=20
}using Myth.Interfaces.Results;
using Myth.Models.Results;
// Create paginated result
var items = await repository.GetOrdersAsync( pagination );
var total = await repository.GetTotalCountAsync();
var totalPages = ( int )Math.Ceiling( ( double )total / pagination.PageSize );
var result = new Paginated<Order>(
pageNumber: pagination.PageNumber,
pageSize: pagination.PageSize,
totalItems: total,
totalPages: totalPages,
items: items
);
// Access properties
Console.WriteLine( $"Page {result.PageNumber} of {result.TotalPages}" );
Console.WriteLine( $"Showing {result.Items.Count()} of {result.TotalItems} items" );
// Return in API
return Ok( result );Implement custom paginated types:
public class CustomPaginatedResult<T> : IPaginated<T> {
public int PageNumber { get; set; }
public int PageSize { get; set; }
public int TotalPages { get; set; }
public int TotalItems { get; set; }
public IEnumerable<T> Items { get; set; }
// Add custom properties
public bool HasNextPage => PageNumber < TotalPages;
public bool HasPreviousPage => PageNumber > 1;
}Helper methods for working with collections.
using Myth.Extensions;
var items = new[] { "apple", "banana", "cherry" };
// Join with separator
var result = items.ToStringWithSeparator( ", " );
// "apple, banana, cherry"
// Custom separator
var result2 = items.ToStringWithSeparator( " | " );
// "apple | banana | cherry"
// Default separator (", ")
var result3 = items.ToStringWithSeparator();
// "apple, banana, cherry"Myth.Commons provides essential DDD building blocks:
// Value Object for Money
public class Money : ValueObject {
public decimal Amount { get; }
public string Currency { get; }
public Money( decimal amount, string currency ) {
Amount = amount;
Currency = currency;
}
protected override IEnumerable<object> GetAtomicValues() {
yield return Amount;
yield return Currency;
}
public Money Add( Money other ) {
if ( Currency != other.Currency )
throw new InvalidOperationException( "Cannot add money with different currencies" );
return new Money( Amount + other.Amount, Currency );
}
}
// Type-safe constants
public class Currency : Constant<Currency, string> {
public static readonly Currency USD = new( nameof( USD ), "USD" );
public static readonly Currency EUR = new( nameof( EUR ), "EUR" );
public static readonly Currency BRL = new( nameof( BRL ), "BRL" );
private Currency( string name, string value ) : base( name, value ) { }
}Perfect for CQRS patterns with scoped service management:
public class GetOrdersQueryHandler : IQueryHandler<GetOrdersQuery, IPaginated<Order>> {
private readonly IScopedService<IOrderRepository> _repository;
public GetOrdersQueryHandler( IScopedService<IOrderRepository> repository ) {
_repository = repository;
}
public async Task<QueryResult<IPaginated<Order>>> HandleAsync(
GetOrdersQuery query,
CancellationToken ct ) {
var result = await _repository.ExecuteAsync( async repo => {
var items = await repo.GetOrdersAsync( query.Pagination, ct );
var total = await repo.GetTotalCountAsync( ct );
return new Paginated<Order>(
query.Pagination.PageNumber,
query.Pagination.PageSize,
total,
( int )Math.Ceiling( ( double )total / query.Pagination.PageSize ),
items
);
} );
return QueryResult<IPaginated<Order>>.Success( result );
}
}Supports Clean Architecture principles:
JsonParsingException for robust error handlingGetAtomicValues() to include all properties that define equalityBuildApp() for ASP.NET Core applicationsTryInitialize() for library code (first-wins pattern)AddScopedServiceProvider() once per applicationContributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the Apache License 2.0 - see the LICENSE for details.
For issues, questions, or contributions, please visit the GitLab repository.