Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Translate ToString() over enums #33706

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -882,10 +882,13 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
}

// if object is nullable, add null safeguard before calling the function
// we special-case Nullable<>.GetValueOrDefault, which doesn't need the safeguard
// we special-case Nullable<>.GetValueOrDefault, which doesn't need the safeguard,
// and Nullable<>.ToString when the object is a nullable value type.
if (methodCallExpression.Object != null
&& @object!.Type.IsNullableType()
&& methodCallExpression.Method.Name != nameof(Nullable<int>.GetValueOrDefault))
&& methodCallExpression.Method.Name != nameof(Nullable<int>.GetValueOrDefault)
&& (!@object!.Type.IsNullableValueType()
|| methodCallExpression.Method.Name != nameof(Nullable<int>.ToString)))
{
var result = (Expression)methodCallExpression.Update(
Expression.Convert(@object, methodCallExpression.Object.Type),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@ namespace Microsoft.EntityFrameworkCore.Query.Internal;
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public class EnumHasFlagTranslator : IMethodCallTranslator
public class EnumMethodTranslator : IMethodCallTranslator
{
private static readonly MethodInfo MethodInfo
private static readonly MethodInfo HasFlagMethodInfo
= typeof(Enum).GetRuntimeMethod(nameof(Enum.HasFlag), [typeof(Enum)])!;

private static readonly MethodInfo ToStringMethodInfo
= typeof(object).GetRuntimeMethod(nameof(ToString), [])!;

private readonly ISqlExpressionFactory _sqlExpressionFactory;

/// <summary>
Expand All @@ -25,7 +28,7 @@ private static readonly MethodInfo MethodInfo
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public EnumHasFlagTranslator(ISqlExpressionFactory sqlExpressionFactory)
public EnumMethodTranslator(ISqlExpressionFactory sqlExpressionFactory)
{
_sqlExpressionFactory = sqlExpressionFactory;
}
Expand All @@ -42,7 +45,7 @@ public EnumHasFlagTranslator(ISqlExpressionFactory sqlExpressionFactory)
IReadOnlyList<SqlExpression> arguments,
IDiagnosticsLogger<DbLoggerCategory.Query> logger)
{
if (Equals(method, MethodInfo)
if (Equals(method, HasFlagMethodInfo)
&& instance != null)
{
var argument = arguments[0];
Expand All @@ -51,6 +54,35 @@ public EnumHasFlagTranslator(ISqlExpressionFactory sqlExpressionFactory)
: _sqlExpressionFactory.Equal(_sqlExpressionFactory.And(instance, argument), argument);
}

if (Equals(method, ToStringMethodInfo)
&& instance is { Type.IsEnum: true, TypeMapping.Converter: ValueConverter converter }
&& converter.GetType() is { IsGenericType: true } converterType)
{
switch (converterType)
{
case not null when converterType.GetGenericTypeDefinition() == typeof(EnumToNumberConverter<,>):
var whenClauses = Enum.GetValues(instance.Type)
.Cast<object>()
.Select(value => new CaseWhenClause(
_sqlExpressionFactory.Constant(value),
_sqlExpressionFactory.Constant(value.ToString(), typeof(string))))
.ToArray();

var elseResult = _sqlExpressionFactory.Coalesce(
_sqlExpressionFactory.Convert(instance, typeof(string)),
_sqlExpressionFactory.Constant(string.Empty));

return _sqlExpressionFactory.Case(instance, whenClauses, elseResult);

case not null when converterType.GetGenericTypeDefinition() == typeof(EnumToStringConverter<>):
// TODO: Unnecessary cast to string, #33733
roji marked this conversation as resolved.
Show resolved Hide resolved
return _sqlExpressionFactory.Convert(instance, typeof(string));

default:
return null;
}
}

return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public RelationalMethodCallTranslatorProvider(RelationalMethodCallTranslatorProv
new CollateTranslator(),
new ContainsTranslator(sqlExpressionFactory),
new LikeTranslator(sqlExpressionFactory),
new EnumHasFlagTranslator(sqlExpressionFactory),
new EnumMethodTranslator(sqlExpressionFactory),
new GetValueOrDefaultTranslator(sqlExpressionFactory),
new ComparisonTranslator(sqlExpressionFactory),
new ByteArraySequenceEqualTranslator(sqlExpressionFactory),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ public SqlServerObjectToStringTranslator(ISqlExpressionFactory sqlExpressionFact
_sqlExpressionFactory.Constant(true.ToString()));
}

// Enums are handled by EnumMethodTranslator

return TypeMapping.TryGetValue(instance.Type, out var storeType)
? _sqlExpressionFactory.Function(
"CONVERT",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ public SqliteObjectToStringTranslator(ISqlExpressionFactory sqlExpressionFactory
_sqlExpressionFactory.Constant(true.ToString()));
}

// Enums are handled by EnumMethodTranslator

return TypeMapping.Contains(instance.Type)
? _sqlExpressionFactory.Convert(instance, typeof(string))
: null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,12 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con
b.HasOne(w => w.Owner).WithMany(g => g.Weapons).HasForeignKey(w => w.OwnerFullName).HasPrincipalKey(g => g.FullName);
});

modelBuilder.Entity<Mission>().Property(m => m.Id).ValueGeneratedNever();
modelBuilder.Entity<Mission>(
b =>
{
b.Property(m => m.Id).ValueGeneratedNever();
b.Property(m => m.Difficulty).HasConversion<string>();
});

modelBuilder.Entity<SquadMission>(
b =>
Expand Down
38 changes: 28 additions & 10 deletions test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,34 @@ public virtual Task ToString_boolean_property_nullable(bool async)
async,
ss => ss.Set<LocustHorde>().Select(lh => lh.Eradicated.ToString()));

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task ToString_enum_property_projection(bool async)
=> AssertQuery(
async,
ss => ss.Set<Gear>().Select(g => g.Rank.ToString()));

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task ToString_nullable_enum_property_projection(bool async)
=> AssertQuery(
async,
ss => ss.Set<Weapon>().Select(w => w.AmmunitionType.ToString()));

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task ToString_enum_contains(bool async)
=> AssertQuery(
async,
ss => ss.Set<Mission>().Where(g => g.Difficulty.ToString().Contains("Med")).Select(g => g.CodeName));

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task ToString_nullable_enum_contains(bool async)
=> AssertQuery(
async,
ss => ss.Set<Weapon>().Where(w => w.AmmunitionType.ToString().Contains("Cart")).Select(g => g.Name));

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Include_multiple_one_to_one_and_one_to_many_self_reference(bool async)
Expand Down Expand Up @@ -3121,16 +3149,6 @@ public virtual Task Projecting_nullable_bool_in_conditional_works(bool async)
new { Prop = cg.Gear != null ? cg.Gear.HasSoulPatch : false }),
e => e.Prop);

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Enum_ToString_is_client_eval(bool async)
=> AssertQuery(
async,
ss =>
ss.Set<Gear>().OrderBy(g => g.SquadId)
.ThenBy(g => g.Nickname)
.Select(g => g.Rank.ToString()));

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Correlated_collections_naked_navigation_with_ToList(bool async)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,8 @@ public static IReadOnlyList<Mission> CreateMissions()
Timeline = new DateTimeOffset(599898024001234567, new TimeSpan(1, 30, 0)),
Duration = new TimeSpan(1, 2, 3),
Date = new DateOnly(2020, 1, 1),
Time = new TimeOnly(15, 30, 10)
Time = new TimeOnly(15, 30, 10),
Difficulty = MissionDifficulty.Low
},
new()
{
Expand All @@ -143,7 +144,8 @@ public static IReadOnlyList<Mission> CreateMissions()
Timeline = new DateTimeOffset(2, 3, 1, 8, 0, 0, new TimeSpan(-5, 0, 0)),
Duration = new TimeSpan(0, 1, 2, 3, 456),
Date = new DateOnly(1990, 11, 10),
Time = new TimeOnly(10, 15, 50, 500)
Time = new TimeOnly(10, 15, 50, 500),
Difficulty = MissionDifficulty.Medium
},
new()
{
Expand All @@ -153,7 +155,8 @@ public static IReadOnlyList<Mission> CreateMissions()
Timeline = new DateTimeOffset(10, 5, 3, 12, 0, 0, new TimeSpan()),
Duration = new TimeSpan(0, 1, 0, 15, 456),
Date = new DateOnly(1, 1, 1),
Time = new TimeOnly(0, 0, 0)
Time = new TimeOnly(0, 0, 0),
Difficulty = MissionDifficulty.Unknown
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public class Mission
public TimeSpan Duration { get; set; }
public DateOnly Date { get; set; }
public TimeOnly Time { get; set; }
public MissionDifficulty Difficulty { get; set; }

public virtual ICollection<SquadMission> ParticipatingSquads { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.EntityFrameworkCore.TestModels.GearsOfWarModel;

public enum MissionDifficulty
{
Unknown = 0,
Low = 1,
Medium = 2,
High = 3,
Extreme = 4
}
Loading
Loading