Inconvenient programmatically working with SharePoint users (SPWeb.EnsureUser)

, ,

According to the Windows SharePoint Services (WSS) v3 SDK the SPWeb.EnsureUser(String) method is all you need while programmatically working with users. Using nothing more than the login name it checks for you whether the particular user exists in the current web and adds it if required. Within a single line of code it retrieves for you a proper reference to a user no matter the membership/role provider. While it’s really that simple while working in the scope of your SharePoint Web Application, things get slightly more challenging when used in combination with a custom code outside of the HttpContext.

What’s exactly the problem?

Let’s have a look at a simple case: you want to programmatically add a user to an existing group. According to the SharePoint SDK it should be enough to call in your code:

string login = "UserName";
string groupName = "Group";
SPUser user = SPContext.Current.Web.EnsureUser(login);
SPGroup group = SPContext.Current.Web.Groups[groupName];
group.AddUser(user);

It is that simple. The above code works correctly: the EnsureUser method returns a valid SPUser object which can be added to the Group group.

First thing you should notice is the use of SPContext to get a reference to the current site. Imagine that you wanted to run the same case as described above from a Console Application: SPContext.Current returns null so in order to get to an instance of SPWeb you would have to change the code a little:

using (SPSite site = new SPSite("http://adventureworks.com"))
{
  using (SPWeb web = site.OpenWeb())
  {
    string login = "UserName";
    string groupName = "Group";
    SPUser user = web.EnsureUser(login);
    SPGroup group = web.Groups[groupName];
    group.AddUser(user);
  }
}

The above code should run in a Console Application without bigger problems. But let’s take our case one step further: let’s check if the code works with custom membership/role providers? Let’s assume our user name looks like forms:UserName:

using (SPSite site = new SPSite("http://adventureworks.com"))
{
  using (SPWeb web = site.OpenWeb())
  {
    string login = "forms:UserName";
    string groupName = "Group";
    SPUser user = web.EnsureUser(login);
    SPGroup group = web.Groups[groupName];
    group.AddUser(user);
  }
}

If you run the above piece of code (assuming you have Forms Based Authentication (FBA) configured correctly, that its membership or role provider is called forms and that there is a user called UserName), all you see is an SPException: SharePoint cannot find the user.

Surprisingly the same piece of code executed from a control or an Application Page runs correctly: the user is being retrieved and added to the group. Does it look like we miss some context here?

Is it all about the context?

Usually when things don’t work in custom Console Applications talking to SharePoint while they work perfectly in the Web UI the odds are high that the particular piece of code requires Context information. You can easily check it by including the following code snippet right before your code:

if (HttpContext.Current == null)
{
  HttpRequest request = new HttpRequest("", web.Url, "");
  HttpContext.Current = new HttpContext(request,
    new HttpResponse(new StringWriter()));
  HttpContext.Current.Items["HttpHandlerSPWeb"] = web;
}

Unfortunately the above doesn’t solve our issue: if you make a call to the SPWeb.EnsureUser method, you will once again get an SPException. So what is wrong here?

The challenge

Usually to find out how a particular piece of SharePoint code works I would open Reflector and have a look at the code flow. By calling the particular pieces of code I would try to find out which piece fails and then try to figure out how to solve the issue. While the SPWeb.EnsureUser method itself is pretty straight-forward, things get complicated the more you dig into the code.

First of all pretty soon you will stumble upon obfuscated code. While it is doable to see what’s going on under the hood you will have to struggle with Intermediate Language (IL) – it’s all you see. Unless you have some serious development experience, it is not that easy to follow the flow of IL.

Second of all the EnsureUser method heavily relies on internal types and methods. Because of this it is complicated (but not impossible) to run pieces of code which are being called by the EnsureUser method. You have to reflect the assemblies and types to get to the right objects and methods. Once again: it’s complicated but not impossible.

So what’s the real problem?

After doing some research I’ve found that at some point one of the internal methods makes call to System.Web.Security.Roles.Providers collection. It’s a public property in the System.Web assembly so there is nothing that holds you back from calling it from your code. It turns out that before the Roles.Provider returns the collection of available role providers it checks whether the plumbing is enabled. Not surprisingly, while querying this property in your Console Application, you will get a ProviderException with a rather cryptic message: “This feature is not enabled”.

Can we do something about it?

Digging even further I have found out that at some point the Roles class tries to retrieve the system.web/roleManager configuration section. Because that section is available in the web.config of a Web Application with FBA configured, the code works properly. But Console Applications don’t have web.config, do they?

Well they don’t, but what you can do is to create a config file like ConsoleApplication1.exe.config (assuming that ConsoleApplication1.exe is the name of your executable after building) and copy there that particular piece of configuration from your web.config. If you run your Console Application now, it should work perfectly.

Inconvenience

The SPWeb.EnsureUser method encapsulates quite a large piece of code from the developers. While it allows us to get a proper user reference with just a single line of code it lacks flexibility. I can imagine quite a few scenarios when you would want to benefit of this method outside of the context of your Web Applications. It’s a shame though that it’s that difficult to find out what the real issue is and how it can be solved.

Possibly related posts

9 Responses to “Inconvenient programmatically working with SharePoint users (SPWeb.EnsureUser)”

  1. Calvin Irwin Says:

    Great post. Although I still cant figure out how to get the console app working to add my users using SPWeb.EnsureUser. I am only using a custom membership provider and I added the following entry to my app.config but it still wont work. Am I missing something?

  2. Waldek Mastykarz Says:

    @Calvin: Have you included the context information as well?

  3. Calvin irwin Says:

    I am not sure what you mean? I have added the providers node and added the connection string node for good measure. I have not instantiated an spcontext object in my code because it's implied with the SPSite inject isn't it? My code looks just like yours In the second last example…

  4. Waldek Mastykarz Says:

    @Calvin: the problem is: it isn't. That's why you have to provide the context information yourself before calling the SPWeb.EnsureUser method.
    What I mean is that you first need to instantiate the SPSite/SPWeb, setup the context information as described above in the context paragraph and then call the SPWeb.EnsureUser method. This all of course assuming that there is a yourapp.exe.config file with your membership provider registered there.

  5. Calvin Irwin Says:

    I can\'t believe how inconvienent this really is…but thanks for your help on this. So I added the code above that you mentioned (Setting the HttpContext) right before my first web.EnsureUser() call and it actually works…but then it stopped working after a short time. I did an IIS reset and now it simply does not work at all. Odd…

  6. Waldek Mastykarz Says:

    @Calvin: while I've heard about some weird issues with authentication/permissions and IIS reset, I've never experienced the problem you've described. Are you sure that nothing has been changed about the site/farm configuration in the meanwhile?

  7. Calvin Irwin Says:

    Yeah I built this configuration from scratch and I am the only one with access right now…I hate admitting defeat – which I am not of course :) but I have a work around (add the users via the site users page) so my code to customize and create sites for all of them works. I will have to revisit this later on as the working/not working is really starting to eat away at my sanity.

    Thanks again for all your help on this. I will let you know my progress if/when I make any.

  8. Richard Bailey Says:

    I wanted to say thanks… this was a GREAT help.

    Word of caution to anyone working on this – HTTPContext doesn't work from a remote location or a mapped drive. So, BE sure you're running this from the local machine… otherwise there are other configs you'll need to deal with.

  9. Waldek Mastykarz Says:

    @Richard: It's not only HttpContext but the whole code. As it's code based on the SharePoint API it has to be executed on a SharePoint server.

Leave a Reply

Security Code:

WP Theme & Icons by N.Design Studio
Entries RSS Comments RSS
Copyright © 2007 - 2010 Waldek Mastykarz

Creative Commons License