Total Pageviews

Wednesday, May 8, 2013

ASP.NET Compatibility Mode

http://blogs.msdn.com/b/wenlong/archive/2006/01/23/516041.aspx



Table of Content:
·         Mixed Transports Mode
·         Under the Hood
·         Opt-In for the Mode
·         HttpContext.Current
·         Globalization
·         Impersonation
·         Session State

Introduction

The biggest difference between traditional ASMX services and hosted WCF services is probably that the latter can be activated through non-HTTP protocols such as net.tcp and net.pipe by Windows Activation Service (WAS). ASMX services can be only activated through HTTP and they are tightly coupled with ASP.NET HTTP pipeline. And thus many ASP.NET features for ASMX services are HTTP-flavored. Such features include:
  • Authentication
  • Url/File authorization
  • Impersonation
  • Session state
  • Request cache
  • Globalization
  • Custom HttpModules
  • Etc
All of these features are related to the following two famous HTTP types:
  • System.Web.HttpApplication
  • System.Web.HttpContext

However, WCF services are designed to be transport independent. Even when they are hosted inside ASP.NET applications, the decoupling of the relationship to the HTTP-flavored features is required so that users are not confused. Also most of those ASP.NET features have counterparts in WCF. Nevertheless, WCF provides a mechanism to support smooth migration from ASMX to WCF by introducing two different hosting modes for WCF services:
  • Mixed Transports Mode
  • ASP.NET compatibility mode
I will describe these two modes in details in the following.

ASP.NET HTTP Pipeline

Let’s first take a quick look at ASP.NET HTTP pipeline. The following link contains great details about this concept:
ASP.NET relies on a pipeline of application (HttpApplication), module (IHttpModule), and handler (IHttpHandler) objects to provide flexibility and extensibility. An HTTP request comes into the pipeline and is examined and filtered by the objects and the response is generated and returned through the pipeline. The modules are registered to handle application events from its Init method. Here are some interesting events in the order in which they are raised:
  • BeginRequest
  • AuthenticateRequest
  • PostAuthenticateRequest
  • AuthorizeRequest
  • ResolveRequestCache
  • AcquireRequestState
  • PreRequestHandlerExecute
  • [Handlers]
  • PostRequestHandlerExecute
  • EndRequest
Modules are registered in the web.config in the <system.web/httpModules> section while handlers are registered in the <system.web/httpHandlers> section. More information about how the events are handled by modules can be found in the following link:
Besides performing request filtering, modules can also intercept the request from the pipeline by calling HttpApplication.CompleteRequest.

Two Modes

Hosted WCF services can be running in two different modes: Mixed Transports Mode vs ASP.NET Compatibility Mode. This is controlled by the application-level configuration flag “aspNetCompatibilityEnabled”:
<system.serviceModel>
<serviceHostingEnvironment aspNetCompatibilityEnabled="false"/>
</system.serviceModel>
This flag is false by default and thus WCF services are running in the Mixed Transports Mode. You can also get this flag at runtime from the static property ServiceHostingEnvironment.AspNetCompatibilityEnabled.

Mixed Transports Mode

In this mode, WCF services do not have ASP.NET HTTP-specific features. Instead, all transports are treated in the same way. A WCF service can have multiple endpoints and they can listen on the same or different transports such as HTTP, net.tcp, net.pipe, or net.msmq.

The disabled ASP.NET HTTP features are:
  • HttpContext.Current: This is always null in this mode. For ASMX services, this is a ThreadStatic property that is stored in the Thread Local Store (TLS). WCF provides a counterpart to this feature: OperationContext.Current.
  • File/Url Authorization: This transport layer authorization feature is normally enabled through the <system.web/authorization> section in web.config for ASMX sevices. It is disabled in the mixed mode in WCF. You need to use message-level authorization implemented in WCF.
  • Impersonation: The ASP.NET impersonation feature is normally enabled through the <system.web/identity> section in web.config for ASMX services. It is not available in WCF transport layer in the mixed mode.
  • Session state: The ASP.NET session state is not supported in the mixed mode. WCF has its own reliable session implementation which provides flexible session state management. The biggest problem for WCF V1 is that the session state does not survive on application recycling and does not work in web garden/farm cases because there is not shared state mechanism between applications or processes which, however, are supported by ASP.NET state service.
  • Other HTTP features: There are other HTTP features which rely on HttpContext.Current are not supported in the mixed mode naturally. For example, you can not expect that you can get the correct result from ConfigurationManager.AppSettings to work. The globalization feature through <system.web/globalization> is not available.
However, hosted WCF services still rely on ASP.NET to provide the hosting environment, application configuration, and deployment. So the following features apply to all transports:
  • HostingEnvironment: The ASP.NET provides the hosting environment to WCF services. Accordingly, the settings under <system.web/hostingEnvironment> apply to WCF services.
  • Compilation system: WCF services are compiled by the BuildManager of ASP.NET. So the settings under <system.web/compilation> controls the compilation of .svc files.

Under the Hood

The ASP.NET compatibility mode is enabled when the aspNetCompatibilityEnabled flag is set to true. I will discuss this in details in the next section. Let’s briefly take a look how the two modes work for HTTP under the hook.

In order to achieve the two modes when hosted in ASP.NET, WCF WebHost layer hooks up into the ASP.NET HTTP pipeline through both module and handler for the HTTP transport. The implementations of both the module and the handler are registered in the machine-wide web.config.

In the mixed mode, the module intercepts the request in the early stage of the pipeline: BeginRequest. When the request comes in, it nulls out HttpContext.Current and reverts the impersonation if the thread is impersonated. Since the request is intercepted at this early stage, other HTTP features are disabled automatically.

In the ASP.NET compatibility mode, however, the module works as a simple request filter. It simply checks the aspNetCompatibilityEnabled flag and skips the request when the flag is true. The handler is finally invoked later in the pipeline to perform the real work for WCF services.

ASP.NET Compatibility Mode

In this mode, WCF services work similarly as ASMX services. That is, all of the existing ASP.NET features are supported in this mode. In this case, it would be confusing to users if other protocols are enabled for the service while those features only work for the HTTP path. So when a WCF service is being activated, the endpoints are checked to ensure that none of them have non-HTTP protocols enabled.

Opt-In for the Mode

Besides the application-level flag aspNetCompatibilityEnabled, users still need to explicitly opt-in for the compatibility mode on the WCF service by using the service attribute AspNetCompatibilityRequirements as following:
[ServiceBehavior]
[AspNetCompatibilityRequirements(RequirementsMode=AspNetCompatibilityRequirementsMode.Allowed)]
class BarService : IHelloContract
{
    // ...
}
The RequirementsMode can have three different values: NotAllowed, Allowed, or Required. The default value is NotAllowed. In this way, users have to explicitly design a WCF service to be running in the compatibility mode and understand the disparities for the two different hosting modes.

When the compatibility mode is turned on, most of the ASP.NET features are automatically supported for WCF services, but there are a few special cases need special treatment. I will describe them below.

HttpContext.Current

Users can enjoy this good feature in the compatibility mode as for ASMX services. For example, you can inspect the current request, check the current application settings, and get config information. In ASP.NET 2.0, ConfigurationManager uses HttpContext.Current to retrieve configuration data. So you can use the following features seamlessly:
  • ConfigurationManager.AppSettings
  • ConfigurationManager.GetSection (which works the same as HttpContext.Current.GetSection)

WCF has a very flexible threading model to support asynchronous programming. Service operations are usually executed on a different thread than the request thread from ASP.NET. So internally WCF lets HttpContext.Current to flow from thread to thread in the scope of service operations.

Globalization

You can set thread cultures through the Globalization section under <system.web> in web.config. The culture information also flows between thread switches.

Impersonation

ASP.NET supports impersonation (see the link for more details) through the following config setting:
<system.web>
    <identity impersonate="true"/>
</system.web>
Supporting this for WCF services is a little bit tricky. WCF has its own impersonation mechanism. Here is how you can set the impersonation flag for a WCF operation:
[OperationBehavior(Impersonation = ImpersonationOption.Required)]
public string Hello(string greeting)
{
    // ...
}
The ImpersonationOption is a three-value enum just as AspNetCompatibilityRequirementsMode: NotAllowed, Allowed, and Required. The default value is NotAllowed. If it is Allowed, you can turn on service-level impersonation from the following config flag “impersonateCallerForAllOperations”:
<behavior name="MyImp">
<serviceAuthorization impersonateCallerForAllOperations="false"/>
</behavior>
There are two cases when a service operation is enabled for WCF impersonation:
  • The operation has OperationBehavior.Impersonation set to “Required”.
  • The operation has OperationBehavior.Impersonation set to “Allowed” and the flag “impersonatedCallerForAllOperations” is set to true.

To achieve best user experience, WCF chooses to support impersonation in the compatibility mode in the following combined way:
WCF impersonation always wins over ASP.NET impersonation. That is, if WCF impersonation is enabled for the service operation, the ASP.NET impersonation is ignored. Otherwise, ASP.NET impersonation setting is used.

Session State

The session state feature for ASP.NET is probably the most important reason why people want to run WCF services in the compatibility mode. Even though WCF V1 implements reliable session in the product, the session state is totally in memory and thus it does not survive when the AppDomain is recycled. It is also not persisted across different processes or servers for Web Garden and Web Farm scenarios.

By running WCF services in the compatibility mode, you can enjoy the full functionalities of ASP.NET session state. You can use InProc, or SqlServer, or StateServer modes to handle state persistence. The following link contains good information on how to use ASP.NET session state in different modes:

By default, WCF does not have cookie enabled on the client side. So when the server requires Cookie, you need to turn on the cookie on the binding for the client through the property HttpTransportBindingElement.AllowCookies.

Sample Code

Here is the sample config for this topic (web.config):
<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <appSettings>
        <add key="Fruit" value="Apple"/>
        <add key="Cookie" value="Saltine Cracker"/>
    </appSettings>
    <system.web>
        <compilation batch="true" debug="true"/>
        <sessionState cookieless="UseCookies"  mode="InProc"/>
        <identity impersonate="true"/>
    </system.web>
    <system.serviceModel>
        <services>
            <service name="HelloWorld.BarService"behaviorConfiguration="MyImp" >
                <endpoint binding="basicHttpBinding"bindingConfiguration="BarBinding"
                          contract="HelloWorld.IHelloContract" />
            </service>
        </services>
        <bindings>
            <basicHttpBinding>
                <binding name="Binding1" maxReceivedMessageSize="30000"/>
                <binding name="BarBinding">
                    <security mode="TransportCredentialOnly">
                        <transport clientCredentialType="Windows"/>
                    </security>
                </binding>
            </basicHttpBinding>
        </bindings>
        <behaviors>
        <behavior name="MyImp">
            <serviceAuthorizationimpersonateCallerForAllOperations="false"/>
        </behavior>
        </behaviors>
        <serviceHostingEnvironment aspNetCompatibilityEnabled="true"/>
    </system.serviceModel>
</configuration>

Here is the service code (Service.svc):
<%@ServiceHost Language = "C#" Debug="true" Service="HelloWorld.BarService"%>
using System;
using System.ServiceModel;
using System.Configuration;
using System.Web;
using System.Web.Configuration;
using System.ServiceModel.Configuration;
using System.Text;
using System.Security.Principal;

namespace HelloWorld
{
    [ServiceContract]
    public interface IHelloContract
    {
        [OperationContract]
        string WhoIAm();

        [OperationContract]
        string WhoIAm2();
       
        [OperationContract]
        string Hello(string greeting);
    }

    [ServiceBehavior(InstanceContextMode=InstanceContextMode.PerCall)]
   [AspNetCompatibilityRequirements(RequirementsMode=AspNetCompatibilityRequirementsMode.Allowed)]
    class BarService : IHelloContract
    {
        public string WhoIAm()
        {
            return WindowsIdentity.GetCurrent().Name;
        }

        [OperationBehavior(Impersonation = ImpersonationOption.Required)]
        public string WhoIAm2()
        {
            return WindowsIdentity.GetCurrent().Name;
        }

        public string Hello(string greeting)
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendFormat("[From Bar Service] You said: {0}\r\n", greeting);

            // Working with AppSettings
            sb.AppendFormat("Bob likes the fruit '{0}'\r\n",ConfigurationManager.AppSettings["Fruit"]);
            sb.AppendFormat("He also likes the cookie '{0}'\r\n",ConfigurationManager.AppSettings["Cookie"]);

            // Working with other config data
            BindingsSectionGroup bsg = (BindingsSectionGroup)ConfigurationManager.GetSection("system.serviceModel/bindings");
            foreach (BasicHttpBindingElement be inbsg.BasicHttpBinding.Bindings)
                sb.AppendFormat("Binding: {0}, MaxReceivedMessageSize: {1}\r\n", be.Name, be.MaxReceivedMessageSize);

            // Working with Session state
            int myCount = 0;
            if (HttpContext.Current.Session["MyCount"] != null)
                myCount = (int)HttpContext.Current.Session["MyCount"];

            HttpContext.Current.Session["MyCount"] = ++myCount;
            sb.AppendFormat("MyCount is '{0}'\r\n", myCount);

            return sb.ToString();
        }
    }
}

Here is the client code (Client.cs):
using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.ServiceModel;

namespace HelloWorldClient
{
    [ServiceContract]
    public interface IHelloContract
    {
        [OperationContract]
        string WhoIAm();

        [OperationContract]
        string WhoIAm2();

        [OperationContract]
        string Hello(string greeting);
    }
   
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                if (args.Length != 1) {
                    Console.WriteLine("Usage: {0} serverUrl",
                       System.Reflection.Assembly.GetExecutingAssembly().GetName().Name);
                    return;
                }

                string fixedUrl = args[0];
                HttpTransportBindingElement be = newHttpTransportBindingElement();
                be.AuthenticationScheme = AuthenticationSchemes.Negotiate;
                be.AllowCookies = true// Enable cookies

                TextMessageEncodingBindingElement tmbe = newTextMessageEncodingBindingElement(MessageVersion.Soap11WSAddressing10,
                    Encoding.UTF8);
                CustomBinding binding = new CustomBinding(tmbe, be);
                ChannelFactory<IHelloContract> channelFactory = newChannelFactory<IHelloContract>(
                    binding, new EndpointAddress(fixedUrl));

                IHelloContract clientService = channelFactory.CreateChannel();

                Console.Write("Type anything to send: ");
                string greeting = Console.ReadLine();

                Console.WriteLine("Am I {0}?", clientService.WhoIAm());
                Console.WriteLine("Am I {0}?", clientService.WhoIAm2());
                for (int i = 0; i < 2; i++)
                    Console.WriteLine(clientService.Hello(greeting));

                ((IChannel)clientService).Close();
                channelFactory.Close();
            }
            catch (Exception ex) {
                Console.WriteLine(ex.ToString());
            }
        }
    }
}

The output for this sample looks like:
Type anything to send: Howdy
Am I Bill?
Am I Bill?
[From Bar Service] You said: Howdy
Bob likes the fruit 'Apple'
He also likes the cookie 'Saltine Cracker'
Binding: Binding1, MaxReceivedMessageSize: 30000
Binding: BarBinding, MaxReceivedMessageSize: 65536
MyCount is '1'

[From Bar Service] You said: Howdy
Bob likes the fruit 'Apple'
He also likes the cookie 'Saltine Cracker'
Binding: Binding1, MaxReceivedMessageSize: 30000
Binding: BarBinding, MaxReceivedMessageSize: 65536
MyCount is '2'

No comments:

Post a Comment