DEV Community

El cat bot
El cat bot

Posted on

C# Delegates, the basics

In C#, we have a powerful feature that can help us reference methods for future execution. This implementation is the Delegate type.

Specifically, it defines methods, parameters and return type. Think about delegates as Callback method that it is passed into other methods.

Ordinary Method call.

Let's say We are writing a program that needs to make a discount calculation based on a client membership type:

  • Basic: 10%
  • Premium: 30%
  • No-membership: 0%
public decimal GetDiscount(decimal price, string customerType)
{
    if(customerType == "Basic")
    {
        return price = price * 0.90M;
    }
    else if(customerType == "Premium")
    {
        return price = price * 0.70M;
    }
    else
    {
        return price;
    }
}

var price = GetDiscount(450, "Premium");

Console.WriteLine(price); // 315.00
Enter fullscreen mode Exit fullscreen mode

After passing a price of 450, discount will be 315. GetDiscount works but if we think about validations, this method shouldn't have to validate that the customer has a membership or not. In terms of Single Responsibility SOLID principle, GetDiscount should only have to calculate discount and return the value. That's all.

Delegate implementation

Fortunately, it is possible to add a delegate to clean up our code:

1. Declare the delegate:

public delegate decimal Calculation(decimal price);
Enter fullscreen mode Exit fullscreen mode

It's worth pointing out that this delegate is a method wrapper. Delegate signature must match with our GetDiscount method. So, method return type and parameters must be equals to the delegate.

2. Modify GetDiscount to receive only Calculation delegate as a parameter:

public decimal GetDiscount(Calculation calculation, decimal price)
{
    return calculation(price);
}
Enter fullscreen mode Exit fullscreen mode

Note our code is reduced to one line of code. That's because GetDiscount performs discount and method logic will be added as parameter when GetDiscount is called.

3. Call GetDiscount and pass our delegate and price as parameters:

GetDiscount(price => price * 0.90M, 450); // 405.00

GetDiscount(price => price * 0.70M, 450); // 315.00
Enter fullscreen mode Exit fullscreen mode

When calling GetDiscount, the first parameter will be our discount delegate using anonymous function. Instead of writing discount process in GetDiscount, we pass it as parameter to be invoked later. The second parameter is price.

The Complete code:

public delegate decimal Calculation(decimal price);

public void Main()
{
    var discountBasic = GetDiscount(price => price * 0.90M, 450);
    var discountPremium =  GetDiscount(price => price * 0.70M, 450);

    Console.WriteLine("Having a price of 450: ");
    Console.WriteLine($"    Premium membership has a discount => {discountBasic}");
    Console.WriteLine($"    Basic membership has a discount  => {discountPremium}");
}

public decimal GetDiscount(Calculation calculation, decimal price)
{
    return calculation(price);
}
Enter fullscreen mode Exit fullscreen mode

Multicast Delegate

There are situations where we have call more than one implementation, one after another, and each method shares signature (same return type, same parameters).

Let's say we have a program that creates an User and after that, it calls three methods to log a message to Local File, Azure and AWS cloud:

public void LogToAWS(string message)
{
    // Save to AWS Cloud....
    Console.WriteLine($"AWS => {message}");
}

public void LogToFile(string message)
{
    // File.Write....
    Console.WriteLine($"File => {message}");
}

public void LogToAzure(string message)
{
    // Save to Azure Cloud....
    Console.WriteLine($"Azure => {message}");
}
Enter fullscreen mode Exit fullscreen mode

A CreateUser method calls each Log implementation:

public void CreateUser()
{
    // Create User logic...
    // User created
    var message = "User created Successfully";

    LogToAWS(message);
    LogToFile(message);
    LogToCloud(message);
}

// AWS => User created Successfully
// File => User created Successfully
// Azure => User created Successfully
Enter fullscreen mode Exit fullscreen mode

That's perfectly working but what happens if new Log destinations are added. For example ElasticSearch and more. It wouldn't be clean adding more methods that do the same thing for different providers.

With Multicast delegates we can declare a class containing a delegate and a centralized method that invokes it. Let's see how it works:

1. Declare LogHandler delegate:

public delegate void LogHandler(string message);
Enter fullscreen mode Exit fullscreen mode

2. Create MyLogger class:

public class MyLogger
{
    public LogHandler Log;

    public void LogMessage(string message)
    {
        Log?.Invoke(message);
    }
}
Enter fullscreen mode Exit fullscreen mode

Here, we declare MyLogger class, a local delegate called LogHandler and LogMessage method. Our method accepts the message and then invokes our delegate with "Log?.Invoke()" syntax. See the method signature is still the same as all the previous Log methods.

3. Modify CreateUser method to call our delegate:

public void CreateUser()
{
    // Create User logic...
    // User created
    var message = "User created Successfully";

    var myLogger = new MyLogger();

    myLogger.Log +=  (message) => 
    { 
        // Send to AWS Cloud....
        Console.WriteLine($"AWS => {message}");
    };
}
Enter fullscreen mode Exit fullscreen mode

Here, we created and instance of MyLogger class, this is where our delegate "Log" is located. After that, we assign our LogToAWS function using anonymous function. You may be asking "What happened with LogToAWS method?". We got rid of it. Why? Although is completely right to call it like this:

myLogger.Log += LogToAWS;

Enter fullscreen mode Exit fullscreen mode

It's cleaner to assign an anonymous function for readability. But it's up to you how to call it.

Then we call LogMessage method and pass our message to invoke our function:

// Execute Delegate
myLogger.LogMessage(message); //AWS => User created Successfully
Enter fullscreen mode Exit fullscreen mode

To add another function to the delegate, just assign it as a concatenation item:

myLogger.Log +=  (message) => 
{ 
    // File.Write....
    Console.WriteLine($"File => {message}");
};
Enter fullscreen mode Exit fullscreen mode

The final code with all possible Loggers and MyLogger class:

public delegate void LogHandler(string message);

public class MyLogger
{
    public LogHandler Log;

    public void LogMessage(string message)
    {
        Log?.Invoke(message);
    }
}

public void CreateUser()
{
    // Create User logic...
    // User created
    var message = "User created Successfully";

    var myLogger = new MyLogger();

    myLogger.Log +=  (message) => 
    { 
        // Send to AWS Cloud....
        Console.WriteLine($"AWS => {message}");
    };

    myLogger.Log +=  (message) => 
    { 
        // File.Write....
        Console.WriteLine($"File => {message}");
    };

    myLogger.Log +=  (message) => 
    { 
        // Send to Azure Cloud....
        Console.WriteLine($"Azure => {message}");
    };

    myLogger.Log +=  (message) => 
    { 
        // Send to Elasticsearch....
        Console.WriteLine($"Elasticsearch => {message}");
    };

    // Execute Delegate
    myLogger.LogMessage(message);

    //AWS => User created Successfully
    //File => User created Successfully
    //Azure => User created Successfully
    //Elasticsearch => User created Successfully
}
Enter fullscreen mode Exit fullscreen mode

I expect you got the idea and have learned the basics about delegates and more important, in which scenarios you can implement them.

So, Thanks for reading and keep learning =)

Top comments (0)