使用 WCF/WPF 演示代码进行动态 LINQ to Entities 查询






4.93/5 (83投票s)
演示了跨 WCF 服务边界进行动态查询的方法。
目录
引言
我已经有一段时间没有撰写大型文章了,所以我想是时候纠正这一点,写一篇内容丰富而扎实的文章了。这篇文章就代表了这种扎实。
我想我应该先说明这篇文章将涵盖什么。实际上,它将涵盖很多内容。基本思路是,某个地方有一个安装了 Northwind 数据库的 SQL Server 数据库,我们将使用 ORM 通过 WCF 服务从该数据库获取数据。WCF 本身将托管在 Windows 服务中,该服务将通过自定义安装程序进行安装。
为了与这个 Windows 托管的 WCF 服务交互,我们将使用一个 WPF 客户端应用程序。WPF 客户端应用程序将允许用户使用自定义查询构建器查询特定实体(仅限客户,因为如果允许所有实体,我将永远写这篇文章),然后该查询构建器将用于发送到 Windows 托管的 WCF 服务,WCF 服务将反过来查询 ORM 以获取查询到的实体,并将结果返回给 WPF 客户端应用程序。
本质上,就是这样;它可能没有那么令人兴奋,但这里有足够的内容可以向您展示如何创建服务/客户端和托管 Windows 服务。此外,在此过程中,我将稍微绕道谈论某些实践和酷代码,这些可能会让您的生活更轻松。
我应该指出,UI 最初只是一个一次性的东西;好吧,我尝试让它看起来不错,但这只是因为我喜欢 WPF。我的意思是,其中仍然有一些不错的想法,比如查询的构建方式,可以移植到更丰富的查询生成器中,但这留给读者作为练习。
必备组件
您需要安装 VS2008(用于 Entity Framework)和 VS2008 SP1,以及 .NET 3.5 SP1。
运行此应用程序的准备工作
为了获得随附的代码(托管在 Windows 服务/WPF 客户端中的 WCF 服务),您需要确保已完成以下操作
- Windows 服务已安装并在安装过程中提供了登录详细信息。
- 关联的 .Config 文件的 LINQ to Entities
connectionStrings
部分已更新,以指向您本地的 SQL Server 安装。 - 当您尝试使用 WPFClient 时,Windows 服务正在运行。
- WPFClient 项目中的 App.Config 已配置为使用正确的安全登录;这在
identity/userPrincipalName
元素中。
为了让您更熟悉这一切,这里是项目结构
查看演示
运行时它是什么样子?嗯,我决定给你们看一些截图,还附带一个关于这个的小视频链接
LINQ to Entities
对于 ORM,我最初考虑使用 LINQ to SQL,但是你知道,我认为微软正在慢慢地(个人观点)将其淘汰,转而支持 LINQ to Entities。所以我想为什么不使用 LINQ to Entities 呢。这就是本文用于 ORM 的技术。
我过去在 LINQ to Entities 还在 BETA 版时就安装过它,那真是太糟糕了;设计器曾经生成三部分 XML
- CSDL:概念模式定义语言 (CSDL)
- MSDL:映射规范语言 (MSL)
- SSDL:存储模式定义语言 (SSDL)
正如我所看到的,设计器过去常常创建这些,但在过去做得并不好,你总是不得不亲自动手修改 XML,而且代码量巨大,一个小型数据库就有几千行。哎呀。
如果你想知道这有多糟糕,只需使用上面三个链接中的任何一个。
使用旧版 LINQ to Entities 测试版维护这三个 XML 部分简直是一场噩梦。幸运的是,新的设计器不再让用户直接担心 XML,我认为这是一件好事。现在的情况是,设计器的后台代码现在是一个 C#|VB 类,它有一个关联的 App.Config 文件,其中包含一个非常奇怪的 ConnectionStrings
部分。让我们来看看其中一个
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<connectionStrings>
<!-- HOME -->
<add name="NorthwindEntities"
connectionString="metadata=res://LINQ2Entities/Northwind.csdl|
res://LINQ2Entities/Northwind.ssdl|
res://LINQ2Entities/Northwind.msl;provider=System.Data.SqlClient;
provider connection string="Data Source=VISTA01\SQLEXPRESS;
Initial Catalog=Northwind;Integrated Security=True;
MultipleActiveResultSets=True""
providerName="System.Data.EntityClient" />
</connectionStrings>
</configuration>
要运行随附的代码,您需要更改安装程序用于安装服务时文件夹内的关联配置;这将在文章后面的“Windows 服务托管和安装”部分中介绍。
我们可以看到,仍然有指向 CSDL/MSL/SSDL 的链接,但它们现在被视为元数据资源。好多了。
因此,如果我们将注意力集中在 LINQ to Entities 设计器上,我们可以看到它与 LINQ to SQL 设计器有些相似,但又略有不同。这是为了允许从多个视图/表构建实体。
但是,在这个例子中,我将使用实体与 Northwind 数据库表之间的一对一映射。
LINQ to Entities SQL 语言
LINQ to Entities 实际上比 LINQ to SQL 更进一步,因为它现在支持一种完整的 SQL 查询语言,据我所知,它实际上与 SQL 语法相同。这是一个小例子
using (AdventureWorksEntities advWorksContext =
new AdventureWorksEntities())
{
string esqlQuery = @"SELECT contactID, AVG(order.TotalDue)
FROM AdventureWorksEntities.SalesOrderHeader
AS order GROUP BY order.Contact.ContactID as contactID";
try
{
foreach (DbDataRecord rec in
new ObjectQuery<DbDataRecord>(esqlQuery, advWorksContext))
{
Console.WriteLine("ContactID = {0} Average TotalDue = {1} ",
rec[0], rec[1]);
}
}
catch (EntityException ex)
{
Console.WriteLine(ex.ToString());
}
catch (InvalidOperationException ex)
{
Console.WriteLine(ex.ToString());
}
}
现在,我们可以用查询存储 String
的事实有点好,因为它意味着我们可以跨服务边界传递查询,这是 LINQ to SQL 开箱即用无法做到的。我的意思是,你无法序列化一个 Expression<Func<Customers,Bool>>
,也无法返回一个 var
,因为 var
具有方法级别的范围。你也无法在客户端创建 LINQ 查询并序列化它们(尽管这会非常酷)并将它们发送到服务运行。因此,LINQ to SQL 绝对是受限的。所以,这种新的 String
查询能力乍一看似乎是一件好事。
但是,让我们思考一下。我们过去可以通过 ADO.NET 使用内联/跨线 SQL 来做这种事情,看看那是多么混乱,更不用说 SQL 注入攻击了。所以,人们不得不问这到底是不是一件好事。我的意思是,有什么能阻止恶意用户创建自己的查询字符串呢?它现在使用 Entity Framework 而不是实际数据库的事实对恶意用户来说并不重要,他们可能会得到相同的结果。
我认为更好的选择是仅仅通过使用动态生成的 Where
子句来限制搜索结果,或者,如果你有时间,创建一个 SQL 查询生成器,该生成器在实际需要创建查询字符串并将其传递给 DB|ORM 的最后一刻之前,从不包含完整的字符串。后一种方法是我们实际在工作中采用的方法;本文将实际讨论动态生成的 Where
子句解决方案。
动态 Where 子句
LINQ to Entities 实际上提供了一个 ObjectQuery<T>.Where
方法,该方法接受一个字符串和一个 ObjectParameter
数组。
根据 ObjectParameter
文档,以下代码应该可以工作。不幸的是,它似乎不喜欢 ObjectParameter
名称。
真烦人!噢,幸好,有办法解决。还记得我说的 LINQ to SQL 无法开箱即用地运行动态查询吗?嗯,那过去是/现在是事实。但是,有一个额外的微软构建的类,它具有 IQueryable<T>
扩展方法,允许 LINQ to Entities 和 LINQ to SQL 都创建动态 Where
子句。
所以,如果我们更改原始查询以使用 DynamicQuery API,我们可以成功运行此查询。但是,我们必须使用编号参数,这有点麻烦,但总比上面所示的 LINQ to Entities 的恐怖场景要好。我只能想象上面所示问题的原因是 LINQ to Entities 实现中有一些奇怪的解析,试图生成实际的 SQL,而 DynamicQuery API 将尝试创建 Expression<Func<Customers,Bool>>
。
这两种 API 有何不同
正如刚才提到的,在使用原生 LINQ to Entites 的 ObjectQuery<T>.Where
方法时,System.Data.Entity
类实际上会尝试生成可用于数据库的实际 SQL。
现在让我们看看 DynamicQuery API 如何工作。它提供了一个针对 IQueryable<T>
类型的扩展方法,并根据输入值创建一个表达式树。
以下是 DynamicQuery API 的一个小节
public static IQueryable<T> Where<T>(this IQueryable<T> source,
string predicate, params object[] values)
{
return (IQueryable<T>)Where((IQueryable)source, predicate, values);
}
public static IQueryable Where(this IQueryable source,
string predicate, params object[] values)
{
if (source == null) throw new ArgumentNullException("source");
if (predicate == null) throw new ArgumentNullException("predicate");
LambdaExpression lambda =
DynamicExpression.ParseLambda(source.ElementType,
typeof(bool), predicate, values);
return source.Provider.CreateQuery(
Expression.Call(
typeof(Queryable), "Where",
new Type[] { source.ElementType },
source.Expression, Expression.Quote(lambda)));
}
你们中的一些人可能还记得我最近写了一篇关于 Expression Trees 的文章,并向你们展示了它们是如何工作的。这个 DynamicQuery API 是我写那篇文章的全部原因。
我希望您现在能明白为什么我们可能想在运行时创建表达式树。它允许我们基于表达式树创建动态查询。
WCF 服务
整体结构
WCF 服务实际上非常简单;整个 ServiceContract
如下所示
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
namespace Service.Portal
{
[ServiceContract(
Namespace = "http://www.sachabarber.net/2008/cp",
SessionMode = SessionMode.Allowed)]
public interface IPortal
{
//Allows FaultContract(s) to callback
[OperationContract(IsOneWay = false)]
//Allows TransactionScope at client
[TransactionFlow(TransactionFlowOption.Allowed)]
//Allows SOAP faults to client
[FaultContract(typeof(ApplicationException))]
[FaultContract(typeof(ArgumentException))]
[FaultContract(typeof(Exception))]
Response ExecuteRequest(Request request);
}
}
从上面的代码可以看出,有一个单独的 ExecuteRequest
OperationContract
,它接受一个 Request
并返回一个 Response
。听起来很简单,但是会有很多很多的 Request
对象吗?嗯,实际上是的,但这只是标准 OO 多态性在起作用,我们可以将 Request
的任何子类存储在其基类中。我稍后会介绍这一点;现在,让我们继续检查服务。可以看出,单个 ExecuteRequest
OperationContract
被标记为 OneWay=false
;这意味着服务会回调客户端。您可能会注意到我没有在任何地方指定 CallBack 接口,那么 OneWay=false
到底是什么意思呢?很简单,它允许将故障发送回客户端。稍后会详细介绍。另一个需要注意的是,如果客户端希望使用事务,服务允许使用事务。
现在,我们来看看 Request
对象,好吗?
using System;
using System.Runtime.Serialization;
namespace Service.Portal
{
/// <summary>
/// A Base request
/// </summary>
[DataContract]
[KnownType(typeof(CustomerRequest))]
[KnownType(typeof(CustomerSearchRequest))]
public abstract class Request : IExtensibleDataObject
{
#region Data
private ExtensionDataObject extensionDataObject=null;
#endregion
#region Abstract Methods
public abstract Response CreateResponse(Object requestResults);
public abstract Object Execute();
#endregion
....
....
}
这基本上就是它的全部。通过使用这种方法,您不必担心服务变更,因为 ServiceContract
始终相同;唯一改变的是 Request
的数量。
这是一个特定的 Response
DataContract
类
namespace Service.Portal
{
[DataContract]
public class CustomerResponse : Response
{
[DataMember(IsRequired = true, Name = "Customers")]
public List<Customers> Customers { get; set; }
}
}
已知类型 (KnownTypes)
WCF 的一个非常酷的功能是,您可以公开一个具有许多子类的类型,例如 Request
,并且您可以使用 KnowTypeAttribute
标记基类(本例中为 Request
),然后 DataContractSerializer
将知道如何处理这些类型。
在随附的演示应用程序中,Request
只有两个子类,但它们应该足以演示使用 KnowType
的概念。
IExtensibleDataObject
"IExtensibleDataObject
接口提供了一个属性,用于设置或返回一个结构,该结构用于存储数据契约之外的外部数据。额外的数据存储在 ExtensionDataObject
类的实例中,并通过 ExtensionData
属性访问。在数据接收、处理和发送回的往返操作中,额外的数据会完整地发送回原始发送方。这对于存储从未来版本的契约接收到的数据非常有用。如果您不实现该接口,则在往返操作期间,任何额外的数据都将被忽略和丢弃。"
MSDN 链接:http://msdn.microsoft.com/en-us/library/system.runtime.serialization.iextensibledataobject.aspx。
实际上,这意味着通过让您的 DataContract
类实现 IExtensibleDataObject
,您正在创建一个可版本化的对象。
在随附的演示代码中,Request
/Response
实现了 IExtensibleDataObject
,如下所示
using System;
using System.Runtime.Serialization;
namespace Service.Portal
{
[DataContract]
[KnownType(typeof(CustomerRequest))]
[KnownType(typeof(CustomerSearchRequest))]
public abstract class Request : IExtensibleDataObject
{
private ExtensionDataObject extensionDataObject=null;
public ExtensionDataObject ExtensionData
{
get { return extensionDataObject; }
set { extensionDataObject = value; }
}
}
}
ExtensionDataObject
具有对象引用和类型信息的内部链表,并且它知道未知数据成员的存储位置。当对象实现 IExtensibleDataObject
并且包含未知数据成员的请求发出时,它们会存储在内部链表中的未知成员列表中。如果请求包含未知类型的未知数据成员,则可以使用 ExtensionDataObject
的内部未知数据成员列表找到并反序列化这些未知数据成员。
基本上,这是一个好主意,我建议你们都这样做。
故障契约
现在我们将访问 WCF 服务,并查看实现 ServiceContract
的类。
WCF 服务实际上非常简单;整个 ServiceContract
如下所示
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
namespace Service.Portal
{
[ServiceContract(
Namespace = "http://www.sachabarber.net/2008/cp",
SessionMode = SessionMode.Allowed)]
public interface IPortal
{
//Allows SOAP faults to client
[FaultContract(typeof(ApplicationException))]
[FaultContract(typeof(ArgumentException))]
[FaultContract(typeof(Exception))]
Response ExecuteRequest(Request request);
}
}
我们可以看到,我们正在捕获 Exception
并抛出一些看起来很奇怪的 FaultException<T>
,其中 FaultException<T>
用于客户端应用程序以捕获契约中指定的 SOAP 故障。可以看出,服务实现设置为按调用,这意味着每次调用都会获得一个新服务,并且我们无需处理线程同步上下文。另一个要提的是,我们有一个小助手,可以在调试模式下使用 IncludeExceptionDetailInFault
,而在发布模式下则不使用。单一操作也标记为不允许模拟。在随附的服务代码中没有必要允许模拟,因为它是调用链中唯一的服务,因此无需将当前凭据流向另一个服务。我还坚持服务参与事务调用,其中客户端代码需要使用 TransactionScope
类来管理事务。TransactionAutoComplete
属性指示如果没有发生未处理的异常,则事务范围会自动完成。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.Reflection;
namespace Service.Portal
{
[ServiceBehavior(
InstanceContextMode= InstanceContextMode.PerCall,
UseSynchronizationContext = false,
IncludeExceptionDetailInFaults =
DebugHelper.IncludeExceptionsInFaults)]
public class Portal : IPortal
{
#region IPortal Members
[OperationBehavior(Impersonation = ImpersonationOption.NotAllowed,
TransactionScopeRequired = true, TransactionAutoComplete = true)]
public Response ExecuteRequest(Request request)
{
Response r = null;
if (request == null)
throw new ArgumentNullException("request");
try
{
Object results = request.Execute();
r = request.CreateResponse(results);
}
...
...
...
catch (Exception ex)
{
Console.WriteLine(String.Format("Server Exception {0}",
ex.Message));
throw new FaultException<Exception>(
new Exception(ex.Message));
}
return r;
}
#endregion
}
//simply debug class to IncludeExceptionsInFaults in debug mode
public static class DebugHelper
{
public const bool IncludeExceptionsInFaults =
#if DEBUG
true;
#else
false;
#endif
}
}
Windows 服务托管和安装
正如我在本文开头所说,WCF 服务实际上托管在 Windows 服务中。在随附的演示中,您会发现一个单独的项目,如果您处于调试模式(您将右键单击 PortalHost 项目并选择“调试”),则可以使用以下代码调试 WCF 服务,或者如果您处于发布模式,则正常运行服务。
using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceProcess;
using System.Text;
using System.Reflection;
namespace PortalHost
{
class Program
{
static void Main(string[] args)
{
#if (!DEBUG)
try
{
ServiceBase[] ServicesToRun;
ServicesToRun = new ServiceBase[] { new Service() };
ServiceBase.Run(ServicesToRun);
}
catch (Exception e)
{
Console.WriteLine("Error occurred " + e.Message);
}
#else
try
{
Console.WriteLine("Starting Service");
Service.StartService();
Console.WriteLine("Service Started");
System.Threading.Thread.Sleep(System.Threading.Timeout.Infinite);
}
catch (Exception e)
{
Console.WriteLine("Error occurred " + e.Message);
}
#endif
}
}
}
实际的 Windows 服务类如下所示
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.ServiceProcess;
using System.Text;
using System.ServiceModel;
using System.Reflection;
using Service.Portal;
using System.ServiceModel.Description;
namespace PortalHost
{
public partial class Service : ServiceBase
{
private static ServiceHost portalHost;
public Service()
{
}
protected override void OnStart(string[] args)
{
Service.StartService();
}
public static void StartService()
{
try
{
//WCF service hosting
portalHost = new ServiceHost(typeof(Portal));
StartServiceHost(portalHost);
}
catch (TargetInvocationException tiEx)
{
Console.WriteLine("Error occurred " + tiEx.Message);
}
catch (Exception ex)
{
Console.WriteLine("Error occurred " + ex.Message);
}
}
....
....
private static void StartServiceHost(ServiceHost serviceHost)
{
try
{
// We will recreate the service host here to be sure we don't have a
//service in a faulted state
serviceHost = new ServiceHost(serviceHost.Description.ServiceType);
Console.WriteLine("Attempting to open Service.Portal service.");
serviceHost.Open();
serviceHost.Faulted += new EventHandler(ServiceHost_Faulted);
}
....
....
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
}
....
....
}
}
此外,PortalHost 项目还包含一个自定义安装程序,如下所示
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Configuration.Install;
using System.Linq;
using System.ServiceProcess.Design;
using System.ServiceProcess;
using System.Windows.Forms;
namespace PortalHost
{
[RunInstaller(true)]
public partial class ProjectInstaller : Installer
{
public ProjectInstaller()
{
InitializeComponent();
}
// Prompt the user for service installation account values.
public static bool GetServiceAccount(ref ServiceProcessInstaller svcInst)
{
bool accountSet = false;
ServiceInstallerDialog svcDialog = new ServiceInstallerDialog();
//Use the ServiceInstallerDialog to customise the username and password for
//the installation process
....
....
return accountSet;
}
}
}
我们正在使用以下支持类 ServiceProcessInstaller
/ServiceInstaller
和一个 ServiceInstallerDialog
(用于在安装期间捕获用户名和密码)。
处理 Windows 服务中出现故障的 WCF 通道
在处理 WCF 时,最不想听到的就是通道出现故障;这真是令人难以置信的坏消息。基本上,一个故障通道是完全无用的。在托管 Windows 服务中,我们能对这种情况做些什么呢?嗯,很简单,我们所做的是捕获 ServiceHost.Faulted
异常,WCF 允许我们这样做(谢天谢地),然后停止/重新启动 WCF 服务。这在下面演示
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.ServiceProcess;
using System.Text;
using System.ServiceModel;
using System.Reflection;
using Service.Portal;
using System.ServiceModel.Description;
namespace PortalHost
{
public partial class Service : ServiceBase
{
private static ServiceHost portalHost;
public Service()
{
}
protected override void OnStart(string[] args)
{
Service.StartService();
}
public static void StartService()
{
....
....
}
private static void StartServiceHost(ServiceHost serviceHost)
{
....
....
}
private static void StopServiceHost(ServiceHost serviceHost)
{
....
....
}
private static void RestartServiceHost(ServiceHost serviceHost)
{
StopServiceHost(serviceHost);
StartServiceHost(serviceHost);
}
private static void ServiceHost_Faulted(Object sender, EventArgs e)
{
ServiceHost serviceHost = sender as ServiceHost;
Console.Write(String.Format("{0} Faulted. Attempting Restart.",
serviceHost.Description.Name));
RestartServiceHost(serviceHost);
}
}
}
安装
如前所述,随附的演示 WCF 服务实际上托管在 Windows 服务中,并且我们大多数人都应该知道,Windows 服务在特定用户帐户下运行。但是如何安装 Windows 服务呢?
随附的演示代码包含一个安装程序,用于安装托管 WCF 服务的 Windows 服务。作为此安装过程的一部分,系统将提示您输入服务的登录凭据。
使用 InstallUtil.exe
Windows 服务可以使用 InstallUtil.exe 命令行实用程序进行安装。为了安装附带的 Portal 服务,您需要类似以下内容
installutil.exe C:\Users\sacha\Desktop\Linq2WCF_NEW\
Linq2WCF\Linq2WCF\PortalHost\bin\Release\PortalHost.exe
运行后会出现一个对话框,您可以在其中输入 Windows 服务使用的帐户详细信息。
使用 MSI
随附的演示解决方案实际上有一个名为“PortalMSIInstaller”的安装程序,这是一个典型的安装程序/设置项目,它只是使用 Windows 服务项目“PortalHost”的输出。使用此项目唯一要注意的是,服务已安装,然后必须配置为使用正确的登录凭据,因为这些凭据不是安装过程的一部分。MSI 还使用 ServiceInstallerDialog
,但此窗口有时会被推到后面,因此您可能需要在安装过程中使用任务管理器来查找它。
服务自定义安装程序中使用的 ServiceInstallerDialog
代码,用于显示以下服务登录屏幕,如下所示
// Prompt the user for service installation account values.
public static bool GetServiceAccount(ref ServiceProcessInstaller svcInst)
{
bool accountSet = false;
ServiceInstallerDialog svcDialog = new ServiceInstallerDialog();
// Query the user for the service account type.
do
{
svcDialog.TopMost = true;
svcDialog.StartPosition = FormStartPosition.CenterScreen;
svcDialog.ShowDialog();
if (svcDialog.Result == ServiceInstallerDialogResult.OK)
{
// Do a very simple validation on the user
// input. Check to see whether the user name
// or password is blank.
if ((svcDialog.Username.Length > 0) &&
(svcDialog.Password.Length > 0))
{
// Use the account and password.
accountSet = true;
svcInst.Account = ServiceAccount.User;
svcInst.Username = svcDialog.Username;
svcInst.Password = svcDialog.Password;
}
}
else if (svcDialog.Result == ServiceInstallerDialogResult.UseSystem)
{
svcInst.Account = ServiceAccount.LocalSystem;
svcInst.Username = null;
svcInst.Password = null;
accountSet = true;
}
if (!accountSet)
{
// Display a message box. Tell the user to
// enter a valid user and password, or cancel
// out to leave the service account alone.
DialogResult result;
result = MessageBox.Show(
"Invalid user name or password for service installation." +
" Press Cancel to leave the service account unchanged.",
"Change Service Account",
MessageBoxButtons.OKCancel,
MessageBoxIcon.Hand);
if (result == DialogResult.Cancel)
{
// Break out of loop.
break;
}
}
} while (!accountSet);
return accountSet;
}
安装程序运行后,它将允许您使用以下对话框输入服务登录凭据,以便在其下运行
安装程序运行后,您需要更改配置文件以指向您自己的 SQL Server 安装。默认配置文件是安装程序项目使用的输出项目的名称,配置文件将类似于“PortalHost.exe.config”,您应该确保更改 <connectionStrings>
部分。
我将把查看“PortalMSIInstaller”项目作为读者的练习;这都是相当标准的东西。
启动 Windows 服务(托管 WCF 服务)
服务安装后,您需要启动它。这可以使用服务管理器轻松完成。
WPF 客户端
随附的“WPFClient”项目执行以下功能
- 提供 WCF 设置诊断屏幕,这在工作中非常宝贵,因为我们可以让非技术用户打印其设置屏幕,而无需查找和打开 App.Config 文件。
- 提供对 LINQ to Entities 检索到的客户对象列表的 3D 轮播视图。
- 提供当前客户的 LINQ to Entities 检索到的订单对象列表。
- 提供一个搜索构建器屏幕,允许用户构建动态查询,以便通过 WCF 请求查询 LINQ to Entities 层。
我将在下面其余部分依次处理这些领域。
WPFClient 项目基本设置
在我们开始深入代码之前,我想谈谈应用程序的大致结构。
Windows
有两个窗口:一个诊断窗口和一个主窗口。主窗口托管一个 3D 旋转木马以及 CurrentCustomerControl
和 CurrentCustomerOrdersControl
控件。
用户控件
CurrentCustomerControl
:一个非常简单的用户控件,显示反射图像和当前选定的 Northwind.Customers 名称。CurrentCustomerOrdersControl
:在ItemsControl
中显示当前选定的 Northwind.Customers.Orders 列表。有三种不同的视图可以向用户显示更多或更少关于绑定数据的信息。SearchControl
:包含 n 个SearchClauseControl
。SearchClauseControl
:允许为绑定对象的单个属性构建搜索子句。
ViewModels
随附代码对主窗口 (CustomerOrdersViewModel
) 和 SearchClauseControl
(SearchClauseViewModel
) 使用 MVVM 模式。
代码库的其余部分
至于代码的其余部分,我认为它们都是相当标准的 WPF 内容,例如附加属性/样式/命令等。
确保为 WCF 服务调用提供了正确的用户
为了正确使用托管在 Windows 服务中的 WCF 服务(假设它已安装并正在运行),您需要提供登录凭据;这是通过随附代码中“WPFClient”项目的 App.Config 文件中的 identity\userPrincipalName
部分完成的。
<identity>
<userPrincipalName value="YOUR PC\YOUR USER" />
</identity>
WCF 设置诊断
我认为,任何能帮助最终用户解决他们可能遇到的问题的帮助都是一件好事。
还应该记住,有些用户可能实际上没有技术头脑,所以他们不会喜欢费力寻找 App.Config 文件并查看 XML;我敢说有些人甚至不知道 XML 是什么。所以在我看来,保护用户免受此困扰是非常必要的。
为此,我创建了一个小的诊断屏幕,允许用户查看与给定客户端 App.Config 文件关联的 WCF 配置的诊断信息。以下代码同样适用于 WPF/WinForms 或使用 WCF 服务的控制台应用程序。
该代码主要使用 System.ServiceModel.Configuration
命名空间中找到的一些基于配置的类,以及一点点反射来搜索 App.Config 文件中的所有相关 WCF 元素,并将其用于在诊断窗口中显示。
主要代码如下所示
/// <summary>
/// Gets client EndPoint information out of the attached App.Config
/// </summary>
public List<String> EndPoints
{
get
{
// Automatically find all client endpoints and related
//bindings defined in app.config
List<String> endpointNames = new List<String>();
try
{
BindingsSection bindingsSection =
ConfigurationManager.GetSection(
"system.serviceModel/bindings") as BindingsSection;
if (bindingsSection == null)
{
Console.WriteLine(
"The configuration file doesn't contain " +
"a system.serviceModel/bindings configuration section");
}
else
{
endpointNames.Add("BINDINGS");
foreach (BindingCollectionElement bce in
bindingsSection.BindingCollections)
{
AnalyseBinding(endpointNames, bce);
}
}
ClientSection clientSection =
ConfigurationManager.GetSection(
"system.serviceModel/client") as ClientSection;
if (clientSection == null)
{
Console.WriteLine("The configuration file doesn't " +
"contain a system.serviceModel/client configuration section");
}
else
{
ChannelEndpointElementCollection endpointCollection =
clientSection.ElementInformation.
Properties[String.Empty].Value as
ChannelEndpointElementCollection;
endpointNames.Add(Environment.NewLine);
endpointNames.Add("ENDPOINTS");
foreach (ChannelEndpointElement
endpointElement in endpointCollection)
{
GetPropetiesFromType(endpointNames,
endpointElement.GetType(), endpointElement);
}
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
return endpointNames;
}
}
这里有两个用于获取属性值的小辅助方法
private void AnalyseBinding(List<string> endpointNames,
BindingCollectionElement bce)
{
foreach (IBindingConfigurationElement be in bce.ConfiguredBindings)
{
GetPropetiesFromType(endpointNames, be.GetType(), be);
}
}
private void GetPropetiesFromType(List<string> endpointNames,
Type type, Object source)
{
PropertyInfo[] pis = type.GetProperties();
if (pis.Length > 0)
{
foreach (PropertyInfo pi in pis)
{
bool foundSysName = false;
String currentValue = pi.GetValue(source, null).ToString();
if (currentValue.Contains("System.Configuration"))
return;
if (currentValue.StartsWith("System.ServiceModel"))
{
Object o = pi.GetValue(source, null);
foundSysName = true;
GetPropetiesFromType(endpointNames, o.GetType(), o);
}
if (!foundSysName)
endpointNames.Add(String.Format("{0} - {1}",
pi.Name, currentValue));
else
endpointNames.Add(String.Format("{0} - {1}",
pi.Name, "<SEE ABOVE>"));
}
endpointNames.Add(Environment.NewLine);
}
}
3D 旋转木马
对于 3D 轮播,我使用的是 Pavan Podila 的 FluidKit CodePlex 贡献中的真正出色的 ElementFlow
。这是一个我过去曾尝试自己创建的优秀 WPF 控件。当我发现 Pavan 的代码时,我基本上放弃了,因为它太酷了。它就像一个普通的面板一样工作,这意味着你可以在 ListBox
、ItemsControl
中使用它,而不是这些控件使用的普通面板。Pavan 做得非常出色,这个 ElementFlow
具有以下功能
SelectedItem
- 反射开|关
- 标准控件中包含许多不同的视图,您可以使用 F12 键查看其运行情况。我只使用以下视图,但还有其他视图
CoverFlow
旋转木马
过山车
旋转名片夹
3D 旋转木马最初加载了 15 个 Northwind.Customers 的列表,其中客户必须有两个以上的订单。这是通过 WCF CustomerRequest
完成的,在应用程序初始启动时执行。
客户订单列表
从用于保存包含订单的 Northwind.Customers 列表的 3D 旋转木马中,选定的 Customer 用于绑定到 CurrentCustomerOrdersControl
控件中的 DP。并且在 CurrentCustomerOrdersControl
控件内部,CustomerOrders DP 设置为当前的 Northwind.Customers.Orders。这在下面显示
/// <summary>
/// CustomerOrders Dependency Property
/// </summary>
public static readonly DependencyProperty CustomerOrdersProperty =
DependencyProperty.Register("CustomerOrders", typeof(List<Orders>),
typeof(CurrentCustomerOrdersControl),
new FrameworkPropertyMetadata(null,
new PropertyChangedCallback(OnCustomerOrdersChanged)));
/// <summary>
/// Gets or sets the CustomerOrders property.
/// </summary>
public List<Orders> CustomerOrders
{
get { return (List<Orders>)GetValue(CustomerOrdersProperty); }
set { SetValue(CustomerOrdersProperty, value); }
}
/// <summary>
/// Handles changes to the CustomerOrders property.
/// </summary>
private static void OnCustomerOrdersChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
List<Orders> orders = e.NewValue as List<Orders>;
if (orders != null)
((CurrentCustomerOrdersControl)d).lstOrders.ItemsSource = orders;
}
/// <summary>
/// BoundCustomer Dependency Property
/// </summary>
public static readonly DependencyProperty BoundCustomerProperty =
DependencyProperty.Register("BoundCustomer", typeof(Customers),
typeof(CurrentCustomerOrdersControl),
new FrameworkPropertyMetadata(null,
new PropertyChangedCallback(OnBoundCustomerChanged)));
/// <summary>
/// Gets or sets the BoundCustomer property.
/// </summary>
public Customers BoundCustomer
{
get { return (Customers)GetValue(BoundCustomerProperty); }
set { SetValue(BoundCustomerProperty, value); }
}
/// <summary>
/// Handles changes to the BoundCustomer property.
/// </summary>
private static void OnBoundCustomerChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
Customers customer = e.NewValue as Customers;
if (customer !=null)
((CurrentCustomerOrdersControl) d).CustomerOrders = customer.Orders;
}
一旦获取到 Northwind.Customers.Orders,它们将用作 CurrentCustomerOrdersControl
中嵌入的 ItemsControl
的源。
CurrentCustomerOrdersControl
上还有三个按钮,可用于更改应用于 ItemsControl
的当前 DataTemplate
。
搜索构建器
在本文的开头,我声明随附的代码使用 LINQ to Entities 框架,并且我们将在 UI 中编写动态查询,然后将其发送到 WCF 服务并用于查询 LINQ to Entities 框架。
这通过两个用户控件在 UI 中完成。
SearchClauseControl
这是一个简单的控件,它接受一个 Type
,然后使用反射来构建各种属性/允许值,用户可以根据当前选定的 Types
属性类型进行选择。与此控件关联的大部分逻辑都是通过 SearchClauseViewModel
完成的。SearchClauseControl
也由父级 SearchControl
调用,以便将当前搜索子句查询添加到任何其他搜索子句查询中。
为 SearchClauseControl
单一提供实际查询字符串的属性如下所示
public String ClauseResult
{
get
{
StringBuilder sb = new StringBuilder(1000);
if (!IsFirst)
sb.Append(String.Format("{0} ", currentOperator));
if (IsCollectionProperty)
sb.Append(String.Format("{0}.Count ", currentProperty.Name));
else
{
if (IsString)
sb.Append(String.Format("{0}.", currentProperty.Name));
else
sb.Append(String.Format("{0} ", currentProperty.Name));
}
if (IsOperatorComparable)
{
sb.Append(String.Format("{0} ", currentCondition));
sb.Append(String.Format("@{0} ", ParameterNumber));
}
if (IsString)
{
sb.Append(String.Format("{0}(@{1})",
currentCondition,
ParameterNumber));
}
return sb.ToString();
}
}
SearchControl
基本上是一个容器,它托管了许多 SearchClauseControl
控件。SearchControl
唯一做的就是根据其所有包含的 SearchClauseControl
控件的当前配置构建一个动态查询字符串。当创建动态查询字符串时,它(通过 SearchRequestedEvent
路由事件)发送到主窗口,主窗口反过来创建一个 CustomerSearchRequest
查询并运行该查询。
当主窗口 (CustomOrdersWindow
) 中看到 SearchControl.SearchRequested
路由事件时,实际发生的是 SearchControl.SearchRequested
事件被订阅,然后调用主窗口的视图模型 (CustomerOrdersViewModel
) 的 SearchCustomerWithOrders()
方法。
此方法如下所示
public void SearchCustomerWithOrders(String whereClause, List<Object> searchParameters)
{
try
{
Service<IPortal>.Use((client) =>
{
using (TransactionScope scope = new TransactionScope())
{
CustomerSearchRequest request = new CustomerSearchRequest();
request.WhereClause = whereClause;
request.SearchParameters = searchParameters;
CustomerResponse response =
(CustomerResponse)client.ExecuteRequest(request);
if (response.Customers != null)
CurrentCustomers =
new ObservableCollection<Customers>(response.Customers);
if (CurrentCustomers.Count > 0)
SelectedCustomer = CurrentCustomers[0];
//complete the Transaction
scope.Complete();
}
});
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
这里实际发生的是我们正在使用 Service<T>
类的静态方法,这确实是一个非常酷的类。让我们看看它。
using System.ServiceModel;
using System;
namespace WpfClient.ServiceProxy
{
/// Service client delegate
public delegate void UseServiceDelegate<T>(T proxy);
/// <summary>
/// A helper class to run code using a WCF client proxy
///
/// This weird class came from
/// http://www.iserviceoriented.com/blog/post/Indisposable+-+WCF+Gotcha+1.aspx
/// </summary>
public static class Service<T>
{
public static ChannelFactory<T> _channelFactory =
new ChannelFactory<T>("PortalServiceTcp");
/// <summary>
/// Creates the WCF service proxy and runs the codeBlock delegate
/// using the WCF service proxy that is created
/// </summary>
/// <param name="codeBlock">The code to run using the WCF Proxy</param>
public static void Use(UseServiceDelegate<T> codeBlock)
{
IClientChannel proxy = (IClientChannel)_channelFactory.CreateChannel();
bool success = false;
try
{
codeBlock((T)proxy);
proxy.Close();
success = true;
}
catch (FaultException<ApplicationException> dex)
{
Console.WriteLine("Client FaultException<ApplicationException>");
}
catch (FaultException<ArgumentException> aex)
{
Console.WriteLine("Client FaultException<ArgumentException>");
}
catch (FaultException<Exception> ex)
{
Console.WriteLine("Client FaultException<Exception>");
}
catch (FaultException fEx)
{
Console.WriteLine("Client FaultException");
}
finally
{
if (!success)
{
proxy.Abort();
}
}
}
}
}
可以看出,该方法具有以下签名:Service<T>.Use(UseServiceDelegate<T> codeBlock)
;所以本质上发生的是创建了一个新的 WCF 代理,然后调用传递给 Service<T>.Use
方法的委托,其中新创建的 WCF 代理类作为参数传递给原始委托(来自前面显示的 CustomerOrdersViewModel.SearchCustomerWithOrders
代码中的 lambda 表达式 (client) => { ...}
)。
很简洁,不是吗?这就是附带的 WPFClient 代码中所有对 WCF 服务的调用方式。
所以现在我们有了一个实际的 WCF 代理,CustomerOrdersViewModel.SearchCustomerWithOrders()
中的 lambda 被运行,它创建了一个新的 CustomerSearchRequest
,然后将其发送到 WCF 服务。让我们检查 CustomerSearchRequest
,看看它是如何工作的。
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.Linq.Expressions;
using System.Linq;
//Linq2Entities
using LINQ2Entities;
using System.Data.Objects;
using System.Linq.Dynamic;
namespace Service.Portal
{
[DataContract]
public class CustomerSearchRequest : Request
{
[DataMember]
public List<Object> SearchParameters { get; set; }
[DataMember]
public String WhereClause { get; set; }
public override Response CreateResponse(Object requestResults)
{
CustomerResponse response = new CustomerResponse();
response.Customers = requestResults as List<Customers>;
return response;
}
public override Object Execute()
{
NorthwindEntities context = new NorthwindEntities();
List<Customers> custs =
context.Customers.Include("Orders")
.Where(this.WhereClause,
this.SearchParameters.ToArray()).ToList();
return custs;
}
#endregion
}
}
多亏了 LINQ to Entities 部分中讨论的 DynamicQuery API 提供的漂亮扩展方法,进行动态查询几乎变得微不足道;我们只需包含一个 Where
子句,并使用在 WPF 客户端中创建的查询字符串以及从 WPF 客户端传递到 WCF 调用的相关参数值。
就是这样
这就是我想说的全部内容,如果您喜欢这篇文章,请投票并发表评论,谢谢。
特别感谢
我只想感谢我现在工作的出色团队,他们让我学到了更多关于 WCF 的知识,并为我提供了某些 WCF 问题的答案。谢谢大家,你们知道我是谁。