Maxim Karpov posted a comment on my last blog entry that included the following statement regarding ASP.NET 2.0:
"To be honest with you, the idea of 70% less code does not sounds all that good. It will be easier to create bad application once again."
This is a reaction I find a fair number of developers have to the next release of ASP.NET. In fact, when I present ASP.NET 2.0 at talks these days, there are typically three standard reactions to the claim of 70% code reduction (and subsequent demonstrations showing why and how):
- Oh my gosh, I no longer have to write code to build my site - will I still have a job after this product ships?
- Excellent! Now I can stop building all of that drudgery code and focus on more important aspects of my application!
- Great, now everyone will think he/she can build a scalable web site with a database backend without writing a line of code. I'm going to have to spend the rest of my professional life fixing sites that claim to be efficient and scalable that were built with drag-and-drop sans code!
Where I think Maxim's reaction falls into category number 3. I personally am quite pleased with the direction they have taken in this next release, and thought it would be worth a blog entry trying to share with you my optimism.
Let's start with a simple ASP.NET 1.1 sample page. I'm going to use a login page as an example, just because the Membership provider in ASP.NET 2.0 is one of the nicest and easiest to motivate. Suppose we have a database table to store login credentials that looks like:
CREATE TABLE users (
id INT IDENTITY,
username VARCHAR (64),
password VARCHAR (64),
PRIMARY KEY (id)
)
We could put together a login page as follows in ASP.NET 1.1 (keeping it all in one page without using code behind for simplicity):
<%@ Page language="C#" %>
<%@ Import Namespace="System.Web.Security" %>
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Configuration" %>
<%@ Import Namespace="System.Data.SqlClient" %>
<script runat="server">
public void OnClick_Login(object src, EventArgs e)
{
string dsn = ConfigurationSettings.AppSettings["dsn"];
string sql = "SELECT COUNT(*) FROM users WHERE "+
"username=@username AND password=@password";
using (SqlConnection conn = new SqlConnection(dsn))
using (SqlCommand cmd = new SqlCommand(sql, conn))
{
cmd.Parameters.Add("@username", _username.Text);
cmd.Parameters.Add("@password", _password.Text);
conn.Open();
int cnt = (int)cmd.ExecuteScalar();
if (cnt == 1)
FormsAuthentication.RedirectFromLoginPage(
_username.Text, false);
else
_errormessage.Text =
"Invalid login: Please try again";
}
}
</script>
<html>
<body>
<form runat="server">
<h2>Login Page</h2>
Enter username:
<asp:TextBox id="_username"
runat="server" />
<br />
Enter password:
<asp:TextBox id="_password"
TextMode="password"
runat="server" />
<br />
<asp:Button text="Login" OnClick="OnClick_Login"
runat="server"/><br />
<asp:Label id="_errormessage" ForeColor="red"
runat="server" />
</form>
</body>
</html>
Now, let's take the fairly common next step of removing the data access code from the page and into a data access layer. Here is our new data access layer class which we would compile and deploy as an assembly in our /bin directory (or perhaps the GAC):
// From MyDal.cs
namespace EssentialAspDotNet2
{
public class MembershipServices
{
public static bool ValidateUser(string username, string password)
{
string dsn = ConfigurationSettings.AppSettings["dsn"];
string sql = "SELECT COUNT(*) FROM users WHERE "+
"username=@username AND password=@password";
using (SqlConnection conn = new SqlConnection(dsn))
using (SqlCommand cmd = new SqlCommand(sql, conn))
{
cmd.Parameters.Add("@username", username);
cmd.Parameters.Add("@password", password);
conn.Open();
int cnt = (int)cmd.ExecuteScalar();
return cnt == 1;
}
}
}
}
This shift of the data access code to a separate layer reduces the code in our page to:
<script runat="server">
public void OnClick_Login(object src, EventArgs e)
{
if (MembershipServices.ValidateUser(
_username.Text, _password.Text))
{
FormsAuthentication.RedirectFromLoginPage(
_username.Text, false);
}
else
{
_errormessage.Text = "Invalid login: Please try again";
}
}
</script>
This is a nicer model than the first login page for several reasons. First, it tends to centralize your data access code into a single location, which makes it easier to spot optimizations, implement caching, and make other improvements without having to search code scattered throughout several pages. It also makes it much easier to change the backend implementation. For example, suppose we decided to store our user passwords as salted hashes - we could do it entirely within our data access class leaving our login page untouched. There are other benefits to using a data abstraction layer like this, but I assume most of you don't need convincing that this is a better approach.
Now, let's look at what it would take to implement this in ASP.NET 2.0. To start with, there is a new Login control that displays a username/password collection form, so our login.aspx now looks like:
<%@ Page language="C#" %>
<html>
<body>
<form runat="server">
<h2>Login Page</h2>
<asp:Login runat="server" id="_login" />
</form>
</body>
</html>
The Login control is one of many new controls that assumes the existence of an associated 'provider', in this case, the MembershipProvider. A provider in ASP.NET 2.0 is an abstract base class that defines a number of methods that are required for a control or set of controls to perform their tasks. In this case, the MembershipProvider has methods like ValidateUser, Createuser, DeleteUser, and so on. There are two built-in MembershipProviders that ship with ASP.NET 2.0 including the AccessMembershipProvider and the SqlMembershipProvider (one for Access and one for Sql Server).
At this point, our login.aspx page is complete, believe it or not. If we try and run the page as it stands, ASP.NET 2.0 will assume the Access provider and will automatically spit out a .mdb file with a pre-defined schema capable of storing users and their credentials. Another option is to change the provider (either in the control or more typically in the configuration file) to use the SqlMembershipProvider. This requires us to run a script in our SQL database (or use the aspnet_regsql.exe utility) to set up the necessary tables. The last option, is to write our own MembershipProvider derivative and implement the functions ourselves. For most major web applications, this last option will be the only way to go because the database schema is already predefined and it will not make sense to inject ASP.NET's user table into the schema.
So in reality, here's what we will have to do to use the new Login control:
namespace EssentialAspDotNet2
{
public class MyMembershipProvider : MembershipProvider
{
private string _connectionString;
public override bool ValidateUser(string name, string password)
{
string sql = "SELECT COUNT(*) FROM users WHERE " +
"username=@username AND password=@password";
using (SqlConnection conn =
new SqlConnection(_connectionString))
using (SqlCommand cmd = new SqlCommand(sql, conn))
{
cmd.Parameters.Add("@username", username);
cmd.Parameters.Add("@password", password);
conn.Open();
int cnt = (int)cmd.ExecuteScalar();
return cnt == 1;
}
}
public override void Initialize(string name,
NameValueCollection config)
{
if (string.IsNullOrEmpty(name))
name = "MyMembershipProvider";
base.Initialize(name, config);
// retrieve connectionStringName attribute value
//
string csn =
config["connectionStringName"];
if (string.IsNullOrEmpty(csn))
throw new HttpException("Missing attribute " +
"'connectionStringName'");
// read in connection string from config file
//
_connectionString =
ConfigurationSettings.ConnectionStrings[csn].
ConnectionString;
if (string.IsNullOrEmpty(_connectionString))
throw new Exception("The connection string " +
csn + "was not found");
// remove all elements from config that were read
//
config.Remove("connectionStringName");
}
// remaining overridden members elided for clarity
// (they all throw NotImplementedException)
}
}
And in order to wire the login control up to our new provider implementation, the following entries in web.config are required:
<configuration>
<connectionStrings>
<add name="mydsn"
connectionString="server=.;trusted_connection=yes;database=test" />
</connectionStrings>
<system.web>
<membership defaultProvider="MyMembershipProvider">
<providers>
<add name="MyMembershipProvider"
type="EssentialAspDotNet2.MyMembershipProvider"
connectionStringName="mydsn" />
</providers>
</membership>
<!-- snip -->
</system.web>
</configuration>
So what is fundamentally different about using providers in ASP.NET 2.0 versus building sites in ASP.NET 1.1 with a data abstraction layer? They're actually quite similar, but what ASP.NET 2.0 does is force us to use an abstracted data access layer. This is why I sometimes refer to the provider model as 'abstracting the data abstraction layer'. The other nice feature of the provider model is that the controls can be more intelligent and can do more for you. In our small example, we didn't have to write any code to interact with the Login control because it assumed the existence of a MembershipProvider derivative. In order to change how the Login control worked, we wrote our own provider that in reality was very similar to the data abstraction class we had to write in ASP.NET 1.1. The last significant difference is that ASP.NET 2.0 gives you a number of built in providers that either assume a certain schema, or generate their own (in the case of the Access provider). This is great for prototyping and building small one-off sites. Migrating a prototype from using an Access backend to using a real SQL backend with your own database schema requires writing a new provider and just plugging it in - no changes to your pages is required.
So, to address the three reactions I mentioned at the beginning of this post:
1. Oh my gosh, I no longer have to write code to build my site - will I still have a job after this product ships?
There will still be plenty of code to write to build large web applications, but a lot of the drudgery code (like binding fields in your form to data) will be eliminated. You can now focus your efforts on building reusable providers paired with controls, and building provider back ends thinking carefully about the efficiency of data access.
2. Excellent! Now I can stop building all of that drugery code and focus on more important aspects of my application!
That's the attitude I think we should all have going forward. The new provider model combined with more intelligent controls should be the final nail in the coffin of classic ASP spaghetti code that still permeates many sites today.
3. Great, now everyone will think he/she can build a scalable web site with a database backend without writing a line of code. I'm going to have to spend the rest of my professional life fixing sites that claim to be efficient and scalable that were built with drag-and-drop sans code!
I hope I've shown that this should not be the case either. Yes, it will be much easier to throw together a site with lots of database interaction and pre-canned functionality, but the entire model is pluggable and you can replace any of the providers with your own implementation without having to worry about conflicting with some hidden data access code buried in a page somewhere.
You might also notice that our final login.aspx page did not have 70% less code than the corresponding 1.1 version. I honestly think this will be true for a lot of web applications. The provider model may reduce the code in a typical small scale site where defining the database schema is not that important, but for most significant sites you will want to control your database schema completely, and this will require writing new providers. What it will force you to do, is partition your application code into data access providers and front-end UI logic, which I believe can only make things better.
Posted
Jul 27 2004, 07:51 AM
by
fritz-onion