My favorites | Sign in
Project Logo
                
Search
for
Updated Mar 11, 2008 by zdeslav.vojkovic
Labels: Featured, Phase-Implementation
GettingStarted  
How to get started with Otis library

Introduction

This article describes how to get going with Otis. It shows how to configure mappings between types and how to perform the transformations

Basic idea

When implementing code for transformation between the types, the required effort can make significant part of overall effort on project, thus making the project more expensive and/or delaying release date. Types which implement these transformations are usually called assemblers, and often consist of heaps of trivial code which has to be properly tested and maintained. The idea behind otis is that the assembler requirements should be expressed declaratively, and the library will take care of generating the assembler types.

Pros & cons

Like any other tool/practice, this is not a silver bullet.

Main benefit of declarative assembler is that it reduces the effort needed for writing and maintenance of mapping code (although I am sure that some people will disagree). Otis also supports aggregate functions which can traverse an object graph and calculate or collect needed data. For really hairy problems, you can register a helper function to perform custom processing which is still easier than writing a whole new assembler.

Disadvantages? In my opinion, the biggest one is lack of type safety due to text expressions being used to define the mapping between types. Otis will try to help you with diagnostic messages, but of course it would be better if compiler could find the error immediately. However, short of this, the best defense is to write a single unit test which tries to build an assembler. If it fails, mappings are invalid, and hopefully, an error message will show you what is wrong.

Setup

Otis setup consists of two tasks (and coresponding two lines of code):

What is left to do is to retrieve the required assembler from the configuration object, and start assembling the objects. Here is the snippet:

    // configure the new Configuration object using metadata of types in the current assembly
    Configuration cfg = new Configuration();            // instantiate a new Configuration, one per application is needed
    cfg.AddAssembly(Assembly.GetExecutingAssembly());   // initialize it

    // retrieve the assembler and transform the source object
    IAssembler<UserDTO, UserEntity> asm = cfg.GetAssembler<UserDTO, UserEntity>();                  // retrieve the assembler
        
    UserEntity entity = ...                             // retrieve a UserEntity instance from somewhere
    UserDTO dto = asm.AssembleFrom(entity);             // do the transformation

Configuring the mappings

There is more than one way to configure mappings

This article only gives an overview of the mapping definitions. For more details, refer to MappingGuide.

Configuration via metadata

To define transformations via type metadata all you have to do is mark target types and their members with appropriate attributes. To specify that a type is a target type of transformation, mark it with [MapClass] attribute. Then, for every member which will be mapped to source type, specify [Map] attribute. In following example, UserDTO is a target type for UserEntity->UserDTO transformation.

Sample

Here is a complete sample of defining the mappings via attributes (for version 0.2), configuring the assembler and transforming the object:

// source class for conversion
class UserEntity
{
    public int Id { get ... }
    public string FirstName { get ... }
    public string LastName { get ... }
    public DateTime BirthDate { get ... }
    public IList<ProjectEntity> Projects { get ... }
    public UserEntity Boss { get ... }  
    public Gender Gender { get ... }  // Gender is an enum with values Male, Female
}

// target type for conversion
[MapClass(typeof(UserEntity))] // defines conversion from UserEntity
class UserDTO
{
    [Map]  // maps to the property of same name   
    public int Id { get ... }

    [Map("$UserName.ToLower()")]
    public string UserName { get ... }

    // expression enclosed in []: treats ' as " so you don't have to write "$FirstName + \" \" + $LastName"
    [Map("[$FirstName + ' ' + $LastName]")] 
    public string FullName { get ... }

    // Projection: map enum values to strings, e.g. Gender.Female => "Mrs."
    [Map("$Gender", Projection = "Gender.Male => Mr.; Gender.Female => Mrs.")]
    public string Title { get ... }

    [Map("$BirthDate", Format="Born on {0:D}")] // converts DateTime to string, and formats it as long date
    public string Birthday { get ... }
    
    [Map] // source is an IList<ProjectEntity>, target is array
    public ProjectDTO[] Projects { get ... }  
    
    [Map("$Projects.Count")]
    public int ProjectCount { get...} 

    // iterates over all tasks in all projects and calculates average tasks duration
    [Map("avg:$Projects/Tasks/Duration")]
    public double AvgTaskDuration { get...} 

    // iterates over all tasks in all projects and returns maximum task duration
    [Map("max:$Projects/Tasks/Duration")]
    public int MaxTaskDuration { get...} 
    
    [Map] // recursively maps the Boss property
    public UserDTO Boss { get ... } 
}

// usage
[Test]
public void Test()
{
    Configuration cfg = new Configuration();            // instantiate a new Configuration, one per application is needed
    cfg.AddType(typeof(UserDTO));                       // initialize it using type metadata, but easier is 
                                                        // cfg.AddAssembly(Assembly.GetExecutingAssembly()) to register all types at once
    IAssembler<UserDTO, UserEntity> asm                 // retrieve the assembler
        = cfg.GetAssembler<UserDTO, UserEntity>();
    UserEntity entity = ...                             // retrieve a UserEntity instance from somewhere
    UserDTO dto = asm.AssembleFrom(entity);             // do the transformation
    
    Assert.AreEqual(dto.Id,              entity.Id);
    Assert.AreEqual(dto.UserName,        entity.UserName.ToLower());
    Assert.AreEqual(dto.FullName,        entity.FirstName + " " + entity.LastName);
    Assert.AreEqual(dto.Title,           entity.Gender == Gender.Female ? "Mrs." : "Mr.");
    Assert.AreEqual(dto.Birthday,        entity.BirthDate.ToString("Born on {0:D}"));
    Assert.AreEqual(dto.Projects.Length, entity.Projects.Count);
    Assert.AreEqual(dto.ProjectCount,    entity.Projects.Count); 
      
    Assert.AreEqual(dto.Boss.Id,         entity.Boss.Id);
    Assert.AreEqual(dto.Boss.UserName,   entity.Boss.UserName.ToLower());
    Assert.AreEqual(dto.Boss.FullName,   entity.Boss.FirstName + " " + entity.LastName);
    
    // calculating max and avg task duration to test the transformation
    int max, cnt, sum;
    
    foreach(Project project in entity.Projects)
        foreach(Task task in project.Tasks)
        {
            cnt++;
            sum = sum + task.Duration; 
            max = task.Duration > max ? task.Duration : max;    
        }    
    double avg = (double)sum / cnt; 
        
    Assert.AreEqual(avg, dto.AvgTaskDuration);
    Assert.AreEqual(max, dto.MaxTaskDuration);        
}

Configuration via XML files

Configuration via XML files makes source code cleaner, and is the preferred way to declare the mappings. These configuration files can be standalone files which are deployed with the application, or built into the application as resources. If you choose this path, target class will look simply like this:

// target type for conversion
class UserDTO
{
    public int Id { get ... }
    public string UserName { get ... }
    public string FullName { get ... }
    public string Title { get ... }
    public string Birthday { get ... }
    public ProjectDTO[] Projects { get ... }  
    public int ProjectCount { get...} 
    public double AvgTaskDuration { get...} 
    public int MaxTaskDuration { get...} 
    public UserDTO Boss { get ... } 
}

The xml mapping looks like this:

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

    <class name="Otis.Sample.UserDTO, Otis.Sample" source="Otis.Sample.Domain.User, Otis.Sample" >
        <member name="Id" />  <!-- maps to the member with the same name -->
        <member name="UserName" /> 
        <member name="Boss" /> 
        <member name="FullName" expression="[$FirstName + ' ' + $LastName]" /> <!-- uses [] to avoid "$FirstName + &quot; &quot; + $LastName" -->
        <member name="Title" expression="$Gender" >
            <map from="Gender.Male" to="Mr." />     <!-- projections -->
            <map from="Gender.Female" to="Mrs." />
        </member> 
        <member name="Birthday"  expression="$BirthDate" format="Born on {0:D}"/>
        <member name="ProjectCount" expression="$Projects.Count" />
        <member name="AvgTaskDuration" expression="avg:$Projects/Tasks/Duration" />
        <member name="MaxTaskDuration" expression="max:$Projects/Tasks/Duration" />
    </class>
</otis-mapping>

Schema for mapping file is available as part of the archive which can be downloaded from this site. It can be copied into xml\schemas folder under the Visual Studio 2005 installation directory, to provide syntax checking in the VS editor. Of course, the mapping file is checked against the schema during the configuration.

If you chose to define mappings as XML resources, client code has to be slightly modified. Instead of

    cfg.AddAssembly(Assembly.GetExecutingAssembly());

which initializes the mapper using attributes on mapped types, you must call

    cfg.AddAssemblyResources(Assembly.GetExecutingAssembly(), "otis.xml"); 

This will tell mapper to read mappings from all assembly resource files which end with "otis.xml".

See Also

IAssembler<Target, Source> interface

Type mapping in detail

Otis implementation and performance

Custom mapping providers


Comment by colin.jack, Feb 25, 2008

To get the example to work I actually had to do this:

cfg.AddAssemblyResources?(Assembly.GetExecutingAssembly?(), "otis.xml");

Comment by zdeslav.vojkovic, Feb 27, 2008

That is needed when mappings are defined as XML resource files. If they are defined via attributes, cfg.AddAssembly?(Assembly.GetExecutingAssembly?()) is fine. Thanks for the info, i will update the documentation.

Comment by mkuczara, Apr 08, 2008

I cannot make this work. I keep getting - error CS0528: 'IAssembler<...>' is already listed in interface list

Comment by zdeslav.vojkovic, Apr 08, 2008

What exactly are you trying to do? XML or attribute mapping? This sounds like you defined same mapping twice (i.e. you have two definitions of mapping from class X to class Y). You can also check the sample application from the source distribution on download page.

Comment by mkuczara, Apr 08, 2008

My problem seems to slightly different. My entity class implements two interfaces (from different assembly)

Comment by zdeslav.vojkovic, Apr 08, 2008

Are you using 0.2 or 0.2.17? do you get this message during compilation or during otis configuration setup? can you send me the full error message?

Comment by mkuczara, Apr 08, 2008

Ok, i used xml configuration, but i still cannot make it work. The problem is that assembler cannot find interfaces that entity class implements. Is there any chance to solve it? Thanks for any help!

Comment by mkuczara, Apr 08, 2008

I sent you an email with more or less detailed info

Comment by mkuczara, May 26, 2008

Hello Zdeslav, I have one question. We are getting null reference exception during transformation , when source object contains null fields. is there an easy way to handle such situation. Thanks!

Details - one of String fields is null which is ok from app point of view.

Comment by zdeslav.vojkovic, May 26, 2008

hi Maciej,

As you might have seen, there is a NullValue? attribute, but due to a bug it is not used in generated mapping code. Additionally, better null value handling is one of the topics for new release. I plan to upload a patch for NullValue? bug tonight or tomorrow evening. I believe that this should solve your problem. New version is delayed due to a bunch of unexpected events, but I will try to release it in next two weeks.

Comment by mkuczara, May 26, 2008

Thanks Zdeslav, Appreciate!

Comment by Farax.Ahmed, Sep 17, 2009

Is it possible to case a collection of UserDTO to a collection of UserEntity??

Comment by zdeslav.vojkovic, Sep 18, 2009

It works for lists and array. You can do list <-> list, array <-> array and list <-> array conversions. Besides, IAssembler interface provides methods ToArray?() and ToList?() if your collection is not a member of a class but a collection instead. Please have a look at unit tests (CollectionMappingTest?.cs, last two tests in EntityMappingTest?.cs)

Comment by linush...@yahoo.com, Oct 02, 2009

It doesn't seem to support inheritance. What would I do if my source or target (or both) class inherits from a base class? Could you provide an example?

Comment by zdeslav.vojkovic, Oct 02, 2009

I am not sure I understand fully. You can just map them normally (see InheritanceMappingTests?.cs in Otis.Tests project). Can you provide more info?

Comment by linush...@yahoo.com, Oct 04, 2009

Please disregard my last question. I got the scenario wrong. Here's the question: Suppose NamedEntity? has a property that is another class,

public class NamedEntity? : Entity {

.... public AnotherClass? Anc {get; set;} ...
}

public class AnotherClass? {

public string id {get; set;}
}

So now if I want to map AnotherCompany?'s Name property to Company.Anc.id how would I do it?

Comment by zdeslav.vojkovic, Oct 05, 2009

Currently it is more complicated than it should be. Have a look at MappingNestedTargets topic in wiki. There is a proposal for simpler handling of such cases at NestedObjectMapping, but it is not implemented yet.

Comment by linush...@yahoo.com, Oct 05, 2009

I understand the concept of projections but they seem like mapping from strings or integers to enum. How would I map from one enum type to another?

Comment by zdeslav.vojkovic, Oct 05, 2009

something like this (let's say UserGender? is an enum):

<member name="Gender" expression="$UserGender">
	<map from="MyNamespace.Gender.Male" to="Male" />
	<map from="MyNamespace.Gender.Female" to="Female" />
</member>

Please make sure that you enter the full name (including namespace) in 'from' attribute. This is a bug in current version.

Comment by zdeslav.vojkovic, Oct 05, 2009

Here's a full sample:

namespace Otis.Tests
{
	public enum E1
	{
		first, 
		last
	}
	public enum E2
	{
		start,
		stop
	}

	public class CL1
	{
		public E1 e1;
	}

	[MapClass(typeof(CL1))]
	public class CL2
	{
		[Map("$e1", Projection = "Otis.Tests.E1.first => start; Otis.Tests.E1.last => stop")]
		public E2 e2;
	}

	[TestFixture]
	public class IntegrationTests
	{
		[Test]
		public void Enums()
		{
			Configuration cfg = new Configuration();
			cfg.AddType<CL2>();
			IAssembler<CL2, CL1> asm = cfg.GetAssembler<CL2, CL1>();
			CL1 cl1 = new CL1();
			cl1.e1 = E1.last;

			CL2 cl = asm.AssembleFrom(cl1);
			Assert.AreEqual(E2.stop, cl.e2);			
		}
  }
}
Comment by linush...@yahoo.com, Oct 05, 2009

Thanks for your reply. I've figured out how to do enum mapping. Here's another challenge of mine: I need to map from a DateTime?? type to a list of a custom type (DateType?)?

public partial class DateType?? : SomeBaseType? {

private List<object> itemsField;
public List<object> Items {
get {
if (itemsField == null) itemsField = new List<object>(); return this.itemsField;
} set {
this.itemsField = value;
}
}

public DateType?? (System.DateTime?? value) {
Items.Add(new date { Value = value } );
}

public DateType?? (System.DateTime??) {
Items.Add(new date { Value = ( System.DateTime?? )value } );
}

}

How would I map from my source (System.DateTime??) to my target, which is a List of DateType? (List<DateType>)?

Thanks.

Comment by zdeslav.vojkovic, Oct 06, 2009

You can't do this just with mapping. The closest you can get is by using @helper@. Search for 'helper' in wiki topics [XmlMappings?] and [MetadataMappings?]. There are also examples in unit test.


Sign in to add a comment
Hosted by Google Code