Generated code - XML Webservices / WCF support
Preface
One hot buzzword of the last couple of years has to be 'webservices', which is short for XML Webservices, i.e.: XML message based services offered through
a normal HTTP based website. As webservices are XML based, the .NET framework uses XmlSerializer to produce XML which reflects the objects returned from
a webmethod and to produce instances of classes from the XML received, for example at the client. This section describes how to use the entity and
entity collection classes of the generated code in a webservice scenario. Every entity and entity collection class implements IXmlSerializable, which
makes it possible to use these classes with webmethods transparently, without the necessity to first produce XML from them using their WriteXml() methods.
Note: |
Please pay attention to the caveats section at the bottom, to be fully aware of the consequences and to avoid compilation
errors when using the generated code in a webservice scenario. |
Note: |
Generics aren't supported in webservices nor are polymorphic fetches using single entity instances. This means that the type returned by the webmethod is the
type of object you will get on the client. If you return an instance of a derived type of the webmethod's returntype (e.g. you return a
ManagerEntity while the method's returntype is EmployeeEntity, you'll get an EmployeeEntity object at the client, not a ManagerEntity object.
Polymorphic fetches using EntityCollection objects are supported. |
Instead of returning entities and entity collections, you could consider to use Data Transfer Objects (DTO's) or small message objects, which are filled using LLBLGen Pro's projection functionality
(see:
Generated code - Fetching DataReaders and projections, SelfServicing or
Generated code - Fetching DataReaders and projections, Adapter) and then used with the XmlSerializer.
Although the XmlSerializer is limited, it can be more efficient if your objects are simple and don't contain interface typed members nor cyclic references.
.NET 3.0 introduces WCF, or Windows Communication Foundation. It's a new framework which replaces the .NET 1.x/2.0 way of doing webservices and remoting. LLBLGen Pro entities are fully usable with WCF similar to webservices. As entity classes are generated for you, using a DataContract isn't supported, but that also wouldn't be that helpful, given the fact that an entity could change while a data contract isn't changeable. At the end of this section, a special WCF paragraph describes how to get started with LLBLGen Pro and WCF illustrating a simple service in code.
For web services and WCF, which both use the IXmlSerializable implementations of the entity and entity collection classes, LLBLGen Pro uses its own Compact25 format, which is very lightweight and very fast to consume and produce as it contains almost no overhead. See for a description of the Compact25 format:
generated code - Xml support.
Example usage
The example discussed here is pretty simple. It offers a service with three methods: GetCustomer, SaveCustomer and GetCustomers. The client consumes the
service to retrieve a customer to have it edited in a winforms application and saves the changed data back into the database using the webservice, plus it
uses the service to display all customers available.
The service project has references to both generated projects: the database generic and the database specific project. The client only has a reference to the
database generic project, as it uses the service for database specific activity, namely the persistence logic to work with the actual data. Because both
client and service have references to the database generic project, they both can use the same types for the entities, in this case the CustomerEntity.
The service
Below is the service code, simplified. As you can see, the code works directly with entity objects and entity collection objects.
// C#
[WebService(Namespace="http://www.llblgen.com/examples")]
public class CustomerService : System.Web.Services.WebService
{
[WebMethod]
public CustomerEntity GetCustomer(string customerID)
{
CustomerEntity toReturn = new CustomerEntity(customerID);
using(DataAccessAdapter adapter = new DataAccessAdapter())
{
adapter.FetchEntity(toReturn);
return toReturn;
}
}
[WebMethod]
public EntityCollection GetCustomers()
{
EntityCollection customers = new EntityCollection(new CustomerEntityFactory());
using(DataAccessAdapter adapter = new DataAccessAdapter())
{
adapter.FetchEntityCollection(customers, null);
return customers;
}
}
[WebMethod]
public bool SaveCustomer(CustomerEntity toSave)
{
using(DataAccessAdapter adapter = new DataAccessAdapter())
{
return adapter.SaveEntity(toSave);
}
}
}
' VB.NET
<WebService(Namespace="http://www.llblgen.com/examples")> _
Public Class CustomerService
Inherits System.Web.Services.WebService
<WebMethod> _
Public Function GetCustomer(customerID As String) As CustomerEntity
Dim toReturn As New CustomerEntity(customerID)
Dim adapter As New DataAccessAdapter()
Try
adapter.FetchEntity(toReturn)
Return toReturn
Finally
adapter.Dispose()
End Try
End Function
<WebMethod> _
Public Function GetCustomers() As EntityCollection
Dim customers As New EntityCollection(New CustomerEntityFactory())
Dim adapter As New DataAccessAdapter()
Try
adapter.FetchEntityCollection(customers, Nothing)
Return customers
Finally
adapter.Dispose()
End Try
End Function
<WebMethod> _
Public Function SaveCustomer(toSave As CustomerEntity) As Boolean
Dim adapter As New DataAccessAdapter()
Try
Return adapter.SaveEntity(toSave)
Finally
adapter.Dispose()
End Try
End Function
End Class
This code forms the code behind of the .asmx file which forms the service entry point.
The client
VS.NET uses wsdl.exe to generate so called
proxy classes. These classes are required to
communicate with the webservices and in fact represent the webservice on the client. When using VS.NET 2002/2003, the wsdl.exe executable produces
correct code with one exception: every class which is exposed through a webmethod (either as return parameter or as method parameter) and which
implements IXmlSerializable (which is the case with entity and entity collection classes) is considered to be a DataSet. This of course isn't correct.
If you're targeting .NET 2.0 specifically (so no .NET 3.0/WCF) and you're using VS.NET 2005, you can generate extra classes in a separate project
which will help VS.NET and wsdl.exe to produce proper proxy
classes with normal entity types and not as DataSets. See the section
.NET 2.0 specific: Schema importers below for
more details about how to use this separate project to get proper proxy classes.
We start by first adding a webreference to the webservice. This triggers VS.NET to put wsdl.exe at work to generate the proxy classes. We then have
to manually adjust these classes to correct the types. After we've fixed this issue, we can write some client code which utilizes the webservice we
just created. Below are the three methods which illustrate a way to use the webservice. Only the relevant code is posted here, but it's enough to
illustrate the point and to get you started with consuming the entities with your own webservices. No error recovery is implemented, just the
minimal code to get the service used.
// C#
// Method which is called after the GetCustomer button is clicked.
private void _getCustomerButton_Click(object sender, System.EventArgs e)
{
// Zeus.CustomerService is the generated proxy for the service.
Zeus.CustomerService service = new Zeus.CustomerService();
// grab the textbox contents and pass it to the service to retrieve the customer entity
CustomerEntity c = (CustomerEntity)service.GetCustomer(_customerIDTextBox.Text);
// as we're displaying the customer in a grid, we have to wrap the customer object in
// an entity collection, as grids only bind to collections.
EntityCollection col = new EntityCollection();
col.Add(c);
_mainGrid.DataSource = col;
}
// Method which is called after the GetAll button is clicked.
private void _getAllButton_Click(object sender, System.EventArgs e)
{
// Zeus.CustomerService is the generated proxy for the service.
Zeus.CustomerService service = new Zeus.CustomerService();
// simply pull all customers from the service into an entity collection.
EntityCollection customers = (EntityCollection)service.GetCustomers();
// bind the collection to the grid.
_mainGrid.DataSource = customers;
}
// Method which is called after the SaveCustomer button is clicked.
// This button works together with the GetCustomer button and assumes
// a customer is loaded in the grid using GetCustomer.
private void _saveCustomer_Click(object sender, System.EventArgs e)
{
// Zeus.CustomerService is the generated proxy for the service.
Zeus.CustomerService service = new Zeus.CustomerService();
// Get the collection currently bound to the grid, which is the wrapper
// around the single customer object received earlier.
EntityCollection customers = (EntityCollection)_mainGrid.DataSource;
// The customer object in the collection is send to the service.
// Inside the object, changed information is stored so the persistence logic
// at the service will be able to save the data.
bool saveResult = service.SaveCustomer((CustomerEntity)customers[0]);
MessageBox.Show("Save result = " + saveResult);
}
' VB.NET
' Method which is called after the GetCustomer button is clicked.
Private Sub _getCustomerButton_Click(sender As Object, e As System.EventArgs)
' Zeus.CustomerService is the generated proxy for the service.
Dim service As New Zeus.CustomerService()
' grab the textbox contents and pass it to the service to retrieve the customer entity
Dim c As CustomerEntity = CType(service.GetCustomer(_customerIDTextBox.Text), CustomerEntity)
' as we're displaying the customer in a grid, we have to wrap the customer object in
' an entity collection, as grids only bind to collections.
Dim col As New EntityCollection()
col.Add(c)
_mainGrid.DataSource = col
End Sub
' Method which is called after the GetAll button is clicked.
Private Sub _getAllButton_Click(sender As Object, e As System.EventArgs)
' Zeus.CustomerService is the generated proxy for the service.
Dim service As New Zeus.CustomerService()
' simply pull all customers from the service into an entity collection.
Dim customers As EntityCollection = CType(service.GetCustomers(), EntityCollection)
' bind the collection to the grid.
_mainGrid.DataSource = customers
End Sub
' Method which is called after the SaveCustomer button is clicked.
' This button works together with the GetCustomer button and assumes
' a customer is loaded in the grid using GetCustomer.
Private Sub _saveCustomer_Click(sender As Object, e As System.EventArgs)
' Zeus.CustomerService is the generated proxy for the service.
Dim service As New Zeus.CustomerService()
' Get the collection currently bound to the grid, which is the wrapper
' around the single customer object received earlier.
Dim customers As EntityCollection = CType(_mainGrid.DataSource, EntityCollection)
' The customer object in the collection is send to the service.
' Inside the object, changed information is stored so the persistence logic
' at the service will be able to save the data.
Dim saveResult As Boolean = service.SaveCustomer((CustomerEntity)customers(0))
MessageBox.Show("Save result = " & saveResult)
End Sub
Custom Member serialization/deserialization
If you add your own member variables to entity classes including their properties, you probably also want these values to be serialized and deserialized into the XML stream. Normally, a custom member exposed as a read/write property is serialized as a string using the ToString() method of the value of the custom property. In a lot of cases this isn't sufficient and you want to perform your own custom xml serialization/deserialization on the value of this custom property, for example if this custom property represents a complex object. To signal that the LLBLGen Pro runtime framework has to call custom xml serialization code for a given property, the property has to be annotated with a
CustomXmlSerializationAttribute attribute. When a property is seen with that attribute, LLBLGen Pro will call the entity method
entity.PerformCustomXmlSerialization to produce valid XML for the custom property. Likewise, when deserializing an XML stream into entities, the LLBLGen Pro runtime framework will call, when it runs into a property annotated with a CustomXmlSerializationAttribute, the method
entity.
PerformCustomXmlDeserialization to deserialize the xml for the property into a valid object. You should override these methods in a partial class of the entity which contains the custom properties.
Custom property serialization/deserialization is a feature of the Compact25 xml format, which is used by Adapter in Webservices/WCF scenarios.
.NET 2.0 specific: Schema importers
In .NET 2.0, Microsoft has updated the wsdl.exe tool and the WSDL format slightly so it's now possible to direct the wsdl.exe tool to generate proxy classes
which use the actual types returned by the webmethods instead of DataSets when the return-types of the methods implement IXmlSerializable. The mechanism isn't
easy however, because Microsoft implemented it in a very complex way. This means that you've to jump through various hoops to get the proxy classes generated
like you want them to be.
Note: |
If you're using .NET 3.0 or higher and WCF, you don't need to use Schema importers, as you can use a ServiceContract. Please see the WCF specific section below for more details. |
To begin, first enable in the adapter preset for .NET 2.0 you're using the tasks under the SD.Tasks.Adapter.Webservices.SchemaImporter group header. Three
tasks are in that task group, all disabled by default. Furthermore, enable the task SD.Tasks.Adapter.Webservices.WebserviceHelperClassGenerator as well.
To re-use the preset later on, you should save the preset under a different name. When you generate code using these classes enabled you'll get an extra
VS.NET project generated, SchemaImporter.vb/csproj. It will contain a single class, SchemaImporter.
Furthermore, in the dbgeneric project, in the HelperClasses namespace a class called WebServiceHelper.vb/cs is generated. The SchemaImporter project
works on the client, in conjunction with the XmlSchema data produced by the service, which is enhanced with extra type info.
Getting it all up and running
Follow the next steps to get proper proxy classes with the generated code. The SchemaImporter project has to be signed with a strong name and has to be placed
in the GAC. The machine.config file of the
developer's machine has to be adjusted as well. This thus means that the service running system doesn't have to
be altered at all.
- Go the the SchemaImporter project.
- In the menu, choose 'Project' and then 'SchemaImporter Properties'.
- Go to the 'Application' tab and press the 'Assembly Information' button.
- Fill in at least the assembly version, make it version 1.0.0.0
- Go to the 'Signing' tab
- Activate the 'Sign the assembly' checkbox
- In the combobox, choose 'New...'
- In the popup window, enter the name for your strong name key and a password, if required.
- Press OK.
You should now see the key included in your project.
- Compile your SchemaImporter project.
Before you compile, you might want to change the name in your project properties, under the 'Application' tab, in the 'Assembly Name' textbox, so it'll
be recognizable.
- We'll now update the machine.config file on the developer's machine. To do that properly, we need some information about the key used for signing the
schemaimporter assembly.
Open the Visual Studio 2005 command prompt and go to the project folder of the SchemaImport project.
- Now enter the following commands:
- sn -p schemaimporter.snk snpub.snk
- sn -t snpub.snk
- The public key token is shown, something like ab123456c78de9f0
- The XML snippet below will be copied into the machine.config file in a moment. Replace the public key token in the xml below, with the just shown public key token.
<system.xml.serialization>
<schemaImporterExtensions>
<add name="SchemaImporter" type="yournamespace.SchemaImporter, assemblyname, Version=1.0.0.0, Culture=neutral, PublicKeyToken=ab123456c78de9f0" />
</schemaImporterExtensions>
</system.xml.serialization>
- Now go the the following folder: C:\Windows\Microsoft.NET\Framework\v2.0.50727\CONFIG. This is on the client developer's machine. You don't
need to alter any data on the webservice machine.
- Open the machine.config file with notepad.
- Find the following tag : </configSections>
- The xml shown in step 10 should be copied after this tag, as it closes the configuration settings, which specifies our
System.Xml.Serialization section. Make sure your public key token is in it.
- Alter the type attribute to your own namespace and assemblyname. In your SchemaImporter project you can find the namespace and
assemblyname. The assemblyname is in your project properties under the 'Application' tab. The namespace is in the SchemaImporter.cs class file.
It should be something like:
type="[rootnamespace].SchemaImporter.EntityClassesSchemaImporter, SchemaImporter"
Example: if you've set your rootnamespace to MyCompany.CoolApp then it should be:
type="MyCompany.CoolApp.SchemaImporter.EntityClassesSchemaImporter, SchemaImporter"
- Save the file
- We're now going to register our assembly in the Global Assembly Cache (GAC) manually. We can also do this automated through the build properties
and a dos command. If you want to do that, skip the rest of this tutorial and go to the next bullet list.
- First, open up the control panel, administrative tools and than choose Microsoft .NET Framework 2.0 Configuration.
- Under 'My Computer' go to the Assembly Cache and choose Add an Assembly to the Assembly Cache.
- Go to your project folder, to the /bin/debug/ folder and add your SchemaImporter assembly (the .dll file).
Now everything should work. We can also register the SchemaImporter every time we build a new version of the solution. To do that, follow the next steps:
- Go to the project properties of your SchemaImporter, and select the 'Build Events' tab.
- Enter the following line in the post-build events box
"C:\Program Files\Microsoft Visual Studio 8\SDK\v2.0\Bin\gacutil" -i "$(TargetPath)"
After these long list of steps, you should be able to add a webreference to your webservice which returns and consumes entities from the generated code which
you generated with the preset which has the WebServiceHelper generator task and the SchemaImporter generator tasks enabled.
Once the proxy is generated, you can see if it worked by going to the web reference and choose to see all files. Take a look at the
reference map and the file under it, Reference.cs. That's the generated proxy.
(Special thanks to Dennis v/d Stelt and Ryan Hurd).
.NET 3.0+ specific: Windows Communication Foundation (WCF) support
In .NET 3.0 and up, you don't need schema importers anymore, as the preferred technique to use is Windows Communication Foundation (WCF). WCF takes care of a lot of plumbing and configuration so it's easier to write service oriented software. To be able to send entities from service to client and back using WCF, you have to define a
ServiceContract. This ServiceContract defines the types involved in the service. It's recommended to define an interface onto which the ServiceContract is defined. Both client and service now know which types are involved in the service and no stub classes are created anymore nor necessary. If you want to have a fixed
DataContract instead, you shouldn't use Entity classes but you should send Data Transfer Objects (DTO)'s which are more or less dumb buckets with data back and forth. The reason is that a DataContract can't change however an entity might change over time, which then would violate the DataContract.
LLBLGen Pro's powerful projection framework can help you with projecting fetched data onto DTO classes to send them over the wire.
Below is a small example of a simple WCF service and client. Its main purpose is to illustrate what to do to get LLBLGen Pro generated code working with WCF. You should check the MSDN library for information about WCF, configuration of WCF services and other WCF documentation to get a WCF service up and running in your environment.
Note: |
When sending entities over the wire using WCF, the 'IsNew'
flag is not passed along as it is determinable on the service side
for new entities. If you use the trick where you set the IsNew flag
manually on an entity and then send the entity over the wire to the
service, the IsNew flag is set to false at the service during
deserialization, so you have to set it back to true in that special
case scenario. |
Interface with ServiceContract
Below is the service interface definition with the ServiceContract. The client code will use this interface to refer to the service and the service will use this interface to implement a common interface for clients to connect to.
// C#
[ServiceContract]
[ServiceKnownType(typeof(CustomerEntity))]
[ServiceKnownType(typeof(EntityCollection))]
public interface IWCFExample
{
[OperationContract]
IEntity2 GetCustomer(string customerID);
[OperationContract]
IEntityCollection2 GetCustomers();
}
' VB.NET
<ServiceContract(), _
ServiceKnownType(GetType(CustomerEntity)), _
ServiceKnownType(GetType(EntityCollection))> _
Public Interface IWCFExample
<OperationContract()> _
Function GetCustomer(customerID As String) As IEntity2
<OperationContract(&)gt; _
Function GetCustomers() As IEntityCollection2
End Interface
Server implementation
Below is the implementation of the IWCFExample interface to be used as a WCF service.
// C#
// class to implement the service logic
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
public class WCFExampleService : IWCFExample
{
public IEntity2 GetCustomer(string customerID)
{
CustomerEntity toReturn = new CustomerEntity(customerID);
using(DataAccessAdapter adapter = new DataAccessAdapter())
{
adapter.FetchEntity(toReturn);
}
return toReturn;
}
public IEntityCollection2 GetCustomers()
{
EntityCollection toReturn = new EntityCollection(new CustomerEntityFactory());
using(DataAccessAdapter adapter = new DataAccessAdapter())
{
adapter.FetchEntityCollection(toReturn, null);
}
return toReturn;
}
}
// class to actually run the service:
public class WCFExampleServerHost
{
public WCFExampleServerHost()
{
WCFExampleService server = new WCFExampleService();
ServiceHost host = new ServiceHost(server);
host.Open();
}
}
' VB.NET
' class to implement the service logic
<ServiceBehavior(InstanceContextMode := InstanceContextMode.Single)> _
Public Class WCFExampleService
Implements IWCFExample
Public Function GetCustomer(customerID As string) As IEntity2 Implements IWCFExample.GetCustomer
Dim toReturn As new CustomerEntity(customerID)
Using adapter As New DataAccessAdapter()
adapter.FetchEntity(toReturn)
End Using
Return toReturn
End Function
Public Function GetCustomers() As IEntityCollection2 Implements IWCFExample.GetCustomers
Dim toReturn As New EntityCollection(New CustomerEntityFactory())
Using adapter As New DataAccessAdapter()
adapter.FetchEntityCollection(toReturn, Nothing)
End Using
Return toReturn
End Function
End Class
' class to actually run the service:
Public Class WCFExampleServerHost
Public Sub New()
Dim server As New WCFExampleService()
Dim host As New ServiceHost(server)
host.Open()
End Sub
End Class
Client usage of service
Below is the code snippet to consume the service defined above. It illustrates the usage of the service.
// C#
ChannelFactory<IWCFExample> channelFactory =
new ChannelFactory<IWCFExample>("WCFExampleServer");
IWCFExample server = channelFactory.CreateChannel();
// Fetch an entity
IEntity2 c = server.GetCustomer("CHOPS");
// Fetch a collection
IEntityCollection2 customers = serverTest.GetCustomers();
' VB.NET
Dim channelFactory As New ChannelFactory(Of IWCFExample)("WCFExampleServer")
Dim server As IWCFExample = channelFactory.CreateChannel()
' Fetch an entity
Dim c As IEntity2 = server.GetCustomer("CHOPS")
' Fetch a collection
Dim customers As IEntityCollection2 = serverTest.GetCustomers()
Configuration of the service
Below is the serviceModel element of the service config file. The settings below are illustrative and your own production service likely will use different values for various WCF settings. Please consult the WCF documentation in the MSDN library for details on the user elements.
<system.serviceModel>
<bindings>
<netTcpBinding>
<binding name="RemoteConfig"
closeTimeout="infinite"
openTimeout="infinite"
sendTimeout="infinite"
receiveTimeout="infinite"
maxBufferSize="65536000"
maxReceivedMessageSize="65536000" />
</netTcpBinding>
</bindings>
<services>
<service name="Service.WCFExampleServer">
<endpoint address="" binding="netTcpBinding" name="WCFExampleServer"
bindingConfiguration="RemoteConfig"
contract="Interfaces.IWCFExample" />
<host>
<baseAddresses>
<add baseAddress="net.tcp://localhost:6543/WCFExampleServer" />
</baseAddresses>
</host>
</service>
</services>
</system.serviceModel>
Configuration of the client
Below is the serviceModel element of the client config file. The settings below are illustrative and your own production service likely will use different values for various WCF settings. Please consult the WCF documentation in the MSDN library for details on the user elements.
<system.serviceModel>
<bindings>
<netTcpBinding>
<binding name="RemoteConfig"
closeTimeout="infinite"
openTimeout="infinite"
sendTimeout="infinite"
receiveTimeout="infinite"
maxBufferSize="65536000"
maxReceivedMessageSize="65536000" />
</netTcpBinding>
</bindings>
<client>
<endpoint address="net.tcp://localhost:6543/WCFExampleServer"
name="WCFServer" binding="netTcpBinding"
bindingConfiguration="RemoteConfig"
contract="Interfaces.IWCFExample" />
</client>
</system.serviceModel>