Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Maintain HTTP Session State in WCF REST Services with HttpWebRequest

4.81/5 (15 votes)
2 Apr 2013CPOL10 min read 112.5K   2.6K  
This article demonstrates how to maintain HTTP Session State in WCF REST Services when consumed from desktop applications, using the HttpWebRequest object.

Introduction

This article demonstrates how to maintain HTTP Session State in WCF REST Services when consumed from desktop applications using the HttpWebRequest object.

Background

REST has been a recent popular topic when developing light-weight service-oriented applications. Because of the origination of the "Representational state transfer" theory, it is a natural choice to host services in a web server, such as an IIS server. It is also a natural choice to use the HTTP Session State to keep the state of the services with each individual client. For example, it is ideal to keep the user's log-in information in the web session, so the service client does not need to send the user's credential every time a call is made. Generally speaking, there are two ways to consume the services:

  • REST services can be consumed from a web browser in JavaScript using AJAX calls. jQuery is one of the most popular JavaScript libraries that supports AJAX calls;
  • REST services can also be consumed from a desktop application using HTTP client libraries. The HttpWebRequest class is a popular choice in the Microsoft world.

To maintain a web session between the service and client, the client needs to send cookies to the service. When the service is called in an AJAX fashion, sending cookies to the service is the web browser's responsibility. But when the service is called from a desktop application, most of the client libraries do not send cookies by default. To maintain the HTTP Session State, we need to make some additional effort at the client side.

The purpose of this article is to show you an example of how to maintain web session when the service is called from desktop applications, with a simple extension of the HttpWebRequest class.

322436/SolutionExplorer.jpg

The attached Visual Studio 2010 solution has three projects:

  • The SharedLibraries project is a class library. It defines some classes shared by the REST service and the client to send data to each other. This project also implements a simple class factory to create HttpWebRequest objects. The HTTP Session State will be automatically maintained if we use the HttpWebRequest objects created by this class factory.
  • The Service project is an ASP.NET project. A simple REST service is implemented here.
  • The Client project is a WPF MVVM application. I will show you how to make calls to the REST service in this project. I will make two service calls. In one call, I will use an HttpWebRequest object created by the default WebRequest class factory. In the other call, I will use an HttpWebRequest object created by the class factory implemented in the SharedLibraries project. You will see that the session is lost in one call but maintained in the other.

I will first introduce the SharedLibraries project to show you the class factory to create the session enabled HttpWebRequest object and the shared classes used by both the service and the client. I will then introduce the "Service" project and the "Client" project to demonstrate how to use the class factory.

The Shared Libraries

322436/SolutionExplorerSharedLib.jpg

The SharedLibraries project is a simple Class Library project. It implements a class factory for us to create session enabled HttpWebRequest objects. It also implements some shared classes used by both the REST service and the client to send data to each other. Let us first take a look at the class library implemented in the CookiedRequestFactory.cs file.

C#
using System;
using System.Net;
using System.Collections.Generic;

namespace SharedLibraries.ClientUtilities
{
    public class CookiedRequestFactory
    {
        // This dictionary keeps all the cookie containers for
        // each domain.
        private static Dictionary<string, CookieContainer> containers
            = new Dictionary<string, CookieContainer>();
    
        public static HttpWebRequest CreateHttpWebRequest(string url)
        {
            // Create a HttpWebRequest object
            var request = (HttpWebRequest)WebRequest.Create(url);
    
            // this gets the dmain part of from the url
            string domain = (new Uri(url)).GetLeftPart(UriPartial.Authority);
    
            // try to get a container from the dictionary, if it is in the
            // dictionary, use it. Otherwise, create a new one and put it
            // into the dictionary and use it.
            CookieContainer container;
            if (!containers.TryGetValue(domain, out container))
            {
                container = new CookieContainer();
                containers[domain] = container;
            }
     
            // Assign the cookie container to the HttpWebRequest object
            request.CookieContainer = container;
    
            return request;
        }
    }
}
  • The static "factory method" CreateHttpWebRequest is for us to create the session enabled HttpWebRequest object.
  • The static Dictionary named containers keeps a CookieContainer for each domain that the application has sent some web request to. Given a URL, say "http://domainb.foo:8080/image.jpg", the domain of the URL is identified by "http://domainb.foo:8080", which includes the port number.

When an HttpWebRequest object is created, the factory method looks into the containers "Dictionary" to see if there is a corresponding CookieContainer. If there is one, the CookieContainer is associated to the HttpWebRequest object. Otherwise, a new CookieContainer is created. This CookieContainer is then associated to the HttpWebRequest object and added into the Dictionary. By doing this, the HttpWebRequest objects created by the factory method always have the same CookieContainer when calling the services in the same domain. This CookieContainer will store all the cookies received from the server. When a REST service call is made, it will also send all the cookies to the service. Upon receiving the cookies, the service can then maintain the HTTP Session State with the client.

Before seeing how to use the CookiedRequestFactory class, let us first take a look at the classes shared by the service and the client. The ServiceResult.cs file defines the format of the data that the service sends to the client:

C#
namespace SharedLibraries.ShareTypes
{
    public class ServiceStatus
    {
        // Default to Fail status
        public ServiceStatus()
        {
            Success = false;
            Message = "Service Call failed";
        }
    
        public bool Success { get; set; }
        public string Message { get; set; }
    }
    
    public class ServiceResult<T>
    { 
        public ServiceResult()
        {
            Status = new ServiceStatus();
        }
    
        public ServiceStatus Status { get; set; }
        public T Result { get; set; }
    }
} 
  • If the service only sends the success/fail status to the client, an instance of the ServiceStatus class is used. The Message property is a free text field. We can put detailed descriptions about the service status.
  • If the service needs to send some data to the client, an instance of the ServiceResult class is used. If the Status field indicates the service call is successful, the Result field then contains the requested data.

The data types that the service and the client use to change data are defined in the ServiceTypes.cs file:

C#
using System;
    
namespace SharedLibraries.ShareTypes
{
    // Client use this class to send the user credential
    // to login to the service
    public class AppUserCredentail
    {
        public string UserName { get; set; }
        public string Password { get; set; }
    }
    
    // Service use this class to send information to the
    // client, if the client is logged in.
    public class Student
    { 
        public int Id { get; set; }
        public string LastName { get; set; }
        public string FirstName { get; set; }
        public DateTime EnrollmentTime { get; set; }
        public int Score { get; set; }
    }
}
  • The AppUserCredentail class is used for the client to send the service the user's access credential. Upon successful login, the service will keep the user's login status in the web session.
  • The Student class is used for the server to send data to the client. If the service finds the expected user's login status in the web session, it will send a list of students to the client upon request.

Now we can take a look at how the REST service is created.

The REST Service

322436/SolutionExplorerService.jpg

There are a couple of ways to create WCF REST services in the Microsoft world. The popular ones include:

  • We can create REST services using MVC controllers. I like this approach and I feel it is a natural choice. If you are interested, you can take a look at this article.
  • We can also use the WCF Web API and you can find some good tutorials from here and here.

But in this article, I will use a different approach. I learned this approach from here and I have used it here. I feel this is a simple way to create REST services and it comes as a bonus help page that you will see later. In this project, the service is implemented in the "StudentService.cs" file:

C#
using System;
using System.Collections.Generic;
using System.Web;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;
using SharedLibraries.ShareTypes;
    
namespace Service.Services
{
    [ServiceContract]
    [AspNetCompatibilityRequirements(RequirementsMode
        = AspNetCompatibilityRequirementsMode.Allowed)]
    public class StudentService
    {
        private readonly HttpContext context;
        public StudentService()
        {
            context = HttpContext.Current;
        }
    
        [OperationContract]
        [WebInvoke(Method = "POST", UriTemplate = "StudentService/Login")]
        public ServiceStatus Login(AppUserCredentail credentail)
        {
            // Initiate status as fail to login.
            var status = new ServiceStatus() 
                { Success = false, Message = "Wrong user name and/or password" };
    
            // For simplicity, this example application has only one user.
            if ((credentail.UserName == "user") && (credentail.Password == "password"))
            {
                status.Success = true;
                status.Message = "Login success";
            }
    
            // Keep the login status in the HttpSessionState
            context.Session["USERLOGGEDIN"] = status.Success? "YES": null;
    
            return status;
        }
    
    
        [OperationContract]
        [WebGet(UriTemplate = "StudentService/GetStudents")]
        public ServiceResult<List<Student>> GetStudents()
        {
            var result = new ServiceResult<List<Student>>();
    
            // Check if client is logged in, if fail, return the status
            if ((string) context.Session["USERLOGGEDIN"] != "YES")
            {
                result.Status.Success = false;
                result.Status.Message = "Not logged in or session is over";
                return result;
            }
    
            // Client is logged in, create a random student list and send back
            var students = new List<Student>();
            var rand = new Random();
            for (int i = 1; i <= 20; i++)
            {
                var student = new Student();
                student.Id = i;
                student.LastName = "LName - " + i.ToString();
                student.FirstName = "FName - " + i.ToString();
                student.EnrollmentTime = DateTime.Now.AddYears(-4);
                student.Score = 60 + (int)(rand.NextDouble() * 40);
    
                students.Add(student);
            }
    
            result.Result = students;
            result.Status.Success = true;
            result.Status.Message = "Success";
    
            return result;
        }
    }
}

The StudentService class implements two OperationContracts:

  • The OperationContract "Login" takes the user access credential to authorize the user to access the service. For simplicity, we have only one user whose user name/password pair is "user/password". Upon successful login, the login status is saved in the web session and the client is informed if the user is authorized to access the service.
  • The "OperationContract" GetStudents checks the user's login status in the web session. If the user is logged in, it will send a list of randomly generated students to the client. If no login information is found, the "Status" field will be marked as fail to tell the client that the user needs to login to make the service call.

To make this WCF REST Service work, we will need to make changes to the Global.asax.cs file and the Web.config file. We will need to add the following code to the Application_Start event in Global.asax.cs:

C#
void Application_Start(object sender, EventArgs e)
{
    RouteTable.Routes.Add(new ServiceRoute("",
        new WebServiceHostFactory(),
        typeof(StudentService)));
}

We will also need to add the following configuration into the "Web.config" file:

XML
<system.serviceModel>
  <serviceHostingEnvironment aspNetCompatibilityEnabled="true"
    multipleSiteBindingsEnabled="true" />
  <standardEndpoints>
    <webHttpEndpoint>
      <standardEndpoint name="" helpEnabled="true"
        automaticFormatSelectionEnabled="true" />
    </webHttpEndpoint>
  </standardEndpoints>
</system.serviceModel>

Yes, as simple as it is. Without an "svc" file and without the Endpoint configuration, the WCF REST Service is completed and works. If you launch this ASP.NET application and type in the URL "http://localhost:2742/help" in the web browser, you can see the help page of this REST service.

322436/ServiceHelpPage.jpg

If you click the links on the help page, you can find detailed instructions on how to consume this service. Most importantly, the URLs and methods to access the two OperationContracts in Debug mode are the following:

  • "Login" - "http://localhost:2742/StudentService/Login", "POST"
  • "GetStudents" - "http://localhost:2742/StudentService/GetStudents", "GET".

We now finish the simple REST service, let us take a look at the client.

The Service Client

322436/SolutionExplorerClient.jpg

The "Client" project is a WPF MVVM application. I am not going into the details on how MVVM is implemented. If you are interested, you can download the attached solution and take a look at it yourself. I made some effort to make sure this application follows the MVVM practices to separate the concerns. In this application, the service is called in the StudentServiceProxy.cs file:

C#
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Net;
using System.Text;
using System.Web.Script.Serialization;
using SharedLibraries.ClientUtilities;
using SharedLibraries.ShareTypes;
    
namespace Client.ClientProxies
{
    public static class StudentServiceProxy
    {
        private static string LoginUrl;
        private static string GetStudentsUrl;
    
        static StudentServiceProxy()
        {
            LoginUrl = ConfigurationManager.AppSettings["LoginUrl"];
            GetStudentsUrl = ConfigurationManager.AppSettings["GetStudentsUrl"];
        }
    
        // login to the service
        public static ServiceStatus Login(AppUserCredentail credentail)
        {
            // Serialize the students to json
            var serializer = new JavaScriptSerializer();
            var jsonRequestString = serializer.Serialize(credentail);
            var bytes = Encoding.UTF8.GetBytes(jsonRequestString);
    
            // Initiate the HttpWebRequest with session support with CookiedFactory
            var request = CookiedRequestFactory.CreateHttpWebRequest(LoginUrl);
            request.Method = "POST";
            request.ContentType = "application/json";
            request.Accept = "application/json";
    
            // Send the json data to the Rest service
            var postStream = request.GetRequestStream();
            postStream.Write(bytes, 0, bytes.Length);
            postStream.Close();
    
            // Get the login status from the service
            var response = (HttpWebResponse) request.GetResponse();
            var reader = new StreamReader(response.GetResponseStream());
            var jsonResponseString = reader.ReadToEnd();
            reader.Close();
            response.Close();
    
            // Deserialize the json and return the result
            return serializer.Deserialize<ServiceStatus>(jsonResponseString);
        }
    
        // Retrieve a list of the students using client created by "CookiedRequestFactory"
        public static ServiceResult<List<Student>> GetStudentsWithCookie()
        {
            var request = CookiedRequestFactory.CreateHttpWebRequest(GetStudentsUrl);
            return GetStudents(request);
        }
    
        // Retrieve a list of the student using client created by "WebRequest"
        public static ServiceResult<List<Student>> GetStudentsWithoutCookie()
        {
            var request = (HttpWebRequest)WebRequest.Create(GetStudentsUrl);
            return GetStudents(request);
        }
    
        // private utility function
        private static ServiceResult<List<Student>> GetStudents(HttpWebRequest request)
        {
            request.Method = "GET";
            request.Accept = "application/json";
    
            var response = (HttpWebResponse)request.GetResponse();
            var reader = new StreamReader(response.GetResponseStream());
            var jsonResponseString = reader.ReadToEnd();
            reader.Close(); 
            response.Close();
    
            var serializer = new JavaScriptSerializer();
            return serializer.Deserialize<ServiceResult<List<Student>>>(jsonResponseString);
        }
    }
}

In this StudentServiceProxy class, I made three calls to the REST service.

  • In the "Login" method, a POST service call is made to the OperationContract "Login". In this call the HttpWebRequest object is created by the CookiedRequestFactory class factory.
  • In the GetStudentsWithCookie method, a GET service call is made to the OperationContract "GetStudents". The HttpWebRequest object used in this call is created by the CookiedRequestFactory class factory.
  • The GetStudentsWithoutCookie method also makes a GET service call to the OperationContract "GetStudents". The difference is that the HttpWebRequest object is directly created by the WebRequest class.

For simplicity reasons, all the service calls are implemented in a synchronous fashion. The URLs of the services are kept in the App.config file in the appSettings section:

XML
<appSettings>
    <add key="LoginUrl" value="http://localhost:2742/StudentService/Login"/>
    <add key="GetStudentsUrl" value="http://localhost:2742/StudentService/GetStudents"/>
</appSettings>

These methods are triggered by the application UI through the view model of the application. To save some space for CodeProject, I want to skip the view model. If you are interested, you can download the attachment and take a look at it yourself. The UI controls are implemented in the following XAML section in the MainWindow.xaml file:

XML
<Grid Margin="10">
    <Grid.RowDefinitions>
        <RowDefinition Height="30" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>
    
    <Grid Grid.Row="0">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
    
        <Button Grid.Column="0" Command="{Binding Path=GetStudentWithCookieCommand}">
            Get Students with Cookie</Button>
        <Button Grid.Column="1" Command="{Binding Path=GetStudentsNoCookieCommand}">
            Get Students without Cookie</Button>
    </Grid>
    
    <DataGrid Grid.Row="1" Margin="0, 5, 0, 0"
                              ItemsSource="{Binding Path=Students, Mode=OneWay}">
    </DataGrid>
</Grid>
    
<!-- Login section-->
<Grid  Visibility="{Binding Path=LoginVisibility}">
    <Rectangle Fill="Black" Opacity="0.08" />
    
    <Border BorderBrush="blue" 
                    BorderThickness="1" CornerRadius="10"
                    Background="White"
                    HorizontalAlignment="Center" VerticalAlignment="Center">
        <Grid Margin="10,10,10,25">
            <Grid.RowDefinitions>
                <RowDefinition Height="auto" />
                <RowDefinition Height="auto" />
                <RowDefinition Height="auto" />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="70" />
                <ColumnDefinition Width="110" />
                <ColumnDefinition Width="auto" />
            </Grid.ColumnDefinitions>
    
            <TextBlock Grid.Row="0" Margin="0,0,0,10"
                                       FontWeight="Bold" Foreground="Gray"
                                       Grid.ColumnSpan="3">
                                Please login to the application
            </TextBlock>
    
            <TextBlock Grid.Row="1" Grid.Column="0">User Name</TextBlock>
            <TextBox Grid.Row="1" Grid.Column="1" Width="100"
                                     Text="{Binding Path=UserCredentail.UserName, Mode=TwoWay}" />
            <TextBlock Margin="0,5,0,0" Grid.Row="2" Grid.Column="0">Password</TextBlock>
            <PasswordBox Margin="0,5,0,0" Grid.Row="2" Grid.Column="1" Width="100"
                                     ff:PasswordBoxAssistant.BindPassword="true" 
                                     ff:PasswordBoxAssistant.BoundPassword
                ="{Binding Path=UserCredentail.Password, Mode=TwoWay,
                                UpdateSourceTrigger=PropertyChanged}"/>
    
            <Button Margin="5,5,0,0" Content="Login" Grid.Row="2" Grid.Column="2" Width="80"
                                    Command="{Binding Path=LoginCommand}"/>
        </Grid>
    </Border>
</Grid>
  • The XAML code in the "Login Section" has text inputs for the user name and password. It also has a button to trigger the "Login" service call.
  • There are two additional buttons in the code. Both of them will trigger calls to the "OperationContract" GetStudents. One of them uses the CookiedRequestFactory class to create the "HttpWebRequest object and the other uses the WebRequest class.

We now complete the demo application, we can run it in Visual Studio in Debug mode.

Run the Example Application

If you set the "Client" project as the start up project, you can debug run the application. When the client application first launches, you can see a pop-up window asking you to login to the service.

322436/RunAppStart.jpg

If you put in the user name/password pair as "user/password" and click on the "Login" button, you can login to the service. Upon successful login, you can click on the "Get Students with Cookie" button, and you can see a list of students are successfully received from the service.

322436/RunAppCallWithCookie.jpg

If you click on "Get Students without Cookie", you can see that we are unable to receive the requested student list, and a message pop-up shows to tell us that we are not logged in.

322436/RunAppCallWithoutCookie.jpg

This example shows us that we need to use the CreateHttpWebRequest factory class to create the HttpWebRequest object if we want to maintain the web session with the REST service.

Points of Interest

  • This article demonstrated how to maintain "Http Session State" in WCF REST Services when consumed from desktop applications using the HttpWebRequest object.
  • Although I have only tested CookiedRequestFactory against one way to implement REST Services, I believe it should be applicable to all possible REST implementations, since the HttpWebRequest object is a general purpose HTTP client library. If you encounter any problems with other types of REST implementations, please let me know. I feel we should meet your requirements with minimal modifications to the CookiedRequestFactory class.
  • I did not expect that I would write this article so long and I hope you are not bored. If you do get bored, you can simply take a look at the CookiedRequestFactory class and try to use it yourself. It should be very simple to use.
  • I hope you like my postings and I hope this article can help you one way or the other.

History

  • First revision - 2/2/2012.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)