美文网首页
70-483.C3O1-5.Debug applications

70-483.C3O1-5.Debug applications

作者: 小镭Ra | 来源:发表于2018-10-29 12:09 被阅读11次

Note Links

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.

MORE INFO ABOUT USING PDBCOPY

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 as activity 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 as NumberOfItems32, 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 as RateOfCountsPerSecond32, 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.

Note Links

相关文章

  • 70-483.C3O1-5.Debug applications

    Note Links Objective 3.1: Validate application input Mana...

  • GPUImage翻译六:Demo代码

    Sample applications Several sample applications are bundl...

  • Mac 中同一个程序打开多个的方法

    open -n /Applications/xxxxx.app 例如:open -n /Applications/...

  • Shader溶解

    [官网基础](file:///Volumes/Applications/Applications/Unity5.3...

  • 升级xcode11后上传ipa文件失败,xcrun altool

    错误提示:/Applications/Xcode.app/Contents/Applications/Applic...

  • Applications

    1.将本章所解释的方法应用于以下两个问题。一定要选择你感兴趣的问题,因为后续章节中的应用将建立在这个问题上。 a....

  • Applications

    1.根据本章中提出的问题来审视自己。不要为你已经知道的事情而沉沦。相反,试着扩大你的自我意识。不要忽视你不太有利的...

  • Applications

    5仔细阅读以下各句子。然后解决什么问题。如果有,一个好的批判性思维者会找到它合适的问。 a电视新闻大肆渲染处理战争...

  • Applications

    1、假设确定了某个有特定论证的人是以自我为中心或以族群为中心的人。这个确认是否构成你否定这个论证的充足理由?为什么...

  • Applications

    1、许多年前,一个专家在思考上做了研究:“受过训练思考者的主要特征大概就是,不会在没有充足的证据下,就直接跳到结论...

网友评论

      本文标题:70-483.C3O1-5.Debug applications

      本文链接:https://www.haomeiwen.com/subject/xnsltqtx.html