Skip to content
494 changes: 494 additions & 0 deletions docs/library/mapping-methods.md
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please change the explanation to focus on "implementing the Select method within a class" without prominently featuring EFCore support. The file name should also be changed accordingly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 7300fec. Renamed file to mapping-methods.md and refocused documentation on "implementing Select methods within a class." EFCore support is now presented as one use case among several (reusability, organization, testability). The documentation now emphasizes general usage patterns first before showing EFCore-specific scenarios.

Large diffs are not rendered by default.

152 changes: 152 additions & 0 deletions src/Linqraft.Core/GenerateSourceCodeSnippets.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ IncrementalGeneratorPostInitializationContext context
"LinqraftAutoGeneratedDtoAttribute.g.cs",
LinqraftAutoGeneratedDtoAttribute
);
context.AddSource(
"LinqraftMappingGenerateAttribute.g.cs",
LinqraftMappingGenerateAttribute
);
}

// Generate expression and headers
Expand Down Expand Up @@ -231,6 +235,49 @@ internal sealed class LinqraftAutoGeneratedDtoAttribute : Attribute
}
""";

/// <summary>
/// Attribute to mark methods for Linqraft mapping generation.
/// This attribute is used to generate extension methods instead of using interceptors,
/// which is useful for EFCore query (pre)compilation and other scenarios where interceptors
/// are not supported.
/// </summary>
[StringSyntax("csharp")]
public const string LinqraftMappingGenerateAttribute = $$"""
{{CommentHeaderPartOnProd}}
#nullable enable
using System;
using System.ComponentModel;
using Microsoft.CodeAnalysis;

namespace Linqraft
{
/// <summary>
/// Marks a method for Linqraft mapping generation. The method must be in a static partial class
/// and contain a SelectExpr call. Linqraft will generate an extension method with the specified
/// name that contains the compiled Select expression.
/// This is useful for EFCore query (pre)compilation support where interceptors cannot be used.
/// </summary>
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
[EmbeddedAttribute]
internal sealed class LinqraftMappingGenerateAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the LinqraftMappingGenerateAttribute class.
/// </summary>
/// <param name="methodName">The name of the extension method to generate.</param>
public LinqraftMappingGenerateAttribute(string methodName)
{
MethodName = methodName;
}

/// <summary>
/// Gets the name of the extension method to generate.
/// </summary>
public string MethodName { get; }
}
}
""";

[StringSyntax("csharp")]
public const string SelectExprExtensions = $$""""
{{CommentHeaderPartOnProd}}
Expand Down Expand Up @@ -367,4 +414,109 @@ private static string GenerateCommentHeaderPart()
[System.Runtime.CompilerServices.OverloadResolutionPriorityAttribute(-1)]
#endif
""";

/// <summary>
/// Generates a mapping class wrapper with the specified methods
/// </summary>
public static string BuildMappingClassCode(
INamedTypeSymbol containingClass,
List<string> methods
)
{
var namespaceName = containingClass.ContainingNamespace?.ToDisplayString();
var hasNamespace = namespaceName != null && namespaceName != "<global namespace>";
var firstIndent = CodeFormatter.Indent(hasNamespace ? 1 : 0);
var indent = CodeFormatter.Indent(1);

// Determine accessibility - use the class's declared accessibility
var accessibility = containingClass.DeclaredAccessibility switch
{
Accessibility.Public => "public",
Accessibility.Internal => "internal",
Accessibility.Private => "private",
_ => "internal",
};

var methodsPart = methods
.Select(method =>
method
.Split('\n')
.Select(m => $"{firstIndent}{indent}{m}")
.Aggregate((a, b) => $"{a}\n{b}")
)
.Aggregate((a, b) => $"{a}\n{b}");
var classPart = $$"""
{{firstIndent}}{{accessibility}} static partial class {{containingClass.Name}}
{{firstIndent}}{
{{methodsPart}}
{{firstIndent}}}
""";
var classWithNamespacePart = hasNamespace
? $$"""
namespace {{namespaceName}}
{
{{classPart}}
}
"""
: classPart;

return $$"""
{{GenerateCommentHeaderPart()}}
{{GenerateHeaderFlagsPart}}
{{GenerateHeaderUsingPart}}
{{classWithNamespacePart}}
""";
}

/// <summary>
/// Generates an extension method for a mapping method (without interceptor)
/// </summary>
public static string GenerateMappingMethod(SelectExprInfo info)
{
if (info.MappingMethodName == null || info.MappingContainingClass == null)
return "";

// Analyze the DTO structure
var dtoStructure = info.GenerateDtoStructure();

if (dtoStructure == null || dtoStructure.Properties.Count == 0)
return "";

// Get the method name from the mapping info
var methodName = info.MappingMethodName;

// Get the DTO class name
var dtoClassName = info.GetParentDtoClassName(dtoStructure);

// Get return type prefix (IQueryable or IEnumerable)
var returnTypePrefix = info.GetReturnTypePrefix();

var sourceTypeFullName = dtoStructure.SourceTypeFullName;

// Build the extension method
var sb = new System.Text.StringBuilder();
sb.AppendLine($"/// <summary>");
sb.AppendLine($"/// Extension method generated by LinqraftMappingGenerate attribute");
sb.AppendLine($"/// </summary>");

// Generate method signature as an extension method
sb.AppendLine($"internal static {returnTypePrefix}<{dtoClassName}> {methodName}(");
sb.AppendLine($" this {returnTypePrefix}<{sourceTypeFullName}> source)");
sb.AppendLine($"{{");
sb.AppendLine($" return source.Select({info.LambdaParameterName} => new {dtoClassName}");
sb.AppendLine($" {{");

// Generate property assignments
foreach (var prop in dtoStructure.Properties)
{
var assignment = info.GeneratePropertyAssignment(prop, 8);
sb.AppendLine($" {prop.Name} = {assignment},");
}

sb.AppendLine($" }});");
sb.AppendLine($"}}");
sb.AppendLine();

return sb.ToString();
}
}
26 changes: 19 additions & 7 deletions src/Linqraft.Core/SelectExprInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,18 @@ public abstract record SelectExprInfo
/// </summary>
public ITypeSymbol? CaptureArgumentType { get; init; }

/// <summary>
/// The name of the method to generate when using LinqraftMappingGenerate attribute.
/// If null, use the normal interceptor-based generation.
/// </summary>
public string? MappingMethodName { get; init; }

/// <summary>
/// The containing class for the mapping method when using LinqraftMappingGenerate attribute.
/// This should be a static partial class.
/// </summary>
public INamedTypeSymbol? MappingContainingClass { get; init; }

/// <summary>
/// Generates DTO class information (including nested DTOs)
/// </summary>
Expand All @@ -87,25 +99,25 @@ public abstract record SelectExprInfo
/// <summary>
/// Generates the DTO structure for analysis and unique ID generation
/// </summary>
protected abstract DtoStructure GenerateDtoStructure();
public abstract DtoStructure GenerateDtoStructure();

/// <summary>
/// Gets the class name for a DTO structure
/// </summary>
protected abstract string GetClassName(DtoStructure structure);
public abstract string GetClassName(DtoStructure structure);

/// <summary>
/// Gets the parent (root) DTO class name
/// </summary>
protected abstract string GetParentDtoClassName(DtoStructure structure);
public abstract string GetParentDtoClassName(DtoStructure structure);

/// <summary>
/// Gets the namespace where DTOs will be placed
/// </summary>
protected abstract string GetDtoNamespace();
public abstract string GetDtoNamespace();

// Get expression type string (for documentation)
protected abstract string GetExprTypeString();
public abstract string GetExprTypeString();

/// <summary>
/// Gets the full name for a nested DTO class using the structure.
Expand Down Expand Up @@ -218,7 +230,7 @@ protected bool IsEnumerableInvocation()
/// <summary>
/// Gets the return type prefix based on whether it's IQueryable or IEnumerable
/// </summary>
protected string GetReturnTypePrefix()
public string GetReturnTypePrefix()
{
return IsEnumerableInvocation() ? "IEnumerable" : "IQueryable";
}
Expand Down Expand Up @@ -264,7 +276,7 @@ protected string GenerateMethodHeaderPart(string dtoName, InterceptableLocation
/// <summary>
/// Generates property assignment code for a DTO property
/// </summary>
protected string GeneratePropertyAssignment(DtoProperty property, int indents)
public string GeneratePropertyAssignment(DtoProperty property, int indents)
{
var expression = property.OriginalExpression;
var syntax = property.OriginalSyntax;
Expand Down
10 changes: 5 additions & 5 deletions src/Linqraft.Core/SelectExprInfoAnonymous.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public record SelectExprInfoAnonymous : SelectExprInfo
/// <summary>
/// Generates the DTO structure for unique ID generation
/// </summary>
protected override DtoStructure GenerateDtoStructure()
public override DtoStructure GenerateDtoStructure()
{
return DtoStructure.AnalyzeAnonymousType(
AnonymousObject,
Expand All @@ -39,21 +39,21 @@ protected override DtoStructure GenerateDtoStructure()
/// <summary>
/// Gets the DTO class name (empty for anonymous types)
/// </summary>
protected override string GetClassName(DtoStructure structure) => "";
public override string GetClassName(DtoStructure structure) => "";

/// <summary>
/// Gets the parent DTO class name (empty for anonymous types)
/// </summary>
protected override string GetParentDtoClassName(DtoStructure structure) => "";
public override string GetParentDtoClassName(DtoStructure structure) => "";

/// <summary>
/// Gets the namespace where DTOs will be placed
/// Anonymous types don't generate separate DTOs, but return caller namespace for consistency
/// </summary>
protected override string GetDtoNamespace() => CallerNamespace;
public override string GetDtoNamespace() => CallerNamespace;

// Get expression type string (for documentation)
protected override string GetExprTypeString() => "anonymous";
public override string GetExprTypeString() => "anonymous";

/// <summary>
/// Generates static field declarations for pre-built expressions (if enabled)
Expand Down
10 changes: 5 additions & 5 deletions src/Linqraft.Core/SelectExprInfoExplicitDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ private List<string> GetParentAccessibilities()
/// <summary>
/// Generates the DTO structure for unique ID generation
/// </summary>
protected override DtoStructure GenerateDtoStructure()
public override DtoStructure GenerateDtoStructure()
{
return DtoStructure.AnalyzeAnonymousType(
AnonymousObject,
Expand Down Expand Up @@ -243,7 +243,7 @@ private string GetAccessibilityString(IPropertySymbol propertySymbol)
/// When NestedDtoUseHashNamespace is enabled, the class name is just "{BestName}Dto"
/// Otherwise, it includes the hash suffix: "{BestName}Dto_{hash}"
/// </summary>
protected override string GetClassName(DtoStructure structure)
public override string GetClassName(DtoStructure structure)
{
if (Configuration?.NestedDtoUseHashNamespace == true)
{
Expand All @@ -255,15 +255,15 @@ protected override string GetClassName(DtoStructure structure)
/// <summary>
/// Gets the parent DTO class name
/// </summary>
protected override string GetParentDtoClassName(DtoStructure structure) => ExplicitDtoName;
public override string GetParentDtoClassName(DtoStructure structure) => ExplicitDtoName;

/// <summary>
/// Gets the namespace where DTOs will be placed
/// </summary>
protected override string GetDtoNamespace() => GetActualDtoNamespace();
public override string GetDtoNamespace() => GetActualDtoNamespace();

// Get expression type string (for documentation)
protected override string GetExprTypeString() => "explicit";
public override string GetExprTypeString() => "explicit";

/// <summary>
/// Generates static field declarations for pre-built expressions (if enabled)
Expand Down
10 changes: 5 additions & 5 deletions src/Linqraft.Core/SelectExprInfoNamed.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public record SelectExprInfoNamed : SelectExprInfo
/// <summary>
/// Generates the DTO structure for unique ID generation
/// </summary>
protected override DtoStructure GenerateDtoStructure()
public override DtoStructure GenerateDtoStructure()
{
return DtoStructure.AnalyzeNamedType(
ObjectCreation,
Expand All @@ -39,23 +39,23 @@ protected override DtoStructure GenerateDtoStructure()
/// <summary>
/// Gets the DTO class name (uses the source type name)
/// </summary>
protected override string GetClassName(DtoStructure structure) => structure.SourceTypeName;
public override string GetClassName(DtoStructure structure) => structure.SourceTypeName;

/// <summary>
/// Gets the parent DTO class name (fully qualified)
/// </summary>
protected override string GetParentDtoClassName(DtoStructure structure) =>
public override string GetParentDtoClassName(DtoStructure structure) =>
structure.SourceTypeFullName;

/// <summary>
/// Gets the namespace where DTOs will be placed
/// Named types use the DTO's own namespace
/// </summary>
protected override string GetDtoNamespace() =>
public override string GetDtoNamespace() =>
SourceType.ContainingNamespace?.ToDisplayString() ?? CallerNamespace;

// Get expression type string (for documentation)
protected override string GetExprTypeString() => "predefined";
public override string GetExprTypeString() => "predefined";

/// <summary>
/// Generates static field declarations for pre-built expressions (if enabled)
Expand Down
35 changes: 35 additions & 0 deletions src/Linqraft.Core/SelectExprMappingInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace Linqraft.Core;

/// <summary>
/// Information about a method marked with [LinqraftMappingGenerate] attribute
/// </summary>
public record SelectExprMappingInfo
{
/// <summary>
/// The method declaration with the attribute
/// </summary>
public required MethodDeclarationSyntax MethodDeclaration { get; init; }

/// <summary>
/// The name of the method to generate (from attribute parameter)
/// </summary>
public required string TargetMethodName { get; init; }

/// <summary>
/// The containing class (must be static partial)
/// </summary>
public required INamedTypeSymbol ContainingClass { get; init; }

/// <summary>
/// The semantic model for this method
/// </summary>
public required SemanticModel SemanticModel { get; init; }

/// <summary>
/// The namespace where the class is defined
/// </summary>
public required string ContainingNamespace { get; init; }
}
Loading