Introduction
Whenever you change web.config file or deploy your website on a new environment, you have to try out many relevant features to confirm if the configuration changes or the environment is correct. Sometimes, you have to run a smoke test on the website to get the peace of mind. Moreover, if some external database, webservice or network connectivity is down, it takes time to nail down exactly where the problem is. Having a self-diagnostics page on your website, like the one you see on your printer, can help quickly identify exactly where the problem is. Here’s a way how you can quickly create a simple self-diagnostics page in a single page using straightforward AJAX technologies. This diagnostics page tests for common configuration settings like connection string, ASP.NET Membership configurations, SMTP settings, <appSettings>
file paths and URLs and some application specific settings to confirm if the configuration is all correct.
Building the Self-diagnostics Page Framework
I have used the UpdatePanel
and an AJAX Timer
to perform each diagnostics step. Every 500ms, it performs an async postback to the page and the page carries out one particular diagnostics step at a time. This way, even if the whole diagnostics process takes a lot of time to complete, it won't time out the page and there won't be a large delay showing the page initially.
<asp:ScriptManager ID="ScriptManager1" runat="server" EnablePartialRendering="true"
ScriptMode="Release">
</asp:ScriptManager>
<asp:UpdatePanel runat="server" UpdateMode="Conditional">
<ContentTemplate>
<asp:Timer runat="server" ID="RefreshTimer" Interval="500"
Enabled="true" OnTick="RefreshTimer_Tick" />
<h1>
Self Diagnostics</h1>
<h2>
Database Configuration</h2>
<ul>
<li>
<asp:Label CssClass="inprogress"
Text="Can read and write to database using the connection string."
runat="server" ID="ConnectionStringStatusLabel" /></li>
</ul>
The AJAX Timer
fires the RefreshTimer_Tick
on the server. It then executes one step at a time. For each diagnostics step, I have put a Label
that shows the step title. The Label
is updated with error and suggestions when it fails. Otherwise, it's marked with a CSS class that shows the tick sign.
protected void RefreshTimer_Tick(object sender, EventArgs e)
{
this._Tasks[this.CurrentTaskNo]();
this.CurrentTaskNo++;
if (this.CurrentTaskNo == this._Tasks.Length)
{
this.RefreshTimer.Enabled = false;
}
}
The _Tasks
is an array of delegates
. Each step is nothing but a function that performs certain diagnostics step. Once you build the _Tasks
with an array of functions, this RefreshTimer_Tick
executes one function at a time.
The list of diagnostics steps are defined in the OnInit
event:
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
this._Tasks = new Action[]
{
() => TestConnectionString(),
() => TestMembershipAPI(),
() => TestWrite(),
() => TestPaths(),
() => TestUrls(),
() => TestSMTP(),
() => TestAppSettings(),
() => TestAnonTemplateUser(),
() => TestRegTemplateUser()
};
}
That’s it. The framework is very simple. The complexity is in the diagnostics steps.
Testing ASP.NET Membership Settings
ASP.NET Membership has three configuration settings that should be tested – the Membership
API, the Profile
API and the Role Manager. The diagnostics function tries to create a new user, a new profile and checks for a certain role to exist in order to confirm the membership settings are all correct. You can certainly change the logic to perform test that suits your need.
private void TestMembershipAPI()
{
try
{
try
{
var newUser = Membership.CreateUser
(Guid.NewGuid().ToString(), Guid.NewGuid().ToString());
}
catch (Exception x)
{
MarkAsFail(MembershipLabel, x.Message,
"Probably wrong connection string name in
<membership> block in web.config.
Don't use the Entity Framework Connection string");
return;
}
Similarly, you can test the Profile Provider to confirm whether the profile provider settings are all correct.
try
{
UserProfile newProfile = ProfileCommon.Create
(Guid.NewGuid().ToString()) as UserProfile;
if (newProfile == null)
{
MarkAsFail(MembershipLabel, "Cannot create new user.",
"You might have wrong connection string name in <profile> block.
Ensure it's the simple connection string, not the entity framework one.
Or you may not have the 'inherits=\"Dropthings.Web.Framework.UserProfile\"
attribute in the <profile> block.");
return;
}
newProfile.IsFirstVisit = false;
newProfile.Fullname = "Test";
newProfile.Save();
}
catch (Exception x)
{
MarkAsFail(MembershipLabel, x.Message,
"Probably wrong connection string name in <profile> block in web.config.
Don't use the Entity Framework Connection string");
return;
}
Here the test code is very specific to my project. You can change this to test whatever way you want.
Test App_Data has Write Permission
Sometimes, we forget to give the ASPNET account or NETWORK SERVICE account write permission to the App_Data folder. The following test confirms that the app_data folder has the right permissions:
private void TestWrite()
{
try
{
File.AppendAllText(Server.MapPath("~/App_Data/" + Guid.NewGuid().ToString()),
Guid.NewGuid().ToString());
MarkAsPass(AppDataLabel);
}
catch (Exception x)
{
MarkAsFail(AppDataLabel, x.Message, "Give read, write,
modify permission to NETWORK SERVICE account to App_Data folder.");
}
}
It performs the test by creating a random file. It only tests if create and write permission is there. It does not test if delete permission is there. You can add it if it suits your project by trying to delete the randomly created file.
Testing File Paths in <appSettings> Block
Most of the time, we put the file paths in the <appSettings>
block. The following test confirms if all the relative paths are correct and exist:
private void TestPaths()
{
bool allPathOK = true;
foreach (string key in ConfigurationManager.AppSettings.AllKeys)
{
string value = ConfigurationManager.AppSettings[key];
if (value.StartsWith("~/"))
{
string fullPath = Server.MapPath(value);
if (!Directory.Exists(fullPath) && !File.Exists(fullPath))
{
MarkAsFail(FilePathLabel, "Invalid path: " + key + "=" +
fullPath, string.Empty);
allPathOK = false;
}
}
}
if (allPathOK)
MarkAsPass(FilePathLabel);
}
It loops through all the <appSettings>
entries and checks if any of the entries has a value that’s in relative file path format. If it finds one, it converts it to local absolute path and confirms the path exists.
Testing External URLs are Reachable in <appSettings> Block
Just like file path, you can test if the external URLs are all working or not.
private void TestUrls()
{
bool allUrlOK = true;
foreach (string key in ConfigurationManager.AppSettings.AllKeys)
{
string value = ConfigurationManager.AppSettings[key];
Uri uri;
if (Uri.TryCreate(value, UriKind.Absolute, out uri))
{
using (WebClient client = new WebClient())
{
try
{
client.DownloadString(uri);
}
catch (Exception x)
{
MarkAsFail(URLReachableLabel, x.Message,
"Unreachable URL: " + key + "=" + uri.ToString());
allUrlOK = false;
}
}
}
}
if (allUrlOK)
MarkAsPass(URLReachableLabel);
}
The test runs through all <appSettings>
entries, checks if any of the values is in URL format. If it is, it tries to perform an HTTP GET
on the URL.
Conclusion
Self diagnostics entirely depends on the type of application you have. The above code shows you some examples that you can start with. You should put some application specific tests to confirm the external webservices, databases, network shares, file paths and important transactions are executing properly. Whenever you have some fault reported on your website, you can first run the self-diagnostics page to confirm if the settings are all correct or not. Then you can spend time checking for specific problem areas. Such a self-diagnostics page helps eliminate manual investigation time and helps you identify problem areas quickly.