diff --git a/README.md b/README.md index 39be61e..ab688bf 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,99 @@ GROUP BY (COALESCE("u"."FirstName", '') || ' ') || COALESCE("u"."LastName", '') ORDER BY (COALESCE("u"."FirstName", '') || ' ') || COALESCE("u"."LastName", '') ``` +#### How do I expand enum extension methods? +When you have an enum property and want to call an extension method on it (like getting a display name from a `[Display]` attribute), you can use the `ExpandEnumMethods` property on the `[Projectable]` attribute. This will expand the enum method call into a chain of ternary expressions for each enum value, allowing EF Core to translate it to SQL CASE expressions. + +```csharp +public enum OrderStatus +{ + [Display(Name = "Pending Review")] + Pending, + + [Display(Name = "Approved")] + Approved, + + [Display(Name = "Rejected")] + Rejected +} + +public static class EnumExtensions +{ + public static string GetDisplayName(this OrderStatus value) + { + // Your implementation here + return value.ToString(); + } + + public static bool IsApproved(this OrderStatus value) + { + return value == OrderStatus.Approved; + } + + public static int GetSortOrder(this OrderStatus value) + { + return (int)value; + } + + public static string Format(this OrderStatus value, string prefix) + { + return prefix + value.ToString(); + } +} + +public class Order +{ + public int Id { get; set; } + public OrderStatus Status { get; set; } + + [Projectable(ExpandEnumMethods = true)] + public string StatusName => Status.GetDisplayName(); + + [Projectable(ExpandEnumMethods = true)] + public bool IsStatusApproved => Status.IsApproved(); + + [Projectable(ExpandEnumMethods = true)] + public int StatusOrder => Status.GetSortOrder(); + + [Projectable(ExpandEnumMethods = true)] + public string FormattedStatus => Status.Format("Status: "); +} +``` + +This generates expression trees equivalent to: +```csharp +// For StatusName +@this.Status == OrderStatus.Pending ? GetDisplayName(OrderStatus.Pending) + : @this.Status == OrderStatus.Approved ? GetDisplayName(OrderStatus.Approved) + : @this.Status == OrderStatus.Rejected ? GetDisplayName(OrderStatus.Rejected) + : null + +// For IsStatusApproved (boolean) +@this.Status == OrderStatus.Pending ? false + : @this.Status == OrderStatus.Approved ? true + : @this.Status == OrderStatus.Rejected ? false + : default(bool) +``` + +Which EF Core translates to SQL CASE expressions: +```sql +SELECT CASE + WHEN [o].[Status] = 0 THEN N'Pending Review' + WHEN [o].[Status] = 1 THEN N'Approved' + WHEN [o].[Status] = 2 THEN N'Rejected' +END AS [StatusName] +FROM [Orders] AS [o] +``` + +The `ExpandEnumMethods` feature supports: +- **String return types** - returns `null` as the default fallback +- **Boolean return types** - returns `default(bool)` (false) as the default fallback +- **Integer return types** - returns `default(int)` (0) as the default fallback +- **Other value types** - returns `default(T)` as the default fallback +- **Nullable enum types** - wraps the expansion in a null check +- **Methods with parameters** - parameters are passed through to each enum value call +- **Enum properties on navigation properties** - works with nested navigation + #### How does this relate to [Expressionify](https://github.com/ClaveConsulting/Expressionify)? Expressionify is a project that was launched before this project. It has some overlapping features and uses similar approaches. When I first published this project, I was not aware of its existence, so shame on me. Currently, Expressionify targets a more focused scope of what this project is doing, and thereby it seems to be more limiting in its capabilities. Check them out though! diff --git a/src/EntityFrameworkCore.Projectables.Abstractions/ProjectableAttribute.cs b/src/EntityFrameworkCore.Projectables.Abstractions/ProjectableAttribute.cs index 4c3082b..58abd20 100644 --- a/src/EntityFrameworkCore.Projectables.Abstractions/ProjectableAttribute.cs +++ b/src/EntityFrameworkCore.Projectables.Abstractions/ProjectableAttribute.cs @@ -23,5 +23,25 @@ public sealed class ProjectableAttribute : Attribute /// or null to get it from the current member. /// public string? UseMemberBody { get; set; } + + /// + /// Get or set whether to expand enum method/extension calls by evaluating them at compile time + /// and generating ternary expressions for each enum value. + /// + /// + /// + /// When enabled, method calls on enum values are resolved at compile time by evaluating + /// the method for each possible enum value and generating a chain of ternary expressions. + /// + /// + /// For example, MyEnumValue.GetDescription() would be expanded to: + /// MyEnumValue == Value1 ? "Description 1" : MyEnumValue == Value2 ? "Description 2" : null + /// + /// + /// This is useful for Where() and OrderBy() clauses where the expression + /// needs to be translated to SQL. + /// + /// + public bool ExpandEnumMethods { get; set; } } } diff --git a/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs b/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs index 98a4b5a..4e27d5a 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs @@ -2,11 +2,6 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Operations; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace EntityFrameworkCore.Projectables.Generator { @@ -16,13 +11,15 @@ public class ExpressionSyntaxRewriter : CSharpSyntaxRewriter readonly INamedTypeSymbol _targetTypeSymbol; readonly SemanticModel _semanticModel; readonly NullConditionalRewriteSupport _nullConditionalRewriteSupport; + readonly bool _expandEnumMethods; readonly SourceProductionContext _context; readonly Stack _conditionalAccessExpressionsStack = new(); - - public ExpressionSyntaxRewriter(INamedTypeSymbol targetTypeSymbol, NullConditionalRewriteSupport nullConditionalRewriteSupport, SemanticModel semanticModel, SourceProductionContext context) + + public ExpressionSyntaxRewriter(INamedTypeSymbol targetTypeSymbol, NullConditionalRewriteSupport nullConditionalRewriteSupport, bool expandEnumMethods, SemanticModel semanticModel, SourceProductionContext context) { _targetTypeSymbol = targetTypeSymbol; _nullConditionalRewriteSupport = nullConditionalRewriteSupport; + _expandEnumMethods = expandEnumMethods; _semanticModel = semanticModel; _context = context; } @@ -56,27 +53,180 @@ public ExpressionSyntaxRewriter(INamedTypeSymbol targetTypeSymbol, NullCondition if (node.Expression is MemberAccessExpressionSyntax memberAccessExpressionSyntax) { var symbol = _semanticModel.GetSymbolInfo(node).Symbol; - if (symbol is IMethodSymbol { IsExtensionMethod: true } methodSymbol) + if (symbol is IMethodSymbol methodSymbol) { - return SyntaxFactory.InvocationExpression( - SyntaxFactory.MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - SyntaxFactory.ParseName(methodSymbol.ContainingType.ToDisplayString(NullableFlowState.None, SymbolDisplayFormat.FullyQualifiedFormat)), - memberAccessExpressionSyntax.Name - ), - node.ArgumentList.WithArguments( - ((ArgumentListSyntax)VisitArgumentList(node.ArgumentList)!).Arguments.Insert(0, SyntaxFactory.Argument( - (ExpressionSyntax)Visit(memberAccessExpressionSyntax.Expression) + // Check if we should expand enum methods + if (_expandEnumMethods) + { + if (TryExpandEnumMethodCall(node, memberAccessExpressionSyntax, methodSymbol, out var expandedExpression)) + { + return expandedExpression; + } + } + + // Fully qualify extension method calls + if (methodSymbol.IsExtensionMethod) + { + return SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.ParseName(methodSymbol.ContainingType.ToDisplayString(NullableFlowState.None, SymbolDisplayFormat.FullyQualifiedFormat)), + memberAccessExpressionSyntax.Name + ), + node.ArgumentList.WithArguments( + ((ArgumentListSyntax)VisitArgumentList(node.ArgumentList)!).Arguments.Insert(0, SyntaxFactory.Argument( + (ExpressionSyntax)Visit(memberAccessExpressionSyntax.Expression) + ) ) ) - ) - ); + ); + } } } return base.VisitInvocationExpression(node); } + private bool TryExpandEnumMethodCall(InvocationExpressionSyntax node, MemberAccessExpressionSyntax memberAccess, IMethodSymbol methodSymbol, out ExpressionSyntax? expandedExpression) + { + expandedExpression = null; + + // Get the receiver expression (the enum instance or variable) + var receiverExpression = memberAccess.Expression; + var receiverTypeInfo = _semanticModel.GetTypeInfo(receiverExpression); + var receiverType = receiverTypeInfo.Type; + + // Handle nullable enum types + ITypeSymbol enumType; + bool isNullable = false; + if (receiverType is INamedTypeSymbol { IsGenericType: true, Name: "Nullable" } nullableType && + nullableType.TypeArguments.Length == 1 && + nullableType.TypeArguments[0].TypeKind == TypeKind.Enum) + { + enumType = nullableType.TypeArguments[0]; + isNullable = true; + } + else if (receiverType?.TypeKind == TypeKind.Enum) + { + enumType = receiverType; + } + else + { + // Not an enum type + return false; + } + + // Get all enum members + var enumMembers = enumType.GetMembers() + .OfType() + .Where(f => f.HasConstantValue) + .ToList(); + + if (enumMembers.Count == 0) + { + return false; + } + + // Visit the receiver expression to transform it (e.g., @this.MyProperty) + var visitedReceiver = (ExpressionSyntax)Visit(receiverExpression); + + // Get the original method (in case of reduced extension method) + var originalMethod = methodSymbol.ReducedFrom ?? methodSymbol; + + // Get the return type of the method to determine the default value + var returnType = methodSymbol.ReturnType; + + // Build a chain of ternary expressions for each enum value + // Start with default(T) as the fallback for non-nullable types, or null for nullable/reference types + ExpressionSyntax? currentExpression; + if (returnType.IsReferenceType || returnType.NullableAnnotation == NullableAnnotation.Annotated || + (returnType is INamedTypeSymbol namedType && namedType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T)) + { + currentExpression = SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression); + } + else + { + // Use default(T) for value types + currentExpression = SyntaxFactory.DefaultExpression( + SyntaxFactory.ParseTypeName(returnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))); + } + + // Build the ternary chain, calling the method on each enum value + foreach (var enumMember in enumMembers.AsEnumerable().Reverse()) + { + // Create the enum value access: EnumType.Value + var enumValueAccess = SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.ParseTypeName(enumType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)), + SyntaxFactory.IdentifierName(enumMember.Name) + ); + + // Create the method call on the enum value: ExtensionClass.Method(EnumType.Value) + var methodCall = CreateMethodCallOnEnumValue(originalMethod, enumValueAccess, node.ArgumentList); + + // Create condition: receiver == EnumType.Value + var condition = SyntaxFactory.BinaryExpression( + SyntaxKind.EqualsExpression, + visitedReceiver, + enumValueAccess + ); + + // Create conditional expression: condition ? methodCall : previousExpression + currentExpression = SyntaxFactory.ConditionalExpression( + condition, + methodCall, + currentExpression + ); + } + + // If nullable, wrap in null check + if (isNullable) + { + var nullCheck = SyntaxFactory.BinaryExpression( + SyntaxKind.EqualsExpression, + visitedReceiver, + SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression) + ); + + currentExpression = SyntaxFactory.ConditionalExpression( + nullCheck, + SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression), + currentExpression + ); + } + + expandedExpression = SyntaxFactory.ParenthesizedExpression(currentExpression); + return true; + } + + private ExpressionSyntax CreateMethodCallOnEnumValue(IMethodSymbol methodSymbol, ExpressionSyntax enumValueExpression, ArgumentListSyntax originalArguments) + { + // Get the fully qualified containing type name + var containingTypeName = methodSymbol.ContainingType.ToDisplayString(NullableFlowState.None, SymbolDisplayFormat.FullyQualifiedFormat); + + // Create the method access expression: ContainingType.MethodName + var methodAccess = SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.ParseName(containingTypeName), + SyntaxFactory.IdentifierName(methodSymbol.Name) + ); + + // Build arguments: the enum value as the first argument (for extension methods), followed by any additional arguments + var arguments = SyntaxFactory.SeparatedList(); + arguments = arguments.Add(SyntaxFactory.Argument(enumValueExpression)); + + // Add any additional arguments from the original call + foreach (var arg in originalArguments.Arguments) + { + arguments = arguments.Add((ArgumentSyntax)Visit(arg)); + } + + return SyntaxFactory.InvocationExpression( + methodAccess, + SyntaxFactory.ArgumentList(arguments) + ); + } + public override SyntaxNode? VisitInterpolation(InterpolationSyntax node) { // Visit the expression first diff --git a/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs b/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs index 339b46b..3692856 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs @@ -57,6 +57,11 @@ static IEnumerable GetNestedInClassPath(ITypeSymbol namedTypeSymbol) .OfType() .FirstOrDefault(); + var expandEnumMethods = projectableAttributeClass.NamedArguments + .Where(x => x.Key == "ExpandEnumMethods") + .Select(x => x.Value.Value is bool b && b) + .FirstOrDefault(); + var memberBody = member; if (useMemberBody is not null) @@ -115,7 +120,7 @@ x is IPropertySymbol xProperty && if (memberBody is null) return null; } - var expressionSyntaxRewriter = new ExpressionSyntaxRewriter(memberSymbol.ContainingType, nullConditionalRewriteSupport, semanticModel, context); + var expressionSyntaxRewriter = new ExpressionSyntaxRewriter(memberSymbol.ContainingType, nullConditionalRewriteSupport, expandEnumMethods, semanticModel, context); var declarationSyntaxRewriter = new DeclarationSyntaxRewriter(semanticModel); var descriptor = new ProjectableDescriptor { diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.FilterOnBooleanEnumExpansion.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.FilterOnBooleanEnumExpansion.DotNet10_0.verified.txt new file mode 100644 index 0000000..df4ace9 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.FilterOnBooleanEnumExpansion.DotNet10_0.verified.txt @@ -0,0 +1,8 @@ +SELECT [o].[Id], [o].[CustomerId], [o].[Priority], [o].[Status] +FROM [Order] AS [o] +WHERE CASE + WHEN [o].[Status] = 0 THEN CAST(0 AS bit) + WHEN [o].[Status] = 1 THEN CAST(1 AS bit) + WHEN [o].[Status] = 2 THEN CAST(0 AS bit) + ELSE CAST(0 AS bit) +END = CAST(1 AS bit) \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.FilterOnBooleanEnumExpansion.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.FilterOnBooleanEnumExpansion.DotNet9_0.verified.txt new file mode 100644 index 0000000..df4ace9 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.FilterOnBooleanEnumExpansion.DotNet9_0.verified.txt @@ -0,0 +1,8 @@ +SELECT [o].[Id], [o].[CustomerId], [o].[Priority], [o].[Status] +FROM [Order] AS [o] +WHERE CASE + WHEN [o].[Status] = 0 THEN CAST(0 AS bit) + WHEN [o].[Status] = 1 THEN CAST(1 AS bit) + WHEN [o].[Status] = 2 THEN CAST(0 AS bit) + ELSE CAST(0 AS bit) +END = CAST(1 AS bit) \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.FilterOnBooleanEnumExpansion.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.FilterOnBooleanEnumExpansion.verified.txt new file mode 100644 index 0000000..df4ace9 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.FilterOnBooleanEnumExpansion.verified.txt @@ -0,0 +1,8 @@ +SELECT [o].[Id], [o].[CustomerId], [o].[Priority], [o].[Status] +FROM [Order] AS [o] +WHERE CASE + WHEN [o].[Status] = 0 THEN CAST(0 AS bit) + WHEN [o].[Status] = 1 THEN CAST(1 AS bit) + WHEN [o].[Status] = 2 THEN CAST(0 AS bit) + ELSE CAST(0 AS bit) +END = CAST(1 AS bit) \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.FilterOnExpandedEnumProperty.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.FilterOnExpandedEnumProperty.DotNet10_0.verified.txt new file mode 100644 index 0000000..51cd671 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.FilterOnExpandedEnumProperty.DotNet10_0.verified.txt @@ -0,0 +1,7 @@ +SELECT [o].[Id], [o].[CustomerId], [o].[Priority], [o].[Status] +FROM [Order] AS [o] +WHERE CASE + WHEN [o].[Status] = 0 THEN N'Pending Review' + WHEN [o].[Status] = 1 THEN N'Approved' + WHEN [o].[Status] = 2 THEN N'Rejected' +END = N'Pending Review' \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.FilterOnExpandedEnumProperty.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.FilterOnExpandedEnumProperty.DotNet9_0.verified.txt new file mode 100644 index 0000000..51cd671 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.FilterOnExpandedEnumProperty.DotNet9_0.verified.txt @@ -0,0 +1,7 @@ +SELECT [o].[Id], [o].[CustomerId], [o].[Priority], [o].[Status] +FROM [Order] AS [o] +WHERE CASE + WHEN [o].[Status] = 0 THEN N'Pending Review' + WHEN [o].[Status] = 1 THEN N'Approved' + WHEN [o].[Status] = 2 THEN N'Rejected' +END = N'Pending Review' \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.FilterOnExpandedEnumProperty.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.FilterOnExpandedEnumProperty.verified.txt new file mode 100644 index 0000000..f3ab7c0 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.FilterOnExpandedEnumProperty.verified.txt @@ -0,0 +1,8 @@ +SELECT [o].[Id], [o].[CustomerId], [o].[Priority], [o].[Status] +FROM [Order] AS [o] +WHERE CASE + WHEN [o].[Status] = 0 THEN N'Pending Review' + WHEN [o].[Status] = 1 THEN N'Approved' + WHEN [o].[Status] = 2 THEN N'Rejected' + ELSE NULL +END = N'Pending Review' \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.OrderByExpandedEnumProperty.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.OrderByExpandedEnumProperty.DotNet10_0.verified.txt new file mode 100644 index 0000000..43bbda6 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.OrderByExpandedEnumProperty.DotNet10_0.verified.txt @@ -0,0 +1,7 @@ +SELECT [o].[Id], [o].[CustomerId], [o].[Priority], [o].[Status] +FROM [Order] AS [o] +ORDER BY CASE + WHEN [o].[Status] = 0 THEN N'Pending Review' + WHEN [o].[Status] = 1 THEN N'Approved' + WHEN [o].[Status] = 2 THEN N'Rejected' +END \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.OrderByExpandedEnumProperty.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.OrderByExpandedEnumProperty.DotNet9_0.verified.txt new file mode 100644 index 0000000..43bbda6 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.OrderByExpandedEnumProperty.DotNet9_0.verified.txt @@ -0,0 +1,7 @@ +SELECT [o].[Id], [o].[CustomerId], [o].[Priority], [o].[Status] +FROM [Order] AS [o] +ORDER BY CASE + WHEN [o].[Status] = 0 THEN N'Pending Review' + WHEN [o].[Status] = 1 THEN N'Approved' + WHEN [o].[Status] = 2 THEN N'Rejected' +END \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.OrderByExpandedEnumProperty.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.OrderByExpandedEnumProperty.verified.txt new file mode 100644 index 0000000..2945827 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.OrderByExpandedEnumProperty.verified.txt @@ -0,0 +1,8 @@ +SELECT [o].[Id], [o].[CustomerId], [o].[Priority], [o].[Status] +FROM [Order] AS [o] +ORDER BY CASE + WHEN [o].[Status] = 0 THEN N'Pending Review' + WHEN [o].[Status] = 1 THEN N'Approved' + WHEN [o].[Status] = 2 THEN N'Rejected' + ELSE NULL +END \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.OrderByIntegerEnumExpansion.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.OrderByIntegerEnumExpansion.DotNet10_0.verified.txt new file mode 100644 index 0000000..55bbdc9 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.OrderByIntegerEnumExpansion.DotNet10_0.verified.txt @@ -0,0 +1,8 @@ +SELECT [o].[Id], [o].[CustomerId], [o].[Priority], [o].[Status] +FROM [Order] AS [o] +ORDER BY CASE + WHEN COALESCE([o].[Priority], 0) = 0 THEN 1 + WHEN COALESCE([o].[Priority], 0) = 1 THEN 2 + WHEN COALESCE([o].[Priority], 0) = 2 THEN 3 + ELSE 0 +END \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.OrderByIntegerEnumExpansion.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.OrderByIntegerEnumExpansion.DotNet9_0.verified.txt new file mode 100644 index 0000000..55bbdc9 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.OrderByIntegerEnumExpansion.DotNet9_0.verified.txt @@ -0,0 +1,8 @@ +SELECT [o].[Id], [o].[CustomerId], [o].[Priority], [o].[Status] +FROM [Order] AS [o] +ORDER BY CASE + WHEN COALESCE([o].[Priority], 0) = 0 THEN 1 + WHEN COALESCE([o].[Priority], 0) = 1 THEN 2 + WHEN COALESCE([o].[Priority], 0) = 2 THEN 3 + ELSE 0 +END \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.OrderByIntegerEnumExpansion.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.OrderByIntegerEnumExpansion.verified.txt new file mode 100644 index 0000000..55bbdc9 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.OrderByIntegerEnumExpansion.verified.txt @@ -0,0 +1,8 @@ +SELECT [o].[Id], [o].[CustomerId], [o].[Priority], [o].[Status] +FROM [Order] AS [o] +ORDER BY CASE + WHEN COALESCE([o].[Priority], 0) = 0 THEN 1 + WHEN COALESCE([o].[Priority], 0) = 1 THEN 2 + WHEN COALESCE([o].[Priority], 0) = 2 THEN 3 + ELSE 0 +END \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectBooleanEnumExpansion.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectBooleanEnumExpansion.DotNet10_0.verified.txt new file mode 100644 index 0000000..32cfbd4 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectBooleanEnumExpansion.DotNet10_0.verified.txt @@ -0,0 +1,7 @@ +SELECT CASE + WHEN [o].[Status] = 0 THEN CAST(0 AS bit) + WHEN [o].[Status] = 1 THEN CAST(1 AS bit) + WHEN [o].[Status] = 2 THEN CAST(0 AS bit) + ELSE CAST(0 AS bit) +END +FROM [Order] AS [o] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectBooleanEnumExpansion.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectBooleanEnumExpansion.DotNet9_0.verified.txt new file mode 100644 index 0000000..32cfbd4 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectBooleanEnumExpansion.DotNet9_0.verified.txt @@ -0,0 +1,7 @@ +SELECT CASE + WHEN [o].[Status] = 0 THEN CAST(0 AS bit) + WHEN [o].[Status] = 1 THEN CAST(1 AS bit) + WHEN [o].[Status] = 2 THEN CAST(0 AS bit) + ELSE CAST(0 AS bit) +END +FROM [Order] AS [o] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectBooleanEnumExpansion.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectBooleanEnumExpansion.verified.txt new file mode 100644 index 0000000..32cfbd4 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectBooleanEnumExpansion.verified.txt @@ -0,0 +1,7 @@ +SELECT CASE + WHEN [o].[Status] = 0 THEN CAST(0 AS bit) + WHEN [o].[Status] = 1 THEN CAST(1 AS bit) + WHEN [o].[Status] = 2 THEN CAST(0 AS bit) + ELSE CAST(0 AS bit) +END +FROM [Order] AS [o] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectEnumMethodWithParameter.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectEnumMethodWithParameter.DotNet10_0.verified.txt new file mode 100644 index 0000000..a75a390 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectEnumMethodWithParameter.DotNet10_0.verified.txt @@ -0,0 +1,6 @@ +SELECT CASE + WHEN [o].[Status] = 0 THEN N'Order Status: Pending' + WHEN [o].[Status] = 1 THEN N'Order Status: Approved' + WHEN [o].[Status] = 2 THEN N'Order Status: Rejected' +END +FROM [Order] AS [o] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectEnumMethodWithParameter.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectEnumMethodWithParameter.DotNet9_0.verified.txt new file mode 100644 index 0000000..a75a390 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectEnumMethodWithParameter.DotNet9_0.verified.txt @@ -0,0 +1,6 @@ +SELECT CASE + WHEN [o].[Status] = 0 THEN N'Order Status: Pending' + WHEN [o].[Status] = 1 THEN N'Order Status: Approved' + WHEN [o].[Status] = 2 THEN N'Order Status: Rejected' +END +FROM [Order] AS [o] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectEnumMethodWithParameter.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectEnumMethodWithParameter.verified.txt new file mode 100644 index 0000000..35f7402 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectEnumMethodWithParameter.verified.txt @@ -0,0 +1,7 @@ +SELECT CASE + WHEN [o].[Status] = 0 THEN N'Order Status: Pending' + WHEN [o].[Status] = 1 THEN N'Order Status: Approved' + WHEN [o].[Status] = 2 THEN N'Order Status: Rejected' + ELSE NULL +END +FROM [Order] AS [o] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectEnumOnNavigationProperty.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectEnumOnNavigationProperty.DotNet10_0.verified.txt new file mode 100644 index 0000000..47ae157 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectEnumOnNavigationProperty.DotNet10_0.verified.txt @@ -0,0 +1,7 @@ +SELECT CASE + WHEN [c].[PreferredPriority] = 0 THEN N'Low Priority' + WHEN [c].[PreferredPriority] = 1 THEN N'Medium Priority' + WHEN [c].[PreferredPriority] = 2 THEN N'High Priority' +END +FROM [OrderWithNavigation] AS [o] +LEFT JOIN [Customer] AS [c] ON [o].[CustomerId] = [c].[Id] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectEnumOnNavigationProperty.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectEnumOnNavigationProperty.DotNet9_0.verified.txt new file mode 100644 index 0000000..47ae157 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectEnumOnNavigationProperty.DotNet9_0.verified.txt @@ -0,0 +1,7 @@ +SELECT CASE + WHEN [c].[PreferredPriority] = 0 THEN N'Low Priority' + WHEN [c].[PreferredPriority] = 1 THEN N'Medium Priority' + WHEN [c].[PreferredPriority] = 2 THEN N'High Priority' +END +FROM [OrderWithNavigation] AS [o] +LEFT JOIN [Customer] AS [c] ON [o].[CustomerId] = [c].[Id] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectEnumOnNavigationProperty.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectEnumOnNavigationProperty.verified.txt new file mode 100644 index 0000000..a00cbba --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectEnumOnNavigationProperty.verified.txt @@ -0,0 +1,8 @@ +SELECT CASE + WHEN [c].[PreferredPriority] = 0 THEN N'Low Priority' + WHEN [c].[PreferredPriority] = 1 THEN N'Medium Priority' + WHEN [c].[PreferredPriority] = 2 THEN N'High Priority' + ELSE NULL +END +FROM [OrderWithNavigation] AS [o] +LEFT JOIN [Customer] AS [c] ON [o].[CustomerId] = [c].[Id] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectExpandedEnumProperty.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectExpandedEnumProperty.DotNet10_0.verified.txt new file mode 100644 index 0000000..d812336 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectExpandedEnumProperty.DotNet10_0.verified.txt @@ -0,0 +1,6 @@ +SELECT CASE + WHEN [o].[Status] = 0 THEN N'Pending Review' + WHEN [o].[Status] = 1 THEN N'Approved' + WHEN [o].[Status] = 2 THEN N'Rejected' +END +FROM [Order] AS [o] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectExpandedEnumProperty.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectExpandedEnumProperty.DotNet9_0.verified.txt new file mode 100644 index 0000000..d812336 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectExpandedEnumProperty.DotNet9_0.verified.txt @@ -0,0 +1,6 @@ +SELECT CASE + WHEN [o].[Status] = 0 THEN N'Pending Review' + WHEN [o].[Status] = 1 THEN N'Approved' + WHEN [o].[Status] = 2 THEN N'Rejected' +END +FROM [Order] AS [o] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectExpandedEnumProperty.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectExpandedEnumProperty.verified.txt new file mode 100644 index 0000000..e490e64 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectExpandedEnumProperty.verified.txt @@ -0,0 +1,7 @@ +SELECT CASE + WHEN [o].[Status] = 0 THEN N'Pending Review' + WHEN [o].[Status] = 1 THEN N'Approved' + WHEN [o].[Status] = 2 THEN N'Rejected' + ELSE NULL +END +FROM [Order] AS [o] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectNullableEnumExpandedProperty.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectNullableEnumExpandedProperty.DotNet10_0.verified.txt new file mode 100644 index 0000000..b727d87 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectNullableEnumExpandedProperty.DotNet10_0.verified.txt @@ -0,0 +1,8 @@ +SELECT CASE + WHEN [o].[Priority] IS NOT NULL THEN CASE + WHEN [o].[Priority] = 0 THEN N'Low Priority' + WHEN [o].[Priority] = 1 THEN N'Medium Priority' + WHEN [o].[Priority] = 2 THEN N'High Priority' + END +END +FROM [Order] AS [o] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectNullableEnumExpandedProperty.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectNullableEnumExpandedProperty.DotNet9_0.verified.txt new file mode 100644 index 0000000..b727d87 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectNullableEnumExpandedProperty.DotNet9_0.verified.txt @@ -0,0 +1,8 @@ +SELECT CASE + WHEN [o].[Priority] IS NOT NULL THEN CASE + WHEN [o].[Priority] = 0 THEN N'Low Priority' + WHEN [o].[Priority] = 1 THEN N'Medium Priority' + WHEN [o].[Priority] = 2 THEN N'High Priority' + END +END +FROM [Order] AS [o] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectNullableEnumExpandedProperty.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectNullableEnumExpandedProperty.verified.txt new file mode 100644 index 0000000..27dd172 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.SelectNullableEnumExpandedProperty.verified.txt @@ -0,0 +1,10 @@ +SELECT CASE + WHEN [o].[Priority] IS NOT NULL THEN CASE + WHEN [o].[Priority] = 0 THEN N'Low Priority' + WHEN [o].[Priority] = 1 THEN N'Medium Priority' + WHEN [o].[Priority] = 2 THEN N'High Priority' + ELSE NULL + END + ELSE NULL +END +FROM [Order] AS [o] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.cs new file mode 100644 index 0000000..d31a85d --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExpandEnumMethodsTests.cs @@ -0,0 +1,221 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using EntityFrameworkCore.Projectables.FunctionalTests.Helpers; +using Microsoft.EntityFrameworkCore; +using VerifyXunit; +using Xunit; + +namespace EntityFrameworkCore.Projectables.FunctionalTests +{ + [UsesVerify] + public class ExpandEnumMethodsTests + { + public enum OrderStatus + { + [Display(Name = "Pending Review")] + Pending, + + [Display(Name = "Approved")] + Approved, + + [Display(Name = "Rejected")] + Rejected + } + + public enum Priority + { + [Description("Low Priority")] + Low, + + [Description("Medium Priority")] + Medium, + + [Description("High Priority")] + High + } + + public record Order + { + public int Id { get; set; } + public OrderStatus Status { get; set; } + public Priority? Priority { get; set; } + public Customer? Customer { get; set; } + + [Projectable(ExpandEnumMethods = true)] + public string StatusName => Status.GetDisplayName(); + + [Projectable(ExpandEnumMethods = true, NullConditionalRewriteSupport = NullConditionalRewriteSupport.Rewrite)] + public string? PriorityDescription => Priority.HasValue ? Priority.Value.GetDescription() : null; + + [Projectable(ExpandEnumMethods = true)] + public bool IsApproved => Status.IsApproved(); + + [Projectable(ExpandEnumMethods = true)] + public int PrioritySortOrder => (Priority ?? ExpandEnumMethodsTests.Priority.Low).GetSortOrder(); + + [Projectable(ExpandEnumMethods = true)] + public string StatusWithPrefix => Status.GetDisplayNameWithPrefix("Order Status: "); + } + + public record Customer + { + public int Id { get; set; } + public string Name { get; set; } = ""; + public Priority PreferredPriority { get; set; } + } + + public record OrderWithNavigation + { + public int Id { get; set; } + public Customer? Customer { get; set; } + + [Projectable(ExpandEnumMethods = true)] + public string CustomerPriorityDescription => Customer!.PreferredPriority.GetDescription(); + } + + [Fact] + public Task FilterOnExpandedEnumProperty() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Where(x => x.StatusName == "Pending Review"); + + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task SelectExpandedEnumProperty() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => x.StatusName); + + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task OrderByExpandedEnumProperty() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .OrderBy(x => x.StatusName); + + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task SelectNullableEnumExpandedProperty() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => x.PriorityDescription); + + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task SelectEnumOnNavigationProperty() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => x.CustomerPriorityDescription); + + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task FilterOnBooleanEnumExpansion() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Where(x => x.IsApproved); + + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task SelectBooleanEnumExpansion() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => x.IsApproved); + + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task OrderByIntegerEnumExpansion() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .OrderBy(x => x.PrioritySortOrder); + + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task SelectEnumMethodWithParameter() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => x.StatusWithPrefix); + + return Verifier.Verify(query.ToQueryString()); + } + } + + public static class EnumExtensions + { + public static string GetDisplayName(this TEnum value) where TEnum : struct, System.Enum + { + var type = value.GetType(); + var memberInfo = type.GetMember(value.ToString())[0]; + var displayAttribute = memberInfo.GetCustomAttributes(typeof(DisplayAttribute), false) + .OfType() + .FirstOrDefault(); + return displayAttribute?.Name ?? value.ToString(); + } + + public static string GetDescription(this TEnum value) where TEnum : struct, System.Enum + { + var type = value.GetType(); + var memberInfo = type.GetMember(value.ToString())[0]; + var descriptionAttribute = memberInfo.GetCustomAttributes(typeof(DescriptionAttribute), false) + .OfType() + .FirstOrDefault(); + return descriptionAttribute?.Description ?? value.ToString(); + } + + public static bool IsApproved(this ExpandEnumMethodsTests.OrderStatus value) + { + return value == ExpandEnumMethodsTests.OrderStatus.Approved; + } + + public static int GetSortOrder(this ExpandEnumMethodsTests.Priority value) + { + return value switch + { + ExpandEnumMethodsTests.Priority.Low => 1, + ExpandEnumMethodsTests.Priority.Medium => 2, + ExpandEnumMethodsTests.Priority.High => 3, + _ => 0 + }; + } + + public static string GetDisplayNameWithPrefix(this ExpandEnumMethodsTests.OrderStatus value, string prefix) + { + return prefix + value.ToString(); + } + } +} diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpandEnumMethodsOnNavigationProperty.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpandEnumMethodsOnNavigationProperty.verified.txt new file mode 100644 index 0000000..966236f --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpandEnumMethodsOnNavigationProperty.verified.txt @@ -0,0 +1,18 @@ +// +#nullable disable +using System; +using System.ComponentModel.DataAnnotations; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_OrderItem_OrderStatusName + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.OrderItem @this) => (@this.Order.Status == global::Foo.OrderStatus.Pending ? global::Foo.EnumExtensions.GetDisplayName(global::Foo.OrderStatus.Pending) : @this.Order.Status == global::Foo.OrderStatus.Approved ? global::Foo.EnumExtensions.GetDisplayName(global::Foo.OrderStatus.Approved) : null); + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpandEnumMethodsReturningBoolean.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpandEnumMethodsReturningBoolean.verified.txt new file mode 100644 index 0000000..318c241 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpandEnumMethodsReturningBoolean.verified.txt @@ -0,0 +1,17 @@ +// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Entity_IsStatusApproved + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Entity @this) => (@this.Status == global::Foo.Status.Pending ? global::Foo.EnumExtensions.IsApproved(global::Foo.Status.Pending) : @this.Status == global::Foo.Status.Approved ? global::Foo.EnumExtensions.IsApproved(global::Foo.Status.Approved) : @this.Status == global::Foo.Status.Rejected ? global::Foo.EnumExtensions.IsApproved(global::Foo.Status.Rejected) : default(bool)); + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpandEnumMethodsReturningInteger.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpandEnumMethodsReturningInteger.verified.txt new file mode 100644 index 0000000..7090676 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpandEnumMethodsReturningInteger.verified.txt @@ -0,0 +1,17 @@ +// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Entity_PrioritySortOrder + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Entity @this) => (@this.Priority == global::Foo.Priority.Low ? global::Foo.EnumExtensions.GetSortOrder(global::Foo.Priority.Low) : @this.Priority == global::Foo.Priority.Medium ? global::Foo.EnumExtensions.GetSortOrder(global::Foo.Priority.Medium) : @this.Priority == global::Foo.Priority.High ? global::Foo.EnumExtensions.GetSortOrder(global::Foo.Priority.High) : default(int)); + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpandEnumMethodsWithDescriptionAttribute.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpandEnumMethodsWithDescriptionAttribute.verified.txt new file mode 100644 index 0000000..8df7bf2 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpandEnumMethodsWithDescriptionAttribute.verified.txt @@ -0,0 +1,18 @@ +// +#nullable disable +using System; +using System.ComponentModel; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Entity_StatusDescription + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Entity @this) => (@this.Status == global::Foo.Status.Pending ? global::Foo.EnumExtensions.GetDescription(global::Foo.Status.Pending) : @this.Status == global::Foo.Status.Approved ? global::Foo.EnumExtensions.GetDescription(global::Foo.Status.Approved) : @this.Status == global::Foo.Status.Rejected ? global::Foo.EnumExtensions.GetDescription(global::Foo.Status.Rejected) : null); + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpandEnumMethodsWithDisplayAttribute.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpandEnumMethodsWithDisplayAttribute.verified.txt new file mode 100644 index 0000000..72bf11c --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpandEnumMethodsWithDisplayAttribute.verified.txt @@ -0,0 +1,18 @@ +// +#nullable disable +using System; +using System.ComponentModel.DataAnnotations; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Entity_MyEnumName + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Entity @this) => (@this.MyValue == global::Foo.CustomEnum.Value1 ? global::Foo.EnumExtensions.GetDisplayName(global::Foo.CustomEnum.Value1) : @this.MyValue == global::Foo.CustomEnum.Value2 ? global::Foo.EnumExtensions.GetDisplayName(global::Foo.CustomEnum.Value2) : null); + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpandEnumMethodsWithMultipleParameters.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpandEnumMethodsWithMultipleParameters.verified.txt new file mode 100644 index 0000000..f626eea --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpandEnumMethodsWithMultipleParameters.verified.txt @@ -0,0 +1,17 @@ +// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Entity_FormattedStatus + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Entity @this) => (@this.Status == global::Foo.Status.Pending ? global::Foo.EnumExtensions.Format(global::Foo.Status.Pending, "[", "]") : @this.Status == global::Foo.Status.Approved ? global::Foo.EnumExtensions.Format(global::Foo.Status.Approved, "[", "]") : @this.Status == global::Foo.Status.Rejected ? global::Foo.EnumExtensions.Format(global::Foo.Status.Rejected, "[", "]") : null); + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpandEnumMethodsWithNullableEnum.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpandEnumMethodsWithNullableEnum.verified.txt new file mode 100644 index 0000000..a38b302 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpandEnumMethodsWithNullableEnum.verified.txt @@ -0,0 +1,18 @@ +// +#nullable disable +using System; +using System.ComponentModel.DataAnnotations; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Entity_MyEnumName + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Entity @this) => @this.MyValue.HasValue ? (@this.MyValue.Value == global::Foo.CustomEnum.First ? global::Foo.EnumExtensions.GetDisplayName(global::Foo.CustomEnum.First) : @this.MyValue.Value == global::Foo.CustomEnum.Second ? global::Foo.EnumExtensions.GetDisplayName(global::Foo.CustomEnum.Second) : null) : null; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpandEnumMethodsWithParameter.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpandEnumMethodsWithParameter.verified.txt new file mode 100644 index 0000000..d1332ed --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpandEnumMethodsWithParameter.verified.txt @@ -0,0 +1,17 @@ +// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Entity_StatusWithPrefix + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Entity @this) => (@this.Status == global::Foo.Status.Pending ? global::Foo.EnumExtensions.GetDisplayNameWithPrefix(global::Foo.Status.Pending, "Status: ") : @this.Status == global::Foo.Status.Approved ? global::Foo.EnumExtensions.GetDisplayNameWithPrefix(global::Foo.Status.Approved, "Status: ") : @this.Status == global::Foo.Status.Rejected ? global::Foo.EnumExtensions.GetDisplayNameWithPrefix(global::Foo.Status.Rejected, "Status: ") : null); + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs index eb50a78..cd93f00 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs @@ -1977,6 +1977,363 @@ public static Dictionary ToDictionary(this Entity entity) return Verifier.Verify(result.GeneratedTrees[0].ToString()); } + [Fact] + public Task ExpandEnumMethodsWithDisplayAttribute() + { + var compilation = CreateCompilation(@" +using System; +using System.ComponentModel.DataAnnotations; +using EntityFrameworkCore.Projectables; + +namespace Foo { + public enum CustomEnum + { + [Display(Name = ""Value 1"")] + Value1, + + [Display(Name = ""Value 2"")] + Value2, + } + + public static class EnumExtensions + { + public static string GetDisplayName(this CustomEnum value) + { + return value.ToString(); + } + } + + public record Entity + { + public int Id { get; set; } + public CustomEnum MyValue { get; set; } + + [Projectable(ExpandEnumMethods = true)] + public string MyEnumName => MyValue.GetDisplayName(); + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task ExpandEnumMethodsWithNullableEnum() + { + var compilation = CreateCompilation(@" +using System; +using System.ComponentModel.DataAnnotations; +using EntityFrameworkCore.Projectables; + +namespace Foo { + public enum CustomEnum + { + [Display(Name = ""First Value"")] + First, + + [Display(Name = ""Second Value"")] + Second, + } + + public static class EnumExtensions + { + public static string GetDisplayName(this CustomEnum value) + { + return value.ToString(); + } + } + + public record Entity + { + public int Id { get; set; } + public CustomEnum? MyValue { get; set; } + + [Projectable(ExpandEnumMethods = true)] + public string MyEnumName => MyValue.HasValue ? MyValue.Value.GetDisplayName() : null; + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task ExpandEnumMethodsWithDescriptionAttribute() + { + var compilation = CreateCompilation(@" +using System; +using System.ComponentModel; +using EntityFrameworkCore.Projectables; + +namespace Foo { + public enum Status + { + [Description(""The item is pending"")] + Pending, + + [Description(""The item is approved"")] + Approved, + + [Description(""The item is rejected"")] + Rejected, + } + + public static class EnumExtensions + { + public static string GetDescription(this Status value) + { + return value.ToString(); + } + } + + public record Entity + { + public int Id { get; set; } + public Status Status { get; set; } + + [Projectable(ExpandEnumMethods = true)] + public string StatusDescription => Status.GetDescription(); + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task ExpandEnumMethodsOnNavigationProperty() + { + var compilation = CreateCompilation(@" +using System; +using System.ComponentModel.DataAnnotations; +using EntityFrameworkCore.Projectables; + +namespace Foo { + public enum OrderStatus + { + [Display(Name = ""Pending Review"")] + Pending, + + [Display(Name = ""Approved"")] + Approved, + } + + public static class EnumExtensions + { + public static string GetDisplayName(this OrderStatus value) + { + return value.ToString(); + } + } + + public record Order + { + public int Id { get; set; } + public OrderStatus Status { get; set; } + } + + public record OrderItem + { + public int Id { get; set; } + public Order Order { get; set; } + + [Projectable(ExpandEnumMethods = true)] + public string OrderStatusName => Order.Status.GetDisplayName(); + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task ExpandEnumMethodsReturningBoolean() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; + +namespace Foo { + public enum Status + { + Pending, + Approved, + Rejected, + } + + public static class EnumExtensions + { + public static bool IsApproved(this Status value) + { + return value == Status.Approved; + } + } + + public record Entity + { + public int Id { get; set; } + public Status Status { get; set; } + + [Projectable(ExpandEnumMethods = true)] + public bool IsStatusApproved => Status.IsApproved(); + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task ExpandEnumMethodsReturningInteger() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; + +namespace Foo { + public enum Priority + { + Low, + Medium, + High, + } + + public static class EnumExtensions + { + public static int GetSortOrder(this Priority value) + { + return (int)value; + } + } + + public record Entity + { + public int Id { get; set; } + public Priority Priority { get; set; } + + [Projectable(ExpandEnumMethods = true)] + public int PrioritySortOrder => Priority.GetSortOrder(); + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task ExpandEnumMethodsWithParameter() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; + +namespace Foo { + public enum Status + { + Pending, + Approved, + Rejected, + } + + public static class EnumExtensions + { + public static string GetDisplayNameWithPrefix(this Status value, string prefix) + { + return prefix + value.ToString(); + } + } + + public record Entity + { + public int Id { get; set; } + public Status Status { get; set; } + + [Projectable(ExpandEnumMethods = true)] + public string StatusWithPrefix => Status.GetDisplayNameWithPrefix(""Status: ""); + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task ExpandEnumMethodsWithMultipleParameters() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; + +namespace Foo { + public enum Status + { + Pending, + Approved, + Rejected, + } + + public static class EnumExtensions + { + public static string Format(this Status value, string prefix, string suffix) + { + return prefix + value.ToString() + suffix; + } + } + + public record Entity + { + public int Id { get; set; } + public Status Status { get; set; } + + [Projectable(ExpandEnumMethods = true)] + public string FormattedStatus => Status.Format(""["", ""]""); + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + #region Helpers Compilation CreateCompilation(string source, bool expectedToCompile = true)