Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DataContractSerializer: Serialize child class as base class #5437

Open
AchimStuy opened this issue Feb 28, 2024 · 5 comments
Open

DataContractSerializer: Serialize child class as base class #5437

AchimStuy opened this issue Feb 28, 2024 · 5 comments

Comments

@AchimStuy
Copy link

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 Base and Child classes as described above.
  • Send instance of Child to 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

@mconnew mconnew transferred this issue from dotnet/runtime Feb 28, 2024
@mconnew
Copy link
Member

mconnew commented Feb 28, 2024

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.

@AchimStuy
Copy link
Author

Thank you, for the detailled answer. Where do I typically have to execute the first lines of your code?

@mconnew
Copy link
Member

mconnew commented Mar 5, 2024

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.

@AchimStuy
Copy link
Author

Yes, I'm using the svcutil generated code. Can I manually provide a ChannelFactory there? Somewhere at initialization of the WCF Client?

@mconnew
Copy link
Member

mconnew commented Mar 12, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants