Skip to content
Piotr Szulc edited this page Dec 20, 2021 · 14 revisions

Code generation

The build time code generation based on C# 9.0 code generators approach by marking MainSourceGenerator class that responsible for mapping generation with GeneratorAttribute and impelmenting ISourceGenerator interface:

[Generator]
public class MainSourceGenerator : ISourceGenerator
{
   public void Execute(GeneratorExecutionContext context)
   {
       if (context.SyntaxReceiver is null)
       {
            throw new ArgumentNullException(nameof(context), $"{nameof(context.SyntaxReceiver)} could not be null");
       }

       if (context.SyntaxReceiver is not MappersSyntaxReceiver receiver)
           return;

       var sourcesMetadata = SourcesMetadata.Create(GetDependencyInjectionType(context.Compilation.ReferencedAssemblyNames));

       foreach (var typeDeclaration in receiver.Candidates)
       {
           var model = context.Compilation.GetSemanticModel(typeDeclaration.SyntaxTree, true);
           var mapperType = model.GetDeclaredSymbol(typeDeclaration) as ITypeSymbol;

           if (mapperType is null || !IsMapperType(mapperType))
               continue;

             sourcesMetadata.AddOrUpdate(new MapperMetadata(mapperType));
       }

        var sourceGenerator = new CodeSourceGenerator(sourcesMetadata);
        sourceGenerator.GenerateMappings(context);
        sourceGenerator.GenerateDependencyInjectionExtensions(context);
     }
}

In the Execute method we check for our defined MappersSyntaxReceiver, which responsible for syntax changes only in interfaces and abstract classes marked with MapperAttribute.

The next step in the method is Mappers Metadata initialization.

Mappers metadata

All objects that store information about mappings here called metadata. It is a facade to Roslyn API that shares only used information about mapped classes:

  1. IMetadata - base interface for all metadata types
  2. ISourcesMetadata - stores all defined mappers in your assembly
  3. IMapperMetadata - encapsulates all data for abstract class or interface that defines mappings
  4. IMethodMetadata - encapsulates mappping method from mapper
  5. IPropertyMetadata - encapsulates a property that is mapped
  6. ITypeMetadata - encapsulates information about types that are mapped

Processors

Processors are responsible for the code generation. The approaches for mappers based on interfaces and abstract classes are different that's why here used strategy pattern:

internal class ProcessorStrategyFactory
{
    private readonly static Dictionary<TypeKind, IProcessorStrategy> _mappersStrategies = new()
    {
        { TypeKind.Interface, new InterfaceProcessorStrategy()},
        { TypeKind.Class, new ClassProcessorStrategy() }
    };

    internal static IProcessorStrategy GetStrategy(IMapperMetadata mapperMetadata)
    {
        if (!_mappersStrategies.ContainsKey(mapperMetadata.TypeKind))
        {
            return new InterfaceProcessorStrategy();
        }

        return _mappersStrategies[mapperMetadata.TypeKind];
    }
}

Base AbstractProcessorStrategy shares common methods for addings and storing diagnostics, catch exceptions, formatting code etc. Each Processor strategy class overrides its GenerateMapperCode that returns preprared but unformatted code ready to be used.

internal abstract class AbstractProcessorStrategy : IProcessorStrategy
{
    private readonly List<DiagnosticsInfo> _diagnostics = new();

    public IResult GenerateCode(IMapperMetadata mapperMetadata) 
    {
        try
        {
            string code = GenerateMapperCode(mapperMetadata);
            return Result.Ok(code, _diagnostics);
        }
        catch (Exception ex)
        {
            return Result.Error(ex);              
        }            
    }

    protected abstract string GenerateMapperCode(IMapperMetadata mapperMetadata);

    protected void PropertyMappingWarning(IPropertyMetadata metadata)
    {
        _diagnostics.Add(new DiagnosticsInfo
        {
            DiagnosticDescriptor = SourceMapperDescriptors.PropertyIsNotMapped,
            Metadata = metadata
        });
    }
}

Dependency injection

The source code generator searches for 3 main dependency container packages (Microsoft.Extensions.DependencyInjection, Autofac.Extensions.DependencyInjection, and StructureMap.Microsoft.DependencyInjection) and generates extension code for one of them. For projects with only mapper classes/interfaces (marked as MapperAttribute, in domain data projects or models contained projects), Compentio.SourceMapper generate mapping class with maps between defined objects. In projects with dependency injection, with reference to projects with mapper class/interface, the MappersDependencyInjectionExtensions class is created, with AddMappers() method based on mappers in referenced projects, which should be used in container definition section. If there no any container packages found, Dependency Injection extension class is not generated. In the simple case, the mapper class/interface, mappings result class and dependency injection extension can be also created in range of one project.

In case of using Microsoft.Extensions.DependencyInjection, AddMappers() method with all generated mappers injections is defined as:

public static IServiceCollection AddMappers(this IServiceCollection services)

and based on IServiceCollection, can be simply added in service configuration section:

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

or in case of using Startup.cs:

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{

	services.AddControllers();
	services.AddMappers();
}

For integration with Autofac due to Autofac.Extensions.DependencyInjection package, AddMappers() method depend on ContainerBuilder and is defined as:

public static ContainerBuilder AddMappers(this ContainerBuilder builder)

Autofac container configuration can be separated in simple module file (in our example AutofacModule), where we place registrations directly with Autofac:

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

That module need to be register in container configure section of Startup.cs file:

/// Registration directly with Autofac. This runs after ConfigureServices so the things
/// here will override registrations made in ConfigureServices
public void ConfigureContainer(ContainerBuilder builder)
{
	builder.RegisterModule(new AutofacModule());
}

To run Autofac mechanism based on modules, we need to call dedicated AutofacServiceProviderFactory() with attached Startup.cs file by using:

Host.CreateDefaultBuilder(args)
	.UseServiceProviderFactory(new AutofacServiceProviderFactory())
	.ConfigureWebHostDefaults(webBuilder =>
	{
		webBuilder.UseStartup<Startup>();
	});

For StructureMap with StructureMap.Microsoft.DependencyInjection package, AddMappers() based on solution used StructureMap.Container and is defined as:

public static ConfigurationExpression AddMappers(this ConfigurationExpression builder)

In example project StructureMap container is builded by running StructureMapContainerBuilderFactory() from program host

Host.CreateDefaultBuilder(args)
	.UseServiceProviderFactory(new StructureMapContainerBuilderFactory())
	.ConfigureWebHostDefaults(webBuilder =>
	{
		webBuilder.UseStartup<Startup>();
	});

where provider is a service provider factory class constructed for StructureMap mechanism (working with Container, not with Registry):

public class StructureMapContainerBuilderFactory : IServiceProviderFactory<Container>
{
	private IServiceCollection _services;

	public Container CreateBuilder(IServiceCollection services)
	{
		_services = services;
		return new Container();
	}

	public IServiceProvider CreateServiceProvider(Container builder)
	{
		builder.Configure(config =>
		{
			config.Populate(_services);
		});

		return builder.GetInstance<IServiceProvider>();
	}
}

This allow to use AddMappers() with generated mappings in Startup.cs configuration file:

/// Registration directly with StructureMap
public void ConfigureContainer(Container builder)
{
	builder.Configure(config =>
	{
		config.AddMappers();
	});
}

Inverse Mapping Mechanism

Inverse mapping mechanism automate create mapping back methods, due to using InverseMapping attribute in mapper class/interface. Due to using dependency injections, SourceMapper generate additional part of mapping class (or interface), so the source mapping class (interface) must be declared as partial. In other way, after extra code generating, Visual Studio show us error communicate about class/interface duplication. Because of complexity, inverse mapping works only for simple mapping methods, without expressions etc...

Interface Inverse Mapping

Inverse mapping is provided by using inverse mapping attributes, where the name of the inverse method is required:

[Mapper]
public partial interface IBooksMapper
{
	[InverseMapping(InverseMethodName = "MapBookToDao")]
	BookDto MapBookToDto(BookDao source);
}

that lead to two methods implementations in mapper result class:

public class BooksMapper : IBooksMapper
{
        public static BooksMapper Create() => new();
        public virtual Compentio.Example.Autofac.App.Entities.BookDto MapBookToDto(Compentio.Example.Autofac.App.Entities.BookDao source)
        {
			...
        }

        public virtual Compentio.Example.Autofac.App.Entities.BookDao MapBookToDao(Compentio.Example.Autofac.App.Entities.BookDto source)
        {
			...
        }
}

If mapped object contain collections or other complex fields/expressions, mappings cant be created by automate. In this case we can create class that inherit mapper result class and override complex objects, for example:

public class CustomBooksMapper : BooksMapper
{
	public override BookDao MapBookToDao(BookDto source)
	{
	    var result = base.MapBookToDao(source);
	    result.LibraryAddressesDao = source.LibraryAddressesDto.Select(a => MapAddressToDao(a)).ToList();
	    return result;
	}
	...
}

and register it in dependency injection section

public class AutofacModule : Module
{
	protected override void Load(ContainerBuilder builder)
	{
	    ...
	    builder.AddMappers();
	    // Override mapper class by custom implementation
	    builder.RegisterType<CustomBooksMapper>().As<IBooksMapper>().SingleInstance();
	}
}

Class Inverse Mapping

Case of class inverse mapping is mainly similar to interfaces inverse mechanism. First of all, we need to mark class as partial and add inverse attribute with the name for inverse method. If mapping between classes need expresions, they both - primary and invers expression methods - should be implemented by developer. Inverse mechanism is not able to generate it by automate:

[Mapper(ClassName = "ClassInvoiceMapper")]
public abstract partial class InvoiceMapper
{
	...
	[InverseMapping(InverseMethodName = "MapInvoiceToDao")]
	public abstract InvoiceDto MapInvoiceToDto(InvoiceDao source);
	...
	[InverseMapping(InverseMethodName = "MapInvoiceItemToDao")]
        public abstract InvoiceItemDto MapInvoiceItemToDto(InvoiceItemDao source);
	
	protected IEnumerable<InvoiceItemDto> ConvertToItemsDto(IEnumerable<InvoiceItemDao> items)
	{
	    return items.Select(i => MapInvoiceItemToDto(i)).AsEnumerable();
	}
}

That prepared mapping class lead to obtained proper result mapping class:

public class ClassInvoiceMapper : InvoiceMapper
{
	public override Compentio.Example.StructureMap.App.Entities.InvoiceDto MapInvoiceToDto(Compentio.Example.StructureMap.App.Entities.InvoiceDao source)
	{
	    ...
	    target.Items = ConvertToItemsDto(source.Items);
	}

	public override Compentio.Example.StructureMap.App.Entities.InvoiceDao MapInvoiceToDao(Compentio.Example.StructureMap.App.Entities.InvoiceDto source)
	{
	    ...
	}

	public override Compentio.Example.StructureMap.App.Entities.InvoiceItemDto MapInvoiceItemToDto(Compentio.Example.StructureMap.App.Entities.InvoiceItemDao source)
	{
		...
	}

	public override Compentio.Example.StructureMap.App.Entities.InvoiceItemDao MapInvoiceItemToDao(Compentio.Example.StructureMap.App.Entities.InvoiceItemDto source)
	{
		...
	}
}

public abstract partial class InvoiceMapper
{
	public abstract Compentio.Example.StructureMap.App.Entities.InvoiceDao MapInvoiceToDao(Compentio.Example.StructureMap.App.Entities.InvoiceDto source);
	public abstract Compentio.Example.StructureMap.App.Entities.InvoiceItemDao MapInvoiceItemToDao(Compentio.Example.StructureMap.App.Entities.InvoiceItemDto source);
}

Now ClassInvoiceMapper can be inherited by, for example, CustomClassInvoiceMapper and complex methods can be overrided, similarly to interface case.

Diagnostics

The main and most visible mapping information is message about not mapped property in a target class. The PropertyMappingWarning method adds such info to the diagnostics dictionary during code generation and after mappers succesfully generated diagnostics warning or errors printed to the Visual Studio Output and Error List.

NuGet Nuget GitHub GitHub top language

Clone this wiki locally