WebApi OData v3 OperationDescriptor returns different Title / Target URLs depending on format (atom vs json)

Consider the following simple ASP.NET Web Api with OData v3.

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);
    }
}

On the client side, I added a simple link to the service.

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);
    }
}

My problem is that depending on the format used to query the object, the identifier of the target URI of the operation descriptor is different. The target URI coming from ATOM works fine, but the one coming from JSON always throws an exception.

Instead of manually concatenating the URI, is there a way for the operation descriptors to work when using both formats (ATOM and JSON)?

, OData v4, MyNamespace.MyAction Title Target URI MyContainer.MyAction.

+4
3

. , , MyContainer , :

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();
        }
    }
} 

. , , , post, (Uffe Lauesen) , - , Title ( Target) ActionDescriptor, json.

odata.net github.

:

, Atom NoOpEntityMetadataBuilder, ( xml ).

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

Json ODataConventionalEntityMetadataBuilder, , :

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;
}

, container.Name, containerElement.Name. Microsoft OData , github, .

+1

github OData/odata.net -, - .

OData JSON. OData: ~/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());
    }
}
+1

Refer to your service's web.config. Update path = "*." in the following line: path = "odata / *". This should handle the dot in the url.

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

All Articles