ラベル DataService の投稿を表示しています。 すべての投稿を表示
ラベル DataService の投稿を表示しています。 すべての投稿を表示

2011年4月7日木曜日

ブチザッキさんへ

お元気ですか。ボクは元気です。

OData and Authentication – Part 7 – Forms Authentication - WCF Data Services Team Blog - Site Home - MSDN Blogs

WCF Data Servicesをプロキシクラス経由で呼び出すときにForm認証してやんよ、の件についてなんですけども。

WCF Data Services で フォーム認証を利用する « ブチザッキ

ナニをナニする系で、ナニしてみたのでご覧いただけたら幸いです。

Custom Membership Providers - Task Manager - CodeProject

プロキシからの呼び出しに介入し、別リクエストでAuthentication_JSON_AppService.axdを呼び出すことでForm認証し、レスポンスからCookieを横取りすることで、認証チケット再利用する部分を、FormsAuthenticationクラスを使って認証チケットを発行してそちらを使うことで実現してみました。

とはいえ、まったく楽チン度と便利さが上がらなかったので、ボツにしようかと思ったのですが、それはそれで少し寂しくなっちゃったので、公開レターの形式にしてみました。

WCF DataServicesをホストするサーバーサイドプロジェクトをASP.NET MVCにして、HomeControllerに以下のアクションメソッドを追加します。

[HttpPost]
public ContentResult FormAuth(string userName, string password)
{
  if (Membership.ValidateUser(userName, password))
  {
    var authTicket = new FormsAuthenticationTicket(
       1,
       userName,
       DateTime.Now,
       DateTime.Now.AddMinutes(30),
       false,
       userName);
    var cookie = 
       FormsAuthentication.FormsCookieName + "=" +
       FormsAuthentication.Encrypt(authTicket) + "; path=" +
       FormsAuthentication.FormsCookiePath + "; " +
       (!FormsAuthentication.RequireSSL
         ? "HttpOnly" : "");
    return Content(cookie);
  }

  return null;
}

続いて、クライアントサイドのプロキシクラス用に実装しているGetCookieの代わりとなる部分を以下のように。

string GetTicket(string userName, string password)
{
  string loginUri = string.Format("{0}/{1}/{2}",
      ServiceUri,
      "Home",
      "FormAuth");
  WebRequest request = HttpWebRequest.Create(loginUri);
  request.ContentType = "application/x-www-form-urlencoded";
  request.Method = "POST";

  string authBody = String.Format(
      "userName={0}&password={1}",
      userName,
      password);
  request.ContentLength = authBody.Length;

  using (StreamWriter w = new StreamWriter(request.GetRequestStream()))
  {
      w.Write(authBody);
      w.Close();

      WebResponse res = request.GetResponse();
      var body = new StreamReader(res.GetResponseStream()).ReadToEnd();
      if (!string.IsNullOrEmpty(body))
      {
          Cookie = body;
      }
      else
      {
          throw new Exception("Invalid username and password");
      }
  }

  return Cookie;
}

GetCookie(userName,password)の部分を上記のGetTicket(userName,password)にすることで、Cookie横取りではなくなりますね。

formauth

だから言ったじゃないですか!ボツにしたかったって...。

2011年3月5日土曜日

ぐっと来てますか?

http://guttokita.cc

@jsakamotoさんからとてもナイスなフィードバックをTwitterでもらったので早速実装しました。

ODataによるデータ出力

http://guttokita.cc/feed.svc

上記URLでOData形式でデータを出力しました。後はUIを組み込めば過去のつぶやきを見放題!ぐへへ。UIをどうやってつくるかは考え中。

guttokitacc1

guttokitacc2

購読しやすい形式にはマッピングしてないので、購読用途には向いてないです。

OData と AtomPub - WCF Data Services を使用した AtomPub サーバーの構築

↑この辺で直接購読しやすくマッピングをしてみようかとも考えて動かしてみたんですが、なんか用途が違う気がしてきたのでやめました。購読用Feedは需要があれば別途用意します。

つぶやいた内容を直接見れる(パーマリンク化)

続いて、つぶやき内容をブラウザから直接見れるようにするパーマリンク化。これまではiPhoneでの利用を想定してたので、トップページからリンクをたどって見る方法しかなかったですが、ブラウザにURLを入れて直接アクセスできるようにしました。ODataと合わせて使うのがいいかも。

IMG_0171 IMG_0172 IMG_0173 IMG_0174 IMG_0175

トップからたどっていくと、つぶやきページの一番下にPermalinkというリンクがあるので、そこに指定されてるURLがパーマリンクです。

permalink1

permalink2

PCで見ると↑こんな感じです。

上記の場合 http://guttokita.cc/Tweets/27 がパーマリンク。

技術的な話

そもそもEF CodeFirstでデータアクセス層を実装してるので、すんなりとはODataになってくれませんでした。ちょびっとだけ手を加える必要があります。というのも、CodeFirstのデフォルトの挙動がプロキシクラスを生成するので、WCF Data Servicesから見たときにIQueryableに見えないといわれて怒られます。

odata

サーバーで要求の処理中にエラーが発生しました。例外メッセージは 'データ コンテキスト型 'SiteContext' に、要素型がエンティティ型でないトップレベルの IQueryable プロパティ 'Accounts' があります。IQueryable プロパティがエンティティ型であることを確認するか、データ コンテキスト型に対して IgnoreProperties 属性を指定して、このプロパティを無視してください。

EF CTP4 Tips & Tricks: WCF Data Service on DbContext « RoMiller.com

CTP4ですがここを参考にObjectContextのオプションでプロキシを生成しないように抑制しておく必要があります。

CTP5での書き方は↓こう。

protected override ObjectContext CreateDataSource()
{
  var db = new SiteContext();
  ((IObjectContextAdapter)db).ObjectContext.ContextOptions.ProxyCreationEnabled = false;
  return ((IObjectContextAdapter)db).ObjectContext;
}

SiteContextっていう名前でDbContextを作ってます。

リレーショナルなテーブルだとこれでもちょっと使いにくくて、フラットで十分じゃないかと思うわけです。であれば、ViewをDBに定義してそれをFeedにしたほうがいいと思ったんですが、CodeFirstでViewってどうするんでしょうね。どうやら普通にエンティティクラス書いてDbSetでDbContextに定義すればいいみたい。でも、それだとテーブル作られちゃうじゃないですか。テーブルをつくるな宣言ってどうするんでしょうね。よくわかんないな~。NotMappedはテーブルには適用されない。しょうがないのでInitializerのSeedでDrop TableとCreate Viewをするという暴挙にでました。何かしら方法があるといいんだけど。

ちなみにODataということは、いろんなフィルターや射影し直せるというわけで、自分のつぶやきの最新3件取得とかも簡単です。

http://guttokita.cc/feed.svc/Tweets?$filter=UserName eq 'takepara'&$orderby=CreateAt desc&$top=3

odata3

うっひょ~い!

URLに指定できるクエリオプションは以下のページをどうぞ

データ サービス リソースへのアクセス (WCF Data Services)

2010年10月17日日曜日

カペタ

カペタが面白いとクマさんが言ってたので読んでみたら(全然グルメ漫画とかじゃなく予想外だったけど)、これがかなり面白くてびっくりした。

サルベージすらしてなくてすいません...。

そろそろ真面目にData Services&PowerPivot使ってデータ集計できるようにしておくかと思い、どりゃっとセットアップしてたんですが、100万レコード超えたりとかしてるでかいテーブルに対して、全然レスポンスが帰ってこないくて困った。そんな方、他にもいないですか?あ、ADO.NET Data Servicesのほうです。.NET3.5のほうです。WCFじゃないほうです。

動作的にあからさまにタイムアウトにもかかわらず、レスポンスストリームに何も出力されないっていう気持ち悪い状況なんですよね。コマンドラインでcurl使ってリクエストしてもレスポンスがないぜ!って怒られる。んで、どのレイヤーでタイムアウトしてるのかな~って思うわけじゃないですか。

なんだかんだ言ってもWCFなんでbindingら辺が怪しいのかな~と思っていろいろセットしてみたんだけど、よくわからん。

ADO.NET Data Services, Entity Framework und SQL/HTTP Timeouts | Marco Scheel aka GeekDotNet

↑ここにいろいろ書いてるのを見て試したんですけど、どれもイマイチ。CreateDataSourceのCommandTimeout設定なんてちょっと素敵な感じですよね。いろんなレイヤー気にするのもな~、と思って思い出したんですよね。

ダウンロードの詳細 : Windows 7 および Windows Server 2008 R2 用 .NET Framework 3.5 SP1 の ADO.NET データ サービス更新プログラム

これ。そーか、と。サーバーサイドページングを有効にしておけば、まるっとすべて解決なんじゃんね。データ件数をそもそも絞ってしまえばどのレイヤででもタイムアウトしないじゃんよ!ってことですよ。PowerPivotはさ、賢いから自動でページングして全データ抽出してくれるんですよ。

Server Paging in Data Services - WCF Data Services Team Blog - Site Home - MSDN Blogs

InitializeServiceにIDataServiceConfigurationでそのままconfigを受け取ってると見えないけど、DataServiceConfirugarionクラスとして受け取るか、asで型指定してしまえば見えるようになるじゃないですか。

var v2 = config as DataServiceConfiguration;
if (v2 != null)
{
 v2.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
 v2.SetEntitySetPageSize("*", 100);
}

SetEntittSetPageSizeですべてのテーブルを100件ページング指定してしまえばいいね!

2010年7月25日日曜日

Data ServicesのPOCO利用

ADO.NET Dataservice/WCF Data ServicesってそれぞれEntity Frameworkを使う場合とPOCOを使う場合と、それぞれを混在させたい場合とあるような気がするけどどうなんでしょう。Read Onlyなら普通に混在できていいんじゃないかと思うけど簡単に出来たりしないのかな~。

データ モデル (ADO.NET Data Services フレームワーク)

試しにNorthwindのProducts/Categories/Order/Order Detailsでやってみた。

まずはVS2008を使ってADO.NET Entity Data Modelを以下のように作成。

astoria1

続いてADO.NET Data Serviceを作成。

 

public class DataEF : DataService<NorthwindEntities>
{
  public static void InitializeService(IDataServiceConfiguration config)
  {
    config.UseVerboseErrors = true; // 追加

    config.SetEntitySetAccessRule("*", 
EntitySetRights.AllRead); config.SetServiceOperationAccessRule("*",
ServiceOperationRights.AllRead); } }

astoria2

普通ですね。

これに以下のようなPOCOクラスも混在させたい。

public class DailyOrderSummary
{
  public int Year { get; set; }
  public int Month { get; set; }
  public int Day { get; set; }
  public decimal Amount { get; set; }
}

このモデルを返すのは以下のようなクエリです。

from o in Context.Orders
where o.OrderDate.HasValue
group o by new 
{ 
  o.OrderDate.Value.Year, 
  o.OrderDate.Value.Month, 
  o.OrderDate.Value.Day 
} into daily
select new DailyOrderSummary
{
  Year = daily.Key.Year,
  Month = daily.Key.Month,
  Day = daily.Key.Day,
  Amount = daily.Sum(d => d.Order_Details
                           .Sum(od => (od.UnitPrice * 
                                       od.Quantity)))
};

がしかし、これを利用するためにDataEfクラスにIQueryableなメソッドを追加してもどうにもこうにも機能してくれないんですよ。Object Service層にはそんなのいないからってことでしょうね。んじゃどうしましょうか。

An ADO.NET Data Services Primer - O'Reilly Answers

ここにあるように一旦POCOだけで動かせる状態にして、そのクラスをプロキシとしてDataService<T>を作ってみます。

public class EfDataModels
{
  public NorthwindEntities Context { get; private set; }

  public EfDataModels()
  {
    Context = new NorthwindEntities();
  }

  public IQueryable<Categories> Categories
  {
    get { return Context.Categories.AsQueryable(); }
  }

  public IQueryable<Products> Products
  {
    get { return Context.Products.AsQueryable(); }
  }

  public IQueryable<Orders> Orders
  {
    get { return Context.Orders.AsQueryable(); }
  }

  public IQueryable<Order_Details> OrderDetails
  {
    get { return Context.Order_Details.AsQueryable(); }
  }

  public IQueryable<DailyOrderSummary> DailyOrderSummaries
  {
    get
    {
      return 
         from o in Context.Orders
         where o.OrderDate.HasValue
         group o by new
                      {
                        o.OrderDate.Value.Year, 
                o.OrderDate.Value.Month, 
                o.OrderDate.Value.Day
                      }
           into daily
           select new DailyOrderSummary
           {
             Year = daily.Key.Year,
             Month = daily.Key.Month,
             Day = daily.Key.Day,
             Amount = daily.Sum(d => d.Order_Details
                        .Sum(od => (od.UnitPrice * 
                          od.Quantity)))
           };
    }
  }
}

これを使ってDataEfを定義することにしてみたけど、これはこれでやっぱりエラー。

astoria3

でもね、上記EfDataModelクラスからEntity Modelで定義したものを除外してDailyOrderSummariesだけにするとうまく行くわけですよ。

astoria4 astoria5

となると、違いはDataService<T>に指定するクラスじゃなくて、モデルの基底クラスなのかなとなりますね。なりませんかね。ソースかドキュメントでもあれば自信を持って基底クラスがEntityObjectのものがいるとPOCO混在出来ないと言えるところなんですが、そのへんよくわからないです。教えてエライ人!

で、それならそれでLINQ to SQLのDataContextを使うという方法もありますね。こっちは基底クラスがいない(objectな)わけで。最初に戻ってLINQ to SQLのDataContextの作成からやり直してみます。

astoria6

EFと一緒です。あ、そうそうちなみにPOCOの場合、キー項目がわからないので、クラスにDataServiceKey属性を付けるのを忘れずに。

ADO.NET Data Services and LINQ-to-SQL - Sergey Zwezdin

なので、先のDailyOrderSummaryクラスは以下のように属性を付けておく必要あり。

[DataServiceKey("Year", "Month", "Day")]
public class DailyOrderSummary
{
  public int Year { get; set; }
  public int Month { get; set; }
  public int Day { get; set; }
  public decimal Amount { get; set; }
}

DataContextに追加したモデルたちもpartialなので同じように定義します。

[DataServiceKey("OrderID")]
partial class Order
{
}

[DataServiceKey("OrderID","ProductID")]
partial class Order_Detail
{
}

[DataServiceKey("ProductID")]
partial class Product
{
}

[DataServiceKey("CategoryID")]
partial class Category
{
}

これをプロキシとなるDataModelsにまるっといれこんで以下のようなクラスができました。

public class DataModels
{
  public DataL2SDataContext Context { get; private set; }

  public DataModels()
  {
    Context = new DataL2SDataContext();
  }

  public IQueryable<Category> Categories
  {
    get { return Context.Categories; }
  }

  public IQueryable<Product> Products
  {
    get { return Context.Products; }
  }

  public IQueryable<Order> Orders
  {
    get { return Context.Orders; }
  }

  public IQueryable<Order_Detail> OrderDetails
  {
    get { return Context.Order_Details; }
  }

  public IQueryable<DailyOrderSummary> DailyOrderSummaries
  {
    get
    {
      return 省略
    }
  }
}

なんか無駄にコードの量が増えてきて面倒になってきた...。

astoria7 astoria8 astoria9

ナイス!

ここまで来たら、VS2010でADO.NET POCO Entity Generatorを使って同じようにやりたくなるってもんですよね。

ADO.NET C# POCO Entity Generator

このジェネレータはT4になってて、EDMXファイルを指定すると、ObjectContex派生のコンテキストクラスと、エンティティPOCOクラスを生成してくれます。

もちろんPOCOエンティティなのでDataServiceKey属性定義は必要だね。やってみよう!

astoria10 astoria11

おぉ~、いいじゃ~ん。と思ったのもつかの間。

astoria12

ジェネレートしたエンティティを参照すると↑こんなの出てきてちゃんと動かない。なんでじゃ~。出力されたエラーを見てみると...。

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<feed xml:base="http://localhost:29890/Data.svc/" xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" xmlns="http://www.w3.org/2005/Atom">
  <title type="text">Categories</title>
  <id>http://localhost:29890/Data.svc/Categories</id>
  <updated>2010-07-25T06:29:58Z</updated>
  <link rel="self" title="Categories" href="Categories" />
  <m:error>
    <m:code></m:code>
    <m:message xml:lang="ja-JP">内部サーバー エラーです。型 'System.Data.Entity.DynamicProxies.Categories_549968D49334B0D9E524C2FC37C19156947873B86A46A78227AFE184CB25AA68' は複合型またはエンティティ型ではありません。</m:message>
  </m:error>

なんじゃこれ。いろいろ調べてみたらDynamicProxiesを使わないようにすればいいという記述を発見。

ADO.NET C# POCO Entity Generator and Data Services
ObjectContextOptions プロパティ (System.Data.Objects)

というわけで、POCO GeneratorのTTファイルをいじることにしてみます。

VSに生成されてる~.Context.ttファイルを開いて、Constructorsリージョンで隠れてる部分を開いて以下のように変更。

#region Constructors

public <#=code.Escape(container)#>()
  : base(ConnectionString, ContainerName)
{
  ContextOptions.ProxyCreationEnabled = false; 
<#
WriteLazyLoadingEnabled(container);
#>
}

public <#=code.Escape(container)#>(string connectionString)
: base(connectionString, ContainerName)
{
  ContextOptions.ProxyCreationEnabled = false; 
<#
WriteLazyLoadingEnabled(container);
#>
}

public <#=code.Escape(container)#>(EntityConnection connection)
: base(connection, ContainerName)
{
  ContextOptions.ProxyCreationEnabled = false; 
<#
WriteLazyLoadingEnabled(container);
#>
}

#endregion

この状態で動かしてみます。

astoria13

今度は通常のエンティティクラスの参照もうまくいきました。ちなみにttファイルをいじってしまえば、DataServiceKey属性もハナからくっついた状態でエンティティクラスを生成させることも出来ますね。

using System.Data.Services.Common;
<#
    BeginNamespace(namespaceName, code);
    bool entityHasNullableFKs = 
       entity.NavigationProperties
             .Any(np => np.GetDependentProperties()
                          .Any(p=>ef.IsNullable(p)));
#>
[DataServiceKey(<#= string.Join(",",entity.KeyMembers.Select(km=>"\""+km.Name+"\""))#>)]
<#=Accessibility.ForType(entity)#> 
 <#=code.SpaceAfter(code.AbstractOption(entity))#>
 partial class <#=code.Escape(entity)#>
 <#=code.StringBefore(" : ", code.Escape(entity.BaseType))#>
{
<#
    region.Begin("Primitive Properties");

    foreach (EdmProperty edmProperty in 
                           entity.Properties
                                 .Where(p => p.TypeUsage.EdmType is PrimitiveType && 
                                             p.DeclaringType == entity))

太字の1行。

ようはPOCOのみで構成してしまえば、Read OnlyのData Servicesは比較的簡単に公開できるね、っていう話。

なんだけど、本質的にはData ServiceのレイヤでごちゃごちゃせずにDBにViewを定義して、そのViewをそのまま公開するのが本筋だと思うわけです。Viewが集計に必要なRaw状態になってれば、あとはPivotTableなり使ってExcelでよしなにやってしまえよ、というのがセルフBIのスタートラインなんじゃないのかね。賢くないことをしようとすると無駄なことをたくさんしないといけなくなるから、そこんとこちゃんとやりたまえよ。