Windows Services Simplified

deceptikon 4 Tallied Votes 408 Views Share

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!

Ketsuekiame commented: Good service foundation template +11
ddanbe commented: Nice! +15
/// <summary>
/// Service controller for the process.
/// </summary>
public class Service : ServiceBase
{
    /// <summary>
    /// Creates and initializes a new service.
    /// </summary>
    public Service()
    {
        CanShutdown = true;
        CanStop = true;
        CanHandlePowerEvent = false;
        CanHandleSessionChangeEvent = false;
        CanPauseAndContinue = false;
    }

    #region Public Interface
    /// <summary>
    /// Starts the service.
    /// </summary>
    public void StartService(string[] args) { OnStart(args); }

    /// <summary>
    /// Stops the service.
    /// </summary>
    public void StopService() { OnStop(); }

    /// <summary>
    /// Shuts down the service.
    /// </summary>
    public void ShutdownService() { OnShutdown(); }
    #endregion

    #region Service Control Interface
    protected override void OnStart(string[] args) { SwitchOn(); }
    protected override void OnStop() { SwitchOff(); }
    protected override void OnShutdown() { SwitchOff(); }
    #endregion

    #region Miscellaneous Helpers
    /// <summary>
    /// Initializes and starts the service worker.
    /// </summary>
    private void SwitchOn()
    {
        try
        {
            // Run the service worker
        }
        catch (Exception ex)
        {
            ExitCode = 1;
            Program.Log.Write(TraceEventType.Error, 0, "Error starting service", ex);
            throw;
        }
    }

    /// <summary>
    /// Stops and cleans up the service worker.
    /// </summary>
    private void SwitchOff()
    {
        // Stop and dispose the service worker
    }
    #endregion
}
Ketsuekiame 860 Master Poster Featured Poster

Nice template :)

The only thing I noticed (mine is almost identicial to yours barring interfaces) wihch I would do differently, is that the Service is responsible for the logging configuration. Personally, I would have made a configuration object, or, had the log manager attempt to read the logging configuration from the active configuration file (or allow you to pass a path to a configuration file of your choosing).

This is just my personal preference though. The area of responsibility in this regard is pretty much a grey area.

On re-read I'm guessing you perhaps thought this was outside the scope of the snippet?

deceptikon 1,790 Code Sniper Team Colleague Featured Poster

On re-read I'm guessing you perhaps thought this was outside the scope of the snippet?

Good guess. The code was taken almost verbatim from one of my services, but I had to cut out a few significant yet not entirely relevant pieces such as licensing, configuration settings, and company-specific names. ;)

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.