Skip to content

Commit

Permalink
Fix argument null exception during projection (#3038)
Browse files Browse the repository at this point in the history
Co-authored-by: John Gathogo <[email protected]>
  • Loading branch information
uffelauesen and gathogojr authored Aug 19, 2024
1 parent 393c548 commit 648c169
Show file tree
Hide file tree
Showing 3 changed files with 228 additions and 8 deletions.
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
}
}

0 comments on commit 648c169

Please sign in to comment.