This is a c# class I use to implement web database applications with Microsoft IIS and Microsoft SQL server. It passes the http context to a selected SQL stored procedure, and writes a response based on the result sets generated by the stored procedure. Thus the entire application is coded as SQL stored procedures, apart from this c# code.
using Data = System.Data;
using Net = System.Net;
using String = System.String;
using Object = System.Object;
/* WebServer which runs under IIS, passing requests to SQL server. Typical web.config file to pass all requests to WebServer is:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.web>
<customErrors mode="Off"/>
</system.web>
<system.webServer>
<handlers>
<add name="WebServer" path="*" verb="*" type="WebServer" />
</handlers>
</system.webServer>
</configuration>
Typical SQL declaration of tables passed to SQL:
CREATE TYPE [perfect].[InfoT] AS TABLE( Kind int NOT NULL, Name nvarchar(100) NOT NULL, Value nvarchar(max) NOT NULL,
PRIMARY KEY ( Name, Kind )
)
CREATE TYPE [perfect].[FileT] AS TABLE( id int NOT NULL, Name varchar(50) NOT NULL, ContentLength int NOT NULL,
ContentType nvarchar(max) NULL, FileName nvarchar(200) NOT NULL, [File] image NULL, PRIMARY KEY ( id ) )
)
*/
public class WebServer : System.Web.IHttpHandler
{
private static Data.SqlClient.SqlConnection GetConn( )
{
return new Data.SqlClient.SqlConnection
( "Initial Catalog=redacted;Data Source=(Local);Max Pool Size=500;User Id=redacted;Password=redacted" );
}
public void ProcessRequest( System.Web.HttpContext ctx )
{
// Each http request is handled by two SQL procedure calls. The first gets the name of the handling procedure, the second computes the response.
try
{
using ( Data.SqlClient.SqlConnection sqlconn = GetConn() )
using ( Data.SqlClient.SqlCommand cmd = new Data.SqlClient.SqlCommand( "perfect.GetHandler", sqlconn ) )
{
Data.SqlClient.SqlParameter p = null;
{ /* Set up table of info to be passed to stored procedure */
Data.DataTable t = new Data.DataTable( );
t.Columns.Add( "Kind", typeof( int ) );
t.Columns.Add( "Name", typeof( string ) );
t.Columns.Add( "Value", typeof( string ) );
AddToDataTable( t, 0, ctx.Request.QueryString );
AddToDataTable( t, 1, ctx.Request.Form );
AddToDataTable( t, 2, ctx.Request.Cookies );
t.Rows.Add( 3, "Host", ctx.Request.Url.Host );
t.Rows.Add( 3, "Path", ctx.Request.Path );
t.Rows.Add( 3, "PathAndQuery", ctx.Request.Url.PathAndQuery );
t.Rows.Add( 3, "IpAddress", ctx.Request.UserHostAddress );
p = cmd.Parameters.AddWithValue( "@Info", t );
p.SqlDbType = Data.SqlDbType.Structured;
}
sqlconn.Open();
cmd.CommandType = Data.CommandType.StoredProcedure;
cmd.CommandText = ( string ) cmd.ExecuteScalar();
if ( ctx.Request.Files.Count > 0 )
{
Data.DataTable ft = GetFileTable( ctx.Request.Files );
p = cmd.Parameters.AddWithValue( "@Files", ft );
p.SqlDbType = Data.SqlDbType.Structured;
}
Data.DataSet ds = new Data.DataSet( );
using ( Data.SqlClient.SqlDataAdapter da = new Data.SqlClient.SqlDataAdapter( cmd ) )
{
da.Fill( ds );
}
// Interpret the dataset
ctx.Response.ContentType = "text/html";
String ShowRecordCount = null;
for ( int i = 0; i < ds.Tables.Count; i += 1 )
{
Data.DataTable t = ds.Tables[i];
if ( ShowRecordCount != null )
{
PutUtf8( ctx, ShowRecordCount + t.Rows.Count );
ShowRecordCount = null;
}
for ( int j = 0; j < t.Rows.Count; j += 1 )
{
Data.DataRow r = t.Rows[j];
int code = 0;
Object value = r[0];
if ( r.ItemArray.Length > 1 )
{
code = (int) value;
value = r[1];
}
if ( code == 0 ) PutUtf8( ctx, (string) value );
else if ( code == 1 ) ctx.Response.ContentType = (string) value;
else if ( code == 2 )
{
byte[] b = (byte[]) value;
ctx.Response.OutputStream.Write( b, 0, b.Length );
}
else if ( code == 4 ) ctx.Response.Expires = (int) value;
else if ( code == 14 ) ctx.Response.StatusCode = (int) value;
else if ( code == 15 ) ctx.Response.Redirect( (string) value );
else if ( code == 16 )
{
System.Web.HttpCookie ck = new System.Web.HttpCookie( (string) value, (string) r[2] );
String Expires = (string) r[3];
if ( Expires != "" ) ck.Expires = System.DateTime.Parse( Expires );
ctx.Response.Cookies.Add( ck );
}
else if ( code == 17 ) ShowRecordCount = (string)value;
}
}
}
}
catch ( System.Exception e )
{
ctx.Response.Write( e );
}
}
public bool IsReusable { get { return true; } }
private void AddToDataTable( Data.DataTable dt, int Kind, System.Collections.Specialized.NameValueCollection nvc )
{
foreach ( string key in nvc.Keys )
if ( key != null ) dt.Rows.Add( Kind, key, nvc[key] );
}
private void AddToDataTable( Data.DataTable dt, int Kind, System.Web.HttpCookieCollection nvc )
{
foreach ( string key in nvc.Keys )
{
dt.Rows.Add( Kind, key, nvc[key].Value );
}
}
private Data.DataTable GetFileTable( System.Web.HttpFileCollection fc )
{
Data.DataTable ft = new Data.DataTable();
ft.Columns.Add( "id", typeof(int) );
ft.Columns.Add( "Name", typeof(string) );
ft.Columns.Add( "ContentLength", typeof(int) );
ft.Columns.Add( "ContentType", typeof(string) );
ft.Columns.Add( "FileName", typeof(string) );
ft.Columns.Add( "File", typeof(byte[]) );
for ( int id = 0; id < fc.Count; id += 1 )
{
System.Web.HttpPostedFile pf = fc[ id ];
int length = pf.ContentLength;
byte [] bytes = new byte[ length ];
pf.InputStream.Read( bytes, 0, length );
ft.Rows.Add( id, fc.GetKey(id), length, pf.ContentType, pf.FileName, bytes );
}
return ft;
}
// Output
private byte [] EncBuffer = new byte[512];
private static byte[] GetBuf( int need )
{ int n = 512; while ( n < need ) n *= 2; return new byte[n]; }
private void PutUtf8( System.Web.HttpContext ctx, String s )
{
int len = s.Length;
int need = System.Text.Encoding.UTF8.GetMaxByteCount( len );
if ( need > EncBuffer.Length ) EncBuffer = GetBuf( need );
int nb = System.Text.Encoding.UTF8.GetBytes( s, 0, len, EncBuffer, 0 );
ctx.Response.OutputStream.Write( EncBuffer, 0, nb );
}
// Logging
static WebServer()
{
System.AppDomain cd = System.AppDomain.CurrentDomain;
cd.UnhandledException += new System.UnhandledExceptionEventHandler( LogException );
Log( "Unhandled exception handler set" );
}
static void LogException( object sender, System.UnhandledExceptionEventArgs args )
{
System.Exception e = (System.Exception) args.ExceptionObject;
Log( "Unhandled exception: " + e.ToString() );
}
static void Log( String message )
{
using ( Data.SqlClient.SqlConnection sqlconn = GetConn() )
{
using ( Data.SqlClient.SqlCommand cmd = new Data.SqlClient.SqlCommand( "perfect.Log", sqlconn ) )
{
cmd.CommandType = Data.CommandType.StoredProcedure;
Data.SqlClient.SqlParameter p = cmd.Parameters.AddWithValue( "@Message", message );
p.SqlDbType = Data.SqlDbType.NVarChar;
cmd.Connection.Open();
cmd.ExecuteNonQuery();
}
}
}
} // End class WebServer
1 Answer 1
Main issues:
- If the main purpose of this webserver is to call stored procedures, then I'd expect to see a
CallStoredProcedure
method somewhere. That should make it easier to understand the purpose of the code, and it lets you clean upProcessRequest
, so it can call procedures and prepare a response without getting bogged down in the details. - There's no documentation about what sort of requests this webserver can handle, and how they should be formatted. The same goes for responses. Maybe you've got documentation elsewhere? Personally I prefer to keep it close to the code itself, to increase the likelyhood that it will be kept in sync.
- It's difficult to discern the meaning of
kind
andcode
. I'd use enums, or at least named constants, to make it apparent what values the code expects to handle, and what the meaning of those values is.
Inconsistencies:
There are a lot of inconsistencies in your code:
- Some type names use an alias, others are written out fully.
- Sometimes you're using C# aliases for basic types like
string
andobject
, sometimes not. - Sometimes an inner scope is indented and put on a separate line, sometimes it's put on the same line.
- Sometimes an inner scope is surrounded by braces, sometimes not.
- Some names are abbreviated, some are not.
- Most parameter names are written in camelCase, as is usual, but a few are not.
All of this makes the code harder to read and understand than it needs to be. Here's what I would change:
- Unless you have a specific reason for using namespace aliases the way you do, I'd do what everyone else does:
using System;
,using System.Data;
,using System.Data.SqlClient;
, and so on. With that, you don't need to write fully qualified names anywhere, which reduces clutter. - Most people use
string
andobject
for variables, parameters and the like, andString
andObject
when they're calling a static method on one of those types. - Always putting inner scopes on a separate line, and indenting them, makes control flow easier to see.
- Regarding braces, some people recommend always use braces, no matter what, to prevent certain kinds of bugs. I think that's a bit excessive for conditional
return
andthrow
statements, but I do try to be consistent. GetConnection
,table
,row
,context
,dataTable
,collection
,fileCollection
and so on should be easier to understand thanGetConn
,t
,r
,ctx
,dt
,nvc
andfc
, without being too long for comfort.- Fields, parameter names and local variables are normally written in camelCase. PascalCase is used for type, method and property names.
Other notes:
- Duplicate type annotations can be removed by relying on type inference:
var table = new DataTable();
is equivalent toDataTable table = new DataTable();
. DataSet.Tables
andDataTable.Rows
can be iterated directly withforeach
.IsReusable
can be simplified topublic bool IsReusable => true;
.UnhandledException += new UnhandledExceptionEventHandler(LogException);
can be simplified toUnhandledException += LogException;
."Unhandled exception: " + ex.ToString()
can be simplified to$"Unhandled exception: {ex}"
.
-
\$\begingroup\$ Thanks for the answer. There is no restriction on the requests or the responses, this is just a "dumb" adapter layer which passes any request to an SQL proc, and the response back to the http client. The SQL execution happens when ExecuteScalar and Fill are called, perhaps that could be explained in a comment. \$\endgroup\$George Barwood– George Barwood2019年04月24日 17:20:12 +00:00Commented Apr 24, 2019 at 17:20
SqlDataAdapter
,SqlCommand
,SqlConnection
all implementIDisposable
and should be disposed. \$\endgroup\$