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

Dav*_*son 5 c# rest rpc asp.net-web-api asp.net-web-api2

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

例如:

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

public IHttpActionResult Post([FromBody]IEnumerable<User> values)
{
    // stuff
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

我的想法如下:

  1. 我可以做到这一点,以便帖子的签名只需要一个列表作为参数。我会放弃只需要一个用户的那个。使用该 api 调用的任何代码只需知道将其实体包装在某种类型的集合中。
  2. 我可以创建两个不同的控制器,api/user 和 api/user,每个控制器都有自己的 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. 完全是别的什么吗?

谢谢你的智慧之言。我真的很感谢任何人/所有人的参与。我真的只是想了解最佳实践是什么。任何可以给出的例子都是超级的!

Dav*_*son 3

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

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

因此,我的答案是在我的 Web API 解决方案中将 REST 调用与 RPC(远程过程调用)结合起来。然而,如果沿着这条路走下去,我的要求是 RPC 调用需要在不同的控制器中,但网址仍然需要指向相同的整体“控制器”(像 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”路由

现在我将分解实际代码来说明我是如何实现这一目标的。

我的控制器选择器如下:

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

这是我的 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

这是我的 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

在我的 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() }
);
Run Code Online (Sandbox Code Playgroud)

因此,当请求传入时,如果 URI 中的第三段(“RpcApi”路由中的 {action})不是整数、不是 guid 且不是空,则将其视为 RPC 调用。同样,如果“DefaultApi”路由模板上的第三段是整数或 Guid 或根本未提供,则将其视为 REST 调用。

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

api/用户/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)
Run Code Online (Sandbox Code Playgroud)

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

此方法确实假设 {id} 段的 REST 调用上的参数必须是要考虑用于 REST 控制器的 int 或 guid。通过此设置,普通的 ol' 字符串将被视为“操作”,从而映射到我的 Rpc 控制器。对于我的场景来说,这很好。我只使用 int 和 Guid 作为 id。

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

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

好吧,这就是我要说的。