DEV Community

loading...

C# Generics and Delegates: A Simple but Real Example

stevebrownlee profile image Steve Brownlee ・6 min read

Originally published at Steve Brownlee


If you search for information about the C# Generics language feature, you see the same abstract code examples about making your own RedundantList<T> (but don't do this, it's just our example) over and over again. It's remarkably difficult to find content where someone shows how they actually used it to make their code more reusable.

Well, the topic of Generics came up in a discussion this week with my students in Day Cohort 26 of Nashville Software School so I went looking and realized that most of the content is unusable for a student. Of course, the purpose and usage of Generics is largely beyond the capabilities of a student with 4 months of development experience to understand, but I wanted show them why they are used with a straightforward example.

I've been using a simple CLI app to teach them the basics of the language syntax, LINQ, classes, OOP, and SQL. I'm just about to connect the dots for them by using Dapper ORM to query a small SQLite database.

In order to create the tables, and seed them with initial data, so that they can practice SQL, I created a DatabaseInterface.cs file with a series of static methods in it to perform those tasks.

Data/DatabaseInterface.cs

...

/*
    Purpose: Check the database to see if the `Exercise` table 
    has been defined. If not, create it and seed it.
*/
public static void CheckExerciseTable()
{
    SqliteConnection db = DatabaseInterface.Connection;

    try
    {
        // Select the ids from the table to see if it exists
        IEnumerable<Exercise> exercises = db.Query<Exercise>
            ("SELECT Id FROM Exercise");
    }
    catch (System.Exception ex)
    {
        /*
            If an exception was thrown with the text "no such table"
            then the table doesn't exist. Execute a CREATE TABLE
            statement to create it.
        */
        if (ex.Message.Contains("no such table"))
        {
            db.Execute(@"CREATE TABLE Exercise (
                `Id`    INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
                `Name`  TEXT NOT NULL,
                `Language` TEXT NOT NULL
            )");

            /*
                Seed the table with some initial entries
            */
            db.Execute(@"INSERT INTO Exercise
                VALUES (null, 'ChickenMonkey', 'JavaScript')");
            db.Execute(@"INSERT INTO Exercise
                VALUES (null, 'Overly Excited', 'JavaScript')");
            db.Execute(@"INSERT INTO Exercise
                VALUES (null, 'Boy Bands & Vegetables', 'JavaScript')");

        }
    }
}

...
Enter fullscreen mode Exit fullscreen mode

I continue to do this for each table that I want in my database. For example, I then check the Instructors table.

Data/DatabaseInterface.cs

...

public static void CheckInstructorsTable()
{
    SqliteConnection db = DatabaseInterface.Connection;

    try
    {
        IEnumerable<Instructor> instructors = db.Query<Instructor>
            ("SELECT Id FROM Instructor");
    }
    catch (System.Exception ex)
    {
        if (ex.Message.Contains("no such table"))
        {
            db.Execute($@"CREATE TABLE Instructor (
                `Id`          INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
                `FirstName`   TEXT NOT NULL,
                `LastName`    TEXT NOT NULL,
                `SlackHandle` TEXT NOT NULL,
                `Specialty`   TEXT,
                `CohortId`    INTEGER NOT NULL,
                FOREIGN KEY(`CohortId`) REFERENCES `Cohort`(`Id`)
            )");

            db.Execute($@"INSERT INTO Instructor
                SELECT null,
                        'Steve',
                        'Brownlee',
                        '@coach',
                        'Dad jokes',
                        c.Id
                FROM Cohort c WHERE c.Name = 'Evening Cohort 1'
            ");

            db.Execute($@"INSERT INTO Instructor
                SELECT null,
                        'Joe',
                        'Shepherd',
                        '@joes',
                        'Analogies',
                        c.Id
                FROM Cohort c WHERE c.Name = 'Day Cohort 13'
            ");

            db.Execute($@"INSERT INTO Instructor
                SELECT null,
                        'Jisie',
                        'David',
                        '@jisie',
                        'Student success',
                        c.Id
                FROM Cohort c WHERE c.Name = 'Day Cohort 21'
            ");


            db.Execute($@"INSERT INTO Instructor
                SELECT null,
                        'Emily',
                        'Lemmon',
                        '@emlem',
                        'Memes',
                        c.Id
                FROM Cohort c WHERE c.Name = 'Day Cohort 21'
            ");
        }
    }
}

...
Enter fullscreen mode Exit fullscreen mode

There are a total of five tables that I create and seed.

  1. Instructor
  2. Student
  3. Exercise
  4. Cohort
  5. StudentExercise

That's five methods that all do the exact same thing, but just work against a different resource. To make the code more reusable, I went through a three step process.

Step 1: Create and Seed Static Methods

The first step was to move the commands for creating a table, and seeding the table, to static methods on the database models. This helps me implement the Single Responsibiliy Principle. Otherwise, the DatabaseInterface class has myriad reasons to change.

/Models/Exercise.cs

using System.Collections.Generic;
using Dapper;
using Microsoft.Data.Sqlite;

namespace nss.Data.Models
{
    public class Exercise
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Language { get; set; }
        public List<Student> AssignedStudents { get; set; }

        public static void Create(SqliteConnection db)
        {
            db.Execute(@"CREATE TABLE Exercise (
                        `Id`       INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
                        `Name`     TEXT NOT NULL,
                        `Language` TEXT NOT NULL
                    )");
        }

        public static void Seed(SqliteConnection db)
        {
            db.Execute(@"INSERT INTO Exercise
                        VALUES (null, 'ChickenMonkey', 'JavaScript')");
            db.Execute(@"INSERT INTO Exercise
                        VALUES (null, 'Overly Excited', 'JavaScript')");
            db.Execute(@"INSERT INTO Exercise
                        VALUES (null, 'Boy Bands & Vegetables', 'JavaScript')");
        }

    }

}
Enter fullscreen mode Exit fullscreen mode

This makes the CheckExerciseTable() method in the DatabaseInterface class cleaner and the application is more modular.

Data/DatabaseInterface.cs

...

public static void CheckExerciseTable()
{
    SqliteConnection db = DatabaseInterface.Connection;

    try
    {
        // This part still tightly coupled to Exercise type  :(
        IEnumerable<Exercise> exercises = db.Query<Exercise>
            ("SELECT Id FROM Exercise");
    }
    catch (System.Exception ex)
    {
        if (ex.Message.Contains("no such table"))
        {
            // Et voilà... Very cleaner! Much modular!  :)
            Exercise.Create(db);
            Exercise.Seed(db);
        }
    }
}

...
Enter fullscreen mode Exit fullscreen mode

Step 2: Generic CheckTable Method

Next, I started the process of making a single method that would check for any table's existence, and create/seed it if needed.

public static void CheckTable<T>()
{
    SqliteConnection db = DatabaseInterface.Connection;

    try
    {
        // Coupling to Exercise type eliminated, but SQL still coupled  :(
        IEnumerable<T> exercises = db.Query<T>("SELECT Id FROM Exercise");
    }
    catch (System.Exception ex)
    {
        if (ex.Message.Contains("no such table"))
        {
            // Made it modular, but still coupled to Exercise type  :(
            Exercise.Create(db);
            Exercise.Seed(db);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

By using the T generic type, I can now invoke the method and use the specified type for the List of results and the Dapper Query method.

Program.cs

DatabaseInterface.CheckTable<Exercise>();
Enter fullscreen mode Exit fullscreen mode

My SQL statement is still querying the Exercise table, so I needed to pass the table name in as a parameter to make this method reusable. Then I refactored the SQL statement to use the parameter.

// When invoked with the code above, T = Exercise
public static void CheckTable<T>(string table)
{
    SqliteConnection db = DatabaseInterface.Connection;

    try
    {
        // More generic! Coupling eliminated here!  :)
        IEnumerable<T> resources = db.Query<T>($"SELECT Id FROM {table}");
    }
    catch (System.Exception ex)
    {
        if (ex.Message.Contains("no such table"))
        {
            // Still tightly coupled to Exercise type  :(
            Exercise.Create(db);
            Exercise.Seed(db);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Action Delegates to Create and Seed

The last part that needs to be reusable is invoking the Create() and Seed() static methods on the appropriate type. Unfortunately, it is illegal in C# to invoke a static method on a generic type.

// T represents the type used (i.e. Student, Exercise, etc.)
T.Create(db);  // Illegal. Much sad.
T.Seed(db);    // Also illegal. Still frown.
Enter fullscreen mode Exit fullscreen mode

The only other option I could think of in this situation is making a delegate; specifically, an Action delegate. Why?

Encapsulates a method that has a single parameter and does not return a value.

That perfectly describes the Create() and Seed() static methods that I have on the data models.

// Hey, look! This method has a single parameter and is void
public static void Create(SqliteConnection db)
{
    db.Execute(@"CREATE TABLE Exercise (
        `Id`       INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
        `Name`     TEXT NOT NULL,
        `Language` TEXT NOT NULL
    )");
}
Enter fullscreen mode Exit fullscreen mode

What this mechanism will allow me to do is pass the reference to the correct static method to the CheckTable<T> method. Take a look.

public static void CheckTable<T>(
    string table,
    Action<SqliteConnection> create,  // <-- Delegate to the correct `Create()`
    Action<SqliteConnection> seed     // <-- Delegate to the correct `Seed()`
)
{
    SqliteConnection db = DatabaseInterface.Connection;

    try
    {
        IEnumerable<T> resources = db.Query<T>($"SELECT Id FROM {table}");
    }
    catch (System.Exception ex)
    {
        if (ex.Message.Contains("no such table"))
        {
            create(db);  // Invoke the Create() reference
            seed(db);    // Invoke the Seed() reference
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Now I can use the CheckTable<T>() method to verify, create, and seed any database table that I want to use. I pass in the following parameters.

  1. A string holding the name of the table I want to use in the SQL
  2. A reference to a static Create() method on the corresponding class
  3. A reference to a static Seed() method on the corresponding class
DatabaseInterface.CheckTable<Exercise>("Exercise", Exercise.Create, Exercise.Seed);
DatabaseInterface.CheckTable<Cohort>("Cohort", Cohort.Create, Cohort.Seed);
DatabaseInterface.CheckTable<Instructor>("Instructor", Instructor.Create, Instructor.Seed);
DatabaseInterface.CheckTable<Student>("Student", Student.Create, Student.Seed);
DatabaseInterface.CheckTable<StudentExercise>("StudentExercise", StudentExercise.Create, StudentExercise.Seed);
Enter fullscreen mode Exit fullscreen mode

Simple and Real

No discussion about obscure data structures that nearly no one will ever use in their daily work. No abstract example code that doesn't convey any real value to a developer. Just a simple example of a real application used to train junior developers in the usage of C# and SQL, and how to use Generics to make the code reusable, and easier to read/maintain.

🍴Fork on Github: https://github.com/nss-day-cohort-26/student-exercises/tree/dapper-sql-advanced

Discussion (2)

pic
Editor guide
Collapse
pchinery profile image
Philip

Either I don't understand your point or I disagree. I think it does make sense to explain the concept of generics with plain lists, as you will use them on a daily basis (and I have to disagree with you: the SQL stuff won't be daily real world code for everyone or probaly even the majority). A list is very easy to understand and you can show that you either have to cast types a lot or implement one list or class to be contained otherwise. With generics, it's one implementation for all. And it's an isolated pattern explained and not combined with actions, static methods, SQL and ORM in the mix. After understand this in isolation, the students can start to learn this in combination with other code and other patterns.

Collapse
stevebrownlee profile image
Steve Brownlee Author

Thanks for the comment, Philip! Explaining concepts in isolation rarely helps students achieve understanding. Adult students need to know how something helps solve problems they are working on. My students happened to be working on that code, thus the example above.