uma árvore de expressão lambda não pode conter um operador de propagação nulo

96

Pergunta : A linha price = co?.price ?? 0,no código a seguir me dá o erro acima. mas se eu remover ?de co.?que excelentes trabalhos. Eu estava tentando seguir este exemplo do MSDN em que eles estão usando ?on-line. select new { person.FirstName, PetName = subpet?.Name ?? String.Empty };Portanto, parece que preciso entender quando usar ?com ??e quando não.

Erro :

uma árvore de expressão lambda não pode conter um operador de propagação nulo

public class CustomerOrdersModelView
{
    public string CustomerID { get; set; }
    public int FY { get; set; }
    public float? price { get; set; }
    ....
    ....
}
public async Task<IActionResult> ProductAnnualReport(string rpt)
{
    var qry = from c in _context.Customers
              join ord in _context.Orders
                on c.CustomerID equals ord.CustomerID into co
              from m in co.DefaultIfEmpty()
              select new CustomerOrdersModelView
              {
                  CustomerID = c.CustomerID,
                  FY = c.FY,
                  price = co?.price ?? 0,
                  ....
                  ....
              };
    ....
    ....
 }
nam
fonte
Por favor, poste o erro ...
Willem Van Onsem
3
Cara, eu gostaria que C # suportasse isso!
nawfal

Respostas:

150

O exemplo que você citou usa LINQ to Objects, onde as expressões lambda implícitas na consulta são convertidas em delegados ... enquanto você está usando EF ou semelhante, com IQueryable<T>consultas, onde as expressões lambda são convertidas em árvores de expressão . Árvores de expressão não suportam o operador condicional nulo (ou tuplas).

Basta fazer do jeito antigo:

price = co == null ? 0 : (co.price ?? 0)

(Acredito que o operador de coalescência nula seja adequado em uma árvore de expressão.)

Jon Skeet
fonte
Caso você esteja usando o LINQ Dinâmico (System.Linq.Dynamic.Core), você pode usar o np()método. Consulte github.com/StefH/System.Linq.Dynamic.Core/wiki/NullPropagation
Stef
11

O código para o qual você vincula usa List<T>. List<T>implementa, IEnumerable<T>mas não IQueryable<T>. Nesse caso, a projeção é executada na memória e ?.funciona.

Você está usando alguns IQueryable<T>, que funcionam de maneira muito diferente. Para IQueryable<T>, uma representação da projeção é criada e seu provedor LINQ decide o que fazer com ela em tempo de execução. Por motivos de compatibilidade com versões anteriores, ?.não pode ser usado aqui.

Dependendo do seu provedor de LINQ, você pode usar simples .e ainda não obter nenhum NullReferenceException.


fonte
@hvd Você poderia explicar por que isso é necessário para compatibilidade com versões anteriores?
entalhe de
1
@jag Todos os provedores LINQ que já foram criados antes da introdução do ?.não estariam preparados para lidar de ?.maneira razoável.
1
Mas o ?.é um novo operador, não? Portanto, o código antigo não seria usado ?.e, portanto, não seria quebrado. Os provedores Linq não estão preparados para lidar com muitas outras coisas, como métodos CLR.
entediado
2
@jag Certo, o código antigo em combinação com provedores LINQ antigos não seria afetado. Código antigo não será usado ?.. O novo código pode estar usando provedores LINQ antigos, que estão preparados para lidar com métodos CLR que eles não reconhecem (lançando uma exceção), uma vez que eles se encaixam perfeitamente no modelo de objeto de árvore de expressão existente. Tipos de nó de árvore de expressão completamente novos não se encaixam.
3
Dado o número de exceções lançadas pelos provedores de LINQ, dificilmente parece uma compensação que valha a pena - "costumávamos não dar suporte, então preferimos nunca poder"
NetMage
1

A resposta de Jon Skeet estava certa, no meu caso eu estava usando DateTimepara minha aula de Entidade. Quando tentei usar like

(a.DateProperty == null ? default : a.DateProperty.Date)

Eu tive o erro

Property 'System.DateTime Date' is not defined for type 'System.Nullable`1[System.DateTime]' (Parameter 'property')

Então, eu precisava mudar DateTime?para minha classe de entidade e

(a.DateProperty == null ? default : a.DateProperty.Value.Date)
Ugur Ozturk
fonte
Não se trata do operador de propagação nula.
Gert Arnold
Gosto de como você menciona que Jon Skeet estava certo, sugerindo que, de alguma forma, ele pode estar errado. Um bom!
Klicker
0

Embora a árvore de expressão não suporte a propagação nula do C # 6.0, o que podemos fazer é criar um visitante que modifique a árvore de expressão para uma propagação nula segura, assim como o operador faz!

Aqui é minha:

public class NullPropagationVisitor : ExpressionVisitor
{
    private readonly bool _recursive;

    public NullPropagationVisitor(bool recursive)
    {
        _recursive = recursive;
    }

    protected override Expression VisitUnary(UnaryExpression propertyAccess)
    {
        if (propertyAccess.Operand is MemberExpression mem)
            return VisitMember(mem);

        if (propertyAccess.Operand is MethodCallExpression met)
            return VisitMethodCall(met);

        if (propertyAccess.Operand is ConditionalExpression cond)
            return Expression.Condition(
                    test: cond.Test,
                    ifTrue: MakeNullable(Visit(cond.IfTrue)),
                    ifFalse: MakeNullable(Visit(cond.IfFalse)));

        return base.VisitUnary(propertyAccess);
    }

    protected override Expression VisitMember(MemberExpression propertyAccess)
    {
        return Common(propertyAccess.Expression, propertyAccess);
    }

    protected override Expression VisitMethodCall(MethodCallExpression propertyAccess)
    {
        if (propertyAccess.Object == null)
            return base.VisitMethodCall(propertyAccess);

        return Common(propertyAccess.Object, propertyAccess);
    }

    private BlockExpression Common(Expression instance, Expression propertyAccess)
    {
        var safe = _recursive ? base.Visit(instance) : instance;
        var caller = Expression.Variable(safe.Type, "caller");
        var assign = Expression.Assign(caller, safe);
        var acess = MakeNullable(new ExpressionReplacer(instance,
            IsNullableStruct(instance) ? caller : RemoveNullable(caller)).Visit(propertyAccess));
        var ternary = Expression.Condition(
                    test: Expression.Equal(caller, Expression.Constant(null)),
                    ifTrue: Expression.Constant(null, acess.Type),
                    ifFalse: acess);

        return Expression.Block(
            type: acess.Type,
            variables: new[]
            {
                caller,
            },
            expressions: new Expression[]
            {
                assign,
                ternary,
            });
    }

    private static Expression MakeNullable(Expression ex)
    {
        if (IsNullable(ex))
            return ex;

        return Expression.Convert(ex, typeof(Nullable<>).MakeGenericType(ex.Type));
    }

    private static bool IsNullable(Expression ex)
    {
        return !ex.Type.IsValueType || (Nullable.GetUnderlyingType(ex.Type) != null);
    }

    private static bool IsNullableStruct(Expression ex)
    {
        return ex.Type.IsValueType && (Nullable.GetUnderlyingType(ex.Type) != null);
    }

    private static Expression RemoveNullable(Expression ex)
    {
        if (IsNullableStruct(ex))
            return Expression.Convert(ex, ex.Type.GenericTypeArguments[0]);

        return ex;
    }

    private class ExpressionReplacer : ExpressionVisitor
    {
        private readonly Expression _oldEx;
        private readonly Expression _newEx;

        internal ExpressionReplacer(Expression oldEx, Expression newEx)
        {
            _oldEx = oldEx;
            _newEx = newEx;
        }

        public override Expression Visit(Expression node)
        {
            if (node == _oldEx)
                return _newEx;

            return base.Visit(node);
        }
    }
}

Ele passa nos seguintes testes:

private static string Foo(string s) => s;

static void Main(string[] _)
{
    var visitor = new NullPropagationVisitor(recursive: true);

    Test1();
    Test2();
    Test3();

    void Test1()
    {
        Expression<Func<string, char?>> f = s => s == "foo" ? 'X' : Foo(s).Length.ToString()[0];

        var fBody = (Expression<Func<string, char?>>)visitor.Visit(f);

        var fFunc = fBody.Compile();

        Debug.Assert(fFunc(null) == null);
        Debug.Assert(fFunc("bar") == '3');
        Debug.Assert(fFunc("foo") == 'X');
    }

    void Test2()
    {
        Expression<Func<string, int>> y = s => s.Length;

        var yBody = visitor.Visit(y.Body);
        var yFunc = Expression.Lambda<Func<string, int?>>(
                                    body: yBody,
                                    parameters: y.Parameters)
                            .Compile();

        Debug.Assert(yFunc(null) == null);
        Debug.Assert(yFunc("bar") == 3);
    }

    void Test3()
    {
        Expression<Func<char?, string>> y = s => s.Value.ToString()[0].ToString();

        var yBody = visitor.Visit(y.Body);
        var yFunc = Expression.Lambda<Func<char?, string>>(
                                    body: yBody,
                                    parameters: y.Parameters)
                            .Compile();

        Debug.Assert(yFunc(null) == null);
        Debug.Assert(yFunc('A') == "A");
    }
}
Leandromoh
fonte