# C# 互操作性入门系列(二):使用平台调用调用Win32 函数
**C#互操作系列文章:**
1. [**C#互操作性入门系列(一):C#中互操作性介绍**](http://www.cnblogs.com/zhili/archive/2013/01/14/NetInterop.html)
2. [**C#互操作性入门系列(二):使用平台调用调用Win32 函数**](http://www.cnblogs.com/zhili/archive/2013/01/21/PInvoke.html)
3. [**C#互操作性入门系列(三):平台调用中的数据封送处理**](http://www.cnblogs.com/zhili/archive/2013/01/23/DataSend.html)
4. [**C#互操作性入门系列(四):在C# 中调用COM组件**](http://www.cnblogs.com/zhili/archive/2013/01/27/COMInterop.html)
**本专题概要:**
* **引言**
* **如何使用平台调用Win32 函数——从实例开始**
* **当调用Win32函数出错时怎么办?——获得Win32函数的错误信息**
* **小结**
**一、引言**
上一专题对.NET 互操作性做了一个全面的概括,其中讲到.NET平台下实现互操作性有三种技术——平台调用,C++ Interop和COM Interop,今天在这个专题中将会大家介绍第一种技术,即平台调用。然而朋友们应该会有这样的疑问,平台调用到底有什么用呢? 为什么我们要用平台调用的技术了?**对于这两个问题的答案就是——平台调用可以帮助我们实现在.NET平台下(也就是指用C#、VB.net语言写的应用程序下)可以调用非托管函数(指定的是C/C++语言写的函数)。**这样如果我们在.NET平台下实现的功能有现有的C/C++ 函数实现了这样的功能,这时候我们完全没必要自己再用托管语言(如C#、vb.net)去实现一个这样的功能,这时候我们应该想到 “拿来主义”,直接使用平台调用技术调用C/C++ 实现的函数。然而在实际应用中,使用平台调用技术来调用Win32 API较为普遍,所以在这个专题中将为大家具体介绍了如何使用平台调用来调用Win32函数以及调用过程中应该注意的问题,下面就从一个具体的实例开始本专题的介绍。
**二、如何使用平台调用Win32 函数——从实例开始**
在前一个专题中已经介绍了使用平台调用来调用非托管函数的步骤:
**(1). 获得非托管函数的信息,即dll的名称,需要调用的非托管函数名等信息**
**(2). 在托管代码中对非托管函数进行声明,并且附加平台调用所需要属性**
**(3). 在托管代码中直接调用第二步中声明的托管函数**
然而调用Win32 API函数还有一些问题需要注意的地方, 首先, **因为很多Win32 API函数都有ANSI和Unicode两个版本,所以在托管代码声明时需要指定调用调用函数的版本。** 然而很多Win32 API函数有ANSI和Unicode两个版本并不是随便说说的,而是有根据的。大家从调用步骤中可以看出,第一步就需要知道非托管函数声明,为了找到需要需要调用的非托管函数,可以借助两个工具——Visual Studio自带的**dumpbin.exe**和**depends.exe**,dumpbin.exe 是一个命令行工具,可以用于查看从非托管DLL中导出的函数等信息,可以通过打开Visual Studio 2010 Command Prompt(中文版为Visual Studio 命令提示(2010)),然后切换到DLL所在的目录,输入 dummbin.exe/exports dllName, 如 **dummbin.exe/exports User32.dll** 来查看User32.dll中的函数声明,关于更多命令的参数可以参看MSDN; 然而 depends.exe是一个可视化界面工具,大家可以从 “**VS安装目录\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\Tools\Bin\**” 这个路径找到,然后双击 depends.exe 就可以出来一个可视化界面(如果某些人安装的VS没有附带这个工具,也可以从官方网站下载:[http://www.dependencywalker.com/](http://www.dependencywalker.com/)),如下图:
![](https://box.kancloud.cn/2016-01-23_56a2eb3973953.png)
上图中 我用红色标示出 MessageBox 有两个版本,而MessageBoxA 代表的就是ANSI版本,而MessageBoxW 代笔的就是Unicode版本,这也是上面所说的依据。下面就看看 MessageBox的C++声明的(更多的函数的定义大家可以从MSDN中找到,这里提供MessageBox的定义在MSDN中的链接:[http://msdn.microsoft.com/en-us/library/windows/desktop/ms645505(v=vs.85).aspx](http://msdn.microsoft.com/en-us/library/windows/desktop/ms645505(v=vs.85).aspx) ):
现在已经知道了需要调用的Win32 API 函数的定义声明,下面就依据平台调用的步骤,在.NET 中实现对该非托管函数的调用,下面就看看.NET中的代码的:
```
using System;
**// 使用平台调用技术进行互操作性之前,首先需要添加这个命名空间** using System.Runtime.InteropServices;
namespace 平台调用Demo
{
class Program
{
// 在托管代码中对非托管函数进行声明,并且附加平台调用所需要属性
// 在默认情况下,CharSet为CharSet.Ansi
// 指定调用哪个版本的方法有两种——通过DllImport属性的CharSet字段和通过EntryPoint字段指定
** // 在托管函数中声明注意一定要加上 static 和extern 这两个关键字**
[DllImport("user32.dll")]
public static extern int MessageBox1(IntPtr hWnd, String text, String caption, uint type);
// 在默认情况下,CharSet为CharSet.Ansi
[DllImport("user32.dll")]
public static extern int MessageBoxA(IntPtr hWnd, String text, String caption, uint type);
// 在默认情况下,CharSet为CharSet.Ansi
[DllImport("user32.dll")]
public static extern int MessageBox(IntPtr hWnd, String text, String caption, uint type);
// 第一种指定方式,通过CharSet字段指定
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
public static extern int MessageBox2(IntPtr hWnd, String text, String caption, uint type);
// 通过EntryPoint字段指定
[DllImport("user32.dll", EntryPoint="MessageBoxA")]
public static extern int MessageBox3(IntPtr hWnd, String text, String caption, uint type);
[DllImport("user32.dll", EntryPoint = "MessageBoxW")]
public static extern int MessageBox4(IntPtr hWnd, String text, String caption, uint type);
static void Main(string[] args)
{
// 在托管代码中直接调用声明的托管函数
// 使用CharSet字段指定的方式,要求在托管代码中声明的函数名必须与非托管函数名一样
// 否则就会出现找不到入口点的运行时错误
//MessageBox1(new IntPtr(0), "Learning Hard", "欢迎", 0);
// 下面的调用都可以运行正确
//MessageBoxA(new IntPtr(0), "Learning Hard", "欢迎", 0);
//MessageBox(new IntPtr(0), "Learning Hard", "欢迎", 0);
// 使用指定函数入口点的方式调用
//MessageBox3(new IntPtr(0), "Learning Hard", "欢迎", 0);
// 调用Unicode版本的会出现乱码
MessageBox4(new IntPtr(0), "Learning Hard", "欢迎", 0);
}
}
}
```
运行正确的结果为:
![](https://box.kancloud.cn/2016-01-23_56a2eb398d565.png)
从代码的注释中可以看出,第一个调用MessageBox1会出现运行时错误,然而为什么改调用会出现 “无法在 DLL“user32.dll”中找到名为“MessageBox1”的入口点。”的错误呢? 为了知道为什么,这里就需要明白通过CharSet字段指定的这种方式的内部执行行为了。之所以会出现这个错误,是因为当指定CharSet为Ansi时,P/Invoke首先会通过根函数名在User32.dll中搜索,即不带后缀A的函数名MessageBox1 进行搜索,如果找到与跟函数一样名称的函数,就调用该函数;
如果没有找到则使用带后缀为A的函数MessageBox1A进行搜索,如果找到,则使用该函数,如果还是没有找到,则会出现 “无法在 DLL“user32.dll”中找到名为“MessageBox1”的入口点。”的错误。把CharSet指定为Unicode时,搜索方式是一样的,只是没找到根函数时会加W后缀进行搜索的。 从上面的搜索调用函数的过程中可以发现,因为user32.dll中既不存在MessageBox1函数也不存在MessageBox1A函数,所以才会出现调用错误。(朋友看到出现错误时,应该会有这样的疑问——我们如何捕捉错误来显示错误信息呢?这个疑问将会在下一部分解释。) 然而使用平台调用技术中,还需要注意下面4点:
(1). DllImport属性的**ExactSpelling**字段如果设置为**true**时,则在托管代码中声明的函数名必须与要调用的非托管函数名完全一致,因为从[ExactSpelling](http://msdn.microsoft.com/zh-cn/library/system.runtime.interopservices.dllimportattribute.exactspelling(v=vs.100).aspx)字面意思可以看出为 "准确拼写"的意思,当**ExactSpelling**设置为**true**时,此时会改变平台调用的行为,此时平台调用只会根据根函数名进行搜索,而找不到的时候不会添加 A或者W来进行再搜索,. MessageBox, platform invoke searches for **MessageBox** and fails when it cannot locate the exact spelling." data-guid="f890f75ea0bb3ec1eaa56ed400e56d45">例如,如果指定 **MessageBox**,则平台调用将搜索 **MessageBox**,如果它找不到完全相同的拼写则会出现找不到入口函数的错误。 从前面的代码中可以看出,我们在代码中并没有指定 **ExactSpelling** 字段,然而代码中却没有出现调用错误,这就说明在C#和托管C++语言中, **ExactSpelling** 默认值就是false的,然而在VB。NET中,**ExactSpelling**的默认值就是true, 所以以上代码如果转化为Vb.NET时,就需要显式指定**ExactSpelling** 字段为false,不然就会出现 “找不到函数入口的错误”。 为了让大家更加容易理解上面的理论,相信大家看到下面一张图会更加理解 **ExactSpelling**字段的含义的**:**
MessageBox, platform invoke searches for **MessageBox** and fails when it cannot locate the exact spelling." data-guid="f890f75ea0bb3ec1eaa56ed400e56d45">**![](https://box.kancloud.cn/2016-01-23_56a2eb399a452.jpg)**
MessageBox, platform invoke searches for **MessageBox** and fails when it cannot locate the exact spelling." data-guid="f890f75ea0bb3ec1eaa56ed400e56d45"> (2). 如果采用设置CharSet的值来控制调用函数的版本时,则需要在托管代码中声明的函数名必须与根函数名一致,否则也会调用出错,这点从平台调用过程中可以很好地理解,如果需要调用非托管函数名为 MessageBoxA,而你在托管代码声明为 MessageBox1,这样在搜索过程中明显就会提示找不到函数名的错误, 也就是上面代码中第一个调用出错的原因。
MessageBox, platform invoke searches for **MessageBox** and fails when it cannot locate the exact spelling." data-guid="f890f75ea0bb3ec1eaa56ed400e56d45"> (3). 如果通过指定DllImport属性的**EntryPoint**字段的方式来调用函数版本时,此时必须相应地指定与之匹配的CharSet设置,意思就是——如果指定EntryPoint为 MessageBoxW,那么必须将CharSet指定为CharSet.Unicode,如果指定EntryPoint为 MessageBoxA,那么必须将CharSet指定为CharSet.Ansi或者不指定,因为 CharSet默认值就是Ansi。**上面代码MessageBox4的调用之所以会出现乱码,是因为CharSet指定为Ansi(也是默认值)时, 平台调用将字符串按照ANSI编码方式封送到非托管内存中(在.NET 中,字符串的编码方式默认为Unicode的),即每个字符仅占一个字节,(而对于Unicode编码的字符串来说,字符串中的每个字符都是使用两个字节进行编码的),当非托管函数MessageBoxW开始执行时,它会把该内存中的数据按照Unicode编码处理,即每两个字节当做是一个Unicode字符,知道遇到双字节的‘\0’ 字符结束。所以非托管函数返回的结果也就出现乱码了。 如果指定EntryPoint 字段的值为MessageBoxA,却把CharSet字段设置为CharSet.Unicode的情况下,也会出现同样的乱码问题,如下图所示:**
MessageBox, platform invoke searches for **MessageBox** and fails when it cannot locate the exact spelling." data-guid="f890f75ea0bb3ec1eaa56ed400e56d45">**![](https://box.kancloud.cn/2016-01-23_56a2eb39b3987.jpg)**
MessageBox, platform invoke searches for **MessageBox** and fails when it cannot locate the exact spelling." data-guid="f890f75ea0bb3ec1eaa56ed400e56d45"> (4). CharSet还有一个可选字段为——CharSet.Auto, 如果把CharSet字段设置为CharSet.Auto,则平台调用会针对目标操作系统适当地自动封送字符串。Unicode on Windows NT, Windows 2000, Windows XP, and the Windows Server 2003 family; the default is Ansi on Windows 98 and Windows Me." data-guid="275999a6d1aff7e55c2abc3094f769ed">在 Windows NT、Windows 2000、Windows XP 和 Windows Server 2003 系列上,默认值为 Unicode;在 Windows 98 和 Windows Me 上,默认值为 Ansi。Auto, languages may override this default." data-guid="9676c285887e8c44ff8277aed31aadd1">尽管公共语言运行时默认值为 Auto,但使用语言可重写此默认值。Ansi." data-guid="0cb899b48496e2934a217215e86dfe6a">例如,默认情况下,C# 将所有方法和类型都标记为 Ansi。所以下面的调用一样也会出现乱码,原因在第三点中已经解释了,下面直接附上测试例子和结果:
```
class Program
{
[DllImport("user32.dll", EntryPoint = "MessageBoxA", CharSet = CharSet.Auto)]
public static extern int MessageBox5(IntPtr hWnd, String text, String caption, uint type);
static void Main(string[] args)
{
MessageBox5(new IntPtr(0), "Learning Hard", "欢迎", 0);
}
}
```
MessageBox, platform invoke searches for **MessageBox** and fails when it cannot locate the exact spelling." data-guid="f890f75ea0bb3ec1eaa56ed400e56d45">Ansi." data-guid="0cb899b48496e2934a217215e86dfe6a">运行结果为:
MessageBox, platform invoke searches for **MessageBox** and fails when it cannot locate the exact spelling." data-guid="f890f75ea0bb3ec1eaa56ed400e56d45">Ansi." data-guid="0cb899b48496e2934a217215e86dfe6a">![](https://box.kancloud.cn/2016-01-23_56a2eb39c57c6.jpg)
**三、当调用Win32函数出错时怎么办?——获得Win32函数的错误信息**
前面部分为大家演示了平台调用的使用以及使用过程需要注意的问题, 当大家了解了这些之后,肯定会有这样的一个疑问,当调用Win32函数过程中遇到由Win32函数返回的错误要怎样去处理呢? 或者由非托管函数的托管定义导致的错误或异常怎么捕捉,就如上面代码中调用**MessageBox1**出现异常时,如何捕捉并给用于一个友好的提示信息呢?对于这个两个问题,下面通过两个具体的例子来演示。
捕捉由托管定义导致的异常演示代码:
```
class Program
{
// 在托管代码中对非托管函数进行声明,并且附加平台调用所需要属性
// 在默认情况下,CharSet为CharSet.Ansi
// 指定调用哪个版本的方法有两种——通过DllImport属性的CharSet字段和通过EntryPoint字段指定
[DllImport("user32.dll")]
public static extern int MessageBox1(IntPtr hWnd, String text, String caption, uint type);
static void Main(string[] args)
{
try
{
MessageBox1(new IntPtr(0), "Learning Hard", "欢迎", 0);
}
catch (DllNotFoundException dllNotFoundExc)
{
Console.WriteLine("DllNotFoundException 异常发生,异常信息为: " + dllNotFoundExc.Message);
}
catch (EntryPointNotFoundException entryPointExc)
{
Console.WriteLine("EntryPointNotFoundException 异常发生,异常信息为: " + entryPointExc.Message);
}
Console.Read();
}
}
```
运行结果为: ![](https://box.kancloud.cn/2016-01-23_56a2eb39daead.png)
捕获由Win32函数本身返回异常的演示代码如下:
```
using System;
using System.ComponentModel;
// 使用平台调用技术进行互操作性之前,首先需要添加这个命名空间
using System.Runtime.InteropServices;
namespace 处理Win32函数返回的错误
{
class Program
{
// Win32 API
// DWORD WINAPI GetFileAttributes(
// _In_ LPCTSTR lpFileName
//);
// 在托管代码中对非托管函数进行声明
[DllImport("Kernel32.dll",**SetLastError**=true,CharSet=CharSet.Unicode)]
public static extern uint GetFileAttributes(string filename);
static void Main(string[] args)
{
// 试图获得一个不存在文件的属性
// 此时调用Win32函数会发生错误
GetFileAttributes("FileNotexist.txt");
// 在应用程序的Bin目录下存在一个test.txt文件,此时调用会成功
//GetFileAttributes("test.txt");
// 获得最后一次获得的错误
int lastErrorCode = Marshal.GetLastWin32Error();
// 将Win32的错误码转换为托管异常
//Win32Exception win32exception = new Win32Exception();
Win32Exception win32exception = new Win32Exception(lastErrorCode);
if (lastErrorCode != 0)
{
Console.WriteLine("调用Win32函数发生错误,错误信息为 : {0}", win32exception.Message);
}
else
{
Console.WriteLine("调用Win32函数成功,返回的信息为: {0}", win32exception.Message);
}
Console.Read();
}
}
}
```
运行结果为: ![](https://box.kancloud.cn/2016-01-23_56a2eb39e7925.png)
要想获得在调用Win32函数过程中出现的错误信息,首先必须将DllImport属性的SetLastError字段设置为true,只有这样,平台调用才会将最后一次调用Win32产生的错误码保存起来,然后会在托管代码调用Win32失败后,通过Marshal类的静态方法GetLastWin32Error获得由平台调用保存的错误码,从而对错误进行相应的分析和处理。这样就可以获得Win32中的错误信息了。
上面代码简单地演示了如何在托管代码中获得最后一次发生的Win32错误信息,然而还可以通过调用Win32 API 提供的**FormatMessage**函数的方式来获得错误信息,然而这种方式有一个很显然的弊端(所以这里就不演示了),当对**FormatMessage**函数调用失败时,这时候就有可能获得不正确的错误信息,所以,推荐采用.NET提供的Win32Exception异常类来获得具体的错误信息。关于更多的**FormatMessage**函数可以参考MSDN: [http://msdn.microsoft.com/en-us/library/ms679351(v=vs.85).aspx](http://msdn.microsoft.com/en-us/library/ms679351(v=vs.85).aspx)
**四、小结**
讲到这里,本专题的内容也就介绍完了,本专题只是简单介绍了使用平台调用技术来调用Win32函数,然而实际的操作远远不是这么简单的,要掌握平台调用的技术,还需要大家在工作过程多多实践。因为在本专题中涉及了一些数据封送一些知识,为了帮助大家更好掌握数据封送处理,在一个专题将为大家带来平台调用中的数据封送处理专题。
- C# 基础知识系列
- C# 基础知识系列 专题一:深入解析委托——C#中为什么要引入委托
- C# 基础知识系列 专题二:委托的本质论
- C# 基础知识系列 专题三:如何用委托包装多个方法——委托链
- C# 基础知识系列 专题四:事件揭秘
- C# 基础知识系列 专题五:当点击按钮时触发Click事件背后发生的事情
- C# 基础知识系列 专题六:泛型基础篇——为什么引入泛型
- C# 基础知识系列 专题七: 泛型深入理解(一)
- C# 基础知识系列 专题八: 深入理解泛型(二)
- C# 基础知识系列 专题九: 深入理解泛型可变性
- C#基础知识系列 专题十:全面解析可空类型
- C# 基础知识系列 专题十一:匿名方法解析
- C#基础知识系列 专题十二:迭代器
- C#基础知识 专题十三:全面解析对象集合初始化器、匿名类型和隐式类型
- C# 基础知识系列 专题十四:深入理解Lambda表达式
- C# 基础知识系列 专题十五:全面解析扩展方法
- C# 基础知识系列 专题十六:Linq介绍
- C#基础知识系列 专题十七:深入理解动态类型
- 你必须知道的异步编程 C# 5.0 新特性——Async和Await使异步编程更简单
- 全面解析C#中参数传递
- C#基础知识系列 全面解析C#中静态与非静态
- C# 基础知识系列 C#中易混淆的知识点
- C#进阶系列
- C#进阶系列 专题一:深入解析深拷贝和浅拷贝
- C#进阶系列 专题二:你知道Dictionary查找速度为什么快吗?
- C# 开发技巧系列
- C# 开发技巧系列 使用C#操作Word和Excel程序
- C# 开发技巧系列 使用C#操作幻灯片
- C# 开发技巧系列 如何动态设置屏幕分辨率
- C# 开发技巧系列 C#如何实现图片查看器
- C# 开发技巧 如何防止程序多次运行
- C# 开发技巧 实现属于自己的截图工具
- C# 开发技巧 如何使不符合要求的元素等于离它最近的一个元素
- C# 线程处理系列
- C# 线程处理系列 专题一:线程基础
- C# 线程处理系列 专题二:线程池中的工作者线程
- C# 线程处理系列 专题三:线程池中的I/O线程
- C# 线程处理系列 专题四:线程同步
- C# 线程处理系列 专题五:线程同步——事件构造
- C# 线程处理系列 专题六:线程同步——信号量和互斥体
- C# 多线程处理系列专题七——对多线程的补充
- C#网络编程系列
- C# 网络编程系列 专题一:网络协议简介
- C# 网络编程系列 专题二:HTTP协议详解
- C# 网络编程系列 专题三:自定义Web服务器
- C# 网络编程系列 专题四:自定义Web浏览器
- C# 网络编程系列 专题五:TCP编程
- C# 网络编程系列 专题六:UDP编程
- C# 网络编程系列 专题七:UDP编程补充——UDP广播程序的实现
- C# 网络编程系列 专题八:P2P编程
- C# 网络编程系列 专题九:实现类似QQ的即时通信程序
- C# 网络编程系列 专题十:实现简单的邮件收发器
- C# 网络编程系列 专题十一:实现一个基于FTP协议的程序——文件上传下载器
- C# 网络编程系列 专题十二:实现一个简单的FTP服务器
- C# 互操作性入门系列
- C# 互操作性入门系列(一):C#中互操作性介绍
- C# 互操作性入门系列(二):使用平台调用调用Win32 函数
- C# 互操作性入门系列(三):平台调用中的数据封送处理
- C# 互操作性入门系列(四):在C# 中调用COM组件
- CLR
- 谈谈: String 和StringBuilder区别和选择
- 谈谈:程序集加载和反射
- 利用反射获得委托和事件以及创建委托实例和添加事件处理程序
- 谈谈:.Net中的序列化和反序列化
- C#设计模式
- UML类图符号 各种关系说明以及举例
- C#设计模式(1)——单例模式
- C#设计模式(2)——简单工厂模式
- C#设计模式(3)——工厂方法模式
- C#设计模式(4)——抽象工厂模式
- C#设计模式(5)——建造者模式(Builder Pattern)
- C#设计模式(6)——原型模式(Prototype Pattern)
- C#设计模式(7)——适配器模式(Adapter Pattern)
- C#设计模式(8)——桥接模式(Bridge Pattern)
- C#设计模式(9)——装饰者模式(Decorator Pattern)
- C#设计模式(10)——组合模式(Composite Pattern)
- C#设计模式(11)——外观模式(Facade Pattern)
- C#设计模式(12)——享元模式(Flyweight Pattern)
- C#设计模式(13)——代理模式(Proxy Pattern)
- C#设计模式(14)——模板方法模式(Template Method)
- C#设计模式(15)——命令模式(Command Pattern)
- C#设计模式(16)——迭代器模式(Iterator Pattern)
- C#设计模式(17)——观察者模式(Observer Pattern)
- C#设计模式(18)——中介者模式(Mediator Pattern)
- C#设计模式(19)——状态者模式(State Pattern)
- C#设计模式(20)——策略者模式(Stragety Pattern)
- C#设计模式(21)——责任链模式
- C#设计模式(22)——访问者模式(Vistor Pattern)
- C#设计模式(23)——备忘录模式(Memento Pattern)
- C#设计模式总结
- WPF快速入门系列
- WPF快速入门系列(1)——WPF布局概览
- WPF快速入门系列(2)——深入解析依赖属性
- WPF快速入门系列(3)——深入解析WPF事件机制
- WPF快速入门系列(4)——深入解析WPF绑定
- WPF快速入门系列(5)——深入解析WPF命令
- WPF快速入门系列(6)——WPF资源和样式
- WPF快速入门系列(7)——深入解析WPF模板
- WPF快速入门系列(8)——MVVM快速入门
- WPF快速入门系列(9)——WPF任务管理工具实现
- ASP.NET 开发
- ASP.NET 开发必备知识点(1):如何让Asp.net网站运行在自定义的Web服务器上
- ASP.NET 开发必备知识点(2):那些年追过的ASP.NET权限管理
- ASP.NET中实现回调
- 跟我一起学WCF
- 跟我一起学WCF(1)——MSMQ消息队列
- 跟我一起学WCF(2)——利用.NET Remoting技术开发分布式应用
- 跟我一起学WCF(3)——利用Web Services开发分布式应用
- 跟我一起学WCF(3)——利用Web Services开发分布式应用
- 跟我一起学WCF(4)——第一个WCF程序
- 跟我一起学WCF(5)——深入解析服务契约 上篇
- 跟我一起学WCF(6)——深入解析服务契约 下篇
- 跟我一起学WCF(7)——WCF数据契约与序列化详解
- 跟我一起学WCF(8)——WCF中Session、实例管理详解
- 跟我一起学WCF(9)——WCF回调操作的实现
- 跟我一起学WCF(10)——WCF中事务处理
- 跟我一起学WCF(11)——WCF中队列服务详解
- 跟我一起学WCF(12)——WCF中Rest服务入门
- 跟我一起学WCF(13)——WCF系列总结
- .NET领域驱动设计实战系列
- .NET领域驱动设计实战系列 专题一:前期准备之EF CodeFirst
- .NET领域驱动设计实战系列 专题二:结合领域驱动设计的面向服务架构来搭建网上书店
- .NET领域驱动设计实战系列 专题三:前期准备之规约模式(Specification Pattern)
- .NET领域驱动设计实战系列 专题四:前期准备之工作单元模式(Unit Of Work)
- .NET领域驱动设计实战系列 专题五:网上书店规约模式、工作单元模式的引入以及购物车的实现
- .NET领域驱动设计实战系列 专题六:DDD实践案例:网上书店订单功能的实现
- .NET领域驱动设计实战系列 专题七:DDD实践案例:引入事件驱动与中间件机制来实现后台管理功能
- .NET领域驱动设计实战系列 专题八:DDD案例:网上书店分布式消息队列和分布式缓存的实现
- .NET领域驱动设计实战系列 专题九:DDD案例:网上书店AOP和站点地图的实现
- .NET领域驱动设计实战系列 专题十:DDD扩展内容:全面剖析CQRS模式实现
- .NET领域驱动设计实战系列 专题十一:.NET 领域驱动设计实战系列总结