Using ASP.NET Membership in Silverlight


The Authentication support in RIA Services uses ASP.NET Membership, Roles, and Profile support by default. While this provides a lot of power, flexibility, and interoperability, it may also leave you wondering “How do I use ASP.NET Membership in Silverlight?” The answer (and I hope it doesn’t surprise you) is “Write a RIA Services DomainService.”


I want to start first by explaining the pattern I’ll use to create the service. Most developers do not start from a clean slate when writing an application. You’re likely to already have some mix of existing services, existing business logic, and existing data access layers. Not only is it easy to reuse these resources when writing a RIA Services application, it is encouraged. No need to rewrite something if it will continue to be useful in your new application. This approach shifts our challenge from figuring out how to rewrite a component to figuring out how to reuse it.


ASP.NET Membership falls squarely into this category. It encompasses business logic and data access in a useful and well-vetted API based on the MembershipUser type. Our first task in making Membership DomainService-friendly is to determine if we can represent the MembershipUser as an entity. There are two qualifications a type must have to be considered an entity. First, it must be public. Second, and often the trickier requirement when dealing with existing types, it must have a primary key marked with a KeyAttribute (from System.ComponentModel.DataAnnotations). MembershipUser is public and has a primary key (UserName and Email) so that is a good start. However, it is also a type we cannot modify. This prevents us from being able to apply the KeyAttribute or any other useful metadata.


There are typically two approaches to solve this problem. The first is to create a proxy type (there are a number of different names for this pattern; including DTOs and Views). An alternate solution is to provide metadata for the type using an approach apart from attributes and the CLR. There is a great sample here that specifies the key attribute using xml.


For this sample, we’ll follow the first approach and create a type similar to MembershipUser to expose from our DomainService. However, since we’re creating it solely for the service, we will be able to control the exact shape and metadata. The resulting type is MembershipServiceUser.


public class MembershipServiceUser
{
public string Comment { get; set; }
[Editable(false)]
public DateTime CreationDate { get; set; }
[Key]
[Editable(false, AllowInitialValue = true)]
public string Email { get; set; }
public bool IsApproved { get; set; }
[Key]
[Editable(false, AllowInitialValue = true)]
public string UserName { get; set; }
// …

public MembershipServiceUser() { }
public MembershipServiceUser(MembershipUser user)
{
this.FromMembershipUser(user);
}

public void FromMembershipUser(MembershipUser user)
{
this.Comment = user.Comment;
this.CreationDate = user.CreationDate;
this.Email = user.Email;
this.IsApproved = user.IsApproved;
this.UserName = user.UserName;
// …
}

public MembershipUser ToMembershipUser()
{
MembershipUser user = Membership.GetUser(this.UserName);

if (user.Comment != this.Comment) user.Comment = this.Comment;
if (user.IsApproved != this.IsApproved) user.IsApproved = this.IsApproved;
// …

return user;
}
}


There are a few things to notice about the MembershipServiceUser as a proxy type. First, all the properties allow both get and set, but additional metadata has been added to the properties to define how they can be used. Second, there are conversion method to move from business logic type to domain service type and back. Finally, MembershipServiceUser only exposes the subset of the properties found on MembershipUser that we want to be available on the client.


Now that we have an entity type that can be exposed from a DomainService, we’re ready to look at making the Membership API available on the client as well. For this, we’ll create a MembershipService that extends the base DomainService type.


[EnableClientAccess(RequiresSecureEndpoint = false
/* This should be set to true before the application is deployed */)]
public class MembershipService : DomainService
{
[RequiresRole(“Administrator”)]
public IEnumerable<MembershipServiceUser> GetUsers()
{
return Membership.GetAllUsers().Cast<MembershipUser>().Select(
u => new MembershipServiceUser(u));
}

[Invoke(HasSideEffects = true)]
public void CreateUser(MembershipServiceUser user, string password)
{
Membership.CreateUser(user.UserName, password, user.Email);
}

[RequiresRole(“Administrator”)]
public void DeleteUser(MembershipServiceUser user)
{
Membership.DeleteUser(user.UserName);
}

[RequiresRole(“Administrator”)]
public void UpdateUser(MembershipServiceUser user)
{
Membership.UpdateUser(user.ToMembershipUser());
}

[RequiresRole(“Administrator”)]
public void ResetPassword(MembershipServiceUser user)
{
user.ToMembershipUser().ResetPassword();
}

    // …
}

Again, there are a few things worth taking a look at. First, this service will eventually need to require a secure endpoint. Every service that accepts a password or other critical user information should be secured. Second, every method (except CreateUser) requires the user to be an “Administrator”. This is advisable since we don’t want just anybody to be able to delete users. Once again we’ve only exposed a subset of the API and we can add or remove methods from the MembershipService based on what we would like to do on the client.


At this point, Membership is available for use from the client. The API is slightly different since we’ve followed standard RIA Services conventions, but all the power is still there. In addition, we’ve made Membership available using a general pattern that can be applied to many kinds of existing services and other business logic.


The full source for the MembershipService is available as part of the Authorization Sample. It’s only a few more methods, but it might give you an idea of how the pattern grows.

Comments (16)

  1. RC says:

    Hi Kyle,

    Do you know how to modify below with a  .where.  I want to limit the returned users by a profile value.

    Membership.GetAllUsers().Cast<MembershipUser>().Select(            u => new MembershipServiceUser(u));

  2. RC says:

    I'm sure there is a linq solution but this worked…

    Dim muFilterList As New List(Of MembershipServiceUser)

           For Each mu As MembershipServiceUser In Membership.GetAllUsers().Cast(Of MembershipUser)().[Select](Function(u) New MembershipServiceUser(u)).ToList

               '      If CDbl(ProfileBase.Create(mu.UserName).GetPropertyValue("zzzz").ToString) = zvalue Then

               muFilterList.Add(mu)

               '    End If

           Next

           Return muFilterList

  3. Jim says:

    Hi Kyle,

    This is a great article and I think I understand the entire premise of it. I'm really getting tripped up on the client side though. I'm sure it's very simple to do but I just can't wrap my head around it. Could you please give me a simple example of what the code would look like on the client? I just want to retrieve the logged on user's user name, how would I do that? Thanks.

  4. kylemc says:

    @Jim

    If you want the user's name (or other authentication information), you should start with this link.

    msdn.microsoft.com/…/ee707361(VS.91).aspx

    If you want the source for this sample, it's linked at the very end of the post.

  5. Jim says:

    Hi Kyle,

    Thanks for the quick response. This was actually a bad example. I did start here and am able to retrieve the username using webcontext.current.user.name. What I'm really after is the user id, email and a couple of other attributes that aren't exposed by webcontext.current.user. This is what led me to this article to begin with. I setup the MembershipServiceUser as you described and included ProviderUserKey in addition to what you laid out. I try getting to it through the client with…

    Dim context as New MembershipContext

    Dim user as New MembershipServiceUser

    …and I can then get to user.ProviderUserKey. It doesn't return anything at run time though. MembershipContext doesn't expose GetUser, even though I included it in the MembershipService. Also, when I try something like

    user = context.GetUsersByEmailQuery("name@company.com")  

    I get "Value of type 'System.ServiceModel.DomainServices.Client.EntityQuery(of  AppNS.MembershipServiceUser)' cannot be converted to 'AppNS.MembershipServiceUser'.

    I've also tried user = WebContext.Current.User but that can't be converted either.

    I guess I'm just missing the plumbing between MembershipContext and MembersheripServiceUser and I just can't figure out how to get the currently logged in user into the MembershipServiceUser. I'm sure it's just a line or two of code. Thanks.

  6. kylemc says:

    @Jim

    Not to avoid the question, but it might be better if you take a look at this link first.

    msdn.microsoft.com/…/ee707349(v=VS.91).aspx

    If you want to get a single item from a Load operation, just use loadOperation.Entities.Single() in the load completion callback.

  7. Zorayr Khalapyan says:

    Thanks for this article. I successfully created the service and am using most of it's functionality.

    However, what I am having trouble with this:

    public void ResetPassword(MembershipServiceUser user)

    {

           user.ToMembershipUser().ResetPassword();

    }

    How am I suppose to utilize this function if after resetting the password, I will lose access to the user? I tried to change this to

    public String ResetPassword(MembershipServiceUser user)

    {

           return user.ToMembershipUser().ResetPassword();

    }

    But I get the following error:

    Value cannot be null.

    Parameter name: passwordAnswer

    It seems that I should set my Membership.RequiresQuestionAndAnswer to false – but how? I am using the default SBAT authentication and database.  

  8. kylemc says:

    @Zorayr

    Yeah, turns out ResetPassword was a bad example. You absolutely do not want to return a string from that method. I think you'd need to get the right data from the client (add a passwordAnswer parameter to the method) and then email the new (temporary) password to the client. I haven't tried it before, but there are probably some good examples of resetting the password with ASP.NET.

  9. Zorayr Khalapyan says:

    Thank you for your answer. I am currently building the administrative side of my Silverlight application and one of the requirements is that an admin should be able to reset a user's password and set it to something random.

    Could you elaborate why the ResetPassword is not working? If an admin has a secure connection with the application, would it still be a bad idea to send the password across?

  10. kylemc says:

    @Zorayr

    I just misused the API in my sample. ResetPassword returns the new password and I didn't do anything with it (though it does change the password just fine). Here's a link to the actual documentation (msdn.microsoft.com/…/d94bdzz2.aspx).

    Returning a password from a service operation is always a risky action. If you can perform a security analysis and guarantee it to be safe in this situation, then feel free to return it. However, it's not something I would advise.

    An alternative approach would be to send the temporary password to the user in an email. Again, I'm not sure what risks are involved, but it seems to be an industry-standard approach. Here's a link off MSDN describing how to send an email (support.microsoft.com/…/310273).

  11. Zorayr Khalapyan says:

    Makes sense. I successfully implemented a password changer that uses that takes the old password as input from the user. When I try to use ResetPassword, I get an error saying ,"Value cannot be null. Parameter name: passwordAnswer." What additional work do I need to complete in order to allow reset without security question? I have been trying to change my Web.config, with no avail. Would you please take a look at this forum post which describes my problem in more detail – forums.silverlight.net/…/527230.aspx. Thanks

  12. Patrick says:

    Hi Kyle,

    I've been trying to adapt your MembershipService Class to use in my project but I have the asp.net user/membership tables in the same sql server database as the rest of my entity framework entities and not in a mdf file like in your example.

    My project compiles fine (and login works – so the application is able to access the asp.net user/membership tables) but when I call GetUsers() using a loadoperation from the client it never returns any entities. Do I need to tell the MembershipService where my asp.net user/membership tables are located? How do I do that?

    Many thanks.

  13. Patrick says:

    Please ignore my previous post. I was not specifying the membership configuration in web.config correctly. All resolved now.

    Keep up the good work!

  14. Shannon says:

    I know this page is a few years old, but if you are still out there.  I am trying to use this example in VS2013 and I am having trouble.  For one thing the DataForm toolkit is gone.  And after I remove all references to the toolkit and get it to open then when I try to save the dds I get an error about the value of a Key Member.  Is there any chance you could post an update to this example?

    Thanks.

  15. Shannon says:

    I was able to get it to work.  Thanks for a good example.

  16. xyk1 says:

    Kyle,

    Just wonder how I can assign roles when creating a user using the provided library?

    Thanks!

    Yukun