Skip to content

Commit

Permalink
Work around anonymous type support
Browse files Browse the repository at this point in the history
  • Loading branch information
roji committed Jan 6, 2023
1 parent 13bf019 commit fd3008a
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 34 deletions.
51 changes: 23 additions & 28 deletions src/EFCore.Design/Query/Internal/CSharpToLinqTranslator.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
Expand Down Expand Up @@ -111,15 +110,14 @@ public Expression Translate(SyntaxNode node, SemanticModel semanticModel)
/// 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 override Expression VisitAnonymousObjectCreationExpression(
AnonymousObjectCreationExpressionSyntax objectCreation)
public override Expression VisitAnonymousObjectCreationExpression(AnonymousObjectCreationExpressionSyntax anonymousObjectCreation)
{
// Creating an actual anonymous object means creating a new type, which can only be done with Reflection.Emit.
// At least for EF's purposes, it doesn't matter, so we build a placeholder.
if (_semanticModel.GetSymbolInfo(objectCreation).Symbol is not IMethodSymbol constructorSymbol)
if (_semanticModel.GetSymbolInfo(anonymousObjectCreation).Symbol is not IMethodSymbol constructorSymbol)
{
throw new InvalidOperationException(
"Could not find symbol for anonymous object creation initializer: " + objectCreation);
"Could not find symbol for anonymous object creation initializer: " + anonymousObjectCreation);
}

var anonymousType = ResolveType(constructorSymbol.ContainingType);
Expand All @@ -130,26 +128,24 @@ public override Expression VisitAnonymousObjectCreationExpression(
var memberInfos = new MemberInfo[parameters.Length];
var arguments = new Expression[parameters.Length];

foreach (var initializer in objectCreation.Initializers)
foreach (var initializer in anonymousObjectCreation.Initializers)
{
if (initializer.NameEquals is null)
{
throw new NotImplementedException("Unnamed anonymous object initializer");
}
else
{
var position = Array.FindIndex(parameters, p => p.Name == initializer.NameEquals.Name.Identifier.Text);
var parameter = parameters[position];
var parameterType = ResolveType(parameter.Type) ?? throw new InvalidOperationException(
"Could not resolve type symbol for: " + parameter.Type);

parameterInfos[position] = new FakeParameterInfo(
initializer.NameEquals.Name.Identifier.Text,
parameterType,
position);
arguments[position] = Visit(initializer.Expression);
memberInfos[position] = anonymousType.GetProperty(parameter.Name)!;
}
// If the initializer's name isn't explicitly specified, infer it from the initializer's expression like the compiler does
var name = initializer.NameEquals is not null
? initializer.NameEquals.Name.Identifier.Text
: initializer.Expression is MemberAccessExpressionSyntax memberAccess
? memberAccess.Name.Identifier.Text
: throw new InvalidOperationException(
$"AnonymousObjectCreation: unnamed initializer with non-MemberAccess expression: {initializer.Expression}");

var position = Array.FindIndex(parameters, p => p.Name == name);
var parameter = parameters[position];
var parameterType = ResolveType(parameter.Type) ?? throw new InvalidOperationException(
"Could not resolve type symbol for: " + parameter.Type);

parameterInfos[position] = new FakeParameterInfo(name, parameterType, position);
arguments[position] = Visit(initializer.Expression);
memberInfos[position] = anonymousType.GetProperty(parameter.Name)!;
}

return New(
Expand Down Expand Up @@ -959,7 +955,7 @@ private Type ResolveType(ITypeSymbol typeSymbol, Dictionary<string, Type>? gener
switch (typeSymbol)
{
case INamedTypeSymbol { IsAnonymousType: true } anonymousTypeSymbol:
_anonymousTypeDefinitions ??= LoadAnonymousTypes();
_anonymousTypeDefinitions ??= LoadAnonymousTypes(anonymousTypeSymbol.ContainingAssembly);
var properties = anonymousTypeSymbol.GetMembers().OfType<IPropertySymbol>().ToArray();
var found = _anonymousTypeDefinitions.TryGetValue(
properties.Select(p => p.Name).OrderBy(p => p).ToArray(),
Expand Down Expand Up @@ -1033,10 +1029,9 @@ static Type GetClrTypeFromAssembly(IAssemblySymbol? assemblySymbol, string name)
?? throw new InvalidOperationException(
$"Couldn't resolve CLR type '{name}' in assembly '{assemblySymbol?.Name}'");

Dictionary<string[], Type> LoadAnonymousTypes()
Dictionary<string[], Type> LoadAnonymousTypes(IAssemblySymbol assemblySymbol)
{
// TODO
var assembly = Assembly.LoadFile("/home/roji/projects/test/EFTest/bin/Debug/net7.0/linux-x64/EFTest.dll");
var assembly = Assembly.Load(assemblySymbol.Name);

return assembly.GetTypes()
.Where(t => t.IsAnonymousType())
Expand Down
55 changes: 49 additions & 6 deletions src/EFCore.Design/Query/Internal/LinqToCSharpTranslator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1084,10 +1084,13 @@ protected override Expression VisitLambda<T>(Expression<T> lambda)
_liftedState.Statements.Clear();
}

// Note that we always explicitly include the parameters' types.
// This is because in some cases, the parameter isn't actually used in the lambda body, and the compiler can't infer its type.
// However, we can't do that when the type is anonymous.
Result = ParenthesizedLambdaExpression(
ParameterList(SeparatedList(lambda.Parameters.Select(p =>
Parameter(Identifier(LookupVariableName(p)))
.WithType(p.Type.GetTypeSyntax())))),
.WithType(p.Type.IsAnonymousType() ? null : p.Type.GetTypeSyntax())))),
body);

var popped = _stack.Pop();
Expand Down Expand Up @@ -1236,14 +1239,26 @@ protected override Expression VisitMethodCall(MethodCallExpression call)

var arguments = TranslateMethodArguments(call.Method.GetParameters(), call.Arguments);

// TODO: don't specify generic parameters if they can all be inferred
SimpleNameSyntax methodIdentifier = call.Method.IsGenericMethod
? GenericName(
// For generic methods, we check whether the generic type arguments are inferrable (e.g. they all appear in the parameters), and
// only explicitly specify the arguments if not. Note that this isn't just for prettier code: anonymous types cannot be explicitly
// named in code.
SimpleNameSyntax methodIdentifier;
if (!call.Method.IsGenericMethod || GenericTypeParameterAreInferrable())
{
methodIdentifier = IdentifierName(call.Method.Name);
}
else
{
Check.DebugAssert(
call.Method.GetGenericArguments().All(ga => !ga.IsAnonymousType()),
"Anonymous type as generic type argument for method whose type arguments aren't inferrable");

methodIdentifier = GenericName(
Identifier(call.Method.Name),
TypeArgumentList(
SeparatedList(
call.Method.GetGenericArguments().Select(ga => ga.GetTypeSyntax()))))
: IdentifierName(call.Method.Name);
call.Method.GetGenericArguments().Select(ga => ga.GetTypeSyntax()))));
}

// Extension syntax
if (call.Method.IsDefined(typeof(ExtensionAttribute), inherit: false)
Expand Down Expand Up @@ -1293,6 +1308,34 @@ static ExpressionSyntax GetMemberAccessesForAllDeclaringTypes(Type type)
}

return call;

bool GenericTypeParameterAreInferrable()
{
var originalDefinition = call.Method.GetGenericMethodDefinition();
var unseenTypeParameters = originalDefinition.GetGenericArguments().ToList();

foreach (var parameter in originalDefinition.GetParameters())
{
ProcessType(parameter.ParameterType);
}

return unseenTypeParameters.Count == 0;

void ProcessType(Type type)
{
if (type.IsGenericParameter)
{
unseenTypeParameters.Remove(type);
}
else if (type.IsGenericType)
{
foreach (var genericArgument in type.GetGenericArguments())
{
ProcessType(genericArgument);
}
}
}
}
}

/// <inheritdoc />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ public async Task GeneratePrecompiledQueries(string projectDir, DbContext contex
// by manually generated code above.
.Append("System")
.Append("System.Collections.Concurrent")
.Append("System.Linq.Expressions")
.Append("System.Runtime.CompilerServices")
.Append("System.Reflection")
.Append("System.Collections.Generic")
Expand Down

0 comments on commit fd3008a

Please sign in to comment.