miso_soup3 Blog

主に ASP.NET 関連について書いています。

ユーザにロールを設定する

状況

  • ユーザにロールを設定する処理を、スマートに書きたい
  • ASP.NETメンバーシップのスキーマを利用
  • NHibernateを利用
  • アプリケーションで使用するロールは決まっている

やりたいこと

↓のようなコードではなく…
User user = _repository.FindByName(userName);

Role eatRole = _roleRepository.FindByRoleName("Eat");
Role cookRole = _roleRepository.FindByRoleName("Cook");

user.AddRole(eatRole);
user.AddRole(cookRole);

Commit();
↓のようなコードを書きたい
User user = _repository.FindByName(userName);

user.SetSurviveRole();

Commit();
Public class User
{
    private IList<Role> _roles = new List<Role>();

    Public void SetSurviveRole()
    {
        _roles.Add(Role.Eat);
        _roles.Add(Role.Cook);
    }
}
  • アプリケーションで利用するロールは既に決まっているので、ロールリポジトリは使用したくない
  • これだとテストも簡単

必要なこと

  • イミュータブルなRoleクラスを用意
  • Roleクラスには、データベースとマッピングするための識別子、Idをもたせる
public class Role
	{
		public Role()
		{
		}

		public Role(Guid id)
		{
			if (id == null || id == Guid.Empty)
				throw new ArgumentNullException();

			_id = id;
		}

		private readonly Guid _id;

		public readonly static Role Eat = new Role(Guid.Parse("7D115923-3C09-4225-B4F9-2AAD955AD1DC"));
		public readonly static Role Cook = new Role(Guid.Parse("72AB766F-71D8-4824-8750-9190BEFDCEF1"));

		/// <summary>
		/// Id
		/// </summary>
		public Guid Id
		{
			get { return _id; }
		}

		/// <summary>
		/// 名称
		/// </summary>
		public String Name
		{
			get
			{
				return _roleNames[_id];
			}
			set { }
		}

	....
	.... あとEqualsとか、ToStringなどのoverrideメソッド

	}
  • Userクラスの_rolesを、データベースとマッピングさせる
  • Userクラスの_rolesの型を、以下の用に変更
private Iesi.Collections.Generic.ISet<Role> _roles = new HashedSet<Role>();
  • 結合テーブル(aspnet_UsersInRoles)を使ったmany-to-manyのマッピングを行う
  • Userクラスのマッピングファイル User.hbm.xmlの中身
<class name="User" table="aspnet_Users" lazy="false">

		<id name="Id" column="UserId">
			<generator class="guid"></generator>
		</id>

		<set name="Roles" access="field.camelcase-underscore"
			 table="aspnet_UsersInRoles">
			<key column="UserId"/>
			<element column="RoleId"
					 type="{ネームスペース}.RoleType, {RoleTypeがあるアセンブリ名}">
			</element>
</class>

Userのマッピングについて

Userクラスの IList_roles に、 データベースの値に合う値をバインドするためには、
aspnet_UsersInRolesテーブルのRoleId(Guid)から、Roleクラスをインスタンス化しなくてはいけないし、
Roleクラスのインスタンスから、RoleId取得しないといけない。

つまり、RoleId は Roleクラス型だよ、みたいなカスタム型を作成しなくてはいけない。

ということで、カスタム型を実装する。
NHibernate 5.2.3. カスタム型

(aspnet_RolesテーブルとRoleクラスのマッピングを定義する方法もあるが、
それだとNHibernateがRoleの状態を管理しようとするらしく、よろしくないらしい。
RoleはEntityではなく、ValueObjectなので、上記の用にする)

カスタム型 RoleType

NHibernate.UserTypes.IUserTypeを実装したクラスを作成する

public class RoleType : IUserType
	{
		public object Assemble(object cached, object owner)
		{
			return DeepCopy(cached);
		}

		public object DeepCopy(object value)
		{
			return value;
		}

		public object Disassemble(object value)
		{
			return DeepCopy(value);
		}

		public bool Equals(object x, object y)
		{
			return OperatorHelper.Equal(x, y);
		}

		public int GetHashCode(object x)
		{
			if (x == null)
				return 0;
			return x.GetHashCode();
		}

		public bool IsMutable
		{
			get { return false; }
		}

		public object NullSafeGet(System.Data.IDataReader rs, string[] names, object owner)
		{
			Guid roleId = Guid.Empty;

			Guid.TryParse(Convert.ToString(NHibernateUtil.String.NullSafeGet(rs, names[0])), out roleId);

			if (roleId == Guid.Empty)
				return null;

			return Role.GetBy(roleId);
		}

		public void NullSafeSet(System.Data.IDbCommand cmd, object value, int index)
		{
			IDataParameter valueParam = (IDataParameter)cmd.Parameters[index];

			if (valueParam == null)
			{
				valueParam.Value = DBNull.Value;
			}
			else
			{
				valueParam.Value = ((Role)value).Id;
			}
		}

		public object Replace(object original, object target, object owner)
		{
			return original;
		}

		public Type ReturnedType
		{
			get { return typeof(Role); }
		}

		public global::NHibernate.SqlTypes.SqlType[] SqlTypes
		{
			get
			{
				return new global::NHibernate.SqlTypes.SqlType[] {
					new global::NHibernate.SqlTypes.SqlType(DbType.Guid)
				};
			}
		}
	}

詳細は、理解していないので説明できない…。
重要なのは、NullSafeGetメソッドと、NullSafeSetメソッド。

以上。

おまけ

データベースの文字列の値を、Enumのプロパティにバインドしたい時は、
NHibernate.Type.EnumStringTypeを実装することでできる