Skip to content

Commit d4b0e04

Browse files
authored
Merge pull request #93 from blehnen/fix/preload-plugin-assemblies
fix: preload plugin assemblies at startup for type resolution
2 parents c6f07ee + 70b09a3 commit d4b0e04

4 files changed

Lines changed: 155 additions & 2 deletions

File tree

Source/DotNetWorkQueue.Dashboard.Api.Tests/Extensions/DashboardExtensionsTests.cs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
using System;
2+
using System.IO;
23
using System.Linq;
4+
using System.Reflection;
35
using DotNetWorkQueue.Dashboard.Api;
46
using DotNetWorkQueue.Dashboard.Api.Configuration;
57
using DotNetWorkQueue.Dashboard.Api.Services;
8+
using FluentAssertions;
69
using Microsoft.Extensions.DependencyInjection;
710
using Microsoft.VisualStudio.TestTools.UnitTesting;
811

@@ -153,5 +156,85 @@ public void AddDotNetWorkQueueDashboard_Does_Not_Register_ConsumerPruningService
153156
Assert.AreEqual(0, pruningServiceRegistrations.Count,
154157
"ConsumerPruningService should not be registered when tracking is disabled");
155158
}
159+
160+
[TestMethod]
161+
public void AddDotNetWorkQueueDashboard_PreloadsAssemblies_From_AssemblyPaths()
162+
{
163+
// Copy a known DLL to a temp "plugin" directory
164+
var pluginDir = Path.Combine(Path.GetTempPath(), "dnwq-preload-test-" + Guid.NewGuid().ToString("N"));
165+
Directory.CreateDirectory(pluginDir);
166+
try
167+
{
168+
// Use Newtonsoft.Json as a test DLL — it's in our bin but let's verify
169+
// the preload path works by copying it and checking it loads from there
170+
var sourceDll = Path.Combine(AppContext.BaseDirectory, "FluentAssertions.dll");
171+
var destDll = Path.Combine(pluginDir, "FluentAssertions.dll");
172+
File.Copy(sourceDll, destDll);
173+
174+
var services = new ServiceCollection();
175+
services.AddLogging();
176+
177+
services.AddDotNetWorkQueueDashboard(options =>
178+
{
179+
options.EnableSwagger = false;
180+
options.AssemblyPaths = new[] { pluginDir };
181+
});
182+
183+
// If PreloadAssemblies threw, we wouldn't get here
184+
var provider = services.BuildServiceProvider();
185+
var opts = provider.GetRequiredService<DashboardOptions>();
186+
opts.AssemblyPaths.Should().ContainSingle().Which.Should().Be(pluginDir);
187+
}
188+
finally
189+
{
190+
Directory.Delete(pluginDir, true);
191+
}
192+
}
193+
194+
[TestMethod]
195+
public void AddDotNetWorkQueueDashboard_PreloadAssemblies_Ignores_NonexistentDir()
196+
{
197+
var services = new ServiceCollection();
198+
services.AddLogging();
199+
200+
// Should not throw even if the directory doesn't exist
201+
services.AddDotNetWorkQueueDashboard(options =>
202+
{
203+
options.EnableSwagger = false;
204+
options.AssemblyPaths = new[] { "/nonexistent/path/that/does/not/exist" };
205+
});
206+
207+
var provider = services.BuildServiceProvider();
208+
provider.GetRequiredService<DashboardOptions>().Should().NotBeNull();
209+
}
210+
211+
[TestMethod]
212+
public void AddDotNetWorkQueueDashboard_PreloadAssemblies_Ignores_InvalidDlls()
213+
{
214+
var pluginDir = Path.Combine(Path.GetTempPath(), "dnwq-preload-invalid-" + Guid.NewGuid().ToString("N"));
215+
Directory.CreateDirectory(pluginDir);
216+
try
217+
{
218+
// Write a non-.NET file with .dll extension
219+
File.WriteAllText(Path.Combine(pluginDir, "NotADotNet.dll"), "this is not a valid dll");
220+
221+
var services = new ServiceCollection();
222+
services.AddLogging();
223+
224+
// Should not throw on invalid DLLs
225+
services.AddDotNetWorkQueueDashboard(options =>
226+
{
227+
options.EnableSwagger = false;
228+
options.AssemblyPaths = new[] { pluginDir };
229+
});
230+
231+
var provider = services.BuildServiceProvider();
232+
provider.GetRequiredService<DashboardOptions>().Should().NotBeNull();
233+
}
234+
finally
235+
{
236+
Directory.Delete(pluginDir, true);
237+
}
238+
}
156239
}
157240
}

Source/DotNetWorkQueue.Dashboard.Api/DashboardExtensions.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
//Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
1818
// ---------------------------------------------------------------------
1919
using System;
20+
using System.IO;
2021
using System.Linq;
2122
using DotNetWorkQueue.Dashboard.Api.Configuration;
2223
using DotNetWorkQueue.Dashboard.Api.Controllers;
@@ -56,6 +57,10 @@ public static IServiceCollection AddDotNetWorkQueueDashboard(
5657
var options = new DashboardOptions();
5758
configureOptions(options);
5859

60+
// Pre-load user assemblies so Newtonsoft's TypeNameHandling binder can resolve
61+
// message types during deserialization (before ResolveMessageBodyType runs).
62+
PreloadAssemblies(options.AssemblyPaths);
63+
5964
services.AddSingleton(options);
6065
services.AddSingleton<IDashboardApi>(sp =>
6166
{
@@ -258,6 +263,30 @@ private static void AddConnectionByTransport(DashboardOptions options, string tr
258263
throw new ArgumentException($"Unknown transport type: '{transport}'. Valid values: SqlServer, PostgreSql, SQLite, LiteDb, Redis.");
259264
}
260265
}
266+
267+
private static void PreloadAssemblies(string[] paths)
268+
{
269+
if (paths == null || paths.Length == 0)
270+
return;
271+
272+
foreach (var dir in paths)
273+
{
274+
if (!Directory.Exists(dir))
275+
continue;
276+
277+
foreach (var dll in Directory.GetFiles(dir, "*.dll"))
278+
{
279+
try
280+
{
281+
System.Reflection.Assembly.LoadFrom(dll);
282+
}
283+
catch
284+
{
285+
// Not a valid .NET assembly — skip silently
286+
}
287+
}
288+
}
289+
}
261290
}
262291

263292
/// <summary>

Source/DotNetWorkQueue.Dashboard.Api/Services/DashboardService.cs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -471,10 +471,15 @@ private static object SanitizeHeaderValue(object value)
471471
/// </summary>
472472
private Type ResolveMessageBodyType(string portableName)
473473
{
474+
_logger.LogDebug("Resolving message body type: {PortableName}", portableName);
475+
474476
// Stage 1: already loaded in AppDomain
475477
var type = Type.GetType(portableName);
476478
if (type != null)
479+
{
480+
_logger.LogDebug("Type resolved from AppDomain: {Type}", type.FullName);
477481
return type;
482+
}
478483

479484
// portableName format: "TypeFullName, AssemblySimpleName"
480485
var commaIndex = portableName.IndexOf(',');
@@ -486,18 +491,30 @@ private Type ResolveMessageBodyType(string portableName)
486491
var dllFileName = assemblySimpleName + ".dll";
487492

488493
// Stage 2: try loading from bin folder
489-
var resolved = TryLoadType(Path.Combine(AppContext.BaseDirectory, dllFileName), typeFullName);
494+
var binPath = Path.Combine(AppContext.BaseDirectory, dllFileName);
495+
var resolved = TryLoadType(binPath, typeFullName);
490496
if (resolved != null)
497+
{
498+
_logger.LogDebug("Type resolved from bin folder: {Path}", binPath);
491499
return resolved;
500+
}
501+
_logger.LogDebug("Assembly not found in bin folder: {Path}", binPath);
492502

493503
// Stage 3: try configured assembly paths
494504
foreach (var dir in _assemblyPaths)
495505
{
496-
resolved = TryLoadType(Path.Combine(dir, dllFileName), typeFullName);
506+
var pluginPath = Path.Combine(dir, dllFileName);
507+
resolved = TryLoadType(pluginPath, typeFullName);
497508
if (resolved != null)
509+
{
510+
_logger.LogDebug("Type resolved from plugin path: {Path}", pluginPath);
498511
return resolved;
512+
}
513+
_logger.LogDebug("Assembly not found in plugin path: {Path}", pluginPath);
499514
}
500515

516+
_logger.LogWarning("Could not resolve type {PortableName}. Searched: bin={BinDir}, plugins=[{PluginDirs}]",
517+
portableName, AppContext.BaseDirectory, string.Join(", ", _assemblyPaths));
501518
return null;
502519
}
503520

docker/dashboard/DOCKERHUB.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,30 @@ docker run -d \
100100

101101
Set the connection string to the mounted path: `"Data Source=/data/sqlite/myqueue.db"`
102102

103+
## User Message Assemblies (POCO DLLs)
104+
105+
If your queues contain custom POCO types, the dashboard needs those assemblies to display typed message bodies. Mount a directory with your DLLs and configure the path:
106+
107+
```bash
108+
docker run -d \
109+
-p 8080:8080 \
110+
-v "$(pwd)/appsettings.json:/app/appsettings.json:ro" \
111+
-v "/path/to/your/dlls:/app/plugins:ro" \
112+
blehnen74/dotnetworkqueue-dashboard:latest
113+
```
114+
115+
Add to `appsettings.json`:
116+
117+
```json
118+
{
119+
"Dashboard": {
120+
"AssemblyPaths": ["/app/plugins"]
121+
}
122+
}
123+
```
124+
125+
You can also build a derived image instead: `FROM blehnen74/dotnetworkqueue-dashboard:latest` and `COPY` your DLLs into `/app/plugins/`.
126+
103127
## Tags
104128

105129
| Tag | Description |

0 commit comments

Comments
 (0)