Summary

Almost every application needs to store configuration information. What good are those 74-page config dialogs if they don't save the settings? But where do you store these settings? The .NET answer to this question is Isolated Storage. Read on to learn how to use it, and how not to.

The source code for the Zip Code Manager is available here.

Table of Contents

Introduction
What is Isolated Storage?
Storages, Files, and Streams, Oh My!
Where Did I Put That, Again?
Sharing Data Files Between Assemblies - or not
Managing Isolated Storage
Recommended Uses of Isolated Storage
Conclusion

Introduction

Applications often need to store state information. Every setting on the ubiquitous "Options" dialog needs to be stored somewhere. In this age of disconnected web-service-based applications, users really appreciate it when your application can pull the zip code list off a local disk file instead of making yet another round trip to a web service.

Back in the days of 16-bit windows, when an application needed to store user settings or caches, the INI file was the place to do it. They had a simple text-file format, and Microsoft even supplied some handy API functions to read and write from them. And life was good. Except that the API's had some really annoying file size limitations. And INI files were flat - there was no simple way to represent a hierarchy. And the worst problem: where do you put the INI file? Many applications in those days actually ended up dumping their INI files in the Windows directory, because that was the only directory guaranteed to exist on a Win-16 system.

When the 32-bit Windows systems were introduced, Microsoft took a swing at fixing the problems with INI files by introducing the Windows registry. The registry is hierarchical, so it's easy to represent nested data. There's no limit on the amount of data you can put in there. And since the registry is globally accessible, there's never a question of where your configuration data is. The registry also nicely takes into account the presence of multiple user accounts through the HKEY_CURRENT_USER key, which changes contents based on who is currently logged into the computer.

Of course, experience has brought the glorious dream to dust. The registry can only be edited via the API's, which make it difficult to search and back up. Registries that get too big slow down the entire system. Since all configuration data is available to any program, a simple slip with regedit or in an installer can render the entire system unusable. Large registries make it difficult to find any one particular piece of data. And now we add the concept of downloadable code - do we really want to give arbitrary code downloaded off the net access to our registry just so it can cache a couple of zip codes? If you like that idea, I've got some swampland in Florida you'll be interested in...

Despite the registry's shortcomings, you have to admit that it does solve the real problems of storing application state and settings. How would you fix the problems the registry created? The ideal solution would provide:

In the .NET Framework, Microsoft has attempted to address the shortcomings of the registry. For the application-wide information (the kind of things you'd put in the HKEY_LOCAL_MACHINE hive in the registry) you have the application's config file. The config file is always in the same directory as the .exe file (solving the "where do I put this?" problem), is XML formatted (solving the "arbitrary data storage" problem for the most part), and each .exe file has its own config file (solving the "separate files per application" part). However, config files are generally for read-only information; they are stored next to the executable file, which is under the Program Files directory. Using any directory under Program Files as writable storage space has always been a bad idea, and under Windows Server 2003 (and future versions of Windows) the Program Files directory and all subdirectories have ACL's that prevent anyone but an Administrator from writing to the directory. So it's ok to create a config file when the administrator installs the program, but don't try to use it for read/write storage. The config file and how to extend it is well documented; here's a good place to start for more information.

But what about the per-user, writable information? Config files don't provide the per-user switching that was so helpful about the HKEY_CURRENT_USER hive in the registry. And writing to the config file is, as mentioned above, a bad idea. Microsoft's solution to storing writable per-user data is Isolated Storage.

What is Isolated Storage?

Isolated storage is, essentially, a special spot on the hard drive that only your application can find. The .NET Framework takes care of managing the actual disk storage. Once you've opened your isolated storage, you can create files or directories in it, and treat it pretty much like any other disk space. The nice thing is that even if your application doesn't have permissions to access the file system (for example, if it's downloaded code via no-touch deployment) you can still use isolated storage.

Isolated storage is morally equivalent to a chroot jail in the Unix world. It is much, much easier to configure and use isolated storage, however.

Storages, Files, and Streams, Oh My!

The System.IO.IsolatedStorage name space contains all the classes you need to use to access isolated storage from your application. Unfortunately, these classes aren't named all that well, and it's easy to get a little confused about what each of these classes does.

The IsolatedStorageFile class is where you start. Despite the name, this class does not represent a file in isolated storage; it represents the actual isolated store. Opening up isolated storage is as simple as this line of code:

C#:
IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForDomain();

VB.NET:
Dim store as IsolatedStorageFile = IsolatedStorageFile.GetUserStoreForDomain()

The GetUserStoreForDomain() method is a static method that gets the correct store for the current user and domain (I'll discuss what domain means below). Each user that runs the application has their own isolated storage section, so the information within the store will not be visible to other users on the machine.

Once you have your store object, you're essentially dealing with a directory. IsolatedStorageFile has methods to create or delete directories, get a list of files in the store, delete the files in the store, and more. Check the documentation on MSDN for a full list of methods. Most of these methods don't get used much. What you really want to do is create or read from a file. To do that, you use the IsolatedStorageFileStream class. This class lets you open a stream onto a file in your isolated storage. Once you've created it, you can use any of the .NET library classes that use Stream objects.

For example, here's how you would take a DataSet object and write it out as XML to a file in Isolated Storage:

C#:

using System.Data;
using System.IO;
using System.IO.IsolatedStorage;

DataSet ds = new DataSet();
// ... fill dataset here ...
IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForDomain();
IsolatedStorageFileSteam stream = new IsolatedStorageFileStream("data.xml", FileMode.Create, store);
ds.WriteXML(stream);
stream.Dispose();
store.Dispose();

VB.NET:

Imports System.Data
Imports System.IO
Imports System.IO.IsolatedStorage

Dim ds as new DataSet()
' ... fill dataset here

Dim store as IsolatedStorageFile = IsolatedStorageFile.GetUserStoreForDomain()
Dim stream as new IsolatedStorageFileStream("data.xml", FileMode.Create, store)
ds.WriteXML(stream)
stream.Dispose()
store.Dispose()

This is no different than dealing with any other file using the .NET framework, with the exception that you use the IsolatedStorageFileStream class instead of the FileStream class. Polymorphism wins again! You can of course use a StreamReader or StreamWriter if you're dealing with other kinds of storage: plain text or binary data for example.

Where Did I Put That, Again?

The basic usage of isolated storage is, as shown above, quite simple. But, as always, the devil is in the details, and the documentation on these details is vague at best. The biggest question is: how does the framework know which isolated storage directory on the disk corresponds to this application? The answer to this question depends on knowing how the framework identifies an application.

The first part of the division is easy: every user gets their own set of isolated storages. This is done by placing the actual storage under \Documents and Settings\<username>\Local Settings\Application Data\IsolatedStorage (in V1.1 of the framework). Each user has their own directory, so they don't interfere with each other.

The second level of identification is the assembly. Each assembly has a name. If the assembly is not signed, then that name is the URL where the assembly was loaded from. This means that for unsigned assemblies, if you have two identical copies of the assembly in different directories, as far as isolated storage is concerned they are different, and will get separate storages. If the assembly is signed, on the other hand, then isolated storage uses the strong name of the assembly, and the location on the disk doesn't enter into the question.

This could be a recipe for disaster, with arbitrary applications loading shared assemblies and getting access to each other's data. However, there is a third way that isolated storage identifies which store to use: the domain. This does not refer to a network domain; it is referring to the AppDomain. Basically, it identifies where the code that's running was started from. If the program is running from the local disk the domain and assembly information are the same (with one exception, see below). If the program was downloaded from the web, on the other hand, then the assembly would use the name of the assembly, but the domain is the URL that the program was downloaded from.

The easiest way to see how this actually works is to use the storeadm.exe program that ships with the .NET framework. If you're using Visual Studio.NET, you can run this from the VS.NET command line prompt. Let's use that zip-code storage application as an example.

Figure 1 - The Zip Code Manager application

Figure 1 shows this application. It's built from two assemblies. ZipCode.exe is the main program and handles the main frame and the "Your City" section at the top. ZipListControl.dll is the second assembly, and has the code for the rest of the form elements in a user control. Both assemblies use isolated storage to hold their information. ZipCode.exe created a file named "city.txt" storing the city name you enter, and ZipListControl.dll creates a file called zipcodes.txt to store the zip codes.

Run the application (source code is available here) and enter in a city name and a couple of zip codes. This will write the data to isolated storage. Then drop to a command line and run "storeadm /list". You'll see something like this:

U:\>storeadm /list
Microsoft (R) .NET Framework Store Admin 1.1.4322.573
Copyright (C) Microsoft Corporation 1998-2002. All rights reserved.

Record #1
[Domain]
<System.Security.Policy.Url version="1">
   <Url>file://C:/Home/Chris/Projects/ZipCode/ZipCode/bin/Debug/ZipCode.exe</Url>
</System.Security.Policy.Url>

[Assembly]
<System.Security.Policy.Url version="1">
   <Url>file://C:/Home/Chris/Projects/ZipCode/ZipCode/bin/Debug/ZipListControl.DLL</Url>
</System.Security.Policy.Url>

        Size : 2048
Record #2
[Domain]
<System.Security.Policy.Url version="1">
   <Url>file://C:/Home/Chris/Projects/ZipCode/ZipCode/bin/Debug/ZipCode.exe</Url>
</System.Security.Policy.Url>

[Assembly]
<System.Security.Policy.Url version="1">
   <Url>file://C:/Home/Chris/Projects/ZipCode/ZipCode/bin/Debug/ZipCode.exe</Url>
</System.Security.Policy.Url>

        Size : 2048

Notice that there are two records: one for each assembly. In both cases, the domain is ZipCode.exe - the assembly that started execution. The assembly in each record is different - one for ZipListControl.DLL, and one for ZipCode.exe. Notice that the URL for the assemblies includes the path to the assembly. This means that if you copy ZipCode.exe to a different directory and run it from there, the path is different, so the domain is different, and so this second copy will get a separate isolated store.

This situation changes somewhat if you strongly name your assemblies. I used the AssemblyKeyFile attribute to specify a key file for both assemblies and re-ran the application. Running storeadm.exe again gives this:

U:\>storeadm /list
Microsoft (R) .NET Framework Store Admin 1.1.4322.573
Copyright (C) Microsoft Corporation 1998-2002. All rights reserved.

Record #1
[Domain]
<StrongName version="1"
            Key="002400000480000094000000060200000024000052534131000400000100010
0FDABE8A12C0E54B8DA90433377303EB460F7742874C44FD1907450C8A064E0AE7A7ADE71F0603AF
B4777C8B715FC528057C200D60979C4F13260723F3E1A86220C1D4BD4A910632975760B0979748A3
0C14A3FD6AAD740499B787A13D17D3408519123C4D87E37DAE9AE8B76C82C15A9D7B300806505919
D1EDDED39C360D8AF"
            Name="ZipCode"
            Version="1.0.1313.26901"/>

[Assembly]
<StrongName version="1"
            Key="002400000480000094000000060200000024000052534131000400000100010
0FDABE8A12C0E54B8DA90433377303EB460F7742874C44FD1907450C8A064E0AE7A7ADE71F0603AF
B4777C8B715FC528057C200D60979C4F13260723F3E1A86220C1D4BD4A910632975760B0979748A3
0C14A3FD6AAD740499B787A13D17D3408519123C4D87E37DAE9AE8B76C82C15A9D7B300806505919
D1EDDED39C360D8AF"
            Name="ZipCode"
            Version="1.0.1313.26901"/>

        Size : 2048
Record #2
[Domain]
<StrongName version="1"
            Key="002400000480000094000000060200000024000052534131000400000100010
0FDABE8A12C0E54B8DA90433377303EB460F7742874C44FD1907450C8A064E0AE7A7ADE71F0603AF
B4777C8B715FC528057C200D60979C4F13260723F3E1A86220C1D4BD4A910632975760B0979748A3
0C14A3FD6AAD740499B787A13D17D3408519123C4D87E37DAE9AE8B76C82C15A9D7B300806505919
D1EDDED39C360D8AF"
            Name="ZipCode"
            Version="1.0.1313.26901"/>

[Assembly]
<StrongName version="1"
            Key="002400000480000094000000060200000024000052534131000400000100010
0FDABE8A12C0E54B8DA90433377303EB460F7742874C44FD1907450C8A064E0AE7A7ADE71F0603AF
B4777C8B715FC528057C200D60979C4F13260723F3E1A86220C1D4BD4A910632975760B0979748A3
0C14A3FD6AAD740499B787A13D17D3408519123C4D87E37DAE9AE8B76C82C15A9D7B300806505919
D1EDDED39C360D8AF"
            Name="ZipListControl"
            Version="1.0.1313.26901"/>

        Size : 2048
Record #3
[Domain]
<System.Security.Policy.Url version="1">
   <Url>file://C:/Home/Chris/Projects/ZipCode/ZipCode/bin/Debug/ZipCode.exe</Url>
</System.Security.Policy.Url>

[Assembly]
<System.Security.Policy.Url version="1">
   <Url>file://C:/Home/Chris/Projects/ZipCode/ZipCode/bin/Debug/ZipListControl.DLL</Url>
</System.Security.Policy.Url>

        Size : 2048
Record #4
[Domain]
<System.Security.Policy.Url version="1">
   <Url>file://C:/Home/Chris/Projects/ZipCode/ZipCode/bin/Debug/ZipCode.exe</Url>
</System.Security.Policy.Url>

[Assembly]
<System.Security.Policy.Url version="1">
   <Url>file://C:/Home/Chris/Projects/ZipCode/ZipCode/bin/Debug/ZipCode.exe</Url>
</System.Security.Policy.Url>

        Size : 2048

Now we have four isolated stores - the two original ones (now records 3 & 4) plus the two new ones. But notice that now the assemblies are referenced using their strong names rather than a URL. This strong name is independent of the pathname, so strongly named assemblies get the same isolated storage regardless of where on the disk the assembly is loaded from.

The final permutation is loading from the web via No-Touch deployment. I put the signed assembly up on a web server and ran it. Running storeadm.exe again gives this (I've edited out the old records for clarity):

Record #4
[Domain]
<System.Security.Policy.Url version="1">
   <Url>http://www.example.com/ZipCode/ZipCode.exe</Url>
</System.Security.Policy.Url>

[Assembly]
<StrongName version="1"
            Key="002400000480000094000000060200000024000052534131000400000100010
0FDABE8A12C0E54B8DA90433377303EB460F7742874C44FD1907450C8A064E0AE7A7ADE71F0603AF
B4777C8B715FC528057C200D60979C4F13260723F3E1A86220C1D4BD4A910632975760B0979748A3
0C14A3FD6AAD740499B787A13D17D3408519123C4D87E37DAE9AE8B76C82C15A9D7B300806505919
D1EDDED39C360D8AF"
            Name="ZipCode"
            Version="1.0.1313.28952"/>

        Size : 2048
Record #5
[Domain]
<System.Security.Policy.Url version="1">
   <Url>http://www.example.com/ZipCode/ZipCode.exe</Url>
</System.Security.Policy.Url>

[Assembly]
<StrongName version="1"
            Key="002400000480000094000000060200000024000052534131000400000100010
0FDABE8A12C0E54B8DA90433377303EB460F7742874C44FD1907450C8A064E0AE7A7ADE71F0603AF
B4777C8B715FC528057C200D60979C4F13260723F3E1A86220C1D4BD4A910632975760B0979748A3
0C14A3FD6AAD740499B787A13D17D3408519123C4D87E37DAE9AE8B76C82C15A9D7B300806505919
D1EDDED39C360D8AF"
            Name="ZipListControl"
            Version="1.0.1313.28952"/>

        Size : 2048

Here, the domain is actually the URL that the .exe file was downloaded from, but the assembly continues to be the strong name of the assembly itself. So, for No-Touch Deployment applications, the isolated store is assigned based on the URL you download from.

Knowing how the system determines which isolated store to open for which assembly helps when planning new deployments of assemblies. Basically, if the assembly is unsigned, if the name or directory changes it'll get a new store. If the assembly is signed, it'll always get the same store. And finally, with No-Touch Deployment, it's all about the URL.

Sharing Data Files Between Assemblies - or not

Notice in the storeadm listings above that each assembly within a domain gets its own store. To double check this, I tried writing some code to open the "zipcode.txt" file created by the DLL from the ZipCode.exe assembly. For my troubles I got a FileNotFoundException. The isolated storages for assemblies are even isolated from each other within a single process.

In my quest to break the walls, I noticed the IsolatedStorageFile.GetStore() method. This is the method that does the actual work of opening up a particular isolated store. The GetUserStoreForDomain() method is just a wrapper around GetStore(), and is equivalent to calling:

	IsolatedStorageFile.GetStore( IsolatedStorageScope.User |
                    IsolatedStorageScope.Assembly |
                    IsolatedStorageScope.Domain ,
                    null,
                    null);

That IsolatedStorageScope enumeration has flags indicating "how isolated do you want". The above set of flags says we want to isolate based on user, assembly and domain. "Ah hah!" I thought. What if I open both stores just based on domain, and remove the Assembly part? Then the two assemblies should share the same store and I can use the same disk files from both.

I went ahead and tried that, and here's what I got:

It was nice of Microsoft to say what combinations of scopes are legal. Basically, assemblies always get their own stores. If you need to communicate data between two assemblies within an application, pass an object via a method call, don't try to use disk files.

Managing Isolated Storage

So, we've got these applications writing stuff to the user's disk. No one assembly can write more than 10 Mb per user (unless configured otherwise), but still, how do we clean it up? The easiest way to "uninstall" cached data is to provide a hook within your assembly to delete the files it creates; this way there's no difficulty with one assembly trying to find another assembly's isolated store.

However, think about the storeadm.exe program. That program finds other isolated storages - in fact, if you run "storeadm /remove" it will delete all the isolated stores for that user. Luckily, storeadm.exe is managed code, so you can open it up with a disassembly tool (my personal favorite is Reflector. Go grab a copy today!) and see how it does it.

The IsolatedStorageFile class has a method, GetEnumerator, that lets you walk the list of a user's storages. This is the method that storeadm.exe calls to build its list of records. However, to use this requires extra code-access permissions, specifically the Unrestricted version of the IsolatedStorageFilePermission. Normally, only administrators running code from the local disk have this permission, so if you want to write an un-installer for isolated storage, be aware that you'll need elevated permissions for this to work.

Recommended Uses of Isolated Storage

So, having looked at the details of what isolated storage is and how it works, some general ideas of when and how to use it form.

Use isolated storage to store:

Do not use isolated storage for:

Conclusion

Isolated storage is a technology in the .NET framework that provides each assembly in a program with a private disk area. This solves the age old problem of where to store cached information and per-user settings, and allows no-touch deployment programs to take advantage of local storage on the client machine without exposing the gaping security hole that full file system access would grant. It's the first place you should look to when thinking about how to store your user configuration or cached information.

Kudos

Thanks to Grant Killian for his review of this article.

About the Author

Chris Tavares is a software consultant in the Portland, Oregon area. He's worked on systems as small as an Intel 8080 with 512 bytes of memory up to large multi-cpu signal processing boxes, but he feels most at home on the Windows platform. He's been programming long enough to distinctly remember paying $400 for a 16k (yes, k, not meg) memory upgrade for his Sinclair ZX-81.

Chris lives in Beaverton, Oregon, with his wife Wendy, son Matthew, one cat and one dog.

If you have any questions or comments, he can be contacted at cct@tavaresstudios.com.

Chris Tavares