`DataContractSerializer`: Serialize child class as base class
Description
I have following on my server:
[DataContract]
public class Base { }
[OperationContract]
void DoSth(Base base) { }
and on my client:
class Child : Base { }
When calling the server with DoSth(child) I get a serialization exception, because Child is not known to the WCF DataContractSerializer:
System.ServiceModel.CommunicationException: There was an error while trying to serialize parameter http://tempuri.org/:base. The InnerException message was 'Type 'Child' with data contract name 'Child:http://schemas.datacontract.org/2004/07/Client' is not expected. Add any types not known statically to the list of known types - for example, by using the KnownTypeAttribute attribute or by adding them to the list of known types passed to DataContractSerializer.'. Please see InnerException for more details.
---> System.Runtime.Serialization.SerializationException: Type 'Child' with data contract name 'Child:http://schemas.datacontract.org/2004/07/Client' is not expected. Add any types not known statically to the list of known types - for example, by using the KnownTypeAttribute attribute or by adding them to the list of known types passed to DataContractSerializer.
And I don't want to make it known, because the server uses only Base's properties. How can I send my data as Base object?
I know, I could create a new Base object and copy all properties, but this is not really elegant.
see also https://stackoverflow.com/questions/78061526/datacontractserializer-deserialize-child-class-into-base-class
Reproduction Steps
- Create WCF server and client.
- Add
BaseandChildclasses as described above. - Send instance of
Childto server.
Expected behavior
Child should be serialized as Base, ignoring all additional properties.
Actual behavior
System.ServiceModel.CommunicationException: There was an error while trying to serialize parameter http://tempuri.org/:base. The InnerException message was 'Type 'Child' with data contract name 'Child:http://schemas.datacontract.org/2004/07/Client' is not expected. Add any types not known statically to the list of known types - for example, by using the KnownTypeAttribute attribute or by adding them to the list of known types passed to DataContractSerializer.'. Please see InnerException for more details.
---> System.Runtime.Serialization.SerializationException: Type 'Child' with data contract name 'Child:http://schemas.datacontract.org/2004/07/Client' is not expected. Add any types not known statically to the list of known types - for example, by using the KnownTypeAttribute attribute or by adding them to the list of known types passed to DataContractSerializer.
Regression?
No response
Known Workarounds
No response
Configuration
.NET 8 on Windows 11, but should not be specific to that environment.
Other information
No response
I think you can solve this by providing a ISerializationSurrogateProvider using DataContractSerializerOperationBehavior.
factory = new ChannelFactory<IDataContractResolverService>(new BasicHttpBinding(), new EndpointAddress(Endpoints.DataContractResolver_Address));
var surrogateProvider = new MySerializationSurrogateProvider();
foreach (var operation in factory.Endpoint.Contract.Operations)
{
DataContractSerializerOperationBehavior behavior =
operation.OperationBehaviors.FirstOrDefault(
x => x.GetType() == typeof(DataContractSerializerOperationBehavior)) as DataContractSerializerOperationBehavior;
behavior.SerializationSurrogateProvider = surrogateProvider;
}
public class MySerializationSurrogateProvider : ISerializationSurrogateProvider
{
public bool mySurrogateProviderIsUsed = false;
public object GetDeserializedObject(object obj, Type targetType)
{
return obj;
}
public object GetObjectToSerialize(object obj, Type targetType)
{
return obj;
}
public Type GetSurrogateType(Type type)
{
if (type == typeof(Child))
{
return typeof(Base);
}
return type;
}
}
I haven't tested this specific code snippet, but I believe this should work for you. Basically when DataContractSerializer (DCS) is presented an object of type Child, we tell it to use the generated serializer for Base, which means it will only care about serializing those properties and will ignore the rest. Then when it comes to actually serialize it, the Child instance gets passed to GetObjectToSerialize and as long as the returned instance is of type Base, DCS doesn't care that it's the exact same instance.
The reason for the default behavior is to avoid data loss. If you intended to serialize Child and had missed specifying KnownTypes or ServiceKnownTypes and we silently just serialized Base properties, you could lose data and not realize until it's too late.
Thank you, for the detailled answer. Where do I typically have to execute the first lines of your code?
Are you using a client generated by dotnet-svcutil? The code snippet I provided is when you are manually instantiating a ChannelFactory and not using the generated client.
Yes, I'm using the svcutil generated code. Can I manually provide a ChannelFactory there? Somewhere at initialization of the WCF Client?
You can access the channel factory on the client. By default every client instance gets its own ChannelFactory instance, but you can turn on channel factory caching as long as you are certain every instance will be using the same binding and endpoint address. The code would look like this:
MyGeneratedClient.CacheSetting = CacheSetting.AlwaysOn;
var client = new MyGeneratedClient();
var surrogateProvider = new MySerializationSurrogateProvider();
foreach (var operation in client.Endpoint.Contract.Operations)
{
// Code from above
}
client.Endpoint does client.ChannelFactory.Endpoint under the hood. If you construct multiple instances of the client throughout the lifetime of your app, you will need to do this once at the start of your app and hold on to that reference as the ChannelFactory caching is based on reference counting and if you do this then dispose of it, you lose the changes.