Search code examples
c#powershelleventscallbackdelegates

Calling a C# delegate and then handling a callback in powershell


Let us say I have a BankBalance class in C# that defines a function DebitTransaction. DebitTransaction performs following operation. If balance > amount to be debited, it debits the amount and also notifies the caller that operation is successful. Otherwise it notifies the caller that operation failed.

Below is the code inside a C# DLL as below

using System;
namespace BankSystem
{
    public class BankBalance
    {
        public int Balance { get; set; } = 1000;
        // Define a delegate that informs the caller about the transaction status
        public delegate void DebitTransactionStatus(string message);
        public void DebitTransaction(int Debit, DebitTransactionStatus debitTransactionStatus)
        {
            if (Balance >= Debit)
            {
                Balance -= Debit;
                debitTransactionStatus("Amount is Debited Successfully");
            }
            else
            {
                debitTransactionStatus("Not enough balance to perform debit operation");
            }
       }
    }
}

Below code is the caller (C# exe calling C# DLL function DebitTransaction)which requests a debit operation.

using System;
using BankSystem;
namespace Transaction
{
    class DebitTransaction
    {
        static void ShowDebitTransactionStatus(string message)
        {
            Console.WriteLine(message);
        }
        static void Main(string[] args)
        {
            BankBalance bankBalance = new BankBalance();
            BankBalance.DebitTransactionStatus debitTransactionStatus = new BankBalance.DebitTransactionStatus(ShowDebitTransactionStatus);
            bankBalance.DebitTransaction(500, debitTransactionStatus);
            bankBalance.DebitTransaction(600, debitTransactionStatus);
        }
    }
}

Above program works and here is the output

Amount is Debited Successfully

Not enough balance to perform debit operation

I need to call C# DLL function DebitTransaction function from powershell (calling C# delegate & then register for the call back) to get the similar result

This would be a great help for me, since I am a C++/C# developer but now doing Automation stuffs in PowerShell due to lack of resources in our company. I am also a newbie in PowerShell. (The Project I am working on has many exported functions which has delegates & callbacks)

I tried couple of links, but those are not working for me. https://renenyffenegger.ch/notes/Microsoft/dot-net/namespaces-classes/System/Delegate/CreateDelegate/example-PowerShell/index


Solution

  • PowerShell can convert a ScriptBlock {...} into a DebitTransactionStatus delegate in this case, updating the code a bit for the demo so it's compatible with C# 5 and compiles in Windows PowerShell 5.1.

    Add-Type @'
    namespace BankSystem
    {
        public class BankBalance
        {
            private int _balance = 1000;
    
            public int Balance
            {
                get { return _balance; }
                set { _balance = value; }
            }
    
            // Define a delegate that informs the caller about the transaction status
            public delegate void DebitTransactionStatus(string message);
    
            public void DebitTransaction(
                int Debit,
                DebitTransactionStatus debitTransactionStatus)
            {
                if (Balance >= Debit)
                {
                    Balance -= Debit;
                    debitTransactionStatus("Amount is Debited Successfully");
                    return;
                }
    
                debitTransactionStatus("Not enough balance to perform debit operation");
           }
        }
    }
    '@
    
    $balance = [BankSystem.BankBalance]::new()
    $balance.DebitTransaction(1000, { param($message) Write-Host $message })
    # Amount is Debited Successfully
    $balance.DebitTransaction(1000, { param($message) Write-Host $message })
    # Not enough balance to perform debit operation
    

    If you have it already compiled you can use Add-Type targeting the path to your assembly instead of embedding the C# code, for example:

    Add-Type -Path path\to\myAssembly.dll
    
    $balance = [BankSystem.BankBalance]::new()
    $balance.DebitTransaction(1000, { param($message) Write-Host $message })
    # Amount is Debited Successfully
    $balance.DebitTransaction(1000, { param($message) Write-Host $message })
    # Not enough balance to perform debit operation
    

    Newer versions of PowerShell 7 will also allow you to pass-in a PSMethod as your delegate:

    class DebitTransaction {
        static [void] ShowDebitTransactionStatus([string] $message) {
            [System.Console]::WriteLine($message)
        }
    }
    
    $balance = [BankSystem.BankBalance]::new()
    $balance.DebitTransaction(1000, [DebitTransaction]::ShowDebitTransactionStatus)
    # Amount is Debited Successfully
    $balance.DebitTransaction(1000, [DebitTransaction]::ShowDebitTransactionStatus)
    # Not enough balance to perform debit operation
    

    In older versions this approach is more cumbersome... You have to create a delegate from the MethodInfo:

    $delegate = [DebitTransaction].GetMethod('ShowDebitTransactionStatus').
        CreateDelegate([BankSystem.BankBalance+DebitTransactionStatus])
    
    $balance = [BankSystem.BankBalance]::new()
    $balance.DebitTransaction(1000, $delegate)
    # Amount is Debited Successfully
    $balance.DebitTransaction(1000, $delegate)
    # Not enough balance to perform debit operation
    

    As suggested in comments, using an event that a consumer can subscribe to would be the standard thing to do.

    Here is a demo of how it'd look like:

    namespace BankSystem;
    
    public class BankBalance
    {
        public int Balance { get; set; } = 1000;
        // Define a delegate that informs the caller about the transaction status
        public delegate void DebitTransactionStatus(string message);
        // Define an event of type DebitTransactionStatus
        public event DebitTransactionStatus? DebitRegistered;
    
        public void DebitTransaction(int Debit)
        {
            if (Balance >= Debit)
            {
                Balance -= Debit;
                // if there is a subscriber send this message
                DebitRegistered?.Invoke("Amount is Debited Successfully");
                return;
            }
    
            // if there is a subscriber send this message
            DebitRegistered?.Invoke("Not enough balance to perform debit operation");
        }
    }
    

    Then on the consumer side (PowerShell), you'd be using Register-ObjectEvent subscribing to the DebitRegistered event:

    $balance = [BankSystem.BankBalance]::new()
    $registerObjectEventSplat = @{
        InputObject = $balance
        EventName   = 'DebitRegistered'
        Action      = {
            param($message)
    
            Write-Host $message
        }
    }
    $evt = Register-ObjectEvent @registerObjectEventSplat
    
    $balance.DebitTransaction(1000)
    # Amount is Debited Successfully
    $balance.DebitTransaction(1000)
    # Not enough balance to perform debit operation