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.
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.
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.
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.