User Preferences

Manage User Settings in Your .NET App with a Custom Preferences API

Ray Djajadinata

This article discusses:

  • Preferences management
  • Isolated storage in Windows
  • A custom preferences API in .NET
This article uses the following technologies:
Win32, the .NET Framework, and C#

Code download available at:CustomPreferences.exe(158 KB)

Contents

A Good Preferences API
Support for Hierarchical Organization of Preferences
Encapsulation of Data Location
Shielding from Backing Store Details
XmlSerializer and Preferences Management
Preference Change Notification
Data Isolation
Security
Application Preferences Shared by All Users
An Easier Way to Manage Preferences
Introducing NAP
Tree Nodes and Leaves
What's Happening Under the Tree?
Simulating Isolation by Assembly
Simulating Isolation by Application Domain
Conclusion

The Microsoft® .NET Framework boasts tons of useful APIs that make it simple to create applications for Windows®. Surprisingly though, the Framework could be stronger in the area of user preference management. If you're a developer for Windows using .NET, you only have two preferences APIs available, one of which is a legacy of 16-bit Windows: the initialization file API. This API is not easily accessible from .NET-compliant languages other than C++ and it is no longer recommended for use (though it is possible using interop). The second option, and the only viable one, is the registry API.

What about the types inside the System.IO.IsolatedStorage namespace? Rather than a preferences API, the isolated storage API is more of a file I/O API that gives you access to your own private (isolated) file system. Although it has some properties that make it suitable for storing user preference settings, using it is equivalent to performing I/O operations. It doesn't make preferences management any easier; it just gives you a storage mechanism in the form of files and directories.

This means that if you want to store your preferences settings in any backing store other than the registry, you'll have to actually write your own code. Whether you use the file system, isolated storage, or a database, there's no API available for preferences management in the .NET Framework. Fortunately, writing code for a custom preferences API is not particularly difficult, as you'll see.

A Good Preferences API

What makes a good preferences API? This is actually a question about two different things. The first concerns how the API is designed. Can you, for example, manipulate your preferences values easily without being burdened with storage-specific details? Can you easily organize your preferences data into logical groups? Can you easily move from one group to another? Can you feel reasonably safe that other applications and users won't trample your data? And so the list goes.

The second point to consider is the API's backing store. For instance, the registry API's backing store is the registry, the central configuration settings data depository for Windows and Windows-based applications. This makes the registry quite delicate: sloppy programming, careless editing, or malicious code can corrupt the registry and bring the whole machine down, preventing Windows from even starting. On the other hand, a database may not always be available (and database queries can be expensive), so a preferences API that requires a database may not be very useful.

The right API and a suitable backing store can go a long way to making your preferences management tasks simpler, so let's consider what to look for in choosing them.

Support for Hierarchical Organization of Preferences

Usually, the more complex your application, the more preferences your users, and you the developer, have to worry about. As such, it is important that the API you use supports organization and partitioning of the preferences data.

Consider a sophisticated development environment such as Visual Studio® .NET. There are a large number of user preferences in such an application, including editor preferences such as whether to convert tabs to spaces as well as debugger preferences such as whether to display values in hexadecimal. In such a situation, it makes sense to store your data in a tree-like structure because it allows for easy hierarchical organization. You can divide preferences into main branches (such as Text Editor or Debugging), and have subbranches when it needs to be broken down into smaller logical groups (such as Edit and Continue, Just-In-Time, or Native). If the preferences API you're using doesn't support organizing preferences like this, then you're stuck with writing your own code to impose a hierarchical structure on your data.

For instance, the initialization files API only offers two levels of organization: by file name and by section. If your preferences organization has a deeper hierarchy than that (for example, Plug-In Development | Compilers | Features), then you can either partition your data into more files or write some extra code that can manage those extra levels.

On the other hand, the registry API gives you a tree structure to start with, as shown in Figure 1. This makes it quite easy for you to partition the preference settings information according to its logical grouping as you do when you're using Visual Studio.

Figure 1 Visual Studio Registry Settings

Encapsulation of Data Location

If you store your preferences data in files, you'll end up maintaining the preferences data plus the location where you store them. That is, at some point you have to hardcode the location (or at least the names) of the files in your source code or in your configuration files somewhere. This means that your preferences code will work correctly only if your files are exactly in the location you specified. If the files are missing for some reason, so is the backing store for your preferences data.

Compare this to APIs such as the registry or isolated storage which shield you from the location of the data store. Unlike files, they are always there. Barring the absence of the required permissions, the registry predefined keys are always available to you. With isolated storage, you can always do the following to get a data store that is isolated by user and by assembly:

IsolatedStorageFile isfAssem = IsolatedStorageFile.GetUserStoreForAssembly();

The following code will get a data store isolated by user, assembly, and application domain:

IsolatedStorageFile isfDomain = IsolatedStorageFile.GetUserStoreForDomain();

This way, there's no question of whether your data store is there or not. You don't need to worry about possible cases of missing registry or missing isolated storage, and the need to hardcode a storage-specific location in your code is reduced.

Shielding from Backing Store Details

A good API allows you to concentrate on the task at hand so that when you're dealing with preferences you don't have to worry about the details of the backing store such as database tables or files and directories. In this regard, the registry API is more preferences-friendly than the isolated storage API. To illustrate this, let's consider a person who's developing a .NET-based messenger application called the Imaginary Messenger. This developer may decide to organize the user preferences as shown in Figure 2.

Figure 2** Imaginary Messenger Options **

Now how would you go about storing this preference data? Based on Figure 2, if you're using the registry, you may organize it as shown in Figure 3. Following is the code that created the registry keys and values in Figure 3:

RegistryKey imKey = Registry.CurrentUser.CreateSubKey(@"Software\ImaginaryMessenger"); RegistryKey optionsPersonalKey = imKey.CreateSubKey(@"Options\Personal"); optionsPersonalKey.Close(); RegistryKey optionsPrivacyKey = imKey.CreateSubKey(@"Options\Privacy"); optionsPrivacyKey.SetValue("AllowNonFriendsToSeeStatus", 0); optionsPrivacyKey.SetValue("LoginAsInvisible", 1); optionsPrivacyKey.SetValue("ShowIdleTimeToFriends", 1); optionsPrivacyKey.Close();

Figure 3 Organized Preferences in the Registry

Although this registry code is clean and straightforward, the equivalent code for isolated storage won't be because, unlike the registry, you won't find a Hashtable-like interface with which to set the values and their names. Instead, you'd want to store each value in a separate file, as shown in Figure 4. You could also put the values into a Hashtable and serialize it into a file like this:

Hashtable ht = new Hashtable(); ht.Add("AllowNonFriendsToSeeStatus", 0); ht.Add("LoginAsInvisible", 1); ht.Add("ShowIdleTimeToFriends", 1); Stream streamToValues = new IsolatedStorageFileStream( @"Options\Privacy\values", FileMode.OpenOrCreate, isf); IFormatter formatter = new SoapFormatter(); formatter.Serialize(streamToValues, ht); streamToValues.Close();

Figure 4 Creating the Isolated Storage

IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForDomain(); isf.CreateDirectory(@"Options"); isf.CreateDirectory(@"Options\Personal"); isf.CreateDirectory(@"Options\Privacy"); // handle preferences under Privacy now... TextWriter writer = new StreamWriter(new IsolatedStorageFileStream( @"Options\Privacy\AllowNonFriendsToSeeStatus", FileMode.OpenOrCreate, isf)); writer.Write(0); writer.Close(); TextWriter writer2 = new StreamWriter(new IsolatedStorageFileStream( @"Options\Privacy\LoginAsInvisible", FileMode.OpenOrCreate, isf)); writer2.Write(1); writer2.Close(); TextWriter writer3 = new StreamWriter(new IsolatedStorageFileStream( @"Options\Privacy\ShowIdleTimeToFriends", FileMode.OpenOrCreate, isf)); writer3.Write(1); writer3.Close(); isf.Close();

Of course, there are a multitude of other ways you can store these values. The point remains, though, that since the isolated storage API is more like a file I/O API than a preferences API, eventually you'll end up writing code to make sense of the contents of the files inside the store. The registry presents a simpler, friendlier interface in this case.

XmlSerializer and Preferences Management

Some of you may be wondering why I don't simply create a class to hold those values and then use XmlSerializer to read and write the class as a whole into a file in the isolated store (see Figure 5). In fact, this approach offers many advantages. You get strong typing for your preferences, which is good. Also, thanks to the forgiving nature of XmlSerializer, adding or removing fields from your preferences class doesn't invalidate the previous serialized versions of the class. It just works as expected most of the time, without requiring you to do any extra work for custom serialization the way BinaryFormatter or SoapFormatter might.

Figure 5 XmlSerializer Stores and Loads Preferences

public class OptionsSettings { private OptionsPersonalSettings personal; private OptionsPrivacySettings privacy; public OptionsSettings() { personal = new OptionsPersonalSettings(); privacy = new OptionsPrivacySettings(); } public OptionsPersonalSettings Personal {...} public OptionsPrivacySettings Privacy {...} public void Store(Stream stream) { XmlSerializer xs = new XmlSerializer(this.GetType()); xs.Serialize(stream, this); } public static OptionsSettings Load(Stream stream) { XmlSerializer xs = new XmlSerializer(typeof(OptionsSettings)); return (OptionsSettings)xs.Deserialize(stream); } } public class OptionsPersonalSettings {...} public class OptionsPrivacySettings { private bool allowNonFriendsToSeeStatus; private bool loginAsInvisible; private bool showIdleTimeToFriends; public bool AllowNonFriendsToSeeStatus {...} public bool LoginAsInvisible {...} public bool ShowIdleTimeToFriends {...} } public class TestSettings { public static void RunTestSettings() { OptionsSettings os = new OptionsSettings(); os.Privacy.AllowNonFriendsToSeeStatus = false; os.Privacy.LoginAsInvisible = true; os.Privacy.ShowIdleTimeToFriends = true; IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForAssembly(); isf.CreateDirectory(@"Options"); Stream stream = new IsolatedStorageFileStream(@"Options\Settings", FileMode.OpenOrCreate, isf); os.Store(stream); stream.Close(); Stream stream2 = new IsolatedStorageFileStream(@"Options\Settings", FileMode.Open, isf); OptionsSettings os2 = OptionsSettings.Load(stream2); stream2.Close(); isf.Close(); } }

Additionally, the directory structures that were coded as paths are now expressed as the relationships between a class and its properties. This means you would use OptionSettings.Privacy.AllowNonFriendsToSeeStatus instead of getting it from the file Options\Privacy\AllowNonFriendsToSeeStatus. This method also enables your application to use IntelliSense® for preferences.

This doesn't obviate the need for a generic preferences API, though. You still need to partition your data, especially when your applications get bigger. Instead of having one huge class that contains every single preference value, you may need to break the preference data into several smaller classes, making each of them more manageable. Also, a preferences API can encapsulate these serialization details into something like this:

// get the node that corresponds to // Options from the preferences tree IPreferencesNode options = IPreferencesNode.GetNodeForPath("/Options"); OptionsSettings os = new OptionSettings(); // set the preferences values here... // serialize the class into the preferences tree. options["Settings"] = os;

Although the registry API feels more natural for managing preferences, it is tied to its backing store, namely, the registry. There is no way you can use the API with another storage mechanism such as the file system or the isolated storage. A change in the backing store will be disruptive to your code because the interface changes. A good preferences API shields you from these details, allowing a change of backing store with minimal code change.

Preference Change Notification

Depending on the nature of your application, sometimes you want to get a notification when some preference data has changed. If the API you're using doesn't support event notification, then you need to raise an event manually whenever you're changing the preference value. If the API does support event notification, then you can just update the preference value and be confident that interested parties will be notified properly.

The APIs available to you have varying degrees of support for events. If you're using the file system, it is possible to monitor changes using the System.IO.FileSystemWatcher class, although the notification may be too coarse because monitoring happens at the file level. If you're using the registry, the .NET version of its API (in the Microsoft.Win32 namespace) doesn't directly support events, so you have to resort to the Win32® function RegNotifyChangeKeyValue to achieve this. Isolated storage doesn't directly expose any events either, although it is theoretically possible to use the FileSystemWatcher to monitor the isolated storage files and raise events, since isolated storage uses the file system underneath anyway.

Data Isolation

So far I've covered things that make an API suitable for dealing with preferences. Other than the API, you also need a backing store to put the preferences data into. There really isn't any perfect backing store. For instance, unlike isolated storage, the registry and the file system offer no isolation from other applications, so you need to take precautions to minimize the likelihood of other applications corrupting your data. But on the other hand, there are scenarios in which you can't use isolated storage. Your choice here really depends on your needs.

The file system and the registry do provide some degree of isolation, but the isolation is by user only. In the file system, the Environment.SpecialFolder.LocalApplicationData special folder is tied to a specific Windows user. Likewise, every Windows user gets their own HKEY_CURRENT_USER key in the Registry, so whatever you put in there is isolated by user.

Beyond that, there's only what I'll call "isolation by convention." That is, you can expect an application from Example Inc. to behave nicely and store its data under HKEY_CURRENT_ USER\Software\Example\UselessApp, but it's always possible that it accidentally writes into the parent key, HKEY_CURRENT_USER\Software, messing with the data of other applications.

Isolated storage greatly reduces the likelihood that an application will write its data to the wrong registry key. Given the right isolation scope, there's no way another application can even touch your data through the isolated storage API. With sufficient permissions, an application can always go directly to the file system and corrupt data it finds there, but it has to be intentionally malicious to do that. Moreover, if a malicious application has already obtained access to the file system like that, all bets are off anyway.

For reasons like these it's critical to choose the right isolation scope. If you choose to isolate your data by user and by assembly only, applications that share assemblies can access one another's data, so you have the isolation by convention problem all over again. To prevent this, you should isolate by user, assembly, and application domain. On the other hand, isolation by user and assembly allows multiple applications to share common data in the assembly's store, which may be what you need.

Security

Isolated storage reduces the potential amount of damage that applications can do to a system. In the same way an application's isolated store keeps other applications out, it also prevents the application itself from damaging other application's data stores. Also, administrators can impose quotas on the amount of isolated storage that can be used, which further reduces the application's potential for damaging the system.

In contrast, poorly written or malicious applications that have access to the file system or the registry can do substantial damage to your system. This makes isolated storage a good choice for a wider range of applications. In some cases, it's the only choice. For instance, by default controls downloaded from the Internet are not allowed to access the file system directly, nor are they allowed to read from or write to the registry, but they can use isolated storage to store their data safely.

Application Preferences Shared by All Users

There are some situations in which the isolated storage is too isolated, such as when you need to store application-level preferences that are shared by all users. With the registry, you can store your app-wide preferences under the HKEY_LOCAL_MACHINE\Software tree. In the file system, you can store your application's shared preferences under the Environment.SpecialFolder.CommonApplicationData special folder, but there is no way to do this with isolated storage because an isolated store is always isolated by user.

Note that a .NET-based application can have an application configuration file which you can access programmatically through the classes in the System.Configuration namespace. However, they have read-only access; System.Configuration will allow you to read the values in, but not write the values out. It may not be practical and it may not even make sense to store preferences values that are shared by all users in application configuration files.

An Easier Way to Manage Preferences

By now it may seem like there is no good all-around solution for preferences management in the .NET Framework. Each of the backing stores has its own strengths and weaknesses, so you have to choose which one to use according to your needs.

Considering this, it seems that an API that is designed for preferences management, and that allows you to choose and switch the backing store according to your needs, would simplify some of the problems. Figure 6 depicts just such an API. With this API, applications never talk to the backing store directly for preferences management. They only interact with the API layer of the Preferences library, which in turn talks to the backing store through a set of well-defined interfaces that I've dubbed the Preferences Service Provider Interface (SPI).

Figure 6 Preferences API for .NET

This design allows application developers to worry about preferences management instead of storage-specific details (which are hidden inside the backing store-specific implementations of the Preferences SPI). Also, since the API layer only interacts with the backing store through the interfaces defined in the SPI layer, you can easily switch to another backing store (or a more efficient implementation of the same back-ing store) with minimal changes to your preferences management code.

Introducing NAP

.NET Application Preferences (NAP) is my own implementation of the API shown in Figure 6. If you have a background in Java, you'll probably find this class library quite similar to the Java Preferences API. Indeed, I wrote this library because I missed the convenience of the Java Preferences API in .NET, yet I wanted to have control over which backing store to use in my applications (the Java Preferences API doesn't expose a standard way to do this). The following code snippet uses the registry to store the same set of preferences you saw in Figure 3:

IPreferencesNode imNode = PreferencesFactory.GetUserRootNode(RegistryStore.Instance); IPreferencesNode optionsPersonalNode = imNode.GetNodeForPath("/Options/Personal"); IPreferencesNode optionsPrivacyNode = imNode.GetNodeForPath("/Options/Privacy"); optionsPrivacyNode["AllowNonFriendsToSeeStatus"] = 0; optionsPrivacyNode["LoginAsInvisible"] = 1; optionsPrivacyNode["ShowIdleTimeToFriends"] = 1;

The result of running this code is shown in Figure 7.

Figure 7 .NET Application Preferences

Note the garbled key names below the Nap\Preferences key. NAP is smart enough to simulate the isolation features of System.IO.IsolatedStorage, so it's unlikely that applications will be able to see (and corrupt) each other's data. Also, NAP makes changing the backing store very easy. To use the file system, only one line needs to be changed, from

IPreferencesNode imNode = PreferencesFactory.GetUserRootNode(RegistryStore.Instance);

to

IPreferencesNode imNode = PreferencesFactory.GetUserRootNode(WindowsFileSystemStore.Instance);

or to use isolated storage:

IPreferencesNode imNode = PreferencesFactory.GetUserRootNode(ISUserStore.Instance);

Note the name ISUserStore, which was chosen because isolated storage doesn't support stores that can be shared by all users. The registry and the file system, however, don't have such a limitation. Again, you only need to change one line of code to get a system node (a preferences node shared by all users):

IPreferencesNode imNode = PreferencesFactory.GetSystemRootNode(RegistryStore.Instance);

Or you can do the following:

IPreferencesNode imNode = PreferencesFactory.GetSystemRootNode(WindowsFileSystemStore.Instance);

This way, you get preferences trees that can be shared by users, yet which are completely isolated by assembly and application domain to prevent applications from corrupting each other's data. (Calling GetSystemRootNode with ISUserStore will throw a NotSupportedException.)

Note that I'm passing an object, not an enumeration, into GetSystemRootNode and GetUserRootNode. This makes it possible for NAP to accept a new backing store provider without having to be recompiled. Simply create the required provider code and pass the backing store provider instance in, like this:

IPreferencesNode imNode = PreferencesFactory.GetSystemRootNode(SQLServerStore.Instance);

Tree Nodes and Leaves

Once you've got your root node, traversing the tree is easy. It's almost like using the command prompt in that you supply paths, which can be either relative or absolute. The best way of illustrating this would be through a code snippet, shown here in Visual Basic:

Dim userRoot As IPreferencesNode = _ PreferencesFactory.GetUserRootNode(RegistryStore.Instance) Dim one As IPreferencesNode = _ userRoot.GetNodeForPath("one") Dim one_a As IPreferencesNode = _ one.GetNodeForPath("a") Dim one_a_i As IPreferencesNode = _ one_a.GetNodeForPath("i") Dim two As IPreferencesNode = _ userRoot.GetNodeForPath("/two") Dim two_a As IPreferencesNode = _ userRoot.GetNodeForPath(" two a") Dim two_a_i As IPreferencesNode = _ userRoot.GetNodeForPath("/two/a/i")

Just like in the file system, a path can be absolute or relative. If it's absolute, the tree is traversed starting from the root. If it's relative, the traversal starts from the node on which GetNodeForPath is called. The result of running the previous code snippet is shown in Figure 8.

Figure 8 Create Node with Absolute Path

Figure 8** Create Node with Absolute Path **

Now that I've got a node, I need to do something with the leaves under that node. Again, NAP makes this very simple. Each leaf has a name and a value associated with it. Dealing with NAP leaves is just like dealing with key-value pairs in a Hashtable. To create a new leaf or modify an existing one, you use the indexer as follows:

one("someBoolValue") = True one_a("singleValue") = 234.534

For the OptionsSettings class that was shown in Figure 5, you can do the following:

Dim os As New OptionsSettings os.Privacy.AllowNonFriendsToSeeStatus = False os.Privacy.LoginAsInvisible = False os.Privacy.ShowIdleTimeToFriends = False one_a_i("optionsSettings") = os

Under the covers, this instance of the OptionsSettings class is automatically serialized using XmlSerializer. Retrieving it is equally easy, as you can see here:

Dim os2 As OptionsSettings = _ one_a_i.GetSerializedObject("optionsSettings", _ GetType(OptionsSettings), Nothing)

Of course, just as you can create nodes and leaves, you can remove them. The methods to do this are pretty straightforward:

' remove all leaves/preferences under one one.RemoveAllPreferences() ' remove a specific leaf one_a_i.RemovePreference("optionsSettings") ' remove the whole branch from the tree, starting from this node one.RemoveNode()

What's Happening Under the Tree?

Another useful feature of NAP is that it allows you to install event handlers to find out what's happening under the preferences tree. For instance, in a GUI application, you may want to find out when a UI-related preference is changed so that you can immediately update the application's appearance accordingly. It is also sometimes useful to know when a child node has been added under a node or removed from under one. The following code snippet illustrates this in C#:

one["someBoolValue"] = true; // we want to know when there's a change in the preference one.PreferenceChanged += new Nap.Event.PreferenceChangeEventHandler(one_PreferenceChanged); one["someBoolValue"] = false;

The handler method looks like this:

private static void one_PreferenceChanged(object sender, Nap.Event.PreferenceChangeEventArgs args) { Console.WriteLine(args.Key); Console.WriteLine(args.NewValue); }

This will print "someBoolValue" and "false" to the console window if you run the code snippet that precedes it. The other two events are ChildNodeAdded and ChildNodeRemoved, which will be fired by a node when it has a new subnode added to it or removed from it. The following code snippet illustrates the handling of the event being raised when a subnode is added to a node:

IPreferencesNode two = userRoot.GetNodeForPath("/two"); two.ChildNodeAdded += new Nap.Event.ChildNodeEventHandler(two_ChildNodeAdded); IPreferencesNode two_a = userRoot.GetNodeForPath("/two/a");

The handler method looks like this:

private static void two_ChildNodeAdded(object sender, Nap.Event.ChildNodeEventArgs args) { Console.WriteLine(args.ChildNode.Name); }

This will print "a" to the console window in this case.

That's all there is to know about using NAP. There are other methods and properties, but they are mostly self-documenting (such as the Root property returning the root node, and the Parent property returning the parent node). Now all that's left is to explore how NAP implements isolation.

Simulating Isolation by Assembly

The isolated storage offers isolation by user, by assembly, and by application domain. User isolation is easy. For the file system, there's the special folder Environment.SpecialFolder.LocalApplicationData, and for the registry there's HKEY_CURRENT_USER. This still leaves you with simulating isolation by assembly and isolation by application domain.

The next question is, what constitutes an assembly's identity? As it turns out, the answers to these questions depend on what you're trying to achieve. For instance, the Hash class in the namespace System.Security.Policy provides an assembly identity that is not ambiguous, but for this purpose it is so unambiguous that it is not useful. If you fix a bug in your code, thereby changing the resulting Microsoft intermediate language (MSIL), according to the Hash class that's a different assembly. If you change a single character in a string literal, that's yet another assembly. Also, consider the nightmare you'll have during development when each revision or bug fix generates a different assembly, rendering all the previously stored data inaccessible.

How about using an assembly's full name as its identity? When an assembly is not strongly named, its full name looks like this:

test, Version=1.0.1474.24153, Culture=neutral, PublicKeyToken=null

By default, the build number and revision number parts of Version will change with every build, so identity based on full name will change also, but you can address this by controlling the version number manually, like so:

[assembly: AssemblyVersion("1.0.0.0")]

Again, this may not be the behavior you would expect. During development, you may want to raise the version number while retaining access to the stored data of the previous builds.

Isolated storage uses a kind of identity that turns out to be quite reliable and practical most of the time. As the .NET Framework documentation makes clear, assembly identity is the evidence of the assembly. This might come from a cryptographic digital signature, the software publisher of the assembly, or from its URL identity. If, for example, an assembly has both a strong name and a software publisher identity, the software publisher identity is the one that is used; if the assembly comes from the Internet and is unsigned, the URL identity is used instead.

During development, if the assembly is unsigned, as long as you're building to the same location (for example, C:\Acme\SpaceInvaders\bin\Debug\Start.exe), the assembly is always considered the same assembly, no matter how many times you perform bug fixes or add features. Now what if the assembly has a strong name? The following code is the result of calling the ToString method on a StrongName instance:

<StrongName version="1" Key="DB5F93741AD8DDC3C03E077E1D9B..." Name="ConsoleApplication1" Version="1.0.1474.39142"/>

The strong name still contains the assembly's version number, which may change with every build. This means that if the strong name is used as an identity, every build may generate a different assembly as well. Fortunately, isolated storage only considers a strong-named assembly different if there is a change in the major version number. To isolated storage, version 1.2.1453.23409 of MyApplication and version 1.9.1498.24890 of MyApplication are the same assembly, but versions 2.0.1453.23409 and 1.0.1453.23409 are different. With information from the .NET Framework Developer's Guide, it was quite easy to code my own isolation by assembly. The code to do this is shown in Figure 9.

Figure 9 Getting the Identity String of an Assembly

public static string GetAssemblyIdentity(Assembly assembly) { string assemblyIdentityBase32String = null; foreach(object ogj in assembly.Evidence){ // If an assembly has both a strong name and a software // publisher identity, then the software publisher identity // is used. else if(obj is Publisher) { Publisher pub = (Publisher)obj; X509Certificate cert = pub.Certificate; assemblyIdentityBase32String = "Publisher." + Base32.ToBase32String(cert.GetCertHash()); break; } else if(obj is StrongName) { StrongName sn = (StrongName)obj; StringBuilder sb = new StringBuilder(sn.Name); sb.Append(sn.PublicKey.ToString()); sb.Append(sn.Version.Major); assemblyIdentityBase32String = "StrongName." + Base32EncodedHash.GetFromString(sb.ToString()); break; } // If the assembly comes from the Internet and is unsigned, // the URL identity is used. else if(obj is Url) { Url url = (Url)obj; assemblyIdentityBase32String = "Url." + Base32EncodedHash.GetFromString(url.Value); break; } } if(assemblyIdentityBase32String == null) { // fall back to assembly's code base assemblyIdentityBase32String = Base32EncodedHash.GetFromString(assembly.EscapedCodeBase); } return assemblyIdentityBase32String; }

I chose the Base32 encoding used in Figure 9 because it is case insensitive, just like Windows directory names and registry keys. Also, the character set used doesn't contain any character that would be illegal for a Windows directory name or a registry key name (such as \). You can take a look at the Base32 encoder implementation in the NAP source code, which is available with the download for this article.

Simulating Isolation by Application Domain

How about application domain identity? According to the .NET Framework Developer's Guide, domain identity represents the evidence of the application, which might be the full URL in a Web application. For code that is hosted by the shell, the domain identity might be based on the application directory path. For example, if the executable runs from the path C:\Office\MyApp.exe, the domain identity would be C:\Office\MyApp.exe.

So, simulating isolation by app domain is even simpler than isolation by assembly. Figure 10 shows the code for generating a string that corresponds to app domain identity.

Figure 10 Getting the Identity String of an App Domain

public static string GetDomainIdentity(AppDomain appDomain) { string domainIdentityBase32String = null; foreach(object obj in appDomain.Evidence){ if(obj is Url) { Url url = (Url)obj; domainIdentityBase32String = "Url." + Base32EncodedHash.GetFromString(url.Value); break; } } if(domainIdentityBase32String == null) { // Unlikely, but fall back, just in case Url is not there domainIdentityBase32String = Base32EncodedHash.GetFromString(appDomain.ToString()); } return domainIdentityBase32String; }

Conclusion

In this article, I've discussed the features that a preferences-friendly API should have. I also reviewed the backing stores that are available to most .NET-based applications and showed how deciding on whether to use the file system, the registry, or isolated storage really depends on your needs.

As I hope I've made clear, my .NET Preferences library allows you to choose or switch to any backing store as necessary, without drastic changes in your code. I've shown how to use this library and how you can simulate the isolation features of .NET isolated storage. I am not saying that NAP is the perfect API for preferences management, but I've found it to be very useful in all of my preferences-related programming tasks so far, and I'm confident that you'll find it useful as well.

Ray Djajadinata is a Senior Software Engineer at Savi Technology. With the engineering team, he deals with supply chain and asset management applications for large enterprises. You can reach Ray at rayfd_2000@yahoo.com.