csharplang
csharplang copied to clipboard
Champion "Allow Generic Attributes"
- [x] Proposed in #4936
- [x] Discussed in LDM
- [x] Prototype: N/A
- [x] Implementation: In Progress
- [ ] Specification: Not Started
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)
I've wanted this for a long time!
I would be happpy to have this, too!
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 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 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.
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.)
I thought part of this proposal was allowing complex params I attribute initialization,
No.
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?
Would this feature support this?
public class GenericClass<T>
{
[SomeAttribute<T>]
public void DoSomething(T input)
{
}
}
public class SomeAttribute<T> : Attribute
{
}
@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 , I am not sure I understand. Can you elaborate?
Is B a concrete type?
@ymassad Sorry. I meant to write this.
[SomeAttribute<B>]
public void DoSomething<B>(B input) { }
I believe this is the spec.
The attribute can't have open generic type it must be instantiated at compile time
@ymassad What attribute instance would you expect typeof(GenericClass<>).GetMethod("DoSomething").GetCustomAttributes()
to contain in your example?
@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 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.
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 , thanks.
@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
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.
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,
}
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 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?
https://github.com/dotnet/roslyn/blob/master/docs/Language%20Feature%20Status.md
@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.
Any chance at all this will make it into C# 9?
@TonyValenti Given that this issue is labeled as "10.0 Candidate", the answer is "No".
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<>))]
.
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.
@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.