aromic1 / Mapper

0 stars 1 forks source link

Mapper

About Mapper

The Mapper Library was created to simplify the often repetitive and error-prone task of mapping data between different object types. Whether you're working on a small project or a large-scale application, the Mapper Library streamlines the process of data transformation, allowing you to focus more on your application's core logic and less on manual object mapping.

The Mapper Library provides a class named Mapper which facilitates the mapping of objects from one type to another. It employs reflection and dynamic type generation to handle various scenarios efficiently, such as simple property mapping, handling complex objects, custom mapping configurations, and more.

This is a tool designed to be used for mapping different type objects in C#. This tool can help you map property values from one object to another. This tool can also be used to create a new object of the set destination type with the property values from source being mapped to the newly created object. It can also be used to create a copy of an object with the same type. It can be used in 2 different ways:

It contains 3 map method overloads:

  1. void Map<TSource, TDestination>(TSource source, TDestination destination) Takes both source and destination. Maps the values from source properties to destination properties for all the properties that are common between the source and destination type. Destination type properties that the source type does not contain will be ignored and their value will not change.
  2. TDestination Map<TSource, TDestination>(TSource source) Takes only source, but the destination type must be explicitly defined. Returns a new object of a destination class or a class that implements the destination Interface. Sets all the common properties with source type to the property value from source object. Properties that the source type does not contain will be set the property type default value.
  3. IEnumerable<TDestination> Map<TSource, TDestination>(IEnumerable<TSource> source)Takes only source, but the destination type must be explicitly defined, both destination type and source type must be types that are assignable from IEnumerable (e.g. List, Array). Returns IEnumerable of destination type objects. Maps the values from source properties to destination properties for all the properties that are common between the source and destination type. Destination type properties that the source type does not contain will be ignored and their value will not change.

Configurationless mapping

Simply create an instance of the Mapper class using its default constructor. Afterward, utilize the mapper's map functions to perform object mapping:

var mapper = new Mapper();
ClassA objectA = new ClassA() { Name = "Name" };
ClassB objectB = new ClassB() { Name = "NoName"};
mapper.Map(objectA, objectB);

When employing this approach, it's important to understand that the Mapper performs mapping "by default." It accomplishes this by identifying and matching properties with the same names between the two types and transferring values from the source object's properties to the destination object's properties. In the given example, objectB's Name property will be updated to "Name" instead of retaining its original value, "NoName."

If the destination's property value is null, you can expect the outcome depending on your property's type

Types mentioned above are currently the types this mapper can work with.

Lets take a look at these classes.

public record AuthorRest (string FirstName, string LastName) { }

public class DrawingRest
{
    public ShapeRest MainShape { get; set; }

    public AuthorRest Author { get; set; }

    public IEnumerable<LineRest> Lines { get; set; }

    public Guid Id { get; set; }
}

public class LineRest
{
    public int Start { get; set; }

    public int End { get; set; }

    public string Name { get; set; }
}

public class ShapeRest
{
    public bool IsGeometryShape { get; set; }

    public Person Person { get; set; }
}

public class Line : ILine
{
    public int Start { get; set; }

    public int End { get; set; }

    public string Name { get; set; }
}
public interface ILine
{
    public int Start { get; set; }

    public int End { get; set; }

    public string Name { get; set; }
}
public interface IDrawingToInherit
{
    public IShape MainShape { get; set; }
}

public class Drawing : IDrawing
{
    public Guid Id { get; set; }

    public IShape MainShape { get; set; }

    public Author Author { get; set; }

    public IEnumerable<ILine> Lines { get; set; }
}

public record Author(string FirstName, string LastName) { }

public interface IDrawing : IDrawingToInherit
{
    public Author Author { get; set; }

    public Guid Id { get; set; }

    public new IShape MainShape { get; set; }

    public IEnumerable<ILine> Lines { get; set; }
}

Now, let's create an object of type DrawingRest and set its properties. Additionally, we'll create another object of type Drawing by invoking its empty constructor. Afterward, we'll call the map function, followed by running NUnit tests to verify if our destinationDrawing has successfully inherited all the properties from the drawingSource.

var drawingSource = new DrawingRest
{ 
    MainShape = new ShapeRest 
    { 
        IsGeometryShape = true 
    }, 
    Lines = new[] 
    { 
        new LineRest
    { 
        Start = 0, End = 1, Name = "Line1" }
    }, 
    Id = Guid.NewGuid(),
    Author = new AuthorRest("Pablo", "Picasso")
};
var drawingDestination = mapper.Map<DrawingRest, Drawing>(drawingSource);
Assert.That(drawingSource.MainShape.IsGeometryShape, Is.EqualTo(drawingDestination.MainShape.IsGeometryShape));
Assert.That(drawingSource.Lines.First().Start, Is.EqualTo(drawingDestination.Lines.First().Start));
Assert.That(drawingSource.Lines.First().End, Is.EqualTo(drawingDestination.Lines.First().End));
Assert.That(drawingSource.Lines.First().Name, Is.EqualTo(drawingDestination.Lines.First().Name));
Assert.That(drawingSource.Author.FirstName, Is.EqualTo(drawingDestination.Author.FirstName));
Assert.That(drawingSource.Author.LastName, Is.EqualTo(drawingDestination.Author.LastName));
Assert.That(drawingSource.Id, Is.EqualTo(drawingDestination.Id));
Cyclic Data Structures

In the context of the mapping process, cyclic data structures refer to scenarios where an object's properties create a loop, ultimately leading back to the original object. This intricate structure can cause issues, particularly stack overflow exceptions, when trying to map these objects due to the recursive nature of the mapping process. To mitigate this concern, a safeguard mechanism has been implemented. Mapper keeps track of references to previously mapped objects during a single mapping process. So, if the same source object reappears, the reference of the destination object to which the source one needs to be mapped will be set to the reference of the destination object that Mapper has already handled. This prevents redundant mapping and resolves the issue of cyclic dependencies in the mapping process. This kind of logic also serves to follow the reference linking pattern from the source and transfer the same pattern to the destination

Max Depth Setting

Additionally, there is a "max depth" setting, set to a default value of 50, which serves to limit the mapping of objects beyond 50 levels of nested properties.

Consider a source object with multiple levels of properties:

sourceObject = {
    sourceProeprty1 = {
        sourceProperty2 = {
            sourceProperty3 = {
                sourceProperty4 = {
                    sourceProperty5 ={
                        ...
                    }
                }
            }
        }
    } 
}

In this scenario, the source properties are nested within each other up to five levels. When mapping this source object to a destination, the default max depth setting of 50 comes into play. Lets say it is actually set to 5 instead of 50. The mapper will traverse through the properties and map them to the destination object until it reaches the fifth level, i.e., sourceProperty5. At this point, the mapper will halt its mapping process to avoid exceeding the defined depth limit. As a result, the properties nested beyond this level will not be mapped further.

However, there might be cases where you need the mapper to delve deeper into the structure than the default depth. This could arise if you have a specific use case that demands a deeper mapping hierarchy. To handle these situations, you can change the default maximum depth setting in your configuration. By setting a custom max depth value, you can instruct the mapper to traverse more layers of nested properties during the mapping process, accommodating your unique requirements.

Configuration setup

To utilize this functionality this is what you need to do. There are 2 options to begin with. 1) Create instance of Configuration and then call the CreateMap<TSource,TDestination>() that creates a mapping configuration between your TSource and TDestination types. After that when you create an instance of the Mapper, pass the configuration to it's constructor.

    var configuration = new Configuration.Configuration();
    configuration.CreateMap<ClassA, ClassB>();
    var mapper = new Mapper(configuration);

2) If you wish for your configuration to be set somewhere other then the function you are actually doing the mapping, you can create your own Configuration class that inherits Configuration and then define your mapping configurations in that class.

        public class TestConfiguration : Configuration.Configuration
        {
            public TestConfiguration()
            {
                CreateMap<ClassA, ClassB>();
                CreateMap<ShapeRest, IShape>();
            }
        }
And then when you create an instance of mapper you can pass an instance of your own Configuration class to it.
```
var configuration = new TestConfiguration();
configuration.CreateMap<ClassA, ClassB>();
var mapper = new Mapper(configuration);  
```

You can either use aproach 1 or aproach 2, but in both cases, there are 5 functions you can call on your created mappingConfigurations.

Here is an example of how you can use this functions.

public class TestConfiguration : Configuration.Configuration
{
    public TestConfiguration()
    {
        CreateMap<DrawingRest, IDrawing>().IgnoreMany(new[] { "Id", "Author" })
            .DefineAfterMap((source,destination) =>
            {
                if(destination.MainShape?.IsGeometryShape == true)
                {
                    destination.Author = new Author("FirstName", "LastName");
                }
            }).SetMaxDepth(7);
        CreateMap<LineRest, ILine>().Ignore("Start").SetMaxDepth(3)
            .DefineBeforeMap((source,destination) => 
            {
                if(source.Start > source.End)
                {
                    source.Start = 0;    
                }
            }
            });
    }
}

Things you should know about when using Mapper

Movitation and Automapper comparison

This project is mostly about reflection, I wanted to gain a deeper understanding and knowledge in the field. I chose to explore reflection because I wanted to dig into how it works and what it can do. The tool I developed in this process was inspired by AutoMapper and is designed to meet similar needs.

This tool is particularly useful in web development and projects that use an "onion-layer" architecture. It helps smoothly connect different parts of a software system, making it easier to move data between entity, domain, and REST models. It's like a handy tool for mapping data in your software system. The main difference between this tool and AutoMapper is that AutoMapper uses Expressions which are a combination of operands (variables, literals, method calls) and operators that can be evaluated to a single value. With AutoMapper you have the advantage to set a more complex configuration between the types then with this Mapper, but it also has flaws, especially when no mapping configuration is defined, sometimes it works very randomly. When you do not have a configuration set, sometimes it throws an exception AutoMapperConfigException

, but sometimes it works even without setting the configuration.

This tool works more intuitively than AutoMapper by default. The goal of this project was to make a mapper that can map the objects in the simplest way possible, you just need to define your source and it's type along with destination and destination type and the mapper will do the mapping for you. It iterates trought the destination properties and sets the values from the source to the properties that are common following the rules mentioned in the previous paragraph. You don't need to set any configuration at all if you don't need it for a specific reason. On the other hand, if you do need it, there are a few ways in which you can alter the mapper behavior, all mentioned in the "Configuration setup" paragraph.

Also, when I tried to do this with using AutoMapper, drawingRest being a class DrawingRest object

var configAutoMapper = new AutoMapper.MapperConfiguration(cfg =>
{
    cfg.CreateMap<DrawingRest, IDrawing>();
});
var mapper2 = new AutoMapper.Mapper(configAutoMapper);

mapper2.Map<IDrawing>(drawingRest);

AutoMapperIDrawingException

I got this exception, but when I used this mapper to do the same thing it did not fail.

When I try to do this using AutoMapper

public class DrawingRest
{
    public DrawingRest(DrawingRest? parent = null)
    {
        Parent = parent ?? this;
    }

    public DrawingRest Parent { get; set; }
}

public class Drawing
{
    public Drawing(Drawing? parent = null)
    {
        Parent = parent ?? this;
    }

    public Drawing Parent { get; set; }
}

[Test]
public void AutoMapperStackOverflowExample()
{
    var configAutoMapper = new AutoMapper.MapperConfiguration(cfg =>
    {
        cfg.CreateMap<DrawingRest, Drawing>();
    });
    var mapper2 = new AutoMapper.Mapper(configAutoMapper);

    var drawingRest = new DrawingRest();
    var newDrawing = mapper2.Map<Drawing>(drawingRest);
}

I get the following Stack overflow exception "The active test run was aborted. Reason: Test host process crashed : Stack overflow. at System.Collections.Concurrent.ConcurrentDictionary`2[[AutoMapper.Internal.MapRequest, AutoMapper, Version=12.0.0.0, Culture=neutral, PublicKeyToken=be96cd2c38ef1005]...". Something like that would not occur when using this Mapper, the reason why is explained in the Cyclic Data Structures paragraph.

I'm not trying to say that this tool is better than AutoMapper overall, but there are certainly cases where it works better, like these exemples above.