Sunday, May 30, 2010

Linq to SharePoint for Anonymous users performance Part 2

Recently I posted about the bad performance of LINQ to SharePoint when using my anonymous users hack. Well I did some more testing and it turns out that my first test was much too premature. It seems that the statistics I was seeing were due to something entirely different, I am guessing the internal implementation of Linq to SharePoint vs CAML queries, but that is pure speculation.

First I will introduce 2 acronyms because I am a lazy typist:

L2SP - Linq to SharePoint
AL2SP - Anonymous Linq to SharePoint (Uses the workaround I suggested previously)

My recent testing was a bit more structured and thought out, and it showed that there is actually virtually no difference in using L2SP and my AL2SP work-around.

I set up two tests, one was reading all the items from a small list of 20 items 100 times consecutively, an attempt at simulating many web requests of a small list. The second test involved a large list of 5000 items, where each item was named with a random string. The query here was to pull out the top 100 items when ordered by name, thus simulating a random read of items from a large list.

In each test, the Title property of the first list item would be accessed in order to ensure that the retrieved items would be used by the test code and the query would have to run. I noticed that running a CAML query was much too fast before I added this, I suspect there is some clever optimization that skips the query if the results don't get used.

I ran these tests for a signed in user, in which case the code uses L2SP and old school CAML query. I then repeated the tests for an anonymous user which then uses AL2SP and old school CAML query.

Here are the results: The user using IE is logged in and the Chrome user is anonymous.

It is interesting to note that the CAML query does a better job in the 100 times operation, I suspect that there is some cost to setting up the Linq data context and translating the Linq query. However Linq does a much better job in the large list test.

Most important however is that the test results are pretty much the same for AL2SP and L2SP. This means that performance is NOT a problem when using AL2SP. I am curious if any other issues come up with this technique but it looks like it is back in my bag of tricks!

If you want to run the tests I created yourself, I have put the source code here. If you deploy the solution, you get the two lists created and you just need to place the test web part somewhere in the same site. Note that the code was meant just for this test, so it can easily fail if used otherwise. Also note that the feature activation takes a while since it created a list with 5000 items.


Wednesday, May 26, 2010

Linq to SharePoint for Anonymous users performance

UPDATE: I have been doing some more testing and I am coming to some results that are very different from my initial quick test here. I will have a post about it soon, but for now I'll say that the performance seems to be more than acceptable.

Not that long ago, I was happy to post that I figured out a way to run Linq to SharePoint for an anonymous user. I have some bad news for those who want to use it. I had a quick chat with Waldek Mastykarz the other day at a DIWUG event and I realized why he wasn't quite as excited about my solution as I was. There is quite a performance hit incurred when switching to a secure context, and my solution requires that this is done for all operations, including read operations. With the old CAML query approach, reads can be done without the context switch.

Today I wanted to get an idea of how much this performance hit really would be. So I put together a very quick (and probably not perfect) test. I won't post the code here just yet, but suffice to say that one section of the code ran the Linq query with my "solution" and the other just ran a CAML query the old fashioned way. Each section ran 100 times and used a stopwatch to measure execution time.

The results:

First page load - so JIT stuff has to happen here - so perhaps not fair

[4600] 0009: 2010-05-26 18:49:27.466 [CBI] 100 Linq Queries took 6000 milliseconds
[4600] 0009: 2010-05-26 18:49:27.472 [CBI] 100 CAML Queries took 2 milliseconds


Subsequent page refreshes

[4600] 0009: 2010-05-26 18:49:49.303 [CBI] 100 Linq Queries took 4271 milliseconds
[4600] 0009: 2010-05-26 18:49:49.307 [CBI] 100 CAML Queries took 3 milliseconds

[4600] 0009: 2010-05-26 18:50:03.791 [CBI] 100 Linq Queries took 4973 milliseconds
[4600] 0009: 2010-05-26 18:50:03.796 [CBI] 100 CAML Queries took 3 milliseconds

[4600] 0013: 2010-05-26 18:51:03.388 [CBI] 100 Linq Queries took 4445 milliseconds
[4600] 0013: 2010-05-26 18:51:03.392 [CBI] 100 CAML Queries took 3 milliseconds


From this I would say that there is a <cough>significant</cough> performance issue with the approach I hacked up. I would say that you forget about using it unless someone can come up with some way to cache the datacontext (I tried this and failed so far) or some other clever solution.

The reason I did not post the code here is that it's a quick hack of code that is part of a client web site that I am working on, and I don't want to breach any contract, etc. I also think that a blog post is coming soon about Linq performance in SharePoint in general, where I will do some more rigorous testing.

Sorry for the bad news.

Tuesday, May 11, 2010

Insert item into a (sub) folder using Linq to Sharepoint

Someone had a question on how to insert items into a sub folder of a list in SharePoint 2010 when using Linq. Not obvious at first, I thought I'd blog the answer:

Let's say that you Create someObject based on a class generated by SPMetal. Let's also say that this object is suitable for passing to the InsertOnSubmmit method of the data context. Then you call the following to insert the item into the root of the list:


dataContext.SomeList.InsertOnSubmit(someObject);


Now you want to insert that item into a folder in the list, called Folder1. What you need to do is use the Path property on the someObject. This property is present if you use SPMetal to generate the Linq classes.

I created a folder in my SomeList called Folder1, and then I set the Path property like so:


mynewObject.Path = "/Lists/MyList/Folder1";


Then call the InsertOnSubmit method as usual and your item will be in the right folder! I am not yet sure how to create the folder through linq, and do note that exceptions are thrown if the folder is not there. I'll try to solve that one in a later post.

Making Linq to SharePoint work for Anonymous users

Earlier today I ran into the issue of Linq to SharePoint not working for anonymous users. The issue is discussed at a number of places:


http://blog.mastykarz.nl/sharepoint-2010-linq-doesnt-support-anonymous-users/


http://social.technet.microsoft.com/Forums/en-US/sharepoint2010programming/thread/9b59abcb-6bce-42f1-9eae-ad9561753044


Seeing as 95% of the SharePoint work I do is on public facing web sites, this was a real disappoinment for me. SharePoint 2010 is supposed to be much more useable internet sites - so this was a bit of a shock. Linq to SharePoint was one of those features we all looked forward to!


Before going back to CAML queries, I thought I would have a look at what potential workarounds or even hacks I could come up with to get this working.


My first try was to just wrap my code in the usual SPSecurity.RunWithElevatedPrivileges method. That didn't work. A number of experiments later (and then finding this post), I arrived at this picture form Reflector:



The class we are looking at here is responsible for creating the data connection for the Microsoft.SharePoint.Linq.DataContext class. The important thing to notice here is the highlighted line. If the SPContext.Current is not null, the code uses the SPSite object from the current SPContext.


As documented all over the web, the RunWithElevatedPrivileges method will be of no help if you do not create new SP* objects. See http://msdn.microsoft.com/en-us/library/bb466220.aspx "...You cannot use the objects available through the Microsoft.SharePoint.SPContext.Current property. That is because those objects were created in the security context of the current user..."


So since the code shown in reflector does exactly what the above mentioned article warns against, browsing your site as an anonymous user eventually causes a login prompt on your site since the current SPContext represents that anonymous user who rightly should NOT be able to run Linq queries against your database.


Next I was inspired by this post and thought: What if I can somehow mess with the SPContext.Current object? If I could get it to be null, I could force that code in the SPServerDataConnection class to create a new SPSite object!


The SPContext.Current object is read only. However, it derives in one way or another from the HttpContext.Current, and that is writeable. So my next attempt was to check if my user was an anonymous user, and if so, set the HttpContext to null. Short story - it worked!


After some cleanup, I created the following helper method:

public static class AnonymousContextSwitch
{
public static void RunWithElevatedPrivelligesAndContextSwitch(SPSecurity.CodeToRunElevated secureCode)
{
try
{
//If there is a SPContext.Current object and there is no known user, we need to take action
bool nullUser = (SPContext.Current != null && SPContext.Current.Web.CurrentUser == null);

HttpContext backupCtx = HttpContext.Current;
if (nullUser)
HttpContext.Current = null;

SPSecurity.RunWithElevatedPrivileges(secureCode);

if (nullUser)
HttpContext.Current = backupCtx;
}
catch (Exception ex)
{
string errorMessage = "Error running code in null http context";
//Use your favourite form of logging to log the error message and exception ....
}
}
}


The logic is as follows:

  1. Check if the situation requires action: Is the user anonymous?

  2. Backup the current HttpContext

  3. Set the current HttpContext to null - thus forcing the creation of new SP* objects

  4. Use The RunWithElevatedPrivileges method to execute code specified by the caller. Note that I reuse the SPSecurity.CodeToRunElevated delegate to mimic the RunWithElevatedPrivileges method.

  5. Set the current HttpContext to the backed up object


Calling this function is identical to the way RunWithElevatedPrivileges is called:

string currentWebUrl = SPContext.Current.Web.Url;
AnonymousContextSwitch.RunWithElevatedPrivelligesAndContextSwitch(
delegate
{
MyDataContext dctx = new MyDataContext(currentWebUrl);
//... your code...
});


Just remember that the SPContext.Current object should NOT be referenced inside the delegate code. This will throw all sorts of Null Reference exceptions. If you need data such as the current web url, throw it in a string variable before you switch the context. See the example above.


I have tested this code for retrieving as well as updating data in a list and it worked great. I have also tested this for a logged in as well as an anonymous user and it seems to work for both.


Lastly, I need to say that I just figured this out today and I have NO idea what the long term impact of this will be, or if it will work as expected in all cases. Use at your own risk and all that. Happy coding!

Thursday, May 06, 2010

FieldRef element not working with custom content type

I have been working on SharePoint 2010 for the last few weeks and have found the new development tools quite useful. So far I have been able to avoid any CAML issues like typos that plagued MOSS 2007 projects. Today I ran into something interesting however.

I was creating a custom content type with some custom site columns, but after activating all the necessary features, my content type only had site columns in it that were from the parent content type. None of the site columns defined in the FieldRefs section were included in the new content type. The logs showed no errors, and I was quite stuck. I spent a few hours experimenting and finally found the solution. Comments in the XML. Yeah, don't get me started, comments should not influence functionality but in this case they do.

So basically, this content type definition doesn't include any of the fields added in the FieldRef section:


  <ContentType ID="0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF390058d217b15a4549e79a7dfacfb6577993"
Name="Generic Page"
Description="Generic Page"
Group="My Content Types"
Sealed="FALSE"
Inherits="TRUE"
Version="0">
<FieldRefs>
<!-- Comment -->
<FieldRef ID="{4B9D42FA-8081-49AB-9F89-72FAB3C6609C}"/>
</FieldRefs>
</ContentType>

But this one does.


  <ContentType ID="0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF390058d217b15a4549e79a7dfacfb6577993"
Name="Generic Page"
Description="Generic Page"
Group="My Content Types"
Sealed="FALSE"
Inherits="TRUE"
Version="0">
<FieldRefs>
<FieldRef ID="{4B9D42FA-8081-49AB-9F89-72FAB3C6609C}"/>
</FieldRefs>
</ContentType>

The only difference is the comment. Good job and gold star to the guy who wrote the XML parser for this.