After spending last weekend working on and blogging about Silverlight 3 and .NET RIA services, I decided I’d look to build out a membership, profile and role provider that would use Windows Azure storage. Much to my delight, I stumbled into the AspProvidersDemo code that comes with the Windows Azure SDK or perhaps the Visual Studio 2008 Tools for Azure.
No matter, you need them both to follow along with this post. If you have not already, you should look at my previous post and make sure you prepare your environment for Silverlight 3 in addition to signing up for your Azure account and installed the tools mentioned
You can download the entire solution file (434KB) and skip to the momentous striking of your F5 key if you like. Or you can follow along here and blunder through this adventure as I did. (I recommend cheating now and downloading the code.)
Here’s the step-by-step details. I’ll try to spare the you excruciating minutiae and keep it as exciting as possible.
I started by creating a standard Cloud Service application called MyFilesCloudService with a web role called WebFilesRole. I then added a Silverlight Business Application called Adventure. Unfortunately, this template does not allow you to select the web role application to host the Silverlight app.
I removed the Adventure.Web application and in the web role’s project properties added the Silverlight app in the Silverlight Application tab. (ERROR: This turned out to be a problem which I solved by added a throwaway standard Silverlight app to the solution, selecting the WebFilesRole app as the host. I am still not certain why, but I’ll spare you the grisly details of experimentation with the web.config. If you haven’t already, this is a good place to stop and download the code.)
I copied the AspProviders and StorageClient projects from the Azure SDK demos folder into the solution directory and added them to the solution. I also copied the relevant sections from the web.config for the web role and the ServiceConfiguration.cscfg and ServiceDefinition.csdef files in cloud service project.
I hit F5 for kicks and get (via Event Viewer) an event 3007, “A compilation error has occurred.” Upon further digging I realize that the profile provider is configured to inherit it’s ProfileBase from UserProfile. The class is in the demo’s web role. Steal that too. Here it is as added to the web role in my project:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Profile;
using System.Web.Security;
namespace WebFilesRole
{
public class UserProfile : ProfileBase
{
public static UserProfile GetUserProfile(string username)
{
return Create(username) as UserProfile;
}
public static UserProfile GetUserProfile()
{
return Create(Membership.GetUser().UserName) as UserProfile;
}
[SettingsAllowAnonymous(false)]
public string Country
{
get { return base["Country"] as string; }
set { base["Country"] = value; }
}
[SettingsAllowAnonymous(false)]
public string Gender
{
get { return base["Gender"] as string; }
set { base["Gender"] = value; }
}
[SettingsAllowAnonymous(false)]
public int Age
{
get { return (int)(base["Age"]); }
set { base["Age"] = value; }
}
}
}
I boldly hit F5 again and get this gem:
Configuration Error
Initialization of data service structures (tables and/or blobs) failed!
The most probable reason for this is that the storage endpoints are not configured correctly.
Line 133: type="Microsoft.Samples.ServiceHosting.AspProviders.TableStorageSessionStateProvider"
A little searching and googling and I learn that I need to right-click on my cloud service application and select “Create Test Storage Tables.” I do it and bada-bing, I get this nice dialog and Output window text:
DevTableGen : Generating database 'MyFilesCloudService'
DevTableGen : Generating table 'Roles' for type 'Microsoft.Samples.ServiceHosting.AspProviders.RoleRow'
DevTableGen : Generating table 'Sessions' for type 'Microsoft.Samples.ServiceHosting.AspProviders.SessionRow'
DevTableGen : Generating table 'Membership' for type 'Microsoft.Samples.ServiceHosting.AspProviders.MembershipRow'
===== Create test storage tables succeeded =====
Aha! I go examine my local SQL Server instance and sure enough, there’s a new DB called MyFilesCloudService with some interesting tables. You can take at look at your own when you’ve read far enough along here to learn to click that “Create Test Storage Tables” magic context menu item too.
So I experiment a little and create a couple of test tables like this:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Microsoft.Samples.ServiceHosting.StorageClient;
namespace WebFilesRole
{
public class MyTestDataServiceContext : TableStorageDataServiceContext
{
public IQueryable Roles
{
get
{
return this.CreateQuery("MyTest");
}
}
}
public class MyTestRow : TableStorageEntity
{
public string MyTestName { get; set; }
}
}
Note the nice and easy TableStorageEntity and it’s TableStorageDataServiceContext. Just don’t make the mistake I did and forget to name the property something unique. I tried Roles (yeah, a copy/past error) and got a nasty message like this:
No table generated for property 'Roles' of class 'WebFilesRole.MyTestDataServiceContext' because the name matches (or differs only in case) from the name of a previously generated table
I add an AppInitializer class to make sure these tables get created in the cloud when run there. First, I add a bit of code to the Application_BeginRequest method in the Global.asax.cs (the one I just added but didn’t tell you about).
protected void Application_BeginRequest(object sender, EventArgs e)
{
HttpApplication app = sender as HttpApplication;
if (app != null)
{
HttpContext context = app.Context;
AppInitializer.Initialize(context);
}
}
I then add the initializer class at the bottom of that same code file.
internal static class AppInitializer
{
static object lob = new object();
static bool alreadyInitialized = false;
public static void Initialize(HttpContext context)
{
if (alreadyInitialized) return;
lock (lob)
{
if (alreadyInitialized) return;
InitializeAppStartFirstRequest(context);
alreadyInitialized = true;
}
}
private static void InitializeAppStartFirstRequest(HttpContext context)
{
StorageAccountInfo account = StorageAccountInfo.GetDefaultTableStorageAccountFromConfiguration();
TableStorage.CreateTablesFromModel(typeof(Microsoft.Samples.ServiceHosting.AspProviders.MembershipRow));
TableStorage.CreateTablesFromModel(typeof(Microsoft.Samples.ServiceHosting.AspProviders.RoleRow));
TableStorage.CreateTablesFromModel(typeof(Microsoft.Samples.ServiceHosting.AspProviders.SessionRow));
TableStorage.CreateTablesFromModel(typeof(Room));
}
}
I then add some test code into the Default.aspx.cs which I won’t bore you with here. You can look at it in the downloaded solution. I got a weird error with the session test, but after a reboot, it went away, so I’ll chalk that up to the development fabric being a CTP.
Now I want to get back to working Silverlight into the picture. I need to create an admin user for my test login, so I add some code to the AppInitializer class in the Global.asax.cs file like this:
MembershipUser user = Membership.GetUser("admin");
if (null == user)
{
//create admin user
MembershipCreateStatus status = MembershipCreateStatus.Success;
Membership.CreateUser("admin", "admin", "admin@admin.com", "admin-admin", "admin",
true, Guid.NewGuid(), out status);
//add admin user to admin role
if (status == MembershipCreateStatus.Success)
{
if (!Roles.RoleExists("admin"))
{
Roles.CreateRole("admin");
}
Roles.AddUserToRole("admin", "admin");
}
//add profile data to admin user
UserProfile profile = UserProfile.Create("admin") as UserProfile;
profile.Age = 40; //not my true age
profile.Country = "US";
profile.Gender = "M";
profile.Save();
}
I look at the UserProfile class and know that the DomainService’s User class needs the same properties in order for the Silverlight RiaContext to know about them. I discovered in the metadata code the following comments in the UpdateUser method of the System.Web.Ria.ApplicationServices.AuthenticationBase<T> base class used for the AuthenticationService domain service class:
// Remarks:
// By default, the user is persisted to the System.Web.Profile.ProfileBase.
// In writing the user to the profile, the provider copies each property in
// T into the corresponding value in the profile. This behavior can be tailored
// by marking specified properties with the System.Web.Ria.ApplicationServices.ProfileUsageAttribute.
I know now that I want the UserProfile and the User classes to have the same profile properties, so I add an interface above the UserProfile class like this:
public interface IUserProfile
{
string Country { get; set; }
string Gender { get; set; }
int Age { get; set; }
}
And then add the same properties found in UserProfile to the User class in the AuthenticationService.cs file as follows:
public class User : UserBase, IUserProfile
{
// NOTE: Profile properties can be added for use in Silverlight application.
// To enable profiles, edit the appropriate section of web.config file.
// public string MyProfileProperty { get; set; }
public string Country { get; set; }
public string Gender { get; set; }
public int Age { get; set; }
}
I try to run it and get the following error on the Silverlight app when I try to login using admin/admin: "The specified resource was not found." A little digging reveals that I need two things: first, some additions to the web.config file that I was missing, and second, the ServiceDefinition.csdef had to have it’s enableNativeCodeExecution set to true. Here’s the pieces:
<!-- handlers and httpHandlers sections require the following additions -->
<handlers>
<add name="DataService" verb="GET,POST" path="DataService.axd" type="System.Web.Ria.DataServiceFactory, System.Web.Ria, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
</handlers>
<httpHandlers>
<add path="DataService.axd" verb="GET,POST" type="System.Web.Ria.DataServiceFactory, System.Web.Ria, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" validate="false"/>
</httpHandlers>
<!-- the ServiceDefinition.csdef gets the enableNativeCodeExecution set to true -->
<WebRole name="WebFilesRole" enableNativeCodeExecution="true">
Once those changes were made, I was able to run the Silverlight application, login using admin/admin and logout. Now one more item on the agenda for this post. I want to see the profile information we added in the AppInitializer code. So I modify the LoginControl.xaml and LoginControl.xaml.cs as follows.
<StackPanel x:Name="logoutControls" Style="{StaticResource LoginPanelStyle}">
<TextBlock Text="welcome " Style="{StaticResource WelcomeTextStyle}"/>
<TextBlock Text="{Binding Path=User.Name}" Style="{StaticResource WelcomeTextStyle}"/>
<TextBlock Text=" | " Style="{StaticResource SpacerStyle}"/>
<TextBlock Text="" x:Name="ProfileText" Style="{StaticResource WelcomeTextStyle}"/>
<TextBlock Text=" | " Style="{StaticResource SpacerStyle}"/>
<Button x:Name="logoutButton" Content="logout" Click="LogoutButton_Click" Style="{StaticResource LoginRegisterLinkStyle}" />
</StackPanel>
With the code behind changed like this:
private void UpdateLoginState()
{
if (RiaContext.Current.User.AuthenticationType == "Windows")
{
VisualStateManager.GoToState(this, "windowsAuth", true);
}
else //User.AuthenticationType == "Forms"
{
VisualStateManager.GoToState(this,
RiaContext.Current.User.IsAuthenticated ? "loggedIn" : "loggedOut", true);
if (RiaContext.Current.User.IsAuthenticated)
{
this.ProfileText.Text = string.Format("age:{0}, country:{1}, gender:{2}",
RiaContext.Current.User.Age,
RiaContext.Current.User.Country,
RiaContext.Current.User.Gender);
}
}
}
Now when I login, I get to look at something like this:
Cool. In Part 2, I’ll modify the UserProfile to capture the data I want to keep in my Adventure application and complete the user registration changes to the Silverlight application as well as clean up and prepare the app for some real application development in follow-on posts.
If you have any questions or ways to do this better, I’d love to hear from you.