カスタムメンバシッププロバイダを作成します。今回作成するメンバシッププロバイダはxmlファイルをユーザ情報のストアとします。今回は第一弾として、ユーザ情報を現すクラスを定義します。XmlSerializerを使用したサンプルについては、XmlSerializerを使ってインスタンスをXML形式でシリアライズする を参照して下さい。

1.XmlMembershipProviderの仕様

今回作成する仕様はMembershipプロバイダでサポートするメソッドをすべてサポートしていません。以下の機能はサポートしません。

  • maxInvalidPasswordAttempts, passwordAttemptWindowを設定することによる自動ロックアウト
  • applicationName指定によるアプリケーションごとのユーザの管理
  • requireUniqueEmailを設定した場合の重複チェック

 機能として実装してはいませんが、プログラムの修正、追加をすれば対応することができると思います。

2.ユーザ情報のドメインデータ作成

2.1 作成するスキーマ

今回作成するユーザストアのスキーマは次のようになります。usersがルート要素で、各ユーザごとにuser要素のタグが作成されます。user要素のname属性がユーザIDを現します。applicationnameはアプリケーション名を現します。子属性はMembershipUserを構成するのに必要なプロパティとパスワード情報から構成されます。

<?xml version="1.0"?>
<users xmlns="urn:Handcraft.UserInfoList">
  <user xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" applicationName="/" name="test1" xmlns="urn:Handcraft.UserInfo">
    <approved>true</approved>
    <lockouted>false</lockouted>
    <comment />
    <email />
    <providerUserKey>69dfc154-0f50-495c-85e3-ace0730a947b</providerUserKey>
    <creationDate>2008-10-05T16:38:36.852297+09:00</creationDate>
    <lastActivityDate>2008-10-05T16:38:36.852297+09:00</lastActivityDate>
    <lastLockoutDate>1754-01-01T00:00:00</lastLockoutDate>
    <lastLoginDate>2008-10-05T16:38:36.852297+09:00</lastLoginDate>
    <lastPasswordChangedDate>2008-10-05T16:38:36.852297+09:00</lastPasswordChangedDate>
    <passwordQuestion />
    <passwordAnswer />
    <password>HJ9Lwvnk6rC3hDfXvIe8huCgLvI=</password>
    <passwordSalt>sA==</passwordSalt>
  </user>
  <user xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" applicationName="/" name="test2" xmlns="urn:Handcraft.UserInfo">
    <approved>true</approved>
    <lockouted>false</lockouted>
    <comment />
    <email />
    <providerUserKey>dfe453bf-468d-4e62-89f6-cecb2f1e5650</providerUserKey>
    <creationDate>2008-10-05T16:38:36.877686+09:00</creationDate>
    <lastActivityDate>2008-10-05T16:38:36.877686+09:00</lastActivityDate>
    <lastLockoutDate>1754-01-01T00:00:00</lastLockoutDate>
    <lastLoginDate>2008-10-05T16:38:36.877686+09:00</lastLoginDate>
    <lastPasswordChangedDate>2008-10-05T16:38:36.877686+09:00</lastPasswordChangedDate>
    <passwordQuestion />
    <passwordAnswer />
    <password>/ZIv8g/wxhkH2LAymE+b17wg3fA=</password>
    <passwordSalt>QQ==</passwordSalt>
  </user>
</users>

2.2 ソリューションの作成

2.1で記載したスキーマに一致するXmlSerializerでシリアライズ可能なクラスを作成します。各クラスにはXmlMembershipProviderから利用されるメソッドも定義しています。Visual Studioを起動し、空のソリューションを作成します。ソリューション名はCustomProviderとします。

作成したソリューションにクラスライブラリプロジェクトCustomProviderを新規します。既定で作成されるClass1.csを削除し、各ユーザ情報を現すクラスUserInfoクラスを定義するために、UserInfo.csファイルを新規作成し、次のように編集します。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Serialization;
using System.Web.Security;
using System.Text.RegularExpressions;

namespace CustomProvider
{
    [XmlRoot("user", Namespace = "urn:Handcraft.UserInfo")]
    public class UserInfo
    {
        public UserInfo()
        {
            ApplicationName = "/";
            UserName = string.Empty;
            IsApproved = false;
            IsLockedOut = false;
            Comment = string.Empty;
            Email = string.Empty;
            ProviderUserKey = Guid.Empty;
            CreationDate = MinumDateTime();
            LastActivityDate = MinumDateTime();
            LastLockoutDate = MinumDateTime();
            LastLoginDate = MinumDateTime();
            LastPasswordChangedDate = MinumDateTime();
            PasswordQuestion = string.Empty;
            EncodedPasswordAnswer = string.Empty;
            EncodedPassword = string.Empty;
            PasswordSalt = string.Empty;
        }
        public static UserInfo CreateUserInfo(string applicationName, string userName, bool isApproved, bool isLockedOut,
                        string comment, string email, Guid providerUserKey, string passwordQuestion, string encodedPasswordAnswer, string encodedPassword, string passwordSalt)
        {
            UserInfo user = new UserInfo();
            user.ApplicationName = applicationName;
            user.UserName = userName;
            user.IsApproved = isApproved;
            user.IsLockedOut = isLockedOut;
            user.Comment = comment;
            user.Email = email;
            user.ProviderUserKey = providerUserKey;
            user.PasswordQuestion = passwordQuestion;
            user.EncodedPasswordAnswer = encodedPasswordAnswer;
            user.EncodedPassword = encodedPassword;
            user.PasswordSalt = passwordSalt;

            DateTime now = DateTime.Now;
            user.CreationDate = now;
            if (isApproved)
            {
                user.LastActivityDate = now;
                user.LastLoginDate = now;
            }
            else
            {
                user.LastActivityDate = user.MinumDateTime();
                user.LastLoginDate = user.MinumDateTime();
            }

            if (isLockedOut) user.LastLockoutDate = now;
            else user.LastLockoutDate = user.MinumDateTime();

            user.LastPasswordChangedDate = now;

            return user;
        }

        /// <summary>アプリケーション名</summary>
        [XmlAttribute("applicationName")]
        public string ApplicationName { get; set; }
        /// <summary>ユーザID</summary>
        [XmlAttribute("name")]
        public string UserName { get; set; }
        /// <summary>承認済みか</summary>
        [XmlElement("approved")]
        public bool IsApproved { get; set; }
        /// <summary>ロックアウト中か</summary>
        [XmlElement("lockouted")]
        public bool IsLockedOut { get; set; }
        /// <summary>コメント</summary>
        [XmlElement("comment")]
        public string Comment { get; set; }
        /// <summary>email</summary>
        [XmlElement("email")]
        public string Email { get; set; }
        /// <summary>プロバイダユーザキー</summary>
        [XmlElement("providerUserKey")]
        public Guid ProviderUserKey { get; set; }
        [XmlElement("creationDate")]
        public DateTime CreationDate { get; set; }
        /// <summary>最終活動日時</summary>
        [XmlElement("lastActivityDate")]
        public DateTime LastActivityDate { get; set; }
        /// <summary>最終ロックアウト日時</summary>
        [XmlElement("lastLockoutDate")]
        public DateTime LastLockoutDate { get; set; }
        /// <summary>最終ログイン日時</summary>
        [XmlElement("lastLoginDate")]
        public DateTime LastLoginDate { get; set; }
        /// <summary>最終パスワード変更日時</summary>
        [XmlElement("lastPasswordChangedDate")]
        public DateTime LastPasswordChangedDate { get; set; }
        /// <summary>パスワードの質問</summary>
        [XmlElement("passwordQuestion")]
        public string PasswordQuestion { get; set; }
        /// <summary>パスワードの答え</summary>
        [XmlElement("passwordAnswer")]
        public string EncodedPasswordAnswer { get; set; }
        /// <summary>パスワード</summary>
        [XmlElement("password")]
        public string EncodedPassword { get; set; }
        /// <summary>パスワードサルト</summary>
        [XmlElement("passwordSalt")]
        public string PasswordSalt { get; set; }

        public virtual MembershipUser CreateMembershipUser(string providerName)
        {
            return new MembershipUser(providerName, UserName, ProviderUserKey, Email,
                                        PasswordQuestion, Comment, IsApproved, IsLockedOut,
                                        CreationDate, LastLoginDate, LastActivityDate,
                                        LastPasswordChangedDate, LastLockoutDate);
        }
        protected virtual DateTime MinumDateTime()
        {
            return new DateTime(1754, 1, 1, 0, 0, 0, 0);
        }
    }
}

次に、UserInfoList.csファイルを作成し、ユーザのコレクションを表すUserInfoListクラスを以下のように記述します。UserInfoListは作成するメンバシッププロバイダからさまざまな機能をデリゲートされるため、いくつかMembershipProviderと同じメソッドを定義しています。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Serialization;
using System.Web.Security;
using System.Text.RegularExpressions;

namespace CustomProvider
{
    [XmlRoot("users", Namespace = "urn:Handcraft.UserInfoList")]
    public class UserInfoList : IXmlSerializable
    {
        /// <summary>
        /// ユーザ情報を格納するコレクション
        /// キーにUserInfo.UserNameを使用する
        /// </summary>
        private SortedList<string, UserInfo> _users = new SortedList<string, UserInfo>();
        /// <summary>
        /// UserInfoを追加する
        /// </summary>
        /// <param name="userInfo"></param>
        public void AddUserInfo(UserInfo userInfo)
        {
            _users.Add(userInfo.UserName, userInfo);
        }
        public UserInfo GetUserInfo(string name)
        {
            try
            {
                return _users[name];
            }
            catch (KeyNotFoundException)
            {
                return null;
            }
        }
        public UserInfo GetUserInfo(Guid providerUserKey)
        {
            foreach (UserInfo user in _users.Values)
            {
                if (user.ProviderUserKey.Equals(providerUserKey)) return user;
            }
            return null;
        }
        public bool RemoveUserInfo(string name)
        {
            return _users.Remove(name);
        }
        public MembershipUserCollection FindUsersByEmail(string providerName, string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            IList<UserInfo> list = new List<UserInfo>();
            foreach (UserInfo user in _users.Values)
            {
                if (Regex.IsMatch(user.Email, emailToMatch))
                    list.Add(user);
            }

            totalRecords = list.Count;

            return SelectMembershipUser(providerName, pageIndex, pageSize, list);
        }

        public MembershipUserCollection FindUsersByName(string providerName, string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            IList<UserInfo> list = new List<UserInfo>();
            foreach (UserInfo user in _users.Values)
            {
                if (Regex.IsMatch(user.UserName, usernameToMatch))
                    list.Add(user);
            }
            totalRecords = list.Count;

            return SelectMembershipUser(providerName, pageIndex, pageSize, list);
        }

        public string GetUserNameByEmail(string email)
        {
            foreach (UserInfo user in _users.Values)
            {
                if (user.Email.Equals(email))
                    return user.UserName;
            }
            return null;

        }
        public MembershipUserCollection GetAllUsers(string providerName, int pageIndex, int pageSize, out int totalRecords)
        {
            IList<UserInfo> list = _users.Values;
            totalRecords = list.Count;

            return SelectMembershipUser(providerName, pageIndex, pageSize, list);
        }
        public int GetNumberOfUsersOnline(TimeSpan userIsOnlineTimeWindow)
        {
            DateTime compareDateTime = DateTime.Now.Subtract(userIsOnlineTimeWindow);
            int cnt = 0;
            foreach (UserInfo user in _users.Values)
            {
                if (user.LastActivityDate > compareDateTime)
                    ++cnt;
            }
            return cnt;
        }
        #region IXmlSerializable Membersの実装

        public System.Xml.Schema.XmlSchema GetSchema()
        {
            return null;
        }

        /// <summary>
        /// 独自でデシリアライズを行う
        /// </summary>
        /// <param name="reader"></param>
        public void ReadXml(System.Xml.XmlReader reader)
        {
            XmlSerializer s = new XmlSerializer(typeof(UserInfo));

            reader.Read();
            while (reader.NodeType != System.Xml.XmlNodeType.EndElement)
            {
                UserInfo o = s.Deserialize(reader) as UserInfo;
                AddUserInfo(o);
            }
        }

        /// <summary>
        /// 独自でシリアライズを行う
        /// </summary>
        /// <param name="writer"></param>
        public void WriteXml(System.Xml.XmlWriter writer)
        {
            XmlSerializer s = new XmlSerializer(typeof(UserInfo));
            foreach (UserInfo val in _users.Values)
            {
                s.Serialize(writer, val);
            }
        }
        /// <summary>
        /// 指定されたpageIndex,pageSizeに含まれる
        /// MembershipUserCollectionのリストを作成する。
        /// </summary>
        /// <param name="providerName"></param>
        /// <param name="pageIndex"></param>
        /// <param name="pageSize"></param>
        /// <param name="users"></param>
        /// <returns></returns>
        private MembershipUserCollection SelectMembershipUser(string providerName, int pageIndex, int pageSize, IList<UserInfo> users)
        {
            MembershipUserCollection membershipUsers = new MembershipUserCollection();

            if (pageIndex > users.Count) return membershipUsers;

            for (int i = pageIndex; i < (pageIndex + pageSize); ++i)
            {
                if (users.Count <= i) break;

                membershipUsers.Add(users[i].CreateMembershipUser(providerName));
            }
            return membershipUsers;
        }

        #endregion
    }
}

XmlSerializerでシリアライズするためにプロパティにXmlElementなどがアノテートされていますが、詳細についてはMSDNライブラリや本サイトのサンプルXmlSerializerを使ってインスタンスをXML形式でシリアライズする を参照して下さい。

今回の説明は以上です。次回はXmlMembershipProviderを実装してみます。