mbraceproject / FsPickler

A fast multi-format message serializer for .NET
http://mbraceproject.github.io/FsPickler/
MIT License
323 stars 54 forks source link

FsPickler ignores interfaces when trying to resolve a pickler. #80

Closed 0x53A closed 6 years ago

0x53A commented 7 years ago

The following test fails and says NonSerializableWithInterface is not serializable

namespace MBrace.FsPickler.Tests

open System
open System.IO
open System.Collections
open System.Collections.Generic
open System.Reflection
open System.Runtime.Serialization
open System.Threading.Tasks

open MBrace.FsPickler
open MBrace.FsPickler.Hashing
open MBrace.FsPickler.Combinators
open MBrace.FsPickler.Json

open MBrace.FsPickler.Tests.TestTypes

open NUnit.Framework
open FsUnit
open FsCheck

[<TestFixture>]
module ``Interface Tests`` =
    open System.Threading

    type ITest =
        abstract member Int : int

    [<AutoSerializable(false)>]
    type NonSerializableWithInterface(i) =
        interface ITest with
            member x.Int = i
        member x.Int = i

    let mutable isPicklerFactoryRegistered = false
    let registerFactory() =
        if isPicklerFactoryRegistered then () else
        let factory (resolver:IPicklerResolver) =
            let intPickler = resolver.Resolve<int>()
            let reader state =
                let i = intPickler.Read state "i"
                NonSerializableWithInterface(i) :> ITest
            let writer state (value:ITest) =
                intPickler.Write state "i" value.Int
            Pickler.FromPrimitives(reader, writer, useWithSubtypes=true)
        FsPickler.RegisterPicklerFactory<ITest>(factory)
        isPicklerFactoryRegistered <- true

    [<Test; Category("Pickler tests")>]
    let ``interface pickler is used for implementation`` () =
        registerFactory()

        let serializer = FsPickler.CreateBinarySerializer()
        use ms = new MemoryStream()
        serializer.Serialize(ms, NonSerializableWithInterface(123456), leaveOpen=true)
        ms.Seek(0L, SeekOrigin.Begin) |> ignore
        let v : NonSerializableWithInterface = serializer.Deserialize(ms)
        Assert.AreEqual(123456, v.Int)

I am currently blocked by this:

I am trying to serialize Akka.Net objects with FsPickler. Akka.Net uses a pattern where some types are not serializable, but they implement an interface ISurrogated which has a member ToSurrogate which returns an ISurrogate. The result of that is then serialized.

On deserialization, the whole thing is switched: If it sees an ISurrogate, it calls FromSurrogate which returns the original object.

The whole thing is implemented here, that may be easier to understand than my explanation: https://github.com/akkadotnet/akka.net/blob/c8181f0a26e91efdf249035a1c5402ef2f3b7ec7/src/core/Akka/Serialization/NewtonSoftJsonSerializer.cs

Anyway, without being able to register a pickler for an interface, I currently don't know how I could serialize Akka.Net objects.


Should it be possible to register a pickler-factory for an interface?

This would enable a lot of scenarios which are currently not possible, but may also introduce non-determinism (which interface should be chosen if there are multiple?)

eiriktsarpalis commented 7 years ago

@0x53A @dsyme here's a potential workaround:

serializer.Serialize<ITest>(ms, NonSerializableWithInterface(123456), leaveOpen=true)

Because the library is based on picklers, the specified type is significant.

eiriktsarpalis commented 7 years ago

In general, I would be very careful with that useWithSubtypes parameter. It only really works assuming there are no open hierarchies, IOW the pickler has intimate knowledge of all possible subtypes that can be. Failure to observe this will result in unsoundness. It's really an internal flag used for MemberInfo and F# unions that somehow found its way in a public pickler constructor. Your example seems slightly artificial since it really only assumes one implementation for the interface

0x53A commented 7 years ago

@eiriktsarpalis yes, the example is obviously artificial, but it reflects my use case of serializing an akka.net actor ref.

To be more specific, I need to serialize a number of types which themselves contain the IActorRef, so I don't think the workaround is applicable in that case.

Also, I do not know - at the point where I am called - what the concrete type is, so my call to the serializer is serializer.Serialize<obj>, where the actual type may contain an akka.net object at any level.


In this case, the ISurrogated and ISurrogate are interfaces of akka.net specifically for serialization, and the pickler can and does handle all possible subtypes.

It would obviously not make sense to register a pickler for a general interface like IComparable.


The next time I'm at work (Monday), I will extract the relevant parts out into a reproduction, that will hopefully make my use case clearer.

Thank you!

0x53A commented 7 years ago

Did I say tomorrow? I meant in three weeks ...

Attached is a project demonstrating this issue. My work firewall blocks github git-authentication, so I needed to upload it as a zip.

The project contains my self-built version of FsPickler, so if you run it as-is, it should work.

If you modify paket.dependencies to remove the custom feed and run update, it will stop working and fail instead.

The relevant part is in FsSerializer.fs, where I inherit from Akka.Serialization.Serializer.

To run it:

open two command lines:

a) AkkaFsPicklerRepro.exe server
It will print out a uri

b) AkkaFsPicklerRepro.exe client <uri>
Now you can type anything in the console, it will send that as a message to the server actor.

The failure when using the official FsPickler is:

[WARNING][28.06.2017 16:19:40][Thread 0008][[akka://LRIEGER-10-neg-nemetschek-de-14284/system/endpointManager/reliableEndpointWriter-akka.tcp%3A%2F%2FLRIEGER-10-neg-nemetschek-de-13812%40lrieger-10.neg.nemetschek.de%3A50536-1#184503285]] Association with remote system akka.tcp://LRIEGER-10-neg-nemetschek-de-13812@lrieger-10.neg.nemetschek.de:50536 has failed; address is now gated for 5000 ms. Reason is: [Akka.Remote.EndpointException: Failed to write message to the transport ---> MBrace.FsPickler.FsPicklerException: Error serializing object of type 'System.Object'. ---> MBrace.FsPickler.NonSerializableTypeException: Type 'Akka.Actor.FutureActorRef' is not serializable.

AkkaFsPicklerRepro.zip