在同一控制器上的Web Api中进行多个POST调用的最佳实践

时间:2014-07-12 03:39:01

标签: c# rest asp.net-web-api rpc asp.net-web-api2

我有一个问题,那些做Web API REST服务的人。如何设计服务来处理单个实体的POST,以及是否能够接收所述实体集合的POST?

例如:

public IHttpActionResult Post([FromBody]User value)
{
    // stuff
}

public IHttpActionResult Post([FromBody]IEnumerable<User> values)
{
    // stuff
}

开箱即用,这不起作用,因为默认路由匹配这两者。

我知道有几种不同的方法可以解决这个问题,但我想要学习最佳实践&#34;方式。

你做了什么来完成同样的行为?

我的想法如下:

  1. 我可以这样做,所以帖子的签名只需要一个List作为参数。我将取消仅占用一个用户的那个。使用该api调用的任何代码都必须知道将其实体包装在某种集合中。
  2. 我可以创建两个不同的控制器,api / user和api / users,每个控制器都有自己的POST。这种方法并不真正适用于REST,因为api / user检索所有用户,而api / user / 1检索Id == 1的用户,那么api / users意味着什么? api / users / 1是什么意思?等等......所以可能不是这个选项。
  3. 尝试使用一组自定义约束与控制器中的ActionName属性结合使用,并为每个POST写入路由(我不确定这个是否可以工作)。
  4. 将其设为RPC调用。如果是这种情况,您为RPC控制器命名了什么?当一些是REST而一些是RPC时,你在解决方案中将它们放在哪里?我应该使用什么标准来确定是否需要RPC调用,或者我应该将其保留为REST?
  5. 还有其他什么吗?
  6. 谢谢你的智慧之言。我非常感谢任何/所有人参与此活动。我真的只想弄清楚最佳做法是什么。任何可以给出的例子都是超级的!

3 个答案:

答案 0 :(得分:3)

我最终结合了原来的第三和第四个想法。

我正在添加自己的答案,以证明我是如何使用它的。在我完成的所有谷歌搜索中,我没有找到关于如何做到这一点的清晰示例。我决定不做一个单独的调用,总是需要一个IEnumerable,无论是想发布一个还是多个。做出这个决定的原因是我想的时间越长,我就越意识到插入的一个或多个用户所涉及的行为是完全不同的。例如,如果我提交一个用户并且由于未填写必填字段而未通过验证,我希望收到一个错误响应,其中包含服务器拒绝它的原因的详细信息。如果一次提交多个用户,情况仍然如此吗?我是否需要为帖子中失败的每个用户提供错误原因?根据我的需要,答案是否定的。这需要以不同的方式处理。

因此,我的答案是在我的web api解决方案中将REST调用与RPC(远程过程调用)结合起来。但是,如果沿着这条道路走下去,我的要求是RPC调用需要在不同的控制器中,但是网址仍然需要指向相同的整体&#34; Controller&#34; (路径的{controller}部分,如api / {controller})。

例如,此web api url接受REST动词Get,Post,Put和Delete:

  

API /用户

我提交多个用户的电话需要在以下地址接受POST:

  

API /用户/导入

...但是每个调用的逻辑都需要在不同的控制器中。

我能够通过以下方式实现这一目标:

  • 编写IHttpControllerSelector的自定义实现
  • 配置了2个路由映射,一个用于REST,另一个用于RPC
  • 编写2个自定义路由约束以确定REST或RPC路由参数
  • 修改DefaultApi路由映射以使用REST约束
  • 添加&#34; RpcApi&#34;使用RPC约束的路由

现在我打破了实现代码的方法。

我的ControllerSelector如下:

public class MyHttpControllerSelector : IHttpControllerSelector
{
    private const string ActionKey = "action";
    private const string ControllerKey = "controller";

    private readonly HttpConfiguration _configuration;
    private readonly Lazy<Dictionary<string, HttpControllerDescriptor>> _controllers;

    public MyHttpControllerSelector(HttpConfiguration config)
    {
        _configuration = config;
        _controllers = new Lazy<Dictionary<string, HttpControllerDescriptor>>(InitializeControllerDictionary);
    }

    private Dictionary<string, HttpControllerDescriptor> InitializeControllerDictionary()
    {
        var dictionary = new Dictionary<string, HttpControllerDescriptor>(StringComparer.OrdinalIgnoreCase);

        var controllerTypes = GetControllerTypes();

        foreach (var type in controllerTypes)
        {
            var controllerName = type.Name.Remove(type.Name.Length - DefaultHttpControllerSelector.ControllerSuffix.Length);

            dictionary[controllerName] = new HttpControllerDescriptor(_configuration, type.Name, type);  
        }

        return dictionary;
    }

    private IEnumerable<Type> GetControllerTypes()
    {
        var assembliesResolver = _configuration.Services.GetAssembliesResolver();
        var controllersResolver = _configuration.Services.GetHttpControllerTypeResolver();

        return controllersResolver.GetControllerTypes(assembliesResolver);
    }

    private static T GetRouteVariable<T>(IHttpRouteData routeData, string name)
    {
        object result = null;

        if (routeData.Values.TryGetValue(name, out result))
        {
            return (T)result;
        }

        return default(T);
    }

    public HttpControllerDescriptor SelectController(HttpRequestMessage request)
    {
        var routeData = GetRouteData(request);

        var controllerName = GetRequestedControllerName(routeData);

        var actionName = GetRequestedActionName(routeData);

        var isApiRoute = GetIsApiRoute(routeData);

        var controllerSelectorKey = GetControllerSelectorKey(actionName, controllerName, isApiRoute);

        return GetControllerDescriptor(request, controllerSelectorKey);
    }

    private bool GetIsApiRoute(IHttpRouteData routeData)
    {
        return routeData.Route.RouteTemplate.Contains("api/");
    }

    private static IHttpRouteData GetRouteData(HttpRequestMessage request)
    {
        var routeData = request.GetRouteData();

        if (routeData == null)
            throw new HttpResponseException(HttpStatusCode.NotFound);

        return routeData;
    }

    private HttpControllerDescriptor GetControllerDescriptor(HttpRequestMessage request, string controllerSelectorKey)
    {
        HttpControllerDescriptor controllerDescriptor = null;

        if (!_controllers.Value.TryGetValue(controllerSelectorKey, out controllerDescriptor))
        {
            throw new HttpResponseException(HttpStatusCode.NotFound);
        }

        return controllerDescriptor;
    }

    private static string GetControllerSelectorKey(string actionName, string controllerName, bool isApi)
    {
        return string.IsNullOrWhiteSpace(actionName) || !isApi
            ? controllerName
            : string.Format("{0}{1}", controllerName, "Rpc");
    }

    private static string GetRequestedControllerName(IHttpRouteData routeData)
    {
        string controllerName = GetRouteVariable<string>(routeData, ControllerKey);

        if (controllerName == null)
        {
            throw new HttpResponseException(HttpStatusCode.NotFound);
        }

        return controllerName;
    }

    private static string GetRequestedActionName(IHttpRouteData routeData)
    {
        return GetRouteVariable<string>(routeData, ActionKey);
    }

    public IDictionary<string, HttpControllerDescriptor> GetControllerMapping()
    {
        return _controllers.Value;
    }
}

这是我的IsRestConstraint:

public class IsRestConstraint : IHttpRouteConstraint
{
    public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values,
                      HttpRouteDirection routeDirection)
    {
        if (values.ContainsKey(parameterName))
        {
            string id = values[parameterName] as string;

            return string.IsNullOrEmpty(id) || IsRest(id);
        }
        else
        {
            return false;
        }
    }

    private bool IsRest(string actionName)
    {
        bool isRest = false;

        Guid guidId;
        int intId;

        if (Guid.TryParse(actionName, out guidId))
        {
            isRest = true;
        }
        else if (int.TryParse(actionName, out intId))
        {
            isRest = true;
        }

        return isRest;
    }
}

这是我的IsRpcConstraint:

public class IsRpcConstraint : IHttpRouteConstraint
{
    public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values,
                      HttpRouteDirection routeDirection)
    {
        if (values.ContainsKey(parameterName))
        {
            string action = values[parameterName] as string;

            return !string.IsNullOrEmpty(action) && IsRpcAction(action);
        }
        else
        {
            return false;    
        }
    }

    private bool IsRpcAction(string actionName)
    {
        bool isRpc = true;

        Guid guidId;
        int intId;

        if (Guid.TryParse(actionName, out guidId))
        {
            isRpc = false;
        }
        else if (int.TryParse(actionName, out intId))
        {
            isRpc = false;
        }

        return isRpc;
    }
}

在我的WebApiConfig中,我的路由如下所示(注意我还用我的MyHttpControllerSelector替换默认的IHttpControllerSelector,以及我使用自定义约束IsRpcConstraint和IsRestConstraint的地方):

config.MapHttpAttributeRoutes();

config.Services.Replace(typeof(IHttpControllerSelector), new MyHttpControllerSelector(config));

config.Routes.MapHttpRoute(
    name: "RpcApi",
    routeTemplate: "api/{controller}/{action}/{id}",
    defaults: new { id = RouteParameter.Optional },
    constraints: new { action = new IsRpcConstraint() }
);

config.Routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional },
    constraints: new { id = new IsRestConstraint() }
);

因此,当请求进入时,如果URI中的第三个段(&#34; RpcApi&#34;路由中的{action})不是整数,而不是guid,则认为它是RPC调用,并且不是空的。同样,如果&#34; DefaultApi&#34;上的第三个段,则将其视为REST调用。路径模板是一个整数或一个Guid OR根本不提供。

这样,请求就会映射到正确的路由,MyHttpControllerSelector会相应地选择适当的控制器。因此,如果正在进行呼叫:

  

API /用户/ 1

然后MyHttpControllerSelector将使用名为UserController的控制器。同样,如果正在进行呼叫:

  

API /用户/导入

然后MyHttpControllerSelector将使用名为UserRpcController的控制器,并且调用将自动映射到其中的Import操作。

到目前为止,这已经让我所有为RPC支持所做的就是添加一个控制器&#34; Rpc&#34;在它有我的域实体的前缀(在我的情况下,用户)。它可能是TreeController和TreeRpcController,DogController和DogRpcController,端点将是:

api/Tree       (TreeController)
api/Tree/1     (TreeController)
api/Tree/grow  (TreeRpcController)
api/Dog        (DogController)
api/Dog/1      (DogController)
api/Dog/bark   (DogRpcController)

最重要的是,我得到了一个干净的WebApiConfig。它不会受到每条路线中许多特定路线模板和控制器选择的污染。无论将多少REST控制器和RPC控制器添加到解决方案中,我只需要指定2个路由映射。

这种方法确实假设{id}段的REST调用上的参数必须是要考虑用于REST控制器的int或guid。通过这种设置,一个普通的&#39;字符串将被视为&#34; action&#34;并因此映射到我的Rpc控制器。对于我的情况,这很好。我只使用int和Guid来获取ID。

我还要补充一点,到目前为止,没有要求在此web api服务中使用任何类型的UI。在某些时候,即将到来,所以我有控制器选择器MyHttpControllerSelector设置为自动返回常规控制器(非RPC),如果它没有检测到&#34; api /&#34;在正在使用的路由模板中。这是为了支持路线模板,例如:

  

{控制器} / {行动} / {ID}

这是一种常规的MVC风格控制器路径。

我将MyHttpControllerSelector建模在这里找到的一个:

http://blogs.msdn.com/b/webdev/archive/2013/03/08/using-namespaces-to-version-web-apis.aspx

实际代码链接在文章的底部,指向此处:

http://aspnet.codeplex.com/SourceControl/changeset/view/dd207952fa86#Samples/WebApi/NamespaceControllerSelector/NamespaceHttpControllerSelector.cs

这是一个关于如何使用自定义控制器选择器使用命名空间进行Web api服务版本控制的示例。这种技术有点过时,因为据我所知,更新版本的web api更好地支持版本控制。但是我使用这个类作为我的起点,因为它缓存了使用反射检索的值的结果来选择正确的控制器,这对于提高性能的请求中的后续调用很重要。为了我的目的,我修改了很多。

嗯,这就是我要说的全部。

答案 1 :(得分:1)

我通常喜欢控制器中每个http动词只有一个方法的方法。 主要是因为这为瘦控制器提供了单一职责。我也喜欢将该方法命名为动词(Get,Post,Update,Delete等)

它还具有使URL管理变得非常容易的额外好处。在很多情况下,web api是从javascript中命中的,你必须将url存储在配置文件或javascript文件中。如果你在控制器中使用每个动词的单个方法,你可以对所有动词使用相同的url,只需依靠web api就可以根据标题中的https动词提供正确的方法。

我看到属性修饰路径也是如此有用,但我担心它在某种意义上是邀请用很多方法创建非常厚的控制器。

答案 2 :(得分:1)

您的第二个Post方法已经是第一个方法的超集,这种情况与您的第一个想法非常相似。您不需要&#34;使其成为帖子的签名只需将List作为参数&#34; 。事实上它更好,因为它可以接受任何可枚举的对象。请注意,IEnumerable描述了行为,而List是该行为的实现。当您使用IEnumerable时,您可以让编译器将工作推迟到以后,可能会在此过程中进行优化。

因此,为了复制第一个Post方法的功能,您将传递IEnumerable派生的User个对象集合,这些对象只包含一个User对象。当然,你的API调用者需要知道这一点,但它确实不应该成为他们的问题。

如果这听起来不太有用,那么您可能应该根据第三个想法来调查您的路线选项(全局或本地装饰)。您甚至可以考虑属性路由,即检测所提供参数的类型并将请求路由到适当的操作方法。