VSTO (Visual Studio Tools for Office) is a great way to put together addins for the various MS Office applications (especially Word, Excel, and Outlook).
And Log4Net is a fantastic and unbelievably flexible logging framework for .net applications.
So naturally, I wanted to use them together.
And doing so is not bad at all, till it came to configuration…
A Disclaimer (with a note of Encouragement)
Log4Net is not the most straightforward package out there. It’s extremely flexible, and very easy to work with once you’ve gotten used to it, but getting into the Tao of the thing took a few days, for me, anyway.
Don’t let that discourage you. It really is a spectacular framework for handling virtually any aspect of logging in your applications. And it really doesn’t take much “setup code” at all to get it operational.
The Super Highway
The easiest way to configure log4net in a .net application (VSTO addins included) is to simply call Configure on the XMLConfigurator object:
That’ll work, but unfortunately, since your VSTO addin is a DLL, log4net will, by default, look in the current app.config file, which, if you’re running in Word, for instance, will be WinWord.exe.config in the folder where WinWord.exe lives.
Since WinWord.exe.config is Word’s config file, it’s probably not the best idea in the world to go shoe-horning your own (or log4net’s) config stuff in there as well. Not to mention how do you get your config information easily into that file during installation (or properly remove it during an uninstall).
The Scenic Byway
What you really want is for your VSTO addin DLL to have it’s own config file. Something that lives in the same folder as your DLL itself, and can easily be installed and removed.
Sure enough, that Configure method has an overload that accepts a FileInfo structure for an arbitrary XML Config file. So you can just do this:
Dim MyConfigFile = Me.GetType.Assembly.ManifestModule.Name & ".config" If My.Computer.FileSystem.FileExists(MyConfigFile) Then Dim fi = My.Computer.FileSystem.GetFileInfo(MyConfigFile) log4net.Config.XmlConfigurator.Configure(fi) End If
What this does effectively is construct a filename based on the name of whatever assembly the current code is defined within, but with a “.config” extension.
It then checks for the existence of that file. Since there’s no path on the file, it only looks in the current directory, but that’s fine, since the config file will always be located in the same folder as it’s DLL.
And finally, if the file is found, it retrieves a FILEINFO object for it and configures Log4Net with that file.
Bumps along the Road
Unfortunately, that will get you farther, but not by much.
There are two problems.
- In debug mode in the IDE, the “current directory” is, indeed, the same folder as the one with your addin DLL in it. But, when running in release mode, in production, with Visual Studio completely out of the picture, the current directory is very likely the folder where WinWord.exe is located. Not good.
- Worse, in production, VSTO addins are generally copied to a “shadow cache” folder by the .net framework, so that the original files can be upgraded in place easily while the application is in use.
Unfortunately, your config will will not get copied to the shadow cache.
This means that for determining where to look for your config file, using something like:
- Me.GetType.Assembly.CodeBase or
won’t work, because often times, they’ll point you off into the wilds of the assembly cache folder and not the \Program Files\MyCompany\MyProduct\ folder where you installed your addin and where you most likely would prefer your MyProduct.dll.config file to live.
In the end, I found the best solution to be:
- Look in the “current directory” for your config file.
- If you find it, use it from there. This accommodated easy debugging while in the IDE because you can easily get to and edit your config file.
- If you don’t find it, construct the path to the app’s \Program Files\ folder and check there.
- If it’s not there either, just fall back to the default and call XMLConfigurator.Configure() with no parameters and let it default everything.
A routine that puts all that together looks like this:
Private Sub ConfigureLog4Net() Dim MyConfig = Me.GetType.Assembly.ManifestModule.Name & ".config" If Not My.Computer.FileSystem.FileExists(MyConfig) Then '---- not in current dir, so check in our Program Files folder Dim pth = Path.Combine(My.Computer.FileSystem.SpecialDirectories.ProgramFiles, My.Application.Info.CompanyName) pth = Path.Combinepth, My.Application.Info.ProductName) MyConfig = System.IO.Path.Combine(pth, MyConfig) End If If My.Computer.FileSystem.FileExists(MyConfig) Then Dim fi = My.Computer.FileSystem.GetFileInfo(MyConfig) log4net.Config.XmlConfigurator.Configure(fi) Else log4net.Config.XmlConfigurator.Configure() End If End Sub
To use this, you’ll need to make sure that your installation package installs your addin DLL and it’s config file into the \Program Files\Company Name\Product Name folder.
This is the pretty typical case, though, so that shouldn’t be a worry.
Later on Down the Road
This obviously begs the next question. What about other application settings? Log4Net reads stuff out of an arbtrary config file you specify, but the My.Settings object does not. It’ll still end up looking in the default place, which is the host application’s config file (again, WinWord.exe.config, or Excel.exe.config, etc).
I hope to cover a decent solution to that in a later post.