-
Notifications
You must be signed in to change notification settings - Fork 164
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fixes #766, Support in memory filtering after ODataQueryOptions.ApplyTo #824
base: release-8.x
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,218 @@ | ||||||||||||||||||
//----------------------------------------------------------------------------- | ||||||||||||||||||
// <copyright file="IQueryableODataExtension.cs" company=".NET Foundation"> | ||||||||||||||||||
// Copyright (c) .NET Foundation and Contributors. All rights reserved. | ||||||||||||||||||
// See License.txt in the project root for license information. | ||||||||||||||||||
// </copyright> | ||||||||||||||||||
//------------------------------------------------------------------------------ | ||||||||||||||||||
|
||||||||||||||||||
using System; | ||||||||||||||||||
using System.Collections; | ||||||||||||||||||
using System.Collections.Generic; | ||||||||||||||||||
using System.Diagnostics.CodeAnalysis; | ||||||||||||||||||
using System.Globalization; | ||||||||||||||||||
using System.Linq; | ||||||||||||||||||
using System.Reflection; | ||||||||||||||||||
using Microsoft.AspNetCore.OData.Common; | ||||||||||||||||||
using Microsoft.AspNetCore.OData.Edm; | ||||||||||||||||||
using Microsoft.AspNetCore.OData.Formatter.Deserialization; | ||||||||||||||||||
using Microsoft.AspNetCore.OData.Query.Container; | ||||||||||||||||||
using Microsoft.AspNetCore.OData.Query.Wrapper; | ||||||||||||||||||
using Microsoft.OData; | ||||||||||||||||||
using Microsoft.OData.Edm; | ||||||||||||||||||
|
||||||||||||||||||
namespace Microsoft.AspNetCore.OData.Query | ||||||||||||||||||
{ | ||||||||||||||||||
/// <summary> | ||||||||||||||||||
/// The cast options. | ||||||||||||||||||
/// </summary> | ||||||||||||||||||
public class ODataCastOptions | ||||||||||||||||||
{ | ||||||||||||||||||
/// <summary> | ||||||||||||||||||
/// Gets/sets the map provider. | ||||||||||||||||||
/// </summary> | ||||||||||||||||||
public Func<IEdmModel, IEdmStructuredType, IPropertyMapper> MapProvider { get; set; } | ||||||||||||||||||
} | ||||||||||||||||||
Comment on lines
+25
to
+34
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we move each class to its own file? |
||||||||||||||||||
|
||||||||||||||||||
/// <summary> | ||||||||||||||||||
/// Provides a set of static methods for querying data structures that implement <see cref="IQueryable"/> | ||||||||||||||||||
/// </summary> | ||||||||||||||||||
public static class IQueryableODataExtension | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Usually, extension classes on interfaces do not have the
Suggested change
|
||||||||||||||||||
{ | ||||||||||||||||||
/// <summary> | ||||||||||||||||||
/// Converts the source to the specified type. | ||||||||||||||||||
/// </summary> | ||||||||||||||||||
/// <typeparam name="TResult">The type to convert the source to.</typeparam> | ||||||||||||||||||
/// <param name="source">The source.</param> | ||||||||||||||||||
/// <param name="options">The cast options.</param> | ||||||||||||||||||
/// <returns>The converted object if it's OData object. Otherwise, return same source or the default.</returns> | ||||||||||||||||||
[SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "<Pending>")] | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you really want to suppress this, we should add the justification there. |
||||||||||||||||||
public static TResult OCast<TResult>(this object source, ODataCastOptions options = null) | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think "OCast" is a really bad name that is not intuitive/semantic at all... I don't have an immediate suggestion, but I'd strongly suggest reconsidering this name. |
||||||||||||||||||
{ | ||||||||||||||||||
if (source is null) | ||||||||||||||||||
{ | ||||||||||||||||||
return default; | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
Type sourceType = source.GetType(); | ||||||||||||||||||
if (source is SelectExpandWrapper wrapper) | ||||||||||||||||||
{ | ||||||||||||||||||
return (TResult)CreateInstance(wrapper, sourceType, options); | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
if (typeof(TResult) == sourceType) | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This check looks weird to me as it is. Would recommend swapping the values:
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can't we just do:
Suggested change
? |
||||||||||||||||||
{ | ||||||||||||||||||
return (TResult)source; | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
if (typeof(TResult).IsAssignableFrom(sourceType)) | ||||||||||||||||||
{ | ||||||||||||||||||
return (TResult)Convert.ChangeType(source, typeof(TResult), CultureInfo.InvariantCulture); | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
return default; | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
/// <summary> | ||||||||||||||||||
/// Converts the elements of an <see cref="IQueryable"/> to the specified type. | ||||||||||||||||||
/// </summary> | ||||||||||||||||||
/// <typeparam name="TResult">The type to convert the elements of source to.</typeparam> | ||||||||||||||||||
/// <param name="source">The <see cref="IQueryable"/> that contains the elements to be converted.</param> | ||||||||||||||||||
/// <param name="options">The cast options.</param> | ||||||||||||||||||
/// <returns>Contains each element of the source sequence converted to the specified type.</returns> | ||||||||||||||||||
[SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "<Pending>")] | ||||||||||||||||||
public static IEnumerable<TResult> OCast<TResult>(this IQueryable source, ODataCastOptions options = null) | ||||||||||||||||||
{ | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. shouldn't we cast to iqueryable and then do the operations? in cases where the queryable is not a selectexpand, what's written here will result in executing the query on the server just to find out that the method should have been a no-op |
||||||||||||||||||
if (source is null) | ||||||||||||||||||
{ | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this won't throw until enumeration; use |
||||||||||||||||||
throw Error.ArgumentNull(nameof(source)); | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
foreach (var item in source) | ||||||||||||||||||
{ | ||||||||||||||||||
yield return OCast<TResult>(item, options); | ||||||||||||||||||
} | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
/// <summary> | ||||||||||||||||||
/// Create the object based on <see cref="SelectExpandWrapper"/> | ||||||||||||||||||
/// </summary> | ||||||||||||||||||
/// <param name="wrapper">The select expand wrapper.</param> | ||||||||||||||||||
/// <param name="resultType">The created type.</param> | ||||||||||||||||||
/// <param name="options">The options.</param> | ||||||||||||||||||
/// <returns>The created object.</returns> | ||||||||||||||||||
private static object CreateInstance(SelectExpandWrapper wrapper, Type resultType, ODataCastOptions options) | ||||||||||||||||||
{ | ||||||||||||||||||
object instance; | ||||||||||||||||||
Type instanceType = resultType; | ||||||||||||||||||
IEdmModel model = wrapper.Model; | ||||||||||||||||||
if (wrapper.UseInstanceForProperties) | ||||||||||||||||||
{ | ||||||||||||||||||
instance = wrapper.InstanceValue; | ||||||||||||||||||
instanceType = instance.GetType(); | ||||||||||||||||||
} | ||||||||||||||||||
else | ||||||||||||||||||
{ | ||||||||||||||||||
if (wrapper.InstanceType != null && wrapper.InstanceType != resultType.FullName) | ||||||||||||||||||
{ | ||||||||||||||||||
// inheritance | ||||||||||||||||||
IEdmTypeReference typeReference = wrapper.GetEdmType(); | ||||||||||||||||||
instanceType = model.GetClrType(typeReference); // inheritance type | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
instance = Activator.CreateInstance(instanceType); | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
IDictionary<string, object> properties; | ||||||||||||||||||
if (options != null && options.MapProvider != null) | ||||||||||||||||||
{ | ||||||||||||||||||
properties = wrapper.ToDictionary(options.MapProvider); | ||||||||||||||||||
} | ||||||||||||||||||
Comment on lines
+126
to
+129
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Prefer pattern matching for this type of check+extract:
Suggested change
|
||||||||||||||||||
else | ||||||||||||||||||
{ | ||||||||||||||||||
properties = wrapper.ToDictionary(); | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
foreach (var property in properties) | ||||||||||||||||||
{ | ||||||||||||||||||
string propertyName = property.Key; | ||||||||||||||||||
object propertyValue = property.Value; | ||||||||||||||||||
Comment on lines
+135
to
+138
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since
Suggested change
|
||||||||||||||||||
if (propertyValue == null) | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For consistency in new code.
Suggested change
|
||||||||||||||||||
{ | ||||||||||||||||||
// If it's null, we don't need to do anything. | ||||||||||||||||||
continue; | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
PropertyInfo propertyInfo = GetPropertyInfo(instanceType, propertyName); | ||||||||||||||||||
if (propertyInfo == null) | ||||||||||||||||||
{ | ||||||||||||||||||
throw new ODataException(Error.Format(SRResources.PropertyNotFound, instanceType.FullName, propertyName)); | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
bool isCollection = TypeHelper.IsCollection(propertyInfo.PropertyType, out Type elementType); | ||||||||||||||||||
|
||||||||||||||||||
if (isCollection) | ||||||||||||||||||
{ | ||||||||||||||||||
IList<object> collection = new List<object>(); | ||||||||||||||||||
IEnumerable collectionPropertyValue = propertyValue as IEnumerable; | ||||||||||||||||||
foreach (var item in collectionPropertyValue) | ||||||||||||||||||
Comment on lines
+156
to
+157
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a mistake.
Suggested change
|
||||||||||||||||||
{ | ||||||||||||||||||
object itemValue = CreateValue(item, elementType, options); | ||||||||||||||||||
collection.Add(itemValue); | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
IEdmTypeReference typeRef = model.GetEdmTypeReference(propertyInfo.PropertyType); | ||||||||||||||||||
IEdmCollectionTypeReference collectionTypeRef = typeRef.AsCollection(); | ||||||||||||||||||
|
||||||||||||||||||
DeserializationHelpers.SetCollectionProperty(instance, propertyName, collectionTypeRef, collection, true, null); | ||||||||||||||||||
} | ||||||||||||||||||
else | ||||||||||||||||||
{ | ||||||||||||||||||
object itemValue = CreateValue(propertyValue, elementType, options); | ||||||||||||||||||
propertyInfo.SetValue(instance, itemValue); | ||||||||||||||||||
} | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
return instance; | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
private static object CreateValue(object value, Type elementType, ODataCastOptions options) | ||||||||||||||||||
{ | ||||||||||||||||||
Type valueType = value.GetType(); | ||||||||||||||||||
|
||||||||||||||||||
if (typeof(ISelectExpandWrapper).IsAssignableFrom(valueType)) | ||||||||||||||||||
{ | ||||||||||||||||||
SelectExpandWrapper subWrapper = value as SelectExpandWrapper; | ||||||||||||||||||
Comment on lines
+182
to
+184
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks dangerous to me. We are checking for a match with the interface It could be possible that the value implements the interface but is not the concrete |
||||||||||||||||||
return CreateInstance(subWrapper, elementType, options); | ||||||||||||||||||
} | ||||||||||||||||||
else | ||||||||||||||||||
{ | ||||||||||||||||||
return value; | ||||||||||||||||||
} | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
private static PropertyInfo GetPropertyInfo(Type type, string propertyName) | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like this could be extracted to its own separate extension method. To me this is such a generic concern that doesn't need to be inside the OData-specific extension class. |
||||||||||||||||||
{ | ||||||||||||||||||
PropertyInfo propertyInfo = null; | ||||||||||||||||||
var properties = type.GetProperties().Where(p => p.Name == propertyName).ToArray(); | ||||||||||||||||||
if (properties.Length <= 0) | ||||||||||||||||||
{ | ||||||||||||||||||
propertyInfo = null; | ||||||||||||||||||
} | ||||||||||||||||||
else if (properties.Length == 1) | ||||||||||||||||||
{ | ||||||||||||||||||
propertyInfo = properties[0]; | ||||||||||||||||||
} | ||||||||||||||||||
else | ||||||||||||||||||
{ | ||||||||||||||||||
// resolve 'new' modifier | ||||||||||||||||||
propertyInfo = properties.FirstOrDefault(p => p.DeclaringType == type); | ||||||||||||||||||
Comment on lines
+195
to
+208
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Duplicated code. |
||||||||||||||||||
if (propertyInfo == null) | ||||||||||||||||||
{ | ||||||||||||||||||
throw new ODataException(Error.Format(SRResources.AmbiguousPropertyNameFound, propertyName)); | ||||||||||||||||||
} | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
return propertyInfo; | ||||||||||||||||||
} | ||||||||||||||||||
} | ||||||||||||||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks very convoluted to me. Wouldn't this result in the exact same behavior?
Also, I wonder if it isn't possible to use one of the
GetProperty
overloads withBindingFlags
to get the same behavior in an even cleaner way without having to go through every single property on the type.Did you check that possibility?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did use BindingFlags, but it seems it can't work. (Maybe what I tried is not enough).