Introduction
One challenge faced when migrating an on-premise application into Cloud is how to redesign how the application deals with files and directories. For a regular .NET based application, when there is a business file needed to be kept somewhere, the application will just save the file into a local or shared drive via the .NET IO API. The problem in the cloud environment is a local or shared drive is not recommended for storing business files anymore. The recommended place for storing business files is cloud storage, such as BLOB in Windows Azure and S3 in Amazon AWS. So, to design something to let the application not be dependent on the actual storage used is crucial for applications to be cloud ready.
How to Deal with the Difference Between Local Storage and Cloud Storage
The local storage in Windows contains both files and directories. File is a block of arbitrary information, or resource for storing information, which is available to a computer program and is usually based on some kind of durable storage. Directory, also known as a folder, is a virtual container within a digital file system, in which groups of computer files and possibly other directories can be kept and organized. However, the normal meaning of file and directory is not available in cloud anymore. The cloud storage has the faith to deal with distribution, scalability, availability, and performance, so it doesn’t directly expose itself as file and directory. Use Windows Azure BLOB storage as example, BLOB storage has a three layer structure: Account, Container, and Blob. Account is the access point for BLOB storage. Application must provide the correct account information to get into BLOB storage service in Windows Azure. Account can be thought of as a hard drive in a computer. Container is a grouping of a set of BLOBs. Container can be thought of as a partition in a computer. BLOB is a file of any type and size. BLOB can be thought of as a file in computer. How about a directory? There is no direct correspondence concept of a directory in BLOB storage. All BLOBs are immediately under a container. However, a special type of BLOB can be created to represent a directory. By doing this, a regular directory structure can still be created in BLOB storage.
The difference between local storage and cloud storage adds burden on the application developer to have two sets of code dealing with different storage types. The old approach for this situation is to let code for local storage and code for cloud storage co-exist in an application, and then the application can go to a particular branch based on a condition. This is OK if a limited conditions application wants to support this feature. How about supporting a new type of cloud storage? The code must be changed to add a new branch in the application to deal with the new storage type. Is there a better way?
Strategy Pattern
Strategy pattern is a particular software design pattern, whereby the algorithm can be selected at runtime.
For the storage situation, different storage accessing implementations can be thought of as different algorithms on handling file and directory. By having a storage interface defined for common file and directory operations, we can implement them in different concert classes. Therefore, to support a new type of storage, it’s enough to just add another concert implementation. The context of Strategy pattern will bring in the appropriate implementation at runtime.
Storage Interface
The first step in realizing the Strategy pattern is to design interfaces for the algorithm. In our case, they are storage interfaces. There are two storage interfaces: IDirectory
and IFile
.
For demo purposes, only three methods are defined in IDirectory
and IFile
.
Storage Interface Implementation
To allow application access to local storage and cloud storage, we need to have two sets of implementations.
Local Directory and File
Source Code
IFile
public class LocalFile: IFile
{
public void Create(string path, Stream s)
{
FileStream fs = System.IO.File.Create(LocalStorageUtility.GetRealPath(path));
s.Position = 0;
byte[] bytes = new byte[s.Length];
s.Read(bytes, 0, bytes.Length);
fs.Write(bytes, 0, bytes.Length);
fs.Close();
}
public void Delete(string path)
{
System.IO.File.Delete(LocalStorageUtility.GetRealPath(path));
}
public bool Exists(string path)
{
return System.IO.File.Exists(LocalStorageUtility.GetRealPath(path));
}
}
IDirectory
public class LocalDirectory: IDirectory
{
public void CreateDirectory(string path)
{
Directory.CreateDirectory(LocalStorageUtility.GetRealPath(path));
}
public void Delete(string path)
{
Directory.Delete(LocalStorageUtility.GetRealPath(path));
}
public bool Exists(string path)
{
return Directory.Exists(LocalStorageUtility.GetRealPath(path));
}
}
Cloud Directory and File
Source Code
CloudDirectory
public class CloudDirectory: IDirectory
{
public void CreateDirectory(string path)
{
string cloudPath = CloudStorageUtility.GetCloudPath(path);
if (cloudPath.IndexOf("/") != -1)
{
CloudBlob blob = CloudStorageUtility.Client.GetBlobReference
(cloudPath + "/:SpaceBlockPlaceholder:");
blob.UploadText("");
}
else
{
CloudBlobContainer container =
CloudStorageUtility.Client.GetContainerReference(cloudPath);
container.CreateIfNotExist();
}
}
public void Delete(string path)
{
string cloudPath = CloudStorageUtility.GetCloudPath(path);
if (cloudPath.IndexOf("/") != -1)
{
CloudBlobDirectory dir =
CloudStorageUtility.Client.GetBlobDirectoryReference(cloudPath + "/");
IEnumerable<ilistblobitem> items = dir.ListBlobs();
foreach (IListBlobItem item in items)
{
if (item.GetType() == typeof(CloudBlobDirectory))
{
Delete(item.Uri.PathAndQuery);
}
else if (item.GetType() == typeof(CloudBlob) ||
item.GetType().BaseType == typeof(CloudBlob))
{
((CloudBlob)item).DeleteIfExists();
}
}
}
else
{
CloudBlobContainer container =
CloudStorageUtility.Client.GetContainerReference(cloudPath);
container.Delete();
}
}
public bool Exists(string path)
{
string cloudPath = CloudStorageUtility.GetCloudPath(path);
CloudBlobContainer container =
CloudStorageUtility.Client.GetContainerReference(cloudPath);
try
{
container.FetchAttributes();
return true;
}
catch
{
return false;
}
}
}</ilistblobitem>
CloudFile
public class CloudFile: IFile
{
public void Create(string path, Stream s)
{
CloudBlob blob = CloudStorageUtility.Client.GetBlobReference
(CloudStorageUtility.GetCloudPath(path));
MemoryStream ms = new MemoryStream();
s.Position = 0;
byte[] bytes = new byte[s.Length];
s.Read(bytes, 0, bytes.Length);
ms.Write(bytes, 0, bytes.Length);
ms.Position = 0;
blob.UploadFromStream(ms);
}
public void Delete(string path)
{
CloudBlob blob = CloudStorageUtility.Client.GetBlobReference
(CloudStorageUtility.GetCloudPath(path));
blob.DeleteIfExists();
}
public bool Exists(string path)
{
CloudBlob blob = CloudStorageUtility.Client.GetBlobReference
(CloudStorageUtility.GetCloudPath(path));
try
{
BlobRequestOptions opts = new BlobRequestOptions();
opts.UseFlatBlobListing = true;
blob.FetchAttributes(opts);
return true;
}
catch (StorageClientException ex)
{
if (ex.ErrorCode == StorageErrorCode.ResourceNotFound)
{
return false;
}
else
{
throw;
}
}
}
}
Switch the Implementation
The context should be able to switch different implementations at runtime. One way to achieve this is by using .NET Reflection. The context will use information in the configuration file to create an IDirectory
or IFile
instance and let the application use it.
How to Run the Demo Application
The demo application includes unit tests for LocalStorage
and CloudStorage
. The local storage unit test creates a folder and file in the Demo.Tests
project’s Debug folder. The cloud storage unit test creates a folder and file in Windows Azure's BLOB storage, therefore Windows Azure storage must be setup before you can run it.
Set up Windows Azure Storage
- Login Windows Azure.
- Click STORAGE to show all existing cloud storages.
- Click the NEW button to add a new cloud storage.
- The newly created storage includes BLOB, Table, and Queue in Windows Azure.
To allow unit test code to connect with your BLOB storage, you must also have the right StorageConnectionString
in your config file. The StorageConnectionString
has format like this:
DefaultEndpointsProtocol=[Protocol];AccountName=[Account Name];AccountKey=[Account Key]
Example: DefaultEndpointsProtocol=https;AccountName=librarydemo;AccountKey=abcdefghijklmnopqrstuvwxyz
The https is recommended for DefaultEndpointsProtocol
. AccountName
is the name of your cloud storage. AccountKey
can be gotten from the manage key window by clicking the MANAGE KEYS button in your storage page.
Summary
Moving to cloud is the trend in computer industry, there are many things that need to be considered when we are moving an on-premise application into cloud. By wisely using a Design Pattern, we can make an application be cloud ready without sacrificing the on-premise option.