ModuWeb is a .NET web application that supports dynamic runtime loading, reloading, and unloading of external modules (.dll files).
Each module is self-contained and can expose custom HTTP routes, CORS policies, and request handlers.
- π Hot-reloadable modules β automatically reloads modules when their
.dllfiles are updated or replaced. - π File system watching β monitors the
modules/folder for.dllchanges usingFileSystemWatcher. - π Per-module CORS β modules define their own CORS rules.
- π Custom middleware routing β routes HTTP requests to appropriate modules based on URL.
- πΎ Session support β every module can create and/or use session storage.
- β‘ Event system β allows modules to subscribe to and react to system events.
- π¬ Message system β enables modules to communicate with each other.
- π§Ύ Built-in logger β simple color-coded console logger for info, warnings, and errors.
- πΌοΈ Razor view engine β runtime Razor compilation via RazorLight for HTML pages with models.
- π‘ Server-Sent Events (SSE) β built-in support for real-time server-to-client streaming with a fluent Razor helper.
ModuWeb/
β
βββ Properties/
β βββ launchSettings.json # Startup settings for dev mode
β
βββ Events/
β βββ Events.cs # Contains all events
β βββ ModuleLoadedEventArgs.cs # Args for event about loaded module
β βββ ModuleMessageSentEventArgs.cs # Args for event about sent message
β βββ ModuleUnloadedEventArgs.cs # Args for event about unloaded module
β βββ RequestReceivedEventArgs.cs # Args for event about received http request
β βββ SafeEvent.cs # Base and safe class for events
β
βββ examples/ # Examples modules
β
βββ Cors/
β βββ DynamicCorsPolicyProvider.cs # CORS policy provider per module
β βββ Headers.cs # CORS headers constants
β βββ ModuleCorsGuardMiddleware.cs # Middleware for handling CORS per module
β
βββ Extensions/
β βββ ArrayExtension.cs # Little extension for array
β βββ HttpRequestExtension.cs # Extension for get request data (from query string or json body)
β βββ HttpResponseExtension.cs # Extensions for Razor page rendering and SSE streaming
β βββ JsonOptionExtension.cs # JSON serializer options (camelCase, null handling)
β βββ SessionExtensions.cs # Session helper extensions for HttpContext
β βββ SseHtmlHelper.cs # Fluent SSE helper for Razor views (Sse.Stream(...).Bind(...))
β βββ StringExtension.cs # Little extension for string.Replace(old, new, count)
β
βββ ModuleLoadSystem/
β βββ ModuleLoadContext.cs # Custom AssemblyLoadContext
β βββ ModuleManager.cs # Loads/unloads modules and handles lifecycle
β βββ ModuleWatcher.cs # Watches for module file changes
β
βββ ModuleMessenger/
β βββ ModuleMessage.cs # Module message that every moudle can create and receive
β βββ ModuleMessenger.cs # System handler for module messages
β
βββ SessionSystem/
β βββ ISessionService.cs # Interface of session service
β βββ LiteDbSessionService.cs # Session service for create and working with sessions
β βββ SessionData.cs # Data that store into database
β
βββ Storage/
β βββ IStorageService.cs # Interface of storage service
β βββ LiteDbStorageService.cs # Data that store into database
β
βββ ViewEngine/
β βββ IModuleViewEngine.cs # Interface for module view engine
β βββ ModuleViewEngine.cs # RazorLight-based runtime Razor compilation
β
βββ appsettings.json # Default appsettings
βββ LICENSE.txt # License for this project
βββ Logger.cs # Static logger with color output
βββ ModuleBase.cs # Base class for all modules
βββ ModuleMiddleware.cs # Middleware for routing requests to modules
βββ Program.cs # Application entry point
βββ QueryParser.cs # Tool for parse args from query
βββ RouteDictionary.cs # Path + method β handler registry
To run the project, make sure you have the .NET Runtime (Microsoft.AspNetCore.App) or SDK version 9.0.2 or higher installed.
dotnet --list-sdksIf it's installed, you must see something like that:
9.0.200 [C:\Program Files\dotnet\sdk]
If it's not installed, you need to install it there.
dotnet --list-runtimesIf it's installed, you must see something like that:
Microsoft.AspNetCore.App 9.0.2 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
If it's not installed, you need to install it there. Choose Run server apps.
- Clone the repository:
git clone https://github.com/Chaleshka/ModuWeb.git
cd ModuWeb- Build the solution using .NET SDK 9.0.2+.
dotnet build- Run the app:
dotnet run- Download the latest release from the Releases page
- Extract the archive to your preferred directory
- Launch the app:
# Windows
ModuWeb.exe
# Linux/macOS:
dotnet ModuWeb.dllAfter launching the program, the modules folder will be created. You need to put all the modules you need in it.
Also, if dependencies are required, drop them in the modules/dependencies folder.
If everything is fine with the modules, they will be loaded automatically.
Firstly create project:
dotnet new classlib -n ModuleName
cd ModuleNameThen you need to add to dependencies ModuWeb.dll.
Important: To use HttpContext and other ASP.NET Core types in your module, add a FrameworkReference to your .csproj file:
<ItemGroup>
<!-- Add this to get HttpContext and ASP.NET Core dependencies -->
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<!-- Reference to ModuWeb.dll -->
<Reference Include="ModuWeb">
<HintPath>path/to/ModuWeb.dll</HintPath>
</Reference>
</ItemGroup>This way you don't need to manually add NuGet packages for ASP.NET Core dependencies.
A module must inherit from ModuleBase and override methods such as:
public class HelloWorldModule : ModuleBase
{
public override async Task OnModuleLoad()
{
Map("hello", "GET", HelloWorldHandler);
}
public async Task HelloWorldHandler(HttpContext context)
{
context.Response.StatusCode = 200;
await context.Response.WriteAsync("Hello World!");
}
}Map(string path, string method, Func<HttpContext, Task> handler)β maps a route.Handle(...)β receives and routes the request.WithOriginsCors,WithHeadersCors,BlockFailedCorsRequestsβ specify CORS policies.ModuleNameβ name of module that will used for some system tools.OnModuleLoad()β optional initialization logic.OnModuleUnLoad()β optional cleanup logic.
Module files may have unique names:
- index.dll β special module name used to handle the main page (/ or /index).
You can also see the examples in examples.
Modules can render HTML pages using Razor (.cshtml) templates via RazorLight. Views are embedded as resources in the module DLL.
- Mark
.cshtmlfiles as Embedded Resource in your.csproj:
<ItemGroup>
<EmbeddedResource Include="Views\**\*.cshtml" />
</ItemGroup>Views are registered automatically when the module is loaded β no extra code needed.
- Render a page from a handler:
private async Task PageHandler(HttpContext context)
{
var model = new { Title = "Hello", Message = "World" };
await context.Response.WriteRazorPageAsync("Views/Index.cshtml", model);
}- Access model data in
.cshtml:
<h1>@Model.Title</h1>
<p>@Model.Message</p>Override GetInitialViewData in your module to provide common data that will be automatically available in every Razor view β without passing it manually each time. Useful for base paths, locale, user info, app settings, etc.
protected override Dictionary<string, object> GetInitialViewData(HttpContext context) => new()
{
int i = 0;
["Title"] = $"Some title #" + (++i).ToString(),
["Lang"] = context.Request.Headers["Accept-Language"].FirstOrDefault() ?? "en",
["Year"] = DateTime.Now.Year
};These values are merged into the model and accessible in .cshtml as @Model.BasePath, @Model.Lang, etc.:
<html lang="@Model.Lang">
<head>
<title>@Model.Title</title>
</head>
<body>
<footer>Β© @Model.Year</footer>
</body>
</html>This is called automatically by WriteRazorPageAsync when no explicit viewData parameter is passed. If you pass viewData manually, GetInitialViewData is skipped.
ModuWeb has built-in SSE support on both sides: a server-side extension for streaming data and a client-side Razor helper for receiving it β no jQuery or manual JavaScript needed.
In your module handler, use WriteSseAsync to push data to the client on a fixed interval:
using ModuWeb.Extensions;
// Simple (synchronous generator)
private async Task StreamHandler(HttpContext context)
{
await context.Response.WriteSseAsync(() => new
{
time = DateTime.Now.ToString("HH:mm:ss"),
date = DateTime.Now.ToString("yyyy-MM-dd")
}, intervalMs: 5000);
}
// Async generator (for DB queries, HTTP calls, etc.)
private async Task StreamHandler(HttpContext context)
{
await context.Response.WriteSseAsync(async ct =>
{
var data = await GetSensorDataAsync(ct);
return new { temperature = data.Temp, humidity = data.Hum };
}, intervalMs: 2000);
}The extension handles Content-Type, Cache-Control, flushing, JSON serialization, and client disconnect automatically.
You can also send named events:
await context.Response.WriteSseAsync(() => payload, intervalMs: 1000, eventName: "sensor-update");Instead of writing JavaScript manually, use the fluent Sse helper directly in .cshtml:
@using ModuWeb.Extensions
<p id="serverTime">Loading...</p>
<p id="lastUpdate"></p>
@(Sse.Stream("time-stream")
.Bind("#serverTime", "time")
.Bind("#lastUpdate", "date", "Updated: {0}")
.Render())This generates all the EventSource JavaScript automatically. No jQuery, no <script> blocks.
| Method | Description |
|---|---|
.Bind("#id", "field") |
Sets element's textContent to the JSON field value |
.Bind("#id", "field", "Format: {0}") |
Same, with a format string |
.OnMessage("js code") |
Raw JS executed on each message (has access to data) |
.OnOpen("js code") |
Raw JS executed when connection opens |
.OnError("js code") |
Raw JS executed on connection error |
.On("eventName", e => e.Bind(...)) |
Bindings for a named SSE event |
@using ModuWeb.Extensions
@(Sse.Stream("time-stream")
.Bind("#serverTime", "time")
.Bind("#lastUpdate", "datetime", "Updated: {0}")
.OnOpen("document.getElementById('status').textContent='Connected'")
.OnError("document.getElementById('status').textContent='Reconnecting...'")
.Render())Note: Always wrap in
@(...)for multi-line expressions.Render()returns raw HTML that won't be escaped by Razor.
SSE is optional. ModuWeb ships with jQuery (/jquery-4.0.0.js) available for all modules. You can use classic polling with $.get / $.ajax if you prefer β or combine both approaches in the same project:
<script src="/jquery-4.0.0.js"></script>
<script>
function updateTime() {
$.get('time').done(function (data) {
$('#serverTime').text(data.time);
$('#lastUpdate').text('Updated: ' + data.datetime);
});
}
setInterval(updateTime, 1000);
updateTime();
</script>- Dependencies should be placed in
modules/dependencies/. They will be copied automatically. - Modules are loaded into memory. Dependencies only as
- A failed module load is logged but does not crash the host.
- The middleware checks the base API path (from configuration) and maps requests accordingly.
- Empty string into path in Map will mean base url with some method.
After placing a sample DLL in modules/, you can access its route via:
http://localhost:5000/{ModuleName}/{Route}
For example, with a module named HelloWorld:
GET http://localhost:5000/HelloWorld/hello
The example/ folder includes working example modules you can compile and test.
This project is open-source and free to use, modify, and distribute.