Writing a Basic Plugin
In the previous section, we created a plugin project using the scaffolding template. Now, open the Plugin.cs file in the project, and we will delve into its code structure.
Code Structure
Below is the default code generated by the template, taking sharwapi.apimgr as an example:
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using sharwapi.Contracts.Core;
namespace sharwapi.Plugin.apiMgr;
// Implementing IApiPlugin interface
public class SharwApiMgrPlugin : IApiPlugin
{
// 1. Identity Information
public string Name => "sharw.apimgr"; // Unique ID
public string DisplayName => "API Manager"; // Display Name
public string Version => "1.0.0"; // Version
// Declare Dependencies (Optional)
// Key: Dependent Plugin Name (Name)
// Value: Dependent Version Range (Supports standard NuGet range syntax)
public IReadOnlyDictionary<string, string> Dependencies => new Dictionary<string, string>
{
{ "sharw.core", "[1.0, 2.0)" }, // Depends on sharw.core, version >=1.0 and <2.0
{ "another.plugin", "1.*" } // Depends on another.plugin, major version 1
};
// Enable Automatic Route Prefix
public bool UseAutoRoutePrefix => true;
// Define Default Configuration (Optional)
public object? DefaultConfig => new { MySetting = "DefaultValue" };
// 2. Register Services
public void RegisterServices(IServiceCollection services, IConfiguration configuration)
{
// Register your business services here
}
// 3. Configure Pipeline
public void Configure(WebApplication app)
{
// Add request processing middleware here
}
// 4. Define Endpoints
public void RegisterRoutes(IEndpointRouteBuilder app, IConfiguration configuration)
{
// Define API endpoints here
}
}TIP
If you need more complex validation logic (e.g., optional dependencies), see Advanced Dependency Configuration
Meta Information
This part defines the plugin's information.
Name: The globally unique ID of the plugin.
- Convention: Format is
author.plugin(all lowercase). Automatically replaced when created using a template. - Example:
"sharw.apimgr"
- Convention: Format is
DisplayName: The display name of the plugin, can use Chinese.
Version: Plugin version number, following Semantic Versioning.
Dependencies: Declares plugin dependencies.
- Key: The
Nameof the dependent plugin. - Value: The version range of the dependency.
- Supported Formats:
- Standard range:
[1.0, 2.0)(>=1.0 and <2.0),1.0(>=1.0) - Floating range:
1.*(Any version with major version 1)
- Standard range:
- Key: The
UseAutoRoutePrefix: Recommended to enable. When
true, the main program automatically adds the/{plugin-name}prefix (e.g.,/sharw.apimgr) to your endpoints.DefaultConfig: Sets the default configuration file.
- When the plugin loads for the first time and the configuration file does not exist, the main program automatically generates this object as a
config/plugin-name.jsonfile (e.g.,/sharw.apimgr.json). - For detailed usage, refer to Plugin Configuration.
- When the plugin loads for the first time and the configuration file does not exist, the main program automatically generates this object as a
DataDirectory: The full path to the plugin's dedicated data directory. Defaults to
{BaseDir}/data/{Name}/. The host automatically creates this directory on startup.GetDataPath(relativePath): A shorthand method that combines a relative path and returns the full absolute path. For example,
GetDataPath("plugin.db")returns{DataDirectory}/plugin.db.- For detailed usage, refer to Plugin Data Directory.
Register Services (RegisterServices)
In SharwAPI, we adopt the standard Dependency Injection (DI) pattern.
You don't need to manually create (new) complex objects (like database connection services, HTTP client services) in your code. Just register them in RegisterServices.
Thereafter, wherever you need to use these tools, the system will automatically inject the prepared instances for you to use directly.
Simple Dependency Injection Example:
Suppose my plugin needs to access Google, requiring a browser tool (HttpClient). I can call
AddHttpClientinRegisterServicesto add a singleton. During the subsequent execution of the plugin, I can let this HttpClient be injected into where I need it via dependency injection.
public void RegisterServices(IServiceCollection services, IConfiguration configuration)
{
// Scenario: My plugin needs to access Google, requiring a browser tool (HttpClient)
// Action: The main program registers this service
// Note: To prevent conflicts, it is recommended to specify a name for the registered HttpClient here (i.e., sharw.apimgr.client below)
services.AddHttpClient("sharw.apimgr.client", client =>
{
client.BaseAddress = new Uri("https://google.com");
client.Timeout = TimeSpan.FromSeconds(10);
});
// Scenario: I wrote a class named MyDatabase to operate the database, and this class also needs to use the HttpClient service
// Action: Register it so the entire plugin can share this single instance
// Also, if this service declares a need for some other service, DI will automatically inject those dependencies into the constructor.
// (Interface input is not mandatory; you can also input the MyDataBase class directly)
services.AddSingleton<IMyDataBaseService>();
}
// My MyDataBase class
public class MyDataBaseService : IMyDataBaseService
{
private readonly HttpClient _googleHttpClient;
// Indicate need for HttpClient in constructor
// After AddSingleton<IMyDataBaseService>() above
// Dependency injection will automatically inject the httpClient.
public MyDataBaseService(HttpClient googleHttpClient)
{
_googleHttpClient = googleHttpClient;
}
// Other implementations...
}
interface IMyDataBaseService
{
// Other things...
}Advanced: Function Calls Between Plugins
RegisterServices is also the primary way for function calls between plugins.
- Providing Functions: If you write a service class (e.g.,
MyDatabase), and want it to be used by other plugins, register it in the container (services.AddSingleton<MyDatabase>()). - Using Functions: Just like
MyDatabasedeclaring a need forHttpClient, other plugins simply declare a need forMyDatabasein their constructors or routes, and the main program will automatically inject it into them.
Configure Pipeline (Configure)
When a user accesses an API, the request doesn't arrive at the destination instantly; instead, it flows through a pipe like water. The Configure method allows you to install "checkpoints" in the pipe. All requests must pass through these checkpoints before reaching the specific API.
Code Example:
public void Configure(WebApplication app)
{
// Install a simple middleware (valve)
app.Use(async (context, next) =>
{
// Pre-processing
Console.WriteLine($"[{Name}] Someone accessed: {context.Request.Path}");
// Call next() to release
// If you don't call next(), the request will be intercepted here and will never reach the API
await next();
});
}Advanced: Information Passing Between Plugins
Configure can also be a way to pass request context information between plugins.
- Passing Info: For example, an Auth plugin can parse the user Token here and store user information in
HttpContext. - Getting Info: Subsequent Log plugins or business plugins can read this info from
HttpContextto know "who the current user is".
For details, see: Configure Middleware #Communication Between Plugins
Define Endpoints (RegisterRoutes)
This is the core part of the plugin, used to establish the mapping between API requests and code processing.
Code Example:
public void RegisterServices(IServiceCollection services, IConfiguration configuration)
{
// Register a class named MyDatabase to operate the database
services.AddSingleton<MyDatabase>();
}
public void RegisterRoutes(IEndpointRouteBuilder app, IConfiguration configuration)
{
// Note: If you enabled UseAutoRoutePrefix => true
// The app here is already a route group containing the /{plugin-id} prefix
// Define specific API endpoints
// MyDatabase will be automatically injected, no manual creation needed
// (Provided MyDatabase has been registered in RegisterServices)
// Final URL: /sharw.apimgr/hello
app.MapGet("/hello", (MyDatabase db) =>
{
return db.GetData();
});
}