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

Creating Native Plugins Using [KernelFunction] Attribute in Semantic Kernel

One of the most elegant ways to create native plugins in Semantic Kernel is using the [KernelFunction] attribute. Unlike the reflection-based approach where you manually create functions from methods, the attribute-based approach enables automatic discovery - Semantic Kernel scans your class and automatically registers all methods marked with [KernelFunction].

This article provides a comprehensive guide to creating attribute-based native plugins. We'll build two real-world examples:

  1. DataAnalyzerPlugin - Statistical analysis and data processing (parameterless constructor)
  2. FileManagerPlugin - File system operations with dependency injection (parameterized constructor)

By the end, you'll understand:

  1. How [KernelFunction] attribute auto-discovery works
  2. The difference between ImportPluginFromType and ImportPluginFromObject
  3. When to use parameterless vs parameterized constructors
  4. Best practices for attribute-based plugin development
  5. Handling state and dependencies in plugins

Understanding the [KernelFunction] Attribute

The [KernelFunction] attribute is a marker that tells Semantic Kernel:

"This method should be exposed as a kernel function"

Key Benefits

  1. Automatic Discovery: No manual reflection code needed
  2. Clean Syntax: Methods are self-documenting
  3. Type Safety: Compile-time checking of signatures
  4. Convention over Configuration: Less boilerplate code
  5. IDE Support: Full IntelliSense and refactoring support

Basic Syntax

using Microsoft.SemanticKernel;
using System.ComponentModel;

public class MyPlugin
{
[KernelFunction("function_name")]
[Description("What this function does")]
public string MyMethod([Description("Parameter description")] string input)
{
return input.ToUpper();
}
}

Attribute Breakdown

  1. [KernelFunction("function_name")]: Marks method as a kernel function with a specific name
  2. [Description("...")]: Provides documentation (on class, method, and parameters)
  3. Parameter descriptions: Help AI understand what inputs are expected

ImportPluginFromType vs ImportPluginFromObject

Semantic Kernel provides two methods for importing attribute-based plugins:

1. ImportPluginFromType

Use when: Your plugin class has a parameterless constructor

kernel.ImportPluginFromType<DataAnalyzerPlugin>("data_analyzer");

What happens:

  1. Semantic Kernel creates an instance using new DataAnalyzerPlugin()
  2. Scans all methods marked with [KernelFunction]
  3. Registers them as kernel functions
  4. Associates them with the plugin name

Advantages:

  1. Simple, one-line registration
  2. Semantic Kernel manages instance lifecycle
  3. Perfect for stateless plugins

Limitations:

  1. Requires parameterless constructor
  2. Cannot inject dependencies
  3. Cannot pass configuration

2. ImportPluginFromObject

Use when: Your plugin class has a parameterized constructor or needs dependencies

var logger = new Logger("AppLogger");
var plugin = new FileManagerPlugin(logger, @"C:\temp");
kernel.ImportPluginFromObject(plugin, "file_manager");

What happens:

  1. You create the instance with your dependencies
  2. Pass the pre-configured instance to Semantic Kernel
  3. Semantic Kernel scans it for [KernelFunction] methods
  4. Registers all discovered functions

Advantages:

  1. Full control over instance creation
  2. Dependency injection support
  3. Configuration flexibility
  4. Supports stateful plugins

Use cases:

  1. Plugins requiring database connections
  2. Services needing logging or telemetry
  3. Plugins with configuration settings
  4. Stateful operations

Example 1: DataAnalyzerPlugin (Parameterless Constructor)

Let's build a statistical analysis plugin with a parameterless constructor:

using Microsoft.SemanticKernel;
using System.ComponentModel;

[Description("Analyzes numerical data and provides statistical information")]
public class DataAnalyzerPlugin
{
[KernelFunction("calculate_average")]
[Description("Calculates the arithmetic mean of a collection of numbers")]
public double CalculateAverage(
[Description("Array of numbers to calculate average")] double[] numbers)
{
if (numbers == null || numbers.Length == 0)
return 0;

return numbers.Average();
}

[KernelFunction("find_min_max")]
[Description("Finds the minimum and maximum values in a collection")]
public string FindMinMax(
[Description("Array of integers to analyze")] int[] numbers)
{
if (numbers == null || numbers.Length == 0)
return "No data provided";

var min = numbers.Min();
var max = numbers.Max();
return $"Min: {min}, Max: {max}";
}

[KernelFunction("sort_numbers")]
[Description("Sorts a collection of numbers in ascending or descending order")]
public string SortNumbers(
[Description("Array of integers to sort")] int[] numbers,
[Description("True for ascending, false for descending")] bool ascending = true)
{
if (numbers == null || numbers.Length == 0)
return "[]";

var sorted = ascending
? numbers.OrderBy(x => x).ToArray()
: numbers.OrderByDescending(x => x).ToArray();

return $"[{string.Join(", ", sorted)}]";
}

[KernelFunction("calculate_median")]
[Description("Calculates the median value of a collection of numbers")]
public double CalculateMedian(
[Description("Array of numbers to find median")] double[] numbers)
{
if (numbers == null || numbers.Length == 0)
return 0;

var sorted = numbers.OrderBy(x => x).ToArray();
int mid = sorted.Length / 2;

if (sorted.Length % 2 == 0)
return (sorted[mid - 1] + sorted[mid]) / 2.0;

return sorted[mid];
}
}

Key Features of DataAnalyzerPlugin

  1. Class-Level Description: Documents the plugin's overall purpose
  2. Multiple Functions: Four different statistical operations
  3. Various Return Types: double, string - Semantic Kernel handles all .NET types
  4. Array Parameters: Demonstrates handling collections
  5. Optional Parameters: ascending parameter has a default value
  6. Input Validation: Checks for null/empty arrays

Registering the Plugin

var kernel = builder.Build();

// Simple one-line registration
kernel.ImportPluginFromType<DataAnalyzerPlugin>("data_analyzer");

That's it! Semantic Kernel:

  1. Creates a new DataAnalyzerPlugin()
  2. Discovers all 4 methods with [KernelFunction]
  3. Registers them as: calculate_average, find_min_max, sort_numbers, calculate_median

Using the Plugin

// Calculate average
var avgResult = await kernel.InvokeAsync(
"data_analyzer",
"calculate_average",
new KernelArguments { ["numbers"] = new[] { 10.5, 20.3, 15.7, 8.9, 12.1 } });
Console.WriteLine($"Average: {avgResult}");
// Output: Average: 13.5

// Find min and max
var minMaxResult = await kernel.InvokeAsync(
"data_analyzer",
"find_min_max",
new KernelArguments { ["numbers"] = new[] { 45, 12, 78, 23, 56, 8, 90 } });
Console.WriteLine($"Min-Max: {minMaxResult}");
// Output: Min-Max: Min: 8, Max: 90

// Sort numbers
var sortResult = await kernel.InvokeAsync(
"data_analyzer",
"sort_numbers",
new KernelArguments
{
["numbers"] = new[] { 45, 12, 78, 23, 56 },
["ascending"] = false // Sort descending
});
Console.WriteLine($"Sorted: {sortResult}");
// Output: Sorted: [78, 56, 45, 23, 12]

Example 2: FileManagerPlugin (Parameterized Constructor)

Now let's build a plugin that requires dependencies - a file manager with logging:

[Description("Manages file operations with logging capabilities")]
public class FileManagerPlugin
{
private readonly Logger _logger;
private readonly string _baseDirectory;

// Constructor with dependencies
public FileManagerPlugin(Logger logger, string baseDirectory)
{
_logger = logger;
_baseDirectory = baseDirectory;
_logger.Log($"FileManagerPlugin initialized with base directory: {baseDirectory}");
}

[KernelFunction("list_files")]
[Description("Lists all files in the base directory with optional extension filter")]
public string ListFiles(
[Description("File extension filter (e.g., '.txt', '.pdf')")] string extension = "*")
{
_logger.Log($"Listing files with extension: {extension}");

try
{
if (!Directory.Exists(_baseDirectory))
{
_logger.Log($"Directory not found: {_baseDirectory}");
return $"Directory not found: {_baseDirectory}";
}

var searchPattern = extension == "*" ? "*.*" : $"*{extension}";
var files = Directory.GetFiles(_baseDirectory, searchPattern);

if (files.Length == 0)
{
return $"No files found with extension '{extension}'";
}

var fileNames = files.Select(Path.GetFileName);
return string.Join("\n", fileNames);
}
catch (Exception ex)
{
_logger.Log($"Error listing files: {ex.Message}");
return $"Error: {ex.Message}";
}
}

[KernelFunction("get_file_info")]
[Description("Gets detailed information about a specific file")]
public string GetFileInfo(
[Description("Name of the file to get information about")] string fileName)
{
_logger.Log($"Getting info for file: {fileName}");

try
{
var fullPath = Path.Combine(_baseDirectory, fileName);

if (!File.Exists(fullPath))
{
return $"File not found: {fileName}";
}

var fileInfo = new FileInfo(fullPath);
return $"Name: {fileInfo.Name}\n" +
$"Size: {fileInfo.Length} bytes\n" +
$"Created: {fileInfo.CreationTime}\n" +
$"Modified: {fileInfo.LastWriteTime}\n" +
$"Extension: {fileInfo.Extension}";
}
catch (Exception ex)
{
_logger.Log($"Error getting file info: {ex.Message}");
return $"Error: {ex.Message}";
}
}

[KernelFunction("count_files")]
[Description("Counts the total number of files in the base directory")]
public int CountFiles()
{
_logger.Log("Counting total files");

try
{
if (!Directory.Exists(_baseDirectory))
return 0;

return Directory.GetFiles(_baseDirectory).Length;
}
catch (Exception ex)
{
_logger.Log($"Error counting files: {ex.Message}");
return -1;
}
}

[KernelFunction("get_directory_size")]
[Description("Calculates the total size of all files in the base directory in MB")]
public string GetDirectorySize()
{
_logger.Log("Calculating directory size");

try
{
if (!Directory.Exists(_baseDirectory))
return "Directory not found";

var files = Directory.GetFiles(_baseDirectory);
long totalBytes = files.Sum(file => new FileInfo(file).Length);
double totalMB = totalBytes / (1024.0 * 1024.0);

return $"{totalMB:F2} MB ({totalBytes:N0} bytes)";
}
catch (Exception ex)
{
_logger.Log($"Error calculating directory size: {ex.Message}");
return $"Error: {ex.Message}";
}
}
}

Supporting Logger Class

public class Logger
{
private readonly string _name;
private readonly List<string> _logs = [];

public Logger(string name)
{
_name = name;
}

public void Log(string message)
{
var logEntry = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] [{_name}] {message}";
_logs.Add(logEntry);
Console.WriteLine(logEntry);
}

public IReadOnlyList<string> GetLogs() => _logs.AsReadOnly();
}

Key Features of FileManagerPlugin

  1. Dependency Injection: Constructor accepts Logger and baseDirectory
  2. State Management: Maintains _logger and _baseDirectory as private fields
  3. Error Handling: Try-catch blocks with logging
  4. Optional Parameters: extension parameter with default value
  5. Real-World Functionality: Actual file system operations
  6. Logging Integration: Every operation is logged

Registering with Dependencies

// Create dependencies
var logger = new Logger("AppLogger");
var baseDirectory = @"C:\temp";

// Create plugin instance with dependencies
var fileManager = new FileManagerPlugin(logger, baseDirectory);

// Register the pre-configured instance
kernel.ImportPluginFromObject(fileManager, "file_manager");

Why ImportPluginFromObject?

  1. We need to pass logger and baseDirectory to the constructor
  2. ImportPluginFromType cannot handle this - it requires parameterless constructor
  3. We create the instance ourselves and hand it to Semantic Kernel

Using the FileManagerPlugin

// List all text files
var listResult = await kernel.InvokeAsync(
"file_manager",
"list_files",
new KernelArguments { ["extension"] = ".txt" });
Console.WriteLine($"Files Found:\n{listResult}");
// Output:
// document.txt
// notes.txt
// readme.txt

// Get info about a specific file
var infoResult = await kernel.InvokeAsync(
"file_manager",
"get_file_info",
new KernelArguments { ["fileName"] = "example.txt" });
Console.WriteLine($"File Info:\n{infoResult}");
// Output:
// Name: example.txt
// Size: 1024 bytes
// Created: 2024-01-15 10:30:00
// Modified: 2024-01-20 14:45:00
// Extension: .txt

// Count all files
var countResult = await kernel.InvokeAsync(
"file_manager",
"count_files",
new KernelArguments());
Console.WriteLine($"Total Files: {countResult}");
// Output: Total Files: 15

// Get directory size
var sizeResult = await kernel.InvokeAsync(
"file_manager",
"get_directory_size",
new KernelArguments());
Console.WriteLine($"Directory Size: {sizeResult}");
// Output: Directory Size: 2.45 MB (2,568,192 bytes)

Observing the Logging

Every operation is logged:

[2024-01-22 15:30:45] [AppLogger] FileManagerPlugin initialized with base directory: C:\temp
[2024-01-22 15:30:46] [AppLogger] Listing files with extension: .txt
[2024-01-22 15:30:47] [AppLogger] Getting info for file: example.txt
[2024-01-22 15:30:48] [AppLogger] Counting total files
[2024-01-22 15:30:49] [AppLogger] Calculating directory size

Complete Working Example

Here's the full Program.cs demonstrating both approaches:

using Microsoft.SemanticKernel;
using System.ComponentModel;

var apiKey = Environment.GetEnvironmentVariable("OPEN_AI_KEY");

if (string.IsNullOrWhiteSpace(apiKey))
{
Console.WriteLine("Please set the OPEN_AI_KEY environment variable.");
return;
}

var builder = Kernel.CreateBuilder();
builder.AddOpenAIChatCompletion(
modelId: "gpt-4o",
apiKey: apiKey);

var kernel = builder.Build();

Console.WriteLine("=== Native Plugin Using [KernelFunction] Attribute ===\n");

// Example 1: Parameterless constructor - ImportPluginFromType
Console.WriteLine("--- Example 1: Using ImportPluginFromType ---\n");

kernel.ImportPluginFromType<DataAnalyzerPlugin>("data_analyzer");

// Example 2: Parameterized constructor - ImportPluginFromObject
Console.WriteLine("\n--- Example 2: Using ImportPluginFromObject ---\n");

var logger = new Logger("AppLogger");
var fileManager = new FileManagerPlugin(logger, @"C:\temp");

kernel.ImportPluginFromObject(fileManager, "file_manager");

// Test both plugins
var avgResult = await kernel.InvokeAsync(
"data_analyzer",
"calculate_average",
new KernelArguments { ["numbers"] = new[] { 10.5, 20.3, 15.7 } });

var listResult = await kernel.InvokeAsync(
"file_manager",
"list_files",
new KernelArguments { ["extension"] = ".txt" });

Inspecting Plugin Metadata

Examine what Semantic Kernel discovered:

static void PrintPluginsWithFunctions(Kernel kernel)
{
Console.WriteLine("Registered 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 Type: {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();
}
}
}

PrintPluginsWithFunctions(kernel);

Output:

Registered Plugins and Functions:

Plugin: data_analyzer
Description: Analyzes numerical data and provides statistical information
Function Count: 4

Function: calculate_average
Description: Calculates the arithmetic mean of a collection of numbers
Return Type: {"type":"number"}
Parameters:
- numbers: {"type":"array","items":{"type":"number"}}

Function: find_min_max
Description: Finds the minimum and maximum values in a collection
Return Type: {"type":"string"}
Parameters:
- numbers: {"type":"array","items":{"type":"integer"}}

Function: sort_numbers
Description: Sorts a collection of numbers in ascending or descending order
Return Type: {"type":"string"}
Parameters:
- numbers: {"type":"array","items":{"type":"integer"}}
- ascending: {"type":"boolean"}

Function: calculate_median
Description: Calculates the median value of a collection of numbers
Return Type: {"type":"number"}
Parameters:
- numbers: {"type":"array","items":{"type":"number"}}

Plugin: file_manager
Description: Manages file operations with logging capabilities
Function Count: 4

Function: list_files
Description: Lists all files in the base directory with optional extension filter
Return Type: {"type":"string"}
Parameters:
- extension: {"type":"string"}

Function: get_file_info
Description: Gets detailed information about a specific file
Return Type: {"type":"string"}
Parameters:
- fileName: {"type":"string"}

Function: count_files
Description: Counts the total number of files in the base directory
Return Type: {"type":"integer"}
Parameters: (none)

Function: get_directory_size
Description: Calculates the total size of all files in the base directory in MB
Return Type: {"type":"string"}
Parameters: (none)

When to Use Each Approach

Use ImportPluginFromType When:

✅ Plugin is stateless ✅ No dependencies required ✅ Simple, self-contained operations ✅ Parameterless constructor is acceptable

Examples:

  1. Mathematical calculators
  2. String utilities
  3. Data validators
  4. Format converters

Use ImportPluginFromObject When:

✅ Plugin needs dependencies (logger, database, config) ✅ Constructor requires parameters ✅ Plugin maintains state ✅ Integration with DI container

Examples:

  1. File system operations
  2. Database access
  3. External API calls
  4. Services with configuration
  5. Stateful services (counters, caches)

Advanced Patterns

Pattern 1: Dependency Injection with DI Container

// Register plugin in DI container
services.AddSingleton<IFileService, FileService>();
services.AddSingleton<FileOperationsPlugin>();

// Get from container
var serviceProvider = services.BuildServiceProvider();
var filePlugin = serviceProvider.GetRequiredService<FileOperationsPlugin>();

// Register in kernel
kernel.ImportPluginFromObject(filePlugin, "file_ops");

Pattern 2: Configuration-Based Plugin

public class DatabasePlugin
{
private readonly string _connectionString;

public DatabasePlugin(IConfiguration configuration)
{
_connectionString = configuration.GetConnectionString("Default");
}

[KernelFunction("query_users")]
[Description("Queries users from database")]
public async Task<string> QueryUsers(string filter)
{
// Use _connectionString
}
}

// Usage
var config = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Build();

var dbPlugin = new DatabasePlugin(config);
kernel.ImportPluginFromObject(dbPlugin, "database");

Pattern 3: Stateful Plugin with Singleton Instance

[Description("Maintains application-wide counter state")]
public class CounterPlugin
{
private int _count = 0;

[KernelFunction("increment")]
[Description("Increments the counter and returns new value")]
public int Increment()
{
return Interlocked.Increment(ref _count);
}

[KernelFunction("get_count")]
[Description("Gets current counter value")]
public int GetCount() => _count;

[KernelFunction("reset")]
[Description("Resets counter to zero")]
public void Reset()
{
_count = 0;
}
}

// Create singleton instance
var counter = new CounterPlugin();
kernel.ImportPluginFromObject(counter, "counter");

// All invocations share the same state
await kernel.InvokeAsync("counter", "increment"); // Returns 1
await kernel.InvokeAsync("counter", "increment"); // Returns 2
await kernel.InvokeAsync("counter", "get_count"); // Returns 2

Pattern 4: Multiple Instances of Same Plugin

// Create multiple file managers for different directories
var tempManager = new FileManagerPlugin(logger, @"C:\temp");
var docsManager = new FileManagerPlugin(logger, @"C:\Documents");
var downloadsManager = new FileManagerPlugin(logger, @"C:\Downloads");

// Register with different names
kernel.ImportPluginFromObject(tempManager, "temp_files");
kernel.ImportPluginFromObject(docsManager, "document_files");
kernel.ImportPluginFromObject(downloadsManager, "download_files");

// Use separately
await kernel.InvokeAsync("temp_files", "count_files");
await kernel.InvokeAsync("document_files", "count_files");
await kernel.InvokeAsync("download_files", "count_files");

Best Practices

1. Always Use Descriptions

// ✅ GOOD: Comprehensive descriptions
[Description("Analyzes numerical data and provides statistical information")]
public class DataAnalyzerPlugin
{
[KernelFunction("calculate_average")]
[Description("Calculates the arithmetic mean of a collection of numbers")]
public double CalculateAverage(
[Description("Array of numbers to calculate average")] double[] numbers)
{ }
}

// ❌ BAD: No descriptions
public class DataAnalyzerPlugin
{
[KernelFunction("calculate_average")]
public double CalculateAverage(double[] numbers)
{ }
}

2. Validate Inputs

// ✅ GOOD: Input validation
[KernelFunction("calculate_average")]
public double CalculateAverage(double[] numbers)
{
if (numbers == null || numbers.Length == 0)
return 0;
return numbers.Average();
}

// ❌ BAD: No validation (throws exception on null)
[KernelFunction("calculate_average")]
public double CalculateAverage(double[] numbers)
{
return numbers.Average();
}

3. Use Meaningful Function Names

// ✅ GOOD: Clear, action-oriented names
[KernelFunction("calculate_average")]
[KernelFunction("find_min_max")]
[KernelFunction("list_files")]

// ❌ BAD: Vague or cryptic names
[KernelFunction("func1")]
[KernelFunction("process")]
[KernelFunction("do_stuff")]

4. Handle Errors Gracefully

// ✅ GOOD: Try-catch with meaningful error messages
[KernelFunction("read_file")]
public string ReadFile(string fileName)
{
try
{
return File.ReadAllText(fileName);
}
catch (FileNotFoundException)
{
return $"File not found: {fileName}";
}
catch (UnauthorizedAccessException)
{
return $"Access denied: {fileName}";
}
catch (Exception ex)
{
return $"Error reading file: {ex.Message}";
}
}

// ❌ BAD: Let exceptions bubble up (breaks AI flow)
[KernelFunction("read_file")]
public string ReadFile(string fileName)
{
return File.ReadAllText(fileName); // Can throw
}

5. Keep Functions Focused

// ✅ GOOD: Single responsibility
[KernelFunction("calculate_average")]
public double CalculateAverage(double[] numbers) { }

[KernelFunction("calculate_median")]
public double CalculateMedian(double[] numbers) { }

// ❌ BAD: Doing too much
[KernelFunction("analyze_data")]
public string AnalyzeData(double[] numbers, string operation)
{
// Branches based on operation - should be separate functions
}

Comparison: Reflection vs Attribute-Based Approaches

AspectReflection (CreateFunctionFromMethod)Attribute ([KernelFunction])
RegistrationManual, explicitAutomatic discovery
Code VolumeMore boilerplateLess boilerplate
FlexibilityCan wrap any methodRequires attribute marking
DiscoverabilityManual scanningAutomatic
Best ForWrapping existing codeNew plugins from scratch
Learning CurveSteeperGentler
Type SafetyCompile-timeCompile-time
DependenciesManual instance creationFlexible (both approaches)

Real-World Use Cases

1. Weather Service Plugin

[Description("Provides weather information")]
public class WeatherPlugin
{
private readonly HttpClient _httpClient;
private readonly string _apiKey;

public WeatherPlugin(HttpClient httpClient, string apiKey)
{
_httpClient = httpClient;
_apiKey = apiKey;
}

[KernelFunction("get_current_weather")]
[Description("Gets current weather for a city")]
public async Task<string> GetCurrentWeather(
[Description("City name")] string city)
{
var response = await _httpClient.GetAsync(
$"https://api.weather.com/v1/current?city={city}&key={_apiKey}");
return await response.Content.ReadAsStringAsync();
}
}

2. Database Operations Plugin

[Description("Performs database queries")]
public class DatabasePlugin
{
private readonly IDbConnection _connection;

public DatabasePlugin(IDbConnection connection)
{
_connection = connection;
}

[KernelFunction("query_users")]
[Description("Queries users from database")]
public async Task<string> QueryUsers(
[Description("Filter criteria")] string filter)
{
var users = await _connection.QueryAsync<User>(
"SELECT * FROM Users WHERE Name LIKE @Filter",
new { Filter = $"%{filter}%" });
return JsonSerializer.Serialize(users);
}
}

3. Email Service Plugin

[Description("Sends emails")]
public class EmailPlugin
{
private readonly IEmailService _emailService;

public EmailPlugin(IEmailService emailService)
{
_emailService = emailService;
}

[KernelFunction("send_email")]
[Description("Sends an email to a recipient")]
public async Task<string> SendEmail(
[Description("Recipient email address")] string to,
[Description("Email subject")] string subject,
[Description("Email body")] string body)
{
await _emailService.SendAsync(to, subject, body);
return $"Email sent successfully to {to}";
}
}

Common Pitfalls and Solutions

Pitfall 1: Forgetting [KernelFunction] Attribute

// ❌ This method won't be discovered
public string Process(string input) { }

// ✅ Add the attribute
[KernelFunction("process_data")]
public string Process(string input) { }

Pitfall 2: Using ImportPluginFromType with Parameterized Constructor

public class MyPlugin
{
public MyPlugin(string config) { } // Parameterized constructor
}

// ❌ This will fail - no parameterless constructor
kernel.ImportPluginFromType<MyPlugin>("my_plugin");

// ✅ Use ImportPluginFromObject instead
var plugin = new MyPlugin("config_value");
kernel.ImportPluginFromObject(plugin, "my_plugin");

Pitfall 3: Not Handling Null/Empty Inputs

// ❌ Will throw NullReferenceException
[KernelFunction("process")]
public int Count(string[] items)
{
return items.Length;
}

// ✅ Add validation
[KernelFunction("process")]
public int Count(string[] items)
{
if (items == null || items.Length == 0)
return 0;
return items.Length;
}

Summary: Attribute-Based Plugin Creation Workflow

┌─────────────────────────────────────────┐
│ 1. CREATE PLUGIN CLASS │
│ - Add [Description] on class │
│ - Define constructor (with/without │
│ parameters) │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ 2. MARK METHODS WITH [KernelFunction] │
│ - [KernelFunction("function_name")] │
│ - [Description("What it does")] │
│ - Add parameter descriptions │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ 3. REGISTER PLUGIN │
│ Parameterless constructor: │
│ ImportPluginFromType<T>() │
│ │
│ Parameterized constructor: │
│ Create instance + pass deps │
│ ImportPluginFromObject() │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ 4. SEMANTIC KERNEL AUTO-DISCOVERS │
│ - Scans for [KernelFunction] │
│ - Reads descriptions │
│ - Extracts parameter info │
│ - Registers all functions │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ 5. INVOKE FUNCTIONS │
│ kernel.InvokeAsync( │
│ "plugin_name", │
│ "function_name", │
│ new KernelArguments { ... }) │
└─────────────────────────────────────────┘
Share this lesson: