Click here to Skip to main content
15,897,371 members
Articles / Web Development / CSS

Building a 3-Tier App with Silverlight 3, .NET RIA Services, and Azure Table Storage

Rate me:
Please Sign up or sign in to vote.
4.89/5 (28 votes)
11 Jul 2009CDDL19 min read 166K   1.7K   148  
This article presents the techniques and caveats of building a 3-tire Azure hosted application using Silverlight 3 (presentation tier), .NET RIA services (business logic and data access), and Windows Azure Table (data storage).
//
// <copyright file="StorageAccountInfo.cs" company="Microsoft">
//     Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
//
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Globalization;
using System.Configuration;
using System.Collections.Specialized;
using System.Diagnostics;
using Microsoft.ServiceHosting.ServiceRuntime;

namespace Microsoft.Azure.StorageClient
{
    /// <summary>
    /// Objects of this class encapsulate information about a storage account and endpoint configuration. 
    /// Associated with a storage account is the account name, the base URI of the account and a shared key. 
    /// </summary>
    public class StorageAccountInfo
    {

        /// <summary>
        /// The default configuration string in configuration files for setting the queue storage endpoint.
        /// </summary>
        public static readonly string DefaultQueueStorageEndpointConfigurationString = "QueueStorageEndpoint";

        /// <summary>
        /// The default configuration string in configuration files for setting the blob storage endpoint.
        /// </summary>
        public static readonly string DefaultBlobStorageEndpointConfigurationString = "BlobStorageEndpoint";

        /// <summary>
        /// The default configuration string in configuration files for setting the table storage endpoint.
        /// </summary>
        public static readonly string DefaultTableStorageEndpointConfigurationString = "TableStorageEndpoint";

        /// <summary>
        /// The default configuration string in configuration files for setting the storage account name.
        /// </summary>
        public static readonly string DefaultAccountNameConfigurationString = "AccountName";

        /// <summary>
        /// The default configuration string in configuration files for setting the shared key associated with a storage account.
        /// </summary>
        public static readonly string DefaultAccountSharedKeyConfigurationString = "AccountSharedKey";

        /// <summary>
        /// The default configuration string in configuration files for setting the UsePathStyleUris option.
        /// </summary>
        public static readonly string DefaultUsePathStyleUrisConfigurationString = "UsePathStyleUris";

        /// <summary>
        /// The default prefix string in application config and Web.config files to indicate that this setting should be looked up 
        /// in the fabric's configuration system.
        /// </summary>
        public static readonly string CSConfigStringPrefix = "CSConfigName";

        private bool? _usePathStyleUris;

        /// <summary>
        /// Constructor for creating account info objects.
        /// </summary>
        /// <param name="baseUri">The account's base URI.</param>
        /// <param name="usePathStyleUris">If true, path-style URIs (http://baseuri/accountname/containername/objectname) are used.
        /// If false host-style URIs (http://accountname.baseuri/containername/objectname) are used,
        /// where baseuri is the URI of the service..
        /// If null, the choice is made automatically: path-style URIs if host name part of base URI is an 
        /// IP addres, host-style otherwise.</param>
        /// <param name="accountName">The account name.</param>
        /// <param name="base64Key">The account's shared key.</param>
        public StorageAccountInfo(Uri baseUri, bool? usePathStyleUris, string accountName, string base64Key)
            : this(baseUri, usePathStyleUris, accountName, base64Key, false)
        { 
        }

        /// <summary>
        /// Constructor for creating account info objects.
        /// </summary>
        /// <param name="baseUri">The account's base URI.</param>
        /// <param name="usePathStyleUris">If true, path-style URIs (http://baseuri/accountname/containername/objectname) are used.
        /// If false host-style URIs (http://accountname.baseuri/containername/objectname) are used,
        /// where baseuri is the URI of the service.
        /// If null, the choice is made automatically: path-style URIs if host name part of base URI is an 
        /// IP addres, host-style otherwise.</param>
        /// <param name="accountName">The account name.</param>
        /// <param name="base64Key">The account's shared key.</param>
        /// <param name="allowIncompleteSettings">true if it shall be allowed to only set parts of the StorageAccountInfo properties.</param>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase")]
        public StorageAccountInfo(Uri baseUri, bool? usePathStyleUris, string accountName, string base64Key, bool allowIncompleteSettings)
        {
            if (baseUri == null && !allowIncompleteSettings)
            {
                throw new ArgumentNullException("baseUri");
            }
            if (string.IsNullOrEmpty(base64Key) && !allowIncompleteSettings)
            {
                throw new ArgumentNullException("base64Key");
            }
            if (baseUri != null) 
            {
                string newAccountName = null;
                Uri newBaseUri = null;
                if (IsStandardStorageEndpoint(baseUri, out newAccountName, out newBaseUri)) {
                    if (!string.IsNullOrEmpty(newAccountName) && 
                        !string.IsNullOrEmpty(accountName) &&
                        string.Compare(accountName, newAccountName, StringComparison.Ordinal) != 0)
                    {
                        throw new ArgumentException("The configured base URI " + baseUri.AbsoluteUri + " for the storage service is incorrect. " + 
                                                    "The configured account name " + accountName + " must match the account name " + newAccountName +
                                                    " as specified in the storage service base URI." +
                                                     GeneralAccountConfigurationExceptionString);
                    }
                    Debug.Assert((newBaseUri == null && newAccountName == null) || (newBaseUri != null && newAccountName != null));
                    if (newAccountName != null && newBaseUri != null) {
                        accountName = newAccountName;
                        baseUri = newBaseUri;
                    }
                }
            }
            if (string.IsNullOrEmpty(accountName) && !allowIncompleteSettings)
            {
                throw new ArgumentNullException("accountName");
            }
            if (!string.IsNullOrEmpty(accountName) && accountName.ToLowerInvariant() != accountName) {
                throw new ArgumentException("The account name must not contain upper-case letters. " + 
                                "The account name is the first part of the URL for accessing the storage services as presented to you by the portal or " +
                                "the predefined storage account name when using the development storage tool. " + 
                                GeneralAccountConfigurationExceptionString);
            }

            BaseUri = baseUri;
            AccountName = accountName;
            Base64Key = base64Key;
            if (usePathStyleUris == null && baseUri == null && !allowIncompleteSettings) {
                throw new ArgumentException("Cannot determine setting from empty URI.");
            }
            else if (usePathStyleUris == null)
            {
                if (baseUri != null)
                {
                    _usePathStyleUris = Utilities.StringIsIPAddress(baseUri.Host);
                }
                else
                {
                    _usePathStyleUris = null;
                }
            }
            else
            {
                UsePathStyleUris = usePathStyleUris.Value;
            }
        }

        /// <summary>
        /// The base URI of the account.
        /// </summary>
        public Uri BaseUri
        {
            get;
            set;
        }

        /// <summary>
        /// The account name.
        /// </summary>
        public string AccountName
        {
            get;
            set;
        }

        /// <summary>
        /// The account's key.
        /// </summary>
        public string Base64Key
        {
            get;
            set;
        }

        /// <summary>
        /// If set, returns the UsePathStyleUris properties. If the property has not been explicitly set, 
        /// the implementation tries to derive the correct value from the base URI.
        /// </summary>
        public bool UsePathStyleUris
        {
            get
            {
                if (_usePathStyleUris == null)
                {
                    if (BaseUri == null)
                    {
                        return false;
                    }
                    else
                    {
                        return Utilities.StringIsIPAddress(BaseUri.Host);
                    }
                }
                else
                {
                    return _usePathStyleUris.Value;
                }
            }
            set {
                _usePathStyleUris = value;
            }
        }

        /// <summary>
        /// Retrieves account settings for the queue service from the default settings. 
        /// </summary>
        public static StorageAccountInfo GetDefaultQueueStorageAccountFromConfiguration(bool allowIncompleteSettings)
        {
            return GetAccountInfoFromConfiguration(DefaultQueueStorageEndpointConfigurationString, allowIncompleteSettings);
        }

        /// <summary>
        /// Retrieves account settings for the queue service from the default settings. 
        /// Throws an exception in case of incomplete settings.
        /// </summary>
        public static StorageAccountInfo GetDefaultQueueStorageAccountFromConfiguration()
        {
            return GetAccountInfoFromConfiguration(DefaultQueueStorageEndpointConfigurationString, false);
        }

        /// <summary>
        /// Retrieves account settings for the table service from the default settings. 
        /// </summary>
        public static StorageAccountInfo GetDefaultTableStorageAccountFromConfiguration(bool allowIncompleteSettings)
        {
            return GetAccountInfoFromConfiguration(DefaultTableStorageEndpointConfigurationString, allowIncompleteSettings);
        }

        /// <summary>
        /// Retrieves account settings for the table service from the default settings. 
        /// Throws an exception in case of incomplete settings.
        /// </summary>
        public static StorageAccountInfo GetDefaultTableStorageAccountFromConfiguration()
        {
            return GetAccountInfoFromConfiguration(DefaultTableStorageEndpointConfigurationString, false);
        }

        /// <summary>
        /// Retrieves account settings for the blob service from the default settings. 
        /// </summary>
        public static StorageAccountInfo GetDefaultBlobStorageAccountFromConfiguration(bool allowIncompleteSettings)
        {
            return GetAccountInfoFromConfiguration(DefaultBlobStorageEndpointConfigurationString, allowIncompleteSettings);
        }

        /// <summary>
        /// Retrieves account settings for the blob service from the default settings. 
        /// Throws an exception in case of incomplete settings.
        /// </summary>
        public static StorageAccountInfo GetDefaultBlobStorageAccountFromConfiguration()
        {
            return GetAccountInfoFromConfiguration(DefaultBlobStorageEndpointConfigurationString, false);
        }

        /// <summary>
        /// Gets settings from default configuration names except for the endpoint configuration string.
        /// </summary>
        public static StorageAccountInfo GetAccountInfoFromConfiguration(string endpointConfiguration, bool allowIncompleteSettings) 
        {
            return GetAccountInfoFromConfiguration(DefaultAccountNameConfigurationString, 
                                       DefaultAccountSharedKeyConfigurationString,
                                       endpointConfiguration, 
                                       DefaultUsePathStyleUrisConfigurationString,
                                       allowIncompleteSettings);
        }

        /// <summary>
        /// Gets settings from default configuration names except for the endpoint configuration string. Throws an exception 
        /// in the case of incomplete settings.
        /// </summary>
        public static StorageAccountInfo GetAccountInfoFromConfiguration(string endpointConfiguration)
        {
            return GetAccountInfoFromConfiguration(DefaultAccountNameConfigurationString,
                                       DefaultAccountSharedKeyConfigurationString,
                                       endpointConfiguration,
                                       DefaultUsePathStyleUrisConfigurationString,
                                       false);
        }

        /// <summary>
        /// Gets a configuration setting from application settings in the Web.config or App.config file. 
        /// When running in a hosted environment, configuration settings are read from .cscfg
        /// files.
        /// </summary>
        public static string GetConfigurationSetting(string configurationSetting, string defaultValue, bool throwIfNull)
        {
            if (string.IsNullOrEmpty(configurationSetting))
            {
                throw new ArgumentException("configurationSetting cannot be empty or null", "configurationSetting");
            }

            string ret = null;

            // first, try to read from appsettings
            ret = TryGetAppSetting(configurationSetting);

            // settings in the csc file overload settings in Web.config
            if (RoleManager.IsRoleManagerRunning)
            {
                string cscRet = TryGetConfigurationSetting(configurationSetting);
                if (!string.IsNullOrEmpty(cscRet))
                {
                    ret = cscRet;
                }

                // if there is a csc config name in the app settings, this config name even overloads the 
                // setting we have right now
                string refWebRet = TryGetAppSetting(StorageAccountInfo.CSConfigStringPrefix + configurationSetting);
                if (!string.IsNullOrEmpty(refWebRet))
                {
                    cscRet = TryGetConfigurationSetting(refWebRet);
                    if (!string.IsNullOrEmpty(cscRet))
                    {
                        ret = cscRet;
                    }
                }
            }

            // if we could not retrieve any configuration string set return value to the default value
            if (string.IsNullOrEmpty(ret) && defaultValue != null)
            {
                ret = defaultValue;
            }

            if (string.IsNullOrEmpty(ret) && throwIfNull)
            {
                throw new ConfigurationErrorsException(
                    string.Format(CultureInfo.InvariantCulture, "Cannot find configuration string {0}.", configurationSetting));
            }
            return ret;
        }

        /// <summary>
        /// Retrieves account information settings from configuration settings. First, the implementation checks for 
        /// settings in an application config section of an app.config or Web.config file. These values are overwritten 
        /// if the same settings appear in a .csdef file.
        /// The implementation also supports indirect settings. In this case, indirect settings overwrite all other settings.
        /// </summary>        
        /// <param name="accountNameConfiguration">Configuration string for the account name.</param>
        /// <param name="accountSharedKeyConfiguration">Configuration string for the key.</param>
        /// <param name="endpointConfiguration">Configuration string for the endpoint.</param>
        /// <param name="usePathStyleUrisConfiguration">Configuration string for the path style.</param>
        /// <param name="allowIncompleteSettings">If false, an exception is thrown if not all settings are available.</param>
        /// <returns>StorageAccountInfo object containing the retrieved settings.</returns>        
        public static StorageAccountInfo GetAccountInfoFromConfiguration(string accountNameConfiguration,
                                                                         string accountSharedKeyConfiguration,
                                                                         string endpointConfiguration, 
                                                                         string usePathStyleUrisConfiguration,
                                                                         bool allowIncompleteSettings)
        {
            if (string.IsNullOrEmpty(endpointConfiguration))
            {
                throw new ArgumentException("Endpoint configuration is missing", "endpointConfiguration");
            }
            string endpoint = null;
            string name = null;
            string key = null;
            string pathStyle = null;

            name = TryGetAppSetting(accountNameConfiguration);
            key = TryGetAppSetting(accountSharedKeyConfiguration);
            endpoint = TryGetAppSetting(endpointConfiguration);
            pathStyle = TryGetAppSetting(usePathStyleUrisConfiguration);


            // settings in the csc file overload settings in Web.config
            if (RoleManager.IsRoleManagerRunning)
            {
                // get config settings from the csc file
                string cscName = TryGetConfigurationSetting(accountNameConfiguration);
                if (!string.IsNullOrEmpty(cscName))
                {
                    name = cscName;
                }
                string cscKey = TryGetConfigurationSetting(accountSharedKeyConfiguration);
                if (!string.IsNullOrEmpty(cscKey))
                {
                    key = cscKey;
                }
                string cscEndpoint = TryGetConfigurationSetting(endpointConfiguration);
                if (!string.IsNullOrEmpty(cscEndpoint))
                {
                    endpoint = cscEndpoint;
                }
                string cscPathStyle = TryGetConfigurationSetting(usePathStyleUrisConfiguration);
                if (!string.IsNullOrEmpty(cscPathStyle))
                {
                    pathStyle = cscPathStyle;
                }

                // the Web.config can have references to csc setting strings
                // these count event stronger than the direct settings in the csc file
                string refWebName = TryGetAppSetting(CSConfigStringPrefix + accountNameConfiguration);
                if (!string.IsNullOrEmpty(refWebName))
                {
                    cscName = TryGetConfigurationSetting(refWebName);
                    if (!string.IsNullOrEmpty(cscName))
                    {
                        name = cscName;
                    }
                }
                string refWebKey = TryGetAppSetting(CSConfigStringPrefix + accountSharedKeyConfiguration);
                if (!string.IsNullOrEmpty(refWebKey))
                {
                    cscKey = TryGetConfigurationSetting(refWebKey);
                    if (!string.IsNullOrEmpty(cscKey))
                    {
                        key = cscKey;
                    }
                }
                string refWebEndpoint = TryGetAppSetting(CSConfigStringPrefix + endpointConfiguration);
                if (!string.IsNullOrEmpty(refWebEndpoint))
                {
                    cscEndpoint = TryGetConfigurationSetting(refWebEndpoint);
                    if (!string.IsNullOrEmpty(cscEndpoint))
                    {
                        endpoint = cscEndpoint;
                    }
                }
                string refWebPathStyle = TryGetAppSetting(CSConfigStringPrefix + usePathStyleUrisConfiguration);
                if (!string.IsNullOrEmpty(refWebPathStyle))
                {
                    cscPathStyle = TryGetConfigurationSetting(refWebPathStyle);
                    if (!string.IsNullOrEmpty(cscPathStyle))
                    {
                        pathStyle = cscPathStyle;
                    }
                }
            }

            if (string.IsNullOrEmpty(key) && !allowIncompleteSettings)
            {
                throw new ArgumentException("No account key specified!");
            }
            if (string.IsNullOrEmpty(endpoint) && !allowIncompleteSettings)
            {
                throw new ArgumentException("No endpoint specified!");
            }
            if (string.IsNullOrEmpty(name))
            {
                // in this case let's try to derive the account name from the Uri
                string newAccountName = null;
                Uri newBaseUri = null;
                if (IsStandardStorageEndpoint(new Uri(endpoint), out newAccountName, out newBaseUri))
                {
                    Debug.Assert((newAccountName != null && newBaseUri != null) || (newAccountName == null && newBaseUri == null));
                    if (newAccountName != null && newBaseUri != null)
                    {
                        endpoint = newBaseUri.AbsoluteUri;
                        name = newAccountName;
                    }
                }
                if (string.IsNullOrEmpty(name) && !allowIncompleteSettings)
                {
                    throw new ArgumentException("No account name specified.");
                }
            }

            bool? usePathStyleUris = null;
            if (!string.IsNullOrEmpty(pathStyle))
            {
                bool b;
                if (!bool.TryParse(pathStyle, out b))
                {
                    throw new ConfigurationErrorsException("Cannot parse value of setting UsePathStyleUris as a boolean");
                }
                usePathStyleUris = b;
            }
            Uri tmpBaseUri = null;
            if (!string.IsNullOrEmpty(endpoint))
            {
                tmpBaseUri = new Uri(endpoint);
            }
            return new StorageAccountInfo(tmpBaseUri, usePathStyleUris, name, key, allowIncompleteSettings);
        }


        /// <summary>
        /// Checks whether all essential properties of this object are set. Only then, the account info object 
        /// should be used in ohter APIs of this library.
        /// </summary>
        /// <returns></returns>
        public bool IsCompleteSetting()
        {
            return BaseUri != null && Base64Key != null && AccountName != null;
        }

        /// <summary>
        /// Checks whether this StorageAccountInfo object is complete in the sense that all properties are set.
        /// </summary>
        public void CheckComplete()
        {
            if (!IsCompleteSetting())
            {
                throw new ConfigurationErrorsException("Account information incomplete!");
            }
        }

        #region Private methods

        private static string TryGetConfigurationSetting(string configName)
        {
            string ret = null;
            try
            {
                ret = RoleManager.GetConfigurationSetting(configName);
            }
            catch (RoleException)
            {
                return null;
            }
            return ret;
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", 
                                                          Justification = "Make sure that nothing prevents us to read from the fabric's configuration envrionment.")]
        private static string TryGetAppSetting(string configName)
        {
            string ret = null;
            try
            {
                ret = ConfigurationSettings.AppSettings[configName];
            }
            // some exception happened when accessing the app settings section
            // most likely this is because there is no app setting file
            // we assume that this is because there is no app settings file; this is not an error
            // and explicitly all exceptions are captured here
            catch (Exception)
            {
                return null;
            }
            return ret;
        }

        private static string GeneralAccountConfigurationExceptionString {
            get {
                return "If the portal defines http://test.blob.core.windows.net as your blob storage endpoint, the string \"test\" " + 
                       "is your account name, and you can specify http://blob.core.windows.net as the BlobStorageEndpoint in your " +
                       "service's configuration file(s).";
            }
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase")]
        private static bool IsStandardStorageEndpoint(Uri baseUri, out string newAccountName, out Uri newBaseUri) {
            if (baseUri == null) {
                throw new ArgumentNullException("baseUri");
            }

            newAccountName = null;
            newBaseUri = null;

            string host = baseUri.Host;
            if (string.IsNullOrEmpty(host)) {
                throw new ArgumentException("The host part of the Uri " + baseUri.AbsoluteUri + " must not be null or empty.");
            }
            if (host != host.ToLowerInvariant()) {
                throw new ArgumentException("The specified host string " + host + " must not contain upper-case letters.");
            }

            string suffix = null;
            if (host.EndsWith(StorageHttpConstants.StandardPortalEndpoints.TableStorageEndpoint, StringComparison.Ordinal)) {
                suffix = StorageHttpConstants.StandardPortalEndpoints.TableStorageEndpoint;
            }
            if (host.EndsWith(StorageHttpConstants.StandardPortalEndpoints.BlobStorageEndpoint, StringComparison.Ordinal))
            {
                suffix = StorageHttpConstants.StandardPortalEndpoints.BlobStorageEndpoint;
            }
            if (host.EndsWith(StorageHttpConstants.StandardPortalEndpoints.QueueStorageEndpoint, StringComparison.Ordinal))
            {
                suffix = StorageHttpConstants.StandardPortalEndpoints.QueueStorageEndpoint;
            }
            // a URL as presented on the portal was specified, lets find out whether it is in the correct format
            if (suffix != null) {
                int index = host.IndexOf(suffix, StringComparison.Ordinal);
                Debug.Assert(index != -1);
                if (index > 0) {
                    string first = host.Substring(0, index);
                    Debug.Assert(!string.IsNullOrEmpty(first));
                    if (first[first.Length-1] != StorageHttpConstants.ConstChars.Dot[0]) {
                        return false;
                    }
                    first = first.Substring(0, first.Length - 1);
                    if (string.IsNullOrEmpty(first)) {
                        throw new ArgumentException("The configured base URI " + baseUri.AbsoluteUri + " for the storage service is incorrect. " + 
                                                     GeneralAccountConfigurationExceptionString);
                    }
                    if (first.Contains(StorageHttpConstants.ConstChars.Dot)) {
                        throw new ArgumentException("The configured base URI " + baseUri.AbsoluteUri + " for the storage service is incorrect. " + 
                                                     GeneralAccountConfigurationExceptionString);
                    }
                    newAccountName = first;
                    newBaseUri = new Uri(baseUri.Scheme + Uri.SchemeDelimiter + suffix + baseUri.PathAndQuery);                    
                }
                return true;
            }
            return false;
        }


        #endregion

    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Common Development and Distribution License (CDDL)


Written By
Technical Lead
United States United States
https://github.com/modesty

https://www.linkedin.com/in/modesty-zhang-9a43771

https://twitter.com/modestyqz

Comments and Discussions