Objective 3.1: Validate application input
Managing data integrity
Four different types of data integrity:
- Entity integrity
States that each entity (a record in a database) should be uniquely identifiable. In a database, this is achieved by using a primary key column. A primary key uniquely identifies each row of data. It can be generated by the database or by your application. - Domain integrity
Refers to the validity of the data that an entity contains. This can be about the type of data and the possible values that are allowed (a valid postal code, a number within a certain range, or a default value, for example). - Referential integrity
The relationship that entities have with each other, such as the relationship between an order and a customer. - User-defined integrity
Comprises specific business rules that you need to enforce. A business rule for a web shop might involve a new customer who is not allowed to place an order above a certain dollar amount.
LISTING 3-1 Customer and Address classes
public class Customer
{
public int Id { get; set; }
[Required, MaxLength(20)]
public string FirstName { get; set; }
[Required, MaxLength(20)]
public string LastName { get; set; }
[Required]
public Address ShippingAddress { get; set; }
[Required]
public Address BillingAddress { get; set; }
}
public class Address
{
public int Id { get; set; }
[Required, MaxLength(20)]
public string AddressLine1 { get; set; }
[Required, MaxLength(20)]
public string AddressLine2 { get; set; }
[Required, MaxLength(20)]
public string City { get; set; }
[RegularExpression(@"^[1-9][0-9]{3}\s?[a-zA-Z]{2}$")]
public string ZipCode { get; set; }
}
You can use the following predefined attributes:
- DataTypeAttribute
- RangeAttribute
- RegularExpressionAttribute
- RequiredAttribute
- StringLengthAttribute
- CustomValidationAttribute
- MaxLengthAttribute
- MinLengthAttribute
LISTING 3-2 Saving a new customer to the database
public class ShopContext : DbContext
{
public IDbSet<Customer> Customers { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
// Make sure the database knows how to handle the duplicate address property
modelBuilder.Entity<Customer>().HasRequired(bm => bm.BillingAddress)
.WithMany().WillCascadeOnDelete(false);
}
}
using (ShopContext ctx = new ShopContext())
{
Address a = new Address
{
AddressLine1 = "Somewhere 1",
AddressLine2 = "At some floor",
City = "SomeCity",
ZipCode = "1111AA"
};
Customer c = new Customer()
{
FirstName = "John",
LastName = "Doe",
BillingAddress = a,
ShippingAddress = a,
};
ctx.Customers.Add(c);
ctx.SaveChanges();
}
If you forget to set the FirstName property, the Entity Framework throws the following exception:
System.Data.Entity.Validation.DbEntityValidationException : Validation failed for one or
more entities. See ‘EntityValidationErrors’ property for more details.
LISTING 3-3 Running manual validation
public static class GenericValidator<T>
{
public static IList<ValidationResult> Validate(T entity)
{
var results = new List<ValidationResult>();
var context = new ValidationContext(entity, null, null);
Validator.TryValidateObject(entity, context, results);
return results;
}
}
A transaction
helps you group a set of related operations on a database. It ensures that those operations are seen as one distinct action. If one fails, they all fail and can easily be rolled back.
Using Parse, TryParse, and Convert
LISTING 3-4 Using Parse
string value = "true";
bool b = bool.Parse(value);
Console.WriteLine(b); // displays True
LISTING 3-5 Using TryParse
string value = "1";
int result;
bool success = int.TryParse(value, out result);
if (success)
{
// value is a valid integer, result contains the value
}
else
{
// value is not a valid integer
}
LISTING 3-6 Using configuration options when parsing a number
CultureInfo english = new CultureInfo("En");
CultureInfo dutch = new CultureInfo("Nl");
string value = "€19,95";
decimal d = decimal.Parse(value, NumberStyles.Currency, dutch);
Console.WriteLine(d.ToString(english)); // Displays 19.95
When parsing a DateTime
, you must take into account things such as time zone differences and cultural differences, especially when working on an application that uses globalization.
MORE INFO ABOUT DateTime.Parse Method
The difference between Parse/TryParse
and Convert
is that Convert
enables null values. It doesn’t throw an ArgumentNullException
; instead, it returns the default value for the supplied type.
LISTING 3-7 Using Convert with a null value
int i = Convert.ToInt32(null);
Console.WriteLine(i); // Displays 0
A difference between Convert
and the Parse
methods is that Parse takes a string only as input, while Convert can also take other base types as input.
LISTING 3-8 Using Convert to convert from double to int
double d = 23.15;
int i = Convert.ToInt32(d);
Console.WriteLine(i); // Displays 23
When you are parsing user input, the best choice is the TryParse method.
Using regular expressions
A regular expression is a specific pattern used to parse and find matches in strings. It is sometimes called regex
or regexp
.
MORE INFO ABOUT Regular expressions examples
LISTING 3-9 Manually validating a ZIP Code
static bool ValidateZipCode(string zipCode)
{
// Valid zipcodes: 1234AB | 1234 AB | 1001 AB
if (zipCode.Length < 6) return false;
string numberPart = zipCode.Substring(0, 4);
int number;
if (!int.TryParse(numberPart, out number)) return false;
string characterPart = zipCode.Substring(4);
if (numberPart.StartsWith("0")) return false;
if (characterPart.Trim().Length < 2) return false;
if (characterPart.Length == 3 && characterPart.Trim().Length != 2)
return false;
return true;
}
LISTING 3-10 Validate a ZIP Code with a regular expression
static bool ValidateZipCodeRegEx(string zipCode)
{
Match match = Regex.Match(zipCode, @"^[1-9][0-9]{3}\s?[a-zA-Z]{2}$",
RegexOptions.IgnoreCase);
return match.Success;
}
LISTING 3-11 Validate a ZIP Code with a regular expression
RegexOptions options = RegexOptions.None;
Regex regex = new Regex(@"[ ]{2,}", options);
string input = "1 2 3 4 5";
string result = regex.Replace(input, " ");
Console.WriteLine(result); // Displays 1 2 3 4 5
In this example, every single space is allowed but multiple spaces are replaced with a single space.
Validating JSON and XML
LISTING 3-13 Deserializing an object with the JavaScriptSerializer
var serializer = new JavaScriptSerializer();
var result = serializer.Deserialize<Dictionary<string, object>>(json);
JavaScriptSerializer
is in the System.Web.Script.Serialization
namespace, in System.Web.Extensions
DLL.
You can generate an XSD file with Xsd.exe, which is a part of Visual Studio.
Xsd.exe person.xml
LISTING 3-16 Validating an XML file with a schema
public void ValidateXML()
{
string xsdPath = "person.xsd";
string xmlPath = "person.xml";
XmlReader reader = XmlReader.Create(xmlPath);
XmlDocument document = new XmlDocument();
document.Schemas.Add("", xsdPath);
document.Load(reader);
ValidationEventHandler eventHandler =
new ValidationEventHandler(ValidationEventHandler);
document.Validate(eventHandler);
}
static void ValidationEventHandler(object sender,
ValidationEventArgs e)
{
switch (e.Severity)
{
case XmlSeverityType.Error:
Console.WriteLine("Error: {0}", e.Message);
break;
case XmlSeverityType.Warning:
Console.WriteLine("Warning {0}", e.Message);
break;
}
}
Objective summary
- Validating application input is important to protect your application against both
mistakes and attacks. - Data integrity should be managed both by your application and your data store.
- The Parse, TryParse, and Convert functions can be used to convert between types.
- Regular expressions, or regex, can be used to match input against a specified pattern
or replace specified characters with other values. - When receiving JSON and XML files, it’s important to validate them using the built-in
types, such as with JavaScriptSerializer and XML Schemas.
Objective 3.2 Perform symmetric and asymmetric encryption
Using symmetric and asymmetric encryption
Cryptography
is about encrypting
and decrypting
data.
A symmetric algorithm
uses one single key to encrypt and decrypt the data. You need to pass
your original key to the receiver so he can decrypt your data.
An asymmetric algorithm
uses two different keys that are mathematically related to each other. One key is completely public and can be read and used by everyone. The other part is private and should never be shared with someone else.
Symmetric encryption
is faster than asymmetric encryption
and is well-suited for larger data sets. Asymmetric encryption
is not optimized for encrypting long messages, but it can be very useful for decrypting a small key. Combining these two techniques can help you transmit large amounts of data in an encrypted way.
Initialization vector(IV) is used to add some randomness to encrypting data. It makes sure that the same data results in a different encrypted message each time.
MORE INFO ABOUT .NET Framework Cryptography Model
Working with encryption in the .NET Framework
The .NET Framework has a managed implementation of the Advanced Encryption Standard (AES)
algorithm in the AesManaged
class. All cryptography classes can be found in the System.Security.Cryptography
class.
LISTING 3-17 Use a symmetric encryption algorithm
public static void EncryptSomeText()
{
string original = "My secret data!";
using (SymmetricAlgorithm symmetricAlgorithm =
new AesManaged())
{
byte[] encrypted = Encrypt(symmetricAlgorithm, original);
string roundtrip = Decrypt(symmetricAlgorithm, encrypted);
// Displays: My secret data!
Console.WriteLine("Original: {0}", original);
Console.WriteLine("Round Trip: {0}", roundtrip);
}
}
static byte[] Encrypt(SymmetricAlgorithm aesAlg, string plainText)
{
ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);
using (MemoryStream msEncrypt = new MemoryStream())
{
using (CryptoStream csEncrypt =
new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
{
using (StreamWriter swEncrypt = new StreamWriter(csEncrypt))
{
swEncrypt.Write(plainText);
}
return msEncrypt.ToArray();
}
}
}
static string Decrypt(SymmetricAlgorithm aesAlg, byte[] cipherText)
{
ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);
using (MemoryStream msDecrypt = new MemoryStream(cipherText))
{
using (CryptoStream csDecrypt =
new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
{
using (StreamReader srDecrypt = new StreamReader(csDecrypt))
{
return srDecrypt.ReadToEnd();
}
}
}
}
The SymmetricAlgorithm
class has both a method for creating an encryptor and a decryptor. By using the CryptoStream
class, you can encrypt or decrypt a byte sequence.
You can use the RSACryptoServiceProvider
and DSACryptoServiceProvider
classes for asymmetric encryption.
LISTING 3-18 Exporting a public key
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
string publicKeyXML = rsa.ToXmlString(false);
string privateKeyXML = rsa.ToXmlString(true);
Console.WriteLine(publicKeyXML);
Console.WriteLine(privateKeyXML);
// Displays:
//<RSAKeyValue>
// <Modulus>
// tYo35ywT0Q0KCNhFPu207bS8rrTk91YaxNcD2ElQ1eoWpdYnoCsdj1KaW/as9zFLYW5slg5Qq8ltdkxZuU
// fh0j2t+7ZFH8RRAD808GkZTrUi1zv3yqMjQDphHOcNfWh+dQrPmp1ShFxEGuA9Y4Ij9RINU5jcfviPa
// B1ClLXaGbc=
// </Modulus>
// <Exponent>AQAB</Exponent>
//</RSAKeyValue>
//<RSAKeyValue>
// <Modulus>
// tYo35ywT0Q0KCNhFPu207bS8rrTk91YaxNcD2ElQ1eoWpdYnoCsdj1KaW/as9zFLYW5slg5Qq8ltdkxZuU
// fh0j2t+7ZFH8RRAD808GkZTrUi1zv3yqMjQDphHOcNfWh+dQrPmp1ShFxEGuA9Y4Ij9RINU5jcfviPa
// B1ClLXaGbc=
// </Modulus>
// <Exponent>AQAB</Exponent>
// <P>
// 4uhNaN3cPSUzr+KxHmpKyeaD39RT+kWjjDcn/9sTAV/HmDzFzjsiov3KyJ+3XCXucx5TU0lhDOLc/
// cO+Xrquqw==
// </P>
// <Q>
// zNDVw6oL7YNglrFAeqmgIL3Oj2PkUxrWvoYHCbuFwJKpkWvFBRwZfKXHzzU0zaU5bGdX7M24hW8z5s0
// eF9CRJQ==
// </Q>
// <DP>
// jkS+/GhWxZPEw5vsF7jnaY3502ZqvPna4HhYwQgX832dRKueDn9vaSidc4sIyWMTDeTOs+LHUfAQRZ/
// shbKg/w==
// </DP>
// <DQ>
// HV4QWJboUO0Wi2Ts/umViTxOAudq1LOzeOwU1ENsITmmULCoNlxaFzJaHQ7e/GGlgzKqO80fmRph0c
// U1fGqudQ==
// </DQ>
// <InverseQ>
// BW1VUOgXpkRnn2twvb72uxcbK6+o9ns3xa4Ypm+++7vzlg6t/Iyvk94xNJWjjgR+XsSpN6JEtztWol8
// bv8HEyA==
// </InverseQ>
// <D>
// IOZUrUNyr+8iA2pWWkowAOhBTZQg7qYfIc8ptjfLO4k544IFGmTV7ZR1vvbcb8vyMk0Vxrf/bLKLcOX
// zWL2rMeWYGuoTbZEeUbr0SlmesHARL7X/feCm9MIyPjhlhJieRVG3h4f+TyAVo70jmYVcSou+xAaad3
// 7o3Pa8Vny6qIk=
// </D>
//</RSAKeyValue>
LISTING 3-19 Using a public and private key to encrypt and decrypt data
UnicodeEncoding ByteConverter = new UnicodeEncoding();
byte[] dataToEncrypt = ByteConverter.GetBytes("My Secret Data!");
byte[] encryptedData;
using (RSACryptoServiceProvider RSA = new RSACryptoServiceProvider())
{
RSA.FromXmlString(publicKeyXML);
encryptedData = RSA.Encrypt(dataToEncrypt, false);
}
byte[] decryptedData;
using (RSACryptoServiceProvider RSA = new RSACryptoServiceProvider())
{
RSA.FromXmlString(privateKeyXML);
decryptedData = RSA.Decrypt(encryptedData, false);
}
string decryptedString = ByteConverter.GetString(decryptedData);
Console.WriteLine(decryptedString); // Displays: My Secret Data!
The .NET Framework offers a secure location for storing asymmetric keys in a key container
.
LISTING 3-20 Using a key container for storing an asymmetric key
string containerName = "SecretContainer";
CspParameters csp = new CspParameters() { KeyContainerName = containerName };
byte[] encryptedData;
using (RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(csp))
{
encryptedData = RSA.Encrypt(dataToEncrypt, false);
}
Loading the key from the key container is the exact same process. You can securely store your asymmetric key without malicious users being able to read it.
Using hashing
LISTING 3-21 A naïve set implementation
class Set<T>
{
private List<T> list = new List<T>();
public void Insert(T item)
{
if (!Contains(item))
list.Add(item);
}
public bool Contains(T item)
{
foreach (T member in list)
if (member.Equals(item))
return true;
return false;
}
}
Hashing is the process of taking a large set of data and mapping it to a smaller data set of fixed length. For example, mapping all names to a specific integer. Instead of checking the complete name, you would have to use only an integer value.
LISTING 3-22 A set implementation that uses hashing
class Set<T>
{
private List<T>[] buckets = new List<T>[100];
public void Insert(T item)
{
int bucket = GetBucket(item.GetHashCode());
if (Contains(item, bucket))
return;
if (buckets[bucket] == null)
buckets[bucket] = new List<T>();
buckets[bucket].Add(item);
}
public bool Contains(T item)
{
return Contains(item, GetBucket(item.GetHashCode()));
}
private int GetBucket(int hashcode)
{
// A Hash code can be negative. To make sure that you end up with a positive
// value cast the value to an unsigned int. The unchecked block makes sure that
// you can cast a value larger than int to an int safely.
unchecked
{
return (int)((uint)hashcode % (uint)buckets.Length);
}
}
private bool Contains(T item, int bucket)
{
if (buckets[bucket] != null)
foreach (T member in buckets[bucket])
if (member.Equals(item))
return true;
return false;
}
}
As a general guideline, the distribution of hash codes must be as random as possible. This is why the set implementation uses the GetHashCode method on each object to calculate in which bucket it should go.
Equal items should have equal hash codes. Combined with the encryption technologies that the .NET Framework offers, hashing is an important technique to validate the authenticity of a message.
MORE INFO ABOUT Object.GetHashCode Method
LISTING 3-23 Using SHA256Managed to calculate a hash code
UnicodeEncoding byteConverter = new UnicodeEncoding();
SHA256 sha256 = SHA256.Create();
string data = "A paragraph of text";
byte[] hashA = sha256.ComputeHash(byteConverter.GetBytes(data));
data = "A paragraph of changed text";
byte[] hashB = sha256.ComputeHash(byteConverter.GetBytes(data));
data = "A paragraph of text";
byte[] hashC = sha256.ComputeHash(byteConverter.GetBytes(data));
Console.WriteLine(hashA.SequenceEqual(hashB)); // Displays: false
Console.WriteLine(hashA.SequenceEqual(hashC)); // Displays: true
Managing and creating certificates
Digital certificates
are the area where both hashing and asymmetric encryption come together.
EXAMPLE:
If Alice sends a message to Bob, she first hashes her message to generate a hash code. Alice then encrypts the hash code with her private key to create a personal signature. Bob receives Alice’s message and signature. He decrypts the signature using Alice’s public key and now he has both the message and the hash code. He can then hash the message and see whether his hash code and the hash code from Alice match.
When working on your development or testing environment, You can create certificates by using the Makecert.exe
tool. This tool generates X.509 certificates
for testing purposes. The X.509 certificate is a widely used standard for defining digital certificates.
makecert testCert.cer
This command generates a file called testCert.cer that you can use as a certificate. You first need to install this certificate on your computer to be able to use it. After installation, it’s stored in a certificate store. The following line creates a certificate and installs it in a custom certificate store named testCertStore:
makecert -n "CN=WouterDeKort" -sr currentuser -ss testCertStore
LISTING 3-24 Signing and verifying data with a certificate
public static void SignAndVerify()
{
string textToSign = "Test paragraph";
byte[] signature = Sign(textToSign, "cn=WouterDeKort");
// Uncomment this line to make the verification step fail
// signature[0] = 0;
Console.WriteLine(Verify(textToSign, signature));
}
static byte[] Sign(string text, string certSubject)
{
X509Certificate2 cert = GetCertificate();
var csp = (RSACryptoServiceProvider)cert.PrivateKey;
byte[] hash = HashData(text);
return csp.SignHash(hash, CryptoConfig.MapNameToOID("SHA1"));
}
static bool Verify(string text, byte[] signature)
{
X509Certificate2 cert = GetCertificate();
var csp = (RSACryptoServiceProvider)cert.PublicKey.Key;
byte[] hash = HashData(text);
return csp.VerifyHash(hash,
CryptoConfig.MapNameToOID("SHA1"),
signature);
}
private static byte[] HashData(string text)
{
HashAlgorithm hashAlgorithm = new SHA1Managed();
UnicodeEncoding encoding = new UnicodeEncoding();
byte[] data = encoding.GetBytes(text);
byte[] hash = hashAlgorithm .ComputeHash(data);
return hash;
}
private static X509Certificate2 GetCertificate()
{
X509Store my = new X509Store("testCertStore",
StoreLocation.CurrentUser);
my.Open(OpenFlags.ReadOnly);
var certificate = my.Certificates[0];
return certificate;
}
MakeCert is deprecated, use New-SelfSignedCertificate instead.
Using code access permissions
CAS, Code Access Security
When using CAS, you need to ask for permission to execute certain operations or access protected resources. CLR enforces security restrictions on managed code and makes sure that your code has the correct permissions to access privileged resources.
Applications that are installed on your computer or on your local intranet have full trust. When running in a sandboxed environment such as Internet Explorer or SQL Server, CAS restricts the operations that an application can execute.
CAS performs the following functions in the .NET Framework:
- Defines permissions for accessing system resources.
- Enables code to demand that its callers have specific permissions. For example, a library that exposes methods that create files should enforce that its callers have the right for file input/output.
- Enables code to demand that its callers possess a digital signature. This way, code can make sure that it’s only called by callers from a particular organization or location.
- Enforces all those restrictions at runtime.
The call stack
is a data structure that stores information about all the active methods at a specific moment.
CAS walks the call stack and sees whether every element on the stack has the required permissions. This way, you can be sure that a less-trusted method cannot call some restricted code through a highly trusted method.
The base class for all things related to CAS is System.Security.CodeAccessPermission
. Permissions that inherit from CodeAccessPermission are permissions such as FileIOPermission
, ReflectionPermission
, or SecurityPermission
. When applying one of those permissions, you ask the CLR for the permission to execute a protected operation or access a resource.
You can specify CAS in two ways: declarative or imperative.
Declarative means that you use attributes to apply security information to your code.
LISTING 3-25 Declarative CAS
[FileIOPermission(SecurityAction.Demand,
AllLocalFiles = FileIOPermissionAccess.Read)]
public void DeclarativeCAS()
{
// Method body
}
The imperative way means that you explicitly ask for the permission in the code.
LISTING 3-26 Imperative CAS
FileIOPermission f = new FileIOPermission(PermissionState.None);
f.AllLocalFiles = FileIOPermissionAccess.Read;
try
{
f.Demand();
}
catch (SecurityException s)
{
Console.WriteLine(s.Message);
}
MORE INFO ABOUT CODE ACCESS SECURITY
Securing string data
A SecureString
automatically encrypts its value, and also pinned to a specific memory location. It is a mutable string that can be made read-only when necessary. It implements IDisposable
.
A SecureString doesn’t completely solve all security problems. Because it needs to be initialized at some point, the data that is used to initialize the SecureString is still in memory. To minimize this risk and force you to think about it, SecureString can deal with only individual characters at a time. It’s not possible to pass a string directly to a SecureString.
LISTING 3-27 Initializing a SecureString
using (SecureString ss = new SecureString())
{
Console.Write("Please enter password: ");
while (true)
{
ConsoleKeyInfo cki = Console.ReadKey(true);
if (cki.Key == ConsoleKey.Enter) break;
ss.AppendChar(cki.KeyChar);
Console.Write("*");
}
ss.MakeReadOnly();
}
LISTING 3-28 Getting the value of a SecureString
public static void ConvertToUnsecureString(SecureString securePassword)
{
IntPtr unmanagedString = IntPtr.Zero;
try
{
unmanagedString = Marshal.SecureStringToGlobalAllocUnicode(securePassword);
Console.WriteLine(Marshal.PtrToStringUni(unmanagedString));
}
finally
{
Marshal.ZeroFreeGlobalAllocUnicode(unmanagedString);
}
}
The finally statement makes sure that the string is removed from memory even if there is an exception thrown in the code.
TABLE 3-1 Methods for working with SecureString
Decrypt method | Clear memory method |
---|---|
SecureStringToBSTR | ZeroFreeBSTR |
SecureStringToCoTaskMemAnsi | ZeroFreeCoTaskMemAnsi |
SecureStringToCoTaskMemUnicode | ZeroFreeCoTaskMemUnicode |
SecureStringToGlobalAllocAnsi | ZeroFreeGlobalAllocAnsi |
SecureStringToGlobalAllocUnicode | ZeroFreeGlobalAllocUnicode |
It’s important to realize that a SecureString is not completely secure.
Objective summary
- A symmetric algorithm uses the same key to encrypt and decrypt data.
- An asymmetric algorithm uses a public and private key that are mathematically linked.
- Hashing is the process of converting a large amount of data to a smaller hash code.
- Digital certificates can be used to verify the authenticity of an author.
- CAS are used to restrict the resources and operations an application can access and execute.
- System.Security.SecureString can be used to keep sensitive string data in memory.
Objective 3.3 Manage assemblies
Assemblies are completely self-contained, they contain all the information they need to run. This is called the assembly’s manifest
.
Assembly is language-neutral.
An assembly can be versioned, which enables you to have different versions of a specific assembly on one system without causing conflicts.
Signing assemblies using a strong name
A regular assembly is what Visual Studio generates for you by default.
A strong-named assembly is signed with a public/private key pair that uniquely identifies the publisher of the assembly and the content of the assembly.
Benefits of strong-named assembly:
- Strong names guarantee uniqueness
No other assembly can have the exact same strong name. - Strong names protect your versioning lineage
Users can be sure that the new version originates from the same publisher. - Strong names provide a strong integrity check
The .NET Framework sees whether a strong-named assembly has changed since the moment it was signed.
A strong-named assembly can reference only other assemblies that are also strongly named.
You can view the public key by using the Strong Name tool (Sn.exe) .
LISTING 3-29 Inspecting the public key of a signed assembly
C:\>sn -Tp C:\Windows\Microsoft.NET\Framework\v4.0.30319\System.D
ata.dll
Microsoft (R) .NET Framework Strong Name Utility Version 4.0.30319.17929
Copyright (c) Microsoft Corporation. All rights reserved.
Identity public key (hash algorithm: Unknown):
00000000000000000400000000000000
Signature public key (hash algorithm: sha256):
002400000c800000140100000602000000240000525341310008000001000100613399aff18ef1
a2c2514a273a42d9042b72321f1757102df9ebada69923e2738406c21e5b801552ab8d200a65a2
35e001ac9adc25f2d811eb09496a4c6a59d4619589c69f5baf0c4179a47311d92555cd006acc8b
5959f2bd6e10e360c34537a1d266da8085856583c85d81da7f3ec01ed9564c58d93d713cd0172c
8e23a10f0239b80c96b07736f5d8b022542a4e74251a5f432824318b3539a5a087f8e53d2f135f
9ca47f3bb2e10aff0af0849504fb7cea3ff192dc8de0edad64c68efde34c56d302ad55fd6e80f3
02d5efcdeae953658d3452561b5f36c542efdbdd9f888538d374cef106acf7d93a4445c3c73cd9
11f0571aaf3d54da12b11ddec375b3
Public key token is b77a5c561934e089
The public key token is a small string that represents the public key. It is generated by hashing the public key and taking the last eight bytes. If you reference another assembly, you store only the public key token, which preserves space in the assembly manifest. The CLR does not use the public key token when making security decisions because it could happen that several public keys have the same public key token.
You can use delayed signing
to avoid private key leak. When using delayed signing, you use only the public key to sign an assembly and you delay using the private key until the project is ready for deployment.
A strongly named assembly does not prove that the assembly comes from the original publisher. It only shows that the person who created the assembly has access to the private key. If you want to make sure that users can verify you as the publisher, you have to use something called Authenticode.
Putting an assembly in the GAC
GAC, Global Assembly Cache
Normally, you want to avoid installing assemblies in the GAC. One reason to deploy to the GAC is when an assembly is shared by multiple applications. Other reasons for installing an assembly into the GAC can be the enhanced security (normally only users with administrator rights can alter the GAC) or the situation where you want to deploy multiple versions of the same assembly.
Deploying an assembly in the GAC can be done in two ways:
- For production scenarios, use a specific installation program that has access to the GAC such as the Windows Installer 2.0.
- In development scenarios, use a tool called the Global Assembly Cache tool (Gacutil.exe).
View the content of your GAC by running the following command from a developer command prompt:
gacutil -l
Installing an assembly in the GAC can be done with the following command:
gacutil –i [assembly name]
Remove an assembly from the GAC:
gacutil –u [assembly name]
Versioning assemblies
AssemblyFileVersionAttribute
is the one that should be incremented with each build. This is not something you want to do on the client, where it would get incremented with every developer build. Instead, you should integrate this into your build process on your build server.
AssemblyVersionAttribute
should be incremented manually. This should be done when you plan to deploy a specific version to production.
You can deploy multiple versions of the same assembly to the GAC and avoid the DLL problem that happened with regular DLL files. This is called side-by-side hosting
, in which multiple versions of an assembly are hosted together on one computer.
These configuration files can be used to influence the binding of referenced assemblies:
- Application configuration files
- Publisher policy files
- Machine configuration files
LISTING 3-30 Redirecting assembly bindings to a newer version
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="myAssembly"
publicKeyToken="32ab4ba45e0a69a1"
culture="en-us" />
<!-- Redirecting to version 2.0.0.0 of the assembly. -->
<bindingRedirect oldVersion="1.0.0.0"
newVersion="2.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
MORE INFO ABOUT HOW THE RUNTIME LOCATES ASSEMBLIES
You can specify extra locations where the CLR should look in the configuration file of the application.
LISTING 3-31 Specifying additional locations for assemblies
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<probing privatePath="MyLibraries"/>
</assemblyBinding>
</runtime>
</configuration>
A codebase
element can specify a location for an assembly that is outside of the application’s directory. These assemblies have to be strongly named if they are not in the current application’s folder.
LISTING 3-32 Specifying additional locations for assemblies
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<codeBase version="1.0.0.0"
href= "http://www.mydomain.com/ReferencedAssembly.dll"/>
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
Creating a WinMD assembly
WinMD, Windows Metadata
WinMD files can contain both code and metadata.
Objective summary
- An assembly is a compiled unit of code that contains metadata.
- An assembly can be strongly signed to make sure that no one can tamper with the content.
- Signed assemblies can be put in the GAC.
- An assembly can be versioned, and applications will use the assembly version they were developed with. It’s possible to use configuration files to change these bindings.
- A WinMD assembly is a special type of assembly that is used by WinRT to map nonnative languages to the native WinRT components.
Objective 3.4 Debug an application
Build configurations
In release mode
, the compiled code is fully optimized, and no extra information for debugging purposes is created.
In debug mode
, there is no optimization applied, and additional information is outputted.
Example demonstrate the difference between a debug and a release build:
LISTING 3-33 A simple console application
using System;
using System.Threading;
public static class Program
{
public static void Main()
{
Timer t = new Timer(TimerCallback, null, 0, 2000);
Console.ReadLine();
}
private static void TimerCallback(Object o)
{
Console.WriteLine("In TimerCallback: " + DateTime.Now);
GC.Collect();
}
}
When you run this application in debug mode, it does a nice job of outputting the time every 2 seconds and keeps on doing this until you terminate the application. But when you execute this application in release mode, it outputs the current date and time only once. The compiler optimizes the code. In this scenario, it sees that the Timer object is not used anymore, so it’s no longer considered a root object and the garbage collector removes it from memory.
In debug configuration, the compiler inserts extra no-operation (NOP) instructions
and branch instructions
. NOP instructions are instructions that effectively don’t do anything (for example, an assignment to a variable that’s never used). A branch instruction is a piece of code that is executed conditionally (for example, when some variable is true or false).
MORE INFO ABOUT DEBUGGER BASICS
Creating and managing compiler directives
LISTING 3-34 Checking for the debug symbol
public void DebugDirective()
{
#if DEBUG
Console.WriteLine("Debug mode");
#else
Console.WriteLine("Not debug");
#endif
}
When using the #if directive, you can use the operators you are used to from C#: ==
(equality), !=
(inequality), &&
(and), ||
(or) and !
(not) to test for true or false.
LISTING 3-35 Defining a custom symbol
#define MySymbol
// …
public void UseCustomSymbol()
{
#if MySymbol
Console.WriteLine("Custom symbol is defined");
#endif
}
Using directives this way can make your code harder to understand, and you should try to avoid them if possible. A scenario in which using preprocessor directives can be necessary is when you are building a library that targets multiple platforms.
LISTING 3-36 Using preprocessor directives to target multiple platforms
public Assembly LoadAssembly<T>()
{
#if !WINRT
Assembly assembly = typeof(T).Assembly;
#else
Assembly assembly = typeof(T).GetTypeInfo().Assembly;
#endif
return assembly;
}
#undef
can be used to remove the definition of a symbol. This can be used in a situation where you want to debug a piece of code that’s normally included only in a release build. You can then use the #undef directive to remove the debug symbol.
You can use #warning
and #error
to report an error or warning to the compiler.
LISTING 3-37 The warning and error directives
#warning This code is obsolete
#if DEBUG
#error Debug build is not allowed
#endif
The #line
directive can be used to modify the compiler’s line number and even the name of the file. You can also hide a line of code from the debugger.
LISTING 3-38 The line directive
#line 200 "OtherFileName"
int a; // line 200
#line default
int b; // line 4
#line hidden
int c; // hidden
int d; // line 7
LISTING 3-39 The pragma warning directive
#pragma warning disable
while (false)
{
Console.WriteLine("Unreachable code");
}
#pragma warning restore
LISTING 3-40 Disabling and enabling specific warnings
#pragma warning disable 0162, 0168
int i;
#pragma warning restore 0162
while (false)
{
Console.WriteLine("Unreachable code");
}
#pragma warning restore
LISTING 3-41 Call a method only in a debug build
public void SomeMethod()
{
#if DEBUG
Log("Step1");
#endif
}
private static void Log(string message)
{
Console.WriteLine("message");
}
LISTING 3-42 Applying the ConditionalAttribute
[Conditional("DEBUG")]
private static void Log(string message)
{
Console.WriteLine("message");
}
LISTING 3-43 Applying the DebuggerDisplayAttribute
[DebuggerDisplay("Name = {FirstName} {LastName")]
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
Managing program database files and symbols
A program database (PDB) file, which is an extra data source that annotates your application’s code with additional information that can be useful during debugging.
/debug:full
vs /debug:pdbonly
:
When you specify the full
flag, a PDB file is created, and the specific assembly has debug information. With the pdbonly
flag, the generated assembly is not modified, and only the PDB file is generated. pdbonly
is recommended in release build.
The important thing is that this GUID is created at compile time, so if you recompile your application, you get a new PDB file that matches your recompiled build exactly.
Objective summary
- Visual Studio build configurations can be used to configure the compiler.
- A debug build outputs a nonoptimized version of the code that contains extra instructions to help debugging.
- A release build outputs optimized code that can be deployed to a production environment.
- Compiler directives can be used to give extra instructions to the compiler. You can use them, for example, to include code only in certain build configurations or to suppress certain warnings.
- A program database (PDB) file contains extra information that is required when debugging an application.
Objective 3.5 Implement diagnostics in an application
Logging and tracing
Tracing
is a way for you to monitor the execution of your application while it’s running.
Logging
is always enabled and is used for error reporting.
LISTING 3-45 Using the Debug class
Debug.WriteLine("Starting application");
Debug.Indent();
int i = 1 + 2;
Debug.Assert(i == 3);
Debug.WriteLineIf(i > 0, "i is greater than 0");
LISTING 3-46 Using the TraceSource class
TraceSource traceSource = new TraceSource("myTraceSource",
SourceLevels.All);
traceSource.TraceInformation("Tracing application..");
traceSource.TraceEvent(TraceEventType.Critical, 0, "Critical trace");
traceSource.TraceData(TraceEventType.Information, 1,
new object[] { "a", "b", "c" });
traceSource.Flush();
traceSource.Close();
// Outputs:
// myTraceSource Information: 0 : Tracing application..
// myTraceSource Critical: 0 : Critical trace
// myTraceSource Information: 1 : a, b, c
Options for the TraceEventType
enum:
- Critical
This is the most severe option. It should be used sparingly and only for very serious and irrecoverable errors. - Error
This enum member has a slightly lower priority than Critical, but it still indicates that something is wrong in the application. It should typically be used to flag a problem that has been handled or recovered from. - Warning
This value indicates something unusual has occurred that may be worth
investigating further. For example, you notice that a certain operation suddenly takes longer to process than normal or you flag a warning that the server is getting low on memory. - Information
This value indicates that the process is executing correctly, but there is some interesting information to include in the tracing output file. It may be information that a user has logged onto a system or that something has been added to the database. - Verbose
This is the loosest of all the severity related values in the enum. It should be
used for information that is not indicating anything wrong with the application and is likely to appear in vast quantities. For example, when instrumenting all methods in a type to trace their beginning and ending, it is typical to use the verbose event type. - Stop, Start, Suspend, Resume, Transfer
These event types are not indications of severity, but mark the trace event as relating to the logical flow of the application. They are known asactivity event types
and mark a logical operation’s starting or stopping, or transferring control to another logical operation.
Both the Debug
and TraceSource
classes have a Listeners
property. This property holds a collection of TraceListeners
, which process the information from the Write
, Fail
, and Trace
methods.
Both the Debug
and the TraceSource
class use an instance of the DefaultTraceListener
class. The DefaultTraceListener writes to the Output window and shows the message box when assertion fails.
TABLE 3-2 TraceListeners in the .NET Framework
Name | Output |
---|---|
ConsoleTraceListener | Standard output or error stream |
DelimitedListTraceListener | TextWriter |
EventLogTraceListener | EventLog |
EventSchemaTraceListener | XML-encoded, schema-compliant log file |
TextWriterTraceListener | TextWriter |
XmlWriterTraceListener | XML-encoded data to a TextWriter or stream |
If you don’t want the DefaultTraceListener
to be active, you need to clear the current listeners collection. You can add as many listeners as you want.
LISTING 3-47 Configuring TraceListener.
Stream outputFile = File.Create("tracefile.txt");
TextWriterTraceListener textListener =
new TextWriterTraceListener(outputFile);
TraceSource traceSource = new TraceSource("myTraceSource",
SourceLevels.All);
traceSource.Listeners.Clear();
traceSource.Listeners.Add(textListener);
traceSource.TraceInformation("Trace output");
traceSource.Flush();
traceSource.Close();
Instead of configuring the listeners through code, you can also use a configuration file.
LISTING 3-48 Using a configuration file for tracing
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.diagnostics>
<sources>
<source name="myTraceSource" switchName="defaultSwitch">
<listeners>
<add initializeData="output.txt"
type="System.Diagnostics.TextWriterTraceListeer"
name="myLocalListener">
<filter type="System.Diagnostics.EventTypeFilter"
initializeData="Warning"/>
</add>
<add name="consoleListener" />
<remove name="Default"/>
</listeners>
</source>
</sources>
<sharedListeners>
<add initializeData="output.xml" type="System.Diagnostics.XmlWriterTraceListener"
name="xmlListener" traceOutputOptions="None" />
<add type="System.Diagnostics.ConsoleTraceListener" name="consoleListener"
traceOutputOptions="None" />
</sharedListeners>
<switches>
<add name="defaultSwitch" value="All" />
</switches>
</system.diagnostics>
</configuration>
The configuration file also defines a switch
, which is used by a trace source to determine whether it should do something with a trace message it receives. This way, you can determine which trace messages you want to see. Lowering the number of messages enhances performance and will result in a smaller output file. After you have found the particular area that you want to focus on, you can set your switch to a more detailed level.
A filter
is applied to an individual listener. When you have multiple listeners for one single trace source, you can use filters to determine which trace events are actually processed by the listener. You could have a listener that sends text messages only for the critical events in a trace source, for example.
LISTING 3-49 Writing data to the event log
using System;
using System.Diagnostics;
class MySample
{
public static void Main()
{
if (!EventLog.SourceExists("MySource"))
{
EventLog.CreateEventSource("MySource", "MyNewLog");
Console.WriteLine("CreatedEventSource");
Console.WriteLine("Please restart application");
Console.ReadKey();
return;
}
EventLog myLog = new EventLog();
myLog.Source = "MySource";
myLog.WriteEntry("Log event!");
}
}
These messages can then be viewed by the Windows Event Viewer.
You can also read programmatically from the event log.
LISTING 3-50 Reading data from the event log
EventLog log = new EventLog("MyNewLog");
Console.WriteLine("Total entries: " + log.Entries.Count);
EventLogEntry last = log.Entries[log.Entries.Count - 1];
Console.WriteLine("Index: " + last.Index);
Console.WriteLine("Source: " + last.Source);
Console.WriteLine("Type: " + last.EntryType);
Console.WriteLine("Time: " + last.TimeWritten);
Console.WriteLine("Message: " + last.Message);
The EventLog also gives you the option to subscribe to changes in the event log.
LISTING 3-51 Writing data to the event log
using System;
using System.Diagnostics;
class EventLogSample
{
public static void Main()
{
EventLog applicationLog = new EventLog("Application", ".", "testEventLogEvent");
applicationLog.EntryWritten += (sender, e) =>
{
Console.WriteLine(e.Entry.Message);
};
applicationLog.EnableRaisingEvents = true;
applicationLog.WriteEntry("Test message", EventLogEntryType.Information);
Console.ReadKey();
}
}
Profiling your application
Profiling
is the process of determining how your application uses certain resources.
With performance, one thing is always true: Don’t get into premature optimizations.
A guideline is to write your code as easy and maintainable as possible. When you run into performance problems, you can use a profiler to actually measure which part of your application is causing problems.
A simple way of measuring the execution time of some code is by using the Stopwatch
class.
LISTING 3-52 Using the StopWatch class
using System;
using System.Diagnostics;
using System.Text;
namespace Profiling
{
class Program
{
const int numberOfIterations = 100000;
static void Main(string[] args)
{
Stopwatch sw = new Stopwatch();
sw.Start();
Algorithm1();
sw.Stop();
Console.WriteLine(sw.Elapsed);
sw.Reset();
sw.Start();
Algorithm2();
sw.Stop();
Console.WriteLine(sw.Elapsed);
Console.WriteLine("Ready…");
Console.ReadLine(); }
private static void Algorithm2()
{
string result = "";
for (int x = 0; x < numberOfIterations; x++)
{
result += ‘a’;
}
}
private static void Algorithm1()
{
StringBuilder sb = new StringBuilder();
for (int x = 0; x < numberOfIterations; x++)
{
sb.Append(‘a’);
}
string result = sb.ToString();
}
}
}
// Displays
// 00:00:00.0007635
// 00:00:01.4071420
MORE INFO ABOUT PERFORMANCE TOOLS
Creating and monitoring performance counters
LISTING 3-53 Reading data from a performance counter
using System;
using System.Diagnostics;
namespace PerformanceCounters
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Press escape key to stop");
using (PerformanceCounter pc =
new PerformanceCounter("Memory", "Available Bytes"))
{
string text = "Available memory: ";
Console.Write(text);
do
{
while (!Console.KeyAvailable)
{
Console.Write(pc.RawValue);
Console.SetCursorPosition(text.Length, Console.CursorTop);
}
} while (Console.ReadKey(true).Key != ConsoleKey.Escape);
}
}
}
}
All performance counters are part of a category, and within that category they have a unique name. To access the performance counters, your application has to run under full trust, or the account that it’s running under should be an administrator or be a part of the Performance Monitor Users group.
LISTING 3-54 Reading data from a performance counter
using System;
using System.Diagnostics;
namespace PerformanceCounters
{
class Program
{
static void Main(string[] args)
{
if (CreatePerformanceCounters())
{
Console.WriteLine("Created performance counters");
Console.WriteLine("Please restart application");
Console.ReadKey();
return;
}
var totalOperationsCounter = new PerformanceCounter(
"MyCategory",
"# operations executed",
"",
false);
var operationsPerSecondCounter = new PerformanceCounter(
"MyCategory",
"# operations / sec",
"",
false);
totalOperationsCounter.Increment();
operationsPerSecondCounter.Increment();
}
private static bool CreatePerformanceCounters()
{
if (!PerformanceCounterCategory.Exists("MyCategory"))
{
CounterCreationDataCollection counters =
new CounterCreationDataCollection
{
new CounterCreationData(
"# operations executed",
"Total number of operations executed",
PerformanceCounterType.NumberOfItems32),
new CounterCreationData(
"# operations / sec",
"Number of operations executed per second",
PerformanceCounterType.RateOfCountsPerSecond32)
};
PerformanceCounterCategory.Create("MyCategory",
"Sample category for Codeproject", counters);
return true;
}
return false;
}
}
}
Useful types of performance counters:
-
NumberOfItems32/NumberOfItems64
These types can be used for counting the number of operations or items.NumberOfItems64
is the same asNumberOfItems32
, except that it uses a larger field to accommodate for larger values. -
RateOfCountsPerSecond32/RateOfCountsPerSecond64
These types can be used to calculate the amount per second of an item or operation.RateOfCountsPerSecond64
is the same asRateOfCountsPerSecond32
, except that it uses larger fields to accommodate for larger values. -
AvergateTimer32
Calculates the average time to perform a process or process an item.
Objective summary
- Logging and tracing are important to monitor an application that is in production and should be implemented right from the start.
- You can use the Debug and TraceSource classes to log and trace messages. By configuring different listeners, you can configure your application to know which data to send where.
- When you are experiencing performance problems, you can profile your application to find the root cause and fix it.
- Performance counters can be used to constantly monitor the health of your applications.
Chapter summary
- Validating application input is important to ensure the stability and security of your application. You can use the Parse, TryParse, and Convert functions to parse user input. Regular Expressions can be used for matching patterns.
- Cryptography uses symmetric and asymmetric algorithms together with hashing to secure data.
- Code access permissions can be used to restrict the types of operations a program may execute.
- An assembly is a self-contained unit that contains application code and metadata. An assembly can be signed, versioned, and shared by putting it in the GAC.
- By selecting the correct build configurations, you can output additional information to create program database files that can be used to debug an application.
- By using logging, tracing, and performance counters, you can monitor an application while it’s in production.
网友评论