使用protobuf序列化F#区分联合

时间:2014-07-22 10:24:55

标签: f# protobuf-net

有没有办法让protobuf序列化/反序列化F#的歧视联盟?

我尝试使用protobuf序列化邮件。消息是F#记录和受歧视的联合。

序列化似乎对记录工作正常,但我不能让它与受歧视的工会合作。

在下面的代码中,testMessageA和testMessageB的测试是绿色的。测试testMessageDU为红色。

module ProtoBufSerialization

open FsUnit
open NUnit.Framework

open ProtoBuf

type MessageA = {
  X: string;
  Y: int;
}

type MessageB = {
  A: string;
  B: string;
}

type Message =
| MessageA of MessageA
| MessageB of MessageB

let serialize msg =
  use ms = new System.IO.MemoryStream()
  Serializer.SerializeWithLengthPrefix(ms, msg, PrefixStyle.Fixed32)
  ms.ToArray()

let deserialize<'TMessage> bytes =
  use ms = new System.IO.MemoryStream(buffer=bytes)
  Serializer.DeserializeWithLengthPrefix<'TMessage>(ms, PrefixStyle.Fixed32)

[<Test>]
let testMessageA() =
  let msg = {X="foo"; Y=32}
  msg |> serialize |> deserialize<MessageA> |> should equal msg

[<Test>]
let testMessageB() =
  let msg = {A="bar"; B="baz"}
  msg |> serialize |> deserialize<MessageB> |> should equal msg

[<Test>]
let testMessageDU() =
  let msg = MessageA {X="foo"; Y=32}
  msg |> serialize |> deserialize<Message> |> should equal msg

我尝试在类型Message上添加ProtoInclude和KnownType等不同属性,在MessageA和MessageB类型上添加CLIMutable,但似乎没有任何帮助。

我不希望将我的DU映射到类以使序列化工作...

3 个答案:

答案 0 :(得分:4)

我玩了非常有用的生成输出,看起来基本上一切正常 - 除了 Message.MessageA子类型。这些非常接近工作 - 它们基本上与“自动元组”代码(匹配所有成员的构造函数)相同,除了自动元组当前不适用于子类型。 / p>

认为应该可以调整代码以自动工作,通过扩展自动元组代码在这种情况下工作(我试图想到任何可能的不良副作用那,但我没有看到任何)。我没有具体的时间框架,因为我需要在多个项目和全职日间工作,家庭,志愿者工作和(等等)之间平衡时间。

在短期内,以下C#足以使其发挥作用,但我不认为这将是一个有吸引力的选择:

RuntimeTypeModel.Default[typeof(Message).GetNestedType("MessageA")]
                .Add("item").UseConstructor = false;
RuntimeTypeModel.Default[typeof(Message).GetNestedType("MessageB")]
                .Add("item").UseConstructor = false;

顺便说一下,这里的属性是无益的,应该避免:

| [<ProtoMember(1)>] MessageA of MessageA
| [<ProtoMember(2)>] MessageB of MessageB

如果他们做了什么,他们就会重复<ProtoInclude(n)>的意图。如果更方便在那里指定它们,那可能会很有趣。但我发现真的有趣的是,F#编译器完全忽略了AttributeUsageAttribute[ProtoMember]是:

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field,
    AllowMultiple = false, Inherited = true)]
public class ProtoMemberAttribute {...}

是的,F#编译器明确地将其(非法)粘贴在方法上:

[ProtoMember(1)]
[CompilationMapping(SourceConstructFlags.UnionCase, 0)]
public static ProtoBufTests.Message NewMessageA(ProtoBufTests.MessageA item)

顽皮的F#编译器!

答案 1 :(得分:3)

我使用protobuf-net加注event sourcing DUs并非常感谢json.net v6's seamless support for DUs

我最初希望优先使用protobuf-net的原因是:

  1. 我从来没有证明我正在寻求的性能差距
  2. 我希望在我的邮件合同中能够抵御字段重命名(依赖于通过[<ProtoMember(n)>]解决),这可以通过以下方式减轻:

    • 字段名称别名(即使用属性告诉F#以旧名称编译)
    • 通过在EventXXXV2旁边EventXxx添加同一个DU
    • ,可以使用DU的优势模式匹配版本事件
  3. 我找不到比以前更清洁的方式:

    let registerSerializableDuInModel<'TMessage> (model:RuntimeTypeModel) =
        let baseType = model.[typeof<'TMessage>]
        for case in typeof<'TMessage> |> FSharpType.GetUnionCases do
            let caseType = case.Name |> case.DeclaringType.GetNestedType 
            baseType.AddSubType(1000 + case.Tag, caseType) |> ignore
            let caseTypeModel = model.[caseType]
            caseTypeModel.Add("item").UseConstructor <- false
        baseType.CompileInPlace()
    
    let registerSerializableDu<'TMessage> () = registerSerializableDuInModel<'TMessage> RuntimeTypeModel.Default
    
    registerSerializableDu<Message> ()
    

    解决[<ProtoInclude(100, "ProtoBufTests+Message+MessageA")>]残骸的需要。 (我还在思考F#和protbuf-net改进的混合是否能最好地解决这个问题)

    一个非常重要的区别是[<ProtoContract; CLIMutable>]喷洒不需要(除ProtoIncludeProtoMember之外)。

    代码转储:

    module FunDomain.Tests.ProtobufNetSerialization
    
    open ProtoBuf
    open ProtoBuf.Meta
    
    open Swensen.Unquote
    open Xunit
    
    open System.IO
    open Microsoft.FSharp.Reflection
    
    [<ProtoContract; CLIMutable>]
    type MessageA = {
        [<ProtoMember(1)>] X: string;
        [<ProtoMember(2)>] Y: int option;
    }
    
    [<ProtoContract>]
    [<CLIMutable>]
    type MessageB = {
        [<ProtoMember(1)>] A: string;
        [<ProtoMember(2)>] B: string;
    }
    
    [<ProtoContract>]
    type Message =
        | MessageA of MessageA
        | MessageB of MessageB
    
    let serialize msg =
        use ms = new MemoryStream()
        Serializer.SerializeWithLengthPrefix(ms, msg, PrefixStyle.Fixed32)
        ms.ToArray()
    
    let deserialize<'TMessage> bytes =
        use ms = new MemoryStream(buffer=bytes)
        Serializer.DeserializeWithLengthPrefix<'TMessage>(ms, PrefixStyle.Fixed32)
    
    let registerSerializableDuInModel<'TMessage> (model:RuntimeTypeModel) =
        let baseType = model.[typeof<'TMessage>]
        for case in typeof<'TMessage> |> FSharpType.GetUnionCases do
            let caseType = case.Name |> case.DeclaringType.GetNestedType 
            baseType.AddSubType(1000 + case.Tag, caseType) |> ignore
            let caseTypeModel = model.[caseType]
            caseTypeModel.Add("item").UseConstructor <- false
        baseType.CompileInPlace()
    
    let registerSerializableDu<'TMessage> () = registerSerializableDuInModel<'TMessage> RuntimeTypeModel.Default
    
    registerSerializableDu<Message> ()
    
    let [<Fact>] ``MessageA roundtrips with null`` () =
        let msg = {X=null; Y=None}
        let result = serialize msg
        test <@ msg = deserialize result @>
    
    let [<Fact>] ``MessageA roundtrips with Empty`` () =
        let msg = {X=""; Y=None}
        let result = serialize msg
        test <@ msg = deserialize result @>
    
    let [<Fact>] ``MessageA roundtrips with Some`` () =
        let msg = {X="foo"; Y=Some 32}
        let result = serialize msg
        test <@ msg = deserialize result @>
    
    let [<Fact>] ``MessageA roundtrips with None`` () =
        let msg = {X="foo"; Y=None}
        let result = serialize msg
        test <@ msg = deserialize result @>
    
    let [<Fact>] ``MessageB roundtrips`` () =
        let msg = {A="bar"; B="baz"}
        let result = serialize msg
        test <@ msg = deserialize result @>
    
    let [<Fact>] ``roundtrip pair``() =
        let msg1 = MessageA {X="foo"; Y=Some 32}
        let msg1' = msg1 |> serialize |> deserialize
        test <@ msg1' = msg1 @>
    
        let msg2 = MessageB {A="bar"; B="baz"}     
        let msg2' = msg2 |> serialize |> deserialize
        test <@ msg2' = msg2 @>
    
    let [<Fact>] many() =
        for _ in 1..1000 do
            ``roundtrip pair``()      
    

答案 2 :(得分:0)

我最终做的是这样的事情

    let typeModel = TypeModel.Create()
    let resultType = typedefof<Result>
    let resultNestedTypes = resultType.GetNestedTypes() |> Array.filter (fun x -> x.Name <> "Tags")
    for nestedType in resultNestedTypes do 
        let model = typeModel.Add( nestedType, true )
        model.UseConstructor <- false
        nestedType.GetFields( BindingFlags.NonPublic ||| BindingFlags.Instance ||| BindingFlags.GetField ) |> Array.map (fun x -> x.Name ) |> Array.sort |> model.Add |> ignore

        types.[ nestedType.Name ] <- nestedType

在我的情况下,types是启动应用程序时构建的联合类型的字典。我需要在序列化数据之前将消息中的名称保存到以后才能加载它。

只要添加新字段,这将有效,因为每个字段都变为item1。如果需要删除字段,我认为可以很容易地扩展以从字段名称中获取字段顺序号,如此

type Result = 
    | Success of Item1: string * Item3:bool
    | Failure of string

然后在Item之后提取数字,或者提供最好的工作。有很多方法。