ORM + .NET Remoting 完整例子程序 虽然现在都流行WCF,也没有必要抛弃已经掌握的.NET Remoting
LLBL Gen作为项目开发的ORM框架,选择.NET Remoting作为分布式技术框架。一直也很想把ERP框架从.NET Remoting升级到WCF,只是关于方法重载的配置方法需要特殊处理。举例说明如下
public interface IEmployeeManager { EmployeeEntity GetEmployee(System.Int32 Employeeid); EmployeeEntity GetEmployee(System.Int32 Employeeid, IPrefetchPath2 prefetchPath); EmployeeEntity GetEmployee(System.Int32 Employeeid, IPrefetchPath2 prefetchPath, ExcludeIncludeFieldsList fieldList); }
IEmployeeManager接口用于对员工表Employee进行CRUD操作。上面三个重载方法是用来从数据库中读取员工信息
对.NET Remoting技术实现,只需要派生于MarshalByRefObject,实现IEmployeeManager接口的方法即可
public class EmployeeManager : MarshalByRefObject, IEmployeeManager { public EmployeeEntity GetEmployee(System.Int32 Employeeid) { return GetEmployee(Employeeid, null); } public EmployeeEntity GetEmployee(System.Int32 Employeeid, IPrefetchPath2 prefetchPath) { return GetEmployee(Employeeid, prefetchPath, null); } public EmployeeEntity GetEmployee(System.Int32 Employeeid, IPrefetchPath2 prefetchPath, ExcludeIncludeFieldsList fieldList) { EmployeeEntity _Employee = new EmployeeEntity(Employeeid); using (DataAccessAdapterBase adapter = GetCompanyDataAccessAdapter()) { bool found = adapter.FetchEntity(_Employee, prefetchPath, null, fieldList); if (!found) throw new RecordNotFoundException("Invalid Employee"); } return _Employee; } }
这样对于.NET Remoting是没有问题的。然后对于WCF技术,它不支持方法重载overload,需要添加Name特性
public interface IEmployeeManager { [OperationContract(Name="Employeeid")] EmployeeEntity GetEmployee(System.Int32 Employeeid); [OperationContract(Name="EmployeeidPrefetchPath")] EmployeeEntity GetEmployee(System.Int32 Employeeid, IPrefetchPath2 prefetchPath); [OperationContract(Name="EmployeeidPrefetchPathFieldList")] EmployeeEntity GetEmployee(System.Int32 Employeeid, IPrefetchPath2 prefetchPath, ExcludeIncludeFieldsList fieldList); }
每次加这么多参数很麻烦,于是就制造一个工具,专门用于生成Name的参数值
甚至可以把这个工具的代码直接集成到Code Smith的模板中,这样大大减少了工作量,提高工作效率。
回到主题,来看看LLBL Gen的.NET Remoting版本,这个例子被直接映射到了ERP框架中。
定义接口方法
public interface IService { int GetNumberOfCustomers(); void GetOrderStatistics(out decimal averageOrderPrice, out decimal highestOrderPrice,
out int numberOfOrders, out int orderIdWithHighestPrice); OrderEntity GetOrderAndCustomerWithHighestPrice(int orderId); CustomerEntity GetCustomerWithMostOrdersAndNumberOfOrders(out int numberOfOrders); EntityCollection<CustomerEntity> GetAllCustomers(); EntityCollection<CustomerEntity> GetAllCustomersFilteredOnProduct(int productId); EntityCollection<ProductEntity> GetProductsSortedBySortExpression(SortExpression sorter); bool RemoveOrder(OrderEntity toRemove); CustomerEntity GetCustomerWithFullOrders(string customerId); bool SaveCustomer(CustomerEntity toSave, bool recursive); CustomerEntity GetCustomer(string customerId); void SaveOrders(EntityCollection<OrderEntity> orders); }
从技术的角度来讲,有二种类型的方法:单向的,传入参数,执行动作,不返回值;双工,传入参数,返回值。
接口的实现代码
public class Service : MarshalByRefObject, IService { public int GetNumberOfCustomers() { Console.WriteLine("GetNumberOfCustomers called"); using(DataAccessAdapter adapter = new DataAccessAdapter()) { // simply obtain the count of the customerid and return that. return (int)adapter.GetScalar(CustomerFields.CustomerId, AggregateFunction.CountRow); } } }
其它的代码参考下载的例子程序,它的实现代码可以用代码生成器生成。不带参数的DataAccessAdapter 表示从默认生成的配置文件中读取连接字符串,内容如下
<?xml version="1.0" encoding="utf-8" ?> <configuration> <appSettings> <add key="Northwind.ConnectionString.SQL Server (SqlClient)" value="data source=.\sqlexpress;initial
catalog=Northwind;integrated security=SSPI;persist security info=False;packet size=4096"/>
</appSettings>
</configuration>
服务器端的实现,把接口的实现代码发布成服务
// register a channel and assign the service type to it. This opens the service to the outside world at TCP port 65100. TcpChannel channel = new TcpChannel(65100); ChannelServices.RegisterChannel(channel, true); Type serverType = Type.GetType("RemotingService.Service"); RemotingConfiguration.RegisterWellKnownServiceType(serverType, "theEndPoint", WellKnownObjectMode.Singleton); // switch on FastSerialization SerializationHelper.Optimization = SerializationOptimization.Fast;
客户端实现代码,读取服务并调用它来实现数据库的读写
// grab service object first. TcpChannel channel = new TcpChannel(); ChannelServices.RegisterChannel(channel, true); MarshalByRefObject o = (MarshalByRefObject)RemotingServices.Connect(typeof(IService), string.Format(
"tcp://{0}:65100/theEndPoint", SERVERNAME)); IService service = o as IService; if(service == null) { throw new Exception("Service couldn't be obtained. Aborting"); } MainForm.DalService = service; // switch on FastSerialization SerializationHelper.Optimization = SerializationOptimization.Fast;
之后的代码中,都会应用MainForm.DalService来获取客户数据以及客户的产品资料。
这个例子还有几点可以讨论的地方,请参考如下的思考
1 服务的调用代码。如果有多个.NET Remoting服务 ,应该做成工厂模式,并且提高cache以减少内存的消耗。
工厂模式的参考代码如下所示
IService service; if(!_cache.Contains(service)) service=CreateService(service) return service;
这样实现的好处是,当这个接口被多个客户段代码调用时,服务实现只被创建了一次,节约内存。虽然.NET的垃圾回收器已经很智能了,但是这种带缓存版本的写法,比没有缓存要有效率。
2 没有使用配置文件,而是直接在代码中创建TcpChannel的方式。在学.NET Remoting的时,也喜欢用各种书中推荐的方式,用配置文件而不是直接硬编码到代码中。在实践中发现,这样每次都需要把已经配置好的配置文件的片段拷贝到新项目的配置文件中,比较麻烦。一般来说,Channel和Port在部署到客户服务器上是不会变动的。于是就改变做法,把Channel的创建直接写到代码中,Port留给配置文件来指定。这种方式不需要增加配置文件节,方便部署。
3 当方法需要返回多个值时,参考接口IService的GetOrderStatistics方法,这里使用的是out修饰参数。把值赋给传入的参数,方法调用完成后,再获取参数值。这里,推荐用结构struct设计成返回值,以应对增加参数的变化。
方法参数的传入也一样,当有多个参数要传入到方法内部时,可以有struct把这些参数简单封装一下,作为一个整体传入方法中。在未来有对这个方法进入维护时,也会相对容易一些。
文章中提到的例子程序,请到LLBL Gen官方网站下载,代码名称是Example_NorthwindRemotingExampleCS_Adapter。