Skip to content
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

Fix argument null exception during projection #3038

Merged
merged 4 commits into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/Microsoft.OData.Client/ALinq/DataServiceQueryProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -250,8 +250,8 @@ private TElement ParseAggregateSingletonResult<TElement>(QueryResult queryResult
{
case ODataReaderState.ResourceEnd:
entry = reader.Item as ODataResource;
IEnumerable<ODataProperty> properties = entry.Properties.OfType<ODataProperty>();
if (entry != null && properties.Any())
IEnumerable<ODataProperty> properties = entry.Properties?.OfType<ODataProperty>();
if (entry != null && properties?.Any() == true)
{
ODataProperty aggregationProperty = properties.First();
ODataUntypedValue untypedValue = aggregationProperty.Value as ODataUntypedValue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -618,7 +618,7 @@ internal object ProjectionValueForPath(MaterializerEntry entry, Type expectedTyp
ODataNestedResourceInfo link = null;
ODataProperty odataProperty = null;
ICollection<ODataNestedResourceInfo> links = entry.NestedResourceInfos;
IEnumerable<ODataProperty> properties = entry.Entry.Properties.OfType<ODataProperty>();
IEnumerable<ODataProperty> properties = entry.Entry.Properties?.OfType<ODataProperty>();
ClientEdmModel edmModel = this.MaterializerContext.Model;
for (int i = 0; i < path.Count; i++)
{
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -753,7 +753,7 @@ internal object ProjectionValueForPath(MaterializerEntry entry, Type expectedTyp
this.InstanceAnnotationMaterializationPolicy.SetInstanceAnnotations(propertyName, linkEntry.Entry, expectedType, entry.ResolvedObject);
}

properties = linkEntry.Properties.OfType<ODataProperty>();
properties = linkEntry.Properties?.OfType<ODataProperty>();
links = linkEntry.NestedResourceInfos;
result = linkEntry.ResolvedObject;
entry = linkEntry;
Expand Down Expand Up @@ -840,7 +840,7 @@ internal object ProjectionDynamicValueForPath(MaterializerEntry entry, Type expe

object result = null;
ODataProperty odataProperty = null;
IEnumerable<ODataProperty> properties = entry.Entry.Properties.OfType<ODataProperty>();
IEnumerable<ODataProperty> properties = entry.Entry.Properties?.OfType<ODataProperty>();

for (int i = 0; i < path.Count; i++)
{
Expand All @@ -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)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
//---------------------------------------------------------------------
// <copyright file="ProjectionTests.cs" company="Microsoft">
// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
// </copyright>
//---------------------------------------------------------------------

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
{
/// <summary>
/// Projection tests
/// </summary>
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<Person>("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<Person>(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<string, string>
{
{"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<Person> People
{
get
{
if ((this._People == null))
{
this._People = base.CreateQuery<Person>("People");
}

return this._People;
}
}

private DataServiceQuery<Person> _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
}
}