diff --git a/docs/docs/breaking-changes/5-0.md b/docs/docs/breaking-changes/5-0.md index dc21c2fef4..edd80df66b 100644 --- a/docs/docs/breaking-changes/5-0.md +++ b/docs/docs/breaking-changes/5-0.md @@ -22,6 +22,7 @@ description: How to upgrade to Mapperly 5.0 and a list of all its breaking chang - Copy constructors are no longer selected over constructors annotated with `[MapperConstructor]` or when `[MapProperty]` configurations are present. - Generic and runtime target type mappings now correctly match derived types handled by a child `[MapDerivedType]` mapping. - The default severity of `RMG068` (cannot inline user implemented queryable expression mapping) has been raised from info to warning, see below. +- `RMG032` is now an error instead of a warning. Queryable projection mappings that relied on the previous fallback behavior must opt out of inlining, see below. ## Inaccessible members from other assemblies included @@ -179,6 +180,33 @@ public partial Dto Map(int[]? source) } ``` +## `RMG032` promoted to error + +Mapperly previously emitted `RMG032` as a warning when a queryable projection required an enum mapping strategy that cannot be translated to an expression tree +(`ByName`, `ByValueCheckDefined`, explicit enum mappings, ignored enum values, or cast-based mappings between enums with different underlying types). +When this happened, Mapperly silently fell back to a cast-based mapping which produced incorrect code at runtime and cascading false `RMG037` / `RMG038` diagnostics. + +Starting with v5.0, `RMG032` is reported as an error and no fallback mapping is produced. +To opt out of inlining and keep the referenced mapping method as-is in the expression tree, apply the new `[MapperNoExpressionInlining]` attribute or +set `Mapper(NoExpressionInlining = true)` on the callee mapper. +See the [queryable projections documentation](../configuration/queryable-projections.mdx) for more details. + +```csharp +[Mapper(EnumMappingStrategy = EnumMappingStrategy.ByName)] +public static partial class EnumMapper +{ + [MapperNoExpressionInlining] + public static partial TargetStatus MapToStatus(this SourceStatus source); +} + +// or, per mapper +[Mapper(EnumMappingStrategy = EnumMappingStrategy.ByName, NoExpressionInlining = true)] +public static partial class EnumMapper +{ + public static partial TargetStatus MapToStatus(this SourceStatus source); +} +``` + ## Constructor selection Previously, when a type had a copy constructor (a single-parameter constructor accepting the same type), diff --git a/docs/docs/configuration/queryable-projections.mdx b/docs/docs/configuration/queryable-projections.mdx index 55874bdd17..57820613a5 100644 --- a/docs/docs/configuration/queryable-projections.mdx +++ b/docs/docs/configuration/queryable-projections.mdx @@ -122,6 +122,112 @@ public static partial class CarMapper It is important that the types in the user-implemented mapping method match the types of the objects to be mapped exactly. Otherwise, Mapperly cannot resolve the user-implemented mapping methods. +## Controlling inlining + +Mapperly automatically tries to inline referenced mapping methods into expression trees for queryable projections. +In some cases, this inlining can cause issues — for example, when enum mappings with different underlying types +produce false diagnostics in expression context. + +You can prevent inlining of specific methods using the `MapperNoExpressionInlining` attribute, +or prevent inlining of all methods in a mapper using the `NoExpressionInlining` property on the `Mapper` attribute. + +### Per-method opt-out + +```csharp +[Mapper(EnumMappingStrategy = EnumMappingStrategy.ByName)] +public static partial class EnumMapper +{ + // highlight-start + [MapperNoExpressionInlining] + // highlight-end + public static partial TargetStatus MapToStatus(this SourceStatus source); +} + +[Mapper] +[UseStaticMapper(typeof(EnumMapper))] +public static partial class Mapper +{ + public static partial IQueryable ProjectToDto(this IQueryable q); + public static partial CarDto MapToDto(this Car car); +} +``` + +### Per-mapper opt-out + +```csharp +// highlight-start +[Mapper(EnumMappingStrategy = EnumMappingStrategy.ByName, NoExpressionInlining = true)] +// highlight-end +public static partial class EnumMapper +{ + public static partial TargetStatus MapToStatus(this SourceStatus source); +} +``` + +### Runtime behavior + +The `NoExpressionInlining` setting changes how the projection is materialized at runtime. + + + + +With inlining enabled, Mapperly rebuilds the mapping logic directly into the expression tree. +The entire projection runs in SQL — but the rebuilt expression may produce incorrect results +for non-trivial type pairs (e.g., enum mappings with different underlying types): + +```csharp +// What runs under the hood: +var dtos = await dbContext.Cars + .Select(x => new CarDto + { + // highlight-start + // Runs entirely in SQL, but the cast may be wrong + // for enums with different underlying types + Status = (TargetStatus)x.Status, + // highlight-end + }) + .ToListAsync(); +``` + + + + +With inlining disabled, the mapping method call is preserved as-is. +The ORM cannot translate it to SQL, so Entity Framework will split the query +into a server-side projection and client-side mapping: + +```csharp +// What runs under the hood: +var dtos = await dbContext.Cars + .Select(x => new + { + // highlight-start + // Only the needed columns are fetched from the database + Status = x.Status, + // highlight-end + }) + // highlight-start + .AsAsyncEnumerable() // ← everything before runs in SQL, everything after is client-side + // highlight-end + .Select(x => new CarDto + { + // highlight-start + Status = EnumMapper.MapToStatus(x.Status), // ← runs in C# + // highlight-end + }) + .ToListAsync(); +``` + + + + +:::warning +When inlining is disabled, the mapping method call cannot be translated to SQL. +Entity Framework will evaluate it client-side, which means the relevant columns are still fetched +from the database but the mapping logic runs in C# instead of SQL. +Only use this when the default inlining behavior causes issues. +::: + ## Additional parameters Queryable projection methods can have [additional parameters](./additional-mapping-parameters.mdx). diff --git a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs index f0e1190379..d3e23c4504 100644 --- a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs +++ b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs @@ -142,4 +142,15 @@ public class MapperAttribute : Attribute /// partial methods are discovered. /// public bool AutoUserMappings { get; set; } = true; + + /// + /// Whether to prevent mapping methods of this mapper from being inlined + /// into expression trees for queryable projection mappings. + /// When true, methods from this mapper referenced via + /// will not be inlined or rebuilt in expression context. + /// This only applies to expression / queryable projection mappings; + /// regular (non-expression) mappings are unaffected. + /// Defaults to false. + /// + public bool NoExpressionInlining { get; set; } } diff --git a/src/Riok.Mapperly.Abstractions/MapperNoExpressionInliningAttribute.cs b/src/Riok.Mapperly.Abstractions/MapperNoExpressionInliningAttribute.cs new file mode 100644 index 0000000000..29e4c35a21 --- /dev/null +++ b/src/Riok.Mapperly.Abstractions/MapperNoExpressionInliningAttribute.cs @@ -0,0 +1,15 @@ +using System.Diagnostics; + +namespace Riok.Mapperly.Abstractions; + +/// +/// Prevents a mapping method from being inlined into expression trees for queryable projection mappings. +/// When applied, the method call is preserved as-is instead of being rebuilt in expression context. +/// This only applies to expression / queryable projection mappings; +/// regular (non-expression) mappings are unaffected. +/// This is useful when inlining causes issues such as false enum mapping diagnostics +/// due to expression tree limitations. +/// +[AttributeUsage(AttributeTargets.Method)] +[Conditional("MAPPERLY_ABSTRACTIONS_SCOPE_RUNTIME")] +public sealed class MapperNoExpressionInliningAttribute : Attribute; diff --git a/src/Riok.Mapperly/AnalyzerReleases.Unshipped.md b/src/Riok.Mapperly/AnalyzerReleases.Unshipped.md index 17d4678ce3..fa78d6b449 100644 --- a/src/Riok.Mapperly/AnalyzerReleases.Unshipped.md +++ b/src/Riok.Mapperly/AnalyzerReleases.Unshipped.md @@ -1,2 +1,8 @@ ; Unshipped analyzer release ; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +### Changed Rules + +Rule ID | New Category | New Severity | Old Category | Old Severity | Notes +--------|--------------|--------------|--------------|--------------|------ +RMG032 | Mapper | Error | Mapper | Warning | The enum mapping strategy ByName, ByValueCheckDefined, explicit enum mappings and ignored enum values cannot be used in projection mappings diff --git a/src/Riok.Mapperly/Configuration/MapperConfiguration.cs b/src/Riok.Mapperly/Configuration/MapperConfiguration.cs index 68893d366a..eae3842b31 100644 --- a/src/Riok.Mapperly/Configuration/MapperConfiguration.cs +++ b/src/Riok.Mapperly/Configuration/MapperConfiguration.cs @@ -130,6 +130,11 @@ public record MapperConfiguration /// public bool? AutoUserMappings { get; init; } + /// + /// Whether to prevent mapping methods of this mapper from being inlined into expression trees for queryable projection mappings. + /// + public bool? NoExpressionInlining { get; init; } + /// /// The default enum naming strategy. /// Can be overwritten on specific enums via mapping method configurations. diff --git a/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs b/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs index 4d625f49f3..750f76f043 100644 --- a/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs +++ b/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs @@ -26,6 +26,7 @@ public static MapperConfiguration Merge(MapperConfiguration highPriority, Mapper IncludedConstructors = highPriority.IncludedConstructors ?? lowPriority.IncludedConstructors, PreferParameterlessConstructors = highPriority.PreferParameterlessConstructors ?? lowPriority.PreferParameterlessConstructors, AutoUserMappings = highPriority.AutoUserMappings ?? lowPriority.AutoUserMappings, + NoExpressionInlining = highPriority.NoExpressionInlining ?? lowPriority.NoExpressionInlining, EnumNamingStrategy = highPriority.EnumNamingStrategy ?? lowPriority.EnumNamingStrategy, }; } @@ -101,6 +102,9 @@ public static MapperAttribute MergeToAttribute(MapperConfiguration mapperConfigu mapper.AutoUserMappings = mapperConfiguration.AutoUserMappings ?? defaultMapperConfiguration.AutoUserMappings ?? mapper.AutoUserMappings; + mapper.NoExpressionInlining = + mapperConfiguration.NoExpressionInlining ?? defaultMapperConfiguration.NoExpressionInlining ?? mapper.NoExpressionInlining; + mapper.EnumNamingStrategy = mapperConfiguration.EnumNamingStrategy ?? defaultMapperConfiguration.EnumNamingStrategy ?? mapper.EnumNamingStrategy; diff --git a/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs b/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs index 3968e59d0f..f2d351e861 100644 --- a/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs +++ b/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs @@ -13,11 +13,13 @@ namespace Riok.Mapperly.Configuration; public class MapperConfigurationReader { private readonly Dictionary _resolvedConfigurations = new(); + private readonly Dictionary _resolvedMapperConfigurations = new(SymbolEqualityComparer.Default); private readonly AttributeDataAccessor _dataAccessor; private readonly MappingCollection _mappings; private readonly GenericTypeChecker _genericTypeChecker; private readonly DiagnosticCollection _diagnostics; private readonly WellKnownTypes _types; + private readonly MapperConfiguration _defaultMapperConfiguration; public MapperConfigurationReader( AttributeDataAccessor dataAccessor, @@ -35,9 +37,9 @@ SupportedFeatures supportedFeatures _genericTypeChecker = genericTypeChecker; _diagnostics = diagnostics; _types = types; + _defaultMapperConfiguration = defaultMapperConfiguration; - var mapperConfiguration = _dataAccessor.AccessSingle(mapperSymbol); - var mapper = MapperConfigurationMerger.MergeToAttribute(mapperConfiguration, defaultMapperConfiguration); + var mapper = BuildMapperConfiguration(mapperSymbol); MapperConfiguration = new MappingConfiguration( mapper, @@ -61,6 +63,25 @@ SupportedFeatures supportedFeatures public MappingConfiguration MapperConfiguration { get; } + /// + /// Builds the effective configuration for a mapper symbol + /// by merging its with the default mapper configuration + /// ( and MSBuild options). + /// + public MapperAttribute BuildMapperConfiguration(ISymbol mapperSymbol) + { + if (_resolvedMapperConfigurations.TryGetValue(mapperSymbol, out var mapper)) + return mapper; + + var mapperConfiguration = + _dataAccessor.AccessFirstOrDefault(mapperSymbol) ?? new MapperConfiguration(); + + mapper = MapperConfigurationMerger.MergeToAttribute(mapperConfiguration, _defaultMapperConfiguration); + _resolvedMapperConfigurations[mapperSymbol] = mapper; + + return mapper; + } + public MappingConfiguration BuildFor(MappingConfigurationReference reference, bool supportsDeepCloning) { if (_resolvedConfigurations.TryGetValue(reference, out var resolved)) diff --git a/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs index 2efbaf7938..a38b9e1b03 100644 --- a/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs @@ -201,6 +201,17 @@ private INewInstanceMapping TryInlineMapping(INewInstanceMapping mapping) { return mapping switch { + // check if NoInline is requested + IUserMapping { NoExpressionInlining: true } => mapping, + + // methods declared in another assembly are never inlined: + // whether their syntax is accessible depends on the compilation stage + // (available for compilation references, unavailable for compiled assembly references), + // treat them always as non-inlinable to generate deterministic code + IUserMapping externalAssemblyMapping + when !SymbolEqualityComparer.Default.Equals(externalAssemblyMapping.Method.ContainingAssembly, Compilation.Assembly) => + ReportCannotInline(mapping, externalAssemblyMapping.Method), + // inline existing mapping UserImplementedMethodMapping implementedMapping => InlineOrRebuild(implementedMapping) ?? implementedMapping, @@ -216,6 +227,12 @@ private INewInstanceMapping TryInlineMapping(INewInstanceMapping mapping) }; } + private INewInstanceMapping ReportCannotInline(INewInstanceMapping mapping, IMethodSymbol method) + { + ReportDiagnostic(DiagnosticDescriptors.QueryableProjectionMappingCannotInline, method); + return mapping; + } + private INewInstanceMapping? InlineOrRebuild(UserImplementedMethodMapping mapping) { // Try inline first diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumToEnumMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumToEnumMappingBuilder.cs index 8298275c86..fde6661d8c 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumToEnumMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumToEnumMappingBuilder.cs @@ -36,27 +36,34 @@ public static class EnumToEnumMappingBuilder if (!ctx.IsConversionEnabled(MappingConversionType.EnumToEnum)) return null; + // A cast-based enum mapping in expression context fails to translate + // members between enums with different underlying types (e.g. sbyte vs byte) + // because boxed value equality across primitive types returns false. + // Report RMG032 and bail out instead of cascading RMG037/RMG038. + if (ctx.IsExpression && !SymbolEqualityComparer.Default.Equals(sourceEnumType, targetEnumType)) + return ReportNotSupportedInProjectionMappings(ctx); + // map enums by strategy return ctx.Configuration.Enum.Strategy switch { - EnumMappingStrategy.ByName when ctx.IsExpression => BuildCastMappingAndDiagnostic(ctx), + EnumMappingStrategy.ByName when ctx.IsExpression => ReportNotSupportedInProjectionMappings(ctx), EnumMappingStrategy.ByValue when ctx is { IsExpression: true, Configuration.Enum.HasExplicitConfigurations: true } => - BuildCastMappingAndDiagnostic(ctx), - EnumMappingStrategy.ByValueCheckDefined when ctx.IsExpression => BuildCastMappingAndDiagnostic(ctx), + ReportNotSupportedInProjectionMappings(ctx), + EnumMappingStrategy.ByValueCheckDefined when ctx.IsExpression => ReportNotSupportedInProjectionMappings(ctx), EnumMappingStrategy.ByName => BuildNameMapping(ctx), EnumMappingStrategy.ByValueCheckDefined => BuildEnumToEnumCastMapping(ctx, checkTargetDefined: true), _ => BuildEnumToEnumCastMapping(ctx), }; } - private static INewInstanceMapping BuildCastMappingAndDiagnostic(MappingBuilderContext ctx) + private static INewInstanceMapping? ReportNotSupportedInProjectionMappings(MappingBuilderContext ctx) { ctx.ReportDiagnostic( DiagnosticDescriptors.EnumMappingNotSupportedInProjectionMappings, ctx.Source.ToDisplayString(), ctx.Target.ToDisplayString() ); - return BuildEnumToEnumCastMapping(ctx, true); + return null; } private static INewInstanceMapping BuildEnumToEnumCastMapping( diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/GenericUserImplementedExistingTargetMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/GenericUserImplementedExistingTargetMethodMapping.cs index d1654dbacd..2dd656c216 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/GenericUserImplementedExistingTargetMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/GenericUserImplementedExistingTargetMethodMapping.cs @@ -17,7 +17,8 @@ internal sealed class GenericUserImplementedExistingTargetMethodMapping( MethodParameter sourceParameter, MethodParameter targetParameter, MethodParameter? referenceHandlerParameter, - bool isExternal + bool isExternal, + bool noExpressionInlining ) : IExistingTargetUserMapping { public ITypeSymbol SourceType => sourceParameter.Type; @@ -30,6 +31,8 @@ bool isExternal public bool IsExternal => isExternal; + public bool NoExpressionInlining => noExpressionInlining; + public bool IsSynthetic => false; public IEnumerable BuildAdditionalMappingKeys(TypeMappingConfiguration config) => []; @@ -68,7 +71,8 @@ ITypeSymbol concreteTargetType concreteTargetType, typeArguments, referenceHandlerParameter, - isExternal + isExternal, + noExpressionInlining ); } } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/GenericUserImplementedNewInstanceMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/GenericUserImplementedNewInstanceMethodMapping.cs index f06a942a3a..a4a398da18 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/GenericUserImplementedNewInstanceMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/GenericUserImplementedNewInstanceMethodMapping.cs @@ -19,7 +19,8 @@ internal sealed class GenericUserImplementedNewInstanceMethodMapping( MethodParameter? referenceHandlerParameter, MethodParameter? targetOriginalValueParameter, bool isExternal, - UserImplementedMethodMapping.TargetNullability targetNullability + UserImplementedMethodMapping.TargetNullability targetNullability, + bool noExpressionInlining ) : INewInstanceUserMapping { public ITypeSymbol SourceType => sourceParameter.Type; @@ -32,6 +33,8 @@ UserImplementedMethodMapping.TargetNullability targetNullability public bool IsExternal => isExternal; + public bool NoExpressionInlining => noExpressionInlining; + public bool IsSynthetic => false; public IEnumerable BuildAdditionalMappingKeys(TypeMappingConfiguration config) => []; @@ -77,7 +80,8 @@ ITypeSymbol concreteTargetType referenceHandlerParameter, concreteTargetOriginalValueParameter, isExternal, - targetNullability + targetNullability, + noExpressionInlining ); } } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/IUserMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/IUserMapping.cs index fbb29f9ba4..2f985a4cdf 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/IUserMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/IUserMapping.cs @@ -18,4 +18,7 @@ public interface IUserMapping : ITypeMapping /// E.g. base class or imported via /// bool IsExternal { get; } + + /// + bool NoExpressionInlining { get; } } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetGenericTypeMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetGenericTypeMapping.cs index 6a28a721e9..c0fcd451c5 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetGenericTypeMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetGenericTypeMapping.cs @@ -24,7 +24,8 @@ public class UserDefinedExistingTargetGenericTypeMapping( MethodParameter sourceParameter, MethodParameter targetParameter, MethodParameter? referenceHandlerParameter, - bool enableReferenceHandling + bool enableReferenceHandling, + bool noExpressionInlining ) : MethodMapping(method, sourceParameter, referenceHandlerParameter, targetParameter.Type), IExistingTargetUserMapping { private const string SourceName = "source"; @@ -43,6 +44,8 @@ bool enableReferenceHandling public bool IsExternal => false; + public bool NoExpressionInlining => noExpressionInlining; + private MethodParameter TargetParameter { get; } = targetParameter; /// diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetMethodMapping.cs index 768967f7f1..c21b83018b 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetMethodMapping.cs @@ -17,7 +17,8 @@ public class UserDefinedExistingTargetMethodMapping( MethodParameter sourceParameter, MethodParameter targetParameter, MethodParameter? referenceHandlerParameter, - bool enableReferenceHandling + bool enableReferenceHandling, + bool noExpressionInlining ) : MethodMapping(method, sourceParameter, referenceHandlerParameter, targetParameter.Type), IExistingTargetUserMapping { private IExistingTargetMapping? _delegateMapping; @@ -28,6 +29,8 @@ bool enableReferenceHandling public bool IsExternal => false; + public bool NoExpressionInlining => noExpressionInlining; + private MethodParameter TargetParameter { get; } = targetParameter; /// diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExpressionMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExpressionMethodMapping.cs index cb37c64c66..fd8b707054 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExpressionMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExpressionMethodMapping.cs @@ -14,7 +14,8 @@ public class UserDefinedExpressionMethodMapping( IMethodSymbol method, ITypeSymbol expressionSourceType, ITypeSymbol expressionTargetType, - ITypeSymbol returnType + ITypeSymbol returnType, + bool noExpressionInlining ) : NewInstanceMethodMapping(method, CreateSourceParameter(expressionSourceType), null, returnType), INewInstanceUserMapping { private INewInstanceMapping? _delegateMapping; @@ -35,6 +36,8 @@ ITypeSymbol returnType public bool IsExternal => false; + public bool NoExpressionInlining => noExpressionInlining; + public void SetDelegateMapping(INewInstanceMapping mapping) => _delegateMapping = mapping; public override IEnumerable BuildBody(TypeMappingBuildContext ctx) diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceGenericTypeMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceGenericTypeMapping.cs index d82a08b206..a3fe3fd61d 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceGenericTypeMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceGenericTypeMapping.cs @@ -22,7 +22,8 @@ public class UserDefinedNewInstanceGenericTypeMapping( ITypeSymbol targetType, bool enableReferenceHandling, NullFallbackValue? nullArm, - ITypeSymbol objectType + ITypeSymbol objectType, + bool noExpressionInlining ) : UserDefinedNewInstanceRuntimeTargetTypeMapping( method, @@ -31,7 +32,8 @@ ITypeSymbol objectType targetType, enableReferenceHandling, nullArm, - objectType + objectType, + noExpressionInlining ) { public override MethodDeclarationSyntax BuildMethod(SourceEmitterContext ctx) diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceMethodMapping.cs index 5ed1653a7e..76683faaf6 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceMethodMapping.cs @@ -16,7 +16,8 @@ public class UserDefinedNewInstanceMethodMapping( MethodParameter? referenceHandlerParameter, ITypeSymbol targetType, bool enableReferenceHandling, - bool isDerivedTypeMapping + bool isDerivedTypeMapping, + bool noExpressionInlining ) : NewInstanceMethodMapping(method, sourceParameter, referenceHandlerParameter, targetType), INewInstanceUserMapping { private INewInstanceMapping? _delegateMapping; @@ -31,6 +32,8 @@ bool isDerivedTypeMapping public bool IsExternal => false; + public bool NoExpressionInlining { get; } = noExpressionInlining; + /// /// The reference handling is enabled but is only internal to this method. /// No reference handler parameter is passed. diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeMapping.cs index 6da754783c..1b33e2a901 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeMapping.cs @@ -20,7 +20,8 @@ public abstract class UserDefinedNewInstanceRuntimeTargetTypeMapping( ITypeSymbol targetType, bool enableReferenceHandling, NullFallbackValue? nullArm, - ITypeSymbol objectType + ITypeSymbol objectType, + bool noExpressionInlining ) : NewInstanceMethodMapping(method, sourceParameter, referenceHandlerParameter, targetType), INewInstanceUserMapping { private const string IsAssignableFromMethodName = nameof(Type.IsAssignableFrom); @@ -38,6 +39,8 @@ ITypeSymbol objectType public bool IsExternal => false; + public bool NoExpressionInlining => noExpressionInlining; + /// /// The reference handling is enabled but is only internal to this method. /// No reference handler parameter is passed. diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeParameterMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeParameterMapping.cs index a361c558fa..7de21de74d 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeParameterMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeParameterMapping.cs @@ -16,7 +16,8 @@ public class UserDefinedNewInstanceRuntimeTargetTypeParameterMapping( bool enableReferenceHandling, ITypeSymbol targetType, NullFallbackValue? nullArm, - ITypeSymbol objectType + ITypeSymbol objectType, + bool noExpressionInlining ) : UserDefinedNewInstanceRuntimeTargetTypeMapping( method, @@ -25,7 +26,8 @@ ITypeSymbol objectType targetType, enableReferenceHandling, nullArm, - objectType + objectType, + noExpressionInlining ) { protected override ParameterListSyntax BuildParameterList() => diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedExistingTargetMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedExistingTargetMethodMapping.cs index 4ef478957c..27b0aa77dd 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedExistingTargetMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedExistingTargetMethodMapping.cs @@ -20,7 +20,8 @@ public class UserImplementedExistingTargetMethodMapping( ITypeSymbol sourceType, ITypeSymbol targetType, MethodParameter? referenceHandlerParameter, - bool isExternal + bool isExternal, + bool noExpressionInlining ) : ExistingTargetMapping(sourceType, targetType), IExistingTargetUserMapping, IParameterizedMapping { public IMethodSymbol Method { get; } = method; @@ -29,6 +30,8 @@ bool isExternal public bool IsExternal { get; } = isExternal; + public bool NoExpressionInlining { get; } = noExpressionInlining; + public IReadOnlyCollection AdditionalSourceParameters { get; } = method .Parameters.Where(p => diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedGenericExistingTargetMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedGenericExistingTargetMethodMapping.cs index 6fd8b3981c..a35b7dea0c 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedGenericExistingTargetMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedGenericExistingTargetMethodMapping.cs @@ -20,7 +20,8 @@ public class UserImplementedGenericExistingTargetMethodMapping( ITypeSymbol concreteTargetType, IReadOnlyList typeArguments, MethodParameter? referenceHandlerParameter, - bool isExternal + bool isExternal, + bool noExpressionInlining ) : UserImplementedExistingTargetMethodMapping( receiver, @@ -31,7 +32,8 @@ bool isExternal concreteSourceType, concreteTargetType, referenceHandlerParameter, - isExternal + isExternal, + noExpressionInlining ) { protected override SimpleNameSyntax BuildMethodName() diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedGenericMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedGenericMethodMapping.cs index 055ed895cf..92c25f17b1 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedGenericMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedGenericMethodMapping.cs @@ -21,7 +21,8 @@ public class UserImplementedGenericMethodMapping( MethodParameter? referenceHandlerParameter, MethodParameter? targetOriginalValueParameter, bool isExternal, - UserImplementedMethodMapping.TargetNullability targetNullability + UserImplementedMethodMapping.TargetNullability targetNullability, + bool noExpressionInlining ) : UserImplementedMethodMapping( receiver, @@ -33,7 +34,8 @@ UserImplementedMethodMapping.TargetNullability targetNullability referenceHandlerParameter, targetOriginalValueParameter, isExternal, - targetNullability + targetNullability, + noExpressionInlining ) { protected override SimpleNameSyntax BuildMethodName() diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedInlinedExpressionMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedInlinedExpressionMapping.cs index 7722a32da1..20996912e8 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedInlinedExpressionMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedInlinedExpressionMapping.cs @@ -25,6 +25,7 @@ ExpressionSyntax mappingBody public IMethodSymbol Method => userMapping.Method; public bool? Default => userMapping.Default; public bool IsExternal => userMapping.IsExternal; + public bool NoExpressionInlining => userMapping.NoExpressionInlining; public override ExpressionSyntax Build(TypeMappingBuildContext ctx) { diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedMethodMapping.cs index 1ea8959d01..faee97f7f5 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedMethodMapping.cs @@ -21,7 +21,8 @@ public class UserImplementedMethodMapping( MethodParameter? referenceHandlerParameter, MethodParameter? targetOriginalValueParameter, bool isExternal, - UserImplementedMethodMapping.TargetNullability targetNullability + UserImplementedMethodMapping.TargetNullability targetNullability, + bool noExpressionInlining ) : NewInstanceMapping(sourceType, targetType), INewInstanceUserMapping, IParameterizedMapping { public enum TargetNullability @@ -39,6 +40,8 @@ public enum TargetNullability public MethodParameter? TargetOriginalValueParameter { get; } = targetOriginalValueParameter; + public bool NoExpressionInlining { get; } = noExpressionInlining; + public IReadOnlyCollection AdditionalSourceParameters { get; } = method .Parameters.Where(p => diff --git a/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs index 771be5d94d..a152d9934d 100644 --- a/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs @@ -77,6 +77,8 @@ protected SimpleMappingBuilderContext(SimpleMappingBuilderContext ctx, Location? public SemanticModel? GetSemanticModel(SyntaxTree syntaxTree) => _compilationContext.GetSemanticModel(syntaxTree); + public MapperAttribute BuildMapperConfiguration(ISymbol mapperSymbol) => _configurationReader.BuildMapperConfiguration(mapperSymbol); + public virtual bool IsConversionEnabled(MappingConversionType conversionType) => Configuration.Mapper.EnabledConversions.HasFlag(conversionType); diff --git a/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs b/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs index 9c13c957f6..53ae928ffd 100644 --- a/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs +++ b/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs @@ -189,6 +189,8 @@ private static bool IsMappingMethodCandidate(SimpleMappingBuilderContext ctx, IM return null; } + var noExpressionInlining = GetNoExpressionInlining(ctx, method, isExternal); + // Generic user-implemented methods are stored as templates // that are matched against concrete type pairs during mapping resolution. if (method.IsGenericMethod) @@ -199,7 +201,8 @@ private static bool IsMappingMethodCandidate(SimpleMappingBuilderContext ctx, IM receiver, parameters, isExternal, - userMappingConfig.Default ?? isDefault + userMappingConfig.Default ?? isDefault, + noExpressionInlining ); } @@ -214,7 +217,8 @@ private static bool IsMappingMethodCandidate(SimpleMappingBuilderContext ctx, IM parameters.Source.Type, parameters.Target!.Value.Type, parameters.ReferenceHandler, - isExternal + isExternal, + noExpressionInlining ); } @@ -229,7 +233,8 @@ private static bool IsMappingMethodCandidate(SimpleMappingBuilderContext ctx, IM parameters.ReferenceHandler, parameters.TargetOriginalValueParameter, isExternal, - targetTypeNullability + targetTypeNullability, + noExpressionInlining ); } @@ -239,7 +244,8 @@ private static bool IsMappingMethodCandidate(SimpleMappingBuilderContext ctx, IM string? receiver, MappingMethodParameters parameters, bool isExternal, - bool? isDefault + bool? isDefault, + bool noExpressionInlining ) { if (method.ReturnsVoid) @@ -253,7 +259,8 @@ private static bool IsMappingMethodCandidate(SimpleMappingBuilderContext ctx, IM parameters.Source, targetParam, parameters.ReferenceHandler, - isExternal + isExternal, + noExpressionInlining ); } @@ -266,7 +273,8 @@ private static bool IsMappingMethodCandidate(SimpleMappingBuilderContext ctx, IM parameters.ReferenceHandler, parameters.TargetOriginalValueParameter, isExternal, - targetTypeNullability + targetTypeNullability, + noExpressionInlining ); } @@ -310,10 +318,12 @@ string sourceParameterName return null; } - if (TryBuildRuntimeTargetTypeMapping(ctx, methodSymbol) is { } userMapping) + var noExpressionInlining = GetNoExpressionInlining(ctx, methodSymbol, isExternal: false); + + if (TryBuildRuntimeTargetTypeMapping(ctx, methodSymbol, noExpressionInlining) is { } userMapping) return userMapping; - if (TryBuildExpressionMapping(ctx, methodSymbol) is { } expressionMapping) + if (TryBuildExpressionMapping(ctx, methodSymbol, noExpressionInlining) is { } expressionMapping) return expressionMapping; if (!UserMappingMethodParameterExtractor.BuildParameters(ctx, methodSymbol, out var parameters)) @@ -329,7 +339,7 @@ string sourceParameterName } if (methodSymbol.IsGenericMethod) - return BuildGenericTypeMapping(ctx, methodSymbol, parameters); + return BuildGenericTypeMapping(ctx, methodSymbol, parameters, noExpressionInlining); if (parameters.Target.HasValue) { @@ -338,7 +348,8 @@ string sourceParameterName parameters.Source, parameters.Target.Value, parameters.ReferenceHandler, - ctx.Configuration.Mapper.UseReferenceHandling + ctx.Configuration.Mapper.UseReferenceHandling, + noExpressionInlining ) { AdditionalSourceParameters = parameters.AdditionalParameters, @@ -363,7 +374,8 @@ string sourceParameterName ctx.SymbolAccessor.UpgradeNullable(methodSymbol.ReturnType), ctx.Configuration.Mapper.UseReferenceHandling, ctx.AttributeAccessor.HasAttribute>(methodSymbol) - || ctx.AttributeAccessor.HasAttribute(methodSymbol) + || ctx.AttributeAccessor.HasAttribute(methodSymbol), + noExpressionInlining ) { AdditionalSourceParameters = parameters.AdditionalParameters, @@ -374,7 +386,8 @@ string sourceParameterName private static IUserMapping BuildGenericTypeMapping( SimpleMappingBuilderContext ctx, IMethodSymbol methodSymbol, - MappingMethodParameters parameters + MappingMethodParameters parameters, + bool noExpressionInlining ) { if (parameters.Target.HasValue) @@ -384,7 +397,8 @@ MappingMethodParameters parameters parameters.Source, parameters.Target.Value, parameters.ReferenceHandler, - ctx.Configuration.Mapper.UseReferenceHandling + ctx.Configuration.Mapper.UseReferenceHandling, + noExpressionInlining ); } @@ -394,13 +408,15 @@ MappingMethodParameters parameters ctx.SymbolAccessor.UpgradeNullable(methodSymbol.ReturnType), ctx.Configuration.Mapper.UseReferenceHandling, GetTypeSwitchNullArm(methodSymbol, parameters), - ctx.Compilation.ObjectType.WithNullableAnnotation(NullableAnnotation.NotAnnotated) + ctx.Compilation.ObjectType.WithNullableAnnotation(NullableAnnotation.NotAnnotated), + noExpressionInlining ); } private static UserDefinedNewInstanceRuntimeTargetTypeParameterMapping? TryBuildRuntimeTargetTypeMapping( SimpleMappingBuilderContext ctx, - IMethodSymbol methodSymbol + IMethodSymbol methodSymbol, + bool noExpressionInlining ) { if (methodSymbol.IsGenericMethod) @@ -419,13 +435,15 @@ IMethodSymbol methodSymbol ctx.Configuration.Mapper.UseReferenceHandling, ctx.SymbolAccessor.UpgradeNullable(methodSymbol.ReturnType), GetTypeSwitchNullArm(methodSymbol, runtimeTargetTypeParams), - ctx.Compilation.ObjectType.WithNullableAnnotation(NullableAnnotation.NotAnnotated) + ctx.Compilation.ObjectType.WithNullableAnnotation(NullableAnnotation.NotAnnotated), + noExpressionInlining ); } private static UserDefinedExpressionMethodMapping? TryBuildExpressionMapping( SimpleMappingBuilderContext ctx, - IMethodSymbol methodSymbol + IMethodSymbol methodSymbol, + bool noExpressionInlining ) { if (methodSymbol.IsGenericMethod) @@ -455,7 +473,13 @@ IMethodSymbol methodSymbol var sourceType = ctx.SymbolAccessor.UpgradeNullable(funcTypeArgs.TypeArguments[0]); var targetType = ctx.SymbolAccessor.UpgradeNullable(funcTypeArgs.TypeArguments[1]); - return new UserDefinedExpressionMethodMapping(methodSymbol, sourceType, targetType, ctx.SymbolAccessor.UpgradeNullable(returnType)); + return new UserDefinedExpressionMethodMapping( + methodSymbol, + sourceType, + targetType, + ctx.SymbolAccessor.UpgradeNullable(returnType), + noExpressionInlining + ); } private static bool HasTargetOriginalValueParameter(SimpleMappingBuilderContext ctx, IMethodSymbol method) => @@ -490,6 +514,19 @@ out bool hasAttribute return userMappingAttr ?? new UserMappingConfiguration(); } + private static bool GetNoExpressionInlining(SimpleMappingBuilderContext ctx, IMethodSymbol method, bool isExternal) + { + if (ctx.SymbolAccessor.HasAttribute(method)) + return true; + + if (!isExternal) + return ctx.Configuration.Mapper.NoExpressionInlining; + + // the configuration of external mappers is not merged into ctx.Configuration, + // build it from the MapperAttribute of the containing type and the default configuration + return ctx.BuildMapperConfiguration(method.ContainingType).NoExpressionInlining; + } + private static IEnumerable ExtractNamedUserImplementedMappings( SimpleMappingBuilderContext ctx, ITypeSymbol mapperSymbol, diff --git a/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs b/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs index e14e23f0d6..9083fdc309 100644 --- a/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs +++ b/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs @@ -233,9 +233,9 @@ public static class DiagnosticDescriptors public static readonly DiagnosticDescriptor EnumMappingNotSupportedInProjectionMappings = new( "RMG032", "The enum mapping strategy ByName, ByValueCheckDefined, explicit enum mappings and ignored enum values cannot be used in projection mappings", - "The enum mapping strategy ByName, ByValueCheckDefined, explicit enum mappings and ignored enum values cannot be used in projection mappings to map from {0} to {1}", + "The enum mapping strategy ByName, ByValueCheckDefined, explicit enum mappings and ignored enum values cannot be used in projection mappings to map from {0} to {1}, consider applying [MapperNoExpressionInlining] to the mapping method or Mapper(NoExpressionInlining = true) to the containing mapper", DiagnosticCategories.Mapper, - DiagnosticSeverity.Warning, + DiagnosticSeverity.Error, true ); diff --git a/src/Riok.Mapperly/Riok.Mapperly.targets b/src/Riok.Mapperly/Riok.Mapperly.targets index 6a1a0f4e24..e46aa1f1d4 100644 --- a/src/Riok.Mapperly/Riok.Mapperly.targets +++ b/src/Riok.Mapperly/Riok.Mapperly.targets @@ -23,5 +23,6 @@ + diff --git a/test/Riok.Mapperly.Abstractions.Tests/_snapshots/PublicApiTest.PublicApiHasNotChanged.verified.cs b/test/Riok.Mapperly.Abstractions.Tests/_snapshots/PublicApiTest.PublicApiHasNotChanged.verified.cs index f2602782f8..c790dc75b0 100644 --- a/test/Riok.Mapperly.Abstractions.Tests/_snapshots/PublicApiTest.PublicApiHasNotChanged.verified.cs +++ b/test/Riok.Mapperly.Abstractions.Tests/_snapshots/PublicApiTest.PublicApiHasNotChanged.verified.cs @@ -1,4 +1,4 @@ -[assembly: System.Runtime.Versioning.TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName=".NET Standard 2.0")] +[assembly: System.Runtime.Versioning.TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName=".NET Standard 2.0")] namespace Riok.Mapperly.Abstractions { public enum EnumMappingStrategy @@ -138,6 +138,7 @@ public MapperAttribute() { } public Riok.Mapperly.Abstractions.IgnoreObsoleteMembersStrategy IgnoreObsoleteMembersStrategy { get; set; } public Riok.Mapperly.Abstractions.MemberVisibility IncludedConstructors { get; set; } public Riok.Mapperly.Abstractions.MemberVisibility IncludedMembers { get; set; } + public bool NoExpressionInlining { get; set; } public bool PreferParameterlessConstructors { get; set; } public Riok.Mapperly.Abstractions.PropertyNameMappingStrategy PropertyNameMappingStrategy { get; set; } public Riok.Mapperly.Abstractions.RequiredMappingStrategy RequiredEnumMappingStrategy { get; set; } @@ -208,6 +209,12 @@ public MapperIgnoreTargetValueAttribute(object target) { } } [System.AttributeUsage(System.AttributeTargets.Method)] [System.Diagnostics.Conditional("MAPPERLY_ABSTRACTIONS_SCOPE_RUNTIME")] + public sealed class MapperNoExpressionInliningAttribute : System.Attribute + { + public MapperNoExpressionInliningAttribute() { } + } + [System.AttributeUsage(System.AttributeTargets.Method)] + [System.Diagnostics.Conditional("MAPPERLY_ABSTRACTIONS_SCOPE_RUNTIME")] public sealed class MapperRequiredMappingAttribute : System.Attribute { public MapperRequiredMappingAttribute(Riok.Mapperly.Abstractions.RequiredMappingStrategy requiredMappingStrategy) { } diff --git a/test/Riok.Mapperly.IntegrationTests/Mapper/ProjectionMapper.cs b/test/Riok.Mapperly.IntegrationTests/Mapper/ProjectionMapper.cs index edfab900ee..62105e5d15 100644 --- a/test/Riok.Mapperly.IntegrationTests/Mapper/ProjectionMapper.cs +++ b/test/Riok.Mapperly.IntegrationTests/Mapper/ProjectionMapper.cs @@ -8,7 +8,18 @@ namespace Riok.Mapperly.IntegrationTests.Mapper { - [Mapper(EnumMappingStrategy = EnumMappingStrategy.ByValue, AutoUserMappings = false)] + [Mapper(NoExpressionInlining = true)] + public static partial class EnumProjectionMapper + { + [MapEnum(EnumMappingStrategy.ByValue)] + public static partial TestEnumDtoByValue MapToTestEnumDtoByValue(TestEnum source); + + [MapEnum(EnumMappingStrategy.ByName)] + public static partial TestEnumDtoByName MapToTestEnumDtoByName(TestEnum source); + } + + [UseStaticMapper(typeof(EnumProjectionMapper))] + [Mapper(AutoUserMappings = false)] public static partial class ProjectionMapper { public static partial IQueryable ProjectToDto(this IQueryable q); diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ExpressionShouldMapObject_NET8_0.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ExpressionShouldMapObject_NET8_0.verified.txt index 8ed74a080b..45aca6d760 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ExpressionShouldMapObject_NET8_0.verified.txt +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ExpressionShouldMapObject_NET8_0.verified.txt @@ -9,7 +9,7 @@ NestedNullableTargetNotNullable: {}, StringNullableTargetNotNullable: fooBar, EnumValue: DtoValue2, - EnumName: 10, + EnumName: Value10, EnumRawValue: 30, EnumStringValue: Value10, EnumReverseStringValue: Value10, diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ExpressionShouldMapObject_NETFRAMEWORK4_8.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ExpressionShouldMapObject_NETFRAMEWORK4_8.verified.txt index 83f8d4d3d9..ee60f58fc9 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ExpressionShouldMapObject_NETFRAMEWORK4_8.verified.txt +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ExpressionShouldMapObject_NETFRAMEWORK4_8.verified.txt @@ -9,7 +9,7 @@ NestedNullableTargetNotNullable: {}, StringNullableTargetNotNullable: fooBar, EnumValue: DtoValue2, - EnumName: 10, + EnumName: Value10, EnumRawValue: 30, EnumStringValue: Value10, EnumReverseStringValue: Value10, diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ExpressionShouldTranslateToQuery_NET8_0.verified.sql b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ExpressionShouldTranslateToQuery_NET8_0.verified.sql index 481f89469c..3dc53a70b3 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ExpressionShouldTranslateToQuery_NET8_0.verified.sql +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ExpressionShouldTranslateToQuery_NET8_0.verified.sql @@ -1,7 +1,7 @@ SELECT "o"."CtorValue", "o"."IntValue", "o"."IntInitOnlyValue", "o"."RequiredValue", "o"."StringValue", "o"."RenamedStringValue", "i"."IdValue", "i0"."IdValue", CASE WHEN "t"."IntValue" IS NOT NULL THEN "t"."IntValue" ELSE 0 -END, "t"."IntValue" IS NOT NULL, "t"."IntValue", "t0"."IntValue" IS NOT NULL, "t0"."IntValue", COALESCE("o"."StringNullableTargetNotNullable", ''), "o0"."Id", "o0"."CtorValue", "o0"."DateTimeValueTargetDateOnly", "o0"."DateTimeValueTargetTimeOnly", "o0"."EnumName", "o0"."EnumRawValue", "o0"."EnumReverseStringValue", "o0"."EnumStringValue", "o0"."EnumValue", "o0"."FlatteningIdValue", "o0"."IgnoredIntValue", "o0"."IgnoredStringValue", "o0"."IntInitOnlyValue", "o0"."IntValue", "o0"."ManuallyMapped", "o0"."ManuallyMappedModified", "o0"."ManuallyMappedNullableToNonNullable", "o0"."NestedNullableIntValue", "o0"."NestedNullableTargetNotNullableIntValue", "o0"."NullableFlatteningIdValue", "o0"."NullableUnflatteningIdValue", "o0"."RecursiveObjectId", "o0"."RenamedStringValue", "o0"."RequiredValue", "o0"."StringNullableTargetNotNullable", "o0"."StringValue", "o0"."SubObjectSubIntValue", "o0"."UnflatteningIdValue", "o"."Id", "i1"."SubIntValue", "t1"."IntValue", "t1"."TestObjectProjectionId", "t2"."IntValue", CAST("o"."EnumValue" AS INTEGER), CAST("o"."EnumName" AS INTEGER), CAST("o"."EnumRawValue" AS INTEGER), "o"."EnumStringValue", "o"."EnumReverseStringValue", "i1"."SubIntValue" IS NOT NULL, "i1"."BaseIntValue", date("o"."DateTimeValueTargetDateOnly"), "o"."DateTimeValueTargetTimeOnly", "o"."ManuallyMapped", "o"."ManuallyMappedModified" + 10, COALESCE("o"."ManuallyMappedNullableToNonNullable", -1), "t3"."Value", "t3"."Id", "i2"."Id", "i2"."TestObjectProjectionId", "i2"."Value", "l"."Value", "l"."Id" +END, "t"."IntValue" IS NOT NULL, "t"."IntValue", "t0"."IntValue" IS NOT NULL, "t0"."IntValue", COALESCE("o"."StringNullableTargetNotNullable", ''), "o0"."Id", "o0"."CtorValue", "o0"."DateTimeValueTargetDateOnly", "o0"."DateTimeValueTargetTimeOnly", "o0"."EnumName", "o0"."EnumRawValue", "o0"."EnumReverseStringValue", "o0"."EnumStringValue", "o0"."EnumValue", "o0"."FlatteningIdValue", "o0"."IgnoredIntValue", "o0"."IgnoredStringValue", "o0"."IntInitOnlyValue", "o0"."IntValue", "o0"."ManuallyMapped", "o0"."ManuallyMappedModified", "o0"."ManuallyMappedNullableToNonNullable", "o0"."NestedNullableIntValue", "o0"."NestedNullableTargetNotNullableIntValue", "o0"."NullableFlatteningIdValue", "o0"."NullableUnflatteningIdValue", "o0"."RecursiveObjectId", "o0"."RenamedStringValue", "o0"."RequiredValue", "o0"."StringNullableTargetNotNullable", "o0"."StringValue", "o0"."SubObjectSubIntValue", "o0"."UnflatteningIdValue", "o"."Id", "i1"."SubIntValue", "t1"."IntValue", "t1"."TestObjectProjectionId", "t2"."IntValue", "o"."EnumValue", "o"."EnumName", CAST("o"."EnumRawValue" AS INTEGER), "o"."EnumStringValue", "o"."EnumReverseStringValue", "i1"."SubIntValue" IS NOT NULL, "i1"."BaseIntValue", date("o"."DateTimeValueTargetDateOnly"), "o"."DateTimeValueTargetTimeOnly", "o"."ManuallyMapped", "o"."ManuallyMappedModified" + 10, COALESCE("o"."ManuallyMappedNullableToNonNullable", -1), "t3"."Value", "t3"."Id", "i2"."Id", "i2"."TestObjectProjectionId", "i2"."Value", "l"."Value", "l"."Id" FROM "Objects" AS "o" INNER JOIN "IdObject" AS "i" ON "o"."FlatteningIdValue" = "i"."IdValue" LEFT JOIN "IdObject" AS "i0" ON "o"."NullableFlatteningIdValue" = "i0"."IdValue" diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ExpressionShouldTranslateToQuery_NET9_0.verified.sql b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ExpressionShouldTranslateToQuery_NET9_0.verified.sql index 49bb79e593..b2c1853d1b 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ExpressionShouldTranslateToQuery_NET9_0.verified.sql +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ExpressionShouldTranslateToQuery_NET9_0.verified.sql @@ -1,7 +1,7 @@ SELECT "o"."CtorValue", "o"."IntValue", "o"."IntInitOnlyValue", "o"."RequiredValue", "o"."StringValue", "o"."RenamedStringValue", "i"."IdValue", "i0"."IdValue", CASE WHEN "t"."IntValue" IS NOT NULL THEN "t"."IntValue" ELSE 0 -END, "t"."IntValue" IS NOT NULL, "t"."IntValue", "t0"."IntValue" IS NOT NULL, "t0"."IntValue", COALESCE("o"."StringNullableTargetNotNullable", ''), "o0"."Id", "o0"."CtorValue", "o0"."DateTimeValueTargetDateOnly", "o0"."DateTimeValueTargetTimeOnly", "o0"."EnumName", "o0"."EnumRawValue", "o0"."EnumReverseStringValue", "o0"."EnumStringValue", "o0"."EnumValue", "o0"."FlatteningIdValue", "o0"."IgnoredIntValue", "o0"."IgnoredStringValue", "o0"."IntInitOnlyValue", "o0"."IntValue", "o0"."ManuallyMapped", "o0"."ManuallyMappedModified", "o0"."ManuallyMappedNullableToNonNullable", "o0"."NestedNullableIntValue", "o0"."NestedNullableTargetNotNullableIntValue", "o0"."NullableFlatteningIdValue", "o0"."NullableUnflatteningIdValue", "o0"."RecursiveObjectId", "o0"."RenamedStringValue", "o0"."RequiredValue", "o0"."StringNullableTargetNotNullable", "o0"."StringValue", "o0"."SubObjectSubIntValue", "o0"."UnflatteningIdValue", "o"."Id", "i1"."SubIntValue", "t1"."IntValue", "t1"."TestObjectProjectionId", "t2"."IntValue", CAST("o"."EnumValue" AS INTEGER), CAST("o"."EnumName" AS INTEGER), CAST("o"."EnumRawValue" AS INTEGER), CASE "o"."EnumStringValue" +END, "t"."IntValue" IS NOT NULL, "t"."IntValue", "t0"."IntValue" IS NOT NULL, "t0"."IntValue", COALESCE("o"."StringNullableTargetNotNullable", ''), "o0"."Id", "o0"."CtorValue", "o0"."DateTimeValueTargetDateOnly", "o0"."DateTimeValueTargetTimeOnly", "o0"."EnumName", "o0"."EnumRawValue", "o0"."EnumReverseStringValue", "o0"."EnumStringValue", "o0"."EnumValue", "o0"."FlatteningIdValue", "o0"."IgnoredIntValue", "o0"."IgnoredStringValue", "o0"."IntInitOnlyValue", "o0"."IntValue", "o0"."ManuallyMapped", "o0"."ManuallyMappedModified", "o0"."ManuallyMappedNullableToNonNullable", "o0"."NestedNullableIntValue", "o0"."NestedNullableTargetNotNullableIntValue", "o0"."NullableFlatteningIdValue", "o0"."NullableUnflatteningIdValue", "o0"."RecursiveObjectId", "o0"."RenamedStringValue", "o0"."RequiredValue", "o0"."StringNullableTargetNotNullable", "o0"."StringValue", "o0"."SubObjectSubIntValue", "o0"."UnflatteningIdValue", "o"."Id", "i1"."SubIntValue", "t1"."IntValue", "t1"."TestObjectProjectionId", "t2"."IntValue", "o"."EnumValue", "o"."EnumName", CAST("o"."EnumRawValue" AS INTEGER), CASE "o"."EnumStringValue" WHEN 10 THEN 'Value10' WHEN 20 THEN 'Value20' WHEN 30 THEN 'Value30' diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ExpressionShouldTranslateToResult.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ExpressionShouldTranslateToResult.verified.txt index fe59dd382c..7ce990a727 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ExpressionShouldTranslateToResult.verified.txt +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ExpressionShouldTranslateToResult.verified.txt @@ -61,7 +61,7 @@ ] }, EnumValue: DtoValue2, - EnumName: 10, + EnumName: Value10, EnumStringValue: 0, EnumReverseStringValue: Value10, ManuallyMapped: { @@ -82,7 +82,7 @@ NestedNullableTargetNotNullable: {}, StringNullableTargetNotNullable: fooBar, EnumValue: DtoValue2, - EnumName: 10, + EnumName: Value10, EnumRawValue: 30, EnumStringValue: Value10, EnumReverseStringValue: Value10, diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ProjectionShouldTranslateToQuery_NET8_0.verified.sql b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ProjectionShouldTranslateToQuery_NET8_0.verified.sql index 481f89469c..3dc53a70b3 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ProjectionShouldTranslateToQuery_NET8_0.verified.sql +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ProjectionShouldTranslateToQuery_NET8_0.verified.sql @@ -1,7 +1,7 @@ SELECT "o"."CtorValue", "o"."IntValue", "o"."IntInitOnlyValue", "o"."RequiredValue", "o"."StringValue", "o"."RenamedStringValue", "i"."IdValue", "i0"."IdValue", CASE WHEN "t"."IntValue" IS NOT NULL THEN "t"."IntValue" ELSE 0 -END, "t"."IntValue" IS NOT NULL, "t"."IntValue", "t0"."IntValue" IS NOT NULL, "t0"."IntValue", COALESCE("o"."StringNullableTargetNotNullable", ''), "o0"."Id", "o0"."CtorValue", "o0"."DateTimeValueTargetDateOnly", "o0"."DateTimeValueTargetTimeOnly", "o0"."EnumName", "o0"."EnumRawValue", "o0"."EnumReverseStringValue", "o0"."EnumStringValue", "o0"."EnumValue", "o0"."FlatteningIdValue", "o0"."IgnoredIntValue", "o0"."IgnoredStringValue", "o0"."IntInitOnlyValue", "o0"."IntValue", "o0"."ManuallyMapped", "o0"."ManuallyMappedModified", "o0"."ManuallyMappedNullableToNonNullable", "o0"."NestedNullableIntValue", "o0"."NestedNullableTargetNotNullableIntValue", "o0"."NullableFlatteningIdValue", "o0"."NullableUnflatteningIdValue", "o0"."RecursiveObjectId", "o0"."RenamedStringValue", "o0"."RequiredValue", "o0"."StringNullableTargetNotNullable", "o0"."StringValue", "o0"."SubObjectSubIntValue", "o0"."UnflatteningIdValue", "o"."Id", "i1"."SubIntValue", "t1"."IntValue", "t1"."TestObjectProjectionId", "t2"."IntValue", CAST("o"."EnumValue" AS INTEGER), CAST("o"."EnumName" AS INTEGER), CAST("o"."EnumRawValue" AS INTEGER), "o"."EnumStringValue", "o"."EnumReverseStringValue", "i1"."SubIntValue" IS NOT NULL, "i1"."BaseIntValue", date("o"."DateTimeValueTargetDateOnly"), "o"."DateTimeValueTargetTimeOnly", "o"."ManuallyMapped", "o"."ManuallyMappedModified" + 10, COALESCE("o"."ManuallyMappedNullableToNonNullable", -1), "t3"."Value", "t3"."Id", "i2"."Id", "i2"."TestObjectProjectionId", "i2"."Value", "l"."Value", "l"."Id" +END, "t"."IntValue" IS NOT NULL, "t"."IntValue", "t0"."IntValue" IS NOT NULL, "t0"."IntValue", COALESCE("o"."StringNullableTargetNotNullable", ''), "o0"."Id", "o0"."CtorValue", "o0"."DateTimeValueTargetDateOnly", "o0"."DateTimeValueTargetTimeOnly", "o0"."EnumName", "o0"."EnumRawValue", "o0"."EnumReverseStringValue", "o0"."EnumStringValue", "o0"."EnumValue", "o0"."FlatteningIdValue", "o0"."IgnoredIntValue", "o0"."IgnoredStringValue", "o0"."IntInitOnlyValue", "o0"."IntValue", "o0"."ManuallyMapped", "o0"."ManuallyMappedModified", "o0"."ManuallyMappedNullableToNonNullable", "o0"."NestedNullableIntValue", "o0"."NestedNullableTargetNotNullableIntValue", "o0"."NullableFlatteningIdValue", "o0"."NullableUnflatteningIdValue", "o0"."RecursiveObjectId", "o0"."RenamedStringValue", "o0"."RequiredValue", "o0"."StringNullableTargetNotNullable", "o0"."StringValue", "o0"."SubObjectSubIntValue", "o0"."UnflatteningIdValue", "o"."Id", "i1"."SubIntValue", "t1"."IntValue", "t1"."TestObjectProjectionId", "t2"."IntValue", "o"."EnumValue", "o"."EnumName", CAST("o"."EnumRawValue" AS INTEGER), "o"."EnumStringValue", "o"."EnumReverseStringValue", "i1"."SubIntValue" IS NOT NULL, "i1"."BaseIntValue", date("o"."DateTimeValueTargetDateOnly"), "o"."DateTimeValueTargetTimeOnly", "o"."ManuallyMapped", "o"."ManuallyMappedModified" + 10, COALESCE("o"."ManuallyMappedNullableToNonNullable", -1), "t3"."Value", "t3"."Id", "i2"."Id", "i2"."TestObjectProjectionId", "i2"."Value", "l"."Value", "l"."Id" FROM "Objects" AS "o" INNER JOIN "IdObject" AS "i" ON "o"."FlatteningIdValue" = "i"."IdValue" LEFT JOIN "IdObject" AS "i0" ON "o"."NullableFlatteningIdValue" = "i0"."IdValue" diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ProjectionShouldTranslateToQuery_NET9_0.verified.sql b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ProjectionShouldTranslateToQuery_NET9_0.verified.sql index 49bb79e593..b2c1853d1b 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ProjectionShouldTranslateToQuery_NET9_0.verified.sql +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ProjectionShouldTranslateToQuery_NET9_0.verified.sql @@ -1,7 +1,7 @@ SELECT "o"."CtorValue", "o"."IntValue", "o"."IntInitOnlyValue", "o"."RequiredValue", "o"."StringValue", "o"."RenamedStringValue", "i"."IdValue", "i0"."IdValue", CASE WHEN "t"."IntValue" IS NOT NULL THEN "t"."IntValue" ELSE 0 -END, "t"."IntValue" IS NOT NULL, "t"."IntValue", "t0"."IntValue" IS NOT NULL, "t0"."IntValue", COALESCE("o"."StringNullableTargetNotNullable", ''), "o0"."Id", "o0"."CtorValue", "o0"."DateTimeValueTargetDateOnly", "o0"."DateTimeValueTargetTimeOnly", "o0"."EnumName", "o0"."EnumRawValue", "o0"."EnumReverseStringValue", "o0"."EnumStringValue", "o0"."EnumValue", "o0"."FlatteningIdValue", "o0"."IgnoredIntValue", "o0"."IgnoredStringValue", "o0"."IntInitOnlyValue", "o0"."IntValue", "o0"."ManuallyMapped", "o0"."ManuallyMappedModified", "o0"."ManuallyMappedNullableToNonNullable", "o0"."NestedNullableIntValue", "o0"."NestedNullableTargetNotNullableIntValue", "o0"."NullableFlatteningIdValue", "o0"."NullableUnflatteningIdValue", "o0"."RecursiveObjectId", "o0"."RenamedStringValue", "o0"."RequiredValue", "o0"."StringNullableTargetNotNullable", "o0"."StringValue", "o0"."SubObjectSubIntValue", "o0"."UnflatteningIdValue", "o"."Id", "i1"."SubIntValue", "t1"."IntValue", "t1"."TestObjectProjectionId", "t2"."IntValue", CAST("o"."EnumValue" AS INTEGER), CAST("o"."EnumName" AS INTEGER), CAST("o"."EnumRawValue" AS INTEGER), CASE "o"."EnumStringValue" +END, "t"."IntValue" IS NOT NULL, "t"."IntValue", "t0"."IntValue" IS NOT NULL, "t0"."IntValue", COALESCE("o"."StringNullableTargetNotNullable", ''), "o0"."Id", "o0"."CtorValue", "o0"."DateTimeValueTargetDateOnly", "o0"."DateTimeValueTargetTimeOnly", "o0"."EnumName", "o0"."EnumRawValue", "o0"."EnumReverseStringValue", "o0"."EnumStringValue", "o0"."EnumValue", "o0"."FlatteningIdValue", "o0"."IgnoredIntValue", "o0"."IgnoredStringValue", "o0"."IntInitOnlyValue", "o0"."IntValue", "o0"."ManuallyMapped", "o0"."ManuallyMappedModified", "o0"."ManuallyMappedNullableToNonNullable", "o0"."NestedNullableIntValue", "o0"."NestedNullableTargetNotNullableIntValue", "o0"."NullableFlatteningIdValue", "o0"."NullableUnflatteningIdValue", "o0"."RecursiveObjectId", "o0"."RenamedStringValue", "o0"."RequiredValue", "o0"."StringNullableTargetNotNullable", "o0"."StringValue", "o0"."SubObjectSubIntValue", "o0"."UnflatteningIdValue", "o"."Id", "i1"."SubIntValue", "t1"."IntValue", "t1"."TestObjectProjectionId", "t2"."IntValue", "o"."EnumValue", "o"."EnumName", CAST("o"."EnumRawValue" AS INTEGER), CASE "o"."EnumStringValue" WHEN 10 THEN 'Value10' WHEN 20 THEN 'Value20' WHEN 30 THEN 'Value30' diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ProjectionShouldTranslateToResult.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ProjectionShouldTranslateToResult.verified.txt index fe59dd382c..7ce990a727 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ProjectionShouldTranslateToResult.verified.txt +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ProjectionShouldTranslateToResult.verified.txt @@ -61,7 +61,7 @@ ] }, EnumValue: DtoValue2, - EnumName: 10, + EnumName: Value10, EnumStringValue: 0, EnumReverseStringValue: Value10, ManuallyMapped: { @@ -82,7 +82,7 @@ NestedNullableTargetNotNullable: {}, StringNullableTargetNotNullable: fooBar, EnumValue: DtoValue2, - EnumName: 10, + EnumName: Value10, EnumRawValue: 30, EnumStringValue: Value10, EnumReverseStringValue: Value10, diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.SnapshotGeneratedSource_NET8_0.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.SnapshotGeneratedSource_NET8_0.verified.cs index 72abee8f9c..8d126fa198 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.SnapshotGeneratedSource_NET8_0.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.SnapshotGeneratedSource_NET8_0.verified.cs @@ -39,8 +39,8 @@ public static partial class ProjectionMapper } ) ) : default, - EnumValue = (global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue)x.EnumValue, - EnumName = (global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName)x.EnumName, + EnumValue = global::Riok.Mapperly.IntegrationTests.Mapper.EnumProjectionMapper.MapToTestEnumDtoByValue(x.EnumValue), + EnumName = global::Riok.Mapperly.IntegrationTests.Mapper.EnumProjectionMapper.MapToTestEnumDtoByName(x.EnumName), EnumRawValue = (byte)x.EnumRawValue, EnumStringValue = (string)x.EnumStringValue.ToString(), EnumReverseStringValue = global::System.Enum.Parse(x.EnumReverseStringValue, false), @@ -121,8 +121,8 @@ public static partial class ProjectionMapper } ) ) : default, - EnumValue = (global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue)x.EnumValue, - EnumName = (global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName)x.EnumName, + EnumValue = global::Riok.Mapperly.IntegrationTests.Mapper.EnumProjectionMapper.MapToTestEnumDtoByValue(x.EnumValue), + EnumName = global::Riok.Mapperly.IntegrationTests.Mapper.EnumProjectionMapper.MapToTestEnumDtoByName(x.EnumName), EnumRawValue = (byte)x.EnumRawValue, EnumStringValue = (string)x.EnumStringValue.ToString(), EnumReverseStringValue = global::System.Enum.Parse(x.EnumReverseStringValue, false), @@ -187,8 +187,8 @@ public static partial class ProjectionMapper { target.NullableReadOnlyObjectCollection = null; } - target.EnumValue = (global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue)testObject.EnumValue; - target.EnumName = (global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName)testObject.EnumName; + target.EnumValue = global::Riok.Mapperly.IntegrationTests.Mapper.EnumProjectionMapper.MapToTestEnumDtoByValue(testObject.EnumValue); + target.EnumName = global::Riok.Mapperly.IntegrationTests.Mapper.EnumProjectionMapper.MapToTestEnumDtoByName(testObject.EnumName); target.EnumRawValue = (byte)testObject.EnumRawValue; target.EnumStringValue = MapToString(testObject.EnumStringValue); target.EnumReverseStringValue = MapToTestEnumDtoByName(testObject.EnumReverseStringValue); diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.SnapshotGeneratedSource_NETFRAMEWORK4_8.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.SnapshotGeneratedSource_NETFRAMEWORK4_8.verified.cs index be69037d66..c1c98f9e85 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.SnapshotGeneratedSource_NETFRAMEWORK4_8.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.SnapshotGeneratedSource_NETFRAMEWORK4_8.verified.cs @@ -39,8 +39,8 @@ public static partial class ProjectionMapper } ) ) : default, - EnumValue = (global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue)x.EnumValue, - EnumName = (global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName)x.EnumName, + EnumValue = global::Riok.Mapperly.IntegrationTests.Mapper.EnumProjectionMapper.MapToTestEnumDtoByValue(x.EnumValue), + EnumName = global::Riok.Mapperly.IntegrationTests.Mapper.EnumProjectionMapper.MapToTestEnumDtoByName(x.EnumName), EnumRawValue = (byte)x.EnumRawValue, EnumStringValue = (string)x.EnumStringValue.ToString(), EnumReverseStringValue = (global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName)global::System.Enum.Parse( @@ -125,8 +125,8 @@ public static partial class ProjectionMapper } ) ) : default, - EnumValue = (global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue)x.EnumValue, - EnumName = (global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName)x.EnumName, + EnumValue = global::Riok.Mapperly.IntegrationTests.Mapper.EnumProjectionMapper.MapToTestEnumDtoByValue(x.EnumValue), + EnumName = global::Riok.Mapperly.IntegrationTests.Mapper.EnumProjectionMapper.MapToTestEnumDtoByName(x.EnumName), EnumRawValue = (byte)x.EnumRawValue, EnumStringValue = (string)x.EnumStringValue.ToString(), EnumReverseStringValue = (global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName)global::System.Enum.Parse( @@ -195,8 +195,8 @@ public static partial class ProjectionMapper { target.NullableReadOnlyObjectCollection = null; } - target.EnumValue = (global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue)testObject.EnumValue; - target.EnumName = (global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName)testObject.EnumName; + target.EnumValue = global::Riok.Mapperly.IntegrationTests.Mapper.EnumProjectionMapper.MapToTestEnumDtoByValue(testObject.EnumValue); + target.EnumName = global::Riok.Mapperly.IntegrationTests.Mapper.EnumProjectionMapper.MapToTestEnumDtoByName(testObject.EnumName); target.EnumRawValue = (byte)testObject.EnumRawValue; target.EnumStringValue = MapToString(testObject.EnumStringValue); target.EnumReverseStringValue = MapToTestEnumDtoByName(testObject.EnumReverseStringValue); diff --git a/test/Riok.Mapperly.Tests/Mapping/EnumTest.cs b/test/Riok.Mapperly.Tests/Mapping/EnumTest.cs index 112cc18884..897ca8e1e1 100644 --- a/test/Riok.Mapperly.Tests/Mapping/EnumTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/EnumTest.cs @@ -43,6 +43,36 @@ public void EnumToOtherEnumTypeShouldCast() .HaveSingleMethodBody("return (global::E2)source;"); } + [Fact] + public void EnumToOtherEnumByNameWithDifferentUnderlyingTypesShouldSwitch() + { + var source = TestSourceBuilder.Mapping( + "E1", + "E2", + TestSourceBuilderOptions.Default with + { + EnumMappingStrategy = EnumMappingStrategy.ByName, + }, + "enum E1 : short {A, B, C}", + "enum E2 : byte {A, B, C}" + ); + + TestHelper + .GenerateMapper(source) + .Should() + .HaveSingleMethodBody( + """ + return source switch + { + global::E1.A => global::E2.A, + global::E1.B => global::E2.B, + global::E1.C => global::E2.C, + _ => throw new global::System.ArgumentOutOfRangeException(nameof(source), source, "The value of enum E1 is not supported"), + }; + """ + ); + } + [Fact] public void CustomClassToEnumWithBaseTypeCastShouldCast() { diff --git a/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionEnumTest.cs b/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionEnumTest.cs index d0ad6e6688..52b3161673 100644 --- a/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionEnumTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionEnumTest.cs @@ -41,7 +41,16 @@ TestSourceBuilderOptions.Default with .Should() .HaveDiagnostic( DiagnosticDescriptors.EnumMappingNotSupportedInProjectionMappings, - "The enum mapping strategy ByName, ByValueCheckDefined, explicit enum mappings and ignored enum values cannot be used in projection mappings to map from C to D" + "The enum mapping strategy ByName, ByValueCheckDefined, explicit enum mappings and ignored enum values cannot be used in projection mappings to map from C to D, consider applying [MapperNoExpressionInlining] to the mapping method or Mapper(NoExpressionInlining = true) to the containing mapper" + ) + .HaveDiagnostic(DiagnosticDescriptors.CouldNotMapMember, "Could not map member A.Value of type C to B.Value of type D") + .HaveDiagnostic( + DiagnosticDescriptors.SourceMemberNotMapped, + "The member Value on the mapping source type A is not mapped to any member on the mapping target type B" + ) + .HaveDiagnostic( + DiagnosticDescriptors.SourceMemberNotFound, + "The member Value on the mapping target type B was not found on the mapping source type A" ) .HaveAssertedAllDiagnostics(); } @@ -71,13 +80,56 @@ TestSourceBuilderOptions.Default with .Should() .HaveDiagnostic( DiagnosticDescriptors.EnumMappingNotSupportedInProjectionMappings, - "The enum mapping strategy ByName, ByValueCheckDefined, explicit enum mappings and ignored enum values cannot be used in projection mappings to map from C to D" + "The enum mapping strategy ByName, ByValueCheckDefined, explicit enum mappings and ignored enum values cannot be used in projection mappings to map from C to D, consider applying [MapperNoExpressionInlining] to the mapping method or Mapper(NoExpressionInlining = true) to the containing mapper" ) - .HaveDiagnostic(DiagnosticDescriptors.TargetEnumValueNotMapped, "Enum member Value2 (200) on D not found on source enum C") - .HaveDiagnostic(DiagnosticDescriptors.SourceEnumValueNotMapped, "Enum member Value2 (100) on C not found on target enum D") .HaveAssertedAllDiagnostics(); } + [Fact] + public void EnumToAnotherEnumByNameWithMapperNoExpressionInliningShouldNotInline() + { + var source = TestSourceBuilder.CSharp( + """ + using Riok.Mapperly.Abstractions; + using System.Linq; + + public class A { public C Value { get; set; } } + + public class B { public D Value { get; set; } } + + public enum C { Value1, Value2 } + + public enum D { Value1, Value2 } + + [Mapper(EnumMappingStrategy = EnumMappingStrategy.ByName, NoExpressionInlining = true)] + public static partial class Mapper + { + public static partial IQueryable Map(IQueryable q); + + public static partial D MapEnum(C src); + } + """ + ); + + TestHelper + .GenerateMapper(source) + .Should() + .HaveMethodBody( + "Map", + """ + #nullable disable + return global::System.Linq.Queryable.Select( + q, + x => new global::B() + { + Value = MapEnum(x.Value), + } + ); + #nullable enable + """ + ); + } + [Fact] public Task EnumToString() { diff --git a/test/Riok.Mapperly.Tests/Mapping/UseStaticMapperTest.cs b/test/Riok.Mapperly.Tests/Mapping/UseStaticMapperTest.cs index 3668ec1ff6..f6b5dc7288 100644 --- a/test/Riok.Mapperly.Tests/Mapping/UseStaticMapperTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/UseStaticMapperTest.cs @@ -429,20 +429,22 @@ public void UseStaticGenericMapperStaticMethodFromAnotherAssemblyAsReference() { var result = ExecuteStaticGenericMapperStaticMethodFromAnotherAssemblyCompilation(asCompilationReference: true); - result.HaveMethodBody( - "ProjectToTarget", - """ - #nullable disable - return global::System.Linq.Queryable.Select( - source, - x => new global::Mapper.Target() - { - DateTime = new global::System.DateTimeOffset(x.DateTime, global::System.TimeSpan.Zero), - } - ); - #nullable enable - """ - ); + result + .HaveDiagnostic(DiagnosticDescriptors.QueryableProjectionMappingCannotInline) + .HaveMethodBody( + "ProjectToTarget", + """ + #nullable disable + return global::System.Linq.Queryable.Select( + source, + x => new global::Mapper.Target() + { + DateTime = global::Riok.Mapperly.TestDependency.Mapper.DateTimeMapper.MapToDateTimeOffset(x.DateTime), + } + ); + #nullable enable + """ + ); } /// @@ -773,4 +775,509 @@ public static partial class Mapper .Should() .HaveDiagnostic(DiagnosticDescriptors.QueryableProjectionMappingCannotInline); } + + [Fact] + public void ProjectionWithUseStaticEnumMapperByNameWithDifferentUnderlyingTypesShouldDiagnose() + { + var source = TestSourceBuilder.CSharp( + """ + using Riok.Mapperly.Abstractions; + using System.Linq; + + namespace Source + { + public enum Status : sbyte + { + Planned = 0, + Generated = 1, + Paused = 2, + Running = 3, + } + } + + namespace Target + { + public enum Status : byte + { + Planned = 0, + Generated = 1, + Paused = 2, + Running = 3, + } + } + + public class Entity + { + public int Id { get; set; } + public Source.Status Status { get; set; } + } + + public class Domain + { + public int Id { get; set; } + public Target.Status Status { get; set; } + } + + [Mapper(EnumMappingStrategy = EnumMappingStrategy.ByName)] + public static partial class EnumMappingExtensions + { + public static partial Target.Status MapToStatus(this Source.Status source); + } + + [Mapper] + [UseStaticMapper(typeof(EnumMappingExtensions))] + public static partial class Mapper + { + public static partial IQueryable ProjectToDomain(this IQueryable queryable); + + public static partial Domain MapToDomain(this Entity entity); + } + """ + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic( + DiagnosticDescriptors.EnumMappingNotSupportedInProjectionMappings, + "The enum mapping strategy ByName, ByValueCheckDefined, explicit enum mappings and ignored enum values cannot be used in projection mappings to map from Source.Status to Target.Status, consider applying [MapperNoExpressionInlining] to the mapping method or Mapper(NoExpressionInlining = true) to the containing mapper" + ) + .HaveDiagnostic(DiagnosticDescriptors.QueryableProjectionMappingCannotInline) + .HaveAssertedAllDiagnostics(); + } + + [Fact] + public void ProjectionWithUseStaticEnumMapperByNameWithNoExpressionInliningShouldWork() + { + var source = TestSourceBuilder.CSharp( + """ + using Riok.Mapperly.Abstractions; + using System.Linq; + + namespace Source + { + public enum Status : sbyte + { + Planned = 0, + Generated = 1, + Paused = 2, + Running = 3, + } + } + + namespace Target + { + public enum Status : byte + { + Planned = 0, + Generated = 1, + Paused = 2, + Running = 3, + } + } + + public class Entity + { + public int Id { get; set; } + public Source.Status Status { get; set; } + } + + public class Domain + { + public int Id { get; set; } + public Target.Status Status { get; set; } + } + + [Mapper(EnumMappingStrategy = EnumMappingStrategy.ByName)] + public static partial class EnumMappingExtensions + { + [MapperNoExpressionInlining] + public static partial Target.Status MapToStatus(this Source.Status source); + } + + [Mapper] + [UseStaticMapper(typeof(EnumMappingExtensions))] + public static partial class Mapper + { + public static partial IQueryable ProjectToDomain(this IQueryable queryable); + + public static partial Domain MapToDomain(this Entity entity); + } + """ + ); + + var generated = TestHelper.GenerateMapper(source, TestHelperOptions.AllowDiagnostics); + + generated.Diagnostics.ShouldNotContain(x => x.Descriptor.Id == DiagnosticDescriptors.QueryableProjectionMappingCannotInline.Id); + generated + .Should() + .HaveMethodBody( + "MapToDomain", + """ + var target = new global::Domain(); + target.Id = entity.Id; + target.Status = global::EnumMappingExtensions.MapToStatus(entity.Status); + return target; + """ + ) + .HaveMethodBody( + "ProjectToDomain", + """ + #nullable disable + return global::System.Linq.Queryable.Select( + queryable, + x => new global::Domain() + { + Id = x.Id, + Status = global::EnumMappingExtensions.MapToStatus(x.Status), + } + ); + #nullable enable + """ + ); + } + + [Fact] + public void MapperNoExpressionInliningOnNonProjectionMethodShouldHaveNoEffect() + { + var source = TestSourceBuilder.CSharp( + """ + using Riok.Mapperly.Abstractions; + + namespace Source + { + public enum Status : sbyte + { + Planned = 0, + Generated = 1, + Paused = 2, + Running = 3, + } + } + + namespace Target + { + public enum Status : byte + { + Planned = 0, + Generated = 1, + Paused = 2, + Running = 3, + } + } + + public class Entity + { + public int Id { get; set; } + public Source.Status Status { get; set; } + } + + public class Domain + { + public int Id { get; set; } + public Target.Status Status { get; set; } + } + + [Mapper(EnumMappingStrategy = EnumMappingStrategy.ByName, NoExpressionInlining = true)] + public static partial class EnumMappingExtensions + { + [MapperNoExpressionInlining] + public static partial Target.Status MapToStatus(this Source.Status source); + } + + [Mapper] + [UseStaticMapper(typeof(EnumMappingExtensions))] + public static partial class Mapper + { + public static partial Domain MapToDomain(this Entity entity); + } + """ + ); + + TestHelper + .GenerateMapper(source) + .Should() + .HaveMethodBody( + "MapToDomain", + """ + var target = new global::Domain(); + target.Id = entity.Id; + target.Status = global::EnumMappingExtensions.MapToStatus(entity.Status); + return target; + """ + ); + } + + [Fact] + public void ProjectionWithUsedMapperNoExpressionInliningShouldNotInline() + { + var source = TestSourceBuilder.CSharp( + """ + using Riok.Mapperly.Abstractions; + using System.Linq; + + namespace Source + { + public enum Status : sbyte + { + Planned = 0, + Generated = 1, + Paused = 2, + Running = 3, + } + } + + namespace Target + { + public enum Status : byte + { + Planned = 0, + Generated = 1, + Paused = 2, + Running = 3, + } + } + + public class Entity + { + public int Id { get; set; } + public Source.Status Status { get; set; } + } + + public class Domain + { + public int Id { get; set; } + public Target.Status Status { get; set; } + } + + [Mapper(EnumMappingStrategy = EnumMappingStrategy.ByName, NoExpressionInlining = true)] + public static partial class EnumMappingExtensions + { + public static partial Target.Status MapToStatus(this Source.Status source); + } + + [Mapper] + [UseStaticMapper(typeof(EnumMappingExtensions))] + public static partial class Mapper + { + public static partial IQueryable ProjectToDomain(this IQueryable queryable); + + public static partial Domain MapToDomain(this Entity entity); + } + """ + ); + + var generated = TestHelper.GenerateMapper(source, TestHelperOptions.AllowDiagnostics); + + generated.Diagnostics.ShouldNotContain(x => x.Descriptor.Id == DiagnosticDescriptors.QueryableProjectionMappingCannotInline.Id); + generated + .Should() + .HaveMethodBody( + "MapToDomain", + """ + var target = new global::Domain(); + target.Id = entity.Id; + target.Status = global::EnumMappingExtensions.MapToStatus(entity.Status); + return target; + """ + ) + .HaveMethodBody( + "ProjectToDomain", + """ + #nullable disable + return global::System.Linq.Queryable.Select( + queryable, + x => new global::Domain() + { + Id = x.Id, + Status = global::EnumMappingExtensions.MapToStatus(x.Status), + } + ); + #nullable enable + """ + ); + } + + [Fact] + public void ProjectionWithUsedMapperDefaultsNoExpressionInliningShouldNotInline() + { + var source = TestSourceBuilder.CSharp( + """ + using Riok.Mapperly.Abstractions; + using System.Linq; + + [assembly: MapperDefaults(NoExpressionInlining = true)] + + namespace Source + { + public enum Status : sbyte + { + Planned = 0, + Generated = 1, + Paused = 2, + Running = 3, + } + } + + namespace Target + { + public enum Status : byte + { + Planned = 0, + Generated = 1, + Paused = 2, + Running = 3, + } + } + + public class Entity + { + public int Id { get; set; } + public Source.Status Status { get; set; } + } + + public class Domain + { + public int Id { get; set; } + public Target.Status Status { get; set; } + } + + [Mapper(EnumMappingStrategy = EnumMappingStrategy.ByName)] + public static partial class EnumMappingExtensions + { + public static partial Target.Status MapToStatus(this Source.Status source); + } + + [Mapper(NoExpressionInlining = false)] + [UseStaticMapper(typeof(EnumMappingExtensions))] + public static partial class Mapper + { + public static partial IQueryable ProjectToDomain(this IQueryable queryable); + + public static partial Domain MapToDomain(this Entity entity); + } + """ + ); + + var generated = TestHelper.GenerateMapper(source, TestHelperOptions.AllowDiagnostics); + + generated.Diagnostics.ShouldNotContain(x => x.Descriptor.Id == DiagnosticDescriptors.QueryableProjectionMappingCannotInline.Id); + generated + .Should() + .HaveMethodBody( + "MapToDomain", + """ + var target = new global::Domain(); + target.Id = entity.Id; + target.Status = global::EnumMappingExtensions.MapToStatus(entity.Status); + return target; + """ + ) + .HaveMethodBody( + "ProjectToDomain", + """ + #nullable disable + return global::System.Linq.Queryable.Select( + queryable, + x => new global::Domain() + { + Id = x.Id, + Status = global::EnumMappingExtensions.MapToStatus(x.Status), + } + ); + #nullable enable + """ + ); + } + + [Fact] + public void ProjectionWithCalledMethodNoExpressionInliningShouldNotInline() + { + var source = TestSourceBuilder.CSharp( + """ + using Riok.Mapperly.Abstractions; + using System.Linq; + + namespace Source + { + public enum Status : sbyte + { + Planned = 0, + Generated = 1, + Paused = 2, + Running = 3, + } + } + + namespace Target + { + public enum Status : byte + { + Planned = 0, + Generated = 1, + Paused = 2, + Running = 3, + } + } + + public class Entity + { + public int Id { get; set; } + public Source.Status Status { get; set; } + } + + public class Domain + { + public int Id { get; set; } + public Target.Status Status { get; set; } + } + + [Mapper(EnumMappingStrategy = EnumMappingStrategy.ByName)] + public static partial class EnumMappingExtensions + { + [MapperNoExpressionInlining] + public static partial Target.Status MapToStatus(this Source.Status source); + } + + [Mapper] + [UseStaticMapper(typeof(EnumMappingExtensions))] + public static partial class Mapper + { + public static partial IQueryable ProjectToDomain(this IQueryable queryable); + + public static partial Domain MapToDomain(this Entity entity); + } + """ + ); + + var generated = TestHelper.GenerateMapper(source, TestHelperOptions.AllowDiagnostics); + + generated.Diagnostics.ShouldNotContain(x => x.Descriptor.Id == DiagnosticDescriptors.QueryableProjectionMappingCannotInline.Id); + generated + .Should() + .HaveMethodBody( + "MapToDomain", + """ + var target = new global::Domain(); + target.Id = entity.Id; + target.Status = global::EnumMappingExtensions.MapToStatus(entity.Status); + return target; + """ + ) + .HaveMethodBody( + "ProjectToDomain", + """ + #nullable disable + return global::System.Linq.Queryable.Select( + queryable, + x => new global::Domain() + { + Id = x.Id, + Status = global::EnumMappingExtensions.MapToStatus(x.Status), + } + ); + #nullable enable + """ + ); + } }