OBSERVAÇÃO:este site foi descontinuado. O site será desativado após 31 de janeiro de 2023, e o tráfego será redirecionado para o novo site em https://protobuf.dev. Enquanto isso, as atualizações serão feitas apenas para protobuf.dev.

Código gerado por C#

Mantenha tudo organizado com as coleções Salve e categorize o conteúdo com base nas suas preferências.

Esta página descreve exatamente qual código C# o compilador de buffer de protocolo gera para as definições de protocolo usando a sintaxe proto3. Leia o guia do idioma proto3 antes de ler este documento.

Invocação do compilador

O compilador de buffer de protocolo produz a saída C# quando invocado com a sinalização de linha de comando --csharp_out. O parâmetro para a opção --csharp_out é o diretório em que você quer que o compilador grave a saída C#. No entanto, dependendo de outras opções, o compilador pode criar subdiretórios do diretório especificado. O compilador cria um único arquivo de origem para cada entrada de arquivo .proto, definindo uma extensão de .cs como padrão, mas configurável pelas opções do compilador.

Somente as mensagens proto3 são compatíveis com o gerador de código C#. Verifique se cada arquivo .proto começa com uma declaração de:

syntax = "proto3";

Opções específicas de C#

É possível fornecer mais opções de C# para o compilador de buffer de protocolo usando a sinalização de linha de comando --csharp_opt. Veja a seguir as opções aceitas:

  • file_extension: define a extensão de arquivo do código gerado. O padrão é .cs, mas uma alternativa comum é .g.cs para indicar que o arquivo contém o código gerado.
  • base_namespace: quando essa opção é especificada, o gerador cria uma hierarquia de diretórios para o código-fonte gerado correspondente aos namespaces das classes geradas, usando o valor da opção para indicar qual parte do namespace precisa ser considerada como a "base" para o diretório de saída. Por exemplo, com a seguinte linha de comando:
    protoc --proto_path=bar --csharp_out=src --csharp_opt=base_namespace=Example player.proto
    em que player.proto tem uma opção csharp_namespace de Example.Game, o compilador de buffer de protocolo gera um arquivo src/Game/Player.cs que está sendo criado. Essa opção geralmente corresponde à opção de namespace padrão em um projeto C# no Visual Studio. Se a opção for especificada, mas com um valor vazio, o namespace C# completo, conforme usado no arquivo gerado, será usado para a hierarquia do diretório. Se a opção não for especificada, os arquivos gerados serão simplesmente gravados no diretório especificado por --csharp_out sem criar nenhuma hierarquia.
  • internal_access: quando essa opção é especificada, o gerador cria tipos com o modificador de acesso internal em vez de public.
  • serializing: quando essa opção é especificada, o gerador adiciona o atributo [Serializable] às classes de mensagem geradas.

É possível especificar várias opções separando-as com vírgulas, como no exemplo a seguir:

protoc --proto_path=src --csharp_out=build/gen --csharp_opt=file_extension=.g.cs,base_namespace=Example,internal_access src/foo.proto

Estrutura do arquivo

O nome do arquivo de saída é derivado do nome de arquivo .proto, convertendo-o em Pascal-case, tratando os sublinhados como separadores de palavras. Por exemplo, um arquivo chamado player_record.proto vai resultar em um arquivo de saída chamado PlayerRecord.cs, em que a extensão pode ser especificada usando --csharp_opt, como mostrado acima.

Cada arquivo gerado tem a forma a seguir, em termos de membros públicos. A implementação não é mostrada aqui.

namespace [...]
{
  public static partial class [... descriptor class name ...]
  {
    public static FileDescriptor Descriptor { get; }
  }

  [... Enums ...]
  [... Message classes ...]
}

O namespace é inferido da package do proto, usando as mesmas regras de conversão que o nome do arquivo. Por exemplo, um pacote proto de example.high_score resultaria em um namespace de Example.HighScore. É possível modificar o namespace padrão gerado para um .proto específico usando a opção de arquivo csharp_namespace.

Cada tipo enumerado e mensagem de nível superior resulta em uma enumeração ou classe declarada como membros do namespace. Além disso, uma única classe parcial estática é sempre gerada para o descritor de arquivo. Isso é usado para operações baseadas em reflexão. A classe do descritor recebe o mesmo nome do arquivo, sem a extensão. No entanto, se houver uma mensagem com o mesmo nome (como é muito comum), a classe descritor será colocada em um namespace Proto aninhado para evitar colisões com a mensagem.

Como exemplo de todas essas regras, considere o arquivo timestamp.proto, que é fornecido como parte dos buffers de protocolo. Uma versão cortada de timestamp.proto tem esta aparência:

syntax = "proto3";
package google.protobuf;
option csharp_namespace = "Google.Protobuf.WellKnownTypes";

message Timestamp { ... }

O arquivo Timestamp.cs gerado tem a seguinte estrutura:

namespace Google.Protobuf.WellKnownTypes
{
  namespace Proto
  {
    public static partial class Timestamp
    {
      public static FileDescriptor Descriptor { get; }
    }
  }

  public sealed partial class Timestamp : IMessage<Timestamp>
  {
    [...]
  }
}

Mensagens

Se a declaração for simples:

message Foo {}

O compilador de buffer de protocolo gera uma classe parcial selada chamada Foo, que implementa a interface IMessage<Foo>, conforme mostrado abaixo com declarações de membro. Veja os comentários inline para mais informações.

public sealed partial class Foo : IMessage<Foo>
{
  // Static properties for parsing and reflection
  public static MessageParser<Foo> Parser { get; }
  public static MessageDescriptor Descriptor { get; }

  // Explicit implementation of IMessage.Descriptor, to avoid conflicting with
  // the static Descriptor property. Typically the static property is used when
  // referring to a type known at compile time, and the instance property is used
  // when referring to an arbitrary message, such as during JSON serialization.
  MessageDescriptor IMessage.Descriptor { get; }

  // Parameterless constructor which calls the OnConstruction partial method if provided.
  public Foo();
  // Deep-cloning constructor
  public Foo(Foo);
  // Partial method which can be implemented in manually-written code for the same class, to provide
  // a hook for code which should be run whenever an instance is constructed.
  partial void OnConstruction();

  // Implementation of IDeepCloneable<T>.Clone(); creates a deep clone of this message.
  public Foo Clone();

  // Standard equality handling; note that IMessage<T> extends IEquatable<T>
  public override bool Equals(object other);
  public bool Equals(Foo other);
  public override int GetHashCode();

  // Converts the message to a JSON representation
  public override string ToString();

  // Serializes the message to the protobuf binary format
  public void WriteTo(CodedOutputStream output);
  // Calculates the size of the message in protobuf binary format
  public int CalculateSize();

  // Merges the contents of the given message into this one. Typically
  // used by generated code and message parsers.
  public void MergeFrom(Foo other);

  // Merges the contents of the given protobuf binary format stream
  // into this message. Typically used by generated code and message parsers.
  public void MergeFrom(CodedInputStream input);
}

Todos esses membros estão sempre presentes. A opção optimize_for não afeta a saída do gerador de código C#.

Tipos aninhados

Uma mensagem pode ser declarada dentro de outra. Por exemplo:

message Foo {
  message Bar {
  }
}

Nesse caso, ou se uma mensagem contiver uma enumeração aninhada, o compilador vai gerar uma classe Types aninhada e, em seguida, uma classe Bar dentro da classe Types. Portanto, o código totalmente gerado será:

namespace [...]
{
  public sealed partial class Foo : IMessage<Foo>
  {
    public static partial class Types
    {
      public sealed partial class Bar : IMessage<Bar> { ... }
    }
  }
}

A classe Types intermediária é inconveniente, mas é necessária para lidar com o cenário comum de um tipo aninhado que tem um campo correspondente na mensagem. Caso contrário, você acabaria com uma propriedade e um tipo com o mesmo nome aninhado na mesma classe, o que seria um C# inválido.

Campos

O compilador de buffer de protocolo gera uma propriedade C# para cada campo definido em uma mensagem. A natureza exata da propriedade depende da natureza do campo: o tipo e se ele é singular, repetido ou um campo de mapa.

Singular

Qualquer campo singular gera uma propriedade de leitura/gravação. Um campo string ou bytes gerará um ArgumentNullException se um valor nulo for especificado. Buscar um valor de um campo que não foi definido explicitamente retornará uma string vazia ou ByteString. Os campos de mensagem podem ser definidos como valores nulos, o que apaga o campo. Isso não é equivalente a definir o valor como uma instância "vazia" do tipo de mensagem.

Campos repetidos

Cada campo repetido gera uma propriedade somente leitura do tipo Google.Protobuf.Collections.RepeatedField<T>, em que T é o tipo de elemento do campo. Na maioria dos casos, isso funciona como List<T>, mas tem uma sobrecarga Add para permitir que um conjunto de itens seja adicionado de uma só vez. Isso é conveniente ao preencher um campo repetido em um inicializador de objetos. Além disso, RepeatedField<T> tem suporte direto para serialização, desserialização e clonagem, mas isso geralmente é usado por código gerado em vez de código de aplicativo escrito manualmente.

Os campos repetidos não podem conter valores nulos, nem mesmo de tipos de mensagem, com exceção dos tipos de wrapper anuláveis explicados abaixo.

Campos do mapa

Cada campo do mapa gera uma propriedade somente leitura do tipo Google.Protobuf.Collections.MapField<TKey, TValue>, em que TKey é o tipo de chave do campo e TValue é o tipo de valor do campo. Na maioria dos casos, isso funciona como o Dictionary<TKey, TValue>, mas tem uma sobrecarga Add extra para permitir que outro dicionário seja adicionado de uma só vez. Isso é conveniente ao preencher um campo repetido em um inicializador de objetos. Além disso, MapField<TKey, TValue> tem suporte direto para serialização, desserialização e clonagem, mas isso geralmente é usado por código gerado em vez de código de aplicativo escrito manualmente. As chaves no mapa não podem ser nulas. Os valores poderão ser se o tipo de campo singular correspondente for compatível com valores nulos.

Oneof Fields

Cada campo em um deles tem uma propriedade diferente, como um campo singular normal. No entanto, o compilador também gera uma propriedade adicional para determinar qual campo no enum foi definido, com um enum e um método para limpar um deles. Por exemplo, para esta definição de campo "

oneof avatar {
  string image_url = 1;
  bytes image_data = 2;
}
".

O compilador gerará estes membros públicos:

enum AvatarOneofCase
{
  None = 0,
  ImageUrl = 1,
  ImageData = 2
}

public AvatarOneofCase AvatarCase { get; }
public void ClearAvatar();
public string ImageUrl { get; set; }
public ByteString ImageData { get; set; }

Se uma propriedade for o "caso" atual, a busca dela retornará o valor definido para ela. Caso contrário, a busca da propriedade retornará o valor padrão para o tipo de propriedade: somente um membro de um de um pode ser definido por vez.

Definir qualquer propriedade constituinte do um mudará o "caso" informado. Assim como em um campo singular normal, não é possível definir um único campo com um tipo string ou bytes como um valor nulo. Definir um campo de tipo de mensagem como nulo é equivalente a chamar o método Clear específico.

Campos do tipo de wrapper

A maioria dos tipos conhecidos no proto3 não afeta a geração de código, mas os tipos de wrapper (StringWrapper, Int32Wrapper etc.) alteram o tipo e o comportamento das propriedades.

Todos os tipos de wrapper que correspondem aos tipos de valor C# (Int32Wrapper, DoubleWrapper, BoolWrapper etc.) são mapeados para Nullable<T>, em que T é o tipo não anulável correspondente. Por exemplo, um campo do tipo DoubleValue resulta em uma propriedade C# do tipo Nullable<double>.

Os campos de tipo StringWrapper ou BytesWrapper resultam na geração de propriedades C# do tipo string e ByteString, mas com um valor padrão de nulo e permitindo que nulo seja definido como o valor da propriedade.

Para todos os tipos de wrapper, valores nulos não são permitidos em um campo repetido, mas são permitidos como valores para entradas de mapa.

Enumerações

Considerando uma definição de enumeração como a seguinte:

enum Color {
  COLOR_RED = 0;
  COLOR_GREEN = 5;
  COLOR_BLUE = 1234;
}

O compilador de buffer de protocolo gerará um tipo de enumeração C# chamado Color com o mesmo conjunto de valores. Os nomes dos valores de enumeração são convertidos para torná-los mais idiomáticos para os desenvolvedores de C#:

  • Se o nome original começa com a forma maiúscula do próprio nome da enumeração, ele é removido.
  • O resultado é convertido para o padrão Pascal Case.
Portanto, o proto Color acima seria o seguinte código C#:
enum Color
{
  Red = 0,
  Green = 5,
  Blue = 1234
}

Essa transformação de nome não afeta o texto usado na representação JSON das mensagens.

O idioma .proto permite que vários símbolos de enumeração tenham o mesmo valor numérico. Símbolos com o mesmo valor numérico são sinônimos. Eles são representados em C# da mesma forma, com vários nomes correspondentes ao mesmo valor numérico.

Uma enumeração não aninhada faz com que uma enumeração C# seja gerada como um novo membro do namespace. Uma enumeração aninhada faz com que uma enumeração C# seja gerada na classe aninhada Types dentro da classe correspondente à mensagem em que a enumeração está aninhada.

Serviços

O gerador de código C# ignora completamente os serviços.