-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fix invalid reflection being used when sorting in EF Core 3.0, see #5
- Loading branch information
1 parent
6b62d7a
commit 5343116
Showing
9 changed files
with
185 additions
and
32 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
using System; | ||
using System; | ||
using System.Collections; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
|
@@ -9,30 +9,32 @@ namespace LightQuery.Shared | |
{ | ||
public static class QueryableProcessor | ||
{ | ||
private static PropertyInfo GetPropertyInfoRecursively(this IQueryable queryable, string propName) | ||
private static (Type declaringType, PropertyInfo property) GetPropertyInfoRecursively(this IQueryable queryable, string propName) | ||
{ | ||
string[] nameParts = propName.Split('.'); | ||
if (nameParts.Length == 1) | ||
{ | ||
return queryable.ElementType.GetTypeInfo().GetProperty(CamelizeString(propName)) ?? queryable.ElementType.GetTypeInfo().GetProperty(propName); | ||
var property = queryable.ElementType.GetTypeInfo().GetProperty(CamelizeString(propName)) ?? queryable.ElementType.GetTypeInfo().GetProperty(propName); | ||
return (property?.DeclaringType, property); | ||
} | ||
|
||
//Getting Root Property - Ex : propName : "User.Name" -> User | ||
var propertyInfo = queryable.ElementType.GetTypeInfo().GetProperty(CamelizeString(nameParts[0])) ?? queryable.ElementType.GetTypeInfo().GetProperty(nameParts[0]); | ||
var originalDeclaringType = propertyInfo.DeclaringType; | ||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong.
GeorgDangl
Author
Owner
|
||
if (propertyInfo == null) | ||
{ | ||
return null; | ||
return (null, null); | ||
} | ||
|
||
for (int i = 1; i < nameParts.Length; i++) | ||
{ | ||
propertyInfo = propertyInfo.PropertyType.GetProperty(CamelizeString(nameParts[i])) ?? propertyInfo.PropertyType.GetProperty(nameParts[i]); | ||
if (propertyInfo == null) | ||
{ | ||
return null; | ||
return (null, null); | ||
} | ||
} | ||
return propertyInfo; | ||
return (originalDeclaringType, propertyInfo); | ||
} | ||
|
||
private static LambdaExpression CreateExpression(Type type, string propertyName) | ||
|
@@ -63,12 +65,13 @@ public static IQueryable ApplySorting(this IQueryable queryable, QueryOptions qu | |
} | ||
|
||
var orderingProperty = GetPropertyInfoRecursively(queryable, queryOptions.SortPropertyName); | ||
if (orderingProperty == null) | ||
if (orderingProperty.declaringType == null | ||
|| orderingProperty.property == null) | ||
{ | ||
return queryable; | ||
} | ||
|
||
var orderByExp = CreateExpression(queryable.ElementType, queryOptions.SortPropertyName); | ||
var orderByExp = CreateExpression(orderingProperty.declaringType, queryOptions.SortPropertyName); | ||
if (orderByExp == null) | ||
{ | ||
return queryable; | ||
|
@@ -79,7 +82,7 @@ public static IQueryable ApplySorting(this IQueryable queryable, QueryOptions qu | |
queryable = queryable.WrapInNullChecksIfAccessingNestedProperties(queryable.ElementType, queryOptions.SortPropertyName); | ||
var wrappedExpression = Expression.Call(typeof(Queryable), | ||
orderMethodName, | ||
new[] { queryable.ElementType, orderingProperty.PropertyType }, | ||
new [] { orderingProperty.declaringType, orderingProperty.property.PropertyType }, | ||
queryable.Expression, | ||
Expression.Quote(orderByExp)); | ||
var result = queryable.Provider.CreateQuery(wrappedExpression); | ||
|
@@ -100,7 +103,6 @@ private static IQueryable WrapInNullChecksIfAccessingNestedProperties(this IQuer | |
// queryable | ||
// .Where(x => x.Product != null) | ||
// .Where(x => x.Product.Data != null) | ||
|
||
for (var i = 0; i < members.Length - 1; i++) | ||
{ | ||
var member = members[i]; | ||
|
@@ -114,7 +116,6 @@ private static IQueryable WrapInNullChecksIfAccessingNestedProperties(this IQuer | |
var memberPath = members | ||
.TakeWhile((mem, index) => index <= i) | ||
.Aggregate((c, n) => c + "." + n); | ||
var propertyType = GetPropertyInfoRecursively(queryable, memberPath).PropertyType; | ||
var notNullExpression = Expression.NotEqual(body, Expression.Constant(null)); | ||
var notNullLambda = Expression.Lambda(notNullExpression, param); | ||
var whereMethodName = nameof(Queryable.Where); | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
118 changes: 118 additions & 0 deletions
118
test/LightQuery.Shared.Tests/Regression/InheritedPropertiesSort.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
using Microsoft.Data.Sqlite; | ||
using Microsoft.EntityFrameworkCore; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Threading.Tasks; | ||
using Xunit; | ||
|
||
namespace LightQuery.Shared.Tests.Regression | ||
{ | ||
public class InheritedPropertiesSortWithSqlite | ||
{ | ||
public class UserBase | ||
{ | ||
public int Id { get; set; } | ||
public string Name { get; set; } | ||
public int? FavoriteRestaurantId { get; set; } | ||
public Restaurant FavoriteRestaurant { get; set; } | ||
} | ||
|
||
public class User : UserBase { } | ||
|
||
public class Restaurant | ||
{ | ||
public int Id { get; set; } | ||
public string Street { get; set; } | ||
} | ||
|
||
public class AppDbContext : DbContext | ||
{ | ||
public AppDbContext(DbContextOptions options) : base(options) | ||
{ | ||
} | ||
|
||
public DbSet<UserBase> Users { get; set; } | ||
} | ||
|
||
private static Dictionary<string, SqliteConnection> _connections = new Dictionary<string, SqliteConnection>(); | ||
|
||
private static async Task<AppDbContext> GetContextAsync() | ||
{ | ||
var connectionId = Guid.NewGuid().ToString(); | ||
var connectionString = new SqliteConnectionStringBuilder | ||
{ | ||
DataSource = $"in-memory-{connectionId}.db", | ||
Cache = SqliteCacheMode.Shared, | ||
Mode = SqliteOpenMode.Memory | ||
}.ToString(); | ||
var connection = new SqliteConnection(connectionString); | ||
await connection.OpenAsync(); | ||
_connections.Add(connectionId, connection); | ||
|
||
var serviceProvider = new ServiceCollection() | ||
.AddDbContext<AppDbContext>(options => options | ||
.UseSqlite(connectionString)) | ||
.BuildServiceProvider(); | ||
using (var setupContext = serviceProvider.CreateScope().ServiceProvider.GetRequiredService<AppDbContext>()) | ||
{ | ||
await setupContext.Database.EnsureCreatedAsync(); | ||
setupContext.Users.Add(new UserBase { Name = "George", FavoriteRestaurant = new Restaurant { Street = "Main Street" } }); | ||
setupContext.Users.Add(new UserBase { Name = "Steve", FavoriteRestaurant = new Restaurant { Street = "Forest Street" } }); | ||
setupContext.Users.Add(new UserBase { Name = "Bob" }); | ||
await setupContext.SaveChangesAsync(); | ||
} | ||
|
||
return serviceProvider.GetRequiredService<AppDbContext>(); | ||
} | ||
|
||
[Fact] | ||
public async Task CanSortOnPropertyFromBaseClassWhenProjectedToDerivedClass() | ||
{ | ||
var context = await GetContextAsync(); | ||
|
||
var usersQueryable = context.Users | ||
.Select(u => new User | ||
{ | ||
Name = u.Name | ||
}); | ||
|
||
var queryOptions = new QueryOptions | ||
{ | ||
SortPropertyName = nameof(User.Name) | ||
}; | ||
|
||
var orderedQuery = QueryableProcessor.ApplySorting(usersQueryable, queryOptions); | ||
|
||
dynamic dynamicOrderedQuery = orderedQuery; | ||
|
||
var count = Queryable.Count(dynamicOrderedQuery); | ||
|
||
Assert.Equal(3, count); | ||
} | ||
|
||
[Fact] | ||
public async Task CanSortOnNestedProperty() | ||
{ | ||
// This test ensures that relational sorting works with SQLiteS | ||
var context = await GetContextAsync(); | ||
|
||
var usersQueryable = context.Users; | ||
var queryOptions = new QueryOptions | ||
{ | ||
SortPropertyName = "FavoriteRestaurant.Street" | ||
}; | ||
|
||
var orderedQuery = QueryableProcessor.ApplySorting(usersQueryable, queryOptions); | ||
|
||
dynamic dynamicOrderedQuery = orderedQuery; | ||
|
||
var count = Queryable.Count(dynamicOrderedQuery); | ||
|
||
// It should only have two results since the added null checks remove | ||
// the user with the missing FavoriteRestaurant | ||
Assert.Equal(2, count); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
propertyInfo can be null so this could throw NRE.