Walkthrough: Serialize Self-Tracking Entities
The walkthrough in this topic demonstrates the scenario in which a Windows Communication Foundation (WCF) service exposes a series of operations that return entity graphs. Next, a client application manipulates that graph and submits the modifications to a service operation that validates and saves the updates to a database using the Entity Framework. For more information see, Working with Self-Tracking Entities.
Typically, you will want to separate the model project from the project that contains the self-tracking entity types. Then, your client will only need to include the entity types project.
One way to achieve this isolation is to separate the self-tracking entity types from the model and move the template that generates the entity types into a separate class library. The location of the .edmx file will need to be included into the self-tracking entity template in order to access the metadata. If you move templates from the original project into other projects, you must open the template files in an editor and modify the inputFile string to the relative location of the .edmx file. For more information on working with templates, see ADO.NET Self-Tracking Entity Template.
Another way to separate the entity types from the model is to leave the template files in the original project, but disable the code generation for the templates. Link to the templates from a different project so the code is generated in that different project instead of the original one. When you add an item to a project as a link, the actual contents of the item are maintained in the location indicated by the original project. This method of separating the entity types from the model is demonstrated in this walkthrough.
This walkthrough completes the following actions:
Creates the class library project that contains the School-based model.
Uses the ADO.NET Self-Tracking Entity Generator template to generate the entity types, the typed ObjectContext, and an extension class that contains overloaded ApplyChanges methods.
Creates the class library project that links to the self-tracking entity types template that is created in the first project.
Creates the WCF service that exposes a set of operations that return entity graphs and applies changes made on the client to the database by using the Entity Framework.
Creates the client applications (console and Windows Presentation Foundation [WPF]) that manipulate the graph and submit the modifications by using the operations exposed on the WCF service.
Note
You can download the STESchoolModelExample on the Entity Framework Documentation Samples site on MSDN Code Gallery.
To create the class library project that contains the self-tracking entities and object context classes
Create a new class library project. Type STESchoolModel for the project and the solution name.
Remove the default source code file that was added to the project.
Use the Entity Data Model Wizard to generate a model based on the Department, Course, OnlineCourse, and OnsiteCourse tables in the School database. For more information, see School Model.
Open the .edmx file in the ADO.NET Entity Data Model Designer (Entity Designer).
Follow the inheritance mapping instructions in the Walkthrough: Mapping Inheritance - Table-per-Type topic.
Right-click an empty area on the Entity Designer surface, point to Add Code Generation Item, and then select ADO.NET Self-Tracking Entity Generator. Change the default template name to SchoolModel.
Note
When the template files are added to the project, a security warning may pop up asking you to accept only if you trust the source of the template. Click Accept.
The SchoolModel.Context.tt and SchoolModel.tt folders are added to the project. Under the SchoolModel.Context.tt folder, there are two files that define typed ObjectContext and the extension class that contains overloaded ApplyChanges methods. Under the SchoolModel.tt folder, there are files that define entity types and also a helper class that contains the change-tracking logic used by the self-tracking entities.
The next two steps illustrate disabling code generation in this project. The code generation will be later enabled for the types in the STESchoolModelTypes class library and for the object context in the STESchoolModelService.
Select SchoolModel.tt. In the Properties window, clear TextTemplatingFileGenerator from the CustomTool property. Delete the files under the SchoolModel.tt folder.
Select SchoolModel.Context.tt. In the Properties window, clear the value for the CustomTool property. Delete the files under the SchoolModel.Context.tt folder.
If you are working with the Visual Basic project, you might need to click Show All Files in Solution Explorer to see all files in the project.
Compile the project.
To create the class library project that links to the self-tracking types template
Create a new class library project named STESchoolModelTypes in the same solution as the previous project.
Remove the default source code file that was added to the project.
Add a link to the SchoolModel.tt file so that the self-tracking entity types are generated in this solution. In Solution Explorer, right-click STESchoolModelTypes, click Add, and then click Existing Item.
In the Add Existing Item dialog box, browse to the STESchoolModel project, and click SchoolModel.tt (do not press ENTER). In the Add list, select Add as Link.
Add a reference to the System.Runtime.Serialization library. This library is needed for the WCF DataContract and DataMember attributes that are used on the serializable entity types.
Compile the project.
To create and configure the WCF Service Application project
Create a WCF Service Application project named STESchoolModelService in the same solution as the previous project.
Add a reference to System.Data.Entity.dll.
Add a reference to the STESchoolModel and STESchoolModelTypes projects.
Add a link to the SchoolModel.Context.tt file so that the context types are generated in this solution. In Solution Explorer, right-click STESchoolModelService, click Add, and then click Existing Item.
In the Add Existing Item dialog, browse to the STESchoolModel project and click SchoolModel.Context.tt (do not press Enter). In the Add list, select Add as Link.
In the Properties window for the SchoolModel.Context.tt file, type STESchoolModelTypes in the Custom Tool Namespace property. This adds the object context type to the same namespace as the namespace for the self-tracking entity types, which is required.
(Visual Basic only) Add
Import STESchoolModelTypes
to the source files generated from SchoolModel.Context.tt file. Open the SchoolModel.Context.tt file and find theImports System
string. AddImport STESchoolModelTypes
after other imports. The generated source files will include this namespace.Add the connection string to the Web.config file so that the Entity Framework runtime can find the metadata. Open the app.config file for the STESchoolModel project. Copy the connectionStrings element, and then add it as a child element of the configuration element of the Web.config file.
Open the service interface file. By default, it is called IService1.
Add the namespace where the self-tracking entities are defined: STESchoolModelTypes.
Replace the service interface definition with the following code:
<ServiceContract()> _ Public Interface IService1 <OperationContract()> _ Sub UpdateDepartment(ByVal updated As Department) <OperationContract()> _ Function GetDepartments() As List(Of Department) End Interface
[ServiceContract] public interface IService1 { [OperationContract] void UpdateDepartment(Department updated); [OperationContract] List<Department> GetDepartments(); }
Open the service source code. By default, it is called Service1.srv.cs or Service1.srv.vb.
Add the namespace where the self-tracking entities are defined: STESchoolModelTypes.
(Visual Basic only) Add
Imports STESchoolModelService.STESchoolModelTypes
to the Service1.srv.cs file.Replace the service class definition with the following code:
Note: |
---|
You should always perform validation on the updated object before applying the changes. |
Public Class Service1
Implements IService1
''' <summary>
''' Updates department and its related courses.
''' </summary>
Public Sub UpdateDepartment(ByVal updated As Department) Implements IService1.UpdateDepartment
Using context As New STESchoolModelTypes.SchoolEntities()
Try
' Perform validation on the updated order before applying the changes.
' The ApplyChanges method examines the change tracking information
' contained in the graph of self-tracking entities to infer the set of operations
' that need to be performed to reflect the changes in the database.
context.Departments.ApplyChanges(updated)
context.SaveChanges()
Catch ex As UpdateException
' To avoid propagating exception messages that contain sensitive data to the client tier,
' calls to ApplyChanges and SaveChanges should be wrapped in exception handling code.
Throw New InvalidOperationException("Failed to update the department. Try your request again.")
End Try
End Using
End Sub
''' <summary>
''' Gets all the departments and related courses.
''' </summary>
Public Function GetDepartments() As List(Of Department) Implements IService1.GetDepartments
Using context As New STESchoolModelTypes.SchoolEntities()
' Use System.Data.Objects.ObjectQuery(T).Include to eagrly load the related courses.
Return context.Departments.Include("Courses").OrderBy(Function(d) d.Name).ToList()
End Using
End Function
End Class
public class Service1 : IService1
{
/// <summary>
/// Updates department and its related courses.
/// </summary>
public void UpdateDepartment(Department updated)
{
using (SchoolEntities context =
new SchoolEntities())
{
try
{
// Perform validation on the updated order before applying the changes.
// The ApplyChanges method examines the change tracking information
// contained in the graph of self-tracking entities to infer the set of operations
// that need to be performed to reflect the changes in the database.
context.Departments.ApplyChanges(updated);
context.SaveChanges();
}
catch (UpdateException ex)
{
// To avoid propagating exception messages that contain sensitive data to the client tier,
// calls to ApplyChanges and SaveChanges should be wrapped in exception handling code.
throw new InvalidOperationException("Failed to update the department. Try your request again.");
}
}
}
/// <summary>
/// Gets all the departments and related courses.
/// </summary>
public List<Department> GetDepartments()
{
using (SchoolEntities context = new SchoolEntities())
{
// Use System.Data.Objects.ObjectQuery(T).Include to eagrly load the related courses.
return context.Departments.Include("Courses").OrderBy(d => d.Name).ToList();
}
}
}
- Compile the project.
To test the service with a console client application
Create a console application. Type STESchoolModelTest for the project name, in the same solution as the previous project.
Add a reference to the STEASchoolModelService service. To add a reference to the service, in Solution Explorer, right-click the reference folder, and then select Add Service Reference.
By default, WCF generates a proxy that returns the IEnumerable collection. Because STESchoolModelService's GetDepartments method returns List, you must configure the service to specify the appropriate return type.
Right-click the service name (ServiceReference1), and then select Configure Service Reference…. In the Configure Service Reference dialog, select the System.Collections.Generic.List type from the Collection type list.
Add a reference to the STESchoolModelTypes project.
Open the app.config file, and then add the connection string to the file. Open the app.config file of the STESchoolModel projects, and then copy the connectionStrings element to add it as a child element of the configuration element of the Web.config file.
Open the file that contains the main function. Include the following namespaces: STESchoolModelTest.ServiceReference1 and the STESchoolModelTypes namespaces (where the self-tracking types are defined).
Paste the following code into your main function. The code contains function calls to the methods that are defined in the next step.
' Note, the service's GetDepartments method returns System.Collections.Generic.List. ' By default, when WCF generates a proxy the return collection types are converted to IEnumerable. ' The WCF service has to be configured to specify the List return type. ' To specify the List collection type, open the Configure Service Reference dialog and ' select the System.Collections.Generic.List type from the Collection type list. Console.WriteLine("See the existing departments and courses.") DisplayDepartmentsAndCourses() Console.WriteLine() Console.WriteLine() ' Use some IDs to create ' new Department and Course. ' The newly created objects will ' be then deleted. Dim departmentID As Integer = 100 Dim courseID As Integer = 50 AddNewDepartmentAndCourses(departmentID, courseID) Console.WriteLine("See existing and added.") DisplayDepartmentsAndCourses() Console.WriteLine() UpdateDepartmentAndCourses(departmentID, courseID) Console.WriteLine("See existing and updated.") DisplayDepartmentsAndCourses() Console.WriteLine() DeleteDepartmentAndCourses(departmentID)
// Note, the service's GetDepartments method returns System.Collections.Generic.List. // By default, when WCF generates a proxy the return collection types are converted to IEnumerable. // The WCF service has to be configured to specify the List return type. // To specify the List collection type, open the Configure Service Reference dialog and // select the System.Collections.Generic.List type from the Collection type list. Console.WriteLine("See the existing departments and courses."); DisplayDepartmentsAndCourses(); Console.WriteLine(); Console.WriteLine(); // Use some IDs to create // new Department and Course. // The newly created objects will // be then deleted. int departmentID = 100; int courseID = 50; AddNewDepartmentAndCourses(departmentID, courseID); Console.WriteLine("See existing and added."); DisplayDepartmentsAndCourses(); Console.WriteLine(); UpdateDepartmentAndCourses(departmentID, courseID); Console.WriteLine("See existing and updated."); DisplayDepartmentsAndCourses(); Console.WriteLine(); DeleteDepartmentAndCourses(departmentID);
Add the following methods to the class. The methods show you how to do the following: display the objects returned by the service, add new objects, update objects, and delete objects. For more information, see the code comments.
Private Sub DisplayDepartmentsAndCourses() Using service = New Service1Client() ' Get all the departments. Dim departments As List(Of Department) = service.GetDepartments() For Each d In departments Console.WriteLine("ID: {0}, Name: {1}", d.DepartmentID, d.Name) ' Get all the courses for each department. ' The reason we are able to access ' the related courses is because the service eagrly loaded the related objects ' (using the System.Data.Objects.ObjectQuery(T).Include method). For Each c In d.Courses.OfType(Of OnlineCourse)() Console.WriteLine(" OnLineCourse ID: {0}, Title: {1}", c.CourseID, c.Title) Next For Each c In d.Courses.OfType(Of OnsiteCourse)() Console.WriteLine(" OnSiteCourse ID: {0}, Title: {1}", c.CourseID, c.Title) Next Next End Using End Sub Private Sub AddNewDepartmentAndCourses(ByVal departmentID As Integer, ByVal courseID As Integer) Using service = New Service1Client() Dim newDepartment As New Department() _ With {.DepartmentID = departmentID, _ .Budget = 13000D, _ .Name = "New Department", _ .StartDate = DateTime.Now _ } Dim newCourse As New OnlineCourse() _ With {.CourseID = courseID, _ .DepartmentID = departmentID, _ .URL = "http://www.fineartschool.net/Trigonometry", _ .Title = "New Onsite Course", _ .Credits = 4 _ } ' Add the course to the department. newDepartment.Courses.Add(newCourse) ' The newly create objects are marked as added, the service will insert these into the store. service.UpdateDepartment(newDepartment) ' Let’s make few more changes to the saved object. ' Since the previous changes have now been persisted, call AcceptChanges to ' reset the ChangeTracker on the objects and mark the state as ObjectState.Unchanged. ' Note, AcceptChanges sets the tracking on, so you do not need to call StartTracking ' explicitly. newDepartment.AcceptChanges() newCourse.AcceptChanges() ' Because the change tracking is enabled ' the following change will set newCourse.ChangeTracker.State to ObjectState.Modified. newCourse.Credits = 6 service.UpdateDepartment(newDepartment) End Using End Sub Private Sub UpdateDepartmentAndCourses(ByVal departmentID As Integer, ByVal courseID As Integer) Using service = New Service1Client() ' Get all the departments. Dim departments As List(Of Department) = service.GetDepartments() ' Use LINQ to Objects to query the departments collection ' for the specific department object. Dim department As Department = departments.Single(Function(d) d.DepartmentID = departmentID) department.Budget = department.Budget - 1000D ' Get the specified course that belongs to the department. ' The reason we are able to access the related course ' is because the service eagrly loaded the related objects ' (using the System.Data.Objects.ObjectQuery(T).Include method). Dim existingCourse As Course = department.Courses.[Single](Function(c) c.CourseID = courseID) existingCourse.Credits = 3 service.UpdateDepartment(department) End Using End Sub Private Sub DeleteDepartmentAndCourses(ByVal departmentID As Integer) Using service = New Service1Client() Dim departments As List(Of Department) = service.GetDepartments() Dim department As Department = departments.Single(Function(d) d.DepartmentID = departmentID) ' When MarkAsDeleted is called, the entity is removed from the collection, ' if we modify the collection over which foreach is looping an exception will be thrown. ' That is why we need to make a copy of the courses collection by ' calling department.Courses.ToList(); Dim courses As List(Of Course) = department.Courses.ToList() For Each c In courses ' Marks each comment for the post as Deleted. ' If another entity have a foreign key relationship with this Course object ' an exception will be thrown during save operation. c.MarkAsDeleted() Next department.MarkAsDeleted() service.UpdateDepartment(department) End Using End Sub
static void DisplayDepartmentsAndCourses() { using (var service = new Service1Client()) { // Get all the departments. List<Department> departments = service.GetDepartments(); foreach (var d in departments) { Console.WriteLine("ID: {0}, Name: {1}", d.DepartmentID, d.Name); // Get all the courses for each department. // The reason we are able to access // the related courses is because the service eagrly loaded the related objects // (using the System.Data.Objects.ObjectQuery(T).Include method). foreach (var c in d.Courses.OfType<OnlineCourse>()) { Console.WriteLine(" OnLineCourse ID: {0}, Title: {1}", c.CourseID, c.Title); } foreach (var c in d.Courses.OfType<OnsiteCourse>()) { Console.WriteLine(" OnSiteCourse ID: {0}, Title: {1}", c.CourseID, c.Title); } } } } static void AddNewDepartmentAndCourses(int departmentID, int courseID) { using (var service = new Service1Client()) { Department newDepartment = new Department() { DepartmentID = departmentID, Budget = 13000.000m, Name = "New Department", StartDate = DateTime.Now }; OnlineCourse newCourse = new OnlineCourse() { CourseID = courseID, DepartmentID = departmentID, URL = "http://www.fineartschool.net/Trigonometry", Title = "New Onsite Course", Credits = 4 }; // Add the course to the department. newDepartment.Courses.Add(newCourse); // The newly create objects are marked as added, the service will insert these into the store. service.UpdateDepartment(newDepartment); // Let’s make few more changes to the saved object. // Since the previous changes have now been persisted, call AcceptChanges to // reset the ChangeTracker on the objects and mark the state as ObjectState.Unchanged. // Note, AcceptChanges sets the tracking on, so you do not need to call StartTracking // explicitly. newDepartment.AcceptChanges(); newCourse.AcceptChanges(); // Because the change tracking is enabled // the following change will set newCourse.ChangeTracker.State to ObjectState.Modified. newCourse.Credits = 6; service.UpdateDepartment(newDepartment); } } static void UpdateDepartmentAndCourses(int departmentID, int courseID) { using (var service = new Service1Client()) { // Get all the departments. List<Department> departments = service.GetDepartments(); // Use LINQ to Objects to query the departments collection // for the specific department object. Department department = departments.Single(d => d.DepartmentID == departmentID); department.Budget = department.Budget - 1000.00m; // Get the specified course that belongs to the department. // The reason we are able to access the related course // is because the service eagrly loaded the related objects // (using the System.Data.Objects.ObjectQuery(T).Include method). Course existingCourse = department.Courses.Single(c => c.CourseID == courseID); existingCourse.Credits = 3; service.UpdateDepartment(department); } } static void DeleteDepartmentAndCourses(int departmentID) { using (var service = new Service1Client()) { List<Department> departments = service.GetDepartments(); Department department = departments.Single(d => d.DepartmentID == departmentID); // When MarkAsDeleted is called, the entity is removed from the collection, // if we modify the collection over which foreach is looping an exception will be thrown. // That is why we need to make a copy of the courses collection by // calling department.Courses.ToList(); List<Course> courses = department.Courses.ToList(); foreach (var c in courses) { // Marks each comment for the post as Deleted. // If another entity have a foreign key relationship with this Course object // an exception will be thrown during save operation. c.MarkAsDeleted(); } department.MarkAsDeleted(); service.UpdateDepartment(department); } }
To test the service with a WPF client application
Create a WPF application. Type STESchoolModelWPFTest for the project name, in the same solution as the previous project.
Add a reference to the STEASchoolModelService service. To add a reference to the service, in Solution Explorer, right-click the reference folder, and then select Add Service Reference.
By default, WCF generates a proxy that returns the IEnumerable collection. Because STESchoolModelService's GetDepartments method returns List, you must configure the service to specify the appropriate return type.
Right-click the service name (ServiceReference1), and then select Configure Service Reference…. In the Configure Service Reference dialog, select the System.Collections.Generic.List type from the Collection type list.
Add a reference to the STESchoolModelTypes project.
Open the app.config file, and then add the connection string to the file. Open the app.config file of the STESchoolModel projects, and then copy the connectionStrings element to add it as a child element of the configuration element of the Web.config file.
You can now delete the app.config file for the STESchoolModel project because it is never used.
By default, the project template adds MainWindow.xaml file and the corresponding code behind file to the project.
Open the MainWindow.xaml, and then replace the default XAML code with the XAML that defines the STESchoolModelWPFTest window in WPF. For more information, see the code comments.
<Window x:Class="STESchoolModelWPFTest.MainWindow" xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="508" Width="919" Loaded="Window_Loaded"> <!-- The code begind code sets the departmentsItemsGrid to the root of the object graph.--> <Grid Name="departmentsItemsGrid"> <!-- comboBoxDepartment points to the root of the graph, that is why the Path is not specified--> <ComboBox DisplayMemberPath="DepartmentID" ItemsSource="{Binding}" IsSynchronizedWithCurrentItem="true" Height="23" Margin="122,12,198,0" Name="comboBoxDepartment" VerticalAlignment="Top"/> <!-- listViewItems Path is set to Courses because it is bound to Department.Courses.--> <ListView ItemsSource="{Binding Path=Courses}" Name="listViewItems" Margin="34,46,34,50" > <ListView.View> <GridView AllowsColumnReorder="False" ColumnHeaderToolTip="Courses" > <GridViewColumn DisplayMemberBinding="{Binding Path=CourseID}" Header="CourseID" Width="70"/> <!--The TextBox controls are embedded in the two of the following columns. This is done to enable editing in the ListView control. --> <GridViewColumn Header="Title" Width="100"> <GridViewColumn.CellTemplate> <DataTemplate> <TextBox Height="25" Width="100" Text="{Binding Path=Title}" /> </DataTemplate> </GridViewColumn.CellTemplate> </GridViewColumn> <GridViewColumn Header="Credits" Width="100" > <GridViewColumn.CellTemplate> <DataTemplate> <TextBox Height="25" Width="100" Text="{Binding Path=Credits}" /> </DataTemplate> </GridViewColumn.CellTemplate> </GridViewColumn> </GridView> </ListView.View> </ListView> <Label Height="28" Margin="34,12,0,0" Name="departmentLabel" VerticalAlignment="Top" HorizontalAlignment="Left" Width="93">Department:</Label> <!--When the Save and Close button is clicked all the objects will be sent to the service where all the updated objects will be saved to the database. --> <Button Height="23" HorizontalAlignment="Right" Margin="0,0,34,12" Name="buttonClose" VerticalAlignment="Bottom" Width="127" Click="buttonClose_Click">Save and Close</Button> </Grid> </Window>
Open the MainWindow.xaml.cs (or .vb) file, and then replace the default code behind with the following code (see the code comments for more explanation).
Class MainWindow Dim departments As List(Of Department) Private Sub Window_Loaded(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles MyBase.Loaded Using service = New Service1Client() ' Set the parent of of your data bound controls to the root of the graph. ' In the xaml page the appropriate paths should be set on each data bound control. ' For the comboBoxDepartment it is empty because it is bound to Departments (which is root). ' For the listViewItems it is set to Courses because it is bound to Department.Courses. ' Note, that the TextBox controls are embedded in the two of the columns in the listViewItems. ' This is done to enable editing in the ListView control. departments = service.GetDepartments() Me.departmentsItemsGrid.DataContext = departments End Using End Sub Private Sub buttonSave_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles buttonSave.Click Using service = New Service1Client() ' Save all the departments and their courses. For Each department In departments service.UpdateDepartment(department) ' Call AcceptChanges on all the objects ' to resets the change tracker and set the state of the objects to Unchanged. department.AcceptChanges() For Each course In department.Courses course.AcceptChanges() Next Next End Using End Sub Private Sub buttonClose_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles buttonClose.Click ' Close the form. Me.Close() End Sub End Class
public partial class MainWindow : Window { private List<Department> departments; public MainWindow() { InitializeComponent(); } private void Window_Loaded(object sender, RoutedEventArgs e) { using (var service = new Service1Client()) { // Set the parent of of your data bound controls to the root of the graph. // In the xaml page the appropriate paths should be set on each data bound control. // For the comboBoxDepartment it is empty because it is bound to Departments (which is root). // For the listViewItems it is set to Courses because it is bound to Department.Courses. // Note, that the TextBox controls are embedded in the two of the columns in the listViewItems. // This is done to enable editing in the ListView control. departments = service.GetDepartments(); this.departmentsItemsGrid.DataContext = departments; } } private void buttonSave_Click(object sender, RoutedEventArgs e) { using (var service = new Service1Client()) { // Save all the departments and their courses. foreach (var department in departments) { service.UpdateDepartment(department); // Call AcceptChanges on all the objects // to resets the change tracker and set the state of the objects to Unchanged. department.AcceptChanges(); foreach (var course in department.Courses) course.AcceptChanges(); } } } private void buttonClose_Click(object sender, RoutedEventArgs e) { //Close the form. this.Close(); } }
See Also
Concepts
Working with Self-Tracking Entities
Serializing Objects
Building N-Tier Applications