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.
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
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
{
private static Dictionary<string, CookieContainer> containers
= new Dictionary<string, CookieContainer>();
public static HttpWebRequest CreateHttpWebRequest(string url)
{
var request = (HttpWebRequest)WebRequest.Create(url);
string domain = (new Uri(url)).GetLeftPart(UriPartial.Authority);
CookieContainer container;
if (!containers.TryGetValue(domain, out container))
{
container = new CookieContainer();
containers[domain] = container;
}
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:
namespace SharedLibraries.ShareTypes
{
public class ServiceStatus
{
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:
using System;
namespace SharedLibraries.ShareTypes
{
public class AppUserCredentail
{
public string UserName { get; set; }
public string Password { get; set; }
}
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
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:
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)
{
var status = new ServiceStatus()
{ Success = false, Message = "Wrong user name and/or password" };
if ((credentail.UserName == "user") && (credentail.Password == "password"))
{
status.Success = true;
status.Message = "Login success";
}
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>>();
if ((string) context.Session["USERLOGGEDIN"] != "YES")
{
result.Status.Success = false;
result.Status.Message = "Not logged in or session is over";
return result;
}
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:
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.
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
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:
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"];
}
public static ServiceStatus Login(AppUserCredentail credentail)
{
var serializer = new JavaScriptSerializer();
var jsonRequestString = serializer.Serialize(credentail);
var bytes = Encoding.UTF8.GetBytes(jsonRequestString);
var request = CookiedRequestFactory.CreateHttpWebRequest(LoginUrl);
request.Method = "POST";
request.ContentType = "application/json";
request.Accept = "application/json";
var postStream = request.GetRequestStream();
postStream.Write(bytes, 0, bytes.Length);
postStream.Close();
var response = (HttpWebResponse) request.GetResponse();
var reader = new StreamReader(response.GetResponseStream());
var jsonResponseString = reader.ReadToEnd();
reader.Close();
response.Close();
return serializer.Deserialize<ServiceStatus>(jsonResponseString);
}
public static ServiceResult<List<Student>> GetStudentsWithCookie()
{
var request = CookiedRequestFactory.CreateHttpWebRequest(GetStudentsUrl);
return GetStudents(request);
}
public static ServiceResult<List<Student>> GetStudentsWithoutCookie()
{
var request = (HttpWebRequest)WebRequest.Create(GetStudentsUrl);
return GetStudents(request);
}
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:
<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>
<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.
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.
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.
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.