19

I am trying to abstract the auto-generated ODataController class in VS 2013 because the code looks identical across different controllers except the name of the POCO, so, I did the following:

 public abstract class ODataControllerBase<T,DB> : ODataController
        where T : class, IIdentifiable, new()
        where DB : DbContext, new() 
 {
     protected DB _DataContext;

     public ODataControllerBase() : base()
     {
         _DataContext = new DB();
     }

     // only one function shown for brevity
     [Queryable]
     public SingleResult<T> GetEntity([FromODataUri] int key)
     {
         return SingleResult.Create(_DataContext.Set<T>().Where(Entity => Entity.Id.Equals(key)));
     }  
 }

IIdentifiable is an interface that forces the T parameter to have a readable/writable Id integer property.

The implementation looks like this (POCOs and DataContexts should've already been created)

public class MyObjectsController : ODataControllerBase<MyObject,MyDbContext>
{
    public MyObjectsController() : base()
    {
    }

    // That's it - done because all the repetitive code has been abstracted.
}

Now, my WebApiConfig's Register function contains the following only:

public static void Register(HttpConfiguration config)
{
    ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
    builder.EntitySet<MyObject>("MyObjects");
    config.Routes.MapODataRoute("odata", "odata", builder.GetEdmModel());  
}

I run the project, http://localhost:10000/odata/MyObjects and I get the response:

<m:error>
   <m:code/>
   <m:message xml:lang="en-US">No HTTP resource was found that 
      matches the request URI `http://localhost:10000/odata/MyObjects.`
   </m:message>
   <m:innererror>
       <m:message>No routing convention was found to select an action 
            for the OData path with template '~/entityset'.
       </m:message>
       <m:type/>
       <m:stacktrace/>
   </m:innererror>
 </m:error>

What is missing? What should I remove? Is this something we can't do, i.e. are we really required to inherit ODataController directly with no intermediate parent class?

James
  • 2,195
  • 1
  • 19
  • 22
Mickael Caruso
  • 8,721
  • 11
  • 40
  • 72
  • 1
    do you have an action which returns all objects..example an action like `Get()`? – Kiran Jan 13 '14 at 23:14
  • Yes. The example function is a Get function that returns one object... Unless the Get action MUST also be named GetMyObject because action names are route-sensitive? If that is so, then this is one of those secrets. – Mickael Caruso Jan 14 '14 at 01:57
  • 7
    Problem Fixed: Changed action GetEntity([FromODataUri]int key) to plain Get([FromODataUri]int key). When abstracting controllers, don't append anything to the CRUD actions. – Mickael Caruso Jan 14 '14 at 02:24
  • 2
    @MickaelCaruso - You want to post the answer yourself, this questions looks un-answered from the outside – Yishai Galatzer Oct 08 '14 at 15:29
  • I had the same issue, but mine was caused by using protected instead of public on the Get method in the BaseController. The base method has to be public. – Jason Willett Aug 06 '15 at 01:27

2 Answers2

2

In one of our projects We also use a generic ODataController base class where we actually use GetEntity for retrieving single entities and GetEntitySet for retrieving a list of entities.

According to your supplied URL and the resulting error message, the ODATA framework cannot find an ODataAction for ~/entityset. As you have given http://localhost:10000/odata/MyObjects as the example, the action in question cannot be public SingleResult<T> GetEntity([FromODataUri] int key) as this only corresponds to a query like this http://localhost:10000/odata/MyObjects(42).

Our code for a generic controller looks like this:

public abstract class OdataControllerBase<T> : ODataController
    where T : class, IIdentifiable, new()
{
    protected OdataControllerBase(/* ... */)
        : base()
    {
        // ...
    }

    public virtual IHttpActionResult GetEntity([FromODataUri] long key, ODataQueryOptions<T> queryOptions)
    {
        // ...

        return Ok(default(T));
    }

    public virtual async Task<IHttpActionResult> GetEntitySet(ODataQueryOptions<T> queryOptions)
    {
        // ...

        return Ok<IEnumerable<T>>(default(List<T>));
    }

    public virtual IHttpActionResult Put([FromODataUri] long key, T modifiedEntity)
    {
        // ...

        return Updated(default(T));
    }

    public virtual IHttpActionResult Post(T entityToBeCreated)
    {
        // ...

        return Created(default(T));
    }

    [AcceptVerbs(HTTP_METHOD_PATCH, HTTP_METHOD_MERGE)]
    public virtual IHttpActionResult Patch([FromODataUri] long key, Delta<T> delta)
    {
        // ...

        return Updated(default(T));
    }

    public virtual IHttpActionResult Delete([FromODataUri] long key)
    {
        // ...

        return Updated(default(T));
    }
}

The code for a specific controller then is as short as this:

public partial class KeyNameValuesController : OdataControllerBase<T>
{
    public KeyNameValuesController(/* ... */)
        : base()
    {
        // there is nothing to be done here
    }
}

However we found out that both Get methods (for single result and enumerable result) actually have to start with Get. First we tried List instead of GetEntitySet and this did not work, as the framework then expects a POST for the List action).

You can actually verify and diagnose the resolving process by supplying a custom IHttpActionSelector as described in Routing and Action Selection in ASP.NET Web API (ahving a look at ASP.NET WEB API 2: HTTP Message Lifecycle might also be worth it).

So actually it is possible to use GetEntity as your method name as you originally tried in your example and there is no need to rename it to simple Get. In addition, there is no need for any modification in your ODATA configuration.

Ronald Rink 'd-fens'
  • 1,289
  • 1
  • 10
  • 27
-1

To determine which action to invoke, the framework uses a routing table. The Visual Studio project template for Web API creates a default route:

routes.MapHttpRoute(
name: "API Default",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);

Routing by Action Name

With the default routing template, Web API uses the HTTP method to select the action. However, you can also create a route where the action name is included in the URI:

routes.MapHttpRoute(
name: "ActionApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);

I configured config as follows:

config.Routes.MapHttpRoute(
            name: "GetMessage",
            routeTemplate: "api/{controller}/{action}/{quoteName}",
            defaults: new { quoteName = RouterParameters.Optional }
        );

Access your URI like this:

http://localhost:42201/api/Extract/GetMessage/Q3

OR

http://localhost:42201/api/Extract/GetMessage/?quotename=Q3
Uri Agassi
  • 36,848
  • 14
  • 76
  • 93