LLBLGen Pro feature highlights: assigning attributes based on rules to properties in generated code.
(This post is part of a series of posts about features of the LLBLGen Pro system)
One of the things which makes some people hate generated code is that it's effectively 'read-only' code: changing it will make you run the risk that your changes are likely gone when you re-generate the code as the files are overwritten by the code generator. The code generated by LLBLGen Pro has several facilities to circumvent this aspect: it offers extensibility through either user-code regions, partial classes, methods you can override and for example our own O/R mapper framework has a dependency-injection system built-in to plug-in objects like validators, auditors and authorizers at runtime into instances of the generated classes.
While this is sufficient in many cases, there are several constructs in generated code which you might want to alter or append code to, one of them being the requirement to append attributes to generated classes and properties. One of these attributes might be the Validation attributes build into the .NET framework, which are used by various UI and service frameworks. Since v3.0, the LLBLGen Pro designer has a facility where the user can define attribute definitions, using macros, on model elements, like entities, entity fields, navigators etc. While this worked OK, the main drawback was that it required manual labor to define the same attribute to many elements and maintain them manually.
In v3.5 we therefore added a rule system for assigning attributes to model elements. In this post I'll go briefly into how this works and what you can accomplish with this with very little effort. As an example I'll assign a StringLengthAttribute to all string-typed entity fields in my model. Using the rule system combined with the macros defined by the designer I can define the attribute only once and let the designer and code generator do the work for me. After all, tools are there to lower the amount of work we developers have to do, right?
Project creation and attribute definition
I'll first create a project, using the database-first reverse engineering facilities, from the SQL Server database Northwind using the steps in the Quick Start Guide 'I have a database and need to get source code'. The LLBLGen Pro designer supports multiple O/R mapper frameworks ('target frameworks') and it doesn't matter which target framework you want to use, the feature is supported across all supported frameworks.
After this Quick Start Guide, I have an LLBLGen Pro designer project, entity definitions mapped onto the Northwind tables, and also code generated by the designer representing my model and mappings, using the target framework I chose when I created the project.
For illustration purposes, I'm going to choose the Entity Framework v4.x as my target framework and will opt for generating code using the DbContext aware preset, so my code requires Entity Framework v4.3. When I look at the properties generated in the class generated for the Employee entity, I see:
... /// <summary>Gets or sets the BirthDate field. </summary> [DataMember] public Nullable<System.DateTime> BirthDate { get; set;} /// <summary>Gets or sets the City field. </summary> [DataMember] public System.String City { get; set;} ....
The string typed field City has an attribute DataMemberAttribute but no StringLengthAttribute attribute. We can of course add the StringLengthAttribute to the string properties in the code file, but when we re-generate the code, those changes are gone. Instead we'll go into the designer and define the attribute there, so we can simply direct the code generator to do the work for us.
We open the project settings via the menu Project -> Settings and navigate to the node Convents -> Code Generation -> Attributes. The following is shown:
We see that the DataMember attribute is present for NormalField. As this attribute is defined at the project level, it's applied to all normal fields in the model when code is generated (you can disable per field the 'inherited' attributes, if the attribute is applied to it, so you have total freedom). This attribute is inherited from the target framework chosen, in this case Entity Framework v4.x: by choosing 'Entity Framework v4' as the target framework, we automatically get the attribute definitions which are common for the framework chosen.
Let's add the StringLengthAttribute to the list of attributes for NormalField. If we don't define a rule for this attribute, every field will get the StringLengthAttribute, which is not what we want, we only want it for string typed fields. Additionally, we will use the macros built into the attribute system of LLBLGen Pro to make the definition flexible so we have to define the attribute just once and it will be applied to every string field using the aspects of that field.
To define the attribute, type the attribute construct in the first cell of the new row at the top of the grid and press enter. The complete attribute definition is:
StringLength($length, ErrorMessage = "The {$Name} value cannot exceed $length characters.")
Here we use a couple of macros: one for the length ($length), one for the name ( {$Name} ) which will be replaced with the length and name respectively of the field the attribute is applied to and one for the assignment operator ($=) as VB.NET and C# use different assignment operators in attribute definitions. This keeps the attribute target language independent.
No rule has been defined yet, so we click the Edit... button next to the StringLength attribute we just defined. A dialog pops up which allows us to define the rule for this particular attribute definition. When we're done it looks something like this:
We click Ok and close the Project Settings dialog by clicking Ok again and re-generate the code. When we again look at the City property in the Employee class, we'll see:
... /// <summary>Gets or sets the BirthDate field. </summary> [DataMember] public Nullable<System.DateTime> BirthDate { get; set;} /// <summary>Gets or sets the City field. </summary> [DataMember] [StringLength(15, ErrorMessage = "The City value cannot exceed 15 characters.")] public System.String City { get; set;} ... /// <summary>Gets or sets the FirstName field. </summary> [DataMember] [StringLength(10, ErrorMessage = "The FirstName value cannot exceed 10 characters.")] public System.String FirstName { get; set;} ....
The City property has the StringLengthAttribute applied to it, with the proper length filled in (City in Employee has length 15), as well as the name of the field, 'City', is properly added to the attribute definition. We also see that the field BirthDate doesn't have the StringLengthAttribute applied as it's not a string typed field, however the string typed field FirstName has the attribute and with different values as the attribute applied to the City field.
The powerful thing about this is, besides that it took us not more than 20 seconds to define it for the entire model, that if we change the field name of City or the length of the field, and re-generate the code, the attribute definition changes as well. Also if we change the type of the string field to something else, the attribute will be automatically removed next time the code is re-generated. Additionally, if we add a new field of type string, it automatically get the StringLengthAttribute applied to it, without us doing anything, the system does this for us. This makes defining attributes this way simple, quick and effortless: maintenance of the attribute definition is low, almost 'set and forget'.
When we try to compile the code, it doesn't compile however: StringLengthAttribute is defined in the assembly System.ComponentModel.DataAnnotations, an assembly and namespace which aren't added by default. To overcome this, we go back to the Project Settings using the menu Project -> Settings and navigate to Conventions -> Code Generation -> Additional Namespaces. The default element is 'Entity' and that's exactly the element we'll define an additional namespace definition for. We simple add
System.ComponentModel.DataAnnotations
to the grid by typing this in the new row at the top, ending it with Enter.
This simple action means that each entity class will get a using System.ComponentModel.DataAnnotations; statement (or if you're using VB.NET, an Imports System.ComponentModel.DataAnnotations statement). When we re-generate the code, the classes generated have indeed the necessary namespace at the top. By adding the proper reference to System.ComponentModel.DataAnnotations to the VS.NET project, everything compiles as it should. LLBLGen Pro doesn't overwrite VS.NET project settings / references in VS.NET projects it generates if the file exists, instead it updates the VS.NET project with file references, so the additional assembly reference we added will be kept even if we re-generate the code.
That's it: two simple actions and model-wide the attribute and namespace are defined, using flexible, generic macros and rules so the designer takes care of the work without forcing us to remember to remove or update the attribute if something changes to the field the attribute has been applied to or when we add a new field.
Only with LLBLGen Pro. The example project used for this post is attached below. Enjoy!