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 2 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 @@ -12,10 +12,13 @@ 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
= typeof(Enum).GetRuntimeMethod(nameof(Enum.HasFlag), [typeof(Enum)])!;
private static readonly MethodInfo HasFlagMethodInfo
= typeof(Enum).GetRuntimeMethod(nameof(Enum.HasFlag), new[] { typeof(Enum) })!;
roji marked this conversation as resolved.
Show resolved Hide resolved

private static readonly MethodInfo ToStringMethodInfo
= typeof(object).GetRuntimeMethod(nameof(ToString), new Type[] { })!;
roji marked this conversation as resolved.
Show resolved Hide resolved

private readonly ISqlExpressionFactory _sqlExpressionFactory;

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,43 @@ public EnumHasFlagTranslator(ISqlExpressionFactory sqlExpressionFactory)
: _sqlExpressionFactory.Equal(_sqlExpressionFactory.And(instance, argument), argument);
}

if (Equals(method, ToStringMethodInfo)
&& instance != null
&& instance.Type.IsEnum)
roji marked this conversation as resolved.
Show resolved Hide resolved
{
var converterType = instance.TypeMapping?.Converter?.GetType();

if (converterType is not null
&& converterType.IsGenericType)
roji marked this conversation as resolved.
Show resolved Hide resolved
{
if (converterType.GetGenericTypeDefinition() == typeof(EnumToNumberConverter<,>))
roji marked this conversation as resolved.
Show resolved Hide resolved
{
var whenClauses = Enum.GetValues(instance.Type)
.Cast<object>()
.Select(value => new CaseWhenClause(
_sqlExpressionFactory.Constant(value),
_sqlExpressionFactory.Constant(value.ToString(), typeof(string))))
.ToArray();

SqlExpression elseResult = _sqlExpressionFactory.Convert(instance, typeof(string));

if (instance is ColumnExpression { IsNullable: true })
roji marked this conversation as resolved.
Show resolved Hide resolved
{
elseResult = _sqlExpressionFactory.Coalesce(
elseResult,
_sqlExpressionFactory.Constant(string.Empty, typeof(string)));
roji marked this conversation as resolved.
Show resolved Hide resolved
}

return _sqlExpressionFactory.Case(instance, whenClauses, elseResult);
}
else if (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));
}
}
}

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