How correctly to access own Logger class - vb.net

I got my own simple logger class in class library project which is used in many other projects belongs to my solution. Right now i am using it in this way in every class that consume it in top:
Private Logger As New Logger("C:\someLogFile.log")
then later in code i am using it:
Logger.LogIt("Start part extracting files...")
I always need in every class create instance of it and its boring sometimes... I know i could create static Logger class and overcome the issue but would it be correct way or the way i am doing is ok according to OOP? What do you think?
EDIT: What do you think about this solution which is using Interface i've just implemented it what do you think?:
First project (Logger project) which is used in many other projects around one solution:
Imports System.IO
Public NotInheritable Class Logger
#Region "Fields"
Private Shared ReadOnly _locker As New Object()
#End Region
#Region "Constructors"
Public Sub New()
End Sub
#End Region
#Region "Log function"
Public Sub LogIt(ByVal msg As String, ByVal logMessage As String, Optional Path As String = "", Optional ByVal IsDebug As Boolean = False)
If File.Exists(Path) Then
If IsDebug Then
Debug.Print(DateTime.Now & "> | " & msg & " | " & logMessage)
Else
Using w As TextWriter = File.AppendText(Path)
w.WriteLine(DateTime.Now & "> | " & msg & " | " & logMessage)
w.Flush()
End Using
End If
Else
If IsDebug Then
Debug.Print(DateTime.Now & "> | " & msg & " | " & logMessage)
Else
SyncLock _locker
Using w As TextWriter = File.CreateText(Path)
w.WriteLine(DateTime.Now & "> | " & msg & " | " & logMessage)
w.Flush()
End Using
End SyncLock
End If
End If
End Sub
#End Region
i added additionaly ILog.vb interface to this project:
Public Interface ILog
Property LoggerPath As String
Sub LogIt(ByVal msg As String, ByVal logMessage As String, ByVal Path As String, Optional ByVal IsDebug As Boolean = False)
End Interface
Now one of class which would use Logger for instance this below. (LoggerPath will come to constructor of MainProcessRunner from xml serialization on the beggining) e.g C:/file.txt:
Public Class MainProcessRunner
Implements ILog
Private Property LoggerPath As String Implements ILog.LoggerPath
Private CollectionList As New List(Of ImportRunner)
Public Sub New(ByVal LoggerPath As String)
Me.LoggerPath = LoggerPath
CollectionList.Add(New ImportRunner(GPCollectTimePeriod.EveryMidnight, KpiName.Availability, MobileGenerationName.GSM, Me))
For Each item In CollectionList
If TypeOf item Is ICollectPeriod Then
Dim runDaily As ICollectPeriod = TryCast(item, ICollectPeriod)
runDaily.RunDaily()
End If
Next
...
Public Sub LogIt(msg As String, logMessage As String, Path As String, Optional IsDebug As Boolean = False) Implements ILog.LogIt
Dim Logger As New Logger
Logger.LogIt(Alert.Write(MsgType.INFO), logMessage, Me.LoggerPath, True)
End Sub
End class
As you can see inside constructor i am calling other class by:
runDaily.RunDaily()
then inside below class ILog interface is passed by so therefore i will have log path already from MainProcessRunner and then i need to only fill msgtype and message string:
Imports ImportJob
Public Class ImportRunner
Implements ICollectPeriod
Private Log As ILog
Private BeginImport As New ImportStart
Private GPCollectTimePeriodInMinutes As GPCollectTimePeriod
Private KpiName As KpiName
Private MobileGeneration As MobileGenerationName
Public Sub New(ByVal GPCollectTimePeriodInMinutes As GPCollectTimePeriod, ByVal KpiName As KpiName, ByVal MobileGenerationName As MobileGenerationName, ByVal log As ILog)
Me.GPCollectTimePeriodInMinutes = GPCollectTimePeriodInMinutes
Me.KpiName = KpiName
Me.MobileGeneration = MobileGenerationName
Me.Log = log
End Sub
Public Sub RunDaily() Implements ICollectPeriod.RunDaily
Log.LogIt(Alert.Write(MsgType.INFO), "sdsd", Log.LoggerPath, True)
....
End Class
In this line Log.LoggerPath is known as (C:/file.txt:)
Log.LogIt(Alert.Write(MsgType.INFO), "sdsd", Log.LoggerPath, True)
From class ImpotStart i can pass through Log as ILog further to the next class constructor if needed if that class would implement ILog interface. and so on..
What do you think about that solution?

What you need is a design pattern "Singleton" which assures only a single instance (the singleton) of the class can be created:
using System;
namespace Singleton.Structural
{
class Singleton
{
private static Singleton _instance;
// Constructor is 'protected'
protected Singleton()
{
}
public static Singleton Instance()
{
// Uses lazy initialization.
// Note: this is not thread safe.
if (_instance == null)
{
_instance = new Singleton();
}
return _instance;
}
}
}

The OOP way would be to perform dependency injection to send the same instance of the Logger class into each class that requires it.
However, there is absolutely nothing wrong with just making your class static.

You are doing the right thing only. If you tired of calling object.Method name.
Put this (i.e object method ) in base class of you project with a Common Method and call that method in everypage.
Public void LogError(string error)
{
Logger.LogIt(error);
}
You can call the same by LogError in catch statement.
Better you read below link for better Logging method for you project
http://www.asp.net/web-forms/overview/getting-started/getting-started-with-aspnet-45-web-forms/aspnet-error-handling

i read that static/share can be used if some object is used in almost every area in solution so logger is good enough to use it as static/shared and make change only to message text and path pass by its constructor.
I dont think that singleton could be good in this case because then he would need to track his reference to every class, and it would be really complicated if he would have 20 projects with multiple classes over one solution so i think its better to make Logger static.
or just stay what you doing right now that every class has their own Logger instance. The problem could be with file path only if you got big flow of a data.

Related

Logging errors and events, best practice?

I'm currently developing a 3-tier application and now I have come to logging, I am curios as to the best practices. Below is my interface and model for a LogEntry, I then implement that in a service called JsonLogService which serializes the entries and stores them in the file system.
Public Interface ILogService
Sub Log(logEntry As LogEntry)
Sub Log(source As String, type As LogEntryType, message As String)
Sub Log(source As String, type As LogEntryType, format As String, ParamArray arguments() As Object)
End Interface
Public Enum LogEntryType
Information
Warning
[Error]
Debug
End Enum
Public Class LogEntry
Public Property Source As String
Public Property Type As LogEntryType
Public Property Message As String
Public Property LoggedAt As Date = DateTime.Now
Public Sub New()
End Sub
Public Sub Now(source As String, type As LogEntryType, message As String)
Me.Source = source
Me.Type =
Me.Message = message
End Sub
Public Sub New(source As String, type As LogEntryType, format As String, ParamArray arguments() As Object)
Me.Source = source
Me.Type = type
Me.Message = String.Format(format, arguments)
End Sub
I'm currently using this service in my managers to log when events happen which works well, but now I am unsure how to implement this for catching errors.
Does it actually make sense to catch errors in my managers methods because surely my tests should prevent any errors from happening there?

Public Shared Readonly Member optimized out in VB.Net?

I have this weird behavior in VB.Net I am stuck with. At the moment I am not able to determine whether I am missing some concept or if it is a bug.
I have a base class:
Public MustInherit Class BaseType(Of T As BaseType(Of T)) :
Implements IEquatable(Of BaseType(Of T))
Protected Sub New(key As Integer, value As List(Of String))
LogManager.GetLogger("INFO").Info("Strings: " & vbTab & value.Count())
If key <> -1 Then
enumValues.Add(CType(Me, T))
End If
End Sub
Protected Shared Function TypeFromStringBase(name As String) As T
For Each b As T In enumValues
LogManager.GetLogger("DEBUG").Info(b.Names(0))
If b.Names.Contains(name) Then
Return b
End If
Next
Return TypeFromStringBase("Keine")
End Function
End Class
And a Class that inherits it:
Public Class AnschlussType : Inherits BaseType(Of AnschlussType) :
Implements IEquatable(Of AnschlussType)
Public Shared ReadOnly Rund As New AnschlussType(1, {"Rund", "1"})
Public Shared ReadOnly Flach As New AnschlussType(2, {"Flach", "2"})
Public Shared ReadOnly Gewinde As New AnschlussType(3, {"Gewinde", "3"})
Public Shared ReadOnly Kein As New AnschlussType(4, {"Kein", "None", "4"})
Private Sub New(key As Integer, names As String())
MyBase.New(key, names.ToList())
End Sub
Public Shared Function TypeFromString(name As String) As AnschlussType
Return TypeFromStringBase(name)
End Function
End Class
Here is the weird part I don't get. The first time you call AnschlussType.TypeFromString("Some String"), VB should create all the Public Shared ReadOnly members. This results in four calls to BaseType.New. Each of those calls then adds its own type to the enumValues List.
After all those initializations, finally, the AnschlussType.TypeFromString call will be executed. There it calls TypeFromStringBase which iterates over the enumValues List we filled right before.
This all works fine in the DEBUG mode.
Here is the weird part I don't get. Now I tried RELEASE mode. The enumValues.Count would always stay 0. I assumed this because the logger does not print anything, which means it doesn't iterate, which means it is zero. So I investigated a little more and put a logging statement into BaseType.New. And this does NOT log at all. This leads me to the conclusion that New is not executed at all.
Let me emphasize that this all works great in DEBUG mode and with other subtypes that have Public Shared ReadOnly members in the same matter.
But this does not work in RELEASE mode.
Does anyone have a hint for me what I could be missing?
If a static constructor (Section 10.11) exists in the class, execution of the static field initializers occurs immediately prior to executing that static constructor. Otherwise, the static field initializers are executed at an implementation-dependent time prior to the first use of a static field of that class.
Assuming VB works like C#, your shared (i.e. static) fields aren't being initialized because you haven't used them.
Try creating a shared constructor.

Chaining overloaded constructors

I am trying to create an efficient class with minimum code-duplication.
I have this defined:
Public Class Foo
Private _firstName as string = ""
Private _lastName as string = ""
Public Sub New(ByVal userGUID As Guid)
'query DB to get firstName and lastName
Me.New(dt.Rows(0)("FirstName").ToString(),dt.Rows(0)("LastName").ToString())
End Sub
Public Sub New(ByVal firstName As String, ByVal lastName As String)
_firstName = firstName.toUpper()
_lastName = lastName.toUpper()
Validate()
End Sub
Private Sub Validate()
' Throw error if something is wrong
End Sub
End Class
The Constructor with firstName and lastName parameters is the end-point constructor that does validation. A constructor with userGUID as a parameter would query DB to obtain name and call the final constructor. This way all execution should be directed towards one of the constructors that actually does all validation etc etc. The idea behind it is that if I add new contructors, I only have to pull necessary data (firstname/lastname) and call the final constructor that does validation.
However, there is a compilation error preventing me from using this system on line Me.New(dt.Rows(0)("FirstName").ToString(),dt.Rows(0)("LastName").ToString()). Apparently this line has to be the first line in the constructor. But If I have this as first line, it will break the validation process because validation will throw an error due to no firstname/lastname. I have to query the DB in order to pull that info.
I am aware that I can assign values here and call validation from this constructor too, but this will effectively isolate this constructor from the final one, thus duplicating code and adding to maintenance a bit. FYI, in the example below I only have 2 constructors, but in reality i have several more. If each will do its own assignment it just adds up to maintenance that much.
So, is there a way to achieve my task by executing some code and THEN calling an overloaded constructor?
Thank you for any insight
UPDATE 1:
Per the_lotus comment, I am including dt definition. There is a workaround for this issue. Basically I would take the validation and assignment out of the final constructor and put it into a function. All constructors would call this function, thus eliminating the need to chain constructors. It doesn't look bad, but I would like to understand why in order to chain constructors I have to put constructor calls on the first line.
Here is new code:
Public Class Foo
Private _firstName As String = ""
Private _lastName As String = ""
Public Sub New(ByVal userGUID As Guid)
Dim dt As New DataTable
' query DB to get firstName and lastName
' Assume I populate dt with at least one DataRow
AssignAndValidate(dt.Rows(0)("FirstName").ToString(), dt.Rows(0)("LastName").ToString())
'Me.New(dt.Rows(0)("FirstName").ToString(), dt.Rows(0)("LastName").ToString())
End Sub
Public Sub New(ByVal firstName As String, ByVal lastName As String)
AssignAndValidate(firstName, lastName)
End Sub
Private Sub Validate()
' Throw error if something is wrong
End Sub
Private Sub AssignAndValidate(ByVal firstName As String, ByVal lastName As String)
_firstName = firstName.ToUpper()
_lastName = lastName.ToUpper()
Validate()
End Sub
End Class
One curious not to mention: online code converters (vb.net to C#) have no issues converting chained constructor calls NOT on the first line. The C# code comes back as this.#ctor(dt.Rows(0)("FirstName").ToString(), dt.Rows(0)("LastName").ToString()); However, If I try to convert back to VB.NET, it fails.
What you are looking is a factory method
Public Class Foo
Public Shared Function GetFooFromGuid(ByVal userGUID As Guid) As Foo
' Query db
return New Foo(dt.Rows(0)("FirstName").ToString(), dt.Rows(0)("LastName").ToString())
End Function
End Class
Or an initialization function
Public Class Foo
Public Sub New(ByVal userGUID As Guid)
' query DB to get firstName and lastName
Initialize(dt.Rows(0)("FirstName").ToString(), dt.Rows(0)("LastName").ToString())
End Sub
Public Sub New(ByVal firstName As String, ByVal lastName As String)
Initialize(firstName, lastName)
End Sub
Private Sub Initialize(ByVal firstName As String, ByVal lastName As String)
End Sub
End Class
Personally, I wouldn't call the database inside a New.
What I don't like is the fact that you accessing DB on constructor, and also that you validate in constructor. I see this as design issue. Below there are 3 examples of overloaded constructors. All three work. You may need #3. Init your dt in a static (vb - shared) method. You can also substitute your fname/lname parameters with one parameter that contains both. And that will work with #3 for you
public class A
{
public A() : this ("xxx")
{
}
public A(string x)
{
}
}
public class A
{
public A()
{
}
public A(string x): this ()
{
}
}
public class A
{
public A() : this(GetXxx())
{
}
public A(string x)
{
}
private static string GetXxx()
{
return "xxx";
}
}
Why constructor chaining? Because your object can have default values in many properties and you may have many constructors, each adding a property. Internally, one constructor may set 5 properties and other 4 constructors set only 1 property.
For example:
public class Door
{
private string _material = "wood";
private int _locks = 1;
private int _hinges = 3;
public Door()
{
}
public Door(int locks) : this()
{
_locks = locks;
}
public Door(int locks, int hinges) : this(locks)
{
_hinges = hinges;
}
}

Log4net - how to log calling method name in WCF

I have written a Singleton wrapper for log4net and it will be called across all layers of my WCF service. I am unable to log calling method name. Pls suggest if any inputs from my below code. Thanks.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Imports log4net
Imports System.Reflection
Public Class Logger
Implements ILog
Private Shared m_instance As Logger
Private Shared log As log4net.ILog = Nothing
Public Sub New()
If log Is Nothing Then
'log = log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType) ' -> This always logs 'Logger'
log = log4net.LogManager.GetLogger(Assembly.GetCallingAssembly(), "MyLogger") ' -> This always logs 'MyLogger'
End If
End Sub
Public Shared ReadOnly Property Write() As Logger
Get
m_instance = New Logger()
Return m_instance
End Get
End Property
Public Sub Debug(msg As String) Implements ILog.Debug
log.Debug(msg)
End Sub
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Public Interface ILog
Sub Debug(msg As String)
End Interface
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
I have made it more simple as below to log method name. But I have to pass classname as parameter. Pls suggest is this acceptable design?
''''' Calling code ''''''''''''''''''''
Logger(of MyClassName).Debug("message")
''''''Log4net wrapper'''''''''''''''''''''
Imports log4net
Imports System.Reflection
Public NotInheritable Class Logger(Of T)
Private Shared log As log4net.ILog = Nothing
Private Shared ReadOnly Property LogProvider() As log4net.ILog
Get
If (log Is Nothing) Then
Dim CallingMethod As String = GetType(T).ToString() & "." & (New StackTrace()).GetFrame(2).GetMethod().Name
log = log4net.LogManager.GetLogger(CallingMethod)
End If
Return log
End Get
End Property
Public Shared Sub Debug(msg As String)
LogProvider.Debug(msg)
End Sub
A logging instance can only have one name, you can't change it. If you want to use a diffrent name in each method you need to make a logger for ech method.
log = log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType) ' -> This always logs 'Logger'
log = log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().Name) ' -> This gives you the name of the current method
log = log4net.LogManager.GetLogger(Assembly.GetCallingAssembly(), "MyLogger") ' -> This always logs 'MyLogger'
Creating the logger inside your method like:
ILog log = log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().Name) ' -> This gives you the name of the current method
You will have a logger with the name of the method, I would add the name of the class either:
ILog log = log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType + "." + MethodBase.GetCurrentMethod().Name) ' -> This gives you the name of the current method

unloading a DLL until it's needed

I'm having a hard time wrapping my head around some of the answers I've been reading here about unloading a plugin DLL using AppDomains. Here's my architecture:
In my solution, I have a SharedObjects project containing a ModuleBase class that all plugins (separate projects within the solution) inherit. In the SharedObjects project I also have an interface that all plugins implement (so if I have six plugins, they all implement the same interface and therefore the main program using these plugins doesn't need to know or even care what the name of the plugin's class was when it was compiled; they all implement the same interface and therefore expose the same information). Each plugin project has a project reference to the SharedObjects project. (As a side note, may be important, may not be - that SharedObjects project has a project reference to another solution, CompanyObjects containing a number of commonly-used classes, types, objects, etc.) When it's all said and done, when any given plugin compiles, the output directory contains the following DLLs:
The compiled DLL of the plugin itself
The DLL from the SharedObjects project
The DLL from the CompanyObjects project
Four prerequisite 3rd-party DLLs referenced in the CompanyObjects project
My main program creates a reference to the class where I'm doing all my plugin-related work (that class, PluginHelpers, is stored in the SharedObjects project). The program supplies an OpenFileDialog so that the user can choose a DLL file. Now, as it's running right now, I can move just the plugin DLLs to a separate folder and load them using the Assembly.LoadFrom(PathToDLL) statement. They load without error; I check to make sure they're implementing the interface in the SharedObjects project, gather some basic information, and initialize some background work in the plugin DLL itself so that the interface has something to expose. Problem is, I can't upgrade those DLLs without quitting the main program first because as soon as I use LoadFrom those DLLs are locked.
From this MSDN site I found a solution to the problem of locked DLLs. But I'm getting the same "File or dependency not found" error as the OP using the code that worked for the OP. I even get the error when I open the DLL from the release folder which includes the rest of those DLLs.
The FusionLog is even more confusing: there's no mention of the path I was trying to open; it's trying to look in the directory where I'm debugging the main program from, which is a completely different project on a completely different path than the plugins, and the file it's looking for is the name of the DLL but in the folder where the program is running. At this point I have no idea why it's disregarding the path I gave it and looking for the DLL in a completely different folder.
For reference, here's my Loader class and the code I'm using to (try to) load the DLLs:
Private Class Loader
Inherits MarshalByRefObject
Private _assembly As [Assembly]
Public ReadOnly Property TheAssembly As [Assembly]
Get
Return _assembly
End Get
End Property
Public Overrides Function InitializeLifetimeService() As Object
Return Nothing
End Function
Public Sub LoadAssembly(ByVal path As String)
_assembly = Assembly.Load(AssemblyName.GetAssemblyName(path))
End Sub
Public Function GetAssembly(ByVal path As String) As Assembly
Return Assembly.Load(AssemblyName.GetAssemblyName(path)) 'this doesn't throw an error
End Function
End Class
Public Sub Add2(ByVal PathToDll As String)
Dim ad As AppDomain = AppDomain.CreateDomain("TempPluginDomain")
Dim l As Loader = ad.CreateInstanceAndUnwrap(
GetType(Loader).Assembly.FullName,
GetType(Loader).FullName
)
Dim theDll As Assembly = l.GetAssembly(PathToDll) 'error happens here
'there's another way to do it that makes the exact point of the error clear:
'Dim theDll As Assembly = Nothing
'l.LoadAssembly(PathToDll) 'No problems here. The _assembly variable is successfully set
'theDll = l.TheAssembly 'Here's where the error occurs, as soon as you try to read that _assembly variable.
AppDomain.Unload(ad)
End Sub
Can anyone point me in the right direction so I can load and unload DLLs only as-needed and without any dependency errors?
I think I finally got it. It ended up being a few things - I needed the shared DLLs all in one place, and as Hans mentioned above, I needed my appdomains squared away. My solution architecture looks like this: a folder with all my plugin projects; a "Shared Objects" assembly with one class file for the base plugin architecture, and a second class containing my "plugin wrapper" class and supporting classes; and a console app that ties everything together. Each plugin project has a project reference to the shared objects project, as does the console app. Nothing references the plugins directly.
So in my Shared Objects project, I have the code for my PluginBase class and my IPlugin interface:
Public Interface IPlugin
ReadOnly Property Result As Integer
Sub Calculate(ByVal param1 As Integer, ByVal param2 As Integer)
End Interface
Public MustInherit Class PluginBase
Inherits MarshalByRefObject
'None of this is necessary for the example to work, but I know I'll need to use an inherited base class later on so I threw it into the example now.
Protected ReadOnly Property PluginName As String
Get
Return CustomAttributes("AssemblyPluginNameAttribute")
End Get
End Property
Protected ReadOnly Property PluginGUID As String
Get
Return CustomAttributes("AssemblyPluginGUIDAttribute")
End Get
End Property
Protected IsInitialized As Boolean = False
Protected CustomAttributes As Dictionary(Of String, String)
Protected Sub Initialize()
CustomAttributes = New Dictionary(Of String, String)
Dim attribs = Me.GetType.Assembly.GetCustomAttributesData
For Each attrib In attribs
Dim name As String = attrib.Constructor.DeclaringType.Name
Dim value As String
If attrib.ConstructorArguments.Count = 0 Then
value = ""
Else
value = attrib.ConstructorArguments(0).ToString.Replace("""", "")
End If
CustomAttributes.Add(name, value)
Next
IsInitialized = True
End Sub
End Class
<AttributeUsage(AttributeTargets.Assembly)>
Public Class AssemblyPluginNameAttribute
Inherits System.Attribute
Private _name As String
Public Sub New(ByVal value As String)
_name = value
End Sub
Public Overridable ReadOnly Property PluginName As String
Get
Return _name
End Get
End Property
End Class
<AttributeUsage(AttributeTargets.Assembly)>
Public Class AssemblyPluginGUIDAttribute
Inherits System.Attribute
Private _g As String
Public Sub New(ByVal value As String)
_g = value
End Sub
Public Overridable ReadOnly Property PluginGUID As String
Get
Return _g
End Get
End Property
End Class
And I have my PluginWrapper class with its supporting classes:
Imports System.IO
Imports System.Reflection
''' <summary>
''' The wrapper for plugin-related activities.
''' </summary>
''' <remarks>Each wrapper contains: the plugin; code to load and unload it from memory; and the publicly-exposed name and GUID of the plugin.</remarks>
Public Class PluginWrapper
Private _pluginAppDomain As AppDomain = Nothing
Private _isActive As Boolean = False
Private _plugin As IPlugin = Nothing
Private _pluginInfo As PluginInfo = Nothing
Private _pluginPath As String = ""
Public ReadOnly Property IsActive As Boolean
Get
Return _isActive
End Get
End Property
Public ReadOnly Property PluginInterface As IPlugin
Get
Return _plugin
End Get
End Property
Public ReadOnly Property PluginGUID As String
Get
Return _pluginInfo.PluginGUID
End Get
End Property
Public ReadOnly Property PluginName As String
Get
Return _pluginInfo.PluginName
End Get
End Property
Public Sub New(ByVal PathToPlugin As String)
_pluginPath = PathToPlugin
End Sub
Public Sub Load()
Dim l As New PluginLoader(_pluginPath)
_pluginInfo = l.LoadPlugin()
Dim setup As AppDomainSetup = New AppDomainSetup With {.ApplicationBase = System.IO.Directory.GetParent(_pluginPath).FullName}
_pluginAppDomain = AppDomain.CreateDomain(_pluginInfo.PluginName, Nothing, setup)
_plugin = _pluginAppDomain.CreateInstanceAndUnwrap(_pluginInfo.AssemblyName, _pluginInfo.TypeName)
_isActive = True
End Sub
Public Sub Unload()
If _isActive Then
AppDomain.Unload(_pluginAppDomain)
_plugin = Nothing
_pluginAppDomain = Nothing
_isActive = False
End If
End Sub
End Class
<Serializable()>
Public NotInheritable Class PluginInfo
Private _assemblyname As String
Public ReadOnly Property AssemblyName
Get
Return _assemblyname
End Get
End Property
Private _typename As String
Public ReadOnly Property TypeName
Get
Return _typename
End Get
End Property
Private _pluginname As String
Public ReadOnly Property PluginName As String
Get
Return _pluginname
End Get
End Property
Private _pluginguid As String
Public ReadOnly Property PluginGUID As String
Get
Return _pluginguid
End Get
End Property
Public Sub New(ByVal AssemblyName As String, ByVal TypeName As String, ByVal PluginName As String, ByVal PluginGUID As String)
_assemblyname = AssemblyName
_typename = TypeName
_pluginname = PluginName
_pluginguid = PluginGUID
End Sub
End Class
Public NotInheritable Class PluginLoader
Inherits MarshalByRefObject
Private _pluginBaseType As Type = Nothing
Private _pathToPlugin As String = ""
Public Sub New()
End Sub
Public Sub New(ByVal PathToPlugin As String)
_pathToPlugin = PathToPlugin
Dim ioAssemblyFile As String = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(_pathToPlugin), GetType(PluginBase).Assembly.GetName.Name) & ".dll")
Dim ioAssembly As Assembly = Assembly.LoadFrom(ioAssemblyFile)
_pluginBaseType = ioAssembly.GetType(GetType(PluginBase).FullName)
End Sub
Public Function LoadPlugin() As PluginInfo
Dim domain As AppDomain = Nothing
Try
domain = AppDomain.CreateDomain("Discovery")
Dim loader As PluginLoader = domain.CreateInstanceAndUnwrap(GetType(PluginLoader).Assembly.FullName, GetType(PluginLoader).FullName)
Return loader.Load(_pathToPlugin)
Finally
If Not IsNothing(domain) Then
AppDomain.Unload(domain)
End If
End Try
End Function
Private Function Load(ByVal PathToPlugin As String) As PluginInfo
Dim r As PluginInfo = Nothing
Try
Dim objAssembly As Assembly = Assembly.LoadFrom(PathToPlugin)
For Each objType As Type In objAssembly.GetTypes
If Not ((objType.Attributes And TypeAttributes.Abstract) = TypeAttributes.Abstract) Then
If Not objType.GetInterface("SharedObjects.IPlugin") Is Nothing Then
Dim attribs = objAssembly.GetCustomAttributes(False)
Dim pluginGuid As String = ""
Dim pluginName As String = ""
For Each attrib In attribs
Dim name As String = attrib.GetType.ToString
If name = "SharedObjects.AssemblyPluginGUIDAttribute" Then
pluginGuid = CType(attrib, AssemblyPluginGUIDAttribute).PluginGUID.ToString
ElseIf name = "SharedObjects.AssemblyPluginNameAttribute" Then
pluginName = CType(attrib, AssemblyPluginNameAttribute).PluginName.ToString
End If
If (Not pluginGuid = "") And (Not pluginName = "") Then
Exit For
End If
Next
r = New PluginInfo(objAssembly.FullName, objType.FullName, pluginName, pluginGuid)
End If
End If
Next
Catch f As FileNotFoundException
Throw f
Catch ex As Exception
'ignore non-valid dlls
End Try
Return r
End Function
End Class
Finally, each plugin project looks a little like this:
Imports SharedObjects
<Assembly: AssemblyPluginName("Addition Plugin")>
<Assembly: AssemblyPluginGUID("{4EC46939-BD74-4665-A46A-C99133D8B2D2}")>
Public Class Plugin_Addition
Inherits SharedObjects.PluginBase
Implements SharedObjects.IPlugin
Private _result As Integer
#Region "Implemented"
Public Sub Calculate(ByVal param1 As Integer, ByVal param2 As Integer) Implements SharedObjects.IPlugin.Calculate
If Not IsInitialized Then
MyBase.Initialize()
End If
_result = param1 + param2
End Sub
Public ReadOnly Property Result As Integer Implements SharedObjects.IPlugin.Result
Get
Return _result
End Get
End Property
#End Region
End Class
To set it all up, the main program creates a new instance of the PluginWrapper class, supplies a path to a DLL, and loads it:
Dim additionPlugin As New PluginWrapper("C:\path\to\Plugins\Plugin_Addition.dll")
additionPlugin.Load()
Once you're done doing whatever you need to do with the program...
additionPlugin.PluginInterface.Calculate(3, 2)
...and retrieving the results...
Console.WriteLine("3 + 2 = {0}", additionPlugin.PluginInterface.Result)
...just unload the plugin:
additionPlugin.Unload()
If you need to reload it while the wrapper is still in memory, just call the Load() method again - all the information it needs to create a new AppDomain and reload the assembly is in there. And, in answer to my initial question, once the Unload() method has been called, the assembly is freed and can be replaced/upgraded as necessary, which was the whole point of doing this in the first place.
Part of where I was getting tripped up earlier was that I wasn't including the SharedObjects.dll file in the same folder as the plugins. What I found is that any referenced assembly needs to be present. So in my post-build events for both my plugins and the Shared Objects project, I have this: xcopy /y $(ProjectDir)$(OutDir)$(TargetFileName) c:\path\to\Plugins. Every time I build the solution, all the DLLs are placed in the folder where they need to be.
Sorry if this is a little long, but this is a little complicated. Maybe there's a shorter way to do it...but at the moment, this gets me everything I need.