Click here to Skip to main content
15,867,686 members
Articles / Web Development / ASP.NET

Credit Card Validator control for ASP.NET

Rate me:
Please Sign up or sign in to vote.
4.79/5 (115 votes)
30 Aug 2002CPOL13 min read 855.6K   18.3K   283   124
Article outlining how to create a credit card validator control fully derived from BaseValidator.

Sample Image

Introduction

A 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.

Image 2

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 BaseValidator and CreditCardValidator classes - an is a relationship - demonstrating inheritance from BaseValidator to the more specialised CreditCardValidator class. New with the third incarnation of the control is the AcceptedCardTypes property which is used to specify what types of card should pass the validation using the CardType enumeration.

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 IsValidCardType, and whether this is used during the validation is set by the ValidateCardType property.

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 Formula

The CreditCardValidator control will perform a check on the contents of the textbox using Luhn's formula which is used to validate card numbers. It can be used to check against a number of cards, including the following:

  • MasterCard
  • VISA
  • American Express
  • Diners Club/Carte Blanche
  • enRoute
  • Discover
  • JCB
  • Solo*
  • Switch*

* 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:

  1. Double the value of alternating digits
    The first step is to double each of the alternating digits in the number. But the trick is to start with the second digit from the right and work backwards. Say we have a credit card number 1234 5678 1234 5670. We'll start with the rightmost number 7, double it, and then do the same for every other digit.

    1234 5678 1234 5670

    This will give us the following values.

    7 x 2 = 14
    5 x 2 = 10
    3 x 2 = 6
    .
    .
    etc.

  2. Add the separate digits of all the products
    Now we'll the separate digits of all the products, and come up with a final sum.

    (1 + 4) + (1 + 0) + 6 + 2 + (1 + 4) + (1 + 0) + 6 + 2 = 28

    Be sure to add the digits, not just the number.

  3. Add the unaffected digits
    Now we'll go back to the original number and add all the digits that we didn't double. We'll still start from the right, but this time we'll start from the rightmost number.

    1234 5678 1234 5670
    0 + 6 + 4 + 2 + 8 + 6 + 4 + 2 = 32

  4. Add the results and divide by 10
    Finally, we'll add both the results and divide the answer by 10.

    28 + 32 = 60

    60 is evenly divided by 10, so the credit card number is well formed and ready for further processing.

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 BaseValidator it's possible to produce a control which behaves exactly as any other validator for the easiest deployment.

Luhn's implementation

The code for Luhn's formula is in the ValidateCardNumber method which is implemented as follows:

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.

  1. Build an ArrayList which will contain the alternating digits taken in step 1. This is so that the original values can be used again in step 2 but without looping back through the number. This was primarily done for readability.
  2. Once the list has been created, a sum is created of the individual digits if the number is greater than 9 (i.e. has more than one digit).
  3. The original digits that were untouched are added together, these create the OriginalSum variable. This is then added to the number created as a result of steps 1 and 2, and the value is divided by 10 and the result tested against 0 which provides the return value for the function.

If an exception is thrown throughout the code, then false is returned.

Card Type Validation

Each 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:

Card Type Prefix Number Length
MasterCard 51-55 16
VISA 4 13 or 16
American Express 34 or 37 15
Diners Club/Carte Blanche 300-305,36,38 14
enRoute 2014,2149 15
Discover 6011 16
JCB 3 16
JCB 2131,1800 15

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 CardType (which is an instance of an Int32-based enumerated type) will be used as a set of bit flags - each bit reflects a single card type. So, 0...0001 is MasterCard, 0...0010 is VISA. By using a set of bit flags it will be possible to set a variable to more than one card type, and be able to determine which ones are to be supported.

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 AcceptedCardTypes property (and then stored in the _cardTypes member variable). The code looks like this:

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 cardNumber for each possible card type. The regular expression is very simple, and searches for any of the numbers separated by the pipe character. So, for the AMEX type it searches for either 34 or 37. Because the string is prefixed by the epsilon (^) character, the search is performed at the start of the cardNumber. This search is performed through the IsMatch static method of the Regex class.

A further test is also done at the same time to determine whether or not the card's type exists in the AcceptedCardTypes property:

&& ((_cardTypes & CardType.Amex)!=0)

Provided both the tests return true (i.e. the prefix is matched, and the card type exists in the _cardTypes member variable) a length test is performed and provided the card number is a valid length then the IsValidCardType method will return true.

If the card type is not recognised then as long as Unknown has been set in _cardTypes then IsValidCardType will return true - since the user will have specified that an Unknown card type is an accepted card type. Otherwise, it will return false and will fail.

The _cardTypes variable is set using a property accessor which is implemented as follows:

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. AcceptedCardTypes = CardType.VISA | CardType.Amex etc.)

That's the second validation method implemented, all that's left to do is to produce the BaseValidator derived class that uses the above methods to validate the text in the associated control.

The Validation Control's implementation

That is the majority of the code written, all that's left to do is produce a class dervied from System.Web.UI.WebControls.BaseValidator and override the necessary functions. Incidentally, I used Cenk Civici's article for a "ListControl SelectedItem Validator" as the main source for this.

The BaseValidator requires that we override the EvaluateIsValid method, which suprisingly enough, is the function that determines whether or not the associated control has valid content -- in our case whether or not the associated text box has a valid credit card number entered. Following Cenk Cevi's article I also included an implementation of the ControlPropertiesValid helper function that determines whether the control specified by the ControlToValidate property is a valid control - thus ensuring that it is a textbox we're checking. Since most controls have a text property it's probably not a major issue but it would be strange to be validating the text property of a button etc., so as an extra precaution I included it.

Firstly, ControlPropertiesValid

protected 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 ControlToValidate and checks that it does indeed point to something, and then checks whether it is a TextBox. If so, it sets the member variable _creditCardTextBox to the TextBox on the web form. If anything bad happens it returns false.

Finally, EvaluateIsValid

This method is declared as abstract in the BaseValidator class, and so has to be implemented by our derived class. It is also the method that is called to check that the contents of the associated control is valid.

The CreditCardValidator control includes two additional properties, one of which is ValidateCardType which can be used to set whether or not the card type should also be checked. If it is to be checked, then the length is checked before the number is evaluated against Luhn's formula. However, if the ValidateCardType property is set to false then the card number is validated against Luhn's formula directly.

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 ValidateCardNumber method succeeds, the validation is considered a success.

Using the Credit Card Validator Control

That's all that's necessary to have the CreditCardValidator control finished. Now for a quick example of how it can be used in a real web form (the full code for this is included as a download at the top of the page).

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... For example, the following code would bind the validator control to a TextBox called CardNumber:

<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 CreditCardValidator offers a few properties that are not taken from the BaseValidator base type, these are ValidateCardType and AcceptedCardTypes. The ValidateCardType property sets whether or not the card type should be checked. This will mean that a length test is performed, as well as specifying which card types should be accepted. The AcceptedCardTypes property can be set using the CardType enumeration. In the above code the accepted types are Amex, VISA, and MasterCard. If unrecognised card types are also to be accepted then you can include "Unknown" in the list. If you want all the recognised types accepted then you can use "All". Thus, to accept anything you should use "Unknown, All".

Since I chose to have the error message displayed in a ValidationSummary control, I set the Display property to none to ensure that it wasn't displayed in-line. Otherwise, the error message would be put at the location of this control.

Conclusion

ASP.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 improvement

At 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

  • Update: 24/08/2002 - I have added support for specifying the card types that should be accepted by the control. The changes were made to the implementation of the IsValidLength method which has been renamed IsValidCardType. The card types to be accepted can be set using the AcceptedCardTypes property, which uses the CardType enumeration.
  • Update: 21/08/2002 - shortly after posting the first version, I included support for validating the length of the number. The article below shows the new updated version only.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Web Developer
United Kingdom United Kingdom
I graduated with a first class BSc honours degree in a Computer Science/Management Science hybrid Smile | :) from Loughborough University. I live in London and currently work as a .NET Developer in central London.

I'm also currently blogging my progress at further developing my articles into commercial components. Read my blog here.

I've also recently started dabbling at digital photography, and digital re-touching, and developing small simple multiplayer games in Flash.

Comments and Discussions

 
Generalvalidator not working in mozilla Pin
Craig Anderson29-Jun-04 6:33
Craig Anderson29-Jun-04 6:33 
GeneralRe: validator not working in mozilla Pin
maheswariragu4-Jun-07 1:13
maheswariragu4-Jun-07 1:13 
GeneralClient Side Validator. Pin
Kanwar Manish28-May-04 2:25
Kanwar Manish28-May-04 2:25 
GeneralRe: Client Side Validator. Pin
intrader22-Sep-04 21:14
intrader22-Sep-04 21:14 
GeneralRe: Client Side Validator. Pin
intrader24-Sep-04 6:38
intrader24-Sep-04 6:38 
GeneralRe: validator not working in mozillaxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxRe: validator not working in mozillaxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxRe: validator no Pin
kogore27-Jan-06 14:18
kogore27-Jan-06 14:18 
GeneralRe: validator not working in mozillaxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxRe: validator not working in mozillaxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxRe: validator no Pin
nonintanon10-Oct-07 4:29
nonintanon10-Oct-07 4:29 
GeneralUpdated article posted Pin
Paul Ingles29-Feb-04 5:56
Paul Ingles29-Feb-04 5:56 
http://www.codeproject.com/useritems/CreditCardValidator2.asp[^]

--
Paul
"Put the key of despair into the lock of apathy. Turn the knob of mediocrity slowly and open the gates of despondency - welcome to a day in the average office."
- David Brent, from "The Office"

MS Messenger: paul@oobaloo.co.uk
Download my PGP public key

GeneralRe: Updated article posted Pin
eirikr5-Jun-04 16:34
eirikr5-Jun-04 16:34 
GeneralRe: Updated article posted Pin
Paul Ingles6-Jun-04 22:11
Paul Ingles6-Jun-04 22:11 
GeneralRe: Updated article posted Pin
Md Anjum Rizwi21-May-07 2:56
Md Anjum Rizwi21-May-07 2:56 
GeneralUnavailability of demo Pin
Paul Ingles8-Feb-04 12:10
Paul Ingles8-Feb-04 12:10 
QuestionEvaluate true false? Pin
emcconnell16-Dec-03 10:12
emcconnell16-Dec-03 10:12 
AnswerRe: Evaluate true false? Pin
emcconnell16-Dec-03 10:14
emcconnell16-Dec-03 10:14 
AnswerRe: Evaluate true false? Pin
Paul Ingles8-Feb-04 12:09
Paul Ingles8-Feb-04 12:09 
QuestionExpiration Date? Pin
srt74-Dec-03 6:39
srt74-Dec-03 6:39 
AnswerRe: Expiration Date? Pin
Paul Ingles9-Dec-03 21:58
Paul Ingles9-Dec-03 21:58 
AnswerRe: Expiration Date? Pin
waleexy25-May-05 3:34
susswaleexy25-May-05 3:34 
GeneralCard Type drop down Pin
Prashant Danda13-Oct-03 10:18
Prashant Danda13-Oct-03 10:18 
GeneralRe: Card Type drop down Pin
Paul Ingles14-Oct-03 12:08
Paul Ingles14-Oct-03 12:08 
GeneralRe: Card Type drop down Pin
Prashant Danda14-Oct-03 12:28
Prashant Danda14-Oct-03 12:28 
GeneralRe: Card Type drop down Pin
Paul Ingles14-Oct-03 12:58
Paul Ingles14-Oct-03 12:58 
GeneralSupport for ascx control Pin
javier_morales1-Sep-03 14:57
javier_morales1-Sep-03 14:57 
GeneralRe: Support for ascx control Pin
Paul Ingles7-Sep-03 4:29
Paul Ingles7-Sep-03 4:29 
GeneralRe: Support for ascx control Pin
pirklk4-Oct-03 10:29
pirklk4-Oct-03 10:29 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.