Reputation: 5986
Building an ODataController with a base Get method for the following query:
http://localhost:8080/api/Bases
is quite straightforward:
[EnableQuery]
public IHttpActionResult Get()
{
return Ok(new List<Base>());
}
In the same style, I'm trying to implement the "cast" route ("~/entityset/cast"), which is defined in the OData V4 convention part 4.9 but this is quite undocumented. So I dug into some source code and found that for the following URL:
http://localhost:8080/api/Bases/MyNamespace.DerivedA
I could define the following method in the same controller:
[EnableQuery]
public IHttpActionResult GetFromDerivedA()
{
return Ok(new List<DerivedA>());
}
Which works BUT I have like a dozen types that inherit from Base
. Instead of declaring one method per derived type, is there a way I could use something like:
[EnableQuery]
public IHttpActionResult GetFrom<T>()
where T : Base
{
return Ok(new List<T>());
}
I'm using:
I can create a new RoutingConvention and have the overriden SelectAction return my generic method, but it seems I'll have to forget the generic method approach:
"Cannot call action method
'System.Web.Http.IHttpActionResult GetFrom[T]()'
on controller 'MyProject.Controllers.BasesController' because the action method is a generic method."
How about this then, is this possible?
[EnableQuery]
public IHttpActionResult GetFrom(Type derivedType)
{
//snip!
}
If not, any other ideas?
Upvotes: 2
Views: 1806
Reputation: 5986
Here's a way I've been able to accomplish this, with a bit of reflection. It's quite a long way but the resulting controller method is so simple, it's worth it.
First, create a new RoutingConvention. Notice we'll be forwarding all cast requests to a method named GetFrom
:
public class CastRoutingConvention : EntitySetRoutingConvention
{
public override string SelectAction(ODataPath odataPath, HttpControllerContext controllerContext, ILookup<string, HttpActionDescriptor> actionMap)
{
if (odataPath.PathTemplate == "~/entityset/cast")
{
HttpMethod httpMethod = controllerContext.Request.Method;
var collectionType = (IEdmCollectionType)odataPath.EdmType;
var entityType = (IEdmEntityType)collectionType.ElementType.Definition;
var type = AppDomain.CurrentDomain.GetAssemblies()
.Where(a => !a.IsDynamic)
.SelectMany(a => a.DefinedTypes)
.FirstOrDefault(t => t.FullName == entityType.FullTypeName());
controllerContext.RouteData.Values["type"] = type;
if (httpMethod == HttpMethod.Get)
return "GetFrom";
else if (httpMethod == HttpMethod.Post)
return "PostFrom";
else
return base.SelectAction(odataPath, controllerContext, actionMap);
}
else
return base.SelectAction(odataPath, controllerContext, actionMap);
}
}
Next, add it to the OData configuration:
public static void Register(HttpConfiguration config)
{
config.MapHttpAttributeRoutes();
var builder = new ODataConventionModelBuilder() { Namespace = "Default" };
builder.DataServiceVersion = Version.Parse("4.0");
//snip! entity configuration
var conventions = ODataRoutingConventions.CreateDefault();
conventions.Insert(0, new CastRoutingConvention());
config.MapODataServiceRoute(
routeName:"ODataRoute",
routePrefix: "api",
routingConventions: conventions,
pathHandler: new DefaultODataPathHandler(),
model: builder.GetEdmModel());
}
Now, because the default model binders will not read arbitrary parameter names from the route data dictionary, we need a custom model binder for route data:
using System;
using System.Web.Http.Controllers;
using System.Web.Http.ModelBinding;
namespace Example
{
public class RouteDataModelBinder : IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
object model;
if (!actionContext.RequestContext.RouteData.Values.TryGetValue(bindingContext.ModelName, out model))
{
bindingContext.ModelState.AddModelError(bindingContext.ModelName, $"No route data named '{bindingContext.ModelName}'.");
return false;
}
else if (!bindingContext.ModelType.IsAssignableFrom(model.GetType()))
{
try
{
model = Convert.ChangeType(model, bindingContext.ModelType);
}
catch
{
bindingContext.ModelState.AddModelError(bindingContext.ModelName, $"Route data cannot be converted to type '{bindingContext.ModelType.FullName}'.");
return false;
}
}
bindingContext.Model = model;
return true;
}
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Parameter, Inherited = true, AllowMultiple = false)]
public class RouteDataAttribute : ModelBinderAttribute
{
public RouteDataAttribute()
{
this.BinderType = typeof(RouteDataModelBinder);
}
}
}
Finally, add the needed method in the controller. Notice how trivial it is:
[EnableQuery]
public IHttpActionResult GetFrom([RouteData]Type type)
{
var ofType = typeof(Queryable).GetMethod("OfType").MakeGenericMethod(type);
return Ok((IQueryable<Base>)ofType.Invoke(null, new object[] { this.Context.Bases }));
}
Since I'm using Entity Framework and I can't use GetType()
, I have to use another reflection trick to call OfType<T>()
with a Type
instance. If you're working with in-memory entities, just scrap the last part and use a plain:
return Ok(inRamEntities.Where(e => e.GetType() == type));
Upvotes: 2