With over 10 million hits a day, funda.nl is probably the largest ASP.NET website which uses Solr on a Windows platform.
While all our data (i.e. real estate properties) is stored in SQL Server, we're using Solr 1.4.1 to return the faceted search results as fast as we can. And yes, Solr is very fast. We did do
some heavy stress testing on our Solr server, which allowed us to do over 1,000 req/sec on a single 64-bits Solr instance; and that's including converting search-url's to Solr
http-queries and deserializing Solr's result-XML back to .NET objects!
Let me tell you about faceted search and how to integrate Solr in a .NET/Windows environment. I'll bet it's easier than you think :-)
What is faceted search?
Faceted search is the clustering of search results into categories, allowing users to drill into search results. By showing the number of hits
for each facet category, users can easily see how many results match that category.
If you're still a bit confused, this example from CNET explains it all:
The SQL solution for faceted search
Our ("pre-Solr") solution for faceted search was done by adding a lot of redundant columns to our SQL tables and doing a COUNT(...) for each of those
columns:
So if a user was searching for real estate properties in the city 'Amsterdam', our facet-query would be something like:
SELECT COUNT(hasGarden), COUNT(yearBuilt1930_1940), COUNT(yearBuilt1941_1950), COUNT(etc...)
FROM Houses
WHERE city = 'Amsterdam'
While this solution worked fine for a couple of years, it wasn't very easy for developers to add new facets. And also, performing COUNT's on all matched rows
only performs well if you have a limited amount of rows in a table (i.e. less than a million rows).
Enter Solr
"Solr is an open source enterprise search server based on the Lucene Java search library, with XML/HTTP and JSON APIs, hit highlighting,
faceted search, caching, replication, and a web administration interface." (quoted from Wikipedia's page on Solr)
Solr isn't a database, it's more like a big index. Every time you upload data to Solr, it will analyze the data and create an inverted index from it (like the index-pages of a book). This way Solr can lookup data very quickly. To explain the inner workings of Solr is beyond the scope of this post, but if you want to learn more, please visit the Solr Wiki pages.
Getting faceted search results from Solr is very easy; first let me show you how to send a http-query to Solr:
http://localhost:8983/solr/select?q=city:Amsterdam
This will return an XML document containing the search results (in this example only three houses in the city of Amsterdam):
<response>
<result name="response" numFound="3" start="0">
<doc>
<long name="id">3203</long>
<str name="city">Amsterdam</str>
<str name="steet">Keizersgracht</str>
<bool name="hasGarden">false</bool>
<int name="yearBuilt">1932</int>
</doc>
<doc>
<long name="id">3205</long>
<str name="city">Amsterdam</str>
<str name="steet">Vondelstraat</str>
<bool name="hasGarden">true</bool>
<int name="yearBuilt">1938</int>
</doc>
<doc>
<long name="id">4293</long>
<str name="city">Amsterdam</str>
<str name="steet">Trompstraat</str>
<bool name="hasGarden">true</bool>
<int name="yearBuilt">1949</int>
</doc>
</result>
</response>
By adding a facet-querypart for the fields "hasGarden" and "yearBuilt", Solr will return the facets for those particular fields.
...&facet.field=hasGarden&facet.query=yearBuilt:[1930 TO 1940]&facet.query=yearBuilt:[1941 TO 1950]
The complete XML response from Solr now looks like this (note the "face_counts" node at the end of the result-XML returned by Solr):
<response>
<result name="response" numFound="3" start="0">
<doc>
<long name="id">3203</long>
<str name="city">Amsterdam</str>
<str name="steet">Keizersgracht</str>
<bool name="hasGarden">false</bool>
<int name="yearBuilt">1932</int>
</doc>
<doc>
<long name="id">3205</long>
<str name="city">Amsterdam</str>
<str name="steet">Vondelstraat</str>
<bool name="hasGarden">true</bool>
<int name="yearBuilt">1938</int>
</doc>
<doc>
<long name="id">4293</long>
<str name="city">Amsterdam</str>
<str name="steet">Trompstraat</str>
<bool name="hasGarden">true</bool>
<int name="yearBuilt">1949</int>
</doc>
</result>
<lst name="facet_counts">
<lst name="facet_queries">
<int name="yearBuilt:[1930 TO 1940]">2</int>
<int name="yearBuilt:[1941 TO 1950]">1</int>
</lst>
<lst name="facet_fields">
<lst name="hasGarden">
<int name="true">2</int>
<int name="false">1</int>
</lst>
</lst>
</lst>
</response>
Trying Solr yourself
To run Solr on your local machine and experiment with it, you should read the Solr tutorial. This tutorial really takes less than one hour, in which you will install Solr, upload sample data and get some query results. And yes, it works on Windows without a problem (you do need Java installed).
Note that in the Solr tutorial, you will use Jetty as a Java Servlet Container (that's why you must start it using "java -jar start.jar"). In our environment we prefer to use Apache Tomcat to host Solr, which installs like a Windows service and works more like .NET developers expect. See the SolrTomcat page.
Some best practices for running Solr on Windows:
- Use the 64-bits version of Tomcat. In our tests, this doubled the req/sec we were able to handle!
- Use a .NET XmlReader to convert Solr's XML output-stream to .NET objects. Don't use XPath; it won't scale well.
- Use filter queries ("fq" parameter) instead of the normal "q" parameter where possible. Filter queries are cached by Solr and will speed up Solr's response time (see FilterQueryGuidance)
In my next post I’ll talk about how to keep Solr's indexed data in sync with the data in your SQL tables. Timestamps / rowversions will help you out
here!
Inspired by Scott Guthrie's MyLists example webapplication, I've made a simple (but fully functional) weblogging application in Visual Studio 2005, which you can download here (all sources included).
Add this template to your VS2005 VWD ProjectTemplates folder (or just unzip it somewhere and open it with Visual Web Developer). The Visual Web Developer C#-ProjectTemplates folder is default located in \My Documents\Visual Studio 2005\Templates\ProjectTemplates\Visual Web Developer\CSharp.
Note that I've created this weblog example-application in only 4 hours(!), and it contains not a single line of code.
Yep, that's right: 0 lines of code!!! I accomplished this by using:
- SQL2005 Database designer of VS.NET 2005 (no code)
- TableAdapter Wizard to create typed datasets (no code)
- ObjectDataSources using the typed datasets (no code)
- GridView and DetailsView controls to create/read/update/delete postings and comments (no code)
- FreeTextBox for richtext creation/editing of postings (no code)
Features of this QuickBlog webapplication:
- Master- and Contentpages
- Table-less design
- Skinning en Theming
- Creating and editing of Postings (by admin)
- Readers can add Comments (admin can remove them)
- Login control, Membership
- A lot of GridViews and DetailsViews
- RSS feed
Some screenshots:

Visitor writing a comment:

After logging on, the admin can manage postings and comments (or create a new posting):

So you want to use Ajax to make your webapplications more interactive and better performing, but you don't want the hassle of writing cross-browser JavaScript?
And you also want to keep using the ASP.NET server-side programming model?
Then open-source MagicAjax.NET is the right solution for you!
MagicAjax is easily integrated in your ASP.NET 1.1 and 2.0 webapplications:
- Step 1: Add the MagicAjax HttpModule in your web.config
- Step 2: You put the part of your page that you want to have Ajax functionality inside an AjaxPanel (<ajax:ajaxpanel>)
That's all you need to do! The MagicAjax framework takes care all of the intrinsic details for you.
We've put together a few example pages to show the power of MagicAjax.NET. See the online demo on http://demo2.magicajax.net/ (note that the server is sometimes very slow):
You can view the sources of all demo pages online, so you can see for yourself how easy it is to use MagicAjax.
Tip: Also have a look at these MagicAjax demo's using FireFox. Fully supported!
Next you see a screenshot of the WebPart Framework in ASP.NET 2.0 using MagicAjax.NET.
Moving, adding and removing of WebParts is all done without a single visual postback; the WebParts just appear on your page!

After attending the PDC, I thought it would be cool to use Atlas for solving a common postback-overhead problem in ASP.NET : 2 connected Master-Detail dropdown lists.
Using plain ASP.NET 1.x, the way to implement this is by setting the 'AutoPostBack' property of the master dropdown to 'true' and then fill the detail dropdown after a postback. But we all know postbacks are slow and bad for user experience; a much better way to solve this problem is by using client-side JavaScript and XML.
An good example (e.g. used by Michael Schwarz for demonstrating Ajax.NET) is the case where the master dropdown shows a list of car brands, and the detail dropdown shows a list of available models for the selected brand. When the user selects a car brand, the car model dropdown is filled clientside, so no need for a postback to the webserver!

I've assembled an example project containing 4 techniques for implementing Master-Detail dropdowns in ASP.NET, which you can download here (including source code!!!). The 4 techniques in my example project are:
- Server-side using ASP.NET 1.x/2.0 AutoPostBack
- Client-side using ASP.NET 2.0 (beta 2) Client Script Callback
- Client-side using ASP.NET 1.x/2.0 Ajax.NET
- Client-side using ASP.NET 2.0 (beta 2) Atlas script
Note: For these examples to work, you need ASP.NET 2.0 beta 2 installed. For the examples to run on RC, some changes have to be made for the Client Script Callback example.
Note2: Thanks to Bertrand Le Roy for helping me out on the Atlas code!
A few things I haven't solved for the Atlas example yet:
- The databinding of the detail dropdown needs the selectedValue of the master dropdown. Someway, I can't just bind this value direct from the dropdown, so I used a hidden textbox for this (binded to the selectedValue of the master dropdown).
- I use the AtlasScript 'setProperty' to enable/disable the postback button. I want to enable the button when the selectedValue of the detail dropdown is unequal to an empty string, but I cant' seem to find a transformation function for a comparison like this.
Please give me feedback when you think my Atlas example could be improved.


As you may or may not know, the current beta 2 version of Visual Studio 2005 Team System only supports manual team builds against the Team Foundation server. I think the final release of VSTS is going to support scheduled builds (e.g. nightly builds), but I don't think continuous integration will be supported (because Microsoft don't practice CI themselves).
However my company enforces their developers to use Continuous Integration on all projects, so I had to build a CI process for Team System myself.
I thought it was a good idea to share the results with you :)
I enabled continuous integration by setting up a scheduled MSBuild project that runs every 5 minutes. It includes 4 custom MSBuild Tasks I've created (also see Visio drawing at the bottom):
- CheckWorkspaceUpToDate : Checks if any files were changed on the local server workspace since last run. Uses the h.exe (as of Hatteras) command.
- StartTeamBuild : Starts a Team Build by calling the BuildContoller.asmx WebServices (thanks to Aaron Engel for sample code)
- GetUnexaminedBuild : If available, returns a finished build from Team Foundation where build quality is "Unexamed". The status of this build will be notified to all team members using messenger, and the build's build quality will be set to 'Notification Sent'.
- MessengerNotification : Task to send an arbitrary message through MSN messenger (using SendMSN that uses the DotMSN library; thanks to James Simmonds and B.Geertsema)
Because you're all such nice people, I decided to share my code and binaries. Download it here.
To run this Continuous Integration process on your own server, take the following steps:
- Create a Team Build using the wizard in Team System (if you already have created a Team Build, skip this step).
- Create a workspace on your server, and map your project's repository-folder to a local file folder on that server. Do a 'Get Latest' on this folder. Note: first time you do a teambuild, a workspace will automatically be created on the buildserver; it's best to use this generated workspace.
- From the downloaded zip, extract "Macaw.MSBuild.Tasks.dll" and "DotMSN.dll" (or build them yourself). Add these libraries to the GAC.
- From the downloaded zip, extract "/MSBuild Examples/ContinuousIntegration.proj" and put this somewhere on your hard disk (e.g. c:\teambuilder)
- Edit the ContinuousIntegration.proj MSBuild project, and change all properties in the <PropertyGroup> to match your environment.
- Test ContinuousIntegration.proj (run as argument of MSBuild.exe, which can be found in the .NET framework folder).
- If ContinuousIntegration.proj is working well, schedule it to run every 5 minutes: schtasks /create /SC MINUTE /MO 5 /TR [batch file to run] /TN [scheduled task name]. Note: The [batch file to run] should contain a command like: "C:\WINDOWS\Microsoft.NET\Framework\v2.0.50215\MSBuild.exe c:\teambuilder\continuousintegration.proj".
Have a look at this HTML snippet. When clicking the 'clickme' innerText of div2, I expect the browser to alert "div2", because div2 is the first child of its parent, div1.
<
div id="div1">
<div id="div2" onclick="alert(this.parentNode.childNodes[0].id)">clickme</div>
</div> I've tested this HTML snippet on both IE and FireFox, and guess what?
IE reacts as expected by alerting "div2".
However, FireFox alerts "undefined"....?
Apparently, in FireFox div2 is considered to be the second childNode of div1; not the first. I guess this is because FireFox parses the whitespace between div1 and div2 as being a seperate node! BUT WHY????
Did the creators of FireFox (Mozilla Foundation) implemented this by design? Is it ever going to be fixed in FireFox? Or do we just have to live with it, and write seperate javascript for IE and FireFox?
[Note: of course I can let IE and FF return the same results by removing all whitespace between the div1 and div2 tags, but that would result in human-unreadable and hard to maintain Html.]
Postscript:
I've received a lot of comments about the fact that FireFox precisely (I'd say 'rigidly') follows the w3c DOM recommendation on this. I'm aware of the w3c specs, but I still think it sucks having to write seperate code for IE and FF (or include a library of wrapper functions).
Personally, I think the IE dom-representation is the more sensible one, whether it's in the recommendation or not.
And FireFox just made a very bad 'first impression' to me :(
Read these links on more information why Mozilla has chosen to preserve whitespaces in the DOM:
If you've been experimenting with the ASP.NET 2.0 Portal Framework (like me!), you know there are 4 out-of-the-box EditorParts that you can place in the EditorZone (the EditorZone is the Zone where you can edit a WebPart's properties). The 4 standard EditorParts are:
- PropertyGridEditorPart
- AppearanceEditorPart
- BehaviorEditorPart
- LayoutEditorPart
The Problem
These EditorParts work great. However, if you place all 4 EditorParts in the EditorZone, they will show a very long list of editable properties. Not very user-friendly.
The Solution
By using stylesheet classes and a little client-side javascript, you can make the EditorParts expand and collapse when you click on their titles. You can even show a cool plus/minus icon next to the EditorPart's title (see screenshot).

How it's done: The Code
In the EditorZone tag, set the CssClass-attribute of the EditorParts you want default to be collapsed to 'EditorPartHidden' (this is a css class I'll show you later on). Also set the CssClass of the PartTitleStyle to 'EditorPartTitle' (so a client-side behavior can set on the EditorPart's title).
<
asp:EditorZone runat="server" ID="EditorZone1">
<ZoneTemplate>
<asp:AppearanceEditorPart CssClass="EditorPartHidden" ID="epart1" runat="server" />
<asp:BehaviorEditorPart CssClass="EditorPartHidden" ID="epart2" runat="server" />
<asp:LayoutEditorPart CssClass="EditorPartHidden" ID="epart3" runat="Server" />
</ZoneTemplate>
<PartTitleStyle CssClass="EditorPartTitle" />
</asp:EditorZone> Next you can see the 'EditorPartHidden' and 'EditorPartTitle' css classes (put these in your stylesheet). Note that the 'EditorPartTitle' class is using a background image to show the 'plus' sign next to the title (also needs a padding-left to move the title a bit to the right, otherwise the title will be placed on top of the 'plus' sign).
A behavior (toggle.htc) is set on the EditorPartTitle to perform the expand/collapse. Note: behaviors are IE 5.5+ only (or does FireFox supports this also?).
Note: I replaced the .htc (IE only behavior) by cross-browser javascript.
.EditorPartHidden
{
display: none;
} .EditorPartTitle
{
behavior: url(htc/toggle.htc); //note: I replaced the .htc (IE only) by cross-browser javascript
background-position: left;
background-repeat: no-repeat;
background-image: url(images/plus.gif);
cursor: pointer;
padding-left: 14px;
font-size: x-small;
} ...and this is the javascript code for the expand/collapse behavior. It sets onclick events to all EditorPart titles, which toggles the image (plus/minus) and the EditorPart's content. Include this code on your .aspx page (or use Page.ClientScript.RegisterClientScriptBlock).
<script type="text/javascript">
// Create expanding titles for Legends with className 'EditorPartTitle'
function CreateExpandingTitles() {
var elements = document.getElementsByTagName("LEGEND");
for (i=0; i<elements.length; i++) {
if (elements[i].className && elements[i].className == "EditorPartTitle")
elements[i].onclick = new Function("toggle(this);");
}
} // Call function CreateExpandingTitles on window onload
if (window.addEventListener)
window.addEventListener('load', CreateExpandingTitles, false);
else if (window.attachEvent)
window.attachEvent('onload', CreateExpandingTitles);
function toggle(titleElement) {
// Find nextSibling's firstChild (i.e. DIV with class 'EditorPartStyleHidden')
// For IE this is nextSibling.childNodes[0], but due to an error in current
// version of FireFox (1.0.4) this is nextSibling.childNodes[1]
var firstChild = (titleElement.nextSibling.childNodes[0].id)
? titleElement.nextSibling.childNodes[0]
: titleElement.nextSibling.childNodes[1];
// Toggle image and show/hide EditorPart display
if (firstChild.style.display == "block") {
firstChild.style.display = "none";
titleElement.style.backgroundImage = "url(images/plus.gif)";
} else {
firstChild.style.display = "block";
titleElement.style.backgroundImage = "url(images/minus.gif)";
}
}
</script>
Some developers in the ASP.NET 2.0 Forums have asked questions on how to copy a WebPart (including it's configuration) from one page to another.
The standard solution for this is to use the 'Export/Import' functionality that ASP.NET 2.0 offers for WebParts. Note: for a WebPart to be export-enabled, its 'ExportMode' property should have a value other than 'None' (which is the default) and enableExport="true" should be set in the webParts section of the web.config. The export and import of a (configured) WebPart is done in six steps:
- Switch to EditDisplayMode
- Select 'Export' from the verbs menu of the WebPart you want to copy
- Download the WebPart file to your local PC
- Navigate to the destination page, switch to CatalogDisplayMode
- Select the 'Imported Web Part Catalog', and upload the WebPart file
- Add the uploaded WebPart to a zone
Not very user-friendly, is it? Most users wouldn't even know what to do with the downloaded Webpart file.
However, the ASP.NET 2.0 portal framework is very extensible, so I've build a custom solution where the user can just 'copy' the WebPart to a serverside 'clipboard' (i.e. the Session object), and 'paste' it from a custom Catalog (which I named the 'Copied Web Part Catalog'). So this is done without downloading and uploading WebParts files!
Here's a screenshot of the copy/paste functionality:

So you want to know how this is done? I'll show you the code in 3 parts:
Part 1: Add a custom 'Copy' verb to your WebPart code.
/// <summary>
/// Override the Verbs collection to add the 'copy webpart' verb
/// </summary>
public override WebPartVerbCollection Verbs
{
get
{
if (_verbs == null)
{
WebPartVerb myVerb = new WebPartVerb("copy", OnWebPartCopy);
myVerb.Description = "Copy this WebPart";
myVerb.Text = "Copy";
myVerb.ImageUrl = "~/images/CopyVerb.gif";
_verbs = new WebPartVerbCollection(new WebPartVerb[] { myVerb });
}
return _verbs;
}
} /// <summary>
/// Called when the user selects 'Copy WebPart' from the verbs menu.
/// The selected WebPart is Exported to a xml string, and added to
/// a list in the Session. This list is used by the CopiedCatalogPart
/// to add the copied WebParts to a new page or zone
/// </summary>
public void OnWebPartCopy(object sender, WebPartEventArgs e)
{
//get reference to WebPart to copy
WebPart webPart = e.WebPart;
//temporarily set ExportMode to 'All' to enable WebPart exporting
//this is a kind of hack, but assumed legitimate because the exported WebPart
//stays on the server (in Session)
WebPartExportMode origExportMode = ExportMode;
webPart.ExportMode = WebPartExportMode.All;
//export WebPart to string
StringWriter stringWriter = new StringWriter();
XmlTextWriter xmlwriter = new XmlTextWriter(stringWriter);
WebPartManager.ExportWebPart(webPart, xmlwriter);
string copiedWebPart = stringWriter.ToString();
//reset original ExportMode of WebPart
webPart.ExportMode = origExportMode;
//reset original ExportMode in exported WebPart string
XmlDocument doc = new XmlDocument();
doc.LoadXml(copiedWebPart);
XmlNode exportModeNode = doc.SelectSingleNode("//property[@name='ExportMode']");
if (exportModeNode != null)
{
exportModeNode.InnerText = this.ExportMode.ToString();
copiedWebPart = doc.OuterXml;
}
//get or create list of copied WebParts
List<KeyValuePair<string, string>> copiedWebParts;
if (Context.Session["COPIED_WEBPART_KEY"] != null)
{
copiedWebParts = (List<KeyValuePair<string, string>>)
Context.Session["COPIED_WEBPART_KEY"];
}
else
{
copiedWebParts = new List<KeyValuePair<string, string>>();
}
//insert new copied WebPart as first on list
copiedWebParts.Insert(0, new KeyValuePair<string, string>(
string.Format("{0} [copied {1}]", this.Title, DateTime.Now.ToLongTimeString()),
copiedWebPart));
//add list to Session
Context.Session["COPIED_WEBPART_KEY"] = copiedWebParts;
} Part 2: The CopiedWebPartCatalogPart
using
System;
using System.Collections.Generic;
using System.Web.UI.WebControls.WebParts;
namespace myClassLib.CatalogParts
{
/// <summary>
/// Catalog for reading WebParts copied to the users Session (copy/paste functionality)
/// </summary>
public class CopiedWebPartCatalogPart : CatalogPart
{
/// <summary>
/// Overrides the Title to display "Copied Web Part Catalog" by default
/// </summary>
public override string Title
{
get
{
string title = base.Title;
return string.IsNullOrEmpty(title) ? "Copied Web Part Catalog" : title;
}
set
{
base.Title = value;
}
}
//dictionary to hold the availabe copied webparts
private Dictionary<WebPartDescription, string> _webparts
= new Dictionary<WebPartDescription, string>();
/// <summary>
/// Returns the WebPartDescriptions for the catalog part
/// </summary>
public override WebPartDescriptionCollection GetAvailableWebPartDescriptions()
{
_webparts.Clear();
if (Context.Session["COPIED_WEBPART_SESSION_KEY"] != null)
{
List<KeyValuePair<string, string>> copiedWebParts =
(List<KeyValuePair<string, string>>)Context.Session["COPIED_WEBPART_SESSION_KEY"];
foreach (KeyValuePair<string, string> copiedWebPart in copiedWebParts)
{
WebPartDescription desc = new WebPartDescription(
copiedWebParts.IndexOf(copiedWebPart).ToString(),
copiedWebPart.Key,
null,
null);
_webparts[desc] = copiedWebPart.Value;
}
}
return new WebPartDescriptionCollection(_webparts.Keys);
}
/// <summary>
/// Returns a new instance of the WebPart specified by the description
/// </summary>
public override WebPart GetWebPart(WebPartDescription description)
{
//get a xmltextreader to the exported WebPart
System.Xml.XmlTextReader reader =
new System.Xml.XmlTextReader(new System.IO.StringReader(_webparts[description]));
string errorMessage; //will contain errorMessage on import errors
//return an instance of the requested WebPart
return WebPartManager.ImportWebPart(reader, out errorMessage);
}
}
} Part 3: Add the CopiedWebPartCatalogPart in the CatalogZone of your aspx page
<%@ Register TagPrefix="myCatalogParts" Assembly="myClassLib" Namespace="myClassLib.CatalogParts" %>
<asp:CatalogZone ID="CatalogZone" runat="server" >
<ZoneTemplate>
<asp:PageCatalogPart ID="PagePart" runat="server" />
<asp:ImportCatalogPart Visible="false" ID="ImportPart" runat="Server" />
<myCatalogParts:CopiedWebPartCatalogPart ID="PastePart" runat="server" />
</ZoneTemplate>
</asp:CatalogZone>
The Problem
I think the 'default document' option in IIS is a great way to get clean url's without those .aspx extensions. So instead of calling the url 'http://www.mysite.com/news/default.aspx', you could also call 'http://www.mysite.com/news', which will get you to the same page. This is because IIS finds the default document 'default.aspx' in the news folder.
However, the default SiteMapProvider in ASP.NET 2.0 isn't aware of IIS's default documents, and won't highlight the selected node in a Menu or TreeView contol. Take a look at this xml siteMap file; it will result in links to /default.aspx and /news/default.aspx, but the selected node in my Menu won't be highlighted because the url of the siteMapNode and the request's url don't match:
<?
xml version="1.0" encoding="utf-8"?>
<siteMap>
<siteMapNode title="Home" url="~">
<siteMapNode title="News" url="~/news" />
</siteMapNode>
</siteMap> The Solution
Luckily for me, ASP.NET 2.0 has a provider model for the SiteMaps. This means I've just got to override the CurrentNode property of the default XmlSiteMapProvider and add extra logic for recognizing default documents. Take a look at the code of my FolderAware_XmlSiteMapProvider:
using
System;
using System.Web;
class FolderAware_XmlSiteMapProvider : XmlSiteMapProvider
{
public override SiteMapNode CurrentNode
{
get
{
// first let the base resolve the current node
SiteMapNode currentNode = base.CurrentNode;
if (currentNode == null)
{
// now check if a node exists that's pointing to a folder, and current
// RawUrl maps to the folder's default document (default.aspx)
HttpContext context = HttpContext.Current;
if (context != null)
{
string text = context.Request.RawUrl.ToLowerInvariant();
int index = text.IndexOf("/default.aspx");
if (index != -1)
{
currentNode = FindSiteMapNode(text.Substring(0, index));
if (currentNode != null && !currentNode.IsAccessibleToUser(context))
{
currentNode = null;
}
}
}
}
return currentNode;
}
}
} For your webapplication to use this new FolderAware_XmlSiteMapProvider, you've got to define it in your web.config:
<
siteMap defaultProvider="FolderAware_XmlSiteMapProvider">
<providers>
<clear/>
<add siteMapFile="web.sitemap" name="FolderAware_XmlSiteMapProvider" type="FolderAware_XmlSiteMapProvider" />
</providers>
</siteMap> ...and now the Menu, TreeView and BreadCrumbs contols will highlight the currently selected page even if a request to a webfolder was made!
About impersonating your ASP.NET code
By using basic or integrated IIS security, you can set the security context on whose behalf the code is running to the current user by adding these two lines in your web.config:
<
authentication mode="Windows"/>
<identity impersonate="true"/> This can be usefull when you want to call WebServices inside you code that needs the current user's credentials (note: you'll need Kerberos authentication not to run into the two-hop problem, see http://blogs.msdn.com/mjeelani/archive/2004/12/07/275921.aspx).
The problem: No impersonation inside HttpHandlers
While your pages and controls will now run on behalf of the current user, for some reason this impersonation is not done inside your custom HttpHandler! Although the user is authenticated (HttpContext.Current.User.IsAuthenticated is true), all code is still running as ASPNET or the default AppPool identity (just add a watch on System.Security.Principal.WindowsIdentity.GetCurrent()). So by calling a WebService inside your HttpHandler code using the DefaultCredentials, the wrong credentials will be passed and you'll probably get a 404.
The solution: Do your own impersonation
public
IHttpHandler GetHandler(HttpContext context, string requestType, string url, string pathTranslated)
{
...
// Create a WindowsImpersonationContext object by impersonating the Windows identity.
WindowsImpersonationContext impersonationContext = ((WindowsIdentity)context.User.Identity).Impersonate();
try
{
//
// Your impersonated calls to WebServices here
//
}
catch {}
finally
{
if (impersonationContext != null)
{
// always undo impersonation
impersonationContext.Undo();
}
}
... ...so, after you impersonate to the identity inside the current HttpContext, your code will now be running on behalf of the currently logged on user, and you can call WebServices using the current user's credentials.
More Posts
Next page »