The Perfect Service - Part 1
 
Published: 29 Dec 2005
Unedited - Community Contributed
Abstract
In this article, Ambrose illustrates how to use a drag-n-drop/xcopy .NET Windows services manager that can make your life a lot easier if you find yourself needing to implement multiple Windows services in your enterprise. The next article in the series goes into some depth, exploring how the service manager works so that you can take those concepts and apply them in your own development.
by J. Ambrose Little
Feedback
Average Rating: This article has not yet been rated.
Views (Total / Last 10 Days): 18344/ 21

Introduction

Note: This article was originally published in June 2004 on 15Seconds.com, but it has gotten good reviews, and the techniques, I believe, are still valid. I hope the readers of ASPAlliance find it as useful as I have.

[Download Code]

I was hanging out with some local user group types last night when the "Perfect Storm" (the movie based on a severe storm in 1991) came up. Since I was just talking about this very flexible Windows service I had written, it suddenly came to me to name the article about it "The Perfect Service." Of course, this is not to say that it is indeed the perfect service, but all the same, I thought it sounded really good at the time (or maybe that was the Scotch...).

In any case, this article is the first in a two part series that covers first how to use the "perfect service" and then explores the details of how it works. The first part covers the details of how to get the service running and then how to add plug-n-play services to it. The second part will explore the details, which includes the use of advanced topics such as loading and unloading application domains, using .NET Remoting, and using reflection.

I realize that I am not the first, nor will I likely be the last, to attempt such a feat. In fact, I've referenced two other articles covering more or less the same goal in the resources section. However, I believe that this particular implementation is superior in its ease of use and flexibility.

First off, I need to discuss terminology. This is necessary because for some reason the English language does not offer too many alternatives to the word "service" in the way that we mean it when we talk about Windows services, Web services, services in service-oriented architecture, and the like. I think the best alternative that isn't overloaded in the IT world is "ministration," and while I'm all for complicated-sounding words, I think I'll just stick with the industry term to describe the concept I am referring to and use a modifier to clarify when necessary.

The new concept I'm introducing is related to the title of my Windows service, which is ".NET Service Manager." This Windows service essentially provides you with drag and drop or XCOPY deployment of what would normally have to be installed as individual Windows services in themselves. Basically, any long-running or timed process that you want to be run even when a user is not logged in qualifies as a candidate for a Windows service.

Unfortunately, Windows services, even in .NET, are not the easiest thing to implement, and you have to manually install them and start and stop them using the Services MMC snap-in. This basically involves a fair amount of drudgery and, if you have many processes like this, can make your services list become unfriendly.

Enter the .NET Service Manager. This application enables you write, install, and start (using the MMC snap-in) the Windows service just once. At that point, you are ready to do some real work, using the same (or better) XCOPY story that, for instance, ASP.NET has. By simply applying an assembly attribute and implementing a simple interface, you can enable any .NET code library (DLL) assembly to run under the .NET Service Manager. I've decided to call such assemblies "managed services," which simply means they act like a Windows service but are managed by the .NET Service Manager.

For the most part, in this series, when I refer to a "service," I will qualify it as either a Windows service (formerly known as "NT service") or managed service, which indicates the kind of service described in the previous paragraph. This article will cover the details of implementing a managed service, so you can take away from it the ability to use the .NET Service Manager and all the conveniences it provides without having to know how it works. The next part will cover how all of this is made possible.

Not only does the .NET Service Manager enable you to install and start a service by simply copying it to the process directory, it also enables you to update it on the fly (using the same deployment mechanism) without needing to stop or start the service manager itself. Doing this will not affect the other managed services because they are all running in their own application domains, and it is not necessary to stop the service manager itself. Similarly, you can stop an individual managed service by removing (deleting or moving) its DLL from the service process directory-so uninstalling is as easy as installing.

Further, you may be aware that .NET does not support config files for code libraries. There are some good reasons behind this, but I have found numerous occasions where having this capability would be quite handy. Managed services are one of them, so I've enabled an easy-to-use interface for storing settings in an app.config-like file (e.g., SampleService.dll.config) that follows the same conventions as a typical config file with app settings. We will also cover an example of implementing this technology.

There are only two steps required to enable your code to run as a managed service:

  1. Implement the ServiceBroker.IService interface on one of your types.
  2. Apply the ServiceBroker.ServiceEntryPointAttribute to your assembly, using it to specify a display name for the service and the type that serves as the service entry point-this must be a type that implements the interface from Step 1.

There is an optional third step that you can use to take advantage of the config file capabilities, but you only need this if your managed service requires any configurable settings. All three of these will be covered in this article.

You will, of course, also need to have a reference to the ServiceBroker.dll assembly. It is installed with the .NET Service Manager, so you can just reference it in the installation directory (e.g., C:\Program Files\Littlechip\.NET Service Manager\) or copy it to some other common assembly reference directory as demonstrated in the article "Where's My Assembly?" and reference it there.

Implementing ServiceBroker.IService

ServiceBroker is an assembly that comes with the .NET Service Manager. It provides the engine for the service manager as well as the types needed for the interaction between the service manager and your managed services. The IService interface is detailed in Listing 1.

Listing 1 IService.cs

using System;

 

namespace ServiceBroker

{

/// <summary>

/// Interface that all services using the

/// ServiceBroker must implement on the type specified

/// by the <see cref="ServiceEntryPointAttribute.TypeName"/>.

/// </summary>

public interface IService

{

/// <summary>

/// Starts the service functionality.

/// </summary>

void StartService();

/// <summary>

/// Stops the service functionality.

/// </summary>

void StopService();

}

}

As you can see, the managed service interface is very simple -- it requires only a start and a stop method. This, of course, corresponds to the typical operations you have available from the Services MMC snap-in. Note if your Windows service is "restarted," it will simply call the StopService method and the StartService method, so this is really all we need.

For the purposes of this example, I have implemented a very simple managed service that only serves to illustrate the implementation. It is called SampleService, and the type in that assembly that implements IService is listed next.

Listing 2 Test.cs

using System;

 

namespace SampleService

{

/// <summary>

/// Summary description for Test.

/// </summary>

public class Test : ServiceBroker.IService

{

#region IService Members

 

public void StartService()

{

ServiceBroker.Logger.WriteToLog(AppSettings["StartText"],

System.Diagnostics.EventLogEntryType.Information);

}

 

public void StopService()

{

ServiceBroker.Logger.WriteToLog(AppSettings["StopText"],

System.Diagnostics.EventLogEntryType.Information);

}

 

#endregion

}

}

You will note the reference to an AppSettings member-this has been omitted in this listing for simplicity's sake but will be covered later. What's important here is that we've simply specified that we're implementing the IService interface and then have provided some very basic functionality to be executed in each method-writing a message to the event log.

Of course, if you were implementing a service, it would likely involve the use of one or more System.Threading.Timer instances to schedule some activity to occur on a regular basis, or maybe you'd implement a System.IO.FileSystemWatcher to watch for file system activity. I don't need to tell you what you'd do because if you're reading this, you probably already have a specific need for Windows service functionality and can easily infer from the example how you can use this to your advantage.

Applying the Attribute

Obviously, I could have chosen not to apply an attribute and simply used reflection to look through the types in your assembly for the first one that implemented the ServiceBroker.IService interface. Apart from potentially taking a long time to locate such a type, I decided to go with a custom attribute to also enable you to specify a friendly display name for your managed service. This is used to log items pertaining to that service.

The good news is that it is very simple to apply a custom attribute to your assembly. If you are using Visual Studio, an Assembly.cs (or .vb for VB) file is generated with the code library projects by default, and this is where you are expected to place your assembly-level attributes. You can actually put your assembly-level attributes in any code file as long as they are after any using statements and before any type or namespace declarations. In this sample, the Assembly.cs file is used.

Listing 3 Assembly.cs

[assembly: ServiceBroker.ServiceEntryPoint("SampleService", "SampleService.Test")]

[assembly: System.Reflection.AssemblyVersion("1.0.*")]

 

The typical Assembly.cs file is generated with a ton of comments and a handful of standard assembly-level attributes that address title, description, company, copyright, etc. I've removed everything, including the using (Imports in VB) statements, except for the AssemblyVersionAttribute because I think if you don't set anything else, you should at least set your assembly version.

I also, as I'm sure you've noticed, applied my custom attribute from the ServiceBroker assembly. The constructor for that attribute takes two arguments-the display name of your managed service, and the full type name of the type that you want the service manager to use to start and stop your service. Clearly, the type specified here should implement the IService interface, such as the Test type from Listing 2; hence, we are specifying that the SampleService.Test type be used for our purposes.

If you don't need any configurable applications settings, at this point, you're done. All you'd need to do is compile this and drop it into the .NET Service Manager's process directory (i.e., where the .exe is located), and if it is running, it will automatically load up your assembly, find and instantiate the service entry point type, and call that type's implementation of the IService.StartService method.

If you needed to update, you'd simply make your changes, compile, and drop the new version out in that same directory. If the service manager is running, it will detect the new version, stop and unload the old version, and load and start the new version. Similarly, you can get it to stop and unload by just moving or deleting the DLL file from the same directory.

Extending Flexibility with DLL Config Files

Thanks to the Config class in the ServiceBroker, adding a .config file to your managed service is easy. Unfortunately, Visual Studio is not set up to handle generating the assemblyName.dll.config file using the app.config file like it does for Windows forms and services applications, so there is a bit of extra work to copy the config file around with your assembly. This just means you'll want to copy out your config file before or with your assembly, so it will be there when the assembly is loaded by the service manager.

In any case, the format of the config file is just like your standard app.config file, except that the only things recognized by the ServiceBroker config handler are app settings. You can see the config file being used with the SampleService assembly in the next listing.

Listing 4 SampleService.dll.config

<?xml version="1.0" encoding="utf-8" ?>

<configuration>

<appSettings>

<add key="StartText"

value="This is where you would implement logic to start your service." />

<add key="StopText"

value="This is where you would implement logic to stop your service." />

</appSettings>

</configuration>

To use the config file, we just need to add a few lines of code to our test class (the ones I excluded in the first listing); these are now visible with the entire Test class in Listing 5.

Listing 5 The Complete Test.cs

using System;

 

namespace SampleService

{

/// <summary>

/// Summary description for Test.

/// </summary>

public class Test : ServiceBroker.IService

{

internal static ServiceBroker.Config AppSettings =

new ServiceBroker.Config();

 

#region IService Members

 

public void StartService()

{

ServiceBroker.Logger.WriteToLog(AppSettings["StartText"],

System.Diagnostics.EventLogEntryType.Information);

}

 

public void StopService()

{

ServiceBroker.Logger.WriteToLog(AppSettings["StopText"],

System.Diagnostics.EventLogEntryType.Information);

}

 

#endregion

}

}

So all you add is one line to declare a ServiceBroker.Config static (Shared in VB) identifier. You have declared it as internal (Friend in VB) because you only want it to be visible to types within this assembly. Now you can begin accessing your app settings in code by using the Test.AppSettings field, which has a string indexer, much like System.Configuration.ConfigurationSettings.AppSettings does, so the usage is very similar to standard app settings access. You could in fact get even closer by creating a ConfigurationSettings class and putting the AppSettings member there, but then you might be too close for comfort because it could easily conflict with the same class in the System.Configuration namespace, i.e., if you had a using (Imports in VB) statement for that namespace.

And again, that's all there is to it. You now have a XCOPY-deployable, hot-updatable, easily-configurable Windows service. I should also note that unlike other app.config files I could mention, ServiceBroker will monitor the config file for changes and update the cached settings (they are loaded into memory for faster access throughout the application) when the file is updated, and it will do so without restarting the managed service.

Please note that the installer installs the .NET Service Manager to run under the Local System account. I would recommend that you consider changing this to a less-privileged account if that will work for you. This is also a limitation of the .NET Service Manager--all of the managed services will run under the permissions of the Windows service identity. If you need a more fine-grained control, you will still need to develop separate Windows services.

All you need to do to get started creating and deploying your managed services is to download the installer, run it, start the .NET Service Manager Windows service, and you'll be on your way. The next article in this series will cover just how the service manager makes all of this possible.

Related Resources

Creating an Extensible Windows Service - by Rob Chartier
Build a Plug 'n' Play XML Windows Service - by Dan Wahlin
Adding and Removing References - MSDN Library
Implementing Existing Interfaces - MSDN Library
Applying Attributes - MSDN Library
<appSettings> Element - MSDN Library

 



User Comments

Title: re: Could not load type 'SampleService.Test'   
Name: tom
Date: 2009-04-10 6:49:04 PM
Comment:
I was getting the same error as the other 2 posters and
found a fix that 'works on my machine':

SampleService project - AssemblyInfo.cs

the fix:
[assembly: ServiceBroker.ServiceEntryPoint("SampleService", typeof(SampleService.Test))]

changed:
"SampleService.Test"
to:
typeof(SampleService.Test)

and it worked.
Title: Mr   
Name: Andre
Date: 2009-04-03 7:39:47 AM
Comment:
Tried exactly the same with the same tools and got the Same Error, but with the following stack trace:
StackTrace Information
*********************************************

Server stack trace:
at System.RuntimeTypeHandle._GetTypeByName(String name, Boolean throwOnError, Boolean ignoreCase, Boolean reflectionOnly, StackCrawlMark& stackMark, Boolean loadTypeFromPartialName)
at System.RuntimeTypeHandle.GetTypeByName(String name, Boolean throwOnError, Boolean ignoreCase, Boolean reflectionOnly, StackCrawlMark& stackMark)
at System.RuntimeType.PrivateGetType(String typeName, Boolean throwOnError, Boolean ignoreCase, Boolean reflectionOnly, StackCrawlMark& stackMark)
at System.Type.GetType(String typeName, Boolean throwOnError)
at ServiceBroker.ServiceEntryPointAttribute..ctor(String serviceName, String entryPointTypeName)
at System.Reflection.CustomAttribute._CreateCaObject(Void* pModule, Void* pCtor, Byte** ppBlob, Byte* pEndBlob, Int32* pcNamedArgs)
at System.Reflection.CustomAttribute.CreateCaObject(Module module, RuntimeMethodHandle ctor, IntPtr& blob, IntPtr blobEnd, Int32& namedArgs)
at System.Reflection.CustomAttribute.GetCustomAttributes(Module decoratedModule, Int32 decoratedMetadataToken, Int32 pcaCount, RuntimeType attributeFilterType, Boolean mustBeInheritable, IList derivedAttributes)
at System.Reflection.CustomAttribute.GetCustomAttributes(Assembly assembly, RuntimeType caType)
at System.Reflection.Assembly.GetCustomAttributes(Type attributeType, Boolean inherit)
at ServiceBroker.RemoteServiceHandler.LoadService(String assemblyName)
at System.Runtime.Remoting.Messaging.StackBuilderSink._PrivateProcessMessage(IntPtr md, Object[] args, Object server, Int32 methodPtr, Boolean fExecuteInContext, Object[]& outArgs)
at System.Runtime.Remoting.Messaging.StackBuilderSink.PrivateProcessMessage(RuntimeMethodHandle md, Object[] args, Object server, Int32 methodPtr, Boolean fExecuteInContext, Object[]& outArgs)
Title: Could not load type 'SampleService.Test'   
Name: Cameron
Date: 2008-11-10 11:03:29 PM
Comment:
Hi.

I followed the article and compiled the source code with no errors, installed the .net Service Manager successfully.
My projects are pointing to framework 2.0 and are compiled using C# 2008.

When I drop in the SampleService.dll and SampleService.dll.config where the service is running the Application event log raises the following (plus stack trace not included).

Exception Type: System.TypeLoadException
Message: Could not load type 'SampleService.Test' from assembly 'ServiceBroker, Version=1.0.604.0, Culture=neutral, PublicKeyToken=null'.

Any ideas?






Community Advice: ASP | SQL | XML | Regular Expressions | Windows


©Copyright 1998-2024 ASPAlliance.com  |  Page Processed at 2024-04-19 12:22:00 AM  AspAlliance Recent Articles RSS Feed
About ASPAlliance | Newsgroups | Advertise | Authors | Email Lists | Feedback | Link To Us | Privacy | Search