Edit: zie 2e post voor een belangrijke update.
Ik zit met een probleem voor een soort van plugin systeem wat ik al een hele tijd geleden ontwikkeld heb en ook al een tijdje in gebruik is.
Excuus voor een lange post. Ik zit al 2 dagen tegen dit probleem aan te lopen maar ik kom er niet uit. Ik heb geprobeerd een simpel voorbeeld project te bouwen.
Heel in het kort is het probleem:
Er zijn 4 projects die met de plugins te maken hebben in dit voorbeeld:
In dit voorbeeld doen de plugins niks anders als: een aantal properties definieren waarvan de waarde tijdens runtime opgehaald moet worden.
IPlugin is enkel een interface met een Version property en een Load functie:
PluginV1 is een simpel voorbeeld van een plugin:
PluginV2 is een voorbeeld van een nieuwe versie, waarbij de waardes van de properties veranderen:
PluginBase ten slotte heeft de belangrijke logica:
Een belangrijk punt: het project BaseLibrary (met de PluginBase class) kan niet als reference worden opgenomen in de hoofd applicatie. Dit heeft onder andere te maken met:
De oplossing die ik daarvoor gebruik is dat ik de plugins inclusief de dll van de BaseLibrary mee geef. In het kort schrijf ik de dll file van de plugin zelf (bijv TestPluginV1.dll) en ook de dll van de BaseLibrary beiden naar een plugin file (gewoon raw bytes).
Bij het inladen van de plugin weet ik dat het eerste deel uit de plugin bestaat en het tweede deel uit de BaseLibrary, en beide zijn nodig om de assemblies te laden.
Het inladen van de plugin gebeurt dan als volgt:
Wat er wel werkt:
Tot nu toe werkt dit prima. Ik kan op deze manier een plugin inladen vanuit een project wat enkel de interface kent (IPlugin), maar niet de base class (PluginBase), en ook niet de plugin class (PluginV1 of V2).
Voorbeeld:
Wat er niet werkt:
Als ik nu opnieuw een plugin laadt, zonder de applicatie te herstarten, dan lijkt de PluginBase / BaseLibrary niet correct opnieuw ingeladen te worden. Als voorbeeld laad ik nu eerst V1, en meteen daarna plugin V2. Zoals boven te zien heeft V2 andere waardes. Bij het builden van V2 is ook de assembly versie op V2 gezet, en de Version property in PluginBase stond ook op '2'.
De output:
De Property1 en Property2 waarden van de PluginV2 class zijn wel goed, maar de Version property (in de PluginBase class!) is niet goed.
Als ik ook de assemblies die geladen zijn uitprint dan staat TestPluginV1 (v1.0.0.0) en TestPluginV2 (v2.0.0.0) er correct in. Echter BaseLibrary staat er twee keer met v1.0.0.0 in...
Als ik het deel met V1 laden skip, en gewoon meteen V2 laadt, dan gaat het wel gewoon goed en krijg ik inderdaad als output versie 2:
---
Ik weet niet wat er hier mis gaat.
Is er een of andere caching actief waardoor via AssemblyResolve niet de juiste "v2" van de PluginBase / BaseLibrary geladen wordt?
Alvast bedankt voor enige tips!
Ik zit met een probleem voor een soort van plugin systeem wat ik al een hele tijd geleden ontwikkeld heb en ook al een tijdje in gebruik is.
Excuus voor een lange post. Ik zit al 2 dagen tegen dit probleem aan te lopen maar ik kom er niet uit. Ik heb geprobeerd een simpel voorbeeld project te bouwen.
Heel in het kort is het probleem:
- Mijn plugin is een simpele instance van een class die inherit van een base class.
- De plugin instance zit in een project / assembly / dll, terwijl de base class in een ander project / assembly / dll zit.
- Nu kan ik meerdere versies van een plugin maken, met ook meerdere versies van de base class.
- Bij het dynamisch inladen van de plugin assembly moet ik dan ook de dll van de base class geladen worden. Dit regel ik via AssemblyResolve event.
- Als ik eerst "v1" van de plugin laadt, en daarna "v2", dan lijkt het laden van de base class niet opnieuw te gebeuren en nog steeds de oude assembly te gebruiken.
Er zijn 4 projects die met de plugins te maken hebben in dit voorbeeld:
Project / assembly | Class / interface | Uitleg |
---|---|---|
PluginInterfaceLibrary | IPlugin | Interface die elke plugin moet implementeren |
BaseLibrary | PluginBase : IPlugin | Abstract, basis implementatie van de plugins. |
TestPluginV1 | PluginV1 : PluginBase | Versie 1 van een plugin als voorbeeld. |
TestPluginV2 | PluginV2 : PluginBase | Versie 2 van een plugin als voorbeeld. |
In dit voorbeeld doen de plugins niks anders als: een aantal properties definieren waarvan de waarde tijdens runtime opgehaald moet worden.
IPlugin is enkel een interface met een Version property en een Load functie:
C#:
1
2
3
4
5
| public interface IPlugin { int Version { get; } Dictionary<string, object> Load(); } |
PluginV1 is een simpel voorbeeld van een plugin:
C#:
1
2
3
4
5
| public class PluginV1 : PluginBase { public int Property1 { get; set; } = 1234; public int Property2 { get; set; } = 5678; } |
PluginV2 is een voorbeeld van een nieuwe versie, waarbij de waardes van de properties veranderen:
C#:
1
2
3
4
5
| public class PluginV2 : PluginBase { public int Property1 { get; set; } = -1; public int Property2 { get; set; } = -2; } |
PluginBase ten slotte heeft de belangrijke logica:
- Via GetType().GetProperties() worden alle properties opgehaald en opgeslagen
- in de Load functie wordt vervolgens de waarde van de properties uit de plugin instance gelezen en als dictionary terug gestuurd
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| public abstract class PluginBase : IPlugin { private List<PropertyInfo> _properties; protected PluginBase() { LoadProperties(); } public int Version => 1; private void LoadProperties() { // Loop over properties of type and store them _properties = new List<PropertyInfo>(); foreach (var prop in GetType().GetProperties()) { _properties.Add(prop); } } public Dictionary<string, object> Load() { // On load, read the property values from the instance and return them in a dictionary var values = new Dictionary<string, object>(); foreach (var prop in _properties) { var value = prop.GetValue(this); values.Add(prop.Name, value); } return values; } } |
Een belangrijk punt: het project BaseLibrary (met de PluginBase class) kan niet als reference worden opgenomen in de hoofd applicatie. Dit heeft onder andere te maken met:
- Niet alle plugins hoeven PluginBase te zijn. Zolang ze maar IPlugin implementeren.
- Er zijn verschillende soorten PluginBase. In dit voorbeeld maar 1 om het simpel te houden, in het echt zijn er 10+ verschillende die allemaal verschillende werking hebben. Deze gaan ook naar verschillende gebruikers en het is niet wenselijk dat de hoofd applicatie al deze implementaties kent, of naar andere gebruikers stuurt.
De oplossing die ik daarvoor gebruik is dat ik de plugins inclusief de dll van de BaseLibrary mee geef. In het kort schrijf ik de dll file van de plugin zelf (bijv TestPluginV1.dll) en ook de dll van de BaseLibrary beiden naar een plugin file (gewoon raw bytes).
Bij het inladen van de plugin weet ik dat het eerste deel uit de plugin bestaat en het tweede deel uit de BaseLibrary, en beide zijn nodig om de assemblies te laden.
Het inladen van de plugin gebeurt dan als volgt:
- Lees de bytes op zo'n manier dat ik weet welke bytes bij de plugin horen en welke bij de BaseLibrary
- Gebruik Assembly.Load om de plugin dll te laden.
- Vind het type van de plugin dat IPlugin implementeert en maak hier een instance van.
- Op dit moment zal er een AssemblyResolve event plaatsvinden omdat BaseLibrary (met PluginBase base class) nog niet ingeladen is. Hier laad ik vervolgens de BaseLibrary assembly die dus uit dezelfde plugin (raw byte) file komt.
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
| private static IPlugin LoadPluginFile(string filePath) { // Load the byte content of the two assemblies var bytes = LoadBytes(filePath); // Listen for AssemblyResolve event AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => { if (e.Name.StartsWith("BaseLibrary")) { Console.WriteLine(" --- AssemblyResolve requesting: " + e.Name); var baseAssm = Assembly.Load(bytes.PluginBase); Console.WriteLine(" --- > Loaded assembly: " + baseAssm.FullName); return baseAssm; } return null; }; // Load the main assembly // This will trigger AssemblyResolve, which loads the PluginBase var assm = Assembly.Load(bytes.MainPlugin); // Find the plugin type that implements IPlugin var type = assm.GetTypes() .Where(typeof(IPlugin).IsAssignableFrom) .Where(t => t != typeof(IPlugin)) .Where(t => !t.IsAbstract) .First(); // Create instance and return it return (IPlugin)Activator.CreateInstance(type); } private static PluginByteContainer LoadBytes(string filePath) { // Read the bytes var bytes = File.ReadAllBytes(filePath); using (var stream = new MemoryStream(bytes)) using (var reader = new BinaryReader(stream)) { // read the lengths var libraryLength = reader.ReadInt32(); var baseLength = reader.ReadInt32(); // read the contents var libraryBytes = reader.ReadBytes(libraryLength); var baseBytes = reader.ReadBytes(baseLength); return new PluginByteContainer { MainPlugin = libraryBytes, PluginBase = baseBytes }; } } private class PluginByteContainer { public byte[] MainPlugin { get; set; } public byte[] PluginBase { get; set; } } |
Wat er wel werkt:
Tot nu toe werkt dit prima. Ik kan op deze manier een plugin inladen vanuit een project wat enkel de interface kent (IPlugin), maar niet de base class (PluginBase), en ook niet de plugin class (PluginV1 of V2).
Voorbeeld:
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| static void Main(string[] args) { Console.WriteLine("Start loading plugin v1."); // Load plugin v1 var plugin = LoadPluginFile(FileV1); // Run and print output Console.WriteLine(" Loading plugin version: " + plugin.Version); var values = plugin.Load(); foreach (var value in values) { Console.WriteLine(" - Loaded value: " + value); } Console.ReadKey(); } |
Dit klopt: ik laad een file waarin plugin V1 zit, die komt inderdaad met versie 1 van alle assemblies. Ook zie ik de property values die horen bij PluginV1, en de Version property (in PluginBase) zegt v1.Start loading plugin v1.
--- AssemblyResolve requesting: BaseLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
--- > Loaded assembly: BaseLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
Loading plugin version: 1
- Loaded value: [Property1, 1234]
- Loaded value: [Property2, 5678]
- Loaded value: [Version, 1]
Wat er niet werkt:
Als ik nu opnieuw een plugin laadt, zonder de applicatie te herstarten, dan lijkt de PluginBase / BaseLibrary niet correct opnieuw ingeladen te worden. Als voorbeeld laad ik nu eerst V1, en meteen daarna plugin V2. Zoals boven te zien heeft V2 andere waardes. Bij het builden van V2 is ook de assembly versie op V2 gezet, en de Version property in PluginBase stond ook op '2'.
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| static void Main(string[] args) { Console.WriteLine("Start loading plugin v1."); // Load plugin v1 var plugin = LoadPluginFile(FileV1); // Run and print output Console.WriteLine(" Loading plugin version: " + plugin.Version); var values = plugin.Load(); foreach (var value in values) { Console.WriteLine(" - Loaded value: " + value); } // Reset plugin = null; Console.WriteLine(); // Load plugin v2 Console.WriteLine("Start loading plugin v2"); plugin = LoadPluginFile(FileV2); // Run again and print output Console.WriteLine(" Loading plugin version: " + plugin.Version); values = plugin.Load(); foreach (var value in values) { Console.WriteLine(" - Loaded value: " + value); } Console.WriteLine(); Console.WriteLine(); // List the assemblies loaded Console.WriteLine("Assemblies loaded:"); foreach (var assm in AppDomain.CurrentDomain.GetAssemblies().OrderBy(a => a.FullName)) { Console.WriteLine(" " + assm.FullName); } Console.ReadKey(); } |
De output:
Ik zie ook dat na het laden van PluginV2 de BaseLibrary v2 opgevraagd wordt. Echter, hij laadt nog steeds versie 1!.Start loading plugin v1.
--- AssemblyResolve requesting: BaseLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
--- > Loaded assembly: BaseLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
Loading plugin version: 1
- Loaded value: [Property1, 1234]
- Loaded value: [Property2, 5678]
- Loaded value: [Version, 1]
Start loading plugin v2
--- AssemblyResolve requesting: BaseLibrary, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null
--- > Loaded assembly: BaseLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
Loading plugin version: 1
- Loaded value: [Property1, -1]
- Loaded value: [Property2, -2]
- Loaded value: [Version, 1]
Assemblies loaded:
AssemblyLoadTest, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
BaseLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
BaseLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
PluginInterfaceLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
TestPluginV1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
TestPluginV2, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null
De Property1 en Property2 waarden van de PluginV2 class zijn wel goed, maar de Version property (in de PluginBase class!) is niet goed.
Als ik ook de assemblies die geladen zijn uitprint dan staat TestPluginV1 (v1.0.0.0) en TestPluginV2 (v2.0.0.0) er correct in. Echter BaseLibrary staat er twee keer met v1.0.0.0 in...
Als ik het deel met V1 laden skip, en gewoon meteen V2 laadt, dan gaat het wel gewoon goed en krijg ik inderdaad als output versie 2:
Dit laat zien dat de plugin file wel degelijk v2 van de BaseLibrary heeft.Start loading plugin v2
--- AssemblyResolve requesting: BaseLibrary, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null
--- > Loaded assembly: BaseLibrary, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null
Loading plugin version: 2
- Loaded value: [Property1, -1]
- Loaded value: [Property2, -2]
- Loaded value: [Version, 2]
Assemblies loaded:
AssemblyLoadTest, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
BaseLibrary, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null
mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
PluginInterfaceLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
TestPluginV2, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null
---
Ik weet niet wat er hier mis gaat.
Is er een of andere caching actief waardoor via AssemblyResolve niet de juiste "v2" van de PluginBase / BaseLibrary geladen wordt?
Alvast bedankt voor enige tips!
[ Voor 7% gewijzigd door NickThissen op 26-05-2021 23:55 ]