|
||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionA while ago I started working on converting an eCommerce payment gateway's (DataCash) COM server to a native .NET assembly using their XML API. Once I had got a basic version working I decided to produce a simple web form to test it out, and so opened it up for all comers (and received some very generous donations from CP members -- thanks guys :). As part of this web form I wanted to include support to check that users had entered a card number, expiration date etc., and then wanted to extend it further to include support for checking that the card number was valid before issuing a request to the payment gateway's server. This is the result, a drop-in replacement for any of the other validation controls. Incidentally, you can see a demo of the validator in use (as well as the card payment gateway assembly) at the following address: https://ssl500.securepod.com/oobaloo/DataCash/, besides this you may also be interested in the everything you ever wanted to know about CC's guide. Before getting into any of the implementation details here is a simple UML class diagram to show the rough layout of the Control.
The diagram is missing information about parameter types since its not essential
to understanding the model. For those who are not familiar with UML, it shows
a specialisation relationship between the The control includes support for validating card numbers in two ways. Firstly,
through checking the card number using Luhn's formula, the details of which
are included in the next part of the article. Secondly, the card type itself
is examined, and the length is checked. The card type can be determined through
a prefix and each type has a specified length, by examining these an additional
level of control can be added - the types of card to accept. The method that
implements this is The main way the card number is going to be validated is through Luhn's formula, so firstly a little bit of background information and a demo of how the validation is performed. Luhn's FormulaThe
* These are UK only cards from my recollection, but have been tested by myself and work. Information on the history of the formula can be found on WebOpedia but so you don't have to read that here is a summary of how it is performed:
This will be converted into a method which will perform all of the steps listed
above on the contents of the specified textbox. By deriving the new validator
control from Luhn's implementationThe code for Luhn's formula is in the private static bool ValidateCardNumber( string cardNumber ) { try { // Array to contain individual numbers System.Collections.ArrayList CheckNumbers = new ArrayList(); // So, get length of card int CardLength = cardNumber.Length; // Double the value of alternate digits, starting with the second digit // from the right, i.e. back to front. // Loop through starting at the end for (int i = CardLength-2; i >= 0; i = i - 2) { // Now read the contents at each index, this // can then be stored as an array of integers // Double the number returned CheckNumbers.Add( Int32.Parse(cardNumber[i].ToString())*2 ); } int CheckSum = 0; // Will hold the total sum of all checksum digits // Second stage, add separate digits of all products for (int iCount = 0; iCount <= CheckNumbers.Count-1; iCount++) { int _count = 0; // will hold the sum of the digits // determine if current number has more than one digit if ((int)CheckNumbers[iCount] > 9) { int _numLength = ((int)CheckNumbers[iCount]).ToString().Length; // add count to each digit for (int x = 0; x < _numLength; x++) { _count = _count + Int32.Parse( ((int)CheckNumbers[iCount]).ToString()[x].ToString() ); } } else { // single digit, just add it by itself _count = (int)CheckNumbers[iCount]; } CheckSum = CheckSum + _count; // add sum to the total sum } // Stage 3, add the unaffected digits // Add all the digits that we didn't double still starting from the // right but this time we'll start from the rightmost number with // alternating digits int OriginalSum = 0; for (int y = CardLength-1; y >= 0; y = y - 2) { OriginalSum = OriginalSum + Int32.Parse(cardNumber[y].ToString()); } // Perform the final calculation, if the sum Mod 10 results in 0 then // it's valid, otherwise its false. return (((OriginalSum+CheckSum)%10)==0); } catch { return false; } } The code includes comments that explains how it works, however, here is a summary.
If an exception is thrown throughout the code, then false is returned. Card Type ValidationEach of the card types mentioned above can be tested for a given length based upon the numerical prefix. The prefixes and lengths are in the table below:
These types can be put into an enumeration. This will allow us to include a property that users can set specifying which types to accept, and then test the property during the validation to determine which types should be accepted.
[Flags, Serializable]
public enum CardType
{
MasterCard = 0x0001,
VISA = 0x0002,
Amex = 0x0004,
DinersClub = 0x0008,
enRoute = 0x0010,
Discover = 0x0020,
JCB = 0x0040,
Unknown = 0x0080,
All = CardType.Amex | CardType.DinersClub |
CardType.Discover | CardType.Discover |
CardType.enRoute | CardType.JCB |
CardType.MasterCard | CardType.VISA
}
The This card type check is to be performed alongside the length check (ensuring that the card number matches the card type's expected length) and for this check we will use a Regular Expression using the .NET Framework's Regex class. Regular Expressions let you perform pattern matches, and can be extremely powerful. For more details on Regular Expressions take a look at .NET Framework Regular Expressions on MSDN, and if you only want to include this kind of validation you can use the Regular Expression validation control. The card type check also includes support for the end user to specify which
card types should pass the validation, this is set through the public bool IsValidCardType( string cardNumber ) { // AMEX -- 34 or 37 -- 15 length if ( (Regex.IsMatch(cardNumber,"^(34|37)")) && ((_cardTypes & CardType.Amex)!=0) ) return (15==cardNumber.Length); // MasterCard -- 51 through 55 -- 16 length else if ( (Regex.IsMatch(cardNumber,"^(51|52|53|54|55)")) && ((_cardTypes & CardType.MasterCard)!=0) ) return (16==cardNumber.Length); // VISA -- 4 -- 13 and 16 length else if ( (Regex.IsMatch(cardNumber,"^(4)")) && ((_cardTypes & CardType.VISA)!=0) ) return (13==cardNumber.Length||16==cardNumber.Length); // Diners Club -- 300-305, 36 or 38 -- 14 length else if ( (Regex.IsMatch(cardNumber,"^(300|301|302|303|304|305|36|38)")) && ((_cardTypes & CardType.DinersClub)!=0) ) return (14==cardNumber.Length); // enRoute -- 2014,2149 -- 15 length else if ( (Regex.IsMatch(cardNumber,"^(2014|2149)")) && ((_cardTypes & CardType.DinersClub)!=0) ) return (15==cardNumber.Length); // Discover -- 6011 -- 16 length else if ( (Regex.IsMatch(cardNumber,"^(6011)")) && ((_cardTypes & CardType.Discover)!=0) ) return (16==cardNumber.Length); // JCB -- 3 -- 16 length else if ( (Regex.IsMatch(cardNumber,"^(3)")) && ((_cardTypes & CardType.JCB)!=0) ) return (16==cardNumber.Length); // JCB -- 2131, 1800 -- 15 length else if ( (Regex.IsMatch(cardNumber,"^(2131|1800)")) && ((_cardTypes & CardType.JCB)!=0) ) return (15==cardNumber.Length); else { // Card type wasn't recognised, provided Unknown is in the // CardTypes property, then return true, otherwise return false. if ( (_cardTypes & CardType.Unknown)!=0 ) return true; else return false; } } It's not the prettiest of code, but it effectively performs a RegEx comparison
on the A further test is also done at the same time to determine whether or not the
card's type exists in the && ((_cardTypes & CardType.Amex)!=0) Provided both the tests return true (i.e. the prefix is matched, and the card
type exists in the If the card type is not recognised then as long as The public string AcceptedCardTypes { get { return _cardTypes.ToString(); } set { _cardTypes = (Etier.CardType) Enum.Parse(typeof(Etier.CardType), value, false ); } } This enables users to specify the card types using a string (e.g. "Amex,
VISA, Unknown"), as opposed to having to programmatically set it via. the
OnLoad event (e.g. That's the second validation method implemented, all that's left to do is to
produce the The Validation Control's implementationThat is the majority of the code written, all that's left to do is produce
a class dervied from The Firstly, ControlPropertiesValidprotected override bool ControlPropertiesValid() { // Should have a text box control to check Control ctrl = FindControl(ControlToValidate); if ( null != ctrl ) { if (ctrl is System.Web.UI.WebControls.TextBox) { _creditCardTextBox = (System.Web.UI.WebControls.TextBox) ctrl; return ( null != _creditCardTextBox ); } else return false; } else return false; } The code first finds the Finally, EvaluateIsValidThis method is declared as abstract in the The protected override bool EvaluateIsValid() { if (_validateCardType) // should the length be validated also? { // Check the length, if the length is fine then validate the // card number if (IsValidCardType(_creditCardTextBox.Text)) return ValidateCardNumber( _creditCardTextBox.Text ); else return false; // Invalid length } else // Check that the text box contains a valid number using // the ValidateCardNumber method return ValidateCardNumber( _creditCardTextBox.Text ); } Provided the Using the Credit Card Validator ControlThat's all that's necessary to have the The first that needs to be done is to include the declaration at the top of the aspx page that imports the assembly and maps the namespace to a prefix. This also requires that the assembly's DLL file is copied to the bin directory for the application. The location of this directory depends upon the setup of your application, but assuming there's a virtual directory off the root called /CreditCard/ then your bin directory would be /CreditCard/bin/. <%@ Register TagPrefix="etier" Namespace="Etier" Assembly="CreditCardValidator" %> This allows you to then add the control to the page in the form of <etier:CreditCardValidator Id="MyValidator" ControlToValidate="CardNumber" ErrorMessage="Please enter a valid credit card number" Display="none" RunAt="server" EnableClientScript="False" ValidateCardType="True" AcceptedCardTypes="Amex, VISA, MasterCard" /> The Since I chose to have the error message displayed in a ConclusionASP.NET provides a huge amount of functionality built in, and I can't remember how many times I've produced custom code to do the very thing that Validation controls provide. This, coupled with the Object-Oriented nature of .NET, means that you can extend the existing .NET offerings into something you can use time and again. Now that xcopy deployment is a real option with web applications, dropping in support for your own custom validation controls can be done in a fraction of the time than with traditional ASP development. This concludes my second article, and I hope its of use to people. It doesn't really show anything new, but does roll up some important functionality into a re-usable control that can be deployed and used extremely easily. Things that are missing/Ideas for improvementAt the moment, all validation is taken care of on the server-side. This is fine since the aim was to prevent requests being issued to the payment gateway with incorrect details - especially since there's likely to be a delay of a few seconds. However, since the validation algorithm is not a secret and is relatively simple to implement, a client side version could be implemented to check the value before the data is posted back to the form and thus saving round-trips. If anyone decides to extend this to include client side scripting, or has any comments or questions then it'd be great to hear from you. History
| |||||||||||||||||||||||||||||||||||||||||||||||