OData / AspNetCoreOData

ASP.NET Core OData: A server library built upon ODataLib and ASP.NET Core

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

$compute inside $expand not translating $this for single navigations

Xriuk opened this issue · comments

Assemblies affected
ASP.NET Core OData 8.2.4

Describe the bug
$this is working correctly inside a nested $compute in $expand when expanding a collection navigation, but not when expanding a single navigation.

Reproduce steps
The following request url which expands a collection navigation:

https://.../odata/DisposalContainers?$expand=Packagings($select=Id,Test;$compute=$this/Material/Code as Test)

Produces an expression like this, which works:

DbSet<DisposalContainer>()
    .Select($it => new SelectAllAndExpand<DisposalContainer>{ 
        Model = TypedLinqParameterContainer<IEdmModel>.TypedProperty, 
        Instance = $it, 
        UseInstanceForProperties = True, 
        Container = new NamedProperty<IEnumerable<SelectSome<DisposalPackaging>>>{ 
            Name = "Packagings", 
            Value = $it.Packagings
                .Select($it => new SelectSome<DisposalPackaging>{ 
                    Model = TypedLinqParameterContainer<IEdmModel>.TypedProperty, 
                    Container = new NamedPropertyWithNext0<int?>{ 
                        Name = "Id", 
                        Value = (int?)$it.Id, 
                        Next0 = new NamedProperty<string>{ 
                            Name = "Test", 
                            Value = $it.Material.Code 
                        }
                         
                    }
                     
                }
                ) 
        }
         
    }
    )

While a request url like this which expands a single navigation:

https://.../odata/DisposalMaterials?$expand=Destination($select=Id,Test;$compute=$this/Color/Value as Test)

Produces an expression like this, which throws the exception below:

DbSet<DisposalMaterial>()
    .Select($it => new SelectAllAndExpand<DisposalMaterial>{ 
        Model = TypedLinqParameterContainer<IEdmModel>.TypedProperty, 
        Instance = $it, 
        UseInstanceForProperties = True, 
        Container = new SingleExpandedProperty<SelectSome<DisposalDestination>>{ 
            Name = "Destination", 
            Value = new SelectSome<DisposalDestination>{ 
                Model = TypedLinqParameterContainer<IEdmModel>.TypedProperty, 
                Container = new NamedPropertyWithNext0<int?>{ 
                    Name = "Id", 
                    Value = (int?)$it.Destination.Id, 
                    Next0 = new NamedProperty<string>{ 
                        Name = "Test", 
                        Value = $it.Color.Value 
                    }
                     
                }
                 
            }
            , 
            IsNull = (int?)$it.Destination.Id == null 
        }
         
    }
    )

Specifically in this part

Next0 = new NamedProperty<string>{ 
    Name = "Test", 
    Value = $it.Color.Value 
}

It creates an $it expression (like above for the collection), which appears to be different from the top $it (DbSet<DisposalMaterial>().Select($it => ...)), because otherwise the error would have been different (like not being able to cast DisposalMaterial to DisposalDestination, or DisposalMaterial not having a Color property). It throws an exception like this:

The LINQ expression '$it' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'. See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.

Data Model

public class DisposalContainer {
    [Key]
    public int Id { get; set; }
    
    [Required]
    public string Name { get; set; } = null!;
    
    public ICollection<DisposalPackaging>? Packagings { get; set; }
}

public class DisposalPackaging {
    [Key]
    public int Id { get; set; }
    
    [Required]
    public DisposalMaterial? Material { get; set; }
    
    public ICollection<DisposalContainer>? Containers { get; set; }
}

public class DisposalMaterial {
    [Key]
    public int Id { get; set; }
    
    [Required]
    [MaxLength(10)]
    public string Code { get; set; } = null!;
    
    [Required]
    public DisposalDestination? Destination { get; set; }
    
    public ICollection<DisposalPackaging>? Packagings { get; set; }
}

public class DisposalDestination {
    [Key]
    public int Id { get; set; }

    [Required]
    public Color Color { get; set; } = null!;
    
    public ICollection<DisposalMaterial>? Materials { get; set; }
}

public class Color {
    public string Value { get; set; } = null!;
}

Hi there, it appears that, for both of the above examples, $this is not actually necessary (though there's nothing wrong with having it).

Regarding the error that you are seeing, from the OData standard:

The $this literal can be used in $filter and $orderby expressions nested within $expand and $select for collection-valued properties and navigation properties.

$this is only allowed when the $compute is nested in a $expand on a collection-valued navigation property. This is why the first request succeeds and the second one fails, because Destination is a single-valued navigation property.

My suggestion is to either remove the use of $this entirely in these cases, or to only use $this for collection-valued properties per the standard.