65.9K
CodeProject 正在变化。 阅读更多。
Home

演示自定义属性:构建一个程序集搜索工具。

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.56/5 (9投票s)

2005年12月13日

CPOL

32分钟阅读

viewsIcon

52599

downloadIcon

335

通过自定义属性和反射构建一个程序集搜索系统。

引言

标准的.NET属性在功能上类似于C++预处理器或pragma命令。它们可用于向编译器提供有关程序中某个项的附加信息。它们可用于简化某些编程任务,例如:以声明方式设置安全要求(CodeAccessSecurityAttribute)、控制类或结构数据字段的物理布局(StructLayoutAttribute)、为.NET类设置COM GUID(GuidAttribute)等。它们甚至可用于标记不推荐使用的项(ObsoleteAttribute)。毫无疑问,属性是.NET中一个非常有用的特性。

一个更有用的特性是自定义属性,开发者可以通过它来定义和实现自己的属性。就像预定义属性一样,这些用户定义的属性可以用于将附加信息增强到程序元素(如类、属性和方法)上,并通过反射来访问这些信息。

现在,正如我们所知,网页是用可以通过搜索引擎抓取的关键字标记的。这使得我们能够使用Google和Yahoo等流行搜索引擎轻松搜索网站、文档和其他资源。如果我们能以某种方式搜索包含特定关键字的.NET程序集,那岂不是很好?在本文中,我将演示如何借助自定义属性以及我们将自己开发的、能够递归搜索文件夹的程序集搜索工具来实现这一点。

我将介绍一个名为KeywordAttribute的自定义属性的示例实现。KeywordAttribute背后的思想直接来源于Adam Nathan在他的著作《.NET and COM The Complete Interoperability Guide (SAMS Publishing)》中提到的同名示例属性。Adam的想法是提供一个自定义属性,该属性可用于用可搜索的关键字以及相关性级别标记.NET实体(类、属性、方法、委托等)。这在精神上类似于网页如何用搜索引擎检查的关键字来标记。

以下是KeywordAttribute的一个用例示例

[Keyword("Applies Keyword", 7), Keyword("MyClass", 10), Keyword("class")] 
public class MyClass 
{ 
    public MyClass() 
    { 
        ... 
        ... 
        ... 
    } 
}

请注意,KeywordAttribute属性可以通过使用指示关键字字符串的字符串参数来构造。它也可以通过关键字字符串和指示相关性值的整数来构造。如果省略相关性值,则使用默认值。

在本文中,我选择逐字采用Adam Nathan的完整KeywordAttribute类。Adam的实现已经很简单优雅。在他的书中,Adam还提供了一个浏览器应用程序的示例,该应用程序可用于列出程序集中包含的所有带有关键字属性的项。我努力超越Adam的简单浏览器,构建一个完整的程序集文件搜索系统,该系统能够递归扫描文件文件夹,并拾取包含被KeywordAttribute属性标记的类型(这些类型由特定关键字和等于或高于某个最小值的相关性值参数化)的程序集的信息。

我将假设读者对.NET开发工作和C#语言的使用有充分的理解。此外,还需要具备自定义属性和反射方面的先验知识。

总体概述

以下是本文主体部分组织的总体概述

  • KeywordAttribute自定义属性

    我们将研究Adam Nathan对此自定义属性的实现。我们将大量使用他在书中阐述的Adam的原始代码。我们将构建一个KeywordAttribute类库,并为其添加强名称,以便将其注册到GAC作为共享程序集,同时确保使用它的程序集也可以注册到GAC。请注意,这个类库本身很有用,可以用于各种项目中。

  • IDirectoryLister接口

    我们开始开发程序集搜索工具,方法是定义一个名为IDirectoryLister的接口。该接口定义了由提供收集包含在顶级文件夹中的所有文件和子目录信息的具体类来实现的结构、属性和方法。

  • DirectoryListerBase

    然后,我们将构建一个用C#编写的具体基类(DirectoryListerBase),该类实现IDirectoryLister接口。此基类用于提供可以由其自身使用或由目标特定派生类继承的工作函数。

  • AssemblyFilesLister

    DirectoryListerBase派生的目标特定类的一个示例是AssemblyFilesLister。此类基于DirectoryListerBase的基类功能,从而出现了一个专门的目录列表类,该类仅收集程序集。它本质上是IDirectoryLister接口的程序集具体实现。DirectoryListerBaseAssemblyFilesLister类都是非常有用的通用类,读者可以在其他项目中使用的。

  • KeywordAttributeAssemblyLister应用程序

    然后,我们将创建KeywordAttributeAssemblyLister应用程序,该应用程序将使用IDirectoryLister接口以及DirectoryListerBaseAssemblyFilesLister类提供的服务。此应用程序将通过仅收集使用KeywordAttribute属性(由特定关键字字符串和等于或高于某个最小值的相关性值参数化)的程序集来提供进一步的过滤服务(超越AssemblyFilesLister提供的)。

  • 测试带有关键字属性的程序集

    我预先编写了几个测试程序集,它们使用KeywordAttribute属性(参数为名为“test”的特定关键字和各种相关性值)。这些程序集将帮助我们测试KeywordAttributeAssemblyLister应用程序。

事不宜迟,让我们开始研究Adam Nathan精彩的KeywordAttribute

KeywordAttribute自定义属性

KeywordAttribute自定义属性的源代码是本文源代码zip文件中包含的KeywordAttribute.sln解决方案的一部分。解压后,可以在以下目录中找到:<主目录>\KeywordAttribute,其中<主目录>是您解压文件的位置。KeywordAttribute类的源代码如下所示

using System; 
namespace KeywordAttributeNamespace 
{ 
    // Summary description for Keyword. 
    [AttributeUsage(AttributeTargets.All, AllowMultiple=true)] 
    public class KeywordAttribute : Attribute 
    { 
        private string m_strKeyword; 
        private int m_relevance; 
      
        public KeywordAttribute(string strKeyword) 
        { 
            m_strKeyword = strKeyword; 
            m_relevance = 5; 
        } 
        public KeywordAttribute(string strKeyword, int iRelevance) 
        { 
            m_strKeyword = strKeyword; 
            m_relevance = iRelevance; 
        } 
        public string Keyword 
        { 
            get { return m_strKeyword; } 
        } 
      
        // Property to get/set the relevance 
        public int Relevance 
        { 
            get { return m_relevance; } 
        } 
    } 
}

Attribute类和AttributeUsageAttribute

正如您所见,它非常简单优雅。KeywordAttribute类继承自Attribute类,并且本身被标准属性AttributeUsageAttributeAllowMultipleAttribute标记

[AttributeUsage(AttributeTargets.All, AllowMultiple=true)]:这些设置表明KeywordAttribute可以*多重*应用于*所有*程序实体,这些实体是

  • 程序集
  • 构造函数
  • 委托
  • 枚举
  • 事件
  • 字段
  • 接口
  • 方法
  • 模块
  • 参数
  • 属性
  • 返回值
  • 结构

KeywordAttribute类成员

KeywordAttribute类包含两个private成员数据

  • private string m_strKeyword;
  • private int m_relevance;

它们保存特定的关键字字符串和相关性值。

定义了两个构造函数:第一个只接受一个关键字字符串(默认使用相关性值5),第二个接受一个关键字字符串和一个指示相关性值的整数。KeywordAttribute类没有默认构造函数,必须通过参数化的构造函数之一来实例化。

为关键字字符串和相关性值定义了Get访问器。没有定义Set访问器(不需要)。Get访问器在KeywordAttributeAssemblyLister应用程序执行的反射过程中使用。

KeywordAttribute程序集是强命名的

KeywordAttribute程序集是强命名的。这有两个重要原因:

  • KeywordAttribute程序集是一个共享程序集。

    一旦程序集使用了KeywordAttribute,KeywordAttribute程序集就成为一个依赖项。请注意,KeywordAttributeAssemblyLister应用程序本身也引用KeywordAttribute,因此KeywordAttribute程序集也是KeywordAttributeAssemblyLister应用程序的依赖项。

    在这种情况下,KeywordAttributeAssemblyLister应用程序及其加载的(可能使用KeywordAttribute的)所有程序集所引用的KeywordAttribute程序集必须是相同的。

    这是因为.NET类型的标识与其包含的程序集相关联。换句话说,包含的程序集是.NET类型标识的一部分。因此,如果多个程序集包含类型的定义(即使是完全相同的类型),.NET会将它们全部视为独立且不相关的类型。因此,当项目在运行时需要类型定义时,它引用的程序集和加载的程序集很重要。

    因此,我们将KeywordAttribute程序集注册到GAC,以确保它是一个共享资源。稍后,当我们研究KeywordAttributeAssemblyLister应用程序的源代码时,我将回到这一点。

  • 使用KeywordAttribute的程序集可能希望注册到GAC。

    为了允许使用KeywordAttribute的程序集注册到全局程序集缓存 (GAC),KeywordAttribute程序集本身必须使用强名称进行编译。

在KeywordAttribute解决方案目录(keyfile.snk)中已预先生成了一个强名称密钥文件,并在KeywordAttribute解决方案的AssemblyInfo.cs文件中引用了它:[assembly: AssemblyKeyFile("..\\..\\keyfile.snk")]。

现在KeywordAttribute程序集是强命名的,它也可以注册到GAC。这可以通过gacutil.exe工具完成。我专门创建了一个批处理文件RegisterAssemblyToGAC.bat来执行此操作。一旦读者编译了KeywordAttribute解决方案,请调用此批处理文件将KeywordAttribute.dll注册到GAC。

IDirectoryLister接口

程序集搜索工具需要对指定顶级目录的子目录进行详尽的递归搜索。搜索的目标是包含由特定关键字字符串和等于或高于最小值的相关性值参数化的KeywordAttribute属性标记的类型的特定程序集。

与其开发一个执行上述专用任务的类,不如我选择将此操作提升为一个抽象接口。这样,使用此接口的客户端应用程序可以选择一个具体的实现来实现文件搜索操作。

我们通过定义一个名为IDirectoryLister的接口来做到这一点。该接口定义了由提供收集包含在顶级文件夹中的所有文件和子目录信息的具体类来实现的必要属性和方法。

此接口在DirectoryListerInterface.sln解决方案中定义,并附带两个相关的结构。此解决方案包含在本文的源代码zip文件中。解压后,可以在以下目录中找到:<主目录>\DirectoryLister\Interfaces\DirectoryListerInterface。源代码如下

using System; 
namespace DirectoryListerInterfaceNamespace 
{ 
  public struct FileEntry 
  { 
    public string m_strName; 
    public long m_lSize; 
    public DateTime m_dtLastModification; 
  } 
  public struct DirectoryEntry 
  { 
    public string m_strName; 
    public long m_lSize; 
    public DateTime m_dtLastModification; 
    public DirectoryEntry[] m_deArray; 
    public FileEntry[] m_feArray; 
  } 
  public interface IDirectoryLister 
  { 
    string DirectoryToList { get; set; } 
    
    bool Recursive { get; set; } 
    DirectoryEntry DirectoryEntry { get; } 
    bool BeginListing(); 
  } 
}

FileEntry struct是一个相关的结构,用于提供文件的基本描述。DirectoryEntry struct是另一个相关的结构,用于包含在目标目录中找到的目录的信息。DirectoryEntry包含一个名为m_deArray的字段,它是一个DirectoryEntry struct的数组。它还包含一个名为m_feArray的字段,它是一个FileEntry struct的数组。因此,单个DirectoryEntry struct是一个包含目录树中文件和子文件夹引用的自包含对象。

IDirectoryLister接口定义了目录列表对象的必需属性和方法。DirectoryToList字符串属性用于指示(给具体实现)要扫描的顶级目录。

Recursive bool属性用于指示文件扫描操作是递归的还是仅限于目标顶级目录。

DirectoryEntry属性的类型为DirectoryEntry,客户端使用它来获取有关顶级目标目录的信息。扫描完成后,此属性将包含目标顶级目录中所有文件和子目录的信息。

BeginListing()方法只是告诉具体实现开始扫描过程。

定义了这个接口后,我们现在开始进行一些具体的开发工作。第一站是DirectoryListerBase类。

DirectoryListerBase

DirectoryListerBase类是用C#编写的。它实现了前面描述的IDirectoryLister接口。这个基类用于提供可以由客户端直接使用或由专门的派生类继承的工作方法。

DirectoryListerBase类的源代码是本文源代码zip文件中包含的DirectoryListerBase.sln解决方案的一部分。解压后,可以在以下目录中找到:<主目录>\DirectoryLister\Implementations\DirectoryListerBase,其中<主目录>是您解压文件的位置。DirectoryListerBase类的主要部分如下

using System; 
using DirectoryListerInterfaceNamespace; 
using System.IO; 
namespace DirectoryListerBaseNamespace 
{ 
  public class DirectoryListerBase : IDirectoryLister 
  { 
    #region Code pertaining to internal member data. 
    string m_strDirectoryToList; 
    bool m_bRecursive; 
    DirectoryEntry m_deRoot; 
    #endregion 
    #region Code pertaining to constructors and destructors. 
    public DirectoryListerBase() 
    { 
      InitMemberData(); 
    } 
    #endregion 
    #region Code pertaining to IDirectoryLister interface implementations 
    public string DirectoryToList 
    { 
      get { return m_strDirectoryToList; } 
      set { m_strDirectoryToList = value; } 
    } 
    public bool Recursive 
    { 
      get { return m_bRecursive; } 
      set { m_bRecursive = value; } 
    } 
    public DirectoryEntry DirectoryEntry 
    { 
      get { return m_deRoot; } 
    } 
    public bool BeginListing() 
    { 
      if (m_strDirectoryToList == "") 
      { 
        return false; 
      } 
      m_deRoot.m_strName = m_strDirectoryToList; 
      m_deRoot.m_lSize = 0; 
      m_deRoot.m_dtLastModification 
        = Directory.GetLastWriteTime(m_strDirectoryToList); 
      GatherDirectories(m_strDirectoryToList, ref m_deRoot); 
      return true; 
    } 
    #endregion 
    ... 
    ... 
    ... 
}

我主要列出了DirectoryListerBase类的接口实现部分,以便能够单独讨论类的私有内部函数。

DirectoryToList string属性通过定义和使用m_strDirectoryToList string成员数据来简单实现。Recursive bool属性同样通过m_bRecursive bool成员数据来实现。

BeginListing()方法更有趣。它首先检查m_strDirectoryToList string成员,看它是否为空。如果是,这意味着DirectoryToList string属性尚未由客户端应用程序设置,因此没有什么可做的,只能以false的值退出。

假设DirectoryToList string属性已设置为正确的值,接下来的事情是填充一个名为m_deRootprivate DirectoryEntry struct,尽可能多地填充数据。然后将此成员DirectoryEntry struct传递给名为GatherDirectories()的内部成员函数。我们将在下面更详细地研究GatherDirectories()函数。

GatherDirectories()方法

GatherDirectories()的目标是扫描指定目录(参数1)的内容,并将找到的每个文件和子目录的信息存储在指定的DirectoryEntry对象(参数2)中。如果DirectoryListerBase类的Recursive bool属性设置为true,则每个子目录都会被进一步单独检查,并且在子目录中找到的任何文件或子目录都会被进一步处理。然后递归地重复此过程,直到搜索并处理完指定(参数1)目录的所有文件和子目录为止。

GatherDirectories()方法的源代码如下

protected virtual void GatherDirectories
(
  string strDirectoryName, 
  ref DirectoryEntry directory_entry_to_set
) 
{ 
  try 
  { 
    // First thing to do : gather all the files 
    // from the current directory. 
    GatherFiles(strDirectoryName, ref directory_entry_to_set); 
    string[] strSubdirectoryNames 
      = Directory.GetDirectories(strDirectoryName); 
    int iIndex = 0; 
     
    // Next, gather all the sub-directories inside 
    // the current directory. 
    directory_entry_to_set.m_deArray = 
               new DirectoryEntry[strSubdirectoryNames.Length]; 
    iIndex = 0; 
    foreach(string strSubdirectoryName in strSubdirectoryNames) 
    { 
      directory_entry_to_set.m_deArray[iIndex].m_strName = 
                                           strSubdirectoryName; 
      directory_entry_to_set.m_deArray[iIndex].m_lSize = 0; 
      directory_entry_to_set.m_deArray[iIndex].m_dtLastModification = 
                       Directory.GetLastWriteTime(strSubdirectoryName); 
      if (Recursive) 
      { 
        GatherDirectories
        (
          strSubdirectoryName, 
          ref directory_entry_to_set.m_deArray[iIndex]
        ); 
      } 
      iIndex++; 
    } 
  } 
  catch(Exception e) 
  { 
    System.Console.WriteLine("Exception Message : {0}", e.Message); 
    System.Console.WriteLine("Exception TargetSite : {0}", 
                                                     e.TargetSite); 
    System.Console.WriteLine("Exception Source : {0}", e.Source); 
  } 
}

为了实现GatherDirectories()的目标,使用了另一个内部方法GatherFiles()。我们稍后将研究GatherFiles()的内部工作原理。目前,只需注意GatherFiles()将扫描指定目录(其第一个参数)中包含的文件(不处理子目录),并将有关每个发现文件的信息存储在指定的DirectoryEntry struct(其第二个参数)的单个FileEntry struct中:GatherFiles(strDirectoryName, ref directory_entry_to_set);

因此,GatherDirectories()方法调用GatherFiles()来收集当前目录(strDirectoryName)中包含的所有文件。请注意,我们将有关所有找到的文件信息存储在directory_entry_to_setm_feArray成员中。

GatherDirectories()然后准备一个当前目录strDirectoryName中包含的子目录数组。这是通过使用System.IO命名空间中定义的Directory类的GetDirectories()方法来完成的。返回的子目录数组存储在本地字符串数组变量strSubdirectoryNames中:string[] strSubdirectoryNames = Directory.GetDirectories(strDirectoryName);

然后,我们实例化当前DirectoryEntry struct directory_entry_to_setDirectoryEntry数组成员m_deArray。此数组的大小由strSubdirectoryNames的数组大小确定:directory_entry_to_set.m_deArray = new DirectoryEntry[strSubdirectoryNames.Length];

然后,我们遍历strSubdirectoryNames数组中包含的每个子目录名

iIndex = 0; 
foreach(string strSubdirectoryName in strSubdirectoryNames) 
{ 
  directory_entry_to_set.m_deArray[iIndex].m_strName 
    = strSubdirectoryName; 
  directory_entry_to_set.m_deArray[iIndex].m_lSize = 0; 
  directory_entry_to_set.m_deArray[iIndex].m_dtLastModification 
    = Directory.GetLastWriteTime(strSubdirectoryName); 
  if (Recursive) 
  { 
    GatherDirectories
    (
      strSubdirectoryName, 
      ref directory_entry_to_set.m_deArray[iIndex]
    ); 
  } 
  iIndex++; 
}

关于每个子目录(我们知道它的名称)的所有可用信息都存储在当前DirectoryEntry struct directory_entry_to_setDirectoryEntry数组m_deArray的元素中。

然后,如果Recursive属性设置为true,我们通过调用当前子目录上的GatherDirectories()来执行递归,并将为子目录指定的当前DirectoryEntry数组元素(directory_entry_to_set.m_deArray[iIndex])作为参数传递。这种递归将确保对给定顶级目录中的所有文件和子目录进行详尽的搜索和处理。

现在我们将详细研究GatherFiles()方法。

GatherFiles()方法

如前所述,GatherFiles()方法扫描指定目录(第一个参数)中包含的文件,并将每个发现文件的信息存储在指定的DirectoryEntry struct(第二个参数)的m_feArray数组中。此函数如下

protected virtual void GatherFiles
(
  string strDirectoryName, 
  ref DirectoryEntry directory_entry_to_set
) 
{ 
  try 
  { 
    // Get all files from given directory name. 
    string[] strFileNames 
      = Directory.GetFiles(strDirectoryName); 
    int iIndex = 0; 
    directory_entry_to_set.m_feArray 
      = new FileEntry[strFileNames.Length]; 
    foreach(string strFileName in strFileNames) 
    { 
      FileInfo fi = new FileInfo(strFileName); 
      (directory_entry_to_set.m_feArray)[iIndex].m_strName = 
                                                    strFileName; 
      (directory_entry_to_set.m_feArray)[iIndex].m_lSize = 
                                                      fi.Length; 
      (directory_entry_to_set.m_feArray)[iIndex].m_dtLastModification = 
                                                        fi.LastWriteTime; 
      iIndex++; 
    } 
  } 
  catch(Exception e) 
  { 
    System.Console.WriteLine("Exception Message : {0}", e.Message); 
    System.Console.WriteLine("Exception TargetSite : {0}", e.TargetSite); 
    System.Console.WriteLine("Exception Source : {0}", e.Source); 
  } 
}

GatherFiles()首先准备一个包含当前目录strDirectoryName中所有文件名的数组:string[] strFileNames = Directory.GetFiles(strDirectoryName);。文件名数组存储在strFileNames中,它是一个字符串数组。然后实例化当前DirectoryEntry structdirectory_entry_to_set)的当前FileEntry数组成员(m_feArray):directory_entry_to_set.m_feArray = new FileEntry[strFileNames.Length];。数组的大小设置为strFileNames数组的大小。然后,GatherFiles()strFileNames数组中包含的所有文件名进行迭代

foreach(string strFileName in strFileNames) 
{ 
  FileInfo fi = new FileInfo(strFileName); 
  (directory_entry_to_set.m_feArray)[iIndex].m_strName 
    = strFileName; 
  (directory_entry_to_set.m_feArray)[iIndex].m_lSize = fi.Length; 
  (directory_entry_to_set.m_feArray)[iIndex].m_dtLastModification 
    = fi.LastWriteTime; 
  iIndex++; 
}

通过使用FileInfo类(定义在System.IO命名空间中)来获取与每个文件相关的文件信息。然后将相关数据存储在当前DirectoryEntry structdirectory_entry_to_set)的FileEntry数组((directory_entry_to_set.m_feArray)[iIndex])的元素中。

关于DirectoryListerBase类的最终说明

DirectoryListerBase类本身是一个有用的类。它可以通用地用于需要收集任何顶级目录中包含的文件和子目录的项目。另外,请注意,它的GatherDirectories()GatherFiles()方法都已被声明为virtual函数。这意味着任何DirectoryListerBase派生类都可以选择重写它们中的任何一个,如果需要不同的目录或文件搜索算法。我们将在后面实现AssemblyFilesLister类时充分利用这一特性。

AssemblyFilesLister

AssemblyFilesLister类是一个用C#编写的类,它基于DirectoryListerBase类提供的功能,提供了一个专门的IDirectoryLister接口实现,该实现仅专注于程序集文件。

AssemblyFilesLister类的源代码是本文源代码zip文件中包含的AssemblyFilesLister.sln解决方案的一部分。解压后,可以在以下目录中找到:<主目录>\DirectoryLister\Implementations\AssemblyFilesLister,其中<主目录>是您解压文件的位置。

AssemblyFilesLister类的骨架大纲源代码列表如下

using System; 
using DirectoryListerInterfaceNamespace; 
using DirectoryListerBaseNamespace; 
using System.IO; 
namespace AssemblyFilesListerNamespace 
{ 
  public class AssemblyFilesLister : DirectoryListerBase 
  { 
    protected override void GatherFiles
    (
      string strDirectoryName, 
      ref DirectoryEntry directory_entry_to_set
    ) 
    { 
      ... 
      ... 
      ... 
    } 
    protected bool IsAssemblyFile(string strFileName) 
    { 
      ... 
      ... 
      ... 
    } 
  } 
}

关于AssemblyFilesLister类最重要的一个方面是它继承自DirectoryListerBase。因此,它继承了DirectoryListerBase的所有功能,包括IDirectoryLister接口实现。AssemblyFilesLister需要做的就是重写其基类的相关virtual函数。只有两个virtual函数需要重写:GatherDirectories()GatherFiles()DirectoryListerBase.GatherDirectories()提供的功能对于AssemblyFilesLister来说已经足够了。这里不需要任何修改。然而,DirectoryListerBase.GatherFiles()需要进一步的微调。此基类实现发现目录中的*所有*文件。我们只需要收集*程序集*文件。因此,我们重写了GatherFiles()并为AssemblyFilesLister提供了一个自定义版本。

AssemblyFilesLister.GatherFiles()

AssemblyFilesLister.GatherFiles()的源代码如下

protected override void GatherFiles
(
  string strDirectoryName, 
  ref DirectoryEntry directory_entry_to_set
) 
{ 
  // Get all files from given directory name. 
  string[] strFileNames = Directory.GetFiles(strDirectoryName); 
  int iCountOfAssemblies = 0; 
  int iIndex = 0; 
  // First determine how many assembly files there are 
  // in this current directory. 
  foreach(string strFileName in strFileNames) 
  { 
    if (IsAssemblyFile(strFileName)) 
    { 
      iCountOfAssemblies++; 
    } 
  } 
  if (iCountOfAssemblies > 0) 
  { 
    directory_entry_to_set.m_feArray 
      = new FileEntry[iCountOfAssemblies]; 
    foreach(string strFileName in strFileNames) 
    { 
      if (IsAssemblyFile(strFileName)) 
      { 
        FileInfo fi = new FileInfo(strFileName); 
        (directory_entry_to_set.m_feArray)[iIndex].m_strName 
          = strFileName;  
        (directory_entry_to_set.m_feArray)[iIndex].m_lSize 
          = fi.Length; 
        (directory_entry_to_set.m_feArray)[iIndex].m_dtLastModification 
          = fi.LastWriteTime; 
        iIndex++; 
      } 
    } 
  } 
}

与它的基类对应物一样,AssemblyFilesLister.GatherFiles()使用Directory类的GetFiles()方法来获取包含在其指定目录strDirectoryName(第一个参数)中的所有文件名的数组:string[] strFileNames = Directory.GetFiles(strDirectoryName);

文件名存储在strFileNames字符串数组中。现在,因为strFileNames包含strDirectoryName中*所有*文件的名称,所以我们不能使用它的数组大小来实例化指定的DirectoryEntry struct directory_entry_to_setFileEntry数组成员m_feArray

相反,我们需要迭代strFileNames数组中的文件名,以确定其中有多少文件实际上是程序集文件。

// First determine how many assembly files there are in this current 
// directory. 
foreach(string strFileName in strFileNames) 
{ 
  if (IsAssemblyFile(strFileName)) 
  { 
    iCountOfAssemblies++; 
  } 
}

这是通过内部方法IsAssemblyFile()实现的。我们将在稍后更详细地研究IsAssemblyFile()方法。现在,只需注意它返回一个bool,指示输入文件名是否实际上是程序集文件。如果strFileNames数组中的文件名是程序集文件,我们就会增加一个内部计数器iCountOfAssemblies。在迭代结束时,此计数器将告诉我们当前目录中有多少程序集文件。如果此计数大于零,我们将继续实例化directory_entry_to_set.m_feArray并重复之前的迭代,但这次不是简单地计算程序集文件的出现次数,而是存储每个已发现的程序集文件的文件信息。

if (iCountOfAssemblies > 0) 
{ 
    directory_entry_to_set.m_feArray = 
                         new FileEntry[iCountOfAssemblies]; 
    foreach(string strFileName in strFileNames) 
    { 
      if (IsAssemblyFile(strFileName)) 
      { 
        FileInfo fi = new FileInfo(strFileName); 
        (directory_entry_to_set.m_feArray)[iIndex].m_strName 
          = strFileName;  
        (directory_entry_to_set.m_feArray)[iIndex].m_lSize 
          = fi.Length; 
        (directory_entry_to_set.m_feArray)[iIndex].m_dtLastModification 
          = fi.LastWriteTime; 
        iIndex++; 
      } 
    } 
}

在提供了自己的GatherFiles()版本后,当在AssemblyFilesLister上调用GatherDirectories()时,在调用GatherFiles()的点上,将调用AssemblyFilesLister.GatherFiles()版本。

protected virtual void GatherDirectories
(
  string strDirectoryName, 
  ref DirectoryEntry directory_entry_to_set
) 
{ 
  try 
  { 
    // First thing to do : gather all the files 
    // from the current directory. 
    // AssemblyFilesLister.GatherFiles() will be 
    // called below. 
    GatherFiles(strDirectoryName, ref directory_entry_to_set); 
    ... 
    ... 
    ...

IsAssemblyFile

IsAssemblyFile()方法如下

protected bool IsAssemblyFile(string strFileName) 
{ 
  bool bRet = false; 
  try 
  { 
    System.Reflection.AssemblyName testAssembly = 
      System.Reflection.AssemblyName.GetAssemblyName(strFileName); 
    bRet = true; 
  } 
  catch (System.IO.FileNotFoundException) 
  { 
    bRet = false; 
  } 
  catch (System.BadImageFormatException) 
  { 
    bRet = false; 
  } 
  catch (System.IO.FileLoadException) 
  { 
    bRet = false; 
  } 
  
  return bRet; 
}

如上所述,IsAssemblyFile()被指定返回一个bool,指示输入文件名(参数strFileName)是否是程序集文件。代码直接取自MSDN在线C#程序员参考的代码示例,标题为“如何:确定文件是否为程序集(C# 编程指南)”。

基本上,要以编程方式确定文件是否为程序集,我们可以调用System.Reflection.AssemblyName.GetAssemblyName()方法,传递要测试的文件的完整文件路径和名称。如果抛出BadImageFormatException异常,则该文件不是程序集。这个简单的原则在IsAssemblyFile()方法中使用。

关于AssemblyFilesLister类的最终说明

AssemblyFilesLister类将是最终的KeywordAttributeAssemblyLister应用程序将使用的类。我们通过AssemblyFilesLister类实现的,是一个完整的IDirectoryLister接口实现,该实现基于DirectoryListerBase类提供的完整功能,并特别关注程序集文件搜索。

接下来我们需要构建一个更精细的过滤工具,能够选择那些包含被KeywordAttribute属性标记的类型(该属性由特定关键字字符串参数化)的程序集文件。这在KeywordAttributeAssemblyLister应用程序中实现,我们将接下来进行研究。

KeywordAttributeAssemblyLister应用程序

最后,我们到达了KeywordAttributeAssemblyLister应用程序。KeywordAttributeAssemblyLister是一个用C#编写的应用程序。它使用IDirectoryLister接口公开的函数来搜索指定的顶级目录,以查找包含被KeywordAttribute属性(由特定关键字字符串和最小相关性级别参数化)标记的类型的特定程序集。搜索可以是递归的或非递归的。

KeywordAttributeAssemblyLister应用程序的源代码包含在本文的源代码zip文件中。解压后,可以在以下目录中找到:<主目录>\DirectoryLister\Clients\KeywordAttributeAssemblyLister,其中<主目录>是您解压文件的位置。

KeywordAttributeAssemblyLister应用程序包含在KeywordAttributeAssemblyLister命名空间中,该命名空间位于ClassMain.cs源代码文件中。我们现在仔细分析KeywordAttributeAssemblyLister命名空间中定义的每个实体。

KeywordAttributedType结构

KeywordAttributedType struct用于包含有关被特定KeywordAttribute属性标记的类型的信息。也就是说,在源代码中,该类型被标记为类似以下内容:Keyword("some_keyword", some_relevance_value)

struct定义如下

public struct KeywordAttributedType 
{ 
  public string m_TypeName; 
  public int m_iRelevance; 
}

其中m_TypeName是一个字符串,指示类型的名称;m_iRelevance指示与该类型关联的关键字的相关性值。

KeywordAttributedAssembly结构

KeywordAttributedAssembly struct用于包含有关包含被特定KeywordAttribute属性标记的类型(或类型集合)的程序集的信息。此struct定义如下

public struct KeywordAttributedAssembly 
{ 
  public string m_strAssemblyFullPath; 
  public string m_strKeyword; 
  public ArrayList m_KeywordAttributedTypesArray; 
}

其中m_strAssemblyFullPath是程序集文件的完整路径。m_strKeyword是在KeywordAttribute中使用的关键字。m_KeywordAttributedTypesArray是一个ArrayList,其中包含KeywordAttributedType struct

这两个struct背后的基本思想是,最终,当需要显示预期数据时,我们必须有一个KeywordAttributedAssembly struct的数组。每个KeywordAttributedAssembly struct将代表一个程序集文件,其中包含被特定关键字字符串参数化的KeywordAttribute属性标记的类型。它的KeywordAttributedType ArrayList将包含所有带有关键字属性的类型名称以及每个类型的相关性值。

ClassMain

ClassMain是整个KeywordAttributeAssemblyLister应用程序的主类。其static Main()函数是应用程序的入口点。Main()在两个附加的内部方法SearchAndCollectKeywordAttributesAssemblies()CollectKeywordAttributedTypes()的帮助下,几乎提供了整个应用程序所需的功能。

Main()方法

Main()函数源代码如下

static void Main(string[] args) 
{ 
  // We make use of the services of an IDirectoryLister 
  // interface implemented 
  // by the AssemblyFilesLister class. 
  IDirectoryLister dl = new AssemblyFilesLister(); 
  // The "dl" DirectoryLister object will return to us a 
  // DirectoryEntry object filled with sub-directories 
  // and files contained inside a target directory. 
  DirectoryEntry de; 
  // KeywordAttributedAssembliesArray is an ArrayList object 
  // which holds an array of KeywordAttributedAssembly entries. 
  // Each entry represents an assembly that uses 
  // the specified Keyword Attribute. 
  ArrayList KeywordAttributedAssembliesArray = new ArrayList(); 
  int i = 0; 
  // Indicate to the IDirectoryLister object which directory 
  // to scan. 
  // In our case, this will be the directory supplied by the user in 
  // the first argument to this program. 
  dl.DirectoryToList = args[0]; 
  // Indicate that we want the directory scanning to be recursive. 
  dl.Recursive = true; 
  // Tell the IDirectoryLister object to begin scanning. 
  dl.BeginListing(); 
  // At this point, directory scanning is done and we obtain 
  // the main directory entry object. 
  de = dl.DirectoryEntry; 
  Console.WriteLine
  (
    "Searching [{0}] for Assemblies Attributed with Keyword \"{1}\" 
                                        and Minimum Relevance {2}.", 
                                        args[0], 
                                        args[1], 
                                        args[2]
  ); 
  Console.WriteLine(); 
  // Search through the main directory and all its subdirectories 
  // and scan for Keyword Attributed Assemblies. 
  SearchAndCollectKeywordAttributesAssemblies
  (
      de, 
      args[1], 
      Convert.ToInt32(args[2]), 
      ref KeywordAttributedAssembliesArray
  ); 
  // Once done, KeywordAttributedAssembliesArray will contain 
  // KeywordAttributedAssembly objects each of which contains 
  // information on one assembly that uses the specified 
  // Keyword Attribute. 
  for (i = 0; i < KeywordAttributedAssembliesArray.Count; i++) 
  { 
    KeywordAttributedAssembly keyword_attributed_assembly = 
      (KeywordAttributedAssembly)(KeywordAttributedAssembliesArray[i]);
    Console.WriteLine
    (
      "Listing Types in Assembly : [{0}]", 
      keyword_attributed_assembly.m_strAssemblyFullPath
    ); 
    // Each keyword_attributed_assembly contains an internal array 
    // of KeywordAttributedType objects. Each object contains 
    // information on a type that uses the specified Keyword 
    // Attribute. The relevance level is also included. 
    int j = 0; 
    for 
    (
       j = 0; 
       j < keyword_attributed_assembly.m_KeywordAttributedTypesArray.Count; 
       j++
    ) 
    { 
        KeywordAttributedType keyword_attributed_type = 
                                               (KeywordAttributedType)
        (keyword_attributed_assembly.m_KeywordAttributedTypesArray[j]); 
      
        Console.WriteLine("Type Name : {0}. Relevance : {1}.", 
                              keyword_attributed_type.m_TypeName, 
                              keyword_attributed_type.m_iRelevance); 
    } 
  
    Console.WriteLine(); 
  } 
}

Main()方法使用由AssemblyFilesLister类实现的IDirectoryLister接口的服务。

// We make use of the services of an IDirectoryLister 
// interface implemented 
// by the AssemblyFilesLister class. 
IDirectoryLister dl = new AssemblyFilesLister(); 
// The "dl" DirectoryLister object will return to us 
// a DirectoryEntry object filled with sub-directories 
// and files contained inside a target directory. 
DirectoryEntry de;

然后,Main()指示目录列表对象递归地扫描指定的顶级目录(第一个命令行参数),以收集目录中包含的所有程序集文件。

  // Indicate to the IDirectoryLister object which directory 
  // to scan. In our case, this will be the directory supplied 
  // by the user in the first argument to this program. 
  dl.DirectoryToList = args[0]; 
  // Indicate that we want the directory scanning 
  // to be recursive. 
  dl.Recursive = true; 
  // Tell the IDirectoryLister object to begin scanning. 
  dl.BeginListing();

一旦所有文件的集合准备就绪,IDirectoryLister接口实现的DirectoryEntry成员将包含指定顶级目录(args[0])中所有程序集的树。

  // At this point, directory scanning is done 
  // and we obtain the main directory entry object. 
  de = dl.DirectoryEntry;

这个程序集树需要进一步过滤。我们需要从中选出那些被KeywordAttribute属性(由特定关键字字符串参数化且相关性值大于某个最小值的)标记的程序集。这是通过SearchAndCollectKeywordAttributesAssemblies()方法实现的。

  // Search through the main directory and all its 
  // subdirectories and scan for 
  // Keyword Attributed Assemblies. 
  SearchAndCollectKeywordAttributesAssemblies
  (
    de, 
    args[1], 
    Convert.ToInt32(args[2]), 
    ref KeywordAttributedAssembliesArray
  );

SearchAndCollectKeywordAttributesAssemblies()完成后,ArrayList对象KeywordAttributedAssembliesArray将包含KeywordAttributedAssembly structs。每个struct将包含相关程序集的完整路径、所需的关键字字符串以及一个包含KeywordAttributedType structs的ArrayList。每个KeywordAttributedType struct将包含一个已被KeywordAttribute属性标记的类型的名称,该属性由特定的关键字字符串和特定的相关性值参数化。然后,我们遍历这些最终的程序集文件并显示所有相关信息。

SearchAndCollectKeywordAttributesAssemblies()方法

SearchAndCollectKeywordAttributesAssemblies()方法源代码如下

static void SearchAndCollectKeywordAttributesAssemblies
(
  DirectoryEntry de, 
  string strKeyword, 
  int iMinRelevance, 
  ref ArrayList KeywordAttributedAssembliesArray
) 
{ 
  if (de.m_feArray != null) 
  { 
    foreach(FileEntry fe in de.m_feArray) 
    { 
      CollectKeywordAttributedTypes
      (
        fe.m_strName, 
        strKeyword, 
        iMinRelevance, 
        ref KeywordAttributedAssembliesArray
      ); 
    } 
  } 
  if (de.m_deArray != null) 
  { 
    foreach(DirectoryEntry _de in de.m_deArray) 
    { 
      SearchAndCollectKeywordAttributesAssemblies
      (
        _de, 
        strKeyword, 
        iMinRelevance, 
        ref KeywordAttributedAssembliesArray
      ); 
    } 
  } 
}

这是一个简单的函数。它首先遍历输入DirectoryEntry structde)的m_feArray中包含的文件。此数组中的每个文件都是一个程序集文件。对于每个程序集文件,我们调用CollectKeywordAttributedTypes()进行进一步处理。我们将在稍后详细研究CollectKeywordAttributedTypes()。现在,只需注意它处理一个程序集文件(文件名作为参数传递)以获取有关类型(在程序集中定义)的信息,这些类型被KeywordAttribute属性(由特定关键字字符串和大于某个最小值的相关性值参数化)标记。

不要忘记,输入的DirectoryEntry struct de还将包含一个子目录数组(即m_deArray)。SearchAndCollectKeywordAttributesAssemblies()方法将递归地调用自身来处理此数组中的每个子目录。

CollectKeywordAttributedTypes()方法

CollectKeywordAttributedTypes()方法使用Assembly类的LoadFrom()方法加载一个程序集文件。

static void CollectKeywordAttributedTypes
(
  string strAssembly, 
  string strKeyword, 
  int iMinRelevance, 
  ref ArrayList KeywordAttributedAssembliesArray
) 
{ 
  Assembly assembly; 
  try 
  { 
    // Load the assembly from a filename 
    assembly = Assembly.LoadFrom(strAssembly); 
  } 
  catch (Exception) 
  { 
    // Could not load the assembly. 
    return; 
  } 
  ... 
  ... 
  ...

加载的程序集由名为“assembly”的Assembly类实例表示。然后,我们定义一个KeywordAttributedAssembly struct变量以及一个布尔变量bAddToKeywordAttributedAssembliesArray

KeywordAttributedAssembly current_keyword_attributed_assembly 
                               = new KeywordAttributedAssembly(); 
bool bAddToKeywordAttributedAssembliesArray = false;

KeywordAttributedAssembly struct变量current_keyword_attributed_assembly用于保存我们刚刚加载和正在处理的当前程序集的信息。我们将用所有可用的信息填充它。

// Prepare a new AssemblyWithKeyword object in case we are indeed 
// currently working with an Assembly that uses the Keyword attribute. 
current_keyword_attributed_assembly.m_strAssemblyFullPath = strAssembly; 
current_keyword_attributed_assembly.m_strKeyword = strKeyword; 
current_keyword_attributed_assembly.m_KeywordAttributedTypesArray = 
                                                        new ArrayList();

然而,只有当bool变量bAddToKeywordAttributedAssembliesArray设置为true时,它才有用。bAddToKeywordAttributedAssembliesArray仅在当前程序集包含被KeywordAttribute属性标记的类型(这些属性由指定的关键字字符串和等于或高于某个最小值的相关性值参数化)时才为true。如果是这种情况,我们将把current_keyword_attributed_assembly添加到输入的KeywordAttributedAssembliesArray ArrayList中,这样当CollectKeywordAttributedTypes()返回时,调用者将有一个相关的程序集列表,每个程序集都符合要求。我们稍后将看到这一点。

接下来,我们尝试提取附加到当前程序集(在“程序集”级别)的所有类型为KeywordAttribute的自定义属性。

// Collect all the Keyword Attrbutes from the Assembly itself. 
// Check for the KeywordAttribute on the Assembly itself. 
KeywordAttribute[] keyword_attributes_in_assembly 
  = (KeywordAttribute[])
    (assembly.GetCustomAttributes(typeof(KeywordAttribute), true));

提取的KeywordAttribute属性(如果有)存储在KeywordAttribute数组keyword_attributes_in_assembly中。

回想一下前面“The KeywordAttribute Custom Attribute”部分的讨论,KeywordAttribute程序集是强命名的,并且被制作成一个共享资源并注册到GAC。这里就是这种安排的好处所在:如果KeywordAttributeAssemblyLister应用程序引用的KeywordAttribute程序集与当前正在检查的程序集引用的KeywordAttribute程序集*不是*相同的,那么上面带有第一个参数为typeof(KeywordAttribute)GetCustomAttributes()调用将返回一个空数组。事实上,所有这种性质的GetCustomAttributes()调用都将产生空数组。

这发生在KeywordAttributeAssemblyLister加载一个KeywordAttribute程序集,而当前程序集(正在检查的)加载另一个。记住,.NET类型的标识与其包含的程序集相关联。对于.NET运行时来说,KeywordAttributeAssemblyLister使用的KeywordAttribute类型与当前加载程序集使用的KeywordAttribute类型不同,因为涉及两个独立的包含程序集。通过确保KeywordAttribute程序集注册到GAC,使其成为KeywordAttributeAssemblyLister及其加载的所有程序集的共享依赖项,我们避免了这个问题。

现在,如果返回的数组keyword_attributes_in_assembly的大小非零,则表示该程序集确实具有类型为KeywordAttribute的关联属性。这意味着该程序集在“程序集”级别使用了至少一个KeywordAttribute属性,例如:[assembly: Keyword("test", 5)]

然而,拥有类型为KeywordAttribute的关联属性并不意味着该程序集符合我们应用程序的列出条件。在这里,我们确保任何发现的KeywordAttribute属性都由特定的关键字字符串和等于或高于指定最小值的相关性值参数化。

if (keyword_attributes_in_assembly.Length > 0) 
{ 
  // We enumerate through the Keyword Attributes in the assembly. 
  foreach (KeywordAttribute a in keyword_attributes_in_assembly) 
  {    
    // We filter through the keyword attributes and select 
    // only those whose keywords match the search keyword (strKeyword) 
    // and whose relevance is at least equal to the required 
    // relevance (i.e. iMinRelevance). 
    if ((a.Keyword == strKeyword) && (a.Relevance >= iMinRelevance)) 
    { 
      KeywordAttributedType keyword_attributed_type 
        = new KeywordAttributedType(); 
      keyword_attributed_type.m_TypeName = "assembly"; 
      keyword_attributed_type.m_iRelevance = a.Relevance; 
      // Add the current TypeWithKeyword object to the ArrayList 
      // of TypeWithKeyword objects in the Current AssemblyWithKeyword 
      // object (current_assembly_with_keyword). 
      current_keyword_attributed_assembly.m_KeywordAttributedTypesArray.Add
      ((object)keyword_attributed_type); 
      // Indicate that the Current AssemblyWithKeyword object 
      // (current_assembly_with_keyword) 
      // is to be added to the input reference 
      // ArrayList AssemblyWithKeywordArray later on.   
      bAddToKeywordAttributedAssembliesArray = true; 
    } 
  } 
}

我们遍历keyword_attributes_in_assembly中的元素,对于每个KeywordAttribute属性,我们检查其关键字字符串是否与输入的strKeyword参数匹配,并且其相关性值是否等于或高于输入的iMinRelevance参数。如果是,我们就找到了一个由匹配的KeywordAttribute属性标记的类型(实际上是程序集本身)。

因此,我们创建一个KeywordAttributedType struct,用适当的值填充它,然后将其添加到前面创建的current_keyword_attributed_assemblyKeywordAttributedType数组m_KeywordAttributedTypesArray中。我们还将bool bAddToKeywordAttributedAssembliesArray设置为true。后面将讨论将此bool变量设置为true的效果。

接下来,我们执行一个大的迭代,遍历程序集中定义的所有类型。对于每个类型,我们将执行一系列操作来提取类型本身中、类型成员中以及如果成员是方法,则提取方法参数中的自定义属性(特别是KeywordAttribute类型)。

foreach (Type t in assembly.GetTypes()) 
{  
  // Check for the KeywordAttribute on the type itself. 
  KeywordAttribute[] keyword_attributes_in_types 
    = (KeywordAttribute[])
      (t.GetCustomAttributes(typeof(KeywordAttribute), true)); 
  
  if (keyword_attributes_in_types.Length > 0) 
  { 
    ... 
  } 
  // Enumerate over the members of the current type 
  foreach (MemberInfo member in t.GetMembers()) 
  { 
    // Check for the KeywordAttribute on the member 
    KeywordAttribute[] keyword_attributes_in_type_members 
      = (KeywordAttribute [])
         (member.GetCustomAttributes(typeof(KeywordAttribute), true)); 
    if (keyword_attributes_in_type_members.Length > 0) 
    { 
      ... 
    } 
    // If the current member is a method, we determine if any of the 
    // method's parameters is also Keyword Attributed. 
    if (member.MemberType == MemberTypes.Method) 
    { 
     ... 
    } 
  } 
}

收集类型本身中包含的KeywordAttribute属性

我们首先获取与类型本身关联的自定义属性(特别是KeywordAttribute类型)。这些KeywordAttribute属性(如果有)存储在KeywordAttribute数组keyword_attributes_in_types中。

// Check for the KeywordAttribute on the type 
KeywordAttribute[] keyword_attributes_in_types 
  = (KeywordAttribute[])
    (t.GetCustomAttributes(typeof(KeywordAttribute), true));

如果此数组的大小非零,则我们遍历数组中包含的每个KeywordAttribute属性,并且只选择那些由特定关键字字符串(输入参数strKeyword)和高于或等于最小值的相关性值(输入参数iMinRelevance)参数化的属性。

if (keyword_attributes_in_types.Length > 0) 
{ 
  foreach (KeywordAttribute a in keyword_attributes_in_types) 
  { 
    if ((a.Keyword == strKeyword) && (a.Relevance >= iMinRelevance)) 
    { 
      KeywordAttributedType keyword_attributed_type = 
                                       new KeywordAttributedType(); 
      keyword_attributed_type.m_TypeName = t.ToString(); 
      keyword_attributed_type.m_iRelevance = a.Relevance; 
      current_keyword_attributed_assembly.m_KeywordAttributedTypesArray.Add
                                           ((object)keyword_attributed_type); 
      bAddToKeywordAttributedAssembliesArray = true; 
    } 
  } 
}

像我们为程序集级别的KeywordAttribute所做的那样,对于每个选定的KeywordAttribute,我们创建一个KeywordAttributedType struct keyword_attributed_type。然后,我们将它的m_TypeName字段设置为当前类型的字符串形式,并将它的m_iRelevance字段设置为当前KeywordAttribute的相关性值。下面列出了这样一个KeywordAttribute属性类型的示例

[Keyword("test", 7)] 
public class KeywordAttributedClass01 
{ 
   ... 
   ... 
   ... 
}

在上面的示例中,KeywordAttributedClass01类是一个示例类型,它被KeywordAttribute属性标记,该属性由“test”和相关性值7参数化。我们还将这个keyword_attributed_type struct添加到前面创建的current_keyword_attributed_assemblyKeywordAttributedType数组m_KeywordAttributedTypesArray中。

最后,我们将bool变量bAddToKeywordAttributedAssembliesArray设置为true。后面将讨论将此bool变量设置为true的效果。

收集类型成员中包含的KeywordAttribute属性

请注意,我们仍在遍历程序集中定义的类型。我们刚刚处理了与当前*类型*关联的相关的KeywordAttribute属性。接下来,我们将遍历当前类型的*所有*成员。

// Enumerate over the members of the current type 
foreach (MemberInfo member in t.GetMembers()) 
{ 
   ... 
   ... 
   ... 
}

我们首先将附加到当前成员的所有KeywordAttribute自定义属性收集到一个KeywordAttribute数组keyword_attributes_in_type_members中。

// Check for the KeywordAttribute on the member 
KeywordAttribute[] keyword_attributes_in_type_members 
  = (KeywordAttribute [])
    (member.GetCustomAttributes(typeof(KeywordAttribute), true));

然后,一如既往,如果这个数组的大小非零,这意味着当前成员至少使用了一个KeywordAttribute属性。然后,我们遍历每个KeywordAttribute属性,只选择那些由特定关键字字符串strKeyword和等于或高于最小相关性值iMinRelevance参数化的属性。

if (keyword_attributes_in_type_members.Length > 0) 
{ 
  foreach (KeywordAttribute a in keyword_attributes_in_type_members) 
  { 
    if ((a.Keyword == strKeyword) && (a.Relevance >= iMinRelevance)) 
    { 
      ... 
      ... 
      ... 
    } 
  } 
}

一如既往,对于每个相关的KeywordAttribute属性,我们创建一个KeywordAttributedType struct,用相关可用信息填充它,然后将其添加到current_keyword_attributed_assemblym_KeywordAttributedTypesArray数组中。

KeywordAttributedType keyword_attributed_type = 
                                         new KeywordAttributedType(); 
keyword_attributed_type.m_TypeName = t.ToString() + "." + member.Name; 
keyword_attributed_type.m_iRelevance = a.Relevance; 
current_keyword_attributed_assembly.m_KeywordAttributedTypesArray.Add
                                     ((object)keyword_attributed_type); 
bAddToKeywordAttributedAssembliesArray = true;

最后,我们将bAddToKeywordAttributedAssembliesArray设置为true。请注意,bAddToKeywordAttributedAssembliesArray可能已经设置为true,但这无关紧要。

收集类型的方法成员参数中包含的KeywordAttribute属性

现在,在处理类型中包含的每个成员时,我们确定当前成员是否为Method。如果是,我们必须处理方法的参数,并确定是否有任何参数被KeywordAttribute属性标记。

if (member.MemberType == MemberTypes.Method) 
{ 
  MethodInfo mi = (MethodInfo)member; 
  foreach(ParameterInfo p in mi.GetParameters()) 
  { 
    KeywordAttribute[] keyword_attributes_in_method_parameters = 
                                               (KeywordAttribute [])
        (p.GetCustomAttributes(typeof(KeywordAttribute), true)); 
    if (keyword_attributes_in_method_parameters.Length > 0) 
    { 
      ... 
      ... 
      ... 
    } 
  } 
}

对于每个被KeywordAttribute属性标记的方法参数,KeywordAttribute数组keyword_attributes_in_method_parameters的大小将非零。我们处理其中每一个,只选择那些由特定关键字字符串strKeyword和大于或等于iMinRelevance的相关性值参数化的。

foreach (KeywordAttribute a in keyword_attributes_in_method_parameters) 
{ 
  if ((a.Keyword == strKeyword) && (a.Relevance >= iMinRelevance)) 
  { 
    ... 
    ... 
    ... 
  } 
}

一如既往,对于每个相关的KeywordAttribute属性,我们将创建一个KeywordAttributeType struct,用可用信息填充它,并将其添加到current_keyword_attributed_assemblym_KeywordAttributedTypesArray数组成员中。

KeywordAttributedType keyword_attributed_type = 
                                new KeywordAttributedType(); 
keyword_attributed_type.m_TypeName = "Parameter to Method " 
                                               + member.Name 
                                               + "() : " 
                                               + p.Name; 
keyword_attributed_type.m_iRelevance = a.Relevance; 
current_keyword_attributed_assembly.m_KeywordAttributedTypesArray.Add
                                     ((object)keyword_attributed_type); 
bAddToKeywordAttributedAssembliesArray = true;

最后,将bool变量bAddToKeywordAttributedAssembliesArray设置为true

将current_keyword_attributed_assembly添加到输入的KeywordAttributedAssembliesArray中

如果bool变量bAddToKeywordAttributedAssembliesArray设置为true,则之前创建的KeywordAttributedAssembly struct current_keyword_attributed_assembly将被添加到由CollectKeywordAttributedTypes()方法的调用者通过引用提供的输入的KeywordAttributedAssembliesArray中。

if (bAddToKeywordAttributedAssembliesArray) 
{ 
  KeywordAttributedAssembliesArray.Add
  ((object)current_keyword_attributed_assembly); 
}

回想一下,之前定义的KeywordAttributedAssembly struct current_keyword_attributed_assembly是为了保存正在处理的当前程序集的信息。我们提到,除非至少有一个类型被被特定关键字字符串和等于或高于特定最小值的相关性级别参数化的KeywordAttribute属性标记,否则current_keyword_attributed_assembly将无用。我们提到,如果确实如此,bAddToKeywordAttributedAssembliesArray将被设置为true

因此,在CollectKeywordAttributedTypes()方法的末尾,调用者的原始ArrayList KeywordAttributedAssembliesArray将可能增加一个KeywordAttributedAssembly struct,也可能不增加。

构建源代码

下载源代码zip文件KeywordAttributeSystem_src.zip后,将其解压到您选择的文件夹。接下来,构建顺序很重要。请在编译过程中遵循以下指南:

  • 首先构建KeywordAttribute程序集并将其注册到GAC。

    首先应该构建KeywordAttribute项目(KeywordAttribute.sln)。它将在以下目录中找到:<主目录>\KeywordAttribute,其中<主目录>是您解压文件的位置。之后,通过调用项目文件夹中包含的批处理文件RegisterAssemblyToGAC.bat将生成的KeywordAttribute.dll注册到全局程序集缓存。

  • 构建DirectoryListerInterface项目。

    接下来应该编译DirectoryListerInterface.sln项目。它将在以下目录中找到:<主目录>\DirectoryLister\Interfaces\DirectoryListerInterface,其中<主目录>是您解压文件的位置。然后可以将生成的程序集DirectoryListerInterface.dll引用到我们接下来将编译的实现代码中。

  • 构建DirectoryListerBase项目。

    然后应该构建DirectoryListerBase.sln项目。它将在以下目录中找到:<主目录>\DirectoryLister\Implementations\DirectoryListerBase,其中<主目录>是您解压文件的位置。然后可以将生成的程序集DirectoryListerBase.dll引用到另一个实现项目AssemblyFilesLister.sln

  • 构建AssemblyFilesLister项目。

    接下来应该构建AssemblyFilesLister.sln项目。它将在以下目录中找到:<主目录>\DirectoryLister\Implementations\AssemblyFilesLister,其中<主目录>是您解压文件的位置。然后可以将生成的程序集AssemblyFilesLister.dll引用到KeywordAttributeAssemblyLister项目。

  • 构建KeywordAttributeAssemblyLister项目。

    现在应该编译KeywordAttributeAssemblyLister.sln项目。它将在以下目录中找到:<主目录>\DirectoryLister\Clients\KeywordAttributeAssemblyLister,其中<主目录>是您解压文件的位置。请注意,此项目对KeywordAttribute程序集的引用应该使“复制本地”属性设置为“False”。这非常重要。这将确保在运行时,加载并使用GAC中注册的KeywordAttribute程序集(而不是本地副本)。

  • 构建测试程序集。

    我准备了几个测试程序集项目,位于以下目录:<主目录>\DirectoryLister\Clients\KeywordAttributedAssemblies,其中<主目录>是您解压文件的位置。这些项目生成测试程序集,每个程序集包含使用KeywordAttribute属性的各种类型。所有测试程序集都是强命名的,因此可以注册到GAC。

    现在可以逐个构建测试程序集。与KeywordAttributeAssemblyLister.sln项目一样,每个测试程序集对KeywordAttribute程序集的引用应使“复制本地”属性设置为“False”

    这样,在运行时,当KeywordAttributeAssemblyLister.exe加载每个测试程序集时,将在GAC中注册的(而不是本地副本)KeywordAttribute程序集(这是一个公共依赖程序集)将被加载和使用。

测试KeywordAttributeAssemblyLister应用程序

KeywordAttributeAssemblyLister应用程序接受三个命令行参数:

  • 参数1:要扫描程序集的顶级目录的完整路径。
  • 参数2:关键字字符串。
  • 参数3:最小相关性级别。

因此,要扫描目录“c:\MyAssemblies”中包含由关键字字符串“test”和最小相关性值3参数化的KeywordAttribute属性标记的类型的程序集,我们将在命令行键入以下内容:KeywordAttributeAssemblyLister.exe c:\MyAssemblies test 3

下图显示了使用我的测试程序集(位于目录:D:\Limbl\Temp\DirectoryLister\Clients\KeywordAttributedAssemblies)搜索关键字“test”和最小相关性值8的示例搜索结果。

要扫描GAC,请将GAC的完整路径作为第一个参数传递给KeywordAttributeAssemblyLister应用程序,例如“C:\WINDOWS\assembly”。

结论

我们已经演示了一个非常有用的自定义属性示例,该示例具有商业价值,这要归功于Adam Nathan。KeywordAttributeAssemblyLister应用程序有许多可能的增强方式,包括将其转换为*类库*或将其变成*远程服务器*,该服务器可以提供返回目标计算机GAC中存储的相关KeywordAttribute属性程序集列表的功能。

对于.NET初学者来说,我希望本文演示了如何轻松创建*自定义属性*并利用*反射*的多功能性。如前所述,DirectoryListerBaseAssemblyFilesLister类本身是非常有用的类,可以用于各种项目中。

我希望您从本文中受益。如果您在解释性文本中发现任何错误,或者在源代码中发现任何错误,或者您只是需要与我澄清关于本文的任何问题,请随时与我联系。

致谢和参考

  • Professional C#,作者:Simon Robinson、Ollie Cornes、Jay Glynn、Buston Harvey、Craig McQueen、Jerod Moemeka、Christian Nagel、Morgan Skinner、Karli Watson。由Wrox Press出版。
  • .NET and COM The Complete Interoperability Guide,作者:Adam Nathan。由SAMS Publishing出版。KeywordAttribute属性的源代码取自Adam Nathan的书。
© . All rights reserved.