// // ServiceHostBase.cs // // Author: // Atsushi Enomoto // // Copyright (C) 2005-2006 Novell, Inc. http://www.novell.com // // Permission is hereby granted, free of charge, to any person obtaining // a copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to // the following conditions: // // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.ServiceModel.Channels; using System.ServiceModel.Configuration; using System.ServiceModel.Description; using System.ServiceModel.Dispatcher; using System.ServiceModel.Security; using System.Reflection; using System.Threading; namespace System.ServiceModel { public abstract partial class ServiceHostBase : CommunicationObject, IExtensibleObject, IDisposable { // It is used for mapping a ServiceHostBase to HttpChannelListener precisely. internal static ServiceHostBase CurrentServiceHostHack; ServiceCredentials credentials; ServiceDescription description; UriSchemeKeyedCollection base_addresses; TimeSpan open_timeout, close_timeout, instance_idle_timeout; List contexts; ReadOnlyCollection exposed_contexts; ChannelDispatcherCollection channel_dispatchers; IDictionary contracts; int flow_limit = int.MaxValue; IExtensionCollection extensions; protected ServiceHostBase () { open_timeout = DefaultOpenTimeout; close_timeout = DefaultCloseTimeout; credentials = new ServiceCredentials (); contexts = new List (); exposed_contexts = new ReadOnlyCollection (contexts); channel_dispatchers = new ChannelDispatcherCollection (this); } public event EventHandler UnknownMessageReceived; internal void OnUnknownMessageReceived (Message message) { if (UnknownMessageReceived != null) UnknownMessageReceived (this, new UnknownMessageReceivedEventArgs (message)); else // FIXME: better be logged throw new EndpointNotFoundException (String.Format ("The request message has the target '{0}' with action '{1}' which is not reachable in this service contract", message.Headers.To, message.Headers.Action)); } public ReadOnlyCollection BaseAddresses { get { if (base_addresses == null) base_addresses = new UriSchemeKeyedCollection (); return new ReadOnlyCollection (base_addresses.InternalItems); } } internal Uri CreateUri (string scheme, Uri relativeUri) { Uri baseUri = base_addresses.Contains (scheme) ? base_addresses [scheme] : null; if (relativeUri == null) return baseUri; if (relativeUri.IsAbsoluteUri) return relativeUri; if (baseUri == null) return null; var s = relativeUri.ToString (); if (s.Length == 0) return baseUri; var l = baseUri.LocalPath; var r = relativeUri.ToString (); if (l.Length > 0 && l [l.Length - 1] != '/' && r [0] != '/') return new Uri (String.Concat (baseUri.ToString (), "/", r)); else return new Uri (String.Concat (baseUri.ToString (), r)); } public ChannelDispatcherCollection ChannelDispatchers { get { return channel_dispatchers; } } public ServiceAuthorizationBehavior Authorization { get; private set; } public ServiceCredentials Credentials { get { return credentials; } } public ServiceDescription Description { get { return description; } } protected internal IDictionary ImplementedContracts { get { return contracts; } } public IExtensionCollection Extensions { get { if (extensions == null) extensions = new ExtensionCollection (this); return extensions; } } protected internal override TimeSpan DefaultCloseTimeout { get { return DefaultCommunicationTimeouts.Instance.CloseTimeout; } } protected internal override TimeSpan DefaultOpenTimeout { get { return DefaultCommunicationTimeouts.Instance.OpenTimeout; } } public TimeSpan CloseTimeout { get { return close_timeout; } set { close_timeout = value; } } public TimeSpan OpenTimeout { get { return open_timeout; } set { open_timeout = value; } } public int ManualFlowControlLimit { get { return flow_limit; } set { flow_limit = value; } } protected void AddBaseAddress (Uri baseAddress) { if (base_addresses == null) throw new InvalidOperationException ("Base addresses must be added before the service description is initialized"); base_addresses.Add (baseAddress); } public ServiceEndpoint AddServiceEndpoint ( string implementedContract, Binding binding, string address) { return AddServiceEndpoint (implementedContract, binding, new Uri (address, UriKind.RelativeOrAbsolute)); } public ServiceEndpoint AddServiceEndpoint ( string implementedContract, Binding binding, string address, Uri listenUri) { Uri uri = new Uri (address, UriKind.RelativeOrAbsolute); return AddServiceEndpoint ( implementedContract, binding, uri, listenUri); } public ServiceEndpoint AddServiceEndpoint ( string implementedContract, Binding binding, Uri address) { return AddServiceEndpoint (implementedContract, binding, address, null); } public ServiceEndpoint AddServiceEndpoint ( string implementedContract, Binding binding, Uri address, Uri listenUri) { EndpointAddress ea = new EndpointAddress (BuildAbsoluteUri (address, binding)); ContractDescription cd = GetContract (implementedContract, binding.Namespace == "http://schemas.microsoft.com/ws/2005/02/mex/bindings"); if (cd == null) throw new InvalidOperationException (String.Format ("Contract '{0}' was not found in the implemented contracts in this service host.", implementedContract)); return AddServiceEndpointCore (cd, binding, ea, listenUri); } #if NET_4_0 public virtual void AddServiceEndpoint (ServiceEndpoint endpoint) { if (endpoint == null) throw new ArgumentNullException ("endpoint"); ThrowIfDisposedOrImmutable (); if (endpoint.Address == null) throw new ArgumentException ("Address on the argument endpoint is null"); if (endpoint.Contract == null) throw new ArgumentException ("Contract on the argument endpoint is null"); if (endpoint.Binding == null) throw new ArgumentException ("Binding on the argument endpoint is null"); if (!ImplementedContracts.Values.Any (cd => cd.ContractType == endpoint.Contract.ContractType) && endpoint.Binding.Namespace != "http://schemas.microsoft.com/ws/2005/02/mex/bindings") // special case throw new InvalidOperationException (String.Format ("Contract '{0}' is not implemented in this service '{1}'", endpoint.Contract.Name, Description.Name)); Description.Endpoints.Add (endpoint); } #endif Type PopulateType (string typeName) { Type type = Type.GetType (typeName); if (type != null) return type; foreach (ContractDescription cd in ImplementedContracts.Values) { type = cd.ContractType.Assembly.GetType (typeName); if (type != null) return type; } return null; } ContractDescription mex_contract, help_page_contract; ContractDescription GetContract (string name, bool mexBinding) { // FIXME: not sure if they should really be special cases. switch (name) { case "IHttpGetHelpPageAndMetadataContract": if (help_page_contract == null) help_page_contract = ContractDescription.GetContract (typeof (IHttpGetHelpPageAndMetadataContract)); return help_page_contract; case "IMetadataExchange": // this is certainly looking special (or we may // be missing something around ServiceMetadataExtension). // It seems .NET WCF has some "infrastructure" // endpoints. .NET ServiceHost fails to Open() // if it was added only IMetadataExchange // endpoint (and you'll see the word // "infrastructure" in the exception message). if (mexBinding && Description.Behaviors.Find () == null) break; if (mex_contract == null) mex_contract = ContractDescription.GetContract (typeof (IMetadataExchange)); return mex_contract; } Type type = PopulateType (name); if (type == null) return null; foreach (ContractDescription cd in ImplementedContracts.Values) { // This check is a negative side effect of the above match-by-name design. if (cd.ContractType == typeof (IMetadataExchange)) continue; if (cd.ContractType == type || cd.ContractType.IsSubclassOf (type) || type.IsInterface && cd.ContractType.GetInterface (type.FullName) == type) return cd; } return null; } internal Uri BuildAbsoluteUri (Uri address, Binding binding) { if (!address.IsAbsoluteUri) { // Find a Base address with matching scheme, // and build new absolute address if (!base_addresses.Contains (binding.Scheme)) throw new InvalidOperationException (String.Format ("Could not find base address that matches Scheme {0} for endpoint {1}", binding.Scheme, binding.Name)); Uri baseaddr = base_addresses [binding.Scheme]; if (!baseaddr.AbsoluteUri.EndsWith ("/") && address.OriginalString.Length > 0) // with empty URI it should not add '/' to possible file name of the absolute URI baseaddr = new Uri (baseaddr.AbsoluteUri + "/"); address = new Uri (baseaddr, address); } return address; } internal ServiceEndpoint AddServiceEndpointCore ( ContractDescription cd, Binding binding, EndpointAddress address, Uri listenUri) { if (listenUri != null) listenUri = BuildAbsoluteUri (listenUri, binding); foreach (ServiceEndpoint e in Description.Endpoints) if (e.Contract == cd && e.Binding == binding && e.Address == address && e.ListenUri.Equals (listenUri)) return e; ServiceEndpoint se = new ServiceEndpoint (cd, binding, address); // FIXME: should we reject relative ListenUri? se.ListenUri = listenUri ?? address.Uri; Description.Endpoints.Add (se); return se; } protected virtual void ApplyConfiguration () { if (Description == null) throw new InvalidOperationException ("ApplyConfiguration requires that the Description property be initialized. Either provide a valid ServiceDescription in the CreateDescription method or override the ApplyConfiguration method to provide an alternative implementation"); ServiceElement service = GetServiceElement (); if (service != null) LoadConfigurationSection (service); #if NET_4_0 // simplified configuration AddServiceBehaviors (String.Empty, false); #endif // TODO: consider commonBehaviors here // ensure ServiceAuthorizationBehavior Authorization = Description.Behaviors.Find (); if (Authorization == null) { Authorization = new ServiceAuthorizationBehavior (); Description.Behaviors.Add (Authorization); } // ensure ServiceDebugBehavior ServiceDebugBehavior debugBehavior = Description.Behaviors.Find (); if (debugBehavior == null) { debugBehavior = new ServiceDebugBehavior (); Description.Behaviors.Add (debugBehavior); } } void AddServiceBehaviors (string configurationName, bool throwIfNotFound) { #if NET_4_0 if (configurationName == null) return; #else if (String.IsNullOrEmpty (configurationName)) return; #endif ServiceBehaviorElement behavior = ConfigUtil.BehaviorsSection.ServiceBehaviors [configurationName]; if (behavior == null) { if (throwIfNotFound) throw new ArgumentException (String.Format ("Service behavior configuration '{0}' was not found", configurationName)); return; } KeyedByTypeCollection behaviors = Description.Behaviors; foreach (var bxe in behavior) { IServiceBehavior b = (IServiceBehavior) bxe.CreateBehavior (); if (behaviors.Contains (b.GetType ())) continue; behaviors.Add (b); } } void ApplyServiceElement (ServiceElement service) { //base addresses HostElement host = service.Host; foreach (BaseAddressElement baseAddress in host.BaseAddresses) { AddBaseAddress (new Uri (baseAddress.BaseAddress)); } // behaviors AddServiceBehaviors (service.BehaviorConfiguration, true); // endpoints foreach (ServiceEndpointElement endpoint in service.Endpoints) { ServiceEndpoint se; #if NET_4_0 var binding = String.IsNullOrEmpty (endpoint.Binding) ? null : ConfigUtil.CreateBinding (endpoint.Binding, endpoint.BindingConfiguration); if (!String.IsNullOrEmpty (endpoint.Kind)) { var contract = String.IsNullOrEmpty (endpoint.Contract) ? null : GetContract (endpoint.Contract, false); se = ConfigUtil.ConfigureStandardEndpoint (contract, endpoint); if (se.Binding == null) se.Binding = binding; if (se.Address == null && se.Binding != null) // standard endpoint might have empty address se.Address = new EndpointAddress (CreateUri (se.Binding.Scheme, endpoint.Address)); if (se.Binding == null && se.Address != null) // look for protocol mapping se.Binding = ConfigUtil.GetBindingByProtocolMapping (se.Address.Uri); AddServiceEndpoint (se); } else { if (binding == null && endpoint.Address != null) // look for protocol mapping binding = ConfigUtil.GetBindingByProtocolMapping (endpoint.Address); se = AddServiceEndpoint (endpoint.Contract, binding, endpoint.Address); } #else var binding = ConfigUtil.CreateBinding (endpoint.Binding, endpoint.BindingConfiguration); se = AddServiceEndpoint (endpoint.Contract, binding, endpoint.Address); #endif // endpoint behaviors EndpointBehaviorElement epbehavior = ConfigUtil.BehaviorsSection.EndpointBehaviors [endpoint.BehaviorConfiguration]; if (epbehavior != null) foreach (var bxe in epbehavior) { IEndpointBehavior b = (IEndpointBehavior) bxe.CreateBehavior (); se.Behaviors.Add (b); } } } private ServiceElement GetServiceElement() { Type serviceType = Description.ServiceType; if (serviceType == null) return null; return ConfigUtil.ServicesSection.Services [serviceType.FullName]; } protected abstract ServiceDescription CreateDescription ( out IDictionary implementedContracts); protected void InitializeDescription (UriSchemeKeyedCollection baseAddresses) { this.base_addresses = baseAddresses; IDictionary retContracts; description = CreateDescription (out retContracts); contracts = retContracts; ApplyConfiguration (); } protected virtual void InitializeRuntime () { //First validate the description, which should call all behaviors //'Validate' method. ValidateDescription (); //Build all ChannelDispatchers, one dispatcher per user configured EndPoint. //We must keep thet ServiceEndpoints as a seperate collection, since the user //can change the collection in the description during the behaviors events. ServiceEndpoint[] endPoints = new ServiceEndpoint[Description.Endpoints.Count]; Description.Endpoints.CopyTo (endPoints, 0); var builder = new DispatcherBuilder (this); foreach (ServiceEndpoint se in endPoints) { var commonParams = new BindingParameterCollection (); foreach (IServiceBehavior b in Description.Behaviors) b.AddBindingParameters (Description, this, Description.Endpoints, commonParams); var channel = builder.BuildChannelDispatcher (Description.ServiceType, se, commonParams); if (!ChannelDispatchers.Contains (channel)) ChannelDispatchers.Add (channel); } //After the ChannelDispatchers are created, and attached to the service host //Apply dispatching behaviors. // // This behavior application order is tricky: first only // ServiceDebugBehavior and ServiceMetadataBehavior are // applied, and then other service behaviors are applied. // It is because those two behaviors adds ChannelDispatchers // and any other service behaviors must be applied to // those newly populated dispatchers. foreach (IServiceBehavior b in Description.Behaviors) if (b is ServiceMetadataBehavior || b is ServiceDebugBehavior) b.ApplyDispatchBehavior (Description, this); foreach (IServiceBehavior b in Description.Behaviors) if (!(b is ServiceMetadataBehavior || b is ServiceDebugBehavior)) b.ApplyDispatchBehavior (Description, this); builder.ApplyDispatchBehaviors (); } private void ValidateDescription () { foreach (IServiceBehavior b in Description.Behaviors) b.Validate (Description, this); foreach (ServiceEndpoint endPoint in Description.Endpoints) endPoint.Validate (); #if NET_4_0 // In 4.0, it seems that if there is no configured ServiceEndpoint, infer them from the service type. if (Description.Endpoints.Count == 0) { foreach (Type iface in Description.ServiceType.GetInterfaces ()) if (iface.GetCustomAttributes (typeof (ServiceContractAttribute), true).Length > 0) foreach (var baddr in BaseAddresses) { if (!baddr.IsAbsoluteUri) continue; var binding = ConfigUtil.GetBindingByProtocolMapping (baddr); if (binding == null) continue; AddServiceEndpoint (iface.FullName, binding, baddr); } } #endif if (Description.Endpoints.FirstOrDefault (e => e.Contract != mex_contract && !e.IsSystemEndpoint) == null) throw new InvalidOperationException ("The ServiceHost must have at least one application endpoint (that does not include metadata exchange endpoint) defined by either configuration, behaviors or call to AddServiceEndpoint methods."); } protected void LoadConfigurationSection (ServiceElement element) { ApplyServiceElement (element); } protected override sealed void OnAbort () { OnCloseOrAbort (TimeSpan.Zero); } Action close_delegate; Action open_delegate; protected override sealed IAsyncResult OnBeginClose ( TimeSpan timeout, AsyncCallback callback, object state) { if (close_delegate != null) close_delegate = new Action (OnClose); return close_delegate.BeginInvoke (timeout, callback, state); } protected override sealed IAsyncResult OnBeginOpen ( TimeSpan timeout, AsyncCallback callback, object state) { if (open_delegate == null) open_delegate = new Action (OnOpen); return open_delegate.BeginInvoke (timeout, callback, state); } protected override void OnClose (TimeSpan timeout) { OnCloseOrAbort (timeout); } void OnCloseOrAbort (TimeSpan timeout) { DateTime start = DateTime.Now; ReleasePerformanceCounters (); List l = new List (ChannelDispatchers); foreach (ChannelDispatcherBase e in l) { try { TimeSpan ts = timeout - (DateTime.Now - start); if (ts < TimeSpan.Zero) e.Abort (); else e.Close (ts); } catch (Exception ex) { Console.WriteLine ("ServiceHostBase failed to close the channel dispatcher:"); Console.WriteLine (ex); } } } protected override sealed void OnOpen (TimeSpan timeout) { DateTime start = DateTime.Now; InitializeRuntime (); for (int i = 0; i < ChannelDispatchers.Count; i++) { // Skip ServiceMetadataExtension-based one. special case. for (int j = i + 1; j < ChannelDispatchers.Count; j++) { var cd1 = ChannelDispatchers [i]; var cd2 = ChannelDispatchers [j]; if (cd1.IsMex || cd2.IsMex) continue; // surprisingly, some ChannelDispatcherBase implementations have null Listener property. if (cd1.Listener != null && cd2.Listener != null && cd1.Listener.Uri.Equals (cd2.Listener.Uri)) throw new InvalidOperationException ("Two or more service endpoints with different Binding instance are bound to the same listen URI."); } } var waits = new List (); foreach (var cd in ChannelDispatchers) { var wait = new ManualResetEvent (false); cd.Opened += delegate { wait.Set (); }; waits.Add (wait); cd.Open (timeout - (DateTime.Now - start)); } WaitHandle.WaitAll (waits.ToArray ()); } protected override void OnEndClose (IAsyncResult result) { if (close_delegate == null) throw new InvalidOperationException ("Async close operation has not started"); close_delegate.EndInvoke (result); } protected override sealed void OnEndOpen (IAsyncResult result) { if (open_delegate == null) throw new InvalidOperationException ("Aync open operation has not started"); open_delegate.EndInvoke (result); } protected override void OnOpened () { base.OnOpened (); } [MonoTODO] protected void ReleasePerformanceCounters () { } void IDisposable.Dispose () { Close (); } } /// /// Builds ChannelDispatchers as appropriate to service the service endpoints. /// partial class DispatcherBuilder { ServiceHostBase host; public DispatcherBuilder (ServiceHostBase host) { this.host = host; } Dictionary built_dispatchers = new Dictionary (); Dictionary ep_to_dispatcher_ep = new Dictionary (); internal static Action ChannelDispatcherSetter; internal ChannelDispatcher BuildChannelDispatcher (Type serviceType, ServiceEndpoint se, BindingParameterCollection commonParams) { //Let all behaviors add their binding parameters AddBindingParameters (commonParams, se); // See if there's an existing channel that matches this endpoint var version = se.Binding.GetProperty (commonParams); if (version == null) throw new InvalidOperationException ("At least one BindingElement in the Binding must override GetProperty method to return a MessageVersion and no prior binding element should return null instead of calling GetInnerProperty method on BindingContext."); ChannelDispatcher cd = FindExistingDispatcher (se); EndpointDispatcher ep; if (cd != null) { ep = cd.InitializeServiceEndpoint (serviceType, se); } else { // Use the binding parameters to build the channel listener and Dispatcher. lock (HttpTransportBindingElement.ListenerBuildLock) { ServiceHostBase.CurrentServiceHostHack = host; IChannelListener lf = BuildListener (se, commonParams); cd = new ChannelDispatcher (lf, se.Binding.Name); cd.MessageVersion = version; if (ChannelDispatcherSetter != null) { ChannelDispatcherSetter (cd); ChannelDispatcherSetter = null; } ServiceHostBase.CurrentServiceHostHack = null; } ep = cd.InitializeServiceEndpoint (serviceType, se); built_dispatchers.Add (se.Binding, cd); } ep_to_dispatcher_ep[se] = ep; return cd; } ChannelDispatcher FindExistingDispatcher (ServiceEndpoint se) { return built_dispatchers.FirstOrDefault ((KeyValuePair p) => se.Binding == p.Key).Value; } internal void ApplyDispatchBehaviors () { foreach (KeyValuePair val in ep_to_dispatcher_ep) ApplyDispatchBehavior (val.Value, val.Key); } private void ApplyDispatchBehavior (EndpointDispatcher ed, ServiceEndpoint endPoint) { foreach (IContractBehavior b in endPoint.Contract.Behaviors) b.ApplyDispatchBehavior (endPoint.Contract, endPoint, ed.DispatchRuntime); foreach (IEndpointBehavior b in endPoint.Behaviors) b.ApplyDispatchBehavior (endPoint, ed); foreach (OperationDescription operation in endPoint.Contract.Operations) { if (operation.InCallbackContract) continue; // irrelevant foreach (IOperationBehavior b in operation.Behaviors) b.ApplyDispatchBehavior (operation, ed.DispatchRuntime.Operations [operation.Name]); } } private void AddBindingParameters (BindingParameterCollection commonParams, ServiceEndpoint endPoint) { commonParams.Add (ChannelProtectionRequirements.CreateFromContract (endPoint.Contract)); foreach (IContractBehavior b in endPoint.Contract.Behaviors) b.AddBindingParameters (endPoint.Contract, endPoint, commonParams); foreach (IEndpointBehavior b in endPoint.Behaviors) b.AddBindingParameters (endPoint, commonParams); foreach (OperationDescription operation in endPoint.Contract.Operations) { foreach (IOperationBehavior b in operation.Behaviors) b.AddBindingParameters (operation, commonParams); } } static IChannelListener BuildListener (ServiceEndpoint se, BindingParameterCollection pl) { Binding b = se.Binding; if (b.CanBuildChannelListener (pl)) return b.BuildChannelListener (se.ListenUri, "", se.ListenUriMode, pl); if (b.CanBuildChannelListener (pl)) return b.BuildChannelListener (se.ListenUri, "", se.ListenUriMode, pl); if (b.CanBuildChannelListener (pl)) return b.BuildChannelListener (se.ListenUri, "", se.ListenUriMode, pl); if (b.CanBuildChannelListener (pl)) return b.BuildChannelListener (se.ListenUri, "", se.ListenUriMode, pl); if (b.CanBuildChannelListener (pl)) return b.BuildChannelListener (se.ListenUri, "", se.ListenUriMode, pl); if (b.CanBuildChannelListener (pl)) return b.BuildChannelListener (se.ListenUri, "", se.ListenUriMode, pl); throw new InvalidOperationException ("None of the listener channel types is supported"); } } }