This project is read-only.

Knak.Mapper Tutorial

Default Mapping Convention

  1. MATCH public readable properties in the source type to public writable properties in the target type
  2. WHERE the names are exactly equal
  3. AND the types are compatible

Compatible types are:
  • EXACTLY the same type
  • OR they are both arrays of compatible types
  • OR they are both primitive types with defined typecasting from S to T

The matching process completes when all eligible target properties have been mapped, or when all eligible source properties have been checked against the targets.

Complex source types are "flattened" when searching for eligible properties, so the search will continue to the next level following the same rules until the target is completely mapped. (See examples below).

Default Mapping Examples

Let's take the following types from a fictitious ordering system:

    public class Address {
        public string Street { get; set; }
        public string City { get; set; }
        public string State { get; set; }
    }

    public class Customer {
        public string Name { get; set; }
        public int Age { get; set; }
        public Address Address { get; set; }
    }

    public class Product {
        public string Name { get; set; }
        public decimal Price { get; set; }
    }

    public class OrderDetail {
        public Product Product { get; set; }
        public int Quantity { get; set; }
        public decimal Total { get { return Product.Price * Quantity; } }
    }

    public class Order {
        public Customer Customer { get; set; }
        public DateTime OrderDate { get; set; }
        public OrderDetail[] Details { get; set; }
        public decimal Total {
            get {
                decimal total = 0;
                foreach (var d in Details)
                    total += d.Total;
                return total;
            }
        }
    }

    public class CustomerInfo {
        public string Name { get; set; }
        public int Age { get; set; }
    }

    public class OrderInfo {
        public DateTime OrderDate { get; set; }
        public decimal Total { get; set; }
        public string CustomerName { get; set; }
        public string City { get; set; }
        public string State { get; set; }
    }

Let's say we need to map a Customer to a CustomerInfo for data transfer. The default mapping will look like this:

var customer = new Customer { Name = "John Doe", Age = 30, 
   Address = new Address { Street = "123 Street", City = "Miami", State = "FL" } };

var customerInfo = new CustomerInfo().MapFrom(customer);

If you need to see what just happened, the mappings and mapping expression generated by Knak on the fly is available here:

Mapper.Of<Customer, CustomerInfo>.Mappings
Mapper.Of<Customer, CustomerInfo>.Expression

---------------- Mappings ---------------------
System.String : Name -> System.String : Name
System.Int32 : Age -> System.Int32 : Age
 
---------------- Expression ---------------------
.Lambda Mapper<Knak.Mapper+PropertyMapper`2+MapperCallback[Knack.Console.Customer,Knack.Console.CustomerInfo]>(
    Knack.Console.Customer $source,
    Knack.Console.CustomerInfo $target) {
    .Block() {
        $target.Name = $source.Name;
        $target.Age = $source.Age
    }
}

What if you need to clone the customer?

var customer2 = customer.MapTo(new Customer());

And the mappings are:

Mapper.Of<Customer, Customer>.Mappings
Mapper.Of<Customer, Customer>.Expression

---------------- Mappings ---------------------
System.String : Name -> System.String : Name
System.Int32 : Age -> System.Int32 : Age
Knack.Console.Address : Address -> Knack.Console.Address : Address
 
---------------- Expression ---------------------
.Lambda Mapper<Knak.Mapper+PropertyMapper`2+MapperCallback[Knack.Console.Customer,Knack.Console.Customer]>(
    Knack.Console.Customer $source,
    Knack.Console.Customer $target) {
    .Block() {
        $target.Name = $source.Name;
        $target.Age = $source.Age;
        .If (
            $source.Address != null & $target.Address == null
        ) {
            $target.Address = .New Knack.Console.Address()
        } .Else {
            .Default(System.Void)
        };
        .Call Knak.Mapper.MapTo(
            $source.Address,
            $target.Address)
    }
}

Flattening and Arrays

Now, let's see how Order is mapped to OrderInfo:

var customer = new Customer { Name = "John Doe", Age = 30, 
   Address = new Address { Street = "123 Street", City = "Miami", State = "FL" } };
var details = new OrderDetail[] { 
    new OrderDetail { Product = new Product { Name = "Soda", Price = .50M }, 
        Quantity = 2 }, 
    new OrderDetail { Product = new Product { Name = "Chips", Price = 1.25M }, 
        Quantity = 4 }
};
var order = new Order { Customer = customer, OrderDate = DateTime.Now, Details = details };

var orderinfo = new OrderInfo().MapFrom(order);

The mapper is flattening Order when matching City and State fields from Customer.Address.

---------------- Mappings ---------------------
System.DateTime : OrderDate -> System.DateTime : OrderDate
System.Decimal : Total -> System.Decimal : Total
System.String : Customer.Address.City -> System.String : City
System.String : Customer.Address.State -> System.String : State

If we add OrderDetail[] to OrderInfo, the mappings will now include the details:

public class OrderInfo {
    public DateTime OrderDate { get; set; }
    public decimal Total { get; set; }
    public string CustomerName { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public OrderDetail[] Details { get; set; }
}

---------------- Mappings ---------------------
System.DateTime : OrderDate -> System.DateTime : OrderDate
Knack.Console.OrderDetail[] : Details -> Knack.Console.OrderDetail[] : Details
System.Decimal : Total -> System.Decimal : Total
System.String : Customer.Address.City -> System.String : City
System.String : Customer.Address.State -> System.String : State

---------------- Expression ---------------------
.Lambda Mapper<Knak.Mapper+PropertyMapper`2+MapperCallback[Knack.Console.Order,K
nack.Console.OrderInfo]>(
    Knack.Console.Order $source,
    Knack.Console.OrderInfo $target) {
    .Block() {
        $target.OrderDate = $source.OrderDate;
        $target.Details = .Call Knak.Mapper.MapArray($source.Details);
        $target.Total = $source.Total;
        .If ($source.Customer != null) {
            .Block() {
                .If (($source.Customer).Address != null) {
                    .Block() {
                        $target.City = (($source.Customer).Address).City;
                        $target.State = (($source.Customer).Address).State
                    }
                } .Else {
                    .Default(System.Void)
                }
            }
        } .Else {
            .Default(System.Void)
        }
    }
}

Custom Bindings and Attributes

If we don't have full control over the types and we need to customize the mappings, we can bind source expressions to target properties using the static Mapper. Make sure you set the bindings before calling the mapping functions (remember the mappings are cached the first time they are used). You can also reset the bindings and cache using Mapper.Of<S, T>.Binder.Reset().

So, let's bind the customer name and ignore the city and order details:

Mapper.Of<Order, OrderInfo>.Binder
   .Bind(s => s.Customer.Name, t => t.CustomerName)
   .Ignore(t => t.Details)
   .Ignore(t => t.City);
var orderinfo = new OrderInfo().MapFrom(order);

---------------- Mappings ---------------------
System.String : s => s.Customer.Name -> System.String : CustomerName
System.DateTime : OrderDate -> System.DateTime : OrderDate
System.Decimal : Total -> System.Decimal : Total
System.String : Customer.Address.State -> System.String : State

----------------Expression ---------------------
.Lambda Mapper<Knak.Mapper+PropertyMapper`2+MapperCallback[Knack.Console.Order,K
nack.Console.OrderInfo]>(
    Knack.Console.Order $source,
    Knack.Console.OrderInfo $target) {
    .Block() {
        $target.CustomerName = .Invoke (.Lambda #Lambda1<System.Func`2[Knack.Con
sole.Order,System.String]>)($source);
        $target.OrderDate = $source.OrderDate;
        $target.Total = $source.Total;
        .If ($source.Customer != null) {
            .Block() {
                .If (($source.Customer).Address != null) {
                    .Block() {
                        $target.State = (($source.Customer).Address).State
                    }
                } .Else {
                    .Default(System.Void)
                }
            }
        } .Else {
            .Default(System.Void)
        }
    }
}

.Lambda #Lambda1<System.Func`2[Knack.Console.Order,System.String]>(Knack.Console
.Order $s) {
    ($s.Customer).Name
}

We can also control the mappings using property attributes in the source and target types.
  • IgnoreAttribute to ignore the property when matching types.
  • InputAttribute to use the property as input only (Source), and ignore it in the target type.
  • OutputAttribute to use the property as output only (Target), and ignore it in the source type.

You can see some examples using attributes in the Knak.Data Tutorial.

Last edited Dec 3, 2012 at 2:36 PM by knak, version 3

Comments

No comments yet.