You can map derived or nested objects (even recursively) by reusing existing maps to keep your code DRY.

Nested maps

For this you can use the IMapper (or IAsyncMapper) instance you will find in the mapping context.

In case of async maps you should also pass down the provided CancellationToken to allow interrupting async operations if needed.

public class MyMaps :
	INewMap<Product, ProductDto>,
	IAsyncMergeMap<Category, CategoryDto>
{
	ProductDto? INewMap<Product, ProductDto>.Map(Product? source, MappingContext context){
		if(source == null)
			return null;
		else{
			return new ProductDto{
				Code = source.Code,
				// Use the nested map for Category
				Category = context.Mapper.Map<Category, CategoryDto>(source.Category),
				...
			};
		}
	}

	async Task<CategoryDto?> IMergeMap<Category, CategoryDto>.MapAsync(Category? source, CategoryDto? destination, AsyncMappingContext context){
		if(source != null){
			destination ??= new CategoryDto();
			destination.Id = source.Id;
			// Use the async nested map to map the parent category (remember to pass down
			// the CancellationToken)
			destination.Parent = await context.Mapper.MapAsync<Category, CategoryDto>(source.Parent, destination.Parent, context.CancellationToken);
			...
		}
		return destination;
	}
}

Hierarchy maps

You could even map hierarchies by reusing existing maps.

// LimitedProduct derives from Product
public class MyMaps :
	IMergeMap<Product, ProductDto>,
	IMergeMap<LimitedProduct, LimitedProductDto>
{
	ProductDto? IMergeMap<Product, ProductDto>.Map(Product? source, ProductDto? destination, MappingContext context){
		if(source != null){
			destination ??= new ProductDto();
			destination.Code = source.Code;
			...
		}
		return destination;
	}

	LimitedProductDto? IMergeMap<LimitedProduct, LimitedProductDto>.Map(LimitedProduct? source, LimitedProductDto? destination, MappingContext context){
		// Needed to prevent constructing ProductDto in parent map
		destination ??= new LimitedProductDto();
		
		// Map parents (returned destination may be null, depending on the mapping)
		destination = context.Mapper.Map<Product, ProductDto>(source, destination) as LimitedProductDto;
		
		// Map child properties
		if(source != null){
			destination ??= new LimitedProductDto();
			destination.LimitedProp = source.LimitedProp;
			...
		}
		return destination;
	}
}

Nested projections

When creating projection expression you can reuse existing maps by using the NestedProjector instance you will find in the projection context.

When the final map will be created the nested projector map will be replaced with the actual nested expression inline with the arguments replaced.

public class MyMaps :
	IProjectionMap<Category, CategoryDto>,
	IProjectionMap<Product, ProductDto>
{

	Expression<Func<Category, CategoryDto>> IProjectionMap<Category, CategoryDto>.Project(ProjectionContext context){
		return source => new CategoryDto{
			Id = source.Id,
			...
		};
	}

	Expression<Func<Product, ProductDto> IProjectionMap<Product, ProductDto>.Project(ProjectionContext context){
		return source => new ProductDto{
			Code = source.Code,
			Category = source.Category != null ? context.Projector.Project<Category, CategoryDto>(source.Category) : null,
			...
		};
	}
}

The resulting expression will look like this:

source => new ProductDto{
	Code = source.Code,
	Category = source.Category != null ? new CategoryDto{
		Id = source.Category.Id,
		...
	} : null,
	...
};

Be careful with nesting projections as this can lead to complex evaluations when the expression will be parsed/translated.

Inline expressions

When creating projection expression you can reuse existing Expression<Func<...>> by using the NestedProjector instance you will find in the projection context.

When the final map will be created the inline Expression will be replaced with the actual expression with the arguments replaced.

Up to 16 arguments are supported, just like the System.Func<...> delegate.

public class MyMaps :
	IProjectionMap<Product, ProductDto>
{

	static readonly Expression<Func<Category, CategoryDto>> MyInlineExpression = source => new CategoryDto{
		Id = source.Id,
		...
	};

	Expression<Func<Product, ProductDto> IProjectionMap<Product, ProductDto>.Project(ProjectionContext context){
		return source => new ProductDto{
			Code = source.Code,
			Category = context.Projector.Inline(MyInlineExpression, source.Category),
			...
		};
	}
}

The resulting expression will look like this:

source => new ProductDto{
	Code = source.Code,
	Category = new CategoryDto{
		Id = source.Category.Id,
		...
	},
	...
};

Extend projections

In addition to using nested projections you can also extend existing projections by using the NestedProjector instance you will find in the projection context. For example derived classes could reuse parents’ projections to avoid redeclaring members.

public class Product : BaseProduct { ... }

public class ProductDto : BaseProductDto { ... }

public class MyMaps :
	IProjectionMap<BaseProduct, BaseProductDto>,
	IProjectionMap<Product, ProductDto>
{

	Expression<Func<BaseProduct, BaseProductDto>> IProjectionMap<BaseProduct, BaseProductDto>.Project(ProjectionContext context){
		return source => new BaseProductDto{
			Id = source.Id,
			...
		};
	}

	Expression<Func<Product, ProductDto> IProjectionMap<Product, ProductDto>.Project(ProjectionContext context){
		// The first type argument is the resulting type, the second is the base type for assignability
		return source => context.Projector.Merge<ProductDto, BaseProductDto>(
			context.Projector.Project<BaseProduct, BaseProductDto>(source), // source is a Product, so it is also a BaseProduct
			// Declare the other members
			new ProductDto{
				Code = source.Code,
				...
			});
	}
}

The resulting expression will look like this:

source => new ProductDto{
	// Members from BaseProduct, BaseProductDto
	Id = source.Id,
	...
	// Members from Product, ProductDto
	Code = source.Code,
	...
};

You can also use reuse existing maps as interfaces for maximum extendability,

public class Product { ... }

public class RegularProductDto : IMyInterface { ... }

public class LimitedProductDto : IMyInterface { ... }

public class MyMaps :
	IProjectionMap<Product, RegularProductDto>,
	IProjectionMap<Product, ProductDto>
{

	Expression<Func<Product, RegularProductDto>> IProjectionMap<Product, RegularProductDto>.Project(ProjectionContext context){
		return source => new RegularProductDto{
			Id = source.Id,
			...
		};
	}

	Expression<Func<Product, LimitedProductDto> IProjectionMap<Product, LimitedProductDto>.Project(ProjectionContext context){
		// The first type argument is the resulting type, the second is the base type for assignability
		return source => context.Projector.Merge<LimitedProductDto, IMyInterface>(
			// The cast is necessary to only consider members of the interface (otherwise unrelated members from RegularProductDto
			// could be included triggering errors, since RegularProductDto and LimitedProductDto are unrelated, they just share an interface)
			(IMyInterface)context.Projector.Project<Product, RegularProductDto>(source),

			// Declare the other members, no cast needed here since we are not considering the interface but the whole class
			new LimitedProductDto{
				Code = source.Code,
				...
			});
	}
}