diff --git a/RESTier.sln b/RESTier.sln index 16290cd1..b244d77d 100644 --- a/RESTier.sln +++ b/RESTier.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.25123.0 +VisualStudioVersion = 14.0.25420.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{D5E947EB-03CB-4D04-8937-FF2131BB1F04}" EndProject diff --git a/src/GlobalSuppressions.cs b/src/GlobalSuppressions.cs index b790cae3..394d0939 100644 --- a/src/GlobalSuppressions.cs +++ b/src/GlobalSuppressions.cs @@ -136,6 +136,9 @@ [assembly: SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", Target = "System.Linq.Expressions.ExpressionHelpers.#GetEnumerableItemType(System.Type)")] [assembly: SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", Target = "System.Linq.Expressions.ExpressionHelpers.#GetSelectExpandElementType(System.Type)")] [assembly: SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", Target = "System.Linq.Expressions.ExpressionHelpers.#OfType(System.Linq.IQueryable,System.Type)")] +[assembly: SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", Target = "System.Linq.Expressions.ExpressionHelpers.#RemoveUnneededStatement(System.Linq.Expressions.MethodCallExpression)")] +[assembly: SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", Target = "System.Linq.Expressions.ExpressionHelpers.#RemoveSelectExpandStatement(System.Linq.Expressions.MethodCallExpression)")] +[assembly: SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", Target = "System.Linq.Expressions.ExpressionHelpers.#RemoveAppendWhereStatement(System.Linq.Expressions.Expression)")] [assembly: SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", Target = "System.Linq.Expressions.ExpressionHelpers.#Select(System.Linq.IQueryable,System.Linq.Expressions.LambdaExpression)")] [assembly: SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", Target = "System.Linq.Expressions.ExpressionHelpers.#SelectMany(System.Linq.IQueryable,System.Linq.Expressions.LambdaExpression,System.Type)")] [assembly: SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", Target = "System.Linq.Expressions.ExpressionHelpers.#StripQueryMethod(System.Linq.Expressions.Expression,System.String)")] diff --git a/src/Microsoft.Restier.Core/Properties/Resources.Designer.cs b/src/Microsoft.Restier.Core/Properties/Resources.Designer.cs index 8343a988..5684be17 100644 --- a/src/Microsoft.Restier.Core/Properties/Resources.Designer.cs +++ b/src/Microsoft.Restier.Core/Properties/Resources.Designer.cs @@ -312,6 +312,15 @@ internal static string QuerySourcerMissing { } } + /// + /// Looks up a localized string similar to The request resource is not found.. + /// + internal static string ResourceNotFound { + get { + return ResourceManager.GetString("ResourceNotFound", resourceCulture); + } + } + /// /// Looks up a localized string similar to Should specify an interface type T for the handler.. /// diff --git a/src/Microsoft.Restier.Core/Properties/Resources.resx b/src/Microsoft.Restier.Core/Properties/Resources.resx index b3d934b1..3b3b7c31 100644 --- a/src/Microsoft.Restier.Core/Properties/Resources.resx +++ b/src/Microsoft.Restier.Core/Properties/Resources.resx @@ -213,4 +213,7 @@ {0} is not a supported EDM type. + + The request resource is not found. + \ No newline at end of file diff --git a/src/Microsoft.Restier.Core/Query/DefaultQueryHandler.cs b/src/Microsoft.Restier.Core/Query/DefaultQueryHandler.cs index af917f40..e415dfa8 100644 --- a/src/Microsoft.Restier.Core/Query/DefaultQueryHandler.cs +++ b/src/Microsoft.Restier.Core/Query/DefaultQueryHandler.cs @@ -2,12 +2,14 @@ // Licensed under the MIT License. See License.txt in the project root for license information. using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; using Microsoft.OData.Edm; +using Microsoft.Restier.Core.Exceptions; using Microsoft.Restier.Core.Properties; namespace Microsoft.Restier.Core.Query @@ -17,6 +19,10 @@ namespace Microsoft.Restier.Core.Query /// internal static class DefaultQueryHandler { + private const string ExpressionMethodNameOfWhere = "Where"; + private const string ExpressionMethodNameOfSelect = "Select"; + private const string ExpressionMethodNameOfSelectMany = "SelectMany"; + /// /// Asynchronously executes the query flow. /// @@ -76,6 +82,9 @@ public static async Task QueryAsync( }; var task = method.Invoke(executor, parameters) as Task; result = await task; + + await CheckSubExpressionResult( + context, cancellationToken, result.Results, visitor, executor, expression); } else { @@ -98,6 +107,123 @@ public static async Task QueryAsync( return result; } + private static async Task CheckSubExpressionResult( + QueryContext context, + CancellationToken cancellationToken, + IEnumerable enumerableResult, + QueryExpressionVisitor visitor, + IQueryExecutor executor, + Expression expression) + { + if (enumerableResult.GetEnumerator().MoveNext()) + { + // If there is some result, will not have additional processing + return; + } + + var methodCallExpression = expression as MethodCallExpression; + + // This will remove unneeded statement which includes $expand, $select,$top,$skip,$orderby + methodCallExpression = methodCallExpression.RemoveUnneededStatement(); + if (methodCallExpression == null || methodCallExpression.Arguments.Count != 2) + { + return; + } + + if (methodCallExpression.Method.Name == ExpressionMethodNameOfWhere) + { + // Throw exception if key as last where statement, or remove $filter where statement + methodCallExpression = CheckWhereCondition(methodCallExpression); + if (methodCallExpression == null || methodCallExpression.Arguments.Count != 2) + { + return; + } + + // Call without $filter where statement and with Key where statement + if (methodCallExpression.Method.Name == ExpressionMethodNameOfWhere) + { + // The last where from $filter is removed and run with key where statement + await ExecuteSubExpression(context, cancellationToken, visitor, executor, methodCallExpression); + return; + } + } + + if (methodCallExpression.Method.Name != ExpressionMethodNameOfSelect + && methodCallExpression.Method.Name != ExpressionMethodNameOfSelectMany) + { + // If last statement is not select property, will no further checking + return; + } + + var subExpression = methodCallExpression.Arguments[0]; + + // Remove appended statement like Where(Param_0 => (Param_0.Prop != null)) if there is one + subExpression = subExpression.RemoveAppendWhereStatement(); + + await ExecuteSubExpression(context, cancellationToken, visitor, executor, subExpression); + } + + private static async Task ExecuteSubExpression( + QueryContext context, + CancellationToken cancellationToken, + QueryExpressionVisitor visitor, + IQueryExecutor executor, + Expression expression) + { + // get element type + Type elementType = null; + var queryType = expression.Type.FindGenericType(typeof(IQueryable<>)); + if (queryType != null) + { + elementType = queryType.GetGenericArguments()[0]; + } + + var query = visitor.BaseQuery.Provider.CreateQuery(expression); + var method = typeof(IQueryExecutor) + .GetMethod("ExecuteQueryAsync") + .MakeGenericMethod(elementType); + var parameters = new object[] + { + context, query, cancellationToken + }; + var task = method.Invoke(executor, parameters) as Task; + var result = await task; + + var any = result.Results.Cast().Any(); + if (!any) + { + // Which means previous expression does not have result, and should throw ResourceNotFoundException. + throw new ResourceNotFoundException(Resources.ResourceNotFound); + } + } + + private static MethodCallExpression CheckWhereCondition(MethodCallExpression methodCallExpression) + { + // This means a select for expand is appended, will remove it for resource existing check + var lastWhere = methodCallExpression.Arguments[1] as UnaryExpression; + var lambdaExpression = lastWhere.Operand as LambdaExpression; + if (lambdaExpression == null) + { + return null; + } + + var binaryExpression = lambdaExpression.Body as BinaryExpression; + if (binaryExpression == null) + { + return null; + } + + // Key segment will have ConstantExpression but $filter will not have ConstantExpression + var rightExpression = binaryExpression.Right as ConstantExpression; + if (rightExpression != null) + { + // This means where statement is key segment but not for $filter + throw new ResourceNotFoundException(Resources.ResourceNotFound); + } + + return methodCallExpression.Arguments[0] as MethodCallExpression; + } + private class QueryExpressionVisitor : ExpressionVisitor { private readonly QueryExpressionContext context; diff --git a/src/Microsoft.Restier.Publishers.OData/RestierController.cs b/src/Microsoft.Restier.Publishers.OData/RestierController.cs index b61b861f..777d10e5 100644 --- a/src/Microsoft.Restier.Publishers.OData/RestierController.cs +++ b/src/Microsoft.Restier.Publishers.OData/RestierController.cs @@ -433,8 +433,6 @@ private HttpResponseMessage CreateQueryResponse( IQueryable query, IEdmType edmType, bool isIfNoneMatch, ETag etag) { IEdmTypeReference typeReference = GetTypeReference(edmType); - - // TODO, GitHubIssue#328 : 404 should be returned when requesting property of non-exist entity BaseSingleResult singleResult = null; HttpResponseMessage response = null; @@ -505,13 +503,7 @@ private HttpResponseMessage CreateQueryResponse( var entityResult = query.SingleOrDefault(); if (entityResult == null) { - // TODO GitHubIssue#288: 204 expected when requesting single nav property which has null value - // ~/People(nonexistkey) and ~/People(nonexistkey)/BestFriend, expected 404 - // ~/People(key)/BestFriend, and BestFriend is null, expected 204 - throw new HttpResponseException( - this.Request.CreateErrorResponse( - HttpStatusCode.NotFound, - Resources.ResourceNotFound)); + return this.Request.CreateResponse(HttpStatusCode.NoContent); } // Check the ETag here diff --git a/src/Microsoft.Restier/Microsoft.Restier.nuspec b/src/Microsoft.Restier/Microsoft.Restier.nuspec index 9f9e4445..de349e31 100644 --- a/src/Microsoft.Restier/Microsoft.Restier.nuspec +++ b/src/Microsoft.Restier/Microsoft.Restier.nuspec @@ -2,7 +2,7 @@ Microsoft.Restier - 0.6.0-beta + 0.6.0 RESTier Microsoft Microsoft diff --git a/src/Shared/ExpressionHelpers.cs b/src/Shared/ExpressionHelpers.cs index 04b7ba35..76e3435c 100644 --- a/src/Shared/ExpressionHelpers.cs +++ b/src/Shared/ExpressionHelpers.cs @@ -12,6 +12,9 @@ internal static class ExpressionHelpers private const string MethodNameOfQueryTake = "Take"; private const string MethodNameOfQuerySelect = "Select"; private const string MethodNameOfQuerySkip = "Skip"; + private const string MethodNameOfQueryWhere = "Where"; + private const string MethodNameOfQueryOrderBy = "OrderBy"; + private const string InterfaceNameISelectExpandWrapper = "ISelectExpandWrapper"; private const string ExpandClauseReflectedTypeName = "SelectExpandBinder"; public static IQueryable Select(IQueryable query, LambdaExpression select) @@ -116,6 +119,112 @@ internal static Type GetEnumerableItemType(this Type enumerableType) return enumerableType; } + internal static MethodCallExpression RemoveUnneededStatement(this MethodCallExpression methodCallExpression) + { + if (methodCallExpression == null || methodCallExpression.Arguments.Count != 2) + { + return methodCallExpression; + } + + if (methodCallExpression.Method.Name == MethodNameOfQuerySelect) + { + // Check where it is expand case or select, if yes, need to get rid of last select + methodCallExpression = RemoveSelectExpandStatement(methodCallExpression); + if (methodCallExpression == null || methodCallExpression.Arguments.Count != 2) + { + return methodCallExpression; + } + } + + if (methodCallExpression.Method.Name == MethodNameOfQueryTake) + { + // Check where it is top query option, and if yes, remove it. + methodCallExpression = methodCallExpression.Arguments[0] as MethodCallExpression; + if (methodCallExpression == null || methodCallExpression.Arguments.Count != 2) + { + return methodCallExpression; + } + } + + if (methodCallExpression.Method.Name == MethodNameOfQuerySkip) + { + // Check where it is skip query option, and if yes, remove it. + methodCallExpression = methodCallExpression.Arguments[0] as MethodCallExpression; + if (methodCallExpression == null || methodCallExpression.Arguments.Count != 2) + { + return methodCallExpression; + } + } + + if (methodCallExpression.Method.Name == MethodNameOfQueryOrderBy) + { + // Check where it is orderby query option, and if yes, remove it. + methodCallExpression = methodCallExpression.Arguments[0] as MethodCallExpression; + if (methodCallExpression == null || methodCallExpression.Arguments.Count != 2) + { + return methodCallExpression; + } + } + + return methodCallExpression; + } + + internal static MethodCallExpression RemoveSelectExpandStatement(this MethodCallExpression methodCallExpression) + { + // This means a select for expand is appended, will remove it for resource existing check + var expandSelect = methodCallExpression.Arguments[1] as UnaryExpression; + var lambdaExpression = expandSelect.Operand as LambdaExpression; + if (lambdaExpression == null) + { + return methodCallExpression; + } + + var memberInitExpression = lambdaExpression.Body as MemberInitExpression; + if (memberInitExpression == null) + { + return methodCallExpression; + } + + Type returnType = lambdaExpression.ReturnType; + var wrapperInterface = returnType.GetInterface(InterfaceNameISelectExpandWrapper); + if (wrapperInterface != null) + { + methodCallExpression = methodCallExpression.Arguments[0] as MethodCallExpression; + } + + return methodCallExpression; + } + + internal static Expression RemoveAppendWhereStatement(this Expression expression) + { + var methodCallExpression = expression as MethodCallExpression; + if (methodCallExpression == null || methodCallExpression.Method.Name != MethodNameOfQueryWhere) + { + return expression; + } + + // This means there may be an appended statement Where(Param_0 => (Param_0.Prop != null)) + var appendedWhere = methodCallExpression.Arguments[1] as UnaryExpression; + var lambdaExpression = appendedWhere.Operand as LambdaExpression; + if (lambdaExpression == null) + { + return expression; + } + + var binaryExpression = lambdaExpression.Body as BinaryExpression; + if (binaryExpression != null && binaryExpression.NodeType == ExpressionType.NotEqual) + { + var rightExpression = binaryExpression.Right as ConstantExpression; + if (rightExpression != null && rightExpression.Value == null) + { + // remove statement like Where(Param_0 => (Param_0.Prop != null)) + expression = methodCallExpression.Arguments[0]; + } + } + + return expression; + } + private static Expression StripQueryMethod(Expression expression, string methodName) { var methodCall = expression as MethodCallExpression; diff --git a/test/ODataEndToEnd/Microsoft.OData.Service.Sample.Tests/PropertyAccessTests.cs b/test/ODataEndToEnd/Microsoft.OData.Service.Sample.Tests/PropertyAccessTests.cs index 6035de27..d82097af 100644 --- a/test/ODataEndToEnd/Microsoft.OData.Service.Sample.Tests/PropertyAccessTests.cs +++ b/test/ODataEndToEnd/Microsoft.OData.Service.Sample.Tests/PropertyAccessTests.cs @@ -250,22 +250,48 @@ public void QueryNullRawEnumPropertyOfSingleEntity() /// Note: 1. No test case of collection with primitive/enum/complex as EF does not support /// 2. Complex can not be null in EF [Theory] + // Filter with no result, them empty collection returned. + [InlineData("/People?$filter=LastName eq 'xxx'", 200)] + // Filter cause no result returned. + [InlineData("/People(1)?$filter=LastName eq 'xxx'", 204)] // Single primitive property with null value [InlineData("/People(4)/LastName", 204)] // Single primitive property $value with null value [InlineData("/People(4)/LastName/$value", 204)] // single navigation property with null value - // TODO Should be 204, cannot differentiate ~/People(nonexistkey) vs /People(5)/NullSingNav now - [InlineData("/People(4)/BestFriend", 404)] + [InlineData("/People(4)/BestFriend", 204)] + // single navigation property with null value + [InlineData("/People(4)/BestFriend?$expand=Friends", 204)] // single navigation property's propery and navigation property has null value - // TODO should be 404 - [InlineData("/People(4)/BestFriend/LastName", 204)] + [InlineData("/People(4)/BestFriend/LastName", 404)] // single navigation property's property with null value [InlineData("/People(5)/BestFriend/LastName", 204)] // collection of navigation property with empty collection value [InlineData("/People(5)/Friends", 200)] + // Filter on empty collection + [InlineData("/People(5)/Friends?$filter=LastName eq 'xxx'", 200)] + // collection of navigation property with empty collection value + [InlineData("/People(5)/Friends?$expand=Friends", 200)] + // collection of navigation property with empty collection value + [InlineData("/People(5)/Friends?$expand=Friends($select=FirstName,LastName)", 200)] // collection of navigation property with null collection value [InlineData("/People(7)/Friends", 200)] + // Non exist entity + [InlineData("/People(77)", 404)] + // Non exist entity + [InlineData("/People(77)?$expand=Friends", 404)] + // Non exist entity + [InlineData("/People(77)?$filter=LastName eq 'xxx'", 404)] + // Non exist entity + [InlineData("/People(77)?$expand=Friends&&$filter=LastName eq 'xxx'", 404)] + // Non exist entity + [InlineData("/People(77)/Friends", 404)] + // Non exist entity + [InlineData("/People(77)/Friends?$filter=LastName eq 'xxx'", 404)] + // Non exist entity + [InlineData("/People(77)/Friends?$skip=1&$top=1&$filter=LastName eq 'xxx'&$select=FirstName,LastName", 404)] + // Non exist entity + [InlineData("/People(77)/Friends?$skip=1&$top=1&$filter=LastName eq 'xxx'&$expand=Friends&$select=FirstName,LastName", 404)] public void QueryPropertyWithNullValueStatusCode(string url, int expectedCode) { TestGetStatusCodeIs(url, expectedCode); diff --git a/test/ODataEndToEnd/Microsoft.OData.Service.Sample.Tests/TrippinE2EEtagTestCases.cs b/test/ODataEndToEnd/Microsoft.OData.Service.Sample.Tests/TrippinE2EEtagTestCases.cs index 321a3fc3..6e5852b6 100644 --- a/test/ODataEndToEnd/Microsoft.OData.Service.Sample.Tests/TrippinE2EEtagTestCases.cs +++ b/test/ODataEndToEnd/Microsoft.OData.Service.Sample.Tests/TrippinE2EEtagTestCases.cs @@ -72,7 +72,7 @@ public void IfMatchCRUDTest() { flight = this.TestClientContext.Flights.ByKey(new Dictionary() { { "flightId", flightId } }).GetValue(); } - catch (DataServiceQueryException e) + catch (DataServiceQueryException) { } Assert.Equal(412, statusCode); @@ -101,7 +101,7 @@ public void IfMatchCRUDTest() { this.TestClientContext.SaveChanges(); } - catch (DataServiceRequestException e) + catch (DataServiceRequestException) { } this.TestClientContext.Detach(flight); @@ -125,7 +125,7 @@ public void IfMatchCRUDTest() { this.TestClientContext.SaveChanges(); } - catch (DataServiceRequestException e) + catch (DataServiceRequestException) { } this.TestClientContext.Detach(flight); @@ -171,7 +171,7 @@ public void IfMatchCRUDTest() { this.TestClientContext.SaveChanges(); } - catch (DataServiceRequestException e) + catch (DataServiceRequestException) { } Assert.Equal(412, statusCode); @@ -197,7 +197,7 @@ public void IfMatchCRUDTest() { flight = this.TestClientContext.Flights.ByKey(new Dictionary() { { "flightId", flightId } }).GetValue(); } - catch (DataServiceQueryException e) + catch (DataServiceQueryException) { } Assert.Equal(404, statusCode); @@ -255,7 +255,7 @@ public void IfNoneMatchCRUDTest() { flight = this.TestClientContext.Flights.ByKey(new Dictionary() {{"flightId", flightId}}).GetValue(); } - catch (DataServiceQueryException e) + catch (DataServiceQueryException) { } Assert.Equal(304, statusCode); @@ -285,7 +285,7 @@ public void IfNoneMatchCRUDTest() { this.TestClientContext.SaveChanges(); } - catch (DataServiceRequestException e) + catch (DataServiceRequestException) { } this.TestClientContext.Detach(flight); @@ -313,7 +313,7 @@ public void IfNoneMatchCRUDTest() { this.TestClientContext.SaveChanges(); } - catch (DataServiceRequestException e) + catch (DataServiceRequestException) { } this.TestClientContext.Detach(flight); @@ -361,7 +361,7 @@ public void IfNoneMatchCRUDTest() { this.TestClientContext.SaveChanges(); } - catch (DataServiceRequestException e) + catch (DataServiceRequestException) { } Assert.Equal(412, statusCode); @@ -381,13 +381,13 @@ public void IfNoneMatchCRUDTest() this.TestClientContext.SaveChanges(); Assert.Equal(204, statusCode); - // Query the flight again and entiy does not exist. + // Query the flight again and entity does not exist. this.TestClientContext.SendingRequest2 -= sendRequestEvent; try { flight = this.TestClientContext.Flights.ByKey(new Dictionary() { { "flightId", flightId } }).GetValue(); } - catch (DataServiceQueryException e) + catch (DataServiceQueryException) { } Assert.Equal(404, statusCode); diff --git a/test/ODataEndToEnd/Microsoft.OData.Service.Sample.Tests/TrippinE2EQueryTestCases.cs b/test/ODataEndToEnd/Microsoft.OData.Service.Sample.Tests/TrippinE2EQueryTestCases.cs index 532e7513..92646240 100644 --- a/test/ODataEndToEnd/Microsoft.OData.Service.Sample.Tests/TrippinE2EQueryTestCases.cs +++ b/test/ODataEndToEnd/Microsoft.OData.Service.Sample.Tests/TrippinE2EQueryTestCases.cs @@ -23,7 +23,7 @@ public void DerivedTypeQuery(string uriStringAfterServiceRoot, string expectedSu } [Theory] - [InlineData("People(1)/Microsoft.OData.Service.Sample.Trippin.Models.Employee", 404)] + [InlineData("People(1)/Microsoft.OData.Service.Sample.Trippin.Models.Employee", 204)] [InlineData("People/Microsoft.OData.Service.Sample.Trippin.Models.Employee/UserName", 500)] [InlineData("People/Microsoft.OData.Service.Sample.Trippin.Models.Employee/Cost", 500)] public void DerivedTypeQuery(string url, int expectedCode) diff --git a/test/ODataEndToEnd/Microsoft.OData.Service.Sample.Tests/TrippinInMemoryE2ETest.cs b/test/ODataEndToEnd/Microsoft.OData.Service.Sample.Tests/TrippinInMemoryE2ETest.cs index 4da1e1e1..49e03f11 100644 --- a/test/ODataEndToEnd/Microsoft.OData.Service.Sample.Tests/TrippinInMemoryE2ETest.cs +++ b/test/ODataEndToEnd/Microsoft.OData.Service.Sample.Tests/TrippinInMemoryE2ETest.cs @@ -101,8 +101,7 @@ public void TestAutoExpandedNavigationProperty() // single complex property with null value [InlineData("/People(5)/HomeAddress", 204)] // single complex property's propery and complex property has null value - // TODO should be 404 - [InlineData("/People(5)/HomeAddress/Address", 204)] + [InlineData("/People(5)/HomeAddress/Address", 404)] // single complex property's property with null value [InlineData("/People(6)/HomeAddress/Address", 204)] // collection of complex property with empty collection value @@ -113,11 +112,9 @@ public void TestAutoExpandedNavigationProperty() // Collection of complex property with null collection [InlineData("/People(7)/Locations", 200)] // single navigation property with null value - // TODO Should be 204, cannot differentiate ~/People(nonexistkey) vs /People(5)/NullSingNav now - [InlineData("/People(5)/BestFriend", 404)] + [InlineData("/People(5)/BestFriend", 204)] // single navigation property's propery and navigation property has null value - // TODO should be 404 - [InlineData("/People(5)/BestFriend/MiddleName", 204)] + [InlineData("/People(5)/BestFriend/MiddleName", 404)] // single navigation property's property with null value [InlineData("/People(6)/BestFriend/MiddleName", 204)] // collection of navigation property with empty collection value @@ -134,37 +131,29 @@ public void QueryPropertyWithNullValueStatusCode(string url, int expectedCode) [Theory] // Single primitive property - // TODO should be 404 - [InlineData("/People(15)/MiddleName", 204)] + [InlineData("/People(15)/MiddleName", 404)] // Single primitive property $value - // TODO should be 404 - [InlineData("/People(15)/MiddleName/$value", 204)] + [InlineData("/People(15)/MiddleName/$value", 404)] // Collection of primitive property - // TODO should be 404 - [InlineData("/People(15)/Emails", 200)] + [InlineData("/People(15)/Emails", 404)] // Collection of primitive property $value // TODO should be bad request 400 as this is not allowed, 404 is returned by WebApi Route Match method [InlineData("/People(15)/Emails/$value", 404)] // single complex property - // TODO should be 404 - [InlineData("/People(15)/HomeAddress", 204)] + [InlineData("/People(15)/HomeAddress", 404)] // single complex property's property - // TODO should be 404 - [InlineData("/People(15)/HomeAddress/Address", 204)] + [InlineData("/People(15)/HomeAddress/Address", 404)] // collection of complex property - // TODO should be 404 - [InlineData("/People(15)/Locations", 200)] + [InlineData("/People(15)/Locations", 404)] // collection of complex property's propery // TODO should be bad request 400 as this is not allowed?? 404 is returned by WebApi Route Match method [InlineData("/People(15)/Locations/Address", 404)] // single navigation property [InlineData("/People(15)/BestFriend", 404)] // single navigation property's propery - // TODO should be 404 - [InlineData("/People(15)/BestFriend/MiddleName", 204)] + [InlineData("/People(15)/BestFriend/MiddleName", 404)] // collection of navigation property - // TODO should be 404 - [InlineData("/People(15)/Friends", 200)] + [InlineData("/People(15)/Friends", 404)] // collection of navigation property's property // TODO should be bad request 400 as this is not allowed, 404 is returned by WebApi Route Match method [InlineData("/People(15)/Friends/MiddleName", 404)] diff --git a/test/ODataEndToEnd/Microsoft.OData.Service.Sample.Tests/UrlConventionsTests.cs b/test/ODataEndToEnd/Microsoft.OData.Service.Sample.Tests/UrlConventionsTests.cs index 56ba294e..f3114c9c 100644 --- a/test/ODataEndToEnd/Microsoft.OData.Service.Sample.Tests/UrlConventionsTests.cs +++ b/test/ODataEndToEnd/Microsoft.OData.Service.Sample.Tests/UrlConventionsTests.cs @@ -175,10 +175,7 @@ public void FunctionImportSingleEntity() [Fact] public void FunctionImportNullSingleEntity() { - // TODO GitHubIssue#288: 204 expected when requesting single nav property which has null value - // ~/People(nonexistkey) and ~/People(nonexistkey)/BestFriend, expected 404 - // ~/People(key)/BestFriend, and BestFriend is null, expected 204 - TestGetStatusCodeIs("GetNullEntity", 404); + TestGetStatusCodeIs("GetNullEntity", 204); } [Fact] diff --git a/test/ODataEndToEnd/Microsoft.OData.Service.Sample.Trippin/Api/TrippinApi.cs b/test/ODataEndToEnd/Microsoft.OData.Service.Sample.Trippin/Api/TrippinApi.cs index febca760..320d5b58 100644 --- a/test/ODataEndToEnd/Microsoft.OData.Service.Sample.Trippin/Api/TrippinApi.cs +++ b/test/ODataEndToEnd/Microsoft.OData.Service.Sample.Trippin/Api/TrippinApi.cs @@ -586,11 +586,11 @@ protected override IServiceCollection ConfigureApi(IServiceCollection services) private class TrippinModelExtender : IModelBuilder { - public async Task GetModelAsync(ModelContext context, CancellationToken cancellationToken) + public Task GetModelAsync(ModelContext context, CancellationToken cancellationToken) { var builder = new ODataConventionModelBuilder(); builder.EntityType(); - return builder.GetEdmModel(); + return Task.FromResult(builder.GetEdmModel()); } }