Multi Tenancy + WCF services

Topics: Core, Writing modules
Aug 7, 2012 at 11:01 PM
Edited Aug 7, 2012 at 11:04 PM

Hi,

I'm creating a module for testing the possibilites with Orchard Multi Tenancy and WCF.

I've created a simple service that successfully returns the current sites SiteName (see code below).

But when I try the module with tenants, the WCF service always hit the default tenant (no matter if I test the module at another tenants route).
Note: I have to restart the application after enabling the WCF module at a new tenant, the WCF route crashes after enabling. 

Trying the module out only in a tenant (disabled in the default tenant) results in an error (at line 57 in OrchardServiceHostFactory).

I've come to the conclusion that the Orchard.WCF.OrchardServiceHostFactory in project Orchard.Framework selects the first/default address from an array of addresses, an array only containing the default tenants address (line 35):

ShellSettings shellSettings = runningShellTable.Match(baseAddresses.First().Host, baseAddresses.First().LocalPath);

 

I tried to modify OrchardServiceHostFactory with some quick custom code, without luck:

var host = HttpContext.Current.Request.Url.DnsSafeHost;
var localPath = HttpContext.Current.Request.AppRelativeCurrentExecutionFilePath;
ShellSettings shellSettings = runningShellTable.Match(host, localPath);

Even the Request.Url contains the default tenants address (and not the one I'm connecting to).

After testing the OrchardServiceHostFactory I realized that the class only triggers on the first request (from any tenant). And then I got a bit stucked...

 

So, I want to figure out a way to make Orchard WCF ServiceRoutes selected at the right tenants workcontext/shell in Multi Tenancy scenarios.

Questions:
Am I doing anything wrong, or is the Orchard WCF implementation by design made only for the default tenant?
Does anyone got experience with WCF and Multi Tenancy? Or have a clue about what class to dig in now, to make it work at other tenants?

 


 

Code I used as a sample WCF service (remember changing the Orchard Web.config with code below, and reference System.ServiceModel + System.ServiceModel.Activation in the module)

WCF Service Contract interface:   

[ServiceContract]
    public interface IWCFService {
        [OperationContract]
        string GetSiteName();
        [OperationContract]
        string GetBaseUrl();
    }

 

WCF Orchard Service Implementation class + IWCFService Dependency interface:   

[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
    public class WCFService : IWCFServiceDependency {
        private readonly ISiteService _siteService;
        //private readonly ISite _site;
        public WCFService(ISiteService siteService) {
            _siteService = siteService;
            //_site = siteService.GetSiteSettings();
        }
        public string GetSiteName() {
            return _siteService.GetSiteSettings().SiteName;
        }
        public string GetBaseUrl() {
            return _siteService.GetSiteSettings().BaseUrl;
        }
    }
    // Ochard Dependency Injection
    public interface IWCFServiceDependency : IWCFService, IDependency { }

 

Route provider:   

public class Routes : IRouteProvider {
        public void GetRoutes(ICollection<RouteDescriptor> routes) {
            foreach (var routeDescriptor in GetRoutes())
                routes.Add(routeDescriptor);
        }
        public IEnumerable<RouteDescriptor> GetRoutes() {
            return new[] {
                             new RouteDescriptor {   
                                Priority = -1,
                                Route = _serviceRoute
                            }     
                         };
        }
        private static ServiceRoute _serviceRoute = new ServiceRoute(
                                                 "WCF/TestRoute",
                                                 new OrchardServiceHostFactory(),
                                                 typeof(IWCFServiceDependency));
    }

 

Orchard root Web.Config configuration: 

<system.serviceModel>
    <serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" />
 </system.serviceModel>

 

Controller connecting to the WCF Service:       

public ActionResult Index() {
            string sitename = "";
            ISite site = _siteService.GetSiteSettings();
            EndpointAddress endpointAddress = new EndpointAddress(site.BaseUrl + "/WCF/TestRoute");
            ServiceWrapper<IWCFService>.Use(client => {
                sitename = client.GetSiteName();
            }, endpointAddress);
            return View((object)sitename);
        }

 

Service Wrapper helper class (for connecting to services easier):   

public class ServiceWrapper<T> {
        public static void Use(Action<T> action, EndpointAddress endpointAddress) {
            ChannelFactory<T> factory = new ChannelFactory<T>(new BasicHttpBinding(), endpointAddress);
            T client = factory.CreateChannel();
            bool success = false;
            try {
                action(client);
                ((IClientChannel)client).Close();
            } catch (CommunicationException e) {
                ((IClientChannel)client).Abort();
                factory.Abort();
            } catch (TimeoutException e) {
                ((IClientChannel)client).Abort();
                factory.Abort();
            } catch (Exception e) {
                ((IClientChannel)client).Abort();
                factory.Abort();
                throw;
            } finally {
                if (!success) {
                    ((IClientChannel)client).Abort();
                    factory.Abort();
                }
            }
        }
    }
Aug 23, 2012 at 8:43 PM

Hi thisisit, I just realized that WCF is not working on multi-tenant and found your post. Before diving in any further, I wonder if you have made any progress. Look forward to your reply.

Aug 23, 2012 at 10:32 PM

No, unfortunately not. It seems like the problem is somehow connected to the registration of ServiceRoutes when the Orchard application starts. But I’m not really sure where to start fixing the bug in WCF or what may actually cause the problem.

I think I'll try out the new Web API as an alternative to WCF. Hopefully Orchard 1.6 will be upgraded to .NET 4.5 and with the Web API implemented. Otherwise, I've read about a few successful implementations of Web API in Orchard already (check out the forum, especially this thread http://orchard.codeplex.com/discussions/353559). I expect that the Web API will work with multi tenancy, since it is "just" an extended controller (from what I’ve learned so far).

If you are going to look into a fix for WCF, start out with the customization of OrchardServiceHostFactory, described in the beginning of my first post, and debug from there. Keep this thread updated if you find out anything exciting :-)

Sep 9, 2014 at 4:03 PM
Edited Sep 9, 2014 at 4:08 PM

DefaultOrchardHost.cs probable bug (Multitenancy):

I think this
  /// <summary>
        /// Starts a Shell and registers its settings in RunningShellTable
        /// </summary>
        private void ActivateShell(ShellContext context) {
            Logger.Debug("Activating context for tenant {0}", context.Settings.Name);
            context.Shell.Activate();

                _shellContexts = (_shellContexts ?? Enumerable.Empty<ShellContext>())
                                .Where(c => c.Settings.Name != context.Settings.Name)
                                .Concat(new[] { context })
                                .ToArray();
            _runningShellTable.Add(context.Settings);
        }
must become something like
 private static readonly object _populateShellContextsLock = new object();
        /// <summary>
        /// Starts a Shell and registers its settings in RunningShellTable
        /// </summary>
        private void ActivateShell(ShellContext context) {
            Logger.Debug("Activating context for tenant {0}", context.Settings.Name);
            context.Shell.Activate();

            lock (_populateShellContextsLock)
            {
                _shellContexts = (_shellContexts ?? Enumerable.Empty<ShellContext>())
                                .Where(c => c.Settings.Name != context.Settings.Name)
                                .Concat(new[] { context })
                                .ToArray();
            }
            _runningShellTable.Add(context.Settings);
        }
because the method is called in parallel from here
  void CreateAndActivateShells() {
            Logger.Information("Start creation of shells");

            // is there any tenant right now ?
            var allSettings = _shellSettingsManager.LoadSettings().ToArray();

            // load all tenants, and activate their shell
            if (allSettings.Any()) {
                Parallel.ForEach(allSettings, settings => {
                    try {
                        var context = CreateShellContext(settings);
                        ActivateShell(context);
                    }
                    catch (Exception e) {
                        Logger.Error(e, "A tenant could not be started: " + settings.Name);
                    }
                });
            }
            // no settings, run the Setup
            else {
                var setupContext = CreateSetupContext();
                ActivateShell(setupContext);
            }

            Logger.Information("Done creating shells");
        }
and without synchronization _shellContexts will contain something different.

Perhaps this is resolution for using WCF + Multitenancy

Also override OrchardServiceHostFactory
 internal class ODataServiceFactory : OrchardServiceHostFactory
    {
        private Uri[] GetBaseAddresses(Uri[] baseAddresses)
        {
            IHttpContextAccessor accessor = HostContainer.Resolve<IHttpContextAccessor>();
            IRunningShellTable runningShellTable = HostContainer.Resolve<IRunningShellTable>();
            ShellSettings shellSettings = runningShellTable.Match(accessor.Current());

            List<Uri> newBaseAddresses = new List<Uri>();
            foreach (var baseAddress in baseAddresses)
            {
                if (!string.IsNullOrEmpty(shellSettings.RequestUrlHost))
                {
                    UriBuilder ub = new UriBuilder(baseAddress);
                    ub.Host = shellSettings.RequestUrlHost;
                    newBaseAddresses.Add(ub.Uri);
                }
                else
                    newBaseAddresses.Add(baseAddress);
            }
            return newBaseAddresses.ToArray();
        }

        public override ServiceHostBase CreateServiceHost(string constructorString, Uri[] baseAddresses)
        {

            return base.CreateServiceHost(constructorString, GetBaseAddresses(baseAddresses));
        }

        protected override ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses)
        {
            return new DataServiceHost(serviceType, GetBaseAddresses(baseAddresses));
        }
    }
ISSUE