Myth.Morph 4.2.1-preview.1

This is a prerelease version of Myth.Morph.
dotnet add package Myth.Morph --version 4.2.1-preview.1
                    
NuGet\Install-Package Myth.Morph -Version 4.2.1-preview.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Myth.Morph" Version="4.2.1-preview.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Myth.Morph" Version="4.2.1-preview.1" />
                    
Directory.Packages.props
<PackageReference Include="Myth.Morph" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Myth.Morph --version 4.2.1-preview.1
                    
#r "nuget: Myth.Morph, 4.2.1-preview.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Myth.Morph@4.2.1-preview.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Myth.Morph&version=4.2.1-preview.1&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Myth.Morph&version=4.2.1-preview.1&prerelease
                    
Install as a Cake Tool

<img style="float: right;" src="myth-morph-logo.png" alt="drawing" width="250"/>

Myth.Morph

NuGet Version NuGet Version

License

pt-br en

A lightweight .NET object transformation library designed for clean architecture and Domain-Driven Design. Myth.Morph provides a declarative, schema-based approach to object mapping with zero reflection overhead during transformation and full dependency injection integration.

Why Myth.Morph?

Unlike heavy mapping libraries that rely on runtime reflection and conventions, Myth.Morph gives you explicit control over transformations while keeping your code clean and maintainable:

  • Self-documenting mappings: Transformations are defined where they belong - in the source type
  • Type-safe: Compile-time checking for property bindings
  • DI-aware: Service provider access for complex transformations and async operations
  • Performance-focused: Schema compilation at startup, zero reflection during mapping
  • Clean separation: Keep DTOs, entities, and view models cleanly separated with explicit transformation rules

Perfect for CQRS, Clean Architecture, and DDD applications where explicit transformations matter.

Features

  • Bidirectional Mapping Patterns: Support for both IMorphableTo<T> and IMorphableFrom<T> patterns
  • Entity Framework Proxy Support: Automatic detection and handling of EF Core lazy-loading proxies
  • Inheritance Hierarchy Resolution: Configurable depth traversal for complex type hierarchies
  • Declarative Schema Configuration: Define transformations using fluent API with compile-time safety
  • Automatic Property Mapping: Convention-based mapping for matching property names
  • Manual Binding: Four binding strategies for maximum flexibility
  • Async Support: First-class async/await support for I/O-bound transformations
  • Dependency Injection: Full service provider access in transformation logic
  • Generic Collections: Automatic mapping of collections with element transformation
  • Nested Objects: Recursive transformation support for complex object graphs
  • Ignore Properties: Explicit exclusion of properties from mapping
  • Comprehensive Logging: Detailed trace logging for debugging transformations

Installation

dotnet add package Myth.Morph

Quick Start

1. Register Services

// In Program.cs or Startup.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddMorph();

var app = builder.BuildApp(); // Use BuildApp() instead of Build()

For console applications:

var services = new ServiceCollection();
services.AddMorph();

var provider = services.BuildWithGlobalProvider();

2. Define Transformations

Myth.Morph supports two mapping patterns:

IMorphableTo Pattern (Source → Destination)

The source type defines how to transform to the destination:

public class CreateUserDto : IMorphableTo<User> {
    public string Name { get; set; }
    public string Email { get; set; }
    public DateTime BirthDate { get; set; }

    public void MorphTo( Schema<User> schema ) {
        schema
            .Bind(u => u.FullName, () => Name)
            .Bind(u => u.EmailAddress, () => Email)
            .Bind(u => u.Age, () => DateTime.Today.Year - BirthDate.Year);
    }
}
IMorphableFrom Pattern (Destination ← Source)

The destination type defines how to be created from the source:

public class UserDto : IMorphableFrom<User> {
    public string DisplayName { get; set; }
    public string Email { get; set; }
    public int Age { get; set; }

    public void MorphFrom( Schema<User> schema ) {
        schema
            .Bind(() => DisplayName, u => $"{u.FirstName} {u.LastName}")
            .Bind(() => Email, u => u.EmailAddress)
            .Bind(() => Age, u => DateTime.Today.Year - u.BirthDate.Year);
    }
}

Note: Use IMorphableTo when the source knows about the destination (DTOs → Entities). Use IMorphableFrom when the destination knows about the source (Entities → DTOs).

3. Transform Objects

var dto = new CreateUserDto {
    Name = "John Doe",
    Email = "john@example.com",
    BirthDate = new DateTime(1990, 1, 1)
};

var user = dto.To<User>();

Binding Strategies

Myth.Morph provides four binding strategies to handle different transformation scenarios.

1. Direct Value Binding

Map a property to a computed value:

public void MorphTo( Schema<User> schema ) {
    schema.Bind(u => u.FullName, () => $"{FirstName} {LastName}");
}

2. Service Provider Binding

Access services from DI container for complex transformations:

public void MorphTo( Schema<Order> schema ) {
    schema.Bind(o => o.Customer, sp => {
        var customerService = sp.GetRequiredService<ICustomerService>();
        return customerService.GetCustomerById(CustomerId);
    });
}

3. Async Direct Binding

For async operations without service provider:

public void MorphTo( Schema<User> schema ) {
    schema.BindAsync(u => u.Avatar, async () => {
        await Task.Delay(100); // Simulate async work
        return "default-avatar.png";
    });
}

4. Async Service Provider Binding

Combine async operations with DI:

public void MorphTo( Schema<Product> schema ) {
    schema.BindAsync(p => p.Reviews, async sp => {
        var reviewService = sp.GetRequiredService<IReviewService>();
        return await reviewService.GetReviewsAsync(ProductId);
    });
}

Automatic Property Mapping

Properties with matching names and compatible types are automatically mapped:

public class UserDto : IMorphable<User> {
    public string Name { get; set; }      // Auto-maps to User.Name
    public string Email { get; set; }     // Auto-maps to User.Email
    public int Age { get; set; }          // Auto-maps to User.Age

    public void MorphTo( Schema<User> schema ) {
        // Only define custom mappings - automatic mapping handles the rest
        schema.Ignore(u => u.InternalId);
    }
}

Automatic Mapping Features

  • Name matching: Properties with identical names are mapped automatically
  • Type conversion: Handles primitive type conversions (int to long, etc.)
  • Nested objects: Recursively transforms nested objects using registered mappings
  • Null handling: Safely handles null values and nullable types
  • Collection mapping: Automatically maps compatible collection types

Ignoring Properties

Exclude properties from both manual and automatic mapping:

public void MorphTo( Schema<User> schema ) {
    schema
        .Ignore(u => u.InternalId)
        .Ignore(u => u.CreatedBy)
        .Ignore(u => u.ModifiedBy);
}

Collection Transformations

Transform collections with type-safe extension methods:

// Transform enumerable
IEnumerable<UserDto> dtos = GetUserDtos();
IEnumerable<User> users = dtos.To<User>();

// Transform with service provider
IEnumerable<User> users = dtos.To<User>(serviceProvider);

// Async collection transformation
IEnumerable<User> users = await dtos.ToAsync<User>();

// Type-safe collection transformation
List<UserDto> dtoList = GetUserDtos();
IEnumerable<User> users = dtoList.To<UserDto, User>();

Nested Object Mapping

Myth.Morph automatically handles nested transformations:

public class OrderDto : IMorphable<Order> {
    public int OrderId { get; set; }
    public List<OrderItemDto> Items { get; set; }

    public void MorphTo( Schema<Order> schema ) {
        schema
            .Bind(o => o.Id, () => OrderId)
            .BindAsync(o => o.Items, async sp =>
                // Nested collection transformation
                await Items.ToAsync<OrderItem>(sp)
            );
    }
}

// OrderItemDto also implements IMorphable<OrderItem>
public class OrderItemDto : IMorphable<OrderItem> {
    public string ProductName { get; set; }
    public decimal Price { get; set; }

    public void MorphTo( Schema<OrderItem> schema ) {
        // Automatic mapping handles properties
    }
}

Entity Framework Proxy Support

Myth.Morph automatically detects and handles Entity Framework Core lazy-loading proxies (Castle.Proxies), ensuring seamless transformations even when working with proxied entities.

How It Works

When EF Core uses lazy-loading proxies, it creates dynamic proxy types that inherit from your entity:

// Your entity
public class User {
    public int Id { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Order> Orders { get; set; } // virtual for lazy loading
}

// EF Core creates: Castle.Proxies.UserProxy : User

Myth.Morph automatically resolves the proxy to the base entity type, ensuring your mappings work correctly:

// This works even if user is a proxy
var userDto = user.To<UserDto>();

// Collections with proxies also work
var userDtos = users.To<UserDto>(); // users may contain proxy instances

Configuring Inheritance Fallback

Enable inheritance fallback for advanced proxy and inheritance scenarios:

services.AddMorph(settings => {
    settings.EnableInheritanceFallback = true;
    settings.MaxInheritanceDepth = 5; // How deep to traverse the hierarchy
    settings.IncludeInterfacesInFallback = false; // Whether to check interfaces
});

Example with EF Core

public class UserRepository {
    private readonly DbContext _context;

    public async Task<UserDto> GetUserWithOrdersAsync(int userId) {
        // EF Core may return a proxy with lazy-loaded Orders
        var user = await _context.Users
            .Include(u => u.Orders)
            .FirstAsync(u => u.Id == userId);

        // Myth.Morph automatically handles the proxy
        return user.To<UserDto>();
    }
}

public class UserDto : IMorphableFrom<User> {
    public int Id { get; set; }
    public string Name { get; set; }
    public List<OrderDto> Orders { get; set; }

    public void MorphFrom(Schema<User> schema) {
        // Automatic mapping works even with proxies
        schema.BindAsync(() => Orders, async (u, sp) =>
            await u.Orders.ToAsync<OrderDto>(sp)
        );
    }
}

Proxy Detection

Myth.Morph detects proxies by:

  • Checking for Castle.Proxies namespace
  • Identifying dynamic assemblies
  • Analyzing type hierarchy patterns

No configuration needed - it just works! 🎯

Advanced Configuration

Custom Assembly Scanning

Limit assemblies scanned for IMorphable implementations:

services.AddMorph(settings => {
    settings.AddAssembly(Assembly.GetExecutingAssembly());
    settings.AddAssemblies(typeof(UserDto).Assembly, typeof(OrderDto).Assembly);
});

Generic Type Mappings

Register mappings between generic interfaces and concrete types:

services.AddMorph(settings => {
    settings.AddGenericMorph(typeof(IList<>), typeof(List<>));
    settings.AddGenericMorph(typeof(ICustomCollection<>), typeof(CustomCollection<>));

    // Type-safe generic mapping
    settings.AddGenericMapping<IMyInterface<>, MyImplementation<>>();
});

Default Generic Mappings

Myth.Morph includes these default mappings:

  • IList<>List<>
  • ICollection<>List<>
  • IDictionary<,>Dictionary<,>
  • ISet<>HashSet<>
  • IReadOnlyCollection<>ReadOnlyCollection<>
  • IReadOnlyList<>List<>
  • IReadOnlySet<>HashSet<>

Clear Default Mappings

services.AddMorph(settings => {
    settings.ClearGenericMappings()
            .AddGenericMapping<IList<>, ArrayList>(); // Use custom mapping
});

Real-World Examples

CQRS Command to Entity

Use IMorphableTo when commands/DTOs transform into entities:

public class CreateProductCommand : IMorphableTo<Product> {
    public string Name { get; set; }
    public decimal Price { get; set; }
    public string CategoryId { get; set; }

    public void MorphTo( Schema<Product> schema ) {
        schema
            .Bind(p => p.Name, () => Name)
            .Bind(p => p.Price, () => Price)
            .Bind(p => p.Category, sp => {
                var categoryRepo = sp.GetRequiredService<ICategoryRepository>();
                return categoryRepo.GetById(CategoryId);
            })
            .Bind(p => p.CreatedAt, () => DateTime.UtcNow)
            .Bind(p => p.IsActive, () => true);
    }
}

API Response to Domain Model

public class UserApiResponse : IMorphable<User> {
    public string Id { get; set; }
    public string FullName { get; set; }
    public string EmailAddress { get; set; }

    public void MorphTo( Schema<User> schema ) {
        schema
            .Bind(u => u.UserId, () => Guid.Parse(Id))
            .Bind(u => u.Name, () => FullName)
            .Bind(u => u.Email, () => EmailAddress)
            .BindAsync(u => u.Preferences, async sp => {
                var prefService = sp.GetRequiredService<IPreferenceService>();
                return await prefService.GetUserPreferencesAsync(Id);
            });
    }
}

Entity to DTO with Computed Properties

Use IMorphableFrom when entities transform into DTOs for read operations:

public class Product {
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public DateTime CreatedAt { get; set; }
    public bool IsActive { get; set; }
}

public class ProductDto : IMorphableFrom<Product> {
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public string DisplayName { get; set; }
    public string Status { get; set; }

    public void MorphFrom( Schema<Product> schema ) {
        // Id, Name, Price auto-mapped from matching properties
        schema
            .Bind(() => DisplayName, p => $"{p.Name} (#{p.Id})")
            .Bind(() => Status, p => p.IsActive ? "Active" : "Inactive")
            .Ignore(p => p.CreatedAt); // Don't map this property
    }
}

// Transform entity to DTO (works with EF proxies too!)
var product = await _context.Products.FindAsync(1);
var dto = product.To<ProductDto>();

Event Sourcing Integration

public class UserRegisteredEvent : IMorphable<User> {
    public Guid UserId { get; set; }
    public string Email { get; set; }
    public string Name { get; set; }
    public DateTime RegisteredAt { get; set; }

    public void MorphTo( Schema<User> schema ) {
        schema
            .Bind(u => u.Id, () => UserId)
            .Bind(u => u.Email, () => Email)
            .Bind(u => u.FullName, () => Name)
            .Bind(u => u.CreatedDate, () => RegisteredAt)
            .Bind(u => u.IsActive, () => true)
            .Bind(u => u.EmailVerified, () => false);
    }
}

Repository Pattern Integration

public class ProductService {
    private readonly IProductRepository _repository;
    private readonly IServiceProvider _serviceProvider;

    public ProductService( IProductRepository repository, IServiceProvider serviceProvider ) {
        _repository = repository;
        _serviceProvider = serviceProvider;
    }

    public async Task<ProductDto> GetProductAsync( int productId ) {
        var product = await _repository.GetByIdAsync(productId);
        return product.To<ProductDto>(_serviceProvider);
    }

    public async Task<IEnumerable<ProductDto>> GetAllProductsAsync() {
        var products = await _repository.GetAllAsync();
        return await products.ToAsync<ProductDto>(_serviceProvider);
    }

    public async Task<Product> CreateProductAsync( CreateProductDto dto ) {
        var product = dto.To<Product>(_serviceProvider);
        return await _repository.AddAsync(product);
    }
}

Checking Mapping Availability

Verify if a mapping exists before attempting transformation:

var dto = new UserDto();

// Check if mapping exists
if (dto.CanBindTo<User>()) {
    var user = dto.To<User>();
}

// Type-safe check
if (dto.CanBindTo<UserDto, User>()) {
    var user = dto.To<User>();
}

Exception Handling

Myth.Morph provides specific exceptions for different error scenarios:

Exception Types

  • BinderNotFoundException: No mapping registered between source and destination types
  • BindException: Property or field binding operation failed
  • InvalidMorphConfigurationException: SchemaRegistry not properly configured in DI

Example

try {
    var user = dto.To<User>();
} catch ( BinderNotFoundException ex ) {
    logger.LogError("No mapping found from {Source} to {Dest}", ex.SourceType, ex.DestType);
} catch ( BindException ex ) {
    logger.LogError("Property binding failed: {Message}", ex.Message);
} catch ( InvalidMorphConfigurationException ex ) {
    logger.LogError("Morph not configured: {Message}", ex.Message);
}

Performance Considerations

Best Practices

  1. Reuse Service Provider: Pass the same service provider instance when transforming multiple objects
var users = new List<User>();
foreach (var dto in dtos) {
    users.Add(dto.To<User>(serviceProvider)); // Reuse provider
}
  1. Use Async for I/O: Always use async bindings for database or API calls
schema.BindAsync(u => u.Profile, async sp => {
    var service = sp.GetRequiredService<IProfileService>();
    return await service.GetProfileAsync(UserId); // Async I/O
});
  1. Limit Assembly Scanning: Reduce startup time by specifying assemblies
services.AddMorph(settings => {
    settings.ClearAssemblies()
            .AddAssembly(typeof(MyDto).Assembly);
});
  1. Batch Collections: Transform collections in bulk rather than individually
// Good
var users = dtos.To<User>();

// Avoid
var users = dtos.Select(d => d.To<User>()).ToList();

Logging

Myth.Morph provides comprehensive logging at different levels:

  • Information: Registry initialization, assembly scanning
  • Debug: Mapping executions, type resolution
  • Trace: Property bindings, automatic mappings
  • Warning: Partial type loading, mapping failures
  • Error: Configuration issues, binding exceptions

Enable logging in your application:

builder.Logging.SetMinimumLevel(LogLevel.Debug);

Troubleshooting

"ServiceProvider not configured" Error

// Ensure AddMorph() is called and BuildApp() is used
services.AddMorph();
var app = builder.BuildApp(); // Not builder.Build()

"No mapping found" Error

// Ensure source implements IMorphable<TDestination>
public class MyDto : IMorphable<MyEntity> {
    public void MorphTo( Schema<MyEntity> schema ) { }
}

"SchemaRegistry not found in DI" Error

// Missing AddMorph() registration
services.AddMorph(); // Add this

Mapping Not Working for Generic Collections

// Register generic mapping if needed
services.AddMorph(settings => {
    settings.AddGenericMapping<IMyCollection<>, MyCollection<>>();
});

Property Not Being Mapped

Check these common issues:

  1. Property name mismatch (case-sensitive)
  2. Property not writable (no setter)
  3. Property explicitly ignored
  4. Type incompatibility

Integration with Other Myth Libraries

With Myth.Flow

var result = await Pipeline.Start(createUserDto)
    .Step((dto, sp) => dto.To<User>(sp))
    .StepAsync<IUserRepository>((repo, user) => repo.AddAsync(user))
    .ExecuteAsync();

With Myth.Guard

public class CreateUserDto : IMorphable<User>, IValidatable<CreateUserDto> {
    public string Name { get; set; }
    public string Email { get; set; }

    public void Validate( ValidationBuilder<CreateUserDto> builder, ValidationContextKey? context ) {
        builder.For(Name, x => x.NotEmpty().MinimumLength(3));
        builder.For(Email, x => x.Email());
    }

    public void MorphTo( Schema<User> schema ) {
        schema.Bind(u => u.FullName, () => Name);
        schema.Bind(u => u.EmailAddress, () => Email);
    }
}

// Use in controller
await _validator.ValidateAsync(dto);
var user = dto.To<User>();

With Myth.Repository

public class UserRepository : IUserRepository {
    public async Task<UserDto> GetUserDtoAsync( int userId ) {
        var user = await _dbContext.Users.FindAsync(userId);
        return user.To<UserDto>();
    }
}

Contributing

Contributions are welcome! Please read our contributing guidelines and feel free to submit pull requests.

License

This project is licensed under the Apache License 2.0 - see the LICENSE file for details.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
4.2.1-preview.1 32 12/2/2025
4.2.0 87 11/30/2025
4.2.0-preview.1 44 11/29/2025
4.1.0 162 11/27/2025
4.1.0-preview.3 124 11/27/2025
4.1.0-preview.2 122 11/27/2025
4.1.0-preview.1 122 11/26/2025
4.0.1 149 11/22/2025
4.0.1-preview.8 141 11/22/2025
4.0.1-preview.7 150 11/22/2025
4.0.1-preview.6 135 11/22/2025
4.0.1-preview.5 195 11/21/2025
4.0.1-preview.4 201 11/21/2025
4.0.1-preview.3 208 11/21/2025
4.0.1-preview.2 228 11/21/2025
4.0.1-preview.1 245 11/21/2025
4.0.0 383 11/20/2025
4.0.0-preview.3 337 11/19/2025
4.0.0-preview.2 84 11/15/2025
4.0.0-preview.1 104 11/15/2025
3.10.0 162 11/15/2025
3.0.5-preview.15 145 11/14/2025
3.0.5-preview.14 214 11/12/2025
3.0.5-preview.13 226 11/12/2025
3.0.5-preview.12 218 11/11/2025
3.0.5-preview.11 220 11/11/2025
3.0.5-preview.10 219 11/11/2025
3.0.5-preview.9 211 11/10/2025
3.0.5-preview.8 82 11/8/2025
3.0.5-preview.7 89 11/8/2025
3.0.5-preview.6 85 11/8/2025
3.0.5-preview.5 86 11/8/2025
3.0.5-preview.4 59 11/7/2025
3.0.5-preview.3 135 11/4/2025
3.0.5-preview.2 137 11/4/2025
3.0.5-preview.1 131 11/4/2025
3.0.4 181 11/3/2025
3.0.4-preview.19 69 11/2/2025
3.0.4-preview.18 72 11/1/2025
3.0.4-preview.17 72 11/1/2025
3.0.4-preview.16 71 11/1/2025
3.0.4-preview.15 66 10/31/2025
3.0.4-preview.14 130 10/31/2025
3.0.4-preview.13 137 10/30/2025
3.0.4-preview.12 123 10/23/2025
3.0.4-preview.11 121 10/23/2025
3.0.4-preview.10 121 10/23/2025
3.0.4-preview.9 120 10/23/2025
3.0.4-preview.8 117 10/22/2025
3.0.4-preview.6 115 10/21/2025
3.0.4-preview.5 117 10/21/2025
3.0.4-preview.4 119 10/20/2025
3.0.4-preview.3 125 10/20/2025
3.0.4-preview.2 45 10/18/2025
3.0.4-preview.1 121 10/7/2025
3.0.3 197 8/30/2025
3.0.2 101 8/23/2025
3.0.2-preview.4 123 8/21/2025
3.0.2-preview.3 106 8/16/2025