SkillAgentSearch skills...

SourceMapper

Mappings code generator based on attributes

Install / Use

/learn @alekshura/SourceMapper
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

<img src="/Compentio.Assets/Logo.png" align="left" width="50"> SourceMapper

NuGet Sonar Quality Gate Test Build Nuget GitHub GitHub top language

Introduction

SourceMapper is a code generator that uses attributes placed in interfaces or abstract classes: during build time it generates mapping classes and methods for mappings based on "rules" defined in these attributes.

It is based on Source Generators feature that has been intoduced with C# 9.0 and brings a possibility to generate code during build time.

:point_right: After configuring you mappers you can see, control and override the generated code for the mappings. :point_left:

Installation

Install using nuget package manager:

Install-Package Compentio.SourceMapper

or .NET CLI:

dotnet add package Compentio.SourceMapper

How to use

To define mapping we have to mark mapping abstract class or interface with MapperAttribute:

[Mapper]
public interface INotesMapper
{
    NoteDto MapToDto(NoteDao source);
}

This will generate mapping class with default class name NotesMapper for properties that names are the same for NoteDto and NoteDao classes. The generated class is in the same namespace as its base abstract class of interface. It can be found in project in Visual Studio:

Dependencies -> Analyzers -> Compentio.SourceMapper.Generators.MainSourceGenerator.

When the names are different than we can use Source and Target names of the properties:

[Mapper(ClassName = "InterfaceUserMapper")]
public interface IUserMapper
{
    [Mapping(Source = nameof(UserDao.FirstName), Target = nameof(UserInfo.Name))]
    UserInfo MapToDomainMoodel(UserDao userDao);       
}

The ClassName property in MapperAttribute is responsible for name of the generated mapping class. For default MapperAttribute interface prefix I is removed or Impl suffix added to the generated class name if there is no I prefix in the mapping interface name.

Interface mapping

Use interfaces to prepare basic mapping. In a case when mapped object contains another objects, e.g.:

public class NoteDto
{
    public long Id { get; set; }
    public string Title { get; set; }
    public string Description { get; set; }
    public NoteDocumentDto Document { get; set; }
}

and

public class NoteDao
{
    public long Id { get; set; }
    public string PageTitle { get; set; }
    public string Description { get; set; }
    public NoteDocumentDao Document { get; set; }
}

it is enough to add mapping method to the interface for these types and the code generation processor will match and generate mappings for NoteDto MapToDto(NoteDao source) method:

[Mapper]
public interface INotesMapper
{
    [Mapping(Source = nameof(NoteDao.PageTitle), Target = nameof(NoteDto.Title))]
    NoteDto MapToDto(NoteDao source);

    [Mapping(Source = nameof(NoteDocumentDao.Metadata.CreatorFirstName), Target = nameof(NoteDocumentDto.Autor))]
    NoteDocumentDto MapToDto(NoteDocumentDao source);
}

the output will be:

public class NotesMapper : INotesMapper
{
    public static NotesMapper Create() => new();
    public virtual Compentio.Example.App.Entities.NoteDto MapToDto(Compentio.Example.App.Entities.NoteDao source)
    {
        if (source == null)
            return source;
        var target = new Compentio.Example.App.Entities.NoteDto();
        target.Id = source.Id;
        target.Title = source.PageTitle;
        target.Description = source.Description;
        target.Document = MapDocumentToDto(source.Document);
        return target;
    }

    public virtual Compentio.Example.App.Entities.NoteDocumentDto MapDocumentToDto(Compentio.Example.App.Entities.NoteDocumentDao source)
    {
        if (source == null)
            return source;
        var target = new Compentio.Example.App.Entities.NoteDocumentDto();
        target.Id = source.Id;
        target.Title = source.Title;
        return target;
    }
}

All methds are marked as virtual, so there is a possibility to override them in own mappers code.

Class mapping

For more complicated mapings use abstract class to define mappers. The main difference between abstract class mapper and interface, that Expression property can be used in MappingAttribute:

[Mapper(ClassName = "NotesMappings")]
public abstract class NotesClassMapper
{
    [Mapping(Target = nameof(NoteDocumentDto.Autor), Expression = nameof(ConvertAuthor))]
    public abstract NoteDocumentDto MapToDto(NoteDocumentDao source);

    protected readonly Func<NoteDocumentDao, string> ConvertAuthor = s => s.Metadata.CreatorFirstName + s.Metadata.CreatorLastName;
}

Expression - it is a name of mapping function, that can be used for additional properties mapping.

It must be public or protected, since it is used in generated mapper class that implements abstract mapping class.

Ignore mapping

If for any reason part of the class/interface properties should not be mapped, IgnoreMapping attribute should be used for that. Added IgnoreMapping causes that both source and target property during mapping generation will be omitted, not generating any linked map and not reporting any warning in diagnostics. If we have two classes NoteDao and NoteDto

public class NoteDao
{
	public long Id { get; set; }
	public string PageTitle { get; set; }
	public string Description { get; set; }

	[IgnoreMapping]
	public DateTime ValidFrom { get; set; }

	[IgnoreMapping]
	public DateTime ValidTo { get; set; }

	[IgnoreMapping]
	public string CreatedBy { get; set; }

	[IgnoreMapping]
	public DateTime Created { get; set; }

	[IgnoreMapping]
	public DateTime Modified { get; set; }
}
public class NoteDto
{
	public long Id { get; set; }
	public string Title { get; set; }
	public string Description { get; set; }
}

and we need to map only matched fields, the mapper class NotesClassMapper lead to creating proper mapping result class NotesMappings without any warning:

[Mapper(ClassName = "NotesMappings")]
public abstract partial class NotesClassMapper
{
	[Mapping(Source = nameof(NoteDao.PageTitle), Target = nameof(NoteDto.Title))]
	public abstract NoteDto MapToDto(NoteDao source);
}
public class NotesMappings : NotesClassMapper
{
	public override Compentio.Example.DotNetCore.App.Entities.NoteDto MapToDto(Compentio.Example.DotNetCore.App.Entities.NoteDao source)
	{
		if (source == null)
			return null;
		var target = new Compentio.Example.DotNetCore.App.Entities.NoteDto();
		target.Id = source.Id;
		target.Title = source.PageTitle;
		target.Description = source.Description;
		return target;
	}
}

Mapping collections

Lets assume we need to map two entities:

public class UserDao
{
    public long UserId { get; set; }
    public AddressDao[] UserAddresses { get; set; }
}

to

public class UserInfo
{
    public int Id { get; set; }
    public Address[] Addresses { get; set; }
}

It can be achieved using abstract class mapper:

[Mapper(ClassName = "UserDataMapper")]
public abstract class UserMapper
{
    [Mapping(Source = nameof(UserDao.UserAddresses), Target = nameof(UserInfo.Addresses), Expression = nameof(ConvertAddresses))]
    [Mapping(Source = nameof(UserDao.UserId), Target = nameof(UserInfo.Id), Expression = nameof(ConvertUserId))]
    public abstract UserInfoWithArray MapToDomainModel(UserWithArrayDao userWithArrayDao);

    protected Address[] ConvertAddresses(AddressDao[] addresses)
    {
        return addresses.Select(a => MapAddress(a)).ToArray();
    }
    
    protected static int ConvertUserId(long id)
    {
        return Convert.ToInt32(id);
    }

    public abstract Address MapAddress(AddressDao addressDao);
}

For more examples see Wiki examples.

Dependency injection

The Compentio.SourceMapper searches for 3 main dependency container packages (Microsoft.Extensions.DependencyInjection, Autofac.Extensions.DependencyInjection, and StructureMap.Microsoft.DependencyInjection) and generates extension code. If there no any container packages found, Dependency Injection extension class is not generated.

To simplify adding dependency injection for mappers MappersDependencyInjectionExtensions class is generated, that can be used by adding AddMappers() that adds all mappers defined in the project.

For Microsoft.Extensions.DependencyInjection, in service configuration:

 Host.CreateDefaultBuilder(args)
                .ConfigureServices((_, services) =>
                    services
                    //.here you services
                    //
                    .AddMappers());

In case of Autofac.Extensions.DependencyInjection, container configuration can be separated in module file AutofacModules, where we place registrations directly with Autofac:

public class AutofacModule : Module
{
	protected override void Load(ContainerBuilder builder)
	{
		// other services
		builder.AddMappers();
	}
}

T

View on GitHub
GitHub Stars8
CategoryDevelopment
Updated1y ago
Forks0

Languages

C#

Security Score

75/100

Audited on Sep 10, 2024

No findings