F#+WCFでRESTfulなサービスをこしらえる・自分で定義した判別共用体で遊ぶ
自分で定義した判別共用体も使いたいさ
昨日はoption型を扱える事を確認しましたが、optionだけだと物足りないさ。
自分で定義した判別共用体も使いたいさ。
そりゃ使えないよりは使えた方がいい。役に立つか否かは別として、使わない機能でも盛りだくさんなら嬉しくなるものです。
今日のコードはこんな感じです。
namespace FSharpWCF open System open System.Runtime.Serialization open System.ServiceModel open System.ServiceModel.Web type Card = | Jack | Queen | King | Num of string [<ServiceContract>] type ICardService = interface [<OperationContract>] [<WebGet(UriTemplate="card/{id}")>] abstract GetCard : id:string -> Card end type CardService() = interface ICardService with member this.GetCard id = match id.ToLower() with | "jack" -> Jack | "queen" -> Queen | "king" -> King | _ -> Num "1"
トランプの数字(1〜13)を("1"〜"10", Jack, Queen, King)と定義した判別共用体Cardを用います。
(1から10までのNum をstringにしたのは僕が楽チンをするためです)
configファイルは今までのエントリを見て適当に編集してください。
GetCardの返り値がCard型ですが、このCard型はDataContract属性指定をしていません。
この状態で動くでしょうか。
http://localhost:8080/FSharpWCF/CardService/card/jack
{"_tag":0}
おお、返って来ました。こうなるともちろん、
http://localhost:8080/FSharpWCF/CardService/card/queen
{"_tag":1}
http://localhost:8080/FSharpWCF/CardService/card/king
{"_tag":2}
となります。
ですが、
http://localhost:8080/FSharpWCF/CardService/card/1
にアクセスすると(Num 1 を返すパス)
要求エラー 要求の処理中にサーバーでエラーが発生しました。詳細については、サーバー ログを参照してください。
エラーが返って来ました。
C#で作ったクライアントでGetCard("1")にアクセスすると、このような例外が発生します。
GetCardの返り値はCard型なので、C#の方から定義を覗いてみると、こんなソースがこんにちわ!します。
#region アセンブリ {620DF1F0-5FAF-4290-9850-EC9E5C93E634}, v4.0.30319 // {620DF1F0-5FAF-4290-9850-EC9E5C93E634} #endregion using Microsoft.FSharp.Core; using System; using System.Collections; using System.Diagnostics; using System.Runtime.CompilerServices; namespace FSharpWCF { [Serializable] [DebuggerDisplay("{__DebugDisplay(),nq}")] [CompilationMapping(SourceConstructFlags.SumType)] public class Card : IEquatable<Card>, IStructuralEquatable, IComparable<Card>, IComparable, IStructuralComparable { [CompilerGenerated] [DebuggerNonUserCode] [DebuggerBrowsable(DebuggerBrowsableState.Never)] public bool IsJack {get; } [CompilerGenerated] [DebuggerNonUserCode] [DebuggerBrowsable(DebuggerBrowsableState.Never)] public bool IsKing {get; } [CompilerGenerated] [DebuggerNonUserCode] [DebuggerBrowsable(DebuggerBrowsableState.Never)] public bool IsNum {get; } [CompilerGenerated] [DebuggerNonUserCode] [DebuggerBrowsable(DebuggerBrowsableState.Never)] public bool IsQueen {get; } [CompilerGenerated] [DebuggerNonUserCode] [DebuggerBrowsable(DebuggerBrowsableState.Never)] public static Card Jack {get; } [CompilerGenerated] [DebuggerNonUserCode] [DebuggerBrowsable(DebuggerBrowsableState.Never)] public static Card King {get; } [CompilerGenerated] [DebuggerNonUserCode] [DebuggerBrowsable(DebuggerBrowsableState.Never)] public static Card Queen {get; } [CompilerGenerated] [DebuggerNonUserCode] [DebuggerBrowsable(DebuggerBrowsableState.Never)] public int Tag {get; } [CompilerGenerated] public override sealed int CompareTo(Card obj); [CompilerGenerated] public override sealed int CompareTo(object obj); [CompilerGenerated] public override sealed int CompareTo(object obj, IComparer comp); [CompilerGenerated] public override sealed bool Equals(Card obj); [CompilerGenerated] public override sealed bool Equals(object obj); [CompilerGenerated] public override sealed bool Equals(object obj, IEqualityComparer comp); [CompilerGenerated] public override sealed int GetHashCode(); [CompilerGenerated] public override sealed int GetHashCode(IEqualityComparer comp); [CompilationMapping(SourceConstructFlags.UnionCase, 3)] public static Card NewNum(string item); [Serializable] [DebuggerTypeProxy(typeof(Num@DebugTypeProxy))] [DebuggerDisplay("{__DebugDisplay(),nq}")] public class Num : Card { [CompilationMapping(SourceConstructFlags.Field, 3, 0)] [CompilerGenerated] [DebuggerNonUserCode] public string Item {get; } } public static class Tags { public const int Jack = 0; public const int King = 2; public const int Num = 3; public const int Queen = 1; } } }
コードからJack, Queen, Kingがそれぞれ1,2,3に対応している事は分かりました。
型指定のないJackなどがうまく言っているのは内部でintに変換されているからでしょうね。
それならenumと変わらんのや!!!!
エラーが起こるぶんenumより悪くなっとるんや!!!!
でも Num(1) とかも扱いたいです。
やっぱりDataContract指定してないのがいけないのかな?と思ってこんな事をしてみましたが、
(* 思ったように動かないし惜しくもないコード *) [<DataContract>] type Card = | [<DataMember>] Jack | [<DataMember>] Queen | [<DataMember>] King | [<DataMember>] Num of string
ビックリするほど全然だめでした。
Getもダメなら当然Postもダメだ
Responseとしての判別共用体がエラーを起こす可能性があることは確認出来たので、じゃあRequestならどうかなーって思いました。
結果から言うと一緒でした。
準備としてこんな感じでPOSTオペレーションを実装しました。
[<ServiceContract>] type ICardService = interface [<OperationContract>] [<WebGet(UriTemplate="card/{id}")>] abstract GetCard : id:string -> Card [<OperationContract>] [<WebInvoke(Method="POST", UriTemplate="card")>] abstract PostCard : data:Card -> string end type CardService() = interface ICardService with member this.GetCard id = match id.ToLower() with | "jack" -> Jack | "queen" -> Queen | "king" -> King | _ -> Num "1" member this.PostCard data = match data with | Jack -> "jack" | _ -> "other"
最初、abstract PostCard : data:Card -> string を abstract PostCard : Card -> string と書いていてしばらくはまりました。
Request のメッセージには名前がいるってことでしょうか。
そしてC#クライアントをこんな感じで実装します。
private void button1_Click(object sender, EventArgs e) { var baseuri = new Uri("http://localhost:8080/FSharpWCF/CardService/"); using (var f = new WebChannelFactory<ICardService>(new WebHttpBinding(), baseuri)) { var service = f.CreateChannel(); var message = service.PostCard(Card.Jack); Console.WriteLine(message); var message2 = service.PostCard(Card.NewNum("2")); Console.WriteLine(message2); } }
大方の予想通り、service.PostCard(Card.Jack); はうまく動作します。やっぱり内部で暗黙的にintになっているのでしょう。
さあ、service.PostCard(Card.NewNum("2")); がどうなるかと言うと、
「シリアライズできねーっつーの!」と怒られました。これも何となく予想はついていましたがやっぱり予想通りです。
判別共用体は諦めるしかないのか?
「じゃあじゃあ判別共用体は使えないの!?」と世の判別共用体大好きっこ達は大変焦る事でしょう。
今の問題は「サービス外部とのメッセージに判別共用体を含めるとエラーが起こる」という事であって、内部で使う分には問題ないでしょう。
そうすると考えられるのは「外部とやり取りするメッセージコントラクト(データコントラクト)と判別共用体とを相互変換する」ということになります。
色々難しい事は考えずに実装するとこんな感じでしょうか。
module CardUtil = type Card = |Jack | Queen | King | Num of string with member this.ToStringExt = match this with | Num(x) -> x | Jack -> "Jack"; | Queen -> "Queen" | King -> "King" let ToCard x = match x with | "Jack" -> Jack | "Queen" -> Queen | "King" -> King | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "10" -> Num(x) | _ -> raise (WebFaultException(System.Net.HttpStatusCode.NotFound)) [<DataContract>] type CardData(n) = let mutable card = CardUtil.ToCard(n) [<DataMember(Name = "Card")>] member x.myCard with get() = card.ToStringExt and set(value) = card <- CardUtil.ToCard(value) [<ServiceContract>] type ICardService = interface [<OperationContract>] [<WebGet(UriTemplate="card/{id}")>] abstract GetCard : id:string -> CardData [<OperationContract>] [<WebInvoke(Method="POST", UriTemplate="card")>] abstract PostCard : data:CardData -> string end type CardService() = interface ICardService with member this.GetCard id = new CardData(id) member this.PostCard data = data.myCard