26

I am totally confused about this. I have looked around and can't seem to find a direct answer. I have a .proto file that my project, which has all been java, uses to create some messages.

There is a repeated Info field. Which is a type we created. When I generate the C# classes with protogen, this field comes up as read only and has no setter.

I can't fully build the message without this parameter. So my question is. Are repeated fields supposed to be generated like this and I am supposed to be accessing this read only List some other way? Or is this a bug in the generator?

Generated code:

private readonly global::System.Collections.Generic.List<StringMapEntry> _factoryProperty = new global::System.Collections.Generic.List<StringMapEntry>();
[global::ProtoBuf.ProtoMember(2, Name=@"factoryProperty", DataFormat = global::ProtoBuf.DataFormat.Default)]
public global::System.Collections.Generic.List<StringMapEntry> factoryProperty
{
  get { return _factoryProperty; }
}

Proto file section:

repeated StringMapEntry factoryProperty = 2;

I was probably just missing something really obvious. Thanks for any help!

Mimerr
  • 390
  • 1
  • 5
  • 14

2 Answers2

44

The list is not read only... You just mutate the list it gives you:

var order = new Order();
order.Lines.Add( new OrderLine {...} );

It is actually pretty common for sub-collections to be get-only. That doesn't mean you can't change the contents.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • The List was created as private readonly, I guess I am just not seeing how to mess with it. I will have to look at it later though, we used a different approach to the problem and I forgot about this. It is probably just me being blind. Thanks for the help though! If this is what it was I'll come back and mark it answered. – Mimerr May 23 '13 at 19:07
  • @user there should be a public property. Is there not? – Marc Gravell May 23 '13 at 19:43
  • I will update the main post with the code, I still haven't got a chance to check it out again though. – Mimerr May 24 '13 at 14:12
  • The `private readonly` on the `_factoryProperty` just means that even that class is not allowed to change that variable after its construction/initialization. That is, not allowed to change the variable `_factoryProperty` to anything other than that list. But the list itself is not readonly. – Jesse Chisholm Mar 30 '17 at 00:01
  • @JesseChisholm all true, but this feels like it should be a comment on the question? I don't even mention `_factoryProperty`... – Marc Gravell Mar 30 '17 at 05:39
  • @MarcGravell - oops! you are correct. I had clicked the wrong `add a comment` link. Sigh. – Jesse Chisholm Jul 07 '17 at 16:55
1

This was a new issue for us as well after updating our proto-net executable and related files. It was new behavior we hadn't experienced before.

After a little digging in csharp.xslt, we found the definition for 'repeated' fields:

<xsl:template match="FieldDescriptorProto[label='LABEL_REPEATED']">
    <xsl:variable name="type"><xsl:apply-templates select="." mode="type"/></xsl:variable>
    <xsl:variable name="format"><xsl:apply-templates select="." mode="format"/></xsl:variable>
    <xsl:variable name="field"><xsl:apply-templates select="." mode="field"/></xsl:variable>
    private <xsl:if test="not($optionXml)">readonly</xsl:if> global::System.Collections.Generic.List&lt;<xsl:value-of select="$type" />&gt; <xsl:value-of select="$field"/> = new global::System.Collections.Generic.List&lt;<xsl:value-of select="$type"/>&gt;();
    [<xsl:apply-templates select="." mode="checkDeprecated"/>global::ProtoBuf.ProtoMember(<xsl:value-of select="number"/>, Name=@"<xsl:value-of select="name"/>", DataFormat = global::ProtoBuf.DataFormat.<xsl:value-of select="$format"/><xsl:if test="options/packed='true'">, Options = global::ProtoBuf.MemberSerializationOptions.Packed</xsl:if>)]<!--
    --><xsl:if test="$optionDataContract">
    [global::System.Runtime.Serialization.DataMember(Name=@"<xsl:value-of select="name"/>", Order = <xsl:value-of select="number"/>, IsRequired = false)]
    </xsl:if><xsl:if test="$optionXml">
    [global::System.Xml.Serialization.XmlElement(@"<xsl:value-of select="name"/>", Order = <xsl:value-of select="number"/>)]
    </xsl:if>
    public global::System.Collections.Generic.List&lt;<xsl:value-of select="$type" />&gt; <xsl:call-template name="pascal"/>
    {
      get { return <xsl:value-of select="$field"/>; }<!--
      --><xsl:if test="$optionXml">
      set { <xsl:value-of select="$field"/> = value; }</xsl:if>
    }
  </xsl:template>

I've pulled out the specific parts for the private field and the setter:

private <xsl:if test="not($optionXml)">readonly</xsl:if> ...snip...

public ...snip...
{
  ...snip... 
  <!----><xsl:if test="$optionXml">
  set { <xsl:value-of select="$field"/> = value; }
  </xsl:if>
}

Notice the suspect conditions above for $optionXml. If you just remove those, the field is no longer readonly and the setter is properly generated.

So it then becomes: private ...snip...

public ...snip...
{
  ...snip... 
  set { <xsl:value-of select="$field"/> = value; }
}

Full 'fixed' template:

  <xsl:template match="FieldDescriptorProto[label='LABEL_REPEATED']">
    <xsl:variable name="type"><xsl:apply-templates select="." mode="type"/></xsl:variable>
    <xsl:variable name="format"><xsl:apply-templates select="." mode="format"/></xsl:variable>
    <xsl:variable name="field"><xsl:apply-templates select="." mode="field"/></xsl:variable>
    private global::System.Collections.Generic.List&lt;<xsl:value-of select="$type" />&gt; <xsl:value-of select="$field"/> = new global::System.Collections.Generic.List&lt;<xsl:value-of select="$type"/>&gt;();
    [<xsl:apply-templates select="." mode="checkDeprecated"/>global::ProtoBuf.ProtoMember(<xsl:value-of select="number"/>, Name=@"<xsl:value-of select="name"/>", DataFormat = global::ProtoBuf.DataFormat.<xsl:value-of select="$format"/><xsl:if test="options/packed='true'">, Options = global::ProtoBuf.MemberSerializationOptions.Packed</xsl:if>)]<!--
    --><xsl:if test="$optionDataContract">
    [global::System.Runtime.Serialization.DataMember(Name=@"<xsl:value-of select="name"/>", Order = <xsl:value-of select="number"/>, IsRequired = false)]
    </xsl:if><xsl:if test="$optionXml">
    [global::System.Xml.Serialization.XmlElement(@"<xsl:value-of select="name"/>", Order = <xsl:value-of select="number"/>)]
    </xsl:if>
    public global::System.Collections.Generic.List&lt;<xsl:value-of select="$type" />&gt; <xsl:call-template name="pascal"/>
    {
      get { return <xsl:value-of select="$field"/>; }
      set { <xsl:value-of select="$field"/> = value; }
    }
  </xsl:template>

I played with setting optionXml to false, but it didn't work and you may still want that option enabled anyway.

BihtSift
  • 31
  • 5
  • 1
    Not having the `set` just means you can't do `msg.field = null;` or `msg.field = otherList;` It doesn't prevent you from doing `msg.field.Clear();` or `msg.field.AddRange(otherList);`. – Jesse Chisholm Mar 30 '17 at 00:04
  • @JesseChisholm It's a pain with automatic mapping tools. – SHM Apr 16 '20 at 10:43