In my job I tend to write a lot of Windows services. Due to this I've pared it down to bare essentials so that the code is as simple as possible, but still fully functional for the needs of each service. What this generally means is I don't use the service project in Visual Studio, instead favoring template code. After learning how services work, I found that the project was unnecessarily complex.
Barring an installer, the basic project would have only two files:
- Program.cs: Setup and startup
- Service.cs: The actual service
I usually separate the service from a service worker that does actual work, but that's application specific.
Program.cs sets up logging, any custom application settings, and runs the service normally. However, over the years I've found that a debug mode for my services is invaluable in both development and after deployment. If the executable is run interactively, it runs in a console and displays logging there as well. Here's a sample template for Program.cs:
public class Program
{
/// <summary>
/// Gets or sets the application's data folder.
/// </summary>
public static string ApplicationFolder { get; set; }
/// <summary>
/// Gets or sets the location of the file log.
/// </summary>
public static string LogFolder { get; set; }
/// <summary>
/// Gets or sets the log manager for the application.
/// </summary>
public static LogManager Log { get; private set; }
/// <summary>
/// The main entry point for the application.
/// </summary>
public static void Main(string[] args)
{
var assemblyName = Assembly.GetExecutingAssembly().GetName();
var version = assemblyName.Version;
ApplicationFolder = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
"MyApplication");
LogFolder = Path.Combine(
ApplicationFolder,
string.Format("{0}.{1}.{2}", version.Major, version.Minor, version.Build));
SetupLog();
if (Environment.UserInteractive)
{
StartConsole(args);
}
else
{
StartService(args);
}
}
/// <summary>
/// Runs the process as an installed service.
/// </summary>
private static void StartService(string[] args)
{
ServiceBase.Run(new ServiceBase[] { new Service() });
}
/// <summary>
/// Runs the process in a console window.
/// </summary>
private static void StartConsole(string[] args)
{
try
{
Console.Title = Assembly.GetExecutingAssembly().GetName().Name + " [Debug Mode]";
// Run the service worker
Console.Read();
// Stop and dispose the service worker
}
catch (Exception ex)
{
Log.Write(TraceEventType.Error, 0, ex.Message, ex);
Console.Read();
}
}
/// <summary>
/// Initializes the log manager.
/// </summary>
private static void SetupLog()
{
Log = new LogManager(Assembly.GetExecutingAssembly().GetName().Name);
Log.AddSwitch(Assembly.GetExecutingAssembly().GetName().Name, SourceLevels.All);
if (Environment.UserInteractive)
{
Log.AddListener(new ConsoleTraceListener(false));
}
var listener = new FileLogTraceListener("LogToFile");
listener.AutoFlush = true;
listener.Append = true;
listener.IndentSize = 4;
listener.Location = LogFileLocation.Custom;
listener.CustomLocation = LogFolder;
listener.LogFileCreationSchedule = LogFileCreationScheduleOption.Daily;
Log.AddListener(listener);
}
}
Since that won't compile without the LogManager class, here's a simple one, just for completeness:
/// <summary>
/// Manages a TraceSource and configured attributes for logging.
/// </summary>
public class LogManager
{
/// <summary>
/// Gets the managed trace source.
/// </summary>
public TraceSource Source { get; private set; }
/// <summary>
/// Creates and initializes a new log manager with the specified name.
/// </summary>
/// <param name="sourceName">The name of the trace source.</param>
public LogManager(string sourceName)
{
Source = new TraceSource(sourceName);
}
/// <summary>
/// Adds an existing listener to the log manager.
/// </summary>
/// <param name="listener">A configured trace listener.</param>
public void AddListener(TraceListener listener)
{
if (!Source.Listeners.Contains(listener))
{
Source.Listeners.Add(listener);
}
}
/// <summary>
/// Adds a new trace switch to the log manager.
/// </summary>
/// <param name="switchName">The name of the switch.</param>
/// <param name="level">The desired verbosity level of the switch.</param>
public void AddSwitch(string switchName, SourceLevels level)
{
Source.Switch = new SourceSwitch(switchName);
Source.Switch.Level = level;
}
/// <summary>
/// Writes an entry to the configured trace source.
/// </summary>
/// <param name="traceType">Verbosity level of the entry.</param>
/// <param name="id">A numeric identifier for the entry.</param>
/// <param name="message">The entry message.</param>
/// <param name="exception">An optional exception for error entries.</param>
public void Write(TraceEventType traceType, int id, string message, Exception exception = null)
{
string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
string formattedMessage;
if (exception != null)
{
formattedMessage = string.Format("[{0}] {1}{2}{3}", timestamp, message, Environment.NewLine, exception.ToString());
}
else
{
formattedMessage = string.Format("[{0}] {1}", timestamp, message);
}
Source.TraceEvent(traceType, id, formattedMessage);
}
}
The service code itself in the following code snippet is anticlimactic. Set the project to an output type of Console Application and it will still run as a service, but also as a console app. Woohoo!