Requirements:
Expose a REST service on Windows that will handle Web requests and will call an external long running client Windows application, will wait for it's response, and will call an external REST service to inform about the fulfillment of the request. The REST service must be able to handle a multiple Web requests by putting them in a queue and executing them one by one. If the maximum number of requests is achieved, the service will return an error response to inform it about the impossibility to handle the request. The service must start automatically with Windows.
Solution:
We will create a WCF Service Library, that will be hosted in a Windows Service. For the purpose of testing the WCF Service, we will also create a Console Application that will host the service to make the debugging easier. To simulate a long running operation, we will create a console application that will have a Thread.Sleep(...) inside.
Pre-requisites:
Visual Studio Professional 2012. If you want to use express edition, you will not be able to create a windows service project(though there is a workaround).
Steps:
Creating the solution. We will create a blank solution
Adding the WCF Service Library project
Visual Studio 2012 will generate a sample Service. We will create our own ones. Please delete Service1.cs and IService1.cs files.
Now we will create the WCF service interface that will be implemented by our class that processes the jobs for us. First of all Add a folder named Interfaces where we will put the interface. Then add the interface, and name it IRestJobRequestProcessor. Open the file and replace the create interface with the following code:
[ServiceContract]public interface IRestJobRequestProcessor{// This is for launching a new job using a get method by specifying// the job name in the Uri[ WebGet (UriTemplate = "QueueWorkWebGet/{pJobName}" )]String QueueWorkWebGet( String pJobName);
// Same as QueueWorkWebGet, but using a POST method[ WebInvoke (Method = "POST" , UriTemplate = "QueueWorkWebInvokePost/{pJobName}" )]String QueueWorkWebInvokePost( String pJobName);
// Returns The string that you provide as jobName[ WebGet (UriTemplate = "SimpleGetRequestResponse/{pJobName}" )]String SimpleGetRequestResponse( String pJobName);
[ WebGet (UriTemplate = "ShutdownJobProcessorThread/{pKeyword}" )]String ShutdownJobProcessorThread( String pKeyword);}
Now add a class with the name RestJobRequestServiceProcessor.
Before you add the code from below to this file, please download log4net and add a reference to it(don't forget about its configuration xml, otherwise you will get information in Visual Studio about it not being able to validate the log4net section in App.config), and also add a reference to System.Configuration.dll. First is necessary for us because we want to log the result of the public Web Service we will get when our service finishes the long running task. The second is required for us in order to be able to get the path to the executable the service will call and wait for result. We will configure this path in the App.config later.
So, here is the code for our processor class:
namespace WcfRESTService{// Start the service and browse to http://<machine_name>:<port>/RestJobRequestServiceProcessor/help to view the service's generated help page// NOTE: By default, a new instance of the service is created for each call; change the InstanceContextMode to Single if you want// a single instance of the service to process all calls.[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)] // Required in order to work as a REST[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]// NOTE: If the service is renamed, remember to update the global.asax.cs filepublic class RestJobRequestServiceProcessor : IRestJobRequestProcessor{private static readonly int JobSlots = 3;private static readonly ILog Log = LogManager.GetLogger(typeof(RestJobRequestServiceProcessor));private static BlockingCollection<Process> _jobsBlockingCollection;private static BackgroundWorker _backgroundWorker;private static String _responseRestUri;private static String _clientConsoleAppToLaunchPath;private static int _jobProcessorThreadDown;private static CancellationTokenSource _cancTokenSource;public RestJobRequestServiceProcessor(){try{// I prefer configuring log4net in code, but I left the configuration in the app.config also, for referenceString logForNetConfigFileName = "log4netConfigFile.xml";FileInfo fI = new FileInfo(logForNetConfigFileName);if (fI.Exists){log4net.Config.XmlConfigurator.Configure(fI);}else{log4net.Config.XmlConfigurator.Configure();}_cancTokenSource = new CancellationTokenSource();_jobProcessorThreadDown = 0;_backgroundWorker = new BackgroundWorker();_jobsBlockingCollection = new BlockingCollection<Process>(JobSlots);string devUrl = string.Empty;var executionFilePaths = ConfigurationManager.GetSection("ExecutionFilePaths") as NameValueCollection;if (executionFilePaths != null){_responseRestUri = executionFilePaths["responseRestUri"].ToString();_clientConsoleAppToLaunchPath = executionFilePaths["clientConsoleAppToLaunchPath"].ToString();}Log.Info("responseRestUri: " + _responseRestUri + "; clientConsoleAppToLaunchPath: " + _clientConsoleAppToLaunchPath);_backgroundWorker.DoWork += bw_DoWork;_backgroundWorker.RunWorkerAsync();}catch (Exception exc){Log.Error("Error: ", exc);_cancTokenSource.Cancel();}}void bw_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e){CancellationToken cT = _cancTokenSource.Token;try{while (true){Process p;// We block here until a new Process is availablep = _jobsBlockingCollection.Take(cT);p.Start();// Do not wait for the child process to exit before// reading to the end of its redirected stream.// p.WaitForExit();// Read the output stream first and then wait.string output = p.StandardOutput.ReadToEnd();p.WaitForExit();// Now we will simulate the calling of another REST service,// This will be required if you must inform someone about the task// being finished. Here we just test if it works with an external REST serviceWebRequest request = WebRequest.Create(_responseRestUri);WebResponse ws = request.GetResponse();DataContractJsonSerializer ser = new DataContractJsonSerializer(typeof(List<Todo>));var obj2 = (List<Todo>)ser.ReadObject(ws.GetResponseStream());String resultantTodos = String.Empty;foreach (var todo in obj2){resultantTodos += todo.ToString() + Environment.NewLine;}Log.Info("todos: " + resultantTodos);cT.ThrowIfCancellationRequested();}}catch (OperationCanceledException exc){Log.Error("OperationCanceledException: ", exc);Log.Info("Shutting down worker!");}CleanUp();}private void CleanUp(){// We set the flag that we will not be able to process job requests anymoreInterlocked.Increment(ref _jobProcessorThreadDown);// We drop other Processes that were added to be processedfor (int i = 0; i < _jobsBlockingCollection.Count; i++){_jobsBlockingCollection.Take();}_jobsBlockingCollection.Dispose();}public String QueueWorkWebGet(String pJobName){return PushNewJob(pJobName);}public String QueueWorkWebInvokePost(String pJobName){return PushNewJob(pJobName);}public String SimpleGetRequestResponse(String pJobName){return String.Format("Ping/Pong test. You sent: {0}", pJobName);}private String PushNewJob(String pJobName){String result = String.Empty;if (_jobProcessorThreadDown == 0){Process p = new Process();// Redirect the output stream of the child process.p.StartInfo.UseShellExecute = false;p.StartInfo.RedirectStandardOutput = true;p.StartInfo.FileName = _clientConsoleAppToLaunchPath;p.StartInfo.Arguments = "15000 thisisatest";try{if (_jobsBlockingCollection.TryAdd(p)){result = "Job " + pJobName + " pushed, now in queue: " + _jobsBlockingCollection.Count + " from " + JobSlots;}else{result = "Cannot schedule job, queue full. Please try again later";}}catch (ObjectDisposedException exc){Log.Error("Error: ", exc);result = "Cannot schedule job. Worker already stopped!";}}else{result = "Processor not running!";}return result;}public String ShutdownJobProcessorThread(String pKeyword){String result = "Shutdown request denied!";if (pKeyword.Equals("forfeit9")){result = "Shutdown request registered. Tasks still to be executed and cancelled: " + _jobsBlockingCollection.Count;_cancTokenSource.Cancel();}return result;}}}
Now, lets configure the App.Config file. We need to add the configuration for the log4net, we will need to add the path to our external program that will simulate the long running operation, and also our declaration of the service. Here's the App.Config:
<?xml version="1.0" encoding="utf-8" ?><configuration><configSections><section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" /><section name="ExecutionFilePaths" type="System.Configuration.NameValueSectionHandler"/></configSections><startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" /></startup><log4net><appender name="FileAppender" type="log4net.Appender.RollingFileAppender"><file value="WcfRESTWindowsServiceHost"/><param name="ImmediateFlush" value="true" /><appendToFile value="true"/><rollingStyle value="Date"/><datePattern value=".yyyyMMdd'.log'"/><staticLogFileName value="false"/><layout type="log4net.Layout.PatternLayout"><conversionPattern value="%date [%thread] %-5level %logger - %message%newline" /></layout></appender><root><level value="INFO" /><appender-ref ref="FileAppender" /></root></log4net><ExecutionFilePaths><add key="responseRestUri" value="http://jsonplaceholder.typicode.com/todos" /><add key="clientConsoleAppToLaunchPath" value="LongRunningOpSimulator.exe" /></ExecutionFilePaths><system.serviceModel><services><service name="WcfRESTService.RestJobRequestServiceProcessor" behaviorConfiguration="WcfRESTServiceBehaviour" ><endpoint address="http://localhost:8000/WcfRESTServiceDemo"binding="webHttpBinding" behaviorConfiguration="WebHttpBehaviour"contract="WcfRESTService.Interfaces.IRestJobRequestProcessor"></endpoint></service></services><behaviors><serviceBehaviors><behavior name="WcfRESTServiceBehaviour" ><serviceDebug includeExceptionDetailInFaults="true"/><serviceAuthorization principalPermissionMode="None"></serviceAuthorization></behavior></serviceBehaviors><endpointBehaviors><behavior name="WebHttpBehaviour"><webHttp automaticFormatSelectionEnabled="false" defaultBodyStyle="Wrapped"defaultOutgoingResponseFormat="Json" helpEnabled="true" /></behavior></endpointBehaviors></behaviors></system.serviceModel></configuration>
We need to do an additional thing for this project, to disable self hosting, because we will create special projects that will host our service. We can do that by doing in the properties and unchecking the “Start WCF Service Host when debugging another project in the same solution” checkbox:
Now, next step is to create the Console application that will simulate the long running operation.
static
void
Main(string[]
args)
{
if
(args.Length == 2)
{
int
sleepAmount;
bool
parsed = Int32.TryParse(args[0],
out
sleepAmount);
if
(!parsed)
sleepAmount
= 2 * 1000; //
2 seconds
String
retMessage = args[1];
Console.WriteLine("Received
task request. Message: "
+ retMessage);
Thread.Sleep(sleepAmount);
Console.WriteLine("Done
with the Job");
}
}
|
Next, we need a console application that will execute our Service, before we create a windows service for it. This is necessary because it is easier to Debug the application, in comparison with Debugging a windows service.
In this Project, the most important parts are the Program.cs file and the App.config. While the App.config is a copy paste of the WcfRESTService's App.config, the Program.cs contains very little code:
class Program{static void Main(string[] args){var host = new ServiceHost(typeof(RestJobRequestServiceProcessor));host.Open();Console.WriteLine("The service is ready at {0}", host.Description.Endpoints[0].ListenUri);Console.WriteLine("Press <Enter> to stop the service.");Console.ReadLine();host.Close();}}
Now, we will do a very important thing from my point of view. This is not related to the solution of our problem, but it is a general recommendation that I can give to anyone that does .NET development. We will change the location of bin and obj folders of all our three solutions. Why? Imagine that you are working with a subversion system, and you want to commit. If you'll do that, you will get tons of temporary files, like .obj's, or .pdb's. You will have hard time to find what you really want to commit. So, first of all, we change the bin folder location. Go to every project in this solution, and change:
This will force Visual Studio to create the bin folder two folders up the folder tree. Make sure you have such a structure. Why one back is not enough? Because all three projects will be in the same folder, and when you do the commit, you usually want to do that for all the solution (This is my experience with TurtoiseSVN and Visual SVN). This simplifies a lot of things.
OK, we did this, but there is another unpleasant thing. The obj folder is still generated for every project in it's folder. We don't want that. Sadly, there is not a visual way to change this. You must close the solution, and open each .csproj and(or change) the following property:
<BaseIntermediateOutputPath>..\..\obj\LongRunningOpSimulator\Debug\</BaseIntermediateOutputPath>
|
In order to make sure no collisions happen between obj files, I create a folder for each project in the target obj folder. Also, Note that these changes (changing bin and obj outputs) must be done for every build configuration. Yes, it is a bit of work, but it will save you an important amount of time and nerves :) P.S. Also note that sometimes Visual Studio might create the obj folder inside you project's folder. This is a known bug...
Now, if you'll want to start the application and test it, you need to set the console application to be the Startup Project, and also you need to start Visual Studio as Administrator. The following screen shots represent the results of running the application
If you go and type the address from the screen shot below in your browser, you will get the following result:
Now, if you queue a work named “job1”, you will get the following result:
Allright. We're nearly done. Now, lets create the Windows Service that will host our WCF Service. For that, we will add a new project to our solution, named WcfRESTWindowsService.
Visual studio will create a simple Windows service for us. Now, we need to replace the code in two files. First is Program.cs
namespace
WcfRESTWindowsService
{
[RunInstaller(true)]
public
class
ProjectInstaller
: Installer
{
private
ServiceProcessInstaller
process;
private
ServiceInstaller
service;
public
ProjectInstaller()
{
process
= new
ServiceProcessInstaller();
process.Account
= ServiceAccount.LocalSystem;
service
= new
ServiceInstaller();
service.ServiceName
= "WcfRESTWindowsServiceHost";
Installers.Add(process);
Installers.Add(service);
}
}
}
|
In order that code to work, we need to add a reference to the following Assembly:
Next, we need to Rename our Service1.cs file to WcfRESTWindowsServiceHost, and replace the code in the file with the following one:
namespace
WcfRESTWindowsService
{
[RunInstaller(true)]
public
class
ProjectInstaller
: Installer
{
private
ServiceProcessInstaller
process;
private
ServiceInstaller
service;
public
ProjectInstaller()
{
process
= new
ServiceProcessInstaller();
process.Account
= ServiceAccount.LocalSystem;
service
= new
ServiceInstaller();
service.ServiceName
= "WcfRESTWindowsServiceHost";
Installers.Add(process);
Installers.Add(service);
}
}
}
|
Also, in order this code to work, you must add a reference to System.ServiceModel, and also to our WCF service library.
Copy the contents of the app.config from WCF service project to the Windows service (You'll have 3 app.config with the same content, that's right)
Now, don't forget to configure the output bin and obj paths, for convenience.
We're ready to test the Windows Service. In order to do that, you must open the Developer command prompt, and execute the command shown in the screen shot below, and you will see the corresponding output (to uninstall just precede the name with /u):
In order to see if the installation was successful, go to the control panel, administrative tools, services, and check for our service:
Start it, and access the help section of the service to see if it's working. Normally it should, and just push a job to it:
Now, if you open the log file in the same folder the windows service executable is (in our custom bin output folder), you should see something like this:
DONE! :D
No comments:
Post a Comment
Please comment strictly on the post and its contents. All comments that do not follow this guideline or use this blog as advertising platform, which made me enable moderation, will not be accepted.