5

マルチテナントのブログ アプリケーションがあるとします。アプリケーションの各ユーザーは、サービスによってホストされている多数のブログを持っている場合があります。

私たちの 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 では、次の推奨されるアプローチは次のとおりです。

  1. メッセージ本文または uri で BlogId が指定されている場合は、それを検証して、現在のユーザーに属していることを確認します。そうでない場合は、400 エラーをスローします。
  2. リクエストで BlogId が指定されていない場合は、からデフォルトの BlogId を取得IAccountContextし、コントローラー アクションで使用できるようにします。コントローラーにこのロジックを認識させたくないためIAccountContext、アクションから直接呼び出したくないのです。

[アップデート]

Twitter での議論に続いて、@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 を回答としてマークしました。

4

4 に答える 4

11

これはおそらくあなたが探しているタイプの答えではないのではないかと思いますが、それは議論に謙虚なビットを追加するかもしれません。

あなたはあなたが経験しているすべてのその問題を見て、あなたが推測する必要があるのですべてのフープをジャンプしますblogIdか?それが問題だと思います。サーバー上で別の状態(コンテキスト)を保持しているように見える一方で、RESTはすべてステートレスであり、HTTPのステートレスな性質と衝突します。

BlogIdは、操作の不可欠な部分であり、明示的にリソース識別子の一部である必要があります。したがって、単純にURLに入れます。そうしないと、ここでの問題は、名前が示すように、URL/URIがリソースを実際に一意に識別しないことです。ジョンがそのリソースにアクセスすると、エイミーが表示するときとは異なるリソースが表示されます。

これにより、デザインも簡素化されます。デザインが正しければ、すべてうまく機能します。シンプルさの実現に努めています。

于 2012-08-28T20:44:13.097 に答える
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ですぐに試してみましたが、うまくいくようです。

私はそれがあなたの要件を満たすべきだと思います。

于 2012-08-28T18:54:10.470 に答える
3

おそらく、シナリオにもHttpParameterBindingを使用できます。詳細については、 MikeHongmeiからの投稿をご覧ください。

以下の例:

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>;
                }
            }
        }
    }
}
于 2012-08-28T19:11:55.107 に答える
2

すべてのコントローラー アクションがこれを必要とするわけではないため、通常、この目的のためにアクション フィルターを実装し、そこで検証を行いますが、要件には他の懸念があり、このオプションはオプションではありません。

また、クライアントがBlogIdUri の一部として送信する必要があります。これを使用すると、ボディの余分な逆シリアル化を回避できるためです (コントローラー アクション内でこれを処理したくないため)。

ここにはいくつかの要件があり、それらは重要です。

  • すべてのアクション メソッド内でこれを処理する必要はありません。
  • ID が指定されていない場合は、ID を自動的に取得します。
  • 提供されたが有効でない (たとえば、現在のユーザーに属していない) 場合は、400 Bad Request を返します。

これらの要件を考慮すると、最適なオプションは、ベース コントローラーを介してこれを処理することです。あなたにとっては良い選択肢ではないかもしれませんが、すべての要件を処理します:

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を実装することも検討できますが、その場合、モデルがアプリケーションの別の部分と少し結合される可能性があります。

于 2012-08-28T17:52:01.840 に答える