Showing posts with label Object to Object Mapping. Show all posts
Showing posts with label Object to Object Mapping. Show all posts

Sunday, February 15, 2009

Object/Object Mapping посредством AutoMapper.

В своём предыдущем посте на тему отображения объектов на объекты, я упомянул несколько библиотек, позволяющих осуществлять это самое отображение.  Не так давно появилась разработка от Jimmy Bogard'a под названием AutoMapper. AutoMapper - это преобразователь объектов(Domain Objects->DTO(или других простых объектов различного назначения)), основанный на соглашениях об именовании членов классов для осуществления преобразования по-умолчанию и имеющий fluent-API конфигурации для определения специальных стратегий преобразования.  Разработка очень свежая(зелёная), но поскольку для меня этот вопрос еще полностью не закрыт и я уважаю её автора, то несомненно решил посмотреть на неё.

И так не буду оригинальным с выбором доменной модели для экспериментов :)

Domain

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using DotToolkit.Taijutsu.DomainModel;

namespace Govorov.Nikita.Blog.AutoMapper.DomainModel
{
public class Customer : Entity<long>
{
private IProductDiscountStrategy discountStrategy = new DefaultDiscountStrategy {Percent = 25};
private string name;

protected Customer()
{
}

public Customer(string name)
{
this.name = name;
}

public virtual string Name
{
get { return name; }
protected set { name = value; }
}

public virtual IProductDiscountStrategy DiscountStrategy
{
get { return discountStrategy; }
set { discountStrategy = value; }
}
}

public interface IProductDiscountStrategy
{
decimal Discount(Product product);
}

public class DefaultDiscountStrategy : IProductDiscountStrategy
{
private short percent = 10;

public virtual short Percent
{
get { return percent; }
set { percent = value; }
}

#region IProductDiscountStrategy Members

public decimal Discount(Product product)
{
return new Random().Next()%2 == 0 ? (product.Price*Percent/100) + product.Price : product.Price;
}

#endregion
}

[Serializable]
public class Order : Entity<Guid>, IAggregateRoot
{
private IList<OrderItem> orderItems = new List<OrderItem>();
private Customer customer;

protected Order()
{
}

public Order(Customer owner)
{
this.customer = owner;
}

public virtual Customer Customer
{
get { return customer; }
protected set { customer = value; }
}

protected virtual IList<OrderItem> OrderItems
{
get { return orderItems; }
set { orderItems = value; }
}

public virtual ReadOnlyCollection<OrderItem> Items
{
get { return new ReadOnlyCollection<OrderItem>(OrderItems); }
}

public virtual void IncludeToOrder(short count, Product product)
{
OrderItems.Add(new OrderItem(Customer.DiscountStrategy.Discount(product), count, product));
}

public virtual void IncludeToOrder(Product product)
{
IncludeToOrder(1, product);
}

public virtual decimal GetOrderTotal()
{
return (from item in OrderItems select item.Price).Sum();
}

public virtual int GetProductsCount()
{
return (from item in OrderItems select item.Count).Sum(selector => selector);
}
}

[Serializable]
public class OrderItem : Entity<Guid>
{
private short count;
private decimal price;
private Product product;
private DateTime сreationDate;

protected OrderItem()
{
}

public OrderItem(Product product)
: this(product.Price, 1, product)
{
}

public OrderItem(decimal price, Product product)
: this(price, 1, product)
{
}

public OrderItem(decimal price, short count, Product product)
{
this.price = price;
this.count = count;
this.product = product;
сreationDate = DateTime.Now;
}


public virtual DateTime СreationDate
{
get { return сreationDate; }
protected set { сreationDate = value; }
}

public virtual short Count
{
get { return count; }
protected set { count = value; }
}

public virtual decimal Price
{
get { return price; }
protected set { price = value; }
}

public virtual Product Product
{
get { return product; }
protected set { product = value; }
}
}

[Serializable]
public class ProductKey
{
private long code;
private Vendor vendor;

protected ProductKey()
{
}

public ProductKey(long code, Vendor vendor)
{
this.code = code;
this.vendor = vendor;
}

public virtual long Code
{
get { return code; }
protected set { code = value; }
}

public virtual Vendor Vendor
{
get { return vendor; }
protected set { vendor = value; }
}
}


[Serializable]
public class Product : Entity<ProductKey>
{
private string name;
private decimal price;

protected Product()
{
}

public Product(ProductKey key, string name, decimal price)
{
this.key = key;
this.name = name;
this.price = price;
}

public virtual string Name
{
get { return name; }
protected set { name = value; }
}

public virtual decimal Price
{
get { return price; }
protected set { price = value; }
}
}

[Serializable]
public class Vendor : Entity<Guid>
{
private string title;

protected Vendor()
{
}

public Vendor(string title)
{
this.title = title;
}

public virtual string Title
{
get { return title; }
protected set { title = value; }
}
}
}

преобразовать это нужно в следующее:

Data 

using System;
using System.Collections.Generic;
using System.Text;

namespace Govorov.Nikita.Blog.AutoMapper.Data
{
[Serializable]
public class UserInterfaceOrderData
{
public virtual string CustomerName { get; set; }
public virtual int UniqueProductTypeCount { get; set; }
public virtual decimal OrderTotal { get; set; }
public virtual int ProductsCount { get; set; }
public virtual int ItemsCount { get; set; }
public virtual IList<UserInterfaceOrderItemData> Items { get; set; }

public override string ToString()
{
var builder = new StringBuilder().Append(Environment.NewLine);
foreach (var item in Items)
{
builder.Append(item).Append(Environment.NewLine);
}
return string.Format(" CustomerName: {0},\n UniqueProductTypeCount: {1},\n OrderTotal: {2},\n ProductsCount: {3},\n ItemsCount: {4},\n Items: {5}", CustomerName, UniqueProductTypeCount, OrderTotal, ProductsCount, ItemsCount, builder);
}
}


[Serializable]
public class UserInterfaceOrderItemData
{
public virtual decimal Price { get; set; }
public virtual string ProductName { get; set; }
public virtual string FullProductName { get; set; }
public virtual long ProductCode { get; set; }
public virtual string ProductVendor { get; set; }
public virtual short Count { get; set; }
public virtual DateTime СreationDate { get; set; }

public override string ToString()
{
return string.Format(" ProductName: {1},\n Price: {0},\n FullProductName: {2},\n ProductCode: {3},\n ProductVendor: {4},\n Count: {5},\n СreationDate: {6}", Price, ProductName, FullProductName, ProductCode, ProductVendor, Count, СreationDate);
}
}
}

Код осуществляющий конфигурцию маппера для данного преобразования будет выглядеть следующим образом:

Mapper.CreateMap<Order, UserInterfaceOrderData>()
.ForMember(dto => dto.UniqueProductTypeCount,
map => map.MapFrom(ord =>(from item in ord.Items select item.Product).Distinct().Count()));

Mapper.CreateMap<OrderItem, UserInterfaceOrderItemData>()
.ForMember(dto=>dto.ProductCode,map=>map.MapFrom(ordIt=>ordIt.Product.Key.Code))
.ForMember(dto => dto.ProductVendor, map => map.MapFrom(ordIt => ordIt.Product.Key.Vendor.Title))
.ForMember(dto => dto.FullProductName, map => map.MapFrom(
ordIt => new StringBuilder(ordIt.Product.Key.Vendor.Title)
.Append(" ").Append(ordIt.Product.Name)));

Mapper.AssertConfigurationIsValid(); //Очень полезный метод.

На самом деле выглядит неплохо, но и до идеала явно не дотягивает. Стоит учитывать что на данный момент доступна лишь 0.2.0 Alpha, поэтому очень многого не хватает.  Если проект будет продвигаться такими же темпами, то к первой версии мне кажется он будет единственным пригодным оо маппером для .net (если мои поиски меня не подвели).

Маленькое тестовое приложение, позволяюще наглядно посмотреть  на преобразование:

using System;
using System.Linq;
using System.Text;
using AutoMapper;
using Govorov.Nikita.Blog.AutoMapper.Data;
using Govorov.Nikita.Blog.AutoMapper.DomainModel;

namespace Govorov.Nikita.Blog.AutoMapper
{
internal class Program
{
private static void Main()
{
var microsoft = new Vendor("Microsoft");

var windowsXP = new Product(new ProductKey(1234, microsoft), "Windows XP", 100);
var windowsVista = new Product(new ProductKey(4321, microsoft), "Windows Vista", 200);
var windows7 = new Product(new ProductKey(789, microsoft), "Windows 7", 150);

var customer = new Customer("Горбушка") {DiscountStrategy = new DefaultDiscountStrategy {Percent = 10}};

var order = new Order(customer);

order.IncludeToOrder(windowsXP);
order.IncludeToOrder(windowsVista);
order.IncludeToOrder(windows7);
order.IncludeToOrder(2, windowsXP);


Mapper.CreateMap<Order, UserInterfaceOrderData>()
.ForMember(dto => dto.UniqueProductTypeCount,
map => map.MapFrom(ord =>(from item in ord.Items select item.Product).Distinct().Count()));

Mapper.CreateMap<OrderItem, UserInterfaceOrderItemData>()
.ForMember(dto=>dto.ProductCode,map=>map.MapFrom(ordIt=>ordIt.Product.Key.Code))
.ForMember(dto => dto.ProductVendor, map => map.MapFrom(ordIt => ordIt.Product.Key.Vendor.Title))
.ForMember(dto => dto.FullProductName, map => map.MapFrom(
ordIt => new StringBuilder(ordIt.Product.Key.Vendor.Title)
.Append(" ").Append(ordIt.Product.Name)));

Mapper.AssertConfigurationIsValid(); //Очень полезный метод.

var uiOrderData = Mapper.Map<Order, UserInterfaceOrderData>(order);

Console.WriteLine(uiOrderData);
Console.ReadLine();
}
}
}

Вывод:

CustomerName: Горбушка,
UniqueProductTypeCount: 3,
OrderTotal: 605,
ProductsCount: 5,
ItemsCount: 4,
Items:
ProductName: Windows XP,
Price: 110,
FullProductName: Microsoft Windows XP,
ProductCode: 1234,
ProductVendor: Microsoft,
Count: 1,
СreationDate: 14.02.2009 22:11:08
ProductName: Windows Vista,
Price: 220,
FullProductName: Microsoft Windows Vista,
ProductCode: 4321,
ProductVendor: Microsoft,
Count: 1,
СreationDate: 14.02.2009 22:11:08
ProductName: Windows 7,
Price: 165,
FullProductName: Microsoft Windows 7,
ProductCode: 789,
ProductVendor: Microsoft,
Count: 1,
СreationDate: 14.02.2009 22:11:08
ProductName: Windows XP,
Price: 110,
FullProductName: Microsoft Windows XP,
ProductCode: 1234,
ProductVendor: Microsoft,
Count: 2,
СreationDate: 14.02.2009 22:11:08

Надеюсь, что появится возможность конфигурации и через xml, наподобии как это сделано в otis-lib. Мне нравится fluent-api(привет fluent-nhibernate, Loquacious, fluent-spring, StructureMap и всем остальным), но все таки мне кажется долна быть возможность выбирать.

Thursday, October 16, 2008

Object/Object Mapping(отображение объектов на объекты) в .NET

object-to-object mappingИспользуя в своих приложениях доменную модель (Domain Model), как способ организации бизнес логики, в сервисах приложения сталкиваешься с постоянной необходимостью преобразования объектов домена в объекты данных (Data Transfer Object - DTO) для передачи их во внешние системы, различным клиентским приложениям и т.п.  К сожалению, для .NET я смог найти  только следующие библиотеки:

  1. Otis-lib ;
  2. NBearMapping;
  3. Business Logic Toolkit (проект на rsdn);
  4. NPersist;

Со второй не получилось даже толком познакомится, т.к. ни документации, ни примеров я не обнаружил (но как я понял из исходников, она не дотягивает до следующих двух).  Последние две направлены в большей мере на решение O/R M задач, и поддерживают O/O M как подмножество функций необходимых для решения основных задач (объектно-реляционная проекция), поэтому отсутствие необходимых, для решения моей задачи, функций и наличие в них лишних функций (мой O/R M меня устраивает :-)) вынудили меня вычеркнуть их из списка. Таким образом осталось только одна библиотека - Otis.

Сразу скажу, что и она меня не удовлетворила, не смотря на положительное первоначальное впечатление. И так почему оно возникло? Otis специально создан для отображения объектов домена на DTO и, соотвественно, имеет достаточный набор функций для безболезненного осуществления этого процесса. Пример конфигурации преобразования с использованием Otis:

<?xml version="1.0" encoding="utf-8" ?>
<otis-mapping xmlns="urn:otis-mapping-1.0">

<class name="Otis.Sample.Presentation.ArtistInfo, Otis.Sample" source="Otis.Sample.Domain.Artist, Otis.Sample" >
<member name="Id" />
<member name="Description" expression="[$Name + ' [' + $Country + ']' ]" />
<member name="Records" />
<member name="RecordCount" expression="$Records.Count" />
</class>

<class name="Otis.Sample.Presentation.RecordInfo, Otis.Sample" source="Otis.Sample.Domain.Record, Otis.Sample" helper="ConversionHelper" >
<member name="Id" />
<member name="Description" expression="[$Name + ' [' + $YearPublished + ']' ]" />
<member name="SongCount" expression="$Songs.Count" />
<member name="TotalCommentCount" expression="count:$Songs/Comments" />
<member name="AverageSongRating" expression="avg:$Songs/Ratings/Value" />
<member name="AverageSongDuration" expression="avg:$Songs/DurationInSeconds" format="{0:N2} seconds" />
<member name="AlbumDuration" expression="sum:$Songs/DurationInSeconds" />
<member name="Category" expression="$Style" >
<map from="['Pop']" to="Pop"/>
<map from="['Punk']" to="Rock"/>
<map from="['Rock']" to="Rock"/>
<map from="['Classical']" to="Classical"/>
</member>
</class>

</otis-mapping>


Критические недостатки реализации Otis-lib:
  • невозможность использования в серверных приложениях (отсутствует поддержка многопоточности, кэша сгенерированных классов, сборок);


  • использование CodeDom для генерации кода преобразователей;


  • нерасширяемая архитектура;


Ко всем недостаткам реализации добавляются низкие темпы развития проекта. Со всеми этими недостатками Otis-lib становится непригодной в использовании на реальных проектах.

Мои поиски продолжаются, может быть Вы знаете еще о каких-нибудь преобразователях (object to object mapper)?