【问题标题】:Best Practices for multiple POST calls in Web Api on the same controller同一控制器上 Web Api 中多个 POST 调用的最佳实践
【发布时间】:2014-09-02 18:25:58
【问题描述】:

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

例如:

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

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

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

我知道有几种不同的方法可以解决这个问题,但我想学习“最佳实践”的方法。

你会怎么做来完成同样的行为?

我的想法如下:

  1. 我可以做到,所以帖子的签名只需要一个列表作为参数。我将取消只需要一个用户的那个。任何使用该 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. 完全不同的东西?

感谢您的智慧之言。我非常感谢任何/所有参与其中。我真的只是想了解最佳实践是什么。能举出的例子也太棒了吧!

【问题讨论】:

  • 一篇文章有​​两个参数,一个是单个用户实体,第二个参数是用户列表。然后你可以让它们成为可选的,这样无论你只通过一个还是另一个,路由仍然有效..
  • 我知道这是如何工作的。我对它的唯一保留是它在方法中产生额外的条件逻辑来确定如何进行。我尽量避免使用条件逻辑,每个用例使用一个方法意味着每个人都有一份工作,而且只有一份工作。

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


【解决方案1】:

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

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

我也看到了属性修饰的路由是如何有用的,但我担心它在某种意义上是一种邀请来创建具有大量方法的非常厚的控制器。

【讨论】:

  • 我同意你所说的一切。我喜欢瘦控制器,我对动作属性也有类似的保留。控制器中每个动词的单个方法确实提供了“基于标头中的 https 动词提供正确方法的 web api”。那么,如果我将单个用户对象或它们的数组发送到 api/user,我将如何设置它知道该怎么做?
  • 在传递单个对象或同一对象的数组的场景之间共享动作的一种方法是始终传递数组。在单个对象的情况下,您只需传递一个包含一个元素的数组。那时它对 web api 是透明的。
【解决方案2】:

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

因此,要复制您的第一个Post 方法的功能,您将传递一个IEnumerable 派生的User 对象集合,该集合仅包含一个User 对象。当然,您的 API 调用者需要意识到这一点,但这对他们来说确实不是问题。

如果这听起来没有用,那么也许您应该按照您的第三个想法,调查您的路由选项(全局或局部修饰)。您甚至可以考虑属性路由,即检测您提供的参数的类型并将请求路由到适当的操作方法。

【讨论】:

  • 好的,谢谢。我的意思是说 IEnumerable 就像我在代码示例中所做的那样,但在我描述我的第一个想法时却输入了 List。你是对的,当不需要由它的具体实现带来的功能时,最好使用 IEnumerable。
  • 到目前为止,在你们两人之间,@TGH 和 djikay 听起来好像使用 POST 的一个签名和 IEnumerable 作为参数是“最佳实践”。我会再坚持一段时间,看看有没有其他建议。感谢您的反馈/意见。
  • 正如我所说,您仍然可以通过使用(属性)路由来实现您想要使用单独的操作方法执行的操作,这并不可怕。我只是不相信这是一个好主意在您的特定情况下。但是,一般来说,如果在设计中在概念上有意义的话,我并不反对针对特定操作使用多个控制器方法。
【解决方案3】:

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

我正在添加我自己的答案来展示我是如何让它发挥作用的。在我所做的所有谷歌搜索中,我没有找到一个关于如何做到这一点的清晰示例。我决定不进行一个总是需要一个 IEnumerable 的调用,而不管是否要发布一个或多个。这个决定的原因是我考虑的时间越长,我就越意识到插入一个或多个用户所涉及的行为是完全不同的。例如,如果我提交了一个用户,但由于未填写必填字段而导致验证失败,我希望收到一个错误响应,其中包含服务器拒绝它的原因的详细信息。如果一次提交多个用户,还会这样吗?我需要为每个在发布期间失败的用户提供错误原因吗?根据我的需要,答案是否定的。这需要以不同的方式处理。

因此,我的答案是在我的 web api 解决方案中将 REST 调用与 RPC(远程过程调用)结合起来。但是,如果沿着这条路走,我的要求是 RPC 调用需要在不同的控制器中,但 Web 地址仍需要指向相同的整体“控制器”(像 api/{controller 这样的路由的 {controller} 部分})。

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

api/用户

我提交多个用户的调用需要在以下位置接受 POST:

api/用户/导入

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

我能够通过执行以下操作来实现这一点:

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

现在我将分解实际代码,了解我是如何实现这一点的。

我的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 中,我的路由如下所示(请注意,我还将默认 IHttpControllerSelector 替换为 MyHttpControllerSelector,以及我使用自定义约束 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 中的第三段(“RpcApi”路由中的 {action})不是整数、不是 guid 并且不是空的,则认为是 RPC 调用。同样,如果“DefaultApi”路由模板上的第三个段是整数或者根本没有提供 Guid OR,则它被视为 REST 调用。

这样,请求被映射到它们的正确路由,MyHttpControllerSelector 将相应地选择适当的控制器。因此,如果正在拨打电话:

api/User/1

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

api/用户/导入

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

到目前为止,这已经实现了,所以为了支持 RPC,我所要做的就是添加一个带有“Rpc”的控制器,其中包含我的域实体(在我的例子中是用户)的前缀。它可以是 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 调用上的参数必须是一个 int 或一个 guid 以供 REST 控制器考虑。通过这种设置,一个普通的 ol' 字符串将被视为一个“动作”,因此映射到我的 Rpc 控制器。对于我的场景,这很好。我只对 id 使用整数和 Guid。

我还应该补充一点,到目前为止,此 Web api 服务中不需要任何类型的 UI。在未来的某个时刻,它会到来,所以我有控制器选择器 MyHttpControllerSelector 设置,如果它在正在使用的路由模板中没有检测到“api/”,它会自动返回一个常规控制器(非 RPC)。这是为了支持路由模板,例如:

{控制器}/{动作}/{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 更好地内置了对版本控制的支持。但是我使用这个类作为我的起点,因为它缓存了使用反射检索到的值的结果以选择正确的控制器,这对于请求中的后续调用以提高性能很重要。为了我的目的,我做了很多修改。

好吧,这就是我要说的。

【讨论】:

    猜你喜欢
    • 2015-09-02
    • 1970-01-01
    • 2018-04-28
    • 1970-01-01
    • 2013-12-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多