Skip to content

Object Mapping in ConnectSoft Microservice Template

Purpose & Overview

Object Mapping is the process of transforming objects from one representation to another, enabling clean separation between layers in the microservice architecture. In the ConnectSoft Microservice Template, AutoMapper is used to automatically map between ServiceModel DTOs (Data Transfer Objects) and DomainModel Input/Output models, eliminating boilerplate mapping code and ensuring consistent transformations across the application.

Object mapping provides:

  • Layer Isolation: ServiceModel DTOs remain separate from DomainModel, preventing internal structure leakage
  • Protocol Independence: Same DTOs can be used across REST, gRPC, CoreWCF, GraphQL, and Azure Functions
  • Automatic Transformation: Convention-based mapping reduces manual mapping code
  • Type Safety: Compile-time checking of mapping configurations
  • Maintainability: Centralized mapping profiles make changes easier
  • Testability: Mapping configurations are validated via unit tests

Object Mapping Philosophy

Object mapping is the bridge between layers in Clean Architecture. By using AutoMapper, we eliminate repetitive mapping code while maintaining strict boundaries between ServiceModel (API contracts) and DomainModel (business logic). This ensures that API contracts can evolve independently from domain models, and that internal domain structure never leaks to external clients.

Architecture Overview

Mapping Flow in Clean Architecture

External Clients
Service Model Layer (ServiceModel)
    ├── Request DTOs (CreateMicroserviceAggregateRootRequest)
    ├── Response DTOs (CreateMicroserviceAggregateRootResponse)
    └── DTOs (MicroserviceAggregateRootDto)
    ↓ (AutoMapper)
Domain Model Layer (DomainModel)
    ├── Input Models (CreateMicroserviceAggregateRootInput)
    ├── Output Models (FeatureAUseCaseAOutput)
    └── Processors/Retrievers
Entity Model Layer (EntityModel)
    └── Domain Entities (IMicroserviceAggregateRoot)

Mapping Directions

Direction Source Target Purpose
Request → Input CreateMicroserviceAggregateRootRequest CreateMicroserviceAggregateRootInput Transform API request to domain input
Entity → DTO IMicroserviceAggregateRoot MicroserviceAggregateRootDto Transform domain entity to API response
Output → Response FeatureAUseCaseAOutput FeatureAUseCaseAResponse Transform domain output to API response

AutoMapper Configuration

Service Registration

AutoMapper is registered via extension method:

// MicroserviceRegistrationExtensions.cs
services.AddAutoMapper();

Registration Implementation

// AutoMapperExtensions.cs
internal static void AddAutoMapper(this IServiceCollection services)
{
    ArgumentNullException.ThrowIfNull(services);

    // Create mapper configuration
    var mapperConfig = new MapperConfiguration(mc =>
    {
#if UseGrpc || UseRestApi || UseCoreWCF || UseGraphQL || UseServiceFabric || UseAzureFunction
        mc.AddProfile<MicroserviceServiceModelMappingProfile>();
#endif
    });

    // Create mapper instance
    IMapper mapper = mapperConfig.CreateMapper();

    // Register as singleton
    services.AddSingleton(mapper);
    services.ActivateSingleton<IMapper>();
}

Key Points: - MapperConfiguration: Created once at startup - IMapper: Registered as singleton (thread-safe) - Profiles: Conditionally included based on service model selection - Validation: Configuration is validated at startup (via unit tests)

Mapping Profiles

Profile Structure

Mapping profiles inherit from Profile and define mappings in the constructor:

// MicroserviceServiceModelMappingProfile.cs
namespace ConnectSoft.MicroserviceTemplate.ApplicationModel.Mappings
{
    using AutoMapper;
    using ConnectSoft.MicroserviceTemplate.DomainModel;
    using ConnectSoft.MicroserviceTemplate.EntityModel;
    using ConnectSoft.MicroserviceTemplate.ServiceModel;

    public class MicroserviceServiceModelMappingProfile : Profile
    {
        public MicroserviceServiceModelMappingProfile()
        {
            // Domain Entity → DTO
            this.CreateMap<IMicroserviceAggregateRoot, MicroserviceAggregateRootDto>();

            // Request → Input
            this.CreateMap<GetMicroserviceAggregateRootDetailsRequest, GetMicroserviceAggregateRootDetailsInput>();
            this.CreateMap<CreateMicroserviceAggregateRootRequest, CreateMicroserviceAggregateRootInput>();
            this.CreateMap<DeleteMicroserviceAggregateRootRequest, DeleteMicroserviceAggregateRootInput>();

            // Output → Response
            this.CreateMap<FeatureAUseCaseARequest, FeatureAUseCaseAInput>();
            this.CreateMap<FeatureAUseCaseAOutput, FeatureAUseCaseAResponse>();
        }
    }
}

Profile Location

Profiles are located in: ConnectSoft.MicroserviceTemplate.ApplicationModel/Mappings/

Naming Convention: MicroserviceServiceModelMappingProfile (Profile suffix)

Convention-Based Mapping

Automatic Property Mapping

AutoMapper automatically maps properties with matching names:

// Source
public class CreateMicroserviceAggregateRootRequest
{
    public Guid ObjectId { get; set; }
}

// Target
public class CreateMicroserviceAggregateRootInput
{
    public Guid ObjectId { get; set; }
}

// Mapping (automatic - no configuration needed)
this.CreateMap<CreateMicroserviceAggregateRootRequest, CreateMicroserviceAggregateRootInput>();

Convention Rules: - Name Matching: Properties with same name are mapped automatically - Type Matching: Types must be compatible (or have implicit conversion) - Case Insensitive: Property names are matched case-insensitively - Nested Properties: Nested objects are mapped recursively

Supported Type Conversions

AutoMapper automatically handles:

Source Type Target Type Conversion
string int Parsing (if valid)
int string .ToString()
Guid string .ToString()
DateTime DateTimeOffset Implicit conversion
List<T> IEnumerable<T> Direct assignment
T? T Nullable to non-nullable (if not null)

Custom Mappings

Property Name Mapping

When property names differ, use ForMember:

this.CreateMap<SourceClass, TargetClass>()
    .ForMember(dest => dest.TargetProperty, opt => opt.MapFrom(src => src.SourceProperty));

Example:

// Source
public class UserRequest
{
    public string UserName { get; set; }
}

// Target
public class UserInput
{
    public string Name { get; set; }
}

// Custom mapping
this.CreateMap<UserRequest, UserInput>()
    .ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.UserName));

Ignoring Properties

Ignore properties that shouldn't be mapped:

this.CreateMap<SourceClass, TargetClass>()
    .ForMember(dest => dest.InternalProperty, opt => opt.Ignore());

Example:

// Source
public class UserRequest
{
    public string UserName { get; set; }
    public string InternalSecret { get; set; }
}

// Target
public class UserInput
{
    public string UserName { get; set; }
    // No InternalSecret property
}

// Ignore internal property
this.CreateMap<UserRequest, UserInput>()
    .ForMember(dest => dest.InternalSecret, opt => opt.Ignore());

Value Transformations

Transform values during mapping:

this.CreateMap<SourceClass, TargetClass>()
    .ForMember(dest => dest.UppercaseName, opt => opt.MapFrom(src => src.Name.ToUpperInvariant()));

Example:

// Source
public class UserRequest
{
    public string Email { get; set; }
}

// Target
public class UserInput
{
    public string NormalizedEmail { get; set; }
}

// Transform during mapping
this.CreateMap<UserRequest, UserInput>()
    .ForMember(dest => dest.NormalizedEmail, opt => opt.MapFrom(src => src.Email.ToLowerInvariant()));

Conditional Mapping

Map properties conditionally:

this.CreateMap<SourceClass, TargetClass>()
    .ForMember(dest => dest.Status, opt => opt.MapFrom(src => 
        src.IsActive ? "Active" : "Inactive"));

Nested Object Mapping

AutoMapper automatically maps nested objects if types are mapped:

// Source
public class OrderRequest
{
    public AddressRequest ShippingAddress { get; set; }
}

public class AddressRequest
{
    public string Street { get; set; }
    public string City { get; set; }
}

// Target
public class OrderInput
{
    public AddressInput ShippingAddress { get; set; }
}

public class AddressInput
{
    public string Street { get; set; }
    public string City { get; set; }
}

// Mappings
this.CreateMap<AddressRequest, AddressInput>();
this.CreateMap<OrderRequest, OrderInput>();
// ShippingAddress is automatically mapped

Collection Mapping

Collections are automatically mapped:

// Source
public class OrderRequest
{
    public List<OrderItemRequest> Items { get; set; }
}

// Target
public class OrderInput
{
    public IReadOnlyList<OrderItemInput> Items { get; set; }
}

// Mappings
this.CreateMap<OrderItemRequest, OrderItemInput>();
this.CreateMap<OrderRequest, OrderInput>();
// Items collection is automatically mapped

AfterMap Actions

Perform additional operations after mapping:

this.CreateMap<SourceClass, TargetClass>()
    .AfterMap((src, dest) => 
    {
        dest.CalculatedProperty = src.Value1 + src.Value2;
    });

Example:

this.CreateMap<OrderRequest, OrderInput>()
    .AfterMap((src, dest) =>
    {
        dest.TotalAmount = src.Items.Sum(i => i.Price * i.Quantity);
        dest.CreatedAt = DateTimeOffset.UtcNow;
    });

Usage Patterns

Basic Mapping

In Controllers:

// REST API Controller
public class MicroserviceAggregateRootsServiceController : ControllerBase
{
    private readonly IMapper mapper;
    private readonly IMicroserviceAggregateRootsProcessor processor;

    public MicroserviceAggregateRootsServiceController(
        IMapper mapper,
        IMicroserviceAggregateRootsProcessor processor)
    {
        this.mapper = mapper;
        this.processor = processor;
    }

    [HttpPost("MicroserviceAggregateRoots/")]
    public async Task<CreateMicroserviceAggregateRootResponse> CreateMicroserviceAggregateRoot(
        [FromBody] CreateMicroserviceAggregateRootRequest request,
        CancellationToken token = default)
    {
        // Map Request → Input
        var input = this.mapper.Map<CreateMicroserviceAggregateRootRequest, CreateMicroserviceAggregateRootInput>(request);

        // Execute domain logic
        var entity = await this.processor.CreateMicroserviceAggregateRoot(input, token);

        // Map Entity → DTO
        var dto = this.mapper.Map<IMicroserviceAggregateRoot, MicroserviceAggregateRootDto>(entity);

        // Create response
        return new CreateMicroserviceAggregateRootResponse
        {
            CreatedMicroserviceAggregateRoot = dto
        };
    }
}

Generic Mapping Syntax

AutoMapper supports two mapping syntaxes:

// ✅ Preferred - Explicit type parameters
var input = this.mapper.Map<CreateMicroserviceAggregateRootRequest, CreateMicroserviceAggregateRootInput>(request);

// ✅ Alternative - Type inference
var input = this.mapper.Map<CreateMicroserviceAggregateRootInput>(request);

Recommendation: Use explicit type parameters for clarity and compile-time safety.

Mapping to Existing Instances

Update existing objects:

var existingInput = new CreateMicroserviceAggregateRootInput();
this.mapper.Map(request, existingInput);
// existingInput is updated with values from request

Mapping Collections

// Map list
var inputList = this.mapper.Map<List<CreateMicroserviceAggregateRootRequest>, List<CreateMicroserviceAggregateRootInput>>(requestList);

// Map enumerable
var inputEnumerable = this.mapper.Map<IEnumerable<CreateMicroserviceAggregateRootRequest>, IEnumerable<CreateMicroserviceAggregateRootInput>>(requestEnumerable);

Complete Request-Response Flow

Creating an Aggregate (Example)

flowchart TD
    Client[Client sends Request] --> Controller[Controller receives Request DTO]
    Controller --> Validate[Validate Request]
    Validate --> MapToInput[Map Request → Input]
    MapToInput --> Processor[Processor executes]
    Processor --> Entity[Returns Domain Entity]
    Entity --> MapToDto[Map Entity → DTO]
    MapToDto --> Response[Create Response]
    Response --> Client
Hold "Alt" / "Option" to enable pan & zoom

Step-by-Step Flow:

  1. Client Request: CreateMicroserviceAggregateRootRequest arrives at controller
  2. Validation: Request is validated (DataAnnotations, FluentValidation)
  3. Map to Input: mapper.Map<CreateMicroserviceAggregateRootRequest, CreateMicroserviceAggregateRootInput>(request)
  4. Domain Processing: Processor executes business logic with CreateMicroserviceAggregateRootInput
  5. Entity Return: Processor returns IMicroserviceAggregateRoot
  6. Map to DTO: mapper.Map<IMicroserviceAggregateRoot, MicroserviceAggregateRootDto>(entity)
  7. Response Creation: Response DTO created with mapped DTO
  8. Client Response: CreateMicroserviceAggregateRootResponse returned

Code Example

[HttpPost("MicroserviceAggregateRoots/")]
public async Task<CreateMicroserviceAggregateRootResponse> CreateMicroserviceAggregateRoot(
    [FromBody] CreateMicroserviceAggregateRootRequest request,
    CancellationToken token = default)
{
    // Step 1: Map Request → Input
    var input = this.mapper.Map<CreateMicroserviceAggregateRootRequest, CreateMicroserviceAggregateRootInput>(request);

    // Step 2: Execute domain logic
    var entity = await this.processor.CreateMicroserviceAggregateRoot(input, token);

    // Step 3: Map Entity → DTO
    var dto = this.mapper.Map<IMicroserviceAggregateRoot, MicroserviceAggregateRootDto>(entity);

    // Step 4: Create response
    return new CreateMicroserviceAggregateRootResponse
    {
        CreatedMicroserviceAggregateRoot = dto
    };
}

Testing

Mapping Configuration Validation

Validate mapping configurations via unit tests:

// MicroserviceServiceModelMappingProfileUnitTests.cs
[TestClass]
public class MicroserviceServiceModelMappingProfileUnitTests
{
    private MapperConfiguration? mapperConfiguration;

    [TestInitialize]
    public void InitializeMapperConfiguration()
    {
        this.mapperConfiguration = new MapperConfiguration(cfg =>
        {
#if UseGrpc || UseRestApi || UseCoreWCF || UseGraphQL || UseServiceFabric || UseAzureFunction
            cfg.AddProfile<MicroserviceServiceModelMappingProfile>();
#endif
        });
    }

    [TestMethod]
    public void ValidateMicroserviceServiceModelMappingProfileMappings()
    {
        // Assert
        Assert.IsNotNull(this.mapperConfiguration);

        // Validates all mappings are configured correctly
        this.mapperConfiguration.AssertConfigurationIsValid();
    }
}

Benefits: - Startup Validation: Catches mapping errors at test time, not runtime - Complete Coverage: Validates all mappings in the profile - Missing Properties: Detects unmapped properties - Type Mismatches: Detects incompatible type mappings

Mapping Execution Tests

Test actual mapping behavior:

[TestMethod]
public void Map_RequestToInput_ShouldMapCorrectly()
{
    // Arrange
    var mapper = this.mapperConfiguration.CreateMapper();
    var request = new CreateMicroserviceAggregateRootRequest
    {
        ObjectId = Guid.NewGuid()
    };

    // Act
    var input = mapper.Map<CreateMicroserviceAggregateRootRequest, CreateMicroserviceAggregateRootInput>(request);

    // Assert
    Assert.AreEqual(request.ObjectId, input.ObjectId);
}

[TestMethod]
public void Map_EntityToDto_ShouldMapCorrectly()
{
    // Arrange
    var mapper = this.mapperConfiguration.CreateMapper();
    var entity = new MicroserviceAggregateRootEntity
    {
        ObjectId = Guid.NewGuid(),
        SomeValue = "Test"
    };

    // Act
    var dto = mapper.Map<IMicroserviceAggregateRoot, MicroserviceAggregateRootDto>(entity);

    // Assert
    Assert.AreEqual(entity.ObjectId, dto.ObjectId);
}

Best Practices

Do's

  1. Use Convention-Based Mapping When Possible

    // ✅ GOOD - Automatic mapping when names match
    this.CreateMap<CreateMicroserviceAggregateRootRequest, CreateMicroserviceAggregateRootInput>();
    

  2. Keep Mappings Simple

    // ✅ GOOD - Simple, clear mapping
    this.CreateMap<Request, Input>();
    
    // ❌ BAD - Complex transformations should be in domain logic
    this.CreateMap<Request, Input>()
        .AfterMap((src, dest) => {
            // Complex business logic here ❌
        });
    

  3. Validate Mapping Configuration

    // ✅ GOOD - Validate in unit tests
    mapperConfiguration.AssertConfigurationIsValid();
    

  4. Use Explicit Type Parameters

    // ✅ GOOD - Explicit and clear
    var input = mapper.Map<CreateRequest, CreateInput>(request);
    
    // ⚠️ ACCEPTABLE - Type inference (less explicit)
    var input = mapper.Map<CreateInput>(request);
    

  5. Group Related Mappings

    // ✅ GOOD - Related mappings in same profile
    public class MicroserviceServiceModelMappingProfile : Profile
    {
        public MicroserviceServiceModelMappingProfile()
        {
            // All ServiceModel ↔ DomainModel mappings
            this.CreateMap<Request, Input>();
            this.CreateMap<Entity, Dto>();
        }
    }
    

  6. Document Complex Mappings

    // ✅ GOOD - Document why custom mapping is needed
    /// <summary>
    /// Maps UserRequest to UserInput with email normalization.
    /// </summary>
    this.CreateMap<UserRequest, UserInput>()
        .ForMember(dest => dest.Email, opt => opt.MapFrom(src => src.Email.ToLowerInvariant()));
    

Don'ts

  1. Don't Put Business Logic in Mappings

    // ❌ BAD - Business logic in mapping
    this.CreateMap<OrderRequest, OrderInput>()
        .AfterMap((src, dest) => {
            if (dest.TotalAmount > 1000)
            {
                dest.ApprovalRequired = true; // Business logic ❌
            }
        });
    
    // ✅ GOOD - Business logic in domain layer
    // Mapping stays simple
    this.CreateMap<OrderRequest, OrderInput>();
    // Domain processor handles approval logic
    

  2. Don't Skip Mapping Validation

    // ❌ BAD - No validation
    // Missing unit test for mapping validation
    
    // ✅ GOOD - Validate mapping configuration
    [TestMethod]
    public void ValidateMappings()
    {
        mapperConfiguration.AssertConfigurationIsValid();
    }
    

  3. Don't Expose Domain Entities Directly

    // ❌ BAD - Domain entity exposed
    public async Task<IMicroserviceAggregateRoot> GetEntity(Guid id)
    {
        return await repository.GetByIdAsync(id);
    }
    
    // ✅ GOOD - Map to DTO
    public async Task<MicroserviceAggregateRootDto> GetEntity(Guid id)
    {
        var entity = await repository.GetByIdAsync(id);
        return mapper.Map<IMicroserviceAggregateRoot, MicroserviceAggregateRootDto>(entity);
    }
    

  4. Don't Create Circular Mappings

    // ❌ BAD - Circular reference
    this.CreateMap<Parent, Child>();
    this.CreateMap<Child, Parent>();
    // Can cause stack overflow
    

  5. Don't Map Sensitive Data

    // ❌ BAD - Sensitive data in DTO
    public class UserDto
    {
        public string Password { get; set; } // Never!
        public string ApiKey { get; set; } // Never!
    }
    
    // ✅ GOOD - Map only safe properties
    this.CreateMap<UserEntity, UserDto>()
        .ForMember(dest => dest.Password, opt => opt.Ignore())
        .ForMember(dest => dest.ApiKey, opt => opt.Ignore());
    

Common Mapping Patterns

Pattern 1: Simple Property Mapping

When source and target properties have the same name:

// Source
public class CreateUserRequest
{
    public string UserName { get; set; }
    public string Email { get; set; }
}

// Target
public class CreateUserInput
{
    public string UserName { get; set; }
    public string Email { get; set; }
}

// Mapping (automatic)
this.CreateMap<CreateUserRequest, CreateUserInput>();

Pattern 2: Property Name Transformation

When property names differ:

// Source
public class CreateOrderRequest
{
    public Guid CustomerId { get; set; }
}

// Target
public class CreateOrderInput
{
    public Guid OrderCustomerId { get; set; }
}

// Custom mapping
this.CreateMap<CreateOrderRequest, CreateOrderInput>()
    .ForMember(dest => dest.OrderCustomerId, opt => opt.MapFrom(src => src.CustomerId));

Pattern 3: Computed Properties

When target property is computed:

// Source
public class OrderRequest
{
    public List<OrderItemRequest> Items { get; set; }
}

// Target
public class OrderInput
{
    public int ItemCount { get; set; }
}

// Computed mapping
this.CreateMap<OrderRequest, OrderInput>()
    .ForMember(dest => dest.ItemCount, opt => opt.MapFrom(src => src.Items.Count));

Pattern 4: Conditional Mapping

When mapping depends on source values:

// Source
public class UserRequest
{
    public bool IsActive { get; set; }
}

// Target
public class UserInput
{
    public string Status { get; set; }
}

// Conditional mapping
this.CreateMap<UserRequest, UserInput>()
    .ForMember(dest => dest.Status, opt => opt.MapFrom(src => 
        src.IsActive ? "Active" : "Inactive"));

Pattern 5: Nested Object Mapping

When objects contain nested objects:

// Source
public class OrderRequest
{
    public AddressRequest ShippingAddress { get; set; }
}

public class AddressRequest
{
    public string Street { get; set; }
    public string City { get; set; }
}

// Target
public class OrderInput
{
    public AddressInput ShippingAddress { get; set; }
}

public class AddressInput
{
    public string Street { get; set; }
    public string City { get; set; }
}

// Mappings
this.CreateMap<AddressRequest, AddressInput>();
this.CreateMap<OrderRequest, OrderInput>();
// ShippingAddress is automatically mapped

Troubleshooting

Issue: Mapping Configuration Invalid

Symptoms: AutoMapperConfigurationException during startup or test execution.

Solutions: 1. Check Property Names: Ensure all required properties are mapped 2. Verify Type Compatibility: Check source and target types are compatible 3. Review Ignored Properties: Ensure ignored properties are intentionally excluded 4. Check Nested Mappings: Verify nested object mappings are configured 5. Run Validation Test: mapperConfiguration.AssertConfigurationIsValid() will show specific errors

Example Error:

Unmapped members were found:
  - ObjectId (System.Guid)

Solution: Add mapping or ignore the property:

this.CreateMap<Source, Target>()
    .ForMember(dest => dest.ObjectId, opt => opt.Ignore());

Issue: Null Reference Exceptions

Symptoms: NullReferenceException during mapping.

Solutions: 1. Check Nullable Properties: Ensure nullable properties are handled 2. Use Nullable Reference Types: Enable nullable reference types for better null safety 3. Add Null Checks: Use conditional mapping for nullable properties

Example:

this.CreateMap<Source, Target>()
    .ForMember(dest => dest.OptionalProperty, opt => opt.MapFrom(src => 
        src.OptionalProperty ?? "DefaultValue"));

Issue: Property Not Mapping

Symptoms: Property value is null or default after mapping.

Solutions: 1. Check Property Names: Verify property names match exactly 2. Check Property Types: Ensure types are compatible 3. Verify Mapping Configuration: Check if property is explicitly ignored 4. Review Custom Mapping: Check if custom mapping is overriding automatic mapping

Example:

// Property not mapping - check if ignored
this.CreateMap<Source, Target>()
    .ForMember(dest => dest.PropertyName, opt => opt.MapFrom(src => src.PropertyName));

Issue: Performance Issues

Symptoms: Mapping operations are slow.

Solutions: 1. Compile Mappings: Use CreateMapper() instead of dynamic mapping 2. Avoid Complex Transformations: Move complex logic to domain layer 3. Cache Mapper Instance: Ensure IMapper is singleton (already configured) 4. Profile Mappings: Use profiling to identify slow mappings

Summary

Object mapping in the ConnectSoft Microservice Template provides:

  • Layer Isolation: Clean separation between ServiceModel and DomainModel
  • Automatic Mapping: Convention-based mapping reduces boilerplate code
  • Type Safety: Compile-time checking of mapping configurations
  • Protocol Independence: Same mappings work across REST, gRPC, CoreWCF, GraphQL
  • Testability: Mapping configurations are validated via unit tests
  • Maintainability: Centralized mapping profiles make changes easier
  • Flexibility: Custom mappings for complex scenarios

By following these patterns, teams can:

  • Maintain Clean Boundaries: Keep API contracts separate from domain models
  • Reduce Boilerplate: Eliminate repetitive mapping code
  • Ensure Consistency: Centralized mapping ensures consistent transformations
  • Test Thoroughly: Validate mapping configurations prevent runtime errors
  • Evolve Independently: API contracts can change without affecting domain models

Object mapping is the glue that connects layers in Clean Architecture—enabling clean separation while providing seamless data transformation between API boundaries and domain logic.