There are a lot of Silverlight + RIA samples all over the internet and many of these will happily show you how to properly build functionality with these emerging technologies. Unfortunately, they all seem to be built on the notion that you will be using Microsoft’s Entity Framework with SQL Server for your database needs. Well, where I work, we are tied to an Oracle database and thus the Entity Framework is not really an option. So how could we make this work? I’m going to show you.

Disclaimer: I don’t proclaim to be a Silverlight or RIA expert. This is just a account of what we discovered when building applications with Silverlight and RIA Services against an Oracle database.

The source code for this (and related posts) can be found here. This is a Visual Studio 2010 Solution.

The Application

Silverlight Aggregate Application

This is a simple ticketing application example. Companies use these sorts of applications to track call and customer interactions. Operations you can perform:

  • Open a Ticket
  • Create a new Ticket
  • Search for a Customer when creating a new Ticket
  • Log a Ticket Action
  • View the History of Ticket Actions
  • Edit Customer data when saving a Ticket
  • Edit Product (via dropdown selection) on an existing Ticket

In our production environment we write these applications against an Oracle database. For the purposes of this demo, everything is stored in memory. But the data access part of the RIA Service is abstracted, so you should be able to see how easy it is to hand operations off to a data access layer to work against your preferred database.

Complex Aggregate Roots

To start with, many of the samples for Silverlight and RIA on the internet are very simple. They show basic entities in relatively simple scenarios. A lot of the “magic” happens by just calling on the Entity Framework inside the RIA Services context. We all know by now that’s not the way the real world works, and the first time you try and do something outside the scope of a demo you run into headaches.

What I am going to show you here is a complex root aggregate object that is actually structurally identical to one we used in our real production application. I’ve trimmed it down some (removed a few entities for brevity), but the structure is the same. The core pieces to this aggregate are such:

  • A root parent object
  • Multiple child objects contained in the root
  • A List of child objects
  • Child objects on the list of child objects

Does that sound more like a real world scenario to you? Good, me too. Here’s an object graph to help visualize:

Root Aggregate

Each child object represents a table in an Oracle database. Our goal with our RIA service is to be able to query the child objects (Product, Customer, TicketAction, ActionType, ResultType) separately (so we can fill in dropdowns and forms) and to be able to query and save the root aggregate (Ticket) as an entire object graph all at once. To make this happen, we have to do two things.

First, we have to setup the metadata correctly so that the associations between the objects can be understood by RIA, because RIA is going to do a lot of magic for us. Second, we have to develop the code inside our RIA Service methods properly so that data gets inserted/updated in the proper order when we’re saving an object graph.

Metadata & Foreign Keys

In a typical database scenario, you’re going to have a parent table that will contain the foreign keys of the child tables. However, when you want to actually work with this data in code, you don’t want just the keys to those children showing up in your root object/entity – you want the whole child object/entity. When using an O/RM this sort of thing is handled for us automatically. But when you’re using more primitive database access technologies (ADO.NET with Oracle) then you have to account for these facts yourself. With RIA Services we can make this happen, but we have to provide the parent object with both pieces of data: the foreign key to the child object AND the child object itself.

  [MetadataType(typeof (TicketMetadata))]
  public partial class Ticket
  {
    internal sealed class TicketMetadata
    {
      [Key] public int TicketId;

      [Required]
      public DateTime IncidentDate;

      public int CustomerId;

      [Required]
      [Range(1, 99999999, ErrorMessage = "Must select a Product")]
      public int ProductId; 

      [Include]
      [Association("Ticket_Customer", "CustomerId", "CustomerId", IsForeignKey = true)]
      public Customer Customer;

      [Include]
      [Association("Ticket_Product", "ProductId", "ProductId", IsForeignKey = true)]
      public Product Product;

      [Include]
      [Composition]
      [Association("Ticket_TicketActions", "TicketId", "TicketId")]
      public List TicketActions;
    }
  }

As you can see, we have the ProductId and Product as properties on the Ticket (same with CustomerId and Customer). We instruct RIA to associate these two things via this line:

[Include]
[Association("Ticket_Product", "ProductId", "ProductId", IsForeignKey = true)]
public Product Product;

The important part here is to set the IsForeignKey = true. RIA Services will use this to association to copy key values among related entities. So, for instance, when you set a Product with a ProductID = 3 on the Ticket (parent) object, RIA’s proxy class for Ticket knows to automatically set the Ticket.ProductId = 3.

Collections

Collections are handled slightly differently from individual child objects. There are a couple of ways to deal with them, and your choices depends on whether or not you want your child objects in the collection to be accessible as separate methods on the RIA Service, or only through the parent object. In our case, we wanted these particular child objects to only be accessible via the parent object, so we utilize the Composition attribute. The Composition attribute is well documented. The highlights:

  • Hierarchical change tracking – when a child entity is modified, it’s parent also transitions to the Modified state.
  • When a parent is in the Modified state, all of its children are included in the change-set sent to the server, including any Unmodified children.
  • Operation ordering – Often only CRUD operations for the parent or root type in a compositional hierarchy will be exposed by a DomainService. This allows you to write all your business logic for a hierarchy in a single method. However writing explicit methods for child Types is supported, in which case parent operations are always called before child operations. For example, if a new OrderDetail was added to an existing PurchaseOrder, the Update method for PurchaseOrder would be called before the Insert for the OrderDetail.
  • Public EntitySets for child Types are not generated on the code-genned DomainContext. Children are only accessible via their parent relationship.

Take note of the third item: parent operations are always called before child operations. When you’re writing RIA Services, order of operations is important. As you begin to develop your data access code (whether it’s Oracle or some other DB) it is important to pay attention to the order of the calls as RIA makes them. Returning surrogate keys from INSERT operations will be important when dealing with parent and child data.

The RIA Service

The nuts and bolts of this application lies in the service layer. I’m going to post the code so you can see how simple it is. In the next post, I’ll talk about order of operations – how the calls get made by RIA and in what order, and why you have to do things a certain way.

  [EnableClientAccess()]
  public class TicketService : DomainService
  {
    private TicketDataSource _ticketDataSource;
    private CustomerDataSource _customerDataSource;
    private ProductDataSource _productDataSource;
    private ActionTypeDataSource _actionTypeDataSource;
    private ResultTypeDataSource _resultTypeDataSource;
    private TicketActionDataSource _ticketActionDataSource;

    public TicketService()
    {
      _ticketDataSource = new TicketDataSource();
      _customerDataSource = new CustomerDataSource();
      _productDataSource = new ProductDataSource();
      _actionTypeDataSource = new ActionTypeDataSource();
      _resultTypeDataSource = new ResultTypeDataSource();
      _ticketActionDataSource = new TicketActionDataSource();
    }

    public IQueryable GetTicketById(int id)
    {
      var tickets = _ticketDataSource.GetTickets();
      return tickets.Where(x => x.TicketId == id).AsQueryable();
    }

    public IQueryable GetTickets()
    {
      return _ticketDataSource.GetTickets().AsQueryable();
    }

    public void Insert(Ticket ticket)
    {
      if (ticket.CustomerId == 0)
        ticket.CustomerId = ticket.Customer.CustomerId;

      ticket.TicketId = _ticketDataSource.Insert(ticket);

      foreach (var ticketAction in ticket.TicketActions)
      {
        ticketAction.TicketId = ticket.TicketId;
      }
    }

    public void Update(Ticket ticket)
    {
      foreach (var ticketAction in ticket.TicketActions)
      {
        if (ticketAction.IsNew)
          ticketAction.TicketId = ticket.TicketId;
      }

      _ticketDataSource.Update(ticket);
    }

    public void InsertTicketAction(TicketAction ticketAction)
    {
      ticketAction.TicketActionId = _ticketActionDataSource.Insert(ticketAction);
    }

    public IQueryable GetProducts()
    {
      return _productDataSource.GetProducts().AsQueryable();
    }

    public IQueryable GetActionTypes()
    {
      return _actionTypeDataSource.GetActionTypes().AsQueryable();
    }

    public IQueryable GetResultTypes()
    {
      return _resultTypeDataSource.GetResultTypes().AsQueryable();
    }

    public IQueryable GetCustomers()
    {
      return _customerDataSource.GetCustomers().AsQueryable();
    }

    public IQueryable GetCustomersBySearchCriteria(string firstName, string lastName, string phone)
    {
      return _customerDataSource.GetCustomersBySearchCriteria(firstName, lastName, phone).AsQueryable();
    }

    public void Insert(Customer customer)
    {
      _customerDataSource.Insert(customer);
    }

    public void Update(Customer customer)
    {
      _customerDataSource.Update(customer);
    }
  }

This post is already long. Part 2 coming soon!

Leave a Reply

You must be logged in to post a comment.