破解.NET 2.0配置之谜(一)

引言

NET的美妙特点之一是它的XML配置功能。在.NET 1.x时代,常见的应用程设置、数据库连接字符串、ASP.NET Web服务器配置和基本的自定义配置数据可以存储在.config文件中。自定义配置节可以使用一些基本自定义结构,允许少数几种信息存储 在.config文件中。然而更复杂的配置,最常见的实现是自定义XML结构和自定义解析代码。尽管有多种不同性能的方法完成同样的事情,这种代码将变得 相当复杂。

随着.NET 2.0,自己编写(可能很复杂、低性能、繁琐)代码来管理自定义XML配置结构的时代已经结束了。.NET 2.0内置的XML配置子系统自定义配置能力已经大大革新,拥有一些非常有用的和节省时间的功能。几乎任何XML配置结构你可能需要相对更少的工作且更容 易。此外,反序列化.config中的XML总是可以重载的。这使得任何XML结构可以不失去.NET 2.0配置支持的其它高级功能。

揭露详情

本文继续.NET 2.0的配置系列。本文的目的揭露.NET 2.0配置框架的所有细节和内部运作机制使开发人员能够更好地利用它提供的广泛功能。如果您是.NET 2.0配置的新手,或尚未掌握类型验证和转换的概念,您应该首先阅读以前的文章,可以在以下链接找到:

  1. 揭开.NET 2.0配置之谜(一)
  2. 揭开.NET 2.0配置之谜(二)
  3. 揭开.NET 2.0配置之谜(三)
  4. 解码.NET 2.0配置之谜(一)
  5. 解码.NET 2.0配置之谜(二)

请 注意,本文提供的代码示例只对文中提出的各点澄清。在前面两篇本系列文章提供可编译和可下载的代码示例(译注:代码请到原文链接下载)。这篇文章的目标是 比前面两篇文章讲解更详细且更高级,而不是提供可编译,可运行的代码示例。相反,我们的目标是揭露.NET 2.0配置框架的隐含核心理论和重要的细节。期望的结果是,读完本系列的文章后任何人都有兴趣使用.NET 2.0配置。

配置的主题

欢迎来到.NET 2.0配置之谜系列的第三部分。本文将介绍.NET 2.0配置框架完整详细的细节。我将揭露框架的鲜为人知的特性、功能、怪癖和内部运作机制。几个架构图将展示以提供将要讨论的细节可视化的支持。

  1. Configuration Structure
    1. Hierarchical Configuration
      • Contexts
      • Merging
    2. Configuration Architecture
  2. Configuration Management
    1. ConfigurationManager
      • WebConfigurationManager
      • ConfigurationFileMap
        • ExeConfigurationFileMap
        • WebConfigurationFileMap
    2. Configuration
      • ContextInformation
  3. Configuration Representation
    1. ConfigurationElement
      • ConfigurationSection
      • ConfigurationElementCollection
    2. Non-Element Containers
      • ConfigurationSectionGroup
      • ConfigurationSectionCollection
      • ConfigurationSectionGroupCollection
  4. Configuration Metadata
    1. Contexts
      • ExeContext
      • WebContext
    2. ConfigurationProperty
    3. ConfigurationElementProperty
    4. ElementInformation
      • PropertyInformation

(译注:上面这些英语很简单,我这里就不翻译了,见下面的标题。5、Configuration Serialization,6、Web Configuration and LocationsJon Rista还没有写完,似乎也不准备写了。)

1、配置结构

许 多形式的配置可用于现代的.NET 2.0应用程序。二进制数据、INI文件、数据库、XML、甚至任意文本结构。根据环境、应用类型、使用因素等等,你的配置存储需求可能会改变。在大多数 情况下,大部分.NET应用(以及编写他们的程序员们)只需要能够存储配置……并不真正关心怎么存储。大多数配置往往希望是分层的和可手动编辑的。这使得 XML是一个理想的平台,在其上建立一个配置的存储框架。这也使得.NET 2.0的配置框架对.NET 2.0开发者非常有吸引力。

1.1、层次配置

“分 层”是重要的方面,当涉及到.NET 2.0配置框架时,并在不止一个方面。除了作为XML的一个应用外,且提供了一个分层的媒介存储实际配置设置,.NET 2.0配置框架在自己的权利范围是分层的(译注:the .NET 2.0 configuration framework is hierarchical in it's own right)。根据应用程序的内容,多个配置文件的层次秩序自然是存在的且当代码请求配置时会合并在一起。这种分层结构的配置文件允许设置应用在各级,从 整个机器到应用程序,甚至到个别用户或资源请求。

.NET 2.0配置框架的分层结构可以在下面的图中看出。这个图有几个重点需要注意,配置文件合并的秩序及不同配置环境中可用的配置。根据环境,什么可以配置这些设置,以及那些设置是如何合并的,是不同的。

Hierarchy

图1  Configuration File Hierarchy

1.1.1、上下文

在.NET 的世界中,有两种主要类型的应用程序:Windows应用程序和Web应用程序。Windows应用程序,或可执行文件,有一个相对简单的4层配置结构和 合并过程。另一方面,Web应用程序,为特定的位置应用和合并配置,有一个更复杂的结构。这两个主要类型的应用程序都创建一个配置上下文,管理如何应用配 置。

独立于上下文的是机器级(machine-level)配置。机器级配置应用于任何运行在CLR里的应用程序,以及.NET框架自己 使用的所有其他配置选项。他可能令许多人吃惊,但是.NET框架使用跟本系列讨论的同样的配置框架。可能更吃惊,没有配置功能在.NET框架中观察到的是 特别的或专门的……你可以做同样的事情。

快速检查machine.config文件(在%SYSTEMROOT%\Microsoft.NET\Framework\v2.0.50727\CONFIG下能找到)将证明,这里定义了所有的“预配置”或“默认”ConfigurationSectionGroupConfigurationSection元素。诸如此类appSettings, connectionStrings, system.web等 等。许多默认设置,例如encryption providers、默认ASP.NET memebership、profile和role providers等等,都应用在这里。在相同的目录下,应该也注意到这个文件的comment版、default版(译注:即 machine.config.comments,它包含有用的注释;machine.config.default,包含默认设置。配置系统不使用这些 文件来配置应用程序。)、以及一些不同安全级别的默认web.config文件。然而编辑machine.config文件是危险的,也是唯一的方法应用 设置全局到每个应用程序。在机器(machine)级下,配置分成两种可用的上下文(情况)……Exe和web。

Exe上下文对任何可执 行应用程序可用。这包括Windows Forms,Windows Services 和Console applications。在此上下文中,核心配置基础理解成4个级别的配置:Machine、Application、Roaming User 、User。在“上下文”中每个级别按序更具体。因此,低级别可以重写高级别定义的设置。值得着重注意的是,Roaming User配置比User配置级别更高。这允许用户特定设置存储在台式机和笔记本电脑的是相互独立的(译注:This allows user-specific settings to reside on both a desktop and a laptop independent of each other),而Roaming设置是在两者之间共享的且都可用的。

Web上下文仅仅对运行在 ASP.NET主机上(不一定要有IIS…任何ASP.NET都可以,包括VS2005开发服务器或使用 System.Web.Hosting.ApplicationHost的客户web服务器)可用。不像Exe上下文,以用户相关,web上下文视位置而 定。配置应用于特定的web站点位置,可能显示地配置在一个web.config文件,或隐式地从多个web.config文件合并。基于配个用户基础的 配置在web配置上下文通常是不可行的,即使用户是合理认证的。

1.1.2、合并

.NET 2.0配置的分层性质提供了很大程度的灵活性,允许特定的用户或位置有自己的配置设置。然而,那些配置不是孤立的,多个层次上重复的设置可以在低层次上重 写。我们可以在图1中看到,大部分较具体的配置文件合并到较不具体的,最具体的设置重写了最不具体的。在Exe上下文中,用户(或者更准确地说,本地用 户)的设置时最具体的规定,随后是漫游用户(在两个或更多机器上共享),应用程序,最后是机器。

在web上下文中,合并稍微有点复杂。除了多个*.config文件从更具体的位置(i.e.\wwwroot\siteA\web.config is more specific than \wwwroot\web.config when merged)合并,一个应用程序特定位置的配置可以直接定义在单个web.config文件。web.config和位置合并之间的细微差别将在本文的后面更详细地讨论。

1.2、配置体系结构

.NET 2.0的配置框架自身相当复杂,然而却提供了快速、简单的方法实现自定配置。配置框架不仅提供实现自定义配置的方法,而且提供方法实现自定义数据验证和转 换、自定义providers,公开的自定义序列化和反序列化钩子,甚至允许配置进行加密。关于配置元素的元数据(Metadata)也是公开的,提供关 于加载什么数据、从哪加载数据、怎么加载数据的细节。.NET 2.0的配置框架的体系结构显示在图2中。图中每个特定的元素是本文你进一步将读到的主题。

Architecture2_thumb66

图2 Configuration Framework Architectur

在这个看起来复杂的图背后是一个优美的、合乎逻辑和有效地配置管理系统。我试图将这些体系架构中单独的组件组织成逻辑组,形成了本文的核心部分。

Configuration Management:
检索、存储、查找或映射配置文件。
Configuration Representation: 存储结构用于表示配置节、元素、属性。 Configuration Metadata: 关于配置的来源、什么上下文中可用即其他一个细节 。 Configuration Serialization: 装载和保存配置数据,自定义这些处理。
2、配置管理(Configuration Management

第 一个要详细讨论的概念,逻辑上也是你的项目配置工作开始的地方。配置管理,在本文的范围,是指查找配置(直接或映射到特定配置文件),检索配置节和使用时 存储那些配置节。配置文件可能会被直接加载,提供额外的功能和允许特定*.config文件(VS. 默认映射的)被加载。映射到特定配置文件根据你操作的上下文不同而不同,但是允许任何配置文件被加载,提供必要的赋予权限读取资源。

2.1、ConfigurationManager

ConfigurationManager和后面的WebConfigurationManager,是访问.NET 2.0配置主要的出发点。ConfigurationManager提供了所有核心服务,检索配置节和包含在其中的设置,也提供方法允许配置文件被显示地加载在Exe上下文中。WebConfigurationManager提供了额外的方法显示地加载配置,在web上下文中,同时也作为使用ConfigurationManager的方法的代理。这两个静态类见下图,以供参考。

ConfigurationManagerDiagram_thumb

图3  ConfigurationManager and WebConfigurationManager

ConfigurationManager类提供的主要功能,GetSection(string sectionName),已经在此系列前面的文章详细描述了,这里将不重复了。默认情况下,ConfigurationManager类提供隐式地只读访问配置。通常,配置扩展简单的只读和保存也是需要的。ConfigurationManager公开的几种方法,允许配置文件在一个明确的上下文中打开。第一种方法是使用ConfigurationManager.OpenExeConfiguration()方法。他们提供读/写访问配置文件,通过配置类。

有两个重载的OpenExeConfiguration()方法。一个重载采用一个字符串表示当前运行可执行文件路径作为参数,另外一个采用ConfigurationUserLevel枚举值作为参数。第一种方法你提供的文件名要附加“.config”并加载配置文件。要着重注意的是,OpenExeConfiguration(string exePath)方法是一个非常误导人的方法,因为文件名不一定得是.exe执行文件。配置发烧友的一个重大的发现(不可达的,但…基于我的Internet搜索)可以通过这个方法,提供它的正确使用。考虑一下情形:

需求
定义配置设置在应用程序级。
通过一个所谓的配置类库使用。
允许类库有他自己的 *.dll.config文件。
问题
ConfigurationManager.GetSection() 仅仅返回节,从主 Application.exe.config 文件。
ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None) 只打开主 Application.exe.config 文件。
解决方法
复制 ConfiguredClassLibrary.dll.config 到跟 ConfiguredClassLibrary.dll相同的目录下。
通用ConfigurationManager.GetSection()访问主应用程序设置.
调用 ConfigurationManager.OpenExeConfiguration("ConfiguredClassLibrary.dll") 加载特定的DLL配置文件.
通过上面的方法加载配置对象,访问 ConfiguredClassLibrary.dll.config 的设置。

上述方案,经常是足够了,允许多个配置文件(针对任何程序集,不仅仅是主要的exe)同时使用。尽管OpenExeConfiguration(string exePath)方法调用了一个DLL文件,但配置文件被ConfigurationManager访问不改变。任何其他代码访问存储在Application.exe.config 文件中的配置继续不加改变或冲突的访问。这个概念的简单证明,可以体现在以下代码:

// Assumes Library.dll and its corresponding Library.dll.config file exist, 

// are referenced and properly colocated with the .exe

Console.WriteLine("App (before): " + 
    ConfigurationManager.AppSettings["test"]);

Console.WriteLine("Loading Library.dll.config...");
Configuration other = 
    ConfigurationManager.OpenExeConfiguration("Library.dll");

Console.WriteLine("App (after): " + 
    ConfigurationManager.AppSettings["test"]);
Console.WriteLine("Lib (after): " + 
    other.AppSettings.Settings["test"].Value);

// Expected output

App (before): application
Loading Library.dll.config...
App (after): application
Lib (after): library


<!-- Application.exe.config -->
<configuration>
    <appSettings>
        <add key="test" value="application" />
    </appSettings>
</configuration>

<!-- Library.dll.config -->
<configuration>
    <appSettings>
        <add key="test" value="library" />
    </appSettings>
</configuration>

尽管它的误导性的签名和名字,OpenExeConfiguration(string exePath)是一个非常有效的方法解决大部分频繁配置的问题。这个方法还能从HTTP路径加载配置,可能是一个智能客户端使用ClickOnce部署的方案.。用OpenExeConfiguration(“http://someserver.com/clickonce/someapp.exe”),*.config文件为一个ClickOnce部署的应用程序可能会被确定和从它的正确文件夹加载(这是在安装和更新时自动生成的)。

第二个方法,OpenExeConfiguration(ConfigurationUserLevel level)将加载指定层次是的那个的配置文件。配置层次,在exe上下文可用,允许你指定任意你想要的exe、漫游用户、本地用户的配置。 ConfigurationUserLevel 枚举也有一点令人误解,从它的值命名。它能导致误解这个方法做什么的,及期望返回什么类型的配置。它每个值的真实含义如下:

None
指定没有"用户级别"。
没有用户级别默认为主exe配置文件.
此级别的配置配置路径是 MACHINE/EXE.
存储在 \[AppPath]\[AppName].exe.config.
PerUserRoaming
指定加载漫游用户的配置.
漫游用户的陪孩子是一个共享的配置,在任意的漫游用户间复制.
此级别的配置路径是 MACHINE/EXE/ROAMING_USER.
存储在 [ApplicationDataPath]\[AppName]\[CodedPath]\[Version]\User.config.
PerUserRoamingAndLocal
指定加载本地用户的配置.
此级别的配置路径是 MACHINE/EXE/ROAMING_USER/LOCAL_USER.
存储在 [LocalApplicationDataPath]\[AppName]\[CodedPath]\[Version]\User.config.

记住配置是分层的和可合并的。当请求漫游用户或本地用户的配置时,向上到machine.config将被合并,在给定用户级别应用程序可以访问结构完整的配置。一个有趣的结果是,这个合并允许你请求漫游用户或本地用户的配置,甚至当user.config文件不存在时。返回的配置对象将包含一个不存在的FilePath和HasFile属性是false。

漫游和本地用户配置设置是有趣的东西。一些.NET 2.0配置的微妙的行为变得有条理(或抬起他们丑陋的头,取决于你怎么看)。尽管他们丑陋的头,这些微妙的行为是另一个在自定义方法中使用.NET 2.0配置的重要原因。.NET配置框架提供一些扩展的安全特性,允许锁定设置、节、甚至节组(绝对地,或根据他们访问的等级)。考虑以下代码:

代码

这个例子应该打开本地User.config文件并获得(或建立,如果他不存在)一些CustomSection。这个自定义配置节的值是可编辑的和可保存修改。最终目标是加载任何现有的设置、或用新节和设置建立本地User.config文件,如果他不存在。这似乎很简单,但取决于“CustomSection”是否曾在roaming、exe、machine level定义,加配置节或编辑它,如果它已经存在,可能是不可能的。这种情况可能发生比较频繁,可能会变得非常复杂,当大量使用配置。原因和解决编辑/保存用户配置级别的配置的方案的详细资料,将在本文的配置元数据部分讨论。

除了刚才谈到的,存在一些其他的方法打开配置文件。不像OpenExeConfiguration()方法,它做了几个关于配置文件存储在哪的假定,OpenMappedExeConfiguration()OpenMappedMachineConfiguration()允许你显示地指定你的*.config文件存储在磁盘哪。使用这些方法,你能加载一个备用的machine.config,从你自己选定的位置加载User.config文件(vs.让.NET框架决定路径)等等。当访问machine.config,自定义版本不是必须的,应该使用OpenMachineConfiguration()。关于如何使用这些方法和相应的ConfigurationFileMap类的详细信息,将在ConfigurationFileMap类子部分。

ConfigurationManager最后一个公开的方法,RefreshSection(string sectionName),是另一个我经常被问到的问题之一的答案。上面描述的众多OpenConfiguration方法允许配装备打开读/写及保存改变。有些时候,然而,保存修改到配置通过ConfigurationManager类得不到相应的方法(特别是在web环境下)。有许多方法来解决这些问题,但最简单的方法保存之后立即就是用合适的节名字调用RefreshSection。这强迫(在大多数情况下……极少数情况下我报告他已经没有效了)ConfigurationManager下次调用GetSection() 时从硬盘加载和解析特定的配置节。

2.1.1、WebConfigurationManager

ConfigurationManager类,是访问Exe上下文中配置的第一步,在Web上下文中它是远远不够的。不像可执行文件,web应用程序没有一个确定的本地用户及那些可能创建的User.config文件。事实上,在web应用程序中没有特定用户的配置。尽管如此,考虑特定位置的配置时,web配置可以是一个更复杂的的东西。特定位置配置及如何使用WebConfigurationManager以充分利用它,是本文中将讨论。一个完整的WebConfigurationManager和ConfigurationLocations的讨论将在本文后面部分讨论。

2.1.2、ConfigurationFileMap

ConfigurationFileMap 是ConfigurationManager的OpenMappedExeConfiguration 和 OpenMappedMachineConfiguration方法的必要组成部分。这些类允许指定特定路径的*.config文件和当创建一个配置对象时OpenMapped 方法将执行所有合适的合并。ConfigurationFileMap 类表示一个机器配置文件映射和需要调用OpenMappedMachineConfiguration。此外文件映射类,Exe上下文的ExeConfigurationFileMap,Web上下文的WebConfigurationFileMap ,机器级除外都需要加载配置。

ConfigurationFileMapDiagram_thumb1

图4  The ConfigurationFileMap class hierarchy

2.1.2.1、ExeConfigurationFileMap

ExeConfigurationFileMap 允许你对专门配置机器的准确路径名、exe、漫游用户、本地用户配置文件,一起或零星的,当调用OpenMappedExeConfiguration()时。你不必指定所有的文件,但当配置对象创建时所有的文件将被确定及合并。 如果您指定一个自定义exe和本地配置文件,但不指定机器和漫游文件,默认机器和漫游文件将被找到并与指定exe和用户文件合并。这可能带来意想不到的后果,如果指定文件没有与默认文件保持适当的同步。

string appData = Environment.GetFolderPath(
Environment.SpecialFolder.ApplicationData);
string localData = Environment.GetFolderPath(
Environment.SpecialFolder.LocalApplicationData);

ExeConfigurationFileMap exeMap = 
new ExeConfigurationFileMap();
exeMap.ExeConfigFilename = 
"C:\Application\Default.config";
exeMap.RoamingUserConfigFilename = 
Path.Combine(appData, @"Company\Application\Roaming.config");
exeMap.LocalUserConfigFilename = 
Path.Combine(localData, @"Company\Application\Local.config");

Configuration exeConfig = 
ConfigurationManager.OpenMappedExeConfiguration(
exeMap, ConfigurationUserLevel.None);
Configuration roamingConfig = 
ConfigurationManager.OpenMappedExeConfiguration(exeMap, 
ConfigurationUserLevel.PerUserRoaming);
Configuration localConfig = 
ConfigurationManager.OpenMappedExeConfiguration(exeMap, 
ConfigurationUserLevel.PerUserRoamingAndLocal);

Console.WriteLine("MACHINE/EXE: " + exeConfig.FilePath);
Console.WriteLine(
"MACHINE/EXE/ROAMING_USER: " + roamingConfig.FilePath);
Console.WriteLine(
"MACHINE/EXE/ROAMING_USER/LOCAL_USER: " + localConfig.FilePath);

2.1.2.2、WebConfigurationFileMap

如果ConfigurationManager 类沿着是绿野仙踪(译注:绿野仙踪——the Yellow Brick Road to the Emerald City,应该都知道吧,不知道的去Google或百度)到配置第一步,那么Configuration 类绝对就是第二步。由ConfigurationManager 类公开的OpenConfiguration 方法之一调用将返回一个Configuration 对象。Configuration 对象代表合并的配置,用户级别、位置、你请求的文件映射的任何配置。不像ConfigurationManager ,Configuration 类公开配置节的读/写模式的完整详细细节。节和节组可以创建、删除和调整特们的安全设置,当通过Configuration 类访问时。

ConfigurationDiagram_thumb1

图5  The Configuration class

审查图5后,你应该看到Configuration 类比ConfigurationManager 类公开更多的信息和功能。一些信息是相同的,包括AppSettings ,ConnectionStrings , GetSection()方法。除了GetSection()方法外,Configuration 类也公开了GetSectionGroup(string sectionGroupName)方法,它允许您加载定义在配置文件中的ConfigurationSectionGroup 类。Configuration 类也公开了所有定义的ConfigurationSections 和 ConfigurationSectionGroups集合。Configuration 类也公开了重载的Save 和 SaveAs 方法,允许修改保存回现有的配置文件,或者创建一个新的。从前面的文章,你应该已经熟悉了加载和保存配置节和节组。

Configuration 类的一些特性还没有讨论,他们不是很明显,成为可能的节和节组。除了让你加载和保存已有的配置节和节组,你也可以添加和删除节组。这是一个强大的功能,允 许一个配置文件用代码编程地创建。当漫游或本地用户的配置,需要设置基本应用程序功能不必要的配置时,这是非常有用的。用这种方式创建时,一个重要因素要 注意。默认,节可能只被定义在machine和exe级。如果您需要添加新的配置节,甚至这个节将只在漫游或本地用户的*.config文件使用,你必须 先在exe添加节,然后在用户级修改节设置。考虑以下代码:

Configuration exeConfig = 
ConfigurationManager.OpenExeConfiguration(
ConfigurationUserLevel.None);
if (exeConfig.GetSection("customSection") == null)
{
    CustomSection section = new CustomSection();
    section.SectionInformation.AllowExeDefinition = 
ConfigurationAllowExeDefinition.MachineToLocalUser;
    exeConfig.Sections.Add("customSection", section);
    exeConfig.Save(ConfigurationSaveMode.Minimal);
}

Configuration userConfig = 
ConfigurationManager.OpenExeConfiguration(
ConfigurationUserLevel.PerUserRoamingAndLocal);
CustomSection section = 
userConfig.GetSection("customSection") as CustomSection;
section.SomeSetting = "some value";
userConfig.Save(ConfigurationSaveMode.Minimal);

上面的代码首次执行前,EXE和用户*.config文件应该向这样:

<!-- EXE .config file -->
<configuration>
</configuration>

<!-- USER .config file -->
<!-- DOES NOT EXIST YET! -->

上面的代码首次执行后,EXE和用户*.config文件应该向这样:

<!-- EXE .config file -->
<configuration>
    <configSections>
        <section name="test" type="Example.CustomSection, Example" 
allowExeDefinition="MachineToLocalUser" />
    </configSections>
</configuration>

<!-- USER .config file -->
<configuration>
    <test>
        <add key="key" value="value" />
    </test>
</configuration>

定义的限额(AllowDefinition 和 AllowExeDefinition)是重要因素,当使用多层次配置时。有两个前面提到的那些“微妙的行为”,使用.NET 2.0配置时会很复杂。详细解释定义限额和其他有类似微妙影响的设置将在本文的配置元数据那部分讨论。

2.2、ContextInformation

Configuration 类的一个重要属性(property)是EvaluationContext 属性(property)。这个属性公开了一个ContextInformation 类的实例,他提供访问上下文对象和一个标志指示配置对象代表machine.config,或一个应用程序,或用户级*.config文件。上下文对象公 开基本但有用的信息,可用于简化任务,否则,可能需要更复杂的逻辑。上下文类的详细解释见本文的配置元数据部分。

 

声明:此文是译文,实因水平有限,若有翻译不当之处请不吝指出,以免此译文误导他人,在此谢过! 未完....请继续关注,如果您觉得还不错望不吝推荐,能让更多的人看到,使开发者们更轻松的使用.NET 2.0的配置框架——这是Jon Rista的目的,也是我翻译这个的心愿!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值