Getting Started with Trackable Entities using ASP.NET Web API

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

Compatibility

  • Visual Studio 2012 with Web API 1 and EF 5.0
  • Visual Studio 2013 with Web API 2 and 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.

You’ll also need to install the Entity Framework Power Tools, which you can also add from the Visual Studio Tools menu, Extensions and Updates.

Download the NorthwindSlim database and attach it to SQL Express using SQL Server Management Studio.

  • This is a stripped down version of the Northwind sample database.
  • Download the NorthwindSlim database from here: http://bit.ly/northwindslim.
  • Follow instructions in the zip file for creating the database and running the SQL script.

Detailed instructions on installing these prerequisites can be found here.

Trackable Entities with ASP.NET Web API

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

    CreateWebApiProject

    - What you get is a solution containing 4 projects:
       ConsoleClient
       Client.Entities
       WebApi
       Service.Entities

    WebApiSolutionExplorer

    • The solution also contains a ReadMe file with step-by-step instructions similar to this tutorial.

  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.

    Reverse-Eng

    • Fill out the Connection Properties dialog and click OK.

    cn-prop7

    • 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; }
        [JsonProperty, DataMember]
        private Guid EntityIdentifier { get; set; }
    }
  3. Copy the connection string inserted into the App.config file of the Service.Entities project, and paste it into the ConnectionStrings section of the web.config file in the WebApi project.
    <connectionStrings>
      <add name="NorthwindSlimContext" connectionString="Data Source=.\sqlexpress;Initial Catalog=NorthwindSlim;Integrated Security=True;MultipleActiveResultSets=True"
        providerName="System.Data.SqlClient" />
    </connectionStrings>
    • Build the solution.
  4. Add API controllers to the WebApi project which use Entity Framework to perform CRUD operations (Create, Retrieve, Update, Delete) using trackable entities.
    • Right-click the Controllers folder and select Add New Item.
      Note: In v1 of Trackable Entities you would select Add New Controller, which leveraged ASP.NET MVC scaffolding with customized T4 templates to generate Web API controllers that used Trackable Entities.  This strategy has been discontinued with v 2.0, which instead has a Visual Studio item template for this purpose.

    AddNewEntityController

    • Expand the Trackable category, then open Web, and select Entity Web API Controller.
    • Enter a name for the controller (for example CustomerController) and select Add.

    CustomerController

    • In the Add Entity Web API Controller dialog, select an Entity Name from the dropdown list, then enter an Entity Set Name, which is usually the pluralized form of the entity name, and confirm the selection of the DbContext Name.

    AddEntityControllerDialog

    • This will add a Web API controller with Get, Post, Put and Delete actions.  You will need to possibly replace the primary key name and type. For example, Customer’s primary key is a string type, so you would need to change the id parameter from int to string in the Get and Delete actions.
    • There are also some TODO items in the controller to remind you to add Include operators for reference and/or collection properties.  For example, if Product has a Category property, you would want to add .Include(e => e.Category) to the LINQ query in the Get action, so that each product will have its Category populated when returned from the action.
    • Here is example of a CustomerController for the NorthwindSlim database.
    public class CustomerController : ApiController
    {
        private readonly NorthwindSlimContext _dbContext = new NorthwindSlimContext();
    
        // GET api/Customer
        [ResponseType(typeof(IEnumerable<Customer>))]
        public async Task<IHttpActionResult> GetCustomers()
        {
            IEnumerable<Customer> entities = await _dbContext.Customers
                // TODO: Add Includes for reference and/or collection properties
                .ToListAsync();
    
            return Ok(entities);
        }
    
        // GET api/Customer/5
        [ResponseType(typeof(Customer))]
        public async Task<IHttpActionResult> GetCustomer(string id)
        {
            Customer entity = await _dbContext.Customers
                // TODO: Add Includes for reference and/or collection properties
                .SingleOrDefaultAsync(e => e.CustomerId == id);
    
            if (entity == null)
            {
                return NotFound();
            }
    
            return Ok(entity);
        }
    
        // POST api/Customer
        [ResponseType(typeof(Customer))]
        public async Task<IHttpActionResult> PostCustomer(Customer entity)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }
    
            entity.TrackingState = TrackingState.Added;
            _dbContext.ApplyChanges(entity);
    
    
            try
            {
                await _dbContext.SaveChangesAsync();
            }
            catch (DbUpdateException)
            {
                if (_dbContext.Customers.Any(e => e.CustomerId == entity.CustomerId))
                {
                    return Conflict();
                }
                throw;
            }
    
            await _dbContext.LoadRelatedEntitiesAsync(entity);
            entity.AcceptChanges();
            return CreatedAtRoute("DefaultApi", new { id = entity.CustomerId }, entity);
        }
    
        // PUT api/Customer
        [ResponseType(typeof(Customer))]
        public async Task<IHttpActionResult> PutCustomer(Customer entity)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }
    
            _dbContext.ApplyChanges(entity);
    
            try
            {
                await _dbContext.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!_dbContext.Customers.Any(e => e.CustomerId == entity.CustomerId))
                {
                    return Conflict();
                }
                throw;
            }
    
            await _dbContext.LoadRelatedEntitiesAsync(entity);
            entity.AcceptChanges();
            return Ok(entity);
        }
    
        // DELETE api/Customer/5
        public async Task<IHttpActionResult> DeleteCustomer(string id)
        {
            Customer entity = await _dbContext.Customers
                // TODO: Include child entities if any
                .SingleOrDefaultAsync(e => e.CustomerId == id);
            if (entity == null)
            {
                return Ok();
            }
    
            entity.TrackingState = TrackingState.Deleted;
            _dbContext.ApplyChanges(entity);
    
            try
            {
                await _dbContext.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!_dbContext.Customers.Any(e => e.CustomerId == entity.CustomerId))
                {
                    return Conflict();
                }
                throw;
            }
    
            return Ok();
        }
    
        protected override void Dispose(bool disposing)
        {
            _dbContext.Dispose();
            base.Dispose(disposing);
        }
    }
    • Notice that actions are asynchronous by default, and that update operations call DbContext.ApplyChanges to set entity state based on each entity’s TrackingState property. Also notice that DbContext.LoadRelatedEntities is called after saving changes. This populates missing reference properties on added items. And before returning the entity to the client, AcceptChanges is called so that TrackingState is set to Unchanged.
  5. Build the solution, then test the controller by clicking on the WebApi project and selecting View in Browser, either from the context or the File menu.
    - Click on the API link to see the Help page for the controller you added.

    webapi-help6

    • Select one of the GET methods to test.  Click the Test API button then Send.  You should see the results displayed.

    prod-get-response6

  6. Now that we have the server-side up and running, it’s time to configure the client. We’ll start by reverse engineering Code First entities for 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; }
    }
  7. Add other controllers to the WebApi project.  For this demonstration, we’ll add controllers for Customer and Order entities.
    • These will be called from the client Console application that we will configure in the next step.
    • We’re going to modify code in the Order controller just a little bit, so that we can bring in order details. These need to be brought in explicitly because child entities are eager-loaded only when you specify the required Include statements.
    • Start with the GetOrders and GetOrder actions, and insert and Include that brings in related OrderDetails.  For this we’ll use a string instead of a lambda expression, so that each OrderDetail’s Product property is also populated.
    // GET api/Order
    [ResponseType(typeof(IEnumerable<Order>))]
    public async Task<IHttpActionResult> GetOrders()
    {
        IEnumerable<Order> orders = await _dbContext.Orders
            .Include(o => o.Customer)
            .Include("OrderDetails.Product") // include details with products
            .ToListAsync();
        
        return Ok(orders);
    }
    
    // GET api/Order/5
    [ResponseType(typeof(Order))]
    public async Task<IHttpActionResult> GetOrder(int id)
    {
        Order order = await _dbContext.Orders
            .Include(o => o.Customer)
            .Include("OrderDetails.Product") // include details with products
            .SingleOrDefaultAsync(o => o.OrderId == id);
    
        if (order == null)
        {
            return NotFound();
        }
        return Ok(order);
    }
    
    • We will also add a GetOrders method that accepts a CustomerId, so that we can get orders for a particular customer.
    // GET api/Order?customerId=ABCD
    [ResponseType(typeof(IEnumerable<Order>))]
    public async Task<IHttpActionResult> GetOrders(string customerId)
    {
        IEnumerable<Order> orders = await _dbContext.Orders
            .Include(o => o.Customer)
            .Include("OrderDetails.Product") // include details with products
            .Where(o => o.CustomerId == customerId)
            .ToListAsync();
    
        return Ok(orders);
    }

    • Next, we'll update DeleteOrder to also remove order details. To begin with, we’ll add 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.
    // DELETE api/Order/5
    public async Task<IHttpActionResult> DeleteOrder(int id)
    {
        Order order = await _dbContext.Orders
            .Include(o => o.OrderDetails) // Include details
            .SingleOrDefaultAsync(o => o.OrderId == id);
        if (order == null)
        {
            return Ok();
        }
    
        order.TrackingState = TrackingState.Deleted;
        _dbContext.ApplyChanges(order);
    
        try
        {
            await _dbContext.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!_dbContext.Orders.Any(o => o.OrderId == order.OrderId))
            {
                return Conflict();
            }
            throw;
        }
    
        return Ok();
    }
  8. Finally, you’ll write code in the Client.ConsoleApp project to invoke controller actions on the WebApi service.
    • First, grab the dynamically generated port number from the WebApi 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-number8

    - Open Program.cs and replace the port number for the service base address.
    - Uncomment the client models “using” directive.
    - Then uncomment all the remaining code in Program.cs.

  9. At this point you should be able to build and run the client console application.

    • Right-click the ConsoleClient project, and select Set as Startup Project.
    • Press F5, or select Start Without Debugging from the Debug menu.
    • After pressing Enter to start the program, you should see a list of Customers returned by the Web API service.
    • Enter one of the listed customer id’s, for example, “ALFKI” and press Enter to see that customer’s orders.
    • Enter one of the order id’s listed, for example, 10643, to see the order and its details.
  10. So far we’ve just been retrieving entities.  Pressing Enter again will create a new order for the selected customer. This will invoke code that executes a POST operation to the Web API service.

    • Next add code that 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
    var changedOrder = changeTracker.GetChanges().SingleOrDefault();
    var updatedOrder = UpdateOrder(client, changedOrder);
    
    • 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 ASP.NET Web API.

Last edited Apr 28, 2014 at 12:51 PM by tonysneed, version 12