Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

POC Additional parameter mapping #550

Closed
wants to merge 10 commits into from
7 changes: 6 additions & 1 deletion benchmarks/Riok.Mapperly.Benchmarks/Program.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
using BenchmarkDotNet.Running;
using Riok.Mapperly.Benchmarks;

BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
// BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);

var source = new SourceGeneratorBenchmarks();
source.SetupLargeCompile();
source.LargeCompile();
170 changes: 170 additions & 0 deletions samples/Riok.Mapperly.Sample/CarMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,180 @@

namespace Riok.Mapperly.Sample;

// Tasks

// Existing Mapper
// Link all try find
// Pass context
// Update TryFind to handle parameters
// Not a fan of placeholder parameters

// Idea
// Context should pass down all available params
// FindOrBuildMapping should update current context with used params
// When creating Builder ctx.CurrentParams as arg

// Scope custom methods current UserMethodScope

// Problem: with recursion and resolving used parameters
// Normally when referencing itself or a method builder Mapperly will use a place holder
// method invocation and continue building
// The initialization of the body is left until later, where self referencing can be handled by
// calling itself via Find as it has been registered in the MappingCollection

// Extra params require that all children be calculated and initialized
// this is so that we can calculate what parameters are available, recursively passing
// the params up to the creator
// This is a problem: we haven't added the method mapper to mappingCollection
// so when a self referential mapping occurs it tries to construct the method all over.
// FindOrBuild will cause it to be built as it hasn't been registered
// Leading to recursion
// We can't add the mapping to MappingCollection as it hasn't been fully initialized,
// we don't know what parameters are in use.
// Use some kind of promise/callback system

// Solutions
// Check how mapperly detects self referencing, - probably relies upon the Find/Queue trick
// Register a temporary/real mapping of source, target and parameters
// Rework new instance/member mapping to pre calculate the used parameters
// Perhaps some kind of scoped Context where Find will return the mapping itself????
// - Probably infinitely loop, same as placeholder trick
// Whenever a self referencing loop is created pass all parameters in


// Parameter Resolving:
// Additional parameters can be complex type with members
// Those members can match/resolve to existing methods parameters

// Shared: use (source/target) TypeKey maps to list of mappings
// Incomplete: use TypeKey to find in dict
// Scoped: use TypeKey reset on exit scope

// Need to define a way of sorting mappings


// Method Resolving:
// Can't have more than one of a Source/Target in a scope
// Problem: How to resolve user methods, go for simple or longer?

// Always: always use UserMethod where possible
// Fallback: Try to generate mapping, using UserMethod if parameters match

// Method resolving poses a challenge due to the issue of matching convertable types, names and order.
// Order is challenging because a MethodMapping generated with certain parameters in scope could would
// create an entirely different method for a similar Parameter scope. Even if order of parameters in maintained

// Scoped: methods only match when they are user methods
// Mapperly can generate mapping methods but the method may only be used by the parent MethodMapping
// This solves the issue of determining if a function can be reused
// Pros: Faster to generate, easier to follow control flow
// Cons: code bloat, repeated functions
// - Always match user methods as long as names and types match
// - Same as above but ignore types??

// Smart resolved: methods are matched by name and sequential order
// Pros: Code reuse
// Cons: Slower to resolve, bug prone

// Combo: allow limited function reuse when Source, Target and Parameters match
// Extra parameters will likely be the same between methods, ie int id will be common

// Problem: how to support UserMethodMapping????
// How to handle covariance? IE Dog -> IAnimal
// - Maybe let user explicitly map
// Should use matching name.
// How to handle order
// - Could scan through context params looking for a matching value

// Enums of source and target have different numeric values -> use ByName strategy to map them
[Mapper(EnumMappingStrategy = EnumMappingStrategy.ByName)]
public static partial class CarMapper
{
[MapProperty(nameof(Car.Manufacturer), nameof(CarDto.Producer))] // Map property with a different name in the target type
public static partial CarDto MapCarToDto(Car car);

// public static partial C Map(A src, int value, int v1);
//
// public static partial DogDto Map(Dog src, int value, int v1);
//
// public static partial DataADto Map(DataA src);
//
// public static partial DataBDto Map(DataB src);

public static partial B Map(A src, int value);

public static C MapToC(string str, int value) => new C() { StringValue = str, Value = value.ToString() };
}

public class A
{
public string Nested { get; set; }
}

public class B
{
public C Nested { get; init; }
}

public class C
{
public string StringValue { get; init; }
public string Value { get; init; }
}

// public class A
// {
// public string StringValue { get; set; }
// }
//
// public class B
// {
// public string StringValue { get; set; }
// public string Value { get; init; }
// }
//
// public record C
// {
// public int Value { get; init; }
// public int V1 { get; init; }
// }
//
// public class Dog
// {
// public DogOwner Owner { get; set; }
// public int Value { get; set; }
// }
//
// public class DogOwner
// {
// public string Name { get; set; }
// }
//
// public class DogOwnerDto
// {
// public string Name { get; set; }
// }
//
// public class DogDto
// {
// public DogOwnerDto Owner { get; set; }
// public int Value { get; set; }
// }
//
// public record DataA(DataC Nest);
//
// public record DataADto(DataD Nest);
//
// public record DataB(DataC Nest);
//
// public record DataBDto(DataD Nest);
//
// public record DataC(int Value)
// {
// public int Value { get; init; } = Value;
// }
//
// public record DataD
// {
// public int Value { get; init; }
// }
2 changes: 2 additions & 0 deletions src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Riok.Mapperly.Descriptors.MappingBuilders;
using Riok.Mapperly.Descriptors.ObjectFactories;
using Riok.Mapperly.Helpers;
using Riok.Mapperly.Symbols;

namespace Riok.Mapperly.Descriptors;

Expand Down Expand Up @@ -67,6 +68,7 @@ private void ExtractUserMappings()
_objectFactories,
userMapping.Method,
userMapping.SourceType,
userMapping.Parameters,
userMapping.TargetType
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Riok.Mapperly.Descriptors.Mappings.ExistingTarget;
using Riok.Mapperly.Descriptors.Mappings.UserMappings;
using Riok.Mapperly.Helpers;
using Riok.Mapperly.Symbols;

namespace Riok.Mapperly.Descriptors;

Expand Down Expand Up @@ -63,7 +64,7 @@ conversionType is not MappingConversionType.EnumToString and not MappingConversi
/// <returns>The <see cref="ITypeMapping"/> if a mapping was found or <c>null</c> if none was found.</returns>
public override ITypeMapping? FindMapping(ITypeSymbol sourceType, ITypeSymbol targetType)
{
if (_inlineExpressionMappings.Find(sourceType, targetType) is { } mapping)
if (_inlineExpressionMappings.Find(sourceType, targetType, Parameters) is { } mapping)
return mapping;

// user implemented mappings are also taken into account
Expand Down Expand Up @@ -118,11 +119,11 @@ bool reusable
{
sourceType = sourceType.UpgradeNullable();
targetType = targetType.UpgradeNullable();
var mapping = _inlineExpressionMappings.Find(sourceType, targetType);
var mapping = _inlineExpressionMappings.Find(sourceType, targetType, Parameters);
if (mapping != null)
return mapping;

userSymbol ??= (MappingBuilder.Find(sourceType, targetType) as IUserMapping)?.Method;
userSymbol ??= (MappingBuilder.Find(sourceType, targetType, Parameters) as IUserMapping)?.Method;

mapping = BuildMapping(userSymbol, sourceType, targetType, false);
if (mapping != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ private HashSet<string> InitIgnoredUnmatchedProperties(IEnumerable<string> allPr

private HashSet<string> GetSourceMemberNames()
{
return Mapping.SourceType.GetAccessibleMappableMembers().Select(x => x.Name).ToHashSet();
var additionalParams = Mapping.Parameters.Select(x => x.Name);
return Mapping.SourceType.GetAccessibleMappableMembers().Select(x => x.Name).Concat(additionalParams).ToHashSet();
}

private Dictionary<string, IMappableMember> GetTargetMembers()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,30 +20,36 @@ public void BuildMappingBodies()
{
foreach (var (typeMapping, ctx) in _mappings.DequeueMappingsToBuildBody())
{
switch (typeMapping)
{
case NewInstanceObjectMemberMethodMapping mapping:
NewInstanceObjectMemberMappingBodyBuilder.BuildMappingBody(ctx, mapping);
break;
case NewInstanceObjectMemberMapping mapping:
NewInstanceObjectMemberMappingBodyBuilder.BuildMappingBody(ctx, mapping);
break;
case IMemberAssignmentTypeMapping mapping:
ObjectMemberMappingBodyBuilder.BuildMappingBody(ctx, mapping);
break;
case UserDefinedNewInstanceMethodMapping mapping:
UserMethodMappingBodyBuilder.BuildMappingBody(ctx, mapping);
break;
case UserDefinedExistingTargetMethodMapping mapping:
UserMethodMappingBodyBuilder.BuildMappingBody(ctx, mapping);
break;
case UserDefinedNewInstanceRuntimeTargetTypeParameterMapping mapping:
RuntimeTargetTypeMappingBodyBuilder.BuildMappingBody(ctx, mapping);
break;
case UserDefinedNewInstanceGenericTypeMapping mapping:
RuntimeTargetTypeMappingBodyBuilder.BuildMappingBody(ctx, mapping);
break;
}
_mappings.ClearScope();
BuildBody(typeMapping, ctx);
}
}

public static void BuildBody(IMapping typeMapping, MappingBuilderContext ctx)
{
switch (typeMapping)
{
case NewInstanceObjectMemberMethodMapping mapping:
NewInstanceObjectMemberMappingBodyBuilder.BuildMappingBody(ctx, mapping);
break;
case NewInstanceObjectMemberMapping mapping:
NewInstanceObjectMemberMappingBodyBuilder.BuildMappingBody(ctx, mapping);
break;
case IMemberAssignmentTypeMapping mapping:
ObjectMemberMappingBodyBuilder.BuildMappingBody(ctx, mapping);
break;
case UserDefinedNewInstanceMethodMapping mapping:
UserMethodMappingBodyBuilder.BuildMappingBody(ctx, mapping);
break;
case UserDefinedExistingTargetMethodMapping mapping:
UserMethodMappingBodyBuilder.BuildMappingBody(ctx, mapping);
break;
case UserDefinedNewInstanceRuntimeTargetTypeParameterMapping mapping:
RuntimeTargetTypeMappingBodyBuilder.BuildMappingBody(ctx, mapping);
break;
case UserDefinedNewInstanceGenericTypeMapping mapping:
RuntimeTargetTypeMappingBodyBuilder.BuildMappingBody(ctx, mapping);
break;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,41 @@ public static void BuildMappingBody(MappingBuilderContext ctx, NewInstanceObject
ObjectMemberMappingBodyBuilder.BuildMappingBody(mappingCtx);
}

private static bool TryParams(INewInstanceBuilderContext<IMapping> ctx, IMappableMember member, out MemberPath? memberPath)
{
memberPath = null;
foreach (var parameter in ctx.BuilderContext.Parameters)
{
Console.WriteLine(parameter.Name);
Console.WriteLine("Param");
if (
MemberPath.TryFindParameter(
parameter,
MemberPathCandidateBuilder.BuildMemberPathCandidates(member.Name),
ctx.IgnoredSourceMemberNames,
StringComparer.OrdinalIgnoreCase,
out var sourceMemberPath
)
)
{
Console.WriteLine("TyrFind");
ctx.BuilderContext.UsedParameters.Add(parameter);
Console.WriteLine($"Used Params: {ctx.BuilderContext.UsedParameters.Count}");

memberPath = sourceMemberPath;
return true;
}
}

return false;
}

private static void BuildInitOnlyMemberMappings(INewInstanceBuilderContext<IMapping> ctx, bool includeAllMembers = false)
{
var memberNameComparer =
ctx.BuilderContext.MapperConfiguration.PropertyNameMappingStrategy == PropertyNameMappingStrategy.CaseSensitive
? StringComparer.Ordinal
: StringComparer.OrdinalIgnoreCase;
var initOnlyTargetMembers = includeAllMembers
? ctx.TargetMembers.Values.ToArray()
: ctx.TargetMembers.Values.Where(x => x.CanOnlySetViaInitializer()).ToArray();
Expand All @@ -51,8 +84,9 @@ private static void BuildInitOnlyMemberMappings(INewInstanceBuilderContext<IMapp
ctx.Mapping.SourceType,
MemberPathCandidateBuilder.BuildMemberPathCandidates(targetMember.Name),
ctx.IgnoredSourceMemberNames,
memberNameComparer,
out var sourceMemberPath
)
) && !MemberPath.TryParams(ctx, targetMember, out sourceMemberPath)
)
{
ctx.BuilderContext.ReportDiagnostic(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public static void BuildMappingBody(IMembersContainerBuilderContext<IMemberAssig
ctx.IgnoredSourceMemberNames,
memberNameComparer,
out var sourceMemberPath
)
) || MemberPath.TryParams(ctx, targetMember, out sourceMemberPath)
)
{
BuildMemberAssignmentMapping(ctx, sourceMemberPath, new MemberPath(new[] { targetMember }));
Expand Down
Loading