Monday, January 16, 2012

Hosting Silverlight application without IIS web-server


This post shows an example of a small application that hosts WCF services and html-pages with a Silverlight application. This application is the simple web-server, in fact, so we do not need IIS at all. 
The presented source code can be used as a template for similar projects. Just compile the solution in Visual Studio (VS)  and start it as Administrator, then type http://[computername]:8080 in your browser. You can also access to this application from remote computers (please do not forget about the firewall).
Here is the screenshot of this simple application:

The VS solution contains 4 projects:
  • SlAndWcfHosting.App – hosts WCF services as Console application;
  • SlAndWcfHosting.Svc – hosts WCF services as Windows service;
  • SlAndWcfHosting.Services – implements WCF services and the ServerHost class;
  • SlClient – the simple Silverlight application that shows UTC time of the server. 

The MainService class implements the Windows service.

  1. using System.ServiceProcess;
  2. using SlAndWcfHosting.Services;
  3.  
  4. namespace SlAndWcfHosting.Svc
  5. {
  6.     public partial class MainService : ServiceBase
  7.     {
  8.         private readonly ServerHost _serverHost;
  9.  
  10.         public MainService()
  11.         {
  12.             InitializeComponent();
  13.  
  14.             _serverHost = new ServerHost(OnRequestStop);
  15.             _serverHost.RequestAdditionalTime += OnServerHostRequestAdditionalTime;
  16.         }
  17.  
  18.         protected override void OnStart(string[] args)
  19.         {
  20.             _serverHost.Start(args);
  21.         }
  22.  
  23.         protected override void OnStop()
  24.         {
  25.             _serverHost.Stop();
  26.         }
  27.  
  28.         private void OnRequestStop()
  29.         {
  30.             Stop();
  31.         }
  32.  
  33.         void OnServerHostRequestAdditionalTime(object sender, ServerHost.RequestAdditionalTimeEventArgs e)
  34.         {
  35.             RequestAdditionalTime(e.Milliseconds);
  36.         }
  37.     }
  38. }


The ServerHost class hosts all services add can start and stop them. It uses the ServiceHost class to host usual WCF services and the WebServiceHost class to host our small http-service. 

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Diagnostics;
  4. using System.IO;
  5. using System.Reflection;
  6. using System.ServiceModel;
  7. using System.ServiceModel.Web;
  8.  
  9. namespace SlAndWcfHosting.Services
  10. {
  11.     public class ServerHost
  12.     {
  13.         public event EventHandler<RequestAdditionalTimeEventArgs> RequestAdditionalTime;
  14.  
  15.         private readonly List<ServiceHost> _serviceHosts = new List<ServiceHost>();
  16.  
  17.         public ServerHost(Action requestStop)
  18.         {
  19.             if (requestStop == null)
  20.                 throw new ArgumentNullException("requestStop");
  21.  
  22.             string exePath = Assembly.GetEntryAssembly().Location;
  23.             string exeFolder = Path.GetDirectoryName(exePath);
  24.             if (exeFolder != null)
  25.             {
  26.                 Environment.CurrentDirectory = exeFolder;
  27.             }
  28.         }
  29.  
  30.         public void Start(string[] args)
  31.         {
  32.             Trace.TraceInformation("The service is starting...");
  33.  
  34.             try
  35.             {
  36.                 DoStart();
  37.             }
  38.             catch (Exception x)
  39.             {
  40.                 Trace.TraceError("Service failed to start: {0}. \r\n\r\n{1}", x.Message, x.ToString());
  41.                 throw;
  42.             }
  43.         }
  44.  
  45.         public void Stop()
  46.         {
  47.             CloseServiceHosts();
  48.             Trace.TraceInformation("The service stopped.");
  49.         }
  50.         
  51.  
  52.         private void DoStart()
  53.         {
  54.             DoRequestAdditionalTime(1000);
  55.  
  56.             //TODO: add initialization code here. Use DoRequestAdditionalTime() to request additional time.
  57.  
  58.             StartWebServices();
  59.         }
  60.  
  61.         private void StartWebServices()
  62.         {
  63.             Trace.TraceInformation(@"Starting Web services...");
  64.             var httpServiceHost = new WebServiceHost(typeof(WebServer.HttpService));
  65.             _serviceHosts.AddRange(new[]
  66.             {
  67.                 new ServiceHost(typeof(TimeService)),
  68.                 httpServiceHost
  69.             });
  70.  
  71.             foreach (var serviceHost in _serviceHosts)
  72.             {
  73.                 DoRequestAdditionalTime(2000);
  74.                 serviceHost.Open();
  75.             }
  76.  
  77.             int httpPort = httpServiceHost.BaseAddresses[0].Port;
  78.             Trace.TraceInformation(string.Format(@"Web services started. Http port is {0}", httpPort));
  79.         }
  80.  
  81.         private void DoRequestAdditionalTime(int milliseconds)
  82.         {
  83.             if(RequestAdditionalTime != null)
  84.             {
  85.                 RequestAdditionalTime(this, new RequestAdditionalTimeEventArgs(milliseconds));
  86.             }
  87.         }
  88.         
  89.         private void CloseServiceHosts()
  90.         {
  91.             // close hosts in the reverse sequence (order)
  92.             for (int index = _serviceHosts.Count - 1; index >= 0; index--)
  93.             {
  94.                 var serviceHost = _serviceHosts[index];
  95.                 if (serviceHost != null)
  96.                 {
  97.                     DoRequestAdditionalTime(1000);
  98.                     serviceHost.Close();
  99.                 }
  100.             }
  101.             _serviceHosts.Clear();
  102.         }
  103.  
  104.         public class RequestAdditionalTimeEventArgs : EventArgs
  105.         {
  106.             private readonly int _milliseconds;
  107.  
  108.             public RequestAdditionalTimeEventArgs(int milliseconds)
  109.             {
  110.                 _milliseconds = milliseconds;
  111.             }
  112.  
  113.             public int Milliseconds
  114.             {
  115.                 get { return _milliseconds; }
  116.             }
  117.         }
  118.     }
  119.  
  120.     
  121. }


The TimeService class implements WCF service with our “business–logic”.

  1. using System;
  2. using System.ServiceModel;
  3.  
  4. namespace SlAndWcfHosting.Services
  5. {
  6.     [ServiceContract(Name = "TimeService")]
  7.     interface ITimeService
  8.     {
  9.         [OperationContract]
  10.         DateTime GetServerUtcTime();        
  11.     }
  12. }

  1. using System;
  2.  
  3. namespace SlAndWcfHosting.Services
  4. {
  5.     public class TimeService : ITimeService
  6.     {
  7.         public DateTime GetServerUtcTime()
  8.         {
  9.             return DateTime.UtcNow;
  10.         }
  11.     }
  12. }

The HttpService class implements the web-server as WCF web-service.  The service contract uses the WebGet attribute that makes it possible to call a WCF service with a normal HTTP GET call. It allows giving up web-pages, pictures, xap-files and other resources located in the “WebRoot” folder over HTTP. Note that our web-server is able to add a content type to the outgoing response

  1. using System.IO;
  2. using System.ServiceModel;
  3. using System.ServiceModel.Web;
  4.  
  5. namespace SlAndWcfHosting.Services.WebServer
  6. {
  7.     [ServiceContract( Name = "HttpService")]
  8.     public interface IHttpService
  9.     {
  10.         [OperationContract, WebGet(UriTemplate = "")]
  11.         Stream GetDefaultPage();
  12.  
  13.         [OperationContract, WebGet(UriTemplate = "/{pagename}")]
  14.         Stream GetPage(string pagename);
  15.  
  16.         [OperationContract, WebGet(UriTemplate = "/?downloadFile={filename}")]
  17.         Stream DownloadFile(string filename);       
  18.     }
  19. }

  1. using System.Collections.Generic;
  2. using System.IO;
  3. using System.ServiceModel.Web;
  4. using System.Text;
  5.  
  6. namespace SlAndWcfHosting.Services.WebServer
  7. {    
  8.     public class HttpService : IHttpService
  9.     {
  10.         private const string WebRootDir = @"WebRoot\";
  11.         private const string Error404PageFormat = @"<html><head> <title>404 Not Found</title> </head><body> <h1>Not Found</h1> <p>The requested URL <b>{0}</b> was not found on this server.</p> </body></html> ";
  12.         private const string DefaultPageFileName = @"index.html";
  13.         private const string DefaultContentType = @"application/octet-stream";
  14.  
  15.         private static readonly IDictionary<string, string> MimeTypes;
  16.        
  17.         static HttpService()
  18.         {
  19.             MimeTypes = GetMimeTypes();
  20.         }
  21.  
  22.         public virtual Stream GetDefaultPage()
  23.         {
  24.             return GetContent(DefaultPageFileName);
  25.         }
  26.  
  27.         public Stream GetPage(string pagename)
  28.         {
  29.             return GetContent(pagename);
  30.         }
  31.  
  32.         public Stream DownloadFile(string filename)
  33.         {
  34.             return GetContent(filename, true);
  35.         }
  36.  
  37.         protected static Stream GetContent(string filename, bool download = false)
  38.         {
  39.             if (WebOperationContext.Current != null)
  40.             {
  41.                 WebOperationContext.Current.OutgoingResponse.ContentType = GetContentTypeByExtension(filename);
  42.                 if (download)
  43.                 {
  44.                     WebOperationContext.Current.OutgoingResponse.Headers.Add(
  45.                         "Content-disposition", string.Format("attachment;filename={0}", filename));
  46.                 }
  47.             }
  48.  
  49.             string filePath = WebRootDir + filename;
  50.             if (!File.Exists(filePath))
  51.             {
  52.                 return GetFileNotFoundPage(filename);
  53.             }
  54.  
  55.             return new FileStream(filePath, FileMode.Open, FileAccess.Read);
  56.         }
  57.  
  58.         private static string GetContentTypeByExtension(string fileName)
  59.         {
  60.             string ext = Path.GetExtension(fileName);
  61.             ext = string.IsNullOrEmpty(ext) ? string.Empty : ext.Substring(1);
  62.  
  63.  
  64.             string result;
  65.             if (!MimeTypes.TryGetValue(ext, out result))
  66.             {
  67.                 result = DefaultContentType;
  68.             }
  69.  
  70.             return result;
  71.         }
  72.  
  73.         private static Stream GetFileNotFoundPage(string url)
  74.         {
  75.             if (WebOperationContext.Current != null)
  76.             {
  77.                 WebOperationContext.Current.OutgoingResponse.SetStatusAsNotFound();
  78.                 WebOperationContext.Current.OutgoingResponse.ContentType = "text/html";
  79.             }
  80.             string pageContent = string.Format(Error404PageFormat, url);
  81.             return new MemoryStream(Encoding.UTF8.GetBytes(pageContent));
  82.         }
  83.  
  84.         private static Dictionary<string, string> GetMimeTypes()
  85.         {
  86.             return new Dictionary<string, string>
  87.                        {
  88.                            {"avi", "video/x-msvideo"},
  89.                            {"bin", "application/octet-stream"},
  90.                            {"bmp", "image/bmp"},
  91.                            {"css", "text/css"},
  92.                            {"der", "application/x-x509-ca-cert"},
  93.                            {"dll", "application/x-msdownload"},
  94.                            {"doc", "application/msword"},
  95.                            {"dot", "application/msword"},
  96.                            {"exe", "application/octet-stream"},
  97.                            {"gif", "image/gif"},
  98.                            {"gtar", "application/x-gtar"},
  99.                            {"gz", "application/x-gzip"},
  100.                            {"h", "text/plain"},
  101.                            {"htm", "text/html"},
  102.                            {"html", "text/html"},
  103.                            {"ico", "image/x-icon"},
  104.                            {"ief", "image/ief"},
  105.                            {"jfif", "image/pipeg"},
  106.                            {"jpe", "image/jpeg"},
  107.                            {"jpeg", "image/jpeg"},
  108.                            {"jpg", "image/jpeg"},
  109.                            {"js", "application/x-javascript"},
  110.                            {"m3u", "audio/x-mpegurl"},
  111.                            {"mov", "video/quicktime"},
  112.                            {"movie", "video/x-sgi-movie"},
  113.                            {"mp2", "video/mpeg"},
  114.                            {"mp3", "audio/mpeg"},
  115.                            {"mpa", "video/mpeg"},
  116.                            {"mpe", "video/mpeg"},
  117.                            {"mpeg", "video/mpeg"},
  118.                            {"mpg", "video/mpeg"},
  119.                            {"pdf", "application/pdf"},
  120.                            {"rtf", "application/rtf"},
  121.                            {"swf", "application/x-shockwave-flash"},
  122.                            {"tif", "image/tiff"},
  123.                            {"tiff", "image/tiff"},
  124.                            {"txt", "text/plain"},
  125.                            {"xaml", "application/xaml+xml"},
  126.                            {"xap", "x-silverlight-app"},
  127.                            {"z", "application/x-compress"},
  128.                            {"zip", "application/zip"}
  129.                        };
  130.         }
  131.     }
  132. }

The app.config file configures WCF-services on port 8080.  The TimeService service uses the BasicHttpBinding. The HttpService service uses the WebHttpBinding binding and the WebHttpBehavior behavior.

  1. <?xml version="1.0"?>
  2. <configuration>  
  3.   
  4.   <system.serviceModel>
  5.     <behaviors>
  6.       <endpointBehaviors>
  7.         <behavior name="WebHttpBehavior">
  8.           <webHttp/>
  9.         </behavior>
  10.       </endpointBehaviors>
  11.  
  12.       <serviceBehaviors>
  13.         <behavior name="DefaultServiceBehavior">
  14.           <!-- To avoid disclosing metadata information, set the value below to false and remove the metadata endpoint above before deployment -->
  15.           <serviceMetadata httpsGetEnabled="false" httpGetEnabled="true"/>
  16.           <!-- To receive exception details in faults for debugging purposes, set the value below to true.  Set to false before deployment to avoid disclosing exception information -->
  17.           <serviceDebug includeExceptionDetailInFaults="true"/>
  18.           <serviceAuthorization principalPermissionMode="None"/>
  19.           <serviceCredentials>
  20.             <windowsAuthentication includeWindowsGroups="false"/>
  21.           </serviceCredentials>
  22.         </behavior>
  23.       </serviceBehaviors>
  24.  
  25.     </behaviors>
  26.  
  27.     <services>
  28.       <service name="SlAndWcfHosting.Services.WebServer.HttpService">
  29.         <endpoint address="" behaviorConfiguration="WebHttpBehavior" binding="webHttpBinding"
  30.                   contract="SlAndWcfHosting.Services.WebServer.IHttpService" />
  31.         <host>
  32.           <baseAddresses>
  33.             <add baseAddress="http://localhost:8080/" />
  34.           </baseAddresses>
  35.         </host>
  36.       </service>
  37.       <service name="SlAndWcfHosting.Services.TimeService" behaviorConfiguration="DefaultServiceBehavior"  >
  38.         <endpoint address="" binding="basicHttpBinding"
  39.                   contract="SlAndWcfHosting.Services.ITimeService" />
  40.         <host>
  41.           <baseAddresses>
  42.             <add baseAddress="http://localhost:8080/TimeService" />
  43.           </baseAddresses>
  44.         </host>
  45.       </service>
  46.     </services>
  47.  
  48.   </system.serviceModel>
  49.  
  50.   <system.diagnostics>
  51.     <!--log Trace massages into Event Log-->
  52.     <trace autoflush="false" indentsize="4">
  53.       <listeners>
  54.         <add name="eventLogListener" type="System.Diagnostics.EventLogTraceListener" initializeData="SlAndWcfHosting" />
  55.         <remove name="Default" />
  56.       </listeners>
  57.     </trace>
  58.   </system.diagnostics>
  59.   
  60.   <startup>
  61.     <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/>
  62.   </startup>
  63.  
  64. </configuration>


The SlClient project contains a trivial Silverlight application with reference to the TimeService service. It also contains additional web-resources that should be hosted by our web-server: 
The output xap-file and all web-resources are copied into the WebRoot folder on post-built event of the SlClient project.


  1. xcopy "$(ProjectDir)WebRoot\*.*" "$(TargetDir)" /y
  2.  
  3. if exist "$(ProjectDir)..\SlAndWcfHosting.App" (
  4. xcopy "$(ProjectDir)WebRoot\*.*" "$(ProjectDir)..\SlAndWcfHosting.App\Bin\$(ConfigurationName)\WebRoot\" /Y /F
  5. xcopy "$(TargetDir)$(TargetName).xap" "$(ProjectDir)..\SlAndWcfHosting.App\Bin\$(ConfigurationName)\WebRoot\" /Y /F
  6. )
  7.  
  8. if exist "$(ProjectDir)..\SlAndWcfHosting.Svc" (
  9. xcopy "$(ProjectDir)WebRoot\*.*" "$(ProjectDir)..\SlAndWcfHosting.Svc\Bin\$(ConfigurationName)\WebRoot\" /Y /F
  10. xcopy "$(TargetDir)$(TargetName).xap" "$(ProjectDir)..\SlAndWcfHosting.Svc\Bin\$(ConfigurationName)\WebRoot\" /Y /F
  11. )

The MainPage class implements the “Get server time” button handler. The SetAddress method sets an address where xap-file was loaded from.

  1. using System;
  2. using System.Linq;
  3. using System.ServiceModel;
  4. using System.Windows;
  5. using System.Windows.Controls;
  6. using SlClient.TimeServiceReference;
  7.  
  8. namespace SlClient
  9. {
  10.     public partial class MainPage : UserControl
  11.     {
  12.         public MainPage()
  13.         {
  14.             InitializeComponent();
  15.         }
  16.  
  17.         private void Button1Click(object sender, RoutedEventArgs e)
  18.         {
  19.             var client = new TimeServiceClient();
  20.             SetAddress(client);
  21.  
  22.             client.GetServerUtcTimeCompleted += ClientGetServerUtcTimeCompleted;
  23.             client.GetServerUtcTimeAsync();
  24.         }
  25.  
  26.         void ClientGetServerUtcTimeCompleted(object sender, GetServerUtcTimeCompletedEventArgs e)
  27.         {
  28.             label1.Content = e.Result;
  29.         }
  30.  
  31.         private static void SetAddress<TChannel>(ClientBase<TChannel> client) where TChannel : class
  32.         {
  33.             Uri hostSource = Application.Current.Host.Source;
  34.  
  35.             if (!string.IsNullOrEmpty(hostSource.Host))
  36.             {
  37.                 UriBuilder uriBulder = new UriBuilder(client.Endpoint.Address.Uri);
  38.                 uriBulder.Scheme = hostSource.Scheme;
  39.                 uriBulder.Host = hostSource.Host;
  40.                 uriBulder.Port = hostSource.Port;
  41.                 client.Endpoint.Address = new EndpointAddress(uriBulder.Uri, client.Endpoint.Address.Headers.ToArray());
  42.             }
  43.         }      
  44.     }
  45. }

No comments:

Post a Comment