lambda drzewa wyrażeń nie może zawierać operatora propagującego wartość null

90

Pytanie : Linia price = co?.price ?? 0,w poniższym kodzie daje mi powyższy błąd. ale jeśli usunę ?z co.?niego, działa dobrze. Próbowałem naśladować ten przykład MSDN, w którym używają ?on-line select new { person.FirstName, PetName = subpet?.Name ?? String.Empty };. Wygląda na to, że muszę zrozumieć, kiedy używać ?z, ??a kiedy nie.

Błąd :

lambda drzewa wyrażeń nie może zawierać operatora propagującego wartość null

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
źródło
Proszę opublikować błąd ...
Willem Van Onsem
3
Człowieku, chciałem, żeby C # to obsługiwał!
nawfal

Odpowiedzi:

141

Przykład, z którego był cytowany, używa LINQ to Objects, w którym niejawne wyrażenia lambda w zapytaniu są konwertowane na delegatów ... podczas gdy używasz EF lub podobnego, z IQueryable<T>zapytaniami, w których wyrażenia lambda są konwertowane na drzewa wyrażeń . Drzewa wyrażeń nie obsługują operatora warunkowego o wartości null (lub krotek).

Po prostu zrób to w stary sposób:

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

(Uważam, że operator łączący wartość null jest w porządku w drzewie wyrażeń).

Jon Skeet
źródło
Jeśli używasz Dynamic LINQ (System.Linq.Dynamic.Core), możesz użyć np()metody. Zobacz github.com/StefH/System.Linq.Dynamic.Core/wiki/NullPropagation
Stef Heyenrath
10

Kod, do którego prowadzi łącze, używa List<T>. List<T>narzędzi, IEnumerable<T>ale nie IQueryable<T>. W takim przypadku projekcja jest wykonywana w pamięci i ?.działa.

Używasz niektórych IQueryable<T>, co działa zupełnie inaczej. W przypadku IQueryable<T>tworzona jest reprezentacja projekcji, a dostawca LINQ decyduje, co z nią zrobić w czasie wykonywania. Ze względu na kompatybilność wsteczną ?.nie można jej tutaj użyć.

W zależności od dostawcy LINQ możesz być w stanie użyć zwykłego .i nadal nie uzyskać żadnego NullReferenceException.


źródło
@hvd Czy możesz wyjaśnić, dlaczego jest to wymagane w celu zapewnienia zgodności wstecznej?
jag
1
@jag Wszyscy dostawcy LINQ, którzy zostali już utworzeni przed wprowadzeniem ?., nie byliby przygotowani do obsługi ?.w żaden rozsądny sposób.
1
Ale czy ?.nowy operator nie? Tak więc stary kod nie używałby ?.i nie został uszkodzony. Dostawcy Linq nie są przygotowani do obsługi wielu innych rzeczy, takich jak metody CLR.
jag
2
@jag Dobrze, stary kod w połączeniu ze starymi dostawcami LINQ nie będzie miał wpływu. Stary kod nie będzie używany ?.. Nowy kod może używać starych dostawców LINQ, którzy przygotowani do obsługi metod CLR, których nie rozpoznają (przez zgłoszenie wyjątku), ponieważ pasują one dobrze do istniejącego modelu obiektów drzewa wyrażeń. Całkowicie nowe typy węzłów drzewa wyrażeń nie pasują.
3
Biorąc pod uwagę liczbę wyjątków zgłoszonych już przez dostawców LINQ, nie wydaje się to opłacalnym kompromisem - „wcześniej nie wspieraliśmy, więc wolelibyśmy nigdy nie
robić
1

Odpowiedź Jona Skeeta była prawidłowa, w moim przypadku użyłem DateTimedla mojej klasy Entity. Kiedy próbowałem użyć like

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

Miałem błąd

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

Musiałem więc zmienić DateTime?klasę encji i

(a.DateProperty == null ? default : a.DateProperty.Value.Date)
Ugur Ozturk
źródło
Nie chodzi o operator propagacji zerowej.
Gert Arnold
Podoba mi się, jak wspominasz, że Jon Skeet miał rację, sugerując, że w jakiś sposób może się mylić. Dobry!
Klicker
0

Chociaż drzewo wyrażeń nie obsługuje propagowania wartości null w języku C # 6.0, możemy utworzyć gościa, który modyfikuje drzewo wyrażeń w celu bezpiecznego propagowania wartości null, tak jak robi to operator!

Tu jest moje:

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

Zdaje następujące testy:

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
źródło