David Rousset - HTML5 & Gaming Technical Evangelist

HTML5 & Gaming Technical Evangelist

How to open a WCF RIA Services application to other type of clients: the SOAP endpoint (3/5)

How to open a WCF RIA Services application to other type of clients: the SOAP endpoint (3/5)

Rate This
  • Comments 19

BookClubScreen021We’ll see here how to open the application used in the 2 previous articles with a SOAP endpoint that will offer us “classical” WCF calls. We’ll then see how to use this new endpoint from a WPF client that will add and delete items. We’ll do the same operations from a Windows Phone 7 client also. The tricky point to be resolved in both cases will be linked to the authentication cookie to handle. It’s needed to pass-through the forms based authentication in place and to be able to execute actions restricted to authenticated users and/or members of a specific role. You will be able to download the Visual Studio 2010 solution containing the job done during these 3 articles (SL Business App, WPF client & Windows Phone 7 application) at the end of this post. You’ll be able also to view these 3 clients in action thanks to a HTML5 video.

This article is the third one out of 5: 

1 – Review of the initial application
2 – How to expose the service as an OData stream consumed by Excel and from a web application built with WebMatrix 
3 – How to expose the service as a “standard” WCF Service used from a WPF application and a Windows Phone 7 application (this article)
4 – How to expose the service as a JSON stream to be used by a HTML5/jQuery web application 
5 – Step by step procedure to publish this WCF RIA Services application to Windows Azure & SQL Azure

Enabling the SOAP endpoint on a WCF RIA Services DomainService

This will be done of course inside the web.config file. However, the Silverlight 4 tools for Visual Studio 2010 doesn’t install by default the required assemblies needed to enable this endpoint. You need to download & install the WCF RIA Services toolkit for that.

Once the toolkit installed, add a reference to the Microsoft.ServiceModel.DomainServices.Hosting assembly and open the web.config file. Under the OData endpoint, add this new endpoint:

<add name="Soap" type="Microsoft.ServiceModel.DomainServices.Hosting.SoapXmlEndpointFactory, 
Microsoft.ServiceModel.DomainServices.Hosting, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=31bf3856ad364e35" />

Once done, you’ll be able to enter and test this URL in your browser:

http://nameofyourserver:port/ClientBin/NameOfYourSolution-Web-NameOfYourDomainService.svc

And you can use it also to generate the client proxy thanks to the “Add Service Reference…” action under Visual Studio. For instance, you can add a reference to my service deployed in Azure here:

http://bookclub.cloudapp.net/ClientBin/BookShelf-Web-Services-BookClubService.svc

Using the SOAP endpoint from a WPF client

Let’s see how to call the remote methods exposed in our DomainService from a WPF client using a very light UI (== ugly). The purpose is to concentrate ourselves on the essential part. Still, in order to have something not too ugly, I’ve used the free themes available on codeplex here: http://wpfthemes.codeplex.com/

1 – To simplify also the logic, we’ll do read/write operations on the category entities rather than on the books. As the code source shared with the first article only contains the reading logic inside the DomainService to return the categories, we need to update it. To add support for add, update and delete, use this code inside the BookClubService class:

[RequiresRole("Admin", 
ErrorMessage = "You must be part of the Administrator role to insert, update or delete a category.")] public void InsertCategory(Category category) { if ((category.EntityState != EntityState.Detached)) { this.ObjectContext.ObjectStateManager.ChangeObjectState(category, EntityState.Added); } else { this.ObjectContext.Categories.AddObject(category); } } [RequiresRole("Admin",
ErrorMessage = "You must be part of the Administrator role to insert, update or delete a category.")] public void UpdateCategory(Category category) { this.ObjectContext.Categories.AttachAsModified(category, this.ChangeSet.GetOriginal(category)); } [RequiresRole("Admin",
ErrorMessage = "You must be part of the Administrator role to insert, update or delete a category.")] public void DeleteCategory(Category category) { if ((category.EntityState == EntityState.Detached)) { this.ObjectContext.Categories.Attach(category); } this.ObjectContext.Categories.DeleteObject(category); }

These 3 methods allow us to add, update and delete a category. They can only be called from authenticated users members of the “Admin” group. We then ask to the ASP.NET layer (used by WCF RIA Services) to check the authentication level of the incoming requests.

2 – The next step is to add some references to the 2 services that will be available as SOAP after adding the new endpoint. The first one gives you access to the books & categories entities via the URL I’ve just shared you above. The 2nd one handles the authentication part natively available in each Silverlight Business Application with this code:

[EnableClientAccess]
public class AuthenticationService : AuthenticationBase<User> { }

In my case, it’s available in Azure on this URL: http://bookclub.cloudapp.net/ClientBin/BookShelf-Web-AuthenticationService.svc

Name these 2 references BookClubService and BookClubAuthService.

3 – Once these references added, we will be able to start querying your RIA Services from your WPF client. Add these 3 private members and change the default constructor of the main windows of your WPF application by this:

BookClubAuthService.AuthenticationServiceSoapClient authClient;
BookClubService.BookClubServiceSoapClient bookClubClient;
BookClubAuthService.User currentAuthUser;

public MainWindow()
{
    InitializeComponent();
    ThemeManager.ApplyTheme(this, "ShinyBlue");
    authClient = new BookClubAuthService.AuthenticationServiceSoapClient();
    bookClubClient = new BookClubService.BookClubServiceSoapClient();
}

We’re instantiating the 2 client proxies. If you’d like to return all the available categories and to display them inside a ListBox control, here the code-behind to use:

var categories = bookClubClient.GetCategories();
lstCategories.ItemsSource = categories.RootResults;

And here is the piece of XAML to use:

<ListBox Name="lstCategories">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding CategoryName}" />
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

Please note that I’m using the synchronous methods generated for the WPF proxy. Inside a Windows Phone 7 or Silverlight client, the proxy generation will only offer you some asynchronous calls. Finally, this first logic gives us this kind of output:

BookClubScreen016

4 – Fine. Up to now, everything was very simple and we had the same results using the OData endpoint. If we want to go a bit further to update the categories’ entities, we need to be authenticated and member of the “Admin” group. For the authentication part, this is very straightforward. You just have to use the 2nd proxy built on the authentication service which gives us the Login() and Logout() method. Then use the following code:

BookClubAuthService.QueryResultOfUser result = 
authClient.Login(txtUsername.Text, txtPassword.Password, true, ""); if (result.RootResults != null && result.RootResults.Count() > 0) { currentAuthUser = result.RootResults.First(); loginStatus.Content = "Welcome " + currentAuthUser.DisplayName; imgUser.Source = new BitmapImage(new Uri(currentAuthUser.PictureURL)); } else { loginStatus.Content = "Error while logging " + txtUsername.Text; }

We can even retrieve the user’s picture he had inserted with its webcam via the main Silverlight 4 application. You’ll then have this kind of output:

BookClubScreen017

You may think at this stage that the job is over as:

- We were properly authenticated by the BookClubAuthService service
- We should then be able to do some protected calls to the BookClubService ?

This is not the case. Indeed, when you go through the FBA (Forms Based Authentication) mechanism, a special cookie is built and sent back in every requests in the HTTP headers to guarantee that the callers is the user having the appropriate rights. This header is hidden and you don’t have to care about it when you’re building a “Silverlight Business Application”. The tooling and the framework handle it for you.

So, we need to find a way to share this cookie between the authentication service and the books & categories entities services.

Here is a first way to do that:

string sharedCookie;

BookClubAuthService.AuthenticationServiceSoapClient authClient = 
new BookClubAuthService.AuthenticationServiceSoapClient();
using (new OperationContextScope(authClient.InnerChannel)) { authClient.Login("theuser", @"thepassword", true, ""); // Extract the cookie embedded in the received web service response // and stores it locally HttpResponseMessageProperty response = (HttpResponseMessageProperty) OperationContext.Current.IncomingMessageProperties[ HttpResponseMessageProperty.Name]; sharedCookie = response.Headers["Set-Cookie"]; } BookClubServiceSoapClient client = new BookClubServiceSoapClient(); QueryResultOfCategory result; using (new OperationContextScope(client.InnerChannel)) { // Embeds the extracted cookie in the next web service request // Note that we manually have to create the request object since // since it doesn't exist yet at this stage HttpRequestMessageProperty request = new HttpRequestMessageProperty(); request.Headers["Cookie"] = sharedCookie; OperationContext.Current.OutgoingMessageProperties[ HttpRequestMessageProperty.Name] = request; result = client.GetCategories(); }

This sample code shows you how to do an “authenticated” calls to the GetCategories() method. This means that if you’ve set some restrictions by attribute like the RequiresAuthentication on the GetCategories() method on the WCF RIA Services part, you’ll be able to goes through thanks to this approach. However, this forces us to use an important amount of additional code to transfer the cookie during each methods call!

To avoid that, we have to find a way to automatically extract the cookie and re-inject it between incoming and outgoing HTTP requests whatever the remote WCF service is called. For that, you can use the “messages inspector” approach of WCF. This one lives between the end of the tunnel and our user code. It can then analyze the stream before sending it on the network and it can also have a look to the answer back from the network before giving it back to the client. The idea is then to build something similar than this diagram:

InspectorVisio

I’m then highly recommending you reading the excellent Enricon Campidoglio’s article: Managing shared cookies in WCF. I’ve simply implemented his solution. Here are the steps to use it.

First, you need to copy these 3 files from his solution: CookieManagerBehaviorExtension.cs, CookieManagerEndpointBehavior.cs and CookieManagerMessageInspector.cs into your WPF project and potentially then change the namespace used in those files by your own. These files contain the code that will inspect every incoming/outgoing message to track any authentication cookie to extract it and re-inject if for you if needed.

To use it, go to the app.config file and under this tag:

<system.serviceModel>

Add those one:

<behaviors>
  <endpointBehaviors>
    <behavior name="EnableCookieManager">
      <cookieManager />
    </behavior>
  </endpointBehaviors>
</behaviors>
<extensions>
  <behaviorExtensions>
    <add name="cookieManager" type="WpfBookShelf.CookieManagerBehaviorExtension, WpfBookShelf, 
Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
" /> </behaviorExtensions> </extensions>

Then, you need to say to the 2 endpoints that they have to use this endpoint’s behavior:

behaviorConfiguration="EnableCookieManager"

Which should normally give you this kind of XML file:

<endpoint address="http://bookclub.cloudapp.net/Services/BookShelf-Web-AuthenticationService.svc/Soap"
    behaviorConfiguration="EnableCookieManager"
    binding="basicHttpBinding" bindingConfiguration="BasicHttpBinding_AuthenticationServiceSoap"
    contract="BookClubAuthService.AuthenticationServiceSoap" name="BasicHttpBinding_AuthenticationServiceSoap" />
<endpoint address="http://bookclub.cloudapp.net/Services/BookShelf-Web-Services-BookClubService.svc/Soap"
    behaviorConfiguration="EnableCookieManager"
    binding="basicHttpBinding" bindingConfiguration="BasicHttpBinding_BookClubServiceSoap"
    contract="BookClubService.BookClubServiceSoap" name="BasicHttpBinding_BookClubServiceSoap" />

Once done, we’ll see that the code is much more readable and nice to review! For instance, here is a sample code that will logon the user, generate an add operation followed by a delete operation terminated by a logout:

//Using the first proxy to log in
BookClubAuthService.AuthenticationServiceSoapClient authClient = new BookClubAuthService.AuthenticationServiceSoapClient();
authClient.Login("adminuser", "password", true, "");

//Using the second proxy to manipulate the data
//exposed by RIA Services. The cookie retrieved by the first call
//to the Login() method will be reinjected for you by the WCF Message Inspector
BookClubServiceSoapClient client = new BookClubServiceSoapClient();
QueryResultOfCategory result;

ChangeSetEntry[] returnChangeSet;
ChangeSetEntry[] newItems = new ChangeSetEntry[1];
newItems[0] = new ChangeSetEntry();

//Adding a new category
Category newCategory = new Category();
newCategory.CategoryName = newCategoryName;
newItems[0].OriginalEntity = null;
newItems[0].Entity = newCategory;
newItems[0].Operation = DomainOperation.Insert;
returnChangeSet = client.SubmitChanges(newItems);

//Reloading the Category Set
result = client.GetCategories();

//Retrieving the just added category
var cToDelete = (from c in result.RootResults
            where c.CategoryName.Equals(newCategoryName)
            select c).FirstOrDefault();

//Deleting the just added category
newItems[0].OriginalEntity = cToDelete;
newItems[0].Entity = cToDelete;
newItems[0].Operation = DomainOperation.Delete;
returnChangeSet = client.SubmitChanges(newItems);

authClient.Logout();

Nice, isn’t it? In the little WPF sample client you’ll find in the source code to download, you’ll have this result. If I’m trying to update something without being authenticated or if I’m authenticated with a non-admin account:

BookClubScreen018

The RIA Services layer has then properly done its job by checking if the user was properly authenticated and by checking if it was member of the “Admin” role before trying to let the add operation be executed. Once properly logged, I can now add a new category from my WPF client:

BookClubScreen019

Once inserted, it’s of course immediately available in the main Silverlight application:

BookClubScreen020

Well, you now know everything to be able to write your own WPF client on top of WCF RIA Services. Let’s now review the job to do on the Windows Phone 7 client.

Using the SOAP endpoint from a Windows Phone 7 client

Connecting to the WCF services exposed by RIA Services from Windows Phone will be very similar to what we’ve done with WPF. Still, there are 2 little differences due to the Silverlight nature of Windows Phone:

1 – The WCF client layer of Windows Phone is less rich than the complete version available with the full .NET Framework. For instance, we don’t have the "message inspectors” support on Windows Phone. We will then have to find another way to handle the authentication cookie.
2 – Every WCF calls are asynchronous in Silverlight and thus, under Windows Phone 7.

For the first point, this is hopefully very easy to handle under Windows Phone 7 thanks to the CookieContainer. To use it, you need to add inside the 2 bindings declared in the WCF XML configuration file this property:

enableHttpCookieContainer="true"

You’ll then have this kind of file:

<binding name="BasicHttpBinding_AuthenticationServiceSoap" maxBufferSize="2147483647"
    enableHttpCookieContainer="true"
          maxReceivedMessageSize="2147483647">
    <security mode="None" />
</binding>
<binding name="BasicHttpBinding_BookClubServiceSoap" maxBufferSize="2147483647"
    enableHttpCookieContainer="true"
          maxReceivedMessageSize="2147483647">
    <security mode="None" />
</binding>

Once done, this code should then work:

CookieContainer cookieContainer = null;

private void btnLogin_Click(object sender, RoutedEventArgs e)
{
    authClient = new BookClubAuthService.AuthenticationServiceSoapClient();

    authClient.LoginCompleted += 
new EventHandler<BookClubAuthService.LoginCompletedEventArgs>(authClient_LoginCompleted); status.Text = "Logging " + username + "..."; authClient.LoginAsync(username, password, true, ""); } void authClient_LoginCompleted(object sender, BookClubAuthService.LoginCompletedEventArgs e) { if (!e.Cancelled) { if (e.Error == null) { if (e.Result.RootResults != null && e.Result.RootResults.Count() > 0) { BookClubAuthService.User user = e.Result.RootResults.First(); status.Text = user.DisplayName + " logged."; cookieContainer = authClient.CookieContainer; client = new BookClubService.BookClubServiceSoapClient { CookieContainer = cookieContainer }; client.GetCategoriesCompleted +=
new EventHandler<BookClubService.GetCategoriesCompletedEventArgs>(client_GetCategoriesCompleted); client.GetCategoriesAsync(); } else { status.Text = "Failed logging " + username + "."; } } } } void client_GetCategoriesCompleted(object sender, BookClubService.GetCategoriesCompletedEventArgs e) { if (e.Result != null) { ObservableCollection<Category> list = e.Result.RootResults; listBox1.ItemsSource = list; } }

This code is creating the following sequence:

1 – When the user clicks on the login button, we’re instantiating the WCF client proxy to the authentication service of RIA Services. We’re then subscribing to LoginCompleted event which will notify us if the authentication succeed or not. Finally, we’re launching the login process in an asynchronous manner via the call to the LoginAsync method.

2 – We will be call-backed inside the authClient_LoginCompleted method where we first check if everything worked as expected. If so, we’re saving the authentication cookie returned by the ASP.NET layer and available inside the CookieContainer object. We’re then using this special object inside the 2nd WCF client proxy that will allow us to do authenticated queries to the methods handling our books & categories.

The add, update & delete operations are done in the same way we’ve seen with the WPF client except that every operation are done asynchronously. For instance, here is my code that deletes a category and then checks what happened in the callback method called once the changes has been submitted:

public void DeleteCategory(string categoryNameToDelete)
{
    try
    {
        ObservableCollection<ChangeSetEntry> deletedItems = new ObservableCollection<ChangeSetEntry>();
        ChangeSetEntry newChangeSetEntry = new ChangeSetEntry();

        Category categoryToDelete = (from c in _categories
                                        where c.CategoryName.Equals(categoryNameToDelete)
                                        select c).FirstOrDefault();

        if (categoryToDelete != null)
        {
            newChangeSetEntry.OriginalEntity = categoryToDelete;
            newChangeSetEntry.Entity = categoryToDelete;
            newChangeSetEntry.Operation = DomainOperation.Delete;

            deletedItems.Add(newChangeSetEntry);
            bookClubClient.SubmitChangesAsync(deletedItems);
        }
        else
        {
            MessageBox.Show("Category not found.");
        }
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
    }
}
void bookClubClient_SubmitChangesCompleted(object sender, SubmitChangesCompletedEventArgs e)
{
    if (e.Error == null)
    {
        ObservableCollection<ChangeSetEntry> returnChangeSet = e.Result;
        var operation = returnChangeSet.FirstOrDefault().Operation;
        if (operation.Equals(DomainOperation.Insert))
            MessageBox.Show("Category added successfully.");
        if (operation.Equals(DomainOperation.Delete))
            MessageBox.Show("Category deleted successfully.");

        LoadCategoriesData();
    }
    else
    {
        MessageBox.Show(e.Error.Message);
    }
}

Thanks to that, you can then quickly create this kind of application (source code available as download at the end of this article) using the template “Windows Phone Pivot Application” as a based.

Displaying all the categories and all the books available in my SQL DB:

BookClubScreen022BookClubScreen023

Trying to insert or delete a category without being first authenticated properly:

BookClubScreen024

Login screen displaying the user’s picture generated from the Webcam with the Silverlight 4 application. We then can add/delete items successfully:

BookClubScreen025BookClubScreen026

You can watch the final result of this article in the video below. I show you how to create a new user using the Silverlight 4 application, how to promote him inside the “Admin” group and how to re-use its identity inside the WPF & the Windows Phone 7 applications to be able to add/delete data exposed by WCF RIA Services and its SOAP endpoints:


Note:
this h264 HTML5 video will be displayed natively by IE9 & Chrome. There is a fallback done using a Silverlight player on other browsers like IE8 for instance.
 


Here is the source code of the Visual Studio 2010 solution hosting all the projects demonstrated up to now:

Conclusion

We’ve seen during these 3 first articles how to put in place a solution frequently needed by my customers. Silverlight 4 used in conjunction with the WCF RIA Services framework relaying on ASP.NET will offer the best productivity to the developers’ team in charge of writing the Web RIA application that will expose the business data.

Adding an OData endpoint will allow non-developers persons to read the data from the PowerPivot Excel Add-in or will be used in relatively simple web application written with WebMatrix using the OData helper.

At last, the SOAP endpoint will allow clients like WPF, Windows Phone 7, Java or others to modify the data by using the “classical” WCF layer of RIA Services. The productivity of the developers that will go through this layer will be inferior to the experience provided by SL4/RIA Services. For instance, you can’t build LINQ queries on the client side to add filtering/sorting logic that can then go over the network to the server’s data access layer. But to try workaround that, you can think of adding additional methods to the DomainService. For instance, if you’d like to return all the books only members of a specific category like the main Silverlight application is able to do via its DomainDataSource, here is the kind of method you should add to the DomainService for the WPF/WP7 clients:

public IQueryable<Book> GetBooksByCategoryId(int categoryId)
{
    return this.ObjectContext.Books
        .Where(b => b.CategoryID.Equals(categoryId))
        .OrderBy(b => b.Title);
}

The WCF client proxy generation will then expose a new GetBooksByCategoryId() method that will offer you part of the logic available in the richer Silverlight 4/RIA Services experience.

You should now be armed to face a lot of different scenarios where WCF RIA Services will definitely be a good friend. Clignement d'œil

See you soon in the next article dedicated to the JSON endpoint accessed from an HTML5 application.

David

Page 2 of 2 (19 items) 12
Leave a Comment
  • Please add 7 and 3 and type the answer here:
  • Post