.NET Delegates for Mere Mortals: Introduction

Introduction

Starting from this tutorial, we’re going to delve into the details of .NET delegates in practice. This is the first one that will help you to understand the whole concept, and the text tutorials will teach you how Seniors use delegates in their practice.

To follow our examples just download the source code.

Ok, let's start our development.

1. Go to Visual Studio, select Console application-> rename your project to "CsharpDelegates”.

2. Create Database folder-> Inside it create a “Models” folder and create the following classes in it.

namespace CsharpDelegates.Database.Models
{
    public class Customer
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Address { get; set; }
        public ICollection<Card> Cards { get; set; }
        public override string ToString()
        {
            return $"Id = {Id}, Name = {Name}, Address = {Address}";
        }
    }
}
using System.Net;

namespace CsharpDelegates.Database.Models
{
    public class Card
    {
        public int Id { get; set; }
        public string HolderName { get; set; }
        public string ExpiryDate { get; set; }
        public string Number { get; set; }
        public Customer Customer { get; set; }
        public int CustomerId { get; set; }
        public override string ToString()
        {
            return $"Id = {Id}, HolderName = {HolderName}, Number = {Number},ExpiryDate = {ExpiryDate}, CustomerId = {CustomerId}";
        }
    }
}
using Microsoft.EntityFrameworkCore;

namespace CsharpDelegates.Database.Models
{
    public class CustomerAppDbContext : DbContext
    {
        public DbSet<Customer> Customers { get; set; }
        public DbSet<Card> Cards { get; set; }
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
           // optionsBuilder.LogTo(message => Console.WriteLine(message));
            optionsBuilder.UseSqlServer(@"Data Source=.\;Initial Catalog=CustomerDb;Integrated Security=SSPI;TrustServerCertificate=True;MultipleActiveResultSets=true");
        }
    }
}

To make sure that you have the framework installed, go to “Tools”->” Nuget package manager”->” package manager console” and type the following.

install-package Microsoft.entityframeworkcore.design

install-package Microsoft.entityframeworkcore.sqlserver

Now we have all libraries installed let's run our migration to create a database with tables

Add-Migration Initial
Update-Database

Now you should have a Migrations folder with Snapshot and Initial Generation. You can add dummy data to your tables directly from the SQL server or use the Seed in Entity framework.

What I want to do is return Cards that the customer is equal to 4.

 private static List<Card> GetCardsByCustomerIdEqualto4()
 {
     List<Card> cards = [];
     using var dbcontext = new CustomerAppDbContext();
     var dbCards = dbcontext.Cards.ToList();
     foreach (var card in dbCards)
     {
         if (card.CustomerId == 4)
         {
             cards.Add(card);
         }
     }
     return cards;
 }

This is a bit odd method that is not reusable. The problem here is, that when we want to return customerId that is equal to 5, we need to create another method.

This is what its reusability looks like.

 Reusability

Let's try to make it better and for that reason, we need to think about its design.

What do we want to reuse? What is a blocker for us to make this method be used for other customers? Of course, we have a direct dependency on the magic number, which is “4”. Let's isolate it and move it to the argument

private static List<Card> GetCardsByCustomerId(int customerId)
{
    List<Card> cards = [];
    using var dbcontext = new CustomerAppDbContext();
    var dbCards = dbcontext.Cards.ToList();
    foreach (var card in dbCards)
    {
        if (card.CustomerId == customerId)
        {
            cards.Add(card);
        }
    }
    return cards;
}

Now our reusability looks like below.

Extensibility diagram

Sometimes later, we as developers realize that we do not just need to search for customers where customerId is equal to the given card customer, but also we need to search for the card’s customerId is greater than, is less than, is greater or equal, and so on. So we need to somehow extend our search.

Ok, but how it is possible?

Let’s think about it. Instead of just moving customerId to the argument, we need to move the whole phrase to the argument. It should look like this.

private static List<Card> _GetCardsByCustomerId((card.CustomerId >= customerId) expression)
{
    List<Card> cards = [];
    using var dbcontext = new CustomerAppDbContext();
    var dbCards = dbcontext.Cards.ToList();
    foreach (var card in dbCards)
    {
        if (expression)
        {
            cards.Add(card);
        }
    }
    return cards;
}

This code of course is not working, this is just a pseudo-code.

It means we create an expression as an argument and from the expression define what we want to do. This is what exactly delegates do for us.

Delegate is a callback that allows you to provide “a part of the method, an expression” from the argument and make the code reusable. Instead of moving just magic numbers to arguments, you should move the whole expression to the argument.

You can create your delegates using the delegate keyword.

public delegate bool CustomerDelegate(int customerId);

Delegate’s signature should match the signature of your method. It means if you’re planning to return a bool and accept an integer for your expression, your delegate should do the same and it means it should have the same signature.

Now we have a delegate, let's use it.

private static List<Card> GetCardsByCustomerDelegate(CustomerDelegate customerDelegate)
{
    List<Card> cards = [];
    using var dbcontext = new CustomerAppDbContext();
    var dbCards = dbcontext.Cards.ToList();
    foreach (var card in dbCards)
    {
        if (customerDelegate(card.CustomerId))
        {
            cards.Add(card);
        }
    }
    return cards;
}

Here is our diagram for the delegate.

Diagram for the delegate

You can call the delegate using multiple syntaxes.

1. Create a separate method for it

static void Main()
{
    //var cards = GetCards(x => x.HolderName == "Hanma Baki");
    var cards = GetCardsByCustomerDelegate(new CustomerDelegate(GetByCustomersGreaterThan100));

    foreach (var card in cards)
    {
        Console.WriteLine(card);
    }
    Console.ReadLine();
}

static bool GetByCustomersGreaterThan100(int customerId)
{
    return customerId > 100;
}

2. Without using a new keyword

 static void Main()
 {
     //var cards = GetCards(x => x.HolderName == "Hanma Baki");
     var cards = GetCardsByCustomerDelegate(GetByCustomersGreaterThan100);

     foreach (var card in cards)
     {
         Console.WriteLine(card);
     }
     Console.ReadLine();
 }

 static bool GetByCustomersGreaterThan100(int customerId)
 {
     return customerId > 100;
 }

3. Using anonymous methods

static void Main()
 {
     //var cards = GetCards(x => x.HolderName == "Hanma Baki");
     var cards = GetCardsByCustomerDelegate(delegate(int i) => i> 100);

     foreach (var card in cards)
     {
         Console.WriteLine(card);
     }
     Console.ReadLine();
 }

4. Using lambda expressions

static void Main()
{
    //var cards = GetCards(x => x.HolderName == "Hanma Baki");
    var cards = GetCardsByCustomerDelegate(i => i > 100);

    foreach (var card in cards)
    {
        Console.WriteLine(card);
    }
    Console.ReadLine();
}

It is better not to create your delegate, but use Action, Func type of Microsoft provided delegates

Conclusion

A delegate in C# is a powerful tool for creating flexible and reusable code. In essence, it acts as a reference to a method, allowing you to treat a method as a value.