Introduction
Many applications, utilities & frameworks are required to use custom attributes to work like entity framework or WCF for example. On the other hand, you can define your own custom attributes to add metadata to your code. The first question we can ask is why custom attributes are often imposed and what this implies.
Custom Attributes
Custom Attributes offer the ability to add metadata to code. In this way, it is possible to exploit them on postbuild or runtime through reflection to offer features. In the background, a custom attribute is just a class that is derived from System.Attribute
:
public class EmailAttribute : Attribute
{
}
It is possible to configure the custom attribute to specialize it to specific code. For example, it can be useful to limit usage of a custom attribute to a parameter:
[AttributeUsage(AttributeTargets.Parameter)]
public class EmailAttribute : System.Attribute
{
}
Here is an example of usage:
static public class MessageSender
{
static public void Send([Email] string address, string title, string message)
{
}
}
In this case, the parameter named address
is flag with EmailAttribute
. In practice, it brings no feature to do that except placing here an additional information like a comment. It is possible to retrieve this information later in a postbuild process or at runtime.
But Why Would I Do That?
In postbuild process, you may need to generate something important based on code like documentation or additional code. In practice, you will often use existing custom attributes provided by .NET framework or third party utilities/frameworks.
Why Do I Care About Usage of Them?
You can use them respecting documentation of .NET Framework or third party but when it is the case, you may ignore what this implies. Indeed, I assume a role of architect in my job but in personal projects too, and I often detect a design issue when using custom attributes. That's why I share these tips with you.
What Kind of Issues May I Meet When Using Custom Attributes?
Custom attributes are not bad at all, but with a bad design, it can introduce mainly 3 issues: difficult extensibility, forget about factoring and too hard coupling.
Can I Avoid or Minimize These Different Issues?
Yes you can! But not in all cases. I will explain what you have to take care of to keep a good design. Even a perfect recommendation does not exist, I want you to have a better understanding to make the best choice.
Extensibility
The first issue is the lost of extensibility. Indeed, using custom attributes as an entry point for a feature can curb extensibility. To understand what I want to explain to you, I will take a common usage of custom attribute: ORM mapping.
Imagine an ORM offers you custom attributes to define your mapping between DAO and database. For example, defining a table name for a specific type.
[Table("T_PRODUCT")]
public class Product
{
}
Every framework that offers feature based on custom attribute must expose an API to it programmatically to keep maximal extensibility because sometimes you have to use legacy type and cannot add custom attributes to it (no source for example).
A common workaround is to wrap the legacy type and add custom attributes to wrap. But believe me, doing it will introduce some technical debts.
You just have to check if an alternative API exists to do it programmatically (Interface, Fluent, XDocument, etc.) to know if you invest in a technology that can introduce an extensibility issue.
Factorization
Custom attributes can have properties to be customizable. It sounds like a cool feature but in some cases, it will introduce boilerplate code that penalizes maintenance. Here is a simple example for data validation with a RegexAttribute
:
public class User
{
public User([Regex(@"^([\w\.\-]+)@([\w\-]+)((\.(\w){2,3})+)$")] string email)
{
}
}
Inlining values seems to be natural but you can forget to factorize it and repeat the same value in many places. Changing values can become very hard if it is replicated in too many places.
Fortunately, you can easily avoid it in 2 ways:
- Value provided to custom attributes are constant and can be stored elsewhere:
public class Email
{
public const string Regex = @"^([\w\.\-]+)@([\w\-]+)((\.(\w){2,3})+)$";
}
public class User
{
public User([Regex(Email.Regex)] string email)
{
}
}
NOTE: Be careful about public
constant that required rebuild to take place and value that is not constant like System.Type
.
- Prefer to use a specialized attribute or define it yourself without configuration value:
public class EmailAttribute : RegexAttribute
{
public EmailAttribute()
base(@"^([\w\.\-]+)@([\w\-]+)((\.(\w){2,3})+)$")
{
}
}
public class User
{
public User([Email] string email)
{
}
}
NOTE: Unfortunately, custom attributes are often sealed.
Coupling
Coupling is the crux of code evolutivity and unfortunately, custom attributes imposed coupling for too many cases. Indeed, there is a lot of effort provided to uncoupling code by design like inversion of control, aspect oriented programming and more. These efforts can be ruined by usage of custom attribute.
For example, if I have a simple Calculator
class with a method to add two int
s like this:
public class Calculator
{
public int Add(int a, int b)
{
return a + b;
}
}
Imagine now, we need to log call of Add
method into a log file and use a log framework or AOP Framework to do it like this:
public class Calculator
{
[LogFile("log.txt")]
public int Add(int a, int b)
{
return a + b;
}
}
When doing it, I just introduce 3 levels of coupling:
- my domain (business) assembly have to reference a third party
- my calculator is flag with a log attribute that has no sense in my business but only for logging business
- my calculator is flag with a specific logger with a value that has no sense in my business but only for file logger
It can be abstract
to minimize coupling by knowing only about logging but not file logging:
public class Calculator
{
[Log]
public int Add(int a, int b)
{
return a + b;
}
}
In this case, 2 levels of coupling stay in my business code. Coupling can be more present if you use many external frameworks... here is an example for logging framework with a REST API Framework:
public class Calculator
{
[Log]
[WebGet(Serialization = Serializations.Json, UriTemplate = "Addition")]
public int Add(int a, int b)
{
return a + b;
}
}
My business must reference all frameworks and I don't like it but it seems that I am forced to do it...
Fortunately, if the logging and Rest API Frameworks have a good design with good API and if I make a small effort, I can stop entirely the coupling by declaring (maybe with a custom attributes too) that my Add
method is special and needs to be logged and exposed. In this example, I mark my method to declare it as a service.
public class Calculator
{
[Service]
public int Add(int a, int b)
{
return a + b;
}
}
My calculator doesn't know about logger and web API, but knows service attribute that is my own attribute and my business can drop reference from logging framework and REST API Framework to concentrate on business only.
But how can I take this kind of decision?
There are only 2 rules to respect to be able to get rid of coupling provided by most of the external frameworks:
- Check if the external framework provides an API to do it with your own custom attribute or programmatically (as explained in the Extensibility section).
- Use custom attribute to only define what represents your code and not what features you want, need or apply.
Conclusion
There are now a lot of third party frameworks with very cool features and easy integration. Most of them offer an integration through custom attributes but you have to be careful about usage to avoid cumulating technical debts. Take a little time to validate if you really want to do coupling with them and what it will cost if you change something in your code/architecture. On the other hand, in some cases, it requires only a few tasks to keep your code clean and profit of third party features. Always taking a little time to make a good decision when you are the software architect is not something you can sacrifice without paying for it later.