我有一个问题,那些做Web API REST服务的人。如何设计服务来处理单个实体的POST,以及是否能够接收所述实体集合的POST?
例如:
public IHttpActionResult Post([FromBody]User value)
{
// stuff
}
public IHttpActionResult Post([FromBody]IEnumerable<User> values)
{
// stuff
}
开箱即用,这不起作用,因为默认路由匹配这两者。
我知道有几种不同的方法可以解决这个问题,但我想要学习最佳实践&#34;方式。
你做了什么来完成同样的行为?
我的想法如下:
答案 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 /用户/导入
...但是每个调用的逻辑都需要在不同的控制器中。
我能够通过以下方式实现这一目标:
现在我打破了实现代码的方法。
我的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
实际代码链接在文章的底部,指向此处:
这是一个关于如何使用自定义控制器选择器使用命名空间进行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调用者需要知道这一点,但它确实不应该成为他们的问题。
如果这听起来不太有用,那么您可能应该根据第三个想法来调查您的路线选项(全局或本地装饰)。您甚至可以考虑属性路由,即检测所提供参数的类型并将请求路由到适当的操作方法。