Can I use a collection initializer for an Attribute?
Asked Answered
A

3

7

Can an attribute in C# be used with a collection initializer?

For example, I'd like to do something like the following:

[DictionaryAttribute(){{"Key", "Value"}, {"Key", "Value"}}]
public class Foo { ... }

I know attributes can have named parameters, and since that seems pretty similar to object initializers, I was wondering if collection initializers were available as well.

Absenteeism answered 3/8, 2011 at 20:4 Comment(2)
Scott, Im sorry - my answer is failed. I dont earn up vote. Please vote down cos its fair play.Ruth
@vladimir77 - You shouldn't have deleted your answer! Sometimes the wrong answer is still helpful ... just update your answer next time, so that people can understand why it's wrong.Absenteeism
R
6

Update: I'm sorry I'm mistaken - pass array of custom type is impossible :(

The types of positional and named parameters for an attribute class are limited to the attribute parameter types, which are:

  1. One of the following types: bool, byte, char, double, float, int, long, short, string.
  2. The type object.
  3. The type System.Type.
  4. An Enum type, provided it has public accessibility and the types in which it is nested (if any) also have public accessibility (Section 17.2).
  5. Single-dimensional arrays of the above types. source

Source: stackoverflow.

You CAN DECLARE passing an array of a custom type:

class TestType
{
  public int Id { get; set; }
  public string Value { get; set; }

  public TestType(int id, string value)
  {
    Id = id;
    Value = value;
  }
}

class TestAttribute : Attribute
{
  public TestAttribute(params TestType[] array)
  {
    //
  }
}

but compilation errors occur on the attribute declaration:

[Test(new[]{new TestType(1, "1"), new TestType(2, "2"), })]
public void Test()
{

}
Ruth answered 3/8, 2011 at 20:27 Comment(3)
Since you have the params in there, you can leave out the new[]{...} for a much nicer syntax. However, are you sure this compiles? I think attribute arguments have limits -- constant expression, typeof expression, or array creation expression -- as was mentioned by @Jason Down.Absenteeism
@Scott: that is an array creation expression. :) (By the way, I'll delete my answer; as Jason pointed out it's no good.)Markowitz
Thanks for the update, and for pointing out the great MSDN reference!Absenteeism
A
6

Section 17.1.3 of the C# 4.0 specification specifically does not allow for multidimensional arrays inside the attribute parameters, so while Foo(string[,] bar) might allow you to call Foo(new [,] {{"a", "b"}, {"key2", "val2"}}), it is unfortunately, not available for attributes.

So with that in mind, a few possibilities to approximate what you want are:

  1. Use a single-dimensional array, with alternating key and value pairs. The obvious downside to this approach is that it's not exactly enforcing names and values.

  2. Allow your parameter to appear multiple times by tagging your attribute definition with the following attribute:

    [AttributeUsage(AllowMultiple=true)]
    

    In this way, you can now define:

    [KeyVal("key1","val1"), KeyVal("key2","val2")]
    public class Foo { ... }
    

    This is a bit wordier than what I'm sure you were hoping for, but it makes a clear delineation between names and values.

  3. Find a JSON package and provide an initializer for your attribute. The performance hit is inconsequential as this is done during code initialization. Using Newtonsoft.Json, for instance, you could make an attribute like so:

        public class JsonAttribute : Attribute
        {
          Dictionary<string, string> _nameValues = 
              new Dictionary<string, string>();
          public JsonAttribute(string jsoninit)
          {
            var dictionary = new Dictionary<string, string>();
            dynamic obj = JsonConvert.DeserializeObject(jsoninit);
            foreach(var item in obj)
              _nameValues[item.Name] = item.Value.Value;
          }
        }
    

    Which would then allow you to instantiate an attribute like so:

    [Json(@"{""key1"":""val1"", ""key2"":""val2""}")]
    public class Foo { ... }
    

    I know it's a little quote-happy, a lot more involved, but there you are. Regardless, in this crazy dynamic world, knowing how to initialize objects with JSON isn't a bad skill to have in your back pocket.

Arroyo answered 3/8, 2011 at 21:26 Comment(4)
Thanks for your suggestions! #1 seems like a decent workaround. #2 is a great solution for some scenarios, but doesn't work for all scenarios. And #3 ... talk about overkill, but thanks for thinking outside the box!Absenteeism
Interestingly enough, solution #3 is most likely to stand the test of time, as any additional attributes for which you'd like to use this technique can be derived directly from JsonAttribute. But if a non-compiling solution is what you are after, I'm certain one of the other answers will be more to your liking. ;-)Arroyo
That's true, your solutions are the only ones that compile :)Absenteeism
I have run into this as well, I ended up using 2 single dimentional arrays, one for the key and one fore the values. Both were string arrays and in the constructor I had a check to ensure that the lengths of the arrays matched up to prevent some rudimentary mistakes. [IsRequired(new[] {"CountryId"}, new[]{"1"})]Untangle
F
3

The short answer is no.

Longer answer: In order for a class to support collection initializers, it needs to implement IEnumerable and it needs to have an add method. So for example:

public class MyClass<T,U> : IEnumerable<T>
{
    public void Add(T t, U u)
    {
    }

    public IEnumerator<T> GetEnumerator()
    {
        throw new NotImplementedException();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

I can then do this:

var mc = new MyClass<int, string> {{1, ""}, {2, ""}};

So using this, let's try to make it work for an attribute. (side note, since attributes don't support generics, I'm just hardcoding it using strings for testing) :

public class CollectionInitAttribute : Attribute, IEnumerable<string>
{
    public void Add(string s1, string s2)
    {

    }

    public IEnumerator<string> GetEnumerator()
    {
        throw new NotImplementedException();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

And now to test it:

[CollectionInit{{"1","1"}}]
public class MyClass
{

}

and that doesn't compile :( I'm not sure where the limitation is exactly, I'm guessing attributes aren't newed up the same way a regular object is and therefore this isn't supported. I'd be curious if this can theoretically be supported by a future version of the language....

Fransiscafransisco answered 3/8, 2011 at 20:26 Comment(4)
Thank you for your short answer and your long one! That's exactly what I tried too, but when it didn't compile, I wasn't sure if it was a faulty syntax or an unsupported feature. I also didn't know that Generics weren't allowed.Absenteeism
FYI I like your answer a lot, but I decided to accept @vladimir77's answer only because it has a definitive answer quoted from MSDN, and it seems to be the best answer for the question.Absenteeism
@scott: no big deal. I just want to point out that this isn't an issue of named parameters, as that's NOT what you were trying to do. You were trying to use the collection intializer syntax, which is a seperate thing altogether...Fransiscafransisco
Yeah, that's true, named parameters are totally unrelated to collection initializers. But I guess the reason I liked @Vladimir77's answer is because it states exactly what an Attribute does support.Absenteeism

© 2022 - 2024 — McMap. All rights reserved.