The records are not actually 'duplicates'.
Understanding the relationship between the providers and the role of the aspnet_Users table may help clear this up.
First, understand that MembershipProvider and RoleProvider do not depend on one another, with a minor exception of some concern bleeding in the MembershipProvider.DeleteUser() method which I will explain later.
The role of Membership is to authenticate a user and, with the help of Authentication, protect access to your resources.
The role of Roles is to control access to resources by role assignment.
These are 2 seperate concerns and are not related, regardless of what the state of aspnet_Users may imply.
The salient issue is that the value that is used to connect all of the providers is username
. The userid
guid is similar and only one aspnet_users record is created for any username if all providers use the same application name because the first thing each provider does is check aspnet_users for a matching username and application name. If it finds one, it uses it, if not, it creates one and assigns a guid to the userid.
While it may appear that the userId guid is the 'global' identifier, this is not the case.
So, with a minor caveat - you can user separate role providers with a common membership store by using the same applicationname for each instance of membership and a different applicationname for each roles instance if you are aware of the issue related next.
The (really stupid) violation of the Seperation of Concerns that I mentioned earlier involves the actions taken by the SqlMembershipProvider when DeleteUser(deleteRelatedData) is called. It is then that SqlMembershipProvider acts as if it is the only dog in the fight, crosses over to roles and profiles to delete records using only it's keys and thus miss records that you expect to be gone.
Consider: (this is using the aforementioned setup of single membership, multiple roles)
You create user "John" with membership, assign him some roles with Roles. You now have to records in aspnet_users for John and everything works swimmingly. "John" is a manager and has appropriate high level roles assigned.
"John" becomes the 'fired guy' for some reason and is deleted via membership. As stated before, in our unique scenario, the membership records are deleted but the roles information remains.
A new "John" is hired to answer the phones and a membership user is created for him.
In his spare time, john explores the company intranet and can't believe how much cool stuff he can do and as a goof decides to give himself a raise in the HR module.
Guess what? new "John" is immediately a manager due to orphaned roles that are keyed off of "username".
So, that is the 'minor' caveat that you must be aware of when deciding to leverage, or in this case, coerce, the intrinsic, well tested and known provider stack.
I have already covered a scenario very similar to this and provide a proof of concept for a simple modification to eliminate this issue here and here .
Alternately, you can simply be aware of this and write some extra code to clean up when you delete a user.
Both of these are a far more attractive option than implementing custom providers.
Cheers and good luck.