C# Interoperability: Calling Unmanaged Code with P/Invoke

Welcome again to another installment in the Mastering C# series. Today, I will walk you through a step-by-step guide on calling unmanaged code in C# using P/Invoke.

P/Invoke (Platform Invocation Services) is a feature in C# that allows you to call functions from unmanaged code, typically written in languages like C or C++. In simpler terms, P/Invoke helps you call functions from external libraries (usually .dll files) written in other languages, right from your C# code.

Pre-requisites

To fully benefit from this article, readers should have the following prerequisites:

  • Basic Knowledge of C# Programming

    • Familiarity with C# syntax and common constructs (variables, functions, classes).

    • Experience working with C# projects in Visual Studio or another IDE.

  • Understanding of Managed vs Unmanaged Code

    • Awareness of the differences between managed (C#/.NET) and unmanaged code (C/C++).

    • Basic understanding of how memory management differs in managed vs unmanaged environments.

  • Familiarity with Data Types in C#

    • Knowledge of common data types in C# (int, float, string, etc.).

    • Awareness of how data types may differ between managed and unmanaged code (e.g., pointers in C).

  • Experience with DLLs (Dynamic Link Libraries)

    • Understanding of what DLLs are and how they are used to encapsulate code in C/C++.

    • Basic knowledge of how to reference external libraries in a C# project.

  • Basic Debugging Skills

    • Familiarity with debugging techniques in Visual Studio (e.g., setting breakpoints, inspecting variables).

    • Experience troubleshooting errors and exceptions in C# applications.

  • Optional: Basic Knowledge of C/C++

    • Some understanding of C/C++ can be helpful, especially when working with unmanaged code.

    • Awareness of how functions, pointers, and memory management work in C/C++.

Table of Contents

  • Introduction to P/Invoke

  • Setting Up P/Invoke in C#

  • Calling Unmanaged Code: A Simple Example

  • Handling Complex Data Types

  • Marshaling Data Between Managed and Unmanaged Code

  • Dealing with Unmanaged Resources

  • Error Handling in P/Invoke

  • Common Pitfalls and Troubleshooting

  • Best Practices for P/Invoke

  • Additional Resources

Introduction to P/Invoke

What is P/Invoke?

P/Invoke (Platform Invocation Services) is a feature in C# that allows you to call functions from unmanaged code, typically written in languages like C or C++. Unmanaged code runs outside of the .NET runtime, meaning it doesn't benefit from automatic memory management and other services that .NET provides.

In simpler terms, P/Invoke helps you call functions from external libraries (usually .dll files) written in other languages, right from your C# code.

Why Use P/Invoke?

P/Invoke is useful when you need to:

  • Use existing functionality from external libraries without rewriting them in C#.

  • Call operating system APIs that are written in unmanaged languages like C/C++.

  • Work with older libraries that don't have managed code alternatives.

In many cases, it saves time and effort by allowing you to reuse code that already exists rather than having to rebuild it from scratch in C#.

Scenarios Where P/Invoke is Useful

Here are some common scenarios where P/Invoke can come in handy:

  1. Interfacing with System APIs:
    If your application needs to interact with low-level system components (like Windows API), P/Invoke allows you to call those APIs directly from your C# code.

  2. Reusing Legacy Code:
    When you have existing C/C++ libraries or legacy systems you can’t modify, P/Invoke allows you to call them from your modern C# application.

  3. Hardware Control:
    Sometimes, interacting with hardware requires calling unmanaged functions (e.g., drivers) that are written in C or C++.

Simple Example of P/Invoke

Let's say we want to use a function from the Windows API to show a message box on the screen. The MessageBox function is part of the User32.dll library in Windows.

Code Sample:

using System;
using System.Runtime.InteropServices; // Required for P/Invoke

class Program
{
    // Declare the external function from User32.dll
    [DllImport("user32.dll", CharSet = CharSet.Auto)]
    public static extern int MessageBox(IntPtr hWnd, String text, String caption, int options);

    static void Main()
    {
        // Call the external function to show a message box
        MessageBox(IntPtr.Zero, "Hello from P/Invoke!", "P/Invoke Example", 0);
    }
}

How It Works:

  • [DllImport]: This attribute tells C# that you're using an external function from a DLL. You specify the DLL name (user32.dll) and any additional details like character encoding (CharSet.Auto).

  • MessageBox: This is the function we're importing from user32.dll. It's declared using extern to tell C# that the implementation is outside of the current code.

  • MessageBox Function Call: In the Main method, we call the MessageBox function with text, caption, and options. The message box will pop up when you run the program.

Summary

P/Invoke allows C# developers to leverage powerful system APIs and external libraries written in unmanaged code. It is especially useful when you need functionality that doesn’t exist in the .NET framework. While it may seem tricky at first, with the right knowledge and examples, you can quickly start using it effectively in your projects.

Setting Up P/Invoke in C

P/Invoke (Platform Invocation) allows C# to call unmanaged code, usually written in C or C++, by importing functions from DLLs (Dynamic Link Libraries). Let's break it down step by step.

Defining External Functions

First, you need to define the external function you want to call from the unmanaged DLL. In C#, you use the DllImport attribute to do this.

Here’s how you can define an external function:

using System;
using System.Runtime.InteropServices;

class Program
{
    // Defining the external function using DllImport
    [DllImport("user32.dll", CharSet = CharSet.Auto)]
    public static extern int MessageBox(IntPtr hWnd, String text, String caption, uint type);

    static void Main()
    {
        // Calling the external function
        MessageBox(IntPtr.Zero, "Hello from P/Invoke!", "P/Invoke Demo", 0);
    }
}

In this example:

  • DllImport("user32.dll") tells the program that the function is located in the user32.dll.

  • The MessageBox function is imported from the unmanaged user32.dll library.

  • MessageBox is called just like any other C# function, but the actual function lives in the DLL.

Importing DLLs

The most important part of P/Invoke is specifying which DLL contains the unmanaged function you want to call. This is done with the DllImport attribute.

Example:

[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern bool Beep(uint dwFreq, uint dwDuration);

In this example:

  • "kernel32.dll": This is the name of the DLL where the Beep function resides.

  • Beep: This function makes a beep sound, and it’s defined in C’s kernel32.dll.

  • CharSet = CharSet.Auto: This specifies how character strings are marshaled (handled between managed and unmanaged code).

  • SetLastError = true: This means that if the function fails, you can retrieve the error code.

You can then call this function in C# just like you would call any other method:

Beep(1000, 500); // Beep at 1000 Hz for 500 ms

Basic Syntax and Structure of P/Invoke

  • DllImport Attribute:
    Used to specify the name of the DLL and other details about the external function.

  • External Function Declaration:
    You define the external function as a static method in C#, but the actual implementation resides in the DLL.

  • Calling the Function:
    Once the function is imported, you can call it like any normal C# method.

Here’s a basic structure of P/Invoke:

using System;
using System.Runtime.InteropServices;

class Program
{
    // Defining an external function using P/Invoke
    [DllImport("SomeUnmanagedLibrary.dll", EntryPoint = "SomeFunction", SetLastError = true)]
    public static extern int SomeFunction(int param1, string param2);

    static void Main()
    {
        // Call the external function
        int result = SomeFunction(123, "Hello!");
        Console.WriteLine("Result: " + result);
    }
}

In this structure:

  • The DllImport attribute is used to import the external function from a DLL.

  • You call the function by providing the necessary parameters (as per the unmanaged function definition).

  • The result is captured and used in your C# program.

Summary

To summarize, the steps to set up P/Invoke in C# include:

  1. Defining external functions using DllImport.

  2. Importing DLLs by specifying their name in the DllImport attribute.

  3. Using the basic syntax and structure of P/Invoke to call the unmanaged code.

By following these simple steps, you can easily use P/Invoke to work with external functions in unmanaged code, right from your C# application!

Calling Unmanaged Code: A Simple Example

When you work with C#, you might need to call functions written in another language, like C or C++. This is where Platform Invocation (P/Invoke) comes in! It allows you to call unmanaged code (like C functions) from your C# program.

Let's walk through a simple example where we call a C function from a C# application.

Step-by-Step Guide to Calling a C Function from C#

Step 1: Create a Simple C Function (Unmanaged Code)

We'll start by creating a C function that we want to call from C#. Here's a basic function in C that adds two integers:

// SimpleMath.c

#include <stdio.h>

__declspec(dllexport) int Add(int a, int b) {
    return a + b;
}

In this code:

  • Add is the function that takes two integers (a and b) and returns their sum.

  • __declspec(dllexport) tells the compiler to export this function, so it can be accessed from other programs.

Now, compile this C code into a DLL (Dynamic Link Library) so it can be used in our C# program.

Step 2: Write the C# Code to Call the C Function

Next, we write the C# code to call this function using P/Invoke.

using System;
using System.Runtime.InteropServices;

class Program
{
    // Step 3: Use DllImport to import the C function
    [DllImport("SimpleMath.dll", CallingConvention = CallingConvention.Cdecl)]
    public static extern int Add(int a, int b);

    static void Main(string[] args)
    {
        // Step 4: Call the C function from C#
        int result = Add(5, 7);
        Console.WriteLine($"The result of adding 5 and 7 is: {result}");
    }
}

Here's what's happening:

  • DllImport is used to tell C# where the unmanaged code (the DLL) is located.

  • "SimpleMath.dll" is the name of the DLL containing the C function.

  • CallingConvention.Cdecl specifies the calling convention used by the C function. This tells the compiler how to handle function calls.

  • Add is the function we are calling, and it takes two integers as arguments.

Step 3: Compile and Run

After compiling both the C code (into a DLL) and the C# code, running the C# program will print:

The result of adding 5 and 7 is: 12

Working with Simple Data Types

The example above shows how to work with simple data types like int. P/Invoke also supports other basic data types like float and string. Let’s extend the example to show how you can work with these types.

Example: Working with float and string

C Function:
// SimpleMath.c

#include <stdio.h>

__declspec(dllexport) float Multiply(float x, float y) {
    return x * y;
}

__declspec(dllexport) const char* SayHello(const char* name) {
    static char buffer[50];
    snprintf(buffer, sizeof(buffer), "Hello, %s!", name);
    return buffer;
}
C# Code:
using System;
using System.Runtime.InteropServices;

class Program
{
    // Import the C functions
    [DllImport("SimpleMath.dll", CallingConvention = CallingConvention.Cdecl)]
    public static extern float Multiply(float x, float y);

    [DllImport("SimpleMath.dll", CallingConvention = CallingConvention.Cdecl)]
    public static extern IntPtr SayHello(string name);

    static void Main(string[] args)
    {
        // Working with float
        float product = Multiply(3.5f, 2.0f);
        Console.WriteLine($"The product of 3.5 and 2.0 is: {product}");

        // Working with string
        IntPtr ptr = SayHello("John");
        string message = Marshal.PtrToStringAnsi(ptr);
        Console.WriteLine(message);
    }
}
Explanation:
  • Multiply: This function takes two float numbers and returns their product.

  • SayHello: This function takes a string (name) and returns a greeting message.

In C#, to work with strings returned from unmanaged code, you can use Marshal.PtrToStringAnsi to convert the IntPtr (returned by the C function) into a C# string.

Output:

The product of 3.5 and 2.0 is: 7
Hello, John!

Summary

With P/Invoke, calling unmanaged functions from C# is straightforward. You:

  1. Write your C function.

  2. Compile it into a DLL.

  3. Use DllImport in C# to call the function.

  4. Handle simple data types like int, float, and string easily.

This process allows you to extend your C# applications by leveraging existing C libraries.

Handling Complex Data Types

Passing and Returning Structures

In P/Invoke, you can pass structures (like struct in C#) to unmanaged code. Structures allow you to bundle multiple related variables, and they can be passed to external functions.

Example:

Let’s say we have a C structure that we want to use in C#:

C Code (Unmanaged):

// C Structure
struct Point {
    int x;
    int y;
};

int AddPoints(struct Point p) {
    return p.x + p.y;
}

You can call this from C# by creating a similar structure and using P/Invoke.

C# Code (Managed):

using System;
using System.Runtime.InteropServices;

// Define the structure in C#
[StructLayout(LayoutKind.Sequential)]
public struct Point {
    public int x;
    public int y;
}

// Import the C function
class Program {
    [DllImport("NativeLib.dll", CallingConvention = CallingConvention.Cdecl)]
    public static extern int AddPoints(Point p);

    static void Main() {
        Point p = new Point { x = 10, y = 20 };
        int result = AddPoints(p);
        Console.WriteLine($"The sum of the points is: {result}");
    }
}

Explanation:

  • StructLayout(LayoutKind.Sequential) ensures that the structure fields are laid out in memory in the same order as they are defined, matching the C structure.

  • The AddPoints function is called with the Point structure, and the result is returned to C#.

Handling Arrays and Strings

When working with arrays and strings, P/Invoke allows you to pass them to unmanaged functions, but there are some specific rules.

Example: Passing an Array

C Code (Unmanaged):

// C function to sum an array
int SumArray(int* array, int size) {
    int sum = 0;
    for (int i = 0; i < size; i++) {
        sum += array[i];
    }
    return sum;
}

C# Code (Managed):

using System;
using System.Runtime.InteropServices;

class Program {
    [DllImport("NativeLib.dll", CallingConvention = CallingConvention.Cdecl)]
    public static extern int SumArray(int[] array, int size);

    static void Main() {
        int[] numbers = { 1, 2, 3, 4, 5 };
        int result = SumArray(numbers, numbers.Length);
        Console.WriteLine($"The sum of the array is: {result}");
    }
}

Explanation:

  • Arrays are passed by reference to unmanaged code, so the int[] in C# is compatible with int* in C.

  • The SumArray function calculates the sum of the array.

Example: Passing a String

Strings can be passed as char* (C-style strings) in unmanaged code.

C Code (Unmanaged):

// C function to print a string
void PrintMessage(const char* message) {
    printf("Message: %s\n", message);
}

C# Code (Managed):

using System;
using System.Runtime.InteropServices;

class Program {
    [DllImport("NativeLib.dll", CallingConvention = CallingConvention.Cdecl)]
    public static extern void PrintMessage(string message);

    static void Main() {
        string message = "Hello from C#!";
        PrintMessage(message);
    }
}

Explanation:

  • In C#, strings are automatically converted to char* (C-style strings) when passed to unmanaged code.

Working with Pointers

Pointers allow you to directly access memory, and in P/Invoke, you can pass pointers to unmanaged functions for performance reasons.

Example: Passing a Pointer to a Function

C Code (Unmanaged):

// C function that increments a number using a pointer
void Increment(int* number) {
    (*number)++;
}

C# Code (Managed):

using System;
using System.Runtime.InteropServices;

class Program {
    [DllImport("NativeLib.dll", CallingConvention = CallingConvention.Cdecl)]
    public static extern void Increment(ref int number);

    static void Main() {
        int value = 5;
        Console.WriteLine($"Before: {value}");
        Increment(ref value);
        Console.WriteLine($"After: {value}");
    }
}

Explanation:

  • We use ref in C# to pass a reference (or pointer) to the integer, allowing the C function to modify the value directly.

Key Tips for Handling Complex Data Types in P/Invoke:

  • Structures need to have the same memory layout as their C counterparts, so use [StructLayout(LayoutKind.Sequential)] to match the memory layout.

  • Arrays and Strings are passed by reference, so you don’t need to worry about manual memory management in C#.

  • Pointers allow direct memory access, but you should use ref and out in C# to work with them safely.

Marshaling Data Between Managed and Unmanaged Code

What is Marshaling?

Marshaling is the process of converting data between managed code (like C#) and unmanaged code (like C/C++). Managed code runs in the .NET runtime, which handles memory and other resources, while unmanaged code works outside of this environment, often needing more manual memory management. Marshaling ensures that data is correctly translated between these two environments when you're calling unmanaged functions from managed code (like using P/Invoke).

Automatic vs. Manual Marshaling

  • Automatic Marshaling:
    When using simple data types (like integers or strings), the .NET runtime can automatically handle the conversion between managed and unmanaged code. For example, when passing a basic int from C# to a C function, the conversion happens automatically without extra effort.

  • Manual Marshaling:
    When working with more complex data types (like structures, arrays, or pointers), you might need to manually control how data is converted between managed and unmanaged code. This often involves using attributes like [MarshalAs] to specify how the data should be marshaled.

Common Data Marshaling Techniques

  1. Basic Types (e.g., int, float, bool)
    These are marshaled automatically by the runtime, and you usually don't need to do anything special.

  2. Strings
    Strings in C# are Unicode (UTF-16), but unmanaged code often expects ASCII or ANSI strings. You can control how a string is marshaled using [MarshalAs(UnmanagedType.LPStr)] for ANSI or [MarshalAs(UnmanagedType.LPWStr)] for Unicode.

  3. Structures
    When passing structs between managed and unmanaged code, the layout of the struct might differ. You can use [StructLayout(LayoutKind.Sequential)] to ensure that the fields are ordered the same way in both environments.

  4. Pointers and Arrays
    Pointers and arrays require more careful handling, as they deal with memory directly. When dealing with unmanaged arrays or pointers, you often need to manually allocate and free memory.

By understanding these marshaling techniques, you ensure smooth communication between your managed C# code and unmanaged libraries, making sure that data is correctly handled and performance remains efficient.

Dealing with Unmanaged Resources

When working with unmanaged code in C#, such as through P/Invoke, you'll need to handle resources that are not automatically managed by the .NET runtime. Here’s how to approach it in a simple way:

Memory Management Considerations

  • Managed vs Unmanaged Memory:
    In C#, the garbage collector automatically handles memory cleanup for managed objects. However, unmanaged resources like files, network connections, or memory allocated by unmanaged code (like C/C++) need to be cleaned up manually.

  • Why It’s Important:
    If unmanaged resources aren’t cleaned up properly, they can lead to memory leaks, which will make your application use more memory over time and eventually crash.

Using SafeHandle and IntPtr

  • IntPtr:
    This is a type used to represent a pointer or a handle from unmanaged code. You’ll often use IntPtr to store references to unmanaged resources like memory addresses or file handles.

  • SafeHandle:
    This is a more secure way to handle unmanaged resources. It automatically helps with cleanup and ensures that your resource is released correctly, even if an exception occurs. It’s safer and more reliable than using IntPtr directly.

When to Use Them:

  • Use IntPtr when you need a direct pointer to unmanaged memory or handles.

  • Use SafeHandle whenever possible, as it automatically handles resource cleanup and reduces the chance of errors.

Best Practices for Resource Cleanup

  • Use SafeHandle:
    If you’re dealing with resources like file handles or network connections, use SafeHandle to manage them. This ensures the resource is released safely.

  • Always Clean Up Resources:
    For any unmanaged resources you allocate (like memory or handles), make sure you release them when they are no longer needed. This can be done using the Dispose pattern in C# or by manually freeing the resource.

  • Try-Finally Block:
    If you’re using IntPtr, always use a try-finally block to ensure the resource is cleaned up, even if something goes wrong.

Example:

IntPtr unmanagedMemory = AllocateUnmanagedMemory();
try
{
    // Use the unmanaged resource
}
finally
{
    FreeUnmanagedMemory(unmanagedMemory);
}

By following these tips, you can manage unmanaged resources effectively and keep your application running smoothly without memory leaks.

Error Handling in P/Invoke

When working with unmanaged code using P/Invoke, it's important to handle errors properly. Here's a simple guide on how to do that.

Handling Errors from Unmanaged Code

When you call functions from unmanaged code, they might fail for various reasons, such as invalid parameters or resource issues. Unlike managed code, where exceptions are thrown, unmanaged code often returns error codes. To check for errors, you should:

  1. Check the Return Value:
    Many unmanaged functions return a value indicating success or failure. If the return value indicates failure (often zero or a specific negative value), you need to handle the error.

  2. Use Error Codes:
    If a function fails, it may provide an error code that tells you what went wrong. You can use the Marshal.GetLastWin32Error() method in C# to retrieve this code.

Using GetLastError() and SetLastError

  1. GetLastError():
    This function retrieves the calling thread's last error code. Here’s how you can use it in your P/Invoke code:

     [DllImport("kernel32.dll")]
     public static extern uint GetLastError();
    
     // Example usage
     uint errorCode = GetLastError();
     Console.WriteLine($"Error Code: {errorCode}");
    
  2. SetLastError:
    Some unmanaged functions can set the last error code, which you can then retrieve. To ensure that the function sets the error code correctly, you can specify the SetLastError flag in your P/Invoke declaration:

     [DllImport("someLibrary.dll", SetLastError = true)]
     public static extern int SomeFunction();
    
     // Example usage
     int result = SomeFunction();
     if (result == 0) // Assuming 0 indicates failure
     {
         uint errorCode = GetLastError();
         Console.WriteLine($"Error occurred: {errorCode}");
     }
    

By properly checking return values and using GetLastError(), you can effectively handle errors when working with unmanaged code in C#. This will help you troubleshoot issues and ensure your application runs smoothly.

Common Pitfalls and Troubleshooting

When working with P/Invoke in C#, you may run into some common challenges. Here’s how to handle them in a simple way:

Debugging P/Invoke Calls

  • Check Your Function Signatures:
    Make sure the method signatures in your C# code match the ones in the unmanaged DLL. Mismatched data types can cause crashes or unexpected behavior.

  • Use Exception Handling:
    Wrap your P/Invoke calls in try-catch blocks. This way, you can catch any exceptions that occur and get useful error messages.

Handling Incorrect Signatures and Memory Leaks

  • Signature Mismatches:
    If you see errors, double-check that the parameters and return types in your C# declaration match those in the unmanaged code. For example, if a C function returns an int, your C# method should return int as well.

  • Memory Management:
    Be cautious about memory allocation. If the unmanaged code allocates memory, ensure you free it appropriately in your C# code to avoid memory leaks. Use the Marshal.FreeHGlobal method when necessary.

Tips to Avoid Common Mistakes

  • Start Simple:
    Begin with basic functions before trying more complex ones. This will help you understand the P/Invoke process without getting overwhelmed.

  • Use Structs Wisely:
    When passing structs to unmanaged code, ensure they are declared correctly in C#. Use the StructLayout attribute to define the memory layout accurately.

  • Keep Documentation Handy:
    Always refer to the documentation of the unmanaged code you’re working with. This will guide you in correctly declaring the functions and handling data types.

By keeping these tips in mind, you can avoid common pitfalls and troubleshoot issues effectively when working with P/Invoke in C#.

Best Practices for P/Invoke

When to Use P/Invoke vs Other Interoperability Techniques

  • Use P/Invoke:
    When you need to call functions from unmanaged code (like C or C++ libraries) directly from your C# application. It’s great for accessing low-level APIs or legacy code.

  • Consider Other Options:
    If you’re working with COM objects, you might want to use COM Interop instead. For .NET libraries, just reference them directly without P/Invoke.

Maintaining Code Readability and Safety

  • Use Descriptive Names:
    Give your P/Invoke methods clear, descriptive names that indicate their purpose. This helps others (and you) understand the code better.

  • Define Structs Clearly:
    If you’re passing complex data types, create C# structs that match the unmanaged structures. This ensures data is transferred correctly.

  • Check for Errors:
    Always handle errors gracefully. Use Marshal.GetLastWin32Error() to get error codes after P/Invoke calls and provide helpful error messages.

Performance Considerations

  • Minimize Cross-Boundary Calls:
    Calling unmanaged code can be slower than calling managed code, so try to minimize these calls. Batch your requests when possible.

  • Avoid Marshaling Complex Types:
    Marshaling (converting data types between managed and unmanaged code) can slow down your application. Use simpler data types (like int or float) when possible.

  • Use StringBuilder for Strings:
    If you need to handle strings, consider using StringBuilder for better performance when passing strings to unmanaged code.

By following these best practices, you can make your P/Invoke code cleaner, safer, and more efficient.

Additional Resources

Here are some helpful resources to deepen your understanding of P/Invoke and make your coding experience smoother:

  • Microsoft P/Invoke Documentation:
    This is the official guide from Microsoft that explains how to use P/Invoke in C#. It includes examples and detailed explanations of the concepts.

  • .NET API Browser:
    Use this tool to explore the .NET libraries and see how different methods work. You can find relevant P/Invoke examples and documentation here.

Useful Libraries and Tools for P/Invoke

  • PInvoke Interop Assistant:
    This tool helps you generate P/Invoke signatures from existing C/C++ headers. It makes it easier to call unmanaged code without worrying about the details.

  • EasyHook:
    A library that allows you to hook unmanaged code and intercept calls. It’s great for more advanced P/Invoke scenarios.

  • DllImport Generator:
    A handy tool to automatically generate DllImport declarations for your unmanaged functions, saving you time on manual coding.

These resources will help you get started with P/Invoke and support your learning journey as you work with unmanaged code in C#. I hope you found this guide helpful and learned something new. Stay tuned for the next article in the Mastering C# series: Exploring the Internals of C# Reflection and Metadata

Happy coding!

20
Subscribe to my newsletter

Read articles from Opaluwa Emidowo-ojo directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Opaluwa Emidowo-ojo
Opaluwa Emidowo-ojo