protobuf を使用して WCF 呼び出しをシリアル化しようとしていますが、オブジェクトがクライアントによってシリアル化されていないようです。注意すべき点:
- 共有 DTO ライブラリを使用しています。
- ChannelFactory を使用してサービスを呼び出しています (そのため、型はデータメンバー属性を失いません)。
- 通常のprotobuf.netコードを使用するだけでオブジェクトをシリアル化および逆シリアル化できるため、型自体は問題ないようです
- protobuf.net のバージョン 2.0.0.480 を使用しています
- 問題は送信メッセージにあるため、サービスコードを投稿していません(メッセージログは以下に投稿されています)
- protobuf エンドポイントの動作を使用しない場合、クライアントとサービスは正常に動作します。
私の主な方法は次のようになります
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();
}
私のクライアント構成は次のようになります。
<?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>
クライアント メッセージをログに記録した後、ログに次のエントリを取得します
<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>
したがって、上記のログ メッセージでわかるように、proto エントリにはデータがありません。私のメンバークラスは次のようになります。
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
}
}
親ユーザー クラスは次のようになります。
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; }
}
}
}
基本クラスは次のようになります。
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
}
}
baseのPrepareMetaDataForSerialization
メソッドは protobuf.net の RuntimeModel を構成しますが、先ほども言いましたが、シリアル化と逆シリアル化は WCF の外でも問題なく動作するので、DTO は問題ないと思います。問題を引き起こしている可能性のあるものに関するアイデアは大歓迎です。