Click here to Skip to main content
Licence CPOL
First Posted 2 Feb 2012
Views 6,422
Bookmarked 15 times

Maintain Http Session State in WCF REST Services with HttpWebRequest

By | 2 Feb 2012 | Article
This article demonstrates how to maintain "Http Session State" in "WCF" "REST" Services when consumed from desktop applications using the "HttpWebRequest" object.

Download RestSession.zip - 32.95 KB

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 the 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 when 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. The "jQuery" is one of the most popular Javascript libraries that supports "Ajax" calls;
  • "REST" services can be also 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 the cookies to the service. When the service is called in the "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 on how to maintain the web session when the service is called from desktop applications with a simple extension of the "HttpWebRequest" class.

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 an "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

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.

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 an 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 be storing 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 send to the client:

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 use. The "Message" property is a free text field. We can put the 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 is defined in the "ServiceTypes.cs" file:

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 a 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

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 a bonus help page that you will see later. In this project, the service is implemented in the "StudentService.cs" file:
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 a 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":

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:

<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.

ServiceHelpPage.jpg

If you click the links on the help page, you can find the detailed instructions on how to consume this service. Most importantly, the urls and methods to access the two "OperationContracts" in the 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

SolutionExplorerClient.jpg

The "Client" project is a "WPF" "MVVM" application. I am not going to the details on how the "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:

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 "synchronous" fashion. The urls of the services are kept in the "App.config" file in the "appSettings" section:

<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:

<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 the text inputs for the user name and the 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 the 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 the Visual Studio in the 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.

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 a successful login, you can click on the "Get Students with Cookie" button, you can see a list of students are successfully received from the service.

RunAppCallWithCookie.jpg

If you click on the "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.

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 the "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)

About the Author

Dr. Song Li



United States United States

Member

I have been working in the IT industry for some time. It is still exciting and I am still learning. I am a happy and honest person, and I want to be your friend.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board. (secure sign-in)
 
Search this forum  
 FAQ
    Noise  Layout  Per page   
  Refresh
GeneralMy vote of 5 Pinmembermanoj kumar choubey23:44 5 Feb '12  
GeneralMy vote of 5 PinmemberDhaval Charadva22:43 3 Feb '12  
GeneralRe: My vote of 5 PinmvpDr. Song Li4:11 4 Feb '12  
QuestionNicely explained PinmemberRupeshKumar3:47 3 Feb '12  
AnswerRe: Nicely explained PinmvpDr. Song Li10:07 3 Feb '12  
GeneralMy vote of 5 PinmemberRupeshKumar3:36 3 Feb '12  
QuestionWord of Caution PinmemberCraig G. Wilson2:41 3 Feb '12  
AnswerRe: Word of Caution PinmvpDr. Song Li3:21 3 Feb '12  
GeneralRe: Word of Caution PinmemberCraig G. Wilson2:48 6 Feb '12  
GeneralRe: Word of Caution PinmvpDr. Song Li3:31 6 Feb '12  

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Mobile
Web04 | 2.5.120517.1 | Last Updated 2 Feb 2012
Article Copyright 2012 by Dr. Song Li
Everything else Copyright © CodeProject, 1999-2012
Terms of Use
Layout: fixed | fluid