diff --git a/src/Microsoft.AspNetCore.OData/ODataController.cs b/src/Microsoft.AspNetCore.OData/ODataController.cs index 7147fda320..c59374c8c6 100644 --- a/src/Microsoft.AspNetCore.OData/ODataController.cs +++ b/src/Microsoft.AspNetCore.OData/ODataController.cs @@ -4,6 +4,7 @@ using Microsoft.AspNet.OData.Common; using Microsoft.AspNet.OData.Results; using Microsoft.AspNetCore.Mvc; +using Microsoft.OData; namespace Microsoft.AspNet.OData { @@ -51,5 +52,131 @@ protected virtual UpdatedODataResult Updated(TEntity entity) return new UpdatedODataResult(entity); } + + /// + /// Creates a that when executed will produce a Bad Request (400) response. + /// + /// Error Message + /// A with the specified values. + protected virtual BadRequestODataResult BadRequest(string message) + { + return new BadRequestODataResult(message); + } + + /// + /// Creates a that when executed will produce a Bad Request (400) response. + /// + /// Parameter of type . + /// A with the specified values. + protected virtual BadRequestODataResult BadRequest(ODataError odataError) + { + return new BadRequestODataResult(odataError); + } + + /// + /// Creates a that when executed will produce a Not Found (404) response. + /// + /// Error Message + /// A with the specified values. + protected virtual NotFoundODataResult NotFound(string message) + { + return new NotFoundODataResult(message); + } + + /// + /// Creates a that when executed will produce a Not Found (404) response. + /// + /// Parameter of type . + /// A with the specified values. + protected virtual NotFoundODataResult NotFound(ODataError odataError) + { + return new NotFoundODataResult(odataError); + } + + /// + /// Creates a that when executed will produce a Unauthorized (401) response. + /// + /// Error Message + /// An with the specified values. + protected virtual UnauthorizedODataResult Unauthorized(string message) + { + return new UnauthorizedODataResult(message); + } + + /// + /// Creates a that when executed will produce a Unauthorized (401) response. + /// + /// Parameter of type . + /// An with the specified values. + protected virtual UnauthorizedODataResult Unauthorized(ODataError odataError) + { + return new UnauthorizedODataResult(odataError); + } + + // ConflictResult and UnprocessableEntityResult were introduced in AspNet core 2.1, which is implemented from .Net standard 2.1 + // https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.conflict?view=aspnetcore-2.1 + // https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.unprocessableentityresult?view=aspnetcore-2.1 +#if !NETSTANDARD2_0 + /// + /// Creates a that when executed will produce a Conflict (409) response. + /// + /// Error Message + /// A with the specified values. + protected virtual ConflictODataResult Conflict(string message) + { + return new ConflictODataResult(message); + } + + /// + /// Creates a that when executed will produce a Conflict (409) response. + /// + /// Parameter of type . + /// A with the specified values. + protected virtual ConflictODataResult Conflict(ODataError odataError) + { + return new ConflictODataResult(odataError); + } + + /// + /// Creates a that when executed will produce an UnprocessableEntity (422) response. + /// + /// Error Message + /// An with the specified values. + protected virtual UnprocessableEntityODataResult UnprocessableEntity(string message) + { + return new UnprocessableEntityODataResult(message); + } + + /// + /// Creates a that when executed will produce an UnprocessableEntity (422) response. + /// + /// Parameter of type . + /// An with the specified values. + protected virtual UnprocessableEntityODataResult UnprocessableEntity(ODataError odataError) + { + return new UnprocessableEntityODataResult(odataError); + } +#endif + + /// + /// Creates a that when executed will produce an response. + /// + /// Http Error code. + /// Http Error Message. + /// An with the specified values. + protected virtual ODataErrorResult ODataErrorResult(string errorCode, string message) + { + return new ODataErrorResult(errorCode, message); + } + + /// + /// Creates a that when executed will produce an response. + /// + /// . + /// An with the specified values. + protected virtual ODataErrorResult ODataErrorResult(ODataError odataError) + { + return new ODataErrorResult(odataError); + } } } diff --git a/src/Microsoft.AspNetCore.OData/Results/BadRequestODataResult.cs b/src/Microsoft.AspNetCore.OData/Results/BadRequestODataResult.cs new file mode 100644 index 0000000000..592760c48e --- /dev/null +++ b/src/Microsoft.AspNetCore.OData/Results/BadRequestODataResult.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.OData; + +namespace Microsoft.AspNet.OData.Results +{ + /// + /// Represents a result that when executed will produce a Bad Request (400) response. + /// + /// This result creates an with status code: 400. + public class BadRequestODataResult : BadRequestResult, IODataErrorResult + { + /// + /// OData Error. + /// + public ODataError Error { get; } + + /// + /// Initializes a new instance of the class. + /// + /// Error Message + public BadRequestODataResult(string message) + { + if (message == null) + { + throw Common.Error.ArgumentNull("message"); + } + + Error = new ODataError + { + Message = message, + ErrorCode = "400" + }; + } + + /// + /// Initializes a new instance of the class. + /// + /// OData Error. + public BadRequestODataResult(ODataError odataError) + { + if (odataError == null) + { + throw Common.Error.ArgumentNull("odataError"); + } + + Error = odataError; + } + + /// + public async override Task ExecuteResultAsync(ActionContext context) + { + ObjectResult objectResult = new ObjectResult(Error) + { + StatusCode = StatusCodes.Status400BadRequest + }; + + await objectResult.ExecuteResultAsync(context).ConfigureAwait(false); + } + } +} diff --git a/src/Microsoft.AspNetCore.OData/Results/ConflictODataResult.cs b/src/Microsoft.AspNetCore.OData/Results/ConflictODataResult.cs new file mode 100644 index 0000000000..83455aa313 --- /dev/null +++ b/src/Microsoft.AspNetCore.OData/Results/ConflictODataResult.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#if !NETSTANDARD2_0 +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.OData; + +namespace Microsoft.AspNet.OData.Results +{ + /// + /// Represents a result that when executed will produce a Conflict (409) response. + /// + /// This result creates an with status code: 409. + public class ConflictODataResult : ConflictResult, IODataErrorResult + { + /// + /// OData Error. + /// + public ODataError Error { get; } + + /// + /// Initializes a new instance of the class. + /// + /// Error Message + public ConflictODataResult(string message) + { + if (message == null) + { + throw Common.Error.ArgumentNull("message"); + } + + Error = new ODataError + { + Message = message, + ErrorCode = "409" + }; + } + + /// + /// Initializes a new instance of the class. + /// + /// OData Error. + public ConflictODataResult(ODataError odataError) + { + if (odataError == null) + { + throw Common.Error.ArgumentNull("odataError"); + } + + Error = odataError; + } + + /// + public async override Task ExecuteResultAsync(ActionContext context) + { + ObjectResult objectResult = new ObjectResult(Error) + { + StatusCode = StatusCodes.Status409Conflict + }; + + await objectResult.ExecuteResultAsync(context).ConfigureAwait(false); + } + } +} +#endif diff --git a/src/Microsoft.AspNetCore.OData/Results/IODataErrorResult.cs b/src/Microsoft.AspNetCore.OData/Results/IODataErrorResult.cs new file mode 100644 index 0000000000..17c42a59b1 --- /dev/null +++ b/src/Microsoft.AspNetCore.OData/Results/IODataErrorResult.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using Microsoft.OData; + +namespace Microsoft.AspNet.OData.Results +{ + /// + /// Provide the interface for the details of a given OData Error result. + /// + public interface IODataErrorResult + { + /// + /// OData Error. + /// + ODataError Error { get; } + } +} diff --git a/src/Microsoft.AspNetCore.OData/Results/NotFoundODataResult.cs b/src/Microsoft.AspNetCore.OData/Results/NotFoundODataResult.cs new file mode 100644 index 0000000000..d3634c74d1 --- /dev/null +++ b/src/Microsoft.AspNetCore.OData/Results/NotFoundODataResult.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.OData; + +namespace Microsoft.AspNet.OData.Results +{ + /// + /// Represents a result that when executed will produce a Not Found (404) response. + /// + /// This result creates an with status code: 404. + public class NotFoundODataResult : NotFoundResult, IODataErrorResult + { + /// + /// OData Error. + /// + public ODataError Error { get; } + + /// + /// Initializes a new instance of the class. + /// + /// Error Message + public NotFoundODataResult(string message) + { + if (message == null) + { + throw Common.Error.ArgumentNull("message"); + } + + Error = new ODataError + { + Message = message, + ErrorCode = "404" + }; + } + + /// + /// Initializes a new instance of the class. + /// + /// OData Error. + public NotFoundODataResult(ODataError odataError) + { + if (odataError == null) + { + throw Common.Error.ArgumentNull("odataError"); + } + + Error = odataError; + } + + /// + public async override Task ExecuteResultAsync(ActionContext context) + { + ObjectResult objectResult = new ObjectResult(Error) + { + StatusCode = StatusCodes.Status404NotFound + }; + + await objectResult.ExecuteResultAsync(context).ConfigureAwait(false); + } + } +} diff --git a/src/Microsoft.AspNetCore.OData/Results/ODataErrorResult.cs b/src/Microsoft.AspNetCore.OData/Results/ODataErrorResult.cs new file mode 100644 index 0000000000..938866689e --- /dev/null +++ b/src/Microsoft.AspNetCore.OData/Results/ODataErrorResult.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.OData; + +namespace Microsoft.AspNet.OData.Results +{ + /// + /// Represents a result that when executed will produce an . + /// + /// This result creates an response. + public class ODataErrorResult : ActionResult, IODataErrorResult + { + /// + /// OData Error. + /// + public ODataError Error { get; } + + /// + /// Initializes a new instance of the class. + /// + public ODataErrorResult(string errorCode, string message) + { + if (errorCode == null) + { + throw Common.Error.ArgumentNull("errorCode"); + } + + if (message == null) + { + throw Common.Error.ArgumentNull("message"); + } + + Error = new ODataError + { + ErrorCode = errorCode, + Message = message + }; + } + + /// + /// Initializes a new instance of the class. + /// + public ODataErrorResult(ODataError odataError) + { + if (odataError == null) + { + throw Common.Error.ArgumentNull("odataError"); + } + + Error = odataError; + } + + /// + public async override Task ExecuteResultAsync(ActionContext context) + { + ObjectResult objectResult = new ObjectResult(Error) + { + StatusCode = Convert.ToInt32(Error.ErrorCode) + }; + + await objectResult.ExecuteResultAsync(context).ConfigureAwait(false); + } + } +} diff --git a/src/Microsoft.AspNetCore.OData/Results/UnauthorizedODataResult.cs b/src/Microsoft.AspNetCore.OData/Results/UnauthorizedODataResult.cs new file mode 100644 index 0000000000..cfa7c1706a --- /dev/null +++ b/src/Microsoft.AspNetCore.OData/Results/UnauthorizedODataResult.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.OData; + +namespace Microsoft.AspNet.OData.Results +{ + /// + /// Represents a result that when executed will produce a Unauthorized (401) response. + /// + /// This result creates an with status code: 401. + public class UnauthorizedODataResult : UnauthorizedResult, IODataErrorResult + { + /// + /// OData Error. + /// + public ODataError Error { get; } + + /// + /// Initializes a new instance of the class. + /// + /// Error Message + public UnauthorizedODataResult(string message) + { + if (message == null) + { + throw Common.Error.ArgumentNull("message"); + } + + Error = new ODataError + { + Message = message, + ErrorCode = "401" + }; + } + + /// + /// Initializes a new instance of the class. + /// + /// OData Error. + public UnauthorizedODataResult(ODataError odataError) + { + if (odataError == null) + { + throw Common.Error.ArgumentNull("odataError"); + } + + Error = odataError; + } + + /// + public async override Task ExecuteResultAsync(ActionContext context) + { + ObjectResult objectResult = new ObjectResult(Error) + { + StatusCode = StatusCodes.Status401Unauthorized + }; + + await objectResult.ExecuteResultAsync(context).ConfigureAwait(false); + } + } +} diff --git a/src/Microsoft.AspNetCore.OData/Results/UnprocessableEntityODataResult .cs b/src/Microsoft.AspNetCore.OData/Results/UnprocessableEntityODataResult .cs new file mode 100644 index 0000000000..5c3213ec55 --- /dev/null +++ b/src/Microsoft.AspNetCore.OData/Results/UnprocessableEntityODataResult .cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#if !NETSTANDARD2_0 +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.OData; + +namespace Microsoft.AspNet.OData.Results +{ + /// + /// Represents a result that when executed will produce a UnprocessableEntity (422) response. + /// + /// This result creates an with status code: 422. + public class UnprocessableEntityODataResult : UnprocessableEntityResult, IODataErrorResult + { + /// + /// OData Error. + /// + public ODataError Error { get; } + + /// + /// Initializes a new instance of the class. + /// + /// Error Message + public UnprocessableEntityODataResult(string message) + { + if (message == null) + { + throw Common.Error.ArgumentNull("message"); + } + + Error = new ODataError + { + Message = message, + ErrorCode = "422" + }; + } + + /// + /// Initializes a new instance of the class. + /// + /// OData Error. + public UnprocessableEntityODataResult(ODataError odataError) + { + if (odataError == null) + { + throw Common.Error.ArgumentNull("odataError"); + } + + Error = odataError; + } + + /// + public async override Task ExecuteResultAsync(ActionContext context) + { + ObjectResult objectResult = new ObjectResult(Error) + { + StatusCode = StatusCodes.Status422UnprocessableEntity + }; + + await objectResult.ExecuteResultAsync(context).ConfigureAwait(false); + } + } +} +#endif diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Common/Controllers/TestControllers.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Common/Controllers/TestControllers.cs index d1279bc500..875d51e068 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Common/Controllers/TestControllers.cs +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Common/Controllers/TestControllers.cs @@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Routing; +using Microsoft.OData; using Xunit; #else using System; @@ -103,6 +104,10 @@ public class TestODataController : ODataController public TestNotFoundObjectResult NotFound(object value) { return new TestNotFoundObjectResult(base.NotFound()); } #endif +#if NETCORE + public new TestNotFoundResult NotFound(ODataError error) { return new TestNotFoundResult(base.NotFound(error)); } +#endif + [NonAction] public new TestBadRequestResult BadRequest() { return new TestBadRequestResult(base.BadRequest()); } @@ -115,18 +120,58 @@ public class TestODataController : ODataController [NonAction] #if NETCORE - public TestBadRequestObjectResult BadRequest(string message) { return new TestBadRequestObjectResult(base.BadRequest(message)); } + public new TestBadRequestResult BadRequest(string message) { return new TestBadRequestResult(base.BadRequest(message)); } #else public new TestBadRequestObjectResult BadRequest(string message) { return new TestBadRequestObjectResult(base.BadRequest(message)); } #endif + [NonAction] +#if NETCORE + public new TestBadRequestResult BadRequest(ODataError error) { return new TestBadRequestResult(base.BadRequest(error)); } +#endif + #if NETCORE public new TestBadRequestObjectResult BadRequest(object obj) { return new TestBadRequestObjectResult(base.BadRequest(obj)); } #else public TestBadRequestObjectResult BadRequest(T obj) { return new TestBadRequestObjectResult(base.Content(HttpStatusCode.BadRequest, obj)); } #endif +#if NETCORE + [NonAction] + public new TestUnauthorizedResult Unauthorized() { return new TestUnauthorizedResult(base.Unauthorized()); } +#endif + +#if NETCORE + [NonAction] + public new TestUnauthorizedResult Unauthorized(string message) { return new TestUnauthorizedResult(base.Unauthorized(message)); } +#endif + +#if NETCORE + [NonAction] + public new TestUnauthorizedResult Unauthorized(ODataError error) { return new TestUnauthorizedResult(base.Unauthorized(error)); } +#endif + +#if NETCOREAPP3_1 + [NonAction] + public new TestConflictResult Conflict() { return new TestConflictResult(base.Conflict()); } + + [NonAction] + public new TestConflictResult Conflict(string message) { return new TestConflictResult(base.Conflict(message)); } + + [NonAction] + public new TestConflictResult Conflict(ODataError error) { return new TestConflictResult(base.Conflict(error)); } + + [NonAction] + public new TestUnprocessableEntityResult UnprocessableEntity() { return new TestUnprocessableEntityResult(base.UnprocessableEntity()); } + + [NonAction] + public new TestUnprocessableEntityResult UnprocessableEntity(string message) { return new TestUnprocessableEntityResult(base.UnprocessableEntity(message)); } + + [NonAction] + public new TestUnprocessableEntityResult UnprocessableEntity(ODataError error) { return new TestUnprocessableEntityResult(base.UnprocessableEntity(error)); } +#endif + [NonAction] public new TestOkResult Ok() { return new TestOkResult(base.Ok()); } @@ -333,7 +378,7 @@ public TestOkObjectResult(T content, TestODataController controller) /// Wrapper for BadRequestResult /// #if NETCORE - public class TestBadRequestResult : TestStatusCodeResult + public class TestBadRequestResult : TestActionResult { public TestBadRequestResult(BadRequestResult innerResult) : base(innerResult) @@ -387,6 +432,45 @@ public TestBadRequestObjectResult(NegotiatedContentResult innerResult) } #endif + /// + /// Wrapper for UnauthorizedResult + /// +#if NETCORE + public class TestUnauthorizedResult : TestActionResult + { + public TestUnauthorizedResult(UnauthorizedResult innerResult) + : base(innerResult) + { + } + } +#endif + + /// + /// Wrapper for ConflictResult + /// +#if NETCOREAPP3_1 + public class TestConflictResult : TestActionResult + { + public TestConflictResult(ConflictResult innerResult) + : base(innerResult) + { + } + } +#endif + + /// + /// Wrapper for UnprocessableEntityResult + /// +#if NETCOREAPP3_1 + public class TestUnprocessableEntityResult : TestActionResult + { + public TestUnprocessableEntityResult(UnprocessableEntityResult innerResult) + : base(innerResult) + { + } + } +#endif + /// /// Wrapper for StatusCodeObjectResult /// diff --git a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Formatter/ODataFormatterTests.cs b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Formatter/ODataFormatterTests.cs index 895d3b184f..c0186a74d4 100644 --- a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Formatter/ODataFormatterTests.cs +++ b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Formatter/ODataFormatterTests.cs @@ -403,6 +403,192 @@ public async Task CustomSerializerWorks() } } + [Fact] + public async Task BadRequestResponseFromODataControllerIsSerializedAsODataError() + { + // Arrange +#if NETCORE + const string expectedResponse = "{\"error\":{\"code\":\"400\",\"message\":\"Update failed.\"}}"; +#else + const string expectedResponse = "{\"error\":{\"code\":\"\",\"message\":\"Update failed.\"}}"; +#endif + + ODataConventionModelBuilder builder = ODataConventionModelBuilderFactory.Create(); + builder.EntitySet("Customers"); + IEdmModel model = builder.GetEdmModel(); + var controllers = new[] { typeof(CustomersController) }; + var server = TestServerFactory.Create(controllers, (config) => + { + config.MapODataServiceRoute("odata", null, model); + }); + + using (HttpClient client = TestServerFactory.CreateClient(server)) + using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Put, "http://localhost/Customers/1")) + { + request.Content = new StringContent( + string.Format(@"{{'@odata.type':'#Microsoft.AspNet.OData.Test.Formatter.EnumCustomer', + 'ID':1,'Color':'Green, Blue','Colors':['Red','Red, Blue']}}")); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); + + // Act + using (HttpResponseMessage response = await client.SendAsync(request)) + { + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + String actualResponse = await response.Content.ReadAsStringAsync(); + Assert.Equal(expectedResponse, actualResponse.Trim()); + + } + } + } + + [Fact] + public async Task NotFoundResponseFromODataControllerIsSerializedAsODataError() + { + // Arrange +#if NETCORE + const string expectedResponse = "{\"error\":{\"code\":\"404\",\"message\":\"Customer not found.\"}}"; +#endif + + ODataConventionModelBuilder builder = ODataConventionModelBuilderFactory.Create(); + builder.EntitySet("Customers"); + IEdmModel model = builder.GetEdmModel(); + var controllers = new[] { typeof(CustomersController) }; + var server = TestServerFactory.Create(controllers, (config) => + { + config.MapODataServiceRoute("odata", null, model); + }); + + using (HttpClient client = TestServerFactory.CreateClient(server)) + using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Customers/1")) + { + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); + + // Act + using (HttpResponseMessage response = await client.SendAsync(request)) + { + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); +#if NETCORE + String actualResponse = await response.Content.ReadAsStringAsync(); + Assert.Equal(expectedResponse, actualResponse.Trim()); +#endif + + } + } + } + + [Fact] + public async Task UnauthorizedResponseFromODataControllerIsSerializedAsODataError() + { + // Arrange +#if NETCORE + const string expectedResponse = "{\"error\":{\"code\":\"401\",\"message\":\"Not authorized to access this resource.\"}}"; +#endif + + ODataConventionModelBuilder builder = ODataConventionModelBuilderFactory.Create(); + builder.EntitySet("Customers"); + IEdmModel model = builder.GetEdmModel(); + var controllers = new[] { typeof(CustomersController) }; + var server = TestServerFactory.Create(controllers, (config) => + { + config.MapODataServiceRoute("odata", null, model); + }); + + using (HttpClient client = TestServerFactory.CreateClient(server)) + using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Customers")) + { + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); + + // Act + using (HttpResponseMessage response = await client.SendAsync(request)) + { + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); +#if NETCORE + String actualResponse = await response.Content.ReadAsStringAsync(); + Assert.Equal(expectedResponse, actualResponse.Trim()); +#endif + + } + } + } + +#if !NETCOREAPP2_0 && NETCORE + [Fact] + public async Task ConflictResponseFromODataControllerIsSerializedAsODataError() + { + // Arrange + const string expectedResponse = "{\"error\":{\"code\":\"409\",\"message\":\"Conflict during update.\"}}"; + + ODataConventionModelBuilder builder = ODataConventionModelBuilderFactory.Create(); + builder.EntitySet("Customers"); + IEdmModel model = builder.GetEdmModel(); + var controllers = new[] { typeof(CustomersController) }; + var server = TestServerFactory.Create(controllers, (config) => + { + config.MapODataServiceRoute("odata", null, model); + }); + + using (HttpClient client = TestServerFactory.CreateClient(server)) + using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Patch, "http://localhost/Customers/1")) + { + request.Content = new StringContent( + string.Format(@"{{'@odata.type':'#Microsoft.AspNet.OData.Test.Formatter.EnumCustomer', + 'ID':1,'Color':'Green, Blue','Colors':['Red','Red, Blue']}}")); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); + + // Act + using (HttpResponseMessage response = await client.SendAsync(request)) + { + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.Conflict, response.StatusCode); + String actualResponse = await response.Content.ReadAsStringAsync(); + Assert.Equal(expectedResponse, actualResponse.Trim()); + + } + } + } + + [Fact] + public async Task UnProcessableEntityResponseFromODataControllerIsSerializedAsODataError() + { + // Arrange + const string expectedResponse = "{\"error\":{\"code\":\"422\",\"message\":\"Cannot process entity.\"}}"; + + ODataConventionModelBuilder builder = ODataConventionModelBuilderFactory.Create(); + builder.EntitySet("Customers"); + IEdmModel model = builder.GetEdmModel(); + var controllers = new[] { typeof(CustomersController) }; + var server = TestServerFactory.Create(controllers, (config) => + { + config.MapODataServiceRoute("odata", null, model); + }); + + using (HttpClient client = TestServerFactory.CreateClient(server)) + using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Delete, "http://localhost/Customers/1")) + { + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); + + // Act + using (HttpResponseMessage response = await client.SendAsync(request)) + { + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.UnprocessableEntity, response.StatusCode); + String actualResponse = await response.Content.ReadAsStringAsync(); + Assert.Equal(expectedResponse, actualResponse.Trim()); + + } + } + } +#endif [Theory] [InlineData("*", "PeopleWithAllAnnotations.json")] [InlineData("-*", "PeopleWithoutAnnotations.json")] @@ -715,6 +901,61 @@ public ITestActionResult GetColors(int key) } } + public class Customer + { + public int ID { get; set; } + public Color Color { get; set; } + public List Colors { get; set; } + } + + public class CustomersController : ODataController + { +#if NETCORE + public IActionResult Put([FromODataUri] int key, [FromBody] Customer customer) + { + return BadRequest("Update failed."); + } + + [EnableQuery] + public IActionResult Get(int key) + { + return NotFound ("Customer not found."); + } + + public IActionResult Get() + { + return Unauthorized("Not authorized to access this resource."); + } +#if !NETCOREAPP2_0 + public IActionResult Patch([FromODataUri] int key, [FromBody] Customer customer) + { + return Conflict("Conflict during update."); + } + + public IActionResult Delete([FromODataUri] int key) + { + return UnprocessableEntity("Cannot process entity."); + } +#endif +#else + public IHttpActionResult Put([FromODataUri] int key, [FromBody] Customer customer) + { + return BadRequest("Update failed."); + } + + [EnableQuery] + public IHttpActionResult Get(int key) + { + return NotFound(); + } + + public IHttpActionResult Get() + { + return Unauthorized(); + } +#endif + } + public class EnumKeyCustomersController : TestODataController { public ITestActionResult Get([FromODataUri]Color key) diff --git a/test/UnitTest/Microsoft.AspNetCore.OData.Test/PublicApi/Microsoft.AspNetCore.OData.PublicApi.bsl b/test/UnitTest/Microsoft.AspNetCore.OData.Test/PublicApi/Microsoft.AspNetCore.OData.PublicApi.bsl index 6e8c3b61f7..cfd6e90c38 100644 --- a/test/UnitTest/Microsoft.AspNetCore.OData.Test/PublicApi/Microsoft.AspNetCore.OData.PublicApi.bsl +++ b/test/UnitTest/Microsoft.AspNetCore.OData.Test/PublicApi/Microsoft.AspNetCore.OData.PublicApi.bsl @@ -118,7 +118,15 @@ ApiExplorerSettingsAttribute(), public abstract class Microsoft.AspNet.OData.ODataController : Microsoft.AspNetCore.Mvc.ControllerBase { protected ODataController () + protected virtual BadRequestODataResult BadRequest (Microsoft.OData.ODataError odataError) + protected virtual BadRequestODataResult BadRequest (string message) protected virtual CreatedODataResult`1 Created (TEntity entity) + protected virtual NotFoundODataResult NotFound (Microsoft.OData.ODataError odataError) + protected virtual NotFoundODataResult NotFound (string message) + protected virtual ODataErrorResult ODataErrorResult (Microsoft.OData.ODataError odataError) + protected virtual ODataErrorResult ODataErrorResult (string errorCode, string message) + protected virtual UnauthorizedODataResult Unauthorized (Microsoft.OData.ODataError odataError) + protected virtual UnauthorizedODataResult Unauthorized (string message) protected virtual UpdatedODataResult`1 Updated (TEntity entity) } @@ -3003,6 +3011,22 @@ public sealed class Microsoft.AspNet.OData.Query.UnsortableAttribute : System.At public UnsortableAttribute () } +public interface Microsoft.AspNet.OData.Results.IODataErrorResult { + Microsoft.OData.ODataError Error { public abstract get; } +} + +public class Microsoft.AspNet.OData.Results.BadRequestODataResult : Microsoft.AspNetCore.Mvc.BadRequestResult, IActionResult, IODataErrorResult { + public BadRequestODataResult (Microsoft.OData.ODataError odataError) + public BadRequestODataResult (string message) + + Microsoft.OData.ODataError Error { public virtual get; } + + [ + AsyncStateMachineAttribute(), + ] + public virtual System.Threading.Tasks.Task ExecuteResultAsync (Microsoft.AspNetCore.Mvc.ActionContext context) +} + public class Microsoft.AspNet.OData.Results.CreatedODataResult`1 : IActionResult { public CreatedODataResult`1 (T entity) @@ -3014,6 +3038,42 @@ public class Microsoft.AspNet.OData.Results.CreatedODataResult`1 : IActionResult public virtual System.Threading.Tasks.Task ExecuteResultAsync (Microsoft.AspNetCore.Mvc.ActionContext context) } +public class Microsoft.AspNet.OData.Results.NotFoundODataResult : Microsoft.AspNetCore.Mvc.NotFoundResult, IActionResult, IODataErrorResult { + public NotFoundODataResult (Microsoft.OData.ODataError odataError) + public NotFoundODataResult (string message) + + Microsoft.OData.ODataError Error { public virtual get; } + + [ + AsyncStateMachineAttribute(), + ] + public virtual System.Threading.Tasks.Task ExecuteResultAsync (Microsoft.AspNetCore.Mvc.ActionContext context) +} + +public class Microsoft.AspNet.OData.Results.ODataErrorResult : Microsoft.AspNetCore.Mvc.ActionResult, IActionResult, IODataErrorResult { + public ODataErrorResult (Microsoft.OData.ODataError odataError) + public ODataErrorResult (string errorCode, string message) + + Microsoft.OData.ODataError Error { public virtual get; } + + [ + AsyncStateMachineAttribute(), + ] + public virtual System.Threading.Tasks.Task ExecuteResultAsync (Microsoft.AspNetCore.Mvc.ActionContext context) +} + +public class Microsoft.AspNet.OData.Results.UnauthorizedODataResult : Microsoft.AspNetCore.Mvc.UnauthorizedResult, IActionResult, IODataErrorResult { + public UnauthorizedODataResult (Microsoft.OData.ODataError odataError) + public UnauthorizedODataResult (string message) + + Microsoft.OData.ODataError Error { public virtual get; } + + [ + AsyncStateMachineAttribute(), + ] + public virtual System.Threading.Tasks.Task ExecuteResultAsync (Microsoft.AspNetCore.Mvc.ActionContext context) +} + public class Microsoft.AspNet.OData.Results.UpdatedODataResult`1 : IActionResult { public UpdatedODataResult`1 (T entity) diff --git a/test/UnitTest/Microsoft.AspNetCore.OData.Test/PublicApi/Microsoft.AspNetCore3x.OData.PublicApi.bsl b/test/UnitTest/Microsoft.AspNetCore.OData.Test/PublicApi/Microsoft.AspNetCore3x.OData.PublicApi.bsl index 39b123fca0..f2b3d7047d 100644 --- a/test/UnitTest/Microsoft.AspNetCore.OData.Test/PublicApi/Microsoft.AspNetCore3x.OData.PublicApi.bsl +++ b/test/UnitTest/Microsoft.AspNetCore.OData.Test/PublicApi/Microsoft.AspNetCore3x.OData.PublicApi.bsl @@ -118,7 +118,19 @@ ApiExplorerSettingsAttribute(), public abstract class Microsoft.AspNet.OData.ODataController : Microsoft.AspNetCore.Mvc.ControllerBase { protected ODataController () + protected virtual BadRequestODataResult BadRequest (Microsoft.OData.ODataError odataError) + protected virtual BadRequestODataResult BadRequest (string message) + protected virtual ConflictODataResult Conflict (Microsoft.OData.ODataError odataError) + protected virtual ConflictODataResult Conflict (string message) protected virtual CreatedODataResult`1 Created (TEntity entity) + protected virtual NotFoundODataResult NotFound (Microsoft.OData.ODataError odataError) + protected virtual NotFoundODataResult NotFound (string message) + protected virtual ODataErrorResult ODataErrorResult (Microsoft.OData.ODataError odataError) + protected virtual ODataErrorResult ODataErrorResult (string errorCode, string message) + protected virtual UnauthorizedODataResult Unauthorized (Microsoft.OData.ODataError odataError) + protected virtual UnauthorizedODataResult Unauthorized (string message) + protected virtual UnprocessableEntityODataResult UnprocessableEntity (Microsoft.OData.ODataError odataError) + protected virtual UnprocessableEntityODataResult UnprocessableEntity (string message) protected virtual UpdatedODataResult`1 Updated (TEntity entity) } @@ -3174,6 +3186,34 @@ public sealed class Microsoft.AspNet.OData.Query.UnsortableAttribute : System.At public UnsortableAttribute () } +public interface Microsoft.AspNet.OData.Results.IODataErrorResult { + Microsoft.OData.ODataError Error { public abstract get; } +} + +public class Microsoft.AspNet.OData.Results.BadRequestODataResult : Microsoft.AspNetCore.Mvc.BadRequestResult, IActionResult, IODataErrorResult, IClientErrorActionResult, IStatusCodeActionResult { + public BadRequestODataResult (Microsoft.OData.ODataError odataError) + public BadRequestODataResult (string message) + + Microsoft.OData.ODataError Error { public virtual get; } + + [ + AsyncStateMachineAttribute(), + ] + public virtual System.Threading.Tasks.Task ExecuteResultAsync (Microsoft.AspNetCore.Mvc.ActionContext context) +} + +public class Microsoft.AspNet.OData.Results.ConflictODataResult : Microsoft.AspNetCore.Mvc.ConflictResult, IActionResult, IODataErrorResult, IClientErrorActionResult, IStatusCodeActionResult { + public ConflictODataResult (Microsoft.OData.ODataError odataError) + public ConflictODataResult (string message) + + Microsoft.OData.ODataError Error { public virtual get; } + + [ + AsyncStateMachineAttribute(), + ] + public virtual System.Threading.Tasks.Task ExecuteResultAsync (Microsoft.AspNetCore.Mvc.ActionContext context) +} + public class Microsoft.AspNet.OData.Results.CreatedODataResult`1 : IActionResult { public CreatedODataResult`1 (T entity) @@ -3185,6 +3225,54 @@ public class Microsoft.AspNet.OData.Results.CreatedODataResult`1 : IActionResult public virtual System.Threading.Tasks.Task ExecuteResultAsync (Microsoft.AspNetCore.Mvc.ActionContext context) } +public class Microsoft.AspNet.OData.Results.NotFoundODataResult : Microsoft.AspNetCore.Mvc.NotFoundResult, IActionResult, IODataErrorResult, IClientErrorActionResult, IStatusCodeActionResult { + public NotFoundODataResult (Microsoft.OData.ODataError odataError) + public NotFoundODataResult (string message) + + Microsoft.OData.ODataError Error { public virtual get; } + + [ + AsyncStateMachineAttribute(), + ] + public virtual System.Threading.Tasks.Task ExecuteResultAsync (Microsoft.AspNetCore.Mvc.ActionContext context) +} + +public class Microsoft.AspNet.OData.Results.ODataErrorResult : Microsoft.AspNetCore.Mvc.ActionResult, IActionResult, IODataErrorResult { + public ODataErrorResult (Microsoft.OData.ODataError odataError) + public ODataErrorResult (string errorCode, string message) + + Microsoft.OData.ODataError Error { public virtual get; } + + [ + AsyncStateMachineAttribute(), + ] + public virtual System.Threading.Tasks.Task ExecuteResultAsync (Microsoft.AspNetCore.Mvc.ActionContext context) +} + +public class Microsoft.AspNet.OData.Results.UnauthorizedODataResult : Microsoft.AspNetCore.Mvc.UnauthorizedResult, IActionResult, IODataErrorResult, IClientErrorActionResult, IStatusCodeActionResult { + public UnauthorizedODataResult (Microsoft.OData.ODataError odataError) + public UnauthorizedODataResult (string message) + + Microsoft.OData.ODataError Error { public virtual get; } + + [ + AsyncStateMachineAttribute(), + ] + public virtual System.Threading.Tasks.Task ExecuteResultAsync (Microsoft.AspNetCore.Mvc.ActionContext context) +} + +public class Microsoft.AspNet.OData.Results.UnprocessableEntityODataResult : Microsoft.AspNetCore.Mvc.UnprocessableEntityResult, IActionResult, IODataErrorResult, IClientErrorActionResult, IStatusCodeActionResult { + public UnprocessableEntityODataResult (Microsoft.OData.ODataError odataError) + public UnprocessableEntityODataResult (string message) + + Microsoft.OData.ODataError Error { public virtual get; } + + [ + AsyncStateMachineAttribute(), + ] + public virtual System.Threading.Tasks.Task ExecuteResultAsync (Microsoft.AspNetCore.Mvc.ActionContext context) +} + public class Microsoft.AspNet.OData.Results.UpdatedODataResult`1 : IActionResult { public UpdatedODataResult`1 (T entity)