Mini-Tweek: customErrors in ASP.NET

As of the latest .NET Service Pack (3.5 SP1, 3.0 SP2, 2.0 SP2) there is a new attribute for the <customErrors> element of your web.config, redirectMode.  It has 2 settings, ResponseRedirect (default, legacy functionality), and ResponseRewrite (new functionality).  The ResponseRedirect setting causes the custom error redirection to work just like all of the previous versions where the server performs a redirect and all state (i.e. exception stack) information is lost.  The only way to get around this previously was to add a Server.Transfer() to the Application_Error method of your global.asax, which rendered the <customErrors> section useless as you had to hard code the URL in global.asax.cs.

This new setting cases the server to do a Server.Transfer() to the defaultRedirect page.  This means that the page state (including exceptions) are intact and that you can use the configurability of the <customErrors> section again.  I recently needed this functionality on a project where I'm the lead developer, but the client wants to write most of the SQL code.  Since he is not local to the test server, the RemoteOnly setting doesn't help.  Since this functionality needs to go on the production server as well, I created a custom error page and added just the error text to the page.  It's just enough information to let him know why the site is failing, but doesn't give hackers any useful information.

Posted by Will Bosacker with no comments

ASP.NET Tweeks (Part 1): PageBase

Modularity is the heart of any application.  This is the first of a multi-part part series of Tweeks that you can use while writing a modular ASP.NET application.  The first module on the list is the use of a PageBase object.  The PageBase class derives from the System.Web.UI.Page class, it should exist in an external components library, and it should replace the System.Web.UI.Page object that every .aspx page derives from.  If the PageBase class, as well as any custom controls, are placed in the web application itself you will run into several issues (especially while debugging) that worst case can cause Visual Studio to crash.  Never place the PageBase class or custom controls in the App_Code folder.

The concept of using a PageBase class has been around for a very long time, but never really got a face lift with the advent of master pages.  Master pages took over some of the functionality of what a typical PageBase class would perform, and required the duplication of common access business tier code.  The next part of this series covers Business Abstraction, to eliminate the duplication of code and to provide a single method of accessing the business tier.  Here is a sample set of PageBase classes w/ direct Business access.  1 for Web Forms, and 1 for Web Content Forms (based on a master page):

using System;
using System.Web.UI;

using BusinessFacade;

/// <summary>
///         Summary description for PageBase.
/// </summary>
/// <typeparam name="TMaster">The master page being used.</typeparam>
public class PageBase<TMaster> : PageBase
      where TMaster : MasterPage
{
      /// <summary>
      ///         Gets the hard typed master page.
      /// </summary>
      public new TMaster Master
      {
            get { return (TMaster)base.Master; }
      }
}

/// <summary>
///         Summary description for PageBase.
/// </summary>
public class PageBase : Page
{
      private CommonDataBF _commonDataBF;
      private UserManagementBF _userManagementBF;


      /// <summary>
      ///         Gets a <see cref="CommonDataBF"/> business object.
      /// </summary>
      public CommonDataBF CommonData
      {
            get
            {
                  if (_commonDataBF == null)
                  {
                        _commonDataBF = new CommonDataBF();
                  }

                  return _commonDataBF;
            }
      }

      /// <summary>
      ///         Gets a <see cref="UserManagementBF"/> business object.
      /// </summary>
      public UserManagementBF UserManagement
      {
            get
            {
                  if (_userManagementBF == null)
                  {
                        _userManagementBF = new UserManagementBF();
                  }

                  return _userManagementBF;
            }
      }
}

To use this, modify the declaration of the code behind page class to derive from this class.  Use the standard PageBase class for normal page forms, and use the generic PageBase class for page content forms (pages that use a master page).  Master pages don't normally need this same type of handling, as there should be a single master page that all other master pages cascade from.  However, you will want your master pages to implement interfaces, which makes it easier to pass-through public properties and methods.  This is an interface that I use, which is inherited by all master pages:

/// <summary>
/// Summary description for IMasterPage.
/// </summary>
public interface IMasterPage
{
    /// <summary>
    /// Gets the <see cref="MembershipUser"/> object for the currently logged in user.
    /// </summary>
    MembershipUser User { get; }

    /// <summary>
    /// Gets the identifier of the currently logged in user.
    /// </summary>
    Guid UserId { get; }

    /// <summary>
    /// Gets or sets the suffix to be added to the HTML page title.
    /// </summary>
    string PageTitleSuffix { get; set; }
}

The parent master obviously is the source of these properties and methods, but here is the pass-through code of a child master page that implements it:

public partial class UserMaster : MasterPage, IMasterPage
{
      /// <summary>
      ///         Gets the <see cref="MembershipUser"/> object for the currently logged in user.
      /// </summary>
      public MembershipUser User
      {
            get { return ((IMasterPage)base.Master).User; }
      }

      /// <summary>
      ///         Gets the identifier of the currently logged in user.
      /// </summary>
      public Guid UserId
      {
            get { return ((IMasterPage)base.Master).UserId; }
      }

      /// <summary>
      ///         Gets or sets the suffix to be added to the HTML page title.
      /// </summary>
      public string PageTitleSuffix
      {
            get { return ((IMasterPage)base.Master).PageTitleSuffix; }
            set { ((IMasterPage)base.Master).PageTitleSuffix = value; }
      }
}

When the master page interfaces and PageBase class are coupled together, you end up with a very nice and easy to use interface with very little duplication of code (the implementation of the master page interfaces on child masters).  You can completely eliminate all duplication of code by declaring a MasterPageBase for each master page that has child master pages, but it may be more difficult to manage and not prudent to do.  Additionally, if you are using user controls, as you should be, interfaces and a UserControlBase class are the best way to pass information back and forth between the page and controls, and even from control to control when using nested user controls.

Posted by Will Bosacker with no comments

.NET Tweek (June 24, 2009)

How To: Extract ASP.NET Membership profile properties in SQL Server 2008 T-SQL

Man, that title is a mouthful, but it works.  There are other posts on this that work with all versions of SQL Server, but this is my take with a bonus.  I have created a second function for the extraction of binary values, for those that need them.  This code uses some new functionalities of SQL Server 2008 and will not work as written on previous versions of SQL Server.  Also, all of the other posts out there are not 100% compiant with the format.  They don't convert a value with a length of -1 to a NULL.  Here is the script to return strings:

/****** Object: UserDefinedFunction [dbo].[fn_GetProfileStringElement] ******/
IF EXISTS(SELECT * FROM [sys].[objects] WHERE [object_id] = OBJECT_ID(N'[dbo].[fn_GetProfileStringElement]') AND [type] IN (N'FN', N'IF', N'TF', N'FS', N'FT'))
	DROP FUNCTION [dbo].[fn_GetProfileStringElement];
GO

SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER OFF;
GO
/******************************************************************************
** Schema: dbo
** Name: fn_GetProfileStringElement
** Desc:
**
** Auth: William Bosacker
** Date: 06/22/2009
**
** Parameters:
** ---------------------------------------------------------------------------
**
**
** Returns:
** ---------------------------------------------------------------------------
**
******************************************************************************/

CREATE FUNCTION [dbo].[fn_GetProfileStringElement] (
	@PropertyName nvarchar(256),
	@PropertyNames nvarchar(max),
	@PropertyValuesString nvarchar(max)
)
RETURNS nvarchar(max)
AS
BEGIN
	DECLARE @SearchString nvarchar(259);
	DECLARE @LeftPointer bigint;
	DECLARE @RightPointer bigint;
	DECLARE @ValueStart bigint;
	DECLARE @ValueLength bigint;


	IF @PropertyName IS NULL OR LEN(@PropertyName) = 0 
		OR @PropertyNames IS NULL OR LEN(@PropertyNames) = 0 
		OR @PropertyValuesString IS NULL OR LEN(@PropertyValuesString) = 0 
	BEGIN
		RETURN NULL;
	END;


	SET @SearchString = @PropertyName + ':S:';
	SET @LeftPointer = CHARINDEX(@SearchString, @PropertyNames, 0);

	IF @LeftPointer != 0
	BEGIN
		-- Find the starting position of the value.
		SET @LeftPointer = @LeftPointer + LEN(@SearchString);
		SET @RightPointer = CHARINDEX(':', @PropertyNames, @LeftPointer);
		SET @ValueStart = CAST(SUBSTRING(@PropertyNames, @LeftPointer, @RightPointer - @LeftPointer) AS bigint);

		-- Find the length of the value.
		SET @LeftPointer = @RightPointer + 1;
		SET @RightPointer = CHARINDEX(':', @PropertyNames, @LeftPointer);
		SET @ValueLength = CAST(SUBSTRING(@PropertyNames, @LeftPointer, @RightPointer - @LeftPointer) AS bigint);

		IF @ValueLength = -1
		BEGIN
			RETURN NULL;
		END;

		RETURN SUBSTRING(@PropertyValuesString, @ValueStart + 1, @ValueLength);
	END;


	RETURN NULL;
END;
GO

Here is the script to return binary values:

/****** Object: UserDefinedFunction [dbo].[fn_GetProfileBinaryElement] ******/
IF EXISTS(SELECT * FROM [sys].[objects] WHERE [object_id] = OBJECT_ID(N'[dbo].[fn_GetProfileBinaryElement]') AND [type] IN (N'FN', N'IF', N'TF', N'FS', N'FT'))
	DROP FUNCTION [dbo].[fn_GetProfileBinaryElement];
GO

SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER OFF;
GO
/******************************************************************************
** Schema: dbo
** Name: fn_GetProfileBinaryElement
** Desc:
**
** Auth: William Bosacker
** Date: 06/22/2009
**
** Parameters:
** ---------------------------------------------------------------------------
**
**
** Returns:
** ---------------------------------------------------------------------------
**
******************************************************************************/

CREATE FUNCTION [dbo].[fn_GetProfileBinaryElement] (
	@PropertyName nvarchar(256),
	@PropertyNames nvarchar(max),
	@PropertyValuesBinary varbinary(max)
)
RETURNS varbinary(max)
AS
BEGIN
	DECLARE @SearchString nvarchar(259);
	DECLARE @LeftPointer bigint;
	DECLARE @RightPointer bigint;
	DECLARE @ValueStart bigint;
	DECLARE @ValueLength bigint;


	IF @PropertyName IS NULL OR LEN(@PropertyName) = 0 
		OR @PropertyNames IS NULL OR LEN(@PropertyNames) = 0 
		OR @PropertyValuesBinary IS NULL OR LEN(@PropertyValuesBinary) = 0 
	BEGIN
		RETURN NULL;
	END;


	SET @SearchString = @PropertyName + ':B:';
	SET @LeftPointer = CHARINDEX(@SearchString, @PropertyNames, 0);

	IF @LeftPointer != 0
	BEGIN
		-- Find the starting position of the value.
		SET @LeftPointer = @LeftPointer + LEN(@SearchString);
		SET @RightPointer = CHARINDEX(':', @PropertyNames, @LeftPointer);
		SET @ValueStart = CAST(SUBSTRING(@PropertyNames, @LeftPointer, @RightPointer - @LeftPointer) AS bigint);

		-- Find the length of the value.
		SET @LeftPointer = @RightPointer + 1;
		SET @RightPointer = CHARINDEX(':', @PropertyNames, @LeftPointer);
		SET @ValueLength = CAST(SUBSTRING(@PropertyNames, @LeftPointer, @RightPointer - @LeftPointer) AS bigint);

		IF @ValueLength = -1
		BEGIN
			RETURN NULL;
		END;

		RETURN SUBSTRING(@PropertyValuesBinary, @ValueStart + 1, @ValueLength);
	END;


	RETURN NULL;
	END;
GO

And last, but not least, here is a sample of how to use the functions:

SELECT
	U.[Username],
	[dbo].[fn_GetProfileStringElement]('FirstName', P.[PropertyNames], P.[PropertyValuesString]) [FirstName],
	[dbo].[fn_GetProfileStringElement]('LastName', P.[PropertyNames], P.[PropertyValuesString]) [LastName],
	[dbo].[fn_GetProfileBinaryElement]('Avatar', P.[PropertyNames], P.[PropertyValuesBinary]) [Avatar]
FROM
	[dbo].[aspnet_Users] U
	JOIN [dbo].[aspnet_Profile] P ON P.[UserId] = U.[UserId]

Posting code in this blog takes a lot of work, and it is possible that I screwed it up.  Please post a comment if you find a problem with the code.

Posted by Will Bosacker with no comments

JawberDoo.com: The story begins...

Welcome to the first post of this new series.  This has been a busy week.  We've done quite a bit of work on the BeHeard.com site, primarily with the "Search" and "AutoComplete".  The detail pages aren't ready yet, but they are next on the list.  Most of my time is being spent on JawberDoo, which is coming along really well.  The source code of the framework library will be released as an open source project once the site is complete.

The framework library is a compilation of libraries that I have been building over the past 8 years.  There are tools that do everything from providing simple formatting of a persons' name, to a full blown base data abstraction/proxy system.  The ASP.NET Live Membership System is coming along as well.  The first part to be released will be the Enterprise ASP.NET Live Membership Proxy System (EALMPS).  EALMPS allows you to create presentation tier and business tier proxies for any single tier membership system (i.e. the SqlMembership system).

I know that many of you like to see pictures, so I decided to snip the following pic of the Solution Explorer from the JawberDoo solution.  It uses the framework library that I discussed about.  You can see the layout of the data abstraction (which is 100% provider driven), and will be covered in an upcoming post.  Site is also using localization with a default language of English and will support French & Spanish in the future:

If you would like to know more about any of the topics discussed in this post, please post a comment.

Posted by Will Bosacker with no comments