Semantic Kernel Plugins Created: 01 Feb 2026 Updated: 01 Feb 2026

Creating Native Plugins in Semantic Kernel Using Reflection

Semantic Kernel is a powerful SDK that orchestrates AI services with conventional code. One of its key features is the plugin system, which allows developers to extend the kernel's capabilities with custom functions. In this article, we'll explore how to create native plugins using .NET reflection, a mechanism that enables runtime inspection and invocation of code metadata.

Understanding Native Functions vs Semantic Functions

Before diving into the implementation, it's important to understand the two types of functions in Semantic Kernel:

  1. Semantic Functions: AI-powered functions created from prompts that leverage LLMs
  2. Native Functions: Traditional C# methods that perform deterministic operations

Native functions are ideal for scenarios that require:

  1. Deterministic behavior
  2. Direct system access
  3. Fast execution without AI overhead
  4. Integration with existing .NET libraries

What is Reflection?

Reflection is a .NET mechanism that allows programs to examine and manipulate their own structure at runtime. Using reflection, you can:

  1. Discover types, methods, and properties dynamically
  2. Invoke methods without compile-time knowledge
  3. Create instances of types at runtime

In the context of Semantic Kernel, reflection enables us to convert regular C# methods into kernel functions dynamically.

Building a Simple String Utilities Plugin

Let's create a practical example using a string manipulation plugin. We'll build a plugin with four functions that demonstrate different aspects of native plugin development.

Step 1: Creating the Utilities Class

First, we define a simple class with utility methods. Notice how we use the [Description] attribute to provide metadata that Semantic Kernel can use:

public class StringUtilities
{
[Description("Converts the input text to uppercase")]
public string ToUpperCase([Description("The text to convert")] string text)
{
return text.ToUpper();
}

[Description("Reverses the input text")]
public string ReverseText([Description("The text to reverse")] string text)
{
char[] charArray = text.ToCharArray();
Array.Reverse(charArray);
return new string(charArray);
}

[Description("Counts the number of words in the input text")]
public int CountWords([Description("The text to count words in")] string text)
{
if (string.IsNullOrWhiteSpace(text))
return 0;

return text.Split([' ', '\t', '\n', '\r'],
StringSplitOptions.RemoveEmptyEntries).Length;
}

[Description("Truncates text to specified maximum length and adds ellipsis")]
public string TruncateText(
[Description("The text to truncate")] string text,
[Description("Maximum length of the text")] int maxLength)
{
if (string.IsNullOrWhiteSpace(text))
return text;

if (text.Length <= maxLength)
return text;

return text[..(maxLength - 3)] + "...";
}
}

Key Points About the Utilities Class

  1. Simple POCO Class: No inheritance or special interfaces required
  2. Description Attributes: Provide human-readable documentation for the AI and developers
  3. Multiple Parameter Types: The TruncateText method demonstrates handling multiple parameters with different types
  4. Return Types: Methods can return various types (string, int, etc.)

Step 2: Creating Functions Using Reflection

Now comes the interesting part - converting these methods into Semantic Kernel functions using reflection:

var kernel = builder.Build();

var toUpperFunction = kernel.CreateFunctionFromMethod(
typeof(StringUtilities).GetMethod(nameof(StringUtilities.ToUpperCase))!,
new StringUtilities(),
"to_upper",
"Converts input text to uppercase.");

var reverseFunction = kernel.CreateFunctionFromMethod(
typeof(StringUtilities).GetMethod(nameof(StringUtilities.ReverseText))!,
new StringUtilities(),
"reverse_text",
"Reverses the input text.");

var countWordsFunction = kernel.CreateFunctionFromMethod(
typeof(StringUtilities).GetMethod(nameof(StringUtilities.CountWords))!,
new StringUtilities(),
"count_words",
"Counts the number of words in the input text.");

var truncateFunction = kernel.CreateFunctionFromMethod(
typeof(StringUtilities).GetMethod(nameof(StringUtilities.TruncateText))!,
new StringUtilities(),
"truncate_text",
"Truncates text to specified length and adds ellipsis.");

Understanding CreateFunctionFromMethod

The CreateFunctionFromMethod method takes four parameters:

  1. MethodInfo: Obtained via reflection using typeof(Class).GetMethod(methodName)
  2. Instance: An instance of the class containing the method
  3. Function Name: The name the function will have in the plugin
  4. Description: Human-readable description of what the function does

The ! operator is used because GetMethod can return null, but we know these methods exist.

Step 3: Importing Functions as a Plugin

Once we have our functions, we group them into a cohesive plugin:

kernel.ImportPluginFromFunctions(
"string_utilities_plugin",
"A plugin for basic string manipulation operations.",
[toUpperFunction, reverseFunction, countWordsFunction, truncateFunction]);

The ImportPluginFromFunctions method takes:

  1. Plugin Name: Identifier for the plugin
  2. Plugin Description: What the plugin does
  3. Functions Collection: Array of functions to include

Testing the Plugin

Let's invoke our plugin functions:

var upperResult = await kernel.InvokeAsync(
"string_utilities_plugin",
"to_upper",
new KernelArguments { ["text"] = "hello world" });
Console.WriteLine($"ToUpper Result: {upperResult}");
// Output: HELLO WORLD

var reverseResult = await kernel.InvokeAsync(
"string_utilities_plugin",
"reverse_text",
new KernelArguments { ["text"] = "Semantic Kernel" });
Console.WriteLine($"Reverse Result: {reverseResult}");
// Output: lenreK citnameS

var countResult = await kernel.InvokeAsync(
"string_utilities_plugin",
"count_words",
new KernelArguments { ["text"] = "This is a simple test" });
Console.WriteLine($"Word Count Result: {countResult}");
// Output: 5

var truncateResult = await kernel.InvokeAsync(
"string_utilities_plugin",
"truncate_text",
new KernelArguments
{
["text"] = "This is a very long article about Semantic Kernel",
["maxLength"] = "30"
});
Console.WriteLine($"Truncate Result: {truncateResult}");
// Output: This is a very long arti...

Inspecting Plugin Metadata

One of the powerful features of this approach is the ability to inspect plugin metadata at runtime:

static void PrintPluginsWithFunctions(Kernel kernel)
{
Console.WriteLine("Kernel plugins and functions:\n");
foreach (var plugin in kernel.Plugins)
{
Console.WriteLine($"Plugin: {plugin.Name}");
Console.WriteLine($"Description: {plugin.Description}");
Console.WriteLine($"Function Count: {plugin.FunctionCount}\n");
foreach (var function in plugin.GetFunctionsMetadata())
{
Console.WriteLine($" Function: {function.Name}");
Console.WriteLine($" Description: {function.Description}");
Console.WriteLine($" Return Schema: {function.ReturnParameter.Schema}");
if (function.Parameters.Count > 0)
{
Console.WriteLine(" Parameters:");
foreach (var parameter in function.Parameters)
{
Console.WriteLine($" - {parameter.Name}: {parameter.Schema}");
}
}
Console.WriteLine();
}
}
}

This will output detailed information about the plugin structure, which is valuable for:

  1. Documentation generation
  2. Debugging
  3. Dynamic UI creation
  4. AI agent function discovery

Advantages of the Reflection Approach

1. Separation of Concerns

Your utility class remains a pure C# class without any Semantic Kernel dependencies.

2. Testability

You can unit test StringUtilities methods independently of Semantic Kernel.

3. Flexibility

Functions can be added or removed from plugins dynamically at runtime.

4. Reusability

The same utility class can be used in multiple contexts, not just Semantic Kernel.

5. Type Safety

You maintain compile-time type checking for your method implementations.

Best Practices

1. Use Descriptive Attributes

Always provide [Description] attributes for methods and parameters. These descriptions help:

  1. AI models understand function purposes
  2. Developers understand the API
  3. Auto-generate documentation

2. Handle Null and Edge Cases

if (string.IsNullOrWhiteSpace(text))
return 0;

Always validate inputs in your native functions.

3. Use Appropriate Function Names

Choose snake_case names that clearly describe the function's purpose:

  1. ? to_upper, count_words, truncate_text
  2. ? func1, process, do_it

4. Group Related Functions

Create cohesive plugins by grouping related functionality together.

5. Consider Performance

Native functions execute synchronously. For I/O-bound operations, consider using async methods:

public async Task<string> FetchDataAsync(string url)
{
// async implementation
}

Mixing Native and Semantic Functions

While this article focuses on native functions, you can combine native and semantic functions in the same plugin:

var nativeFunction = kernel.CreateFunctionFromMethod(...);
var semanticFunction = kernel.CreateFunctionFromPrompt(...);

kernel.ImportPluginFromFunctions(
"hybrid_plugin",
"A plugin mixing native and semantic functions",
[nativeFunction, semanticFunction]);

Real-World Use Cases

The reflection-based plugin approach is ideal for:

  1. Data Validation: Create functions to validate email addresses, phone numbers, etc.
  2. File Operations: Read, write, or process files
  3. API Integration: Call external REST APIs
  4. Database Operations: Query or update databases
  5. Calculations: Perform mathematical or statistical operations
  6. System Integration: Interact with operating system features


Share this lesson: