Export (0) Print
Expand All

Using Entities with Web

This chapter is excerpted from Programming Entity Framework: Building Data Centric Apps with the ADO.NET Entity Framework by Julia Lerman, published by O'Reilly Media

Services are a critical part of today's (and tomorrow's) application environments. You can use entities in service applications, and depending on your needs you can approach the task of using the Entity Framework with services in a number of ways. You can build your own services in the form of ASMX ("classic") and Windows Communication Foundation (WCF) services, which were introduced in .NET 3.0.

In this chapter, you will write an ASMX Web Service and a WCF service leveraging what you've already learned about the Entity Framework. In Chapter 22, Implementing a Smarter WCF Service for Working with Entities, we will revisit WCF services, and there you will use some of the more in-depth knowledge you will gain in the latter part of this book to write a more streamlined WCF service that will impact how the service is consumed.

The services in this chapter will be consumed by a simple Windows Forms client application.

Note
If you have never built either a web service or a WCF service before, have no fear. The walkthroughs will provide you with step-by-step details.

Building a Client That Is Ignorant of the Entity Framework

In this chapter, the samples depend on the Entity Framework only on the server side. The clients that consume the services use a simplified version of the classes that the services provide. The client will not perform any database connections, change tracking, relationship management, or anything else that depends on Object Services. This means that not only does your client not have to install the Entity Framework APIs-or your own model, for that matter-but also that you can create clients that are not even written in .NET, as long as they follow the web services' rules.

You will use a .NET client in this chapter so that you can get some hands-on experience manipulating the objects returned by these services as well as interacting with the services.

In Chapter 22, Implementing a Smarter WCF Service for Working with Entities, you'll learn how you can share some of your model's business logic with the consuming client. For now, we'll keep it a bit simpler to get the basic concepts down.

Unless you want to take advantage of Object Services on the client side, for the sake of either change tracking or relationship management, there's no reason to reference your model assembly or the Entity Framework on the client side. Even if you do perform change tracking on the client side, you will lose those changes when you send the objects back to the service.

Pros and Cons of an Entity Framework-Agnostic Consumer

Without the Entity Framework APIs in the client application, you will be faced with a few additional challenges that you should have some experience handling. The second example in this chapter, which covers use of WCF services, has a lot of relationships; you will find that the lack of references to the model and System.Data.Entity are noticeable and educational.

For one thing, EntityCollections won't exist on the client, as they are a class in System.Data.Entity assembly. As a result, the children of an object are contained in a List rather than an EntityCollection. You'll see toward the end of the chapter how it is necessary to explicitly instantiate the Reservations property of a new customer by calling Customer.Reservations = new List<Reservations>.

Another example is that you don't automatically get two-way relationship navigation. If you add a Reservation to a Customer, you will find that Reservation in Customer.Reservations, but Reservation.Customer will return null. If you needed to navigate in both directions, you would have to explicitly bind them in both ways (e.g., additionally calling Reservation.Customer=myCustomer).

This is not to say that excluding the references in the model assembly and System.Data.Entity is a bad thing. In many scenarios your business rules may prevent you from having the client depend on these things, so it's very useful to see how to build clients in this way. On the other hand, those references can be a welcome inclusion in many situations. The relationship management will be simpler, and although you will get change tracking on the client, keep in mind that the changes stored in the ObjectStateEntry objects will be lost when transferring the objects back to the service. Although you already learned about the ObjectStateEntry objects, you will learn more about this problem, and solutions for it, in later chapters.

In this section, you will build the ASMX Web Service and client. The service will have the ability to return a list of customer names as well as a complete individual customer. It also provides operations to update an existing customer and to insert new customers. The client application shown in Figure 14.1, "A Windows form that will consume a web service that uses the Entity Framework" has features that use each of the service's operations. The client application will have no knowledge of the Entity Framework, the BreakAway model, or their classes.

Figure 14.1. A Windows form that will consume a web service that uses the Entity Framework

A Windows form that will consume a web service that uses the Entity Framework

Building the ASMX Service

Your first task will be to create and implement the web service, which will need to provide the following features:

  • Return a customer list

  • Return an individual customer

  • Insert a new customer

  • Update an existing customer

Once you've built the web service, you'll build a client to consume it.

Creating the web service

Follow these steps to create the web service:

  1. Add a new ASP.NET Web Service Application project to your solution.

  2. Add references to the BreakAwayModel project and to System.Data.Entity.

  3. Copy the ConnectionString section from the BreakAwayModel's app.config file into the new application's web.config file.

The different operations (methods) will be easier to write if you have imported the namespace of the BreakAway model into the code file for the service, which you can do by following these steps:

  1. Open Service1.asmx. By default, this should open the code file Service1.asmx.vb or Service1.asmx.cs.

  2. Import the BreakAwayModel's namespace to simplify coding the methods (see Example 14.1, "Importing the BreakAway model's assembly namespace").

    Example 14.1. Importing the BreakAway model's assembly namespace

    Imports BAGA
    
    using BAGA;
    
  3. Visual Studio adds a HelloWorld WebMethod to new service files. You can remove that if you like.

The GetCustomers method

The first method to create will be GetCustomers, which will return a list of the IDs and names for all of the customers in the database. On the client side, you'll use this to populate a drop-down list for selecting a customer. Since there is no reason to return entire Customer objects, the method will use projection in the query.

The projection will cause you to hit the first bump in the road, though it is not an Entity Framework problem. Instead, the problem is related to anonymous types. The method needs to identify the type that it will return. In this case, because of the projection, it will be returning a List of anonymous types, but there is no way to declare a List of anonymous types in the method signature. There is no such thing as List<anonymous type> because anonymous types are transient objects.

You have three options, listed here in the order of best to worst:

  • Create a class to support the schema of the query results.

  • Return complete customer objects rather than using a projection. However, this will send much more data over the wire than necessary.

  • Use Entity SQL with either EntityClient or an ObjectQuery and return a List of dbDataRecords.

Anonymous Types in Web Services

You cannot use anonymous types as a return type or a received type in web services. Therefore, if you want to return the results of a query projection from a web service, you have a few options.

For one, you can create a class to support the schema of the query results and use that as the return type.

Alternatively, in the case of the Entity Framework you could use Entity SQL instead of LINQ to Entities. Combined with either EntityClient or an ObjectQuery, this will return a List of DbDataRecords. By doing this, however, you won't have a proper contract for the web service, which is a description of the type being transmitted. This may be an easy solution, but it goes against some of the basic tenets of web services.

In this sample, since the return type is a simple type with only two fields, creating a new class won't be too cumbersome, so that's what we'll do.

Note
Classes that are created solely for the purpose of transferring object values between processes follow the pattern called Data Transfer Objects, or DTOs. You'll be using a simple DTO in this service and again in the WCF service later in the chapter. Chapter 22, Implementing a Smarter WCF Service for Working with Entities uses DTOs more heavily to help solve some of the problems we encounter when transferring entities across tiers.

You can define the class in the same code file as the service; just place it after the end of the Service1 class. With Visual Basic, you will need to define the class variables and properties. With C#, you can take advantage of auto-implemented properties.

After the end of the Service1 class, add the class shown in Example 14.2, "The ShortContact class for transferring data from the service".

Example 14.2. The ShortContact class for transferring data from the service

Public Class ShortContact
  Private _ContactID As Integer
  Private _Name As String

  Public Property Name() As String
    Get
      Return _Name
    End Get
    Set(ByVal value As String)
      _Name = value
    End Set
  End Property

  Public Property ContactID() As Integer
    Get
      Return _ContactID
    End Get
    Set(ByVal value As Integer)
      _ContactID = value
    End Set
  End Property

End Class
public class ShortContact
{
    public string Name { get; set; }
    public int ContactID { get; set; }
}

Once the new class exists, you can add the GetCustomers method to the web service class (see Example 14.3, "The GetCustomers WebMethod"). GetCustomers will now return a List of ShortContact types.

Example 14.3. The GetCustomers WebMethod

<WebMethod()> _
Public Function GetCustomers() As List(Of ShortContact)
  Using Context As New BAEntities
    Dim query = From cust In Context.Contacts.OfType(Of Customer)() _
                Order By cust.LastName, cust.FirstName _
                Select New ShortContact With
                           {.ContactID = cust.ContactID, _
                            .Name = cust.LastName.Trim & ", "
                            & cust.FirstName}
                Return query.ToList
  End Using
End Function
[WebMethod()]
public List<ShortContact> GetCustomers()
{
  using (BAEntities Context = new BAEntities())
  {
    var query = from cust in Context.Contacts.OfType<Customer>()
                orderby cust.LastName, cust.FirstName
                select new ShortContact{ContactID = cust.ContactID,
                                       Name = cust.LastName.Trim()
                                       + ", " + cust.FirstName };
                return query.ToList();
  }
}

A few aspects of the query are worth pointing out:

  • Notice the LINQ syntax for projecting into a known type (ShortContact). You need to define what type will be returned from the projection, and then explicitly assign values to the variables in the object. This syntax is not specific to LINQ to Entities, but it is a general LINQ construct.

  • Notice the OfType method in the LINQ query. Though you saw OfType in Chapter 12, Customizing Entity Data Models, this is the first time you will see it in action in an application. OfType forces the EntitySet to return only those contacts who are Customer types.

  • Although in the console application tests you frequently used the Using construct to wrap your code, you are using it here for a very explicit purpose.

    When executing queries in the web service, you want your context and connection to be as short-lived as possible. You should create the context, execute the query (causing the context to open the connection and then close it when the data has been retrieved), and then get rid of the context immediately. With the possibility that many clients will make many calls to your services, you don't want to have any of those contexts or connections hanging around in memory.

  • The last notable piece of code is the ToList call. This forces the query to execute all the way to the end, and the List will contain the objects that result. Remember that the query's job is to execute, and that the query is not serializable. You need to force the objects to give up their dependency on the query. Because you have pushed them into a List, they are free and can easily be serialized and passed to the client application.

Testing the GetCustomers service operation

One of the nice benefits of creating web services in Visual Studio is that Visual Studio will automatically generate a web service interface in HTML where you can see a list of the methods in the service and execute them. This will allow you to easily see what the results of the GetCustomers operation look like:

  1. In the Solution Explorer, right-click the Service1.asmx file and select View in Browser from its context menu.

  2. Click the GetCustomers method, and on the next page click the Invoke button.

    This will show you the results of the GetCustomers method, which will look like Figure 14.2, "The XML payload returned by GetCustomers".

Figure 14.2. The XML payload returned by GetCustomers

The XML payload returned by GetCustomers

Adding a method to return a single customer

The next method you'll need is GetCustomer, to return a single customer. Here we will take the easy road and return a Customer entity.

Add the method in Example 14.4, "The GetCustomer WebMethod" into the web service.

Example 14.4. The GetCustomer WebMethod

<WebMethod()> _
Public Function GetCustomer(ByVal contactID As Integer) As Customer
  Using Context As New BAEntities
    Dim customers = From c In Context.Contacts.OfType(Of Customer) _
                    Where c.ContactID = contactID _
                    Select c
    Return customers.FirstOrDefault
  End Using
End Function
[WebMethod()]
public Customer GetCustomer(int contactID)
{
  using (BAEntities Context = new BAEntities())
  {
    var customers =
        from c in Context.Contacts.OfType<Customer>()
        where c.ContactID == contactID
        select c;
    return customers.FirstOrDefault();
  }
}

This method receives a ContactID value and then queries for a Customer, this time returning the entire Customer object. Use the FirstOrDefault method to return a single object, rather than returning a list. I've suggested FirstOrDefault rather than First because First will throw an Exception if the record cannot be found in the database. FirstOrDefault would return a null object, which may or may not be preferable in your scenario.

Testing the new method

Once again, you can view the ASMX file in the browser:

  1. Open Service1.asmx in the browser.

  2. Select the GetCustomer method.

  3. Enter the ID for one of the contacts-for example, 582 for Catherine Abel.

  4. Click Invoke.

Now you can see the payload of an entity object in web services (see Example 14.5, "A Customer entity with its EntityObject schema"). I have removed any whitespace for readability.

Example 14.5. A Customer entity with its EntityObject schema

<?xml version="1.0" encoding="utf-8"?>
<Customer xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xmlns:xsd="http://www.w3.org/2001/XMLSchema"
 xmlns="http://tempuri.org/">
  <EntityKey>
    <EntitySetName>Contacts</EntitySetName>
    <EntityContainerName>BAEntities</EntityContainerName>
    <EntityKeyValues>
      <EntityKeyMember>
        <Key>ContactID</Key>
        <Value xsi:type="xsd:int">582</Value>
      </EntityKeyMember>
    </EntityKeyValues>
  </EntityKey>
  <ContactID>582</ContactID>
  <FirstName>Catherine</FirstName>
  <LastName>Abel</LastName>
  <Title>Ms.</Title>
  <AddDate>2003-12-14T12:57:40.893</AddDate>
  <ModifiedDate>2008-08-07T08:27:07.033</ModifiedDate>
  <TimeStamp>AAAAAAAAF/8=</TimeStamp>
  <InitialDate>2007-06-29T05:56:51.593</InitialDate>
  <Notes>test43</Notes>
  <BirthDate>1994-01-05T00:00:00</BirthDate>
  <HeightInches>55</HeightInches>
  <WeightPounds>124</WeightPounds>
  <DietaryRestrictions>
</DietaryRestrictions>
  <CustomerTypeReference>
    <EntityKey>
      <EntitySetName>CustomerTypes</EntitySetName>
      <EntityContainerName>BAEntities</EntityContainerName>
      <EntityKeyValues>
        <EntityKeyMember>
          <Key>CustomerTypeID</Key>
          <Value xsi:type="xsd:int">3</Value>
        </EntityKeyMember>
      </EntityKeyValues>
    </EntityKey>
  </CustomerTypeReference>
  <PrimaryDestinationReference>
    <EntityKey>
      <EntitySetName>Destinations</EntitySetName>
      <EntityContainerName>BAEntities</EntityContainerName>
      <EntityKeyValues>
        <EntityKeyMember>
          <Key>DestinationID</Key>
          <Value xsi:type="xsd:int">49</Value>
        </EntityKeyMember>
      </EntityKeyValues>
    </EntityKey>
  </PrimaryDestinationReference>
  <SecondaryDestinationReference>
    <EntityKey>
      <EntitySetName>Destinations</EntitySetName>
      <EntityContainerName>BAEntities</EntityContainerName>
      <EntityKeyValues>
        <EntityKeyMember>
          <Key>DestinationID</Key>
          <Value xsi:type="xsd:int">52</Value>
        </EntityKeyMember>
      </EntityKeyValues>
    </EntityKey>
  </SecondaryDestinationReference>
  <PrimaryActivityReference>
    <EntityKey>
      <EntitySetName>Activities</EntitySetName>
      <EntityContainerName>BAEntities</EntityContainerName>
      <EntityKeyValues>
        <EntityKeyMember>
          <Key>ActivityID</Key>
          <Value xsi:type="xsd:int">16</Value>
        </EntityKeyMember>
      </EntityKeyValues>
    </EntityKey>
  </PrimaryActivityReference>
  <SecondaryActivityReference>
    <EntityKey>
      <EntitySetName>Activities</EntitySetName>
      <EntityContainerName>BAEntities</EntityContainerName>
      <EntityKeyValues>
        <EntityKeyMember>
          <Key>ActivityID</Key>
          <Value xsi:type="xsd:int">19</Value>
        </EntityKeyMember>
      </EntityKeyValues>
    </EntityKey>
  </SecondaryActivityReference>
</Customer>

This is an awfully large payload for what amounts to a small quantity of data. Even though the method is not returning a full graph, it is returning the complete schema of the Customer object, which includes references to all of the related data.

In many applications, this won't be an issue. In others you most likely will want to create another, simpler class, as you did for ShortContact, and return that rather than the EntityObject. This type of class is referred to as a Data Transfer Object (DTO), also known as a value object. DTO is a well-known design pattern for transferring objects between applications. For this example, the EntityObject is perfectly fine, but it's still important to be aware of what you are sending through the pipe.

XML Serialization, Web Services, and Graphs

This demo is specifically returning only a single object, and not graphs. The Customer does not have Reservations or Trips attached. The reason is that XML serialization is not able to retain the whole graph. Even if you created a graph showing a customer plus his reservations on the server side, when the data arrives on the client side the reservations will be gone. This is not an Entity Framework problem, but an age-old problem with services and XML serialization. You can solve the problem in a number of ways-for example, by using the Façade pattern. However, because WCF's DataContract serializer is able to serialize graphs, it makes more sense to just use WCF when graphs are involved. The WCF demo later in this chapter will handle graphs.

What about interoperability?

Seeing the output of this web method highlights a question that is very important to many developers: what about interoperability? If you are creating web services that consuming applications written in various technologies will use (e.g., .NET, PHP, and Java), you do not want to return entity objects from your web services. Instead, you should use DTOs.

In the ASMX sample, the GetCustomers method does just that, returning ShortContact rather than the Customer entity object. Its payload was simple XML. Example 14.6, "The XML for a ShortContact type returned by the service" shows a section of that payload for one customer. This is easily consumed by any technology.

Example 14.6. The XML for a ShortContact type returned by the service

<ShortContact>
 <Name>Abel, Catherine</Name>
 <ContactID>582</ContactID>
</ShortContact>

The GetCustomer method, however, returned a Customer entity object that was chock-full of metadata from the Entity Data Model (EDM). It gets really scary when you start dealing with entity references, which in a DTO might be surfaced and would simply be foreign key values, if you even need to return them to the client.

Digging a little deeper, if you look at the WSDL description of the method that returns the Customer entity, things get even more complicated as it contains information regarding the Contact entity type from which the Customer type derives.

WSDL

WSDL stands for Web Services Description Language and is used to provide a full description of the web service, its location, and its operations (methods). When you view the ASMX file in the web service interface, you will see a Service Description link at the top of the page. That will open the service in the WSDL view. You can always browse directly to a web service WSDL by adding ?WSDL to the end of the URL-for example, http://localhost/MyWebService.asmx?wsdl/. The WSDL describes the web service in detail using XML. Try it out!

Although you can leverage this in a .NET client in a number of ways (and even more easily using Visual Studio, because it can generate proxy classes), XML that contains all of this logic can be a nightmare for some consumers.

Figures 14.3 and 14.4 show all of the classes in the web service payload. This should give you a feeling for what a consumer will be dealing with when she isn't using Visual Studio, which, as you will see when you build the client, hides most of this from developers.

Figure 14.3. Dependency of EntityObject on Entity Framework classes

Dependency of EntityObject on Entity Framework classes

Figure 14.4. The ShortContact class, along with some more Entity Framework classes and the CompletedEventArgs for the four web methods

The ShortContact class, along with some more Entity Framework classes and the CompletedEventArgs for the four web methods

Notice the difference between the Customer object, which is dependent on a number of Entity Framework classes, and the ShortContact DTO, which is completely independent.

Note
The more generalized you need your service to be, the more it might make sense to consider ADO.NET Data Services, which can expose data through an EDM in a simplified way over the Web.

Minimally, services that are designed for interoperability need to return data that is not bound to entities.

The insert and update web methods

The web service now has two web methods that return data. It also needs the ability to save data. This web service will allow for updates and inserts:

  1. Add to the web service the insert method shown in Example 14.7, "The InsertCustomer WebMethod".

    Example 14.7. The InsertCustomer WebMethod

    <WebMethod()> _
      Public Function InsertCustomer(ByVal cust As Customer) As String
        Try
          Using context As New BAEntities
              context.AddToContacts(cust)
            context.SaveChanges()
          End Using
          Return cust.ContactID.ToString
        Catch ex As Exception
          Return "Error: " & ex.Message
        End Try
      End Function
    
    [WebMethod()]
    public string InsertCustomer(Customer cust)
    {
      try
      {
        using (BAEntities context = new BAEntities())
        {
          context.AddToContacts(cust);
          context.SaveChanges();
        }
        return cust.ContactID.ToString();
      }
      catch (Exception ex)
      {
        return "Error: " + ex.Message;
      }
    }
    

    This method will insert a new customer and return the contactID that comes back from the SaveChanges method. If something goes wrong, it will return the exception message instead. The return type of the function is String in order to accommodate returning a status message. The client-side code can then react based on whether a value or an error is returned.

  2. Add to the web service the update method shown in Example 14.8, "The UpdateCustomer WebMethod".

    Example 14.8. The UpdateCustomer WebMethod

    <WebMethod()> _
    Public Function UpdateCustomer(ByVal custEdit As Customer) As String
      Try
        Using context As New BAEntities
          Dim custtoUpdate =
             (From c In Context.Contacts.OfType(Of Customer) _
              Where c.ContactID = custEdit.ContactID) _
              .FirstOrDefault
          If Not custtoUpdate Is Nothing Then
            context.ApplyPropertyChanges("Contacts", custEdit)
            context.SaveChanges()
          End If
          Return "Success"
        End Using
      Catch ex As Exception
        Return "Error: " & ex.Message
      End Try
    End Function
    
    [WebMethod()]
    public string UpdateCustomer(Customer custEdit)
    {
      try
      {
        using (BAEntities context = new BAEntities())
        {
          var custtoUpdate = (from c in context.Contacts.OfType<Customer>()
                              where c.ContactID == custEdit.ContactID
                              select c)
                              .FirstOrDefault();
    
          if (custtoUpdate != null)
          {
            context.ApplyPropertyChanges("Contacts", custEdit);
            context.SaveChanges();
          }
          return "Success";
        }
      }
      catch (Exception ex)
      {
        return "Error: " + ex.Message;
      }
    }
    

This update method queries the database to get a current version of the customer, and places an "original" version of the customer in the ObjectContext. Then, ApplyPropertyChanges, which you learned about in Chapter 9, Working with Object Services, takes the Customer that just came back from the client and updates the one that is being tracked by the context. When SaveChanges is called, any values from the incoming Customer that were different from the server values will be updated in the database.

Why were all of these steps necessary? Because the object that comes back from the client will have only its current values, and will have no knowledge of its original values. ObjectContext would not recognize that any updates need to be made. This is the impact of moving objects across tiers. To anyone who is used to just calling a stored procedure and sending in the new values, it may seem confusing because of the extra step. The reasoning behind the code in this method requires further explanation, which I provide in the next sidebar.

The Problem with Change Tracking Across Tiers

The simple example in Example 14.8, "The UpdateCustomer WebMethod" exposed one of the biggest challenges for enterprise developers in version 1 of the Entity Framework: change tracking in multi-tiered applications. In Chapter 9, Working with Object Services, you learned about the role of the ObjectStateEntry within the ObjectContext. The ObjectStateEntry keeps track of the original and current states of each object. The objects themselves know only their current values.

When entity objects are serialized to be passed between services and clients (or for other reasons), only the objects are serialized. Neither the ObjectContext nor the ObjectStateEntry is serialized. Therefore, when the customer is deserialized on the receiving end, it has only its current values. As you have seen, SaveChanges depends on the original values to determine what has changed, and therefore what needs to be sent to the database for updates.

You can solve this problem in a number of ways, and the choices depend on your goals and the code investment you are willing to make. Because this is an introductory sample, you will see the simplest way to perform updates in this scenario, but it is not necessarily the way that is most efficient or provides the best performance.

The path of least resistance

The simplest way to handle this scenario, if you don't mind the extra call to the database, is the method you see in the UpdateCustomer web method: query the database to get the current stored version of the customer into the ObjectContext, update the object with the new values using ApplyPropertyChanges, and then call ObjectContext.SaveChanges.

Again, different business rules will demand different solutions, but in this scenario, there is no need to know what the original values were when that customer was first queried.

Let's take a closer look at the two critical steps performed by the UpdateCustomer method:

  1. The method queries the EDM to get the current server values for the customer.

    The object from the database has the following values:

    • ContactID: 123

    • FirstName: Julie

    • LastName: Lerman

    • Note: [null]

    Because the object came directly from the database, the original values and current values for the object are the same.

  2. The method then calls ApplyPropertyChanges("Contacts", custEdit):

    1. The context inspects the EDM's metadata and learns that in the Contacts EntitySet, the CustomerID property is used for the EntityKey.

    2. Next, the context looks at custEdit and determines that the CustomerID value is 123.

    3. Now it can find the cached object that is tied to the Contacts entity and has the value 123 in the CustomerID field. This is the object that needs to be changed.

    4. The processor sets the current value for each property of the object to the values that it extracts from the changed object.

In the end, the object's values are updated as shown in Table 14.1, "Original and current values of the entity after ApplyPropertyChanges has been called".

Table 14.1. Original and current values of the entity after ApplyPropertyChanges has been called

Object

Original value

Current value

ContactID

123

123

FirstName

Julie

Julie

LastName

Lerman

Lerman

Notes

[null]

"Finally learned to roll her kayak"


When it's time to call SaveChanges, Object Services will find that only the Notes field has changed and the query that goes to the database will be a parameterized command that would translate to the following:

Update Customers Set Notes='Finally learned to roll her kayak' Where ContactID=123
Note
Because these operations involve receiving serialized objects, you can't test them in the browser interface the way you tested the previous operations.

That's it for the web service. Now it's time to build the client to consume it.

Building the Client Application

The client will have a simple interface, shown in Figure 14.1, "A Windows form that will consume a web service that uses the Entity Framework", with a drop-down to show a list of customers and fields for editing a currently selected customer, which can be saved. The UI also allows a user to create and save a new customer.

The steps for creating the application will be as follows:

  1. Create a reference to the web service and proxy classes.

  2. Add the logic for interacting with the service.

  3. Design the user interface.

  4. Tie the UI elements to the various functions for interacting with the service.

Setting up the client project and the proxy to the service

To create a reference to the web service and proxy classes, follow these steps:

  1. Create a new Windows Forms Application project.

    Note that you will not be referencing System.Data.Entity or the BreakAwayModel project in this application. All of that knowledge will come from the web service.

  2. In the Solution Explorer, right-click the new project and select Add Service Reference.

    Note
    If you have used web services in versions of Visual Studio prior to Visual Studio 2008, note that the Web References dialog is now buried within the Service Reference dialog.
  1. Click the Advanced button at the bottom of the Service Reference dialog to open the Service Reference Settings page.

  2. Click Add Web Reference at the bottom of the settings page.

  3. In the Web Reference dialog, choose the option to browse to web services in this solution.

  4. Select the service you just created.

    If you didn't change the service name, it will be Service1. If you had more than one service in the solution, you may need to rely on the project name to properly identify the correct service.

    After you select the service, the dialog will display the same HTML interface showing the operations that are in the service that you saw when testing the service.

  5. On the right, change the web reference name to BAWebService and then click the Add Reference button.

Visual Studio will automatically create proxy classes so that you can work with the web services in the client-side code.

If you click the Show All Files icon in the Solution Explorer, you can expand the web reference to see what files were created, as shown in Figure 14.5, "Expanding the newly created web reference to reveal all of the files Visual Studio created".

Figure 14.5. Expanding the newly created web reference to reveal all of the files Visual Studio created

Expanding the newly created web reference to reveal all of the files Visual Studio created

Reference.vb/Reference.cs contains the proxy class against which you will code. If you haven't used web services before, you might be interested in opening that file to see what's in there.

Note the Customer.DataSource and ShortContact.DataSource. Visual Studio read the WSDL and determined what types were being returned or received by the methods in the web service. Based on this information, it created these DataSources so that you can easily use the two classes in data-binding scenarios, which you will do in the client application.

Adding methods to interact with the web service

Your form will need one method for each different web method you'll be calling. The form will also need to keep track of the current customer being edited.

Add to the form the variable declaration shown in Example 14.9, "The _currentCustomer form variable".

Example 14.9. The _currentCustomer form variable

Private _currentCustomer As BAWebService.Customer
BAWebServices.Customer _currentCustomer;

Add to the form's code-behind the methods shown in Example 14.10, "Form methods for interacting with the web service". Each method will instantiate the web service, call the necessary method, and return any results from the service.

The UpdateCustomer method will call the appropriate service method based on the state of the _currentCustomer object. If it's new, which is easy to tell by its unassigned ContactID, the object is passed to the InsertCustomer method; otherwise, it is passed to the UpdateCustomer method.

Example 14.10. Form methods for interacting with the web service

Private Function GetCustomers() As Array
  Using proxy As New BAWebService.Service1
    Return proxy.GetCustomers.ToArray
  End Using
End Function

Private Function GetCustomer(ByVal id As Integer) _
 As BAWebService.Customer
  Using proxy As New BAWebService.Service1
    Return proxy.GetCustomer(id)
  End Using
End Function

Private Function UpdateCustomer() As String
  Dim status As String
  Using proxy As New BAWebService.Service1
    If _currentCustomer.ContactID = 0 Then
      status = proxy.InsertCustomer(_currentCustomer)
    Else
      status = proxy.UpdateCustomer(_currentCustomer)
    End If
    Return status
  End Using
End Function
private Array GetCustomers()
{
  using (BAWebService.Service1 proxy = new BAWebService.Service1())
  {
    return proxy.GetCustomers().ToArray();
  }
}

private BAWebService.Customer GetCustomer(int id)
{
  using (BAWebService.Service1 proxy = new BAWebService.Service1())
  {
    return proxy.GetCustomer(id);
  }
}

private string UpdateCustomer()
{
  string status = null;
  using (BAWebService.Service1 proxy = new BAWebService.Service1())
  {
    if (_currentCustomer.ContactID == 0)
    {
      status = proxy.InsertCustomer(_currentCustomer);
    }
    else
    {
      status = proxy.UpdateCustomer(_currentCustomer);
    }
    return status;
  }
}

Designing the form

You'll be designing the form to look something like the form shown in Figure 14.1, "A Windows form that will consume a web service that uses the Entity Framework":

  1. Open the form and drag a ComboBox onto it.

  2. Add the FillCombo method, which will populate the ComboBox using the GetCustomers method shown in Example 14.11, "Method for filling the ComboBox with the Customer list".

    Example 14.11. Method for filling the ComboBox with the Customer list

    Private Sub FillCombo()
      With ComboBox1
        .DisplayMember = "Name"
        .ValueMember = "ContactID"
        .DataSource = GetCustomers
      End With
    End Sub
    
    private void FillCombo()
    {
      comboBox1.DisplayMember = "Name";
      comboBox1.ValueMember = "ContactID";
      comboBox1.DataSource = GetCustomers();
    }
    
  3. Modify the form's Load event to call the method shown in Example 14.12, "Calling FillCombo in the form's Load event".

    Example 14.12. Calling FillCombo in the form's Load event

    Private Sub Form1_Load( _
     ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
      FillCombo()
    End Sub
    
    private void Form1_Load(object sender, EventArgs e)
    {
      FillCombo();
    }
    
  4. Return to the Design view of the form.

    To simplify editing the selected Customer, you'll use a DataSource as you did in the Windows Forms application in Chapter 8, Data Binding with Windows Forms and WPF Applications. Thanks to the web reference generation, data sources have already been created for the Customer and ShortContact types exposed by the web service.

  5. Open the Data Sources window and click the Customer object to select it. This will cause a drop-down arrow to appear.

  6. Select Details from the drop-down, as shown in Figure 14.6, "Changing the Customer data source to default as a Details view in the Data Sources window". By default, Customer would normally be displayed as a DataGridView. This Windows Forms feature lets you change the default display to a Details form.

  7. Drag the Customer data source onto the form. This action will create a set of labels and text boxes for each property, a CustomerDataSource, and a navigation toolbar.

  8. Delete the navigation toolbar since you will be using the combo box to control which customer is displayed.

    Note
    The Customer data source also added fields for the entity container name and the EntitySet. Where did they come from? If you open the Customer in the DataSource you'll find that these came from inside the EntityKey property.
  1. Remove any unnecessary labels and text boxes so that your form looks like that shown earlier in Figure 14.1, "A Windows form that will consume a web service that uses the Entity Framework".

Figure 14.6. Changing the Customer data source to default as a Details view in the Data Sources window

Changing the Customer data source to default as a Details view in the Data Sources window

The new form controls will be populated with the selected Customer each time the user changes the selection in the combo box. To trigger this, you'll call the GetCustomer method in the ComboBox's SelectIndexChanged event and bind the results to the _currentCustomer variable, which in turn you will bind to the CustomerBindingSource. Example 14.13, "Retrieving a new Customer when the user makes a selection in the combo box" shows how to do this.

Example 14.13. Retrieving a new Customer when the user makes a selection in the combo box

Private Sub ComboBox1_SelectedIndexChanged( _
 ByVal sender As System.Object, ByVal e As System.EventArgs) _
 Handles ComboBox1.SelectedIndexChanged
  _currentCustomer = GetCustomer(CInt(ComboBox1.SelectedValue))
  CustomerBindingSource.DataSource = currentCustomer
End Sub
private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
{
  _currentCustomer = GetCustomer((Int32)comboBox1.SelectedValue);
  customerBindingSource.DataSource = _currentCustomer;
}

CustomerBindingSource will use its data source, the Customer, to populate the text boxes on the form.

Adding the New and Save buttons

Now you'll need to allow the user to create new customers and save changes to existing or new customers:

  1. Add two buttons to the form. Change their Text fields to New and Save, respectively.

  2. Add the code in Example 14.14, "Creating a new Customer" to the Click event of the New button.

    Example 14.14. Creating a new Customer

    Private Sub NewCust_Click( _
     ByVal sender As System.Object, ByVal e As System.EventArgs) _
     Handles NewCust.Click
      (currentCustomer = New BAWebService.Customer
      CustomerBindingSource.DataSource = currentCustomer
    End Sub
    
    private void NewCust_Click(object sender, EventArgs e)
    {
      _currentCustomer = new Customer();
      customerBindingSource.DataSource = _currentCustomer;
    }
    
  3. Add the code in Example 14.15, "Handling new and modified Customers" to the Click event of the Save button.

    Remember that the InsertCustomer service will return either the new ID of the Customer or an error message, whereas UpdateCustomer returns the "Success" or "Error" string plus the error message. The bulk of the code in this method is for dealing with the result of the InsertCustomer method.

    Example 14.15. Handling new and modified Customers

    Private Sub SaveCust_Click( _
     ByVal sender As System.Object, ByVal e As System.EventArgs) _
     Handles SaveCust.Click
    
      Dim newCust = (_currentCustomer.ContactID = 0)
      Dim status = UpdateCustomer()
     'insert customer returns the id of the new customer
      If newCust Then
        Dim newid As Integer
        If Not Integer.TryParse(status, newid) Then
          MessageBox.Show(status, "Web Service Error")
        Else
          _currentCustomer.ContactID = newid
         'get fresh data for list
          FillCombo()
          ComboBox1.SelectedValue = newid
        End If
      Else
        MessageBox.Show("Update: " & status)
      End If
    End Sub
    
    private void SaveCust_Click(object sender, EventArgs e)
    {
      var newCust = (_currentCustomer.ContactID == 0);
      var status = UpdateCustomer();
      //insert customer returns the id of the new customer
      if (newCust)
      {
        int newid = 0;
        if (!(int.TryParse(status, out newid)))
        {
          MessageBox.Show(status, "Web Service Error");
        }
        else
        {
          _currentCustomer.ContactID = newid;
          //get fresh data for list
          FillCombo();
          comboBox1.SelectedValue = newid;
        }
      }
      else
      {
        MessageBox.Show("Update: " + status);
      }
    }
    

If successful, the InsertCustomer method returns the new ID, which is applied to the _currentCustomer in case the user continues to make changes to the object. If the user then clicks Save again, the UpdateCustomer method will be called rather than inserting that customer to the database a second time.

Although you could add the new customer into the combo box, this method takes the easy route, which also requires another trip to the server, calling the FillCombo method to populate the Customers ComboBox with fresh data.

Testing your application

You have now added all of the logic necessary to get the form to consume the web service. Run the form and watch it work. Be sure to put breakpoints in the form's code as well as the code for the service so that you can debug into the objects and take a closer look at what's going on under the covers.

The client-side Customer versus the server-side Customer

It's important to keep in mind that this client application is not aware of the Entity Framework at all. The definition of the Customer type it is using comes directly from the WSDL description and it is merely an object, not an entity object. No change tracking is occurring on the client side. Only the service tier has any knowledge of the Entity Framework.

If you wanted to use the Entity Framework on the UI side of this solution, you would need to keep in mind that the BreakAwayModel's Customer type is very different from BAWebService.Customer. A common point of confusion for .NET developers using ASMX Web Services is that they attempt to cast the returned object to a local version of what should be the same object. But if you look at the Customer.DataSource in the Data Sources window or open the Reference.vb/Reference.cs class in the Web Reference section of the project, you can see that it is a very different object than the Customer entity.

WCF is a much more sophisticated paradigm for building .NET services that .NET or other clients can consume. With regard to the Entity Framework, a big difference from using ASMX Web Services is that WCF's DataContract serializer is able to persist graphs. The WCF sample you'll build in this section will work with graphs and deal with the challenges introduced by performing updates on graphs that have come across a tier.

In this walkthrough, you will build a WCF service that is similar to the web service you already built, but because you will be working with graphs, the methods will be a bit more complex. Rather than building a whole new client application, you will modify the existing Windows form from the web service walkthrough to work with the WCF service.

Note
Chapter 22, Implementing a Smarter WCF Service for Working with Entities revisits building WCF services with a different pattern. Although the complexity in the service you are about to build here is concentrated in the service methods, in Chapter 22, Implementing a Smarter WCF Service for Working with Entities the service itself is more efficient, and the solution leans even more heavily on Data Transfer Objects.

Figure 14.7, "The client application that will consume the WCF service" shows what the new client application will look like so that you have an idea of what is needed in the service.

Figure 14.7. The client application that will consume the WCF service

The client application that will consume the WCF service

Building the WCF Service

Creating a WCF service is very different from creating an ASMX Web Service. First you will define a set of interfaces that represent contracts for the necessary operations and then you will implement those interfaces as methods in a separate code file.

Creating the service application

We'll start by creating the service application project:

  1. Add a new WCF Service Application project to the solution.

    Note that the project has one file named IService1 and another named Service1. You will use IService1 to describe what the service will do. It contains a list of the operations, but doesn't have the code for implementing them. This type of file is referred to as an interface, which is a very common programming construct. In WCF, this interface acts as a "contract," or a promise regarding what to expect of the service. It is also a place where you can define new data types that will be used to send data to or from the service. These are referred to as DataContracts and are related to the classes you saw created when you referenced the web service from the Windows Forms project earlier.

  2. Add project references to System.Data.Entity and to the BreakAwayModel project.

  3. Replace the ConnectionStrings section in the web.config file with the ConnectionStrings section from the BreakAwayModel project's app.config file.

Note
Unless you are experienced with WCF, if you don't like the default name of Service1, I wouldn't recommend renaming the class because you will have to make a number of changes elsewhere in the project and in the .config file. It's simpler in WCF to just create a new service. You can easily add a new WCF service item to the project and name it yourself, and then use that for the walkthrough.

Defining the operations the service will provide

The operation contract is defined in the interface, IService1. Each method has an OperationContract attribute to indicate that it is an operation that is part of the contract for your service:

  1. Open the IService1 file.

  2. Delete the default (example) operations for GetData and GetDataUsingDataContract.

  3. Add an Imports or using statement to the class for BAGA.

  4. Add the OperationContract methods in Example 14.16, "Defining the service operation contracts" into the IService1 interface. Note that the String for the return type of GetCustomers and GetTrips is a placeholder.

    Example 14.16. Defining the service operation contracts

    <OperationContract()> _
    Function GetCustsandTrips() As String
    
    <OperationContract()> _
    Function GetCustomer(ByVal custID As Integer) As Customer
    
    <OperationContract()> _
    Function UpdateCustomer(ByVal cust As Customer) As String
    
    <OperationContract()> _
    Function InsertCustomer(ByVal cust As Customer) As String
    
    <OperationContract()> _
    Function DeleteCustomer(ByVal custID As Integer) As String
    
    [OperationContract]
    string GetCustsandTrips();
    
    [OperationContract]
    Customer GetCustomer(int custID);
    
    [OperationContract]
    string UpdateCustomer(Customer cust);
    
    [OperationContract]
    string InsertCustomer(Customer cust);
    
    [OperationContract]
    string DeleteCustomer(int custID);
    

Defining the DataContract classes the service will use

In the web service example, you created a DTO called ShortContact. In this WCF service, you'll need ShortContact as well as a few other custom classes. In WCF these are called DataContracts and each is defined by a DataContract attribute.

DataContract classes become part of the contract of what you can expect from the service. The contract will say, "Not only will I provide this set of operations, but I also will send and receive data that has the following schema." By creating classes that are DataContracts, you can provide this information in the WSDL of the service, and both the service and the client can use it easily. Each property you need to serialize is flagged as a DataMember. For C#, because of the need for the DataMember attribute, you don't get to use the auto-implemented properties this time. Sorry.

Note
The entity classes an EDM generates are marked as DataContract classes so that they can automatically be used with WCF solutions. Therefore, the Customer class you'll use will automatically become part of the service contract.

You'll need to create three classes:

  • The first is familiar; it's the same ShortContact you used in the web service.

  • The second, CustsandTrips, is designed to allow you to pass some pick lists back to the client in one call, rather than in two. It has one property that will encapsulate a list of ShortContacts and another that will encapsulate a list of Trips objects.

  • The last, CustEdit, is for customers that the client will return and that need to be updated. It contains one property that encapsulates the Customer object (which will be a graph including the customer's reservations and the trip details for each reservation). The second property, ReservationstoDelete, is a List of integers that will allow the user to delete reservations from a customer. (We'll discuss this in more detail later in this chapter.)

Add the classes in Example 14.17, "Creating the service's DataContract classes" to the IService1 file below the interface. There is a default CompositeType class that you can delete.

Example 14.17. Creating the service's DataContract classes

<DataContract() _
Public Class ShortContact
  Private _ContactID As Integer
  Private _Name As String

  <DataMember()> _
  Public Property Name() As String
    Get
      Return _Name
    End Get
    Set(ByVal value As String)
      _Name = value
    End Set
  End Property
  <DataMember()> _
  Public Property ContactID() As Integer
    Get
      Return _ContactID
    End Get
    Set(ByVal value As Integer)
      _ContactID = value
    End Set
  End Property
End Class

<DataContract()> _
Public Class CustsandTrips
  Private _custs As List(Of ShortContact)
  Private _trips As List(Of Trip)
  <DataMember()> _
  Public Property Custs() As List(Of ShortContact)
    Get
      Return _custs
    End Get
    Set(ByVal value As List(Of ShortContact))
      _custs = value
    End Set
  End Property
  <DataMember()> _
  Public Property Trips() As List(Of Trip)
    Get
      Return _trips
    End Get
    Set(ByVal value As List(Of Trip))
      _trips = value
    End Set
  End Property
End Class

<DataContract()> _
Public Class CusttoEdit
  Private _customer As Customer
  Private _reservationsToDelete As List(Of Integer)
  <DataMember()> _
  Public Property Customer() As BAGA.BreakAwayModel.Customer
    Get
      Return _customer
    End Get
    Set(ByVal value As BAGA.BreakAwayModel.Customer)
      _customer = value
    End Set
  End Property
  <DataMember()> _
  Public Property ReservationsToDelete() As List(Of Integer)
    Get
      Return _reservationsToDelete
    End Get
    Set(ByVal value As List(Of Integer))
      _reservationsToDelete = value
    End Set
  End Property
End Class
[DataContract]
public class ShortContact
{
  int _contactID;
  string _Name;
  [DataMember]
  public int ContactID
  {
    get { return _contactID; }
    set { _contactID = value; }
  }
  [DataMember]
  public string Name
  {
    get { return _Name; }
    set { _Name = value; }
  }
}

[DataContract()]
public class CustsandTrips
{
  private List<ShortContact> _custs;
  private List<Trip> _trips;

  [DataMember()]
  public List<ShortContact> Custs
  {
    get  {  return _custs;  }
    set  {  _custs = value; }
  }

  [DataMember()]
  public List<Trip> Trips
  {
    get  {  return _trips;  }
    set  {  _trips = value; }
  }
}

[DataContract()]
public class CusttoEdit
{
  private Customer _customer;
  private List<int> _reservationsToDelete;

  [DataMember()]
  public BAGA.BreakAwayModel.Customer Customer
  {
    get  {  return _customer;  }
    set  {  _customer = value; }
  }

  [DataMember()]
  public List<int> ReservationsToDelete
  {
    get {   return _reservationsToDelete;  }
    set {   _reservationsToDelete = value; }
  }
}

Modify the GetCustsandTrips operation so that it returns a CustsandTrips type, as shown in Example 14.18, "Fixing the GetCustsandTrips operation signature".

Example 14.18. Fixing the GetCustsandTrips operation signature

<OperationContract()> _
Function GetCustsandTrips() As CustsandTrips
[OperationContract]
CustsandTrips GetCustsandTrips();

Modify the UpdateCustomer operation so that it takes a CusttoEdit type, as shown in Example 14.19, "Fixing the UpdateCustomer operation signature".

Example 14.19. Fixing the UpdateCustomer operation signature

<OperationContract()> _
  Function UpdateCustomer(ByVal cust As CusttoEdit) As String
[OperationContract]
string UpdateCustomer(CusttoEdit cust);

Enabling the model's partial class properties for Trip and Reservation to participate in the service

Although the code-generated entity classes and their properties are marked with DataContract and DataMember attributes by default, the custom properties that you have created are not. The client can benefit from the TripDetail property that you added to the Trip class in customizing_entities in Chapter 10, Customizing Entities. To make this property available as part of the WCF service payload, you'll need to add the DataMember attribute.

You have one more rule to satisfy. DataMembers must have both a getter and a setter to be serializable. Otherwise, you will get an error in the service that is using the class.

Open the Trip's partial class in the model project and modify the TripDetail property by adding the DataMember attribute and the SET clause, as shown in Example 14.20, "Modifying the Trip in the partial class of the EDM".

Example 14.20. Modifying the Trip in the partial class of the EDM

<System.Runtime.Serialization.DataMember()> _
Public Property TripDetails() As String
  Get
    ...existing code...
  End Get
  Set(ByVal value As String)
  End Set
End Property
[System.Runtime.Serialization.DataMember]
public string TripDetails
{
  get
  {
    //existing code
  }
  set{}
}

Do the same for the TripDetails property of the Reservation partial class so that you can use Reservation.TripDetails on the client side.

Implementing the service interface

Now it's time to add some logic to the operations that are based on the interface. You'll do this in the Service1 class.

  1. Remove the default methods.

  2. Implement the IService interface.

    You do this differently in VB and C#. In VB, place your cursor at the end of the Implements IService1 line of code and press the Enter key. In C#, right-click on IService1 in the class declaration, select Implement Interface from the context menu, and select Implement Interface from that context menu's submenu. All of the methods defined in the interface will be stubbed out for you.

  3. Import the BAGA namespace into the class with using or Imports.

  4. Add the code for GetCustsandTrips (see Example 14.21, "Filling in the logic for the CustsandTrips method").

    This contains the same query as the GetCustomers method in the web service, but it also queries for trips with their location information. The results of both queries are packaged into a CustsandTrips object to return to the client.

    Example 14.21. Filling in the logic for the CustsandTrips method

    Public Function GetCustsandTrips() As CustsandTrips _
     Implements IService1.GetCustsandTrips
      Dim ct As New CustsandTrips
      Using Context As New BAEntities
        Dim custquery =
            From cust In context.Contacts.OfType(Of Customer) _
            Order By cust.LastName, cust.FirstName _
            Select New ShortContact With { _
                   .ContactID = cust.ContactID, _
                   .Name = cust.LastName.Trim & ", " & cust.FirstName.Trim}
        ct.Custs = custquery.ToList
        Dim tripQuery = From trip In context.Trips.Include("Destination")
        ct.Trips = tripQuery.ToList
      End Using
      Return ct
    End Function
    
    public CustsandTrips GetCustsandTrips()
    {
      CustsandTrips ct = new CustsandTrips();
      using (var context = new BAEntities())
      {
        var custquery = from cust in context.Contacts.OfType<Customer>()
                        orderby cust.LastName, cust.FirstName
                        select new ShortContact
                        {
                          ContactID = cust.ContactID,
                          Name = cust.LastName.Trim()
                            + ", " + cust.FirstName.Trim()
                        };
        ct.Custs = custquery.ToList();
        var tripQuery = from trip in context.Trips.Include("Destination")
                        select trip;
        ct.Trips = tripQuery.ToList();
      }
      return ct;
    }
    
  5. Add the code for GetCustomer (see Example 14.22, "Logic for the GetCustomer method").

    Here is where things start to differ from the web service. You will return a customer graph that includes the customer's reservations, the trip information for the reservations, and the location information for the trips. This way, you will be able to build a descriptive list of reservations for the customer on the client side.

    Example 14.22. Logic for the GetCustomer method

    Public Function GetCustomer(ByVal custid As Integer) As BAGA.Customer _
     Implements IService1.GetCustomer
      Using context As New BAEntities
        Dim cust = From c In context.Contacts.OfType(Of Customer). _
                   Include("Reservations.Trip.Destination") _
                   Where c.ContactID = custID _
                   Select c
        Return cust.FirstOrDefault
      End Using
    End Function
    
    public Customer GetCustomer(int custID)
    {
      using (BAEntities context = new BAEntities())
      {
        var cust =
            from c in context.Contacts.OfType<Customer>()
              .Include("Reservations.Trip.Destination")
            where c.ContactID == custID
            select c;
        return cust.FirstOrDefault();
      }
    }
    
  6. Add the code in Example 14.23, "Code for the InsertCustomer method" to the InsertCustomer method.

    Example 14.23. Code for the InsertCustomer method

    Public Function InsertCustomer(ByVal cust As BAGA.Customer) As String _
     Implements IService1.InsertCustomer
    
      Try
        Using context As New BAEntities
          RemoveTripsfromGraph(cust)
          context.AddToContacts(cust)
          context.SaveChanges()
        End Using
        Return cust.ContactID.ToString
      Catch ex As Exception
        Return "error: " & ex.Message
      End Try
    End Function
    
    public string InsertCustomer(BAGA.Customer cust)
    {
      try
      {
        using (BAEntities context = new BAEntities())
        {
          RemoveTripsfromGraph(cust);
          context.AddToContacts(cust);
          context.SaveChanges();
        }
        return cust.ContactID.ToString();
      }
      catch (Exception ex)
      {
        return "error: " + ex.Message;
      }
    }
    

    This method is nearly the same as InsertCustomer in the web service. Thanks to the way relationship spanning works, by adding the new Customer to the context, you are also adding the reservations that are attached to it. You may have noticed a new method being called in Example 14.23, "Code for the InsertCustomer method": RemoveTripsfromGraph. This method is related to an issue with adding entity graphs to the context. We will discuss this method in more detail in the following section.

Testing the WCF Services and Operations

Although you can verify that the service runs by viewing it in the browser, you cannot test the operations as you can with an ASMX Web Service. A WCFTestClient utility is available, but it does not display responses that contain entities. Testing the WCF service to see the payload involves trace logging and a number of steps, the description of which is not within the scope of this book. Furthermore, in the end the response is so gnarly that it doesn't make sense to try to look at it here. This is a good reminder of why you should not return entity objects to non-.NET consumers. However, when you get to the client side of this walkthrough, you will be able to debug into the returned Customer and see that the Reservation, Trip, and Location objects have come over in their entirety as part of the customer graph.

Adding graphs to ObjectContext

The concept of relationship spanning and its rules may make it easy to add a graph to a context, yet they have a limitation. Because you are adding the new Customer, everything in the graph will be treated as something to be added. That's very handy for Reservations, but what about Reservation.Trip? Reservation.Trip will actually cause the Add to fail because the Trip entity came from the database and has an EntityKey. When the context attempts to add the trip, an exception will be thrown. The fact that it has an EntityKey tells the context that it is not a new Trip and therefore cannot be added.

How do you add some things from a graph and not others? You need to disassemble part of the graph before it is added to the context, which is not an obvious task.

The reservation will have a Trip entity as the value of Reservation.Trip, but Reservation.TripReference will not contain the trip information when a new customer with a new reservation comes in from the client. To add the graph to the context, you need this to be completely reversed. You cannot add the reservation to the context with the trip attached (SaveChanges will treat Trip as a new item), yet you need the TripReference populated for the foreign key (TripID) to be included in the reservation. The RemoveTripsfromGraph method shows how to solve this puzzle.

Add the RemoveTripsfromGraph method to the Service1 class, as shown in the following code:

Private Sub RemoveTripsfromGraph(ByRef cust As Customer)
  For Each r In cust.Reservations
    Dim tripEntityKey = r.Trip.EntityKey
    r.Trip = Nothing
    r.TripReference.EntityKey = tripEntityKey
  Next
End Sub
private void RemoveTripsfromGraph(Customer cust)
{
  foreach (var r in cust.Reservations)
  {
    var tripEntityKey = r.Trip.EntityKey;
    r.Trip = null;
    r.TripReference.EntityKey = tripEntityKey;
  }
}

What is going on in this little routine after a Customer is passed in? The code iterates through each reservation. It grabs an instance of the EntityKey for that Reservation's Trip, and then drops the Trip. The tripEntityKey retains its values even after the instance of the Trip is gone. Figure 14.8, "The EntityKey for the Trip, which will be used to replace the Trip entity with a TripReference.EntityKey" shows that the value of the EntityKey is still intact even though r.Trip is null.

Figure 14.8. The EntityKey for the Trip, which will be used to replace the Trip entity with a TripReference.EntityKey

The EntityKey for the Trip, which will be used to replace the Trip entity with a TripReference.EntityKey

With the Trip removed, the context will not attempt to add it. However, the Reservation still needs to know which trip the customer is going on. The next line achieves that by setting the EntityKey of the TripReference. You saw this method used in previous examples, beginning with the first Windows Forms application you built in Chapter 8, Data Binding with Windows Forms and WPF Applications. Rather than having a Trip entity, having the EntityKey is enough. Figure 14.9, "The RemoveTripsfromGraph method, which changes the Reservation's Trip from an entity to an EntityReference" shows the Reference with the Trip attached, and then the Reference with the Trip removed but the EntityKey tied to the TripReference. This will provide enough information to populate the foreign key value in the database record when the insert is performed.

Figure 14.9. The RemoveTripsfromGraph method, which changes the Reservation's Trip from an entity to an EntityReference

The RemoveTripsfromGraph method, which changes the Reservation's Trip from an entity to an EntityReference

Deleting objects

Deleting the Customer requires another involved piece of logic, as deleting the Customer means deleting the Customer's Reservations. A referential constraint in the BreakAway database says that every Reservation must be related to a Customer. If you attempt to delete a customer that has reservations, the database will throw an error because it won't allow orphaned reservations.

Note
It is possible in the database to enforce cascading deletes, which means that if the customer is deleted, the database will automatically delete any dependent reservations. In this case, you need to delete only the customer, and the database will handle the rest. You'll learn more about referential constraints and cascading deletes in Chapter 15, Working with Relationships and Associations.

In the meantime, the DeleteCustomer routine will need to explicitly delete all of the related Reservations for the Customer object. Don't forget that the Customer is derived from a contact. The Customer record is only an extension of a Contact record. Therefore, a business decision is involved here: will the Contact record be deleted? In the case of BreakAway Geek Adventures, the rule is not to delete customers and reservation history, but for the sake of your education, we have permission to enable it in this service. So, let's get coding.

To call the DeleteObject method of ObjectContext, the object to be deleted must be in the cache. Therefore, you'll first need to query for the customer using the custID that the client will pass in. Then you can delete the customer and its reservations. Delete the reservations first. Otherwise, when you delete the customer, its relationship to the reservations will be deleted and you won't be able to find the reservations as easily.

Add the code in Example 14.24, "Code for the DeleteCustomer method" to the DeleteCustomer method.

Example 14.24. Code for the DeleteCustomer method

Public Function DeleteCustomer(ByVal custID As Integer) As String _
 Implements IService1.DeleteCustomer
  Using context As New BAEntities
    Try
      Dim custtoDelete = (From cust In context.Contacts _
                          .OfType(Of Customer).Include("Reservations") _
                           Where cust.ContactID = custID Select cust) _
                         .FirstOrDefault
      If Not custtoDelete Is Nothing Then
        For i As Integer = custtoDelete.Reservations.Count - 1 To 0 Step -1
         Dim res = custtoDelete.Reservations.ToArray()(i)
         context.DeleteObject(res)
        Next i
        context.DeleteObject(custtoDelete)
      End If
      context.SaveChanges()
    Catch ex As Exception
      Return ex.Message
    End Try
  End Using
End Function
public string DeleteCustomer(int custID)
{
  using (BAEntities context = new BAEntities())
  {
    try
    {
      var custtoDelete = (
              from cust in context.Contacts.OfType<Customer>()
                .Include("Reservations")
              where cust.ContactID == custID
              select cust).FirstOrDefault();
      if (custtoDelete != null)
      {
       for (int i = custtoDelete.Reservations.Count - 1; i >= 0; i--)
       {
        var res = custtoDelete.Reservations.ToArray()[i];
        context.DeleteObject(res);
       }
        context.DeleteObject(custtoDelete);
      }
      context.SaveChanges();
      return "Success";
    }
    catch (Exception ex)
    {
      return ex.Message;
    }
  }
}

What Exactly Is Being Deleted When You Delete Inherited Objects?

Although customers are in a separate database table than contacts, because they derive from contacts in the model, when the Entity Framework sees an instruction to delete a Customer it will delete the Contact record as well, even though this doesn't make sense in the database schema or even in the business logic-it would be handy to remove a customer but leave the contact information intact. If you did want to perform this action, your best bet would be to use a stored procedure. This would be represented as a function in the model and can be called with EntityClient, as you saw in Chapter 13, Working with Stored Procedures When Function Mapping Won't Do.

Updating the ObjectGraph

The last method to fill out is the UpdateCustomer method. Updating just the Customer entity is simple. You did this in the web service in the previous sample. But this is not a single object; it is a graph. Not only will you need to update the customer, but you will also need to deal with its reservations. The reservations might be modified, new, or even deleted. So, although you are updating the Customer overall, you have a lot more logic to consider in this method.

Client rules for identifying changes in an EntityCollection

The states for the reservations that need to be dealt with are newly added reservations, preexisting reservations that have been modified, or reservations that need to be deleted.

Remember that an object's EntityState is stored in ObjectStateEntries. The Reservation objects will have no idea about their state, which means the service will need to determine the state of the Reservations based on a number of assumptions. These assumptions will require that the consuming client follow some rules to ensure that the service will come up with the correct conclusions about the state of each Reservation.

New Reservations do not need to be too challenging, as you can identify them by the fact that their ReservationID has not been created yet and therefore is equal to 0. As long as the client does not populate the ID or does not remove the ID value from preexisting reservations, this assumption will work.

Reservations with a ReservationID value that is greater than 0 should be those that preexisted. These will be either modified or unchanged.

Note
The service won't need to do anything with unchanged reservations, so the client could remove these before returning the graph to the service, thereby reducing the amount of data sent over the wire.

Another rule the client needs to follow is that it must not delete an object if it wants it to be deleted from the database. If the object is deleted on the client side, it will not be returned to the service and will therefore be ignored. In this service, we will attack the deleted-object problem by requiring that the client sends back a list of the IDs of objects that should be deleted. That is the purpose of the ReservationstoDelete property of the CusttoEdit class you defined in the service interface.

The UpdateCustomer method

This code for the UpdateCustomer method begins similarly to the Update method in the web service. It first extracts the Customer object from the incoming type into the customer variable. Then it performs a query to get a current version of that Customer from the database, along with the Customer's Reservations.

Finally it uses ApplyPropertyChanges to update that Customer entity with the values of the Customer that came from the client.

Enter the code in Example 14.25, "Code for the UpdateCustomer method, with placeholders" into the UpdateCustomer method.

Example 14.25. Code for the UpdateCustomer method, with placeholders

Public Function UpdateCustomer(ByVal cust As CusttoEdit) As String _
 Implements IService1.UpdateCustomer
  Try
    Dim customer = custEdit.Customer
    Using context As New BreakAwayEntities
      Dim custtoUpdate = (From c _
                          In context.Contacts.OfType(Of Customer) _
                                             .Include("Reservations") _
                          Where c.ContactID = customer.ContactID) _
                         .FirstOrDefault
      If Not custtoUpdate Is Nothing Then
        context.ApplyPropertyChanges("Contacts", customer)

        '[Code for Existing Reservations will go here]
        '[Code for New Reservations will go here]
        '[Code for Deleted Reservations will go here]

         Context.SaveChanges()
      End If
    End Using
  Catch ex As Exception
    Return "error: " & ex.Message
  End Try
End Function
public string UpdateCustomer(CusttoEdit cust)
{
  try
  {
    var customer = cust.Customer;
    using (BAEntities context = new BAEntities())
    {
      var custtoUpdate = (
              from c in context.Contacts.OfType<Customer>()
                                        .Include("Reservations")
              where  c.ContactID == customer.ContactID
              select c)
              .FirstOrDefault();

      if (custtoUpdate != null)
      {
        context.ApplyPropertyChanges("Contacts", customer);

        //Code for Existing Reservations will go here;
        //Code for new Reservations() will go here;
        //Code for Deleted Reservations will go here;

        context.SaveChanges();
      }
      return "Success";
    }
  }
  catch (Exception ex)
  {
    return "Error: " + ex.Message;
  }
}

Why call ApplyPropertyChanges if there were no changes?

If the client has returned unchanged objects, after calling ApplyPropertyChanges with these, it will be clear to the SaveChanges method that there is no reason to construct update commands. So, although the ApplyPropertyChanges call might seem unnecessary for those objects, SaveChanges will ignore it and no wasted calls will be made to the database.

Handling existing reservations

The first placeholder is for updating existing reservations. Remember that ApplyPropertyChanges only affects the scalar values. You will need to update any related data separately. Using the same logic as you used earlier, call ApplyPropertyChanges for each Reservation. The bulk of the code here is for handling the possibility that another user or process has deleted the reservation from the database.

Replace the Existing Reservations placeholder with the code in Example 14.26, "Existing Reservations logic for the UpdateCustomer method".

Example 14.26. Existing Reservations logic for the UpdateCustomer method

For Each res In customer.Reservations _
                    .Where(Function(r) r.ReservationID > 0)
  Try
    context.ApplyPropertyChanges("Reservations", res)
  Catch ex As InvalidOperationException
    If ex.Message.Contains("does not contain an ObjectStateEntry") Then
      'Reservation no longer exists in database
      'ignore or insert business logic
    End If
   End Try
Next
foreach (var res in customer.Reservations
                            .Where((r) => r.ReservationID > 0))
{
  try
  {
    context.ApplyPropertyChanges("Reservations", res);
  }
  catch (InvalidOperationException ex)
  {
    if (ex.Message.Contains("does not contain an ObjectStateEntry"))
    {
      //Reservation no longer exists in database
      //ignore or insert business logic
    }
  }
}

The preceding code iterates through all reservations coming in from the client that have an existing ID, and then uses ApplyPropertyChanges to update the reservation's matching entity in the context. Recall that the query at the beginning of the method eager-loaded reservations using Include; therefore, the customer's reservations will be in the context. If a user is editing a customer, and another user or process deleted the reservation since the first user called the GetCustomer operation, ApplyPropertyChanges will throw an exception. Unfortunately, it doesn't throw a specific exception, just an InvalidOperationException with no inner exception. Therefore, you must have a way to determine the cause of the problem. The exception's message will read as follows:

"The ObjectStateManager does not contain an ObjectStateEntry with a
reference to an object of type 'BAGA.BreakAwayModel.Reservation'."

The preceding code tests for part of this string, and in this scenario it chooses to just ignore the reservation since someone else has deleted it.

Dealing with new reservations

The code for handling new reservations is next. Replace the New Reservations placeholder with the code in Example 14.27, "New Reservations logic for the UpdateCustomer method".

Example 14.27. New Reservations logic for the UpdateCustomer method

Dim newres = customer.Reservations _
                     .Where(Function(r) r.ReservationID = 0)
For resi = 0 To newres.Count - 1
  Dim res = newres(resi)
  Dim tripEntityKey = res.Trip.EntityKey
  res.Trip = Nothing
  custtoUpdate.Reservations.Add(res)
  res.TripReference.EntityKey = tripEntityKey
Next
var newres = customer.Reservations
                     .Where((r) => r.ReservationID == 0)
                     .ToArray();
for (var resi = 0; resi < newres.Count(); resi++)
{
  var res = newres[resi];
  var tripEntityKey = res.Trip.EntityKey;
  res.Trip = null;
  custtoUpdate.Reservations.Add(res);
  res.TripReference.EntityKey = tripEntityKey;
}

This code begins by using a For Next block rather than a For Each block. This is not random. Within the block, you will be taking the new reservation from the incoming Customer and attaching it to the Customer in the context. As a result, it will no longer be part of the incoming Customer's Reservations collection and the enumeration will break.

Note
The pattern you see in this code is similar to the pattern in the RemoveTripsfromGraph method, except that you are adding the reservation to the Customer entity before reapplying the TripReference.EntityKey value.

Deleting reservations

The last piece of the UpdateCustomer method deals with reservations the user deleted. The client application must send a list of ReservationIDs that need to be deleted. The list is contained in the ReservationstoDelete property of the CusttoEdit type.

Replace the Delete Reservations placeholder with the code in Example 14.28, "Deleted Reservations logic for the UpdateCustomer method".

Example 14.28. Deleted Reservations logic for the UpdateCustomer method

For resi = 0 To cust.ReservationsToDelete.Count - 1
  Dim resid = cust.ReservationsToDelete(0)
  Dim oldReservation As New Object
  Dim resEKey = New EntityKey("BAEntities.Reservations", _
                              "ReservationID", resid)
  If context.TryGetObjectByKey(resEKey, oldReservation) Then
    context.DeleteObject(CType(oldReservation, Reservation))
  End If
Next
for (var resi = 0; resi < cust.ReservationsToDelete.Count; resi++)
{
  var resid = cust.ReservationsToDelete[0];
  object oldReservation = new object();
  var resEKey = new EntityKey("BAEntities.Reservations",
                             "ReservationID", resid);
  if (context.TryGetObjectByKey(resEKey, out oldReservation))
    context.DeleteObject((Reservation)oldReservation);
}

In this chunk of code you will use the IDs the client has passed up to locate the Reservation in the context. TryGetObjectByKey is a twist on the GetObjectByKey method, which you learned about in Chapter 9, Working with Object Services, but this version emulates the TryParse methods of the .NET Framework and prevents an exception from being thrown if the object is not found.

Note
Visual Basic's Option Strict On forces you to declare oldReservation as an object and not as a reservation. The reason is that when you pass oldReservation to TryGetObjectByKey, the method is unable to perform a narrowing conversion from the expected Object type to a Reservation type. This doesn't make much sense, but it's what you have to work with. You'll see that oldReservation is cast back to a Reservation type when it's passed into DeleteObject.

Once the Reservation is located in the cache, you can use the DeleteObject method to ensure that it will be deleted from the database when SaveChanges is called.

Wrapping up the UpdateCustomer method with SaveChanges

After all of these procedures have been performed on the context, it's time to call SaveChanges and send the required commands to the database.

Once you've done that, we'll build the client so that you can test all of the functionality of this WCF service.

Building the Client to Consume the WCF Service

Rather than starting from scratch, in this part of the walkthrough you can either modify the Windows client you built for the ASMX Web Service, or create a new Windows Forms Application project and copy the form from the previous client into the new project. The walkthrough assumes you have already performed one of these steps.

Add two buttons, one for a listbox and the other for a combo box, to the form. Figure 14.10, "The modified client form" shows the new form. Everything above the dotted line should already exist on the form from the earlier sample. Controls below the line are new for this example.

Figure 14.10. The modified client form

The modified client form

As you write the client code, you will encounter a handful of instances in which not having the business logic of the classes available on the client side forces you to write a bit of extra code. This will mostly be for the sake of setting default values. In this walkthrough, because the focus is on learning about what to expect when interacting with a WCF service, it's not worth the effort of creating a business layer. The WCF solution in Chapter 22, Implementing a Smarter WCF Service for Working with Entities will demonstrate how to provide some trimmed-down business classes for the WCF client to use. This will reduce (though not eliminate) the number of rules that the developer of consuming apps needs to be aware of.

First you will add to the project a service reference for the new WCF service:

  1. Right-click the new project in the Solution Explorer and select Add Service Reference from the context menu. This will open the Service Reference Wizard.

  2. Click Discover to display all of the services in the solution.

  3. Select Service1.svc and change its namespace to BAWCFService.

  4. Click the Advanced button to open the Service Reference Settings page.

  5. Under DataType, change the collection type to System.Collections.Generic.List.

    This will ensure that the collections returned by the service arrive in the client as lists rather than the default array.

  6. Click OK to close the settings.

  7. Click OK to close the wizard and add the service reference.

Now you can bind the CustomerBindingSource to the new Customer class:

  1. Click the CustomerBindingSource at the bottom of the Form Design window.

  2. In the Properties window for the BindingSource, select the DataSource property and then click the drop-down arrow.

  3. Expand the data sources until you see BAWCFService.Customer, as shown in Figure 14.11, "Wiring up the WCF service's Customer object", and then select it.

  4. Open the code window for the form.

  5. Add an Imports/Using statement for the WCF service at the top of the form's code file (Example 14.29, "Importing the namespace for the service into the form"). It must be prefaced by the namespace of the client application. Note that the client project's assembly name is required and here you are seeing the name of my example projects-Chapter14WCFClientVB and Chapter14WCFClientCS.

    Example 14.29. Importing the namespace for the service into the form

    Imports Chapter14WCFClientVB.BAWCFService
    
    using Chapter14WCFClientCS.BAWCFService;
    
  6. Add a declaration for the _resDeleteList variable to the form, as shown in Example 14.30, "Declaring the variable to contain the list of deleted reservations". This will be used to contain the list of ReservationIDs that need to be deleted.

    Example 14.30. Declaring the variable to contain the list of deleted reservations

    Dim resDeleteList As List(Of Integer)
    
    List<int> resDeleteList;
    

Figure 14.11. Wiring up the WCF service's Customer object

Wiring up the WCF service's Customer object

At this point, you're ready to update the methods that call the proxy. Essentially, you will be retrofitting the existing methods to work with the WCF service, rather than the ASMX web service of the previous example:

  1. Modify the Imports/using statement that points to BAWebService so that it points to BAWCFService.

  2. Replace the GetCustomers method with the GetCustsandTrips method shown in Example 14.31, "The new GetCustsandTrips method".

    Example 14.31. The new GetCustsandTrips method

    Private Function GetCustsandTrips() As CustsandTrips
      Using proxy As New Service1Client()
        Return proxy.GetCustsandTrips()
      End Using
    End Function
    
    Private CustsandTrips GetCustsandTrips()
    {
      using (Service1Client proxy =  new Service1Client())
      {
        return proxy.GetCustsandTrips();
      }
    }
    
  3. Update the GetCustomer method to instantiate the new WCF proxy class, Service1Client, instead of the proxy class from the previous service, Service1, as shown in Example 14.32, "The modified GetCustomer method".

    Example 14.32. The modified GetCustomer method

    Private Function GetCustomer(ByVal id As Integer) As Customer
      Using proxy As New Service1Client()
        Return proxy.GetCustomer(id)
      End Using
    End Function
    
    private Customer GetCustomer(int id)
    {
      using (Service1Client proxy = new Service1Client())
      {
        return proxy.GetCustomer(id);
      }
    }
    
  4. The UpdateCustomer method now takes a CusttoEdit object as a parameter. Replace the call to UpdateCustomer with the code in Example 14.33, "The modified UpdateCustomer method", which will construct the object and then pass it to the service operation.

    Example 14.33. The modified UpdateCustomer method

    Private Function UpdateCustomer() As String
      Dim status As String = Nothing
      Dim custedit = New CusttoEdit()
      custedit.Customer = _currentCustomer
      custedit.ReservationsToDelete = _resDeleteList
      Using proxy As New Service1Client()
        status = proxy.UpdateCustomer(custedit)
      End Using
      Return status
    End Function
    
    private string UpdateCustomer()
    {
      string status = null;
    
      var custedit = new CusttoEdit();
      custedit.Customer = _currentCustomer;
      custedit.ReservationsToDelete = _resDeleteList;
    
      using (Service1Client proxy = new Service1Client())
      {
        status = proxy.UpdateCustomer(custedit);
      }
      return status;
    }
    
  5. Delete the InsertCustomer method as it is no longer necessary.

  6. Replace the FillCombo with the code in Example 14.34, "The modified FillCombo method". The new combo box is named cboTrips and is populated with the list of trips that the service inside the CustsandTrips object returned.

    Example 14.34. The modified FillCombo method

    Private Sub FillCombo()
      Dim custtrips = GetCustsandTrips()
      With ComboBox1
        .ValueMember = "ContactID"
        .DisplayMember = "Name"
        .DataSource = custtrips.Custs
      End With
      With cboTrips
        .ValueMember = "ID"
        .DisplayMember = "TripDetails"
        .DataSource = custtrips.Trips
      End With
    End Sub
    
    private void FillCombo()
    {
      var custtrips = GetCustsandTrips();
    
      comboBox1.ValueMember = "ContactID";
      comboBox1.DisplayMember = "Name";
      comboBox1.DataSource = custtrips.Custs;
    
      cboTrips.ValueMember = "ID";
      cboTrips.DisplayMember = "TripDetails";
      cboTrips.DataSource = custtrips.Trips;
    }
    

Editing the configuration for the web service client

There's an important task that will be much more memorable if you see the impact of not performing it:

  1. Press the F5 key to run the Windows Forms application. You will quickly hit a Communication Exception telling you that you have exceeded the maximum message size quote for incoming messages. This is simple, but important, to fix.

    When you created the reference to the WCF service, Visual Studio added some configuration information to the app.config file for the Windows application. These configuration settings are specific to communicating with the web service. You could edit the particular setting manually, or use the GUI tool for editing service configurations. Here is how to do the latter.

  2. Right-click the app.config file for the Windows application in the Solution Explorer.

  3. Look for Edit WCF Configuration on the context menu and select it.

    Note
    If this option is not on the menu, you'll need to take a different route. Once you have used this editor, which is called the WCF Service Configuration Editor GUI, it will be on the context menu. From the Visual Studio menu, select Tools→WCF Service Configuration Editor. From the editor's File menu, choose Open; then browse to the app.config file in the filesystem and open it for editing.
  1. Expand the Bindings section to expose the single binding that was automatically created when the WCF service was referenced.

  2. Click the binding.

  3. In the binding's Properties window, locate the MaxReceivedMessageSize property and increase it by adding a 0 to the end of the default value, as shown in Figure 14.12, "Increasing the value of the MaxReceivedMessageSize property in the client's app.config file".

  4. Save the file and close the window.

Figure 14.12. Increasing the value of the MaxReceivedMessageSize property in the client's app.config file

Increasing the value of the MaxReceivedMessageSize property in the client's app.config file

Testing the form again

With the message size problem fixed, you should be able to see how the form is interacting with the service at this point if you'd like. The Customer and Trips combo boxes will be filled and you can select customers to see the text boxes get updated.

Adding functionality to view the reservations

In the new form, when a Customer is selected you will be displaying those reservations in the ListBox. We'll use a separate method for wiring up the ListBox and then call it from the SelectedIndexChanged method.

Add the ShowReservations method in Example 14.35, "The new ShowReservations method" to the form's code.

Example 14.35. The new ShowReservations method

Private Sub ShowReservations()
  ListBox1.DataSource = Nothing
  If _currentCustomer.Reservations.Count > 0 Then
    With ListBox1
      .ValueMember = "ReservationID"
      .DisplayMember = "TripDetails"
      .DataSource = _currentCustomer.Reservations
    End With
  End If
End Sub
private void ShowReservations()
{
  listBox1.DataSource = null;
  if (_currentCustomer.Reservations.Count > 0)
  {
    listBox1.ValueMember = "ReservationID";
    listBox1.DisplayMember = "TripDetails";
    listBox1.DataSource = _currentCustomer.Reservations;
  }
}
Note
DisplayMember depends on the TripDetails property of Reservation, which will be available if you marked the property as a DataMember, as recommended earlier. If you need to do that now, you will need to rebuild the model project, then rebuild the service, and then update the service reference. Update Service Reference is a context menu option when you right-click the service reference you already created. After this, the TripDetails property should be available.

Now, call ShowReservations from the SelectedIndexChanged event of the Customers ComboBox. You'll also need to instantiate the resDeleteList variable in case the user deletes any of the customer's reservations. The method should look like Example 14.36, "The modified ComboBox1_SelectedIndexChanged method" after adding the two lines shown in bold.

Example 14.36. The modified ComboBox1_SelectedIndexChanged method

Private Sub ComboBox1_SelectedIndexChanged _
 (ByVal sender As Object, ByVal e As System.EventArgs) _
 Handles ComboBox1.SelectedIndexChanged
  If ComboBox1.SelectedIndex > -1 Then
    _currentCustomer = _
       GetCustomer(CType(ComboBox1.SelectedValue, Integer))
    CustomerBindingSource.DataSource = _currentCustomer
   'create list of customer's reservations and fill ListBox
    _resDeleteList = New List(Of Integer)
    ShowReservations()
  End If
End Sub
private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
{
  if (comboBox1.SelectedIndex > -1)
    _currentCustomer = GetCustomer((Int32)comboBox1.SelectedValue);
    customerBindingSource.DataSource = _currentCustomer;
    _resDeleteList=new List<int>();
    ShowReservations();
  }
}

resDeleteList needs to be reinstantiated for each customer so that you don't carry over IDs of deleted reservations from a previously selected customer.

Testing the form so far

You are at another point where you can see in action what you've done so far. This time you'll see the currently selected customer's existing reservations populated in the listbox. The form should look something like Figure 14.13, "The form displaying the selected customer's reservations".

Figure 14.13. The form displaying the selected customer's reservations

The form displaying the selected customer's reservations

Finishing the form

Only a few tasks are left for the form's code. First, you need to make small changes to the Add Customer and Save Customer event handlers and add the code for adding and deleting reservations. Let's start with the Add Customer event handler, which needs to accommodate the fact that Reservations is no longer an EntityCollection.

When the Entity Framework is available, the Reservations property of Customer is an EntityCollection. But there is no EntityCollection in the client application. Instead, the Reservations property is a list of reservations, and if no reservations came across the wire with the customer, that list will need to be instantiated if you want to work with it. You'll need to do this in the method called when the user clicks the Add Customer button:

  1. Modify the Click event method for the Add Customer button by adding a line of code to instantiate the Reservations property. This new code goes in between the two existing code lines, as shown in Example 14.37, "Modifying the code for adding a new customer" in bold.

    Note
    Because this solution is not sharing business logic with the consumer, the consumer needs to follow another rule. The TimeStamp fields for new customers and new reservations must be set to a default or WCF will not serialize them. You'll see this in the NewCust_Click method, and a bit later in the code for creating a new reservation.

    Example 14.37. Modifying the code for adding a new customer

    Private Sub NewCust_Click( _
     ByVal sender As System.Object, ByVal e As System.EventArgs) _
     Handles NewCust.Click
    
      _currentCustomer = New Customer()
     'explicitly instantiate a list for the Reservations property
         _currentCustomer.Reservations = New List(Of Reservation)()
      customerBindingSource.DataSource = _currentCustomer
     'clear the reservations listbox and the customer selection
      listBox1.DataSource = Nothing
      comboBox1.SelectedIndex = -1
      Dim newTimeStamp = System.Text.Encoding.Default.GetBytes("0x123")
      _currentCustomer.TimeStamp = newTimeStamp
      _currentCustomer.CustTimeStamp = newTimeStamp
    End Sub
    
    private void NewCust_Click(object sender, EventArgs e)
    {
      _currentCustomer = new Customer();
      //explicitly instantiate a list for the Reservations property
         _currentCustomer.Reservations = new List<Reservation>();
      customerBindingSource.DataSource = _currentCustomer;
      //clear the reservations listbox and the customer selection
      listBox1.DataSource = null;
      comboBox1.SelectedIndex = -1;
      var newTimeStamp=System.Text.Encoding.Default.GetBytes("0x123");
      _currentCustomer.TimeStamp = newTimeStamp;
      _currentCustomer.CustTimeStamp = newTimeStamp;
    }
    
  2. Modify the Click event for the Save Customer button, which will call the UpdateCustomer method (see Example 14.38, "Modifying the code for saving the customer record"). If an error is returned to the status variable, you can display it with the MessageBox.

    Example 14.38. Modifying the code for saving the customer record

    Private Sub SaveCust_Click( _
     ByVal sender As System.Object, ByVal e As System.EventArgs) _
     Handles SaveCust.Click
      Dim status = UpdateCustomer()
     'insert customer returns the id of the new customer
      If Not String.IsNullOrEmpty(status) Then
        MessageBox.Show("Update: " & status)
      End If
    
    End Sub
    
    private void SaveCust_Click(object sender, EventArgs e)
    {
      var status = UpdateCustomer();
      //insert customer returns the id of the new customer
      if (string.IsNullOrEmpty(status)==false)
          MessageBox.Show("Update: " + status);
    }
    

Adding and deleting the reservations

Adding reservations is similar in functionality to what you built in the WPF form where you added activities to a scheduled trip. The user will select a trip from the cboTrips control and then click the new Add button to create a reservation using the selected trip. Then the reservation will be the current customer's list of reservations. The other new button will be used to delete a reservation that is highlighted in the Reservations listbox:

  1. Rename the new buttons so that one is for adding a reservation and the other is for deleting a reservation.

  2. Add the code in Example 14.39, "Adding reservations" to the Click event handler for the Add button.

    In addition to setting the Trip property of the new Reservation, you'll need to set a few defaults such as ReservationDate and TimeStamp. Reservation.TripDetail does not know how to automatically formulate itself from Trip.TripDetails; therefore, you need to explicitly set the value for the benefit of the listbox.

    Example 14.39. Adding reservations

    Private Sub AddRes_Click( _
     ByVal sender As System.Object, ByVal e As System.EventArgs) _
     Handles AddRes.Click
      Dim selTrip = CType(cboTrips.SelectedItem, Trip)
      Dim newRes As New Reservation()
      newRes.Trip = selTrip
      newRes.TripDetails = selTrip.TripDetails
      newRes.ReservationDate = System.DateTime.Now
      newRes.TimeStamp = System.Text.Encoding.Default.GetBytes("0x123")
      _currentCustomer.Reservations.Add(newRes)
      ShowReservations()
    End Sub
    
    private void AddRes_Click(object sender, EventArgs e)
    {
      var selTrip = (Trip)cboTrips.SelectedItem;
    
      Reservation newRes = new Reservation();
      newRes.Trip = selTrip;
      newRes.TripDetails = selTrip.TripDetails;
      newRes.ReservationDate = System.DateTime.Now;
      newRes.TimeStamp = System.Text.Encoding.Default.GetBytes("0x123");
      _currentCustomer.Reservations.Add(newRes);
      ShowReservations();
    }
    

    Note that you have bound the reservation to the customer by adding it to the Customer.Reservations collection and not by setting Customer to the Reservation.Customer property. This is intentional. Without the ObjectContext, you don't get the automatic two-way relationship. Because you set the Reservation.Customer property, the reservation will still not be seen as part of the customer's collection of reservations. Therefore, since none of the code in this Windows form relies on navigating from Reservations back to Customer, but you do need to go from Customer to its Reservations, you join the two by adding the Reservation to the Customer's EntityCollection.

    The final bit of code for modifying Reservations enables users to delete a reservation. It's important to remember here that you can't just delete the reservation without providing the service with some clue that it needs to delete the reservation from the database. This is where you will keep track of the IDs of any deleted reservations for a customer.

  3. Add the code in Example 14.40, "Deleting reservations" to the Click event of the Delete Reservation button.

    Example 14.40. Deleting reservations

    Private Sub DelRes_Click( _
     ByVal sender As System.Object, ByVal e As System.EventArgs) _
     Handles DelRes.Click
      If ListBox1.SelectedIndex >= 0 Then
        _resDeleteList.Add(CInt(Fix(ListBox1.SelectedValue)))
       'find the reservation in the customer's Reservations & remove it
        Dim delRes = _currentCustomer.Reservations. _
         Where(Function(r) r.ReservationID = CInt(ListBox1.SelectedValue))
        _currentCustomer.Reservations.Remove(delRes.First)
        ShowReservations()
      End If
    End Sub
    
    private void DelRes_Click(object sender, EventArgs e)
    {
      if (listBox1.SelectedIndex >= 0)
      {
        _resDeleteList.Add((int)listBox1.SelectedValue);
    
        //find the reservation in the customer's Reservations & remove it
        var delRes = _currentCustomer.Reservations.
         Where(r => r.ReservationID == (int)listBox1.SelectedValue);
            _currentCustomer.Reservations.Remove(delRes.FirstOrDefault());
        ShowReservations();
      }
    }
    

    After you add the reservation ID to _resDeleteList, the code removes the reservation from the Customer's Reservations and refreshes the listbox.

Why No EntityKey and EntityReference in the Client?

Because the client is depending on the service to provide the classes and does not reference System.Data.Entity, it is not possible to create EntityReferences using EntityKeys as you have done in previous samples. The Trip class does not have an EntityKey property because in this client, it does not inherit System.Data.EntityKey. So, there is no way to use the EntityKey for the TripReference.EntityKey value as you have done before. It is possible, however, to build complex types, as you can see in the method where newRes.Customer uses a Customer object as its value.

This goes back to the logic in the web service where you had to find a way to remove the Trip entity from the Reservation, but redefine the relationship with an EntityKey. The service knows that a client will not be able to create an EntityKey, and therefore is able to assume that any relationships are in the form of actual objects.

Running the application and saving your changes

Now you can truly run the client application and test things out.

There is a big difference between the Windows Forms application you wrote in Chapter 8, Data Binding with Windows Forms and WPF Applications and this one. In Chapter 8, Data Binding with Windows Forms and WPF Applications, you were able to rely on an ObjectContext on the client to keep track of all of your changes. This made it possible to perform data entry on multiple customers before calling SaveChanges. In this application, however, you don't have that advantage. When you select a new customer, the current one and any changes you made to that customer will disappear. It's important to save before moving to a new customer. In a production application, you would have checks in place to ensure that the user doesn't inadvertently lose his changes.

You can make plenty of additional tweaks to the Windows form to make it more user-friendly, but you've come to the end of the lessons that are related to consuming entities from a WCF service, so we'll move on.

Peeking under the covers

Don't forget to set breakpoints and watch what is happening throughout the client code and the service code. The most interesting thing that happens under the covers is what you'll see when you check the SQL Profiler during the call to SaveChanges.

This is where you can again witness the order of command events when you are dealing with inherited objects. When you add a customer, first a contact is added and then the customer using the newly generated contactID is added. If you created any reservations for the new customer, those are added last. You can see this order in the SQL Profiler screenshot shown in Figure 14.14, "Watching the insert in SQL Profiler".

Figure 14.14. Watching the insert in SQL Profiler

Watching the insert in SQL Profiler

In this chapter, you learned some of the patterns for using the Entity Framework in a service tier, and built an ASMX Web Service and a WCF service using tools in Visual Studio 2008. When objects are moved across tiers to services, they are serialized, which adds a few challenges to the solution.

XML serialization used for web services does not serialize a full graph, whereas the serializer for WCF is able to do so. Therefore, the web service used explicit operations for inserts, updates, and deletes and dealt with saving only a single entity at a time, whereas the web service, in saving a graph, had to be prepared for a variety of EntityStates in the parent and children of the graph.

The most daunting of the challenges when working across tiers is that although EntityObjects are serialized, the ObjectStateEntry objects that contain the change tracking information are not. This leaves you with no state information when your object reaches its destination. In the examples in this chapter, you solved this problem by retrieving values from the server prior to calling SaveChanges. This is one pattern for overcoming this problem, and you will learn more in later chapters.

Web services and WCF are big topics unto themselves, and wonderful books are devoted solely to these technologies. The samples in this chapter provide some patterns that will be great for many scenarios, but not all. Later in this book you will learn more patterns, but more importantly, throughout the book you will gain the knowledge to achieve whatever architecture you choose for your service-based applications.

Show:
© 2014 Microsoft