WebApi OData v3 OperationDescriptor 根据格式返回不同的 Title/Target URI(atom vs json)

WebApi OData v3 OperationDescriptor returning different Title/Target URI depending on the format (atom vs json)

考虑以下使用 OData v3.

的简单 ASP.NET Web Api

MyEntity.cs

public class MyEntity
{
    public Guid Id { get; set; }
    public string Name { get; set; }
}

MyEntitiesController.cs

public class MyEntitiesController : ODataController
{
    public IEnumerable<MyEntity> Get()
    {
        return new MyEntity[] { new MyEntity() { Id = Guid.NewGuid(), Name = "Name" } };
    }

    [HttpPost]
    public string MyAction()
    {
        return "Hello World!";
    }
}

WebApiConfig.cs

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        var modelBuilder = new ODataConventionModelBuilder();
        modelBuilder.Namespace = "MyNamespace";
        modelBuilder.ContainerName = "MyContainer";
        modelBuilder.EntitySet<MyEntity>("MyEntities");

        var action = modelBuilder.Entity<MyEntity>().Action("MyAction");
        action.Returns<string>();

        foreach (var structuralType in modelBuilder.StructuralTypes)
        {
            // Resets the namespace so that the service contains only 1 namespace.
            structuralType.GetType().GetProperty("Namespace").SetValue(structuralType, "MyNamespace");
        }

        var model = modelBuilder.GetEdmModel();
        config.Routes.MapODataServiceRoute("OData", "odata", model);
    }
}

在客户端,我添加了一个简单的服务参考。

Program.cs

class Program
{
    static void Main(string[] args)
    {
        var contextAtom = new MyContainer(new Uri("http://localhost:63939/odata/"));
        contextAtom.Format.UseAtom();
        var myEntityAtom = contextAtom.MyEntities.First();

        // Outputs: http://localhost:63939/odata/MyEntities(guid'2c2431cd-4afa-422b-805b-8398b9a29fec')/MyAction
        var uriAtom = contextAtom.GetEntityDescriptor(myEntityAtom).OperationDescriptors.First().Target;
        Console.WriteLine(uriAtom);

        // Works fine using ATOM format!
        var responseAtom = contextAtom.Execute<string>(uriAtom, "POST", true);

        var contextJson = new MyContainer(new Uri("http://localhost:63939/odata/"));
        contextJson.Format.UseJson();
        var myEntityJson = contextJson.MyEntities.First();

        // Outputs: http://localhost:63939/odata/MyEntities(guid'f31a8332-025b-4dc9-9bd1-27437ae7966a')/MyContainer.MyAction
        var uriJson = contextJson.GetEntityDescriptor(myEntityJson).OperationDescriptors.First().Target;
        Console.WriteLine(uriJson);

        // Throws an exception using the JSON uri in JSON format!
        var responseJson = contextJson.Execute<string>(uriJson, "POST", true);

        // Works fine using ATOM uri in JSON format!
        var responseJson2 = contextJson.Execute<string>(uriAtom, "POST", true);
    }
}

我的问题是,根据用于查询实体的格式,操作描述符目标 URI 是不同的。来自 ATOM 的目标 URI 工作正常,但来自 JSON 的目标 URI 总是抛出异常。

有没有办法让操作描述符在使用两种格式(ATOM 和 JSON)时工作,而不是手动连接 URI?

请注意,我在使用 OData v4 时遇到了同样的问题,但将 MyNamespace.MyAction 作为标题和目标 URI 而不是 MyContainer.MyAction。

我能够重现这个问题,这是客户端的一个错误。我发现的唯一解决方法是扩展 MyContainer class 以提供调用操作的强类型方法:

namespace <NAMESPACE_OF_MYCONTAINER_CLASS>
{
    public partial class MyContainer
    {
        public double MyAction(Guid id)
        {
            Uri actionUri = new Uri(this.BaseUri,
                String.Format("MyEntities(guid'{0}')/MyAction", id)
                );

            return this.Execute<string>(actionUri, 
                "POST", true).First();
        }
    }
} 

如所述here. I have tracked this problem and seems old, I found this post,其中一个人 (Uffe Lauesen) 在他第二个 post 中解释了当他阅读 ActionDescriptor 的标题(而不是目标)属性 时的一些奇怪行为 class 当使用 json 格式时。

你还在他们的 github page.

中用 odata.net 打开了一个问题

更新:

我跟踪了这​​个问题,Atom 格式使用 NoOpEntityMetadataBuilder,它 return 是非计算操作(使用 atom 格式它解析 xml 并从提要中获取操作)。

internal override IEnumerable<ODataAction> GetActions()
{
    DebugUtils.CheckNoExternalCallers();
    return this.entry.NonComputedActions;
}

而不是 Json 格式使用 ODataConventionalEntityMetadataBuilder,其中 returns 计算的动作与 non-computed 动作串联:

internal override IEnumerable<ODataAction> GetActions()
{
    DebugUtils.CheckNoExternalCallers();
    return ODataUtilsInternal.ConcatEnumerables(this.entryMetadataContext.Entry.NonComputedActions, this.MissingOperationGenerator.GetComputedActions());
}

对于计算操作,我们结束了在 EdmLibraryExtensions 中调用此扩展函数:

internal static string FullName(this IEdmEntityContainerElement containerElement)
{
    Debug.Assert(containerElement != null, "containerElement != null");

    return containerElement.Container.Name + "." + containerElement.Name;
}

所以我相信这里最好没有return container.Name,只有containerElement.Name。 运行 在 github 中解决问题并发布正式版本之前,具有此最小更改的 Microsoft OData 库的补丁版本可以避免该问题。

截止到今天,OData/odata.net github的问题已经分配给某人,但仍然没有消息。

我决定编写自定义 OData 路径处理程序来支持 JSON 操作名称。它与以下 OData 路径模板一起工作 "for me":~/action、~/entityset/key/action 和 ~/entityset/action.

CustomODataPathHandler.cs

internal class CustomODataPathHandler : DefaultODataPathHandler
{
    #region Methods

    protected override ODataPathSegment ParseAtEntityCollection(IEdmModel model, ODataPathSegment previous, IEdmType previousEdmType, string segment)
    {
        ODataPathSegment customActionPathSegment;
        if (TryParseCustomAction(model, previousEdmType, segment, out customActionPathSegment))
        {
            return customActionPathSegment;
        }

        return base.ParseAtEntityCollection(model, previous, previousEdmType, segment);
    }

    protected override ODataPathSegment ParseAtEntity(IEdmModel model, ODataPathSegment previous, IEdmType previousEdmType, string segment)
    {
        ODataPathSegment customActionPathSegment;
        if (TryParseCustomAction(model, previousEdmType, segment, out customActionPathSegment))
        {
            return customActionPathSegment;
        }

        return base.ParseAtEntity(model, previous, previousEdmType, segment);
    }

    protected override ODataPathSegment ParseEntrySegment(IEdmModel model, string segment)
    {
        var container = model.EntityContainers().First();
        if (CouldBeCustomAction(container, segment))
        {
            ODataPathSegment customActionPathSegment;
            if (TryParseCustomAction(model, segment, out customActionPathSegment))
            {
                return customActionPathSegment;
            }
        }

        return base.ParseEntrySegment(model, segment);
    }

    private static bool TryParseCustomAction(IEdmModel model, IEdmType previousEdmType, string segment, out ODataPathSegment pathSegment)
    {
        var container = model.EntityContainers().First();
        if (CouldBeCustomAction(container, segment))
        {
            var actionName = segment.Split('.').Last();
            var action = (from f in container.FindFunctionImports(actionName)
                          let parameters = f.Parameters
                          where parameters.Count() >= 1 && parameters.First().Type.Definition.IsEquivalentTo(previousEdmType)
                          select f).FirstOrDefault();

            if (action != null)
            {
                pathSegment = new ActionPathSegment(action);
                return true;
            }
        }

        pathSegment = null;
        return false;
    }

    private static bool TryParseCustomAction(IEdmModel model, string segment, out ODataPathSegment pathSegment)
    {
        var container = model.EntityContainers().First();
        if (CouldBeCustomAction(container, segment))
        {
            var actionName = segment.Split('.').Last();
            var action = (from f in container.FindFunctionImports(actionName)
                          where f.EntitySet == null && !f.IsBindable
                          select f).FirstOrDefault();

            if (action != null)
            {
                pathSegment = new ActionPathSegment(action);
                return true;
            }
        }

        pathSegment = null;
        return false;
    }

    private static bool CouldBeCustomAction(IEdmEntityContainer container, string segment)
    {
        return segment.StartsWith(container.Name + ".", StringComparison.OrdinalIgnoreCase);
    }

    #endregion
}

请注意,由于 JSON 操作名称包含一个点“.”,我必须在 web.config 中添加一个处理程序(以避免与静态文件处理程序冲突):

web.config

<system.webServer>
  <handlers>
    <add name="UrlRoutingHandler" path="odata/*" verb="*" type="System.Web.Routing.UrlRoutingHandler, System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
  </handlers>
</system.webServer>

此外,WebApiConfig 更改为使用自定义 OData 路径处理程序:

WebApiConfig.cs

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        var modelBuilder = new ODataConventionModelBuilder();
        modelBuilder.Namespace = "MyNamespace";
        modelBuilder.ContainerName = "MyContainer";
        modelBuilder.EntitySet<MyEntity>("MyEntities");

        var action = modelBuilder.Entity<MyEntity>().Action("MyAction");
        action.Returns<string>();

        modelBuilder.Action("Test");

        foreach (var structuralType in modelBuilder.StructuralTypes)
        {
            // Resets the namespace so that the service contains only 1 namespace.
            structuralType.GetType().GetProperty("Namespace").SetValue(structuralType, "MyNamespace");
        }

        var model = modelBuilder.GetEdmModel();
        config.Routes.MapODataServiceRoute("OData", "odata", model, new CustomODataPathHandler(), ODataRoutingConventions.CreateDefault());
    }
}

请查看您的服务web.config。请将以下行中的 path="*." 更新为 path="odata/*"。这是为了处理 url 路径中的点。

<add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="*" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />