Where should I put it? What should I call it? These have been questions for developers and users trying to save information in files since the earliest file-systems appeared. Historically libraries have given us functions for traversing structures and accessing files, but few to help us decide where those files should be.
Isolated Storage is a partial solution to this problem provided by the .NET Framework. It gives us the ability to keep files in an application-specific storage location on a per-user or per-machine basis. As is often the case, the MSDN documentation for this feature is pretty good at the class level. In this article I hope to augment that with a broader view of the key concepts behind Isolated Storage.
Where are they now?
For per-user settings some applications create a file or folder in “My Documents” and give it a name that encourages users to leave it alone, or set the “hidden” flag so that it is usually invisible. Other applications choose to use the registry instead, although mapping between the application object model and the registry is usually cumbersome.
Well-behaved .NET applications can use the Environment.GetFolderPath function to obtain the path for “special” system directories enumerated in Environment.SpecialFolder. Table 1 shows the values for this enumeration.
Table 1: Environment.SpecialFolder values | |
SpecialFolder | Description |
Desktop | The virtual directory for the desktop |
Programs | The directory containing the users program groups (or start menu on more recent OS versions) |
Personal | The folder for document storage (usually “My Documents”) |
Favorites | The Favourites folder |
Startup | The start menu folder for applications that run at startup |
Recent | The folder containing links to recently opened documents |
SendTo | The folder used to populate the “Send To” menu |
StartMenu | The folder for the start menu |
MyMusic | The folder used for storing music |
DesktopDirectory | The physical folder for the desktop |
MyComputer | Empty, as this has no corresponding physical location. |
Templates | The folder for templates |
ApplicationData | The folder for application data other than documents. On most systems this is per-user. Where a user has a roaming profile, this data will form part of the roaming profile. |
LocalApplicationData | The folder for application data other than documents. On most systems this is per-user. This folder will NOT form part of a roaming profile. |
InternetCache | Location where temporary internet files will be stored |
Cookies | Location where cookies will be stored |
History | Location where history (usually from browsing) will be stored |
CommonApplicationData | The folder for application data other than documents that is shared by all users. |
System | The system folder, usually actually named “system32” and under the windows root. |
ProgramFiles | The location where applications are installed, usually “Program Files” |
MyPictures | The folder used for storing pictures* |
CommonProgramFiles | The locations where applications install common or shared components. |
Notice that the “user” folders (marked * in the table) will normally be per-user, the precise location will depend on OS version and configuration. Also notice that on most systems the physical and virtual Desktop give the same location. It is not clear under what circumstances they would differ and this is left as one of the mysteries of Windows.
Applications should use the special folder ApplicationData for user preferences and state, and LocalApplicationData for user settings that depend on the machine (e.g. where choices may be affected by screen resolution or connectivity). The CommonApplicationData special folder should be used for settings and data that apply to all users (e.g. storing a licence or serial number).
Using a designated special folder gives you a root point, but you still have to come up with your own mechanism to ensure that any files/folders you create have names that will not conflict with other applications. Access to these folders is generally only available to relatively privileged code (at least under .NET), because they may contain settings from many applications, some of which could contain personal information! Applications and applets delivered through the web usually do not have the required permissions for file system access.
Isolated storage
Isolated Storage is a .NET Framework feature for storing application state and configuration data using files and directories.
Even applications that have little or no privilege to access the file system due to Code Access Security may still be able to access Isolated Storage – for example, applications deployed using Click Once deployment. The mechanism ensures that applications using Isolated Storage never know (or need to know) the physical location of the files and directories they are accessing. It also provides a system of quotas to limit the amount of disk space a relatively untrusted application can consume.
Although Isolated Storage files and directories are created and managed using a special mechanism, file access uses the common framework Stream mechanism and thus most functions used with standard files can also be used with Isolated Storage files (e.g. serialisation, XML).
Isolated Storage is so called because it creates a store that is effectively inaccessible to other applications, and to the user (through its obscure location). It is not in any way secure however – any user or application with file system permissions can search out the files and read/alter their contents. As such, it should not be used to store personal information or passwords unless they are appropriately encrypted.
Storage scope
Isolated Storage is divided into “Stores”, each of which is a root directory that can contain any number of files and sub-directories. Stores are in “Locations” that have the scope of either the machine or the logged-in user. For per-user locations, there is a further choice as to whether the storage forms part of the users roaming profile or is just local. Within each of these possible storage locations, “Evidence” is used to uniquely reference a specific store. This evidence can be supplied explicitly, or can be inferred from the execution environment. Evidence is divided into “Assembly”, “Domain” and “Application” categories.
When you access Isolated Storage, you specify which combination of location and evidence you wish to use by combining values from the IsolatedStorageScope enumeration. Table 2 describes the individual values in the enumeration, and Table 3 lists the valid combinations.
Table 2: IsolatedStorageScope enumeration | ||
IsolatedStorageScope | Concept | Description |
None | No scope | |
User | Location | Special folder LocalApplicationData |
Roaming | Location | Special folder ApplicationData. Only valid in conjunction with “User”. |
Machine | Location | Special folder CommonApplicationData |
Assembly | Evidence | The calling assembly identity is used as evidence |
Domain | Evidence | The domain identity (usually the origin of the main assembly in the AppDomain) is used as evidence. Only valid in conjunction with “Assembly” |
Application | Evidence | The application identity (the main assembly in the AppDomain) is used as evidence. |
Table 3: Valid IsolatedStorageScope combinations |
User | Application |
User | Assembly |
User | Assembly | Domain |
Roaming | User | Application |
Roaming | User | Assembly |
Roaming | User | Assembly | Domain |
Machine | Application |
Machine | Assembly |
Machine | Assembly | Domain |
This list differs from the majority of lists I have found in published documents and books, and even on the internet, but I have successfully tested all of these combinations, and detailed investigation of the Isolated Storage implementation confirms that they are all fully implemented.
Default evidence
By default, Assembly evidence is collected from the calling assembly (using the Assembly.Evidence property) and Domain evidence is collected from the executing AppDomain (using the Domain.Evidence property). For an executable assembly the AppDomain evidence will be taken from the evidence of the entry assembly. You can obtain the equivalent evidence sets manually using the following code:
Evidence appDomainEvidence = AppDomain.CurrentDomain.Evidence; Evidence assemblyEvidence = Assembly.GetExecutingAssembly().Evidence;
Note that assembly evidence will be collected for the assembly that calls into the Isolated Storage API. If this is a utility assembly that you use in several products, the assembly evidence will be the same for all products!
Application evidence is taken from the Activation Context, a concept that applies primarily to applications deployed using “Click Once”. Evidence can be obtained manually using the following:
ActivationContext activationContext = AppDomain.CurrentDomain. ActivationContext; ApplicationSecurityInfo info = new ApplicationSecurityInfo( activationContext); Evidence applicationEvidence = info.Evidence;
Types of evidence
Isolated Storage generally uses objects from the System.Policy namespace as evidence, specifically Publisher, StrongName, Url, Site and Zone. When using default evidence it will search using the order of precedence shown in Table 4, taking the available evidence object with the highest precedence.
Table 4: Default evidence | ||
Precedence | Evidence Object | Evidence |
1 | Publisher | Full publisher certificate data |
2 | StrongName | Name, public key, major version, |
3 | Url | Normalised URL |
4 | Site | Upper Case, culture invariant site from which assembly was obtained. |
5 | Zone | MyComputer”, “Intranet”, “Trusted”, “Internet”, “Untrusted” or “NoZone” |
The evidence used will be “Normalized” using the Normalize function of the evidence object – this does not necessarily use every part of the raw evidence object. For example, a normalized strong name contains name, public key token and major version only. The rest of the detail from the version is ignored. This is a causes a common “gotcha” when developers change an assembly version from “0.9” to “1.0” and suddenly find that all application state/settings have disappeared.
Another common “gotcha” occurs for unsigned assemblies where the Url will often be used as evidence. In this case, the debug and release builds may be in different locations (giving them different Urls) and thus access different stores! User supplied evidence (Code Access Security policy permitting) can help to avoid some of the potential problems with default evidence, but it should still be in the form of one of the types listed above. Alternatively you can supply your own object implementing the INormalizeForIsolatedStorage interface.
Accessing stores
All access to Isolated Storage is initiated through the IsolatedStorageFile type. To open a store you use the GetStore function, which has a number of overloads. The overload you use depends on what type of store you wish to open, and how you wish to supply evidence. The full list of overloads is shown in Table 5.
Table 5: GetStore overloads | |
GetStore(IsolatedStorageScope s, object applicationIdentity) | • Supply a single object containing application evidence |
GetStore(IsolatedStorageScope s, Type applicationEvidenceType) | • Supply a single object defining the evidence type that should be used from the default evidence set (null to use search precedence shown in Table 4) |
GetStore(IsolatedStorageScope s, object domainIdentity, object assemblyIdentity) | • Supply a objects containing domain and assembly evidence • Assembly evidence will always be used, whether domain evidence is also used depends on the IsolatedStorageScope specified |
GetStore(IsolatedStorageScope s, Type domainEvidenceType, Type assemblyEvidenceType) | • Supply objects defining the evidence type that should be used from the default evidence set (null to use search precedence shown in Table 4) • Assembly evidence will always be used, whether domain evidence is also used depends on the IsolatedStorageScope specified |
GetStore(IsolatedStorageScope scope, Evidence domainEvidence, Type domainEvidenceType, Evidence assemblyEvidence, Type assemblyEvidenceType) | • Supply an evidence collection for each of domain and aseembly evidence • Supply objects defining the evidence type that should be used from the default evidence set (null to use search precedence shown in Table 4) • Assembly evidence will always be used, whether domain evidence is also used depends on the IsolatedStorageScope specified |
To further complicate matters, there are a number of shortcuts that wrap the GetStore function for convenience. Personally I feel that these make code harder to read by hiding important information. The shortcuts along with the internal calls they make to GetStore are shown in Table 6.
Table 6: GetStore shortcuts | |
Function | GetStore Equivalent |
GetUserStoreForApplication | GetStore(IsolatedStorageScope.Application | IsolatedStorageScope.Machine, (Type) null); |
GetUserStoreForAssembly | GetStore(IsolatedStorageScope.Machine | IsolatedStorageScope.Assembly, (Type) null, (Type) null); |
GetUserStoreForDomain | GetStore(IsolatedStorageScope.Machine | IsolatedStorageScope.Assembly | IsolatedStorageScope.Domain, (Type) null, (Type) null); |
GetMachineStoreForApplication | GetStore(IsolatedStorageScope.Application | IsolatedStorageScope.User, (Type) null); |
GetMachineStoreForAssembly | GetStore(IsolatedStorageScope.Assembly | IsolatedStorageScope.Domain | IsolatedStorageScope.User, (Type) null, (Type) null); |
GetMachineStoreForApplication | GetStore(IsolatedStorageScope.Assembly | IsolatedStorageScope.Domain | IsolatedStorageScope.User, (Type) null, (Type) null); |
The IsolatedStorageFile object for your store gives you the ability to manage the directory structure within the store (CreateDirectory, DeleteDirectory, GetDirectoryNames), to manage files (DeleteFiles,GetFileNames) and to manage the store itself (Remove, CurrentSize, MaximumSize).
Accessing files
Files inside a store are created, written and read with the IsolatedStorageFileStream class, which inherits from FileStream. This class has two types of constructor. The first take an IsolatedStorageFile object in addition to the standard FileStream parameters, and use this store for the file. Any paths specified are relative to the root of the store. There are also standard FileStream style constructors with no additional parameters for isolated storage. If you use these versions (or specify an IsolatedStorageFile of null) the IsolatedStorageFileStream will call GetUserStoreForDomain internally. As with the GetStore short cut functions, I think it hides important information using these constructors – I always prefer to explicitly create the IsolatedStorageFile.
The following listing shows some very simple code to create a file in isolated storage, bringing together all of the concepts I have introduced in just 6 lines of code!
using System; using System.IO; using System.IO.IsolatedStorage; namespace SimpleIsolatedStorage { class SimpleIsolatedStorage { static void Main(string[] args) { IsolatedStorageFile isf = IsolatedStorageFile.GetStore( IsolatedStorageScope.Assembly | IsolatedStorageScope.User, (Type)null, (Type)null); IsolatedStorageFileStream isfs = new IsolatedStorageFileStream( “Test.txt”, FileMode.CreateNew, FileAccess.Write, isf); StreamWriter sw = new StreamWriter(isfs); sw.WriteLine(“Hello!”); sw.Close(); // Place a breakpoint on the next line isf.Remove(); } } }
Debugging isolated storage
Debugging file reading and writing problems is made much simpler if you can see the files in question, but by its very nature, isolated storage puts its files in an obfuscated location. If you wish to look at (or tinker with the contents of) isolated storage files you can either search under the root location (see Table 2) or use the debugger to look at the m_RootDir member of your IsolatedStorageFile object in the debugger (in the non-public members section), which contains the file system path of the root folder for the store.
Isolated storage also has its own class of errors, where an application does not appear to be consistently picking up files from the same location. Here the IsolatedStorageFile members ApplicationIdentity, AssemblyIdentity and DomainIdentity help out by showing you the evidence currently being used – usually this is sufficient to figure out what is going on.
You can view the stores currently on your machine (along with the associated evidence used to access them) using the storeadm utility. storeadm is a command line utility that ships in the Platform SDK (with Visual Studio) and can be used to list and remove stores. The “/LIST” option displays all user stores along with the evidence they use. Adding “/ROAMING” or “/MACHINE” changes the location for which stores are listed. There is also a “/REMOVE” option which should be used with great caution as it removes ALL stores, which may include application settings you don’t want to lose.
If you set a breakpoint as indicated in the comment in the above listing and execute the code, you will be able to see the information described above in the debugger and in storeadm.
Uninstallation
The previous code in uses the IsolatedStorageFile.Remove function to tidy up after itself, but obviously for a real application this is not a sensible approach! However, application developers should consider removing isolated storage when the application is uninstalled. Unfortunately there is no simple way to instruct your installer to do this so it has to be done programmatically in an Installer Class, overriding the Uninstall function:
public override void Uninstall(System.Collections. IDictionary savedState) { IsolatedStorageFile isf = IsolatedStorageFile.GetStore( IsolatedStorageScope.Assembly | IsolatedStorageScope.User, (Type)null, (Type)null); isf.Remove(); base.Uninstall(savedState); }
If you create your installer class manually remember to add an [RunInstaller(true)] attribute to it, otherwise it will not be picked up (Visual Studio does this for you if you add the “Installer Class” item to your project).
For uninstallation to be effective the installer class must remove the same store that the application created. Getting the IsolatedStorageScope to match up is straightforward, but unless you are providing your own evidence objects, remember that the execution environment (and thus default evidence) will be different at uninstall time than at run time. You can make sure assembly level evidence matches by putting the installer class in the same assembly as the rest of your isolated storage access functions, but if you use the domain or application options your uninstaller will need to provide appropriate evidence objects directly.
You can test your uninstaller using InstallUtil.exe from the Visual Studio command prompt. Once you are satisfied that it is working, you should add a custom “Uninstall” action to your setup project referencing the assembly containing your installer class and with the InstallerClass property true.
Conclusions
I have found Isolated Storage to be an extremely useful feature, but unless you have a full understanding of how it works there is considerable scope for confusing bugs to creep in to your application. I say this from personal experience. I hope this article has helped to give an overview of the general principles behind Isolated Storage and that, along with the detailed documentation provided by MSDN, it will help you to avoid unwelcome surprises.
Comments