BinaryFormatter - Is it possible to deserialize known class without the assembly?

会有一股神秘感。 提交于 2020-01-02 10:22:13

问题


I am currently trying to interoperate with a program that sends data over the network after first formatting it with C#'s BinaryFormatter. It's a dumb idea, and I hate it, but I have to interoperate with it.

I know what the type looks like, I know it's exact layout. But I can't add a reference to that specific assembly in my program for various reasons.

Given how tightly coupled BinaryFormatter is to the specific type/version, I can't seem to find a way to get it to deserialize despite knowing the data structure.

I'm currently look at either creating a fake assembly with all the right attributes and attempting to link that in (seems really messy), or manually attempting to go through the binary stream and extract the values I'm looking for (I'm looking at the MS documentation on this, and it's clear as mud as to the layout).

Any other great ideas, or has anyone had successes with this in the past? It seems like I know all the information that I need, and BinaryFormatter is just being notoriously fragile about it.

edit:

To answer the question below (which is a good point, btw) there are a couple of reasons.

  1. Project cleanliness. Adding a 5MB reference to an .exe that's external for one feature is a bit off.

  2. The pieces of equipment I'm interoperating with have various versions deployed around the world. The internal data structure for the item I care about is the same across all of them, but the versions of the assembly are different, causing breakage with BinaryFormatter. I could convert the binary stream to a string, search for the version number then load the proper version, but now I have a dozen .exe's laying around waiting for me to load the right one. Then that scheme is not very future proof (well, this whole scheme isn't really future proof, but I'd at least like to abstract away some of the fragility of BinaryFormatter to make my job easier). Just writing this response has got me thinking about using emit or similar to create a custom assembly on the fly.....but man, there has to be an easier way, right? I'm literally looking for a couple of bools in a medium sized data structure.

  3. The object's variables are exposed through get/set properties that have some logic to them, and try to make function calls and update other objects that might not be present on my side (aka, a get gets me the value I need, but also triggers notifications that ripple through the linked dependency and I can get exceptions bubbling up to my application. Talk about code smell!). It turns into a dependency/implementation rabbit hole.

edit2: The manufacturer is working with me to make their system better, but when we advertise "Works with X" we'd like it to just work with X, not require specific versions. Particularly with some of our customer systems that are tightly version controlled, and just updating the offending application becomes a major effort.


回答1:


as you speculated in the comments to your question SerializationSurrogate and SerializationBinder could get you part of the way there. Given that you're only interested in a few properties you could deserialize to a proxy class which you'd populate by enumerating over the SerializationInfo passed to your SerializationSurrogate. Unfortunately, it will only get you part of the way there.

The problem is that accessing the properties is not going to trigger any side-effects in the serialized object because you're only accessing the data using the SerializationSurrogate - none of the binary code is getting executed. The quick & dirty test code below illustrates the problem:

namespace BinaryProxy
{
    using System;
    using System.Collections.Generic;
    using System.IO;

    using System.Runtime.Serialization;
    using System.Runtime.Serialization.Formatters.Binary;

    [Serializable]
    class TestClass
    {


        public bool mvalue;

        public TestClass(bool value)
        {
            BoolValue = value;
        }

        public bool BoolValue
        {
            get
            {
                // won't happen
                SideEffect = DateTime.Now.ToString();
                return mvalue;
            }

            set
            {
                mvalue = value;
            }
        }

        public string SideEffect { get; set; }

    }

    class ProxyTestClass
    {
        private Dictionary<string, object> data = new Dictionary<string, object>();

        public Object GetData(string name)
        {
            if(data.ContainsKey(name))
            {
                return data[name];
            }
            return null;
        }
        public void SetData(string name, object value)
        {
            data[name] = value;
        }

        public IEnumerable<KeyValuePair<string, object>> Dump()
        {
            return data;
        }
    }

    class SurrogateTestClassConstructor : ISerializationSurrogate
    {
        private ProxyTestClass mProxy;
        /// <summary>
        /// Populates the provided <see cref="T:System.Runtime.Serialization.SerializationInfo"/> with the data needed to serialize the object.
        /// </summary>
        /// <param name="obj">The object to serialize. </param>
        /// <param name="info">The <see cref="T:System.Runtime.Serialization.SerializationInfo"/> to populate with data. </param>
        /// <param name="context">The destination (see <see cref="T:System.Runtime.Serialization.StreamingContext"/>) for this serialization. </param>
        /// <exception cref="T:System.Security.SecurityException">The caller does not have the required permission. </exception>
        public void GetObjectData(object obj, SerializationInfo info, StreamingContext context)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Populates the object using the information in the <see cref="T:System.Runtime.Serialization.SerializationInfo"/>.
        /// </summary>
        /// <returns>
        /// The populated deserialized object.
        /// </returns>
        /// <param name="obj">The object to populate. </param>
        /// <param name="info">The information to populate the object. </param>
        /// <param name="context">The source from which the object is deserialized. </param>
        /// <param name="selector">The surrogate selector where the search for a compatible surrogate begins. </param>
        /// <exception cref="T:System.Security.SecurityException">The caller does not have the required permission. </exception>
        public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector)
        {
            if (mProxy == null) mProxy = new ProxyTestClass();
            var en = info.GetEnumerator();
            while (en.MoveNext())
            {
                mProxy.SetData(en.Current.Name, en.Current.Value);


            }
            return mProxy;

        }



    }

    sealed class DeserializeBinder : SerializationBinder
    {
        public override Type BindToType(string assemblyName, string typeName)
        {


            return typeof(ProxyTestClass);
        }
    }

    static class Program
    {

        static void Main()
        {
            var tc = new TestClass(true);
            byte[] serialized;
            using (var fs = new MemoryStream())
            {
                var formatter = new BinaryFormatter();
                formatter.Serialize(fs, tc);
                serialized = fs.ToArray();

                var surrSel = new SurrogateSelector();
                surrSel.AddSurrogate(typeof(ProxyTestClass),
                    new StreamingContext(StreamingContextStates.All), new SurrogateTestClassConstructor());

                using (var fs2 = new MemoryStream(serialized))
                {
                    var formatter2 = new BinaryFormatter();
                    formatter2.Binder = new DeserializeBinder();
                    formatter2.SurrogateSelector = surrSel;
                    var deser = formatter2.Deserialize(fs2) as ProxyTestClass;
                    foreach (var c in deser.Dump())
                    {
                        Console.WriteLine("{0} = {1}", c.Key, c.Value);
                    }
                }

            }

        }
    }
}

In the example above, the TestClass' SideEffect backing field will remain null.

If you don't need the side-effects and just want to access the value of some fields the approach is somewhat viable. Otherwise I can't think of any other viable solution other than following Jon Skeet's suggestion.




回答2:


Are you able to add an extra layer (or a separate service) which is able to add a reference to that specific assembly? If so, you could make that small layer deserialize the binary format and reformat it in something more portable (JSON, protocol buffers, XML, whatever's appropriate). Think of it as an isolation layer from the insanity of BinaryFormatter :)



来源:https://stackoverflow.com/questions/13594831/binaryformatter-is-it-possible-to-deserialize-known-class-without-the-assembly

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!