Compartilhar via


Substitutos de contrato de dados

O substituto de contrato de dados é um recurso avançado criado com base no modelo de contrato de dados. Ele foi desenvolvido para uso na personalização e substituição de tipos em situações em que os usuários desejam alterar a maneira como um tipo é serializado, desserializado ou projetado nos metadados. Um substituto pode ser usado quando um contrato de dados não foi especificado para o tipo, campos e propriedades não estão marcados com o atributo DataMemberAttribute ou os usuários desejam criar variações de esquema dinamicamente.

A serialização e a desserialização são realizadas com o substituto do contrato de dados por meio do uso de DataContractSerializer a fim de realizar a conversão do .NET Framework para um formato adequado, como XML. O substituto do contrato de dados também pode ser usado para modificar os metadados exportados para tipos, ao produzir representações de metadados, como XSD (XML Schema Documents). Na importação, o código é criado com base nos metadados e o substituto também pode ser usado para personalizar o código gerado.

Como o substituto funciona

Um substituto funciona mapeando um tipo (o tipo "original") para outro (o tipo "substituído"). O exemplo a seguir mostra o tipo original Inventory e um novo tipo substituto InventorySurrogated. O tipo Inventory não é serializável, mas o tipo InventorySurrogated é:

public class Inventory
{
    public int pencils;
    public int pens;
    public int paper;
}

Como um contrato de dados não foi definido para essa classe, converta-a em uma classe substituta com um contrato de dados. A classe substituta é mostrada no seguinte exemplo:

[DataContract(Name = "Inventory")]
public class InventorySurrogated
{
    [DataMember]
    public int numpencils;
    [DataMember]
    public int numpaper;
    [DataMember]
    private int numpens;

    public int pens
    {
        get { return numpens; }
        set { numpens = value; }
    }
}

Implementação do IDataContractSurrogate

Para usar o substituto do contrato de dados, implemente a interface IDataContractSurrogate.

Veja a seguir uma visão geral de cada método de IDataContractSurrogate com uma possível implementação.

GetDataContractType

O método GetDataContractType mapeia um tipo para outro. Ele é necessário para serialização, desserialização, importação e exportação.

A primeira tarefa é definir quais tipos serão mapeados para outros. Por exemplo:

public Type GetDataContractType(Type type)
{
    Console.WriteLine("GetDataContractType");
    if (typeof(Inventory).IsAssignableFrom(type))
    {
        return typeof(InventorySurrogated);
    }
    return type;
}
  • Na serialização, o mapeamento retornado por esse método é usado posteriormente para transformar a instância original em uma instância substituta chamando o método GetObjectToSerialize.

  • Na desserialização, o mapeamento retornado por esse método é usado pelo serializador para a desserialização em uma instância do tipo substituto. Em seguida, ele chama GetDeserializedObject para transformar a instância substituta em uma instância do tipo original.

  • Na exportação, o tipo substituto retornado por esse método é refletido a fim de obter o contrato de dados a ser usado para gerar metadados.

  • Na importação, o tipo inicial é alterado para um tipo substituto, que é refletido a fim de que o contrato de dados seja usado em fins como o suporte de referência.

O parâmetro Type é o tipo do objeto que está sendo serializado, desserializado, importado ou exportado. O método GetDataContractType deverá retornar o tipo de entrada se o substituto não manipular o tipo. Caso contrário, retorne o tipo substituto apropriado. Se houver diversos tipos substitutos, diversos mapeamentos poderão ser definidos no método.

O método GetDataContractType não é chamado para primitivos de contrato de dados internos, como Int32 ou String. Para outros tipos, como matrizes, tipos definidos pelo usuário e outras estruturas de dados, esse método será chamado individualmente.

No exemplo anterior, o método verifica se o parâmetro type e Inventory são comparáveis. Em caso afirmativo, o método faz o mapeamento para InventorySurrogated. Sempre que é feita uma chamada para uma serialização, uma desserialização, um esquema de importação ou um esquema de exportação, essa função é chamada primeiro para determinar o mapeamento entre os tipos.

Método GetObjectToSerialize

O método GetObjectToSerialize converte a instância de tipo original na instância de tipo substituto. O método é necessário para a serialização.

O próximo passo é definir a forma como os dados físicos serão mapeados da instância original para a substituta implementando o método GetObjectToSerialize. Por exemplo:

public object GetObjectToSerialize(object obj, Type targetType)
{
    Console.WriteLine("GetObjectToSerialize");
    if (obj is Inventory)
    {
        InventorySurrogated isur = new InventorySurrogated();
        isur.numpaper = ((Inventory)obj).paper;
        isur.numpencils = ((Inventory)obj).pencils;
        isur.pens = ((Inventory)obj).pens;
        return isur;
    }
    return obj;
}

O método GetObjectToSerialize é chamado quando um objeto é serializado. Ele transfere dados do tipo original para os campos do tipo substituto. Os campos podem ser mapeados diretamente para campos substitutos ou manipulações dos dados originais podem ser armazenadas no substituto. Alguns usos possíveis incluem: mapear diretamente os campos, realizar operações nos dados a serem armazenados nos campos substitutos ou armazenar o XML do tipo original no campo substituto.

O parâmetro targetType refere-se ao tipo declarado do membro. Esse parâmetro é o tipo substituto retornado pelo método GetDataContractType. O serializador não impõe que o objeto retornado seja atribuível a esse tipo. O parâmetro obj é o objeto a ser serializado e será convertido em seu substituto, se necessário. Esse método deverá retornar o objeto de entrada se o substituto não manipular o objeto. Caso contrário, o novo objeto substituto será retornado. O substituto não será chamado se o objeto for nulo. Diversos mapeamentos substitutos para diferentes instâncias podem ser definidos dentro deste método.

Ao criar um DataContractSerializer, é possível instruí-lo a preservar as referências de objeto. (Para saber mais, confira Serialização e desserialização). Para isso, defina o preserveObjectReferences parâmetro no respectivo construtor como true. Nesse caso, o substituto é chamado somente uma vez para um objeto, pois todas as serializações subsequentes apenas gravam a referência no fluxo. Se preserveObjectReferences for definido como false, o substituto será chamado sempre que uma instância for encontrada.

Se o tipo da instância serializada for diferente do tipo declarado, as informações de tipo serão gravadas no fluxo, por exemplo, xsi:type, para permitir que a instância seja desserializada na outra extremidade. Esse processo ocorre para todos os objetos, sejam eles substitutos ou não.

O exemplo acima converte os dados da instância Inventory nos dados de InventorySurrogated. Ele verifica o tipo do objeto e executa as manipulações necessárias para a conversão no tipo substituto. Nesse caso, os campos da classe Inventory são copiados diretamente para os da classe InventorySurrogated.

Método GetDeserializedObject

O método GetDeserializedObject converte a instância de tipo substituto na instância de tipo original. Ele é necessário para a desserialização.

A próxima tarefa é definir a maneira como os dados físicos serão mapeados da instância substituta para a original. Por exemplo:

public object GetDeserializedObject(object obj, Type targetType)
{
    Console.WriteLine("GetDeserializedObject");
    if (obj is InventorySurrogated)
    {
        Inventory invent = new Inventory();
        invent.pens = ((InventorySurrogated)obj).pens;
        invent.pencils = ((InventorySurrogated)obj).numpencils;
        invent.paper = ((InventorySurrogated)obj).numpaper;
        return invent;
    }
    return obj;
}

Esse método é chamado somente durante a desserialização de um objeto. Ele fornece mapeamento de dados reverso para a desserialização do tipo substituto de volta para o tipo original. Assim como no método GetObjectToSerialize, alguns usos possíveis seriam a troca direta de dados de campo, a realização de operações nos dados e o armazenamento de dados XML. Ao realizar a desserialização, nem sempre é possível obter os valores de dados exatos do original devido a manipulações na conversão de dados.

O parâmetro targetType refere-se ao tipo declarado do membro. Esse parâmetro é o tipo substituto retornado pelo método GetDataContractType. O parâmetro obj refere-se ao objeto que foi desserializado. O objeto poderá ser convertido novamente no tipo original se for substituído. Esse método retornará o objeto de entrada se o substituto não manipular o objeto. Caso contrário, o objeto desserializado será retornado após a conclusão da conversão. Se houver diversos tipos substitutos, será possível fornecer a conversão de dados de substituto para tipo primário individualmente, indicando cada tipo e a respectiva conversão.

Ao retornar um objeto, as tabelas de objetos internas são atualizadas com o objeto retornado pelo substituto. Quaisquer referências subsequentes a uma instância obterão a instância substituta das tabelas de objetos.

O exemplo anterior converte objetos do tipo InventorySurrogated de volta para o tipo inicial Inventory. Nesse caso, os dados são transferidos diretamente de InventorySurrogated para os campos correspondentes em Inventory. Como não há manipulações de dados, cada campo de membro conterá os mesmos valores de antes da serialização.

Método GetCustomDataToExport

Ao exportar um esquema, o método GetCustomDataToExport é opcional. Ele é usado para inserir dicas ou dados adicionais no esquema exportado. Dados adicionais podem ser inseridos no nível do membro ou do tipo. Por exemplo:

public object GetCustomDataToExport(System.Reflection.MemberInfo memberInfo, Type dataContractType)
{
    Console.WriteLine("GetCustomDataToExport(Member)");
    System.Reflection.FieldInfo fieldInfo = (System.Reflection.FieldInfo)memberInfo;
    if (fieldInfo.IsPublic)
    {
        return "public";
    }
    else
    {
        return "private";
    }
}

Esse método (com duas sobrecargas) permite a inclusão de informações extras nos metadados, tanto no nível do membro quanto do tipo. É possível incluir dicas sobre, por exemplo, se um membro é público ou privado, e comentários que seriam preservados durante a exportação e a importação do esquema. Sem esse método, essas informações seriam perdidas. Ele não causa a inserção ou exclusão de membros ou tipos, mas adiciona dados aos esquemas em qualquer um desses níveis.

O método tem sobrecarga e pode utilizar Type (parâmetro clrtype) ou MemberInfo (parâmetro memberInfo). O segundo parâmetro é sempre Type (parâmetro dataContractType). Esse método é chamado para cada membro e tipo do tipo substituto dataContractType.

Qualquer uma dessas sobrecargas deve retornar null ou um objeto serializável. Um objeto não nulo será serializado como anotação no esquema exportado. No caso da sobrecarga Type, cada tipo exportado para o esquema é enviado a esse método no primeiro parâmetro com o tipo substituto como o parâmetro dataContractType. No caso da sobrecarga MemberInfo, cada membro exportado para o esquema envia as respectivas informações como o parâmetro memberInfo com o tipo substituto no segundo parâmetro.

Método GetCustomDataToExport (Type, Type)

O método IDataContractSurrogate.GetCustomDataToExport(Type, Type) é chamado durante a exportação do esquema para cada definição de tipo. O método adiciona informações aos tipos dentro do esquema durante a exportação. Cada tipo definido é enviado a esse método para determinar se há dados adicionais que precisam ser incluídos no esquema.

Membro GetCustomDataToExport (MemberInfo, Type)

O IDataContractSurrogate.GetCustomDataToExport(MemberInfo, Type) é chamado durante a exportação para cada membro nos tipos que são exportados. Essa função permite personalizar quaisquer comentários sobre os membros que serão incluídos no esquema após a exportação. As informações de cada membro da classe são enviadas a esse método para verificar se algum dado adicional precisa ser adicionado ao esquema.

O exemplo acima pesquisa no dataContractType cada membro do substituto. Em seguida, ele retorna o modificador de acesso apropriado para cada campo. Sem essa personalização, o valor padrão para modificadores de acesso é público. Portanto, todos os membros seriam definidos como públicos no código gerado usando o esquema exportado, independentemente de suas restrições de acesso reais. Ao não usar essa implementação, o membro numpens seria público no esquema exportado, embora tenha sido definido no substituto como privado. Através do uso deste método, no esquema exportado, o modificador de acesso pode ser gerado como privado.

Método GetReferencedTypeOnImport

Este método mapeia o Type do substituto para o tipo original. Ele é opcional para a importação de esquemas.

Ao criar um substituto que importa um esquema e gera um código para ele, a próxima tarefa é definir o tipo de uma instância substituta para o respectivo tipo original.

Se o código gerado precisar fazer referência a um tipo de usuário existente, isso será feito implementando o método GetReferencedTypeOnImport.

Ao importar um esquema, esse método é chamado para cada declaração de tipo a fim de mapear o contrato de dados substituto para um tipo. Os parâmetros de cadeia de caracteres typeName e typeNamespace definem o nome e o namespace do tipo substituto. O valor de retorno de GetReferencedTypeOnImport é usado para determinar se um novo tipo precisa ser gerado. Esse método deve retornar um tipo válido ou nulo. Para tipos válidos, o tipo retornado será usado como tipo referenciado no código gerado. Se null for retornado, nenhum tipo será referenciado e um novo tipo deverá ser criado. Se houver diversos substitutos, será possível realizar o mapeamento de cada tipo substituto para o respectivo tipo inicial.

O parâmetro customData é o objeto originalmente retornado por GetCustomDataToExport. Este customData é usado quando autores substitutos desejam inserir dados/dicas extras nos metadados para uso durante a importação a fim de gerar códigos.

Método ProcessImportedType

O método ProcessImportedType personaliza qualquer tipo criado com base na importação de esquema. Esse método é opcional.

Ao importar um esquema, ele permite que qualquer tipo importado e as informações de compilação sejam personalizados. Por exemplo:

public System.CodeDom.CodeTypeDeclaration ProcessImportedType(System.CodeDom.CodeTypeDeclaration typeDeclaration, System.CodeDom.CodeCompileUnit compileUnit)
{
    Console.WriteLine("ProcessImportedType");
    foreach (CodeTypeMember member in typeDeclaration.Members)
    {
        object memberCustomData = member.UserData[typeof(IDataContractSurrogate)];
        if (memberCustomData != null
          && memberCustomData is string
          && ((string)memberCustomData == "private"))
        {
            member.Attributes = ((member.Attributes & ~MemberAttributes.AccessMask) | MemberAttributes.Private);
        }
    }
    return typeDeclaration;
}

Durante a importação, esse método é chamado para cada tipo gerado. Altere o CodeTypeDeclaration especificado ou modifique o CodeCompileUnit. Isso inclui alterar o nome, os membros, os atributos e muitas outras propriedades de CodeTypeDeclaration. Ao processar CodeCompileUnit, é possível modificar as diretivas, os namespaces, os assemblies referenciados e diversos outros aspectos.

O parâmetro CodeTypeDeclaration contém a declaração de tipo DOM de código. O parâmetro CodeCompileUnit permite a modificação para processar o código. Retornar null resulta no descarte da declaração de tipo. Por outro lado, ao retornar CodeTypeDeclaration, as modificações são preservadas.

Se dados personalizados forem inseridos durante a exportação de metadados, será preciso fornecê-los ao usuário durante a importação para que sejam usados. Os usos possíveis para esses dados personalizados incluem a programação de dicas de modelo ou de outros comentários. Cada instância CodeTypeDeclaration e CodeTypeMember inclui dados personalizados como a propriedade UserData, convertidos no tipo IDataContractSurrogate.

O exemplo acima realiza algumas mudanças no esquema importado. O código preserva membros privados do tipo original usando um substituto. O modificador de acesso padrão ao importar um esquema é public. Portanto, todos os membros do esquema substituto serão públicos, a menos que sejam modificados, como neste exemplo. Durante a exportação, os dados personalizados são inseridos nos metadados sobre quais membros são privados. O exemplo pesquisa os dados personalizados, verifica se o modificador de acesso é privado e modifica o membro apropriado para privado definindo os respectivos atributos. Sem essa personalização, o membro numpens seria definido como público em vez de privado.

Método GetKnownCustomDataTypes

Este método obtém os tipos de dados personalizados definidos no esquema. O método é opcional para a importação de esquemas.

Ele é chamado no início da importação/exportação do esquema. O método retorna os tipos de dados personalizados usados no esquema exportado ou importado. Ele recebe um Collection<T> (o parâmetro customDataTypes), que é uma coleção de tipos. O método deve adicionar tipos conhecidos adicionais a esta coleção. Os tipos de dados personalizados conhecidos são necessários para habilitar a serialização e desserialização de dados personalizados usando DataContractSerializer. Para saber mais, confira Tipos de contrato de dados conhecidos.

Implementação de um substituto

Para usar o substituto de contrato de dados no WCF, é necessário seguir alguns procedimentos especiais.

Para usar um substituto para serialização e desserialização

Use DataContractSerializer para realizar a serialização e a desserialização de dados com o substituto. O DataContractSerializer é criado pelo DataContractSerializerOperationBehavior. O substituto também deve ser especificado.

Para implementar a serialização e a desserialização
  1. Crie uma instância do ServiceHost para seu serviço. Para obter instruções completas, confira Programação básica do WCF.

  2. Para cada ServiceEndpoint do host de serviço especificado, encontre o respectivo OperationDescription.

  3. Pesquise os comportamentos de operação para determinar se uma instância do DataContractSerializerOperationBehavior foi encontrada.

  4. Se um DataContractSerializerOperationBehavior for encontrado, defina sua propriedade DataContractSurrogate para uma nova instância do substituto. Se nenhum DataContractSerializerOperationBehavior for encontrado, crie uma nova instância e defina o membro DataContractSurrogate do novo comportamento para uma nova instância do substituto.

  5. Por fim, adicione esse novo comportamento aos comportamentos de operação atuais, conforme mostrado no seguinte exemplo:

    using (ServiceHost serviceHost = new ServiceHost(typeof(InventoryCheck)))
        foreach (ServiceEndpoint ep in serviceHost.Description.Endpoints)
        {
            foreach (OperationDescription op in ep.Contract.Operations)
            {
                DataContractSerializerOperationBehavior dataContractBehavior =
                    op.Behaviors.Find<DataContractSerializerOperationBehavior>()
                    as DataContractSerializerOperationBehavior;
                if (dataContractBehavior != null)
                {
                    dataContractBehavior.DataContractSurrogate = new InventorySurrogated();
                }
                else
                {
                    dataContractBehavior = new DataContractSerializerOperationBehavior(op);
                    dataContractBehavior.DataContractSurrogate = new InventorySurrogated();
                    op.Behaviors.Add(dataContractBehavior);
                }
            }
        }
    

Para usar um substituto para a importação de metadados

Durante a importação de metadados como WSDL e XSD para gerar códigos do lado do cliente, o substituto precisa ser adicionado ao componente responsável por gerar o código do esquema XSD, XsdDataContractImporter. Para isso, modifique diretamente o WsdlImporter usado para a importação de metadados.

Para implementar um substituto para a importação de metadados
  1. Importe os metadados usando a classe WsdlImporter.

  2. Use o método TryGetValue para verificar se um XsdDataContractImporter foi definido.

  3. Se o método TryGetValue retornar false, crie um novo XsdDataContractImporter e defina a respectiva propriedade Options como uma nova instância da classe ImportOptions. Caso contrário, use o importador retornado pelo parâmetro out do método TryGetValue.

  4. Se o XsdDataContractImporter não tiver um ImportOptions definido, configure a propriedade para ser uma nova instância da classe ImportOptions.

  5. Defina a propriedade DataContractSurrogate do ImportOptions de XsdDataContractImporter para uma nova instância do substituto.

  6. Adicione XsdDataContractImporter à coleção retornada pela propriedade State do WsdlImporter (herdado da classe MetadataExporter.)

  7. Use o método ImportAllContracts do WsdlImporter para importar todos os contratos de dados dentro do esquema. Durante a última etapa, o código é gerado com base nos esquemas carregados por meio de uma chamada ao substituto.

    MetadataExchangeClient mexClient = new MetadataExchangeClient(metadataAddress);
    mexClient.ResolveMetadataReferences = true;
    MetadataSet metaDocs = mexClient.GetMetadata();
    WsdlImporter importer = new WsdlImporter(metaDocs);
    object dataContractImporter;
    XsdDataContractImporter xsdInventoryImporter;
    if (!importer.State.TryGetValue(typeof(XsdDataContractImporter),
        out dataContractImporter))
        xsdInventoryImporter = new XsdDataContractImporter();
    
    xsdInventoryImporter = (XsdDataContractImporter)dataContractImporter;
    xsdInventoryImporter.Options ??= new ImportOptions();
    xsdInventoryImporter.Options.DataContractSurrogate = new InventorySurrogated();
    importer.State.Add(typeof(XsdDataContractImporter), xsdInventoryImporter);
    
    Collection<ContractDescription> contracts = importer.ImportAllContracts();
    

Usar um substituto para a importação de metadados

Por padrão, ao exportar metadados do WCF para um serviço, os esquemas WSDL e XSD precisam ser gerados. O substituto precisa ser adicionado ao componente responsável por gerar o esquema XSD para tipos de contrato de dados, XsdDataContractExporter. Para isso, use um comportamento que implemente o IWsdlExportExtension a fim de modificar o WsdlExporter ou modifique diretamente o WsdlExporter usado para exportar metadados.

Usar um substituto para a exportação de metadados
  1. Crie um novo WsdlExporter ou use o parâmetro wsdlExporter transmitido ao método ExportContract.

  2. Use a função TryGetValue para verificar se um XsdDataContractExporter foi definido.

  3. Se TryGetValue retornar false, crie um novo XsdDataContractExporter com os esquemas XML gerados com base em WsdlExporter e adicione-o à coleção retornada pela propriedade State do WsdlExporter. Caso contrário, use o exportador retornado pelo parâmetro out do método TryGetValue.

  4. Se o XsdDataContractExporter não tiver um ExportOptions definido, defina a propriedade Options para uma nova instância da classe ExportOptions.

  5. Defina a propriedade DataContractSurrogate do ExportOptions de XsdDataContractExporter para uma nova instância do substituto. As etapas subsequentes para a exportação de metadados não exigem nenhuma alteração.

    WsdlExporter exporter = new WsdlExporter();
    //or
    //public void ExportContract(WsdlExporter exporter,
    // WsdlContractConversionContext context) { ... }
    object dataContractExporter;
    XsdDataContractExporter xsdInventoryExporter;
    if (!exporter.State.TryGetValue(typeof(XsdDataContractExporter),
        out dataContractExporter))
    {
        xsdInventoryExporter = new XsdDataContractExporter(exporter.GeneratedXmlSchemas);
    }
    else
    {
        xsdInventoryExporter = (XsdDataContractExporter)dataContractExporter;
    }
    
    exporter.State.Add(typeof(XsdDataContractExporter), xsdInventoryExporter);
    
    if (xsdInventoryExporter.Options == null)
        xsdInventoryExporter.Options = new ExportOptions();
    xsdInventoryExporter.Options.DataContractSurrogate = new InventorySurrogated();
    

Confira também