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" />
考虑以下使用 OData v3.
的简单 ASP.NET Web ApiMyEntity.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" />