我最终结合了我最初的第三和第四个想法。
我正在添加我自己的答案来展示我是如何让它发挥作用的。在我所做的所有谷歌搜索中,我没有找到一个关于如何做到这一点的清晰示例。我决定不进行一个总是需要一个 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 更好地内置了对版本控制的支持。但是我使用这个类作为我的起点,因为它缓存了使用反射检索到的值的结果以选择正确的控制器,这对于请求中的后续调用以提高性能很重要。为了我的目的,我做了很多修改。
好吧,这就是我要说的。