Mapperly is a newer object mapper library that leverages source generation to make it lightning-fast. To get started, we must add a dependency on the Riok.Mapperly NuGet package.
Source generators were introduced with .NET 5, allowing developers to generate C# code during compilation.
There are many ways to create object mappers with Mapperly and many options to adjust the mapping process. The following code sample is similar to the others but using Mapperly. We cover the following ways to use Mapperly:
- Injecting a mapper class.
- Using a static method.
- Using an extension method.
Let’s start with the injected mapper. First, the class must be partial for the source generator to extend it (that is how source generators work). Decorate the class with the [Mapper] attribute (highlighted). Then, in that partial class, we must create one or more partial methods that have the signature of the mappers we want to create (like the MapToProductDetails method), like this:
[Mapper]
public partial class ProductMapper
{
public partial ProductDetails MapToProductDetails(Product product);
}
Upon compilation, the code generator creates the following class (I formatted the code to make it easier to read):
public partial class ProductMapper
{
public partial ProductDetails MapToProductDetails(Product product)
{
var target = new ProductDetails(
product.Id ??
throw new ArgumentNullException(nameof(product.Id)),
product.Name,
product.QuantityInStock
);
return target;
}
}
Mapperly writes the boilerplate code for us in a generated partial class, which is why it is so fast.To use the mapper, we must register it with the IoC Container and inject it into our endpoint. Let’s make it a singleton once again:
builder.Services.AddSingleton<ProductMapper>();
Then, we can inject and use it like this:
app.MapGet(“/products”, async (
IProductRepository productRepository,
ProductMapper mapper,
CancellationToken cancellationToken) =>
{
var products = await productRepository.AllAsync(cancellationToken);
return products.Select(p => mapper.MapToProductDetails(p));
});
The highlighted code in the preceding block shows we can use our mapper like any other class. The biggest drawback is that we may end up injecting many mappers into a single class or endpoint if we do not consider how we create them wisely.Moreover, we must register all of our mappers with the IoC container, which creates a lot of boilerplate code but makes the process explicit. On the other hand, we could scan the assembly for all classes decorated with the [Mapper] attribute.If you want an abstraction layer like an interface for your mapper, you must design that yourself because Mapperly only generates the mappers. Here is an example:
public interface IMapper
{
NotEnoughStock MapToDto(NotEnoughStockException source);
ProductNotFound MapToDto(ProductNotFoundException source);
ProductDetails MapToProductDetails(Product product);
}
[Mapper]
public partial class Mapper : IMapper
{
public partial NotEnoughStock MapToDto(NotEnoughStockException source);
public partial ProductNotFound MapToDto(ProductNotFoundException source);
public partial ProductDetails MapToProductDetails(Product product);
}
The preceding code centralizes all the mapper methods under the same class and interface, allowing you to inject an interface similar to AutoMapper. In subsequent chapters, we explore ways to organize mappers and app code that does not involve creating a central mapper class.
To inspect the generated code, you can add the EmitCompilerGeneratedFiles property in a PropertyGroup tag inside your project file and set its value to true like this:
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>
Then the generated C# files will be available under the obj\Debug\net8.0\generated directory. Change the net8.0 subdirectory to the SDK version and Debug by your configuration.
Next, we explore how to make a static mapper, which follows a very similar process, but we must make both the class and the method static like this:
[Mapper]
public static partial class ExceptionMapper
{
public static partial ProductNotFound Map(ProductNotFoundException exception);
}
Mapperly takes the preceding code and generates the following (formatted for improved readability):
public static partial class ExceptionMapper
{
public static partial ProductNotFound Map(ProductNotFoundException exception)
{
var target = new ProductNotFound(
exception.ProductId,
exception.Message
);
return target;
}
}
Once again, the code generator writes the boilerplate code. The difference is that we don’t have to inject any dependency since it is a static method. We can use it this way (I only included the catch block, the rest of the code is unchanged):
catch (ProductNotFoundException ex)
{
return Results.NotFound(ExceptionMapper.Map(ex));
}
Leave a Reply