Using custom meta data

Generating validation rules

In parts 2 and 3 of this tutorial, you generated C# classes. In part 4, you extended the IssueManager model with custom attribute constraints. In this part, we are going to put these together. You will learn how to access your model constraints from a code generation template and how to generate validation code from these.

For this tutorial, we choose to generate standard .NET data annotations, which can be found in the System.ComponentModel.DataAnnotations namespace.

Accessing your stereotype from a Yellicode template

As you've already seen, Yellicode provides a strongly-typed API named @yellicode/elements that you can use to navigate the types in your model. But how would you navigate your own custom profile in a developer-friendly manner? Well, there's another API for that, and you just created it when you saved your model in Yellicode Modeler.

In your working directory where the tutorial model (csharp-tutorial.ymn) resides, you will also find another file named csharp-tutorial.ts that is auto-generated by Yellicode Modeler. This is your own little API. And just like other TypeScript modules, you can import it into your template.

Open template csharp-tutorial.template.ts and add the following line.

import * as issueManager from './csharp-tutorial';

Writing data annotations

Because we are going to depend on .NET data annotations in our generated code, you will need to make sure that the output file has a reference to it. To do this, first update the writeUsingDirectives call in your template.

csharp.writeUsingDirectives('System','System.ComponentModel.DataAnnotations');

Then, in your template, add a new function named writeDataAnnotations:

const writeDataAnnotations = (csharp: CSharpWriter, property: elements.Property) => {
    if (!issueManager.isCustomConstraints(property)) 
        return; // the property does not have our 'CustomConstraints' stereotype

    if (property.IsRequired) {
       csharp.writeLine(`[Required(ErrorMessage = "${property.name} is required.")]`);
    }
    if (property.MaxStringLength) {
        csharp.writeLine(`[StringLength(${property.MaxStringLength}, ErrorMessage = "The ${property.name} value cannot exceed ${property.MaxStringLength} characters.")]`);
    }
}

The first line in this function uses a function named isCustomConstraints. This function is generated as part of your API and asserts that the property has the CustomConstraints stereotype applied to it (which you did in the previous part). This check is important: not adding it would break the code that follows in case the stereotype is not applied. It is also an assertion for the TypeScript compiler that the fields IsRequired and MaxStringLength are available on the property: try removing the first isCustomConstraints check and your code will not compile.

Then, update the existing writeClassBlock code to use your new writeDataAnnotations function, and update the @yellicode/csharp import to include the PropertyFeatures type:

import { CSharpWriter, PropertyFeatures } from '@yellicode/csharp';
csharp.writeClassBlock(eachClass, () => {
    eachClass.ownedAttributes.forEach(p => {
        // Write XmlDocSummary before our data annotations
        csharp.writeXmlDocSummary(p); 
        writeDataAnnotations(csharp, p);
        // Opt out of XmlDocSummary, we just wrote it
        csharp.writeAutoProperty(p, {features: PropertyFeatures.All & ~PropertyFeatures.XmlDocSummary}); 
        csharp.writeLine();
    })
});

If yellicode is not still running, start it again and inspect the output. It should now contain a updated Issue class that has your new data annotations applied to it.

/// <summary>
/// An issue can be anything from a software bug, to a project task an epic or a story.
/// </summary>
public abstract class Issue
{
    /// <summary>
    /// The type of issue.
    /// </summary>
    public IssueType Type { get; set; }

    /// <summary>
    /// A short, single-line issue title.
    /// </summary>
    [Required(ErrorMessage = "Title is required.")]
    [StringLength(50, ErrorMessage = "The Title value cannot exceed 50 characters.")]
    public string Title { get; set; }

    // ... 
}

Using other validation libraries

Some people prefer not to use .NET Data Annotations and rather bring their own favorite validation library. While this is outside the scope of this tutorial, you should see the benefit of creating a model-driven application here: your rules are stored in the model and not in your code. This means that switching to another library only means updating your code generation template.

For example, the following function generates a validator for the popular FluentValidation library. The rest is left as an exercise to the reader.

/* In the imports section */
import { CSharpWriter, PropertyFeatures, ClassDefinition, MethodDefinition } from '@yellicode/csharp';

/* Somewhere before Generator.generateFromModel. Call this function for each class to generate a validator class for it. */
const writeValidator = (csharp: CSharpWriter, cls: elements.Class) => {   
    const validatorName = `${cls.name}Validator`;
    const validatorClass: ClassDefinition = {name: validatorName, inherits: [`AbstractValidator<${cls.name}>`], accessModifier: 'public' };
    const validatorConstructor: MethodDefinition = {name: validatorName, isConstructor: true, accessModifier: 'public'};

    csharp.writeClassBlock(validatorClass, () => {
        csharp.writeMethodBlock(validatorConstructor, () => {
            cls.ownedAttributes.filter(property => issueManager.isCustomConstraints(property))
            .forEach((property: issueManager.CustomConstraintsProperty) => {
                if (property.IsRequired) {
                    csharp.writeLine(`RuleFor(x => x.${property.name})`);                    
                    csharp.writeLineIndented(`.NotEmpty()`);
                    csharp.writeLineIndented(`.WithMessage("${property.name} is required.");`);
                    csharp.writeLine();
                 }
                 if (property.MaxStringLength) {
                    csharp.writeLine(`RuleFor(x => x.${property.name})`);
                    csharp.writeLineIndented(`.MaximumLength(${property.MaxStringLength})`);
                    csharp.writeLineIndented(`.WithMessage("The ${property.name} value cannot exceed ${property.MaxStringLength} characters.");`);                    
                    csharp.writeLine();
                 }
            });
        })
    })
}

At this point, you've learned all the important aspects of model-driven development with Yellicode. There are many ways to extend Yellicode and use your model to generate code for multiple programming languages. If you are into TypeScript, the last part of the tutorial is for you. Imagine that the types in your model are also consumed - through a REST API - by a web application that is built with TypeScript? Off course, you want to have these types on the client-side too, don't you?

Continue to Part VI - Targeting a different language »