About
This project presents a simple, but fun “Adopt a Dog” Service Application. The service provides a factory creational design pattern to create and return a single Dog object (complete with a cute photo). It is hosted using IIS Express to quickly demo and test the service with a client.
The service interface is defined not in the service application but in a Shared Library. The Shared Library also defines the Dog class object with custom serialization using the ISerializable interface implemented on the Dog class. This project is intended to demo custom serialization and a shared library concept between both the service and the client. With the Shared Library concept of both the service interface definition and the Dog class, there is no need to use SVCUTIL or the Service Wizard to create a tightly coupled service reference. Instead, the client uses a simple ChannelFactory concept to build a channel to the service knowing the interface details in the Shared Library. Also, it can understand and work with the Dog objects as specified in the Shared Library.
A client “tester” windows form application tests the service and provides output to the user in a simple GUI.
In addition, a short discussion and code demo of debugging service errors and activity using diagnostics and a trace listener pattern is included in this article. Some of my lessons learned in debugging service errors and custom serialization may help you on your next project.
Architecture
The demo project consists of these component topics:
- Adopt A Dog WCF Service Application Project “WcfServiceApplication”
- AdoptADogService (Code that Implements the Service Interface)
- config (Configuration for Trace Diagnostics on Service)
- Reference to the Shared Class Library
- Shared Class Library Project “SharedLibrary”
- IAdoptADogService (Interface for Service)
- Dog class (defines a Dog object and Custom Serialization)
- Photos of the Dogs
- Logs for Trace Diagnostics
- Client “Tester to Service” Windows Form Application Project “AdoptADogClient”
- Reference to the Shared Class Library
- Main Form GUI User Interface
- Form Code – Processes GUI User Interface
Adopt a Dog Service Application
A WCF Service Application project was added to my Visual Studio solution. The code is available on GitHub [here].
AdoptADogService (Code that Implements the Service Interface)
The service implementation code is a creational factory pattern that accepts parameters and returns a built Dog object to the client. The service references a shared class library called “SharedLibrary” that contains a class definition for the Dog object, including a constructor, and the interface definition. The code is available on GitHub [here].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
/// <summary> /// AdoptADogService /// Implements the IAdoptADogService interface and service /// </summary> public class AdoptADogService : IAdoptADogService { /// <summary> /// Adopt One Dog - Implementation of Service /// Factory Implementation where the service creates a /// dog object from the specified parameters and returns /// a dog /// </summary> /// <param name="name">Name (string) of your dog</param> /// <param name="breed">Breed (CatBreed) of your dog</param> /// <param name="gender">Gender (GenderType) of your dog</param> /// <param name="age">Age (int) of your dog</param> /// <returns>A Dog object</returns> public Dog AdoptADog(string name, DogBreed breed, GenderType gender, int age) { Dog dog = new Dog(name, breed, gender, age); return dog; } // end of method } // end of class |
Web.config Trace Diagnostics Configuration Settings
The service web configuration settings file is included for reference to help debug and record both activity and errors on the service to aid in testing and development. The code is available on GitHub [here].
Some Helpful Hints About Service Model Debugging
- The web configuration must be setup properly to record messages
- Some of the default parameters need to be adjusted (ex: max) to larger sizes
- Diagnostic customizations are under the <serviceModel> tag
- Diagnostic Trace Listeners are children of <configuration>
- A Text Listener is easier on the eye than an XML listener
- Data will be written to a file location that you must specify
- Most of the issues to listen will be on the System.ServiceModel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
<?xml version="1.0"?> <configuration> <appSettings> <add key="aspnet:UseTaskFriendlySynchronizationContext" value="true" /> </appSettings> <system.web> <compilation debug="true" targetFramework="4.6.1" /> <httpRuntime targetFramework="4.6.1"/> </system.web> <system.serviceModel> <diagnostics performanceCounters="All" wmiProviderEnabled="true" > <messageLogging logEntireMessage="true" logMalformedMessages="true" logMessagesAtServiceLevel="true" logMessagesAtTransportLevel="true" maxMessagesToLog="100000" maxSizeOfMessageToLog="9999999"/> </diagnostics> <behaviors> <serviceBehaviors> <behavior> <!-- To avoid disclosing metadata information, set the values below to false before deployment --> <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true"/> <!-- To receive exception details in faults for debugging purposes, set the value below to true. Set to false before deployment to avoid disclosing exception information --> <serviceDebug includeExceptionDetailInFaults="true"/> </behavior> </serviceBehaviors> </behaviors> <protocolMapping> <add binding="basicHttpsBinding" scheme="https" /> </protocolMapping> <serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" /> </system.serviceModel> <system.webServer> <modules runAllManagedModulesForAllRequests="true"/> <!-- To browse web app root directory during debugging, set the value below to true. Set to false before deployment to avoid disclosing web app folder information. --> <directoryBrowse enabled="true"/> </system.webServer> <system.diagnostics > <sharedListeners> <add name="sharedListener" type="System.Diagnostics.TextWriterTraceListener" initializeData="C:\Users\kathl\Dropbox\Projects\MySerializationDemoWCFAppSharedLib\SharedLibrary\logs\trace.txt" /> </sharedListeners> <sources> <source name="System.ServiceModel" switchValue="Verbose, ActivityTracing" > <listeners> <add name="sharedListener" /> </listeners> </source> <source name="System.ServiceModel.MessageLogging" switchValue="Verbose"> <listeners> <add name="sharedListener" /> </listeners> </source> <source name="System.Runtime.Remoting" switchValue="Verbose, ActivityTracing" > <listeners> <add name="sharedListener" /> </listeners> </source> <source name="System.Runtime.Remoting.MessageLogging" switchValue="Verbose, ActivityTracing" > <listeners> <add name="sharedListener" /> </listeners> </source> </sources> </system.diagnostics> </configuration> |
Shared Class Library
A Class Library project was added to my Visual Studio solution. The code is available on GitHub [here].
IAdoptADogService (Interface for Service)
The ServiceContract for the Adopt A Dog Service allows for one possible option: make single Dog entity. The OperationContract will return a single Dog object, representing a factory created Dog object with the name, breed, age, and gender that was requested by the client. The code is available on GitHub [here].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
/// <summary> /// IAdoptADogService /// Service Interface for the Adopt A Dog Service /// </summary> [ServiceContract] public interface IAdoptADogService { /// <summary> /// Adopt One Dog /// Factory Implementation where the service creates a /// dog object from the specified parameters and returns /// a dog /// </summary> /// <param name="name">Name (string) of your dog</param> /// <param name="breed">Breed (CatBreed) of your dog</param> /// <param name="gender">Gender (GenderType) of your dog</param> /// <param name="age">Age (int) of your dog</param> /// <returns>A Dog object</returns> [OperationContract] Dog AdoptADog(string name, DogBreed breed, GenderType gender, int age); } // end of interface |
Dog Class Hierarchy
The Dog class has the main components:
- Class Level Attributes
- [Serializable]
- [KnownType(typeof(DogBreed))]
- [KnownType(typeof(GenderType))]
- [KnownType(typeof(Bitmap))]
- Properties
- Name (string)
- Breed (DogBreed)
- Gender (GenderType)
- Age (int)
- Photo (Bitmap)
- Constructors
- Default
- Parameterized
- ISerializable Implementation
- Custom Constructor for Deserialization
- Serializer
- Serialization Callbacks
In addition, there are two public Enums in the class file but not in the Dog class.
- DogBreed
- GenderType
The code is available on GitHub [here].
Class Level Attributes
The class level attributes are required for the custom serialization of the Dog class. A dog object is also composed of (contains) three different types that need to be specified for the serialization: Bitmap, DogBreed (Enum), and GenderType (Enum) all of which uses basic Serialization.
1 2 3 4 5 |
[Serializable] [KnownType(typeof(DogBreed))] [KnownType(typeof(GenderType))] [KnownType(typeof(Bitmap))] public class Dog : ISerializable ... |
Properties
The Dog class has a few automatic properties to define a Dog object for adoption: Name (string), Breed (enum), Gender (enum), Age (int), and Photo (Bitmap).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
/// <summary> /// Name of Dog /// </summary> public string Name { get; set; } /// <summary> /// Breed of Dog /// </summary> public DogBreed Breed { get; set; } /// <summary> /// Gender of Dog /// </summary> public GenderType Gender { get; set; } /// <summary> /// Age of Dog /// </summary> public int Age { get; set; } /// <summary> /// Photo of the Dog /// </summary> public Bitmap Photo { get; set; } |
Constructors
The Dog class has both default constructor and a parameterized constructor. The default constructor calls the parameterized constructor with default values.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
/// <summary> /// Default Constructor /// </summary> public Dog() : this("Spot", DogBreed.Beagle, GenderType.Female, 1) { } // end of method /// <summary> /// Parameterized Constructor /// </summary> public Dog(string name, DogBreed breed, GenderType gender, int age) { Name = name; Breed = breed; Gender = gender; Age = age; // Photo Assembly asm = Assembly.GetExecutingAssembly(); string path1 = "SharedLibrary.photos."; string path2 = Breed.ToString(); string filepath = path1 + path2 + ".jpg"; Photo = new Bitmap(asm.GetManifestResourceStream(filepath)); } // end of constructor |
Enums
Although the enums not defined in the logical class, it is still in the same assembly and class file.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
/// <summary> /// List of Dog Breeds /// </summary> public enum DogBreed { Labrador_Retriever = 0, Pug = 1, Siberian_Husky = 2, Bulldog = 3, Beagle = 4 } // end of enum /// <summary> /// List of Gender Types /// </summary public enum GenderType { Male = 0, Female = 1 } // end of enum |
ISerializable Custom Serialization and Deserialization
The class implements the ISerializable interface and has additional callbacks for process. Learn about the Microsoft technical implementation of serialization [here].
Serialization
The “GetObjectData” specifies the custom serialization of an object. It follows the pattern of adding properties to a Data Dictionary Serialization info object:
info.AddValue(“Name of Object (Property)”, PropertyName, typeof(Property Type));
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
/// <summary> /// GetObjectData /// Serializes the data into a Data Dictionary SerializationObject /// </summary> /// <param name="info">The object to be serialized</param> /// <param name="context">context for the steam</param> public void GetObjectData(SerializationInfo info, StreamingContext context) { info.AddValue("Name", Name, typeof(string)); info.AddValue("Breed", Breed, typeof(Enum)); info.AddValue("Gender", Gender, typeof(Enum)); info.AddValue("Age", Age, typeof(Int32)); info.AddValue("Photo", Photo, typeof(Bitmap)); } // end of method |
Deserialization
The constructor for deserialization is
public Dog(SerializationInfo info, StreamingContext context)
and follows the pattern of retrieving objects from the Data Dictionary Serialization info object and placing them into memory:
PropertyName = (Property Type) info.GetValue(“Name of Object (Property)”, typeof(Property Type));
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/// <summary> /// Constructor for Deserialization /// </summary> /// <param name="info">The Serialized object to be deserialized</param> /// <param name="context">context for the steam</param> public Dog(SerializationInfo info, StreamingContext context) { Name = info.GetString("Name"); Breed = (DogBreed) info.GetValue("Breed", typeof(Enum)); Gender = (GenderType) info.GetValue("Gender", typeof(Enum)); Age = info.GetInt32("Age"); Photo = (Bitmap) info.GetValue("Photo", typeof(Bitmap)); } // end of method |
Callbacks
In addition to the required serialization and deserialization methods that implement ISerializable interface, the developer can include additional callback methods that get called and processed during the serialization and deserialization process.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
[OnDeserialized] private void OnDeserialized(StreamingContext ctx) { // Template to Do Stuff } [OnDeserializing] private void OnDeserializing(StreamingContext ctx) { // Template to Do Stuff } [OnSerialized] private void OnSerialized(StreamingContext ctx) { // Template to Do Stuff } [OnSerializing] private void OnSerializing(StreamingContext ctx) { // Template to Do Stuff } |
Client “Tester to Service” Windows Form Application
The client “tester to service” is a simple windows form application project in the same solution that connects to the “Adopt A Dog Service” by use of a ChannelFactory proxy to a specified service name that is defined in the client application configuration. The client program will use this proxy to test the OperationContract or method available in the ServiceContract and return the results to the user on the form window.
The client application shares and references the “Shared Library” – a class library project. This allows the client to know how to deserialize (construct) and utilize the Dog object it receives. Also, it has access to the Adopt a Dog service interface to know exactly how it consumes the service with the help of a ChannelFactory proxy (discussed in the next section).
The user enters in dog details in the form window, then selects “Adopt A Dog” button. The form will validate the user input and then pass the data to the service to receive a newly created Dog object in the list. They can then view the dog details by selecting the dog name in the list. Multiple requests will populate additional dogs in the form list.
After the service builds and sends a single Dog object back to the client, the windows form GUI will update and add the name of the newly received Dog object to the form list. The user can then select on the individual Dog name and it will populate the Dog details in the entry boxes above and change the photo to the correct breed of dog. Note: The Photo is a bitmap that is stored on the shared class library and transmitted in the Serialization process (from service to client) for the Dog Photo property.
The code is available on GitHub [here].
ChannelFactory Proxy and Application Configuration
The application configuration file (App.config) specifies the service endpoint address, binding, and contract information. The contract is in the Shared Class Library that has the service interface. Helpful Tip: The “maxReceivedMessageSize” was set to a larger value to accommodate the large file size Bitmap images that we expect to receive with our Dog objects. The code is available on GitHub [here].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<?xml version="1.0" encoding="utf-8" ?> <configuration> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" /> </startup> <system.serviceModel> <bindings> <basicHttpBinding> <binding name="BasicHttpBinding_IAdoptADogService" maxReceivedMessageSize="1000000"/> </basicHttpBinding> </bindings> <client> <endpoint address="http://localhost:59027/AdoptADogService.svc" binding="basicHttpBinding" bindingConfiguration="BasicHttpBinding_IAdoptADogService" contract="SharedLibrary.IAdoptADogService" name="BasicHttpBinding_IAdoptADogService" /> </client> </system.serviceModel> </configuration> |
Main Program
The main program in the client “tester” windows application first connects to the Adopt A Dog Service using a ChannelFactory proxy to the service. Based on this proxy, it then can call upon and retrieve a single Dog. The Program handles the user interface events, button clicks, validation, service calls, etc. The code is available on GitHub [here].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
/// <summary> /// method InitializeGUI() /// Description: This sets up the GUI for the first time when /// the form loads. /// </summary> private void InitializeGUI() { // Clear Input Controls txtName.Text = string.Empty; numAge.Value = 1; cmbBreed.Text = string.Empty; cmbGender.Text = string.Empty; // Clear Output Controls lstDogs.Items.Clear(); } // end of InitializeGUI() /// <summary> /// Adopt a Dog! /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btnAdoptADog_Click(object sender, EventArgs e) { // Validate the Input if(!ValidateInput()) { MessageBox.Show("Please Enter Dog Details", "Error"); return; } // Assemble the Dog Attributes from the Form string name = txtName.Text; int age = (int)numAge.Value; DogBreed breed = (DogBreed)Enum.Parse(typeof(DogBreed),cmbBreed.Text); GenderType gender = (GenderType)Enum.Parse(typeof(GenderType), cmbGender.Text); // Make a ChannelFactory Proxy to the Service using (ChannelFactory<IAdoptADogService> cf = new ChannelFactory<IAdoptADogService>("BasicHttpBinding_IAdoptADogService")) { cf.Open(); IAdoptADogService proxy = cf.CreateChannel(); if (proxy != null) { // Call the Service Dog dog = proxy.AdoptADog(name, breed, gender, age); // Add to the Master Cat List myDogs.Add(dog); } else { Debug.WriteLine("Proxy is Null"); } } // Update the GUI UpdateGUI(); } // end of method /// <summary> /// Validate the User Form Inputs /// </summary> /// <returns>True if Valid, False Otherwise</returns> public bool ValidateInput() { if(String.IsNullOrEmpty(txtName.Text)) { return false; } if (String.IsNullOrEmpty(cmbBreed.Text)) { return false; } if (String.IsNullOrEmpty(cmbGender.Text)) { return false; } return true; } // end of method /// <summary> /// Update the GUI with the List of Dogs /// </summary> private void UpdateGUI() { lstDogs.Items.Clear(); if (myDogs.Count != 0) { foreach (Dog dog in myDogs) { string str = String.Format("{0}",dog.Name); lstDogs.Items.Add(str); } } } // end of UpdateGUI() /// <summary> /// Select List Box Item Event /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void lstDogs_SelectedIndexChanged(object sender, EventArgs e) { int selectedIndex = lstDogs.SelectedIndex; // Object has not been initialized if ((selectedIndex < 0) || (myDogs.Count == 0)) return; Dog dog = myDogs[selectedIndex]; // if not null if (dog != null) { txtName.Text = dog.Name; numAge.Value = dog.Age; cmbBreed.Text = dog.Breed.ToString(); cmbGender.Text = dog.Gender.ToString(); picDog.Image = dog.Photo; } } // end of method |
Demo
Code
The entire project code repository is available on GitHub here.