|
GettingStarted
How to get started with Otis library
IntroductionThis article describes how to get going with Otis. It shows how to configure mappings between types and how to perform the transformations Basic ideaWhen 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 & consLike 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. SetupOtis 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 transformationConfiguring the mappingsThere is more than one way to configure mappings
Configuration via metadataTo 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. SampleHere 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 filesConfiguration 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 + " " + $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 AlsoIAssembler<Target, Source> interface Otis implementation and performance Custom mapping providers |
Sign in to add a comment
To get the example to work I actually had to do this:
cfg.AddAssemblyResources?(Assembly.GetExecutingAssembly?(), "otis.xml");
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.
I cannot make this work. I keep getting - error CS0528: 'IAssembler<...>' is already listed in interface list
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.
My problem seems to slightly different. My entity class implements two interfaces (from different assembly)
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?
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!
I sent you an email with more or less detailed info
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.
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.
Thanks Zdeslav, Appreciate!
Is it possible to case a collection of UserDTO to a collection of UserEntity??
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)
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?
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?
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 class AnotherClass? {
}So now if I want to map AnotherCompany?'s Name property to Company.Anc.id how would I do it?
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.
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?
something like this (let's say UserGender? is an enum):
Please make sure that you enter the full name (including namespace) in 'from' attribute. This is a bug in current version.
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); } } }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? {
}
How would I map from my source (System.DateTime??) to my target, which is a List of DateType? (List<DateType>)?
Thanks.
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.