Skip to content

Commit 425c828

Browse files
Fix $expand with null properties
1 parent 2bde4e5 commit 425c828

File tree

2 files changed

+120
-7
lines changed

2 files changed

+120
-7
lines changed

src/Microsoft.AspNetCore.OData/Query/Expressions/SelectExpandBinder.cs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -453,15 +453,44 @@ private static Expression RemoveNonStructucalProperties(QueryBinderContext conte
453453

454454
PropertyInfo[] props = elementType.GetProperties();
455455
List<MemberBinding> bindings = new List<MemberBinding>();
456+
ODataQuerySettings settings = context.QuerySettings;
457+
bool isSourceNullable = ExpressionBinderHelper.IsNullable(source.Type);
456458

457459
foreach (PropertyInfo prop in props)
458460
{
461+
bool isPropertyNullable = ExpressionBinderHelper.IsNullable(prop.PropertyType);
462+
459463
foreach (var sp in structuralProperties)
460464
{
461465
if (prop.CanWrite && model.GetClrPropertyName(sp) == prop.Name)
462466
{
463-
MemberExpression propertyExpression = Expression.Property(source, prop);
464-
bindings.Add(Expression.Bind(prop, propertyExpression));
467+
Expression propertyValue = Expression.Property(source, prop);
468+
Type nullablePropertyType = TypeHelper.ToNullable(propertyValue.Type);
469+
Expression nullablePropertyValue = ExpressionHelpers.ToNullable(propertyValue);
470+
471+
if ((settings.HandleNullPropagation == HandleNullPropagationOption.True) || (isSourceNullable && isPropertyNullable))
472+
{
473+
propertyValue = Expression.Condition(
474+
test: Expression.Equal(source, Expression.Constant(value: null)),
475+
ifTrue: Expression.Constant(value: null, type: nullablePropertyType),
476+
ifFalse: nullablePropertyValue);
477+
}
478+
else if (isSourceNullable)
479+
{
480+
object defaultValue = prop.PropertyType.IsValueType ? Activator.CreateInstance(prop.PropertyType) : null;
481+
482+
// property is non-nullable
483+
propertyValue = Expression.Condition(
484+
test: Expression.Equal(source, Expression.Constant(value: null)),
485+
ifTrue: Expression.Constant(value: defaultValue),
486+
ifFalse: propertyValue);
487+
}
488+
else if (isPropertyNullable)
489+
{
490+
propertyValue = nullablePropertyValue;
491+
}
492+
493+
bindings.Add(Expression.Bind(prop, propertyValue));
465494
break;
466495
}
467496
}

test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/SelectExpandBinderTest.cs

Lines changed: 89 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using System.Collections.Generic;
1111
using System.ComponentModel;
1212
using System.ComponentModel.DataAnnotations;
13+
using System.ComponentModel.DataAnnotations.Schema;
1314
using System.Linq;
1415
using System.Linq.Expressions;
1516
using System.Reflection;
@@ -36,6 +37,7 @@ public class SelectExpandBinderTest
3637
private readonly SelectExpandBinder _binder;
3738
private readonly SelectExpandBinder _binder_lowerCamelCased;
3839
private readonly IQueryable<QueryCustomer> _queryable;
40+
private readonly IQueryable<QueryCustomer> _queryable1;
3941
private readonly IQueryable<QueryCustomer> _queryable_lowerCamelCased;
4042
private readonly ODataQueryContext _context;
4143
private readonly ODataQueryContext _context_lowerCamelCased;
@@ -82,7 +84,19 @@ public SelectExpandBinderTest()
8284
QueryOrder order = new QueryOrder { Id = 42, Title = "The order", Customer = customer };
8385
customer.Orders.Add(order);
8486

87+
QueryCustomer customer1 = new QueryCustomer
88+
{
89+
Orders = new List<QueryOrder>()
90+
};
91+
92+
QueryOrder order1 = new QueryOrder { Id = 42, Title = "The order", Customer = customer1 };
93+
QueryOrder order2 = null;
94+
95+
customer1.Orders.Add(order1);
96+
customer1.Orders.Add(order2);
97+
8598
_queryable = new[] { customer }.AsQueryable();
99+
_queryable1 = new[] { customer1 }.AsQueryable();
86100

87101
SelectExpandQueryOption selectExpandQueryOption = new SelectExpandQueryOption("Orders", expand: null, context: _context);
88102

@@ -270,7 +284,7 @@ public void Bind_UsingEFQueryProvider_GeneratedExpression__DoesNot_ContainExpand
270284
// Act
271285
SelectExpandBinder binder = new SelectExpandBinder();
272286
_queryBinderContext.QueryProvider = queryProvider;
273-
IQueryable queryable = binder.ApplyBind(_queryable, selectExpand.SelectExpandClause, _queryBinderContext);
287+
IQueryable queryable = binder.ApplyBind(_queryable1, selectExpand.SelectExpandClause, _queryBinderContext);
274288

275289
// Assert
276290
IEnumerator enumerator = queryable.GetEnumerator();
@@ -282,7 +296,7 @@ public void Bind_UsingEFQueryProvider_GeneratedExpression__DoesNot_ContainExpand
282296
IEnumerable<SelectExpandWrapper<QueryOrder>> innerOrders = partialCustomer.Container
283297
.ToDictionary(PropertyMapper)["Orders"] as IEnumerable<SelectExpandWrapper<QueryOrder>>;
284298
Assert.NotNull(innerOrders);
285-
SelectExpandWrapper<QueryOrder> partialOrder = innerOrders.Single();
299+
SelectExpandWrapper<QueryOrder> partialOrder = innerOrders.FirstOrDefault();
286300

287301
// We only write structural properties to the instance.
288302
// This means that navigation properties on the instance property will be null
@@ -327,7 +341,7 @@ public void Bind_UsingEFQueryProvider_LowerCamelCasedModel_GeneratedExpression__
327341
IEnumerable<SelectExpandWrapper<QueryOrder>> innerOrders = partialCustomer.Container
328342
.ToDictionary(PropertyMapper)["orders"] as IEnumerable<SelectExpandWrapper<QueryOrder>>;
329343
Assert.NotNull(innerOrders);
330-
SelectExpandWrapper<QueryOrder> partialOrder = innerOrders.Single();
344+
SelectExpandWrapper<QueryOrder> partialOrder = innerOrders.FirstOrDefault();
331345

332346
// We only write structural properties to the instance.
333347
// This means that navigation properties on the instance property will be null
@@ -343,6 +357,52 @@ public void Bind_UsingEFQueryProvider_LowerCamelCasedModel_GeneratedExpression__
343357
Assert.Null(innerInnerCustomer.Instance.Orders);
344358
}
345359

360+
[Fact]
361+
public void Bind_SelectAndExpand_WithNullProperties_DoesNotThrowException()
362+
{
363+
// Arrange
364+
IQueryable<User> users;
365+
string expand = "FileRefNavigation";
366+
367+
User user = new User
368+
{
369+
UserId = 1,
370+
Name = "Alex",
371+
Age = 35,
372+
DataFileRef = null,
373+
FileRefNavigation = null
374+
};
375+
376+
users = new[] { user }.AsQueryable();
377+
ODataQueryContext context = new ODataQueryContext(_model, typeof(User)) { RequestContainer = new MockServiceProvider() };
378+
379+
SelectExpandQueryOption selectExpand = new SelectExpandQueryOption(select: null, expand: expand, context: context);
380+
381+
QueryBinderContext queryBinderContext = new QueryBinderContext(_model, _settings, selectExpand.Context.ElementClrType)
382+
{
383+
NavigationSource = context.NavigationSource
384+
};
385+
386+
queryBinderContext.QueryProvider = HandleNullPropagationOptionHelper.EntityFrameworkQueryProviderNamespace;
387+
388+
// Act
389+
SelectExpandBinder binder = new SelectExpandBinder();
390+
IQueryable queryable = binder.ApplyBind(users, selectExpand.SelectExpandClause, queryBinderContext);
391+
392+
// Assert
393+
Assert.NotNull(queryable);
394+
395+
IEnumerator enumerator = queryable.GetEnumerator();
396+
Assert.True(enumerator.MoveNext());
397+
var usr = Assert.IsAssignableFrom<SelectExpandWrapper<User>>(enumerator.Current);
398+
Assert.False(enumerator.MoveNext());
399+
Assert.NotNull(usr.Instance);
400+
Assert.Equal("Microsoft.AspNetCore.OData.Tests.Query.Expressions.User", usr.Instance.GetType().ToString());
401+
IEnumerable<SelectExpandWrapper<DataFile>> fileRefsNavigations = usr.Container
402+
.ToDictionary(PropertyMapper)["FileRefNavigation"] as IEnumerable<SelectExpandWrapper<DataFile>>;
403+
Assert.Null(fileRefsNavigations);
404+
}
405+
346406
[Fact]
347407
public void Bind_GeneratedExpression_CheckNullObjectWithinChainProjectionByKey()
348408
{
@@ -2082,6 +2142,7 @@ public static IEdmModel GetEdmModel()
20822142
builder.EntitySet<QueryOrder>("Orders");
20832143
builder.EntitySet<QueryCity>("Cities");
20842144
builder.EntitySet<QueryProduct>("Products");
2145+
builder.EntitySet<User>("Users");
20852146

20862147
customer.Collection.Function("IsUpgraded").Returns<bool>().Namespace="NS";
20872148
customer.Collection.Action("UpgradeAll").Namespace = "NS";
@@ -2229,9 +2290,9 @@ public class QueryCustomer
22292290

22302291
public IList<QueryAddress> Addresses { get; set; }
22312292

2232-
public QueryOrder PrivateOrder { get; set; }
2293+
public QueryOrder? PrivateOrder { get; set; }
22332294

2234-
public IList<QueryOrder> Orders { get; set; }
2295+
public IList<QueryOrder>? Orders { get; set; }
22352296

22362297
public List<string> TestReadonlyProperty
22372298
{
@@ -2292,4 +2353,27 @@ public enum QueryColor
22922353

22932354
Blue
22942355
}
2356+
2357+
public class User
2358+
{
2359+
[Key]
2360+
public int UserId { get; set; }
2361+
[Required]
2362+
public string Name { get; set; }
2363+
[Required]
2364+
public int Age { get; set; }
2365+
2366+
//Navigations
2367+
[ForeignKey("FileRefNavigation")]
2368+
public int? DataFileRef { get; set; }
2369+
public DataFile? FileRefNavigation { get; set; }
2370+
}
2371+
2372+
public class DataFile
2373+
{
2374+
[Key]
2375+
public int FileId { get; set; }
2376+
[Required]
2377+
public string FileName { get; set; }
2378+
}
22952379
}

0 commit comments

Comments
 (0)