Rendered at 23:34:37 GMT+0000 (Coordinated Universal Time) with Cloudflare Workers.
bob1029 8 hours ago [-]
I try to avoid using custom attributes to configure things like business logic because they struggle to account for interactions between members. Attributes have no affordance for lambdas, delegates, method references, etc. You would need a separate piece of logic that interprets the primitive attribute data in order to provide any emergent properties between members.
A better approach is often to expose some abstract/interface member that allows for the implementation to define its logic using something like a fluent-style contract. In this arrangement you can pass the type itself as an argument to a lambda making it trivial to define logic that should execute over many members at once. Debugging this also tends to be more pleasant. AspNetCore startup code, LINQ and EF are good examples.
Attributes are useful for things that are definitely pure data and only when the information fully belongs to the thing being annotated regardless of context of use. The moment some kind of per-member custom logic is needed it's no longer appropriate. I think things like [Authorize] are borderline. [JsonIgnore] seems like a good attribute to me.
xnorswap 8 hours ago [-]
This is my experience too, I've worked on code bases which made heavy use of attributes, and they work really well to provide static per-type information, but if you see them as a universal problem solver and try to go too fancy, you'll find yourself in trouble.
Fluent builders are nicer to work with than attributes, although it sometimes feels weird if the defaults are nearly fine but not quite, and you wish you could just reach for a single attribute rather than having to traverse down 3 layers of builders to change a single property.
fabian2k 8 hours ago [-]
The only case I've used them was to mark classes that I need to find via reflection and do something with them. For example a migration system where you want to load all migrations that are defined and check if you need to run them. Of course you don't really need an attribute for that, but I find it helpful to leaven a marker on the class that there's something else going on there.
dh2022 8 hours ago [-]
To solve this problem I have seen the following pattern:
1. Create an abstract base class named MigrationBaseClass
2. Have all migrations classes inherit from MigrationBaseClass
3. Use .Net Reflection to get all types that inherit from MigrationBaseClass
4. Do something with these types.
WorldMaker 7 hours ago [-]
Doesn't even need to be an abstract base class. It's just as easy to use reflection to find all implementations in an assembly of an IMigration interface.
(ETA: Though my favorite pattern here became using DI for this instead of reflection. For every IMigration have a `services.AddTransient<IMigration, SomeMigrationImplementationClass>()` somewhere and then your service to run all migrations can just request from DI `IEnumerable<IMigration>`. I can then put the Reflection into a unit test to make sure everything that implements IMigration is registered in the DI container. But using DI in the main assembly to register all the migrations rather than Reflection leaves more room to try to AOT compile the assembly in production builds.)
hvb2 6 hours ago [-]
Assuming you have all the code in your solution, you could do this with a source generator instead and have no need of reflection and are AOT compatible
fabian2k 7 hours ago [-]
yeah, there's plenty of ways to do that. I like having the attribute there so that there is a strong hint that some magic is happening somewhere with that class.
tracker1 8 hours ago [-]
FastEndpoints does this for you with Validators as an example... It can be abstracted cleanly.
wvenable 5 hours ago [-]
This isn't an article about creating or even using attributes; it's a fairly low-level deep dive into how they're implemented.
It's definitely nothing I've ever had to think about when using attributes but I can appreciate someone looking into this and making the case that they could be implemented better.
As for attributes themselves, what I've found is that languages without this facility tend to implement them anyway using whatever hacks they can. In C, this is typically done with macros defining constant values. In scripting languages, it's often done with comments.
gwbas1c 6 hours ago [-]
FWIW: When I come across reflection code it's often a smell, especially when written by a novice developer. Quite often there's a much simpler (and less fragile) way to do something. Reflection is a powerful technique with many gotchas; and the gotchas can quickly outweigh the benefits.
rkagerer 5 hours ago [-]
I used attributes for a serialization framework (long ago before any good ones existed for C#) and found they were exactly what I needed. The solution was easy to understand and reason about, consistent, even somewhat elegant (and I use that adjective cautiously as I've seen lots of code that was "elegant" but fragile). I agree with your forewarnings, but there are absolutely instances where it's the right tool for the job.
gwbas1c 1 hours ago [-]
Yes, and the attributes that TFA whines about are what make NUnit very powerful. [Values] on an enum argument creates useful tests.
That being said, when I started my current job I replaced some complicated reflection-based code with a dictionary of delegates. I also turbocharged a an ASP (.net 4.x) app's startup time by explicitly referencing each controller in startup code.
garganzol 5 hours ago [-]
Nightmares? Compared to other things we can encounter, .NET custom attributes are like a mild ocean breeze on a sunny day. Yes, their binary serialization depends on a functioning type system, and yes, type reference storage is not terribly efficient. But it rarely if ever poses problems in real projects.
pjc50 10 hours ago [-]
.. if you're trying to parse the assemblies by hand for some reason. If you're just trying to handle them with reflection none of this is an issue.
jve 10 hours ago [-]
> If you're just trying to handle them with reflection none of this is an issue
But maybe indicates on how expensive that reflection call can be? Reading multiple .dlls ?
EMM_386 8 hours ago [-]
Be careful with this sort of logic ("reflection=expensive").
Everything should obviously be measured.
I've worked with large .NET code bases that used attributes for things like plugins and it was completely negligible for overall performance in the grand scheme of things.
Rohansi 8 hours ago [-]
.NET typically loads dozens of DLLs at boot. It needs to resolve references for almost everything before it can run any code. It doesn't take that long to boot so I have my doubts but they have surely optimized it a lot there.
pjc50 9 hours ago [-]
I would expect DLL parsing to be a one-off cost at assembly load time. Certainly that handles all the stuff detailed under "Assembly resolution". Assembly resolution is also recursive, so I would expect that to simplify "type tree traversal" by pre-stuffing all the types into a Dictionary.
That also necessarily has the parser for all the "System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" machinery.
>> "While C# does not support types with weird symbols, it is possible to have type names with spaces, commas, brackets, unprintable characters, and more. This is also heavily (ab)used by code obfuscators."
.. hmm. I didn't know that.
I will note that there is a milder version of this problem which you might encounter if you're trying to write a dotnet source generator, which is run inside the Roslyn compiler. You then need to remember that the types in the code being compiled are not directly visible from reflection, you have to ask the compiler to look them up for you in the parsed data.
These cases are easy:
- types in the netstandard2.0 standard library
- types in the assembly currently being compiled
This case is not:
- types in assemblies in the same project which the current assembly depends on
I ended up avoiding handling that at all. It does set some limits on what is easily done with source generators.
(by the way, someone sufficiently dedicated should be able to find the corresponding Microsoft loader code: it's all in the dotnet github)
Uvix 6 hours ago [-]
Wait, I thought one project = one assembly, so you would never have "types in assemblies in the same project which the current assembly depends on".
Should that be same solution instead of same project?
gwbas1c 6 hours ago [-]
> My guess is that Microsoft just does not think it is worth creating an update (especially since attributes usually do not affect runtime behavior).
I wonder if this is why dotnet's startup time is so long? Might be worth revising just to get assembly resolution time down.
tracker1 8 hours ago [-]
FWIW, Custom Attributes in .Net are kind of a pain in geneal, powerful but painfull... Probably why JS still doesnt really have them in practice.
orphea 6 hours ago [-]
FWIW, Custom Attributes in .Net are kind of a pain in geneal, powerful but painful
Why? What's kind of painful in general about them? They are just pieces of static data. You can abuse them, e.g. have some obscure logic somewhere, but you can abuse many things just the same (ahem, Reflection, excuse me), so this factor doesn't make a distinction.
Probably why JS still doesnt really have them in practice.
Did you mean JS doesn't have TS decorators? Those are an entirely different beast.
doctorpangloss 8 hours ago [-]
haha, "use server", use...(use...(...))
6 hours ago [-]
draw_down 7 hours ago [-]
The language doesn't, but I'd say the language integration isn't the tricky part of this kind of cross-cutting-concern code. In JS you could imagine a function that decorates classes in some way, or React HOCs, etc. (We don't do HOCs anymore because we have a new kind of kludge, but we used to.)
The tricky part is as someone mentioned elsewhere in the thread: the attribute doesn't account for interactions well. You might want it to alter its behavior in different situations but the whole point is that it's cross-cutting and treats everything the same. (And I would say, even though I just called React hooks a kludge, that they are less cumbersome in this respect than HOCs were.)
A better approach is often to expose some abstract/interface member that allows for the implementation to define its logic using something like a fluent-style contract. In this arrangement you can pass the type itself as an argument to a lambda making it trivial to define logic that should execute over many members at once. Debugging this also tends to be more pleasant. AspNetCore startup code, LINQ and EF are good examples.
Attributes are useful for things that are definitely pure data and only when the information fully belongs to the thing being annotated regardless of context of use. The moment some kind of per-member custom logic is needed it's no longer appropriate. I think things like [Authorize] are borderline. [JsonIgnore] seems like a good attribute to me.
Fluent builders are nicer to work with than attributes, although it sometimes feels weird if the defaults are nearly fine but not quite, and you wish you could just reach for a single attribute rather than having to traverse down 3 layers of builders to change a single property.
1. Create an abstract base class named MigrationBaseClass 2. Have all migrations classes inherit from MigrationBaseClass 3. Use .Net Reflection to get all types that inherit from MigrationBaseClass 4. Do something with these types.
(ETA: Though my favorite pattern here became using DI for this instead of reflection. For every IMigration have a `services.AddTransient<IMigration, SomeMigrationImplementationClass>()` somewhere and then your service to run all migrations can just request from DI `IEnumerable<IMigration>`. I can then put the Reflection into a unit test to make sure everything that implements IMigration is registered in the DI container. But using DI in the main assembly to register all the migrations rather than Reflection leaves more room to try to AOT compile the assembly in production builds.)
It's definitely nothing I've ever had to think about when using attributes but I can appreciate someone looking into this and making the case that they could be implemented better.
As for attributes themselves, what I've found is that languages without this facility tend to implement them anyway using whatever hacks they can. In C, this is typically done with macros defining constant values. In scripting languages, it's often done with comments.
That being said, when I started my current job I replaced some complicated reflection-based code with a dictionary of delegates. I also turbocharged a an ASP (.net 4.x) app's startup time by explicitly referencing each controller in startup code.
But maybe indicates on how expensive that reflection call can be? Reading multiple .dlls ?
Everything should obviously be measured.
I've worked with large .NET code bases that used attributes for things like plugins and it was completely negligible for overall performance in the grand scheme of things.
That also necessarily has the parser for all the "System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" machinery.
>> "While C# does not support types with weird symbols, it is possible to have type names with spaces, commas, brackets, unprintable characters, and more. This is also heavily (ab)used by code obfuscators."
.. hmm. I didn't know that.
I will note that there is a milder version of this problem which you might encounter if you're trying to write a dotnet source generator, which is run inside the Roslyn compiler. You then need to remember that the types in the code being compiled are not directly visible from reflection, you have to ask the compiler to look them up for you in the parsed data.
These cases are easy:
This case is not: I ended up avoiding handling that at all. It does set some limits on what is easily done with source generators.(by the way, someone sufficiently dedicated should be able to find the corresponding Microsoft loader code: it's all in the dotnet github)
Should that be same solution instead of same project?
I wonder if this is why dotnet's startup time is so long? Might be worth revising just to get assembly resolution time down.
The tricky part is as someone mentioned elsewhere in the thread: the attribute doesn't account for interactions well. You might want it to alter its behavior in different situations but the whole point is that it's cross-cutting and treats everything the same. (And I would say, even though I just called React hooks a kludge, that they are less cumbersome in this respect than HOCs were.)