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)
开箱即用,这不起作用,因为默认路由与这两者都匹配。
我知道有几种不同的方法可以解决这个问题,但我想学习“最佳实践”方法。
为了完成同样的行为,你会做什么?
我的想法如下:
谢谢你的智慧之言。我真的很感谢任何人/所有人的参与。我真的只是想了解最佳实践是什么。任何可以给出的例子都是超级的!
我最终结合了最初的第三个和第四个想法。
我将添加我自己的答案来演示我如何让它发挥作用。在我所做的所有谷歌搜索中,我没有找到关于如何执行此操作的清晰示例。我决定不进行一次始终需要 IEnumerable 的调用,无论要发布一个还是多个。做出这个决定的原因是我思考的时间越长,我就越意识到插入一个或多个用户所涉及的行为是完全不同的。例如,如果我提交一个用户,但由于未填写必填字段而导致验证失败,我希望收到一个错误响应,其中包含服务器拒绝它的原因的详细信息。如果同时提交多个用户,情况还会如此吗?我是否需要为发布过程中失败的每个用户提供错误原因?以我的需求来说,答案是否定的。这需要以不同的方式处理。
因此,我的答案是在我的 Web API 解决方案中将 REST 调用与 RPC(远程过程调用)结合起来。然而,如果沿着这条路走下去,我的要求是 RPC 调用需要在不同的控制器中,但网址仍然需要指向相同的整体“控制器”(像 api/{controller 这样的路由的 {controller} 部分) })。
例如,此 Web api url 接受 REST 动词 Get、Post、Put 和 Delete:
API/用户
我提交多个用户的调用需要接受以下位置的 POST:
api/用户/导入
...但是每个调用的逻辑都需要位于不同的控制器中。
我通过执行以下操作实现了这一目标:
现在我将分解实际代码来说明我是如何实现这一目标的。
我的控制器选择器如下:
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
实际代码链接在文章底部,指向这里:
这是一个有关如何使用自定义控制器选择器来使用命名空间对 Web API 服务进行版本控制的示例。这种技术有点过时了,因为据我所知,较新版本的 Web api 更好地内置了对版本控制的支持。但我使用此类作为起点,因为它缓存使用反射检索的值的结果以选择正确的控制器,这对于请求中的后续调用以提高性能非常重要。为了我的目的,我对其进行了很多修改。
好吧,这就是我要说的。
| 归档时间: |
|
| 查看次数: |
4854 次 |
| 最近记录: |