Protobuf net not serializing when using WCF

点点圈 提交于 2020-01-04 05:08:30

问题


I am trying to use protobuf to serialize my WCF calls, but it seems the object is not getting serialized by the client. Some things to note:

  • I am using a shared DTO library.
  • I am using a ChannelFactory to invoke the service (so the types are not losing their datamember attributes).
  • I can serialize and deserialize the objects just using normal protobuf.net code, so the types themselves seem to be ok
  • I am using version 2.0.0.480 of protobuf.net
  • I haven't posted the service code as the problem is with the outgoing message (message log posted below)
  • The client and service work fine if I don't use the protobuf endpoint behaviour.

My main method looks as follows

static void Main(string[] args)
    {
        Base.PrepareMetaDataForSerialization();
        FactoryHelper.InitialiseFactoryHelper(new ServiceModule());
        Member m = new Member();
        m.FirstName = "Mike";
        m.LastName = "Hanrahan";
        m.UserId = Guid.NewGuid();
        m.AccountStatus = MemberAccountStatus.Blocked;
        m.EnteredBy = "qwertt";
        ChannelFactory<IMembershipService> factory = new ChannelFactory<IMembershipService>("NetTcpBinding_MembershipService");
        var client = factory.CreateChannel();

        using (var ms = new MemoryStream())
        {
            Serializer.Serialize<Member>(ms, m);
            Console.WriteLine(ms.Length.ToString());
            ms.Position = 0;
            var member2 = Serializer.Deserialize<Member>(ms);
            Console.WriteLine(member2.EnteredBy);
            Console.WriteLine(member2.FirstName);
        }

        var result = client.IsMemberAllowedToPurchase(m);

        System.Console.Write(result.IsValid.ToString());
        factory.Close();
        var input = Console.ReadLine();
    }

My client configuration looks as follows:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
</configSections>
<system.serviceModel>
<bindings>
  <netTcpBinding>
    <binding name="NetTcpBinding_Common" closeTimeout="00:01:00"
      openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00"
      transactionFlow="false" transferMode="Buffered" transactionProtocol="OleTransactions"
      hostNameComparisonMode="StrongWildcard" listenBacklog="10" maxBufferPoolSize="524288"
      maxBufferSize="1000065536" maxConnections="10" maxReceivedMessageSize="1000000">
      <readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384"
        maxBytesPerRead="4096" maxNameTableCharCount="16384" />
      <reliableSession ordered="true" inactivityTimeout="00:10:00"
        enabled="false" />
      <security mode="Transport">
        <transport clientCredentialType="Windows" protectionLevel="EncryptAndSign" />
        <message clientCredentialType="Windows" />
      </security>
    </binding>
  </netTcpBinding>
</bindings>
<behaviors>
  <endpointBehaviors>
    <behavior name="Proto.Common.EndpointBehavior">
      <protobuf />
    </behavior>
  </endpointBehaviors>
</behaviors>
<extensions>
  <behaviorExtensions>
    <add name="protobuf" type="ProtoBuf.ServiceModel.ProtoBehaviorExtension, protobuf-net, Version=2.0.0.480, Culture=neutral, PublicKeyToken=257b51d87d2e4d67" />
  </behaviorExtensions>
</extensions>
<client>
  <endpoint address="net.tcp://mikes-pc:12002/MembershipService.svc"
    behaviorConfiguration="Proto.Common.EndpointBehavior" binding="netTcpBinding"
    bindingConfiguration="NetTcpBinding_Common" contract="PricesForMe.Core.Entities.ServiceInterfaces.IMembershipService"
    name="NetTcpBinding_MembershipService">
    <identity>
      <userPrincipalName value="Mikes-PC\Mike" />
    </identity>
  </endpoint>
</client>
<diagnostics>
  <messageLogging
       logEntireMessage="true"
       logMalformedMessages="true"
       logMessagesAtServiceLevel="true"
       logMessagesAtTransportLevel="true"
       maxMessagesToLog="3000"
       maxSizeOfMessageToLog="2000"/>
</diagnostics>
</system.serviceModel>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0" />
</startup>
<system.diagnostics>
<sources>
  <source name="System.ServiceModel"
          switchValue="Information, ActivityTracing"
          propagateActivity="true">
    <listeners>
      <add name="traceListener"
          type="System.Diagnostics.XmlWriterTraceListener"
          initializeData="E:\Work\Logs\IMembershipServiceWcfTrace_Client.svclog"  />
    </listeners>
  </source>
  <source name="System.ServiceModel.MessageLogging">
    <listeners>
      <add name="messages"
      type="System.Diagnostics.XmlWriterTraceListener"
      initializeData="E:\Work\Logs\IMembershipServiceWcfTrace_Client_messages.svclog" />
    </listeners>
  </source>
</sources>
</system.diagnostics>
</configuration>

After logging the client message, I get the following entry in the log

    <MessageLogTraceRecord>
    <s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:s="http://www.w3.org/2003/05/soap-envelope">
    <s:Header>
    <a:Action s:mustUnderstand="1">http://www.pricesforme.com/services/MembershipService/IsMemberAllowedToPurchase</a:Action>
    <a:MessageID>urn:uuid:8b545576-c453-4be6-8d5c-9913e2cca4bf</a:MessageID>
    <ActivityId CorrelationId="b4e9361f-1fbc-4b2d-b7ee-fb493847998a" xmlns="http://schemas.microsoft.com/2004/09/ServiceModel/Diagnostics">6d712899-62fd-4547-9517-e9de452305c6</ActivityId>
    <a:ReplyTo>
    <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
    </a:ReplyTo>
    <VsDebuggerCausalityData xmlns="http://schemas.microsoft.com/vstudio/diagnostics/servicemodelsink"></VsDebuggerCausalityData>
    </s:Header>
    <s:Body>
    <IsMemberAllowedToPurchase xmlns="http://www.pricesforme.com/services/">
    <proto></proto>
    </IsMemberAllowedToPurchase>
    </s:Body>
    </s:Envelope>
    </MessageLogTraceRecord>

So as can been seen in the above log message the proto entry has no data in it. My member class looks as follows:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Runtime.Serialization;
    using PricesForMe.Core.Entities.Common;
    using PricesForMe.Core.Entities.Ordering;

    namespace PricesForMe.Core.Entities.Members
    {
        /// <summary>
        /// This entity represents a member or user of the site.
        /// </summary>
        [DataContract]
        [Serializable]
        public class Member: User
        {
            public Member()
                :base()
            {
                EntityType = Entities.EntityType.Member;
            }

            [DataMember(Order = 20)]
            public int Id { get; set; }

            [DataMember(Order = 21)]
            public string MemberName { get; set; }

            [DataMember(Order = 22)]
            public PaymentInfo DefaultPaymentMethod { get; set; }

            [DataMember(Order = 23)]
            public MemberAccountStatus AccountStatus { get; set; }

            #region static

            public static readonly string CacheCollectionKey = "MemberCollection";

            private static readonly string CacheItemKeyPrefix = "Member:";

            public static string GetCacheItemKey(int id)
            {
                return CacheItemKeyPrefix + id.ToString();
            }

            #endregion
        }
    }

The parent user class looks as follows:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Runtime.Serialization;
    using System.Diagnostics.Contracts;

    namespace PricesForMe.Core.Entities.Common
    {
        /// <summary>
        /// This class represents a user in the system.  For example, a user could be a member or a merchant user.
        /// </summary>
        [DataContract]
        [Serializable]
        public class User: Base
        {
            public User()
                :base()
            {
                EntityType = Entities.EntityType.User;
            }

            [DataMember(Order = 10)]
            public Guid UserId { get; set; }

            [DataMember(Order = 11, Name = "First Name")]
            public string FirstName { get; set; }

            [DataMember(Order = 12, Name = "Last Name")]
            public string LastName { get; set; }

            }
        }
    }

And the base class looks as follows:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Runtime.Serialization;
    using System.Diagnostics.Contracts;
    using System.ComponentModel.DataAnnotations;
    using System.Reflection;
    using ProtoBuf.Meta;

    namespace PricesForMe.Core.Entities
    {
        /// <summary>
        /// This is the base class for all entities involved in the request/response pattern of our services
        /// </summary>
        /// <remarks>
        /// The objects derived from this class are used to transfer data from our service classes to our UIs and back again and they should 
        /// not contain any logic.
        /// </remarks>
        [DataContract]
        [Serializable]
        public abstract class Base
        {
            public Base()
            {
                //Set some defaults for this
                EnteredBy = System.Environment.UserName;
                EnteredSource = System.Environment.MachineName;
            }

            /// <summary>
            /// This is the record timestamp
            /// </summary>
            [DataMember(Order = 2)]
            public DateTime RecordTimeStamp { get; set; }

            /// <summary>
            /// This is the name of the user who last edited the entity
            /// </summary>
            [DataMember(Order = 3)]
            public string EnteredBy { get; set; }

            /// <summary>
            /// This is the source of the last edited entity
            /// </summary>
            [DataMember(Order = 4)]
            public string EnteredSource { get; set; }

            [DataMember(Order = 5)]
            private PricesForMe.Core.Entities.Common.ValidationResult _validationResult = null;
            /// <summary>
            /// Data on the validity of the entity.
            /// </summary>
            public PricesForMe.Core.Entities.Common.ValidationResult ValidationData
            {
                get
                {
                    _validationResult = Validate();
                    return _validationResult;
                }
                set
                {
                    _validationResult = value;
                }
            }

            /// <summary>
            /// Flag denoting if the record is a new record or not.
            /// </summary>
            /// <remarks>
            /// To flag an entity as an existing record call the "FlagAsExistingReport()" method.
            /// </remarks>
            public bool IsNewRecord
            {
                get
                {
                    return _isNewRecord;
                }
            }

            [DataMember(Order = 6)]
            protected bool _isNewRecord = true;
            /// <summary>
            /// Flags the entity as a record that already exists in the database
            /// </summary>
            /// <remarks>
            /// This is a method rather than a field to demonstrait that this should be called with caution (as opposed to inadvertantly setting a flag!)
            /// <para>
            /// Note that this method should only need to be called on object creation if the entity has a composite key.  Otherwise the flag is
            /// set when the id is being set.  It should always be called on saving an entity.
            /// </para>
            /// </remarks>
            public void FlagAsExistingRecord()
            {
                _isNewRecord = false;
            }

            public virtual PricesForMe.Core.Entities.Common.ValidationResult Validate()
            {
                if (_validationResult == null)
                {
                    _validationResult = new PricesForMe.Core.Entities.Common.ValidationResult();
                    _validationResult.MemberValidations = new List<Common.ValidationResult>();
                    _validationResult.RulesViolated = new List<Common.ValidationRule>();
                }
                return _validationResult;
            }

            /// <summary>
            /// This is the type of entity we are working with
            /// </summary>
            [DataMember(Order = 7)]
            private EntityType _entityType = EntityType.Unknown;
            public EntityType EntityType
            {
                get
                {
                    return _entityType;
                }
                protected set
                {
                    _entityType = value;
                }
            }


            /// <summary>
            /// Flag to say if the id generated for this class need to be int64 in size.
            /// </summary>
            [DataMember(Order = 9)]
            public bool IdRequiresInt64 { get; protected set; }

            /// <summary>
            /// This method tells us if the database id has been assigned.  Note that this does
            /// not mean the entity has been saved, only if the id has been assigned (so the id could be greater than 0, but the
            /// entity could still be a NewRecord
            /// </summary>
            /// <returns></returns>
            [DataMember(Order = 8)]
            public bool HasDbIdBeenAssigned { get; protected set; }

            private Guid _validationId = Guid.NewGuid();
            public Guid EntityValidationId
            {
                get
                {
                    return _validationId;
                }
            }

            /// <summary>
            /// Converts an object into another type of object based on the mapper class provided.
            /// </summary>
            /// <remarks>
            /// This method allows us to easily convert between objects without concerning ourselves with the mapping implementation.  This
            /// allows us to use various mapping frameworks (e.g. Automapper, ValueInjector) or create our own custom mapping.
            /// </remarks>
            /// <typeparam name="TDestination">The type we want to convert to</typeparam>
            /// <typeparam name="KMapper">The mapping type</typeparam>
            /// <returns>The new type</returns>
            public TDestination ConvertTo<TDestination, TSource, KMapper>()
                where KMapper : IEntityMapper<TDestination, TSource>
                where TSource : class
            {
                return Base.ConvertToItem<TDestination, TSource, KMapper>(this as TSource);
            }

            /// <summary>
            /// Returns all known child types
            /// </summary>
            public IEnumerable<Type> GetAllTypes()
            {
                Assembly current = Assembly.GetCallingAssembly();
                List<Type> derivedTypes = new List<Type>();
                var allTypes = current.GetTypes();
                foreach (var t in allTypes)
                {
                    if (t.IsAssignableFrom(typeof(Base)))
                    {
                        derivedTypes.Add(t);
                    }
                }
                return derivedTypes;
            }

            #region Static Methods
            /// <summary>
            /// Converts a list of one type to a list of another type
            /// </summary>
            /// <typeparam name="TDestination">The type we want to convert to</typeparam>
            /// <typeparam name="TSource">The source type</typeparam>
            /// <typeparam name="KMapper">The mapper class</typeparam>
            /// <param name="source">The source list of items.</param>
            /// <returns></returns>
            public static List<TDestination> ConvertToList<TDestination, TSource, KMapper>(IEnumerable<TSource> source)
                where KMapper : IEntityMapper<TDestination, TSource>
                where TSource : class
            {
                List<TDestination> result = new List<TDestination>();
                KMapper mapper = Activator.CreateInstance<KMapper>();
                foreach (var item in source)
                {
                    result.Add(mapper.Convert(item));
                }
                return result;
            }

            public static TDestination ConvertToItem<TDestination, TSource, KMapper>(TSource source)
                where KMapper : IEntityMapper<TDestination, TSource>
                where TSource : class
            {
                //Return default (i.e. null for ref objects) if the source is null.
                if (source == null) { return default(TDestination); }

                KMapper mapper = Activator.CreateInstance<KMapper>();
                return mapper.Convert(source);
            }


            private static object _metaLock = new object();
            private static bool _metaDataPrepared = false;
            /// <summary>
            /// Creates protobuf type models from the entities in this assembly
            /// </summary>
            public static void PrepareMetaDataForSerialization()
            {
                lock (_metaLock)
                {
                    if (_metaDataPrepared) { return; }

                    Assembly current = Assembly.GetExecutingAssembly();
                    var allTypes = current.GetTypes();
                    foreach (var t in allTypes)
                    {
                        checkType(t);
                    }
                }
            }

            private static void checkType(Type type)
            {
                Assembly current = Assembly.GetExecutingAssembly();
                var allTypes = current.GetTypes();
                int key = 1000;
                foreach (var t in allTypes)
                {
                    if (t.IsSubclassOf(type) && t.BaseType == type)
                    {
                        RuntimeTypeModel.Default[type].AddSubType(key, t);
                        key++;
                    }
                }
            }

            #endregion
        }
    }

The PrepareMetaDataForSerialization method on base configures the RuntimeModel for protobuf.net, but I mentioned earlier, the serialization and deserialization works fine outside of the WCF, so I think the DTOs are ok. Any ideas around what could be causing the problem are greatly appreciated.


回答1:


k; the element name looks correct (proto, matching XmlProtoSerializer.PROTO_ELEMENT), so protobuf-net definitely tried to do something. It also doesn't include @nil to represent null, so it knows there was data. Beyond that, it serializes the object to a MemoryStream and writes it as base-64 (identical to how byte[] etc is represented, which allows WCF to hoist the data silently and automatically if things like MTOM are enabled). So the question becomes "why would my type serialize to nothing?"

The use of DataContract/DataMember is fine, and matches my existing WCF integration tests.

I wonder how much of this is due to the inheritance (only one of the members shown relates to the concrete type, and I would hazard a guess that Blocked is 0, which has particular handling).

I cannot, however, emphasise enough how unsafe your current inheritance handling is; the numbers matter, and reflection makes no guarantees re order. I would strongly suggest you revisit this and make inheritance numbering more predicatable.

Very minor observation, but there is no need to store EntityType - it is entirey redundant and could be handled via polymorphism with no need for storage.

Also, there's an important bug that _metaDataPrepared is never set to true.

However! Ultimately I cannot reproduce this; I have used your code (or most of it) to generate an integration test, and - it passes; meaning: using WCF, NetTcpBinding, your classes (including your inheritance fixup code), and the protobuf packing, it just works. The data that goes over the wire is the data we expect.

Happy to try to help further, but I need to be able to repro it... and right now, I can't.

The first thing I'd do is add the missing _metaDataPrepared = true; to see if that helps.




回答2:


I thought you need to use ProtoContract instead of DataContract per this SO? Also, make sure you set "Reuse types in referenced assemblies" when configuring service reference. And according to this SO they support data contracts but you have to have the order set [DataMember(Order = 0)] (at least that's what worked for me).



来源:https://stackoverflow.com/questions/10115538/protobuf-net-not-serializing-when-using-wcf

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