Recently, I have encountered an issue that is by-design when using custom entity classes (or Business Entities) with Web Services. Being an advocate of custom entity classes, I have always believed in lightweight objects when it comes to ferrying data across service boundaries. However, little did I realised that when dealing with Web Services, it is not so straight-forward afterall.
Let's explore the problem.
Suppose we have a simple custom entity class like the following:
1using System;
2using System.Collections.Generic;
3using System.Text;
4
5namespace Business.Entities
6{
7 /// <summary>
8 /// Represents a Customer.
9 /// </summary>
10 [Serializable]
11 public class Customer
12 {
13 private decimal customerId;
14 private string firstName;
15 private string lastName;
16
17 public decimal CustomerId
18 {
19 get { return customerId; }
20 set { customerId = value; }
21 }
22
23 public string FirstName
24 {
25 get { return firstName; }
26 set { firstName = value; }
27 }
28
29 public string LastName
30 {
31 get { return lastName; }
32 set { lastName = value; }
33 }
34
35 /// <summary>
36 /// Gets the fullname of the customer.
37 /// </summary>
38 public string FullName
39 {
40 get { return firstName + " " + lastName; }
41 }
42
43 public Customer()
44 {
45 }
46
47 }
48}
49
As you can see, the Customer entity is a pretty straight-forward class. Notice that it is marked as [Serializable] to enable it to serialize across boundaries. Also take note of the FullName readonly property which I purposely created to demonstrate the problem.
Next, let's look a Web Method in our ServiceProvider.
1 [WebMethod]
2 public Customer GetCustomerWithoutTranslation(decimal customerId)
3 {
4 // Return a Customer object.
5 // NOTE: Should call a method from Business Component to retrieve customer.
6 Customer customer = new Customer();
7 customer.CustomerId = 8888;
8 customer.FirstName = "Serena";
9 customer.LastName = "Yeoh";
10
11 return customer;
12 }
Once again, it is a typical Web Service method that returns an instance of our Customer entity class. [Notice that I did not use a Business Component in my example. In real-life applications, it is always a good practice to use a Business Component to return the custom entity.]We are now ready to use our Web Service. We will start by creating a new project and use the Add Web Reference... option to reference our Web Service. We will name the Web Service reference as CustomerService.
Let's write our consuming method.
1 protected void Button2_Click(object sender, EventArgs e)
2 {
3 // Call web service.
4 ServiceProvider.CustomerService svc = new ServiceProvider.CustomerService();
5 Customer customer = svc.GetCustomerWithoutTranslation(8888);
6 }
Up to this point everything seems to make perfect sense but if you attempt to compile your code, you will be treated to the following error:Cannot implicitly convert type 'ServiceProvider.Customer' to 'Business.Entities.Customer'
Replacing the code in line 5 with the following will conveniently solve the problem.
5 ServiceProvider.Customer customer = svc.GetCustomerWithoutTranslation(8888);
If you compile your application now, everything should be alright. However, if you inspect the ServiceProvider.Customer class, you will notice that something is missing - The FullName property is not available. [At this point, you may try to cast the class to Business.Entities.Customer but you will encounter the same error that was mentioned earlier.]So what is wrong? Now, if you go to the definition of the ServiceProvider.Customer class, you will realized that it is not our Business.Entities.Customer class but a class generated for the proxy when we add the web reference (An imposter!). So what happened to our Customer class? The answer is that this is by-design because whatever types that we send across in a Web Service will be serialized into XML and we cannot control the consuming end (i.e. they may be running on different platforms).
After searching the web for nights and posting here, here and here, I have yet to find a satisfying answer. One of the links that were provided in the forums suggested to use CodeDom to generate the classes but I feel that it is an overkill solution.
So how do we solve it? Enter the Entity Translation Service. The idea of the Entity Translation Service is to convert our custom entity classes into XML string and pass them through the Web Services layer. On the consuming end that we have control, we will convert the string back to our custom entity classes. This way, we eliminate the need for generating proxy entity classes in our application while still staying true to the Web Services architecture.
Below is the code for the EntityTranslator. Feel free to use it but remember to give me some credit okie? :p
1
2//===============================================================================
3// TITLE : Entity Translator
4// AUTHOR: Serena Yeoh
5// BLOG : http://serena-yeoh.blogspot.com
6//===============================================================================
7
8using System;
9using System.Collections.Generic;
10using System.Text;
11using System.Diagnostics;
12
13using System.IO;
14using System.Xml.Serialization;
15
16namespace TranslationService
17{
18 /// <summary>
19 /// Provides functions to translate custom entity classes.
20 /// </summary>
21 /// <typeparam name="TEntity">The type of the entity.</typeparam>
22 [Serializable]
23 public class EntityTranslator<TEntity>
24 {
25 public EntityTranslator()
26 {
27 }
28
29 /// <summary>
30 /// Converts an entity to XML string.
31 /// </summary>
32 /// <param name="entity">The entity object.</param>
33 /// <returns>An XML representation of the entity.</returns>
34 public string ToXML(TEntity entity)
35 {
36 string output = string.Empty;
37
38 using (StringWriter writer = new StringWriter())
39 {
40 XmlSerializer serializer = null;
41
42 // Serialize the entity.
43 serializer = new XmlSerializer(typeof(TEntity));
44 serializer.Serialize(writer, entity);
45
46 // Get serialized string.
47 output = writer.ToString();
48 writer.Close();
49 }
50
51 // Return the result.
52 return output;
53 }
54
55 /// <summary>
56 /// Converts a XML string to a custom entity.
57 /// </summary>
58 /// <param name="xmlString">The XML string representing the entity.</param>
59 /// <returns>An entity object.</returns>
60 public TEntity FromXML(string xmlString)
61 {
62 TEntity entity = default(TEntity);
63 XmlSerializer serializer = null;
64
65 using (StringReader reader = new StringReader(xmlString))
66 {
67 // Deserialize the entity.
68 serializer = new XmlSerializer(typeof(TEntity));
69 entity = (TEntity)serializer.Deserialize(reader);
70 }
71
72 return entity;
73 }
74
75 }
76}
77
Basically, the EntityTranslator class accepts a Generic type and manages the serialization/deserialization with an XmlSerializer. As you can see, the code is pretty straight-forward. As long as the custom entity classes are marked with the Serializable attribute, the XmlSerializer will be able to handle them.To use the EntityTranslator, modify the Web Service method.
1 [WebMethod]
2 public string GetCustomer(decimal customerId)
3 {
4 // Return a Customer object.
5 // NOTE: Should call a method from Business Component to retrieve customer.
6 Customer customer = new Customer();
7 customer.CustomerId = 8888;
8 customer.FirstName = "Serena";
9 customer.LastName = "Yeoh";
10
11 // Create translator.
12 EntityTranslator<Customer> translator = new EntityTranslator<Customer>();
13
14 // Return the translated string.
15 return translator.ToXML(customer);
16 }
Notice we are returning a string now instead of a Customer entity class.To call our new Web Service method, we will modify the method on the consuming end.
1 protected void Button1_Click(object sender, EventArgs e)
2 {
3 // Call web service.
4 ServiceProvider.CustomerService svc = new ServiceProvider.CustomerService();
5 string s = svc.GetCustomer(8888);
6
7 // Translate the string to custom entity.
8 EntityTranslator<Customer> translator = new EntityTranslator<Customer>();
9 Customer customer = translator.FromXML(s);
10
11 Response.Write(customer.FullName);
12 }
Notice that we are able to get back our Customer class and access the FullName property now. There you go, problem solved!Before concluding, I would like to mention that although the solution provided above may seem viable but certain considerations have to be taken when using such approach. In the example, we returned a Customer entity class but if we are passing in a Customer entity class to the Web Service method, proper type checking should be done because the string that is coming in could be anything.
So thats it! I hope you guys enjoyed the article ... uh! I mean post. :p
No comments:
Post a Comment