views:

153

answers:

1

After all the answers to my last question about fine-tuning turned out to be more useful than I expected, I thought that I would ask another similar Question about the MembershipProviders as well.

Okay, so firstly, to clarify: I know what a Membership, Role, and Profile provider is, how to implement my own, and how to configure them, and most of the things about them.
Implementing a role and profile provider is pretty straightforward, because they only require simple CRUD most of the time. (A single line of LINQ is enough for about half of the RoleProvider's methods.)

However, the Membership provider is a differend beast. Many of you may realize that it violates the SR (Single Responsibility) principle, because it has to do EVERYTHING related to user management. While this leaves a lot of room for customizations, it has its downsides as well. There is no information on the Internet about what their EXACT expected behaviour is, such as when should they throw exceptions or simply return null, and stuff like that.

I use this sample implementation for reference, but it also contains several contradictions.

  • For example, it uses its own ValidateUser method for checking for credentials in the ChangePassword method. But the ValidateUser also updates the user's LastLoginDate to the current date. So, does the framework expect that I set it in my own provider as well, or is it simply a mistake in the sample?
  • The other is: the ChangePassword method throws an exception every time when validating the new password, but CreateUser doesn't ever throw an exception, it simply returns false.
  • And last, but not least: it counts the invalid password attempts of the user and locks them if it passes a threshold. While this is good, but it requires manual action to unlock the users. Is it a problem if my provider automatically unlocks the user after a certain amount of time?

  • (EDIT) I almost forgot: the CreateUser method in the sample inserts the ID from the method parameter. I actually think this is bad practice, because I use inters with auto incement as IDs, so inserting them from some method parameter is not an option. Should I just ignore the parameter, or require that its value is null and throw an exception if it isn't?

All in all, does ASP.NET have any assumptions about the behaviour of a MembershipProvider?
Is there any documentation which describes when should I throw an exception or just return null?

I also tried to find a set of generic unit tests which would provide some guidance about the expected behaviour, but no luck, I found plenty of articles about "Unit testing is good", and "How to unit test a MembershipProvider", but not one where there would be any actual tests.

Thanks in advance for everyone!

+2  A: 

You can consult MSDN for guidance. For instance, RoleProvider.RemoveUsersFromRoles provides the following guidance:

RemoveUsersFromRoles is called by RemoveUserFromRole , RemoveUsersFromRole , RemoveUserFromRoles , and RemoveUsersFromRoles methods of the Roles class to remove the specified users from the specified roles at the data source. Only roles for the configured ApplicationName are modified.

If any of the specified role names are not found for the configured applicationName, we recommend that your provider throw a ProviderException.

If any of the specified user names are not associated with any of the specified role names for the configured applicationName, we recommend that your provider throw a ProviderException.

If any of the specified user names is null or is an empty string, we recommend that your provider throw an exception.

If any of the specified role names is null or is an empty string, we recommend that your provider throw an exception.

If your data source supports transactions, we recommend that you include each remove operation in a transaction and that you roll back the transaction and throw an exception if any remove operation fails.

And RoleProvider.GetRolesForUser says:

GetRolesForUser is called by the GetRolesForUser method of the Roles class to retrieve the role names that the specified user is associated with from the data source. Only the roles for the configured ApplicationName are retrieved.

If no roles exist for the specified user for the configured applicationName, we recommend that your provider return a string array with no elements.

If the specified user name is null or is an empty string, we recommend that your provider throw an exception.

But, in practice, there are just a few methods and behaviors in each provider that are required and expected. The rest are support for the features that you wish to implement.

After several years of dealing with the default provider stack I have come to understand some of the things that are giving you pause regarding the sample you link to, which is a very simple implementation that cuts corners, for instance, in the ChangePassword method.

If you use reflector and examine SqlMembershipProvider you will notice some salient differences...

For instance:

  • ValidateUser updates login date because, well, the user is logging in and it does so by calling .CheckPassword with true for UpdateLastLoginTime. Change password calls the same method but supplies false.

  • The CreateUser method accepts a ProviderUserKey because the SPROC also will accept a UserId parameter. This is to allow recreation and syncronization between the membership and users tables. As a consumer of the API you would not normally utilize this functionality but the provider stack does internally.

  • Regarding lockout.. It is up to you. That is what implementing custom providers is all about: getting the behavior you want. As I said, there are very few systematic requirements.

So, this brings us to the last part of your question (both paragraphs allude to the same concern): expected behavior and unit testing...

Since the behavior of a provider is largely arbitrary and there are few expected behaviors, an established set of generic tests would contain very few methods. You need to write tests that apply to the behavior of your implementation.

As a final note, I suggest you find and download the Asp.Net provider toolkit samples. It provides you with working source code for the complete Sql provider stack and will give you some insight as to how 'real' providers are implemented.

Afterthoughts:

At this very moment, I am implementing a full SQLite provider stack that is 100% compatible with the default Sql stack. The test suite verifies behavioral parity between my stack and asp.net's. If you hit my blog via my profile and contact me I will let you know when the tests are complete and you can use them as a benchmark as to what exactly is expected from the default stack.

Sky Sanders
+1. Thanks for your answer. However, you left out the most important part. In which methods should I throw exceptions, and in which should I just return false (or null, where that's applicable)?
Venemo
@Venemo - I updated the answer to give you the 'formal' guidance you are asking for as well as the subjective guidance.
Sky Sanders
Thank you very much, this is exactly what I was looking for. :) I didn't know MSDN had such a definitive guide. (Oh, and I will contact you on your blog, if you don't mind.)
Venemo