I can think of a couple ways you could accomplish this.
Tightly Coupled
Use reflections to detect the presence of the DLL. If it is present, load the appropriate classes, and make extra calls to them.
To do this, use Assembly.LoadFile, Assembly.GetType(string), and Activator.CreateInstance(type). Cast the new instance to your abstract base logger type/interface.
This is more or less what you were describing. I don't recommend this, though, because it isn't very flexible, and there are good alternatives.
Loosly Coupled
create an interface or abstract logger class, and use Dependency Injection (Inversion of Control) to inject the logger into the components that need to do logging. If you choose, you can use a dependency injection library to specify which implementations you want in a loosly coupled manner. Configure the DI library to load dependencies from your extra DLL (if present).
Castle.Windsor has a loosly coupled logging interface (the logging facility) that you could look into for the second option.
There is a sort of spectrum between these, as well.
This is the gist of injecting the logger as a dependency (though I'm not using any libraries in this example):
using System;
using System.IO;
public interface ILogger
{
void WriteDebug(string debug);
void WriteInfo(string info);
void WriteError(string error);
}
public class NullLogger : ILogger
{
private static ILogger instance = new NullLogger();
// This singleton pattern is just here for convenience.
// We do this because pattern has you using null loggers constantly.
// If you use dependency injection elsewhere,
// try to avoid the temptation of implementing more singletons :)
public static ILogger Instance
{
get { return instance; }
}
public void WriteDebug(string debug) { }
public void WriteInfo(string info) { }
public void WriteError(string error) { }
}
public class FileLogger : ILogger, IDisposable
{
private StreamWriter fileWriter;
public FileLogger(string filename)
{
this.fileWriter = File.CreateText(filename);
}
public void Dispose()
{
if (fileWriter != null)
fileWriter.Dispose();
}
public void WriteDebug(string debug)
{
fileWriter.WriteLine("Debug - {0}", debug);
}
// WriteInfo, etc
}
public class SomeBusinessLogic
{
private ILogger logger = NullLogger.Instance;
public SomeBusinessLogic()
{
}
public void DoSomething()
{
logger.WriteInfo("some info to put in the log");
}
public ILogger Logger
{
get { return logger; }
set { logger = value; }
}
}
public class Program
{
static void Main(string[] args)
{
// You're free to use a dependency injection library for this,
// or simply check for a DLL via reflections and load a logger from there
using (var logger = new FileLogger("logfile.txt"))
{
var someBusinessLogic = new SomeBusinessLogic()
{
// The component won't know which logger it is using - it just uses it
Logger = logger,
};
someBusinessLogic.DoSomething();
}
}
}