un árbol de expresión lambda no puede contener un operador de propagación nulo

90

Pregunta : La línea price = co?.price ?? 0,del siguiente código me da el error anterior. pero si quito ?de co.?lo bien que funciona. Yo estaba tratando de seguir este ejemplo de MSDN donde se están utilizando ?en la línea select new { person.FirstName, PetName = subpet?.Name ?? String.Empty };lo tanto, parece que necesito para entender cuándo usar ?con ??y cuándo no.

Error :

un árbol de expresión lambda no puede contener un operador de propagación 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
fuente
Publique
3
¡Ojalá C # apoyara esto!
nawfal

Respuestas:

141

El ejemplo del que estaba citando usa LINQ to Objects, donde las expresiones lambda implícitas en la consulta se convierten en delegados ... mientras que está usando EF o similar, conIQueryable<T> consultas, donde las expresiones lambda se convierten en árboles de expresión . Los árboles de expresión no admiten el operador condicional nulo (o tuplas).

Solo hazlo de la manera antigua:

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

(Creo que el operador de fusión nula está bien en un árbol de expresión).

Jon Skeet
fuente
En caso de que esté utilizando Dynamic LINQ (System.Linq.Dynamic.Core), puede utilizar el np()método. Ver github.com/StefH/System.Linq.Dynamic.Core/wiki/NullPropagation
Stef Heyenrath
10

El código al que se vincula usa List<T>. List<T>implementa IEnumerable<T>pero no IQueryable<T>. En ese caso, la proyección se ejecuta en memoria y?. funciona.

Estás usando algunos IQueryable<T>, que funcionan de manera muy diferente. Para IQueryable<T>, se crea una representación de la proyección y su proveedor LINQ decide qué hacer con ella en tiempo de ejecución. Por razones de compatibilidad con versiones anteriores,?. no se puede utilizar aquí.

Dependiendo de su proveedor de LINQ, es posible que pueda usar simple .y aún no obtener ninguno NullReferenceException.


fuente
@hvd ¿Podría explicar por qué es necesario para la compatibilidad con versiones anteriores?
jag
1
@jag Todos los proveedores LINQ que ya se habían creado antes de la introducción de ?.no habrían estado preparados para manejarlos ?.de manera razonable.
1
Pero el ?.es un nuevo operador, ¿no? Por lo tanto, el código antiguo no se usaría ?.y, por lo tanto, no se rompería. Los proveedores de Linq no están preparados para manejar muchas otras cosas, como los métodos CLR.
jag
2
@jag Correcto, el código antiguo en combinación con los proveedores LINQ antiguos no se vería afectado. No se utilizará el código antiguo ?.. Es posible que el código nuevo esté utilizando proveedores LINQ antiguos, que están preparados para manejar métodos CLR que no reconocen (lanzando una excepción), ya que encajan perfectamente en el modelo de objetos de árbol de expresión existente. Los tipos de nodo de árbol de expresión completamente nuevos no encajan.
3
Dada la cantidad de excepciones lanzadas por los proveedores de LINQ, difícilmente parece una compensación que valga la pena: "solíamos no admitir, por lo que preferiríamos nunca poder
hacerlo
1

La respuesta de Jon Skeet fue correcta, en mi caso la estaba usando DateTimepara mi clase de Entidad. Cuando traté de usar like

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

Tuve el error

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

Entonces necesitaba cambiar DateTime?para mi clase de entidad y

(a.DateProperty == null ? default : a.DateProperty.Value.Date)
Ugur Ozturk
fuente
No se trata del operador de propagación nula.
Gert Arnold
Me gusta cómo mencionas que Jon Skeet tenía razón, sugiriendo que de alguna manera es posible que esté equivocado. ¡Buena!
Klicker
0

Si bien el árbol de expresión no admite la propagación nula de C # 6.0, lo que podemos hacer es crear un visitante que modifique el árbol de expresión para una propagación nula segura, ¡tal como lo hace el operador!

Aquí esta el 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);
        }
    }
}

Pasa las siguientes pruebas:

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
fuente