3

What is the name of the syntax used in the Headers property? Headers is defined as public HttpRequestHeaders Headers {get;}. It hurts my head that the left side of the expression is not a setter.


I'm not finding it in hidden features of c# or History of C#

var tokenRequest = new HttpRequestMessage
{
    Method = HttpMethod.Post,
    RequestUri = new Uri("http://localhost"),
    Headers = {
        { HttpRequestHeader.Authorization.ToString(), "abc123" },
        { HttpRequestHeader.ContentType.ToString(), "application/x-www-form-urlencoded" }
    },
    Content = new FormUrlEncodedContent(new Dictionary<string, string> { ["grant_type"] = "client_credentials" })
};
JJS
  • 6,431
  • 1
  • 54
  • 70
  • why would it be a setter, this is simply an initialization list setting the Headers property – pm100 Mar 02 '22 at 21:33
  • 4
    You are probably looking for this page: [Object Initializers with collection read-only property initialization](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/object-and-collection-initializers#object-initializers-with-collection-read-only-property-initialization) – UnholySheep Mar 02 '22 at 21:35
  • Does it "hurt your head" that `readonly int foo = 5;` is valid, even though it's "readonly"? Same concept. – Kirk Woll Mar 02 '22 at 21:45
  • Does this even compile? What type of object is being assigned to Headers? – Nigel Mar 02 '22 at 22:07
  • @KirkWoll readonly int foo = 5; - no this is a field being assigned to during class initialization (before construction). Setting a field is ok to me. Disagree that it's the same, because the property here has no setter. – JJS Mar 03 '22 at 18:47
  • @NigelBess yes. It does compile. – JJS Mar 03 '22 at 18:48
  • @JJS, yes, but auto-properties are backed by compiler generated fields, and the restrictions on only being able to set it when the instance is initialized is precisely the same as readonly fields. Also, it's not happening "before construction" but as a part of it -- you could also initialize these fields _within_ the constructor. Beyond that, if it feels weird to you, I can't stand in your way. :p – Kirk Woll Mar 03 '22 at 20:04
  • @UnholySheep agreed. I believe you're right. This is an example of an IEnumerable property that exposes an Add(T item) method. – JJS Mar 03 '22 at 21:09
  • > this is simply an initialization list setting the Headers property this is the answer I'm looking for ;) My brain never caught this as a case handled by collection initializer syntax. – JJS Mar 03 '22 at 21:10

2 Answers2

2

This is Object Initializers with collection read-only property initialization.

It hurts my head that the left side of the expression is not a setter.

Using a collection initializer you can set the value of a property during the constructor even with no set defined. Specifically for a collection, the documentation linked above says this:

Collection initializers let you specify one or more element initializers when you initialize a collection type that implements IEnumerable and has Add with the appropriate signature as an instance method or an extension method.

This can be really strange (or cool, depending on how you look at it), because you can build your own Add() extension method for almost any IEnumerable type and use it to do some really interesting things.

Also remember, when using a property that is also a collection or other property, you do not need a set to call methods or change property values within this object or collection. Instead, you first get the reference to the collection, and then use whatever access is provided by that nested property.

For example, let's say you have a variable tokenRequest that has a property Headers of type HttpRequestHeaders. The HttpRequestHeaders type in turn has an Add() method. To call that method you first get the reference to the Headers property and the call method on that reference. No set is ever used or needed, and yet you still managed to change a property that only has get defined.

Collection initializers take advantage of this.

JJS
  • 6,431
  • 1
  • 54
  • 70
Joel Coehoorn
  • 399,467
  • 113
  • 570
  • 794
  • *'When using an object initializer, you can set the value or property during the constructor even with no set defined.'* This explanation is not correct. Set is not being called. As explained in the "Object Initializers with collection read-only property initialization" section of the document you linked, leaving out `new` in the collection initializer uses the getter to the collection to call `Add` for each item in the braces – Nigel Mar 02 '22 at 22:21
  • @NigelBess Yes, I know, I'm fixing it. I re-read the initial post and realized it was misleading. It's slightly better now, but still more edits coming in. – Joel Coehoorn Mar 02 '22 at 22:24
  • @JoelCoehoorn suggestion: since the article this heading, "Object Initializers with collection read-only property initialization", would you mind changing the link to that title, and linking to that heading? https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/object-and-collection-initializers#object-initializers-with-collection-read-only-property-initialization – JJS Mar 03 '22 at 22:36
1

I'm not sure if there's an official name for it, but I've seen it being called collection initializer "duck typing", done implicitly by the compiler. The compiler will look for a suitable Add method implemented by the type, as well as the type has to implement the IEnumerable interface. HttpRequestMessage.Headers ultimately fits both these criteria, and even though the property has only a getter, the compiler will translate the collection initializer into consecutive "Add" calls right after the object has been created.

  • 1
    Wow that is some horrible design. That means the left hand side of the `=` is susceptible to a `NullReferenceException`. Weird. – Nigel Mar 02 '22 at 22:13
  • @NigelBess Only if you have a custom `Add()` that throws the NullReference. Otherwise, the object initializer syntax is able to know an object is defined for each set of braces, so there's always a value there to pass to the `Add()` method. – Joel Coehoorn Mar 02 '22 at 22:33
  • @JoelCoehoorn Im talking about the left-hand-side of the `=` being null. Even if the collection is something simple like `List`, if the list is not initialized by the time the constructor completes there will be no list to which to add, and a `NullReferenceException` gets thrown – Nigel Mar 02 '22 at 23:40
  • @NigelBess The source (at the time of this comment) seems to initialize the property backing field to a new dictionary if it is null. [github source](https://github.com/microsoft/referencesource/blob/5697c29004a34d80acdaf5742d7e699022c64ecd/System/net/System/Net/Http/HttpRequestMessage.cs#L98) – Johan van Tonder Mar 03 '22 at 07:28
  • 2
    @JohanvanTonder: that's true *for this particular type*; in the general case Nigel is correct that a seemingly innocuous expression like `new C { X = { 4 } }` can throw an NRE, with `class C { public List X; }`. Of course this is generally not a major problem because it would be rare to leave collection properties uninitialized, and indeed because of other scenarios where NREs lurk this is hardly the only pitfall. It can still be surprising (or distasteful :)) if you're not familiar with what this syntax actually does. – Jeroen Mostert Mar 03 '22 at 14:47