diff --git a/src/Microsoft.OData.Client/ALinq/DataServiceQueryProvider.cs b/src/Microsoft.OData.Client/ALinq/DataServiceQueryProvider.cs index 63bde0aeaf..e04530ffb7 100644 --- a/src/Microsoft.OData.Client/ALinq/DataServiceQueryProvider.cs +++ b/src/Microsoft.OData.Client/ALinq/DataServiceQueryProvider.cs @@ -250,8 +250,8 @@ private TElement ParseAggregateSingletonResult(QueryResult queryResult { case ODataReaderState.ResourceEnd: entry = reader.Item as ODataResource; - IEnumerable properties = entry.Properties.OfType(); - if (entry != null && properties.Any()) + IEnumerable properties = entry.Properties?.OfType(); + if (entry != null && properties?.Any() == true) { ODataProperty aggregationProperty = properties.First(); ODataUntypedValue untypedValue = aggregationProperty.Value as ODataUntypedValue; diff --git a/src/Microsoft.OData.Client/Materialization/ODataEntityMaterializer.cs b/src/Microsoft.OData.Client/Materialization/ODataEntityMaterializer.cs index dbe1d36613..4ef2dc575c 100644 --- a/src/Microsoft.OData.Client/Materialization/ODataEntityMaterializer.cs +++ b/src/Microsoft.OData.Client/Materialization/ODataEntityMaterializer.cs @@ -618,7 +618,7 @@ internal object ProjectionValueForPath(MaterializerEntry entry, Type expectedTyp ODataNestedResourceInfo link = null; ODataProperty odataProperty = null; ICollection links = entry.NestedResourceInfos; - IEnumerable properties = entry.Entry.Properties.OfType(); + IEnumerable properties = entry.Entry.Properties?.OfType(); ClientEdmModel edmModel = this.MaterializerContext.Model; for (int i = 0; i < path.Count; i++) { @@ -663,14 +663,14 @@ internal object ProjectionValueForPath(MaterializerEntry entry, Type expectedTyp // Note that we should only return the default value if the current segment is leaf. // Take for example, select(new { M = (p as Employee).Manager }). If p is Person and Manager is null, we should return null here. // On the other hand select(new { MID = (p as Employee).Manager.ID }) should throw if p is Person and Manager is null. - if (segment.SourceTypeAs != null && !links.Any(p => p.Name == propertyName) && !properties.Any(p => p.Name == propertyName) && segmentIsLeaf) + if (segment.SourceTypeAs != null && !links.Any(p => p.Name == propertyName) && !(properties?.Any(p => p.Name == propertyName) == true) && segmentIsLeaf) { // We are projecting a derived property and entry is of a base type which the property is not defined on. Return null. result = WebUtil.GetDefaultValue(property.PropertyType); break; } - odataProperty = properties.Where(p => p.Name == propertyName).FirstOrDefault(); + odataProperty = properties?.FirstOrDefault(p => p.Name == propertyName); link = odataProperty == null && links != null ? links.Where(p => p.Name == propertyName).FirstOrDefault() : null; if (link == null && odataProperty == null) { @@ -753,7 +753,7 @@ internal object ProjectionValueForPath(MaterializerEntry entry, Type expectedTyp this.InstanceAnnotationMaterializationPolicy.SetInstanceAnnotations(propertyName, linkEntry.Entry, expectedType, entry.ResolvedObject); } - properties = linkEntry.Properties.OfType(); + properties = linkEntry.Properties?.OfType(); links = linkEntry.NestedResourceInfos; result = linkEntry.ResolvedObject; entry = linkEntry; @@ -840,7 +840,7 @@ internal object ProjectionDynamicValueForPath(MaterializerEntry entry, Type expe object result = null; ODataProperty odataProperty = null; - IEnumerable properties = entry.Entry.Properties.OfType(); + IEnumerable properties = entry.Entry.Properties?.OfType(); for (int i = 0; i < path.Count; i++) { @@ -852,7 +852,7 @@ internal object ProjectionDynamicValueForPath(MaterializerEntry entry, Type expe string propertyName = segment.Member; - odataProperty = properties.Where(p => p.Name == propertyName).FirstOrDefault(); + odataProperty = properties?.FirstOrDefault(p => p.Name == propertyName); if (odataProperty == null) { diff --git a/test/FunctionalTests/Microsoft.OData.Client.Tests/ALinq/ProjectionTests.cs b/test/FunctionalTests/Microsoft.OData.Client.Tests/ALinq/ProjectionTests.cs new file mode 100644 index 0000000000..da420e7ebd --- /dev/null +++ b/test/FunctionalTests/Microsoft.OData.Client.Tests/ALinq/ProjectionTests.cs @@ -0,0 +1,220 @@ +//--------------------------------------------------------------------- +// +// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. +// +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Microsoft.OData.Edm; +using Xunit; + +namespace Microsoft.OData.Client.Tests.ALinq +{ + /// + /// Projection tests + /// + public class ProjectionTests + { + private readonly Container ctx; + private readonly string serviceUri = "http://tempuri.org"; + + public ProjectionTests() + { + ctx = new Container(new Uri(serviceUri)); + } + + [Fact] + public void TestProjectionWithNullNestedResourceForQuerySyntaxExpression() + { + // Arrange + InterceptRequestAndMockResponse("{\"@odata.context\":\"http://tempuri.org/$metadata#People(Id,Spouse())\",\"value\":[{\"Id\":2,\"Spouse\":null}]}"); + var query = from p in this.ctx.People + where p.Spouse == null + select new Person + { + Id = p.Id, + Spouse = p.Spouse + }; + var requestUri = query.ToString(); + + // Act + var result = query.ToList(); + + // Assert + Assert.Equal("http://tempuri.org/People?$filter=Spouse eq null&$expand=Spouse&$select=Id", requestUri); + var person = Assert.Single(result); + Assert.Equal(2, person.Id); + Assert.Null(person.Name); + Assert.Null(person.Spouse); + } + + [Fact] + public void TestProjectionWithNullNestedResourceForMethodSyntaxExpression() + { + // Arrange + InterceptRequestAndMockResponse("{\"@odata.context\":\"http://tempuri.org/$metadata#People(Id,Spouse())\",\"value\":[{\"Id\":2,\"Spouse\":null}]}"); + var query = this.ctx.CreateQuery("People").Where(p1 => p1.Spouse == null).Select(p2 =>new Person + { + Id = p2.Id, + Spouse = p2.Spouse + }); + var requestUri = query.ToString(); + + // Act + var result = query.ToList(); + + // Assert + Assert.Equal("http://tempuri.org/People?$filter=Spouse eq null&$expand=Spouse&$select=Id", requestUri); + var person = Assert.Single(result); + Assert.Equal(2, person.Id); + Assert.Null(person.Name); + Assert.Null(person.Spouse); + } + + [Fact] + public void TestProjectionWithNullNestedResourceForAddQueryOption() + { + // Arrange + InterceptRequestAndMockResponse("{\"@odata.context\":\"http://tempuri.org/$metadata#People(Id,Spouse())\",\"value\":[{\"Id\":2,\"Spouse\":null}]}"); + var query = ctx.People.AddQueryOption("$filter", "Spouse eq null").AddQueryOption("$expand", "Spouse").AddQueryOption("$select", "Id"); + var requestUri = query.ToString(); + + // Act + var result = query.ToList(); + + // Assert + Assert.Equal("http://tempuri.org/People?$expand=Spouse&$filter=Spouse eq null&$select=Id", requestUri); + var person = Assert.Single(result); + Assert.Equal(2, person.Id); + Assert.Null(person.Name); + Assert.Null(person.Spouse); + } + + [Theory] + [InlineData("http://tempuri.org/People?$expand=Spouse&$filter=Spouse eq null&$select=Id")] + [InlineData("http://tempuri.org/People?$filter=Spouse eq null&$expand=Spouse&$select=Id")] + public void TestProjectionWithNullNestedResourceForRawRequestUri(string requestUri) + { + // Arrange + InterceptRequestAndMockResponse("{\"@odata.context\":\"http://tempuri.org/$metadata#People(Id,Spouse())\",\"value\":[{\"Id\":2,\"Spouse\":null}]}"); + var query = ctx.Execute(new Uri(requestUri)); + + // Act + var result = query.ToList(); + + // Assert + var person = Assert.Single(result); + Assert.Equal(2, person.Id); + Assert.Null(person.Name); + Assert.Null(person.Spouse); + } + + #region Helper Methods + + protected void InterceptRequestAndMockResponse(string mockResponse) + { + this.ctx.Configurations.RequestPipeline.OnMessageCreating = (args) => + { + var contentTypeHeader = "application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8"; + var odataVersionHeader = "4.0"; + + return new TestHttpWebRequestMessage(args, + new Dictionary + { + {"Content-Type", contentTypeHeader}, + {"OData-Version", odataVersionHeader}, + }, + () => new MemoryStream(Encoding.UTF8.GetBytes(mockResponse))); + }; + } + + #endregion + + #region Types + + [Key("Id")] + public class Person + { + public int Id { get; set; } + + public string Name { get; set; } + + public Person Spouse { get; set; } + } + + public class Container : DataServiceContext + { + public Container(Uri serviceRoot) : + this(serviceRoot, ODataProtocolVersion.V4) + { + } + + public Container(Uri serviceRoot, ODataProtocolVersion protocolVersion) : + base(serviceRoot, protocolVersion) + { + this.ResolveName = ResolveName = (type) => $"NS.{type.Name}"; + this.ResolveType = ResolveType = (typeName) => + { + string namespaceName = typeof(Person).Namespace; + string unqualifiedTypeName = typeName.Substring(typeName.IndexOf('.') + 1); + + Type type = null; + + try + { + type = typeof(Person).GetAssembly().GetType($"{namespaceName}.{unqualifiedTypeName}"); + } + catch + { + } + + return type; + }; + + this.Format.UseJson(BuildEdmModel()); + } + + public virtual DataServiceQuery People + { + get + { + if ((this._People == null)) + { + this._People = base.CreateQuery("People"); + } + + return this._People; + } + } + + private DataServiceQuery _People; + + private static EdmModel BuildEdmModel() + { + var model = new EdmModel(); + + var personEntity = new EdmEntityType("NS", "Person"); + personEntity.AddKeys(personEntity.AddStructuralProperty("Id", EdmCoreModel.Instance.GetInt32(false))); + personEntity.AddStructuralProperty("Name", EdmCoreModel.Instance.GetString(false)); + + personEntity.AddUnidirectionalNavigation( + new EdmNavigationPropertyInfo { Name = "Spouse", Target = personEntity, TargetMultiplicity = EdmMultiplicity.One }); + + var entityContainer = new EdmEntityContainer("NS", "Container"); + + model.AddElement(personEntity); + model.AddElement(entityContainer); + + entityContainer.AddEntitySet("People", personEntity); + + return model; + } + } + + #endregion + } +}