假设我们有一个多租户博客应用程序。该应用程序的每个用户可以具有由该服务托管的多个博客。
我们的API允许阅读和撰写博客文章。在某些情况下,指定BlogId是可选的,例如,获取用ASP.NET标记的所有帖子:
/api/posts?tags=aspnet
如果我们想在特定博客上查看所有使用ASP.NET标记的帖子,我们可以请求:
/api/posts?blogId=10&tags=aspnet
某些API方法需要有效的BlogId,例如在创建新博客文章时:
POST: /api/posts
{
"blogid" : "10",
"title" : "This is a blog post."
}
需要在服务器上验证BlogId,以确保它属于当前(经过身份验证的)用户。如果未在请求中指定,我还想推断用户的默认blogId(为简单起见,您可以假设默认是用户的第一个博客)。
我们有一个IAccountContext
对象,其中包含有关当前用户的信息。如有必要,可以注入。
{
bool ValidateBlogId(int blogId);
string GetDefaultBlog();
}
在ASP.NET Web API中,推荐的方法是:
IAccountContext
检索默认的BlogId并使其可用于控制器操作。我不希望控制器知道这个逻辑,这就是为什么我不想直接从我的行动中调用IAccountContext
。在推特上讨论并考虑到@ Aliostad的建议之后,我决定将博客视为资源并将其作为我的Uri模板的一部分(因此总是需要),即
。GET api/blog/1/posts -- get all posts for blog 1
PUT api/blog/1/posts/5 -- update post 5 in blog 1
我的用于加载单个项目的查询逻辑已更新为按帖子ID和博客ID加载(以避免租户加载/更新其他人的帖子)。
唯一要做的就是验证BlogId。遗憾的是我们不能在Uri参数上使用验证属性,否则@ alexanderb的建议会起作用。相反,我选择使用ActionFilter:
public class ValidateBlogAttribute : ActionFilterAttribute
{
public IBlogValidator Validator { get; set; }
public ValidateBlogAttribute()
{
// set up a fake validator for now
Validator = new FakeBlogValidator();
}
public override void OnActionExecuting(HttpActionContext actionContext)
{
var blogId = actionContext.ActionArguments["blogId"] as int?;
if (blogId.HasValue && !Validator.IsValidBlog(blogId.Value))
{
var message = new HttpResponseMessage(HttpStatusCode.BadRequest);
message.ReasonPhrase = "Blog {0} does not belong to you.".FormatWith(blogId);
throw new HttpResponseException(message);
}
base.OnActionExecuting(actionContext);
}
}
public class FakeBlogValidator : IBlogValidator
{
public bool IsValidBlog(int blogId)
{
return blogId != 999; // so we have something to test
}
}
验证blogId现在只是用[ValidateBlog]
装饰我的控制器/操作的情况。
实际上每个人的答案都有助于解决方案,但我已将@ alexanderb标记为答案,因为它没有将验证逻辑耦合到我的控制器中。
答案 0 :(得分:11)
我担心这可能不是您正在寻找的答案类型,但它可能会为讨论添加一点点。
因为你需要推断出blogId
,你会看到所有麻烦,并且跳过所有的箍?我认为这就是问题所在。 REST是关于无状态的,而你似乎在服务器上持有一个单独的状态(上下文),它与HTTP的无状态特性发生冲突。
BlogId何时是操作的一个组成部分,需要明确地成为资源标识符的一部分 - 因此我将其简单地放在URL中。如果不这样做,那么问题在于 URL / URI 并非真正唯一标识资源 - 与名称暗示不同。如果 John 转到该资源,则会看到与 Amy 之后不同的资源。
这将简化设计,这也是有说服力的。当设计正确时,一切都很好。 我努力实现简单性。
答案 1 :(得分:6)
这是方法,我将如何实现(考虑到,我不是ASP.NET Web API专家)。
所以,首先 - 验证。你需要一个简单的模型,如:
public class BlogPost
{
[Required]
[ValidateBlogId]
public string BlogId { get; set; }
[Required]
public string Title { get; set; }
}
对于此模型,最好实现自定义验证规则。如果blogId
可用,则会根据规则进行验证。实施可以是,
public class ValidateBlogId : ValidationAttribute
{
[Inject]
public IAccountContext Context { get; set; }
public override bool IsValid(object value)
{
var blogId = value as string;
if (!string.IsNullOrEmpty(blogId))
{
return Context.ValidateBlogId(blogId);
}
return true;
}
}
(此前后,我假设使用Ninject,但你可以继续使用Ninject)。
接下来,您不希望公开blogId
初始化的详细信息。该工作的最佳候选人是行动过滤器。
public class InitializeBlogIdAttribute : ActionFilterAttribute
{
[Inject]
public IAccountContext Context { get; set; }
public override void OnActionExecuting(HttpActionContext actionContext)
{
var blogPost = actionContext.ActionArguments["blogPost"] as BlogPost;
if (blogPost != null)
{
blogPost.BlogId = blogPost.BlogId ?? Context.DefaultBlogId();
}
}
}
因此,如果blogPost模型被绑定且没有Id,则将应用默认值。
所以,最后是API控制器
public class PostsController : ApiController
{
[InitializeBlogId]
public HttpResponseMessage Post([FromBody]BlogPost blogPost)
{
if (ModelState.IsValid)
{
// do the job
return new HttpResponseMessage(HttpStatusCode.Ok);
}
return new HttpResponseMessage(HttpStatusCode.BadRequest);
}
}
就是这样。我很快就在我的VS中尝试过,似乎有效。
我认为它应该符合您的要求。
答案 2 :(得分:3)
也可以将HttpParameterBinding用于您的场景。您可以查看Mike和Hongmei中的帖子了解详情。
以下示例:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Runtime.Serialization;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Metadata;
namespace MvcApplication49.Controllers
{
public class PostsController : ApiController
{
public string Get([BlogIdBinding]int blogId, string tags = null)
{
return ModelState.IsValid + blogId.ToString();
}
public string Post([BlogIdBinding]BlogPost post)
{
return ModelState.IsValid + post.BlogId.ToString();
}
}
[DataContract]
public class BlogPost
{
[DataMember]
public int? BlogId { get; set; }
[DataMember(IsRequired = true)]
public string Title { get; set; }
[DataMember(IsRequired = true)]
public string Details { get; set; }
}
public class BlogIdBindingAttribute : ParameterBindingAttribute
{
public override System.Web.Http.Controllers.HttpParameterBinding GetBinding(System.Web.Http.Controllers.HttpParameterDescriptor parameter)
{
return new BlogIdParameterBinding(parameter);
}
}
public class BlogIdParameterBinding : HttpParameterBinding
{
HttpParameterBinding _defaultUriBinding;
HttpParameterBinding _defaultFormatterBinding;
public BlogIdParameterBinding(HttpParameterDescriptor desc)
: base(desc)
{
_defaultUriBinding = new FromUriAttribute().GetBinding(desc);
_defaultFormatterBinding = new FromBodyAttribute().GetBinding(desc);
}
public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider,
HttpActionContext actionContext, CancellationToken cancellationToken)
{
Task task = null;
if (actionContext.Request.Method == HttpMethod.Post)
{
task = _defaultFormatterBinding.ExecuteBindingAsync(metadataProvider, actionContext, cancellationToken);
}
else if (actionContext.Request.Method == HttpMethod.Get)
{
task = _defaultUriBinding.ExecuteBindingAsync(metadataProvider, actionContext, cancellationToken);
}
return task.ContinueWith((tsk) =>
{
IPrincipal principal = Thread.CurrentPrincipal;
object currentBoundValue = this.GetValue(actionContext);
if (actionContext.Request.Method == HttpMethod.Post)
{
if (currentBoundValue != null)
{
BlogPost post = (BlogPost)currentBoundValue;
if (post.BlogId == null)
{
post.BlogId = **<Set User's Default Blog Id here>**;
}
}
}
else if (actionContext.Request.Method == HttpMethod.Get)
{
if(currentBoundValue == null)
{
SetValue(actionContext, **<Set User's Default Blog Id here>**);
}
}
});
}
}
}
[<强>已更新强>] 我的同事Youssef建议使用ActionFilter一个非常简单的方法。以下是使用该方法的示例:
public class PostsController : ApiController
{
[BlogIdFilter]
public string Get(int? blogId = null, string tags = null)
{
}
[BlogIdFilter]
public string Post(BlogPost post)
{
}
}
public class BlogIdFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (actionContext.Request.Method == HttpMethod.Get && actionContext.ActionArguments["blogId"] == null)
{
actionContext.ActionArguments["blogId"] = <Set User's Default Blog Id here>;
}
else if (actionContext.Request.Method == HttpMethod.Post)
{
if (actionContext.ActionArguments["post"] != null)
{
BlogPost post = (BlogPost)actionContext.ActionArguments["post"];
if (post.BlogId == null)
{
post.BlogId = <Set User's Default Blog Id here>;
}
}
}
}
}
答案 3 :(得分:2)
由于并非所有控制器操作都需要这样,通常我会为此目的实现一个动作过滤器并在那里进行验证,但是您的要求还有其他问题需要这个选项,而不是一个选项。
另外,我要求客户端发送BlogId
作为Uri的一部分,因为有了这个,你可以避免额外的反序列化(因为你不想在内部处理这个控制器动作)。
这里有一些要求,它们很重要:
考虑到这些要求,最好的选择是通过基本控制器来处理这个问题。可能是一个不太好的选择,但可以满足您的所有要求:
public abstract class ApiControllerBase : ApiController {
public int BlogId { get; set; }
public override Task<HttpResponseMessage> ExecuteAsync(HttpControllerContext controllerContext, CancellationToken cancellationToken) {
var query = controllerContext.Request.RequestUri.ParseQueryString();
var accountContext = controllerContext.Request.GetDependencyScope().GetService(typeof(IAccountContext));
if (query.AllKeys.Any(x => x.Equals("BlogId", StringComparison.OrdinalIgnoreCase | StringComparison.InvariantCulture))) {
int blogId;
if (int.TryParse(query["BlogId"], out blogId) && accountContext.ValidateBlogId(blogId)) {
BlogId = blogId;
}
else {
ModelState.AddModelError("BlogId", "BlogId is invalid");
TaskCompletionSource<HttpResponseMessage> tcs =
new TaskCompletionSource<HttpResponseMessage>();
tcs.SetResult(
controllerContext.Request.CreateErrorResponse(
HttpStatusCode.BadRequest, ModelState));
return tcs.Task;
}
}
else {
BlogId = accountContext.GetDefaultBlogId();
}
return base.ExecuteAsync(controllerContext, cancellationToken);
}
}
您还可以考虑为RequestModel实施IValidatableObject,但这可能会使您的模型与应用程序的其他部分相关联