Category Archives: ASP.NET Web API

ASP.NET Web API のコントローラーをグループ化する

ASP.NET Web API を利用した大規模なアプリケーションでは、コントローラーを複数のグループに分割し、それぞれのグループで個別に開発したくなります。例えば、ユーザー向け(test1)と管理者向け(test2)の Web API を分けるケースなどがあります。

  • GET api/test1/
  • GET api/admin/test2/

そこで、ASP.NET Web API のコントローラーをグループ化する方法を考えてみました。

ASP.NET MVC の Area を利用した場合

ASP.NET MVC には、区分(Area)というコントローラーをグループ化する機能があります。

まず、ユーザー向けの Web API を実装します。

namespace WebApplication1.Controllers
{
    public class Test1Controller : ApiController
    {
        public string Get()
        {
            return "Test1Controller";
        }
    }
}
namespace WebApplication1
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }
    }
}

次に、管理者向けの Web API を区分(Area)に実装します。

namespace WebApplication1.Areas.Admin.Controllers
{
    public class Test2Controller : ApiController
    {
        public string Get()
        {
            return "Admin.Test1Controller";
        }
    }
}
namespace WebApplication1.Areas.Admin
{
    public class AdminAreaRegistration : AreaRegistration
    {
        public override string AreaName { get { return "Admin"; } }

        public override void RegisterArea(AreaRegistrationContext context)
        {
            context.Routes.MapHttpRoute(
                name: "Admin_DefaultApi",
                routeTemplate: "api/Admin/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }
    }
}

このように実装すると、ユーザー向け(test1)と管理者向け(test2)のコントローラーを定義できますが、2つの課題があります。1つめは、ユーザー向けのコントローラー(test1)を管理者向けのURLからもリクエストできてしまうことです。(その逆も)

  • GET api/test2/
  • GET api/admin/test1/

2つめは、異なる名前空間の同名のコントローラーを定義することができないことです。

ASP.NET Web API の Attribute Routing を利用した場合

ASP.NET Web API 2.0 の新機能に、Attribute Routing があります。

まず、ユーザー向けの Web API を実装します。

namespace WebApplication1.Controllers
{
    [Route("api/test1")]
    public class Test1Controller : ApiController
    {
        public string Get()
        {
            return "Test1Controller";
        }
    }
}

次に、管理者向けの Web API を実装します。

namespace WebApplication1.Admin.Controllers
{
    [Route("api/admin/test2")]
    public class Test2Controller : ApiController
    {
        public string Get()
        {
            return "Admin.Test1Controller";
        }
    }
}

このように実装すると、ユーザー向け(test1)と管理者向け(test2)のコントローラーを定義できます。ユーザー向けのコントローラー(test1)は、管理者向けのURLからはリクエストできません。しかし、異なる名前空間の同名のコントローラーを定義することができない課題は残ります。

まとめ

ASP.NET MVC のコントローラーであれば、区分(Area)を利用することで、それぞれのグループで個別に開発できます。他のグループのURLからリクエストされることはないですし、異なる名前空間の同名のコントローラーも定義できます。一方、ASP.NET Web API では、区分(Area)が上手く機能しないので、Attribute Routing を使うことになりそうです。ただし、同名のコントローラーが定義されないように、命名規則を設けるなどの工夫は行ったほうがよいかと思います。

ASP.NET Web API を経由して Azure Blob Storage にアクセスする

実際に試してみたら、少しハマってしまったので、備忘も兼ねて投稿しておきます。

Azure Storage Client を使って Blob にアクセスできますが、

  • クライアントに Azure Storage のキーや署名情報を公開したくない
  • アクセスログを取りたい

といった要件があった場合、ASP.NET Web API を経由して Blob にアクセスすることで、データが Webサーバー上にあるかのように扱うことができます。

Blob の test コンテナにファイルをアップロード/ダウンロードできる Web API を実装してみました。

[RoutePrefix("api/blob/{blobName}")]
public class BlobController : ApiController
{
    private CloudBlobContainer container;

    public BlobController()
    {
        var storageAccount = CloudStorageAccount.Parse(CloudConfigurationManager.GetSetting("StorageConnectionString"));
        var blobClient = storageAccount.CreateCloudBlobClient();
        this.container = blobClient.GetContainerReference("test");
        this.container.CreateIfNotExists();
    }

    [Route]
    // GET api/blob/sample.jpg
    public async Task<IHttpActionResult> Get(string blobName)
    {
        var blob = await this.container.GetBlobReferenceFromServerAsync(blobName);
        var content = new byte[blob.Properties.Length];
        await blob.DownloadToByteArrayAsync(content, 0);

        var response = new HttpResponseMessage(HttpStatusCode.OK);
        response.Content = new ByteArrayContent(content);
        response.Content.Headers.ContentType = new MediaTypeHeaderValue(blob.Properties.ContentType);
        return ResponseMessage(response);
    }

    [Route]
    // POST api/blob/sample.jpg
    public async Task<IHttpActionResult> Post([FromBody]byte[] content, [FromUri]string blobName)
    {
        var blob = this.container.GetBlockBlobReference(blobName);
        blob.Properties.ContentType = Request.Content.Headers.ContentType.MediaType;
        await blob.UploadFromByteArrayAsync(content, 0, content.Length);

        return Created(Request.RequestUri, "");
    }
}

HTTP の GET メソッドで「api/blob/sample.jpg」にリクエストすると、ファイルをダウンロードできます。

ハマったのはアップロードです。クライアント側は、WPFアプリでリクエストを投げています。HTTP の POST メソッドで「api/blob/sample.jpg」にリクエストすると、HTTP ステータスコードの 415(Unsupported Media Type)が返されます。

private async void Button_Click(object sender, RoutedEventArgs e)
{
var client = new HttpClient();
var content = new ByteArrayContent(File.ReadAllBytes(@"C:\Sample.jpg"));
content.Headers.ContentType = new MediaTypeHeaderValue("image/jpeg");
await client.PostAsync("http://localhost:2839/api/blob/sample.jpg/", content);
}

いろいろと試してみたところ、BlobController の実装を変更することで解決しました。

[Route]
// POST api/blob/sample.jpg
public async Task<IHttpActionResult> Post([FromUri]string blobName)
{
    var content = await Request.Content.ReadAsByteArrayAsync();

    var blob = this.container.GetBlockBlobReference(blobName);
    blob.Properties.ContentType = Request.Content.Headers.ContentType.MediaType;
    await blob.UploadFromByteArrayAsync(content, 0, content.Length);

    return Created(Request.RequestUri, "");
}

byte[] のデータを引数ではなく、Request.Content プロパティから ReadAsByteArrayAsync メソッドで受け取る形に変更したところ、無事にアップロードできました。

これまでは、POSTメソッドの Body で string や int などのプリミティブ型や独自のクラス型を JSON でやり取りする分には問題ありませんでした。何故 byte[] や Stream のような型だとうまくいかないのかの原因までは分からずにスッキリしなかったので、また時間が取れたら確認してみようと思います。

<追記>

原因が分かったので、こちらの記事にまとめました。

<追記>

OWIN で ASP.NET Web API を Windows Azure Active Directory 認証する

前回の投稿では OWIN でマルチホストしましたが、今回はミドルウェアの Windows Azure Active Directory(WAAD)認証ライブラリを利用して、ASP.NET Web API を OAuth 2.0 認証してみます。元ネタはこちらですが、Visual Studio のテンプレート機能を使わずに、手動で構築します。

また、ネイティブ クライアント アプリからの認証では、Active
Directory Authentication Library(ADAL)がいい感じなので、使ってみました。ADAL は、WAAD 向けの認証ライブラリで、以前は Windows Azure Authentication Library として提供されていたライブラリが名称変更されています。

Web API アプリを作成

Web アプリのテンプレートから Empty を選択し、Web API のみチェックして WebApiApp プロジェクトを新規作成します。GETで”api/test”にリクエストすると、ユーザー名を返すシンプルな Web API を追加し、忘れずに Authorize 属性を付けます。デバッグ実行し、レスポンスに 401 が返ることを確認しつつ、localhost のポート番号をメモしておきます。

[Authorize]
public class TestController : ApiController
{
    public string Get()
    {
        return base.User.Identity.Name;
    }
}

Web API アプリを WAAD に登録

ポータルから WAAD に、Web API アプリを追加します。ここでは、WAAD のドメイン名を「example.onmicrosoft.com」とします。新規に WAAD を作成する場合、一度作成すると削除できないようなので、ご注意ください。ウィザード画面で必要な情報を入力します。

add-webapp

ここでは、以下のように設定しました。

  • NAME:WebApiApp
  • SIGN-ON URL:http://localhost:61249/
  • APP ID URI:https://example.onmicrosoft.com/WebApiApp
  • Directory access:SINGLE SIGN-ON

Web API アプリに WAAD 認証を追加

WebApiApp プロジェクトに NuGet パッケージをインストールします。

  • Install-package Microsoft.Owin.Host.SystemWeb
  • Install-package Microsoft.Owin.Security.ActiveDirectory

OWIN パイプラインに WAAD 認証のライブラリを追加します。Audience には、ポータルに登録した WebApiApp の APP ID URI を設定します。

[assembly: OwinStartup(typeof(WebApiApp.Startup))]
namespace WebApiApp
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.UseWindowsAzureActiveDirectoryBearerAuthentication(
                new WindowsAzureActiveDirectoryBearerAuthenticationOptions
                {
                    Audience = "https://example.onmicrosoft.com/WebApiApp",
                    Tenant = "example.onmicrosoft.com"
                });
        }
    }
}

ネイティブ クライアント アプリを WAAD に登録

ポータルから WAAD に、ネイティブ クライアント アプリを追加します。ウィザード画面で必要な情報を入力します。

add-wpfapp

ここでは、以下のように設定しました。

  • NAME:WpfApp
  • REDIRECT URI:https://example.onmicrosoft.com/WpfApp

WpfApp を登録後、さきほど登録した WebApiApp との関連付けを行っておきます。

webapis

ネイティブ クライアント アプリを作成

WPFアプリとして、WpfApp プロジェクトを新規作成します。ネイティブ クライアント アプリの認証では、Webブラウザコントロールを利用した面倒な実装が必要となるのですが、Active Directory Authentication Libraryを使うと、このあたりの実装をライブラリが行ってくれます。WpfApp プロジェクトに NuGet パッケージをインストールします。

  • Install-package Microsoft.IdentityModel.Clients.ActiveDirectory
  • Install-Package Microsoft.AspNet.WebApi.Client

ボタンクリックイベントで WAAD 認証して、WebAPI を呼び出すシンプルなアプリです。9行目のコードで ADAL が認証してくれますので、取得した Bearer トークンをHTTPヘッダーに設定すれば OK です。WpfApp の Client ID は、ポータルから取得できます。

private async void Button_Click(object sender, RoutedEventArgs e)
{
    var authority = "https://login.windows.net/example.onmicrosoft.com";
    var appId = "https://example.onmicrosoft.com/WebApiApp";                // WebApiApp の APP ID URI
    var clientId = "c3d3af4a-1682-4e49-abd8-4a8e580cebf1";                   // WpfApp の Client ID
    var redirectUri = new Uri("https://example.onmicrosoft.com/WpfApp"); // WpfApp の REDIRECT URI

    // Windows Azure Active Directory 認証
    var authRes = new AuthenticationContext(authority).AcquireToken(appId, clientId, redirectUri);

    // 取得したトークンで、Web API を呼び出し
    var client = new HttpClient();
    client.DefaultRequestHeaders.Add(HttpRequestHeader.Authorization.ToString(), authRes.CreateAuthorizationHeader());
    var result = await client.GetStringAsync("http://localhost:61249/api/test");
    this.textBlock1.Text = result;
}

実行結果

まず、ネイティブ クライアント アプリからサインインするユーザーを、ポータルから登録しておきます。ここでは、WAAD のユーザー名を「tony@example.onmicrosoft.com」とします。

WpfApp のボタンをクリックすると、サインイン画面が表示され、認証に成功すると WebAPI から結果を取得できます。

signin

wpfresult

まとめ

クラウドで Web API を公開するなら、セキュリティは避けては通れない道です。OWIN のミドルウェアで提供されるフレームワークやライブラリを自由に組み合わせて、Web アプリを構築できる時代が始まっているのだと実感しました。

OWIN で ASP.NET Web API をマルチホストする

OWIN を少し理解できたので、ASP.NET Web API をマルチホストしてみました。コンソールアプリ、IIS の Webアプリ、Windows Azure の WebRole の3つでホスティングします。

ソリューション構成は、こんな感じです。

solution

Web API のロジックをクラスライブラリにする

WebApiLibrary プロジェクトに NuGet パッケージをインストールします。

  • Install-Package Microsoft.AspNet.WebApi.Owin

GETで”api/test”にリクエストすると、「Hello OWIN !」を返すシンプルな Web API です。

public class TestController : ApiController
{
    public string Get()
    {
        return "Hello OWIN !";
    }
}
public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.MapHttpAttributeRoutes();
        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
    }
}

コンソールアプリでホストする

ConsoleApplication プロジェクトに NuGet パッケージをインストールします。

  • Install-Package Microsoft.AspNet.WebApi.Owin
  • Install-Package Microsoft.Owin.Host.HttpListener
  • Install-Package Microsoft.Owin.Hosting

OWIN パイプラインに Web API を追加します。

[assembly: OwinStartup(typeof(ConsoleApplication.Startup))]
namespace ConsoleApplication
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            var config = new HttpConfiguration();
            WebApiConfig.Register(config);
            app.UseWebApi(config);
        }
    }
}
class Program
{
    static void Main(string[] args)
    {
        using (WebApp.Start("http://localhost:8080/"))
        {
            Console.WriteLine("Press Enter to quit.");
            Console.ReadLine();
        }
    }
}

IIS の Webアプリでホストする

空のWebApplication プロジェクトに NuGet パッケージをインストールします。

  • Install-Package Microsoft.AspNet.WebApi.Owin
  • Install-package Microsoft.Owin.Host.SystemWeb

OWIN パイプラインに Web API を追加します。

[assembly: OwinStartup(typeof(WebApplication.Startup))]
namespace WebApplication
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            var config = new HttpConfiguration();
            WebApiConfig.Register(config);
            app.UseWebApi(config);
        }
    }
}

Windows Azure の WebRole でホストする

上記の WebApplication と同じです。

実行結果

Google Chrome アプリの Postman で実行結果を確認してみます。

result

コンソールアプリ以外のURLでも、同じ結果が返されます。

まとめ

OWIN を利用することで、何でホストしても画一的な実装で書けるのは、大きなメリットです。サーバーとアプリを抽象化するだけでなく、ミドルウェアとして機能を追加できるところも面白いです。Web API 以外では、SignalR や Nancy のほかに、認証系のライブラリもありますし、第3のホストライブラリである Helios にも注目したいです。これからの One ASP.NET を語るうえで、OWIN は欠かせない技術であることは間違いなく、今後の進化に期待が高まります。