Dynamic WCF Services (ASP.NET Virtual Paths)
I have been experimenting with WCF and the ASP.NET VirtualPathProvider. My goal is to host WCF services dynamically (with reflection) in IIS, with no .svc files and no configuration in web.config. With the following code, I think I have succeeded.
I am wondering if there are any better approaches to this. It surprises me a little that, when hosted in IIS, WCF does not seem to have any out-of-the-box capacities for adding services at runtime. In other words, WCF makes use of .svc files instead of just treating the services as virtual paths. If WCF handled them virtually, the services could be configured solely in web.config (with no .svc file), or they could be added entirely at run time like they are with non-IIS hosts. This seems superior than what WCF implements.
I think this kind of code can be very useful. Consider a web application that consists of many assemblies implementing WCF services. These assemblies are agnostic as to whether or not they are running in IIS. With the virtual approach, the main web application could host all of these services, without the need for the creating .svc files in the main application, which would involve more coupling than would be ideal for an extensible application design.
Note that this is basically my first attempt at this -- there may be some bugs in this code.
// VirtualWcf.cs: Load WCF services dynamically (by reflection) under an ASP.NET VirtualPathProvider.// Jason Kresowaty (jason@binarycoder.net)// Use this code at your own risk.// Supports access to WCF services by paths such as "~/Virtual/TestService.svc".// The "Virtual" directory and the "ClassName.svc" file do not really exist on disk but are handled by the VirtualPathProvider.// The service is loaded by reflection per the file name contained in the path. In the above example, reflection// would load the class in this same assembly with the namespace-prefixed name "VirtualWcfApp.TestService".// You must set up Global.asax to load the VirtualPathProvider:// public class Global : System.Web.HttpApplication// {// protected void Application_Start(object sender, EventArgs e)// {// VirtualWcfPathProvider provider = new VirtualWcfPathProvider();// HostingEnvironment.RegisterVirtualPathProvider(provider);// }// }using System;using System.Collections;using System.Collections.Generic;using System.Collections.ObjectModel;using System.Data;using System.Configuration;using System.Web;using System.Web.Caching;using System.Web.Hosting;using System.Globalization;using System.IO;using System.Reflection;using System.ServiceModel;using System.ServiceModel.Activation;using System.ServiceModel.Description;namespace VirtualWcfApp{// Shared constants.public static class Shared
{public static readonly string VirtualWcfDirectoryName = "~/Virtual"; // Must start with "~/", must not end with "/".
public static readonly string ThisAssemblyFullName = typeof(Shared).Assembly.FullName;
public static readonly string ThisNamespaceName = typeof(Shared).Namespace;
}
// This is the VirtualPathProvider that serves up virtual files. The supported// requests are of the form "~/Virtual/ClassName.svc".public class VirtualWcfPathProvider : VirtualPathProvider
{public override bool FileExists(string virtualPath)
{string appRelativeVirtualPath = ToAppRelativeVirtualPath(virtualPath);if (IsVirtualFile(appRelativeVirtualPath)){return true;
}
else{return Previous.FileExists(virtualPath);}
}
public override System.Web.Hosting.VirtualFile GetFile(string virtualPath)
{string appRelativeVirtualPath = ToAppRelativeVirtualPath(virtualPath);if (IsVirtualFile(appRelativeVirtualPath)){string srp = VirtualPathUtility.MakeRelative(Shared.VirtualWcfDirectoryName + "/", virtualPath);
string serviceClass = Shared.ThisNamespaceName + "." + srp;
if (serviceClass.EndsWith(".svc"))
{serviceClass = serviceClass.Substring(0, serviceClass.LastIndexOf(".svc"));}
return new WcfVirtualFile(virtualPath, serviceClass, typeof(VirtualWcfFactory).FullName);
}
else{return Previous.GetFile(virtualPath);}
}
public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies, DateTime utcStart)
{string appRelativeVirtualPath = ToAppRelativeVirtualPath(virtualPath);if (IsVirtualFile(appRelativeVirtualPath) || IsVirtualDirectory(appRelativeVirtualPath)){return null;
}
else{return Previous.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);}
}
private bool IsVirtualFile(string appRelativeVirtualPath)
{if (appRelativeVirtualPath.StartsWith(Shared.VirtualWcfDirectoryName + "/", StringComparison.OrdinalIgnoreCase))
{return true;
}
return false;
}
private bool IsVirtualDirectory(string appRelativeVirtualPath)
{return appRelativeVirtualPath.Equals(Shared.VirtualWcfDirectoryName, StringComparison.OrdinalIgnoreCase);
}
private string ToAppRelativeVirtualPath(string virtualPath)
{string appRelativeVirtualPath = VirtualPathUtility.ToAppRelative(virtualPath);
if (!appRelativeVirtualPath.StartsWith("~/"))
{throw new HttpException("Unexpectedly does not start with ~.");
}
return appRelativeVirtualPath;}
}
// This is the virtual .svc file.// When this file is accessed, a <%@ServiceHost%> tag is returned that gives// the service and factory associated with this VirtualFile.public class WcfVirtualFile : VirtualFile
{private string _Service;
private string _Factory;
public WcfVirtualFile(string vp, string service, string factory)
: base(vp){_Service = service;
_Factory = factory;
}
public override Stream Open()
{MemoryStream ms = new MemoryStream();
StreamWriter tw = new StreamWriter(ms);
tw.Write(string.Format(CultureInfo.InvariantCulture,
"<%@ServiceHost language=c# Debug=\"true\" Service=\"{0}\" Factory=\"{1}\"%>",HttpUtility.HtmlEncode(_Service), HttpUtility.HtmlEncode(_Factory)));
tw.Flush();
ms.Position = 0;
return ms;}
}
// This is the factory loaded when the user tried to use the (virtual) .svc// file. The constructorString is the "Service=" part of the .svc and for our// purposes identifies the name of the service class (in this assembly) to be// loaded by reflection.public class VirtualWcfFactory : ServiceHostFactory
{public override System.ServiceModel.ServiceHostBase CreateServiceHost(string constructorString, System.Uri[] baseAddresses)
{Assembly assem = Assembly.Load(Shared.ThisAssemblyFullName);
Type serviceType = assem.GetType(constructorString);ServiceHost host = new ServiceHost(serviceType, baseAddresses);
foreach (Type iface in serviceType.GetInterfaces())
{ServiceContractAttribute attr = (ServiceContractAttribute)Attribute.GetCustomAttribute(iface, typeof(ServiceContractAttribute));
if (attr != null)
{host.AddServiceEndpoint(iface, new WSHttpBinding(), "");
}
}
ServiceMetadataBehavior metadataBehavior = new ServiceMetadataBehavior();
metadataBehavior.HttpGetEnabled = true;host.Description.Behaviors.Add(metadataBehavior);
return host;}
}
}
All Replies
- Thanks! this post was helpful.
JAY- Edited byJay Kher Tuesday, October 21, 2008 5:20 PM
- I've been struggling with this same problem for some time now (See my post How do I expose WCF web services in IIS without using .svc files?).
I haven't had a chance yet see if I'll be able to use this technique but I'll make sure to respond with the results that I get after I try.
How has this been working out for you so far and have you run into any problems using this technique?
Do we sacrifice anything using this technique such as forcing us to host our services within a ASP.NET Web Application or such? I haven't done my homework on the VirtualPathProvider class yet so I'm sure I'll be able to answer this myself after digging into a bit. - > How has this been working out for you so far
I haven't used this in any applications (basically just developed it as an experiment). So, I can't say. - So far this seems to be working. Of course I also needed to change a few things to fit the goals of my project but the concept of using the VirtualPathProvider to expose virtual .svc files seems to be working like a charm.
The thing I found interesting was that I needed to use the Global.asax file to initialize my VirtualPathProvider in my WCF Service Application, however from the documentation seems that for projects which are not specifically ASP.NET sites the preference is to use a static AppInitialize method. No matter what I tried the AppInitialize method never seemed to fire for me though so I resorted to using the Global.asax file for now. Furthermore, it doesn't seem possible to add a ASP.NET App_Code folder to my WCF Service Applicatino project. I would have guessed the opposite would have been true. - I too have been looking for a solution such as this. I plan on implementing something like this as an HttpHandler or HttpModule listening for *.svc file requests removing the need for the Application_Start implementation. I could then have an independent ServerControl with say a datagrid that could define a request to a Virtual .svc for getting data and whose definition itself would be included in the ServerControl Library. Making the ServerControl Library 100% encapsulated and support dropping into an existing web applicaiton. This would be all ASP.NET/II7. That is the goal anyway. Thanks for the great information!!
hyspdrt Thanks this is exactly what I need for a project.
- Regarding the HttpModule / HttpHandler - did you succeed?
- Not exactly, I still use the Virtual Provider in App_Start as that is how ASP.NET Works, but the concept of encapsulating the Wcf/WebService in the UserControl works as long as the NameSpace of your Service matches the Providers lookup NameSpace. This is required for Assembly/Type resolution. I've enhanced slightly to support WebServices and WcfServices.
I have no .svc or .asmx files, I create the classes (only) in a Company.Web.Services library or directly in a usercontrol using the same NameSpace. During type resolution the CLR locates the Type by finding the assembly in the bin folder, so the net result is the wcf service is remote or isolated from the main WebApplication Project which merely acts as a general host, and any new or additional usercontrols can be created externally and merely dropped into the bin folder to become available for consumption.
As an extension to this model, you could create a NamespaceProvider model, where by you register namespaces for virtual services, and the VirtualServiceProvider could look for the requested Service Type using the list of available namespaces. Or store Type definitions in a database, registering "installed" or available Types, so you'd know ahead of time a specific DLL existed and you could look directly in a specific DLL. I have not performed this extension myself but is an idea I'm floating around with.
Let me know if you have questions as I use this extensively in ASP.NET and Silverlight and works fabulously. The one hitch for .asmx WebServices, is you actually have to have a folder called "Virtual", even though there is nothing in it. WcfServices do not require this folder.
Code Snippet...
static readonly string vdirectoryName = "~/Virtual";
// Hardcoded C# namespace all virtual services must be located in...static readonly string namespaceName = "YourRootNS.Web.Services"; static readonly string factoryType = typeof(VirtualServiceHostFactory).FullName; public override VirtualFile GetFile(string virtualPath) { VirtualFile vfile = null; string appRelativeVirtualPath = this.ToAppRelativeVirtualPath(virtualPath); if (this.IsVirtualFile(appRelativeVirtualPath)) { string serviceClass = virtualPath; serviceClass = VirtualPathUtility.MakeRelative(vdirectoryName + "/", serviceClass);
serviceClass = namespaceName + "." + serviceClass;string fileExt = Path.GetExtension(virtualPath); if (fileExt == ".svc") { serviceClass = serviceClass.Substring(0, serviceClass.LastIndexOf(".svc")); vfile = new VirtualWCFService(virtualPath, serviceClass, factoryType); } else if (fileExt == ".asmx") { serviceClass = serviceClass.Substring(0, serviceClass.LastIndexOf(".asmx")); Type serviceType = HSS.Reflection.LookupType(serviceClass); serviceClass += ", " + serviceType.Assembly.GetName().Name; vfile = new VirtualWebService(virtualPath, serviceClass); } } if (null == vfile) vfile = this.Previous.GetFile(virtualPath); return vfile; }
hyspdrt


