Awesome-omni-skill myth
Complete guide for using the Myth .NET ecosystem - enterprise-grade libraries for building scalable applications with SOLID principles, clean architecture, CQRS, validation, pipelines, and DDD patterns
git clone https://github.com/diegosouzapw/awesome-omni-skill
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/development/myth" ~/.claude/skills/diegosouzapw-awesome-omni-skill-myth && rm -rf "$T"
skills/development/myth/SKILL.mdMyth Ecosystem Skill Documentation
This skill provides complete guidance for using the Myth ecosystem - a comprehensive collection of .NET libraries designed for enterprise applications following SOLID principles, Clean Code, and Domain-Driven Design.
Table of Contents
- Overview
- Architecture & Integration
- Library-Specific Guides
- Common Workflows
- Best Practices
- Examples by Scenario
Overview
What is Myth?
Myth is a modular collection of .NET libraries that work together to provide:
- Myth.Commons: Base utilities, ValueObjects, global ServiceProvider management, JSON extensions
- Myth.DependencyInjection: Auto-discovery and convention-based service registration
- Myth.Flow: Pipeline pattern with Result, retry policies, telemetry
- Myth.Flow.Actions: CQRS, event-driven architecture, message brokers (Kafka, RabbitMQ)
- Myth.Guard: Fluent validation with 100+ rules, context-aware, async service integration
- Myth.Morph: Object transformation and mapping with schema-based bindings
- Myth.Specification: Query specification pattern for encapsulating business rules
- Myth.Repository: Generic repository interfaces with read/write separation
- Myth.Repository.EntityFramework: EF Core implementation with Unit of Work
- Myth.Rest: Fluent REST client with circuit breaker and retry
- Myth.Testing: Testing utilities, mocks, base test classes
- Myth.Tool: CLI tool for code generation with CQRS, DDD, and Clean Architecture patterns
Key Architectural Concepts
MythServiceProvider (Global Service Provider)
All Myth libraries use a centralized, thread-safe service provider for dependency resolution:
// ASP.NET Core - use BuildApp() instead of Build() var app = builder.BuildApp(); // Initializes MythServiceProvider.Current // Console Apps - use BuildWithGlobalProvider() var serviceProvider = services.BuildWithGlobalProvider(); // Accessing global provider var provider = MythServiceProvider.Current; var service = MythServiceProvider.GetRequired();
IScopedService<T>
Allows transient services (like handlers) to use scoped services safely:
public class CreateOrderHandler : ICommandHandler<CreateOrderCommand> { private readonly IScopedService<IOrderRepository> _repository; public async Task<CommandResult> HandleAsync(CreateOrderCommand command, CancellationToken ct) { return await _repository.ExecuteAsync(async repo => { var order = await repo.CreateAsync(command, ct); return CommandResult.Success(); }); } }
Architecture & Integration
Setup for ASP.NET Core Applications
using Myth.Extensions; var builder = WebApplication.CreateBuilder(args); // 1. Add core libraries builder.Services.AddFlow(config => config .UseTelemetry() .UseRetry(maxAttempts: 3, backoffMs: 1000) .UseActions(actions => actions .UseInMemory() // or .UseKafka() / .UseRabbitMQ() .UseCaching() .ScanAssemblies(typeof(Program).Assembly) .EnableAutoSubscription())); builder.Services.AddGuard(); builder.Services.AddMorph(); // 2. Register repositories and services builder.Services.AddDbContext<AppDbContext>(); builder.Services.AddScoped<IOrderRepository, OrderRepository>(); builder.Services.AddScoped<IUnitOfWorkRepository, UnitOfWorkRepository>(); // 3. Build with global service provider (CRITICAL!) var app = builder.BuildApp(); // NOT builder.Build() // 4. Add middleware app.UseGuard(); // Validation exception handling app.MapControllers(); app.Run();
Setup for Console Applications
var services = new ServiceCollection(); services.AddFlow(config => config.UseTelemetry().UseRetry(3, 100)); services.AddGuard(); services.AddMorph(); // Register your services services.AddScoped<IDataService, DataService>(); // Build with global provider var serviceProvider = services.BuildWithGlobalProvider(); // Use services var dataService = serviceProvider.GetRequiredService<IDataService>(); await dataService.ProcessDataAsync();
Library-Specific Guides
1. Myth.Commons
Global Service Provider Management
// Initialize (done automatically by BuildApp/BuildWithGlobalProvider) MythServiceProvider.Initialize(serviceProvider); // Check if initialized if (MythServiceProvider.IsInitialized) { var current = MythServiceProvider.Current; } // Get required (throws if not initialized) var provider = MythServiceProvider.GetRequired(); // Get with fallback var provider = MythServiceProvider.GetOrFallback(fallbackProvider); // Reset (for testing) MythServiceProvider.Reset();
JSON Extensions
// Global configuration JsonExtensions.Configure(settings => { settings.CaseStrategy = CaseStrategy.SnakeCase; settings.IgnoreNullValues = true; settings.MinifyResult = true; }); // Serialize to JSON var json = user.ToJson(); var minifiedJson = user.ToJson(s => s.MinifyResult = true); // Deserialize from JSON var user = json.FromJson<User>(); var safeUser = json.SafeFromJson<User>(); // Returns null if fails // Validate JSON if (content.IsValidJson()) { var data = content.FromJson<Data>(); } // Deserialize or throw with HTTP context var user = json.FromJsonOrThrow<User>(HttpStatusCode.BadRequest);
ValueObjects
// Define ValueObject public class Address : ValueObject { public string Street { get; } public string City { get; } public string Country { get; } public Address(string street, string city, string country) { Street = street; City = city; Country = country; } protected override IEnumerable<object> GetAtomicValues() { yield return Street; yield return City; yield return Country; } } // Usage var addr1 = new Address("123 Main St", "New York", "USA"); var addr2 = new Address("123 Main St", "New York", "USA"); Console.WriteLine(addr1.Equals(addr2)); // True (value equality)
Constants (Type-Safe Enums)
// Define constant public class OrderStatus : Constant<OrderStatus, string> { public static readonly OrderStatus Pending = CreateWithCallerName("P"); public static readonly OrderStatus Active = CreateWithCallerName("A"); public static readonly OrderStatus Completed = CreateWithCallerName("C"); public static readonly OrderStatus Cancelled = CreateWithCallerName("X"); private OrderStatus(string name, string value) : base(name, value) { } } // Usage var status = OrderStatus.FromValue("A"); // Returns Active var all = OrderStatus.GetAll(); // All instances var allValues = OrderStatus.Values.All; // ["P", "A", "C", "X"] string options = OrderStatus.GetOptions(); // "P: Pending | A: Active | ..." // Try get if (OrderStatus.TryFromValue("A", out var result)) { Console.WriteLine(result.Name); // "Active" }
2. Myth.Flow - Pipeline Pattern
Basic Pipeline
public class OrderService { public async Task<Result<OrderDto>> ProcessOrder(CreateOrderRequest request) { return await Pipeline.Start(request) .WithTelemetry("ProcessOrder") .WithRetry(maxAttempts: 3, backoffMs: 100) .StepResultAsync<ValidationService>((svc, ctx) => svc.ValidateAsync(ctx)) .StepResultAsync<OrderCreationService>((svc, ctx) => svc.CreateAsync(ctx)) .TapAsync<EventService>((svc, ctx) => svc.PublishOrderCreatedAsync(ctx)) .Transform<OrderDto>(ctx => new OrderDto { OrderId = ctx.OrderId, Total = ctx.Total }) .ExecuteAsync(); } }
Step Types
// Synchronous step .Step(ctx => { ctx.ProcessedData = ProcessData(ctx.RawData); return ctx; }) // Async step .StepAsync(async ctx => { ctx.Data = await LoadDataAsync(ctx.Id); return ctx; }) // Step with Result pattern .StepResultAsync(async ctx => { if (ctx.IsValid) { return Result<Context>.Success(ctx); } return Result<Context>.Failure("Invalid context"); }) // Side effects (tap) .Tap(ctx => Console.WriteLine($"Processing: {ctx.Id}")) .TapAsync(async ctx => await LogAsync(ctx)) // Conditional execution .When( ctx => ctx.Amount > 1000, pipeline => pipeline .StepAsync(ctx => FraudCheckAsync(ctx)) .StepAsync(ctx => ApprovalAsync(ctx))) // Transform to different type .Transform<OutputDto>(ctx => new OutputDto { Id = ctx.Id, Name = ctx.Name })
Configuration
// Global configuration builder.Services.AddFlow(config => config .UseTelemetry() .UseRetry(maxAttempts: 3, backoffMs: 100) .PropagateExceptions(typeof(ValidationException))); // Per-pipeline configuration Pipeline.Start(context, config => { config.EnableTelemetry = true; config.DefaultRetryAttempts = 5; config.DefaultBackoffMs = 200; })
3. Myth.Flow.Actions - CQRS & Event-Driven
Commands
// Define command public record CreateOrderCommand(CreateOrderDto Data) : ICommand<Guid>; // Command handler public class CreateOrderHandler : ICommandHandler<CreateOrderCommand, Guid> { private readonly IScopedService<IOrderRepository> _repository; private readonly IValidator _validator; private readonly IDispatcher _dispatcher; public async Task<CommandResult<Guid>> HandleAsync( CreateOrderCommand command, CancellationToken ct) { // Validate await _validator.ValidateAsync(command.Data, ValidationContextKey.Create, ct); // Process with pipeline var result = await Pipeline.Start(command.Data) .StepResultAsync(async dto => { var order = dto.To<Order>(); await _repository.ExecuteAsync(repo => repo.AddAsync(order, ct)); return Result<Order>.Success(order); }) .TapAsync(async order => { await _dispatcher.PublishEventAsync( new OrderCreatedEvent(order.Id), ct); }) .ExecuteAsync(ct); if (result.IsFailure) { return CommandResult<Guid>.Failure(result.ErrorMessage); } return CommandResult<Guid>.Success(result.Value.Id); } } // Usage in controller [HttpPost] public async Task<IActionResult> Create(CreateOrderDto dto) { var command = new CreateOrderCommand(dto); var result = await _dispatcher.DispatchCommandAsync<CreateOrderCommand, Guid>(command); return result.IsSuccess ? CreatedAtAction(nameof(Get), new { id = result.Data }, result.Data) : BadRequest(result.ErrorMessage); }
Queries with Caching
// Define query public record GetOrderQuery(Guid OrderId) : IQuery<OrderDto>; // Query handler public class GetOrderHandler : IQueryHandler<GetOrderQuery, OrderDto> { private readonly IScopedService<IOrderRepository> _repository; public async Task<QueryResult<OrderDto>> HandleAsync( GetOrderQuery query, CancellationToken ct) { var spec = SpecBuilder<Order>.Create() .And(o => o.Id == query.OrderId); var order = await _repository.ExecuteAsync(repo => repo.FirstOrDefaultAsync(spec, ct)); if (order == null) { return QueryResult<OrderDto>.Failure("Order not found"); } var dto = order.To<OrderDto>(); return QueryResult<OrderDto>.Success(dto); } } // Usage with cache [HttpGet("{id}")] public async Task<IActionResult> Get(Guid id) { var query = new GetOrderQuery(id); var cacheOptions = new CacheOptions { Enabled = true, Ttl = TimeSpan.FromMinutes(5), CacheKey = $"order:{id}" }; var result = await _dispatcher.DispatchQueryAsync<GetOrderQuery, OrderDto>( query, cacheOptions); return result.IsSuccess ? Ok(result.Data) : NotFound(); }
Events
// Define event public record OrderCreatedEvent(Guid OrderId, decimal Total, string CustomerEmail) : IEvent; // Event handler public class OrderCreatedHandler : IEventHandler<OrderCreatedEvent> { private readonly IScopedService<IEmailService> _emailService; private readonly IScopedService<IInventoryService> _inventoryService; public async Task HandleAsync(OrderCreatedEvent @event, CancellationToken ct) { await Pipeline.Start(@event) .TapAsync(async evt => { await _emailService.ExecuteAsync(svc => svc.SendOrderConfirmationAsync(evt.CustomerEmail, evt.OrderId, ct)); }) .TapAsync(async evt => { await _inventoryService.ExecuteAsync(svc => svc.UpdateStockAsync(evt.OrderId, ct)); }) .ExecuteAsync(ct); } } // Publish event await _dispatcher.PublishEventAsync(new OrderCreatedEvent(orderId, total, email));
Configuration
builder.Services.AddFlow(config => config .UseTelemetry() .UseRetry(3, 1000) .UseActions(actions => actions // Message Broker .UseInMemory(opts => { opts.UseCircuitBreaker = true; opts.UseRetryPolicy = true; opts.UseDeadLetterQueue = true; }) // or .UseKafka(kafka => { kafka.BootstrapServers = "localhost:9092"; kafka.GroupId = "my-app"; kafka.Topic = "events"; }) // or .UseRabbitMQ(rabbit => { rabbit.HostName = "localhost"; rabbit.UserName = "guest"; rabbit.Password = "guest"; rabbit.ExchangeName = "my-exchange"; }) // Caching .UseCaching(cache => { cache.ProviderType = CacheProviderType.Memory; cache.DefaultTtl = TimeSpan.FromMinutes(5); }) // Handler scanning .ScanAssemblies(typeof(Program).Assembly) .EnableAutoSubscription()));
4. Myth.Guard - Validation
Basic Validation
public class CreateUserDto : IValidatable<CreateUserDto> { public string Name { get; set; } public string Email { get; set; } public int Age { get; set; } public List<string> Tags { get; set; } public void Validate(ValidationBuilder<CreateUserDto> builder, ValidationContextKey? context = null) { // String rules builder.For(Name, x => x .NotEmpty() .MinimumLength(2) .MaximumLength(100) .OnlyLetters()); // Email validation builder.For(Email, x => x .NotEmpty() .Email()); // Numeric rules builder.For(Age, x => x .GreaterThan(0) .LessThan(150)); // Collection rules builder.For(Tags, x => x .NotEmpty() .CountBetween(1, 10) .All(tag => !string.IsNullOrWhiteSpace(tag)) .Distinct()); } }
Context-Aware Validation
public void Validate(ValidationBuilder<CreateUserDto> builder, ValidationContextKey? context = null) { // Global rules (always applied) builder.For(Email, x => x.NotEmpty().Email()); builder.For(Age, x => x.GreaterThan(0).LessThan(150)); // Create-specific rules builder.InContext(ValidationContextKey.Create, b => { b.For(Email, x => x .RespectAsync(async (email, ct, sp) => { var userService = sp.GetRequiredService<IUserService>(); return await userService.IsEmailAvailableAsync(email, ct); }) .WithMessage("Email already exists") .WithStatusCode(HttpStatusCode.Conflict)); b.For(Password, x => x .NotEmpty() .MinimumLength(8) .Matches(new Regex(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).*$"))); }); // Update-specific rules builder.InContext(ValidationContextKey.Update, b => { b.For(Age, x => x.GreaterOrEquals(18)); }); }
Async Validation with Services
builder.For(ProductId, x => x .GreaterThan(0) .RespectAsync(async (productId, ct, sp) => { var productService = sp.GetRequiredService<IProductService>(); return await productService.ExistsAsync(productId, ct); }) .WithMessage("Product does not exist") .WithStatusCode(HttpStatusCode.NotFound));
Cross-Property Validation
// Access entire entity in validation builder.For(Amount, x => x .GreaterThan(0) .Respect<OrderDto>((amount, order) => { return order.CustomerType switch { "Premium" => amount <= 50000m, "Gold" => amount <= 25000m, "Silver" => amount <= 10000m, _ => amount <= 5000m }; }) .WithMessage("Amount exceeds limit for customer type")); // Async with entity access builder.For(Email, x => x .RespectAsync<LoginDto>(async (email, login, ct, sp) => { var userService = sp.GetRequiredService<IUserService>(); return await userService.ValidateCredentialsAsync(login.Email, login.Password, ct); }) .WithMessage("Invalid email and password combination"));
Usage in Controllers
[HttpPost] public async Task<IActionResult> Create(CreateUserDto dto) { // Validate and throw on failure await _validator.ValidateAsync(dto, ValidationContextKey.Create); // Or validate without throwing var result = await _validator.ValidateAndReturnAsync(dto, ValidationContextKey.Create); if (!result.IsValid) { return BadRequest(new { errors = result.Errors }); } // Process... return Ok(); }
All Available Rules
String Rules:
,NotEmpty()
,MinimumLength(int)
,MaximumLength(int)LengthBetween(int, int)
,Email()Url()
,OnlyLetters()
,OnlyNumbers()Alphanumeric()
,StartsWith(string)
,EndsWith(string)Contains(string)
,Matches(Regex)BeOneOf(params string[])
,AvailableCharacters(params char[])ForbiddenCharacters(params char[])NoSymbols(char[]?)
Numeric Rules (int, long, decimal, double, float):
,GreaterThan(T)
,GreaterOrEquals(T)
,LessThan(T)LessOrEquals(T)
,Between(T, T)
,Positive()
,Negative()
,Zero()NotZero()
Collection Rules:
,NotEmpty()
,CountBetween(int, int)
,CountGreaterThan(int)CountLessThan(int)
,All(Func<T, bool>)
,Any(Func<T, bool>)None(Func<T, bool>)
,Distinct()DistinctBy<TKey>(Func<T, TKey>)
DateTime/DateOnly Rules:
,Past()
,Future()Today()
,After(DateTime)
,Before(DateTime)Between(DateTime, DateTime)
,AfterOrEquals(DateTime)BeforeOrEquals(DateTime)
Boolean & Enum Rules:
,IsTrue()IsFalse()
,BeInEnum<TEnum>()BeNotInEnum<TEnum>()
Generic Rules (all types):
,NotNull()
,BeNull()
,EqualsTo(T)NotEqualsTo(T)
,BeDefault()NotDefault()
,Respect(Func<T, bool>)RespectAsync(Func<T, CT, SP, Task<bool>>)
,Respect<TEntity>(Func<T, TEntity, bool>)RespectAsync<TEntity>(Func<T, TEntity, CT, SP, Task<bool>>)
Rule Modifiers:
- Custom error message.WithMessage(string)
- Dynamic error message using field value.WithMessage(Func<T, string>)
- Custom HTTP status.WithStatusCode(int | HttpStatusCode)
- Valid options list for error response.WithOptions(IReadOnlyList<string>)
- Auto-generate options from Constant<T,V> types.WithOptions(OptionsType)
- Conditional execution.When(Func<bool>)
- Inverse conditional.Unless(Func<bool>)
- Stop on first failure.SetStopOnFailure(bool)
Error Response Format (RFC 9457 Problem Details)
When using
app.UseGuard() middleware, validation exceptions are automatically formatted following RFC 9457 Problem Details for HTTP APIs:
{ "type": "https://github.com/paulaolileal/myth/blob/main/docs/errors/validation.md", "title": "One or more validation errors occurred", "status": 400, "instance": "/api/users", "traceId": "00-abc123...", "errors": { "email": ["Email already exists"], "password": ["Password must be at least 8 characters"] } }
Key Features:
- Content-Type:
application/problem+json
: HTTP status code (highest from all validation errors)status
: Grouped by field name with all error messageserrors
: Correlation ID for debuggingtraceId- No custom error codes per field (use
andstatus
instead)message
ValidationError Structure:
public sealed class ValidationError { public string Field { get; init; } public string Message { get; init; } public HttpStatusCode StatusCode { get; init; } public IReadOnlyList<string>? Options { get; init; } }
5. Myth.Morph - Object Transformation
IMorphableTo (Source → Destination)
// DTO to Entity transformation public class UserDto : IMorphableTo<User> { public string Name { get; set; } public string Email { get; set; } public DateTime JoinDate { get; set; } public void MorphTo(Schema<User> schema) { // Auto-mapping (same names) // Email is automatically mapped // Custom mapping schema.Bind(u => u.FirstName, () => Name.Split(' ')[0]); schema.Bind(u => u.LastName, () => Name.Split(' ').Last()); schema.Bind(u => u.CreatedAt, () => JoinDate); // With service provider schema.Bind(u => u.Profile, sp => { var service = sp.GetRequiredService<IProfileService>(); return service.GetDefaultProfile(); }); // Async binding schema.BindAsync(u => u.Avatar, async sp => { var service = sp.GetRequiredService<IAvatarService>(); return await service.GetDefaultAvatarAsync(); }); // Ignore properties schema.Ignore(u => u.Id); schema.Ignore(u => u.InternalField); } } // Usage var userDto = new UserDto { Name = "John Doe", Email = "john@example.com" }; var user = userDto.To<User>(serviceProvider);
IMorphableFrom (Destination ← Source)
// Entity to DTO transformation public class OrderDto : IMorphableFrom<Order> { public Guid Id { get; set; } public string Customer { get; set; } public decimal Total { get; set; } public string CreatedDate { get; set; } public int ItemCount { get; set; } public void MorphFrom(Schema<Order> schema) { // Reverse mapping (Order → OrderDto) schema.Bind(() => Id, order => order.Id); schema.Bind(() => Customer, order => order.CustomerName); schema.Bind(() => Total, order => order.TotalAmount); schema.Bind(() => CreatedDate, order => order.CreatedAt.ToString("yyyy-MM-dd")); schema.Bind(() => ItemCount, order => order.Items.Count); // Async reverse mapping schema.BindAsync(() => Customer, async (order, sp) => { var customerService = sp.GetRequiredService<ICustomerService>(); var customer = await customerService.GetByIdAsync(order.CustomerId); return customer?.Name ?? "Unknown"; }); } } // Usage var order = await _repository.GetByIdAsync(orderId); var orderDto = order.To<OrderDto>(serviceProvider);
Collections
// Transform list var users = await _repository.GetAllAsync(); var userDtos = users.To<User, UserDto>(serviceProvider); // Async transformation var productDtos = await products.ToAsync<Product, ProductDto>(serviceProvider);
Check Mapping Availability
if (source.CanBindTo<Destination>(serviceProvider)) { var result = source.To<Destination>(serviceProvider); }
6. Myth.Specification - Query Specifications
Basic Specifications
// Define reusable specifications as extension methods public static class ProductSpecifications { public static ISpec<Product> IsActive(this ISpec<Product> spec) { return spec.And(p => p.IsActive); } public static ISpec<Product> InCategory(this ISpec<Product> spec, string category) { return spec.And(p => p.Category == category); } public static ISpec<Product> PriceRange(this ISpec<Product> spec, decimal min, decimal max) { return spec.And(p => p.Price >= min && p.Price <= max); } public static ISpec<Product> SearchByName(this ISpec<Product> spec, string searchTerm) { return spec.AndIf( !string.IsNullOrEmpty(searchTerm), p => p.Name.Contains(searchTerm)); } public static ISpec<Product> OrderByPrice(this ISpec<Product> spec, bool descending = false) { return descending ? spec.OrderDescending(p => p.Price) : spec.Order(p => p.Price); } }
Usage in Repository
public async Task<List<Product>> SearchProducts(ProductSearchDto search) { var spec = SpecBuilder<Product>.Create() .IsActive() .InCategory(search.Category) .PriceRange(search.MinPrice, search.MaxPrice) .SearchByName(search.SearchTerm) .OrderByPrice(search.SortDescending) .Skip((search.Page - 1) * search.PageSize) .Take(search.PageSize); return await _context.Products .Specify(spec) .ToListAsync(); }
Logical Composition
var spec = SpecBuilder<Order>.Create() .And(o => o.CustomerId == customerId) .Or(o => o.Status == OrderStatus.Pending) .AndIf(fromDate.HasValue, o => o.CreatedAt >= fromDate.Value) .OrIf(toDate.HasValue, o => o.CreatedAt <= toDate.Value) .Not(); // Negate entire spec
Pagination & Ordering
var spec = SpecBuilder<Product>.Create() .IsActive() .Order(p => p.Name) // Ascending .OrderDescending(p => p.Price) // Descending .Skip(20) // Offset .Take(10) // Limit .WithPagination(new Pagination { PageNumber = 2, PageSize = 10 });
Advanced Features
// Distinct var spec = SpecBuilder<Product>.Create() .DistinctBy(p => p.Brand); // Get single item var product = spec.SatisfyingItemFrom(query); // Get all matching items var products = spec.SatisfyingItemsFrom(query); // In-memory validation if (spec.IsSatisfiedBy(entity)) { // Entity matches specification } // Prepare full query (filter + sort + post-process) var preparedQuery = spec.Prepare(query); // Apply only filters var filteredQuery = spec.Filtered(query); // Apply only sorting var sortedQuery = spec.Sorted(query);
7. Myth.Repository - Data Access
Basic Repository
// Interface public interface IProductRepository : IReadWriteRepositoryAsync<Product> { } // Implementation public class ProductRepository : ReadWriteRepositoryAsync<Product>, IProductRepository { public ProductRepository(AppDbContext context) : base(context) { } } // Registration services.AddScoped<IProductRepository, ProductRepository>(); // Usage public class ProductService { private readonly IProductRepository _repository; public async Task<Product> CreateAsync(CreateProductDto dto) { var product = new Product { Name = dto.Name, Price = dto.Price }; await _repository.AddAsync(product); return product; } public async Task<IEnumerable<Product>> GetAllAsync() { return await _repository.GetAllAsync(); } }
Repository with Specifications
public async Task<IPaginated<Order>> SearchOrdersAsync( OrderSearchDto search, CancellationToken ct = default) { var spec = SpecBuilder<Order>.Create() .And(o => o.CustomerId == search.CustomerId) .And(o => search.Statuses.Contains(o.Status)) .And(o => o.CreatedAt >= search.FromDate) .OrderDescending(o => o.CreatedAt) .WithPagination(new Pagination { PageNumber = search.Page, PageSize = search.PageSize }); return await _repository.GetPaginatedAsync(spec, ct); }
Unit of Work
public class OrderService { private readonly IOrderRepository _orderRepository; private readonly IOrderItemRepository _itemRepository; private readonly IUnitOfWorkRepository _unitOfWork; public async Task<Order> CreateOrderAsync(CreateOrderDto dto) { try { await _unitOfWork.BeginTransactionAsync(); var order = new Order { CustomerId = dto.CustomerId, CreatedAt = DateTime.UtcNow }; await _orderRepository.AddAsync(order); foreach (var itemDto in dto.Items) { var item = new OrderItem { OrderId = order.Id, ProductId = itemDto.ProductId, Quantity = itemDto.Quantity }; await _itemRepository.AddAsync(item); } await _unitOfWork.SaveChangesAsync(); await _unitOfWork.CommitTransactionAsync(); return order; } catch { await _unitOfWork.RollbackTransactionAsync(); throw; } } }
Read/Write Separation (CQRS)
// Read repository for queries public interface IProductReadRepository : IReadRepositoryAsync<Product> { } public class ProductReadRepository : ReadRepositoryAsync<Product>, IProductReadRepository { public ProductReadRepository(AppDbContext context) : base(context) { } } // Write repository for commands public interface IProductWriteRepository : IWriteRepositoryAsync<Product> { } public class ProductWriteRepository : WriteRepositoryAsync<Product>, IProductWriteRepository { public ProductWriteRepository(AppDbContext context) : base(context) { } }
8. Myth.Repository.EntityFramework - EF Core Implementation
Provides Entity Framework Core implementation of repository interfaces with Unit of Work pattern.
Basic Setup
using Myth.Contexts; using Myth.Repository.EntityFramework.Repositories; // Define DbContext public class AppDbContext : BaseContext { public DbSet<Product> Products { get; set; } public DbSet<Order> Orders { get; set; } public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // Configure entities } } // Implement Repository public class ProductRepository : ReadWriteRepositoryAsync<Product>, IProductRepository { public ProductRepository(AppDbContext context) : base(context) { } } // Register services.AddDbContext<AppDbContext>(options => options.UseSqlServer(connectionString)); services.AddScoped<IProductRepository, ProductRepository>(); services.AddScoped<IUnitOfWorkRepository, UnitOfWorkRepository<AppDbContext>>();
Unit of Work Pattern
public class OrderService { private readonly IUnitOfWorkRepository _unitOfWork; public async Task ProcessOrderAsync(Order order, CancellationToken ct) { await _unitOfWork.BeginTransactionAsync(ct); try { await _unitOfWork.AddAsync(order, ct); await _unitOfWork.SaveChangesAsync(ct); await _unitOfWork.CommitTransactionAsync(ct); } catch { await _unitOfWork.RollbackTransactionAsync(ct); throw; } } }
9. Myth.Rest - HTTP Client
Fluent REST client with retry policies, circuit breaker, and certificate support.
Basic Usage
using Myth.Rest; // Register services.AddRest(); // Inject factory public class ExternalApiService { private readonly IRestFactory _restFactory; public async Task<UserDto> GetUserAsync(int id) { var result = await _restFactory .Create("https://api.example.com") .Get($"/users/{id}") .WithRetry(maxAttempts: 3, backoffMs: 1000) .WithCircuitBreaker(failureThreshold: 5, breakDurationMs: 30000) .WithTimeout(TimeSpan.FromSeconds(10)) .ExecuteAsync<UserDto>(); return result.Data; } }
POST Request with JSON
var createUserDto = new CreateUserDto { Name = "John", Email = "john@example.com" }; var result = await _restFactory .Create("https://api.example.com") .Post("/users") .WithJsonBody(createUserDto) .WithHeader("Authorization", $"Bearer {token}") .WithRetry(3, 500) .ExecuteAsync<UserDto>();
10. Myth.Testing - Testing Utilities
Provides base classes, mocks, and utilities for comprehensive testing.
Base Test Class
using Myth.Testing.Repositories; using Myth.Testing.Extensions; public class ProductServiceTests : BaseDatabaseTests<AppDbContext> { private readonly ProductService _service; public ProductServiceTests() : base() { AddServices(services => { services.AddScoped<IProductRepository, ProductRepository>(); services.AddScoped<ProductService>(); services.AddGuard(); services.AddMorph(); }); _service = CreateInstance<ProductService>(); } [Fact] public async Task CreateProduct_ShouldSucceed_WhenValid() { // Arrange var dto = new CreateProductDto { Name = "Test", Price = 10.00m }; // Act var result = await _service.CreateProductAsync(dto); // Assert result.Should().NotBeNull(); result.Name.Should().Be("Test"); } }
HTTP Client Mock
using Myth.Testing.Mocks; var httpClientMock = new HttpClientMock() .SetupGet("/users/1") .ReturnsJson(new UserDto { Id = 1, Name = "John" }) .WithStatusCode(HttpStatusCode.OK); var httpClient = httpClientMock.CreateClient();
11. Myth.Tool - Code Generation CLI
CLI tool for generating Myth architecture code with CQRS, DDD, and Clean Architecture patterns.
Installation
dotnet tool install -g Myth.Tool
Project Setup
# Setup new project with clean structure myth setup MyProject --clean # Setup project keeping examples myth setup MyProject
Generate Domain Model
myth create model User \ -p Id:Guid \ -p Name:string:required \ -p Email:string:required \ --validate
Generate CQRS Command
myth create command User CreateUser \ -p Name:string:required \ -p Email:string:required \ --return Guid \ --validate \ --events UserCreated
Generate Query
myth create query User GetUser \ -p Id:Guid:required \ --return GetUserResponse
Generate Repository
myth create repository User --type readwrite
Generate Controller
myth create controller User
Generate Tests
myth create test UserController
Common Workflows
Workflow 1: Complete CQRS with Validation, Pipeline, and Repository
// 1. Define DTOs with validation public class CreateOrderDto : IValidatable<CreateOrderDto> { public Guid CustomerId { get; set; } public List<CreateOrderItemDto> Items { get; set; } public void Validate(ValidationBuilder<CreateOrderDto> builder, ValidationContextKey? context = null) { builder.For(CustomerId, x => x.NotDefault()); builder.For(Items, x => x .NotEmpty() .CountBetween(1, 100) .All(item => item.Quantity > 0)); builder.InContext(ValidationContextKey.Create, b => { b.For(CustomerId, x => x .RespectAsync(async (id, ct, sp) => { var customerService = sp.GetRequiredService<ICustomerService>(); return await customerService.ExistsAsync(id, ct); }) .WithMessage("Customer not found") .WithStatusCode(HttpStatusCode.NotFound)); }); } } public class OrderDto : IMorphableFrom<Order> { public Guid Id { get; set; } public string Customer { get; set; } public decimal Total { get; set; } public int ItemCount { get; set; } public void MorphFrom(Schema<Order> schema) { schema.Bind(() => Id, o => o.Id); schema.Bind(() => Customer, o => o.CustomerName); schema.Bind(() => Total, o => o.TotalAmount); schema.Bind(() => ItemCount, o => o.Items.Count); } } // 2. Define Command public record CreateOrderCommand(CreateOrderDto Data) : ICommand<Guid>; // 3. Define Event public record OrderCreatedEvent(Guid OrderId, decimal Total) : IEvent; // 4. Command Handler with full pipeline public class CreateOrderHandler : ICommandHandler<CreateOrderCommand, Guid> { private readonly IScopedService<IOrderRepository> _repository; private readonly IScopedService<IUnitOfWorkRepository> _unitOfWork; private readonly IValidator _validator; private readonly IDispatcher _dispatcher; public async Task<CommandResult<Guid>> HandleAsync( CreateOrderCommand command, CancellationToken ct) { var result = await Pipeline.Start(command.Data) .WithTelemetry("CreateOrder") .WithRetry(maxAttempts: 3, backoffMs: 100) // Validate .StepResultAsync(async dto => { var validationResult = await _validator.ValidateAndReturnAsync( dto, ValidationContextKey.Create, ct); if (!validationResult.IsValid) { return Result<CreateOrderDto>.Failure( validationResult.Errors.First().Message); } return Result<CreateOrderDto>.Success(dto); }) // Create order .StepResultAsync(async dto => { var order = new Order { CustomerId = dto.CustomerId, Items = dto.Items.Select(i => new OrderItem { ProductId = i.ProductId, Quantity = i.Quantity, Price = i.Price }).ToList(), CreatedAt = DateTime.UtcNow }; order.TotalAmount = order.Items.Sum(i => i.Price * i.Quantity); await _repository.ExecuteAsync(repo => repo.AddAsync(order, ct)); await _unitOfWork.ExecuteAsync(uow => uow.SaveChangesAsync(ct)); return Result<Order>.Success(order); }) // Publish event .TapAsync(async order => { await _dispatcher.PublishEventAsync( new OrderCreatedEvent(order.Id, order.TotalAmount), ct); }) .ExecuteAsync(ct); if (result.IsFailure) { return CommandResult<Guid>.Failure(result.ErrorMessage); } return CommandResult<Guid>.Success(result.Value.Id); } } // 5. Query Handler with Specification public record GetOrderQuery(Guid OrderId) : IQuery<OrderDto>; public class GetOrderHandler : IQueryHandler<GetOrderQuery, OrderDto> { private readonly IScopedService<IOrderRepository> _repository; public async Task<QueryResult<OrderDto>> HandleAsync( GetOrderQuery query, CancellationToken ct) { var spec = SpecBuilder<Order>.Create() .And(o => o.Id == query.OrderId); var order = await _repository.ExecuteAsync(repo => repo.FirstOrDefaultAsync(spec, ct)); if (order == null) { return QueryResult<OrderDto>.Failure("Order not found"); } var dto = order.To<OrderDto>(); return QueryResult<OrderDto>.Success(dto); } } // 6. Event Handler public class OrderCreatedHandler : IEventHandler<OrderCreatedEvent> { private readonly IScopedService<IEmailService> _emailService; public async Task HandleAsync(OrderCreatedEvent @event, CancellationToken ct) { await _emailService.ExecuteAsync(svc => svc.SendOrderConfirmationAsync(@event.OrderId, ct)); } } // 7. Controller [ApiController] [Route("api/[controller]")] public class OrdersController : ControllerBase { private readonly IDispatcher _dispatcher; [HttpPost] public async Task<IActionResult> Create(CreateOrderDto dto) { var command = new CreateOrderCommand(dto); var result = await _dispatcher.DispatchCommandAsync<CreateOrderCommand, Guid>(command); return result.IsSuccess ? CreatedAtAction(nameof(Get), new { id = result.Data }, result.Data) : BadRequest(result.ErrorMessage); } [HttpGet("{id}")] public async Task<IActionResult> Get(Guid id) { var query = new GetOrderQuery(id); var cacheOptions = new CacheOptions { Enabled = true, Ttl = TimeSpan.FromMinutes(5) }; var result = await _dispatcher.DispatchQueryAsync<GetOrderQuery, OrderDto>( query, cacheOptions); return result.IsSuccess ? Ok(result.Data) : NotFound(); } }
Best Practices
1. Always Use BuildApp() or BuildWithGlobalProvider()
// ✅ CORRECT - ASP.NET Core var app = builder.BuildApp(); // ❌ INCORRECT var app = builder.Build(); // ✅ CORRECT - Console App var serviceProvider = services.BuildWithGlobalProvider(); // ❌ INCORRECT var serviceProvider = services.BuildServiceProvider();
2. Use IScopedService<T> for Transient Handlers
// ✅ CORRECT - Handlers are transient but need scoped repos public class CreateOrderHandler : ICommandHandler<CreateOrderCommand, Guid> { private readonly IScopedService<IOrderRepository> _repository; public async Task<CommandResult<Guid>> HandleAsync(...) { return await _repository.ExecuteAsync(async repo => { var order = await repo.CreateAsync(...); return CommandResult<Guid>.Success(order.Id); }); } } // ❌ INCORRECT - Injecting scoped service directly into transient handler public class CreateOrderHandler : ICommandHandler<CreateOrderCommand, Guid> { private readonly IOrderRepository _repository; // ❌ Will cause issues! }
3. Always Validate Before Processing
// ✅ CORRECT public async Task<CommandResult> HandleAsync(CreateOrderCommand command, CancellationToken ct) { await _validator.ValidateAsync(command.Data, ValidationContextKey.Create, ct); // Process... } // ❌ INCORRECT - No validation public async Task<CommandResult> HandleAsync(CreateOrderCommand command, CancellationToken ct) { // Directly process without validation }
4. Use Telemetry and Retry for Production
// ✅ CORRECT return await Pipeline.Start(context) .WithTelemetry("OperationName") .WithRetry(maxAttempts: 3, backoffMs: 100) .StepAsync(...) .ExecuteAsync(); // ❌ INCORRECT - No observability or resilience return await Pipeline.Start(context) .StepAsync(...) .ExecuteAsync();
5. Use Specifications for Queries
// ✅ CORRECT - Reusable, testable, encapsulated var spec = SpecBuilder<Order>.Create() .ForCustomer(customerId) .WithStatus(OrderStatus.Pending) .Recent(TimeSpan.FromDays(30)) .OrderDescending(o => o.CreatedAt); var orders = await _repository.FindAsync(spec); // ❌ INCORRECT - Query logic in repository var orders = await _context.Orders .Where(o => o.CustomerId == customerId) .Where(o => o.Status == OrderStatus.Pending) .Where(o => o.CreatedAt >= DateTime.UtcNow.AddDays(-30)) .OrderByDescending(o => o.CreatedAt) .ToListAsync();
6. Use Context-Aware Validation
// ✅ CORRECT - Different rules per operation public void Validate(ValidationBuilder<UserDto> builder, ValidationContextKey? context = null) { // Global rules builder.For(Email, x => x.NotEmpty().Email()); // Create-specific builder.InContext(ValidationContextKey.Create, b => { b.For(Password, x => x.NotEmpty().MinimumLength(8)); }); // Update-specific builder.InContext(ValidationContextKey.Update, b => { b.For(Age, x => x.GreaterOrEquals(18)); }); } // ❌ INCORRECT - Same rules for all operations public void Validate(ValidationBuilder<UserDto> builder, ValidationContextKey? context = null) { builder.For(Email, x => x.NotEmpty().Email()); builder.For(Password, x => x.NotEmpty().MinimumLength(8)); // Always required! }
7. Use Result Pattern Consistently
// ✅ CORRECT .StepResultAsync(async ctx => { if (ctx.IsValid) { return Result<Context>.Success(ctx); } return Result<Context>.Failure("Invalid context"); }) // ❌ INCORRECT - Throwing exceptions for flow control .StepAsync(async ctx => { if (!ctx.IsValid) { throw new InvalidOperationException("Invalid context"); } return ctx; })
Examples by Scenario
E-Commerce Order Processing
// Complete e-commerce order flow public class OrderProcessingWorkflow { public async Task<Result<OrderConfirmationDto>> ProcessOrderAsync( CreateOrderRequest request) { return await Pipeline.Start(request) .WithTelemetry("ProcessOrder") .WithRetry(maxAttempts: 3, backoffMs: 100) // 1. Validate order .StepResultAsync<IValidator>((validator, req) => validator.ValidateAndReturnAsync(req, ValidationContextKey.Create)) // 2. Check inventory .StepResultAsync<InventoryService>((svc, req) => svc.CheckAvailabilityAsync(req.Items)) // 3. Calculate pricing .StepResultAsync<PricingService>((svc, req) => svc.CalculateTotalAsync(req.Items, req.CouponCode)) // 4. Process payment .StepResultAsync<PaymentService>((svc, ctx) => svc.ProcessPaymentAsync(ctx.Total, ctx.PaymentMethod)) // 5. Reserve inventory .StepResultAsync<InventoryService>((svc, ctx) => svc.ReserveItemsAsync(ctx.Items)) // 6. Create order .StepResultAsync<OrderService>((svc, ctx) => svc.CreateOrderAsync(ctx)) // 7. Publish events .TapAsync<IDispatcher>((dispatcher, ctx) => dispatcher.PublishEventAsync(new OrderCreatedEvent(ctx.OrderId))) // 8. Send notifications .TapAsync<EmailService>((svc, ctx) => svc.SendOrderConfirmationAsync(ctx.CustomerEmail, ctx.OrderId)) .TapAsync<SmsService>((svc, ctx) => svc.SendOrderSmsAsync(ctx.CustomerPhone, ctx.OrderId)) // 9. Transform to response .Transform<OrderConfirmationDto>(ctx => new OrderConfirmationDto { OrderId = ctx.OrderId, Total = ctx.Total, EstimatedDelivery = DateTime.UtcNow.AddDays(5) }) .ExecuteAsync(); } }
User Registration with Email Verification
// Complete user registration workflow public class UserRegistrationWorkflow { public async Task<Result<UserDto>> RegisterUserAsync( RegisterUserRequest request) { return await Pipeline.Start(request) .WithTelemetry("RegisterUser") // 1. Validate .StepResultAsync<IValidator>((validator, req) => validator.ValidateAndReturnAsync(req, ValidationContextKey.Create)) // 2. Hash password .StepAsync<PasswordHasher>((hasher, req) => { req.PasswordHash = hasher.Hash(req.Password); return Task.FromResult(req); }) // 3. Create user .StepResultAsync<UserService>((svc, req) => svc.CreateUserAsync(req)) // 4. Generate verification token .StepAsync<TokenService>((svc, ctx) => { ctx.VerificationToken = svc.GenerateEmailVerificationToken(ctx.UserId); return Task.FromResult(ctx); }) // 5. Send verification email .TapAsync<EmailService>((svc, ctx) => svc.SendVerificationEmailAsync(ctx.Email, ctx.VerificationToken)) // 6. Publish event .TapAsync<IDispatcher>((dispatcher, ctx) => dispatcher.PublishEventAsync(new UserRegisteredEvent(ctx.UserId))) // 7. Transform to DTO .Transform<UserDto>(ctx => ctx.User.To<UserDto>()) .ExecuteAsync(); } }
Complex Search with Filters, Sorting, and Pagination
public class ProductSearchService { private readonly IProductRepository _repository; public async Task<IPaginated<ProductDto>> SearchAsync(ProductSearchRequest request) { // Build specification var spec = SpecBuilder<Product>.Create(); // Apply filters conditionally if (!string.IsNullOrEmpty(request.Category)) { spec = spec.InCategory(request.Category); } if (request.MinPrice.HasValue || request.MaxPrice.HasValue) { spec = spec.PriceRange( request.MinPrice ?? 0, request.MaxPrice ?? decimal.MaxValue); } if (!string.IsNullOrEmpty(request.SearchTerm)) { spec = spec.SearchByName(request.SearchTerm); } if (request.InStock) { spec = spec.And(p => p.StockQuantity > 0); } if (request.Tags?.Any() == true) { spec = spec.And(p => p.Tags.Any(t => request.Tags.Contains(t))); } // Apply sorting spec = request.SortBy switch { "price_asc" => spec.Order(p => p.Price), "price_desc" => spec.OrderDescending(p => p.Price), "name" => spec.Order(p => p.Name), "newest" => spec.OrderDescending(p => p.CreatedAt), _ => spec.Order(p => p.Relevance) }; // Apply pagination spec = spec.WithPagination(new Pagination { PageNumber = request.Page, PageSize = request.PageSize }); // Execute query var paginatedProducts = await _repository.GetPaginatedAsync(spec); // Transform to DTOs var productDtos = paginatedProducts.Items.To<Product, ProductDto>(); return new PaginatedResult<ProductDto> { Items = productDtos, CurrentPage = paginatedProducts.CurrentPage, TotalPages = paginatedProducts.TotalPages, PageSize = paginatedProducts.PageSize, TotalCount = paginatedProducts.TotalCount }; } }
How to Use This Skill
Prompting Examples for AI Assistants
Setup a new project:
"Use the myth skill to setup a new ASP.NET Core application with CQRS, validation, and pipelines"
Add validation to DTO:
"Use the myth skill with library=Guard to add validation rules for a CreateUserDto with email, age, and password fields"
Create a command handler:
"Use the myth skill with library=Flow.Actions and operation=command to create a CreateOrderCommand handler with validation, repository, and event publishing"
Setup specifications:
"Use the myth skill with library=Specification to create reusable query specifications for Product entity with filters for category, price range, and active status"
Add object transformation:
"Use the myth skill with library=Morph to create bidirectional mapping between Order entity and OrderDto"
Configure complete CQRS:
"Use the myth skill to configure a complete CQRS setup with Flow.Actions, Guard validation, Repository pattern, and Specifications for an e-commerce order system"
Response Template
When asked to use the Myth skill, an AI assistant should:
- Identify the specific library/libraries needed from the request
- Determine the operation type (setup, configure, create handler, add validation, etc.)
- Provide complete, working code following Myth conventions:
- Use
for ASP.NET CoreBuildApp() - Use
for handlersIScopedService<T> - Include validation, telemetry, and retry where appropriate
- Follow Result pattern
- Use specifications for queries
- Apply SOLID principles
- Use
- Explain the code with comments and context
- Suggest related patterns or next steps
Additional Resources
- SOLID Principles: All code should follow Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion
- Clean Code: Use meaningful names, small functions, clear abstractions
- DDD: Use ValueObjects, Entities, Aggregates, and Specifications
- CQRS: Separate read and write operations for scalability
- Event-Driven: Use events for cross-cutting concerns and integration
End of Myth Skill Documentation