次の方法で共有


User Context

Besides logging, one of the most common types of ambient context is the user. Who is the user? Was the user authenticated? What is the user allowed to do?

Since being able to answer these questions are such common requirements in software development, the BCL includes the IPrincipal interface that allows you to create implementations to meet that goal. In many cases, however, you need to know more about your user than just a name, authentication flag and role. You may need separate properties for first and last name, title, the user's age, gender or color preference, contact information, relationships to other users, etc.

Obviously, you can define your own user object that implements IPrincipal and IIdentity while also containing all this extra information. If you do this, here's a couple of design guidelines you should consider:

  • If you have any sort of volatile dependency in your User object, you should consider making the definition of your class abstract and otherwise abstracting away the dependencies. A simple example concerns the extraction of information. Imagine that you have a database that contains the user's first and last name, as well as a user ID. When you first create the User object, you may only know the user ID, and since you expect that not all callers will ever access the FirstName and LastName properties, you may want to implement lazy loading from the database. However, you should really make those properties abstract, so that other implementations (such as a test double) of your User class can implement different behavior. 
  • If the distinction between IPrincipal and IIdentity doesn't make a lot of sense to you, you can easily let your User class implement both interfaces. 
  • Consider creating a static convenience property for accessing the underlying user-specific store (see the example below).

Let's look at an example illustrating these guidelines. Imagine that you have a need to track and access the user's first and last name, as well as a display name:

 public abstract partial class User : IPrincipal, IIdentity
{
    protected User()
    {
    }
 
    public abstract string FirstName { get; }
 
    public abstract string LastName { get; }
 
    public abstract string DisplayName { get; }
 
    // More members...
}

The class is abstract since I want to give inheritors some leeway in how they implement the behavior. In a web application, a derived class may look up the user's properties in a table, perhaps even utilizing lazy loading. An enterprise application may look up the data in Active Directory. A federated service may extract the data from a SAML token. Unit testers will probably want to create a stub or similar.

When implementing IPrincipal and IIdentity, a question that sometimes arises regards the distinction between IPrincipal and IIdentity: Which behavior and data should be implemented where? Should I create both a UserPrincipal and a UserIdentity class? In some cases, it makes sense, while in others, it doesn't. If it doesn't make sense to you in your particular case, you can collapse the implementation of both interfaces into the same class. Notice how in the declaration of the User class, I specified that it implements both IPrincipal and IIdentity. Here's the abstract implementation:

 #region IPrincipal Members
  
 public IIdentity Identity
 {
     get { return this; }
 }
  
 public abstract bool IsInRole(string role);
  
 #endregion
  
 #region IIdentity Members
  
 public abstract string AuthenticationType { get; }
  
 public abstract bool IsAuthenticated { get; }
  
 public string Name
 {
     get { return this.DisplayName; }
 }
  
 #endregion

The most tricky aspect is linking the two interfaces to each other via the Identity property, and as you can see, that's dead simple: Just return the object itself. Again, I've cheated a bit since I've just delegated most of the rest of the interface implementation to child classes, but that makes sense, since inheritors may want to implement this functionality in quite different ways: If the backing store is AD, you can basically just wrap WindowsPrincipal and WindowsIdentity. If the backing store is a database, you may want to implement IsInRole with a (cached) stored procedure call. And once more, unit testers will just want to use some sort of test double.

Storing the User object in a user context can obviously be done manually. On Thread.CurrentPrincipal, it's as easy as this:

 Thread.CurrentPrincipal = new FakeUser("Jane", "Doe");

Retriving the User instance, on the other hand, requires a bit more code:

 User u = Thread.CurrentPrincipal as User;
 if (u == null)
 {
     throw new InvalidOperationException("Boo hiss");
 }
 // Use u

It's straightforward code, but tedious to have to write over and over again. At this point, many people might be tempted to move this code to a static method on a helper class, but object-oriented design principles recommend that you locate behavior in the class where it belongs - in this case the User class:

 public static User Current
 {
     get
     {
         /* Possibly use further abstraction here to
          * enable use of HttpContext or similar, as
          * described in previous post. */
         User u = Thread.CurrentPrincipal as User;
         if (u == null)
         {
             // Alternatively use Null Object pattern here.
             throw new InvalidOperationException(
                 "CurrentPrincipal is not a User instance.");
         }
         return u;
     }
     set
     {
         if (value == null)
         {
             throw new ArgumentNullException("value");
         }
         /* Possibly use further abstraction here to
          * enable use of HttpContext or similar, as
          * described in previous post. */
         Thread.CurrentPrincipal = value;
     }
 }

The static Current property is just a convenience property that encapsulates the behavior described above. In this example, I'm just using Thread.CurrentPrincipal as is, but as described in one of my previous posts, this will not work in ASP.NET. For full flexibility, you could then modify the Current property to use an abstract context container for storing and accessing the IPrincipal instance, as is also described in that post.

In this implementation, I'm also throwing an InvalidOperationException if the current IPrincipal context is not a User instance. One alternative to that would be to return a Null Object, in this case perhaps something like this:

 internal class NullUser : User
 {
     internal NullUser()
         : base()
     {
     }
  
     public override string FirstName
     {
         get { return string.Empty; }
     }
  
     public override string LastName
     {
         get { return string.Empty; }
     }
  
     public override string DisplayName
     {
         get { return "Unknown"; }
     }
  
     public override bool IsInRole(string role)
     {
         return false;
     }
  
     public override string AuthenticationType
     {
         get { return "Null"; }
     }
  
     public override bool IsAuthenticated
     {
         get { return false; }
     }
 }

When using a Null Object, however, you should consider whether hiding another IPrincipal instance is the desired behavior. If the current principal is null, it's pretty easy, but it's more difficult to decide on the correct behavior when the current instance implements IPrincipal, but doesn't derive from User. Implicitly suppressing data in this way may lead to logical bugs or unintended behavior, which is why I chose to throw an exception in the above example.

The static Current property provides a convenient and safe way to code against the current user context, since it allows you to write code like this:

 string firstName = User.Current.FirstName;

Writing to the User instance is just as easy, if you have writable properties.

A custom User class can evidently be much more complex than the above example, containing complex behavior and relationships to other users, etc. In any case, the general approach outlined here provides a few guidelines for effectively modeling your application's user context.

Comments