diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Value/TypedEdmStructuredObject.cs b/src/Microsoft.AspNetCore.OData/Formatter/Value/TypedEdmStructuredObject.cs index 468beb486..eabc3b485 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Value/TypedEdmStructuredObject.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Value/TypedEdmStructuredObject.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Concurrent; using System.Diagnostics.Contracts; +using System.Linq; using System.Reflection; using Microsoft.AspNetCore.OData.Common; using Microsoft.AspNetCore.OData.Edm; @@ -108,14 +109,32 @@ internal static Func GetOrCreatePropertyGetter( private static Func CreatePropertyGetter(Type type, string propertyName) { - PropertyInfo property = type.GetProperty(propertyName); + 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); + if (propertyInfo == null) + { + propertyInfo = properties[0]; + } + } - if (property == null) + if (propertyInfo == null) { return null; } - var helper = new PropertyHelper(property); + var helper = new PropertyHelper(propertyInfo); return helper.GetValue; } diff --git a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml index 8b856e5c4..79a127dc9 100644 --- a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml +++ b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml @@ -9618,6 +9618,48 @@ A http request containing the query options. A string representing the query options part of an OData URL. + + + The cast options. + + + + + Gets/sets the map provider. + + + + + Provides a set of static methods for querying data structures that implement + + + + + Converts the source to the specified type. + + The type to convert the source to. + The source. + The cast options. + The converted object if it's OData object. Otherwise, return same source or the default. + + + + Converts the elements of an to the specified type. + + The type to convert the elements of source to. + The that contains the elements to be converted. + The cast options. + Contains each element of the source sequence converted to the specified type. + + + + Create the object based on + + The select expand wrapper. + The created type. + The options. + The created object. + This class describes the model bound settings to use during query composition. @@ -12240,6 +12282,11 @@ Indicates whether the underlying instance can be used to obtain property values. + + + Gets the instance value. + + @@ -12277,6 +12324,11 @@ Gets or sets the instance of the element being selected and expanded. + + + Gets the instance value. + + Represents a result that when executed will produce a Bad Request (400) response. diff --git a/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt b/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt index 5de14d2c3..22057d770 100644 --- a/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt +++ b/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt @@ -934,6 +934,11 @@ Microsoft.AspNetCore.OData.Query.HttpRequestODataQueryExtensions Microsoft.AspNetCore.OData.Query.IODataQueryRequestParser Microsoft.AspNetCore.OData.Query.IODataQueryRequestParser.CanParse(Microsoft.AspNetCore.Http.HttpRequest request) -> bool Microsoft.AspNetCore.OData.Query.IODataQueryRequestParser.ParseAsync(Microsoft.AspNetCore.Http.HttpRequest request) -> System.Threading.Tasks.Task +Microsoft.AspNetCore.OData.Query.IQueryableODataExtension +Microsoft.AspNetCore.OData.Query.ODataCastOptions +Microsoft.AspNetCore.OData.Query.ODataCastOptions.MapProvider.get -> System.Func +Microsoft.AspNetCore.OData.Query.ODataCastOptions.MapProvider.set -> void +Microsoft.AspNetCore.OData.Query.ODataCastOptions.ODataCastOptions() -> void Microsoft.AspNetCore.OData.Query.ODataQueryContext Microsoft.AspNetCore.OData.Query.ODataQueryContext.DefaultQuerySettings.get -> Microsoft.OData.ModelBuilder.Config.DefaultQuerySettings Microsoft.AspNetCore.OData.Query.ODataQueryContext.ElementClrType.get -> System.Type @@ -1709,6 +1714,8 @@ static Microsoft.AspNetCore.OData.Query.Expressions.QueryBinder.ApplyNullPropaga static Microsoft.AspNetCore.OData.Query.Expressions.QueryBinder.GetDynamicPropertyContainer(Microsoft.OData.UriParser.SingleValueOpenPropertyAccessNode openNode, Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext context) -> System.Reflection.PropertyInfo static Microsoft.AspNetCore.OData.Query.HttpRequestODataQueryExtensions.GetETag(this Microsoft.AspNetCore.Http.HttpRequest request, Microsoft.Net.Http.Headers.EntityTagHeaderValue entityTagHeaderValue) -> Microsoft.AspNetCore.OData.Query.ETag static Microsoft.AspNetCore.OData.Query.HttpRequestODataQueryExtensions.GetETag(this Microsoft.AspNetCore.Http.HttpRequest request, Microsoft.Net.Http.Headers.EntityTagHeaderValue entityTagHeaderValue) -> Microsoft.AspNetCore.OData.Query.ETag +static Microsoft.AspNetCore.OData.Query.IQueryableODataExtension.OCast(this object source, Microsoft.AspNetCore.OData.Query.ODataCastOptions options = null) -> TResult +static Microsoft.AspNetCore.OData.Query.IQueryableODataExtension.OCast(this System.Linq.IQueryable source, Microsoft.AspNetCore.OData.Query.ODataCastOptions options = null) -> System.Collections.Generic.IEnumerable static Microsoft.AspNetCore.OData.Query.ODataQueryOptions.IsSystemQueryOption(string queryOptionName) -> bool static Microsoft.AspNetCore.OData.Query.ODataQueryOptions.IsSystemQueryOption(string queryOptionName, bool isDollarSignOptional) -> bool static Microsoft.AspNetCore.OData.Query.ODataQueryOptions.LimitResults(System.Linq.IQueryable queryable, int limit, bool parameterize, out bool resultsLimited) -> System.Linq.IQueryable diff --git a/src/Microsoft.AspNetCore.OData/Query/IQueryableODataExtension.cs b/src/Microsoft.AspNetCore.OData/Query/IQueryableODataExtension.cs new file mode 100644 index 000000000..f20ac4ec3 --- /dev/null +++ b/src/Microsoft.AspNetCore.OData/Query/IQueryableODataExtension.cs @@ -0,0 +1,218 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +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 +{ + /// + /// The cast options. + /// + public class ODataCastOptions + { + /// + /// Gets/sets the map provider. + /// + public Func MapProvider { get; set; } + } + + /// + /// Provides a set of static methods for querying data structures that implement + /// + public static class IQueryableODataExtension + { + /// + /// Converts the source to the specified type. + /// + /// The type to convert the source to. + /// The source. + /// The cast options. + /// The converted object if it's OData object. Otherwise, return same source or the default. + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "")] + public static TResult OCast(this object source, ODataCastOptions options = null) + { + 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) + { + return (TResult)source; + } + + if (typeof(TResult).IsAssignableFrom(sourceType)) + { + return (TResult)Convert.ChangeType(source, typeof(TResult), CultureInfo.InvariantCulture); + } + + return default; + } + + /// + /// Converts the elements of an to the specified type. + /// + /// The type to convert the elements of source to. + /// The that contains the elements to be converted. + /// The cast options. + /// Contains each element of the source sequence converted to the specified type. + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "")] + public static IEnumerable OCast(this IQueryable source, ODataCastOptions options = null) + { + if (source is null) + { + throw Error.ArgumentNull(nameof(source)); + } + + foreach (var item in source) + { + yield return OCast(item, options); + } + } + + /// + /// Create the object based on + /// + /// The select expand wrapper. + /// The created type. + /// The options. + /// The created object. + 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 properties; + if (options != null && options.MapProvider != null) + { + properties = wrapper.ToDictionary(options.MapProvider); + } + else + { + properties = wrapper.ToDictionary(); + } + + foreach (var property in properties) + { + string propertyName = property.Key; + object propertyValue = property.Value; + if (propertyValue == null) + { + // 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 collection = new List(); + IEnumerable collectionPropertyValue = propertyValue as IEnumerable; + foreach (var item in collectionPropertyValue) + { + 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; + return CreateInstance(subWrapper, elementType, options); + } + else + { + return value; + } + } + + private static PropertyInfo GetPropertyInfo(Type type, string propertyName) + { + 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); + if (propertyInfo == null) + { + throw new ODataException(Error.Format(SRResources.AmbiguousPropertyNameFound, propertyName)); + } + } + + return propertyInfo; + } + } +} diff --git a/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectExpandWrapper.cs b/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectExpandWrapper.cs index 194155a62..437e7c560 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectExpandWrapper.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectExpandWrapper.cs @@ -48,6 +48,11 @@ internal abstract class SelectExpandWrapper : IEdmEntityObject, ISelectExpandWra /// public bool UseInstanceForProperties { get; set; } + /// + /// Gets the instance value. + /// + public abstract object InstanceValue { get; } + /// public IEdmTypeReference GetEdmType() { diff --git a/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectExpandWrapperOfT.cs b/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectExpandWrapperOfT.cs index 22ce62991..490eb0dd3 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectExpandWrapperOfT.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectExpandWrapperOfT.cs @@ -36,6 +36,11 @@ public TElement Instance set { UntypedInstance = value; } } + /// + /// Gets the instance value. + /// + public override object InstanceValue => Instance; + protected override Type GetElementType() { return UntypedInstance == null ? typeof(TElement) : UntypedInstance.GetType(); diff --git a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net6.bsl b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net6.bsl index 06096caf0..9e453ea65 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net6.bsl +++ b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net6.bsl @@ -1226,6 +1226,21 @@ public sealed class Microsoft.AspNetCore.OData.Query.HttpRequestODataQueryExtens public static ETag`1 GetETag (Microsoft.AspNetCore.Http.HttpRequest request, Microsoft.Net.Http.Headers.EntityTagHeaderValue entityTagHeaderValue) } +[ +ExtensionAttribute(), +] +public sealed class Microsoft.AspNetCore.OData.Query.IQueryableODataExtension { + [ + ExtensionAttribute(), + ] + public static IEnumerable`1 OCast (System.Linq.IQueryable source, params Microsoft.AspNetCore.OData.Query.ODataCastOptions options) + + [ + ExtensionAttribute(), + ] + public static TResult OCast (object source, params Microsoft.AspNetCore.OData.Query.ODataCastOptions options) +} + public class Microsoft.AspNetCore.OData.Query.ApplyQueryOption { public ApplyQueryOption (string rawValue, Microsoft.AspNetCore.OData.Query.ODataQueryContext context, Microsoft.OData.UriParser.ODataQueryOptionParser queryOptionParser) @@ -1349,6 +1364,12 @@ public class Microsoft.AspNetCore.OData.Query.FilterQueryOption { public void Validate (Microsoft.AspNetCore.OData.Query.Validator.ODataValidationSettings validationSettings) } +public class Microsoft.AspNetCore.OData.Query.ODataCastOptions { + public ODataCastOptions () + + System.Func`3[[Microsoft.OData.Edm.IEdmModel],[Microsoft.OData.Edm.IEdmStructuredType],[Microsoft.AspNetCore.OData.Query.Container.IPropertyMapper]] MapProvider { public get; public set; } +} + public class Microsoft.AspNetCore.OData.Query.ODataQueryContext { public ODataQueryContext (Microsoft.OData.Edm.IEdmModel model, Microsoft.OData.Edm.IEdmType elementType, Microsoft.OData.UriParser.ODataPath path) public ODataQueryContext (Microsoft.OData.Edm.IEdmModel model, System.Type elementClrType, Microsoft.OData.UriParser.ODataPath path) diff --git a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.NetCore31.bsl b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.NetCore31.bsl index 06096caf0..9e453ea65 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.NetCore31.bsl +++ b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.NetCore31.bsl @@ -1226,6 +1226,21 @@ public sealed class Microsoft.AspNetCore.OData.Query.HttpRequestODataQueryExtens public static ETag`1 GetETag (Microsoft.AspNetCore.Http.HttpRequest request, Microsoft.Net.Http.Headers.EntityTagHeaderValue entityTagHeaderValue) } +[ +ExtensionAttribute(), +] +public sealed class Microsoft.AspNetCore.OData.Query.IQueryableODataExtension { + [ + ExtensionAttribute(), + ] + public static IEnumerable`1 OCast (System.Linq.IQueryable source, params Microsoft.AspNetCore.OData.Query.ODataCastOptions options) + + [ + ExtensionAttribute(), + ] + public static TResult OCast (object source, params Microsoft.AspNetCore.OData.Query.ODataCastOptions options) +} + public class Microsoft.AspNetCore.OData.Query.ApplyQueryOption { public ApplyQueryOption (string rawValue, Microsoft.AspNetCore.OData.Query.ODataQueryContext context, Microsoft.OData.UriParser.ODataQueryOptionParser queryOptionParser) @@ -1349,6 +1364,12 @@ public class Microsoft.AspNetCore.OData.Query.FilterQueryOption { public void Validate (Microsoft.AspNetCore.OData.Query.Validator.ODataValidationSettings validationSettings) } +public class Microsoft.AspNetCore.OData.Query.ODataCastOptions { + public ODataCastOptions () + + System.Func`3[[Microsoft.OData.Edm.IEdmModel],[Microsoft.OData.Edm.IEdmStructuredType],[Microsoft.AspNetCore.OData.Query.Container.IPropertyMapper]] MapProvider { public get; public set; } +} + public class Microsoft.AspNetCore.OData.Query.ODataQueryContext { public ODataQueryContext (Microsoft.OData.Edm.IEdmModel model, Microsoft.OData.Edm.IEdmType elementType, Microsoft.OData.UriParser.ODataPath path) public ODataQueryContext (Microsoft.OData.Edm.IEdmModel model, System.Type elementClrType, Microsoft.OData.UriParser.ODataPath path) diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/IQueryableODataExtensionTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/IQueryableODataExtensionTests.cs new file mode 100644 index 000000000..aa867a72d --- /dev/null +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/IQueryableODataExtensionTests.cs @@ -0,0 +1,236 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.OData.Query; +using Microsoft.AspNetCore.OData.Query.Expressions; +using Microsoft.AspNetCore.OData.TestCommon; +using Microsoft.AspNetCore.OData.Tests.Query.Expressions; +using Microsoft.OData.Edm; +using Xunit; + +namespace Microsoft.AspNetCore.OData.Tests.Query +{ + public class IQueryableODataExtensionTests + { + private readonly ODataQuerySettings _settings; + private readonly IEdmModel _model; + + public IQueryableODataExtensionTests() + { + _model = SelectExpandBinderTest.GetEdmModel(); + _settings = new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.False }; + } + + [Fact] + public void OCast_Works_ForSingleObject() + { + // Arrange & Act & Assert + object source = null; + Assert.Null(source.OCast()); + + // Arrange & Act & Assert + QueryCustomer customer = new QueryCustomer + { + Name = "A" + }; + + Assert.Null(customer.OCast()); + + var expectCustomer = customer.OCast(); + Assert.Same(customer, expectCustomer); + } + + [Fact] + public void OCast_Works_ForSingleSelectAndExpand() + { + // Arrange + ODataQueryContext context = new ODataQueryContext(_model, typeof(QueryCustomer)) { RequestContainer = new MockServiceProvider() }; + SelectExpandQueryOption selectExpand = new SelectExpandQueryOption(select: "Name", expand: null, context: context); + QueryCustomer customer = new QueryCustomer + { + Name = "A", + Age = 42 + }; + + SelectExpandBinder binder = new SelectExpandBinder(); + QueryBinderContext queryBinderContext = new QueryBinderContext(_model, _settings, typeof(QueryCustomer)) + { + NavigationSource = context.NavigationSource + }; + + object result = binder.ApplyBind(customer, selectExpand.SelectExpandClause, queryBinderContext); + + // Act + QueryCustomer expectedCustomer = result.OCast(); + + // Assert + Assert.Equal("A", expectedCustomer.Name); + Assert.Equal(0, expectedCustomer.Age); + } + + [Theory] + [InlineData("Name", false)] + [InlineData("Name,Age", true)] + public void OCast_Works_ForQuerableSelectAndExpand(string select, bool withAge) + { + // Arrange + ODataQueryContext context = new ODataQueryContext(_model, typeof(QueryCustomer)) { RequestContainer = new MockServiceProvider() }; + SelectExpandQueryOption selectExpand = new SelectExpandQueryOption(select, expand: null, context: context); + IQueryable customers = new[] { + new QueryCustomer + { + Name = "A", + Age = 42 + }, + new QueryCustomer + { + Name = "B", + Age = 38 + } + }.AsQueryable(); + + SelectExpandBinder binder = new SelectExpandBinder(); + QueryBinderContext queryBinderContext = new QueryBinderContext(_model, _settings, typeof(QueryCustomer)) + { + NavigationSource = context.NavigationSource + }; + + IQueryable queryable = binder.ApplyBind(customers, selectExpand.SelectExpandClause, queryBinderContext); + + // Act + IEnumerable expectedCustomers = queryable.OCast(); + + // Assert + Assert.Collection(expectedCustomers, + e => + { + Assert.Equal("A", e.Name); + + if (withAge) + Assert.Equal(42, e.Age); + else + Assert.Equal(0, e.Age); + }, + e => + { + Assert.Equal("B", e.Name); + + if (withAge) + Assert.Equal(38, e.Age); + else + Assert.Equal(0, e.Age); + }); + } + + [Fact] + public void OCast_Works_ForInheritancePropertiesQuerableSelectAndExpand() + { + // Arrange + ODataQueryContext context = new ODataQueryContext(_model, typeof(QueryVipCustomer)) { RequestContainer = new MockServiceProvider() }; + SelectExpandQueryOption selectExpand = new SelectExpandQueryOption(select: "Taxes($top=2)", expand: null, context: context); + IQueryable customers = new[] { + new QueryVipCustomer + { + Name = "A", + Age = 42, + Taxes = new List { 1, 2, 3, 4, 5} + }, + new QueryVipCustomer + { + Name = "B", + Age = 38, + Taxes = new List { 6, 7, 8, 9, 10} + } + }.AsQueryable(); + + SelectExpandBinder binder = new SelectExpandBinder(); + QueryBinderContext queryBinderContext = new QueryBinderContext(_model, _settings, typeof(QueryCustomer)) + { + NavigationSource = context.NavigationSource + }; + + IQueryable queryable = binder.ApplyBind(customers, selectExpand.SelectExpandClause, queryBinderContext); + + // Act + IEnumerable expectedCustomers = queryable.OCast(); + + // Assert + Assert.Collection(expectedCustomers, + e => + { + QueryVipCustomer vipCustomer = Assert.IsType(e); + Assert.Null(vipCustomer.Name); + Assert.NotNull(vipCustomer.Taxes); + Assert.Equal(new int[] { 1, 2 }, vipCustomer.Taxes); + }, + e => + { + QueryVipCustomer vipCustomer = Assert.IsType(e); + Assert.Null(vipCustomer.Name); + Assert.NotNull(vipCustomer.Taxes); + Assert.Equal(new int[] { 6, 7 }, vipCustomer.Taxes); + }); + } + + [Fact] + public void OCast_Works_ForCollectionSelectAndExpand() + { + QueryCustomer customer = new QueryCustomer + { + Name = "A", + Orders = new List() + }; + QueryOrder order = new QueryOrder { Customer = customer }; + customer.Orders.Add(order); + + QueryCustomer vipCustomer = new QueryVipCustomer + { + Name = "B", + Orders = new List() + }; + order = new QueryOrder { Customer = vipCustomer }; + vipCustomer.Orders.Add(order); + + var queryableOf = new[] { customer, vipCustomer }.AsQueryable(); + + // Arrange + ODataQueryContext context = new ODataQueryContext(_model, typeof(QueryCustomer)) { RequestContainer = new MockServiceProvider() }; + SelectExpandQueryOption selectExpand = new SelectExpandQueryOption("Orders", "Orders,Orders($expand=Customer)", context); + + // Act + SelectExpandBinder binder = new SelectExpandBinder(); + QueryBinderContext queryBinderContext = new QueryBinderContext(_model, _settings, typeof(QueryCustomer)) + { + NavigationSource = context.NavigationSource + }; + + IQueryable queryable = binder.ApplyBind(queryableOf, selectExpand.SelectExpandClause, queryBinderContext); + IEnumerable expectedCustomers = queryable.OCast(); + + // Assert + Assert.Collection(expectedCustomers, + e => + { + QueryCustomer c = Assert.IsType(e); + Assert.NotNull(c.Orders); + QueryOrder order = Assert.Single(c.Orders); + Assert.NotNull(order.Customer); + Assert.Equal("A", order.Customer.Name); + }, + e => + { + QueryVipCustomer c = Assert.IsType(e); + Assert.NotNull(c.Orders); + QueryOrder order = Assert.Single(c.Orders); + Assert.NotNull(order.Customer); + Assert.Equal("B", order.Customer.Name); + }); + } + } +}