csharplang icon indicating copy to clipboard operation
csharplang copied to clipboard

Champion "Allow Generic Attributes"

Open gafter opened this issue 7 years ago • 54 comments

See

  • https://github.com/dotnet/roslyn/issues/953
  • https://roslyn.codeplex.com/discussions/542178
  • https://github.com/dotnet/roslyn/pull/16814 (language implementation PR)
  • https://github.com/dotnet/coreclr/pull/9189 (fix PR for coreclr bug that breaks reflection with generic attributes)

gafter avatar Feb 15 '17 23:02 gafter

I've wanted this for a long time!

AlphaDriveLegal avatar Feb 22 '17 06:02 AlphaDriveLegal

I would be happpy to have this, too!

Mafii avatar Mar 14 '17 11:03 Mafii

Very compelling.

Here's my use case, one among many:

public class ValidateIf<TValidationAttribute> : ValidationAttribute
{
  public TValidationAttribute Attribute { get; private set; }
  //other properties to resemble condition
  public ValidateIf(Action<TValidationAttribute> configure)
  {
    Attribute = configure();
  }
  protected override ValidationResult IsValid(object value, ValidationContext validationContext)
  {
    var shouldValidate = this.ShouldValidate();
    if(!shouldValidate) return ValidationResult.Success;
    else return Attribute.GetValidationResult(value, validationContext);
  }
}

Usage:

public class Person
{
  [ValidateIf<RangeAttribute>(() => new RangeAttribute(params), Condition set up...)]
  public string Name { get; set; }
}

Short in time now, sorry for the poor pseudo, but I'm sure I'm understood.

weitzhandler avatar Aug 04 '17 15:08 weitzhandler

@weitzhandler Your example is pretty flawed since lambdas can't be passed as an attribute parameter; only primitives, string, typeof() expressions, and DateTime literals (VB).

A more sensible example of where a generic attribute would be useful:

// This concerns a bot command system that can parse strings into typed arguments,
// but sometimes the "default" reader for something may be too narrow,
// so this on a parameter would override which class does the reading for that argument.
[AttributeUsage(AttributeTargets.Parameter)]
public sealed class OverrideTypeReaderAttribute<TReader> : Attribute
    where TReader : TypeReader
{
    // We're only interested in the Type info at this point,
    // but we would really like the type-safety of the generic constraint
    public Type TypeReaderType { get; }

    public OverrideTypeReaderAttribute()
    {
        TypeReaderType = typeof(TReader);
    }
}
// Usage:
[Command("foo")]
public Task Foo([OverrideTypeReader<AlternativeBoolReader>] bool switch)
{
    //.....
}

Currently we imitate the constraint by taking a Type parameter in the constructor and checking IsAssignableFrom() at runtime, but pushing the check to compile-time via constraint would be neat and save us from needing to throw an exception if it fails.

Joe4evr avatar Aug 04 '17 19:08 Joe4evr

@Joe4evr I thought part of this proposal was allowing complex params I attribute initialization, which I saw in several issues, maybe in the Roslyn repo. And that's why I was going for an example that utilizes both, and I do vote for both genetic attributes and better and more flexible attribute initialization and setup options.

weitzhandler avatar Aug 05 '17 06:08 weitzhandler

I thought part of this proposal was allowing complex params I attribute initialization

I have not seen this proposal before, not to mention that such a thing would require quite a hefty spec change since as it stands, the compiler needs to be able to serialize the arguments directly in the metadata.

It is possible, if wanted for whatever reason, to instantiate an attribute as just a normal class, and that could use any type of parameter:

var attr = new SomethingSpecialAttribute(GetSomeData());

But that should only be a secondary constructor, otherwise it's totally useless to have that class be an attribute in the first place. (I have an example of where this is used in the same codebase I described above, but that's getting off-topic.)

Joe4evr avatar Aug 05 '17 09:08 Joe4evr

I thought part of this proposal was allowing complex params I attribute initialization,

No.

gafter avatar Oct 28 '17 19:10 gafter

We need a mechanism to understand which target runtime it works on. We need that for many things, and are currently looking at that. Until then, we can't take it.

I suppose that's on the use-site, whereas an abstract generic attribute doesn't seem to be harmful,

abstract class SomeAttribute<T> : Attritbue {}

Could we permit the above without (any) runtime support?

alrz avatar Jan 13 '18 17:01 alrz

Would this feature support this?

public class GenericClass<T>
{
    [SomeAttribute<T>]
    public void DoSomething(T input)
    {
    }
}

public class SomeAttribute<T> : Attribute
{

}

ymassad avatar Dec 05 '18 23:12 ymassad

@ymassad According to the proposal, your example is correct but possibly not for the reasons that you expect. Here is an example that might make things a little clearer.

[SomeAttribute<A>]
public class GenericClass<A>
{
	[SomeAttribute<B>]
	public void DoSomething(B input) { }
}

public class SomeAttribute<C> : Attribute
{
	// `C` is either `A` or `B` according to its target.
}

As you can see from the example, the generic context comes from the attributes target.

roydukkey avatar Dec 06 '18 13:12 roydukkey

@roydukkey , I am not sure I understand. Can you elaborate?

Is B a concrete type?

ymassad avatar Dec 06 '18 13:12 ymassad

@ymassad Sorry. I meant to write this.

[SomeAttribute<B>]
public void DoSomething<B>(B input) { }

I believe this is the spec.

roydukkey avatar Dec 06 '18 17:12 roydukkey

The attribute can't have open generic type it must be instantiated at compile time

AviAvni avatar Dec 06 '18 17:12 AviAvni

@ymassad What attribute instance would you expect typeof(GenericClass<>).GetMethod("DoSomething").GetCustomAttributes() to contain in your example?

jnm2 avatar Dec 06 '18 17:12 jnm2

@jnm2 ,

Thanks. I see now why this is not possible.

what about this:

public class GenericClass<T>
{
    [SomeAttribute(typeof(T)]
    public void DoSomething(T input)
    {
    }
}

public class SomeAttribute : Attribute
{
    public SomeAttribute(Type type)
    {
        this.Type = type;
    }

    public Type Type {get;}
}

typeof(GenericClass<>).GetMethod("DoSomething").GetCustomAttributes() could return a SomeAttribute instance with Type containing the parameter type T.

ymassad avatar Dec 06 '18 17:12 ymassad

@ymassad The problem there (if I remember the metadata format correctly) is that attribute arguments of type System.Type are stored as serialized strings containing the assembly-qualified name. What string can be deserialized (similar to Type.GetType) in such a way that it resolves to a generic type parameter? I think this would require a runtime change.

Edit: !0 (referring to the generic type parameter with index 0) and !!0 (same but for generic method parameters) are how this would work.

jnm2 avatar Dec 06 '18 17:12 jnm2

Yep, https://www.ecma-international.org/publications/files/ECMA-ST/ECMA-335.pdf, section II.23.3 Custom attributes, page 268:

  • If the parameter kind is System.Type, (also, the middle line in above diagram) its value is stored as a SerString (as defined in the previous paragraph), representing its canonical name. The canonical name is its full type name, followed optionally by the assembly where it is defined, its version, culture and public-key-token. If the assembly name is omitted, the CLI looks first in the current assembly, and then in the system library (mscorlib); in these two special cases, it is permitted to omit the assembly-name, version, culture and public-key-token.

jnm2 avatar Dec 06 '18 17:12 jnm2

@jnm2 , thanks.

ymassad avatar Dec 06 '18 18:12 ymassad

@ymassad TypeDescriptor will inject the type as a constructor parameter when it instantiates a type converter specified via TypeConverterAttribute, and maybe you can follow a similar pattern in the meantime?

https://github.com/dotnet/corefx/blob/bfb4ba1e398273cfd3bf15bf25c535f64bf072ea/src/System.ComponentModel.TypeConverter/src/System/ComponentModel/MarshalByValueComponent.cs#L16

https://github.com/dotnet/corefx/blob/bfb4ba1e398273cfd3bf15bf25c535f64bf072ea/src/System.ComponentModel.TypeConverter/src/System/ComponentModel/ComponentConverter.cs#L13-L18

https://github.com/dotnet/corefx/blob/bfb4ba1e398273cfd3bf15bf25c535f64bf072ea/src/System.ComponentModel.TypeConverter/src/System/ComponentModel/ReflectTypeDescriptionProvider.cs#L223-L232

jnm2 avatar Dec 06 '18 18:12 jnm2

The main use case I have is to make the attribute know the type I am placing it on.

public class EntityExamplesAttribute<T> : Attribute
{
    public abstract List<T> Examples {get;set;}
}

Or

public class EntitySeedDataAttribute<T> : Attribute
{
    public abstract List<T> Entities {get;set;}
}

Then I can do inheritance.

public class Entity1ExamplesAttribute : EntityExamplesAttribute<Entity1>
{
    public Entity1Attribute() 
    {
        Examples = new List<Entity1> { 
            new Entity1 { /*   first example entity */ },
            new Entity1 { /*   second example entity */ }
        }
    }
}

And

public class Entity1SeedDataAttribute : EntitySeedDataAttribute<Entity1>
{
    public Entity1Attribute() 
    {
        Examples = new List<Entity1> { 
            new Entity1 { /*   first seed entity */ },
            new Entity1 { /*   second seed entity */ }
        }
    }
}

And then apply to the entity

[Entity1SeedData]
[Entity1Examples]
public class Entity1
{
    // Entity stuff
}

Then I have code that seeds the database on first creation. Then I have code that shows example entities in a documentation page.

I already have it working with this feature, using object boxing and casting as opposed to generics.

rhyous avatar Jan 17 '19 21:01 rhyous

Will it be part of C# 8.X release? Can I start using it with some preview version?

Sorry for by broken C#, as it is not my native language. But I hope to write next kind of DSL with Roslyn generator

// union which stores some stucture so that no need to box with explict layout
[OverlappedUnion(Size = 64)] // analyar will thorw if any size is more than 64
//[OverlappedUnion(Size = 64, ReadOnly = true)]
enum X
{
  [Case<String>]
  A,
    [Case<(String z, int k>>] // x.B.zm x.B.k
    B,
    
    // no data
    [Case]
    [Case<Unit>]
    C,
    
    // fixed(see C# keyword) allocated 23 length char
    [Case<Memory<char>>(23)]
    E,
    
    [Case("CustomPropertyName")]
    D,
}

// generator generates all possible cases
XCase x;

switch (x.Tag)
{
   case X.A:
     // in DEBUG runtime checks A and then does not thorows A
     // in analyzer also checks
     // should work well with descruting
      var v1 = x.A;
     var v2 = x.Value; // object of Union or XStruct for OverlappedUnion
    break;
   case X.C
     break;
   // analyzer ensure default or all cases
}


// design with internal explicit stuct or base class or what ever,
// same API but may require boxing
[Union]
enum X
{
  [Case<String>]
  A,
    [Case<(String z, int k>>]
    B,
    
    // no data
    [Case]
    [Case<Unit>]
    C,
        
    [Case<Memory<char>>(23)]
    E,    
}

dzmitry-lahoda avatar Apr 22 '19 07:04 dzmitry-lahoda

Will it be part of C# 8.X release? Can I start using it with some preview version?

Sadly, despite the Roslyn PR being ready for literally a year now, it doesn't seem like it's gonna make 8.0 unless the language team decides to push the Merge button at literally the last minute.

Which is really sad, because I really want it ASAP. :anger:

Joe4evr avatar Apr 22 '19 18:04 Joe4evr

@Joe4evr Just came across this today and I have to say that this would indeed be awesome to have right away with C# 8. BTW, it seems that something is going int this PR since your comment. Do you guys (@jcouv @AviAvni) mind bringing some information over here about this feature's availability?

thiagomajesk avatar Jun 04 '19 03:06 thiagomajesk

https://github.com/dotnet/roslyn/blob/master/docs/Language%20Feature%20Status.md

image

ufcpp avatar Jun 04 '19 03:06 ufcpp

@ymassad The problem there (if I remember the metadata format correctly) is that attribute arguments of type System.Type are stored as serialized strings containing the assembly-qualified name. What string can be deserialized (similar to Type.GetType) in such a way that it resolves to a generic type parameter? I think this would require a runtime change.

I reread this (https://github.com/dotnet/csharplang/issues/124#issuecomment-444965617) and have to correct myself: !0 (referring to the generic type parameter with index 0) and !!0 (same but for generic method parameters) are how this has worked.

jnm2 avatar Oct 28 '19 17:10 jnm2

Any chance at all this will make it into C# 9?

TonyValenti avatar Aug 13 '20 10:08 TonyValenti

@TonyValenti Given that this issue is labeled as "10.0 Candidate", the answer is "No".

Joe4evr avatar Aug 13 '20 12:08 Joe4evr

Another use case for this:

I'm writing a compile time IOC framework for C# using source generators: https://github.com/YairHalberstadt/stronginject

Registrations are done via attributes, but because generics aren't allowed in attributes you can't do this: [RegisterGeneric(typeof(List<>))]

Instead you have to explicitly pass in the parameters using a generic factory method, e.g:

[Factory] public static List<T> CreateList<T>(IEnumerable<T> ts) => new List<T>(ts);

Whilst this is more flexible it's less concise, and requires you to update the method whenever the parameters change, which is against the spirit of using an IOC container in the first place.

Another thing that might be useful is partially open typeofs, allowing you to register e.g. [RegisterGeneric(typeof(ServiceImpl<string, >), typeof(IService<>))].

YairHalberstadt avatar Aug 26 '20 18:08 YairHalberstadt

While the use cases for this are not broad, it would have a big impact on certain applications.

For example, MVC's ProducesResponseTypeAttribute(Type type, int statusCode) can specify different result types based on the HTTP status. Currently, the following code does not build:

    internal abstract class BaseCrudController<TEntity> : Controller
        where TEntity: class
    {
        private DbContext context;

        //...

        // This next line throws an error
        [ProducesResponseType(typeof(TEntity), 200)]
        //                    ^^^^^^^^^^^^^^^ 
        //                    CS0416: An attribute argument cannot use type parameters
        [ProducesResponseType(400)]
        public virtual async Task<IActionResult> GetById(int id)
        {
            var instance = await this.context.Set<TEntity>().FindAsync(id);
            return instance is null 
                ? (IActionResult)NotFound()
                : (IActionResult)Ok(instance);
        }

        //...

    }

With this feature the annotation could be rewritten as

        [ProducesResponseType<TEntity>(200)]
        [ProducesResponseType(400)]
        public virtual Task<IActionResult> GetById(int id)
        {
            //...

Inheritors of this base could specify a closed generic, if compile time typing was needed, like for a swagger doc or api code gen.

    public class UserController: BaseCrudController<User>
    {
        //...

I would expect reflection calls returning that attribute to behave similarly to reflection calls on generic properties: If the generic is defined in the originating type definition, then return the closed generic. If the originating type is open, return the open definition.

image

johnscott999 avatar Nov 23 '20 20:11 johnscott999

@johnscott999 you can achieve identical behavior by leveraging ActionResult<T> instead of IActionResult. There is no need to explicitly specify a ProducesResponseType in that case.

Having said that, I can see your suggestion being very useful in other contexts.

julealgon avatar Nov 27 '20 14:11 julealgon