diff --git a/docs/docs/configuration/mapper.mdx b/docs/docs/configuration/mapper.mdx
index fc947dd218..28e123fa11 100644
--- a/docs/docs/configuration/mapper.mdx
+++ b/docs/docs/configuration/mapper.mdx
@@ -185,3 +185,24 @@ dotnet_diagnostic.RMG020.severity = error # Unmapped source member
### Strict enum mappings
To enforce strict enum mappings set `RMG037` and `RMG038` to error, see [strict enum mappings](./enum.mdx).
+
+### Static methods in instantiable class
+
+When you need to generate static methods inside nonstatic/instantiable class you can use `GenerateStaticMethods` option.
+
+```csharp
+public interface IMyInterface
+{
+ static abstract CarDto ToDto(Car car);
+}
+
+// highlight-start
+[Mapper(GenerateStaticMethods = true)]
+// highlight-end
+public partial class CarMapper : IMyInterface
+{
+ [MapperIgnoreTarget(nameof(CarDto.MakeId))]
+ [MapperIgnoreSource(nameof(Car.Id))]
+ public static partial CarDto ToDto(Car car);
+}
+```
diff --git a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs
index 2f73dcd219..a08d804937 100644
--- a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs
+++ b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs
@@ -86,4 +86,9 @@ public sealed class MapperAttribute : Attribute
/// Defaults to .
///
public IgnoreObsoleteMembersStrategy IgnoreObsoleteMembersStrategy { get; set; } = IgnoreObsoleteMembersStrategy.None;
+
+ ///
+ /// Enables static methods in instantiable mappers (e.g. public partial class A { public static partial string MapToString(int value); }
)
+ ///
+ public bool GenerateStaticMethods { get; set; }
}
diff --git a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt
index 94add9f456..d41396e9db 100644
--- a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt
+++ b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt
@@ -24,6 +24,8 @@ Riok.Mapperly.Abstractions.MapperAttribute.EnumMappingIgnoreCase.get -> bool
Riok.Mapperly.Abstractions.MapperAttribute.EnumMappingIgnoreCase.set -> void
Riok.Mapperly.Abstractions.MapperAttribute.EnumMappingStrategy.get -> Riok.Mapperly.Abstractions.EnumMappingStrategy
Riok.Mapperly.Abstractions.MapperAttribute.EnumMappingStrategy.set -> void
+Riok.Mapperly.Abstractions.MapperAttribute.GenerateStaticMethods.get -> bool
+Riok.Mapperly.Abstractions.MapperAttribute.GenerateStaticMethods.set -> void
Riok.Mapperly.Abstractions.MapperAttribute.IgnoreObsoleteMembersStrategy.get -> Riok.Mapperly.Abstractions.IgnoreObsoleteMembersStrategy
Riok.Mapperly.Abstractions.MapperAttribute.IgnoreObsoleteMembersStrategy.set -> void
Riok.Mapperly.Abstractions.MapperAttribute.MapperAttribute() -> void
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs
index 5bf8920cf6..942c64f701 100644
--- a/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs
@@ -25,6 +25,7 @@ public abstract class MethodMapping : NewInstanceMapping
private readonly ITypeSymbol _returnType;
private string? _methodName;
+ private bool? _methodIsStatic;
protected MethodMapping(ITypeSymbol sourceType, ITypeSymbol targetType)
: base(sourceType, targetType)
@@ -47,6 +48,7 @@ ITypeSymbol targetType
ReferenceHandlerParameter = referenceHandlerParameter;
_accessibility = method.DeclaredAccessibility;
_methodName = method.Name;
+ _methodIsStatic = method.IsStatic;
_returnType = method.ReturnType.UpgradeNullable();
}
@@ -77,7 +79,7 @@ public virtual MethodDeclarationSyntax BuildMethod(SourceEmitterContext ctx)
ReserveParameterNames(typeMappingBuildContext.NameBuilder, parameters);
return MethodDeclaration(returnType, Identifier(MethodName))
- .WithModifiers(TokenList(BuildModifiers(ctx.IsStatic)))
+ .WithModifiers(TokenList(BuildModifiers(ctx.IsStatic || (_methodIsStatic ?? false))))
.WithParameterList(parameters)
.WithBody(Block(BuildBody(typeMappingBuildContext)));
}
diff --git a/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs b/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs
index ae858eec44..a256e24a04 100644
--- a/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs
+++ b/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs
@@ -16,9 +16,11 @@ internal static IEnumerable ExtractUserMappings(SimpleMappingBuild
// extract user implemented and user defined mappings from mapper
foreach (var methodSymbol in ExtractMethods(mapperSymbol))
{
+ var isStatic = mapperSymbol.IsStatic || ctx.MapperConfiguration.GenerateStaticMethods;
+
var mapping =
- BuilderUserDefinedMapping(ctx, methodSymbol, mapperSymbol.IsStatic)
- ?? BuildUserImplementedMapping(ctx, methodSymbol, null, false, mapperSymbol.IsStatic);
+ BuilderUserDefinedMapping(ctx, methodSymbol, isStatic)
+ ?? BuildUserImplementedMapping(ctx, methodSymbol, null, false, isStatic);
if (mapping != null)
yield return mapping;
}
diff --git a/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs b/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs
index e2b6f36d24..90cf11dee4 100644
--- a/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs
+++ b/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs
@@ -162,7 +162,7 @@ public static class DiagnosticDescriptors
public static readonly DiagnosticDescriptor PartialStaticMethodInInstanceMapper = new DiagnosticDescriptor(
"RMG018",
"Partial static mapping method in an instance mapper",
- "{0} is a partial static mapping method in an instance mapper. Static mapping methods are only supported in static mappers.",
+ $"{{0}} is a partial static mapping method in an instance mapper. Static mapping methods are supported in static mappers or using following mapper configuration: Mapper({nameof(MapperAttribute.GenerateStaticMethods)}=true).",
DiagnosticCategories.Mapper,
DiagnosticSeverity.Error,
true
diff --git a/test/Riok.Mapperly.IntegrationTests/Dto/ITestStaticInterface.cs b/test/Riok.Mapperly.IntegrationTests/Dto/ITestStaticInterface.cs
new file mode 100644
index 0000000000..107cacc890
--- /dev/null
+++ b/test/Riok.Mapperly.IntegrationTests/Dto/ITestStaticInterface.cs
@@ -0,0 +1,9 @@
+namespace Riok.Mapperly.IntegrationTests.Dto
+{
+ public interface ITestStaticInterface
+ {
+#if NET7_0_OR_GREATER
+ static abstract int StaticAbstractDirectInt(int value);
+#endif
+ }
+}
diff --git a/test/Riok.Mapperly.IntegrationTests/Mapper/TestMapperWithStaticMethods.cs b/test/Riok.Mapperly.IntegrationTests/Mapper/TestMapperWithStaticMethods.cs
new file mode 100644
index 0000000000..efd69b22b5
--- /dev/null
+++ b/test/Riok.Mapperly.IntegrationTests/Mapper/TestMapperWithStaticMethods.cs
@@ -0,0 +1,13 @@
+using Riok.Mapperly.Abstractions;
+using Riok.Mapperly.IntegrationTests.Dto;
+
+namespace Riok.Mapperly.IntegrationTests.Mapper
+{
+ [Mapper(GenerateStaticMethods = true)]
+ public partial class TestMapperWithStaticMethods : ITestStaticInterface
+ {
+ public static partial double DirectDouble(double value);
+
+ public static partial int StaticAbstractDirectInt(int value);
+ }
+}
diff --git a/test/Riok.Mapperly.IntegrationTests/TestMapperWithStaticMethodsTest.cs b/test/Riok.Mapperly.IntegrationTests/TestMapperWithStaticMethodsTest.cs
new file mode 100644
index 0000000000..10f09db04d
--- /dev/null
+++ b/test/Riok.Mapperly.IntegrationTests/TestMapperWithStaticMethodsTest.cs
@@ -0,0 +1,44 @@
+using System.Threading.Tasks;
+using Riok.Mapperly.IntegrationTests.Dto;
+using Riok.Mapperly.IntegrationTests.Helpers;
+using Riok.Mapperly.IntegrationTests.Mapper;
+using VerifyXunit;
+using Xunit;
+
+namespace Riok.Mapperly.IntegrationTests
+{
+ [UsesVerify]
+ public class TestMapperWithStaticMethodsTest : BaseMapperTest
+ {
+ [Fact]
+ [VersionedSnapshot(Versions.NET6_0 | Versions.NET7_0)]
+ public Task SnapshotGeneratedSource()
+ {
+ var path = GetGeneratedMapperFilePath(nameof(TestMapperWithStaticMethods));
+ return Verifier.VerifyFile(path);
+ }
+
+ [Theory
+#if !NET7_0_OR_GREATER
+ (Skip = "Requires language version '11.0' or greater")
+#endif
+ ]
+ [InlineData(8)]
+ [InlineData(12)]
+ public void CallStaticInterfaceMemberShouldWork(int value)
+ {
+ Assert.Equal(value, GenericMethod(value));
+
+ static int GenericMethod(int value)
+ where T : ITestStaticInterface
+ {
+#if NET7_0_OR_GREATER
+ return T.StaticAbstractDirectInt(value);
+#else
+ // It's intentional "wrong" value, it's here only to return something
+ return 0;
+#endif
+ }
+ }
+ }
+}
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/TestMapperWithStaticMethodsTest.SnapshotGeneratedSource_NET6_0.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/TestMapperWithStaticMethodsTest.SnapshotGeneratedSource_NET6_0.verified.cs
new file mode 100644
index 0000000000..c682f61f0c
--- /dev/null
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/TestMapperWithStaticMethodsTest.SnapshotGeneratedSource_NET6_0.verified.cs
@@ -0,0 +1,17 @@
+//
+#nullable enable
+namespace Riok.Mapperly.IntegrationTests.Mapper
+{
+ public partial class TestMapperWithStaticMethods
+ {
+ public static partial double DirectDouble(double value)
+ {
+ return value;
+ }
+
+ public static partial int StaticAbstractDirectInt(int value)
+ {
+ return value;
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/TestMapperWithStaticMethodsTest.SnapshotGeneratedSource_NET7_0.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/TestMapperWithStaticMethodsTest.SnapshotGeneratedSource_NET7_0.verified.cs
new file mode 100644
index 0000000000..c682f61f0c
--- /dev/null
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/TestMapperWithStaticMethodsTest.SnapshotGeneratedSource_NET7_0.verified.cs
@@ -0,0 +1,17 @@
+//
+#nullable enable
+namespace Riok.Mapperly.IntegrationTests.Mapper
+{
+ public partial class TestMapperWithStaticMethods
+ {
+ public static partial double DirectDouble(double value)
+ {
+ return value;
+ }
+
+ public static partial int StaticAbstractDirectInt(int value)
+ {
+ return value;
+ }
+ }
+}
\ No newline at end of file