File: System\ServiceModel\Description\ServiceMetadataBehavior.cs
Project: ndp\cdf\src\WCF\ServiceModel\System.ServiceModel.csproj (System.ServiceModel)
//-----------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation.  All rights reserved.
//-----------------------------------------------------------------------------
 
namespace System.ServiceModel.Description
{
    using System.Collections;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.ComponentModel;
    using System.Diagnostics;
    using System.Runtime;
    using System.Runtime.Diagnostics;
    using System.ServiceModel.Channels;
    using System.ServiceModel.Diagnostics;
    using System.ServiceModel.Dispatcher;
    using System.Xml;
 
    public class ServiceMetadataBehavior : IServiceBehavior
    {
        public const string MexContractName = "IMetadataExchange";
        internal const string MexContractNamespace = "http://schemas.microsoft.com/2006/04/mex";
 
        static readonly Uri emptyUri = new Uri(String.Empty, UriKind.Relative);
 
        bool httpGetEnabled = false;
        bool httpsGetEnabled = false;
        Uri httpGetUrl;
        Uri httpsGetUrl;
        Binding httpGetBinding;
        Binding httpsGetBinding;
        Uri externalMetadataLocation = null;
        MetadataExporter metadataExporter = null;
 
        static ContractDescription mexContract = null;
        static object thisLock = new object();
 
        public bool HttpGetEnabled
        {
            get { return this.httpGetEnabled; }
            set { this.httpGetEnabled = value; }
        }
 
        [TypeConverter(typeof(UriTypeConverter))]
        public Uri HttpGetUrl
        {
            get { return this.httpGetUrl; }
            set
            {
                if (value != null && value.IsAbsoluteUri && value.Scheme != Uri.UriSchemeHttp)
                {
                    throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgument(SR.GetString(SR.SFxServiceMetadataBehaviorUrlMustBeHttpOrRelative,
                            "HttpGetUrl", Uri.UriSchemeHttp, value.ToString(), value.Scheme));
                }
                this.httpGetUrl = value;
            }
        }
 
        public bool HttpsGetEnabled
        {
            get { return this.httpsGetEnabled; }
            set { this.httpsGetEnabled = value; }
        }
 
        [TypeConverter(typeof(UriTypeConverter))]
        public Uri HttpsGetUrl
        {
            get { return this.httpsGetUrl; }
            set
            {
                if (value != null && value.IsAbsoluteUri && value.Scheme != Uri.UriSchemeHttps)
                {
                    throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgument(SR.GetString(SR.SFxServiceMetadataBehaviorUrlMustBeHttpOrRelative,
                        "HttpsGetUrl", Uri.UriSchemeHttps, value.ToString(), value.Scheme));
                }
 
                this.httpsGetUrl = value;
            }
        }
 
        public Binding HttpGetBinding
        {
            get { return this.httpGetBinding; }
            set
            {
                if (value != null)
                {
                    if (!value.Scheme.Equals(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase))
                    {
                        throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgument(SR.GetString(SR.SFxBindingSchemeDoesNotMatch,
                            value.Scheme, value.GetType().ToString(), Uri.UriSchemeHttp));
                    }
                    CustomBinding customBinding = new CustomBinding(value);
                    TextMessageEncodingBindingElement textMessageEncodingBindingElement = customBinding.Elements.Find<TextMessageEncodingBindingElement>();
                    if (textMessageEncodingBindingElement != null && !textMessageEncodingBindingElement.MessageVersion.IsMatch(MessageVersion.None))
                    {
                        throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgument(SR.GetString(SR.SFxIncorrectMessageVersion,
                            textMessageEncodingBindingElement.MessageVersion.ToString(), MessageVersion.None.ToString()));
                    }
                    HttpTransportBindingElement httpTransportBindingElement = customBinding.Elements.Find<HttpTransportBindingElement>();
                    if (httpTransportBindingElement != null)
                    {
                        httpTransportBindingElement.Method = "GET";
                    }
                    this.httpGetBinding = customBinding;
                }
            }
        }
 
        public Binding HttpsGetBinding
        {
            get { return this.httpsGetBinding; }
            set
            {
                if (value != null)
                {
                    if (!value.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
                    {
                        throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgument(SR.GetString(SR.SFxBindingSchemeDoesNotMatch,
                            value.Scheme, value.GetType().ToString(), Uri.UriSchemeHttps));
                    }
                    CustomBinding customBinding = new CustomBinding(value);
                    TextMessageEncodingBindingElement textMessageEncodingBindingElement = customBinding.Elements.Find<TextMessageEncodingBindingElement>();
                    if (textMessageEncodingBindingElement != null && !textMessageEncodingBindingElement.MessageVersion.IsMatch(MessageVersion.None))
                    {
                        throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgument(SR.GetString(SR.SFxIncorrectMessageVersion,
                            textMessageEncodingBindingElement.MessageVersion.ToString(), MessageVersion.None.ToString()));
                    }
                    HttpsTransportBindingElement httpsTransportBindingElement = customBinding.Elements.Find<HttpsTransportBindingElement>();
                    if (httpsTransportBindingElement != null)
                    {
                        httpsTransportBindingElement.Method = "GET";
                    }
                    this.httpsGetBinding = customBinding;
                }
            }
        }
 
        [TypeConverter(typeof(UriTypeConverter))]
 
        public Uri ExternalMetadataLocation
        {
            get { return this.externalMetadataLocation; }
            set
            {
                if (value != null && value.IsAbsoluteUri && !(value.Scheme == Uri.UriSchemeHttp || value.Scheme == Uri.UriSchemeHttps))
                {
                    throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgument("ExternalMetadataLocation", SR.GetString(SR.SFxBadMetadataLocationUri, value.OriginalString, value.Scheme));
                }
                this.externalMetadataLocation = value;
            }
        }
 
        public MetadataExporter MetadataExporter
        {
            get
            {
                if (this.metadataExporter == null)
                    this.metadataExporter = new WsdlExporter();
 
                return this.metadataExporter;
            }
            set
            {
                this.metadataExporter = value;
            }
        }
 
        static internal ContractDescription MexContract
        {
            get
            {
                EnsureMexContractDescription();
                return ServiceMetadataBehavior.mexContract;
            }
        }
 
        void IServiceBehavior.Validate(ServiceDescription description, ServiceHostBase serviceHostBase)
        {
        }
 
        void IServiceBehavior.AddBindingParameters(ServiceDescription description, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, BindingParameterCollection parameters)
        {
        }
 
        void IServiceBehavior.ApplyDispatchBehavior(ServiceDescription description, ServiceHostBase serviceHostBase)
        {
            if (description == null)
                throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull("description");
            if (serviceHostBase == null)
                throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull("serviceHostBase");
 
            ApplyBehavior(description, serviceHostBase);
        }
 
        void ApplyBehavior(ServiceDescription description, ServiceHostBase host)
        {
            ServiceMetadataExtension mex = ServiceMetadataExtension.EnsureServiceMetadataExtension(description, host);
            SetExtensionProperties(description, host, mex);
            CustomizeMetadataEndpoints(description, host, mex);
            CreateHttpGetEndpoints(description, host, mex);
        }
 
        private void CreateHttpGetEndpoints(ServiceDescription description, ServiceHostBase host, ServiceMetadataExtension mex)
        {
            bool httpDispatcherEnabled = false;
            bool httpsDispatcherEnabled = false;
 
            if (this.httpGetEnabled)
            {
                httpDispatcherEnabled = EnsureGetDispatcher(host, mex, this.httpGetUrl, Uri.UriSchemeHttp);
            }
 
            if (this.httpsGetEnabled)
            {
                httpsDispatcherEnabled = EnsureGetDispatcher(host, mex, this.httpsGetUrl, Uri.UriSchemeHttps);
            }
 
            if (!httpDispatcherEnabled && !httpsDispatcherEnabled)
            {
                if (this.httpGetEnabled)
                {
                    throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new InvalidOperationException(SR.GetString(SR.SFxServiceMetadataBehaviorNoHttpBaseAddress)));
                }
 
                if (this.httpsGetEnabled)
                {
                    throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new InvalidOperationException(SR.GetString(SR.SFxServiceMetadataBehaviorNoHttpsBaseAddress)));
                }
            }
        }
 
        static bool EnsureGetDispatcher(ServiceHostBase host, ServiceMetadataExtension mex, Uri url, string scheme)
        {
            Uri address = host.GetVia(scheme, url == null ? new Uri(string.Empty, UriKind.Relative) : url);
 
            if (address != null)
            {
                ChannelDispatcher channelDispatcher = mex.EnsureGetDispatcher(address, false /* isServiceDebugBehavior */);
                ((ServiceMetadataExtension.HttpGetImpl)channelDispatcher.Endpoints[0].DispatchRuntime.SingletonInstanceContext.UserObject).GetWsdlEnabled = true;
                return true;
            }
 
            return false;
        }
 
        void SetExtensionProperties(ServiceDescription description, ServiceHostBase host, ServiceMetadataExtension mex)
        {
            mex.ExternalMetadataLocation = this.ExternalMetadataLocation;
            mex.Initializer = new MetadataExtensionInitializer(this, description, host);
            mex.HttpGetEnabled = this.httpGetEnabled;
            mex.HttpsGetEnabled = this.httpsGetEnabled;
 
            mex.HttpGetUrl = host.GetVia(Uri.UriSchemeHttp, this.httpGetUrl == null ? new Uri(string.Empty, UriKind.Relative) : this.httpGetUrl);
            mex.HttpsGetUrl = host.GetVia(Uri.UriSchemeHttps, this.httpsGetUrl == null ? new Uri(string.Empty, UriKind.Relative) : this.httpsGetUrl);
 
            mex.HttpGetBinding = this.httpGetBinding;
            mex.HttpsGetBinding = this.httpsGetBinding;
 
            UseRequestHeadersForMetadataAddressBehavior dynamicUpdateBehavior = description.Behaviors.Find<UseRequestHeadersForMetadataAddressBehavior>();
            if (dynamicUpdateBehavior != null)
            {
                mex.UpdateAddressDynamically = true;
                mex.UpdatePortsByScheme = new Dictionary<string, int>(dynamicUpdateBehavior.DefaultPortsByScheme);
            }
 
            foreach (ChannelDispatcherBase dispatcherBase in host.ChannelDispatchers)
            {
                ChannelDispatcher dispatcher = dispatcherBase as ChannelDispatcher;
                if (dispatcher != null && IsMetadataTransferDispatcher(description, dispatcher))
                {
                    mex.MexEnabled = true;
                    mex.MexUrl = dispatcher.Listener.Uri;
                    if (dynamicUpdateBehavior != null)
                    {
                        foreach (EndpointDispatcher endpointDispatcher in dispatcher.Endpoints)
                        {
                            if (!endpointDispatcher.AddressFilterSetExplicit)
                            {
                                endpointDispatcher.AddressFilter = new MatchAllMessageFilter();
                            }
                        }
                    }
                    break;
                }
            }
 
        }
 
        private static void CustomizeMetadataEndpoints(ServiceDescription description, ServiceHostBase host, ServiceMetadataExtension mex)
        {
            for (int i = 0; i < host.ChannelDispatchers.Count; i++)
            {
                ChannelDispatcher channelDispatcher = host.ChannelDispatchers[i] as ChannelDispatcher;
                if (channelDispatcher != null && ServiceMetadataBehavior.IsMetadataTransferDispatcher(description, channelDispatcher))
                {
                    if (channelDispatcher.Endpoints.Count != 1)
                    {
                        throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(
                            new InvalidOperationException(SR.GetString(SR.SFxServiceMetadataBehaviorInstancingError, channelDispatcher.Listener.Uri, channelDispatcher.CreateContractListString())));
                    }
 
                    DispatchRuntime dispatcher = channelDispatcher.Endpoints[0].DispatchRuntime;
 
                    // set instancing
                    dispatcher.InstanceContextProvider =
                       InstanceContextProviderBase.GetProviderForMode(InstanceContextMode.Single, dispatcher);
 
                    bool isListeningOnHttps = channelDispatcher.Listener.Uri.Scheme == Uri.UriSchemeHttps;
                    Uri listenUri = channelDispatcher.Listener.Uri;
                    ServiceMetadataExtension.WSMexImpl impl = new ServiceMetadataExtension.WSMexImpl(mex, isListeningOnHttps, listenUri);
                    dispatcher.SingletonInstanceContext = new InstanceContext(host, impl, false);
                }
            }
        }
 
        static EndpointDispatcher GetListenerByID(SynchronizedCollection<ChannelDispatcherBase> channelDispatchers, string id)
        {
            for (int i = 0; i < channelDispatchers.Count; ++i)
            {
                ChannelDispatcher channelDispatcher = channelDispatchers[i] as ChannelDispatcher;
                if (channelDispatcher != null)
                {
                    for (int j = 0; j < channelDispatcher.Endpoints.Count; ++j)
                    {
                        EndpointDispatcher endpointDispatcher = channelDispatcher.Endpoints[j];
                        if (endpointDispatcher.Id == id)
                            return endpointDispatcher;
                    }
                }
            }
            return null;
        }
 
        internal static bool IsMetadataDispatcher(ServiceDescription description, ChannelDispatcher channelDispatcher)
        {
            foreach (EndpointDispatcher endpointDispatcher in channelDispatcher.Endpoints)
            {
                if (IsMetadataTransferDispatcher(description, channelDispatcher)
                    || IsHttpGetMetadataDispatcher(description, channelDispatcher))
                    return true;
            }
            return false;
        }
 
        static bool IsMetadataTransferDispatcher(ServiceDescription description, ChannelDispatcher channelDispatcher)
        {
            if (BehaviorMissingObjectNullOrServiceImplements(description, channelDispatcher))
                return false;
 
            foreach (EndpointDispatcher endpointDispatcher in channelDispatcher.Endpoints)
            {
                if (endpointDispatcher.ContractName == ServiceMetadataBehavior.MexContractName
                    && endpointDispatcher.ContractNamespace == ServiceMetadataBehavior.MexContractNamespace)
                    return true;
            }
            return false;
        }
 
        private static bool BehaviorMissingObjectNullOrServiceImplements(ServiceDescription description, object obj)
        {
            if (obj == null)
                return true;
            if (description.Behaviors != null && description.Behaviors.Find<ServiceMetadataBehavior>() == null)
                return true;
            if (description.ServiceType != null && description.ServiceType.GetInterface(typeof(IMetadataExchange).Name) != null)
                return true;
 
            return false;
        }
 
        internal static bool IsHttpGetMetadataDispatcher(ServiceDescription description, ChannelDispatcher channelDispatcher)
        {
            if (description.Behaviors.Find<ServiceMetadataBehavior>() == null)
                return false;
 
            foreach (EndpointDispatcher endpointDispatcher in channelDispatcher.Endpoints)
            {
                if (endpointDispatcher.ContractName == ServiceMetadataExtension.HttpGetImpl.ContractName
                    && endpointDispatcher.ContractNamespace == ServiceMetadataExtension.HttpGetImpl.ContractNamespace)
                    return true;
            }
            return false;
        }
 
        internal static bool IsMetadataEndpoint(ServiceDescription description, ServiceEndpoint endpoint)
        {
            if (BehaviorMissingObjectNullOrServiceImplements(description, endpoint))
                return false;
 
            return IsMetadataEndpoint(endpoint);
 
        }
 
        static bool IsMetadataEndpoint(ServiceEndpoint endpoint)
        {
            return (endpoint.Contract.Name == ServiceMetadataBehavior.MexContractName
                    && endpoint.Contract.Namespace == ServiceMetadataBehavior.MexContractNamespace);
        }
 
        internal static bool IsMetadataImplementedType(ServiceDescription description, Type type)
        {
            if (BehaviorMissingObjectNullOrServiceImplements(description, type))
                return false;
 
            return type == typeof(IMetadataExchange);
        }
 
        internal static bool IsMetadataImplementedType(Type type)
        {
            return type == typeof(IMetadataExchange);
        }
 
        internal void AddImplementedContracts(ServiceHostBase.ServiceAndBehaviorsContractResolver resolver)
        {
            if (!resolver.BehaviorContracts.ContainsKey(MexContractName))
            {
                resolver.BehaviorContracts.Add(MexContractName, ServiceMetadataBehavior.MexContract);
            }
        }
 
        static void EnsureMexContractDescription()
        {
            if (ServiceMetadataBehavior.mexContract == null)
            {
                lock (thisLock)
                {
                    if (ServiceMetadataBehavior.mexContract == null)
                    {
                        ServiceMetadataBehavior.mexContract = CreateMexContract();
                    }
                }
            }
        }
 
        static ContractDescription CreateMexContract()
        {
            ContractDescription mexContract = ContractDescription.GetContract(typeof(IMetadataExchange));
            foreach (OperationDescription operation in mexContract.Operations)
            {
                operation.Behaviors.Find<OperationBehaviorAttribute>().Impersonation = ImpersonationOption.Allowed;
            }
            mexContract.Behaviors.Add(new ServiceMetadataContractBehavior(true));
 
            return mexContract;
 
        }
 
        internal class MetadataExtensionInitializer
        {
            ServiceMetadataBehavior behavior;
            ServiceDescription description;
            ServiceHostBase host;
            Exception metadataGenerationException = null;
 
            internal MetadataExtensionInitializer(ServiceMetadataBehavior behavior, ServiceDescription description, ServiceHostBase host)
            {
                this.behavior = behavior;
                this.description = description;
                this.host = host;
            }
 
            internal MetadataSet GenerateMetadata()
            {
                if (this.behavior.ExternalMetadataLocation == null || this.behavior.ExternalMetadataLocation.ToString() == string.Empty)
                {
                    if (this.metadataGenerationException != null)
                        throw this.metadataGenerationException;
 
                    try
                    {
                        MetadataExporter exporter = this.behavior.MetadataExporter;
                        XmlQualifiedName serviceName = new XmlQualifiedName(this.description.Name, this.description.Namespace);
                        Collection<ServiceEndpoint> exportedEndpoints = new Collection<ServiceEndpoint>();
                        foreach (ServiceEndpoint endpoint in this.description.Endpoints)
                        {
                            ServiceMetadataContractBehavior contractBehavior = endpoint.Contract.Behaviors.Find<ServiceMetadataContractBehavior>();
 
                            // if contract behavior exists, generate metadata when the behavior allows metadata generation
                            // if contract behavior doesn't exist, generate metadata only for non system endpoints
                            if ((contractBehavior != null && !contractBehavior.MetadataGenerationDisabled) ||
                                (contractBehavior == null && !endpoint.IsSystemEndpoint))
                            {
                                EndpointAddress address = null;
                                EndpointDispatcher endpointDispatcher = GetListenerByID(this.host.ChannelDispatchers, endpoint.Id);
                                if (endpointDispatcher != null)
                                {
                                    address = endpointDispatcher.EndpointAddress;
                                }
                                ServiceEndpoint exportedEndpoint = new ServiceEndpoint(endpoint.Contract);
                                exportedEndpoint.Binding = endpoint.Binding;
                                exportedEndpoint.Name = endpoint.Name;
                                exportedEndpoint.Address = address;
                                foreach (IEndpointBehavior behavior in endpoint.Behaviors)
                                {
                                    exportedEndpoint.Behaviors.Add(behavior);
                                }
                                exportedEndpoints.Add(exportedEndpoint);
                            }
                        }
                        WsdlExporter wsdlExporter = exporter as WsdlExporter;
                        if (wsdlExporter != null)
                        {
                            // Pass the BindingParameterCollection into the ExportEndpoints method so that the binding parameters can be using to export WSDL correctly.
                            // The binding parameters are used in BuildChannelListener, during which they can modify the configuration of the channel in ways that might have to
                            // be communicated in the WSDL. For example, in the case of Multi-Auth, the AuthenticationSchemesBindingParameter is used during BuildChannelListener
                            // to set the AuthenticationSchemes supported by the virtual directory on the HttpTransportBindingElement.  These authentication schemes also need
                            // to be in the WSDL, so that clients know what authentication schemes are supported by the service.  (see CSDMain #180381)
                            Fx.Assert(this.host != null, "ServiceHostBase field on MetadataExtensionInitializer should never be null.");
                            wsdlExporter.ExportEndpoints(exportedEndpoints, serviceName, this.host.GetBindingParameters(exportedEndpoints));
                        }
                        else
                        {
                            foreach (ServiceEndpoint endpoint in exportedEndpoints)
                            {
                                exporter.ExportEndpoint(endpoint);
                            }
                        }
 
                        if (exporter.Errors.Count > 0 && DiagnosticUtility.ShouldTraceWarning)
                        {
                            TraceWsdlExportErrors(exporter);
                        }
 
                        return exporter.GetGeneratedMetadata();
                    }
                    catch (Exception e)
                    {
                        this.metadataGenerationException = e;
                        throw;
                    }
                }
                return null;
            }
 
            static void TraceWsdlExportErrors(MetadataExporter exporter)
            {
                foreach (MetadataConversionError error in exporter.Errors)
                {
                    if (DiagnosticUtility.ShouldTraceWarning)
                    {
                        Hashtable h = new Hashtable(2)
                        {
                            { "IsWarning", error.IsWarning },
                            { "Message", error.Message }
                        };
                        TraceUtility.TraceEvent(TraceEventType.Warning, TraceCode.WsmexNonCriticalWsdlExportError,
                            SR.GetString(SR.TraceCodeWsmexNonCriticalWsdlExportError), new DictionaryTraceRecord(h), null, null);
                    }
                }
            }
        }
 
    }
}