[WCF] Controlando minOccur e nillable no WSDL (-> XSD)

Recebi um questionamento de um cliente e minha resposta acabou tendo um desdobramento curioso que compartilho com vocês.

Problema e solução

A pergunta era mais ou menos assim: "Eu tenho um serviço que expõe um tipo complexo e quero alterar o minOccurs de um elemento para ser minOccurs=1. Porém o WSDL não está gerando o que eu estou pedindo, mesmo utilizando o atributo XMLElement na definição da propriedade".
Para ajudá-lo eu montei um projeto simples que expõe um serviço e defini um método que recebe como parâmetro o tipo complexo InfoPapel, que possui algumas propriedades, entre elas a seguinte (mesmos atributos informados pelo cliente, a classe eu inventei):

[XmlElement(IsNullable = false)]
        [DataMember]
        public string NomePapel
        {
            get { return nomePapel; }
            set { nomePapel = value; }
        }

Essa definição irá gerar a seguinte definição de esquema:

  <xs:complexType name="InfoPapel">
    <xs:sequence>
      <xs:element minOccurs="0" name="NomePapel" nillable="true" type="xs:string" />
      <xs:element name="Empresa" nillable="true" type="xs:string" />
    </xs:sequence>
  </xs:complexType>

O problema aqui está na utilização do XmlElement ao invés de utilizar a propriedade IsRequired do atributo DataMember. Para entendimento, o WCF possui três serializadores: XMLSerializer, DataContractSerializer, e NetDataContractSerializer (https://msdn.microsoft.com/en-us/magazine/cc163569.aspx), sendo que por padrão é utilizado o DataContractSerializer. Utilizando o padrão, o serializador vai ignorar o atributo XmlElement, não gerando um WSDL diferente, como esperado.

Se alterarmos nosso código para utilizar as propriedade do DataMember, poderemos controlar efetivamente o esquema gerado, conforme vemos abaixo:

[DataMember(IsRequired = true, Order = 1)]
        public string NomePapel
        {
            get { return nomePapel; }
            set { nomePapel = value; }
        }

  <xs:complexType name="InfoPapel">
    <xs:sequence>
      <xs:element name="NomePapel" nillable="true" type="xs:string" />
      <xs:element name="Empresa" nillable="true" type="xs:string" />
    </xs:sequence>
  </xs:complexType>

Veja que não temos mais o minOccurs="0" na definição do esquema e, sabendo que por padrão o minOccurs é igual a 1 (especificação do XSD), conseguimos o resultado esperado, que era um minOccurs="1".

XmlSerializerFormat

Se você quer utilizar o serializador XML (utilizado pelo ASP.NET web services), precisa colocar junto ao atributo ServiceContract , definido na interface, outro atributo:  XmlSerializerFormat. Utilizando esse atributo, quando o WCF for gerar o WSDL e XSDs relacionados, ele irá levar em conta o atributo XmlElement e ignorar o DataMember.

O que me chamou a atenção foi a ausência da propriedade IsNullable no atributo DataMember e a ausência da propriedade IsRequired no atributo XmlElement.

minOccurs vs. nillable

Se dermos uma olhada na especificação do XML Schema Definition (XSD) em https://www.w3.org/TR/xmlschema-0/, temos uma explicação clara das diferenças: em uma definição de esquema, um elemento pode aparecer de X a Y vezes, sendo controlado pelo minOccurs e maxOccurs, enquanto o nillable define um elemento que aparece no XML mas não possui nenhum valor, utilizando o atributo xsi:nil="true" no elemento. Note que dependendo do objetivo do XML que você está manipulando, pode existir diferenças semânticas na ausência de um elemento (minOccurs = 0) e no aparecimento de um elemento marcado como nulo. E aí?

Quando utilizamos o XmlElement e XmlSerializerFormat, conseguimos dois formatos de saída:

Quando “IsNullable = false” o atributo nillable desaparece e minOccurs = 0.
<xs:element minOccurs="0" maxOccurs="1" name="NomePapel" type="xs:string" />

Quando “IsNullable = true” o atributo nillable aparece e minOccurs = 1!
<xs:element minOccurs="1" maxOccurs="1" name="NomePapel" nillable="true" type="xs:string" />

No primeiro caso ou o elemento não aparece ou ele aparece com um valor definido. Note porém que no segundo caso, como o nulo deverá ser informado no XML como um atributo do elemento, o XSD está definindo o elemento com minOccurs=1, isto é, se existe a possibilidade dele ser nulo, o elemento têm que estar presente.

Já na utiliação do DataContractSerializer com o DataMember, conseguimos obter duas definições diferentes utilizando ou não o IsRequired:

<xs:element minOccurs="0" name="NomePapel" nillable="true" type="xs:string" /> (IsRequired = false)
<xs:element name="NomePapel" nillable="true" type="xs:string" /> (IsRequired = true)

Note que aqui não temos a definição do elemento tipo string para não aceitar nulos, por se tratar de um tipo por referência. Por outro lado, conseguimos ter um minOccurs="0" com nillable="true", o que não consegui através do XmlElement.

Não vi uma maneira fácil (nem sei é possível e se faz sentido) do DataMember definir um elemento string como nillable="false". Alguém sabe?

Finalizando...

Além do lado geek do post e da explicação dos serializers e atributos, fica uma pergunta: porque eu mencionei essa história do minOccurs e nillable? Esse post surgiu num contexto de interoperabilidade e o preciosismo pode até parecer inútil, mas eu acredito que quanto mais conhecermos os detalhes do que está acontecendo com nossos serviços, mais fácil será de resolvermos um eventual problema de interop. Vai que a gente cruza com um gerador de proxies mais enjoadinho...

 

[]s
Luciano Caixeta Moreira
luciano.moreira@microsoft.com
===============================================
This post is provided "AS IS" and confers no right
===============================================