Library tutorials & articles

Using Encryption in .NET

Getting Started

Getting Started

For the sample code and instructions in the remainder of this article, we will be using the Rijndael cipher as implemented in the RijndaelManaged FCL class. OK, let's get started with the code. Our first steps will be to create an instance of the cipher, set the key and block sizes and generate a random key. The following code snippet demonstrates this procedure:

RijndaelManaged InitCipher()
{
  // Create an instance of the cipher
  RijndaelManaged cipher = new RijndaelManaged();
  // Set the key and block size.
  // Although the key size defaults to 256, it's better to be explicit.
  cipher.KeySize = 256;
 
  // BlockSize defaults to 128 bits, so let's set this
  // to 256 for better security
  cipher.BlockSize = 256;
 
  // GenerateKey method utilizes the RNGCryptoServiceProvider
  // class to generate random bytes of necessary length.
  cipher.GenerateKey();
 
  return cipher;
}

The code above initializes a new instance of RijndaelManaged, sets the appropriate key and block sizes and creates a new random key. The value of the key can be retrieved by accessing the Key property of the cipher.

Assuming that the messages encrypted by our cipher are to be transmitted across the network, the generated key must somehow be securely transmitted to the other party. Just how to accomplish this safely is a problem beyond the scope of this article, but this is generally accomplished by using a longer term key, which is used to encrypt "session" keys for transmission between parties. This session key is then used for a relatively short duration communications session. If you are simply encrypting data to be stored on a medium such as a hard drive, you simply need to devise a means of securely storing the key so it can later be used to decrypt the data. I say simply, but this is yet another tricky problem beyond the scope of this article. In fact there is really no way to completely securely store a key online. There are ways to make it difficult for an attacker to access the key, but the fact is that the key is still present on a computer somewhere. If an attacker is able to gain privileged access to the computer, he can read the key. A more secure way to store keys is offline, such as on a disk locked in a safe.

We have a problem now. What if we are the "other party" described above? That is, what if the key has already been decided on by another who is initiating communication with us and we need to make use of the same key? In this case, you can initialize the key directly by assigning the key byte array to the Key property of the cipher. Let's refactor our initialization so that we can add an overload that allows us to play the role of either initiator or responsor. Here's the new code:

RijndaelManaged CreateCipher()
{
  RijndaelManaged cipher = new RijndaelManaged();
  cipher.KeySize  = 256;
  cipher.BlockSize = 256;
  return cipher;
}
RijndaelManaged InitCipher()
{
  RijndaelManaged cipher = CreateCipher();
 
  cipher.GenerateKey();
 
  return cipher;
}
RijndaelManaged InitCipher(byte[] key)
{
  RijndaelManaged cipher = CreateCipher();
  cipher.Key = key;
 
  return cipher;
}

This modified code will now allow us either to initiate a session or participate in an already established session. However, we obviously can't encrypt or decrypt messages yet. Let's create a class, CipherWrapper, to which we will later add encryption and decryption methods.

class CipherWrapper
{
  RijndaelManaged _cipher = null;
  public CipherWrapper()
  {
    _cipher = InitCipher();
  }
  public CipherWrapper(byte[] key)
  {
    _cipher = InitCipher(key);
  }
  public byte[] Key
  {
    get { return _cipher.Key;  }
    set { _cipher.Key = value; }
  }
}

Now we can simply copy/paste our initialization functions into the CipherWrapper class. Note that the key initialization is taken care of by the constructor and a property has been added that allows us to change the key, if need be.

Blocks and Padding

One peculiarity of block ciphers that you should be aware of is that they must operate on entire blocks of data. Data less than one block in length can't be encrypted. So what happens if (as is generally the case) the length of our plain text is not an even multiple of the block size? Enter padding. Padding is extra data added to the end of the message to force the length to be an even multiple of the block size. There are several methods for accomplishing this. These methods are called padding modes, of which there are three supported by version 1.1 of the FCL (the current version as of this writing): None, PKCS7 and Zeros. The padding mode is set by assigning a value from the PaddingMode enumeration to the Padding property of an instance of the cipher.

The PaddingMode.None mode is really not a padding mode at all. If you specify PaddingMode.None, you're telling the cipher that you don't want any padding performed at all. In this case, you must ensure that you only attempt to encrypt data whose length is an even multiple of the block size. If the data doesn't meet this requirement, you will get an exception stating that this is the case. The PaddingMode.Zeros mode simply pads the data with enough zeros to cause the length of the message to be an even multiple of the block size. The problem with PaddingMode.Zeros is that it is not reversible on decryption. Suppose the cipher encounters data that decrypts with five zero bytes at the end of the message. Are there five bytes of padding or are there 4 bytes of padding and an actual zero byte in the original plain text? How would the cipher be able to know? It really can't tell. If you use the PaddingMode.Zeros mode, you must transmit the actual length of the data as part of the message so that when the data is decrypted, the decrypting party will know how many bytes of plain text there are so they can strip the padding from the decrypted message.

The PaddingMode.PKCS7 mode is more interesting. This padding mode fills out the last block of the message with a sequence of bytes, the value of each of which is equal to the total number of padding bytes. For instance, given a block size of 128 bits and a final block plain text value of [AA BB CC DD EE FF], the padding string would be "0A 0A 0A 0A 0A 0A 0A 0A 0A 0A". The 0A value indicates that there are 10 total padding bytes. The padded block would then look like so: [AA BB CC DD EE FF 0A 0A 0A 0A 0A 0A 0A 0A 0A 0A]. In order to reverse this, the cipher simply examines the value of the last byte in the block, verifies that the appropriate number of bytes containing this value exist at the end of the message, and then removes the padding from the decrypted data. You may be wondering what happens if our data length is a perfect multiple of the block size. In this scenario, PaddingMode.None and PaddingMode.Zeros add no padding. However, in the case of PaddingMode.PKCS7, padding must be added because the cipher must be able to reverse even a no-padding situation. In this case, an additional block must be added to the plain text and the value of each byte set to the block size in bytes. In the case of a 128-bit block, a block containing [10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10] would be added to the end of the message.

A couple of new padding modes are added to the .NET 2.0 FCL, ANSIX923 and ISO10126. These modes operate in a manner very similar to PKCS7. Each of the two new modes sets the value of the final byte equal to the total number of padding bytes. ANSIX923 then sets the remaining bytes to zero, while ISO10126 places random data in the remaining padding bytes. If you are using the 1.1 version of the FCL, I would recommend using the PKCS7 mode because it requires the least amount of effort and care on your part. Although, fortunately, this is the default, I would still recommend explicitly setting the Padding property of your cipher to PaddingMode.PKCS7 in your code. Let's modify our CreateCipher method to do this:

RijndaelManaged CreateCipher()
{
  RijndaelManaged cipher = new RijndaelManaged();
  cipher.KeySize  = 256;
  cipher.BlockSize = 256;
  // Here's the new line:
  cipher.Padding = PaddingMode.PKCS7;
  return cipher;
}

If you happen to be using the 2.0 version of the FCL, I would recommend using the ISO10126 mode as it adds random padding bytes, which reduces the predictability of the plain text. In 2.0, PKCS7 is still the default, so in this case, explicitly setting the Padding property to PaddingMode.ISO10126 will be mandatory. In recommending these modes, I realize that you won't always have a choice. You're not operating in a vacuum and you need to play nicely with the other parties with whom you're communicating. However, given the choice, go with PKCS7 in 1.1 and ISO10126 in 2.0.

Comments

  1. 19 Jan 2009 at 18:40
    Good the article. I have used the base and added support for: - automatically Combining Message + IV - uuEncode/Decode support to write as String - Direct String accessors to quick support for Strings - Direct Object accessors to quick support for Objects. As my thank you, i have attached the entire class below: class Cipher { RijndaelManaged _cipher = null; #region Construction public Cipher() { CreateCipher(); _cipher.GenerateKey(); } public Cipher(byte[] key) { CreateCipher(); Key = key; } public Cipher(String keyPhrase) { CreateCipher(); KeyPhrase = keyPhrase; } private void CreateCipher() { _cipher = new RijndaelManaged(); _cipher.KeySize = 256; _cipher.BlockSize = 256; _cipher.Mode = CipherMode.CBC; _cipher.Padding = PaddingMode.ISO10126; } #endregion Construction #region Key Support public byte[] Key { get { return _cipher.Key; } set { _cipher.Key = value; } } // String Key Support public String KeyPhrase { get { return Encoding.ASCII.GetString(_cipher.Key); } set { // Algo supports only 128, 192, or 256 bits // i.e. 16, 24 or 32 bytes // Ensure we have a 32byte key for 256bit encryption String sKey = value; if (sKey.Length > 32) sKey = sKey.Substring(0, 32); else { while (sKey.Length < 32) { if ((sKey.Length % 2) == 0) sKey = "R" + sKey; else sKey = sKey + "j"; } } _cipher.Key = Encoding.ASCII.GetBytes(sKey); } } #endregion Key Support #region Encrypt/Decrypt Message+IV public byte[] EncryptMessage(byte[] plainText, out byte[] iv) { _cipher.GenerateIV(); iv = _cipher.IV; ICryptoTransform transform = _cipher.CreateEncryptor(); byte[] cipherText = transform.TransformFinalBlock(plainText, 0, plainText.Length); return cipherText; } public byte[] DecryptMessage(byte[] cipherText, byte[] iv) { _cipher.IV = iv; ICryptoTransform transform = _cipher.CreateDecryptor(); byte[] plainText = transform.TransformFinalBlock(cipherText, 0, cipherText.Length); return plainText; } #endregion Encrypt/Decrypt Message+IV #region Encrypt/Decrypt wrapper to combine IV + Message void BlockCopy(ref byte[] buf, ref byte[] obj, ref int nOffset, bool bWrite) { if (bWrite) { // Pack Buffer.BlockCopy(obj, 0, buf, nOffset, obj.Length); } else { // Unpack Buffer.BlockCopy(buf, nOffset, obj, 0, obj.Length); } nOffset += obj.Length; } // Enc Wrapper to combine IV + Message public byte[] EncryptMessage(byte[] plainText) { byte[] iv; byte[] encr; encr = EncryptMessage(plainText, out iv); // Pack format: short cIVLen = (short)iv.Length; byte[] data = new byte[iv.Length + encr.Length + sizeof(short)]; byte[] byIV = BitConverter.GetBytes(cIVLen); int nOffset = 0; BlockCopy(ref data, ref byIV, ref nOffset, true); BlockCopy(ref data, ref iv, ref nOffset, true); BlockCopy(ref data, ref encr, ref nOffset, true); return data; } // Enc Wrapper to combine IV + Message public byte[] DecryptMessage(byte[] data) { // Pack format: // Unpack to get IV+encrText short cIVLen = BitConverter.ToInt16(data, 0); int nOffset = sizeof(short); byte[] iv = new byte[cIVLen]; byte[] encr = new byte[data.Length - sizeof(short) - cIVLen]; // Read iv and encr message BlockCopy(ref data, ref iv, ref nOffset, false); BlockCopy(ref data, ref encr, ref nOffset, false); // Decrypt and return return DecryptMessage(encr, iv); } #endregion Encrypt/Decrypt wrapper to combine IV + Message #region UUEncode for String support public String uuEncrypt(byte[] byPlain) { byte[] byEncr = EncryptMessage(byPlain); String sUUEncoded = UUCode(byEncr); return sUUEncoded; } public byte[] uuDecrypt(String sUUEncoded) { byte[] byEncr = UUCode(sUUEncoded); byte[] byPlain = DecryptMessage(byEncr); return byPlain; } public String Encrypt(String s) // Works with string too { return uuEncrypt(Encoding.ASCII.GetBytes(s)); } public String Decrypt(String sEncr) { return Encoding.ASCII.GetString(uuDecrypt(sEncr)); } String UUCode(byte[] byAry) { return Convert.ToBase64String(byAry); } byte[] UUCode(String sUU) { return Convert.FromBase64String(sUU); } #endregion UUEncode for String support #region Object Support public String EncryptObject(Object o) { return uuEncrypt(Obj2Bytes(o)); } public Object DecryptObject(String sEncr) { return Bytes2Obj(uuDecrypt(sEncr)); } // Support for Object Encryption/Decryption directly BinaryFormatter _bf; BinaryFormatter bf { get { if (_bf == null) _bf = new BinaryFormatter(); return _bf; } } public byte[] Obj2Bytes(Object o) { MemoryStream ms = new MemoryStream(); bf.Serialize(ms, o); int nObjSize = (int) ms.Length; byte[] byAry = new byte[nObjSize]; Buffer.BlockCopy(ms.GetBuffer(), 0, byAry, 0, nObjSize); return byAry; } public Object Bytes2Obj(byte[] byAry) { int nObjSize = byAry.Length; MemoryStream ms = new MemoryStream(byAry); Object o = bf.Deserialize(ms); return o; } #endregion Object Support }
  2. 21 May 2005 at 19:53

    I totally agree that varbinary could have been used.  My only problem with that solution is that it seems difficult to work with binary data in ADO.NET and SQL Server.  I may be wrong.  I haven't tried it, but I seem to remember seeing some nasty code dealing with storing images and other binary data in SQL Server.  The conversion to and from Base64 strings is 1 line of code in each case and then I can deal with regular char and varchar fields.

  3. 19 May 2005 at 17:26

    Couldn't you also store this in a Varbinary field instead of trying to convert it to a string for a varchar field?

  4. 18 May 2005 at 17:12

    Dear Steve,
    I was wondering if you had some insight as to how to best to file encrypting with the rijndael encryption.  Most examples I've found only give info. about how to encrypt messages.  I know you said that the CryptoStream object was beyond your scope, but that's where I'm needing to go with this and I like to use a FileStream instead of a MemoryStream.  I would also like to save the encrypted file in place of the original (which I may just have to rename them differently and then delete the original).  I also know you said that you don't have to write the encryption byte for byte, but how do you do this with a FileStream?  And if you do have to do it byte for byte with a FileStream, then how many bytes do I/should I set it to?  Also, all examples I've seen only demonstrate how to Encrypt a file and not Decrypt it again.  On the decryption, though, I only need to display it and not keep or save it anywhere.  Maybe loading it into a MemoryStream might be better?  Keep in mind that this files could be big (most are images).  Is there any advice you could give or maybe another discussion you have?


    Thanks!

  5. 24 Apr 2005 at 00:32

    Exactly.  If you don't use the same IV for encryption and decryption, the message can't be properly decoded.

  6. 23 Apr 2005 at 10:29

    Problem seems to be fixed now - I'd uncommented the initialisation vector code and it looks like it's mandetory.


    Also - replaced the hex encoding with base64 strings (many thanks to Ben Mills on the other thread about storing values in SQL server for pointing me in this direction).

  7. 23 Apr 2005 at 10:26

    Many thanks Ben - I've got a working solution now!


    I think the problem was the fact I'd commented out the initialisation vector code.. I didn't think this was essential.


    Anyhow - I've also removed my conversion routines to and from hex as the conversion to base64 strings is a lot more elegant (and the strings generated shorter).


    Thanks again.

  8. 22 Apr 2005 at 19:32

    I actually ended up cracking the problem in a different way.  Here's what I do to encode a .NET string:


    1.  Turn the string into a byte array using the ASCII encoder.
    2.  Run the byte array through the Rijndael encryptor (some bytes cannot now be represented as ASCII chars).
    3.  Convert the resulting array of bytes to a Base64 string using Convert.ToBase64String().
    4.  The result is an ASCII string which can be strored in regular (non-unicode) database columns.


    To decrypt the string I do the reverse:


    1.  Convert the string from the database into a byte array using Convert.FromBase64String().
    2.  Run the byte array through the Rijndael decryptor.
    3.  Convert the resulting array back into a string using the ASCII encoder.


    The trick with all of this is that the an 8 bit unicode string can be stored as a 7 bit ASCII string by converting it into a Base64 string.  Base 64 strings use 4 bytes to represent every block of 3 byte unicode characters.  See http://email.about.com/cs/standards/a/base64_encoding.htm for a good intro on base64 strings.  The one thing you have to think about is how long to make your database columns.  Just remember that every block of 3 unencrypted characters results in 4 encrypted characters in the database.


    I hope this helps,
    Ben Mills


  9. 22 Apr 2005 at 18:49

    Hi all,


    Excellent article - made a lot of sense.


    I've created a little test app which converts the byte arrays to hex strings (for storing in DB) - and then does the reverse. This all seems to work, but for some strange reason the first 32 characters of the decrypted string are garbled (everything after this point is fine!!).


    I'm guessing it's something to do with padding, but not sure - maybe I'm missing something really obvious!


    Any help would be hugely appreciated. I've posted the code from my test form below (hope you're okay about this and hope it benefits others)..


    cheers.



    using System;
    using System.Drawing;
    using System.Collections;
    using System.ComponentModel;
    using System.Windows.Forms;
    using System.Data;


    using System.Security.Cryptography;
    using System.Text;
    using System.IO;
    using System.Globalization;        // for NumberStyles


    namespace EncryptionTool
    {
       /// <summary>
       /// Summary description for Form1.
       /// </summary>
       public class Form1 : System.Windows.Forms.Form
       {
           private System.Windows.Forms.TextBox txtToEncrypt;
           private System.Windows.Forms.Label label1;
           private System.Windows.Forms.Button btnDecrypt;
           private System.Windows.Forms.Label label2;
           private System.Windows.Forms.TextBox txtKey;
           private System.Windows.Forms.Label label3;
           private System.Windows.Forms.Button btnEncrypt;
           private System.Windows.Forms.TextBox txtEncrypted;
           private System.Windows.Forms.TextBox txtDecrypted;
           private System.Windows.Forms.Label label4;
           /// <summary>
           /// Required designer variable.
           /// </summary>
           private System.ComponentModel.Container components = null;


           public Form1()
           {
               //
               // Required for Windows Form Designer support
               //
               InitializeComponent();


               //
               // TODO: Add any constructor code after InitializeComponent call
               //
           }


           /// <summary>
           /// Clean up any resources being used.
           /// </summary>
           protected override void Dispose( bool disposing )
           {
               if( disposing )
               {
                   if (components != null)
                   {
                       components.Dispose();
                   }
               }
               base.Dispose( disposing );
           }


           #region Windows Form Designer generated code
           /// <summary>
           /// Required method for Designer support - do not modify
           /// the contents of this method with the code editor.
           /// </summary>
           private void InitializeComponent()
           {
               this.txtToEncrypt = new System.Windows.Forms.TextBox();
               this.label1 = new System.Windows.Forms.Label();
               this.txtEncrypted = new System.Windows.Forms.TextBox();
               this.btnDecrypt = new System.Windows.Forms.Button();
               this.label2 = new System.Windows.Forms.Label();
               this.txtKey = new System.Windows.Forms.TextBox();
               this.label3 = new System.Windows.Forms.Label();
               this.btnEncrypt = new System.Windows.Forms.Button();
               this.txtDecrypted = new System.Windows.Forms.TextBox();
               this.label4 = new System.Windows.Forms.Label();
               this.SuspendLayout();
               //
               // txtToEncrypt
               //
               this.txtToEncrypt.Location = new System.Drawing.Point(8, 72);
               this.txtToEncrypt.Multiline = true;
               this.txtToEncrypt.Name = "txtToEncrypt";
               this.txtToEncrypt.Size = new System.Drawing.Size(448, 80);
               this.txtToEncrypt.TabIndex = 1;
               this.txtToEncrypt.Text = "";
               //
               // label1
               //
               this.label1.Location = new System.Drawing.Point(8, 56);
               this.label1.Name = "label1";
               this.label1.TabIndex = 2;
               this.label1.Text = "Text to encrypt";
               //
               // txtEncrypted
               //
               this.txtEncrypted.Location = new System.Drawing.Point(8, 176);
               this.txtEncrypted.Multiline = true;
               this.txtEncrypted.Name = "txtEncrypted";
               this.txtEncrypted.Size = new System.Drawing.Size(448, 80);
               this.txtEncrypted.TabIndex = 3;
               this.txtEncrypted.Text = "";
               //
               // btnDecrypt
               //
               this.btnDecrypt.Location = new System.Drawing.Point(464, 184);
               this.btnDecrypt.Name = "btnDecrypt";
               this.btnDecrypt.TabIndex = 4;
               this.btnDecrypt.Text = "&Decrypt";
               this.btnDecrypt.Click += new System.EventHandler(this.btnDecrypt_Click);
               // <

  10. 22 Apr 2005 at 18:42

    Hi,


    I'm attempting to do the same thing. I've gone down the route of converting the cipher's byte array to a Hex string using the following: -


       BitConverter.ToString(bytEncryptedMessage);


    This creates a hyphon delimitted string (e.g. 0A-10-FA etc...) which can be stored in the database.



    When I want to decrypt I'm first calling the following function: -


    // Assumes the hex string values are "-" delimitted. e.g. 0A-FA-BD
           public byte[] HexStringToBytes(string sHexString)
           {
               string[]        sStringArray;
               byte[]            bytByteArray;
               int                iCount = 0;


               sStringArray = sHexString.Split('-');
               bytByteArray = new byte[sStringArray.Length];
       
               foreach (string sHexValue in sStringArray)
               {
                   bytByteArray[iCount] = byte.Parse(sHexValue, NumberStyles.HexNumber);
                   iCount ++;
               }


               return bytByteArray;
           }


    which re-creates the byte array.




    I thought I'd cracked it, but something really weird is happening!! When decrypted - the first 32 characters are garbage - everything after is fine!!!


    I'm guessing this is something to do with the padding, but not sure.


    Hope this points you in the right direction and would also really appreciate any assistance with the problem I'm having!


  11. 30 Mar 2005 at 14:50

    First of all, I'd like to say that this was a fantastic article.  I was struggling through the MSDN info and other online articles and your article finally made everything clear.


    I want to use this technique to encrypt credit card data before I store it in SQL Server.  I take the ASCII credit card number, convert it to an array of bytes using the ASCII encoder and then encrypt the bytes.  The trouble is that the bytes are now out of the ASCII range (0 to 127), so I can't convert the bytes back to an ASCII string to store in a VarChar column of a SQL Server table.


    It seems like a solution is to convert the encrypted bytes to a unicode string using the unicode encoder and store that value in a NVarChar column.  The only problem with this is that I usually avoid unicode SQL Server columns.  Is this the only solution or do you have any other ideas?

  12. 07 Jan 2005 at 08:39

    Regarding my comment that "there is "no way" to reverse this process without the key."


    I want to be clear that I mean that there is no trivial way to do this.  What I'm trying to say is that the process is as secure as the key and the underlying cipher.


    There is, of course, no such thing as "no way" in cryptography.  We just try to use the best technology we can to make it as difficult and expensive as possible for an attacker to break the system.



    Steve Johnson

  13. 07 Jan 2005 at 08:23

    Using CBC as an example, encryption proceeds as follows:
    1 - XOR Plain Text Block #1 with IV
    2 - Encrypt the block resulting from step 1 with the key
    3 - The Cipher Text block resulting from step 3 is used as Cipher Text block #1
    4 - XOR Plain Text block #2 with Cipher Text block #1
    5 - Encrypt the block resulting from step 4 with the key
    6 - The Cipher Text block resulting from step 5 is used as Cipher Text block #2
    7 - etc, etc...


    There's no way to reverse this process without the key.  The IV is not being used to seed a PRNG; it's simply used as random data, with which to mix the first block of plain text on encryption and to retrieve the initial plain text block on decryption.  As you can see, there is no intermediate "non-randomized" state that can be determined using the cipher text and the IV.



    Steve Johnson

  14. 07 Jan 2005 at 08:05

    Wouldn't this mean that for eavesdroppers using the same random generator (which can not be that big a problem) and the transmitted seed it is possible to retrieve the 'un-randomized' (if thats a word) still encrypted content?


    Wilfred Kuijpers
    dbHost


  15. 07 Jan 2005 at 07:47

    You transmit the IV in the clear (unencrypted).  Just add the IV to your transmitted message so the receiving computer can decrypt the message.  There's no need to encrypt the IV because it is used simply to randomize the cipher text to prevent repetition.



    Steve Johnson

  16. 05 Jan 2005 at 17:08

    Hi,


    Great article,


    Just one question though, how do you transmit your IV's? I mean, decryction usually happens at another computer and for encryption and decryption you suggest to use the iv?


    regards,


    Wilfred Kuijpers
    dbhost

  17. 01 Jan 1999 at 00:00

    This thread is for discussions of Using Encryption in .NET.

Leave a comment

Sign in or Join us (it's free).

Steve Johnson Steve is a senior developer and consultant for 3t Systems, Inc. in Denver, CO, where he spends his days doing architecture, training, and "in-the-trenches" development. Steve began programming Wind...

Related discussion

Related podcasts

  • More jQuery in ASP.NET

    In this episode Chris Brandsma, Rick Strahl, Dave Ward, Bertrand Le Roy, and Scott Koon conclude their discussion of Microsoft's jQuery in ASP.NET announcement1.This episode of the Alt.NET Podcast is brought to you by LLBLGen Pro, the most mature O/R mapper and code generator out there.Are ...

Events coming up

  • Dec 9

    GL.net Group Meeting - December 2009

    Gloucester, United Kingdom

    The beginning of this year holiday season will belong to mocks. Ronnie and Stephen will take us for a tour around exciting world of unit testing.

Want to stay in touch with what's going on? Follow us on twitter!