WebApi icon indicating copy to clipboard operation
WebApi copied to clipboard

this.Created(entity) throws an InvalidOperationException if the entity used with the generic EdmEntityObject type

Open joergmetzler opened this issue 4 years ago • 5 comments

I guess the problem occurs if you use EdmEntityObject directly without deriving a child class. At least I am doing so. We use this type as a generic type to map our own entities which are name-value based. The location in the CreatedNegotiatedContentResult is being resolved internally by a ClrTypeCache which is not correct in my opinion - or a optimization could be done if the type is EdmEntityObject. As workaround I use this.Created(location, entity) in my controller to bypass the (default) calculation of the location.

Used OData-Version: 7.0.0.20629 But according to the repo, the same error still exists.

Here is my code to calculate the location based on the current logic but without using a ClrTypeCache

using System;
using System.Net.Http;
using System.Web.Http.Routing;
using Microsoft.AspNet.OData;
using Microsoft.AspNet.OData.Extensions;
using Microsoft.AspNet.OData.Formatter;
using Microsoft.AspNet.OData.Formatter.Serialization;
using Microsoft.OData.Edm;

namespace MyCoolNamespace
{
    internal class LocationHelper
    {
        private static readonly Lazy<Func<ResourceContext, bool, Uri>> generateODataLinkMethod;

        static LocationHelper()
        {
            generateODataLinkMethod = new Lazy<Func<ResourceContext, bool, Uri>>(() =>
            {
                var resultHelpersType = Type.GetType("Microsoft.AspNet.OData.Results.ResultHelpers, Microsoft.AspNet.OData");
                var generateODataLinkMethod = resultHelpersType.GetMethod(
                    "GenerateODataLink",
                    new[] { typeof(ResourceContext), typeof(bool) }
                );

                return (ResourceContext resourceContext, bool isEntityId) =>
                    (Uri)generateODataLinkMethod.Invoke(null, new object[] { resourceContext, isEntityId });
            });
        }

        // https://github.com/OData/WebApi/blob/7.0.0/src/Microsoft.AspNet.OData/Results/ResultHelpers.cs
        // https://github.com/OData/WebApi/blob/eaeed9a2a031b58b73946a91b1c45b52229cc828/src/Microsoft.AspNet.OData.Shared/Results/ResultHelpers.cs
        // https://github.com/OData/WebApi/blob/eaeed9a2a031b58b73946a91b1c45b52229cc828/src/Microsoft.AspNet.OData.Shared/EdmModelExtensions.cs
        // https://github.com/OData/WebApi/blob/955ee08511485f9b5ca46a4c9d6736a7e0357e85/src/Microsoft.AspNet.OData.Shared/Formatter/ClrTypeCache.cs https://github.com/OData/WebApi/blob/eaeed9a2a031b58b73946a91b1c45b52229cc828/src/Microsoft.AspNet.OData.Shared/Formatter/EdmLibHelpers.cs
        public static Uri GenerateODataLink(HttpRequestMessage request, IEdmEntityObject entity, bool isEntityId)
        {
            var model = request.GetModel();
            var path = request.ODataProperties().Path;
            var navigationSource = path.NavigationSource;

            var resourceContext = new ResourceContext(
                new ODataSerializerContext
                {
                    NavigationSource = navigationSource,
                    Model = model,
                    Url = request.GetUrlHelper() ?? new UrlHelper(request),
                    MetadataLevel = ODataMetadataLevel.FullMetadata, // Used internally to always calculate the links.
                    Request = request,
                    Path = path
                },
                entity.GetEdmType().AsEntity(),
                entity
            );

            return GenerateODataLink(resourceContext, isEntityId);
        }

        private static Uri GenerateODataLink(ResourceContext resourceContext, bool isEntityId)
        {
            return generateODataLinkMethod.Value(resourceContext, isEntityId);
        }
    }
}

joergmetzler avatar Feb 25 '21 13:02 joergmetzler

@joergmetzler Do you have a repro you can share? How are you using the LocationHelper above in your project?

gathogojr avatar Mar 02 '21 17:03 gathogojr

@gathogojr I made a minimal example of my solution here: https://github.com/joergmetzler/ODataIssue2422

You can use this command to trigger the post method: wget -UseBasicParsing 'http://localhost:44377/OData/Persons' -Method POST -Body '{"@odata.context":"http://localhost:44377/OData/$metadata#Persons/$entity","FirstName":"Jörg","LastName":"Metzler"}'

joergmetzler avatar Mar 04 '21 17:03 joergmetzler

It seems that the same issue exists also in AspNetCore (8.0.7).

System.InvalidOperationException: Cannot find the resource type 'Microsoft.AspNetCore.OData.Formatter.Value.EdmEntityObject' in the model. at Microsoft.AspNetCore.OData.Results.ResultHelpers.GetEntityType(IEdmModel model, Object entity) at Microsoft.AspNetCore.OData.Results.ResultHelpers.GenerateODataLink(HttpRequest request, Object entity, Boolean isEntityId) at Microsoft.AspNetCore.OData.Results.CreatedODataResult1.GenerateLocationHeader(HttpRequest request) at Microsoft.AspNetCore.OData.Results.CreatedODataResult1.ExecuteResultAsync(ActionContext context) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeResultFilters>g__Awaited|27_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope) at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger) at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.OData.Routing.ODataRouteDebugMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

HEADERS

Accept: / Accept-Encoding: gzip, deflate, br Connection: keep-alive Content-Length: 21 Content-Type: application/json Host: localhost:4527 User-Agent: PostmanRuntime/7.29.0 Postman-Token: 6669983a-bbd4-4ab1-ad73-e0b29b58928f

xxxammaxxx avatar Feb 10 '22 07:02 xxxammaxxx

Did you manage to get any further with this?

p6345uk avatar May 11 '23 16:05 p6345uk

The workaround of @joergmetzler for AspNetCore (tested with 8.0.7) looks like this: It has been only slightly changed on getting the path and on the initialization of the serializer context (not requiring a URL anymore).

using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OData.Extensions;
using Microsoft.AspNetCore.OData.Formatter;
using Microsoft.AspNetCore.OData.Formatter.Serialization;
using Microsoft.AspNetCore.OData.Formatter.Value;
using Microsoft.AspNetCore.OData.Routing;
using Microsoft.AspNetCore.OData.Routing.Controllers;
using Microsoft.OData.Edm;

namespace AspNetCoreApi.OData
{
	public static class ResultHelper
	{
		private static readonly Lazy<Func<ResourceContext, bool, Uri>> generateODataLinkMethod;

		static ResultHelper()
		{
			generateODataLinkMethod = new Lazy<Func<ResourceContext, bool, Uri>>(() =>
			{
				var resultHelpersType = Type.GetType("Microsoft.AspNetCore.OData.Results.ResultHelpers, Microsoft.AspNetCore.OData");
				var method = resultHelpersType.GetMethod(
					"GenerateODataLink",
					new[] { typeof(ResourceContext), typeof(bool) }
				);

				return (ResourceContext resourceContext, bool isEntityId) =>
					(Uri)method.Invoke(null, new object[] { resourceContext, isEntityId });
			});
		}

		public static IActionResult EntityCreated(this ODataController controller, IEdmEntityObject entity)
		{
			var url = GenerateODataLink(controller.Request, entity);
			return controller.Created(url, entity);
		}

		private static Uri GenerateODataLink(HttpRequest request, IEdmEntityObject entity)
		{
			var model = request.GetModel();
			var path = request.ODataFeature().Path;
			var navigationSource = path.GetNavigationSource();

			var resourceContext = new ResourceContext(
				new ODataSerializerContext
				{
					NavigationSource = navigationSource,
					Model = model,
					MetadataLevel = ODataMetadataLevel.Full, // Used internally to always calculate the links.
					Request = request,
					Path = path
				},
				entity.GetEdmType().AsEntity(),
				entity
			);

			return generateODataLinkMethod.Value(resourceContext, false);
		}
	}
}

xxxammaxxx avatar May 12 '23 09:05 xxxammaxxx