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
Anonymous
June 25, 2010
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));Anonymous
June 25, 2010
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 muFilterListAnonymous
December 16, 2010
The comment has been removedAnonymous
December 17, 2010
@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.Anonymous
December 17, 2010
The comment has been removedAnonymous
January 04, 2011
@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.Anonymous
February 17, 2011
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.Anonymous
February 18, 2011
@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.Anonymous
February 18, 2011
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?Anonymous
February 21, 2011
@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).Anonymous
February 22, 2011
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. ThanksAnonymous
November 06, 2011
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.Anonymous
November 07, 2011
Please ignore my previous post. I was not specifying the membership configuration in web.config correctly. All resolved now. Keep up the good work!Anonymous
April 24, 2013
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.Anonymous
April 25, 2013
I was able to get it to work. Thanks for a good example.Anonymous
February 27, 2014
Kyle, Just wonder how I can assign roles when creating a user using the provided library? Thanks! Yukun