Chapter 22. Implementing a Smarter WCF Service for Working with Entities

In a world where services are increasingly being used to enable distributed computing, no one can take services lightly when approaching the Entity Framework. As with web applications, using services to provide read-only information to client applications is pretty straightforward. However, when you want those clients to be able to update data through your services, the Entity Framework’s change tracking breaks down as you try to move data across tiers, and it creates big problems for performing updates.

In Chapter 14, you wrote your first services using the Entity Framework. The ASMX Web Service demonstrated a simple solution whereby single entities were sent to explicit insert, update, and delete operations. No graphs were involved in the messages being sent to the service. The WCF service in the second half of that chapter went a step further and worked with graphs. This introduced the challenge of determining the state of each child in the graph that came back to the service. You may recall using assumptions such as “if an entity’s ID is 0, it must be new.” You also forced the client to send the deleted entity IDs in a separate object within a data contract. This worked, and it leveraged your existing knowledge of the Entity Framework at that point in the book. But the service was complicated, it forced the client to do a lot of extra work, and in some programming scenarios those assumptions just won’t work.

Now that you have learned so much more about the Entity Framework, you can build a smarter WCF service that reduces the intensity of the work required on the client side and takes advantage of some of the more advanced features of the Entity Framework.

Guidance for writing distributed applications—especially WCF service applications—with the Entity Framework has been one of the features that enterprise developers have requested most since the early betas. The pattern and ideas laid forth in this chapter should go a long way toward helping you construct your service-oriented applications with version 1 of the Entity Framework.

Note

Although this chapter has a lot of code in it, the chapter doesn’t provide the entire solution. For that, please visit the book’s website.

Will Your Client Agree to Your Data Contract?

Although it would be very nice to provide a web service that clients can use randomly, without them having to know much more than what the operations expect and will return, the challenges of change tracking throw a wrench into this plan.

If you want to provide your own services over which you have complete control, consumers of your service will have to make some accommodations on the client side. The solution presented in this chapter requires that the consuming applications adopt a few prewritten classes and follow a small number of very specific rules.

Like with the solutions in Chapter 14, the clients can still be “Entity Framework-agnostic”; in other words, they do not need to reference the Entity Framework APIs or your model’s assembly. In fact, they don’t even need to be .NET applications, though this would require that they rewrite your provided logic to duplicate the functionality.

Shipping DTOs, Not EntityObjects

The approach to this service is quite different from what you wrote in Chapter 14. Here are the key factors for this new service.

Data Transfer Objects (DTOs) will be used to ship the data between the service and the client. As you saw with the few DTOs you constructed in the earlier services, these are much lighter in weight.

In addition to the properties that match the EntityObjects, each DTO will contain its own EntityState property so that you don’t need to rely on the nonexistent ObjectContext to keep track of the ObjectState on the client side.

Remember the XML you saw in Chapter 14 that represented what the payload of a Customer entity object looked like? Conversely, the ShortCust object that you also created in that chapter for both the web and WCF services was very small. This was a DTO.

Because your service will be designed to support Entity Framework-agnostic clients, there is really no need to send all of that extra data down the wire, so for this service you’ll create DTOs for each entity that will be used: Customer, Address, Reservation, Trip, and Payment. You can store the DTOs in a separate project so that you can use the project from different services.

Note

You should give the new project a namespace that will help you differentiate entity classes from the DTO classes when they have the same name, as you will use both in the service implementation. My entity objects are in the BAGA namespace, so the namespace that I’m using for the DTO project is BAGA.DTO.

Building a Data Transfer Object to Take the Place of an Entity Object

As you saw with ShortCust, you need to mark each class as a DataContract and each property as a DataMember attribute. To be serialized, the properties must have a getter and a setter.

Example 22-1 shows part of the new Customer class with a few of its properties. The consumer does not care that the Customer inherits from a Contact; therefore, there is nothing here about the inheritance. The Customer class is independent. You can create a single Customer class with the appropriate fields. In Visual Basic, each property has a local variable associated with it and that variable has been placed directly after the property. In C#, you can take advantage of auto-implemented properties.

Example 22-1. Part of the new Customer DTO class

VB
<DataContract()> _
Partial Public Class Customer

  <DataMember()> _
  Public Property CustomerID() As Integer
    Get
      Return Me._CustomerID
    End Get
    Set(ByVal value As Integer)
      Me._CustomerID = value
    End Set
  End Property
  Private _CustomerID As Integer

  <DataMember()> _
  Public Property FirstName() As String
    Get
      Return Me._FirstName
    End Get
    Set(ByVal value As String)
      Me._FirstName = value
    End Set
  End Property
  Private _FirstName As String

End Class
C#
[DataContract()]
public partial class Customer
{
  [DataMember()]
  public int CustomerID {get ; set; }

  [DataMember()]
  public string FirstName {get ; set; }
}

Replacing EntityReference navigation properties

The navigation properties for entity references, such as the preference properties, have been replaced with one property to contain its key value and another to contain its display value. This is the information that will be useful to a consumer of your service. The VB fields set default values for their properties. If you use the auto-implemented properties in C#, you can set the defaults in the class constructor.

In Example 22-2, the PrimaryActivityName and PrimaryActivityID properties in the Customer DTO replace PrimaryActivityReference. There is no need for an Activity value object here. The name and ID are all the client application will need.

Example 22-2. Properties in the Customer DTO replacing the PrimaryActivityReference

VB
<DataMember()> _
  Public Property PrimaryActivityName() As String
    Get
      Return _PrimaryActivityName
    End Get
    Set(ByVal value As String)
      _PrimaryActivityName = value
    End Set
  End Property
  Private _PrimaryActivityName As String = ""

  <DataMember()> _
  Public Property PrimaryActivityID() As Integer
    Get
      Return _PrimaryActivityID
    End Get
    Set(ByVal value As Integer)
      _PrimaryActivityID = value
    End Set
  End Property
C#
   [DataMember()]
  public string PrimaryActivityName {get;set;}
  [DataMember()]
  public int PrimaryActivityID {get;set;}

  public Customer()
  {
    //initialize properties
    PrimaryActivityName="";
    PrimaryActivityID=0;
  }

Replacing EntityCollection navigation properties

Customer.Reservations and other child EntityCollections have been replaced with properties that return lists, as shown in the Reservations property in Example 22-3.

Example 22-3. A property to expose the Reservations child collection

VB
<DataMember()> _
  Public Property Reservations() As List(Of Reservation)
    Get
      Return _Reservations
    End Get
    Set(ByVal value As List(Of Reservation))
      If (Not (value) Is Nothing) Then
        _Reservations = value
      End If
    End Set
  End Property
  Private _Reservations
C#
[DataMember()]
public List<Reservation> Reservations
{
  get
  {
    return _Reservations;
  }
  set
  {
    if ((value) != null)
      _Reservations = value;
  }
}
  private List<Reservation> _Reservations;

Adjustments have been made to some of the classes to expose properties that will be more logical to the consumer. For example, the Reservation class has properties for ReservationID, ReservationDate, TripDetails, and CustomerID.

Creating EntityState Properties That Do Not Rely on ObjectContext

One of the key features of this new service is to provide a more dependable way to determine an entity’s state when it has been returned from the client. Previously, we used some assumptions that will work for many scenarios, but not all. One assumption was that all added entities would have an ID of 0; another was that a modified ModifiedDate property would indicate that an entity had been changed. Tracking deleted entities was more complicated and required that the client provide a list of IDs for all entities that were deleted on the client side.

Although that method for tracking deleted entities is dependable, it means a lot of extra work on the client’s behalf, as well as in the service. The method to identify added or modified entities is indeterminate, which means it is possible that an entity will have an ID of 0 even if it’s not new, or that a changed entity will have an Unchanged ModifiedDate field.

Rather than depending on these mechanisms, you can add a property directly to the classes that will be edited on the client side so that when the entities are returned, you will know exactly what their state is.

The EntityStateLocal Enums and Interface

Rather than adding the property into each entity in the model, you can create an interface and implement it in the DTO classes that will be used in the service. The interface provides a single property: EntityState_Local.

The EntityState_Local property is tied to a new enum, EntityStateLocal. For consistency, this enum provides the same members and values as the Entity Framework’s System.Data.EntityState enum.

Because this enum needs to be available to the service’s client, it is given a DataContract attribute and each enum has an EnumMember attribute. The files that contain the interface and the enum can go into the same project as the DTOs that need to access them. In addition, you can put both the interface and the enum into a single file in the model’s project.

Example 22-4 shows what the code for the interface and enums looks like.

Example 22-4. The EntityState interface and the enums on which it will depend

VB
Public Interface IEntityStateLocal
  Property EntityState_Local() As EntityStateLocal
End Interface

<DataContract()> _
Public Enum EntityStateLocal
  <EnumMember()> Modified = 16
  <EnumMember()> Detached = 1
  <EnumMember()> Unchanged = 2
  <EnumMember()> Added = 4
  <EnumMember()> Deleted = 8
End Enum
C#
public interface IEntityStateLocal
{
  EntityStateLocal EntityState_Local {get; set;}
}

[DataContract()]
public enum EntityStateLocal: int
{
  [EnumMember()]
  Modified = 16,
  [EnumMember()]
  Detached = 1,
  [EnumMember()]
  Unchanged = 2,
  [EnumMember()]
  Added = 4,
  [EnumMember()]
  Deleted = 8
}

Implementing the IEntityStateLocal Interface in the DTO Classes

The service will allow users to edit customers, reservations, and addresses. After implementing the interface in each of the necessary classes, you need to implement its members—in this case, the EntityState_Local property. The property needs to be serialized as part of the class; therefore, mark it as a DataMember. Additionally, as with the other properties, you will need to declare a local variable for the property to use.

First, modify the partial class declaration, as shown in the following code:

VB
<DataContract()> _
Partial Public Class Customer
  Implements IEntityStateLocal

  <DataMember()> _
  Public Property EntityState_Local() As EntityStateLocal _
   Implements IEntityStateLocal.EntityState_Local
    Get
      Return Me._EntityStateLocal
    End Get
    Set(ByVal value As EntityStateLocal)
      Me._EntityStateLocal = value
    End Set
  End Property
  Private _EntityStateLocal As EntityStateLocal
C#
[DataContract()]
public partial class Customer : IEntityStateLocal
{

  [DataMember()]
  public EntityStateLocal EntityState_Local
  { get ; set ;}

Note

A funny side effect of the automatic property syntax in C# is that it looks exactly the same as the interface.

You can use the same code in any of the classes that the consuming application will modify. In this example, that would be Customer, Address, and Reservation.

You’ll also need to use the IEntityStateLocal interface in the model’s entity classes that align with these DTOs.

Implementing the IEntityStateLocal Interface in the Entity Classes

The DTOs will bring the EntityState values back from the consumer, but when you convert the DTOs back into entities you need to retain those properties. To do that the entity classes need to implement the interface as well. If the interface is in its own project, you can reference it. This is preferable for building reusable code. Otherwise, you’ll minimally need to get the interface code along with the custom enums into the model’s project.

In the model assembly, implement the interface in the same three classes: Customer, Address, and Reservation. You’ll do this by adding the Implements syntax to the partial classes created to extend the code-generated classes. An important part of the implementation means also implementing the EntityState_Local property from the interface. In Example 22-5, the Address partial class shows the implementation.

Example 22-5. The Address entity class implementing IEntityStateLocal

VB
Partial Public Class Address
  Implements IEntityStateLocal
  Dim _localState As EntityStateLocal

  <DataMember()> _
  Public Property EntityState_Local() As EntityStateLocal
   Implements IEntityStateLocal.EntityState_Local
    Get
      Return _localState
    End Get
    Set(ByVal value As EntityStateLocal)
      _localState = value
    End Set
  End Property

   'other class properties
C#
public partial class Address : IEntityStateLocal
{
  [DataMember]
  public EntityStateLocal EntityState_Local { get; set; }

  //other class properties

Warning

The Customer class has a twist. Because it inherits from Contact, you must implement the interface in the Contact class, not in the Customer class.

You’ll see this pay off when it’s time to update the customer in the service.

Designing the Service Interface

This service will allow consumers to modify a customer and its related addresses and reservations, and then save that information. The service contract contains four operations:

  • GetCustomerList returns a list of customer names with their IDs. It uses the same ShortCust class that the previous service used, except that it has been renamed to ShortCustomer.

  • GetCustomer returns a customer graph that includes the customer’s addresses and reservations. The reservations include the reference objects for trip details, payments, and unpaid reservations.

  • SaveCustomer takes a customer graph and updates any customer, address, or reservation information that has been added, deleted, or modified on the client side.

  • GetTrips will provide a list of trips so that the user will be able to create new reservations for customers by selecting for which trip the reservation was made.

Example 22-6 shows the operation contracts for the WCF service.

Example 22-6. Operation contracts for the WCF service

VB
<ServiceContract()> _
Public Interface ICustomerService

  <OperationContract()> _
  Function GetCustomerList() As List(Of ShortCustomer)

  <OperationContract()> _
  Function SaveCustomer(ByVal new_Cust As BAGA.DTO.Customer) As Boolean

  <OperationContract()> _
  Function GetCustomer(ByVal CustomerID As Integer) As BAGA.DTO.Customer

  <OperationContract()> _
  Function GetTrips() As List(Of BAGA.DTO.Trip)

End Interface
C#
[ServiceContract()]
public interface ICustomerService
{
  [OperationContract()]
  List<ShortCustomer> GetCustomerList();

  [OperationContract()]
  bool SaveCustomer(BAGA.DTO.Customer new_Cust);

  [OperationContract()]
  BAGA.DTO.Customer GetCustomer(int CustomerID);

  [OperationContract()]
  List<BAGA.DTO.Trip> GetTrips();
}

Implementing the Service Operations

The CustomerService class contains the implementations of the contract operations defined in ICustomerService, as well as some helper methods. This class will take advantage of the Customer provider and the List provider and in turn the CommandExecutor class that you built for the ASP.NET application in Chapter 21. Your service can reference the project that contains these classes.

Returning CustomerList as ShortCustomers with GetCustomerList

GetCustomerList returns a list of ShortCustomer objects containing customer names and IDs, as shown in Example 22-7. The CustomerProvider class does not contain a meaningful method to return the correct data for this list. You can certainly extend that class do so, however, for this example, we’ll just code the query into the GetCustomerList method and call the CommandExecutor method directly.

Example 22-7. The GetCustomerList service operation

VB
Public Function GetCustomerList() As List(Of ShortCustomer) _
 Implements ICustomerService.GetCustomerList
  Using context As New BAEntities
    Dim custs = From c In context.Contacts.OfType(Of Customer)() _
                Order By c.LastName & c.FirstName _
                Select New ShortCustomer With _
                     {.ContactID = c.ContactID, _
                      .Name = c.LastName.Trim & ", " & c.FirstName.Trim}
    Dim dal as New DAL.CommandExecutor
    Return dal.ExecuteList(custs)
  End Using
End Function
C#
public List<ShortCustomer> GetCustomerList()
{
  using (BAEntities context = new BAEntities())
  {
    var custs = from c in context.Contacts.OfType<Customer>()
                orderby c.LastName + c.FirstName
                select new ShortCustomer {ContactID = c.ContactID,
                       Name = c.LastName.Trim() + ", " + c.FirstName.Trim()};
    var dal=new BAGA.DAL.CommandExecutor();
    return dal.ExecuteList(custs);
  }
}

Returning Additional Reference Lists

GetTrips, shown in Example 22-8, returns a reference list for selecting trips. The type returned in the List is Trip DTO. After creating the reference list of Trips entities, use the same method that you used in Chapter 21.

Example 22-8. The GetTrips service operation

VB
Public Function GetTrips() As List(Of BAGA.DTO.Trip) _
 Implements ICustomerService.GetTrips
  Using listProvider As New BAGA.Providers.ListProvider
    Dim tripsList = CType(ListProvider.GetReferenceList(Of Trip)(""), _
                        List(Of Trip))
  Dim trips = From t In tripsList _
              Select New BAGA.DTO.Trip With _
              {.TripID = t.TripID, .TripDetails = t.TripDetails}
  Return trips.ToList
  End Using
End Function
C#
public List GetTripsDTO()
{
  using (var listProvider=new BAGA.Providers.ListProvider())
  {
    List tripsList = (List)listProvider.GetReferenceList("");

    var trips = from t in tripsList
                select new BAGA.DTO.Trip
                {TripID = t.TripID, TripDetails = t.TripDetails};
    return trips.ToList();
  }
}

Returning a Single Entity Graph for Editing with GetCustomer

GetCustomer gets the appropriate customer by passing the incoming CustomerID to a new method in the CustomerProvider class, GetCustomerwithReservationAndAddresses. Building on previous lessons, this method leverages a compiled LINQ to Entities query, which eager loads the related reservations and addresses for the given customer. As you did in Chapter 20, this query is declared in the CustomerProvider class declarations and defined and compiled in a method called PreCompileCustGraph. Notice in Example 22-9 that the variable is Shared/static so that it is available throughout the lifetime of the web service and only re-compiled if it’s gone out of scope.

Example 22-9. New functionality added to the CustomerProvider class for a compiled query

VB
Public Shared _compiledCustGraphQuery _
 As Func(Of BAEntities, Integer, IQueryable(Of Customer))

Private Sub PreCompileCustGraph()
  _compiledCustGraphQuery = CompiledQuery.Compile _
    (Function(ctx As BAEntities, custID As Integer) _
  	 From c In _commonContext.Contacts.OfType(Of Customer) _
                             .Include("Addresses") _
                             .Include("Reservations.Trip.Destination") _
                             .Include("Reservations.UnpaidReservations") _
                             .Include("Reservations.Payments") _
      Where c.ContactID = custID)
End Sub

Public Function GetCustomerwithReservationsAndAddresses _
 (ByVal ContactID As Int32) As Customer
  If _compiledCustGraphQuery Is Nothing Then
	PreCompileCustGraph()
  End If
  Dim query = _compiledCustGraphQuery.Invoke(_commonContext,ContactID)
  Return _dal.ExecuteFirstorDefault(query)
End Function
C#
static Func<BAEntities, int, IQueryable<Customer>> _compiledCustGraphQuery;

private void PreCompileCustGraph()
{
  _compiledCustGraphQuery = 
        CompiledQuery.Compile((BAEntities ctx, int custID) =>
        from c in _commonContext.Contacts.OfType<Customer>()
                                .Include("Addresses")
                                .Include("Reservations.Trip.Destination")
                                .Include("Reservations.UnpaidReservations")
                                .Include("Reservations.Payments")
         where c.ContactID == custID
         select c);
}	

public Customer GetCustomerwithReservationsAndAddresses(Int32 ContactID)
{
  if (_compiledCustGraphQuery == null)
    PreCompileCustGraph();
  var query = _compiledCustGraphQuery.Invoke(_commonContext,ContactID);
  return _dal.ExecuteFirstorDefault(query);
}

The GetCustomer method, shown in Example 22-10, gets the Customer graph using the new provider method. The returned data is then transformed into a Customer DTO class by a helper method, which in turn converts the Reservations and Addresses children to DTOs.

Example 22-10. The GetCustomer service operation

VB
Public Function GetCustomerDTO(ByVal contactID As Integer) As BAGA.DTO.Customer
  Using custProvider = New BAGA.Providers.CustomerProvider
      Dim cust = custProvider.GetCustomerwithRelatedData(contactID)
      Dim custDTO = CustEF_to_CustDTO(cust)
      custDTO.EntityState_Local = BAGA.DTO.EntityStateLocal.Unchanged
      InitializeChildren(custDTO.Reservations)
      InitializeChildren(custDTO.Addresses)
      Return custDTO
  End Using
End Function
C#
public BAGA.DTO.Customer GetCustomerDTO(int contactID)
{
  using (var custProvider=new BAGA.Providers.CustomerProvider())
  {
    var cust = custProvider.GetCustomerwithRelatedData(contactID);
    var custDTO = CustEF_to_CustDTO(cust);
    custDTO.EntityState_Local = BAGA.DTO.EntityStateLocal.Unchanged;
    InitializeChildren(custDTO.Reservations);
    InitializeChildren(custDTO.Addresses);
    return custDTO;
  }
}

The GetCustomer method calls two other methods in the Service class: CustEF_to_CustDTO and InitializeChildren. Following are the descriptions and code listings for both methods.

Building Methods to Create DTOs from Entity Objects

The set of helper methods shown in Example 22-11 handles transferring the entity objects into the DTOs that the service will return. The methods are abbreviated and do not show all of the properties being set.

Example 22-11. Methods to push entity objects into DTOs

VB
Private Function CustEF_to_CustDTO(ByVal cust As Customer) As BAGA.DTO.Customer
 'select customer scalars into new cust
  Dim custDTO = New BAGA.DTO.Customer With { _
                .CustomerID = cust.ContactID, _
                .TimeStamp=cust.TimeStamp, _
                .FirstName = cust.FirstName, _
                .LastName = cust.LastName, _
                .Notes = cust.Notes}
 'careful with possible null reference values
  If Not cust.CustomerType Is Nothing Then
    custDTO.CustomerTypeID = cust.CustomerType.CustomerTypeID
    custDTO.CustomerTypeName = cust.CustomerType.CustomerTypeName
  End If
  If Not cust.PrimaryActivity Is Nothing Then
    custDTO.PrimaryActivityID = cust.PrimaryActivity.ActivityID
    custDTO.PrimaryActivityName = cust.PrimaryActivity.ActivityName
  End If

 'child collections
  custDTO.Reservations = New List(Of BAGA.DTO.Reservation)
  For Each res In cust.Reservations
    custDTO.Reservations.Add(ReservationEF_toReservationDTO(res))
  Next

  custDTO.Addresses = New List(Of BAGA.DTO.Address)
  For Each add In cust.Addresses
    custDTO.Addresses.Add(AddressEF_toAddressDTO(add))
  Next

  Return custDTO
End Function

Private Function ReservationEF_toReservationDTO(ByVal res As Reservation) _
  As BAGA.DTO.Reservation
  Dim resDTO = New BAGA.DTO.Reservation With { _
               .ReservationID = res.ReservationID, _
               .CustomerID = res.Customer.ContactID, _
               .ReservationDate = res.ReservationDate, _
               .TripDetails = res.TripDetails}

  resDTO.Payments = New List(Of BAGA.DTO.Payment)
  For Each Payment In res.Payments
    resDTO.Payments.Add(PaymentEF_toPaymentDTO(Payment))
  Next

  Return resDTO
End Function

Private Function AddressEF_toAddressDTO(ByVal add As Address) As BAGA.DTO.Address
  Dim addDTO = New BAGA.DTO.Address With { _
               .addressID = add.addressID, _
               .AddressType = add.AddressType, _
               .Street1 = add.Street1, _
               .City = add.City}
  Return adddto
End Function

Private Function PaymentEF_toPaymentDTO(ByVal pmt As Payment) As BAGA.DTO.Payment
  Dim pmtDTO = New BAGA.DTO.Payment With { _
               .Amount = pmt.Amount, _
               .PaymentDate = pmt.PaymentDate}
  Return pmtDTO
End Function
C#
private BAGA.DTO.Customer CustEF_to_CustDTO(Customer cust)
{
  //select customer scalars into new cust

  var custDTO = new BAGA.DTO.Customer
  { FirstName = cust.FirstName,
    LastName = cust.LastName,
    Notes = cust.Notes};

  //careful with possible null reference values
  if (cust.CustomerType != null)
  {
    custDTO.CustomerTypeID = cust.CustomerType.CustomerTypeID;
    custDTO.CustomerTypeName = cust.CustomerType.CustomerTypeName;
  }
  if (cust.PrimaryActivity != null)
  {
    custDTO.PrimaryActivityID = cust.PrimaryActivity.ActivityID;
    custDTO.PrimaryActivityName = cust.PrimaryActivity.ActivityName;
  }

  //transfer child collections
  custDTO.Reservations = new List<BAGA.DTO.Reservation>();
  foreach (var res in cust.Reservations)
    custDTO.Reservations.Add(ReservationEF_toReservationDTO(res));

  custDTO.Addresses = new List<BAGA.DTO.Address>();
  foreach (var add in cust.Addresses)
    custDTO.Addresses.Add(AddressEF_toAddressDTO(add));

  return custDTO;
}
private BAGA.DTO.Reservation ReservationEF_toReservationDTO(Reservation res)
{
  var resDTO = new BAGA.DTO.Reservation
  { ReservationID = res.ReservationID,
    CustomerID = res.Customer.ContactID,
    ReservationDate = res.ReservationDate,
    TripDetails = res.TripDetails };

  resDTO.Payments = new List<BAGA.DTO.Payment>();
  foreach (var Payment in res.Payments)
    resDTO.Payments.Add(PaymentEF_toPaymentDTO(Payment));

  return resDTO;
}
private BAGA.DTO.Address AddressEF_toAddressDTO(Address add)
{
  var addDTO = new BAGA.DTO.Address
  { addressID = add.addressID,
    AddressType = add.AddressType,
    Street1 = add.Street1,
    City = add.City };
  return addDTO;
}
private BAGA.DTO.Payment PaymentEF_toPaymentDTO(Payment pmt)
{
  var pmtDTO = new BAGA.DTO.Payment
  { Amount = pmt.Amount.Value,
    PaymentDate = pmt.PaymentDate.Value};
  return pmtDTO;
}

Initializing the DTO Children

The GetCustomer method in Example 22-10 calls InitializeChildren, a separate method. This method is used to help with the child data to ensure that their EntityState is set to Unchanged before they are passed back to the client as part of the Customer graph. The DTOs don’t have business logic, and therefore cannot define a default. This generic method, shown in Example 22-12, uses a constraint to ensure that the type being passed in implements IEntityStateLocal. This way, you don’t have to worry about classes being passed in that don’t have the EntityState_Local property.

Example 22-12. Initializing the EntityState of the child data

VB
Sub InitializeChildren(Of T As BAGA.DTO.IEntityStateLocal) _
 (ByVal children As List(Of T))
  For Each item In children
    item.EntityState_Local = EntityStateLocal.Unchanged
  Next
End Sub
C#
public void InitializeChildren<T>(List<T> children)
 where T: BAGA.DTO.IEntityStateLocal
{
    foreach (var item in children)
      item.EntityState_Local = BAGA.DTO.EntityStateLocal.Unchanged;
}

Note

If you haven’t seen constraints used with generics before, you’ll notice that there is a big difference in the VB and C# syntax. VB adds the constraint as the generic is being defined, whereas C# accomplishes it with a where clause at the end of the declaration.

Saving Edits Sent Back from the Client with SaveCustomer

SaveCustomer is where the most interesting part of the service happens, although some of its functionality is farmed out to helper methods.

SaveCustomer uses the EntityState_Local values to decide how to treat each entity in the graph, and follows this logical path.

The first task, however, is to deal with the incoming DTOs because the code requires entities. You’ll need to transform them back into their comparable entity objects. You can create a set of reusable helper methods similar to the methods that you called in GetCustomer that transferred the entity data into DTOs. This time, however, you will be transferring the values of the incoming DTOs back to EntityObjects.

When looking at these methods, listed in Example 22-12, there is an important difference to highlight. When porting from entity to DTO, you grabbed the IDs of EntityReference properties and used those in the DTO. For example, the Reservation class has a TripID property rather than a TripReference. Coming back, however, you’ll need to create an EntityKey for the TripReference to ensure that the foreign key is set correctly in the database. Pay attention to this in the code listing in Example 22-13.

Also note that the code is recursively handling the child collections as well.

Example 22-13. Transferring incoming DTOs to entities

VB
Private Function CustDTO_to_EF(ByVal custDTO As BAGA.DTO.Customer) As Customer
  Dim cust As New Customer With _
              {.ContactID = custDTO.CustomerID, _
               .TimeStamp = custDTO.TimeStamp, _
               .EntityState_Local = custDTO.EntityState_Local, _
               .FirstName = custDTO.FirstName, _
               .LastName = custDTO.LastName, _
                *populate all property*}

  For Each resDTO In custDTO.Reservations
    cust.Reservations.Add(ResDTO_to_EF(resDTO))
  Next

  For Each addDTO In custDTO.Addresses
    cust.Addresses.Add(AddressDTO_toAddressEF(addDTO))
  Next

  Return cust

End Function

Private Function ResDTO_to_EF(ByVal resDTO As BAGA.DTO.Reservation) As Reservation
  Dim res As New Reservation With _
            {.ReservationID = resDTO.ReservationID, _
             .TimeStamp = custDTO.TimeStamp, _
             .EntityState_Local = custDTO.EntityState_Local, _
             .ReservationDate = resDTO.ReservationDate _
               *populate all properties* }
    're-create the TripReference from the TripID
    Dim tripKey = New EntityKey("BAEntities.Trips", "TripID", resDTO.TripID)
    res.TripReference.EntityKey = tripKey
    Return res
  End Function
C#
private Customer CustDTO_to_EF(BAGA.DTO.Customer custDTO)
{
  Customer cust = new Customer
                 {ContactID = custDTO.CustomerID,
                  TimeStamp = custDTO.TimeStamp,
                  FirstName = custDTO.FirstName,
                  LastName = custDTO.LastName,
                  EntityState_Local = (EntityStateLocal)custDTO.EntityState_Local};

  foreach (var resDTO in custDTO.Reservations)
    cust.Reservations.Add(ResDTO_to_EF(resDTO));

  foreach (var addDTO in custDTO.Addresses)
    cust.Addresses.Add(AddressDTO_toAddressEF(addDTO));

  return cust;
}

private Reservation ResDTO_to_EF(BAGA.DTO.Reservation resDTO)
{
  Reservation res = new Reservation
                    { ReservationID = resDTO.ReservationID,
                      ReservationDate = resDTO.ReservationDate,
                      TimeStamp = resDTO.TimeStamp,
                      EntityState_Local =
                       (EntityStateLocal)resDTO.EntityState_Local };
  //re-create the TripReference from the TripID
  if (!(resDTO.TripID == null))
  {
    if (resDTO.TripID > 0)
    {
      var tripKey = new EntityKey("BAEntities.Trips", "TripID", resDTO.TripID);
      res.TripReference.EntityKey = tripKey;
    }
  }
  return res;
}

The beauty of using the DTO in the case of the Reservation.Trip property is that you no longer have to worry about an attached trip being treated as a new entity to be added to the database. You may recall the extra code required to deal with that problem in Chapter 14.

Warning

Be sure not to neglect any of the entities’ properties when you build the methods to transfer the DTOs back to entities. If you leave them empty, SaveChanges will update those fields to null values! In Example 22-13, some of the properties have been left out only for the sake of brevity in the code listing. If the properties of the DTO class matched those of the entity exactly, you could write a generic method that performs this transformation. However, the fact that the DTO has special properties makes the possibility of creating a generic method much more difficult.

Building the Main Method for Updating the Graph: SaveCustomer

Now it’s time to take these incoming entities and prepare for the call to SavingChanges. The UpdateCustomer method follows a series of steps in the pattern. The step numbers listed here are identified in the code listing in Example 22-15:

Step 1

Convert incoming DTOs to Entities.

Step 2

Check Customer’s EntityState_Local property. If the Customer is new, just add the entire graph. Even if the Customer is unchanged, there still may be new, modified, or deleted children to deal with. Therefore, you’ll have to process unchanged and modified customers together.

Step 3

Query for a current server version of the graph using a helper method. This is a very important step and it has to do with the timestamp field. When the fresh customer data is retrieved from the database, its TimeStamp may be newer than that of the customer that the consuming application used. If they are different, the concurrency conflict will not be detected on SaveChanges because the original value of the fresh data will be used for the concurrency check.

To get around the concurrency checking issue, you’ll have to perform a little sleight of hand. You’ll notice that in step 3 the graph is queried with MergeOptions set to NoTracking. This will give you an opportunity to override the TimeStamps of all of the entities in the graph before the entities are being change-tracked. After the TimeStamp fields are updated, you can attach the whole graph to the context and the TimeStamp in the OriginalValues will be the one that was retrieved in the GetCustomer operation.

Note

This seems a bit unwieldy, but these are the types of patterns you’ll need to use with version 1 of the Entity Framework to succeed at implementing services.

The GetDetachedCustomerfromServer method in Example 22-14 shows how to code this step. This will be called from the SaveCustomer method, which will take the returned graph and attach it to the current context. The context is passed into this method so that a query can be executed. This method uses a new query that does not eager load the EntityReferences and calls an overload of ExecuteFirstOrDefault that takes a MergeOption, rather than calling the method used to retrieve a more involved Customer graph.

Example 22-14. Forcing the TimeStamp into the original values

VB
Private Function GetDetachedCustomerfromServer _
 (ByVal custEF As Customer, ByVal context As BreakAwayEntities) As Customer
    'EntityRefs are not needed for the update

    Dim custs = context.Contacts.OfType(Of Customer) _
                 .Include("Reservations").Include("Addresses") _
                 .Where("it.ContactID=" & custEF.contactID)
    'do not change-track at first
    Dim dal = new DAL.CommandExecutor
    Dim cust = dal.ExecuteFirstOrDefault(custs, MergeOption.NoTracking)
 

    'update cust timestamp value
    cust.TimeStamp = custEF.TimeStamp

    'update addresses and then reservations timestamp values
    For Each add In cust.Addresses
      Dim addressID = add.addressID
      Dim originalTS = (From a In custEF.Addresses _
                        Where a.addressID = addressID _
                        Select a.TimeStamp) _
                       .FirstOrDefault
      If Not originalTS Is Nothing Then 'unchanged entities did not come back
        add.TimeStamp = originalTS
      End If
    Next

    For Each res In cust.Reservations
      Dim resID = res.ReservationID
      Dim originalTS = (From r In custEF.Reservations _
                        Where r.ReservationID = resID _
                        Select r.TimeStamp) _
                        .FirstOrDefault

      If Not originalTS Is Nothing Then 'unchanged entities did not come back
        res.TimeStamp = originalTS
      End If
    Next
  Return cust
End Function
C#
private Customer GetDetachedCustomerfromServer(Customer custEF, BAEntities context)
{
  //'EntityRefs are not needed for the update

  var custs = context.Contacts.OfType<Customer>()
                     .Include("Reservations")
                     .Include("Addresses")
                     .Where("it.ContactID=" + custEF.ContactID);
  //do not change track at first
  var dal = new BAGA.DAL.CommandExecutor();
  var cust = dal.ExecuteFirstorDefault(custs, MergeOption.NoTracking);

  //update cust timestamp value
  cust.TimeStamp = custEF.TimeStamp;
  //update reservations timestamp values
  foreach (var add in cust.Addresses)
  {
    var addressID = add.addressID;
    var originalTS = (
            from a in custEF.Addresses
            where a.addressID == addressID
            select a.TimeStamp).FirstOrDefault();
    if (originalTS != null) //unchanged entities did not come back
      add.TimeStamp = originalTS;
  }
  foreach (var res in cust.Reservations)
  {
    var resID = res.ReservationID;
    var originalTS = (
            from r in custEF.Reservations
            where r.ReservationID == resID
            select r.TimeStamp).FirstOrDefault();
    if (originalTS != null) //unchanged entities did not come back
      res.TimeStamp = originalTS;
  }
  return cust;
}

With the GetDetachedCustomerfromServer method in place, it’s time to continue on with steps of SaveCustomer in Example 22-15:

Step 4

Update the Customer, if necessary, using ApplyPropertyChanges.

Step 5

Iterate through both sets of children (Addresses and Reservations), checking for Added, Modified, or Deleted entities and performing the necessary actions on those entities. A generic helper method is used so that it can perform the same tasks for any type of EntityCollection.

Step 6

Save the changes.

This last call, in step 6, is actually for the SaveAllChanges wrapper method that you saw in previous chapters. The SaveAllChanges method has been moved to the partial class for BAEntities in the model’s assembly. SaveAllChanges calls SaveChanges and handles OptimisticConcurrency and other entity exceptions.

Example 22-15 shows the code listing for the entire SaveCustomer method as described earlier.

Example 22-15. The SaveCustomer operation method

VB
Public Function SaveCustomer(ByVal custDTO As BAGA.DTO.Customer) As Boolean _
 Implements ICustomerService.SaveCustomer

    'STEP 1: Convert DTO to Entities
  Dim custEF = CustDTO_to_EF(custDTO)

  Try
    Using context = New BAEntities

        'STEP 2: If customer is added, just add to the context
      If cust.EntityState_Local = EntityStateLocal.Added Then
        context.AddToContacts(custEF)

      ElseIf custEF.EntityState_Local =  EntityState.Modified Or _
             custEF.EntityState_Local = EntityState.Unchanged Then

          'STEP 3: Get fresh data from server
        Dim custFromServer = GetDetachedCustomerfromServer(custEF, context)
       'attach and begin change tracking
        context.Attach(custFromServer)

        If custEF.EntityState_Local Then
         'STEP 4: Apply property changes for customer
          If custEF.EntityState_Local = EntityStateLocal.Modified Then
            context.ApplyPropertyChanges("Contacts", custEF)
          End If

         'STEP 5: Deal with addresses and reservations
          UpdateChildren(Of Address) (custEF.Addresses.ToList, _
                                      custFromServer.Addresses, _
                                      context, "Addresses")
          UpdateChildren(Of Reservation)(custEF.Reservations.ToList, _
                                         custFromServer.Reservations, _
                                         context, "Reservations")
        ElseIf EntityState.Deleted Then
        {
          'we're not deleting customers
        }
        End If
      End If

    'STEP 6: Save Changes
      context.SaveAllChanges()

      Return True

    End Using

  Catch ex As Exception
    'handle exceptions properly here
    Return False
  End Try
End Function
C#
public bool SaveCustomer(BAGA.DTO.Customer custDTO)
{

  //STEP 1: Convert DTO to Entities
  var custEF = CustDTO_to_EF(custDTO);

  try
  {
    using (var context = new BAEntities())
    {
      //STEP 2: If customer is added, just add to the context
      if (custEF.EntityState_Local == EntityStateLocal.Added)
        context.AddToContacts(custEF);
      else if (custEF.EntityState_Local == EntityStateLocal.Modified ||
               custEF.EntityState_Local == EntityStateLocal.Unchanged)
      {
        //STEP 3: Get fresh data from server
        var custFromServer = GetDetachedCustomerfromServer(custEF, context);
        //attach and begin change tracking
        context.Attach(custFromServer);

        if (custEF.EntityState_Local == EntityStateLocal.Modified)
        {
          //STEP 4: apply property changes for customer
          if (custEF.EntityState_Local == EntityStateLocal.Modified)
            context.ApplyPropertyChanges("Contacts", custEF);

          //STEP 5: Deal with addresses and reservations
          UpdateChildren<Address>(custEF.Addresses.ToList(),
                                 custFromServer.Addresses,
                                 context, "Addresses");
          UpdateChildren<Reservation>(custEF.Reservations.ToList(),
                                      custFromServer.Reservations,
                                      context, "Reservations");
        }
        else if (custEF.EntityState_Local == EntityStateLocal.Deleted)
          //we're not deleting customers


       //STEP 6: Save Changes;
        context.SaveAllChanges();

        return true;
      }
    }
  }
  catch (Exception ex)
  {
    //handle exceptions properly here
    return false;
  }
  return false;
}

UpdateChildren

The UpdateChildren method called by SaveCustomer and listed in Example 22-16 is a generic method that you can reuse with any EntityCollection as long as the entities in the collection implement the IEntityStateLocal interface. Because it needs to affect the attached entities with those entities that came from the client, you need to pass in both sets of entities.

The generic type (TEntity) is constrained to ensure that only EntityObjects that implement IEntityStateLocal are passed in. This way, you are assured that you have access to the necessary properties.

The method emulates what you’ve done with the Customer. First it finds the children whose state is Added and adds them to the Customer entity, which is attached to the context. Because the Entity Framework will implicitly remove that item from the one collection before adding it to the other, it is necessary to use the pattern you used earlier in this book that makes it possible to remove items from a collection without impacting the enumeration.

Next, the code looks for Modified children and uses the ApplyPropertyChanges method to update the matching entity that is already in the context.

Finally, the method looks for Deleted children. Any children marked for deletion are located in the context using the ObjectStateManager and are then deleted using the DeleteObject method of ObjectContext.

Example 22-16 lists the complete UpdateChildren method.

Example 22-16. The UpdateChildren method

VB
Sub UpdateChildren (Of TEntity As {IEntityStateLocal, EntityObject}) _
 (ByVal childCollectionfromClient As List(Of TEntity), _
 ByVal childCollectionAttached As EntityCollection(Of TEntity), _
 ByVal context As BAEntities, ByVal EntitySetName As String)

 'Move Added Addresses to attached customer
  Dim AddedChildren = childCollectionfromClient _
     .Where(Function(r) r.EntityState_Local = EntityStateLocal.Added)

    For i = AddedChildren.Count - 1 To 0 Step -1
      childCollectionAttached.Add(AddedChildren(i))
    Next

 'ApplyChanges of Modified child to its match in the context
  For Each child In childCollectionfromClient _
   .Where(Function(a) a.EntityState_Local = EntityStateLocal.Modified)
    context.ApplyPropertyChanges(EntitySetName, child)
  Next

 'Delete children from context if necessary
  For Each child In childCollectionfromClient. _
   Where(Function(a) a.EntityState_Local = EntityStateLocal.Deleted)

   'don't assume incoming deleted object is in the cache, use TryGet...
    Dim ose As Objects.ObjectStateEntry
    If context.ObjectStateManager. _
     TryGetObjectStateEntry(child.EntityKey, ose) Then
      context.DeleteObject(ose.Entity)
    End If
  Next

End Sub
C#
public void UpdateChildren<TEntity>
  (List<TEntity> childCollectionfromClient, EntityCollection<TEntity>
   childCollectionAttached, BAEntities context, string EntitySetName)
   where TEntity : EntityObject, IEntityStateLocal
{
  try
  {
    //Apply Addresses to attached customer
    var AddedChildren = childCollectionfromClient
                        .Where(r => r.EntityState_Local == EntityStateLocal.Added);
    for (var i = AddedChildren.Count() - 1; i >= 0; i--)
      childCollectionAttached.Add(AddedChildren.ToList()[i]);

    foreach (var child in childCollectionfromClient
               .Where(a => a.EntityState_Local == EntityStateLocal.Modified))
      context.ApplyPropertyChanges(EntitySetName, child);

    foreach (var child in childCollectionfromClient
              .Where((a) => a.EntityState_Local == EntityStateLocal.Deleted))
    {
      //don't assume incoming deleted object is in the cache
      ObjectStateEntry ose = null;
      if (context.ObjectStateManager
                 .TryGetObjectStateEntry(child.EntityKey, out ose))
        context.DeleteObject(ose.Entity);
    }
  }
  catch (Exception ex)
  {
    throw;
  }
}

That wraps up the service implementation. Now it’s time to see what happens on the client side.

Implementing the Client That Will Use the WCF Service

Although the service has been written with the intent to minimize client effort, clients that consume this service will still need to agree to the contract and rules of this service.

The client is not required to reference any of the .NET APIs to use these services. In fact, the provided classes can be converted to perform the same tasks using a different technology if necessary. This is the reason that only the classes are provided, rather than precompiled assemblies.

The sample client is written using a console application for the UI and a business layer for the interaction with the service.

Rules for the Client to Follow

The client must follow certain rules if the service is to function as expected:

  • The first rule is that entities must have their EntityState_Local properties set appropriately. We’ll write some classes that take onus off the developer and handle that task implicitly. Conceptually, you could provide these classes to consumers of your WCF service.

  • Entities that need to be deleted must be marked for deletion by setting the EntityState_Local property to Deleted. Otherwise, if they are truly deleted, they will not be returned to the service and the service won’t know they need to be deleted.

  • IDs and timestamps must not be modified. If the ID is modified on an entity that needs to be updated or deleted, it won’t be possible for the matching data to be found in the data store. Because the TimeStamp fields are used for concurrency checks, they are also part of the filter used when attempting to update or delete records.

  • Every editable object must be initialized when it is first retrieved from the web service. The client-side initialization provided by the supplemental classes enables the implicit updates to the EntityState_Local fields.

Building a Business Layer Between the UI and the Services

In this client application, you’ll create a layer that interacts with the service. It is a separate project that has the reference to the service and contains the code to interact with the service. In Visual Studio, this is the project to which you would add the service reference.

Note

When creating the service reference to the new WCF service, remember to configure the reference to use System.Collections.Generic.List as the default collection type, as you did in the earlier WCF example.

The classes in this project will have access to the DTO classes that the services return. Additionally, the business layer will contain some business logic that will ensure that the consuming application follows the rules the service has laid out. The logic is embedded in classes that extend the DTO classes to provide some behavior.

These business layer methods can be called by the UI layer, which won’t have to know anything about the services and won’t have as many rules to be concerned with.

Note

When you add the service reference to the business layer project, Visual Studio will apply the necessary service client configuration to the app.config file. When you reference this from the UI layer, the UI will need that configuration information. You can locate the <system.serviceModel> section of the business layer’s .config file and copy it into the .config file of the UI layer.

What’s in These Business Classes?

The business classes provide a small amount of business logic for the entities and help to implement some of the client-side rules. Providing these types of classes to consumers of your service will be a benefit to them and will help to ensure that they can easily follow the service contract.

Because the enums added into the model assembly were marked with the DataContract and EnumMembers attributes, they are available to the developer who is consuming the services. .NET clients will have the same IntelliSense help with the enums as you would if you were referencing the model’s assembly.

Each editable class (Customer, Address, and Reservation) has a partial class. And each partial class provides business logic for the classes using three methods, as discussed in the following three subsections.

The class constructor

The first method is the class constructor, which will be hit when the client explicitly instantiates a new object—for example, if the client application creates a new Address. The constructor sets the EntityState_Local property to Added and it inserts a default TimeStamp field.

Note

The binary TimeStamp field is being created here to comply with a limitation of serialization. The TimeStamp is non-nullable and is a reference type. This combination of attributes means it won’t get a default value the way in which, for example, an integer would automatically be 0. If the new object’s TimeStamp property is null, it will not be possible to serialize the object when it’s time to send it back to the service.

The property changed event

Anytime the property of an entity changes, this event will change the EntityState_Local property to Modified. It will first check to make sure the entity is not already marked as Added or Deleted.

The Initialize method

Objects coming down from the service will not automatically be wired up to the classes’ event handlers. The Initialize method manually ties the object to the PropertyChanged event so that when changes are made to preexisting objects, their EntityState_Local property will be changed.

As an example of implementing this business logic, Example 22-17 lists the Reservation class for the client-side business layer.

Example 22-17. The client-side Reservation class

VB
Imports System.ComponentModel
Namespace BreakAwayServices
  'TODO: modify this namespace to match the namespace
  '      you have given the service

  Public Class Reservation

    Public Sub Initialize()
      AddHandler Me.PropertyChanged, AddressOf entPropertyChanged
    End Sub

    Public Sub New()
      Me.EntityState_Local = EntityStateLocal.Added
     'default timestamp value because it cannot be null
      Me.TimeStamp = System.Text.Encoding.Default.GetBytes("0x123")
    End Sub

    Private Sub entPropertyChanged(ByVal sender As Object, _
     ByVal e As PropertyChangedEventArgs) Handles Me.PropertyChanged
      If EntityState_Local = 0 Or _
       EntityState_Local = EntityStateLocal.Unchanged Then
        Me.EntityState_Local = EntityStateLocal.Modified
      End If
    End Sub

  End Class
End Namespace
C#
using System.ComponentModel;
namespace BreakAwayServices
{
  //TODO: modify this namespace to match the namespace
  //      you have given the service
  public class Reservation
  {

    public void initialize()
    {
      this.PropertyChanged += entPropertyChanged;
    }

    public Reservation()
    {
      this.EntityState_Local = EntityStateLocal.Added;
     //default timestamp value because it cannot be null
      this.TimeStamp = System.Text.Encoding.Default.GetBytes("0x123");
    }


    private void entPropertyChanged(object sender,
                                   PropertyChangedEventArgs e)
    {
      if (EntityState_Local == 0
         || EntityState_Local == EntityStateLocal.Unchanged)
        this.EntityState_Local = EntityStateLocal.Modified;
    }
  }

}

The Address partial class can be implemented using the same code as the Reservation class.

As the parent node of the graph, the Customer class has some additional responsibilities. When a Customer is initialized, its reservations and addresses also need to be initialized. Rather than forcing the client-side developer to manually code all of the initialization, the Customer class’ Initialize method initializes the children for you, thereby initializing the customer graph, as shown in Example 22-18.

Example 22-18. The Initialize method of the Customer class

VB
Public Sub Initialize()
  AddHandler Me.PropertyChanged, AddressOf entPropertyChanged
  For Each add In Me.Addresses
    add.initialize()
  Next
  For Each res In Me.Reservations
    res.initialize()
  Next
End Sub
C#
public void Initialize()
{
  this.PropertyChanged += entPropertyChanged;
  foreach (var add in this.Addresses)
    add.Initialize();
  foreach (var res in this.Reservations)
    res.Initialize();
}

Additionally, the Customer class can help to ensure that children are marked for deletion rather than removed from the collection. You can use the Customer.RemoveChild method instead of the standard Collection.Remove method, and you can use Customer.RemoveAllAddresses and Customer.RemoveAllReservations instead of Collection.RemoveAll, as shown in Example 22-19.

Example 22-19. The Customer.RemoveChild method

VB
Public Sub RemoveChild(ByRef childEntity As Object)
  If TypeOf childEntity Is Address Or _
     TypeOf childEntity Is Reservation Then
    childEntity.EntityState_Local = EntityStateLocal.Deleted
  End If
End Sub
Public Sub RemoveAllAddresses()
  For Each add In Me.Addresses
    add.EntityState_Local = EntityStateLocal.Deleted
  Next
End Sub
Public Sub RemoveAllReservations()
  For Each res In Me.Reservations
    res.EntityState_Local = EntityStateLocal.Deleted
  Next
End Sub
C#
public void RemoveChild(object childEntity)
{
  if (childEntity is Address)
    ((Address)childEntity).EntityState_Local = EntityStateLocal.Deleted;
  if (childEntity is Reservation)
    ((Reservation)childEntity).EntityState_Local = EntityStateLocal.Deleted;
}
public void RemoveAllAddresses()
{
  foreach (var add in this.Addresses)
    add.EntityState_Local = EntityStateLocal.Deleted;
}
public void RemoveAllReservations()
{
  foreach (var res in this.Reservations)
    res.EntityState_Local = EntityStateLocal.Deleted;
}

Calling the Service Operations from the Business Layer

This business layer contains another class with four methods to call the four service operations: GetCustomerList, GetTripList, GetCustomer, and SaveCustomer. Each method calls the related operation from the service.

GetCustomerList, shown in the following code, is straightforward. It instantiates a reference to the service and calls the operation; then the reference to the service is disposed at the end of the using clause:

VB
Public Function GetCustomerList() As List(Of ShortCustomer)
  Using svc = New CustomerServiceClient
    Return svc.GetCustomerList()
  End Using
End Function
C#
public List<ShortCustomer> GetCustomerList()
{
  using (var svc = new CustomerServiceClient())
  {
    return svc.GetCustomerList();
  }
}

GetTripList is equally straightforward, as shown here:

VB
Public Function GetTripList() As List(Of Trip)
  Using svc = New CustomerServiceClient
    Return svc.GetTrips()
  End Using
End Function
C#
public List<Trip> GetTripList()
{
  using (var svc = new CustomerServiceClient())
  {
    return svc.GetTrips();
  }
}

The GetCustomer method, shown next, calls Customer.Initialize after the data has been retrieved for the service. This, in turn, ensures that the customer, addresses, and reservations are all initialized:

VB
Public Function GetCustomer(ByVal ContactID As Integer) As Customer
  Using svc = New CustomerServiceClient
    Dim cust = svc.GetCustomer(ContactID)
    cust.Initialize()
    Return cust
  End Using
End Function
C#
public Customer GetCustomer(int ContactID)
{
  using (var svc = new CustomerServiceClient())
  {
    var cust = svc.GetCustomer(ContactID);
    cust.Initialize();
    return cust;
  }
}

For the sake of efficiency, SaveCustomer removes Address and Reservation objects that do not need updating prior to sending the graph to the service, as shown in Example 22-20. This will reduce the size of the message sent back to the service.

Example 22-20. The SaveCustomer method in the client-side business layer

VB
Public Sub SaveCustomer(ByVal cust As Customer)
  'completely remove unchanged children
  'removing items from a collection requires a particular pattern
  'don't remove reference objects (e.g., the Trip attached to Reservation)

  For i = cust.Addresses.Count - 1 To 0 Step -1
    Dim add = cust.Addresses(i)
    If add.EntityState_Local = EntityStateLocal.Unchanged Then
      cust.Addresses.Remove(add)
    End If
  Next
  For i = cust.Reservations.Count - 1 To 0 Step -1
    Dim res = cust.Reservations(i)
    If res.EntityState_Local = EntityStateLocal.Unchanged Then
      cust.Reservations.Remove(res)
    End If
  Next
  Using svc = New CustomerServiceClient
    svc.SaveCustomer(cust)
  End Using
End Sub
C#
public void SaveCustomer(Customer cust)
{
  //completely remove unchanged children
  //removing items from a collection requires a particular pattern
  //don't remove reference objects (e.g., the Trip attached to Reservation)

  for (var i = cust.Addresses.Count - 1; i >= 0; i--)
  {
    var add = cust.Addresses[i];
    if (add.EntityState_Local == EntityStateLocal.Unchanged)
      cust.Addresses.Remove(add);
  }
  for (var i = cust.Reservations.Count - 1; i >= 0; i--)
  {
    var res = cust.Reservations[i];
    if (res.EntityState_Local == EntityStateLocal.Unchanged)
      cust.Reservations.Remove(res);
  }
  using (var svc = new CustomerServiceClient())
  {
    svc.SaveCustomer(cust);
  }
}

Note

You may recall in Chapter 14 modifying the client configuration’s MaxReceivedMessageSize property. This is an important step, as the graphs can quickly grow and can exceed the default step. Check Figure 14-12 for a reminder of how to change this setting using the tool. Or just modify it manually in the app.config file.

Testing It All with a Simple Console Application

Now you need some code to make this run and to see it in action. Rather than create a full interface, you can just write a little bit of code in a console application to test the basic functionality.

The code in Example 22-21 tests the GetCustomerList function to retrieve a list of customer names and IDs. It also calls GetTripList so that it will be possible to add a new reservation in this example. Next it selects a random customer from the list of customers and uses the ID to call GetCustomer.

Once the customer has been retrieved, you can make a change to a Customer property, change a property in one of the addresses, delete a reservation, and create a new reservation.

Example 22-21. A console application to test the service

VB
Sub Main()
  Dim bal As New BusinessClass
  Dim custlist = bal.GetCustomerList
  Dim triplist = bal.GetTripList

 'select a random customer from the list
  Dim rnd = New Random
  Dim randomPositioninList = rnd.Next(1, custlist.Count)
  Dim ID = custlist(randomPositioninList).ContactID
  Dim cust = bal.GetCustomer(ID)

 'modify the customer
  cust.Notes = cust.Notes.Trim & " Updated on " & Now.ToShortDateString

  If cust.Reservations.Count > 0 Then
   'Remove the last reservation from customer if there are no payments
   'RULE: Call removechild, not remove
     Dim reslast = cust.Reservations(cust.Reservations.Count - 1)
     If reslast.Payments.Count = 0 Then
       cust.RemoveChild(cust.Reservations(cust.Reservations.Count - 1))
     End If
   End If

  'Create a new reservation
    Dim res = New WCFClientBusinessLayer.BreakAwayServices.Reservation
    res.ReservationDate = Now
    'pick a random trip and assign it to the reservation
    randomPositioninList = rnd.Next(1, triplist.Count)
    res.Trip = triplist(randomPositioninList)
    'add the new reservation to the customer
    cust.Reservations.Add(res)

    'modify the customer's first address
    If cust.Addresses.Count > 0 Then
      Dim add = cust.Addresses(0)
      add.Street2 = "PO Box 75"
    End If

    'call the SaveCustomer method in the business layer
    bal.SaveCustomer(cust)
End Sub
C#
public void Main()
{
  BusinessClass bal = new BusinessClass();
  var custlist = bal.GetCustomerList();
  var triplist = bal.GetTripList();

 //select a random customer from the list
  var rnd = new Random();
  var randomPositioninList = rnd.Next(1, custlist.Count);
  var ID = custlist[randomPositioninList].ContactID;
  var cust = bal.GetCustomer(ID);

 //modify the customer
  cust.Notes = cust.Notes.Trim() + " Updated on " +
   DateTime.Now.ToShortDateString();

  if (cust.Reservations.Count()>0)
  {
   //Remove the last reservation from customer if there are no payments
   //RULE: Call removechild, not remove
    var reslast = cust.Reservations[cust.Reservations.Count - 1];
     if (reslast.Payments.Count == 0)
       cust.RemoveChild(reslast);
  }

  //Create a new reservation
  var res = new
  WCFClientBusinessLayer.BreakAwayServices.Reservation();
  res.ReservationDate = System.DateTime.Now;
  //pick a random trip and assign it to the reservation
  randomPositioninList = rnd.Next(1, triplist.Count);
  var trip = triplist[randomPositioninList];
  res.TripID = trip.TripID;
  //add the new reservation to the customer
  cust.Reservations.Add(res);

  if (cust.Addresses.Count()>0)
  {
   //modify the customer's first address
    var add = cust.Addresses[0]; <--replacing parens with square brackets
    add.Street2 = "PO Box 75";
  }
   //call the SaveCustomer method in the business layer
  bal.SaveCustomer(cust);
}

Note

Even though the code checks to make sure there are no dependent payments before deleting a reservation, the database will still throw an error if more payments have been added in the meantime.

By running this module, you’ll be able to see all of the features of the SaveCustomer WCF operation in action: updating, adding, and deleting entities. Set breakpoints in the service to see the various methods doing their jobs, and watch SQL Profiler to see the updates, inserts, and deletes hit the database.

Summary

In this chapter, you leveraged many of the things you learned throughout the book to build a more sophisticated Entity Framework WCF service that doesn’t rely on assumptions to determine the state of data. Of all of the possible ways to approach this problem, the most effective solution requires the consuming application to follow some requirements, yet it still does not need to have references to the Entity Framework APIs, or even to your model’s assembly.

Working in distributed applications with version 1 of the Entity Framework is challenging, but it is possible once you have a grasp of the many tools at your disposal and you understand what behavior to expect from the entities.

Don’t forget to consider ADO.NET Data Services if you don’t need as much granular control over your services. It provides a simple and standardized way for consumers to access your data as a resource through HTTP rather than specific operations. It’s also very easy to set up ADO.NET Data Services to expose your data through your model.

If you are building enterprise applications that need to leverage services, this chapter provided you with a pattern that you can use and expand upon.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.142.173.238