none
Dynamic WCF Services (ASP.NET Virtual Paths)

    Question

  • 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;
        }
      }
    }
    Saturday, July 12, 2008 5:30 PM

All replies

  • Thanks! this post was helpful.

    JAY
    Tuesday, October 21, 2008 5:19 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.
    Wednesday, November 12, 2008 2:45 PM
  • > 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.
    Thursday, November 13, 2008 12:21 PM
  • 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.
    Wednesday, November 26, 2008 9:09 PM
  • 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
    Wednesday, December 03, 2008 6:54 PM
  • Thanks this is exactly what I need for a project.

    Saturday, January 10, 2009 6:13 AM
  • Regarding the HttpModule / HttpHandler - did you succeed?
    Tuesday, March 17, 2009 4:31 PM
  •  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
    Tuesday, March 17, 2009 8:34 PM
  • Hi

    Did someone do this for Ado.net Data Services? I'm trying to customize your given code, but have no idea where to put class constructor, like:

      public class PuControlEntitiesService : DataService<PuControlEntities>
        {
            /// <summary>
            /// This method is called ones to set service wide access rules to service
            /// </summary>
            public static void InitializeService(IDataServiceConfiguration config)
            {
                config.SetEntitySetAccessRule("*", EntitySetRights.AllRead);
                config.SetServiceOperationAccessRule("*", ServiceOperationRights.All);
                config.UseVerboseErrors = true;
            }
        }

    What I'm trying to achive is, that I can put my entity framework classes together with ado.net data services to one dll?!

    Any ideas?
    Monday, March 01, 2010 7:29 AM
  •  I have not done this for ADO.NET Services. But depending on your code base (SL or WIN) I recommend RIA Services. It has it's own built in dynamic web service provider and factory that simplifies to the EntityFramework (if you use it). I do not use RIA Services, but a combination of my VirtualServiceProvider and shared DataModel classes between the Server and Client. This is specific to SL and leverages my own Data Object Model generator.
    hyspdrt
    Monday, March 01, 2010 2:18 PM
  • Can someone post some example created by Visual Studio. Thank you very much.
    Thursday, August 12, 2010 12:18 PM
  • To be clear, and example of what specifically?
    Thursday, August 12, 2010 12:45 PM
  • I need some very simple example how to use this code. I need this functionality but I dont know (for example) how to register VirtualPathProvider because WCF doesn't have Application_Start method. Some "Hello World" application would be great.
    Friday, August 13, 2010 10:31 AM
  • This is for hosting WCF within ASP.NET.  In these cases, you can add a global.asax file to your ASP.NET web site project.  Put Application_Start in the code-behind the global.asax.

     

    Friday, August 13, 2010 11:47 PM
  • If you are looking to use with Silverlight, I have a Framework that is worth looking at on codeplex. It includes a Virtual Service Model and Virtual Client Channel framework. It also includes a lot of other neat features, but there is documentation on how to use the virtual service model if you are interested. http://hsscore.codeplex.com/wikipage?title=VirtualServices

    Also, BinaryCoder provided the example of the global.asax.cs in the original post above...

    If you are not interested in hosting for the web or silverlight, then I recommend you read this from microsoft. http://msdn.microsoft.com/en-us/library/ms730158.aspx


    hyspdrt
    Saturday, August 14, 2010 12:22 PM
  • Hi,

    I have multiple .svc service and I want to load dynamically at runtime I don't want to add all the services in my project and want to call their methods with parameters.

    How I can achieve by following your given code.

    Regards

    Tuesday, May 28, 2013 9:47 AM