首页 文章

适用于'Cast'的OData V4 WebAPI路由约定

提问于
浏览
1

使用以下查询的基本Get方法构建ODataController:

http://localhost:8080/api/Bases

很简单:

[EnableQuery]
public IHttpActionResult Get()
{
    return Ok(new List<Base>());
}

在相同的样式中,我正在尝试实现"cast"路由("~/entityset/cast"),该路由在OData V4 convention part 4.9中定义,但这完全没有记录 . 所以我挖了一些source code并发现对于以下网址:

http://localhost:8080/api/Bases/MyNamespace.DerivedA

我可以在同一个控制器中定义以下方法:

[EnableQuery]
public IHttpActionResult GetFromDerivedA()
{
    return Ok(new List<DerivedA>());
}

哪个有效,但我有十几种从 Base 继承的类型 . 而不是为每个派生类型声明一个方法,有没有办法可以使用类似的东西:

[EnableQuery]
public IHttpActionResult GetFrom<T>()
    where T : Base
{
    return Ok(new List<T>());
}

我正在使用:

  • Microsoft.AspNet.WebApi 5.2.3

  • Microsoft.OData 6.13.0

  • Microsoft.AspNet.OData 5.6.0


更新

我可以创建a new RoutingConvention并使覆盖的SelectAction返回我的泛型方法,但似乎我不得不忘记泛型方法方法:

“无法在控制器'MyProject.Controllers.BasesController'上调用动作方法'System.Web.Http.IHttpActionResult GetFrom [T]()',因为动作方法是一种通用方法 . ”

那怎么样,这可能吗?

[EnableQuery]
public IHttpActionResult GetFrom(Type derivedType)
{
    //snip!
}

如果没有,还有其他想法吗?

1 回答

  • 1

    这是我通过一些反思来实现这一目标的方法 . 这是相当长的路,但最终的控制器方法非常简单,值得 .

    首先,创建一个新的RoutingConvention . 请注意,我们将把所有强制转换请求转发给名为 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);
        }
    }
    

    接下来,将其添加到OData配置:

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

    现在,因为默认模型 Binders 不会从路径数据字典中读取任意参数名称,所以我们需要a custom model binder用于路径数据:

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

    最后,在控制器中添加所需的方法 . 请注意它是多么微不足道:

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

    由于我'm using Entity Framework and I can'使用 GetType() ,我必须使用另一个反射技巧来调用带有 Type 实例的 OfType<T>() . 如果你正在处理内存中的实体,只需废弃最后一部分并使用普通的:

    return Ok(inRamEntities.Where(e => e.GetType() == type));
    

相关问题