Monday, April 26, 2010

Sharepoint : Custom Redirection HttpModule based on UserProfile Property

When a request is made to an ASP.NET application, an instance of HttpApplication class is made to process this request. One instance of the HttpApplication class can process multiple requests during its lifetime, however only one at a time.

When the request is being processed, the application raises a chain of events on to which an implementation of IHttpModule can be hooked on.

If you don’t know about HttpApplication class and the chain of events, please look here:
http://msdn.microsoft.com/en-us/library/system.web.httpapplication.aspx

If you don’t know about IHttpModule interface, please look here:
http://msdn.microsoft.com/en-us/library/system.web.ihttpmodule.aspx

Assuming that you are developing for Sharepoint 2007.

Install WSPBuilder on Visual Studio from here :
http://wspbuilder.codeplex.com/

Create new WSPBuilder Project in Visual Studio.

Add Sharepoint Resources : Microsoft.Office.Server.dll and Microsoft.Sharepoint.dll

Write a class which implement interface IHttpModule.

"PostAuthenticateRequest" is a EventHandler to which you can add an event (delegate) which here is fulfilled by OnAuthenticationCustomRedirect.

The HttpModule’s functionality is to redirect a user based on user’s chosen user profile property. To redirect the user or to write in the event logs on the server, the commands need to be run with elevated privileges and that is exactly what the private functions do.

The redirection happens only if the request is coming from a different host. If surfing within the website the redirection will not take place. This is done by comparing the hosts of the incoming url from current request and the previous url from previous request.

If you don't know about accessing previous urls, please look here :
http://msdn.microsoft.com/en-us/library/system.web.httprequest.urlreferrer.aspx

If the Sharepoint user doesn’t exist and userProfileManager.GetUserProfile(“name”) is called. An exception is thrown. This is the reason why the module is under try-catch block. If such an error occurs then an error is recorded in the Event Log on the server.

By comparing the value of the properties, we can then redirect the user in a customized manner.

public class RedirectModule : IHttpModule
{
#region Implementation of IHttpModule

/// 
/// Initializes a module and prepares it to handle requests.
/// 
/// An  that provides access to the methods, properties, and events common to all application objects within an ASP.NET application 
///                 public void Init(HttpApplication context)
{
//Adding an event handler to handle and redirect the incoming request to corresponding address
context.PostAuthenticateRequest += OnAuthenticationCustomRedirect;
}

/// 
/// Disposes of the resources (other than memory) used by the module that implements .
/// 
public void Dispose()
{

}

#endregion Implementation of IHttpModule

#region Redirection based on User Profile Module

private static void OnAuthenticationCustomRedirect(object sender,EventArgs eventArgs)
{
//The user who is trying to access tabnet
SPUser spUser = null;

try
{
//Getting the HttpRequest
HttpRequest request = ((HttpApplication) sender).Request;

//Host Domain
String requestUrlDomain = "http://" + request.Url.Host;

//Previous Host Domain
String previousRequestUrlDomain = String.Empty;
if(request.UrlReferrer != null)
{
previousRequestUrlDomain = "http://" + request.UrlReferrer.Host;
}

//If coming from within same host, no redirection required
if(!requestUrlDomain.Equals(previousRequestUrlDomain))
{
//Redirect only if going to the home page
if (request.Url.ToString().Equals(requestUrlDomain + “originalhomepage.aspx"))
{
//Getting the HttpContext
HttpContext context = ((HttpApplication)sender).Context;

//Creating SPSite object
SPSite spSite;
//Creating SPWeb object
SPWeb spWeb;

//Checking for the current SPContext
if (SPContext.Current != null)
{
//Getting the SPSite
spSite = SPContext.Current.Site;
//Getting the SPWeb
spWeb = spSite.RootWeb;
//Get the SPUser
spUser = spWeb.CurrentUser;

//Creating the UserProfileManager for the site
UserProfileManager userProfileManager = new UserProfileManager(ServerContext.GetContext(spSite));

//Getting the user profile from the name using the UserProfileManager
UserProfile userProfile = userProfileManager.GetUserProfile(spUser.LoginName);

//Getting all the properties from the UserProfileManager
PropertyCollection propertyCollection = userProfileManager.Properties;
//Filtering out the single property which we are interested in
Property property = propertyCollection.GetPropertyByName("PropertyName");

//Finding that property in the user profile for value
if (userProfile[property.Name].Value != null)
{
String propertyString = userProfile[property.Name].Value.ToString();

//Different actions depending on hosts of each user
switch (propertyString.ToUpper())
{
case "property value 1":
case "property value 2":
//Write the information with the user login name to eventlog
WriteToEventLog("OnAuthenticationCustomRedirect", spUser.LoginName + " has been redirected”,EventLogEntryType.Information);
//Actual redirection
ResponseRedirectElevatedPriviliges(context, requestUrlDomain + “newhomepage.aspx");
break;
//anything else no redirection
default:
break;
}
}
}
}
}
}
catch (Exception exception)
{
String message = "Exception Stack Trace : " + exception.StackTrace;

if(spUser!=null)
{
message += " User Login Name : " + spUser.LoginName;
}

//Write the error with stack trace to the event log
WriteToEventLog("OnAuthenticationCustomRedirect",message, EventLogEntryType.Error);
}
}

#endregion Redirection based on User Profile Module

#region Methods to Run With Elevated Priviliges

/// 
/// /// This method runs with elevated priviliges which writes a log entry to eventlog
/// 
/// /// /// private static void WriteToEventLog(String source, String message, EventLogEntryType eventLogEntryType)
{
SPSecurity.RunWithElevatedPrivileges(
() => EventLog.WriteEntry(source, message, eventLogEntryType));
}

/// 
/// This method runs with elevated privileges which redirects the user to the new URL via the response.
/// 
/// ///     private static void ResponseRedirectElevatedPriviliges(HttpContext context,String url)
    {
        SPSecurity.RunWithElevatedPrivileges(() => context.Response.Redirect(url, false));
    }

    #endregion Methods to Run With Elevated Priviliges
}

C# & Visual Studio: FTP Methods, Testing by Creating Resource Files and Using their Streams

I had come across a scenario where I needed to store certain files to a certain location on a server. This certain location, basically acts like a shelf to hold these files. Just like a neatly arranged shelf, all these files were arranged within a directory structure.

On the Server, I created an FTP site listening at the default port (Port 21).This FTP Server was mapped to the location of my interest.

If you don’t know how to create an FTP Site, please look here:
http://learn.iis.net/page.aspx/301/creating-a-new-ftp-site/

In Visual Studio, through my C# code I wrote a class which provided two public methods:

1) public String WriteFtpMyFileToServer(Stream fileStream)
2) public Boolean DeleteFtpMyFileFromServer(String fileNamePlusPath)

There were private helper methods as well to remove repeated coding (good practice).
Following is a code snippet from method (1) which would write the file to location of interest:

/// 
/// Method to Transfer the file via FTP
/// 
/// The stream on the file to be sent/// String
public String WriteFtpMyFileToServer (Stream fileStream)
{
    .
    .
    .
    .
    .
    //Get the FTP site Uri from ConfigurationManager in string including filepath
    Uri ftpUri = new Uri(String.Format(CultureInfo.CurrentCulture,"{0}{1}",ConfigurationManager.AppSettings["FtpSite"],filePath));

    //Creates an FTP web request
    FtpWebRequest request = (FtpWebRequest)WebRequest.Create(ftpUri);

    //Setting the request method
    request.Method = WebRequestMethods.Ftp.UploadFile;

    //Get the request stream
    Stream ftpStream = request.GetRequestStream();

    //Copy from fileStream to ftpStream
    int bufferLength = 2048; // 2K
    byte[] buffer = new byte[bufferLength];
    int count = 0;
    int readBytes = 0;
    //Byte by Byte
    do
    {
        readBytes = fileStream.Read(buffer, 0, bufferLength);
        ftpStream.Write(buffer, 0, readBytes);
        count += readBytes;
    }while (readBytes != 0);

    // Close both the streams
    fileStream.Close();
    fileStream.Dispose();
    ftpStream.Close();

    // Send the file and Get Response
    FtpWebResponse response = (FtpWebResponse)request.GetResponse();

    //Check the result of our upload and see if successful
    if (response.StatusCode == FtpStatusCode.ClosingData)
    {
        // Close the ftp session
        response.Close();
        fileSentSuccessfully = true;
    }

    .
    .
    .
    //Return String: Message of what happened or Null
    .
    .
    .
}

Its always a good thing to keep constant Uris (in this case FTP Site) in .config files. This is due to the fact that these Uris may change depending on your environments. It may be different in production, development and test environments. You can use the ConfigurationManager to access from these .config files.

Following is a code snippet from method (2) which would delete the file from the location of interest:

/// 
/// This method deletes the specified File via FTP
/// 
/// filepath followed by name/// bool sucess/failure

public Boolean DeleteFtpMyFileFromServer (String fileNamePlusPathOnServer)
{
    .
    .
    .
    //Creating Uri from string
    Uri ftpUri = new Uri(String.Format(CultureInfo.CurrentCulture,"{0}{1}",ConfigurationManager.AppSettings["FtpSite"],fileNamePlusPathOnServer));

    //Creates an FTP web request
    FtpWebRequest request = (FtpWebRequest)WebRequest.Create(ftpUri);

    //Method delete file
    request.Method = WebRequestMethods.Ftp.DeleteFile;

    // Send the command and Get Response
    FtpWebResponse response = (FtpWebResponse)request.GetResponse();

    //Check the result of our upload and see if successful
    if (response.StatusCode == FtpStatusCode.FileActionOK)
    {
        // Close the ftp session
        response.Close();

        return true;
    }

    return false;
}

Personally, I have always believed that throwing exceptions is a really good practice. Even though you don’t see any exceptions thrown in the code snippets above, you can always add code to throw exceptions to validate the parameters to the methods and for any other failure. This really helps to convey messages from the callee class to the caller class about what went wrong.

When this class is live, we would expect a real file stream to be passed to method (1). But how will you write unit tests for these methods?

Writing unit tests for each possible scenario in your methods is the best way for documenting and portraying what is expected from the method you just wrote. It validates your logic in the method, checks whether the method indeed returns what you expect and if the method is working the way you want it to. Overall, it makes the code more robust and reliable. It also helps other developers to understand what you intended to do in the method. Test Driven Development (TDD) and Code Coverage are really good techniques for the same.

If you don’t know what TDD is, please look here:
http://en.wikipedia.org/wiki/Test-driven_development

If you don’t know what Code Coverage is, please look here:
http://en.wikipedia.org/wiki/Code_coverage

To write a test for method (1), I needed to create a file in my test and pass its stream to method (1). It made no sense to create a file each time locally at a certain path and use it for test purposes. This is due to the fact that the underlying directory structure may change from host to host, depending on where the tests are run. Each test should be written independent of the location, environment and of other tests.

This is where resource files come in. A resource file can be any data such as text files, images, audio or video that you application might require. Once a resource file is added to the project, it always stays with the project just like other .cs files (your code).

In my test project, I created a folder within the project called “Resources” and added a text file to it with a test name called “TestFile.txt”. This file had some junk data in it. I clicked on the file and in the Properties dialog below it, changed the Build Action to “Embedded Resource”.

To get the stream of this file in your test class:

/// 
/// Summary description for Your Test Class
/// 

[TestClass]
public class YouTestClassName
{
    #region Private Variables
    .
    .
    .
    //Assembly giving reference to Resource File
    private static Assembly _thisAssembly;
    //File Stream of the file to be transferred (Resource Text File)
    private static Stream _TestFileStream;
    .
    .
    .
    #endregion Private Variables

    #region Test Context

    /// 
    ///Gets or sets the test context which provides
    ///information about and functionality for the current test run.
    ///
    public TestContext TestContext { get; set; }

    #endregion Test Context

    #region Additional test attributes

    // You can use the following additional attributes as you write your tests:
    //
    // Use ClassInitialize to run code before running the first test in the class
    [ClassInitialize]
    public static void MyClassInitialize(TestContext testContext) 
    {
        .
        .
        .
        //Getting the assembly
        _thisAssembly = Assembly.GetExecutingAssembly();
        .
        .
        .
    }

    // Use ClassCleanup to run code after all tests in a class have run
    [ClassCleanup]
    public static void MyClassCleanup() 
    {
        .
        .
        .
    }

    //// Use TestInitialize to run code before running each test 
    //[TestInitialize()]
    //public void MyTestInitialize() 
    //{ 

    //}

    //// Use TestCleanup to run code after each test has run
    //[TestCleanup()]
    //public void MyTestCleanup() 
    //{

    //}

    #endregion Additional test attributes

    /// 
    /// This method tests the FtpFile method successfully
    /// 
    [TestMethod]
    public void FtpFileTest_Success()
   {
        //ARRANGE
        //Getting the file stream
        if (_thisAssembly != null)
        {
            //Getting the stream from resource file
            _TestFileStream = _thisAssembly.GetManifestResourceStream("Namespace.TestFile.txt");
        }

        //ACT
        //Calling the WriteFtpFileToServer method (METHOD(1))
        //_sut is the instance of the class which provides these methods
        _returnString = _sut.WriteFtpFileToServer(_TestFileStream);

        //ASSERT
        Assert.IsNotNull(_returnString);
    }
    .
    .
    .
    //More tests
    .
    .
    .
}
This way, the tests can be run on any machine without worrying about the underlying directory structure of the machine.