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.
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.
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.
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 EntityObject
s, 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.
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
.
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; }
}
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;
}
Customer.Reservations
and
other child EntityCollection
s
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
.
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.
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
}
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 ;}
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.
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
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.
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();
}
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.
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);
}
}
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 Trip
s 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();
}
}
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.
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;
}
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;
}
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 EntityObject
s.
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.
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.
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:
Convert incoming DTOs to Entities.
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.
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 TimeStamp
s 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.
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 EntityReference
s 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:
Update the Customer
, if
necessary, using ApplyPropertyChanges
.
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
.
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;
}
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 EntityObject
s 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.
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.
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
.
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.
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.
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.
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 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.
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.
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
.
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;
}
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);
}
}
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.
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);
}
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.
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.
3.142.173.238