Getting Started with Trackable Entities using WCF

Screen shots are from Visual Studio 2013, but instructions apply to both Visual Studio 2012 and 2013.

Compatibility

  • Both Visual Studio 2012 and 2013 support WCF with EF 6.0 or later.

Prerequisites

Make sure you have installed the Trackable Entities extension for either Visual Studio 2012 or 2013.

  • The extension can be installed from within Visual Studio by selecting the Tools menu, Extensions and Updates, Online tab.

For detailed instructions on installing the prerequisites see here.

Trackable Entities with Windows Communication Foundation

  1. Start by selecting New Project from the File menu.  Under Visual C#, select the Trackable category, then choose “Trackable WCF Service.”
    • Type an appropriate Name and click OK.

    new-proj-tr-wcf

    - What you get is a solution containing 5 projects:
       ConsoleClient
       Client.Entities
       Web
       Service.Core
       Service.Entities
     

    tr-wcf-sln

  2. The first thing you’ll want to do is create some server-side entities and a DbContext for persistence to a database. While you could hand-craft these classes yourself, it’s sometimes easier to start with an existing database and use the Entity Framework Power Tools to reverse engineer Code First classes. That’s what we’ll do here.
    - The Service.Entities project includes custom T4 templates which generate entity classes which implement the ITrackable interface.
    • Right-click the Service.Entities project in the solution explorer and select Entity Framework from the context menu.
    • Then select Reverse Engineer Code First.

    rev-eng-codefirst

    • Fill out the Connection Properties dialog and click OK.

    cn-prop

    • What you get is a set of entities, mapping classes, and a DbContext-derived class.
    • Entities implement ITrackable and also contain Json and DataContract serialization attributes – in order to accommodate circular references which usually result from reverse engineering a database. Here is an example of the Product class.
    [JsonObject(IsReference = true)]
    [DataContract(IsReference = true, Namespace = "http://schemas.datacontract.org/2004/07/TrackableEntities.Models")]
    public partial class Product : ITrackable
    {
        public Product()
        {
            this.OrderDetails = new List<OrderDetail>();
        }
    
        [DataMember]
        public int ProductId { get; set; }
        [DataMember]
        public string ProductName { get; set; }
        [DataMember]
        public Nullable<int> CategoryId { get; set; }
        [DataMember]
        public Nullable<decimal> UnitPrice { get; set; }
        [DataMember]
        public bool Discontinued { get; set; }
        [DataMember]
        public byte[] RowVersion { get; set; }
        [DataMember]
        public Category Category { get; set; }
        [DataMember]
        public List<OrderDetail> OrderDetails { get; set; }
    
        [DataMember]
        public TrackingState TrackingState { get; set; }
        [DataMember]
        public ICollection<string> ModifiedProperties { get; set; }
    }
    • By default the DbContext class is configured to generate dynamic runtime proxies, which are not serializable.
    • The T4 template that generates the DbContext is configured to turn off both proxy creation.
      - Build the solution.
  3. Copy the connection string from the App.config file in the Service.Entities project to the Web.config file in the Web project.
    - For example:

    <connectionStrings>
      <add name="NorthwindSlimContext" connectionString="Data Source=.\sqlexpress;Initial Catalog=NorthwindSlim;Integrated Security=True;MultipleActiveResultSets=True"
        providerName="System.Data.SqlClient" />
    </connectionStrings>
    

  4. Right-click on the Service.Core project, select Add, New Item. Then click on the Trackable category and select “Trackable WCF Service Type.”

    tr-wcf-svc

    • Give it an appropriate name and click Add. For this example we will call it CustomerService.cs. 
    • Provide the information requested by the dialog.

      tr-wcf-svc-dialog

      - Service Entities Project Namespace: Full name of the xxx.Service.Entities project
      - Entity Name: Name of the entity class you want to use
      - Entity Set Name: The plural of the entity class name
      - DbContext Name: Name of the EF context class created in Service.Entities
    • What you get is a WCF service contract interface with a service type implementation. In the case of Customer, you’ll need to replace int with string as the id type.
    • Satisfy each TODO item by replacing x.Id with x.CustomerId. Here’s what you should end up with.
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
    public class CustomerService : ICustomerService, IDisposable
    {
        private readonly NorthwindSlimContext _dbContext;
    
        public CustomerService()
        {
            _dbContext = new NorthwindSlimContext();
        }
    
        public async Task<IEnumerable<Customer>> GetCustomers()
        {
            IEnumerable<Customer> customers = await _dbContext.Customers
                .ToListAsync();
            return customers;
        }
    
        public async Task<Customer> GetCustomer(string id)
        {
            Customer customer = await _dbContext.Customers
                .SingleOrDefaultAsync(c => c.CustomerId == id);
            return customer;
        }
    
        public async Task<Customer> CreateCustomer(Customer customer)
        {
            customer.TrackingState = TrackingState.Added;
            _dbContext.ApplyChanges(customer);
    
            try
            {
                await _dbContext.SaveChangesAsync();
            }
            catch (DbUpdateException updateEx)
            {
                throw new FaultException(updateEx.Message);
            }
    
            await _dbContext.LoadRelatedEntitiesAsync(customer);
            customer.AcceptChanges();
            return customer;
        }
    
        public async Task<Customer> UpdateCustomer(Customer customer)
        {
            _dbContext.ApplyChanges(customer);
    
            try
            {
                await _dbContext.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException updateEx)
            {
                throw new FaultException(updateEx.Message);
            }
    
            await _dbContext.LoadRelatedEntitiesAsync(customer);
            customer.AcceptChanges();
            return customer;
        }
    
        public async Task<bool> DeleteCustomer(string id)
        {
            Customer customer = await _dbContext.Customers
                .SingleOrDefaultAsync(c => c.CustomerId == id);
            if (customer == null)
                return false;
    
            customer.TrackingState = TrackingState.Deleted;
            _dbContext.ApplyChanges(customer);
    
            try
            {
                await _dbContext.SaveChangesAsync();
                return true;
            }
            catch (DbUpdateConcurrencyException updateEx)
            {
                throw new FaultException(updateEx.Message);
            }
        }
    
        public void Dispose()
        {
            var dispose = _dbContext as IDisposable;
            if (dispose != null)
            {
                _dbContext.Dispose();
            }
        }
    }
  5. Add another Trackable WCF Service Type for Order.  We’re going to modify code in the Order controller just a little bit, so that we can bring in related order details.
    • Satisfy each TODO item by replacing x.Id with x.OrderId.
    • Order details need to be brought in explicitly because child entities are eager-loaded only when you want the entire object-graph up front. Start with the GetOrders and GetOrder actions, and insert and Includes that bring in both Customer and OrderDetails.
      - For OrderDetails we’ll use a string instead of a lambda expression, so that each OrderDetail’s Product property is also populated.
    public async Task<IEnumerable<Order>> GetOrders()
    {
        IEnumerable<Order> entities = await _dbContext.Orders
            .Include(o => o.Customer)
            .Include("OrderDetails.Product")
            .ToListAsync();
        return entities;
    }
    
    public async Task<Order> GetOrder(int id)
    {
        Order entity = await _dbContext.Orders
            .Include(o => o.Customer)
            .Include("OrderDetails.Product")
            .SingleOrDefaultAsync(x => x.OrderId == id);
        return entity;
    }
    • We will also add a GetOrders method that accepts a CustomerId, so that we can get orders for a particular customer.
    // Place this in the IOrderService interface
    [OperationContract]
    Task<IEnumerable<Order>> GetCustomerOrders(string customerId);
    
    // Place this in the OrderService class
    public async Task<IEnumerable<Order>> GetCustomerOrders(string customerId)
    {
        IEnumerable<Order> orders = await _dbContext.Orders
            .Include(o => o.Customer)
            .Include("OrderDetails.Product")
            .Where(o => o.CustomerId == customerId)
            .ToListAsync();
        return orders;
    }
    • Then we'll update DeleteOrder to also remove order details by adding an Include that retrieves related order details.  Note that this is only necessary if the relation between Order and OrderDetail in the database does not specify Cascade Deletes.
    public async Task<bool> DeleteOrder(int id)
    {
        Order order = await _dbContext.Orders
            .Include(o => o.OrderDetails) // Include details
            .SingleOrDefaultAsync(c => c.OrderId == id);
        if (order == null)
            return false;
    
        order.TrackingState = TrackingState.Deleted;
        _dbContext.ApplyChanges(order);
    
        try
        {
            await _dbContext.SaveChangesAsync();
            return true;
        }
        catch (DbUpdateConcurrencyException updateEx)
        {
            throw new FaultException(updateEx.Message);
        }
    }
  6. Build the solution. Then expand the Web project in the solution explorer and rename ExampleService.svc to CustomerService.svc.

    • Open the svc file and replace ExampleService with CustomerService in the ServiceHost directive.
    <%@ ServiceHost Service="GettingStarted2.Service.Core.CustomerService" %>
    • Copy and rename the svc file to create OrderService.svc, then open the file and replace CustomerService with OrderService.
    <%@ ServiceHost Service="GettingStarted2.Service.Core.OrderService" %>
    • Right-click on each of the svc files in the Web project in the solution explorer, and select View in Browser.  You should see the service metadata help page displayed.

    svc-help-page

  7. Next generate client-side trackable entities by reverse engineering Code First for the NorthwindSlim database in the Client.Entities project.

    • Right-click the Client.Entities project, then select Entity Framework, Reverse Engineer Code First.
    • Fill out the Connection Properties dialog, just as you did for the Service.Entities project.
    • IMPORTANT: After reverse engineering the database, you will need to delete the Context class, as well as the Mapping folder and all the classes it contains. Those artifacts are only relevant for a server-side project, because the client will not be accessing the database directly.
    • The generated entity classes implement both the INotifyPropertyChanged and ITrackable interfaces. Related child collections are surfaced as ChangeTrackingCollection<T>, so that the client-side change-tracker can do its magic. Below is an example of the Product entity in Client.Entities (some properties have been omitted for clarity).
    [JsonObject(IsReference = true)]
    [DataContract(IsReference = true, Namespace = "http://schemas.datacontract.org/2004/07/TrackableEntities.Models")]
    public partial class Product : ModelBase<Product>, IEquatable<Product>, ITrackable
    {
        [DataMember]
        public int ProductId
        { 
            get { return _ProductId; }
            set
            {
                if (value == _ProductId) return;
                _ProductId = value;
                NotifyPropertyChanged(m => m.ProductId);
            }
        }
        private int _ProductId;
    
        [DataMember]
        public string ProductName
        { 
            get { return _ProductName; }
            set
            {
                if (value == _ProductName) return;
                _ProductName = value;
                NotifyPropertyChanged(m => m.ProductName);
            }
        }
        private string _ProductName;
    
        [DataMember]
        public Nullable<int> CategoryId
        { 
            get { return _CategoryId; }
            set
            {
                if (value == _CategoryId) return;
                _CategoryId = value;
                NotifyPropertyChanged(m => m.CategoryId);
            }
        }
        private Nullable<int> _CategoryId;
    
        [DataMember]
        public ICollection<string> ModifiedProperties { get; set; }
    
        [DataMember]
        public TrackingState TrackingState { get; set; }
    }
  8. Now that you have the service configured, it’s time to set up the client. First, you’ll need to insert the service contract interfaces for ICustomerService and IOrderService into the Client.ConsoleApp project.
    • A convenient way to add the service interfaces is to rename IExampleService.cs to ICustomerService.cs.
      - Uncomment the “using” directive for the entity models, then replace IExampleService with ICustomerService.
      - Then uncomment the interface code and place Product with Customer (turn off whole-word matching).
      - Remember for Customer to replace int with string for the id type.
      - Copy the code file and rename to IOrderService, then repeat the renaming process.
      - Remember to change string back to int, then add the GetCustomerOrders method. Add Async to the method name, and specify a Name parameter on the Operation contract attribute without the Async suffix.
    • This is what the IOrderService contract looks like on the client.
    [ServiceContract(Namespace = "urn:trackable-entities:service")]
    public interface IOrderService
    {
        [OperationContract(Name = "GetOrders")]
        Task<IEnumerable<Order>> GetOrdersAsync();
    
        [OperationContract(Name = "GetCustomerOrders")]
        Task<IEnumerable<Order>> GetCustomerOrdersAsync(string customerId);
    
        [OperationContract(Name = "GetOrder")]
        Task<Order> GetOrderAsync(int id);
    
        [OperationContract(Name = "UpdateOrder")]
        Task<Order> UpdateOrderAsync(Order entity);
    
        [OperationContract(Name = "CreateOrder")]
        Task<Order> CreateOrderAsync(Order entity);
    
        [OperationContract(Name = "DeleteOrder")]
        Task<bool> DeleteOrderAsync(int id);
    }

  9. Now you’ll need to configure the Client.ConsoleApp project.
    • First, grab the dynamically generated port number from the Web project. Right-click on the project in the solution explorer, select Properties, then click on the Web tab and copy the port number from the Project Url.

    port-number-wcf

    • Open App.config in Client.ConsoleApp and paste the port number into the endpoint address.
      - Uncomment the endpoint, replace Example with Customer.
      - Make sure to name the endpoint, customerService.
      - Copy the endpoint to create an OrderService endpoint.
      - Here is what the <system.ServiceModel> section in App.config should look like.
    <system.serviceModel>
        <client>
            <endpoint address="http://localhost:52505/CustomerService.svc"
                    binding="basicHttpBinding" 
                    contract="GettingStarted2.Client.ConsoleApp.ICustomerService"
                    name="customerService">
          </endpoint>
          <endpoint address="http://localhost:52505/OrderService.svc"
                    binding="basicHttpBinding"
                    contract="GettingStarted2.Client.ConsoleApp.IOrderService"
                    name="orderService">
          </endpoint>
        </client>
    </system.serviceModel>

  10. Finally, you’ll write code in the Client.ConsoleApp project to invoke service operations.
    • Open Program.cs and uncomment the code there.
      - Uncomment the “using” directive for the entity models.
      - Uncomment the rest of the code.
    • Build the solution, then set Client.ConsoleApp as the startup project and press F5 to run it.
      - Press enter each time when prompted to execute the program.
    • Part of the program changes an existing order by adding, modifying and removing child order details. Here is where we leverage the client change-tracking collection, which automatically sets the TrackingState property on each order detail as it is added, updated, or removed.
    // Start change-tracking the order
    var changeTracker = new ChangeTrackingCollection<Order>(createdOrder);
    
    // Modify order details
    createdOrder.OrderDetails[0].UnitPrice++;
    createdOrder.OrderDetails.RemoveAt(1);
    createdOrder.OrderDetails.Add(new OrderDetail
    {
        OrderId = createdOrder.OrderId,
        ProductId = 3,
        Quantity = 15,
        UnitPrice = 30
    });
    
    // Submit changes
    Order changedOrder = changeTracker.GetChanges().SingleOrDefault();
    Order updatedOrder = orderService.UpdateOrderAsync(changedOrder).Result;
    • Notice that we call GetChanges on the change tracker to get just the entities that have been inserted, modified or deleted.
    • After we get get updates entities from the Update operation, we'll want to merge those back into the original order that we created.
    // Merge changes
    changeTracker.MergeChanges(updatedOrder);
    • After calling MergeChanges, createdOrder will contain details that were not changed, as well as updated details that were persisted and returned from the server with new identity and concurrency values. Tracking state is set to Unchanged on all entities and any ModifiedProperties present are cleared

So that is how you can get started using the Trackable Entities extension for Visual Studio 2012 and 2013 using Windows Communication Foundation.

Last edited Apr 28, 2014 at 2:30 PM by tonysneed, version 5