While a client is not allowed to update the application's data based on his own calculations, he can explicitly express his assumptions within a service request.
Consider for example a PassOrder service, which accepts a list of items. The service request can be designed as to allow the client to state the price he expects to pay for each item. This price can differ from the one the item is sold at for different reasons. Maybe the client saw the item in last month's publicity, maybe the price has just recently been raised, maybe he simply mistyped the price. No matter the reason, the service requests having mismatches will be rejected.
By explicitly stating the client's assumptions in the service request, we ensure that none of our clients will be unexpectingly billed. The price to pay will alway be the one they expected.
Tuesday, July 29, 2008
Designing Service Requests
A client is never allowed to update the application's data by itself. When a bank's client wants to transfer money between his accounts, the bank does not lend him its account records, hoping the client will not make any mistake substracting the same amount from an account then adding it to another. Instead, the client is asked to fill a transfer request, and the account records are updated by the bank's clerk.
This is the philosophy behind the design of our service requests:
public class TransferServiceRequest
{
/// <summary>
/// The client requesting the transfer.
/// </summary>
public ClientRequestElement Client;
public class ClientRequestElement
{
public int ClientID;
}
/// <summary>
/// The requested transfer.
/// </summary>
public TransferRequestElement Transfer;
public class TransferRequestElement
{
public string FromAccountNumber;
public string ToAccountNumber;
public decimal Amount;
}
}
This approach allows the bank to apply business logic when performing service requests, such as charging fees for transfers less than a thousand dollars.
This is the philosophy behind the design of our service requests:
public class TransferServiceRequest
{
/// <summary>
/// The client requesting the transfer.
/// </summary>
public ClientRequestElement Client;
public class ClientRequestElement
{
public int ClientID;
}
/// <summary>
/// The requested transfer.
/// </summary>
public TransferRequestElement Transfer;
public class TransferRequestElement
{
public string FromAccountNumber;
public string ToAccountNumber;
public decimal Amount;
}
}
This approach allows the bank to apply business logic when performing service requests, such as charging fees for transfers less than a thousand dollars.
Friday, July 25, 2008
Indexing Query Results
A service is allowed to query a database table at most once, which may leave us with a heap of useful yet unclassified data rows.
For example, let us consider a GetClientOrders service, accepting a ClientID and returning this client's orders, detailed with the item names and quantities. This service could call the following SELECT stored procedures :
Order_SelectByClientID
OrderItem_SelectByClientID
Item_SelectByClientID
The GetClientOrders service then needs to build a list of orders, which begins by iterating over the Order data rows. We then need to get the order's items, which asks the following questions :
Which OrderItem data rows belong to the current order?
How can we get the details of each of these items?
We provide the answers to these questions by indexing the OrderItem and Item data rows in memory. This can be done using a Hashtable, or using the FindBy and Select methods of the ADO.NET DataTable :
For each Order: Get OrderItems[orderID]
For each OrderItem: Get Items[itemID]
For example, let us consider a GetClientOrders service, accepting a ClientID and returning this client's orders, detailed with the item names and quantities. This service could call the following SELECT stored procedures :
Order_SelectByClientID
OrderItem_SelectByClientID
Item_SelectByClientID
The GetClientOrders service then needs to build a list of orders, which begins by iterating over the Order data rows. We then need to get the order's items, which asks the following questions :
Which OrderItem data rows belong to the current order?
How can we get the details of each of these items?
We provide the answers to these questions by indexing the OrderItem and Item data rows in memory. This can be done using a Hashtable, or using the FindBy and Select methods of the ADO.NET DataTable :
For each Order: Get OrderItems[orderID]
For each OrderItem: Get Items[itemID]
Thursday, July 24, 2008
Queries With Multiple Valued Criterias
Ideally, we should base our database queries on single valued criterias, let's say all the items from the OrderID 23. But if one of our SELECT stored procedures requires a multiple valued criteria, it should accept it as an NTEXT comma-separated list :
CREATE PROCEDURE [dbo].[Item_SelectByOrderIDs]
(
@OrderIDs NTEXT
)
This stored procedure could then be called with the '1,5,7,' parameter in order to retrieve the items from the OrderIDs 1, 5 and 7. Within the stored procedure, the homemade SplitIDs utility function is used to parse these values back from the NTEXT parameter :
SELECT DISTINCT Item.*
FROM Item INNER JOIN OrderItem ON Item.ItemID=OrderItem.ItemID
WHERE OrderItem.OrderID IN
(
SELECT * FROM SplitIDs(@OrderIDs)
)
CREATE PROCEDURE [dbo].[Item_SelectByOrderIDs]
(
@OrderIDs NTEXT
)
This stored procedure could then be called with the '1,5,7,' parameter in order to retrieve the items from the OrderIDs 1, 5 and 7. Within the stored procedure, the homemade SplitIDs utility function is used to parse these values back from the NTEXT parameter :
SELECT DISTINCT Item.*
FROM Item INNER JOIN OrderItem ON Item.ItemID=OrderItem.ItemID
WHERE OrderItem.OrderID IN
(
SELECT * FROM SplitIDs(@OrderIDs)
)
Querying the Database
All our services are designed to query each database table at most once. This minimizes the use of the database, as all the required data is gathered in one go. To achieve this goal, we have to design SELECT stored procedures that sometimes accept criterias from related tables. For example, let's say our database has the following schema :
Client (1) - (*) Order (1) - (*) OrderItem (*) - (1) Item
If we need to query the Item table based on an ItemID, OrderID or ClientID, we end up with these stored procedures :
Item_SelectByItemID:
SELECT * FROM Item WHERE ItemID=@ItemID;
Item_SelectByOrderID:
SELECT DISTINCT Item.*
FROM Item INNER JOIN OrderItem ON Item.ItemID=OrderItem.ItemID
WHERE OrderItem.OrderID=@OrderID;
Item_SelectByClientID:
SELECT DISTINCT Item.*
FROM Item INNER JOIN OrderItem ON Item.ItemID=OrderItem.ItemID
INNER JOIN Order ON ItemOrder.OrderID=Order.OrderID
WHERE Order.ClientID=@ClientID
Client (1) - (*) Order (1) - (*) OrderItem (*) - (1) Item
If we need to query the Item table based on an ItemID, OrderID or ClientID, we end up with these stored procedures :
Item_SelectByItemID:
SELECT * FROM Item WHERE ItemID=@ItemID;
Item_SelectByOrderID:
SELECT DISTINCT Item.*
FROM Item INNER JOIN OrderItem ON Item.ItemID=OrderItem.ItemID
WHERE OrderItem.OrderID=@OrderID;
Item_SelectByClientID:
SELECT DISTINCT Item.*
FROM Item INNER JOIN OrderItem ON Item.ItemID=OrderItem.ItemID
INNER JOIN Order ON ItemOrder.OrderID=Order.OrderID
WHERE Order.ClientID=@ClientID
Monday, July 21, 2008
Resizing Uploaded Pictures
As tantalizing as it is to use GetThumbnailImage to generate a thumbnail from an uploaded picture, we should not rely on this Bitmap method. Indeed, the uploaded picture may already contain an embedded thumbnail, and GetThumbnailImage will use it if possible. Should the embedded thumbnail be smaller that the one we need, it will be enlarged to ugly results.
Instead, we should use the Graphics class to create our thumbnail from scratch. Still, we should take care not to create a thumbnail larger than the original picture.
Bitmap resizedBitmap = new Bitmap(resizedWidth, resizedHeight, PixelFormat.Format48bppRgb);
Graphics resizedGraphic = Graphics.FromImage(resizedBitmap);
resizedGraphic.CompositingQuality = CompositingQuality.HighQuality;
resizedGraphic.InterpolationMode = InterpolationMode.HighQualityBicubic;
resizedGraphic.PixelOffsetMode = PixelOffsetMode.HighQuality;
resizedGraphic.SmoothingMode = SmoothingMode.HighQuality;
resizedGraphic.DrawImage(originalBitmap, 0, 0, resizedWidth, resizedHeight);
resizedGraphic.Dispose();
Instead, we should use the Graphics class to create our thumbnail from scratch. Still, we should take care not to create a thumbnail larger than the original picture.
Bitmap resizedBitmap = new Bitmap(resizedWidth, resizedHeight, PixelFormat.Format48bppRgb);
Graphics resizedGraphic = Graphics.FromImage(resizedBitmap);
resizedGraphic.CompositingQuality = CompositingQuality.HighQuality;
resizedGraphic.InterpolationMode = InterpolationMode.HighQualityBicubic;
resizedGraphic.PixelOffsetMode = PixelOffsetMode.HighQuality;
resizedGraphic.SmoothingMode = SmoothingMode.HighQuality;
resizedGraphic.DrawImage(originalBitmap, 0, 0, resizedWidth, resizedHeight);
resizedGraphic.Dispose();
Using Uploaded Pictures
The following makes sure a valid picture file was uploaded.
this.ctlUpload.HasFile == true;
this.ctlUpload.PostedFile.ContentType == "image/bmp";
this.ctlUpload.PostedFile.ContentType == "image/pjpeg";
this.ctlUpload.PostedFile.ContentType == "image/gif";
this.ctlUpload.PostedFile.ContentType == "image/x-png";
The uploaded picture can then be accessed with :
HttpPostedFile postedFile = this.ctlUpload.PostedFile;
Bitmap picture = new Bitmap(postedFile.InputStream);
this.ctlUpload.HasFile == true;
this.ctlUpload.PostedFile.ContentType == "image/bmp";
this.ctlUpload.PostedFile.ContentType == "image/pjpeg";
this.ctlUpload.PostedFile.ContentType == "image/gif";
this.ctlUpload.PostedFile.ContentType == "image/x-png";
The uploaded picture can then be accessed with :
HttpPostedFile postedFile = this.ctlUpload.PostedFile;
Bitmap picture = new Bitmap(postedFile.InputStream);
Managing Uploaded Files
In the context of a web site, a file can be addressed either by it's network address as seen by the web servers, either by it's URL as seen by the browsers. These two adresses needs to be available to our application, the first to manage the files internally, the later to serve our users.
These two addressing schemes do not use the same format, and may not share the same paths. A network address and it's related URL might indeed look like :
\\fileserver\share\picture1.jpg
http://www.mysite.com/pictures/picture1.jpg
If we need to keep track of the files uploaded to our website, how should we store their location in our database? We should take care to strip any information relating to a specific addressing scheme, and retain only the file's name.
picture1.jpg
It is our web site's configuration and application logic that should provide for the reconstruction of the original addresses.
These two addressing schemes do not use the same format, and may not share the same paths. A network address and it's related URL might indeed look like :
\\fileserver\share\picture1.jpg
http://www.mysite.com/pictures/picture1.jpg
If we need to keep track of the files uploaded to our website, how should we store their location in our database? We should take care to strip any information relating to a specific addressing scheme, and retain only the file's name.
picture1.jpg
It is our web site's configuration and application logic that should provide for the reconstruction of the original addresses.
Wednesday, July 16, 2008
Using the Validation Controls
Any data post back over HTTP is text, and most of ASP.NET controls expose their value as text as well. Any control whose value will be parsed then needs to be validated as being required and of the correct type. We can do so using the validation controls :
<asp:RequiredFieldValidator ID="valRequired" runat="server" ControlToValidate="" ErrorMessage="*" />
<asp:CompareValidator ID="valInteger" runat="server" ControlToValidate="" Operator="DataTypeCheck" Type="Integer" ErrorMessage="*" />
We can also use the validation controls to make sure a value was chosen from a DropDownList, other than the prompt item with an associated value of zero :
<asp:CompareValidator ID="valNonZero" runat="server" ControlToValidate="" Operator="NotEqual" Type="Integer" ValueToCompare="0" ErrorMessage="*" />
<asp:RequiredFieldValidator ID="valRequired" runat="server" ControlToValidate="" ErrorMessage="*" />
<asp:CompareValidator ID="valInteger" runat="server" ControlToValidate="" Operator="DataTypeCheck" Type="Integer" ErrorMessage="*" />
We can also use the validation controls to make sure a value was chosen from a DropDownList, other than the prompt item with an associated value of zero :
<asp:CompareValidator ID="valNonZero" runat="server" ControlToValidate="" Operator="NotEqual" Type="Integer" ValueToCompare="0" ErrorMessage="*" />
Friday, July 11, 2008
Global Resources
We can share resources among the different pages of our website by using a resource file residing in the App_GlobalResources folder. These global resources can be accessed from code :
this.GetGlobalResourceObject("ResourceFileName", "ResourceName");
Also, the resources can be globalized, and the localization of the returned resource depends on the culture specified in the configuration file :
</system.web>
<globalization uiCulture="fr" culture="fr-CA" />
</system.web>
this.GetGlobalResourceObject("ResourceFileName", "ResourceName");
Also, the resources can be globalized, and the localization of the returned resource depends on the culture specified in the configuration file :
</system.web>
<globalization uiCulture="fr" culture="fr-CA" />
</system.web>
Thursday, July 10, 2008
Relative Paths in Master Pages
When it executes, a master page is first merged with one of its content pages, and this merge is then rendered in the folder of the content page. This means that all relative paths in the master page become relative to the folder of the content page, and that these paths will be broken if the master page and content page do not reside in the same folder.
Relatives paths should still be used in master pages, but we have to make them relative to our website's root, and let ASP.NET resolve them when the master page executes :
<img src="<%=ResolveUrl("~/images/logo.gif")%>">
The ResolveUrl method is not available in the HEAD section of the master page, but marking this element with runat="server" ensures that the CSS style sheets are linked properly.
Relatives paths should still be used in master pages, but we have to make them relative to our website's root, and let ASP.NET resolve them when the master page executes :
<img src="<%=ResolveUrl("~/images/logo.gif")%>">
The ResolveUrl method is not available in the HEAD section of the master page, but marking this element with runat="server" ensures that the CSS style sheets are linked properly.
Wednesday, July 9, 2008
Securing Website Folders
We can restrict access to certain folders of our website based on the roles we have defined in the Membership API. The following configuration ensures that only users in the Member role have access to this site's section :
<configuration>
<system.web>
<authorization>
<allow roles="Member" />
<deny users="*" />
</authorization>
</system.web>
</configuration>
This requirement is enforced by ASP.NET based both on Forms Authentication and the Membership API. When a user accesses the secured folder, it's username is extracted from the authentication cookie we have previously set through Forms Authentication. This username is then looked up in the Membership API to check if it is part of the allowed roles. If so, the page is rendered. Otherwise, the user is redirected to the Forms Authentication login URL.
<configuration>
<system.web>
<authorization>
<allow roles="Member" />
<deny users="*" />
</authorization>
</system.web>
</configuration>
This requirement is enforced by ASP.NET based both on Forms Authentication and the Membership API. When a user accesses the secured folder, it's username is extracted from the authentication cookie we have previously set through Forms Authentication. This username is then looked up in the Membership API to check if it is part of the allowed roles. If so, the page is rendered. Otherwise, the user is redirected to the Forms Authentication login URL.
Using the Membership API
Once the Membership API has been configured, we can use the Web Site Administration Tool to manually create users and roles. But more importantly, we can invoke the Membership API from code :
string password = Membership.GeneratePassword(Membership.MinRequiredPasswordLength, Membership.MinRequiredNonAlphanumericCharacters
MembershipUser membershipUser = Membership.CreateUser(userName, password, emailAddress);
Roles.AddUserToRole(userName, "Member");
The following shortcut is also provided to obtain the Membership API user whose username matches the one obtained from the Forms Authentication cookie :
MembershipUser currentUser = Membership.GetUser();
Guid userID = (Guid)currentUser.ProviderUserKey;
string password = Membership.GeneratePassword(Membership.MinRequiredPasswordLength, Membership.MinRequiredNonAlphanumericCharacters
MembershipUser membershipUser = Membership.CreateUser(userName, password, emailAddress);
Roles.AddUserToRole(userName, "Member");
The following shortcut is also provided to obtain the Membership API user whose username matches the one obtained from the Forms Authentication cookie :
MembershipUser currentUser = Membership.GetUser();
Guid userID = (Guid)currentUser.ProviderUserKey;
Tuesday, July 8, 2008
Configuring the Membership API
The Membership API needs a database where to store it's information. We can create it's tables and stored procedures within our own website's database, by running the aspnet_regsql.exe utility.
We then need to point the Membership API to our database. This is done through the following web configuration :
<configuration>
<system.web>
<membership defaultProvider="MembershipProvider">
<providers>
<add name="MembershipProvider" type="System.Web.Security.SqlMembershipProvider" connectionStringName="" applicationName="" />
</providers>
</membership>
The connectionStringName must be defined in the <connectionStrings> section of the configuration file, while the applicationName must be set in order to prevent deployment issues. Also, if we intend to use the Roles functionality, we need to configure it in a similar way, through the roleManager configuration element.
We then need to point the Membership API to our database. This is done through the following web configuration :
<configuration>
<system.web>
<membership defaultProvider="MembershipProvider">
<providers>
<add name="MembershipProvider" type="System.Web.Security.SqlMembershipProvider" connectionStringName="" applicationName="" />
</providers>
</membership>
The connectionStringName must be defined in the <connectionStrings> section of the configuration file, while the applicationName must be set in order to prevent deployment issues. Also, if we intend to use the Roles functionality, we need to configure it in a similar way, through the roleManager configuration element.
Monday, July 7, 2008
Forms Authentication
Once a user has been succesfully authenticated, we can provide him with an ID card in the form of a cookie, by using FormsAuthentication :
FormsAuthentication.SetAuthCookie(username, true);
This cookie is encrypted in a way that make it very difficult to spoof an ID card :
.ASPXAUTH7A70ED1522FD48DAF9916C9A24AB7A19117478989BA2
41DCF79D7E6FA321D9E538A3533F7F9778499121B2D1D8A4F9C36
55B805DBA7E8EE19233AC8733A20BED170FF675B6CBA8DAD92BA5
9FB2B1D5B7184C7A8A30E8251726A41D6CA5E92C3Blocalhost/9
728267362585629942014186081504029942010*
The client's browser then flashes this ID card every time it requests a page from our web site. IIS decrypts the cookie and assigns the username it contains to the HttpContext of the page's execution :
this.Context.User.Identity.Name
We can see this identity as an integral part of the submitted request, and we can trust it as being legitimate. This functionality is activated through the following web configuration :
<configuration>
<system.web>
<authentication mode="Forms"/>
</system.web>
</configuration>
FormsAuthentication.SetAuthCookie(username, true);
This cookie is encrypted in a way that make it very difficult to spoof an ID card :
.ASPXAUTH7A70ED1522FD48DAF9916C9A24AB7A19117478989BA2
41DCF79D7E6FA321D9E538A3533F7F9778499121B2D1D8A4F9C36
55B805DBA7E8EE19233AC8733A20BED170FF675B6CBA8DAD92BA5
9FB2B1D5B7184C7A8A30E8251726A41D6CA5E92C3Blocalhost/9
728267362585629942014186081504029942010*
The client's browser then flashes this ID card every time it requests a page from our web site. IIS decrypts the cookie and assigns the username it contains to the HttpContext of the page's execution :
this.Context.User.Identity.Name
We can see this identity as an integral part of the submitted request, and we can trust it as being legitimate. This functionality is activated through the following web configuration :
<configuration>
<system.web>
<authentication mode="Forms"/>
</system.web>
</configuration>
Subscribe to:
Comments (Atom)