有没有办法在ServiceStack中注入对F#Option类型的支持?

时间:2012-10-11 01:15:55

标签: f# servicestack

以下更新...

我最近开始尝试使用F#中的ServiceStack,所以很自然地我开始使用porting the Hello World sample

open ServiceStack.ServiceHost
open ServiceStack.ServiceInterface
open ServiceStack.WebHost.Endpoints

[<CLIMutable; Route("/hello"); Route("/hello/{Name}")>]
type Hello = { Name : string }

[<CLIMutable>]
type HelloResponse = { Result : string }

type HelloService() =
    inherit Service()

    member x.Any(req:Hello) =
        box { Result = sprintf "Hello, %s!" req.Name }

type HelloAppHost() =
    inherit AppHostBase("Hello Web Services", typeof<HelloService>.Assembly)
    override x.Configure container = ()

type Global() =
    inherit System.Web.HttpApplication()

    member x.Application_Start() =
        let appHost = new HelloAppHost()
        appHost.Init()

这很有效。它非常简洁,易于使用,我喜欢它。但是,我注意到样本中定义的路由允许不包括Name参数。当然,Hello, !看起来像输出一样蹩脚。我可以使用String.IsNullOrEmpty,但在F#中通过使用Option类型明确表示可选的内容是惯用的。所以我相应地修改了我的Hello类型,看看会发生什么:

[<CLIMutable; Route("/hello"); Route("/hello/{Name}")>]
type Hello = { Name : string option }

一旦我这样做,F#类型系统迫使我处理Name可能没有值的事实,所以我将HelloService更改为此以获得编译的所有内容:< / p>

type HelloService() =
    inherit Service()

    member x.Any(req:Hello) =
        box { Result = 
                match req.Name with
                | Some name -> sprintf "Hello, %s!" name
                | None -> "Hello!" }

当我不提供Name参数时,这会编译并运行完美。但是,当我提供名字时......

  

KeyValueDataContractDeserializer:转换为类型时出错:类型   定义应以'{'开头,期待序列化类型   'FSharpOption`1',字符串以:World

开头

这当然不是一个完全的惊喜,但它让我想到了我的问题:

编写一个可以将T类型的实例包装到类型FSharpOption<T>的实例中的函数对我来说是微不足道的。 ServiceStack中是否有任何钩子可以让我在反序列化期间提供这样的功能?我看了,但我找不到任何钩子,我希望我只是在找错了地方

这对于F#的使用比起初看起来更重要,因为默认情况下,F#中定义的类不允许为null。因此,将一个类作为另一个类的可选属性的唯一(令人满意的,非hacky)方法是,您猜对了Option类型。


更新

我能够通过进行以下更改来实现这一目标:

在ServiceStack源代码中,我将此类型公开: ServiceStack.Text.Common.ParseFactoryDelegate

......我也公开了这个领域: ServiceStack.Text.Jsv.JsvReader.ParseFnCache

将这两件事公开后,我能够在F#中编写此代码来修改ParseFnCache字典。我必须在创建AppHost实例之前运行此代码 - 如果我在AppHost的Configure方法中运行它,它就无法运行。

JsvReader.ParseFnCache.[typeof<Option<string>>] <- 
    ParseFactoryDelegate(fun () -> 
        ParseStringDelegate(fun s -> (if String.IsNullOrEmpty s then None else Some s) |> box))

这适用于我原来的测试用例,但除了我必须对ServiceStack的内部进行细微更改这一事实之外,它很糟糕,因为我必须为我希望能够包装的每种类型执行一次Option<T>

如果我能以通用的方式做到这一点会更好。在C#术语中,如果我可以向ServiceStack提供Func<T, Option<T>>和ServiceStack,当反序列化其泛型类型定义与我的函数的返回类型匹配的属性时,反序列化T然后将是真棒。将结果传递给我的函数。

这样的东西会非常方便,但如果它实际上是ServiceStack的一部分而不是我可能在其他地方破坏某些东西的丑陋黑客,那么我可以使用每次包装类型的方法。

2 个答案:

答案 0 :(得分:4)

因此ServiceStack中有几个可扩展点,在框架级别,您可以添加自己的Custom Request Binder,这允许您提供自己使用的模型绑定器,例如:

base.RequestBinders.Add(typeof(Hello), httpReq => {
    var requestDto = ...;
    return requestDto;
});

但是你需要自己处理不同内容类型的模型绑定,请参阅CreateContentTypeRequest了解ServiceStack的工作方式。

然后在JSON Serializer级别有钩子,例如:

JsConfig<Hello>.OnDeserializedFn = dto => newDto;

这允许您修改返回类型的实例,但它仍然需要是相同的类型,但它看起来像F#选项修改器更改了类型的结构定义?

但我愿意添加任何可以使ServiceStack更适合F#的钩子。 使用选项将普通Hello类型一般转换为F#Hello类型的代码是什么样的?

答案 1 :(得分:1)

我唯一能想到的是用您自己的类型替换选项类型,具有从stringmyOption的隐式转换,以及您需要的任何其他类型。

不是那么好,但可行。您的类型可能还需要序列化。

type myOption = 
   | None 
   | Some of string
   static  member public op_Implicit (s:string) = if s <> null then Some s else None
   member public this.Value = match this with 
                              | Some s -> s
                              | _      -> null
   member this.Opt = match this with 
                     | Some s -> Option.Some s
                     | None   -> Option.None 

您的记录类型将是

[<CLIMutable>]
type Hello = 
   { Name : myOption }

另一方面,ServiceStack 开源的,所以也许可以在那里做点什么。