列挙を使用したメタデータの表現

メタデータとは何ですか? これは、プログラムアルゴリズムで使用される説明情報です。 たとえば、データベースのテーブルとフィールドの名前、必要なパラメーターが格納されているレジストリキーの名前、またはプログラムがアクセスするオブジェクトのプロパティの名前を指定できます。 拡張メソッドと拡張属性を使用して、enumを使用してメタデータを簡単に表現する方法を教えてください。





クエリ生成タスク



しばらく前、私は人気のあるドキュメント管理システムに特定の機能を実装するプラグインを開発していました。 特に、特定の条件を満たすドキュメントのリストを表示する必要がありました。 ドキュメントのリストを作成するには、埋め込み言語(XPathのサブセット)でサーバーへの要求を作成する必要がありました。 クエリはパラメータのセットに応じて動的に構築されますが、一部のパラメータは指定されない場合があります。 したがって、ワイルドカードで静的文字列を使用することは受け入れられませんでした。 この問題を解決するには? 最も簡単な方法は、クエリを生成する関数で検索属性と検索条件に直接一致する文字列値を使用することです。



 string BuildQuery() { ... if (IsDateFromSpecified) strQuery += "DateFrom >= '" + DateFromValue.ToString() + "'"; ... }
      
      











このオプションは、いくつかのシナリオで非常に適用可能だと思います(腐ったリンゴを投げる必要はありません)。 たとえば、1つの関数で1回だけ言及される2つの属性で動作する単純なユーティリティが作成される場合、メタデータから庭をフェンスすることは不適切である可能性があります。 しかし、リクエストが1ダースの条件で構成されており、リクエストが異なる場合でも、控えめに言って説明したアプローチは不適切です。 このアプローチがうまくいかないと思う理由をいくつか挙げます。





定数を選択



どうする? 最初の最も明白な解決策は、属性名に定数を使用し、フォーマットされた文字列を使用して条件を記述することです。 結果は次のようになります。



 const string MORE_OR_EQUALS_EXPRESSION = "{0} >= '{1}'"; const string DATE_FROM_ATTRIBUTE = "DateFrom"; string BuildQuery() { ... if (IsDateFromSpecified) strQuery += string.Format(MORE_OR_EQUALS_EXPRESSION, DATE_FROM_ATTRIBUTE, DateFromValue.ToString()); ... }
      
      









const string MORE_OR_EQUALS_EXPRESSION = "{0} >= '{1}'"; const string DATE_FROM_ATTRIBUTE = "DateFrom"; string BuildQuery() { ... if (IsDateFromSpecified) strQuery += string.Format(MORE_OR_EQUALS_EXPRESSION, DATE_FROM_ATTRIBUTE, DateFromValue.ToString()); ... }













もちろん、結果のコードは保守が簡単で、変更も簡単ですが、読みやすくなったとは言えません。 少し考えた後、解決策を見つけました。実際、それを共有したかったのです。 以下に詳細を開示しますが、現時点では、要求を生成するためのコードの単なる例です(実動コードから)。



 // -  predicate.Add(DGA.FlowId.EqualsTo(WorksFlowId)); //     predicate.Add(DGA.DogovorOfWorkID.EqualsTo(CurrentDocument.Id)); // if (DateFrom != null) predicate.Add(DGA.WorkEndsAt.MoreOrEqualThan(DateFrom.Value)); if (DateTo != null) predicate.Add(DGA.WorkStartsAt.LessOrEqualThan(DateTo.Value)); //   if (OnlyActive) predicate.Add(DGA.WorkState.EqualsTo(WorkState.Planned.ToString()));
      
      









// - predicate.Add(DGA.FlowId.EqualsTo(WorksFlowId)); // predicate.Add(DGA.DogovorOfWorkID.EqualsTo(CurrentDocument.Id)); // if (DateFrom != null) predicate.Add(DGA.WorkEndsAt.MoreOrEqualThan(DateFrom.Value)); if (DateTo != null) predicate.Add(DGA.WorkStartsAt.LessOrEqualThan(DateTo.Value)); // if (OnlyActive) predicate.Add(DGA.WorkState.EqualsTo(WorkState.Planned.ToString()));













私の意見では、このコードは十分に明確です。 クエリ言語のすべての構文構造はメソッドとして設計されているため、リクエストの構文はコンパイル段階で制御されます。 結果のコードは読みやすく、変更も簡単です。 そして、列挙、拡張、および属性を使用して、このようなコードを達成することができました。



列挙を使用します



そのため、文字列定数の代わりに、検索フィールドを列挙として設計しました。 検索属性の文字列名(読みにくいXPath風の外観を持っています)は、Description属性に含まれています。



 public enum DGA { [Description("doc_RegCard/rc_Index/date__")] RegDate, [Description("doc_RegCard/rc_Index/text__")] RegNum, [Description("doc_RegCard/rc_Index/text___")] Manager, [Description("doc_RegCard/rc_FlowKey")] FlowId, [Description("doc_RegCard/rc_Index/text__")] Title, [Description("doc_RegCard/rc_Index/text__")] Subject, ... }
      
      









public enum DGA { [Description("doc_RegCard/rc_Index/date__")] RegDate, [Description("doc_RegCard/rc_Index/text__")] RegNum, [Description("doc_RegCard/rc_Index/text___")] Manager, [Description("doc_RegCard/rc_FlowKey")] FlowId, [Description("doc_RegCard/rc_Index/text__")] Title, [Description("doc_RegCard/rc_Index/text__")] Subject, ... }













属性の各基本条件は、この列挙の拡張メソッドとして作成されます。 条件の各行は一度しか使用されないため、それらを別々の定数に入れ始めませんでしたが、関数で直接使用します。 これらの関数の一部の例を次に示します。



 public static string LessOrEqualThan(this DGA attrib, DateTime val) { return attrib.LessOrEqualThan(System.Xml.XmlConvert .ToString(val, System.Xml.XmlDateTimeSerializationMode.Unspecified)); } public static string LessOrEqualThan(this DGA attrib, string val) { return String.Format("{0} <= '{1}'", attrib.GetAttribName(), val); } public static string InList(this DGA attrib, IEnumerable<string> values) { return String.Format("{0} in list({1})", attrib.GetAttribName(), values .Select(x => "'" + x + "'") .Aggregate((x, y) => x + ", " + y)); }
      
      









public static string LessOrEqualThan(this DGA attrib, DateTime val) { return attrib.LessOrEqualThan(System.Xml.XmlConvert .ToString(val, System.Xml.XmlDateTimeSerializationMode.Unspecified)); } public static string LessOrEqualThan(this DGA attrib, string val) { return String.Format("{0} <= '{1}'", attrib.GetAttribName(), val); } public static string InList(this DGA attrib, IEnumerable<string> values) { return String.Format("{0} in list({1})", attrib.GetAttribName(), values .Select(x => "'" + x + "'") .Aggregate((x, y) => x + ", " + y)); }













さて、別の拡張メソッド-実際の属性名を取得します:



 public static class DGAExt { public static string GetAttribName(this DGA attrib) { if (_dgaNames != null) return _dgaNames[attrib]; return EnumHelper.GetDescription(attrib); } } public static class EnumHelper { /// <summary> /// Retrieve the description on the enum, eg /// [Description("Bright Pink")] /// BrightPink = 2, /// Then when you pass in the enum, it will retrieve the description /// </summary> /// <param name="en">The Enumeration</param> /// <returns>A string representing the friendly name</returns> public static string GetDescription(Enum en) { var desc = GetAttribute<DescriptionAttribute>(en); if (desc != null) return desc.Description; return en.ToString(); } public static T GetAttribute<T>(Enum en) where T : System.Attribute { Type type = en.GetType(); MemberInfo[] memInfo = type.GetMember(en.ToString()); if (memInfo.Length > 0) { var attrs = memInfo[0].GetCustomAttributes(typeof(T), false).Cast<T>(); return attrs.FirstOrDefault(); } return null; } }
      
      









public static class DGAExt { public static string GetAttribName(this DGA attrib) { if (_dgaNames != null) return _dgaNames[attrib]; return EnumHelper.GetDescription(attrib); } } public static class EnumHelper { /// <summary> /// Retrieve the description on the enum, eg /// [Description("Bright Pink")] /// BrightPink = 2, /// Then when you pass in the enum, it will retrieve the description /// </summary> /// <param name="en">The Enumeration</param> /// <returns>A string representing the friendly name</returns> public static string GetDescription(Enum en) { var desc = GetAttribute<DescriptionAttribute>(en); if (desc != null) return desc.Description; return en.ToString(); } public static T GetAttribute<T>(Enum en) where T : System.Attribute { Type type = en.GetType(); MemberInfo[] memInfo = type.GetMember(en.ToString()); if (memInfo.Length > 0) { var attrs = memInfo[0].GetCustomAttributes(typeof(T), false).Cast<T>(); return attrs.FirstOrDefault(); } return null; } }













おわりに





この例では、列挙型を使用する方が文字列定数よりはるかに便利であることを示したいと思いました。 列挙型は明示的に定義された型ですが、コンパイラの文字列定数はすべて同じです。 列挙ごとに、この型に固有のメソッドのみを宣言できます。 これにより、コンパイル段階で定数の正しい使用を制御できます。 また、属性の助けを借りて、各列挙要素にさまざまな方法で使用できる追加情報を提供できます。 たとえば、例として引用されたタスクでは、最後に、いくつかの検索フィールドの名前を構成ファイルに配置する必要がありました。 これらのフィールドに対応する列挙要素は、構成ファイル内のレコードキーを含む追加の属性を受け取りました。 現在、小道具の名前を取得する関数は、そのような属性の存在をチェックし、必要に応じて構成ファイルにアクセスします。 同時に、クエリ文字列生成コードは変更されていません-つまり 定数を使用する利点はすべて保存されます。



All Articles