Part II - Generating the back-end

In this part of the tutorial, you will build the ASP.NET Core Web API. You won't build a UI yet. At the end of this part, you have generated:

  • Entities: POCO (Plain Old CLR Object) classes for the Entity Framework data model.
  • An Entity Framework DataContext.
  • A class that configures Entity Framework using Fluent Configuration.
  • REST Resources: DTO's (Data Transfer Objects) that can consumed by the front-end.
  • Mapping logic between REST Resources and Entities.
  • Controllers for the basic CRUD actions.

To keep the tutorial simple, the app doesn’t use a persistent database. The app stores items in an in-memory database.

Creating the Entity Framework model

Your first template will generate POCO entities for the Entity Framework. Open file CodeGen/api/entities.template.ts and insert the following TypeScript code:

import * as elements from '@yellicode/elements';
import { TextWriter } from '@yellicode/core';  
import { Generator } from '@yellicode/templating';  
import { CSharpWriter } from '@yellicode/csharp';
import * as ef from './lib/entity-framework';
import * as utils from './lib/utils';

const outputDirectory = '../../api/data';
const options = { outputFile: `${outputDirectory}/Entities.cs` };

Generator.generateFromModel(options, (textWriter: TextWriter, model: elements.Model) => {
    const writer = new CSharpWriter(textWriter);
    const classOptions = { implements: ['IEntity'] } // implement our custom IEntity interface 
    utils.writeCodeGenerationWarning(writer);
    writer.writeUsingDirectives('System', 'System.Collections.Generic');
    writer.writeLine(); // insert a blank line
    writer.writeNamespaceBlock({ name: 'Bookstore.Api.Data' }, () => {
        model.getAllClasses().forEach(c => {
            writer.writeLine();
            writer.writeClassBlock(c, () => {
                c.ownedAttributes.forEach(att => {
                    ef.writeEntityProperty(writer, att);
                    writer.writeLine();
                });
            }, classOptions);

            // Create join tables for many-to-many-relations
            c.ownedAttributes.filter(att => ef.isManyToMany(att)).forEach((att) => {
                ef.writeJoinTable(writer, att);
            });
        });
    });
});

The Generator.generateFromModel function retreives the bookstore model and let's us execute a code generation template against the model. As you can see, the template uses a CSharpWriter from the @yellicode/csharp NPM package. This is a custom code writer that is specialised for writing C# code. Also, there is a helper module (alias 'ef') in the project for dealing with Entity Framework specifics (this might become a NPM package in the future).

The template loops over all classes in the model and generates a C# class for each one. In addition, Entity Framework requires join tables for many-to-many relations (remember the Book -> Authors relation?), so why not generate these as well!

You won't need to create all other Entity Framework templates, but feel free to inspect the following files in the in the CodeGen/api folder:

  • model-configuration.template.ts: generates a Fluent Configuration for for many-to-many relations (to keep it simple, the rest is based on conventions).
  • data-context.template.ts: generates the BookstoreContext data context, with a DbSet property for each type of entity.

Creating the REST API

Our REST API does not expose Entity Framework entities directly. Instead, we will generate REST resources that correspond to classes in our Bookstore model. The resources won't have navigation properties like the EF entities do but will instead point to other resources through their id (or, more precisely, through a ResourceInfo object that is already part of the project). Open file CodeGen/api/resources.template.ts and insert the following TypeScript code:

import * as elements from '@yellicode/elements';
import { TextWriter } from '@yellicode/core';  
import { Generator } from '@yellicode/templating';  
import { SuffixingTransform, RenameTargets } from '@yellicode/elements';
import { CSharpWriter } from '@yellicode/csharp';
import * as utils from './lib/utils';

const outputDirectory = '../../api/resources';

/**
 * Writes all properties of a REST resource. 
 */
const resourcePropertiesTemplate = (writer: CSharpWriter, c: elements.Class): void => {
    // Write a property for each attribute. It depends on the original attribute type what to write here.
    c.ownedAttributes.forEach(att => {
        writer.writeLine();
        if (!elements.isClass(att.type)) {
            // A primitive type: write the property as-is.
            writer.writeAutoProperty(att);
        }
        else if (!att.isMultivalued()) {
            // A complex type, single-valued, create a foreign key (ResourceInfo) property instead.
            const optionalModifier = att.isOptional() ? '?' : '';
            writer.writeXmlDocSummary(att);
            writer.writeLine(`public ResourceInfo ${att.name} {get; set;}`);
        }
        else {
            // A complex type, multi-valued, create a foreign key (ResourceInfo) collection property instead.
            writer.writeXmlDocSummary(att);
            writer.writeLine(`public ICollection ${att.name} {get; set;}`);
        }
    });
    // Add a custom DisplayName property. We will use this property in the front-end.
    writer.writeXmlDocSummary(`Gets the display name of this ${c.name}. This property is generated.`);
    writer.writeLine(`public string DisplayName {get; set;}`);
}

// Add a 'Resource' suffix to each class in the model
const transform = new SuffixingTransform(RenameTargets.classes, 'Resource');
const options = { outputFile: `${outputDirectory}/Resources.cs`, modelTransform: transform };

Generator.generateFromModel(options, (textWriter: TextWriter, model: elements.Model) => {
    const writer = new CSharpWriter(textWriter);
    utils.writeCodeGenerationWarning(writer);
    writer.writeUsingDirectives('System', 'System.Collections.Generic');
    writer.writeLine();
    writer.writeNamespaceBlock({ name: 'Bookstore.Api.Resources' }, () => {
        model.getAllClasses().forEach(c => {
            writer.writeClassBlock(c, () => {
                resourcePropertiesTemplate(writer, c);
            })
            writer.writeLine();
        });
    });
});

This template has a similar form as the previous template, but changes the form of the classes a bit. First, in order to distinguish resources from entities, each class is renamed with a 'Resource' suffix using the SuffixingTransform class. Secondly, where the entities have navigation properties for complex types, the resources get simple ResourceInfo properties.

The other API templates are for conversion between resources and entities (CodeGen/api/resource-mapper.template.ts) and, of course, API controllers that allow the basic CRUD actions (CodeGen/api/controllers.template.ts) on our resources. We won't discuss these in detail, so just have a look at their contents if you are interested.

Generating the code

Now let's put the templates to work and generate your API code. First, you need to configure the API templates in Yellicode's configuration file, codegenconfig.json.

  1. Open file CodeGen/codegenconfig.json (this file is empty).
  2. Insert the following contents:
    {
        "templates": [ 
            {
                "templateFile": "./api/entities.template.ts",
                "modelFile":"./bookstore"
            },
            {
                "templateFile": "./api/data-context.template.ts",
                "modelFile":"./bookstore"
            },  
            {
                "templateFile": "./api/model-configuration.template.ts",
                "modelFile":"./bookstore"
            },        
            {
                "templateFile": "./api/controllers.template.ts",
                "modelFile":"./bookstore"
            },
            {
                "templateFile": "./api/resources.template.ts",
                "modelFile":"./bookstore"
            },
            {
                "templateFile": "./api/resource-mapper.template.ts",
                "modelFile":"./bookstore"
            }      
        ],
        "compileTypeScript": true
    }            
            
  3. Save the file.

Yellicode is configured and all your templates are ready to run!

  1. Open the CodeGen folder in the terminal.
  2. Start Yellicode.
    yellicode --watch
  3. Inspect the Api project and verify that new C# files are generated in the Controllers, Data and Resource folders.

Before you start the application, let's first seed it with some test data.

  1. Open file Api/Data/DbInitializer.cs and uncomment it.
  2. Open file Api/Startup.cs and, in the Configure method, uncomment the line that calls DbInitializer.Initialize():
    public void Configure(IApplicationBuilder app)
    {
        // Seed the database with some initial test data
        using (var serviceScope = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>().CreateScope())
        {
            var context = serviceScope.ServiceProvider.GetService<BookstoreContext>();
            // DbInitializer.Initialize(context); // <-- uncomment this after uncommenting DbInitializer.cs
        }
        // ....
                
    }
    

Now (re)start the API and navigate to http://localhost:5000/api/book. You should now see a JSON response containing a list of books and their relations.

Making changes to the model

Let's see what happens when you make changes to the model. If you haven't yet, open the bookstore model (CodeGen/bookstore.ymn) in Yellicode Modeler. As an example, let's extend the Book class with a Rating property.

  1. Open the Book class and add a new attribute by clicking the + sign in the attributes table and name it Rating.
  2. As Type, choose integer and choose public Visibility.
  3. Press CLTRL+S to save the model.

Because the Yellicode CLI is running in --watch mode, our updated code is generated almost instantly. Restart the API and refresh http://localhost:5000/api/book to see a new json field named rating.

Prepare for part III

That's it for the API. Before proceeding to part III, add a bit of manual code: in the resources template, the template added a DisplayName property to each resource. However, the display name should be filled differently for each type, so let's add - well, uncomment - a few lines:

  1. Open file Api/Data/EntityDisplayNameExtensions.cs.
  2. Uncomment the different overloads of GetDisplayName()
  3. Restart the API.

This completes part II. You have a fully functional REST API up and running. You can test it using Postman or curl, or just continue to part III.

Continue to Part III - Generating the front-end »