How to pass a Noda Time (or any third-party type) object as a parameter in WCF?

扶醉桌前 提交于 2020-01-03 08:56:21

问题


I have a service which uses Noda Time types (LocalDate and ZonedDateTime) in OperationContract parameters, but when I try sending for example LocalDate(1990,7,31) the server receives an object with the default value (1970/1/1). No error is thrown by the client or the server.

Previously it worked well with the corresponding BCL types (DateTimeOffset). I understand Noda Time types may not be "known" by WCF, but I don't see how I am supposed to add them. I checked this page in the documentation about known types, but it does not help.

Is there any way to do this to avoid a dirty (and possibly incomplete) manual conversion/serialization from and to a BCL type?

Thank you.


回答1:


Thanks to Aron's suggestion, I was able to come up with an implementation of IDataContractSurrogate, which is very helpful to pass objects of non base types through WCF (not only Noda Time).

For those who are interested, here is the complete code with explanations, supporting LocalDate, LocalDateTime and ZonedDateTime. The serialization method can of course be customized to meet the requirements, for example using Json.NET serialization, as my simple implementation will not serialize era/calendar information.

Alternatively, I have posted the full code on this Gist: https://gist.github.com/mayerwin/6468178.

First, the helper class that takes care of serializing/converting to base types:

public static class DatesExtensions {
    public static DateTime ToDateTime(this LocalDate localDate) {
        return new DateTime(localDate.Year, localDate.Month, localDate.Day);
    }

    public static LocalDate ToLocalDate(this DateTime dateTime) {
        return new LocalDate(dateTime.Year, dateTime.Month, dateTime.Day);
    }

    public static string Serialize(this ZonedDateTime zonedDateTime) {
        return LocalDateTimePattern.ExtendedIsoPattern.Format(zonedDateTime.LocalDateTime) + "@O=" + OffsetPattern.GeneralInvariantPattern.Format(zonedDateTime.Offset) + "@Z=" + zonedDateTime.Zone.Id;
    }

    public static ZonedDateTime DeserializeZonedDateTime(string value) {
        var match = ZonedDateTimeRegex.Match(value);
        if (!match.Success) throw new InvalidOperationException("Could not parse " + value);
        var dtm = LocalDateTimePattern.ExtendedIsoPattern.Parse(match.Groups[1].Value).Value;
        var offset = OffsetPattern.GeneralInvariantPattern.Parse(match.Groups[2].Value).Value;
        var tz = DateTimeZoneProviders.Tzdb.GetZoneOrNull(match.Groups[3].Value);
        return new ZonedDateTime(dtm, tz, offset);
    }

    public static readonly Regex ZonedDateTimeRegex = new Regex(@"^(.*)@O=(.*)@Z=(.*)$");
}

Then a ReplacementType class which contains the serialized data (Serialized should only store types that are known by the WCF serializer) and can be passed over WCF:

public class ReplacementType {
    [DataMember(Name = "Serialized")]
    public object Serialized { get; set; }
    [DataMember(Name = "OriginalType")]
    public string OriginalTypeFullName { get; set; }
}

The serialization/deserialization rules are wrapped in Translator generic classes to simplify adding rules to the surrogate (only one surrogate is assigned to the service endpoint so it should contain all the necessary rules):

public abstract class Translator {
    public abstract object Serialize(object obj);
    public abstract object Deserialize(object obj);
}

public class Translator<TOriginal, TSerialized> : Translator {
    private readonly Func<TOriginal, TSerialized> _Serialize;

    private readonly Func<TSerialized, TOriginal> _Deserialize;

    public Translator(Func<TOriginal, TSerialized> serialize, Func<TSerialized, TOriginal> deserialize) {
        this._Serialize = serialize;
        this._Deserialize = deserialize;
    }

    public override object Serialize(object obj) {
        return new ReplacementType { Serialized = this._Serialize((TOriginal)obj), OriginalTypeFullName = typeof(TOriginal).FullName };
    }

    public override object Deserialize(object obj) {
        return this._Deserialize((TSerialized)obj);
    }
}

Finally the surrogate class, each translation rule can be easily added in the static constructor:

public class CustomSurrogate : IDataContractSurrogate {
    /// Type.GetType only works for the current assembly or mscorlib.dll
    private static readonly Dictionary<string, Type> AllLoadedTypesByFullName = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes()).Distinct().GroupBy(t => t.FullName).ToDictionary(t => t.Key, t => t.First());

    public static Type GetTypeExt(string typeFullName) {
        return Type.GetType(typeFullName) ?? AllLoadedTypesByFullName[typeFullName];
    }

    private static readonly Dictionary<Type, Translator> Translators;
    static CustomSurrogate() {
        Translators = new Dictionary<Type, Translator> {
            {typeof(LocalDate), new Translator<LocalDate, DateTime>(serialize: d => d.ToDateTime(), deserialize: d => d.ToLocalDate())},
            {typeof(LocalDateTime), new Translator<LocalDateTime, DateTime>(serialize:  d => d.ToDateTimeUnspecified(), deserialize: LocalDateTime.FromDateTime)},
            {typeof(ZonedDateTime), new Translator<ZonedDateTime, string> (serialize: d => d.Serialize(), deserialize: DatesExtensions.DeserializeZonedDateTime)}
        };
    }

    public Type GetDataContractType(Type type) {
        if (Translators.ContainsKey(type)) {
            type = typeof(ReplacementType);
        }
        return type;
    }

    public object GetObjectToSerialize(object obj, Type targetType) {
        Translator translator;
        if (Translators.TryGetValue(obj.GetType(), out translator)) {
            return translator.Serialize(obj);
        }
        return obj;
    }

    public object GetDeserializedObject(object obj, Type targetType) {
        var replacementType = obj as ReplacementType;
        if (replacementType != null) {
            var originalType = GetTypeExt(replacementType.OriginalTypeFullName);
            return Translators[originalType].Deserialize(replacementType.Serialized);
        }
        return obj;
    }

    public object GetCustomDataToExport(MemberInfo memberInfo, Type dataContractType) {
        throw new NotImplementedException();
    }

    public object GetCustomDataToExport(Type clrType, Type dataContractType) {
        throw new NotImplementedException();
    }

    public void GetKnownCustomDataTypes(Collection<Type> customDataTypes) {
        throw new NotImplementedException();
    }

    public Type GetReferencedTypeOnImport(string typeName, string typeNamespace, object customData) {
        throw new NotImplementedException();
    }

    public CodeTypeDeclaration ProcessImportedType(CodeTypeDeclaration typeDeclaration, CodeCompileUnit compileUnit) {
        throw new NotImplementedException();
    }
}

And now to use it, we define a service named SurrogateService:

[ServiceContract]
public interface ISurrogateService {
    [OperationContract]
    Tuple<LocalDate, LocalDateTime, ZonedDateTime> GetParams(LocalDate localDate, LocalDateTime localDateTime, ZonedDateTime zonedDateTime);
}

public class SurrogateService : ISurrogateService {
    public Tuple<LocalDate, LocalDateTime, ZonedDateTime> GetParams(LocalDate localDate, LocalDateTime localDateTime, ZonedDateTime zonedDateTime) {
        return Tuple.Create(localDate, localDateTime, zonedDateTime);
    }
}

To run on a completely standalone basis with the client and server on the same machine (in a Console application), we just need to add the following code to a static class and call the function Start():

public static class SurrogateServiceTest {
    public static void DefineSurrogate(ServiceEndpoint endPoint, IDataContractSurrogate surrogate) {
        foreach (var operation in endPoint.Contract.Operations) {
            var ob = operation.Behaviors.Find<DataContractSerializerOperationBehavior>();
            ob.DataContractSurrogate = surrogate;
        }
    }

    public static void Start() {
        var baseAddress = "http://" + Environment.MachineName + ":8000/Service";
        var host = new ServiceHost(typeof(SurrogateService), new Uri(baseAddress));
        var endpoint = host.AddServiceEndpoint(typeof(ISurrogateService), new BasicHttpBinding(), "");
        host.Open();
        var surrogate = new CustomSurrogate();
        DefineSurrogate(endpoint, surrogate);

        Console.WriteLine("Host opened");

        var factory = new ChannelFactory<ISurrogateService>(new BasicHttpBinding(), new EndpointAddress(baseAddress));
        DefineSurrogate(factory.Endpoint, surrogate);
        var client = factory.CreateChannel();
        var now = SystemClock.Instance.Now.InUtc();
        var p = client.GetParams(localDate: now.Date, localDateTime: now.LocalDateTime, zonedDateTime: now);

        if (p.Item1 == now.Date && p.Item2 == now.LocalDateTime && p.Item3 == now) {
            Console.WriteLine("Success");
        }
        else {
            Console.WriteLine("Failure");
        }
        ((IClientChannel)client).Close();
        factory.Close();

        Console.Write("Press ENTER to close the host");
        Console.ReadLine();
        host.Close();
    }
}

Voilà ! :)



来源:https://stackoverflow.com/questions/18632361/how-to-pass-a-noda-time-or-any-third-party-type-object-as-a-parameter-in-wcf

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