un albero delle espressioni lambda non può contenere un operatore di propagazione nullo


96

Domanda : la riga price = co?.price ?? 0,nel codice seguente mi dà l'errore precedente. ma se rimuovo ?da co.?esso funziona bene. Stavo cercando di seguire questo esempio MSDN dove stanno usando ?in linea select new { person.FirstName, PetName = subpet?.Name ?? String.Empty };Quindi, sembra che abbia bisogno di capire quando usare ?con ??e quando non farlo.

Errore :

un albero delle espressioni lambda non può contenere un operatore di propagazione nullo

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,
                  ....
                  ....
              };
    ....
    ....
 }


3
Amico, avrei voluto che C # lo supportasse!
nawfal

Risposte:


150

L'esempio da cui stavi citando usa LINQ to Objects, dove le espressioni lambda implicite nella query vengono convertite in delegati ... mentre stai usando EF o simili, con IQueryable<T>query, in cui le espressioni lambda vengono convertite in alberi delle espressioni . Gli alberi delle espressioni non supportano l'operatore condizionale null (o le tuple).

Fallo alla vecchia maniera:

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

(Credo che l'operatore di coalescenza null vada bene in un albero delle espressioni.)


Nel caso in cui si utilizzi Dynamic LINQ (System.Linq.Dynamic.Core), è possibile utilizzare il np()metodo. Vedi github.com/StefH/System.Linq.Dynamic.Core/wiki/NullPropagation
Stef Heyenrath

11

Il codice a cui ti colleghi utilizza List<T>. List<T>implementa IEnumerable<T>ma non IQueryable<T>. In tal caso, la proiezione viene eseguita in memoria e ?.funziona.

Ne stai usando alcuni IQueryable<T>, il che funziona in modo molto diverso. Per IQueryable<T>, viene creata una rappresentazione della proiezione e il provider LINQ decide cosa farne in fase di esecuzione. Per motivi di compatibilità con le versioni precedenti, ?.non può essere utilizzato qui.

A seconda del provider LINQ, potresti essere in grado di utilizzare plain .e ancora non ottenerne NullReferenceException.


@hvd Potresti spiegare perché è necessario per la retrocompatibilità?
jag

1
@jag Tutti i provider LINQ che erano già stati creati prima dell'introduzione di ?.non sarebbero stati preparati a gestirli ?.in modo ragionevole.

1
Ma ?.è un nuovo operatore no? Quindi il vecchio codice non sarebbe stato utilizzato ?.e quindi non sarebbe stato rotto. I fornitori di Linq non sono preparati a gestire molte altre cose come i metodi CLR.
jag

2
@jag Giusto, il vecchio codice in combinazione con i vecchi provider LINQ non sarebbe interessato. Il vecchio codice non verrà utilizzato ?.. Il nuovo codice potrebbe utilizzare vecchi provider LINQ, che sono pronti a gestire metodi CLR che non riconoscono (generando un'eccezione), poiché si adattano perfettamente al modello di oggetti dell'albero delle espressioni esistente. Tipi di nodi dell'albero delle espressioni completamente nuovi non si adattano.

3
Dato il numero di eccezioni già lanciate dai provider LINQ, difficilmente sembra un compromesso utile - "prima non supportavamo, quindi preferiremmo non poterlo mai"
NetMage

1

La risposta di Jon Skeet era giusta, nel mio caso stavo usando DateTimeper la mia classe Entity. Quando ho provato a usare like

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

Ho avuto l'errore

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

Quindi avevo bisogno di cambiare DateTime?per la mia classe di entità e

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

Non si tratta dell'operatore di propagazione null.
Gert Arnold

Mi piace come dici che Jon Skeet aveva ragione, suggerendo che è in qualche modo possibile che si sbagli. Buono!
Klicker

0

Sebbene l'albero delle espressioni non supporti la propagazione dei valori nulli in C # 6.0, ciò che possiamo fare è creare un visitatore che modifichi l'albero delle espressioni per una propagazione nullo sicura, proprio come fa l'operatore!

Ecco il mio:

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);
        }
    }
}

Supera i seguenti test:

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");
    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.