Vibrant discussion about CSLA .NET and using the framework to build great business applications.
I've seen a fair number of posts on this (one more this morning) so I thought I'd share some code.
My solution is to completely leverage the implementation of the provided SqlMembershipProvider and SqlRoleProvider. Of course, these are really only 2-tier and so the CslaProviders are here to facilitate the 3-tier behavior - using a command object to remote over and execute the appropriate logic on the app server.
The intention for us is to use the membership & role providers if we should move to Windows forms as well. Thus, this has been integrated in the principal and identity classes.
#1: Setup DB
So one of the first things you'll need to do to roll with this implementation is to run the appropriate utility that configures all the necessary tables and stored procedures for the implementation of SqlMembershipProvider and SqlRoleProvider that exists in the .Net framework. This utility is: aspnet_regsql.exe. There's plenty of info online on how to do this.
The next thing I might suggest is to download the sample code of the implementation for the SqlMembershipProvider and SqlRoleProvider - you can find it here: http://weblogs.asp.net/scottgu/archive/2006/04/13/442772.aspx. The reason why I might suggest it is because you can include it within your project and debug step through it. It's entirely optional, however.
#2: Web.config file
In the Web.config, when you're setting up your role provider and membership provider for the respective API's, it'll look like this:
<membership defaultProvider="CslaMembershipProvider"> <providers> <!-- Note that the connection string is irrelevant on the client side Web app --> <!-- It is supplied on the remoting site's Web config. SqlMembershipProvider balks if it's not there. --> <clear/> <add name="CslaMembershipProvider" type="<YourNameSpaceForTheCslaProviders>.CslaMembershipProvider" connectionStringName="<YourDBConnString>" functionalProvider="Microsoft.Samples.SqlMembershipProvider" passwordFormat="Encrypted" minRequiredNonalphanumericCharacters="0" minRequiredPasswordLength="6" enablePasswordRetrieval="false" requiresUniqueEmail="true" requiresQuestionAndAnswer="true" enablePasswordReset="true" description="Csla enabled membership provider"/> </providers></membership><roleManager enabled="true" defaultProvider="CslaRoleProvider" cacheRolesInCookie="true" cookieName=".RolesCookie" cookieTimeout="30" cookieSlidingExpiration="true" cookieProtection="All"> <providers> <add name="CslaRoleProvider" type="<YourNameSpaceForTheCslaProviders>.CslaRoleProvider" functionalProvider="Microsoft.Samples.SqlRoleProvider" connectionStringName="<YourDBConnString>" description="Csla enabled role provider"/> </providers></roleManager>
The functionalProvider is the only additional key - otherwise the setup is exactly how the default providers would be implemented. The functionalProvider tells the CslaProvider which implemented provider it should utilize. Note that I happen to be using the samples ones - if you weren't using the sample ones, you'd use: System.Web.Security.SqlMembershipProvider, etc.
The web.config portions above should match between the remoting app and the web app. The one exception here is that you don't need to place a real connection string in the web app's web.config. Data access won't be happening from the web application. However, if you don't include the key, the built-in providers will throw an exception. So just "connectionStringName="none"" is sufficient.
Since we're leveraging the implementation of the built-in providers, feel free to utilize all of the bells and whistles that it provides. Encrypting passwords, etc.
#3: Principal & Identity objects
The identity object gets it's "authenticated" value by leveraging the membership API.
private void DataPortal_Fetch(Criteria criteria)
{
bool authenticated = System.Web.Security.Membership.ValidateUser(criteria.Username, criteria.Password);
if (authenticated)
_isAuthenticated = true;
_name = criteria.Username;
}
else
_isAuthenticated = false;
_name = "";
And the principal gets its roles via the roles API:
public override bool IsInRole(string role)
return Roles.IsUserInRole(Identity.Name, role);
To be honest I don't know if it caches the roles yet (haven't looked), but obviously that wouldn't be a problem to implement.
#4: Authenticating via login page
protected void Login1_Authenticate(object sender, AuthenticateEventArgs e)
e.Authenticated = <PrincipalClass>.LogOn(Login1.UserName, Login1.Password);
HttpContext.Current.Session["CslaPrincipal"] = Csla.ApplicationContext.User;
When authenticating, simply going through the principal's login will invoke the appropriate membership and role API.
#5: CslaRoleProvider & CslaMembershipProvider
These classes effectively "wrap" a functional provider - instantiating and initializing the provider so that whenever the Role API or Membership API calls it, that it calls the appropriate "functional provider".
The initialization of them looks like this:
/// <summary>
/// Initialize is overridden to take the name/value collection and strip off the one thing
/// we care about - the functionalProvider. It's removed to not cause exceptions on the
/// implemented providers.
/// </summary>
public override void Initialize(string name, NameValueCollection config)
if (config == null)
throw new ArgumentNullException("config");
// Determine the functional provider, instantiate and initialize it.
Type functionalProviderType = Type.GetType(config["functionalProvider"]);
config.Remove("functionalProvider");
_wrappedProvider = (MembershipProvider)Activator.CreateInstance(functionalProviderType);
_wrappedProvider.Initialize(name, config);
That is, create an instance of the right functional provider and initialize it with the config information.
And with each invocation of the different methods, we simply delegate to the underlying behavior of the functional provider. But what happens is that before a delegation occurs, it looks to see if it needs to execute the functionality on a remoting application - if it does, it uses a command object to facilitate the execution of the method on the app server, returning the appropriate return values and out parameters. There's some duplication of logic in the "ExecuteRemoteMethod" (exists both in the role and membership providers) but I'm not worried about it at this point.
It should be stated that this hasn't been given the rounds in a production environment but it seems to have worked well for us up to this point and I feel like it should be a sound approach. Also, I've only pursued this approach with the Sql providers within the .Net framework - how well it might work with any others, I haven't explored.
Happy coding,
Chris
p.s. the two providers were put into one txt file as I couldn't seem to figure out multiple attachments.
There was one additional thing I forgot to mention.
On the remoting directory'sWeb.config file, you'll need to specify "applicationName" as a member for both within the Web config file.
That is, if your main web application is "twiggy", then you'll need in your remoting Web.config to have: applicationName="/twiggy" so that when it runs the appropriate membership and/or role api implementation that it is querying for the authorization/authentication of the appropriate application.
I'm sorry to bump this thread, but I'm having a slight problem.
The SqlMembershipProvider seems to work, but the SqlRoleProvider is not. I get this error when trying to put it into Web.config:
Parser Error Message: Value cannot be null.Parameter name: type
Source Error:
Line 252: <providers>Line 253: <add name="SqlRoleProvider"Line 254: type="Azavia.Web.Security.SqlRoleProvider"Line 255: functionalProvider="System.Web.Security.SqlRoleProvider"Line 256: connectionStringName="main"
Source File: C:\projects\azavia\www\Web\web.config Line: 254
The weird thing is, I am specifying the type parameter, as you can see below.
<roleManager cacheRolesInCookie="true"
<
</
If anyone could help with this, it'd be much appreciated.
Also, does anyone have something similar for the profile provider?
Hey there -
You won't be able to get the profile provider to work in the same way. Unfortunately there are some classes used in the API that are not serializable. I started to try to get it to work but it was going to be too much of a PITA when I ran into the non-serializable classes. I decided I didn't care that much about the profile provider so I hadn't pursued it further.
At first glance I don't see a problem with what you have but I do know that I had included more specific information for the functional provider. Also, you could certainly do as I suggest further above, download the sample code and actually step through it in the debugger - that's probably the best way to diagnose what you're running into.
Good luck,
My portion in me web.config
skagen00:Hey there - You won't be able to get the profile provider to work in the same way. Unfortunately there are some classes used in the API that are not serializable. I started to try to get it to work but it was going to be too much of a PITA when I ran into the non-serializable classes. I decided I didn't care that much about the profile provider so I hadn't pursued it further.
Ah, I see. May I ask then what you do for profile information?
skagen00:At first glance I don't see a problem with what you have but I do know that I had included more specific information for the functional provider. Also, you could certainly do as I suggest further above, download the sample code and actually step through it in the debugger - that's probably the best way to diagnose what you're running into. Good luck, Chris
Oh, for some reason I had to set type to:
type="Azavia.Web.Security.SqlRoleProvider, Azavia.Web"
It didn't require me to do that for the membership provider, so that is strange.
Seems to be working now though, thanks.
One thing; is it required to put that code you specified for the login control? Following the example from project tracker, well I don't think it has anything like that.
Again thank you.
First, just don't really have that big of a need for profile information (and wasn't really fond of how it's stored anyways in the implemented provider) - so I just didn't pursue it further. I initially wanted to carry it through simply "because I could" and that it wouldn't be that much work.
I'm glad you got it to work. As far as the code there for the login control - it's up to you. The authenticated principal is attached to the current thread and it is done so with each request - storing it in the session allows one to grab it and reattach it to the current request so that if roles and so forth need to be checked for authorization purposes - it's there. (I think I showed the global.asax code... but that at least is similar to PTracker I think)
I don't exactly recall how PTracker did it but I found the approach I used above to be a more to my liking. With the implementation I used, the principal and identity objects are intertwined with the role and membership providers. (These can be used in a WinForms client too)
An unhandled exception was generated during the execution of the current web request. Information regarding the origin and location of the exception can be identified using the exception stack trace below.
[SecurityException: Principal must be of type BusinessPrincipal, not System.Web.Security.RolePrincipal]
I don't believe your error is related to the Csla providers.
I looked up the error in Csla and it occurs within SetContext of the DataPortal.
Does your principal which you are using for your current user inherit from BusinessPrincipal?
You might try putting a breakpoint within this method and see what the value is during execution.
good luck,
Hi Chris,
Thanks a lot for posting the code. It's really a clever solution.
I have a problem getting all members GetAllUsers() and I'm wondering if you can help me.
_wrappedProvider is null and I don't know why it has not been inicialised at this time. So, it is giving me the null exception.
Exception Details: System.NullReferenceException: Object reference not set to an instance of an object.
Any idea?
Thanks.
Have you successfully had other methods work? (i.e. is this your first attempt?)
Have you tried throwing a breakpoint within here to see what is happening?
When you invoke System.Web.Security.Membership.GetAllUsers(), it will call Provider.GetAllUsers. Provider, as a property of Membership, calls Initialize(), and within here it will attempt to initialize the CslaMembershipProvider (provided it has been properly configured in the Web.config).
In CslaMembershipProvider, here's where _wrappedProvider is instantiated:
public override void Initialize(string name, NameValueCollection config) { if (config == null) { throw new ArgumentNullException("config"); } // Determine the functional provider, instantiate and initialize it. Type functionalProviderType = Type.GetType(config["functionalProvider"]); config.Remove("functionalProvider"); _wrappedProvider = (MembershipProvider)Activator.CreateInstance(functionalProviderType); _wrappedProvider.Initialize(name, config); }
So, try throwing a breakpoint in there and see what occurs. I suspect you've got something a touch off in your configuration. Remember as well if you're using remoting that you need the Web.config components over on the app server as when remoting occurs the membership API needs to initialize itself with the right information.
Thanks a lot for your reply. Sorry if I haven't sent you enough information.
To be honest, the only method I have tested until now is the ValidateUser();
Seems to be working fine. I've throwed the breakpoint, even before bother you with this problem. What happened maybe is mine lack of knowledge about this framework. Anyway...
First, it stops on Initialize() just when I try to log in (I don't know if it's right). It calls the asp:Login and authenticate the user correctly. At the end I got my Csla.ApplicationContext.User, using my TMSPrincipal and TMSIdentity. Until here everything looks fine.
Have a look at my web.config:
<membership defaultProvider="CslaMembershipProvider" userIsOnlineTimeWindow="15"> <providers> <clear /> <add name="CslaMembershipProvider" type="CslaMembershipProvider" connectionStringName="TMSSecurity" functionalProvider="Microsoft.Samples.SqlMembershipProvider" applicationName="\" enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="true" requiresUniqueEmail="false" passwordFormat="Hashed" maxInvalidPasswordAttempts="5" passwordAttemptWindow="10" description="Csla enabled membership provider" /> </providers> </membership> <!-- functionalProvider="Microsoft.Samples.SqlRoleProvider" --> <roleManager enabled="true" cacheRolesInCookie="true" cookieName=".ASPROLES" cookieRequireSSL="true" defaultProvider="CslaRoleProvider"> <providers> <clear /> <add connectionStringName="TMSSecurity" applicationName="\" name="CslaRoleProvider" type="CslaRoleProvider" functionalProvider="Microsoft.Samples.SqlRoleProvider" description="Csla enabled role provider" /> </providers> </roleManager>
So, it throws the exception when I try to create an ObjectDataSource using CslaMembershipProvider, and associate it with a ListBox. Even logged on the page (which means passing through Initialize()) my _wrappedProvider is null when it GetAllUsers() and it is what I am struggling to understand.
What is really weird is that if I throw a breakpoint on ValidateUser(), the _wrappedProvider is not null and working absolutelly fine: Watch result: {Microsoft.Samples.SqlMembershipProvider}
Any idea will be really appreciated. Thanks a lot Chris.
I can't tell you that the role is using the methods...The role provider seems to be working fine, but I'm not sure I'm using correctly:
My TMSPrincipal:
public override bool IsInRole(string role) { TMSIdentity identity = (TMSIdentity)this.Identity; return identity.IsInRole(role); }
//public override bool IsInRole(string role) //{ // return Roles.IsUserInRole(Identity.Name, role); //}
Note that I use IsInRole straight from System.Web.Security what I think is not ideal, because this way I'm not using my CslaRoleProvider, right?
My TMSIdentity:
private void DataPortal_Fetch(Criteria criteria) { bool authenticated = System.Web.Security.Membership.ValidateUser(criteria.Username, criteria.Password); if (authenticated) { _isAuthenticated = true; _name = criteria.Username; foreach (string role in System.Web.Security.Roles.GetRolesForUser(_name)) { _roles.Add(role); } } else { _isAuthenticated = false; _name = ""; } }
Ohhh... the provider is working fine except when you're trying to bind a listbox to GetAllUsers() as an ObjectDataSource, it's not working?
Are you binding it to Membership.GetAllUsers(), or are you calling the provider directly? (I don't think offhand it would matter but I'd have to look).
If you simply throw a button on a form and call Membership.GetAllUsers(), do you get the results you're looking for? (in the handler just throw an object obj = Membership.GetAllUsers();). If you're not getting users, it should be possible to step through the debugger to see what is occurring - if it's simply not working, try dumbing it down & shut off remoting if that's turned on in your app and see if that remedies the problem. There are undoubtedly some possible ways to narrow down the problem.
Do you have remoting turned on? If so, I want to remind you to make sure the information is in the remoting app's web.config. And if you have the membership provider in an assembly, you may need to be more specific when declaring the type.
My web.config is below. (xxx is just suppressed info)
<!--
<doublepost>