OFFICIAL
MICROSOFT
LEARNING
PRODUCT
10265A Developing Data Access Solutions with Microsoft Visual Studio 2010 ®
®
Be sure to access the extended learning content on your Course Companion CD enclosed on the back cover of the book.
ii
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Information in this document, including URL and other Internet Web site references, is subject to change without notice. Unless otherwise noted, the example companies, organizations, products, domain names, e-mail addresses, logos, people, places, and events depicted herein are fictitious, and no association with any real company, organization, product, domain name, e-mail address, logo, person, place or event is intended or should be inferred. Complying with all applicable copyright laws is the responsibility of the user. Without limiting the rights under copyright, no part of this document may be reproduced, stored in or introduced into a retrieval system, or transmitted in any form or by any means (electronic, mechanical, photocopying, recording, or otherwise), or for any purpose, without the express written permission of Microsoft Corporation. Microsoft may have patents, patent applications, trademarks, copyrights, or other intellectual property rights covering subject matter in this document. Except as expressly provided in any written license agreement from Microsoft, the furnishing of this document does not give you any license to these patents, trademarks, copyrights, or other intellectual property. The names of manufacturers, products, or URLs are provided for informational purposes only and Microsoft makes no representations and warranties, either expressed, implied, or statutory, regarding these manufacturers or the use of the products with any Microsoft technologies. The inclusion of a manufacturer or product does not imply endorsement of Microsoft of the manufacturer or product. Links may be provided to third party sites. Such sites are not under the control of Microsoft and Microsoft is not responsible for the contents of any linked site or any link contained in a linked site, or any changes or updates to such sites. Microsoft is not responsible for webcasting or any other form of transmission received from any linked site. Microsoft is providing these links to you only as a convenience, and the inclusion of any link does not imply endorsement of Microsoft of the site or the products contained therein. © 2010 Microsoft Corporation. All rights reserved. Microsoft, Excel, Hyper-V, IntelliSense, Internet Explorer, MSDN, SharePoint, Silverlight, SQL Server, Visual Basic, Visual C#, Visual Studio, Windows, Windows Azure, and Windows Server are either registered trademarks or trademarks of Microsoft Corporation in the United States and/or other countries. All other trademarks are property of their respective owners.
Product Number: 10265A Part Number: 06380 Released: 10/2010
MICROSOFT LICENSE TERMS OFFICIAL MICROSOFT LEARNING PRODUCTS - TRAINER EDITION – Pre-Release and Final Release Versions These license terms are an agreement between Microsoft Corporation and you. Please read them. They apply to the Licensed Content named above, which includes the media on which you received it, if any. The terms also apply to any Microsoft •
updates,
•
supplements,
•
Internet-based services, and
•
support services
for this Licensed Content, unless other terms accompany those items. If so, those terms apply. By using the Licensed Content, you accept these terms. If you do not accept them, do not use the Licensed Content. If you comply with these license terms, you have the rights below.
1. DEFINITIONS. a. “Academic Materials” means the printed or electronic documentation such as manuals,
workbooks, white papers, press releases, datasheets, and FAQs which may be included in the Licensed Content.
b. “Authorized Learning Center(s)” means a Microsoft Certified Partner for Learning Solutions
location, an IT Academy location, or such other entity as Microsoft may designate from time to time.
c. “Authorized Training Session(s)” means those training sessions authorized by Microsoft and
conducted at or through Authorized Learning Centers by a Trainer providing training to Students solely on Official Microsoft Learning Products (formerly known as Microsoft Official Curriculum or “MOC”) and Microsoft Dynamics Learning Products (formerly know as Microsoft Business Solutions Courseware). Each Authorized Training Session will provide training on the subject matter of one (1) Course.
d. “Course” means one of the courses using Licensed Content offered by an Authorized Learning Center during an Authorized Training Session, each of which provides training on a particular Microsoft technology subject matter.
e. “Device(s)” means a single computer, device, workstation, terminal, or other digital electronic or analog device.
f.
“Licensed Content” means the materials accompanying these license terms. The Licensed Content may include, but is not limited to, the following elements: (i) Trainer Content, (ii) Student Content, (iii) classroom setup guide, and (iv) Software. There are different and separate components of the Licensed Content for each Course.
g.
“Software” means the Virtual Machines and Virtual Hard Disks, or other software applications that may be included with the Licensed Content.
h. “Student(s)” means a student duly enrolled for an Authorized Training Session at your location.
i.
“Student Content” means the learning materials accompanying these license terms that are for use by Students and Trainers during an Authorized Training Session. Student Content may include labs, simulations, and courseware files for a Course.
j.
“Trainer(s)” means a) a person who is duly certified by Microsoft as a Microsoft Certified Trainer and b) such other individual as authorized in writing by Microsoft and has been engaged by an Authorized Learning Center to teach or instruct an Authorized Training Session to Students on its behalf.
k. “Trainer Content” means the materials accompanying these license terms that are for use by
Trainers and Students, as applicable, solely during an Authorized Training Session. Trainer Content may include Virtual Machines, Virtual Hard Disks, Microsoft PowerPoint files, instructor notes, and demonstration guides and script files for a Course.
l.
“Virtual Hard Disks” means Microsoft Software that is comprised of virtualized hard disks (such as a base virtual hard disk or differencing disks) for a Virtual Machine that can be loaded onto a single computer or other device in order to allow end-users to run multiple operating systems concurrently. For the purposes of these license terms, Virtual Hard Disks will be considered “Trainer Content”.
m. “Virtual Machine” means a virtualized computing experience, created and accessed using
Microsoft® Virtual PC or Microsoft® Virtual Server software that consists of a virtualized hardware environment, one or more Virtual Hard Disks, and a configuration file setting the parameters of the virtualized hardware environment (e.g., RAM). For the purposes of these license terms, Virtual Hard Disks will be considered “Trainer Content”.
n.
“you” means the Authorized Learning Center or Trainer, as applicable, that has agreed to these license terms.
2. OVERVIEW. Licensed Content. The Licensed Content includes Software, Academic Materials (online and electronic), Trainer Content, Student Content, classroom setup guide, and associated media. License Model. The Licensed Content is licensed on a per copy per Authorized Learning Center location or per Trainer basis.
3. INSTALLATION AND USE RIGHTS. a. Authorized Learning Centers and Trainers: For each Authorized Training Session, you may: i.
either install individual copies of the relevant Licensed Content on classroom Devices only for use by Students enrolled in and the Trainer delivering the Authorized Training Session, provided that the number of copies in use does not exceed the number of Students enrolled in and the Trainer delivering the Authorized Training Session, OR
ii. install one copy of the relevant Licensed Content on a network server only for access by classroom Devices and only for use by Students enrolled in and the Trainer delivering the Authorized Training Session, provided that the number of Devices accessing the Licensed Content on such server does not exceed the number of Students enrolled in and the Trainer delivering the Authorized Training Session. iii. and allow the Students enrolled in and the Trainer delivering the Authorized Training Session to use the Licensed Content that you install in accordance with (ii) or (ii) above during such Authorized Training Session in accordance with these license terms.
i.
Separation of Components. The components of the Licensed Content are licensed as a single unit. You may not separate the components and install them on different Devices.
ii. Third Party Programs. The Licensed Content may contain third party programs. These license terms will apply to the use of those third party programs, unless other terms accompany those programs.
b. Trainers: i.
Trainers may Use the Licensed Content that you install or that is installed by an Authorized Learning Center on a classroom Device to deliver an Authorized Training Session.
ii. Trainers may also Use a copy of the Licensed Content as follows:
A. Licensed Device. The licensed Device is the Device on which you Use the Licensed Content. You may install and Use one copy of the Licensed Content on the licensed Device solely for your own personal training Use and for preparation of an Authorized Training Session.
B. Portable Device. You may install another copy on a portable device solely for your own personal training Use and for preparation of an Authorized Training Session.
4. PRE-RELEASE VERSIONS. If this is a pre-release (“beta”) version, in addition to the other provisions in this agreement, these terms also apply:
a. Pre-Release Licensed Content. This Licensed Content is a pre-release version. It may not
contain the same information and/or work the way a final version of the Licensed Content will. We may change it for the final, commercial version. We also may not release a commercial version. You will clearly and conspicuously inform any Students who participate in each Authorized Training Session of the foregoing; and, that you or Microsoft are under no obligation to provide them with any further content, including but not limited to the final released version of the Licensed Content for the Course.
b. Feedback. If you agree to give feedback about the Licensed Content to Microsoft, you give to
Microsoft, without charge, the right to use, share and commercialize your feedback in any way and for any purpose. You also give to third parties, without charge, any patent rights needed for their products, technologies and services to use or interface with any specific parts of a Microsoft software, Licensed Content, or service that includes the feedback. You will not give feedback that is subject to a license that requires Microsoft to license its software or documentation to third parties because we include your feedback in them. These rights survive this agreement.
c. Confidential Information. The Licensed Content, including any viewer, user interface, features
and documentation that may be included with the Licensed Content, is confidential and proprietary to Microsoft and its suppliers. i.
Use. For five years after installation of the Licensed Content or its commercial release, whichever is first, you may not disclose confidential information to third parties. You may disclose confidential information only to your employees and consultants who need to know the information. You must have written agreements with them that protect the confidential information at least as much as this agreement.
ii.
Survival. Your duty to protect confidential information survives this agreement.
iii. Exclusions. You may disclose confidential information in response to a judicial or governmental order. You must first give written notice to Microsoft to allow it to seek a
protective order or otherwise protect the information. Confidential information does not include information that •
becomes publicly known through no wrongful act;
•
you received from a third party who did not breach confidentiality obligations to Microsoft or its suppliers; or
•
you developed independently.
d.
Term. The term of this agreement for pre-release versions is (i) the date which Microsoft informs you is the end date for using the beta version, or (ii) the commercial release of the final release version of the Licensed Content, whichever is first (“beta term”).
e.
Use. You will cease using all copies of the beta version upon expiration or termination of the beta term, and will destroy all copies of same in the possession or under your control and/or in the possession or under the control of any Trainers who have received copies of the pre-released version.
f.
Copies. Microsoft will inform Authorized Learning Centers if they may make copies of the beta version (in either print and/or CD version) and distribute such copies to Students and/or Trainers. If Microsoft allows such distribution, you will follow any additional terms that Microsoft provides to you for such copies and distribution.
5. ADDITIONAL LICENSING REQUIREMENTS AND/OR USE RIGHTS. a. Authorized Learning Centers and Trainers: i.
Software.
ii. Virtual Hard Disks. The Licensed Content may contain versions of Microsoft XP, Microsoft Windows Vista, Windows Server 2003, Windows Server 2008, and Windows 2000 Advanced Server and/or other Microsoft products which are provided in Virtual Hard Disks. A. If the Virtual Hard Disks and the labs are launched through the Microsoft Learning Lab Launcher, then these terms apply: Time-Sensitive Software. If the Software is not reset, it will stop running based upon the time indicated on the install of the Virtual Machines (between 30 and 500 days after you install it). You will not receive notice before it stops running. You may not be able to access data used or information saved with the Virtual Machines when it stops running and may be forced to reset these Virtual Machines to their original state. You must remove the Software from the Devices at the end of each Authorized Training Session and reinstall and launch it prior to the beginning of the next Authorized Training Session. B. If the Virtual Hard Disks require a product key to launch, then these terms apply: Microsoft will deactivate the operating system associated with each Virtual Hard Disk. Before installing any Virtual Hard Disks on classroom Devices for use during an Authorized Training Session, you will obtain from Microsoft a product key for the operating system software for the Virtual Hard Disks and will activate such Software with Microsoft using such product key. C. These terms apply to all Virtual Machines and Virtual Hard Disks:
You may only use the Virtual Machines and Virtual Hard Disks if you comply with the terms and conditions of this agreement and the following security requirements: o
You may not install Virtual Machines and Virtual Hard Disks on portable Devices or Devices that are accessible to other networks.
o
You must remove Virtual Machines and Virtual Hard Disks from all classroom Devices at the end of each Authorized Training Session, except those held at Microsoft Certified Partners for Learning Solutions locations.
o
You must remove the differencing drive portions of the Virtual Hard Disks from all classroom Devices at the end of each Authorized Training Session at Microsoft Certified Partners for Learning Solutions locations.
o
You will ensure that the Virtual Machines and Virtual Hard Disks are not copied or downloaded from Devices on which you installed them.
o
You will strictly comply with all Microsoft instructions relating to installation, use, activation and deactivation, and security of Virtual Machines and Virtual Hard Disks.
o
You may not modify the Virtual Machines and Virtual Hard Disks or any contents thereof.
o
You may not reproduce or redistribute the Virtual Machines or Virtual Hard Disks.
ii. Classroom Setup Guide. You will assure any Licensed Content installed for use during an Authorized Training Session will be done in accordance with the classroom set-up guide for the Course. iii. Media Elements and Templates. You may allow Trainers and Students to use images, clip art, animations, sounds, music, shapes, video clips and templates provided with the Licensed Content solely in an Authorized Training Session. If Trainers have their own copy of the Licensed Content, they may use Media Elements for their personal training use. iv. iv Evaluation Software. Any Software that is included in the Student Content designated as “Evaluation Software” may be used by Students solely for their personal training outside of the Authorized Training Session.
b. Trainers Only: i.
Use of PowerPoint Slide Deck Templates. The Trainer Content may include Microsoft PowerPoint slide decks. Trainers may use, copy and modify the PowerPoint slide decks only for providing an Authorized Training Session. If you elect to exercise the foregoing, you will agree or ensure Trainer agrees: (a) that modification of the slide decks will not constitute creation of obscene or scandalous works, as defined by federal law at the time the work is created; and (b) to comply with all other terms and conditions of this agreement.
ii. Use of Instructional Components in Trainer Content. For each Authorized Training Session, Trainers may customize and reproduce, in accordance with the MCT Agreement, those portions of the Licensed Content that are logically associated with instruction of the Authorized Training Session. If you elect to exercise the foregoing rights, you agree or ensure the Trainer agrees: (a) that any of these customizations or reproductions will only be used for providing an Authorized Training Session and (b) to comply with all other terms and conditions of this agreement.
iii. Academic Materials. If the Licensed Content contains Academic Materials, you may copy and use the Academic Materials. You may not make any modifications to the Academic Materials and you may not print any book (either electronic or print version) in its entirety. If you reproduce any Academic Materials, you agree that:
•
The use of the Academic Materials will be only for your personal reference or training use
•
You will not republish or post the Academic Materials on any network computer or broadcast in any media;
•
You will include the Academic Material’s original copyright notice, or a copyright notice to Microsoft’s benefit in the format provided below: Form of Notice: © 2010 Reprinted for personal reference use only with permission by Microsoft Corporation. All rights reserved. Microsoft, Windows, and Windows Server are either registered trademarks or trademarks of Microsoft Corporation in the US and/or other countries. Other product and company names mentioned herein may be the trademarks of their respective owners.
6. INTERNET-BASED SERVICES. Microsoft may provide Internet-based services with the Licensed
Content. It may change or cancel them at any time. You may not use these services in any way that could harm them or impair anyone else’s use of them. You may not use the services to try to gain unauthorized access to any service, data, account or network by any means.
7. SCOPE OF LICENSE. The Licensed Content is licensed, not sold. This agreement only gives you some
rights to use the Licensed Content. Microsoft reserves all other rights. Unless applicable law gives you more rights despite this limitation, you may use the Licensed Content only as expressly permitted in this agreement. In doing so, you must comply with any technical limitations in the Licensed Content that only allow you to use it in certain ways. You may not •
install more copies of the Licensed Content on classroom Devices than the number of Students and the Trainer in the Authorized Training Session;
•
allow more classroom Devices to access the server than the number of Students enrolled in and the Trainer delivering the Authorized Training Session if the Licensed Content is installed on a network server;
•
copy or reproduce the Licensed Content to any server or location for further reproduction or distribution;
•
disclose the results of any benchmark tests of the Licensed Content to any third party without Microsoft’s prior written approval;
•
work around any technical limitations in the Licensed Content;
•
reverse engineer, decompile or disassemble the Licensed Content, except and only to the extent that applicable law expressly permits, despite this limitation;
•
make more copies of the Licensed Content than specified in this agreement or allowed by applicable law, despite this limitation;
•
publish the Licensed Content for others to copy;
•
transfer the Licensed Content, in whole or in part, to a third party;
•
access or use any Licensed Content for which you (i) are not providing a Course and/or (ii) have not been authorized by Microsoft to access and use;
•
rent, lease or lend the Licensed Content; or
•
use the Licensed Content for commercial hosting services or general business purposes.
•
Rights to access the server software that may be included with the Licensed Content, including the Virtual Hard Disks does not give you any right to implement Microsoft patents or other Microsoft intellectual property in software or devices that may access the server.
8. EXPORT RESTRICTIONS. The Licensed Content is subject to United States export laws and
regulations. You must comply with all domestic and international export laws and regulations that apply to the Licensed Content. These laws include restrictions on destinations, end users and end use. For additional information, see www.microsoft.com/exporting.
9. NOT FOR RESALE SOFTWARE/LICENSED CONTENT. You may not sell software or Licensed Content marked as “NFR” or “Not for Resale.”
10. ACADEMIC EDITION. You must be a “Qualified Educational User” to use Licensed Content marked as “Academic Edition” or “AE.” If you do not know whether you are a Qualified Educational User, visit www.microsoft.com/education or contact the Microsoft affiliate serving your country.
11. TERMINATION. Without prejudice to any other rights, Microsoft may terminate this agreement if you fail to comply with the terms and conditions of these license terms. In the event your status as an Authorized Learning Center or Trainer a) expires, b) is voluntarily terminated by you, and/or c) is terminated by Microsoft, this agreement shall automatically terminate. Upon any termination of this agreement, you must destroy all copies of the Licensed Content and all of its component parts.
12. ENTIRE AGREEMENT. This agreement, and the terms for supplements, updates, Internet-
based services and support services that you use, are the entire agreement for the Licensed Content and support services.
13. APPLICABLE LAW. a. United States. If you acquired the Licensed Content in the United States, Washington state law governs the interpretation of this agreement and applies to claims for breach of it, regardless of conflict of laws principles. The laws of the state where you live govern all other claims, including claims under state consumer protection laws, unfair competition laws, and in tort.
b. Outside the United States. If you acquired the Licensed Content in any other country, the laws of that country apply.
14. LEGAL EFFECT. This agreement describes certain legal rights. You may have other rights under the
laws of your country. You may also have rights with respect to the party from whom you acquired the Licensed Content. This agreement does not change your rights under the laws of your country if the laws of your country do not permit it to do so.
15. DISCLAIMER OF WARRANTY. The Licensed Content is licensed “as-is.” You bear the risk of using it. Microsoft gives no express warranties, guarantees or conditions. You may have additional consumer rights under your local laws which this agreement cannot change. To the extent permitted under your local laws, Microsoft excludes the implied warranties of merchantability, fitness for a particular purpose and non-infringement.
16. LIMITATION ON AND EXCLUSION OF REMEDIES AND DAMAGES. YOU CAN RECOVER FROM MICROSOFT AND ITS SUPPLIERS ONLY DIRECT DAMAGES UP TO U.S. $5.00. YOU CANNOT RECOVER ANY OTHER DAMAGES, INCLUDING CONSEQUENTIAL, LOST PROFITS, SPECIAL, INDIRECT OR INCIDENTAL DAMAGES. This limitation applies to •
anything related to the Licensed Content, software, services, content (including code) on third party Internet sites, or third party programs; and
•
claims for breach of contract, breach of warranty, guarantee or condition, strict liability, negligence, or other tort to the extent permitted by applicable law.
It also applies even if Microsoft knew or should have known about the possibility of the damages. The above limitation or exclusion may not apply to you because your country may not allow the exclusion or limitation of incidental, consequential or other damages. Please note: As this Licensed Content is distributed in Quebec, Canada, some of the clauses in this agreement are provided below in French. Remarque : Ce le contenu sous licence étant distribué au Québec, Canada, certaines des clauses dans ce contrat sont fournies ci-dessous en français. EXONÉRATION DE GARANTIE. Le contenu sous licence visé par une licence est offert « tel quel ». Toute utilisation de ce contenu sous licence est à votre seule risque et péril. Microsoft n’accorde aucune autre garantie expresse. Vous pouvez bénéficier de droits additionnels en vertu du droit local sur la protection dues consommateurs, que ce contrat ne peut modifier. La ou elles sont permises par le droit locale, les garanties implicites de qualité marchande, d’adéquation à un usage particulier et d’absence de contrefaçon sont exclues. LIMITATION DES DOMMAGES-INTÉRÊTS ET EXCLUSION DE RESPONSABILITÉ POUR LES DOMMAGES. Vous pouvez obtenir de Microsoft et de ses fournisseurs une indemnisation en cas de dommages directs uniquement à hauteur de 5,00 $ US. Vous ne pouvez prétendre à aucune indemnisation pour les autres dommages, y compris les dommages spéciaux, indirects ou accessoires et pertes de bénéfices. Cette limitation concerne: •
tout ce qui est relié au le contenu sous licence , aux services ou au contenu (y compris le code) figurant sur des sites Internet tiers ou dans des programmes tiers ; et
•
les réclamations au titre de violation de contrat ou de garantie, ou au titre de responsabilité stricte, de négligence ou d’une autre faute dans la limite autorisée par la loi en vigueur.
Elle s’applique également, même si Microsoft connaissait ou devrait connaître l’éventualité d’un tel dommage. Si votre pays n’autorise pas l’exclusion ou la limitation de responsabilité pour les dommages indirects, accessoires ou de quelque nature que ce soit, il se peut que la limitation ou l’exclusion ci-dessus ne s’appliquera pas à votre égard. EFFET JURIDIQUE. Le présent contrat décrit certains droits juridiques. Vous pourriez avoir d’autres droits prévus par les lois de votre pays. Le présent contrat ne modifie pas les droits que vous confèrent les lois de votre pays si celles-ci ne le permettent pas.
Welcome! Thank you for taking our training! We’ve worked together with our Microsoft Certified Partners for Learning Solutions and our Microsoft IT Academies to bring you a world-class learning experience—whether you’re a professional looking to advance your skills or a student preparing for a career in IT. n
Microsoft Certified Trainers and Instructors—Your instructor is a technical and instructional expert who meets ongoing certification requirements. And, if instructors are delivering training at one of our Certified Partners for Learning Solutions, they are also evaluated throughout the year by students and by Microsoft.
n
Certification Exam Benefits—After training, consider taking a Microsoft Certification exam. Microsoft Certifications validate your skills on Microsoft technologies and can help differentiate you when finding a job or boosting your career. In fact, independent research by IDC concluded that 75% of managers believe certifications are important to team performance1. Ask your instructor about Microsoft Certification exam promotions and discounts that may be available to you.
n Customer Satisfaction Guarantee—Our Certified Partners for Learning Solutions offer a satisfaction guarantee and we hold them accountable for it. At the end of class, please complete an evaluation of today’s experience. We value your feedback!
We wish you a great learning experience and ongoing success in your career!
Sincerely, Microsoft Learning www.microsoft.com/learning
1
IDC, Value of Certification: Team Certification and Organizational Performance, November 2006
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
iii
Acknowledgement Microsoft Learning would like to acknowledge and thank the following for their contribution towards developing this title. Their effort at various stages in the development has ensured that you have a good classroom experience.
Lin Joyner – Content Developer Lin is a subject matter expert, technical writer, and course designer with significant experience of creating training content for Microsoft® Visual Studio® and Microsoft SQL Server®. She has worked with Visual Studio and SQL Server since version 6.0 of both products. Lin has designed and written training courses, labs, and elearning materials about SQL Server, .NET development, and development of the Microsoft Office system, often taking the lead content developer role. Before she joined Content Master, Lin was a professional trainer for five years when she held the MCT and MCSD certifications.
Dominic Betts – Content Developer Dominic currently works for the Distributed Systems Development Team at Content Master as a subject matter expert, technical writer, and course designer specializing in Microsoft technologies such as ASP.NET, F#, and Windows® Azure™. Dominic’s recent projects have included a Visual Studio 2008 Extensibility Training Development Kit (TDK), Windows Mobile® Quickstart and Readiness solutions, and advising the Windows Azure team on training content. Dominic has created and taught courses that cover a range of topics such as ASP.NET, .NET threading, and Enterprise Java. Dominic has also written papers and created technical content for the MSDN® Web site and elsewhere. He was the UK's IT Trainer of the Year in 2003.
iv
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Contents Module 1: Introduction to Data Access Technologies Lesson 1: Data Access Technologies Lesson 2: Data Access Scenarios Lab: Analyzing Data Access Scenarios
1-3 1-14 1-19
Module 2: Building Entity Data Models Lesson 1: Introduction to Entity Data Models Lesson 2: Modifying an Entity Data Model Lesson 3: Customizing an Entity Data Model Lab: Using Entity Data Models
2-3 2-18 2-29 2-43
Module 3: Querying Entity Data Lesson 1: Retrieving Data by Using LINQ to Entities Lesson 2: Retrieving Data by Using Entity SQL Lesson 3: Retrieving Data by Using the EntityClient Provider Lesson 4: Retrieving Data by Using Stored Procedures Lesson 5: Unit Testing Your Data Access Code Lab: Querying Entity Data
3-4 3-19 3-26 3-38 3-43 3-51
Module 4: Creating, Updating, and Deleting Entity Data Lesson 1: Understanding Change Tracking in the Entity Framework Lesson 2: Modifying Data in an Entity Data Model Lab: Creating, Updating, and Deleting Entity Data
4-3 4-12 4-28
Module 5: Handling Multi-User Scenarios by Using Object Services Lesson 1: Handling Concurrency in the Entity Framework Lesson 2: Transactional Support in the Entity Framework Lab: Handling Multi-User Scenarios by Using Object Services
5-3 5-19 5-32
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
v
Module 6: Building Optimized Solutions by Using Object Services Lesson 1: The Stages of Query Execution Lesson 2: Change Tracking and Object Materialization Lesson 3: Using Compiled Queries Lesson 4: Using Design-Time Generated Entity Framework Views Lesson 5: Monitoring Performance Lesson 6: Performing Asynchronous Data Modifications Lab: Building Optimized Solutions by Using Object Services
6-3 6-9 6-18 6-24 6-29 6-37 6-44
Module 7: Customizing Entities and Building Custom Entity Class Lesson 1: Overriding Generated Classes Lesson 2: Using Templates to Customize Entities Lesson 3: Creating and Using Custom Entity Classes Lab: Customizing Entities and Building Custom Entity Classes
7-3 7-17 7-34 7-47
Module 8: Using POCO Classes with the Entity Framework Lesson 1: Requirements for POCO Classes Lesson 2: POCO Classes and Lazy Loading Lesson 3: POCO Classes and Change Tracking Lesson 4: Extending Entity Types Lab: Using POCO Classes with the Entity Framework
8-3 8-12 8-18 8-23 8-29
Module 9: Building an N-Tier Solution by Using the Entity Framework Lesson 1: Designing an N-Tier Solution Lesson 2: Defining Operations and Implementing Data Transport Structures Lesson 3: Protecting Data and Operations Lab: Building an N-Tier Solution by Using the Entity Framework
9-4 9-12 9-32 9-40
vi
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Module 10: Handling Updates in an N-Tier Solution by Using the Entity Framework Lesson 1: Tracking Entities and Persisting Changes Lesson 2: Managing Exceptions in an N-Tier Solution Lab: Handling Updates in an N-Tier Solution by Using the Entity Framework
10-3 10-42 10-54
Module 11: Building Occasionally Connected Solutions Lesson 1: Offline Data Caching by Using XML Lesson 2: Using the Sync Framework Lab: Building Occasionally Connected Solutions
11-3 11-22 11-46
Module 12: Querying Data by Using WCF Data Services Lesson 1: Introducing WCF Data Services Lesson 2: Creating a WCF Data Service Lesson 3: Consuming a WCF Data Service Lesson 4: Protecting Data and Operations in a WCF Data Service Lab: Creating and Using WCF Data Services
12-3 12-16 12-53 12-84 12-99
Module 13: Updating Data by Using WCF Data Services Lesson 1: Creating, Updating, and Deleting Data in a WCF Data Service 13-3 Lesson 2: Preventing Unauthorized Updates and Improving Performance 13-22 Lesson 3: Using WCF Data Services with Nonrelational Data 13-30 Lab: Updating Data by Using WCF Data Services 13-42
Module 14: Using ADO.NET Lesson 1: Retrieving and Modifying Data by Using ADO.NET Commands Lesson 2: Retrieving and Modifying Data by Using DataSets Lesson 3: Managing Transactions and Concurrency in Multi User Scenarios Lab: Using ADO.NET
14-4 14-34 14-58 14-72
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
vii
Module 15: Using LINQ to SQL Lesson 1: Implementing a Logical Data Model by Using LINQ to SQL Lesson 2: Managing Performance and Handling Concurrency Lab: Using LINQ to SQL
15-4 15-51 15-81
Appendix: Lab Answer Keys Module 1 Lab: Analyzing Data Access Scenarios Module 2 Lab: Using Entity Data Models Module 3 Lab: Querying Entity Data Module 4 Lab: Creating, Updating, and Deleting Entity Data Module 5 Lab: Handling Multi-User Scenarios by Using Object Services Module 6 Lab: Building Optimized Solutions by Using Object Services Module 7 Lab: Customizing Entities and Building Custom Entity Classes Module 8 Lab: Using POCO Classes with the Entity Framework Module 9 Lab: Building an N-Tier Solution by Using the Entity Framework Module 10 Lab: Handling Updates in an N-Tier Solution by Using the Entity Framework Module 11 Lab: Building Occasionally Connected Solutions Module 12 Lab: Creating and Using WCF Data Services Module 13 Lab: Updating Data by Using WCF Data Services Module 14 Lab: Using ADO.NET Module 15 Lab: Using LINQ to SQL
L1-1 L2-1 L3-1 L4-1 L5-1 L6-1 L7-1 L8-1 L9-1 L10-1 L11-1 L12-1 L13-1 L14-1 L15-1
Handling Updates in an N-Tier Solution by Using the Entity Framework
10-1
Module 10 Handling Updates in an N-Tier Solution by Using the Entity Framework Contents: Lesson 1: Tracking Entities and Persisting Changes
10-3
Lesson 2: Managing Exceptions in an N-Tier Solution
10-42
Lab: Handling Updates in an N-Tier Solution by Using the Entity Framework
10-54
10-2
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Module Overview
When you make data modifications to entities in an n-tier solution, you need to ensure that the changes are tracked and then transferred to the database. When you save changes to the database, exceptions can occur if data integrity is violated or concurrency issues arise. This module describes how you can handle data modifications in an n-tier solution and how to manage the exceptions that can occur during the data modification process.
Objectives After completing this module, you will be able to: •
Use different strategies to track changes in the client application and persist these changes to the database.
•
Trap and handle update and concurrency exceptions in an n-tier solution.
Handling Updates in an N-Tier Solution by Using the Entity Framework
10-3
Lesson 1
Tracking Entities and Persisting Changes
The way that you handle data modifications in an n-tier solution varies depending on the type of object that you choose to serialize your data between tiers. Therefore, it is important that you use the correct method for your choice of object. It is also important to ensure that you configure your connection strings to promote scalability and that you encrypt them. This lesson describes the alternative strategies for tracking changes that users make to entity data in a client application and how to persist these changes to the database in the data access layer.
Objectives After completing this lesson, you will be able to: •
Describe the issues that are associated with change tracking in an n-tier solution.
•
Handle data modifications when the application uses simple entities (SEs).
10-4
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
•
Handle data modifications when the application uses self-tracking entities (STEs).
•
Handle data modifications when the application uses data transfer objects (DTOs).
•
Configure your database connection for scalability and security.
Handling Updates in an N-Tier Solution by Using the Entity Framework
10-5
Tracking Changes in an N-Tier Solution
Key Points In an n-tier solution, you move data over the network between the data access layer and the client. To move entity data that the data access layer has retrieved from the database, you must serialize the objects that contain the data. You must also serialize the objects that contain data that the client application has modified when you return these objects to the data access layer. Windows® Communication Foundation (WCF) handles most of the serialization process for you. However, there is one aspect of the process that you must handle directly: you must ensure that the types that you use are serializable. Change tracking is more complicated in an n-tier solution because when you serialize an entity object in the data access layer, the entity object is detached from the ObjectContext object. Therefore, the ObjectContext object can no longer automatically track changes in that entity object. When you deserialize an object from the client application, you must reattach the entity object to the ObjectContext object, work out what has changed, and then save these changes to the database. This contrasts with the situation in a two-tier application, where entity objects can remain permanently attached to the ObjectContext object, which enables the ObjectContext object to track changes transparently.
10-6
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
There are several methods in the Entity Framework application programming interface (API) that you will find useful when you build an n-tier solution. The following table summarizes these methods. Method name
Description
AddObject
This method adds a detached entity object to the ObjectContext object. You can use this method when you have a new entity object. This method sets the state of the object to Added. If the entity object has related objects, the method also adds these objects to the ObjectContext object and sets their state to Added. You may need to change the state of the related objects to reflect the modifications that have occurred in the client application. When you add an entity object, you can assign a temporary key that will be replaced when the entity object is saved to the database.
Attach and AttachTo
These methods attach a detached object to the ObjectContext object and set its state to Unchanged. The key of the entity object that you are attaching must be unique in the ObjectContext object.
ApplyCurrentValues
This method takes a detached entity object as a parameter. It copies the properties of this object to the current values of an attached object with the same key value. The state of each property value that is different is set to Modified.
ApplyOriginalValues
This method takes a detached entity object as a parameter. It copies the properties of this object to the original values of an attached object with the same key value. The state of each property value that is different is set to Modified.
GetUpdatableOriginalValues
This method gives you update access to the original values of an attached entity object.
CurrentValues
This method gives you update access to the current values of an attached entity object.
ChangeObjectState
This method enables you to change the state of an attached entity object. If you change the state of an entity object to Modified, all of the properties of the entity object will be marked as modified.
Handling Updates in an N-Tier Solution by Using the Entity Framework
Method name
10-7
Description
ChangeRelationshipState
This method enables you to change the state of an attached relationship.
ChangeState
This method enables you to change the state of an attached entity object or relationship.
SetModifiedProperty
This method enables you to set the state of an individual property of an attached entity object.
The following topics in this lesson illustrate how different scenarios use these methods. Question: In an n-tier solution, you are recommended to make your data access layer stateless. What impact does this have when you design a change-tracking mechanism?
Additional Reading For more information about serializing entity objects, see the Serializing Objects (Entity Framework) page at http://go.microsoft.com/fwlink/?LinkID=194068. For more information about building n-tier solutions, see the Building N-Tier Applications (Entity Framework) page at http://go.microsoft.com/fwlink/?LinkID=194080.
10-8
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Change Tracking with Self-Tracking Entities in an N-Tier Solution
Key Points In a previous module, you saw how to use the Text Template Transformation Toolkit (T4) templates to generate STEs from your Entity Data Model (EDM). In addition to generating the entity classes, these templates also generate a custom ObjectContext class to work with the entities. These templates generate entity classes that perform their own change tracking, so when the client modifies an entity object, the entity object stores details of the changes internally. When the STE is attached to the ObjectContext object back in the data access layer, the custom ObjectContext object can read the details of the changes back and work out what changes must be applied to the database. The following code example shows the implementation of the service method that receives a contact STE and the related reward claim STEs from the client and handles the updates to the database.
Handling Updates in an N-Tier Solution by Using the Entity Framework
10-9
[Visual Basic] Public Sub UpdateContactWithRelatedRewardsClaimed(ByVal contact as Contact) ValidateContactUpdate(contact) DeleteRewardsClaimed(contact) ApplyContactValues(contact) ' Save all of the changes. Using context As AdventureWorksEntities = New AdventureWorksEntities() context.Contacts.ApplyChanges(contact) context.SaveChanges() End Using End Sub
[Visual C#] public void UpdateContactWithRelatedRewardsClaimed(Contact contact) { ValidateContactUpdate(contact); DeleteRewardsClaimed(contact); ApplyContactValues(contact); // Save all of the changes. using (AdventureWorksEntities context = new AdventureWorksEntities()) { context.Contacts.ApplyChanges(contact); context.SaveChanges(); } }
The UpdateContactWithRelatedRewardsClaimed method first validates and makes any necessary changes to the detached contact STE and the related reward claim STEs that it receives from the client. It then uses the ApplyChanges extension method to copy all of the changes from the STEs into the ObjectContext object before it calls the SaveChanges method to persist the changes to the database. The following code example illustrates this.
10-10
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
[Visual Basic] Private Sub ApplyContactValues(ByVal contact As Contact) For Each claim In contact.RewardsClaimed Select claim.ChangeTracker.State Case ObjectState.Unchanged Case ObjectState.Added claim.TimeStamp = DateTime.Now claim.ClaimID = GetNextClaimID() contact.CurrentPoints -= claim.PointsUsed contact.ModifiedDate = DateTime.Now Case ObjectState.Modified claim.TimeStamp = DateTime.Now ' ConcurrencyMode.Fixed is ' set on the PointsUsed field in the EDM. contact.CurrentPoints += claim.ChangeTracker.OriginalValues("PointsUsed") contact.CurrentPoints -= claim.PointsUsed contact.ModifiedDate = DateTime.Now Case Else End Select Next claim End Sub
[Visual C#] private void ApplyContactValues(Contact contact) { foreach (var claim in contact.RewardsClaimed) { switch (claim.ChangeTracker.State) { case ObjectState.Unchanged: break; case ObjectState.Added: claim.TimeStamp = DateTime.Now; claim.ClaimID = GetNextClaimID(); contact.CurrentPoints -= claim.PointsUsed;
Handling Updates in an N-Tier Solution by Using the Entity Framework
10-11
contact.ModifiedDate = DateTime.Now; break; case ObjectState.Modified: claim.TimeStamp = DateTime.Now; // ConcurrencyMode.Fixed is // set on the PointsUsed field in the EDM. contact.CurrentPoints += (int)claim.ChangeTracker .OriginalValues["PointsUsed"]; contact.CurrentPoints -= claim.PointsUsed; contact.ModifiedDate = DateTime.Now; break; default: break; } } }
The ApplyContactValues method iterates over the related reward claims to apply the business rules that calculate the CurrentPoints property value of the contact. This method also sets the timestamp values that will be used for concurrency checks. Notice how the method makes use of the ChangeTracker property of the entity object to discover the changes that the client application has made to the entities. The original value of the PointsUsed property is available because it is has a concurrency mode of Fixed in the EDM. The following code example illustrates this. [Visual Basic] Private Sub DeleteRewardsClaimed(ByVal contact As Contact) If contact.ChangeTracker.ObjectsRemovedFromCollectionProperties.Count > 0 Then For Each r In contact.ChangeTracker .ObjectsRemovedFromCollectionProperties("RewardsClaimeds") Dim c As RewardsClaimed = r contact.CurrentPoints += c.PointsUsed c.MarkAsDeleted() Next r End If End Sub
10-12
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
[Visual C#] private void DeleteRewardsClaimed(Contact contact) { if (contact.ChangeTracker .ObjectsRemovedFromCollectionProperties.Count > 0) { foreach (var r in contact.ChangeTracker .ObjectsRemovedFromCollectionProperties["RewardsClaimeds"]) { RewardsClaimed c = (RewardsClaimed)r; contact.CurrentPoints += c.PointsUsed; c.MarkAsDeleted(); } } }
The DeleteRewardsClaimed method uses the ObjectsRemovedFromCollectionProperties collection to identify the rewards claimed that the client application has removed from the contact. The DeleteRewardsClaimed method then uses the MarkAsDeleted extension method to set the state of the entity objects in the ObjectContext object. The client application uses the entity types that the T4 templates generate. You can place these classes in a separate assembly so that the client application has no dependencies on the Entity Framework. The following code example shows how to add a new reward claim to a contact. The entity objects handle all of the change tracking for you. The UpdateContactWithRelatedRewardsClaimed method passes a contact entity and its related rewards claimed entities back to the WCF service. [Visual Basic] Dim contact As RewardsDAL.Contact = service.GetContactAndRelatedRewardsClaimed(1) Dim newClaim As RewardsDAL.RewardsClaimed = New RewardsDAL.RewardsClaimed() newClaim.PointsUsed = 100 newClaim.RewardID = 1 contact.RewardsClaimeds.Add(newClaim) service.UpdateContactWithRelatedRewardsClaimed(contact)
Handling Updates in an N-Tier Solution by Using the Entity Framework
10-13
[Visual C#] RewardsDAL.Contact contact = service.GetContactAndRelatedRewardsClaimed(1); RewardsDAL.RewardsClaimed newClaim = new RewardsDAL.RewardsClaimed(); newClaim.PointsUsed = 100; newClaim.RewardID = 1; contact.RewardsClaimeds.Add(newClaim); service.UpdateContactWithRelatedRewardsClaimed(contact);
The following code example shows how to change a rewards claimed entity. This will also update the points that the contact holds. [Visual Basic] Dim contact As RewardsDAL.Contact = service.GetContactAndRelatedRewardsClaimed(1) contact.RewardsClaimeds(1).PointsUsed = 50 service.UpdateContactWithRelatedRewardsClaimed(contact)
[Visual C#] RewardsDAL.Contact contact = service.GetContactAndRelatedRewardsClaimed(1); contact.RewardsClaimeds[1].PointsUsed = 50; service.UpdateContactWithRelatedRewardsClaimed(contact);
The following code example shows how to remove a rewards claimed entity. This will also update the points that the contact holds. [Visual Basic] Dim contact As RewardsDAL.Contact = service.GetContactAndRelatedRewardsClaimed(1) contact.RewardsClaimeds.Remove(contact.RewardsClaimeds(1)) service.UpdateContactWithRelatedRewardsClaimed(contact)
10-14
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
[Visual C#] RewardsDAL.Contact contact = service.GetContactAndRelatedRewardsClaimed(1); contact.RewardsClaimeds.Remove(contact.RewardsClaimeds[1]); service.UpdateContactWithRelatedRewardsClaimed(contact);
Handling Updates in an N-Tier Solution by Using the Entity Framework
10-15
Demonstration: Change Tracking with Self-Tracking Entities in an N-Tier Solution
Key Points •
An end-to-end walkthrough of a solution that uses STEs.
Demonstration Steps 1.
Log on to the 10265A-GEN-DEV-10 virtual machine as Student with the password Pa$$w0rd.
2.
In the E:\Demofiles folder, run Demo.bat to set up the database for this demonstration, and then run EnvSetup.bat to configure Internet Information Services (IIS).
3.
In the E:\Demofiles\Mod10\Demo1\Solution folder, run ExSetup.bat as an administrator to configure the virtual directory in IIS for the application.
4.
Start Microsoft® Visual Studio® 2010 as administrator.
5.
Open the RewardsSTE solution.
10-16
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
6.
Review the code in the solution.
7.
Run the application.
8.
Close the application and then close Visual Studio.
Question: In this example, the WCF service code implements the business logic that updates the number of points that a contact holds. Where else could you place this logic?
Handling Updates in an N-Tier Solution by Using the Entity Framework
10-17
Change Tracking with Simple Entities in an N-Tier Solution
Key Points This topic demonstrates how to use SEs instead of STEs to implement the same functionality in the Adventure Works application that you saw in the previous topic. This means that you must implement the change-tracking functionality manually. This example uses a set of custom entity classes that you write by hand instead of generating them by using the T4 templates. These entity classes are simple plainold CLR object (POCO) classes that you place in a separate assembly so that you can eliminate any dependencies on the Entity Framework in the client application. The following code example shows the RewardsClaimed entity class. [Visual Basic]
10-18
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Public Class RewardsClaimed Public Overrides Function ToString() As String Return String.Format("ClaimID: {0}" + vbTab + "ContactID: {1}" + vbTab + "RewardID: {2}" + vbTab + "PointsUsed: {3}", Me.ClaimID, Me.ContactID, Me.RewardID, Me.PointsUsed) End Function Public Property ClaimID As Integer Public Property PointsUsed As Integer Public Property RewardID As Integer Public Property ContactID As Integer Public Property TimeStamp As System.DateTime Public Property Contact As Contact Public Property Reward As Reward End Class
[Visual C#] [DataContract(IsReference = true)] [KnownType(typeof(Contact))] [KnownType(typeof(Reward))] public class RewardsClaimed { public override string ToString()
Handling Updates in an N-Tier Solution by Using the Entity Framework
10-19
{ return string.Format("ClaimID: {0}\tContactID: {1}\tRewardID: {2}\tPointsUsed: {3}", this.ClaimID, this.ContactID, this.RewardID, this.PointsUsed); } [DataMember] public int ClaimID { get; set; } [DataMember] public int PointsUsed { get; set; } [DataMember] public int RewardID { get; set; } [DataMember] public int ContactID { get; set; } [DataMember] public System.DateTime TimeStamp { get; set; } [DataMember] public Contact Contact { get; set; } [DataMember] public Reward Reward { get; set; } }
The following code example shows the WCF service method that adds a new rewards claim to a contact. It sets the timestamp values that will be used for concurrency checks. It uses the Attach method to attach the rewards claim and its related contact to the ObjectContext object before it sets the state of the objects and calls the SaveChanges method. [Visual Basic] Public Sub AddRewardsClaim(ByVal claim As RewardsClaimed) claim.Contact.CurrentPoints -= claim.PointsUsed ' Set all of the necessary object properties. claim.ContactID = claim.Contact.ContactID claim.Contact.ModifiedDate = DateTime.Now claim.TimeStamp = DateTime.Now claim.ClaimID = GetNextClaimID() Using context As AdventureWorksEntities = New AdventureWorksEntities()
10-20
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
' Attach the claim (and the related contact) ' - everything has a state of Unchanged. context.RewardsClaimeds.Attach(claim) ' Set the correct entity state values before you call ' the SaveChanges method. context.ObjectStateManager.ChangeObjectState(claim, System.Data.EntityState.Added) context.ObjectStateManager.ChangeObjectState(claim.Contact, System.Data.EntityState.Modified) context.SaveChanges() End Using End Sub
[Visual C#] public void AddRewardsClaim(RewardsClaimed claim) { claim.Contact.CurrentPoints -= claim.PointsUsed; // Set all of the necessary object properties. claim.ContactID = claim.Contact.ContactID; claim.Contact.ModifiedDate = DateTime.Now; claim.TimeStamp = DateTime.Now; claim.ClaimID = GetNextClaimID(); using (AdventureWorksEntities context = new AdventureWorksEntities()) { // Attach the claim (and the related contact) // - everything has a state of Unchanged. context.RewardsClaimeds.Attach(claim); // Set the correct entity state values before you call // the SaveChanges method. context.ObjectStateManager.ChangeObjectState(claim, System.Data.EntityState.Added); context.ObjectStateManager.ChangeObjectState(claim.Contact, System.Data.EntityState.Modified); context.SaveChanges(); } }
Handling Updates in an N-Tier Solution by Using the Entity Framework
10-21
The following code example shows how to handle a change to a rewards claimed entity. Notice how it is necessary to retrieve the existing claim from the database to get the original value of the PointsUsed property. [Visual Basic] Public Sub UpdateRewardsClaim(ByVal claim As RewardsClaimed) ' Set all of the necessary object properties. claim.Contact.ModifiedDate = DateTime.Now claim.TimeStamp = DateTime.Now Using context As AdventureWorksEntities = New AdventureWorksEntities() ' Find the original points to give back to the contact. ' You must run a query here. RewardsClaimed(oldClaim = GetRewardsClaimedDetails(claim.ClaimID)) claim.Contact.CurrentPoints += oldClaim.PointsUsed ' Add the new points. claim.Contact.CurrentPoints -= claim.PointsUsed ' Attach the claim (and the related contact) ' - everything has a state of Unchanged. context.RewardsClaimeds.Attach(claim) ' Set the correct entity state values before you call ' the SaveChanges method. context.ObjectStateManager.ChangeObjectState(claim, System.Data.EntityState.Modified) context.ObjectStateManager.ChangeObjectState(claim.Contact, System.Data.EntityState.Modified) context.SaveChanges() End Using End Sub
[Visual C#] public void UpdateRewardsClaim(RewardsClaimed claim) { // Set all of the necessary object properties. claim.Contact.ModifiedDate = DateTime.Now; claim.TimeStamp = DateTime.Now;
10-22
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
using (AdventureWorksEntities context = new AdventureWorksEntities()) { // Find the original points to give back to the contact. // You must run a query here. RewardsClaimed oldClaim = GetRewardsClaimedDetails(claim.ClaimID); claim.Contact.CurrentPoints += oldClaim.PointsUsed; // Add the new points. claim.Contact.CurrentPoints -= claim.PointsUsed; // Attach the claim (and the related contact) // - everything has a state of Unchanged. context.RewardsClaimeds.Attach(claim); // Set the correct entity state values before you call // the SaveChanges method. context.ObjectStateManager.ChangeObjectState(claim, System.Data.EntityState.Modified); context.ObjectStateManager.ChangeObjectState(claim.Contact, System.Data.EntityState.Modified); context.SaveChanges(); } }
The following code example shows how to remove a rewards claimed entity from a contact entity. [Visual Basic] Public Sub DeleteRewardsClaim(ByVal claim As RewardsClaimed) claim.Contact.CurrentPoints += claim.PointsUsed ' Set all of the necessary object properties. claim.Contact.ModifiedDate = DateTime.Now Using context As AdventureWorksEntities = New AdventureWorksEntities() ' Attach the claim (and the related contact) ' - everything has a state of Unchanged. context.RewardsClaimeds.Attach(claim) ' Set the correct entity state values before you call ' the SaveChanges method.
Handling Updates in an N-Tier Solution by Using the Entity Framework
10-23
context.ObjectStateManager.ChangeObjectState(claim.Contact, System.Data.EntityState.Modified) context.ObjectStateManager.ChangeObjectState(claim, System.Data.EntityState.Deleted) context.SaveChanges() End Using End Sub
[Visual C#] public void DeleteRewardsClaim(RewardsClaimed claim) { claim.Contact.CurrentPoints += claim.PointsUsed; // Set all of the necessary object properties. claim.Contact.ModifiedDate = DateTime.Now; using (AdventureWorksEntities context = new AdventureWorksEntities()) { // Attach the claim (and the related contact) // - everything has a state of Unchanged. context.RewardsClaimeds.Attach(claim); // Set the correct entity state values before you call // the SaveChanges method. context.ObjectStateManager.ChangeObjectState(claim.Contact, System.Data.EntityState.Modified); context.ObjectStateManager.ChangeObjectState(claim, System.Data.EntityState.Deleted); context.SaveChanges(); } }
The following three code examples show how the client application can add a new rewards claimed entity, update an existing rewards claimed entity, and delete a rewards claimed entity.
10-24
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
[Visual Basic] Dim contact As Contact = service.GetContactAndRelatedRewardsClaimed(1) Dim newClaim As New RewardsClaimed() newClaim.PointsUsed = 100 newClaim.RewardID = 1 newClaim.Contact = contact service.AddRewardsClaim(newClaim)
[Visual C#] Contact contact = service.GetContactAndRelatedRewardsClaimed(1); RewardsClaimed newClaim = new RewardsClaimed(); newClaim.PointsUsed = 100; newClaim.RewardID = 1; newClaim.Contact = contact; service.AddRewardsClaim(newClaim);
[Visual Basic] Dim contact As Contact = service.GetContactAndRelatedRewardsClaimed(1) contact.RewardsClaimed(1).PointsUsed = 50 service.UpdateRewardsClaim(contact.RewardsClaimeds(1))
[Visual C#] Contact contact = service.GetContactAndRelatedRewardsClaimed(1); contact.RewardsClaimeds[1].PointsUsed = 50; service.UpdateRewardsClaim(contact.RewardsClaimed[1]);
[Visual Basic] Dim contact As Contact = service.GetContactAndRelatedRewardsClaimed(1) service.DeleteRewardsClaim(contact.RewardsClaimeds(1))
Handling Updates in an N-Tier Solution by Using the Entity Framework
[Visual C#] Contact contact = service.GetContactAndRelatedRewardsClaimed(1); service.DeleteRewardsClaim(contact.RewardsClaimeds[1]);
10-25
10-26
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Demonstration: Change Tracking with Simple Entities in an N-Tier Solution
Key Points •
An end-to-end walkthrough of a solution that uses simple entities.
Demonstration Steps 1.
If you did not perform these steps for the previous walkthrough, in the E:\Demofiles folder, run Demo.bat to set up the database for this demonstration, and then run EnvSetup.bat to configure IIS.
2.
In the E:\Demofiles\Mod10\Demo2\Solution folder, run ExSetup.bat as an administrator to configure the virtual directory in IIS for the application.
3.
Start Visual Studio 2010 as administrator.
4.
Open the RewardsSE solution.
5.
Review the code in the solution.
6.
Run the application.
Handling Updates in an N-Tier Solution by Using the Entity Framework
7.
10-27
Close the application and then close Visual Studio.
Question: In this example, the update logic requires you to retrieve the original version of the rewards claimed entity from the database to get the original value of the PointsUsed property. How can you avoid having to query for this information during the update?
10-28
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Change Tracking with Data Transfer Objects in an N-Tier Solution
Key Points This topic demonstrates how to use DTOs to implement the same functionality in the Adventure Works application that you saw in the previous two topics. This example uses a set of DTO classes that minimize the amount of data that you move over the network and simplify the data modification logic. These DTO classes are placed in a separate assembly to eliminate any dependencies on the Entity Framework in the client application. They do require code to translate between the DTO and the entity objects in the data access layer; they also require code to translate between the DTO and the data representation that the client uses. In this example, the data access layer uses the standard entity objects that the Entity Framework generates directly from the EDM. The following code example shows the DTO class that the application uses to transfer information about a rewards claimed entity. Notice that the class has two PointsUsed fields: one to hold the original value that you retrieve from the database and one to hold the new value that the client sets.
Handling Updates in an N-Tier Solution by Using the Entity Framework
[Visual Basic] Public Class RewardsClaimedDTO Public Property ClaimID As Integer Public Property OriginalPointsUsed As Integer Public Property NewPointsUsed As Integer Public Property RewardID As Integer Public Property ContactID As Integer Public Property TimeStamp As System.DateTime End Class
[Visual C#] [DataContract(IsReference = true)] public class RewardsClaimedDTO { [DataMember] public int ClaimID { get; set; } [DataMember] public int OriginalPointsUsed { get; set; } [DataMember] public int NewPointsUsed { get; set; } [DataMember] public int RewardID { get; set; } [DataMember] public int ContactID { get; set; } [DataMember] public System.DateTime TimeStamp { get; set; } }
10-29
10-30
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
The following code example shows how the WCF service receives a DTO. The method uses the information in the DTO to add a new rewards claimed entity to the database and to update the related contact entity in the database. The AddRewardsClaim method first creates a new RewardsClaimed object and populates it from the DTO. It then retrieves the related contact entity from the database and updates the CurrentPoints property. Next, it adds the new RewardsClaimed object to the ObjectContext object. Finally, it calls the SaveChanges method. [Visual Basic] Public Sub AddRewardsClaim(ByVal claimDTO As RewardsClaimedDTO) ' Create a new RewardsClaimed object. Dim claim As New RewardsClaimed() claim.ContactID = claimDTO.ContactID claim.PointsUsed = claimDTO.NewPointsUsed claim.RewardID = claimDTO.RewardID claim.ClaimID = GetNextClaimID() claim.TimeStamp = DateTime.Now ' Create an entity key for the related Contact object. Dim key As New EntityKey("AdventureWorksEntities.Contacts", "ContactID", claimDTO.ContactID) Using context As AdventureWorksEntities = New AdventureWorksEntities() ' Fetch the related Contact object. Dim contact As Object = Nothing If context.TryGetObjectByKey(key, contact) Then ' Adjust the points and add the claim. contact.CurrentPoints -= claimDTO.NewPointsUsed contact.ModifiedDate = DateTime.Now context.RewardsClaimeds.AddObject(claim) End If context.SaveChanges() End Using End Sub
Handling Updates in an N-Tier Solution by Using the Entity Framework
10-31
[Visual C#] public void AddRewardsClaim(RewardsClaimedDTO claimDTO) { // Create a new RewardsClaimed object. RewardsClaimed claim = new RewardsClaimed(); claim.ContactID = claimDTO.ContactID; claim.PointsUsed = claimDTO.NewPointsUsed; claim.RewardID = claimDTO.RewardID; claim.ClaimID = GetNextClaimID(); claim.TimeStamp = DateTime.Now; // Create an entity key for the related Contact object. EntityKey key = new EntityKey("AdventureWorksEntities.Contacts", "ContactID", claimDTO.ContactID); using (AdventureWorksEntities context = new AdventureWorksEntities()) { // Fetch the related Contact object. object contact = null; if (context.TryGetObjectByKey(key, out contact)) { // Adjust the points and add the claim. ((Contact)contact).CurrentPoints -= claimDTO.NewPointsUsed; ((Contact)contact).ModifiedDate = DateTime.Now; context.RewardsClaimeds.AddObject(claim); } context.SaveChanges(); } }
The following code example demonstrates how to use the information in a DTO to update an existing rewards claimed entity and its related contact entity. [Visual Basic] Public Sub UpdateRewardsClaim(ByVal claimDTO As RewardsClaimedDTO) ' Create the entity keys to retrieve the entities. Dim claimkey As New EntityKey( "AdventureWorksEntities.RewardsClaimed", "ClaimID", claimDTO.ClaimID) Dim contactkey As New EntityKey( "AdventureWorksEntities.Contacts", "ContactID", claimDTO.ContactID)
10-32
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Using context As AdventureWorksEntities = New AdventureWorksEntities() ' Fetch the RewardsClaimed object. Dim claim As Object = Nothing If context.TryGetObjectByKey(claimkey, claim) Then ' Fetch the related contact. Dim contact As Contact = context.GetObjectByKey(contactkey) ' Adjust the points and add the claim. contact.CurrentPoints += claimDTO.OriginalPointsUsed contact.CurrentPoints -= claimDTO.NewPointsUsed contact.ModifiedDate = DateTime.Now claim.PointsUsed = claimDTO.NewPointsUsed claim.TimeStamp = DateTime.Now End If context.SaveChanges() End Using End Sub
[Visual C#] public void UpdateRewardsClaim(RewardsClaimedDTO claimDTO) { // Create the entity keys to retrieve the entities. EntityKey claimkey = new EntityKey("AdventureWorksEntities.RewardsClaimed", "ClaimID", claimDTO.ClaimID); EntityKey contactkey = new EntityKey("AdventureWorksEntities.Contacts", "ContactID", claimDTO.ContactID); using (AdventureWorksEntities context = new AdventureWorksEntities()) { // Fetch the RewardsClaimed object. object claim = null;
Handling Updates in an N-Tier Solution by Using the Entity Framework
10-33
if (context.TryGetObjectByKey(claimkey, out claim)) { // Fetch the related contact. Contact contact = (Contact)context.GetObjectByKey(contactkey); // Adjust the points and add the claim. ((Contact)contact).CurrentPoints += claimDTO.OriginalPointsUsed; ((Contact)contact).CurrentPoints -= claimDTO.NewPointsUsed; ((Contact)contact).ModifiedDate = DateTime.Now; ((RewardsClaimed)claim).PointsUsed = claimDTO.NewPointsUsed; ((RewardsClaimed)claim).TimeStamp = DateTime.Now; } context.SaveChanges(); } }
The following code example demonstrates how to use the information in a DTO to delete an existing rewards claimed entity and update its related contact entity. [Visual Basic] Public Sub DeleteRewardsClaim(ByVal claimDTO As RewardsClaimedDTO) ' Create the entity keys to retrieve the entities. Dim claimkey As New EntityKey( "AdventureWorksEntities.RewardsClaimed", "ClaimID", claimDTO.ClaimID) Dim contactkey As New EntityKey( "AdventureWorksEntities.Contacts", "ContactID", claimDTO.ContactID) Using context As AdventureWorksEntities = New AdventureWorksEntities() ' Fetch the RewardsClaimed object. Dim claim As Object = Nothing If context.TryGetObjectByKey(claimkey, claim) Then
10-34
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
' Fetch the related contact. Dim contact As Contact = context.GetObjectByKey(contactkey) ' Adjust the points and add the claim. contact.CurrentPoints += claimDTO.OriginalPointsUsed contact.ModifiedDate = DateTime.Now context.RewardsClaimeds().DeleteObject(claim) End If context.SaveChanges() End Using End Sub
[Visual C#] public void DeleteRewardsClaim(RewardsClaimedDTO claimDTO) { // Create the entity keys to retrieve the entities. EntityKey claimkey = new EntityKey("AdventureWorksEntities.RewardsClaimed", "ClaimID", claimDTO.ClaimID); EntityKey contactkey = new EntityKey("AdventureWorksEntities.Contacts", "ContactID", claimDTO.ContactID); using (AdventureWorksEntities context = new AdventureWorksEntities()) { // Fetch the RewardsClaimed object. object claim = null; if (context.TryGetObjectByKey(claimkey, out claim)) { // Fetch the related contact. Contact contact = (Contact)context.GetObjectByKey(contactkey); // Adjust the points and add the claim. ((Contact)contact).CurrentPoints +=
Handling Updates in an N-Tier Solution by Using the Entity Framework
10-35
claimDTO.OriginalPointsUsed; ((Contact)contact).ModifiedDate = DateTime.Now; context.RewardsClaimeds .DeleteObject((RewardsClaimed)claim); } context.SaveChanges(); } }
The following three code examples show how the client application can add a new rewards claimed entity, update an existing rewards claimed entity, and delete a rewards claimed entity. [Visual Basic] Dim claims As List(Of RewardsClaimedDTO) = service.GetRewardsClaimedForContact(1) Dim newClaim As New RewardsClaimedDTO() newClaim.NewPointsUsed = 100 newClaim.RewardID = 1 newClaim.ContactID = 1 service.AddRewardsClaim(newClaim)
[Visual C#] List claims = service.GetRewardsClaimedForContact(1); RewardsClaimedDTO newClaim = new RewardsClaimedDTO(); newClaim.NewPointsUsed = 100; newClaim.RewardID = 1; newClaim.ContactID = 1; service.AddRewardsClaim(newClaim);
[Visual Basic] Dim claims As List(Of RewardsClaimedDTO) = service.GetRewardsClaimedForContact(1) claims(1).NewPointsUsed = 50 service.UpdateRewardsClaim(claims(1))
10-36
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
[Visual C#] List claims = service.GetRewardsClaimedForContact(1); claims[1].NewPointsUsed = 50; service.UpdateRewardsClaim(claims[1]);
[Visual Basic] Dim claims As List(Of RewardsClaimedDTO) = service.GetRewardsClaimedForContact(1) service.DeleteRewardsClaim(claims(1))
[Visual C#] List claims = service.GetRewardsClaimedForContact(1); service.DeleteRewardsClaim(claims[1]);
Handling Updates in an N-Tier Solution by Using the Entity Framework
10-37
Demonstration: Change Tracking with Data Transfer Objects in an N-Tier Solution
Key Points •
An end-to-end walkthrough of a solution that uses DTOs.
Demonstration Steps 1.
If you did not perform these steps for a previous walkthrough, in the E:\Demofiles folder, run Demo.bat to set up the database for this demonstration, and then run EnvSetup.bat to configure IIS.
2.
In the E:\Demofiles\Mod10\Demo3\Solution folder, run ExSetup.bat as an administrator to configure the virtual directory in IIS for the application.
3.
Start Visual Studio 2010 as administrator.
4.
Open the RewardsDTO solution.
5.
Review the code in the solution.
6.
Run the application.
10-38
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
7.
Close the application and then close Visual Studio.
Question: What is the purpose of the attributes that are used in the DTO classes?
Handling Updates in an N-Tier Solution by Using the Entity Framework
10-39
Connecting to the Database
Key Points In an n-tier application, the data access layer must connect to the database to retrieve data and to persist changes. One of the key reasons to adopt an n-tier architecture for you application is to improve the scalability of your solution. Making your data access layer stateless is one way to improve the scalability, but you should also consider how your data access layer connects to the database. The connection string in the web.config file in your service project usually determines the properties of your database connection. The following three sections of this topic describe elements of the connection string that are important factors in determining the scalability of your solution.
Connection Pooling Many databases are limited to support a maximum number of connections. The database license often determines this maximum number. To minimize the number of connections that your application uses, you should close connections as soon as you no longer require them. However, it takes time to open and close database connections. Connection pooling is a technique that manages the tradeoff between two strategies for managing database connections in your application. The
10-40
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
first strategy is to hang on to open connections to improve the performance of your application at the expense of other database users. The second strategy is to free up connections as soon as possible for other users at the expense of a performance hit for your application. Connection pooling handles this tradeoff by creating a pool of a limited number of active connections and recycling these connections in your application. When your application closes a connection, it does not really close but instead returns to the pool. When your application opens a connection, it does not really open a new connection but instead gets an existing connection from the pool. In ADO.NET, connection pooling is enabled by default, and ADO.NET creates pools when applications create connections with the same configuration. This means that connection strings must be identical for ADO.NET to be able to pool connections. You can also fine-tune the behavior of the pool by using the connection string elements that the following table describes. Name
Description
Max Pool Size
The maximum number of connections that is allowed in the pool.
Min Pool Size
The minimum number of connections that is allowed in the pool.
Pooling
If the value is true, the database connection is drawn from the appropriate pool. If there is no free connection in the pool, a connection is created and added to the pool.
Multiple Active Result Sets For some scenarios, enabling multiple active result sets (MARS) can provide performance benefits for your database connections. MARS is a feature of Microsoft SQL Server® that enables you to execute multiple batches on a single connection. This enables you to code scenarios that would otherwise require you to use serverside cursors. MARS is enabled by default, and you can control it by using the connection string.
Connection Timeouts Another setting that is defined in the connection string and can affect the responsiveness of the client application is the connection timeout. This setting specifies the length of time a client should wait for a connection to the database before it gives up the attempt and raises an error.
Handling Updates in an N-Tier Solution by Using the Entity Framework
10-41
Protecting Your Connection Strings The connection strings in your application describe how you connect to your database, and in some cases, they can contain user names and passwords. You should take steps to protect this information in your application. The Microsoft .NET Framework enables you to encrypt sensitive information in configuration files by using a feature called Protected Configuration. The .NET Framework includes two protection providers: 1.
The RSAProtectedConfigurationProvider provider uses a public key encryption algorithm.
2.
The DPAPIProtectedConfigurationProvider provider uses the Windows Data Protection API to encrypt data.
Question: What is the effect on the client application of setting the connection timeout to too long a value? What is the effect on the client application of setting the connection timeout to too short a value?
Additional Reading For more information about connection strings, see the SqlConnection.ConnectionString Property page at http://go.microsoft.com/fwlink/?LinkID=194081. For more information about how to protect connection strings, see the Connection Strings and Configuration Files (ADO.NET) page at http://go.microsoft.com/fwlink/?LinkID=194082.
10-42
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Lesson 2
Managing Exceptions in an N-Tier Solution
Two common exceptions can be thrown when you save data modifications to the database. You need to know how to catch and handle these errors to ensure that your users are not affected by them. This lesson explains how to manage exceptions in an n-tier application and describes how to transfer exception information over the network to the client application.
Objectives After completing this lesson, you will be able to: •
Describe the purpose of the UpdateException and OptimisticConcurrencyException exceptions.
•
Handle concurrency exceptions in an n-tier application.
•
Deal with concurrency in an n-tier application.
Handling Updates in an N-Tier Solution by Using the Entity Framework
10-43
The UpdateException and OptimisticConcurrencyException Exceptions
Key Points The Entity Framework can throw two exceptions when you call the SaveChanges method of the ObjectContext object to persist changes to the database. The Entity Framework throws an UpdateException exception when it cannot persist the modifications in the entity objects that the ObjectContext object manages to the database. Reasons for this failure can include: •
An entity object contains invalid foreign key data that results in a referential integrity violation.
•
A property value in an entity object is outside the range that the database permits.
•
The key of the entity object that you are trying to insert already exists in the database.
In these types of scenario, you should report the error to the client. Your client application can then decide what to do.
10-44
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
The Entity Framework throws an OptimisticConcurrencyException exception when it detects that another user or process has modified a record in the database since the current user or process retrieved the record and modified it. The Entity Framework detects this situation by comparing the value of all of the entity properties that you have marked with a Concurrency Mode property value of Fixed in the EDM with the current value in the database. If the two values are the same, the Entity Framework assumes that no one else has modified the record. It is important that either the application or the database changes the value of these fields in the database whenever the record is modified. If your application receives an OptimisticConcurrencyException exception, there are two options available: •
You can assume that the data in the database is correct and discard the current set of changes.
•
You can decide to keep the current set of changes and overwrite the data in the database.
Which option is correct will depend on the specific requirements of your application. You may decide to ask the user which option to take. In this case, you must ensure that you give the user all of the relevant information about the conflict so that he or she can make an informed decision. Question: How do STEs, SEs, and DTOs differ in how they help your application to detect conflicts?
Handling Updates in an N-Tier Solution by Using the Entity Framework
10-45
Handling OptimisticConcurrencyException Exceptions in the Client
Key Points In an n-tier application, you can add code to your data access layer that detects OptimisticConcurrencyException exceptions and then automatically applies one of the two conflict resolution strategies: assume that the data in the database is correct and discard the changes or overwrite the record in the database with the current changes. If you decide to permit your client application to select the option to take, you must send details of the conflict to the client and then enable the client to instruct the data access layer as to which option to take. The data access layer can notify the client application that it has detected an OptimisticConcurrencyException exception by throwing a FaultException exception that it populates with details of the conflict. The client can use the information in the FaultException exception to decide which option to take, and it can then request the data access layer to try to save the changes again.
10-46
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
The data access layer can implement the option that the client selects by using one of the overloaded versions of the Refresh method that take a RefreshMode parameter before saving the changes. The following table shows the possible values of the RefreshMode parameter. RefreshMode value
Description
ClientWins
Loads an entity into the ObjectContext object and applies the changes from the client.
StoreWins
Loads an entity into the ObjectContext object and discard any changes from the client.
Question: What types of information might the client find useful in helping it to determine how to resolve a conflict?
Additional Reading For more information about how to manage concurrency, see the Saving Changes and Managing Concurrency (Entity Framework) page at http://go.microsoft.com/fwlink/?LinkID=194083.
Handling Updates in an N-Tier Solution by Using the Entity Framework
10-47
Managing Concurrency with Self-Tracking Entities
Key Points This topic describes how to extend the Adventure Works application that manages rewards claimed and contact entities to include conflict detection. The code examples in this topic assume that your solution uses STEs and that the contact and rewards claimed entities have at least one field marked with a Concurrency Mode property value of Fixed. The data access layer sends conflict data to the client by using a ConcurrencyFault object. The following code example shows the ConcurrencyFault class and the definition of the UpdateContactWithRelatedRewardsClaimed method in the service interface. [Visual Basic] Public Class ConcurrencyFault Public ExceptionMessage As String End Class
10-48
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
... Sub UpdateContactWithRelatedRewardsClaimed(ByVal contact As Contact)
[Visual C#] [DataContract] public class ConcurrencyFault { [DataMember] public string ExceptionMessage; } ... [FaultContract(typeof(ServiceFault))] [FaultContract(typeof(ConcurrencyFault))] [OperationContract] void UpdateContactWithRelatedRewardsClaimed(Contact contact);
The following code example shows the UpdateContactWithRelatedRewardsClaimed method with exception handling that includes the OptimisticConcurrencyException exception. [Visual Basic] Public Sub UpdateContactWithRelatedRewardsClaimed(ByVal contact As Contact) ValidateContactUpdate(contact) DeleteRewardsClaimed(contact) ApplyContactValues(contact) ' Save all of the changes. Using context As AdventureWorksEntities = New AdventureWorksEntities() context.Contacts.ApplyChanges(contact) Try context.SaveChanges() ' ' Catch ' '
This exception occurs if the SaveChanges method fails because of a concurrency exception. ocEx As OptimisticConcurrencyException Create a ConcurrencyFault object and populate it with information about the causes of the
Handling Updates in an N-Tier Solution by Using the Entity Framework
10-49
' concurrency exception. Dim cf As New ConcurrencyFault() With { .ExceptionMessage = eventMessage } ' Throw a WCF FaultException exception that encapsulates ' the ConcurrencyFault object. Throw New FaultException(Of ConcurrencyFault)( cf, "Concurrency violation") ' This exception occurs if the SaveChanges method fails ' because of some validation or inconsistency error ' in the data. Catch uEx As UpdateException ' Create a ServiceFault object and throw a ' WCF FaultException exception. ' The inner exception of the UpdateException object ' usually contains the reason for the exception. Dim sf = New ServiceFault With { .ExceptionMessage = String.Format( "Update exception occurred in {0}: {1}", "UpdateContactWithRelatedRewardsClaimed", uEx.InnerException.Message) } Throw New FaultException(Of ServiceFault)( sf, "Update Failure") ' Catch and log all of the other exceptions and throw a ' generalized FaultException exception. Catch ex As Exception Throw New FaultException("Failure") End Try End Using End Sub
[Visual C#] public void UpdateContactWithRelatedRewardsClaimed(Contact contact) { ValidateContactUpdate(contact); DeleteRewardsClaimed(contact); ApplyContactValues(contact); // Save all of the changes.
10-50
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
using (AdventureWorksEntities context = new AdventureWorksEntities()) { context.Contacts.ApplyChanges(contact); try { context.SaveChanges(); } // This exception occurs if the SaveChanges method fails // because of a concurrency exception. catch (OptimisticConcurrencyException ocEx) { // Create a ConcurrencyFault object and populate it // with information about the causes of the // concurrency exception. ConcurrencyFault cf = new ConcurrencyFault() { ExceptionMessage = eventMessage }; // Throw a WCF FaultException exception that encapsulates // the ConcurrencyFault object. throw new FaultException( cf, "Concurrency violation"); } // This exception occurs if the SaveChanges method fails // because of some validation or inconsistency error // in the data. catch (UpdateException uEx) { // Create a ServiceFault object and throw a // WCF FaultException exception. // The inner exception of the UpdateException object // usually contains the reason for the exception. ServiceFault sf = new ServiceFault { ExceptionMessage = string.Format( "Update exception occurred in {0}: {1}", "UpdateContactWithRelatedRewardsClaimed", uEx.InnerException.Message) }; throw new FaultException( sf, "Update Failure");
Handling Updates in an N-Tier Solution by Using the Entity Framework
10-51
} // Catch and log all of the other exceptions and throw a // generalized FaultException exception. catch (Exception ex) { throw new FaultException("Failure"); } } }
In this scenario, the Entity Framework can detect a conflict in the contact when you add a new rewards claim, because the data access layer updates the PointsUsed property of the Contact object. Therefore, you should not allow the client to overwrite the changes in the database, because to do so would mean that the contact entity and its related rewards claimed entities were inconsistent.
10-52
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Demonstration: Managing Concurrency with Self-Tracking Entities
Key Points •
An end-to-end walkthrough of a solution that uses STEs.
Demonstration Steps 1.
If you did not perform these steps for a previous walkthrough, in the E:\Demofiles folder, run Demo.bat to set up the database for this demonstration, and then run EnvSetup.bat to configure IIS.
2.
In the E:\Demofiles\Mod10\Demo4\Solution folder, run ExSetup.bat as an administrator to configure the virtual directory in IIS for the application.
3.
Start Visual Studio 2010 as administrator.
4.
Open the RewardsConcurrency solution.
5.
Review the code.
6.
Run the application.
Handling Updates in an N-Tier Solution by Using the Entity Framework
7.
10-53
Close the application and then close Visual Studio.
Question: How can you determine whether a concurrency exception is thrown because another user has modified a record in the database or because another user has deleted a record in the database?
10-54
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Lab: Handling Updates in an N-Tier Solution by Using the Entity Framework
Objectives After completing this lab, you will be able to: •
Track the changes that are made in a client application by using STEs.
•
Handle any concurrency errors that the data access layer detects when it saves changes to the database.
Introduction In this lab, you will extend the Adventure Works Orders n-tier application to support data modifications by using STEs. You will enable the client application to select the strategy for the data access layer to use when the data access layer detects a concurrency error.
Handling Updates in an N-Tier Solution by Using the Entity Framework
10-55
Lab Setup For this lab, you will use the available virtual machine environment. Before you begin the lab, you must: •
Start the 10265A-GEN-DEV-10 virtual machine, and then log on by using the following credentials: •
User name: Student
•
Password: Pa$$w0rd
10-56
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Lab Scenario
The Adventure Works Orders application is an n-tier application that enables users to search for orders by using different criteria. The client application is a Windows Presentation Foundation (WPF) application that uses a WCF service to access the EDM. You have been asked to extend this application to enable users to make changes to orders. Users must be able to create new orders, modify existing orders, and delete orders. This is a multiuser application, so it must be able to detect and handle any concurrency conflicts that occur when two or more users edit the same data simultaneously. Users of the application should be able to choose what action to take when the application detects a conflict.
Exercise 1: Handling Updates in the Data Access Tier Scenario You have been asked to extend the existing Adventure Works Orders application to support the modification of order data. You will use the existing STE classes to
Handling Updates in an N-Tier Solution by Using the Entity Framework
10-57
transfer data modification requests to the data access layer. In this first phase, you will not detect any concurrency errors. The main tasks for this exercise are as follows: 1.
Prepare the environment for the lab.
2.
Create the virtual directory for the Web service.
3.
Enable Secure Sockets Layer (SSL) for the OrdersWebService Web service.
4.
Open the starter project for this exercise.
5.
Modify the entity model and rebuild the self-tracking entities.
6.
Define the PlaceOrder, CancelOrder, and AmendOrder methods in the OrdersService project.
7.
Implement the PlaceOrder, CancelOrder, and AmendOrder methods in the OrdersService project.
8.
Update the WPF client application.
9.
Add a unit test for the PlaceOrder, CancelOrder, and AmendOrder methods.
10. Build and test the application.
X Task 1: Prepare the environment for the lab 1.
Log on to the 10265A-GEN-DEV-10 virtual machine as Student with the password Pa$$w0rd.
2.
In the E:\Labfiles folder, run EnvSetup.bat as an administrator to install IIS and create the user accounts for the application.
3.
In the E:\Labfiles folder, run AWReset.bat to create the AdventureWorks database.
X Task 2: Create the virtual directory for the Web service •
In the E:\Labfiles\Lab10\CS\Ex1\Starter folder (if you are using Microsoft Visual C#®) or the E:\Labfiles\Lab10\VB\Ex1\Starter folder (if you are using Microsoft Visual Basic®), run ExSetup.bat as an administrator. This script adds the required virtual directories to IIS.
10-58
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
X Task 3: Enable SSL for the OrdersWebService Web service 1.
Open IIS Manager as an administrator.
2.
Add a new self-signed certificate named OrdersWebService.
3.
Edit the binding of the default Web site to use HTTPS and the OrdersWebService certificate.
4.
Configure the OrdersWebService application to require SSL security.
5. Close IIS Manager.
X Task 4: Open the starter project for this exercise 1.
Open Visual Studio 2010 as an administrator.
2.
Open the existing solution, OrdersDAL.sln, in the E:\Labfiles\Lab10\VB\Ex1\Starter\OrdersDAL or E:\Labfiles\Lab10\CS\Ex1\Starter\OrdersDAL folder.
X Task 5: Modify the entity model and rebuild the self-tracking entities 1.
Open the AdventureWorks EDM in the ADO.NET Entity Data Model Designer (Entity Designer).
2.
In the AdventureWorks EDM model, set the StoreGeneratedPattern property of the SubTotal field of the SalesOrderHeader entity to None.
3.
Save the AdventureWorks EDM model.
4.
Delete the existing T4 template files.
5.
Re-create the STEs by using the T4 templates.
6.
Move the AdventureWorksModel.tt file to the OrdersClientLibrary project.
7.
Close the AdventureWorks EDM model.
8.
Review the task list.
9.
Open the AdditionalMethods code file by double-clicking the TODO: Ex1 Add CalculateOrderTotal method to recalculate the sub total task in the task list. This task is located in the partial SalesOrderHeader class.
10. Immediately after the TODO: Ex1 - Add CalculateOrderTotal method to recalculate the sub total comment, add a void method called CalculateOrderTotal that recalculates the value of the SubTotal property of
Handling Updates in an N-Tier Solution by Using the Entity Framework
10-59
the SalesOrderHeader object. For each SalesOrderDetail object, the total is calculated according the following formula: LineTotal = UnitPrice * (1 - UnitPriceDiscount) * Quantity 11. Locate the next comment in the AdditionalMethods file by double-clicking the TODO: Ex1 - Validate an order object and verify that it contains order details task in the task list. This task is located in the partial SalesOrderHeader class. 12. Immediately after the TODO: Ex1 - Validate an order object and verify that it contains order details comment, add a method called Validate that returns a Boolean value. This method should return true if the SalesOrderHeader object contains at least one SalesOrderDetail object; otherwise, it should return false. 13. Save the AdditionalMethods file.
X Task 6: Define the PlaceOrder, CancelOrder, and AmendOrder methods in the OrdersService project 1.
Review the task list.
2.
Open the IOrdersService code file by double-clicking the TODO: Ex1 PlaceOrder task in the task list. This task is located in the IOrdersService interface.
3.
Immediately after the TODO: Ex1 - PlaceOrder comment, define a method called PlaceOrder that returns a Boolean value and takes a SalesOrderHeader object as a parameter. Mark the method with the OperationContract attribute and the FaultContract attribute with a type parameter of ServiceFault.
4.
Locate the next comment in the IOrdersService file by double-clicking the TODO: Ex1 - AmendOrder task in the task list. This task is located in the IOrdersService interface.
5.
Immediately after the TODO: Ex1 - AmendOrder comment, define a method called AmendOrder that returns a Boolean value and takes a SalesOrderHeader object as a parameter. Mark the method with the OperationContract attribute and the FaultContract attribute with a type parameter of ServiceFault.
6.
Locate the next comment in the IOrdersService file by double-clicking the TODO: Ex1 - CancelOrder task in the task list. This task is located in the IOrdersService interface.
10-60
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
7.
Immediately after the TODO: Ex1 - CancelOrder comment, define a method called CancelOrder that returns a Boolean value and takes a SalesOrderHeader object as a parameter. Mark the method with the OperationContract attribute and the FaultContract attribute with a type parameter of ServiceFault.
8.
Save the IOrdersService file.
X Task 7: Implement the PlaceOrder, CancelOrder, and AmendOrder methods in the OrdersService project 1.
Review the task list.
2.
Open the OrdersServiceImpl file by double-clicking the TODO: Ex1 updateOrderEntityCollectionAndSaveChangesToDatabase task in the task list. This task is located in the OrdersServiceImpl class.
3.
Immediately after the TODO: Ex1 updateOrderEntityCollectionAndSaveChangesToDatabase comment, define a method called updateOrderEntityCollectionAndSaveChangesToDatabase that returns a Boolean value and takes a string object called operationName as the first parameter and a SalesOrderHeader object called order as the second parameter. In the method, write code to perform the following tasks: a.
Create a new AdventureWorksEntities context object.
b.
Call the ApplyChanges method on the SalesOrderHeaders entity set object in this context, passing the order object as a parameter.
c.
Call the SaveChanges method of the context object. Return true if the number of changes that are saved is greater than zero; otherwise, return false.
d. If any exceptions are thrown, record the details by calling the logException method, and then throw a new FaultException exception. Specify the value of the operationName parameter and the order ID for the order in the log message and the fault. 4.
Locate the next comment in the OrdersServiceImpl file by double-clicking the TODO: Ex1 - PlaceOrder implementation task in the task list. This task is located in the OrdersServiceImpl class.
5.
Immediately after the TODO: Ex1 - PlaceOrder implementation comment, define a method called PlaceOrder that returns a Boolean value and takes a SalesOrderHeader object called newOrder as a parameter. Mark the method
Handling Updates in an N-Tier Solution by Using the Entity Framework
10-61
with the PrincipalPermission attribute and specify that this method can only be run by members of the OrderAdmin security role. In the method, write code to perform the following tasks: a.
Call the Validate method on the SalesOrderHeader object. If the validation fails, create a ServiceFault object with the message "Order has no details" and throw a new FaultException exception that wraps this ServiceFault object together with the message "Orders must have details".
b.
Call the CalculateOrderTotal method on the SalesOrderHeader object.
c.
Call the updateOrderEntityCollectionAndSaveChangesToDatabase method, passing the string "PlaceOrder" as the first parameter and the newOrder object as the second parameter, and return the result of the method call.
6.
Locate the next comment in the OrdersServiceImpl file by double-clicking the TODO: Ex1 - AmendOrder implementation task in the task list. This task is located in the OrdersServiceImpl class.
7.
Immediately after the TODO: Ex1 - AmendOrder implementation comment, define a method called AmendOrder that returns a Boolean value and takes a SalesOrderHeader object called order as a parameter. Mark the method with the PrincipalPermission attribute and specify that this method can only be run by members of the OrderAdmin security role. In the method, write code to perform the following tasks: a.
Call the Validate method on the SalesOrderHeader object. If the validation fails, create a ServiceFault object with the message "Order has no details" and throw a new FaultException exception that wraps this ServiceFault object together with the message "Orders must have details".
b.
Call the CalculateOrderTotal method on the SalesOrderHeader object.
c.
Call the updateOrderEntityCollectionAndSaveChangesToDatabase method, passing the string "AmendOrder" as the first parameter and the order object as the second parameter, and return the result of the method call.
8.
Locate the next comment in the OrdersServiceImpl file by double-clicking the TODO: Ex1 - CancelOrder implementation task in the task list. This task is located in the OrdersServiceImpl class.
9.
Immediately after the TODO: Ex1 - CancelOrder implementation comment, define a method called CancelOrder that returns a Boolean value and takes a SalesOrderHeader object called order as a parameter. Mark the method with the PrincipalPermission attribute and specify that this method can only be
10-62
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
run by members of the OrderAdmin security role. In the method, write code to perform the following tasks: a.
Call the MarkAsDeleted method on the order object.
b.
Call the updateOrderEntityCollectionAndSaveChangesToDatabase method, passing the string "CancelOrder" as the first parameter and the order object as the second parameter, and return the result of the method call.
10. Save the OrdersServiceImpl file. 11. Build the solution and correct any errors.
X Task 8: Update the WPF client application 1.
In the OrderManagement project, open the OrderManagementWindow.xaml file.
2.
Click the Orders By Contact tab. The functionality that the controls on this tab provide has been extended to enable a user to create, edit, and delete orders for a contact, as follows: a.
The New Order button creates a new order. Pressing INSERT in the TreeView control also creates a new order.
b.
Pressing DELETE in the TreeView control deletes an order.
c.
Pressing ENTER in the TreeView control edits an order. You can only change or add items to an order; you cannot delete items from an order.
3.
Update the service reference in the OrderManagement project.
4.
Review the task list.
5.
Open the code file behind the OrderManagementWindow.xaml window by double-clicking the TODO: Ex1 - Save the order to the database task in the task list. This task is located in the editOrder method, which is called when the user has made changes to an order and wants to save them.
6.
Immediately after the TODO: Ex1 - Save the order to the database comment, write code to perform the following tasks: a.
Call the AmendOrder method of the service object, passing the order object as a parameter.
Handling Updates in an N-Tier Solution by Using the Entity Framework
10-63
b.
If the call returns true, set the Content property of the statusOfLastOperation object to "Order saved". This object is a status bar item that appears at the bottom of the window.
c.
If the call returns false, set the Content property of the statusOfLastOperation object to "Order not saved".
7.
Locate the next comment in the OrderManagementWindow.xaml code-behind file by double-clicking the TODO: Ex1 - Delete the order from the database task in the task list. This task is located in the deleteOrder method, which is called when the user wants to cancel an order.
8.
Immediately after the TODO: Ex1 - Delete the order from the database comment, write code to perform the following tasks:
9.
a.
Call the CancelOrder method of the service object, passing the order object as a parameter.
b.
If the call returns true, set the Content property of the statusOfLastOperation object to "Order deleted".
c.
If the call returns false, set the Content property of the statusOfLastOperation object to "Order not deleted".
Locate the next comment in the OrderManagementWindow.xaml code-behind file by double-clicking the TODO: Ex1 - Add the new order to the database task in the task list. This task is located in the addOrder method, which is called when the user wants to place a new order for a contact.
10. Immediately after the TODO: Ex1 - Add the new order to the database comment, write code to perform the following tasks: a.
Call the PlaceOrder method of the service object, passing the order object as a parameter.
b.
If the call returns true, set the Content property of the statusOfLastOperation object to "New order saved".
c.
If the call returns false, set the Content property of the statusOfLastOperation object to "Order not saved".
d. Update the user interface (UI) by simulating the user double-clicking the getOrdersForContact button. 11. Save the code file behind the OrderManagementWindow.xaml window.
10-64
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
X Task 9: Add a unit test for the PlaceOrder, CancelOrder, and AmendOrder methods 1.
Update the service reference in the OrdersServiceTest project.
2.
Review the task list.
3.
Open the OrdersServiceImplTest file by double-clicking the TODO: Ex1 Implement a test for PlaceOrder, AmendOrder, CancelOrder task in the task list. This task is located in the CreateUpdateDeleteOrderTest method.
4.
Immediately after the TODO: Ex1 - Implement a test for PlaceOrder, AmendOrder, CancelOrder comment, add the following test code. This code performs the following tasks: a.
It connects to the Web service as the user Bert with a password of Pa$$w0rd. This user is a member of the OrderAdmin role.
b.
It places a new order and verifies that the service has added the order correctly. It uses the CreateOrder helper method to create a new order and populate a SalesOrderHeader object.
c.
It modifies the order that was just added and verifies that the service has modified the order correctly.
d. It deletes the order that was just added and verifies that the service has deleted the order correctly. [Visual Basic] service.ClientCredentials.Windows.ClientCredential.UserName = "Bert" service.ClientCredentials.Windows.ClientCredential.Password = "Pa$$w0rd" Dim productID As Integer = 709 Dim expectedOrderCount As Integer = 188 Dim expectedOrderDetailsCount As Integer = 2 ' Create a new order. Dim order As SalesOrderHeader = CreateOrder() order.CalculateOrderTotal() service.PlaceOrder(order) Dim actual As IEnumerable(Of SalesOrderHeader) = service.GetOrdersForProduct(productID) ' Check the Order Header was added. Assert.AreEqual(expectedOrderCount + 1, actual.Count())
Handling Updates in an N-Tier Solution by Using the Entity Framework
' Check the Order Details were added. Dim addedOrder As SalesOrderHeader = actual.Last() Assert.AreEqual(expectedOrderDetailsCount, addedOrder.SalesOrderDetails.Count) ' Modify the order. Dim expectedComment As String = "Modified" addedOrder.Comment = expectedComment service.AmendOrder(addedOrder) actual = service.GetOrdersForProduct(productID) Dim modifiedOrder As SalesOrderHeader = actual.Last() Assert.AreEqual(expectedComment, modifiedOrder.Comment) ' Delete the order. service.CancelOrder(modifiedOrder) actual = service.GetOrdersForProduct(productID) Assert.AreEqual(expectedOrderCount, actual.Count())
[Visual C#] service.ClientCredentials.Windows.ClientCredential.UserName = "Bert"; service.ClientCredentials.Windows.ClientCredential.Password = "Pa$$w0rd"; int productID = 709; int expectedOrderCount = 188; int expectedOrderDetailsCount = 2; // Create a new order. SalesOrderHeader order = CreateOrder(); order.CalculateOrderTotal(); service.PlaceOrder(order); IEnumerable actual = service.GetOrdersForProduct(productID); // Check the Order Header was added. Assert.AreEqual(expectedOrderCount+1, actual.Count()); // Check the Order Details were added. SalesOrderHeader addedOrder = actual.Last();
10-65
10-66
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Assert.AreEqual(expectedOrderDetailsCount, addedOrder.SalesOrderDetails.Count); // Modify the order. string expectedComment = "Modified"; addedOrder.Comment = expectedComment; service.AmendOrder(addedOrder); actual = service.GetOrdersForProduct(productID); SalesOrderHeader modifiedOrder = actual.Last(); Assert.AreEqual(expectedComment, modifiedOrder.Comment); // Delete the order. service.CancelOrder(modifiedOrder); actual = service.GetOrdersForProduct(productID); Assert.AreEqual(expectedOrderCount, actual.Count());
5.
Save the OrdersServiceImplTest file.
X Task 10: Build and test the application 1.
Build the solution and correct any errors.
2.
Run all of the tests in the solution.
3.
Verify that all of the tests succeed.
4.
Start the application in Debug mode.
5.
In the Order Management window perform the following tasks: a.
Click the Orders By Contact tab.
b.
In the Username box, type Bert
c.
In the Password box, type Pa$$w0rd
d. In the Contact ID box, type 200 and then click Get. Verify that you can add a new order, modify an existing order, and delete an order. Note that you can add line items to an order, but you cannot remove them after an order has been saved. However, you can amend existing line items. Note the following features of the application and the data in the database: a.
Make sure that you use a valid product ID, for example, 905 or 906.
b.
Make sure that you use a discount of less than 1.0, for example, 0.05 otherwise the order will not be saved (a constraint in the database prevents you from creating orders that have a negative value.)
Handling Updates in an N-Tier Solution by Using the Entity Framework
c.
10-67
The New Order button creates a new order. Pressing INSERT in the TreeView control also creates a new order.
d. Pressing DELETE in the TreeView control deletes an order. e.
Pressing ENTER in the TreeView control edits an order. You can only change or add items to an order; you cannot delete items from an order.
6.
Close the application.
7.
Close the solution.
Exercise 2: Detecting and Handling Order Conflicts Scenario You have been asked to enhance the application so that it detects concurrency errors when two or more users attempt to edit the same order data. When the application detects a conflict, the client application should give the current user the choice of whether to overwrite the existing data in the database or discard the changes. The main tasks for this exercise are as follows: 1.
Open the starter project for this exercise.
2.
Modify the entity model and rebuild the self-tracking entities.
3.
Add the types that are required to serialize faults to the client.
4.
Modify the service implementation to detect and handle concurrency issues.
5.
Update the WPF client application.
6.
Update the unit tests for the PlaceOrder, CancelOrder, and AmendOrder methods.
7.
Build and test the application.
X Task 1: Open the starter project for this exercise 1.
In the E:\Labfiles\Lab10\CS\Ex2\Starter folder (if you are using Visual C#) or the E:\Labfiles\Lab10\VB\Ex2\Starter folder (if you are using Visual Basic), run ExSetup.bat as an administrator.
10-68
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
2.
Open the existing solution, OrdersDAL.sln, in the E:\Labfiles\Lab10\VB\Ex2\Starter\OrdersDAL or E:\Labfiles\Lab10\CS\Ex2\Starter\OrdersDAL folder.
X Task 2: Modify the entity model and rebuild the self-tracking entities 1.
Open the AdventureWorks EDM in the Entity Designer.
2.
In the AdventureWorks EDM model, set the Concurrency Mode property of the RevisionNumber field of the SalesOrderHeader entity to Fixed.
3.
Save the AdventureWorks EDM model.
4.
Delete the existing T4 template files.
5.
Re-create the STEs by using the T4 templates.
6.
Move the AdventureWorksModel.tt file to the OrdersClientLibrary project.
7.
Close the AdventureWorks EDM model.
X Task 3: Add the types that are required to serialize faults to the client 1.
Review the task list.
2.
Open the IOrdersService file by double-clicking the TODO: Ex2 - Create OptimisticConcurrencyExceptionReason enumeration task in the task list. This task is located in the IOrdersService file.
3.
Immediately after the TODO: Ex2 - Create OptimisticConcurrencyExceptionReason enumeration comment, add an enumeration called OptimisticConcurrencyExceptionReason with three values called None, ItemAlreadyDeleted, and ItemAlreadyAddedOrUpdated. Mark the enumeration with the DataContract attribute, and mark each value with the EnumMember attribute.
4.
Locate the next comment in the IOrdersService file by double-clicking the TODO: Ex2 - Create ConcurrencyFault class task in the task list.
5.
Immediately after the TODO: Ex2 - Create ConcurrencyFault class comment, add a class called ConcurrencyFault with two public fields. The first field is called Reason and is of type OptimisticConcurrencyExceptionReason. The second field is called ConflictingValues and is of type Dictionary; both the key and the value are strings. Mark the class with the DataContract attribute, and mark each field with the DataMember attribute. Additionally, mark the class with the ServiceKnownType attribute and specify the type of the
Handling Updates in an N-Tier Solution by Using the Entity Framework
10-69
OptimisticConcurrencyException enumeration; this enables WCF to serialize and deserialize the Reason field correctly. 6.
Locate the next comment in the IOrdersService file by double-clicking the TODO: Ex2 - Define ConflictResolutionStrategy enumeration task in the task list. This task is located in the OrdersService namespace.
7.
Immediately after the TODO: Ex2 - Define ConflictResolutionStrategy enumeration comment, add an enumeration called ConflictResolutionStrategy with three values called None, ClientWins, and StoreWins. Mark the enumeration with the DataContract attribute, and mark each value with the EnumMember attribute.
8.
Locate the next comment in the IOrdersService file by double-clicking the TODO: Ex2 - Add ConflictResolutionStrategy as a known serializable type for the service task in the task list. This task is located just before the IOrdersService interface.
9.
Delete the TODO: Ex2 - Add ConflictResolutionStrategy as a known serializable type for the service comment, and replace it with the ServiceKnownType attribute with a parameter of type ConflictResolutionStrategy.
10. Locate the next comment in the IOrdersService file by double-clicking the TODO: Ex2 - Add ConcurrencyFault to the list of faults thrown by PlaceOrder task in the task list. This task is located just before the PlaceOrder method. 11. Delete the TODO: Ex2 - Add ConcurrencyFault to the list of faults thrown by PlaceOrder comment, and replace it with the FaultContract attribute with a parameter of type ConcurrencyFault. 12. Locate the next comment in the IOrdersService file by double-clicking the TODO: Ex2 - Add parameters to specify the conflict resolution strategy to use in PlaceOrder task in the task list. This task is located just before the PlaceOrder method. 13. Delete the TODO: Ex2 - Add parameters to specify the conflict resolution strategy to use in PlaceOrder comment, and modify the parameter list of the PlaceOrder method to include two additional parameters. The first new parameter is a Boolean parameter called resolveConcurrencyException. The second new parameter is called resolutionStrategy and is of type ConflictResolutionStrategy. 14. Locate the next comment in the IOrdersService file by double-clicking the TODO: Ex2 - Add ConcurrencyFault to the list of faults thrown by
10-70
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
AmendOrder task in the task list. This task is located just before the AmendOrder method. 15. Delete the TODO: Ex2 - Add ConcurrencyFault to the list of faults thrown by AmendOrder comment, and replace it with the FaultContract attribute with a parameter of type ConcurrencyFault. 16. Locate the next comment in the IOrdersService file by double-clicking the TODO: Ex2 - Add parameters to specify the conflict resolution strategy to use in AmendOrder task in the task list. This task is located just before the AmendOrder method. 17. Delete the TODO: Ex2 - Add parameters to specify the conflict resolution strategy to use in AmendOrder comment, and modify the parameter list of the AmendOrder method to include two additional parameters. The first new parameter is a Boolean parameter called resolveConcurrencyException. The second new parameter is called resolutionStrategy and is of type ConflictResolutionStrategy. 18. Locate the next comment in the IOrdersService file by double-clicking the TODO: Ex2 - Add ConcurrencyFault to the list of faults thrown by CancelOrder task in the task list. This task is located just before the CancelOrder method. 19. Delete the TODO: Ex2 - Add ConcurrencyFault to the list of faults thrown by CancelOrder comment, and replace it with the FaultContract attribute with a parameter of type ConcurrencyFault. 20. Locate the next comment in the IOrdersService file by double-clicking the TODO: Ex2 - Add parameters to specify the conflict resolution strategy to use in CancelOrder task in the task list. This task is located just before the CancelOrder method. 21. Delete the TODO: Ex2 - Add parameters to specify the conflict resolution strategy to use in CancelOrder comment, and modify the parameter list of the CancelOrder method to include two additional parameters. The first new parameter is a Boolean parameter called resolveConcurrencyException. The second new parameter is called resolutionStrategy and is of type ConflictResolutionStrategy. 22. Save the IOrdersService file.
Handling Updates in an N-Tier Solution by Using the Entity Framework
10-71
X Task 4: Modify the service implementation to detect and handle concurrency issues 1.
Review the task list.
2.
Open the OrdersServiceImpl file by double-clicking the TODO: Ex2 - Save changes to the database, and possibly attempt to resolve any concurrency errors task in the task list. This task is located in the saveChangesToDatabase method.
3.
Immediately after the TODO: Ex2 - Save changes to the database, and possibly attempt to resolve any concurrency errors comment, write code to perform the following tasks: a.
Call the SaveChanges method of the context object in a try block.
b.
If the number of changes is greater than zero, return true; otherwise, return false.
c.
Create a catch block to handle OptimisticConcurrencyException exceptions.
d. If the resolveConcurrencyException parameter is false, re-throw the exception. e.
If the resolveConcurrencyException parameter is true, check the value of the resolutionStrategy parameter: i.
If the value of the resolutionStrategy parameter is StoreWins, call the Refresh method of the context object to refresh the contents of the changedObject object from the database, and then return false. Note that the changedObject object is a parameter that is passed to the saveChangesToDatabase method. It references the object that is updated, inserted, or deleted from the database.
ii.
If the value of the resolutionStrategy parameter is ClientWins, call the Refresh method of the context object to refresh the contents of the changedObject object from the client. If the number of changes is greater than zero, return true; otherwise, return false.
iii. For any other value of the resolutionStrategy parameter, throw a new Exception exception to report an invalid conflict resolution strategy. 4.
Locate the next comment in the OrdersServiceImpl file by double-clicking the TODO: Ex2 - Create a ConcurrencyFault object, and populate it with the conflicting values task in the task list. This task is located in the determineCauseOfOptimisticConcurrencyException method. This method
10-72
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
runs when a concurrency fault occurs when saving an order to the database. The purpose of this method is to determine the cause of the concurrency exception. 5.
Immediately after the TODO: Ex2 - Create a ConcurrencyFault object, and populate it with the conflicting values comment, write code to perform the following tasks: a.
Create a new ConcurrencyFault object.
b.
Use a Language-Integrated Query (LINQ) to Entities query to determine whether the order object is still in the database.
c.
If the order is not found, set the Reason property of the ConcurrencyFault object to ItemAlreadyDeleted, and set the ConflictingValues property of the ConcurrencyFault object to null (Nothing in Visual Basic).
d. If the order has been updated by another user, set the Reason property of the ConcurrencyFault object to ItemAlreadyAddedOrUpdated, and add the properties of the order in the database to a new Dictionary object in the ConflictingValues property of the ConcurrencyFault object. e.
Return the populated ConcurrencyFault object.
6.
Locate the next comment in the OrdersServiceImpl file by double-clicking the TODO: Ex2 - Add parameters to the updateOrderEntityCollectionAndSaveChangesToDatabase task in the task list. This task is located just before the updateOrderEntityCollectionAndSaveChangesToDatabase method.
7.
Delete the TODO: Ex2 - Add parameters to the updateOrderEntityCollectionAndSaveChangesToDatabase comment, and modify the parameter list of the updateOrderEntityCollectionAndSaveChangesToDatabase method to include two additional parameters. The first new parameter is a Boolean parameter called resolveConcurrencyException. The second new parameter is called resolutionStrategy and is of type ConflictResolutionStrategy.
8.
Locate the next comment in the OrdersServiceImpl file by double-clicking the TODO: Ex2 - Log the details of the concurrency exception task in the task list. This task is located in the updateOrderEntityCollectionAndSaveChangesToDatabase method.
9.
Immediately after the TODO: Ex2 - Log the details of the concurrency exception comment, add code that creates a string containing an error
Handling Updates in an N-Tier Solution by Using the Entity Framework
10-73
message that includes the operation name and the sales order ID. Write the message to the log file by calling the logException method. 10. Locate the next comment in the OrdersServiceImpl file by double-clicking the TODO: Ex2 - Create a ConcurrencyFault object and throw a WCF FaultException that encapsulates the ConcurrencyFault object task in the task list. This task is located in the updateOrderEntityCollectionAndSaveChangesToDatabase method. 11. Immediately after the TODO: Ex2 - Create a ConcurrencyFault object and throw a WCF FaultException that encapsulates the ConcurrencyFault object comment, add code that creates a ConcurrencyFault object by calling the determineCauseOfOptimisticConcurrencyException method and throws a new FaultException exception of type ConcurrencyFault containing this ConcurrencyFault object. 12. Locate the next comment in the OrdersServiceImpl file by double-clicking the TODO: Ex2 - Log the details of the update exception task in the task list. This task is located in the updateOrderEntityCollectionAndSaveChangesToDatabase method. 13. Immediately after the TODO: Ex2 - Log the details of the update exception comment, add code that creates a string containing an error message that includes the operation name and the sales order ID. Write the message to the log file by calling the logException method. 14. Locate the next comment in the OrdersServiceImpl file by double-clicking the TODO: Ex2 - Create a ServiceFault object and throw a WCF FaultException task in the task list. This task is located in the updateOrderEntityCollectionAndSaveChangesToDatabase method. 15. Immediately after the TODO: Ex2 - Create a ServiceFault object and throw a WCF FaultException comment, add code that creates a ServiceFault object by using the message from the InnerException property of the UpdateException exception and throws a new FaultException exception of type ServiceFault by using this ServiceFault object. 16. Locate the next comment in the OrdersServiceImpl file by double-clicking the TODO: Ex2 - Add parameters to the PlaceOrder method task in the task list. This task is located just before the PlaceOrder method. 17. Delete the TODO: Ex2 - Add parameters to the PlaceOrder method comment, and modify the parameter list of the PlaceOrder method to include two additional parameters. The first new parameter is a Boolean parameter called resolveConcurrencyException. The second new parameter is called resolutionStrategy and is of type ConflictResolutionStrategy.
10-74
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
18. Locate the next comment in the OrdersServiceImpl file by double-clicking the TODO: Ex2 - Pass parameters from the PlaceOrder method task in the task list. This task is located in the PlaceOrder method. 19. Modify the parameter list of the call to the updateOrderEntityCollectionAndSaveChangesToDatabase method to include two additional parameters. The first new parameter is the resolveConcurrencyException object. The second new parameter is the resolutionStrategy object. 20. Locate the next comment in the OrdersServiceImpl file by double-clicking the TODO: Ex2 - Add parameters to the AmendOrder method task in the task list. This task is located just before the AmendOrder method. 21. Delete the TODO: Ex2 - Add parameters to the AmendOrder method comment, and modify the parameter list of the AmendOrder method to include two additional parameters. The first new parameter is a Boolean parameter called resolveConcurrencyException. The second new parameter is called resolutionStrategy and is of type ConflictResolutionStrategy. 22. Locate the next comment in the OrdersServiceImpl file by double-clicking the TODO: Ex2 - Pass parameters from the AmendOrder method task in the task list. This task is located in the AmendOrder method. 23. Modify the parameter list of the call to the updateOrderEntityCollectionAndSaveChangesToDatabase method to include two additional parameters. The first new parameter is the resolveConcurrencyException object. The second new parameter is the resolutionStrategy object. 24. Locate the next comment in the OrdersServiceImpl file by double-clicking the TODO: Ex2 - Add parameters to the CancelOrder method task in the task list. This task is located just before the CancelOrder method. 25. Delete the TODO: Ex2 - Add parameters to the CancelOrder method comment, and modify the parameter list of the CancelOrder method to include two additional parameters. The first new parameter is a Boolean parameter called resolveConcurrencyException. The second new parameter is called resolutionStrategy and is of type ConflictResolutionStrategy. 26. Locate the next comment in the OrdersServiceImpl file by double-clicking the TODO: Ex2 - Pass parameters from the CancelOrder method task in the task list. This task is located in the CancelOrder method. 27. Modify the parameter list of the call to the updateOrderEntityCollectionAndSaveChangesToDatabase method to
Handling Updates in an N-Tier Solution by Using the Entity Framework
10-75
include two additional parameters. The first new parameter is the resolveConcurrencyException object. The second new parameter is the resolutionStrategy object. 28. Save the OrdersServiceImpl file. 29. Build the OrdersService project and correct any errors.
X Task 5: Update the WPF client application 1.
Update the service reference in the OrderManagement project.
2.
Review the task list.
3.
Open the code file behind the OrderManagementWindow.xaml window by double-clicking the TODO: Ex2 - Try to save the order to the database task in the task list. This task is located in the editOrder method.
4.
Immediately after the TODO: Ex2 - Try to save the order to the database comment, write code to perform the following tasks: a.
Call the AmendOrder method of the service object, passing the order object as the first parameter, false as the second parameter, and specifying a conflict resolution strategy of None as the third parameter.
b.
If the call returns true, set the Content property of the statusOfLastOperation status bar item to "Order saved".
c.
If the call returns false, set the Content property of the statusOfLastOperation status bar item to "Order not saved".
5.
Locate the next comment in the code file behind the OrderManagementWindow.xaml window by double-clicking the TODO: Ex2 Handle a concurrency exception in AmendOrder task in the task list. This task is located in the editOrder method.
6.
Immediately after the TODO: Ex2 - Handle a concurrency exception in AmendOrder comment, write code to perform the following tasks: a.
If the concurrency fault was caused by another user deleting the item, set the Content property of the statusOfLastOperation status bar item to "Order already cancelled by another user".
b.
If the concurrency fault was caused by another user amending the item, set the Content property of the statusOfLastOperation status bar item to "Order changed by another user", and then prompt the user by using a
10-76
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
message box to determine whether he or she still wants to save the changes. c.
If the user replies "Yes", call the AmendOrder method of the service object, passing the order object as the first parameter, true as the second parameter, and specifying a conflict resolution strategy of ClientWins as the third parameter, and then set the Content property of the statusOfLastOperation status bar item to "Order saved".
d. If the user replies "No", call the AmendOrder method of the service object, passing the order object as the first parameter, true as the second parameter, and specifying a conflict resolution strategy of StoreWins as the third parameter, and then set the Content property of the statusOfLastOperation status bar item to "Order not saved". 7.
Locate the next comment in the OrderManagementWindow.xaml code-behind file by double-clicking the TODO: Ex2 - Try to delete the order from the database task in the task list. This task is located in the deleteOrder method.
8.
Immediately after the TODO: Ex2 - Try to delete the order from the database comment, write code to perform the following tasks:
9.
a.
Call the CancelOrder method of the service object, passing the order object as the first parameter, false as the second parameter, and specifying a conflict resolution strategy of None as the third parameter.
b.
If the call returns true, set the Content property of the statusOfLastOperation status bar item to "Order deleted".
c.
If the call returns false, set the Content property of the statusOfLastOperation status bar item to "Order not deleted".
Locate the next comment in the OrderManagementWindow.xaml code-behind file by double-clicking the TODO: Ex2 - Handle a concurrency exception in CancelOrder task in the task list. This task is located in the deleteOrder method.
10. Immediately after the TODO: Ex2 - Handle a concurrency exception in CancelOrder comment, write code to perform the following tasks: a.
If the concurrency fault was caused by another user deleting the item, set the Content property of the statusOfLastOperation status bar item to "Order already cancelled by another user".
b.
If the concurrency fault was caused by another user amending the item, set the Content property of the statusOfLastOperation status bar item to "Order changed by another user", and then prompt the user by using a
Handling Updates in an N-Tier Solution by Using the Entity Framework
10-77
message box to determine whether he or she still wants to cancel the order. c.
If the user replies "Yes", call the AmendOrder method of the service object, passing the order object as the first parameter, true as the second parameter, and specifying a conflict resolution strategy of ClientWins as the third parameter, and then set the Content property of the statusOfLastOperation status bar item to "Order cancelled".
d. If the user replies "No", call the AmendOrder method of the service object, passing the order object as the first parameter, true as the second parameter, and specifying a conflict resolution strategy of StoreWins as the third parameter, and then set the Content property of the statusOfLastOperation status bar item to "Order not cancelled". 11. Locate the next comment in the OrderManagementWindow.xaml code-behind file by double-clicking the TODO: Ex2 - Try to save the new order to the database task in the task list. This task is located in the addOrder method. 12. Immediately after the TODO: Ex2 - Try to save the new order to the database comment, write code to perform the following tasks: a.
Call the PlaceOrder method of the service object, passing the order object as the first parameter, false as the second parameter, and specifying a conflict resolution strategy of None as the third parameter.
b.
If the call returns true, set the Content property of the statusOfLastOperation status bar item to "New order saved".
c.
If the call returns false, set the Content property of the statusOfLastOperation status bar item to "Order not saved".
13. Save the OrderManagementWindow.xaml code-behind file.
X Task 6: Update the unit tests for the PlaceOrder, CancelOrder, and AmendOrder methods 1.
Update the service reference in the OrdersServiceTest project.
2.
Review the task list.
3.
Open the OrdersServiceImplTest file by double-clicking the TODO: Ex2 - Pass additional parameters to PlaceOrder task in the task list. This task is located in the CreateUpdateDeleteOrderTest method.
10-78
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
4.
Immediately after the TODO: Ex2 - Pass additional parameters to PlaceOrder comment, modify the call to the PlaceOrder method to include two additional parameters. The first new parameter to the PlaceOrder method is true, and the second new parameter specifies a conflict resolution strategy of StoreWins.
5.
Locate the next comment in the OrdersServiceImplTest file by double-clicking the TODO: Ex2 - Pass additional parameters to AmendOrder task in the task list. This task is located in the CreateUpdateDeleteOrderTest method.
6.
Immediately after the TODO: Ex2 - Pass additional parameters to AmendOrder comment, modify the call to the AmendOrder method to include two additional parameters. The second parameter to the AmendOrder method is true, and the third parameter specifies a conflict resolution strategy of StoreWins.
7.
Locate the next comment in the OrdersServiceImplTest file by double-clicking the TODO: Ex2 - Pass additional parameters to CancelOrder task in the task list. This task is located in the CreateUpdateDeleteOrderTest method.
8.
Immediately after the TODO: Ex2 - Pass additional parameters to CancelOrder comment, modify the call to the CancelOrder method to include two additional parameters. The second parameter to the CancelOrder method is true, and the third parameter specifies a conflict resolution strategy of StoreWins.
9.
Locate the next comment in the OrdersServiceImplTest file by double-clicking the TODO: Ex2 - Try to modify the original copy and test for a ConcurrencyFault task in the task list. This task is located in the AmendOrderConcurrencyTest method.
10. Immediately after the TODO: Ex2 - Try to modify the original copy and test for a ConcurrencyFault comment, modify the Comment property of the addedOrder object. Next, call the AmendOrder method and add a test to check that the service returns a concurrency fault, as the following code example shows. [Visual Basic] addedOrder.Comment = "Original order" Dim expected As Boolean = False Try service.AmendOrder(addedOrder, False, ConflictResolutionStrategy.None) Catch cf As FaultException(Of ConcurrencyFault)
Handling Updates in an N-Tier Solution by Using the Entity Framework
10-79
expected = True End Try Assert.IsTrue(expected)
[Visual C#] addedOrder.Comment = "Original order"; bool expected = false; try { service.AmendOrder(addedOrder, false, ConflictResolutionStrategy.None); } catch (FaultException cf) { expected = true; } Assert.IsTrue(expected);
11. Save the OrdersServiceImplTest file.
X Task 7: Build and test the application 1.
Build the solution and correct any errors.
2.
As an administrator, stop and restart IIS.
3.
In the E:\Labfiles folder, run AWReset.bat to reset the AdventureWorks database to a known state.
4.
In Visual Studio, run all of the tests in the solution.
5.
Verify that all of the tests succeed.
6.
Start the application without debugging.
7.
In the Order Management window, click the Orders By Contact tab, and then in the Username box, type Bert
8.
In the Password box, type Pa$$w0rd
9.
In the Contact ID box, type 200 and then click Get. Verify that you can still add a new order, modify an existing order, and delete an order: a.
Make sure that you use a valid product ID, for example, 905 or 906.
10-80
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
b.
Make sure that you use a discount of less than 1.0, for example, 0.05.
c.
The New Order button creates a new order. Pressing INSERT in the TreeView control also creates a new order.
d. Pressing DELETE in the TreeView control deletes an order. e.
Pressing ENTER in the TreeView control edits an order. You can only change or add items to an order; you cannot delete items from an order.
10. If time allows, start a second instance of the application, and attempt to make conflicting changes to the same orders in each instance. Verify that the application detects the conflicts and resolves them. Some possible suggestions include: •
Changing the order quantity for the same order in both instances.
•
Deleting an order in one instance, and attempting to modify the order quantity in the second instance.
•
Deleting the same order in both instances.
11. Close the application. 12. Close Visual Studio.
Handling Updates in an N-Tier Solution by Using the Entity Framework
10-81
Lab Review
Review Questions 1.
Why do you move the STE entity classes to a separate project?
2.
In Exercise 1, how is the order subtotal calculated?
3.
In Exercise 1, how do you report errors to the client application?
4.
In Exercise 2, why is the Concurrency Mode property of the RevisionNumber field set to Fixed?
5.
In Exercise 2, what information can a ConcurrencyFault object contain?
10-82
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Module Review and Takeaways
Review Questions 1.
Why can the ObjectContext object not track changes in an n-tier solution?
2.
When you attach an SE to the ObjectContext object, what state is it in?
3.
List some of the advantages of using DTOs in an n-tier solution.
4.
What are the options for handling a conflict that the data access layer detects when it calls the SaveChanges method?
Best Practices Related to Using Custom Entity Classes in Your Entity Framework Application Supplement or modify the following best practices for your own work situations: •
Select the format for transferring data over the network that is most appropriate for your application.
•
Make sure that all of your entities have a suitable property for detecting conflicts.
Handling Updates in an N-Tier Solution by Using the Entity Framework
•
Make sure that your application can use connection pooling.
•
Select the strategy for resolving conflicts that is most appropriate for your application.
10-83
Building Occasionally Connected Solutions
11-1
Module 11 Building Occasionally Connected Solutions Contents: Lesson 1: Offline Data Caching by Using XML
11-3
Lesson 2: Using the Sync Framework
11-22
Lab: Building Occasionally Connected Solutions
11-46
11-2
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Module Overview
In some scenarios, you will want to enable access to your applications when users are not connected to the corporate network. There are two ways of implementing this: providing offline data for local read-only use or providing offline data for local read/write use, with changes being uploaded to the central database when the user reconnects to the corporate network. This module describes how to access offline or occasionally connected data in client applications.
Objectives After completing this module, you will be able to: •
Cache data in local XML files by using Language-Integrated Query (LINQ) to XML.
•
Implement an occasionally connected application by using the Microsoft® Sync Framework.
Building Occasionally Connected Solutions
11-3
Lesson 1
Offline Data Caching by Using XML
XML is an easy format in which to cache data locally in an application. You can use LINQ to XML to save server data in local XML files; if the Web service that provides the data is subsequently unavailable, you can use the local cached files as your data source. This lesson explains how to test whether a Web service is available, how to use LINQ to XML to cache data locally, how to use LINQ to XML to load data from cached files, and how to encrypt XML files.
Objectives After completing this lesson, you will be able to: •
Detect the availability of a Web service.
•
Describe the functionality of LINQ to XML.
•
Describe the key LINQ to XML classes.
•
Persist entities to XML.
11-4
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
•
Load XML into entities.
•
Encrypt XML files.
Building Occasionally Connected Solutions
11-5
Detecting the Availability of a Web Service
Key Points The easiest way to check the availability of a Web service is to try to access the service. If the service responds, you know that it is available; if it does not, you know that it is unavailable. You can use the DownloadData method of the WebClient class to try to elicit a response from a Web service, as the following code example shows. [Visual Basic] ' Detect the availability of a Web service. Private Sub DetectTheAvailabilityOfWebService(ByVal _string As webServiceUri) Dim response As Byte() Dim result As Boolean Dim client As New System.Net.WebClient() Try response = client.DownloadData(webServiceUri) Catch ex As Exception result = False
11-6
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
End Try Dim str As String = Encoding.UTF8.GetString(response) If str.IndexOf("xml") > -1 Then result = True If result = True Then MessageBox.Show("Web service available at " & webServiceUri) Else MessageBox.Show("Web service unavailable") End If End Sub
[Visual C#] // Detect the availability of a Web service. private void DetectTheAvailabilityOfWebService(webServiceUri string) { byte[] response; bool result; System.Net.WebClient client = new System.Net.WebClient(); try { response = client.DownloadData(webServiceUri); } catch (Exception) { result = false; } string str = Encoding.UTF8.GetString(response); if (str.IndexOf("xml") > -1) result = true; if (result == true) { MessageBox.Show("Web service available at " + webServiceUri); } else { MessageBox.Show("Web service unavailable"); } }
Building Occasionally Connected Solutions
11-7
Question: Why does the code example test for the string "xml" being present in the response?
11-8
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Introduction to LINQ to XML
Key Points As you have seen in previous modules in this course, the LINQ technologies provide a consistent way to interact with databases and objects. LINQ to XML provides an in-memory XML interface that uses the familiar LINQ language to interact with XML. LINQ to XML provides a new way of programming against XML. It supports the in-memory document manipulation functionality that the Document Object Model (DOM) provides, but it also supports LINQ query expressions that make it easier for developers to move from working with databases to working with XML. LINQ to XML provides the following XML programming capabilities: •
Load XML from files or streams
•
Serialize XML to files or streams
•
Create XML
•
Query XML
•
Manipulate an in-memory XML tree
Building Occasionally Connected Solutions
•
Validate XML trees
•
Transform XML trees from one shape to another
Question: Why would you choose to use LINQ to XML instead of DOM?
11-9
11-10
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
LINQ to XML Classes
Key Points The LINQ to XML classes reside in the System.Xml.Linq namespace. The key classes that you will use include: •
XElement. The XElement class is the main class in LINQ to XML programming, and it represents an XML element. You can use it on its own; you do not need to contain it in an XDocument object. This enables you to create and manipulate XML trees without defining documents. You use the XElement class to create elements, modify the contents of elements, create, modify, and delete child elements, add attributes, and serialize elements.
•
XAttribute. The XAttribute class represents an XML attribute or key/value pair. When you add attributes to an element, the order of addition is preserved, and you can then access the attributes knowing their order.
•
XDocument. The XDocument class represents an XML document, including the XML declaration, processing instructions, comments, and an element. The XDocument class can have only one XElement node, just as an XML document can have only one root element.
Building Occasionally Connected Solutions
11-11
Question: Why can you have only one XElement node in an XDocument object?
Additional Reading For more information about LINQ to XML classes, see the LINQ to XML Classes Overview page at http://go.microsoft.com/fwlink/?LinkID=194084.
11-12
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Persisting Entity Data to Local XML Files
Key Points To persist entity data to XML, you can create a new XML element and then use LINQ to XML to convert each entity property into a child XML element, as the following code example shows. [Visual Basic] Dim xml As XElement = Nothing xml = New XElement("Product", From p In products _ Select New XElement("Product", New XElement("ProductID", p.ProductID), New XElement("Name", p.Name), New XElement("ProductNumber", p.ProductNumber), New XElement("Color", p.Color), New XElement("Cost", p.Cost), _ New XElement("ListPrice", p.ListPrice), New XElement("Size", p.Size)))
Building Occasionally Connected Solutions
11-13
[Visual C#] XElement xml = null; xml = new XElement("Product", from p in products select new XElement("Product", new XElement("ProductID", p.ProductID), new XElement("Name", p.Name), new XElement("ProductNumber", p.ProductNumber), new XElement("Color", p.Color), new XElement("Cost", p.Cost), new XElement("ListPrice", p.ListPrice), new XElement("Size", p.Size)));
You can then save the element to a local disk, as the following code example shows. [Visual Basic] xml.Save(productFileName)
[Visual C#] xml.Save(productFileName);
The Save method of the XElement class indents the XML and removes insignificant white space by default. To alter the default behavior, you can use the overload of the Save method that includes the SaveOptions parameter with which you specify indentation and white space options. Question: How can you preserve white space when you save an XElement object?
11-14
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Loading Data from XML Files into Entities
Key Points To load data from an XML file into entities, you create a new XML document and then use LINQ to XML to convert each element into an entity property, as the following code example shows. [Visual Basic] Dim doc As XDocument = Nothing doc = XDocument.Load(productFileName) Dim query = From product In doc.Descendants("Product") Select New Product With { .ProductID = Convert.ToInt32(product.Element("ProductID").Value), .Name = product.Element("Name").Value, .ProductNumber = product.Element("ProductNumber").Value, .Color = product.Element("Color").Value,
Building Occasionally Connected Solutions
11-15
.Cost = product.Element("Cost").Value, .ListPrice = product.Element("ListPrice").Value, .Size = product.Element("Size").Value }
[Visual C#] XDocument doc = null; doc = XDocument.Load(productFileName); var query = from product in doc.Descendants("Product") select new Product { ProductID = Convert.ToInt32(product.Element("ProductID").Value), Name = product.Element("Name").Value, ProductNumber = product.Element("ProductNumber").Value, Color = product.Element("Color").Value, Cost = product.Element("Cost").Value, ListPrice = product.Element("ListPrice").Value, Size = product.Element("Size").Value };
Similar to the Save method of the XElement class, the Load method of the XDocument class has an overload that takes a LoadOptions parameter, enabling you to configure indentation and white space options. Question: How can you configure indentation when you load an XDocument object?
11-16
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Encrypting XML Files
Key Points XML documents are plain text; therefore, there is a risk involved when you save them locally on a hard disk. If the data in your files is sensitive, you should encrypt the XML before you save it to disk. You can encrypt XML data by using symmetric keys, asymmetric keys, and X.509 certificates. You can use the classes in the System.Security.Cryptography.Xml namespace to encrypt your XML documents. After the encryption process is complete, the document will contain an element that contains the encrypted XML. The following code example shows methods to encrypt and decrypt XML documents by using an asymmetric key. [Visual Basic] Imports System Imports System.Xml Imports System.Security.Cryptography Imports System.Security.Cryptography.Xml Class Utilities
Building Occasionally Connected Solutions
11-17
Private Function EncryptXML(ByVal xmlDoc__1 As XmlDocument) As XmlDocument ' Create a new CspParameters object to specify ' a key container. Dim cspParams As New CspParameters() cspParams.KeyContainerName = "XML_ENC_RSA_KEY" ' Create a new RSA key and save it in the container. ' This key will encrypt a symmetric key, which will ' then be encrypted in the XML document. Dim rsaKey As New RSACryptoServiceProvider(cspParams) ' Find the specified element in the XmlDocument ' object and create a new XmlElement object. Dim elementToEncrypt As XmlElement = TryCast(XmlDoc.GetElementsByTagName("Product")(0), XmlElement) ' Throw an XmlException exception if the element was not found. If elementToEncrypt Is Nothing Then Throw New XmlException("Specified element not found") End If Dim sessionKey As RijndaelManaged = Nothing Try ' Create a new instance of the EncryptedXml class ' and use it to encrypt the XmlElement object with ' a new random symmetric key. ' Create a 256-bit Rijndael key. sessionKey = New RijndaelManaged() sessionKey.KeySize = 256 Dim eXml As New EncryptedXml() Dim encryptedElement As Byte() = eXml.EncryptData(elementToEncrypt, sessionKey, False) ' Construct an EncryptedData object and populate ' it with the desired encryption information. Dim edElement As New EncryptedData() edElement.Type = EncryptedXml.XmlEncElementUrl edElement.Id = EncryptionElementID ' Create an EncryptionMethod element to indicate to the ' receiver which algorithm to use for decryption.
11-18
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
edElement.EncryptionMethod = New EncryptionMethod(EncryptedXml.XmlEncAES256Url) ' Encrypt the session key and add it to an ' EncryptedKey element. Dim ek As New EncryptedKey() Dim encryptedKey As Byte() = EncryptedXml.EncryptKey(sessionKey.Key, Alg, False) ek.CipherData = New CipherData(encryptedKey) ek.EncryptionMethod = New EncryptionMethod(EncryptedXml.XmlEncRSA15Url) ' Set the KeyInfoName element to specify the ' name of the RSA key. Dim kin As New KeyInfoName() kin.Value = KeyName ' Add the KeyInfoName element to the ' EncryptedKey object. ek.KeyInfo.AddClause(kin) ' Add the encrypted element data to the ' EncryptedData object. edElement.CipherData.CipherValue = encryptedElement ' Replace the element from the original XmlDocument ' object with the EncryptedData element. EncryptedXml.ReplaceElement(elementToEncrypt, edElement, False) Catch e As Exception ' Re-throw the exception. Throw e Finally If sessionKey IsNot Nothing Then sessionKey.Clear() End If End Try End Function
Private Function DecryptXML(ByVal xmlDoc As XmlDocument, ByVal Alg As RSA, ByVal KeyName As String) As XmlDocument ' Create a new EncryptedXml object. Dim exml As New EncryptedXml(xmlDoc) ' Add a key-name mapping. ' This method can only decrypt documents ' that present the specified key name.
Building Occasionally Connected Solutions
exml.AddKeyNameMapping(KeyName, Alg) ' Decrypt and return the element. exml.DecryptDocument() Return exml End Function End Class
[Visual C#] using System; using System.Xml; using System.Security.Cryptography; using System.Security.Cryptography.Xml; class Utilities { XmlDocument EncryptXML(XmlDocument xmlDoc) { // Create a new CspParameters object to specify // a key container. CspParameters cspParams = new CspParameters(); cspParams.KeyContainerName = "XML_ENC_RSA_KEY"; // Create a new RSA key and save it in the container. // This key will encrypt a symmetric key, which will // then be encrypted in the XML document. RSACryptoServiceProvider rsaKey = new RSACryptoServiceProvider(cspParams); // Find the specified element in the XmlDocument // object and create a new XmlElement object. XmlElement elementToEncrypt = XmlDoc.GetElementsByTagName("Product")[0] as XmlElement; // Throw an XmlException exception if the element was not found. if (elementToEncrypt == null) { throw new XmlException("Specified element not found"); } RijndaelManaged sessionKey = null; try { // Create a new instance of the EncryptedXml class // and use it to encrypt the XmlElement object with // a new random symmetric key.
11-19
11-20
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
// Create a 256-bit Rijndael key. sessionKey = new RijndaelManaged(); sessionKey.KeySize = 256; EncryptedXml eXml = new EncryptedXml(); byte[] encryptedElement = eXml.EncryptData(elementToEncrypt, sessionKey, false); // Construct an EncryptedData object and populate // it with the desired encryption information. EncryptedData edElement = new EncryptedData(); edElement.Type = EncryptedXml.XmlEncElementUrl; edElement.Id = EncryptionElementID; // Create an EncryptionMethod element to indicate to the // receiver which algorithm to use for decryption. edElement.EncryptionMethod = new EncryptionMethod(EncryptedXml.XmlEncAES256Url); // Encrypt the session key and add it to an // EncryptedKey element. EncryptedKey ek = new EncryptedKey(); byte[] encryptedKey = EncryptedXml.EncryptKey(sessionKey.Key, Alg, false); ek.CipherData = new CipherData(encryptedKey); ek.EncryptionMethod = new EncryptionMethod(EncryptedXml.XmlEncRSA15Url); // Set the KeyInfoName element to specify the // name of the RSA key. KeyInfoName kin = new KeyInfoName(); kin.Value = KeyName; // Add the KeyInfoName element to the // EncryptedKey object. ek.KeyInfo.AddClause(kin); // Add the encrypted element data to the // EncryptedData object. edElement.CipherData.CipherValue = encryptedElement; // Replace the element from the original XmlDocument // object with the EncryptedData element. EncryptedXml.ReplaceElement(elementToEncrypt, edElement, false); } catch (Exception e) { // Re-throw the exception.
Building Occasionally Connected Solutions
11-21
throw e; } finally { if (sessionKey != null) { sessionKey.Clear(); } } } XmlDocument DecryptXML(XmlDocument xmlDoc, RSA Alg, string KeyName) { // Create a new EncryptedXml object. EncryptedXml exml = new EncryptedXml(xmlDoc); // Add a key-name mapping. // This method can only decrypt documents // that present the specified key name. exml.AddKeyNameMapping(KeyName, Alg); // Decrypt and return the element. exml.DecryptDocument(); return exml; } }
Question: Why should you secure the session key that you use to encrypt the data?
Additional Reading For more information about encrypting and decrypting XML data, see the XML Encryption and Digital Signatures page at http://go.microsoft.com/fwlink/?LinkID=194085.
11-22
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Lesson 2
Using the Sync Framework
The Sync Framework enables you to write code for applications that must support occasionally connected clients. For example, you can use it to develop an occasionally connected application (OCA) to run on a laptop or handheld device that is not usually connected to the network. Users can work with locally held data while disconnected and then synchronize with the central server data when they are connected at a later date. This lesson explains how synchronization works, how to configure it, how to synchronize data, and how to handle conflicts. It also explains how to use the Configure Data Synchronization dialog box to simplify some of these tasks.
Objectives After completing this lesson, you will be able to: •
Describe classic synchronization scenarios.
•
Describe how synchronization works.
Building Occasionally Connected Solutions
•
Compare synchronization with merge replication and remote data access (RDA).
•
Describe the Sync Framework architecture.
•
Configure synchronization.
•
Synchronize data.
•
Handle conflicts.
•
Use the Configure Data Synchronization dialog box.
•
Describe the security considerations for synchronization.
11-23
11-24
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Synchronization Scenarios
Key Points Often an enterprise must provide mobile and remote workers with access to data. These users may use laptops, handheld devices, or office desktops to connect to their corporate data by using a virtual private network (VPN) or a Web server. This network configuration has a number of disadvantages: •
Network connection. Remote users require a constant network connection to the corporate network to access their data. For users who regularly visit customer sites, this is unlikely to be available.
•
Network speed. When users connect to data over slow or unreliable networks, it is undesirable to require them to download all of their data every time they want to use it.
•
Server scalability. As more users remotely access data, the server performance may degrade to an unacceptable level, requiring additional hardware to serve the additional users.
Building Occasionally Connected Solutions
11-25
To avoid these issues, you can implement an occasionally connected application by using the type of functionality that the Sync Framework provides. In this scenario, data is stored locally on a user's computer or device for offline use. When users have corporate connectivity, they can synchronize their data with that of the server to ensure that each data source has the most up-to-date information. Question: What issues can you foresee when you provide synchronization functionality for remote users?
11-26
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Understanding Synchronization
Key Points During synchronization, data in one data source is used to update data in another data source. The Sync Framework provides ADO.NET classes to enable you to synchronize your data between sources. The data sources can be any database for which there is an ADO.NET data provider.
Change Tracking The Sync Framework uses change tracking to minimize the volume of data that is transferred between data sources. Change tracking enables the database to track insertions, deletions, and changes made to rows. Consequently, when the two databases synchronize, each database has a list of the rows that have changed, and there is no need to transfer entire tables of data over the connection. Change tracking is often implemented by using row versions and triggers. This requires changes to the tables in every data source, and the running of the triggers can impact performance. Microsoft SQL Server® 2008 provides a new type of change tracking, SQL Server 2008 Change Tracking, in which the server tracks changes to rows without the use of triggers. This avoids the schema changes and
Building Occasionally Connected Solutions
11-27
performance implications of traditional change tracking when you are synchronizing SQL Server databases. SQL Server 2008 also provides customizable retention thresholds for tracking data. This prevents tracking tables growing indefinitely and uses a background process to clean up old tracking data.
Synchronization Modes The Sync Framework supports four modes of operation that determine the direction of data flow during the synchronization process: •
Snapshot. In this mode, the client table is dropped at the beginning of the synchronization process, and then a snapshot of the server table is applied to the client.
•
Download only. In this mode, only rows that have been inserted, updated, or deleted at the server are downloaded to the client. Changes at the client are not passed to the server.
•
Upload only. In this mode, only rows that have been inserted, updated, or deleted at the client are uploaded to the server. Changes at the server are not passed to the client.
•
Bidirectional. In this mode, the client downloads changes from the server and uploads changes to the server. Conflicts may occur.
Conflicts Whenever multiple users update data at different locations, data conflicts may occur. When you synchronize the data sources, one change has to take precedence over the other. The Sync Framework provides you with conflict detection and resolution functionality that you can use to write conflict-resolution logic in your applications. Question: What advantages over traditional change tracking does SQL Server Change Tracking provide?
Additional Reading For more information about synchronization, see the Introduction to Sync Framework Database Synchronization page at http://go.microsoft.com/fwlink/?LinkID=194086.
11-28
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Synchronization vs. Merge Replication and RDA
Key Points The Sync Framework may at first appear to provide similar services to those that merge replication and RDA provide. However, there are key differences in the functionality of each of these technologies and hence the scenarios where they are best employed.
Merge Replication Merge replication is intended as a tool for database administrators to synchronize SQL Server databases. It provides wizards and stored procedures that simplify the configuration and maintenance of the system. The Sync Framework is intended as a tool for developers to synchronize many types of data source, including heterogeneous databases, over services such as Windows® Communication Foundation (WCF). If your application requires synchronization of non-SQL Server databases or synchronization over different protocols, you should use the Sync Framework.
Building Occasionally Connected Solutions
11-29
RDA RDA is designed for developers to synchronize data between SQL Server Compact 3.5 databases and other editions of SQL Server.
Comparisons Use the information in the following table to determine the appropriate technology for your synchronization applications. Key feature
RDA
Merge replication
Sync Framework
Synchronize by using services
No
No
Yes
Heterogeneous database support
No
No
Yes
Incremental change tracking
Upload only
Yes
Yes
Conflict detection and resolution
No
Yes
Yes
Automatic schema and data initialization
Yes
Yes
Yes
Large data set support
Yes
Yes
Yes
Automatic schema change propagation
No
Yes
No
Automatic data repartitioning
No
Yes
No
Device support
Yes
Yes
Yes
Question: You have an application that accesses data in a SQL Server database. Users are either based at one of three local offices with desktop computers, at home with laptops, or visiting customer sites with PDAs. You want to provide a way for all users to work with local data and then send their changes to the central server whenever they have a network connection. They should also receive changes made by other users since they were last connected. Which technology should you use?
11-30
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Additional Reading For more information about merge replication, see the Merge Replication page at http://go.microsoft.com/fwlink/?LinkID=194087. For more information about RDA, see the Using Remote Data Access (RDA) page at http://go.microsoft.com/fwlink/?LinkID=194088.
Building Occasionally Connected Solutions
11-31
Sync Framework Architecture
Key Points The Sync Framework consists of a set of components that work together to provide synchronization functionality to your applications.
Synchronization Providers A synchronization provider communicates between a data source and other components in the synchronization system. It hides the complexities of the data store from the other synchronization components and enables you to write data store–agnostic synchronization code. Providers retrieve changes that have been made since the last synchronization session, apply incremental changes, and detect conflicts. Providers are available for a range of data source types: •
Database synchronization providers. You can use the database synchronization providers to support collaborative and offline scenarios. For SQL Server Compact databases, use the SqlCeSyncProvider provider, for other SQL Server databases, use the SqlSyncProvider provider, and for other database types, use the DbSyncProvider provider.
11-32
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
•
File synchronization provider. You can use the file synchronization provider to synchronize files and folders in NTFS, FAT, or server message block (SMB) file systems.
•
Web feed synchronization components. You can use the Web feed synchronization components to write providers that represent FeedSync XML files or to synchronize data of another type with an RSS or Atom feed.
•
Custom providers. You can use custom providers to create synchronization providers for any type of data store.
Synchronization Orchestrators The synchronization orchestrator (or agent) communicates with the two synchronization providers in a session to retrieve and apply changes to the two data stores. It begins by determining the order and direction in which changes should be applied. Next, it calls the remote synchronization provider to retrieve and apply changes at the remote data store. It then calls the local synchronization provider to retrieve and apply changes at the local data store.
Synchronization Adapters A synchronization adapter is defined for each table to be synchronized. The synchronization adapter supplies the synchronization provider with the Structured Query Language (SQL) commands that it requires to work with the database. When you work with SQL Server databases, the Sync Framework automatically generates the commands for you. Question: You want to synchronize a SQL Server Express database. Which synchronization provider should you use?
Building Occasionally Connected Solutions
11-33
Configuring Synchronization
Key Points Before you can create a synchronization system, you must decide how you want to initialize the local database, which synchronization direction you want to support, and if appropriate, which change-tracking mechanism you want to use.
Initializing the Local Database When you set up synchronization, the first task is to initialize the local database. Generally, to initialize the database, the user schema and data must be copied from the server database to the client database, and change tracking must then be enabled on the database. The Sync Framework automatically performs this process the first time that a table is synchronized. Alternatively, you can manually create the client database and specify that it should be retained when synchronization happens. During the initialization process, some types of constraint are not copied, and some column types are managed differently. It is at this stage that the client database is assigned a ClientId property to uniquely identify it to the server.
11-34
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Synchronization Settings There are two key settings that you can configure when you synchronize data: •
SyncDirection. You use the SyncDirection property of a synchronizing table to configure the direction in which data flows during the synchronization process. You can set it to Snapshot, DownloadOnly, UploadOnly, or Bidirectional.
•
SyncAdapter. You use the SyncAdapter properties to configure the SQL commands to use to retrieve and insert data during the synchronization session. For example, you configure the SelectIncrementalInsertsCommand property to a SQL statement that retrieves the initial and incremental inserts from the server. Similarly, you configure the InsertCommand property to a SQL statement that applies inserts made at the client to the server during upload-only and bidirectional synchronization.
Change Tracking By default, changes are automatically tracked in the client database so that those changes can be synchronized with the server database. If you want to implement bidirectional or download-only synchronization, you must track changes in the server database. There are two ways to do this: create your own custom tracking mechanism by using row versions and triggers or use SQL Server 2008 Change Tracking. The first option can be complex and inhibit performance, so it is recommended that when you use SQL Server 2008, you use the inbuilt change tracking. SQL Server Change Tracking avoids the use of triggers and does not require the creation of extra database objects to track data. To configure change tracking, you first enable change tracking on the database and then on each table that requires tracking. Question: When should you use a custom change-tracking system?
Additional Reading For code examples and more information about initializing the local database, see the How to: Initialize the Client Database and Work with Table Schema page at http://go.microsoft.com/fwlink/?LinkID=194089. For more information about synchronization settings, see the How to: Specify Snapshot, Download, Upload, and Bidirectional Synchronization page at http://go.microsoft.com/fwlink/?LinkID=194090.
Building Occasionally Connected Solutions
For more information about change tracking, see the Tracking Changes in the Server Database page at http://go.microsoft.com/fwlink/?LinkID=194091.
11-35
11-36
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Synchronizing Data
Key Points After you configure synchronization, you can write code to perform the synchronization session. In this topic, you will see how to perform an upload-only synchronization.
f Upload client changes to a server database 1.
Add references to the following namespaces: •
Microsoft.Synchronization
•
Microsoft.Synchronization.Data
•
Microsoft.Synchronization.Data.Server
•
Microsoft.Synchronization.Data.SqlServerCe
•
System.Data
•
System.Data.SqlClient
•
System.Data.SqlServerCe
Building Occasionally Connected Solutions
2.
3.
Derive a class from the SqlServerSyncProvider class. Add code to the class to perform the following steps: a.
In the class constructor, instantiate a SqlConnection object, passing the connection string of the database as a parameter to the constructor.
b.
Create a SqlCommand object to retrieve a new anchor value from the server and set the SelectNewAnchorCommand property of the class to this SqlCommand object.
c.
Create a SyncAdapter object, define commands for the SelectIncrementalInsertsCommand, InsertCommand, UpdateCommand, and DeleteCommand properties of the SyncAdapter object, and then add the SyncAdapter object to the server synchronization provider.
Derive a class from the SqlCeClientSyncProvider class. Add code to the class to perform the following step: •
4.
11-37
In the class constructor, define the ConnectionString property of the class.
Derive a class from the SyncAgent class. Add code to the class constructor to perform the following steps: a.
Set the LocalProvider property of the class to your SqlCeClientSyncProvider object.
b.
Set the RemoteProvider property of the class to your SqlServerSyncProvider object.
c.
Declare and instantiate a SyncGroup object.
d. Declare and instantiate a SyncTable object for each table to be included in the synchronization.
5.
e.
Set the CreationOption property of each of the SyncTable objects.
f.
Set the SyncDirection property of each of the SyncTable objects.
g.
Set the SyncGroup property of each of the SyncTable objects to group related tables into one synchronization transaction.
h.
Add each SyncTable object to the SyncTables collection of the SyncAgent object.
In your application code, add code to perform the following steps: a.
Declare and instantiate your SyncAgent object.
11-38
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
b.
Call the Synchronize method of your SyncAgent object.
Question: What do you use a SyncGroup object for?
Additional Reading For more information about how to perform the other types of synchronization, see the How to: Specify Snapshot, Download, Upload, and Bidirectional Synchronization page at http://go.microsoft.com/fwlink/?LinkId=194090. For a complete code listing of the synchronization process, see the How to: Upload Incremental Data Changes to a Server page at http://go.microsoft.com/fwlink/?LinkID=194093.
Building Occasionally Connected Solutions
11-39
Handling Conflicts
Key Points Data conflicts can occur in synchronization applications where changes can be made at more than one node. Conflicts and errors are detected at the row level if a change has been made to that row at two or more nodes. Although the Sync Framework includes conflict detection, it is preferable to design your applications so that conflicts do not occur; the detection and resolution process increases network traffic and processing requirements. If the conflict occurs from an upload of data, it is detected by the server synchronization provider (typically the SqlServerSyncProvider provider). If the conflict occurs from a download of data, it is detected by the client synchronization provider (typically the SqlCeClientSyncProvider provider). When a conflict occurs, the synchronization provider raises the ApplyChangeFailed event, and you can determine the outcome of the conflict by providing a handler for this event. The handler is passed an ApplyChangeFailedEventArgs parameter that has two roles. First, it gives you information about the conflict that has occurred. Second, it enables you to determine the result of the conflict.
11-40
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
The Conflict member is a SyncConflict object that gives details about the conflict. The SyncConflict class has a member named ConflictType that indicates what type of conflict has occurred, as summarized in the following table. ConflictType value ClientInsertServerInsert
Description The client and server both insert a row that has the same primary key.
ClientUpdateServerUpdate The client and server change the same row. ClientUpdateServerDelete
The client updates a row and the server deletes the same row.
ClientDeleteServerUpdate
The client deletes a row and the server updates the same row.
ErrorsOccurred
An error prevents a row from being applied. The Error property of the ApplyChangeFailedEventArgs parameter gives more details.
In addition, the Conflict property has two properties named ClientChange and ServerChange, which are DataTable objects, that indicate which rows from the client and server that conflicted. You can use all of this information to decide the outcome of the conflict. You inform the Sync Framework of your decision by setting the Action property of the ApplyChangeFailedEventArgs parameter. The Action property is an ActionApply property with the values that are described in the following table. ActionApply value
Description
Continue
This is the default behavior. The change is not performed.
RetryApplyingRow
Another attempt is made to change the row.
RetryWithForceWrite
Another attempt is made to change the row, regardless of the values that will be changed.
The ApplyChangeFailedEventArgs parameter has two properties named Connection and Transaction so you can access the database and, if necessary, change the data, which is useful for the RetryApplyingRow setting. If you use the RetryWithForceWrite setting, you indicate that you want the change to occur regardless of the values that will be changed. Forced changes on the client are handled by the client synchronization provider. Forced changes on the server
Building Occasionally Connected Solutions
11-41
require logic in the code that applies changes to the server; in this case, the @sync_force_write session variable is set so that the SQL statement that applies the change can use this variable to force the write. Question: Where do you catch conflicts and write code to resolve them?
11-42
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Using the Configure Data Synchronization Dialog Box
Key Points In addition to hand coding data synchronization, you can use features in Microsoft Visual Studio® to configure synchronization. If you add a local database cache item to a project to store data locally, it automatically includes a .sync file that will include the configuration of the synchronization. To configure the file, you use the Configure Data Synchronization dialog box that opens when you add the local database cache item to the project. By default, the Configure Data Synchronization dialog box creates a downloadonly synchronization. However, you can modify the code after it is generated to support upload-only or bidirectional synchronization.
f Configure data synchronization 1.
Define a server connection to a SQL Server database.
2.
Define a client connection to a SQL Server Compact 3.5 database. If you want to create a new local database, leave the default setting for this value.
3.
Add tables to the synchronization group.
Building Occasionally Connected Solutions
11-43
4.
Click Show Code Example to copy the code example to initiate synchronization.
5.
If you do not want to use download-only synchronization and plan to modify the generated code to support upload-only synchronization, clear the Use SQL Server change tracking check box. Also clear this check box if you do not currently have access to the server.
6.
Click OK.
After you configure synchronization by using the dialog box, you must write code in your application to perform the synchronization. If you copied the code when you configured the synchronization, you can simply paste this in the appropriate place in your application code. The following code example shows the synchronization code that the dialog box generates. [Visual Basic] ' Call SyncAgent.Synchronize() to initiate the synchronization process. ' Synchronization only updates the local database, not your project's data source. Dim syncAgent As New LocalDataCacheSyncAgent() Dim syncStats As Microsoft.Synchronization.Data.SyncStatistics = syncAgent.Synchronize() ' TODO: Reload your project data source from the local database (for example, call the TableAdapter.Fill method).
[Visual C#] // Call SyncAgent.Synchronize() to initiate the synchronization process. // Synchronization only updates the local database, not your project's data source. LocalDataCacheSyncAgent syncAgent = new LocalDataCacheSyncAgent(); Microsoft.Synchronization.Data.SyncStatistics syncStats = syncAgent.Synchronize(); // TODO: Reload your project data source from the local database (for example, call the TableAdapter.Fill method).
The code that is added to the application is in partial classes; therefore, if you want to change the synchronization type, you can create a partial class for the SyncAgent implementation and change the SyncDirection properties of the tables in the OnInitialized method.
11-44
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Question: You use the Configure Data Synchronization dialog box to add and configure a local database cache in your project, but no synchronization occurs. What else must you do?
Building Occasionally Connected Solutions
11-45
Security Considerations for Synchronization
Key Points Consider the following security recommendations when you configure synchronization: •
Use the principle of least permission on synchronizing tables.
•
Configure server applications and server databases to expose minimal surface area to attack. For example, if you use Internet Information Services (IIS) as part of an n-tier application, do not enable the FTP service unless another application explicitly requires it.
•
Encrypt or password-protect data on disk and in transit.
•
Use stored procedures instead of SQL statements.
•
Validate data before you save it.
Question: How can you encrypt data in transit?
11-46
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Lab: Building Occasionally Connected Solutions
Objectives After completing this lab, you will be able to: •
Use LINQ to XML to cache entity data in XML files.
•
Use the Sync Framework to synchronize a SQL Server Compact 3.5 database with a SQL Server Express database.
Introduction In this lab, you will use LINQ to XML to cache data in local XML files. You will also use the Sync Framework to synchronize local data with server data.
Building Occasionally Connected Solutions
11-47
Lab Setup For this lab, you will use the available virtual machine environment. Before you begin the lab, you must: •
Start the 10265A-GEN-DEV-11 virtual machine, and then log on by using the following credentials: •
User name: Student
•
Password: Pa$$w0rd
11-48
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Lab Scenario
You have been asked to extend the Orders application to support salespeople who need to be able to query and update the SalesOrderHeader and SalesOrderDetails tables when they work in the field. You decide to evaluate two different technologies for building this application. One technology caches data locally in XML files when it is retrieved from the existing data access tier and then uses LINQ to XML to access this data if the data access tier is inaccessible. The other technology caches data locally in a SQL Server Compact database and then uses the Sync Framework to refresh the data cache and merge changes when the application is online.
Exercise 1: Modifying the Orders Application to Use Offline XML Data Scenario In this exercise, you will modify an existing application that connects to the existing data access tier Web service. You will modify the logic in the application to cache data locally in encrypted XML files when it is retrieved. If the Web service is
Building Occasionally Connected Solutions
11-49
not available, the application will use LINQ to XML to query the local XML data instead. In this mode, the data is read-only. The main tasks for this exercise are as follows: 1.
Prepare the environment for the lab.
2.
Open the starter project for this exercise.
3.
Write code to construct XML file names.
4.
Write code to determine whether the Web service is available.
5.
Write code to cache the contacts.
6.
Write code to cache the orders.
7.
Build and test the application.
f Task 1: Prepare the environment for the lab 1.
Log on to the 10265A-GEN-DEV-11 virtual machine as Student with the password Pa$$w0rd.
2.
In the E:\Labfiles folder, run EnvSetup.bat as an administrator. This file configures IIS and creates the required users and groups.
3.
In the E:\Labfiles folder, run AWReset.bat.
4.
In the E:\Labfiles\Lab11\VB\Ex1\Starter folder (if you are using Microsoft Visual Basic®), or E:\Labfiles\Lab11\CS\Ex1\Starter folder (if you are using Microsoft Visual C#®), run ExSetup.bat as an administrator. This script adds the required virtual directories to IIS.
5.
Open IIS Manager as an administrator.
6.
Add a new self-signed certificate named OrdersWebService.
7.
Edit the binding of the default Web site to use HTTPS and the OrdersWebService certificate.
8.
Configure the OrdersWebService application to require Secure Sockets Layer (SSL) security.
9.
Close IIS Manager.
f Task 2: Open the starter project for this exercise 1.
Run Visual Studio 2010 as an administrator.
11-50
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
2.
Open the existing solution, OrdersDAL.sln, in the E:\Labfiles\Lab11\VB\Ex1\Starter\OrdersDAL (if you are using Visual Basic) or E:\Labfiles\Lab11\CS\Ex1\Starter\OrdersDAL (if you are using Visual C#) folder.
f Task 3: Write code to construct XML file names 1.
In Visual Studio, review the task list.
2.
Open the code file behind the OrderManagementWindow.xaml window by double-clicking the TODO: Add the System.Xml.Linq namespace task in the task list.
3.
Immediately after the comment, add a statement that brings the System.Xml.Linq namespace into scope.
4.
Locate the next comment in the code file behind the OrderManagementWindow.xaml window by double-clicking the TODO: Define paths and filenames for XML files task in the task list.
5.
Immediately after the comment, add code that defines three private string variables named filePath, contactsFile, and ordersFile.
6.
Locate the next comment in the code file behind the OrderManagementWindow.xaml window by double-clicking the TODO: Set the file paths task in the task list. This task is located in the OrderManagementWindow constructor.
7.
Immediately after the comment, add code that sets the filePath variable to the path of the user's My Documents folder and sets the contactsFile variable to this path concatenated with the file name contacts.xml.
8.
Immediately after the next comment, add code that sets the ordersFile variable to the My Documents path concatenated with the word orders. The remainder of the file name will be constructed at run time.
f Task 4: Write code to determine whether the Web service is available 1.
Locate the next comment in the code file behind the OrderManagementWindow.xaml window by double-clicking the TODO: Check that the Web service is available. Return True if it is, false otherwise task in the task list.
2.
Immediately after the comment, add a private method called CheckWebServiceExists that takes an OrdersWebServiceClient object as a
Building Occasionally Connected Solutions
11-51
parameter and returns a Boolean value. This method should try to access the Orders Web service and return true or false to indicate whether a response was obtained.
f Task 5: Write code to cache the contacts 1.
Locate the next comment in the code file behind the OrderManagementWindow.xaml window by double-clicking the first TODO: Check whether the Web service is still operational task in the task list. . This task is located in the getContacts_Click method.
2.
Immediately after the comment, add a call to the CheckWebServiceExists method. If the service does not exist, call the getContactsFromLocalCache method and return from the method.
3.
Locate the next comment in the code file behind the OrderManagementWindow.xaml window by double-clicking the TODO: Cache the contacts task in the task list.
4.
Immediately after the comment, add a call to the SaveContactsToLocalCache method, passing the contacts object.
5.
Locate the next comment in the code file behind the OrderManagementWindow.xaml window by double-clicking the TODO: Get contact information from the local cache file task in the task list.
6.
Delete the existing code in this method, and then add code that performs the following tasks:
7.
a.
Instantiate a new instance of the list of contacts and then update the message in the statusMessage status bar item with the text "Fetching contacts …"
b.
If the contacts XML file exists, call the LoadContactsFromLocalCache method, display the data in the contactsGrid grid, display the number of contacts retrieved in the numContactRows label, and then update the message in the statusMessage status bar item with the text "Ready".
c.
If there are no cached contacts, update the message in the statusMessage status bar item with the text "No cached data available".
Locate the next comment in the code file behind the OrderManagementWindow.xaml window by double-clicking the TODO: Save contact information to the local cache file task in the task list.
11-52
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
8.
9.
Delete the existing code in this method, and then add code that performs the following tasks: a.
Construct an XElement object that contains the data in the contacts object that is passed to the method.
b.
Save the XElement object to the file specified by the contactsFile variable.
Locate the next comment in the code file behind the OrderManagementWindow.xaml window by double-clicking the TODO: Helper method. Read contact information from the local cache file task in the task list.
10. Delete the existing code in this method, and then add code that performs the following tasks: a.
Load the contents of the contacts file into an XDocument object.
b.
Convert the XML into a list of Contact objects and return this list.
f Task 6: Write code to cache the orders 1.
Locate the next comment in the code file behind the OrderManagementWindow.xaml window by double-clicking the second TODO: Check whether the Web service is still operational task in the task list. This task is located in the getOrders_Click method.
2.
Immediately after the comment, add a call to the CheckWebServiceExists method. If the service does not exist, call the getOrdersFromLocalCache method and return from the method.
3.
Locate the next comment in the code file behind the OrderManagementWindow.xaml window by double-clicking the first TODO: Cache the orders task in the task list. This task is located in the getOrders_Click method.
4.
Immediately after the comment, add a call to the SaveOrdersToLocalCache method, passing the orders object and the name of the cache file as parameters. Construct the name of the cache file by concatenating the text "General.xml" to the end of the value in the ordersFile string variable.
5.
Locate the next comment in the code file behind the OrderManagementWindow.xaml window by double-clicking the third TODO: Check whether the Web service is still operational task in the task list. This task is located in the getOrdersForContact_Click method.
Building Occasionally Connected Solutions
11-53
6.
Immediately after the comment, add a call to the CheckWebServiceExists method. If the service does not exist, call the getOrdersForContactFromLocalCache method and return from the method.
7.
Locate the next comment in the code file behind the OrderManagementWindow.xaml window by double-clicking the second TODO: Cache the orders task in the task list. This task is located in the getOrdersForContact_Click method.
8.
Immediately after the comment, add a call to the SaveOrdersToLocalCache method, passing the orders object and the name of the cache file as parameters. Construct the name of the cache file by concatenating the text "contactN.xml" to the end of the value in the ordersFile string variable, where N is the contactID of the contact.
9.
Locate the next comment in the code file behind the OrderManagementWindow.xaml window by double-clicking the fourth TODO: Check whether the Web service is still operational task in the task list. This task is located in the getOrdersForProduct_Click method.
10. Immediately after the comment, add a call to the CheckWebServiceExists method. If the service does not exist, call the getOrdersForProductFromLocalCache method and return from the method. 11. Locate the next comment in the code file behind the OrderManagementWindow.xaml window by double-clicking the third TODO: Cache the orders task in the task list. This task is located in the getOrdersForProduct_Click method. 12. Immediately after the comment, add a call to the SaveOrdersToLocalCache method, passing the orders object and the name of the cache file as parameters. Construct the name of the cache file by concatenating the text "productN.xml" to the end of the value in the ordersFile string variable, where N is the productID of the product. 13. Locate the next comment in the code file behind the OrderManagementWindow.xaml window by double-clicking the TODO: Get general order information from a local cache file task in the task list. 14. Delete the existing code in this method, and then add code that performs the following tasks: a.
Create a new empty list of orders and update the statusMessage status bar item with the text "Fetching orders …".
b.
If the general orders XML file exists, call the LoadOrdersFromLocalCache method to populate the list of orders, call
11-54
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
the displayOrders method to display the data in the ordersTree TreeView control in the window, display the number of orders in the numOrderRows label, and then update the statusMessage status bar item with the text "Ready". Note: The general orders XML file has the name "xxxxGeneral.xml" where the value of the xxxx prefix is specified by the ordersFile variable.
c.
If there are no cached orders, update the statusMessage status bar item with the text "No cached data available" and clear the ordersTree TreeView control.
15. Locate the next comment in the code file behind the OrderManagementWindow.xaml window by double-clicking the TODO: Get order information for a specified contact from a local cache file task in the task list. 16. Delete the existing code in this method, and then add code that performs the following tasks: a.
Create a new empty list of orders and update the statusMessage status bar item with the text "Fetching orders …".
b.
If the XML file containing orders for the specified contact exists, call the LoadOrdersForContactFromLocalCache method to populate the list of orders, call the displayOrders method to display the data in the ordersForContractTree TreeView control in the window, display the number of orders in the numOrderForContactRows label, and then update the statusMessage status bar item with the text "Ready".
Note: The orders XML file has the name "contactN.xml" located in the folder specified by the ordersFile variable where N is the contact ID.
c.
If there are no cached orders, update the statusMessage status bar item with the text "No cached data available" and clear the ordersForContractTree TreeView control.
17. Locate the next comment in the code file behind the OrderManagementWindow.xaml window by double-clicking the TODO: Get order information for a specified product from a local cache file task in the task list.
Building Occasionally Connected Solutions
11-55
18. Delete the existing code in this method, and then add code that performs the following tasks: a.
Create a new empty list of orders and update the statusMessage status bar item with the text "Fetching orders …".
b.
If the XML file containing orders for the specified contact exists, call the LoadOrdersForProductFromLocalCache method to populate the list of orders, call the displayOrders method to display the data in the ordersForProductTree TreeView control in the window, display the number of orders in the numOrderForProductRows label, and then update the statusMessage status bar item with the text "Ready".
Note: The orders XML file has the name "productN.xml" located in the folder specified by the ordersFile variable where N is the product ID.
c.
If there are no cached orders, update the statusMessage status bar item with the text "No cached data available" and clear the ordersForProductTree TreeView control.
19. Locate the next comment in the code file behind the OrderManagementWindow.xaml window by double-clicking the TODO: Save order information to the specified cache file task in the task list. 20. Delete the existing code in this method, and then add code that performs the following tasks: a.
Construct an XElement object that contains the data in the orders object that is passed to the method.
b.
Save the XElement object to the file specified by the fileName variable.
21. Locate the next comment in the code file behind the OrderManagementWindow.xaml window by double-clicking the TODO: Helper method to load general order information from a local cache file task in the task list. 22. Delete the existing code in this method, and then add code that performs the following tasks: a.
Load the contents of the orders file into an XDocument object. The name of the orders file is held in the fileName variable.
b.
Iterate through the XML content in the XDocument object and convert it into a list of Order objects, and then return this list.
11-56
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
23. Locate the next comment in the code file behind the OrderManagementWindow.xaml window by double-clicking the TODO: Helper method to load order information for a contact from a local cache file task in the task list. 24. Delete the existing code in this method, and then add code that performs the following tasks: a.
Load the contents of the orders file into an XDocument object. The name of the orders file is held in the fileName variable.
b.
Iterate through the XML content in the XDocument object and convert it into a list of Order objects, and then return this list.
Note: Use the getOrderDetailsFromCache method to retrieve the order details for each order from the local cache.
25. Locate the next comment in the code file behind the OrderManagementWindow.xaml window by double-clicking the TODO: Helper method to load order information for a product from a local cache file task in the task list. 26. Delete the code in this method, and then add code that performs the following tasks: a.
Load the contents of the contacts file into an XDocument object. The name of the orders file is held in the fileName variable.
b.
Iterate through the XML content in the XDocument object and convert it into a list of Order objects, and then return this list.
Note: Use the getOrderDetailsForProductFromCache method to retrieve the order details for each order from the local cache.
27. Locate the next comment in the code file behind the OrderManagementWindow.xaml window by double-clicking the TODO: Helper method to retrieve order details for an order task in the task list. 28. Delete the existing code in this method, and uncomment the code at end of the method definition so that the method receives an enumerable list of XElement objects containing order information as a parameter. 29. Add code to the method that performs the following tasks:
Building Occasionally Connected Solutions
11-57
a.
Instantiate a trackable collection of SalesOrderDetail objects.
b.
Extract the details of each order from the list of XElement objects and store them in the list of SalesOrderDetail objects.
c.
Add each SalesOrderDetail object to the trackable collection and then return this collection.
30. Locate the next comment in the code file behind the OrderManagementWindow.xaml window file by double-clicking the TODO: Helper method to retrieve order details for an order for a specified product task in the task list. 31. Delete the existing code in this method, and uncomment the code at end of the method definition so that the method receives an enumerable list of XElement objects containing order information and the productID value as parameters. 32. Add code to the method that performs the following tasks: a.
Instantiate a trackable collection of SalesOrderDetail objects.
b.
Extract the order details from the list of XElement objects and store them in the list of SalesOrderDetail objects.
c.
Add each SalesOrderDetail object to the trackable collection and then return it.
f Task 7: Build and test the application 1.
Build the solution and correct any errors.
2.
Start the application.
3.
In the Username box, type Fred and in the Password box, type Pa$$w0rd
4.
Retrieve contacts from 1 to 412.
5.
Open Windows Explorer, move to the C:\Users\Admin\My Documents folder, and then verify that a new XML file named contacts.xml has been created.
6.
Open the contacts.xml file in Windows Internet Explorer® to verify that the contact data that is displayed in the application has been written to the file.
7.
In the Order Management application, in the Username box, type Bert and in the Password box, type Pa$$w0rd
8.
On the General Orders tab, retrieve orders from 1 to 43784.
11-58
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
9.
In Windows Explorer, verify that a new XML file named ordersGeneral.xml has been created.
10. Open the ordersGeneral.xml file in Internet Explorer to verify that the contact data that is displayed in the application has been written to the file. 11. On the Orders By Contact tab, retrieve orders for contact 1. 12. In Windows Explorer, verify that a new XML file named orderscontact1.xml has been created. 13. On the Orders By Product tab, retrieve orders for product 776. 14. In Windows Explorer, verify that a new XML file named ordersproduct776.xml has been created. 15. Close Windows Explorer, and then close the Order Management application. 16. Open IIS Manager, and then stop the Orders Web service. 17. In Visual Studio, start the Order Management application. 18. In the Order Management application, in the Username box, type Fred and in the Password box, type Pa$$w0rd 19. Retrieve contacts from 1 to 412, and then verify that you can access the cached data. 20. In the Order Management application, in the Username box, type Bert and in the Password box, type Pa$$w0rd Note: It is not actually necessary to specify the credentials of a user when retrieving information from the local cache; these credentials are only required by the Web service.
21. On the General Orders tab, retrieve orders from 1 to 43784, and then verify that you can access the cached data. 22. On the Orders By Contact tab, retrieve orders for contact 1, and then verify that you can access the cached data. 23. On the Orders By Product tab, retrieve orders for product 776, and then verify that you can access the cached data. 24. On the Orders By Product tab, retrieve orders for product 777, and then verify that there is no cached data available. 25. Close the application. 26. In IIS Manager, start the Orders Web service.
Building Occasionally Connected Solutions
11-59
27. In Visual Studio, run all of the tests in the solution. 28. Verify that all of the tests succeed. 29. Save and close the solution, and then close Visual Studio.
Exercise 2: Modifying the Orders Application to Synchronize Locally Cached Data Scenario In this exercise, you will use the Sync Framework to implement an OCA. Data will be cached locally on the client computer by using a SQL Server Compact database. You will modify the Orders application to use this local database and synchronize with the data access tier when a connection is available. The main tasks for this exercise are as follows: 1.
Open the starter project for this exercise.
2.
Add local data caching to the Orders.
3.
Configure the synchronization.
4.
Add synchronization code to the OrdersService project.
5.
Add synchronization code to the user interface.
6.
Build and test the application.
f Task 1: Open the starter project for this exercise 1.
Open Visual Studio 2010.
2.
Open the existing solution, OrdersDAL.sln, in the E:\Labfiles\Lab11\VB\Ex2\Starter\OrdersDAL (if you are using Visual Basic) or E:\Labfiles\Lab11\CS\Ex2\Starter\OrdersDAL (if you are using Visual C#) folder.
f Task 2: Add local data caching to the Orders application 1.
Add a new local database cache named AWCache to the OrdersDAL project.
2.
Configure the server connection to connect to the AdventureWorks database on the local computer.
11-60
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
3.
Configure the client connection to connect to the AdventureWorksLocal.sdf SQL Server Compact 3.5 database in the E:\Labfiles\Lab11\VB\Ex2\Starter (if you are using Visual Basic) or E:\Labfiles\Lab11\CS\Ex2\Starter (if you are using Visual C#) folder.
4.
Add the Contact (Person), SalesOrderDetail (Sales), and SalesOrderHeader (Sales) tables to the synchronized database.
5.
Configure synchronization to not use SQL Server Change Tracking, and then initiate the first-time synchronization.
6.
Rebuild the solution.
f Task 3: Configure synchronization 1.
Add a new class named AWCacheSyncAgent to the OrdersDAL project.
2.
Modify the AWCacheSyncAgent class to be a public partial class.
3.
Add a private partial method called OnInitialized to the class. This method should take no parameters and not return a value. In this method, add code that sets the SyncDirection property of each synchronized table to be Bidirectional. You can access the synchronized tables by using the _person_ContactSyncTable, _sales_SalesOrderDetailSyncTable, and _sales_SalesOrderHeaderSyncTable fields in the AWCacheSyncAgent class.
Note: If you are using Visual Basic, do not declare the OnInitialized method as Partial.
f Task 4: Add synchronization code to the OrdersService project 1.
Add a reference to the Microsoft.Synchronization.Data version 2.0.0.0 assembly to the OrdersService project.
2.
Review the task list.
3.
Open the code file for the IOrdersService interface by double-clicking the first TODO: Synchronize the AdventureWorksLocal SQL Server CE database with the AdventureWorks database in SQL Server Express task in the task list. This task is located in the IOrdersService code file.
4.
Below the summary and returns comments, declare a method named SyncWithServer that takes no parameters and returns a Boolean value.
Building Occasionally Connected Solutions
11-61
5.
Open the code file for the OrdersServiceImpl class by double-clicking the TODO: Namespace containing types required by Synchronization Services task in the task list.
6.
After the comment, add a statement that brings the Microsoft.Synchronization.Data namespace into scope.
7.
Locate the next comment by double-clicking the second TODO: Synchronize the AdventureWorksLocal SQL Server CE database with the AdventureWorks database in SQL Server Express task in the task list.
8.
Below the summary and returns comments, implement the SyncWithServer method. In this method, use the AWCacheSyncAgent object to synchronize the local SQL Server Compact data with SQL Server Express.
f Task 5: Add synchronization code to the user interface 1.
Locate the SyncDatabase_Click method in the code file behind the OrderManagementWindow.xaml window by double-clicking the TODO: Add code here to synchronize data between SQL Server Express and AdventureWorksLocal.sdf task in the task list.
2.
Add code to this method to call the SyncWithServer method and display the success of the call to the user.
f Task 6: Build and test the application 1.
Build the solution and correct any errors.
2.
Start the application without debugging.
3.
Synchronize the data with SQL Server Express.
4.
Retrieve the orders for the contact with a Contact ID value of 1.
5.
Display the edit window for the details of the second order that this contact made.
6.
Change the quantity of the order to 4 and the discount to 0.5.
7.
Synchronize the data with SQL Server Express.
8.
In Visual Studio, using Server Explorer, examine the data in the AdventureWorks SQL Server Express database and verify that the changes were synchronized.
11-62
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
9.
In Server Explorer, locate the details for order 44132, change the OrderQty value to 99, and then press ENTER.
10. Return to the Order Management application and synchronize the data with SQL Server Express again. 11. Requery the orders for contact 1 and verify that order 44132 has been updated. 12. Close the Order Management application. 13. Save and close the solution, and then close Visual Studio.
Building Occasionally Connected Solutions
11-63
Lab Review
Review Questions 1.
Which class do you use to create attributes?
2.
What direction of synchronization does the Configure Data Synchronization dialog box create?
11-64
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Module Review and Takeaways
Review Questions 1.
What key advantage over DOM does LINQ to XML provide?
2.
Why would you choose to use synchronization instead of merge replication?
Best Practices Related to Caching Data by Using LINQ to XML Supplement or modify the following best practices for your own work situations: •
Unless you require functionality from the XDocument class, use the XElement class for simpler coding.
•
If you save sensitive data in XML files, encrypt the files to ensure greater security.
Building Occasionally Connected Solutions
11-65
Best Practices Related to Synchronization Supplement or modify the following best practices for your own work situations: •
If you implement download-only or bidirectional synchronization by using SQL Server 2008 databases, use SQL Server 2008 Change Tracking for better performance.
•
Configure server applications to expose the minimal surface area for attack.
•
Encrypt or password-protect data while it is being synchronized.
•
Validate data before you save it.
Querying Data by Using WCF Data Services
12-1
Module 12 Querying Data by Using WCF Data Services Contents: Lesson 1: Introducing WCF Data Services
12-3
Lesson 2: Creating a WCF Data Service
12-16
Lesson 3: Consuming a WCF Data Service
12-53
Lesson 4: Protecting Data and Operations in a WCF Data Service
12-84
Lab: Creating and Using WCF Data Services
12-99
12-2
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Module Overview
Windows® Communication Foundation (WCF) Data Services enables you to create highly flexible data services that you can use to provide access to data across the Internet or a corporate network. You can access these services by using Representational State Transfer (REST)-style URIs, and a wide variety of applications can easily consume them. WCF Data Services is built on top of standard Internet protocols such as HTTP and the Atom Publishing Protocol, so it is an ideal choice for delivering data to AJAX applications and rich interactive applications (RIAs) that were built by using technologies such as Microsoft® Silverlight®.
Objectives After completing this module, you will be able to: •
Describe the purpose and features of WCF Data Services.
•
Expose data by using a WCF Data Service.
•
Implement a client application that can consume a WCF Data Service.
•
Grant and restrict access to resources that a WCF Data Service exposes.
Querying Data by Using WCF Data Services
12-3
Lesson 1
Introducing WCF Data Services
WCF Data Services follows the REST architectural model and uses open Web standards such as the Open Data Protocol (OData). By following these standards, you can build solutions based on WCF Data Services that a wide variety of client applications can easily access, regardless of the technology that is used to implement them.
Objectives After completing this lesson, you will be able to: •
Describe the REST model for Web services.
•
Describe the role of OData.
•
Describe how WCF Data Services expose data by using the REST model.
•
Describe options for hosting a WCF Data Service.
12-4
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Introduction to the REST Model
Key Points The REST model uses a navigational scheme to represent business objects and resources over a network. In a REST-based architecture, clients initiate a request to the server for a given resource and the server processes the request and returns the appropriate response. REST services are designed to be accessed over HTTP, and require clients to use URLs or HTTP headers to access data and command operations. Typically, clients interact with REST services by using HTTP verbs such as GET, POST, PUT, and DELETE, which they can use to query, create, modify, and delete resources. For example, an organization might provide access to customer information, exposing the details of each customer as a single resource, by using a scheme similar to the following. http://adventureworks.com/customers/14
Accessing this URL causes the Web service to retrieve the data for Customer 14. This data can be returned in several simple formats such as JavaScript Object Notation (JSON) or "plain old XML" (POX), or even syndication formats such as
Querying Data by Using WCF Data Services
12-5
Atom. If the Adventure Works organization chooses to use POX, the result that is returned by querying the URL shown above might look something like the following code example. 14 Yang John Mr
The key to designing a REST-based solution is to understand how to divide a business model into a set of resources. In some cases, such as customers, this might be straightforward, but in other situations, it can be more of a challenge. Unlike services that have been developed by using WCF, which use SOAP, messages that are sent and received by using the REST model are much more compact. This is primarily because REST does not provide the same routing, policy, or security facilities that the WS-* specifications provide, and you have to rely on the underlying infrastructure that the Web server provides to protect REST Web services. However, this minimalist approach means that a REST Web service is usually much more efficient than the equivalent SOAP Web service when transmitting and receiving messages. Question: How does the REST model of Web services differ from the SOAP model?
12-6
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
The Role of the Open Data Protocol
Key Points Within an organization, the ability to share and reuse data is often limited by the application that creates or manages it. Sometimes, the data is organized and structured in such a way that it is difficult to utilize it, or even gain access to it, from applications other than those that were originally intended to be used with it. The need to reuse information across application boundaries increases as applications become more interconnected, and this information becomes more valuable to an organization the more it can be shared and accessed by other systems. Unfortunately, the use of closed data formats often limits this access. The OData protocol is an open Web protocol for querying and updating data that removes many of the obstacles that prevent interoperability between services publishing data and the applications that consume it. It is very useful for building interoperable REST Web services. OData makes it possible to expose data from a wide variety of data sources that can then be consumed programmatically by any client that is using a simple HTTP/AtomPub protocol as a means of communication.
Querying Data by Using WCF Data Services
12-7
OData provides a URL syntax in addition to defining common query expressions that can make it easier to access and query data. WCF Data Services fully supports the OData protocol. You can build a WCF Data Service to expose an entity data model as a set of resources by using the semantics and formats that the OData protocol specifies. A WCF Data Service can utilize the relationships between entities in an Entity Data Model (EDM), and publish these as navigational links between resources. Apart from WCF, several Microsoft tools and services support the OData protocol, including Microsoft SharePoint® Server 2010, Microsoft Excel® 2010 (through Microsoft SQL Server® PowerPivot for Excel), Windows Azure™ Storage, and SQL Server 2008 R2. Microsoft provides client libraries for building Microsoft .NET Framework applications, Silverlight, and AJAX, and client libraries are also available for PHP and Java. Question: What is the purpose of the OData protocol?
12-8
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Exposing Data by Using a WCF Data Service
Key Points By using WCF Data Services, you can create highly flexible data services that are tightly integrated with the Web and that client applications can easily consume. WCF Data Services use URIs to address data and simple, well-known formats to represent that data, such as XML and Atom. This results in data being served as a REST-style resource collection that is easy to interact with by using the standard HTTP verbs. By using WCF Data Services, you can expose data from relational data sources such as SQL Server through an EDM conceptual schema that is created by using the ADO.NET Entity Framework, and enable a client application to query and maintain data by using this schema. Note: WCF Data Services can also expose nonrelational data by using technologies, but this requires building customized classes. WCF Data Services operate most naturally with the model that the ADO.NET Entity Framework presents.
Querying Data by Using WCF Data Services
12-9
Defining a WCF Data Service A WCF Data Service is based on the System.Data.Services.DataService generic class. This class expects a type parameter that is a collection that contains at least one property that implements the IQueryable interface, such as the ObjectContext class for an entity set that is defined by using the Entity Framework. The DataService type implements the basic functionality to expose the entities in this collection as a series of REST resources. The following code example shows the definition of a WCF Data Service based on an ObjectContext class called AdventureWorks that is generated by using the ADO.NET Entity Framework. [Visual Basic] Public Class AdventureWorksDataService Inherits DataService(Of AdventureWorks) ... End Class
[Visual C#] public class AdventureWorksDataService : DataService { ... }
You can implement methods in a WCF Data Service that specify the entities to expose from the underlying entity model, and that configure the size of datasets that the data service presents. You can also override methods that are inherited from the DataService class to customize the way in which the service operates. By default, WCF Data Services use a simple addressing scheme that exposes the entity sets that are defined within the specified entity data model. When you consume a WCF Data Service, you address these entity resources as an entity set that contains instances of an entity type. The URI in the following code example returns all of the Reward entities that were defined in the entity model that was used to construct a WCF Data Service. http://adventureworks.com/Sales.svc/Rewards
The "/Rewards" element of the URI points to the Rewards entity set, which is the container for Reward instances.
12-10
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Question: What is the purpose of the DataService class?
Querying Data by Using WCF Data Services
12-11
Hosting a WCF Data Service
Key Points You host a WCF Data Service within an environment such as ASP.NET, which provides the required networking functionality, or by using a custom WCF host application.
Hosting a WCF Data Service by Using an ASP.NET Web Application If you use the WCF Data Services template to add a data service to an ASP.NET Web application or Web site, the template automatically configures the Web application or Web site with a WCF endpoint that a client application can use to connect to the service. You can also add an endpoint for a WCF Data Service to an ASP.NET Web application as follows: 1.
Add a service (.svc) file to the Web application.
2.
In the markup of the .svc file, set the value of the Factory attribute to System.Data.Services.DataServiceHostFactory. This type constructs instances of the specified DataService type by using the information that is provided in the configuration file.
12-12
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
3.
Set the value of the Service attribute to the fully qualified class name of the WCF Data Service.
The resultant ServiceHost declaration should resemble the following code example. [Visual Basic] WCFDataServiceHost.svc
[Visual C#] WCFDataServiceHost.svc
Hosting a WCF Data Service by Using the WebServiceHost Class To host a WCF Data Service in a Windows service or application, you can use the WebServiceHost class. The WebServiceHost class is a specialized implementation of the ServiceHost class that is used for hosting services by using HTTP and HTTPS. The WebServiceHost class can create an instance of a service object, configure the endpoints of the service by using the specified bindings and create listener objects for each endpoint address. When you create a WebServiceHost object, you specify the type of the class that implements the service in addition to the base address that the service should listen to for requests, as the following code example shows. [Visual Basic] ... Dim baseAddress As New Uri("http://localhost:8000/dataService") Dim host As New WebServiceHost(GetType(AdventureWorksDataService), baseAddress)
[Visual C#] ... Uri baseAddress = new Uri("http://localhost:8000/dataService");
Querying Data by Using WCF Data Services
12-13
WebServiceHost host = new WebServiceHost(typeof(AdventureWorksDataService),baseAddress);
If you do not specify an endpoint for a service, the WebServiceHost class automatically creates a default endpoint at the service's base address for HTTP and HTTPS base addresses. In this case, the WebServiceHost class automatically performs the following tasks: •
It configures the endpoint's binding to work with the associated Internet Information Services (IIS) security settings when it is used in a secure virtual directory.
•
It disables the HTTP help page and the WSDL GET functionality for the metadata endpoint.
•
It adds the WebHttpBehavior behavior to all endpoints that do not already have the behavior and that have a WebMessageEncodingElement configuration element.
•
If all of the operations on the service have either empty HTTP request bodies or deal with the HTTP request body as a stream, the WebServiceHost class automatically configures the appropriate content type mapper for the binding.
If you specify an endpoint manually, you must bind the WebServiceHost object to a WebHttpBinding binding, which you can create programmatically by calling the AddServiceEndpoint method as shown in the following code example. [Visual Basic] ... Dim binding As New WebHttpBinding() host.AddServiceEndpoint(GetType(System.Data.Services.IRequestHandler), binding, "WebServiceHost")
[Visual C#] ... WebHttpBinding binding = new WebHttpBinding(); host.AddServiceEndpoint(typeof(System.Data.Services.IRequestHandler), binding, "WebServiceHost");
Note: The WebHttpBinding binding is required by services that send and receive requests that contain POX data, rather than SOAP messages.
12-14
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
After you have created a WebServiceHost object and defined an endpoint, the service can start listening for requests by calling the Open method as shown in the following code example. [Visual Basic] ... host.Open()
[Visual C#] ... host.Open();
You stop a service by calling the Close method of the WebServiceHost class. This method stops the WCF runtime listening for any more requests and gracefully shuts down the service. The following table describes the events that the WebServiceHost class provides. You can use these events to track the state of the WebServiceHost object. Event
Description
Closed
Occurs when the WebServiceHost object has shut down.
Closing
Occurs when the WebServiceHost object is executing the Close method.
Faulted
Occurs when the WebServiceHost object encounters an unrecoverable error.
Opened
Occurs when the WebServiceHost object has successfully opened and is accepting requests.
Opening
Occurs when the WebServiceHost object is opening.
UnknownMessageReceived
Occurs when the WebServiceHost object receives an unknown message.
Querying Data by Using WCF Data Services
12-15
Question: What is the difference between the WebServiceHost class that is used to host a WCF Data Service and the ServiceHost class that many WCF services use?
Additional Reading For more information about how to host a REST Web service in a custom application, see the WebServiceHost Class page at http://go.microsoft.com/fwlink/?LinkID=194094.
12-16
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Lesson 2
Creating a WCF Data Service
You can add a WCF Data Service to a WCF service and to a Web application. Microsoft Visual Studio® provides the WCF Data Service template that you can use to build a WCF Data Service. This template generates a very basic data service class based on the DataService type and adds configuration information to the host configuration file. The idea is that you use this class and configuration as a starting point and add the necessary functionality to expose your data.
Objectives After completing this lesson, you will be able to: •
Implement a WCF Data Service that exposes data from an entity model.
•
Access the data that is exposed through a WCF Data Service from a Web browser.
•
Filter, select, and navigate through data by using query expressions.
•
Page through data that a WCF Data Service has exposed.
Querying Data by Using WCF Data Services
12-17
•
Configure a WCF Data Service by using the DataServiceConfiguration object.
•
Handle exceptions in a WCF Data Service.
•
Implement an operation in a WCF Data Service.
•
Deploy a WCF Data Service.
12-18
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Building a WCF Data Service for an Entity Data Model
Key Points To build a WCF Data Service that can expose relational data that is stored in a SQL Server, or other third-party, database, you should start by using the ADO.NET Entity Framework to define the conceptual model and generate the appropriate entity classes, as described earlier in this course. You implement a WCF Data Service in an ASP.NET Web application, an ASP.NET Web site, or a WCF service. Use the appropriate Visual Studio template for the type of solution that you want to build. The examples shown in this lesson are based on an ASP.NET Web site. After you have created an ASP.NET Web site, you can use the WCF Data Service template to generate a basic data service and add it to your application, as described in the following procedure.
X Add a WCF Data Service to an ASP.NET Web site 1.
In Solution Explorer, right-click the Web site project, and then click Add New Item.
Querying Data by Using WCF Data Services
12-19
2.
In the Add New Item dialog box, in the Installed Templates pane, select WCF Data Service.
3.
In the Name box, enter a name for the WCF Data Service, and then click Add.
A new WCF Data Service is added to your project and has an .svc extension. The WCF Data Service template generates the starter code that is needed to develop the service. The following code example shows the code that is provided by Visual Studio 2010 for a WCF Data Service called ProductDataService. [Visual Basic] Public Class ProductDataService ' TODO: replace [[class name]] with your data class name Inherits DataService(Of [[class name]]) ' This method is called only once to initialize service-wide policies. Public Shared Sub InitializeService(ByVal config As DataServiceConfiguration) ' TODO: set rules to indicate which entity sets and service operations are visible, updatable, etc. ' Examples: ' config.SetEntitySetAccessRule("MyEntityset", EntitySetRights.AllRead) ' config.SetServiceOperationAccessRule("MyServiceOperation", ServiceOperationRights.All) config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2 End Sub End Class
[Visual C#] public class ProductDataService: DataService< /* TODO: put your data source class name here */ > { // This method is called only once to initialize service-wide policies. public static void InitializeService(DataServiceConfiguration config) { // TODO: set rules to indicate which entity sets and service operations are visible, updatable, etc. // Examples:
12-20
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
// config.SetEntitySetAccessRule("MyEntityset", EntitySetRights.AllRead); // config.SetServiceOperationAccessRule("MyServiceOperation", ServiceOperationRights.All); config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2; } }
Notice that the WCF Data Service inherits from the generic DataService class. To enable the WCF Data Service to publish the data in an ADO.NET Entity Framework data model, specify the type of the ObjectContext object that is created for the entity model as the type parameter to the DataService class. In the following code example, the AdventureWorksEntities type is an ObjectContext object that is generated for an entity model. [Visual Basic] Public Class ProductDataService Inherits DataService(Of AdventureWorksEntities) ... End Class
[Visual C#] public class ProductDataService: DataService { ... }
Enabling Access to Resources in the Data Service For security reasons, WCF Data Services do not automatically expose any resources, such as entity collections that the entity model implements. You specify a policy that enables or disables access to resources in the InitializeService method of your data service. This method takes a DataServiceConfiguration object, which you can use to define the access policy. The DataServiceConfiguration class provides the SetEntitySetAccessRule method. The following code example shows an example of using this method. [Visual Basic] Public Class ProductDataService Inherits DataService(Of AdventureWorksEntities) Public Shared Sub InitializeService(ByVal config As DataServiceConfiguration)
Querying Data by Using WCF Data Services
12-21
config.SetEntitySetAccessRule("*", EntitySetRights.All) End Sub End Class
[Visual C#] public class ProductDataService: DataService { public static void InitializeService(DataServiceConfiguration config) { config.SetEntitySetAccessRule("*", EntitySetRights.All); } }
The parameters to the SetEntitySetAccessRule method are the name of a resource, and the access rights to grant over that resource. You can specify a resource explicitly, or you can use wildcard characters. The "*" value that is shown in the code example is a shorthand way of specifying all resources that the WCF Data Service publishes. The EntitySetRights.All value grants unrestricted access to these resources. Important: The previous code example uses the * wildcard character and the EntitySetRights.All value to set access rights on all of the entity collections that are available in the entity model. This is bad practice, and is shown for illustrative purposes. You should set access rights for resources on an individual basis. This is described in more detail later in this module.
Question: What constraint is placed on the type parameter for the DataService class?
12-22
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Accessing a WCF Data Service by Using a Web Browser
Key Points You can quickly access a WCF Data Service by using a Web browser. You specify the URL of a resource that the service exposes, and the browser issues the appropriate HTTP GET request to the service. The WCF Data Service processes the request and returns the data for the requested resource in AtomPub feed format. The URI format that WCF Data Services uses is based on the underlying entity model. You can access entity sets and individual entities in addition to traversing the relationships between entities. In the following code example, the ProductDataService WCF Data Service provides access to product information in the AdventureWorks database. The ProductDataService WCF Data Service is hosted by a Web site with the URL of http://MyHost/AdventureWorksSite. Using this service, you can retrieve a list of all of the entities that the service exposes by using a URI such as the following. http://MyHost/AdventureWorksSite/ProductDataService.svc
The data is returned in the format that is shown in the following code example.
Querying Data by Using WCF Data Services
12-23
Default Products ProductDocuments ProductListPriceHistories ProductModels ProductReviews
In this example, the ProductDataService WCF Data Service publishes data from entity sets called Products, ProductDocuments, ProductListPriceHistories, and ProductReviews. You can list all of the data in an entity set by specifying the entity name. The following example fetches all of the Products from the ProductDataService WCF Data Service. http://MyHost/AdventureWorksSite/ProductDataService.svc/Products
Important: WCF Data Services is case-sensitive. If you specify data by using the wrong case, WCF Data Services returns an HTTP 400 error.
To return a single entity from an entity set, you supply a filter predicate. This filter predicate represents the primary key in single-key entities. For example, to return the product that has a ProductID of 321, you use the following URI.
12-24
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
http://MyHost/AdventureWorksSite/ProductDataService.svc/Products(321)
The data that is returned contains all of the information for this product. This information includes the data type and value held in each field of the product, as the following code example shows. http://localhost:49224/AdventureWorksSite/ProductDataService.svc/P roducts(321) 2010-02-19T16:20:17Z
Querying Data by Using WCF Data Services
12-25
321 Chainring Nut CN-6137 false false Silver 1000 750 0.0000 0.0000 0 1998-0601T00:00:00 3314b1d7-ef69-4431-b6dddc75268bd5df 2004-0311T10:01:36.827
Note: By default, Windows Internet Explorer® interprets the data that is returned by a query such as this as an AtomPub feed, and displays the data in AtomPub format. You can display the data in the format shown above by turning off the feed-reading view in the Internet Options dialog box in Internet Explorer.
You can fetch the data for an individual field in an entity by specifying the name of the field at the end of the URI. For example, to fetch the name of product 321, use the following URI.
12-26
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
http://MyHost/AdventureWorksSite/ProductDataService.svc/Products(321)/ Name
The data that is returned looks like the following code example. Chainrin g Nut
Navigating Related Data Notice that, in the earlier code example, the data that is returned for product 321 includes links to related entities. You can use these links to fetch data from those entities. For example, to fetch all of the reviews for product 321, you can use the following URI. http://MyHost/AdventureWorksSite/ProductDataService.svc/Products(321)/ ProductReviews
This query references another entity set and returns a list of all matching entities from this set. You can query individual product reviews, and fields within products reviews, by specifying the ID of the review and appending the name of the field to the end of the URI, following the pattern shown in the previous examples. Question: Based on the ProductDataService example, what information does the following URI identify? http://MyHost/AdventureWorksSite/ProductDataService.svc/ProductModels( 1)/Products
Querying Data by Using WCF Data Services
12-27
Filtering, Selecting, and Ordering Data by Using Query Expressions
Key Points When you query a resource, all of the data for that resource is retrieved by default. However, by using WCF Data Services, you can use HTTP query strings to limit and sort data. WCF Data Services supports a range of operators and expressions that you can combine to refine the data that a query returns in addition to sorting data. The following table describes the main query options that are available. Query option
Description
$filter
Filters the returned entity set by using query operators and functions.
$select
Restricts the data that the URI returns to those fields that the query specifies.
$orderby
Orders results by using the specified query operators.
12-28
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Query option $expand
Description Returns a set of related entities together with the addressed entity in a single response.
You construct query expressions by combining query options, query operators, and query functions. For example, the following URI returns all Product entities where the Color property is equal to Red. http://MyHost/AdventureWorksSite/ProductDataService.svc /Products?$filter=Color eq 'Red'
The $filter option enables you to combine references to columns and literals to construct expressions by using query operators. The following table lists some of the common logical and arithmetic operators that are available. Query operator
Description
eq
Returns true if both operands have the same value.
ne
Returns false if both operands have the same value.
lt
Returns true if the first operand is less than the second.
gt
Returns true if the first operand is greater than the second.
le
Returns true if the first operand is less than or equal to the second.
ge
Returns true if the first operand is greater than or equal to the second.
and
Combines expressions together. Returns true if both expressions are true.
or
Combines expressions together. Returns true if either expression is true.
not
Negates an expression.
add
Returns the sum of the operands (if they are numeric), or concatenates the operands together (if they are strings).
sub
Returns the value resulting from subtracting the second operand from the first. This operator is only defined for numeric operands.
Querying Data by Using WCF Data Services
Query operator
12-29
Description
mul
Returns the product of two operands. This operator is only defined for numeric operands.
div
Returns the result of dividing the first operand by the second. This operator is only defined for numeric operands.
mod
Returns the integer remainder after dividing the first operand by the second. This operator is only defined for numeric operands.
Selecting Data The $filter option enables you to specify which entities you want to include in the results that are returned by referencing a URI. You can use the $select option to limit the fields or properties that are returned for each entity. The following example shows how to fetch the ProductID, Name, and ProductNumber fields for all Product entities. http://MyHost/AdventureWorksSite/ProductDataService.svc /Products?$select=ProductID,Name,ProductNumber
Sorting Data You can specify the order in which data is presented by using the $orderby option. You indicate the sort keys as a comma-delimited list of field or property names. Data is presented in ascending order of the specified sort keys, although you can specify the desc sort key to retrieve data in descending order. The following example fetches the ProductID, Name, and ProductNumber fields for all Product entities in descending order of the Name field. http://MyHost/AdventureWorksSite/ProductDataService.svc /Products?$select=ProductID,Name,ProductNumber&orderby=Name desc
Note: You can combine query options by using the & character to join elements of an HTTP query.
Fetching Related Data You can fetch data that is related to an entity by using the $expand option. You specify a comma-separated list of related entities, and WCF Data Services navigates the relationships between these entities to retrieve the data that is associated with
12-30
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
the entity that is specified as the primary target of the query. The following example fetches the data for all Product entities, and for each Product entity, it also retrieves the related ProductReview and ProductDocument entities. http://MyHost/AdventureWorksSite/ProductDataService.svc /Products?$expand=ProductReviews,ProductDocuments
The names that are specified in the $expand option should be the names of the corresponding navigation properties that are used to link the entities together in the underlying entity model. Note that, if you attempt to directly fetch data from an entity that is not directly related to the primary entity that is specified by a query, the WCF Data Service generates an HTTP 400 error. However, you can fetch data indirectly through a related entity set. For example, in the Adventure Works database, the Product table is not directly related to the ProductModelIllustration table; instead, Product is related to another table called ProductModel, which is in turn related to ProductModelIllustration. If you include these tables in the entity model for the WCF Data Service, you can use the following query to fetch information from the Products entity set and retrieve all related data from the ProductModelIllustrations entity set through the ProductModel entity set. http://MyHost/AdventureWorksSite/ProductDataService.svc/Products?$expa nd=ProductModel/ProductModelIllustrations
Question: You use the following query to attempt to retrieve the data for all products that have a ProductID greater than 100, but the query fails with an HTTP 400 error. Why? http://MyHost/AdventureWorksSite/ProductDataService.svc /Products?$select ProductID gt 100
Additional Reading For more information about addressing resources in a WCF Data Service, see the Addressing Resources (WCF Data Services) page at http://go.microsoft.com/fwlink/?LinkID=194095.
Querying Data by Using WCF Data Services
12-31
Paging Through Data That a WCF Data Service Exposes
Key Points By default, when you specify a query, WCF Data Services returns all resources that satisfy that query. If you have a large set of resources, performing an unfiltered query could potentially return a large amount of data and consume considerable bandwidth. In most cases, rather than submitting a query that fetches all of the data at once, you should instead use paging to fetch the data in manageable chunks. This strategy also avoids wasting bandwidth if the user is only really interested in the first few rows.
Implementing Paging in a Query WCF Data Services provides the $top and $skip options to implement paging when you query data. The $top option fetches the first n items of data that satisfy the query, where n is a value that the user specifies. The following example retrieves the first five Product items from the ProductDataService service. http://MyHost/AdventureWorksSite/ProductDataService.svc /Products?$top=5
12-32
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
The $skip option enables you to specify that a query should omit the first n results. You can use $skip and $top together to fetch data in chunks, or pages (page is just a term that means a chunk of data). The following example retrieves the next five Product items from the ProductDataService service, starting at item 6. http://MyHost/AdventureWorksSite/ProductDataService.svc/Products?$skip =5&$top=5
Note that, if you include options such as $filter and $orderby, they are evaluated first before the $top and $skip options are applied.
Limiting Page Sizes in a WCF Data Service A WCF Data Service can limit the size of pages that a query returns by using the DataServiceConfiguration object that configures the service. The DataServiceConfiguration class provides the SetEntitySetPageSize method, which you can use to specify the maximum page size when the user queries an entity. The best place to call this method is in the InitializeService method that runs when the service is first instantiated. The SetEntitySetPageSize method takes two parameters: the name of an entity set, and the maximum page size for that entity set. The following code example limits the page size for the Products entity set to 10 items. [Visual Basic] Public Class ProductDataService Inherits DataService(Of AdventureWorksEntities) Public Shared Sub InitializeService(ByVal config As DataServiceConfiguration) ... config.SetEntitySetPageSize("Products", 10) End Sub End Class
[Visual C#] public class ProductDataService: DataService { public static void InitializeService(DataServiceConfiguration config) { ... config.SetEntitySetPageSize("Products", 10); } }
Querying Data by Using WCF Data Services
12-33
You can specify the "*"wildcard character for the name of the entity set, in which case the maximum page size is set for all entities that the WCF Data Service exposes. You can specify the value 0 for the second parameter to remove the pagesize limitation. Any limits that you specify in this way override the maximum page size that the user may specify when fetching data. For example, if you limit the page size of the Products entity set to 10 items, if the user attempts to fetch the top 20 items, only the first 10 will be retrieved. However, if the user attempts to list the top five items, only the first five items will be fetched. Important: Paging requires that a service implements the V2 OData protocol. You can specify the version of the OData protocol that a WCF Data Service implements by setting the MaxProtocolVersion property of the DataServiceBehavior property of the DataServiceConfiguration object to the value DataServiceProtocolVersion.V2, as the following code example shows.
[Visual Basic] Public Class ProductDataService Inherits DataService(Of AdventureWorksEntities) Public Shared Sub InitializeService(ByVal config As DataServiceConfiguration) ... config.DataServiceBehavior.MaxProtocolVersion = _ DataServiceProtocolVersion.V2 End Sub End Class
[Visual C#] public class ProductDataService: DataService { public static void InitializeService(DataServiceConfiguration config) { ... config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2; } }
12-34
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Question: You are using paging to fetch data in chunks. The WCF Data Service limits the maximum page size to 20 items for all entities. How can you fetch items 45 to 54 inclusive from a particular entity set?
Querying Data by Using WCF Data Services
12-35
Configuring a WCF Data Service by Using a DataServiceConfiguration Object
Key Points When a WCF Data Service starts running, it creates a DataServiceConfiguration object and passes it to the InitializeService static method of the data service class. In the InitializeService method, you have seen that you can call methods on this DataServiceConfiguration object to modify the behavior of a WCF Data Service, specify which entity sets are visible through the service, indicate the access rights that users have over those entities, and set the maximum page size for retrieving data. The DataServiceConfiguration class also provides properties that you can use to limit the operations that a user can perform by using your service.
Limiting Use of the $expand Option When you query data by using the $expand option, the WCF Data Service fetches data that is related to the primary entity that you specify in query. This could potentially retrieve a large amount of information. You can set the MaxExpandCount property of the DataServiceConfiguration object to limit the number of related entity types that a query returns that specifies the $expand
12-36
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
option. If the user specifies a query that exceeds this limit, the query fails with an HTTP 400 error. Note: The MaxExpandCount property limits the number of related entity types, not the number of related entities. For example, if you set MaxExpandCount to 1, you can perform queries that fetch data from a single entity set that is related to the primary entity. There might be any number of related entities in this entity set. However, if you try to fetch related data from more than one related entity set, you will receive an HTTP 400 error.
When you use the $expand option to fetch data that is related to the primary entity, remember that you can also fetch data that is indirectly related to the primary entity by specifying an appropriate path, as shown in the following example. http://MyHost/AdventureWorksSite/ProductDataService.svc/Products?$expa nd=ProductModel/ProductModelIllustrations
You can limit the amount of indirection that a WCF Data Service supports by using the MaxExpandDepth property of the DataServiceConfiguration object. In this example, the depth of the query is 2. If you want to restrict queries to fetching directly related data only, you can set the MaxExpandDepth property to 1. The following code example shows how to set the MaxExpandCount and MaxExpandDepth properties. [Visual Basic] Public Class ProductDataService Inherits DataService(Of AdventureWorksEntities) Public Shared Sub InitializeService(ByVal config As DataServiceConfiguration) ... config.MaxExpandCount = 1 config.MaxExpandDepth = 1 End Sub End Class
Querying Data by Using WCF Data Services
12-37
[Visual C#] public class ProductDataService: DataService { public static void InitializeService(DataServiceConfiguration config) { ... config.MaxExpandCount = 1; config.MaxExpandDepth = 1; } }
Note: Setting MaxExpandCount or MaxExpandDepth to zero effectively disables the $expand option for a WCF Data Service.
Limiting Counts and Projections The DataServiceConfiguration class contains another property called MaxResultsPerCollection that you can use to restrict the number of items that are returned from each entity set in a query. The DataServiceBehavior property of the DataServiceConfiguration class enables you to define some additional behaviors. In particular, the AcceptProjectionRequests property is a Boolean value that indicates whether queries can include the $select option to limit the fields that a query returns. In addition, the AcceptCountRequests property is another Boolean value that indicates whether queries can include the $count and $inlinecount options that return the number of matching entities. For example, the following query returns a single-integer value that indicates the number of Product entities in the ProductDataService service. http://MyHost/AdventureWorksSite/ProductDataService.svc/Products/$coun t
Counting data can temporarily lock resources. You can disable this functionality as shown in the following code example. [Visual Basic] Public Class ProductDataService Inherits DataService(Of AdventureWorksEntities) Public Shared Sub InitializeService(ByVal config As DataServiceConfiguration)
12-38
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
... config.DataServiceBehavior.AcceptCountRequests = False End Sub End Class
[Visual C#] public class ProductDataService: DataService { public static void InitializeService(DataServiceConfiguration config) { ... config.DataServiceBehavior.AcceptCountRequests = false; } }
Question: You want to prevent users from using the $expand option to fetch data that is related to an entity, so you set the MaxExpandDepth property of the DataServiceConfiguration object for the service to zero. However, all queries—not just queries that use the $expand option—now fail with an HTTP 400 error. Why is this, and what value should you have set the MaxExpandDepth property to?
Querying Data by Using WCF Data Services
12-39
Handling Exceptions in a WCF Data Service
Key Points By default, when an exception occurs in a WCF Data Service when processing a request, the exception is passed to the HandleException method in the DataService class (if the exception has not already been caught and handled elsewhere in your service). You can customize the way in which exceptions are handled by overriding this method in your data service. The HandleExceptionArgs object that is passed as a parameter to this method has properties that provide information about the cause of the exception. The two most useful properties are Exception, which contains an exception object that provides the details of the exception, and ResponseStatusCode, which contains the HTTP status code that will be returned to the client application that is submitting the request to your service. A common technique is to record the details of the exception to the application event log, as shown in the following code example. [Visual Basic] Public Class ProductDataService Inherits DataService(Of AdventureWorksEntities) ...
12-40
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Protected Overloads Overrides Sub HandleException(ByVal args As HandleExceptionArgs) logException(args.Exception, args.ResponseStatusCode.ToString()) MyBase.HandleException(args) End Sub Private Sub logException(ByVal ex As Exception, ByVal responseCode As String) If Not EventLog.SourceExists(eventSource) Then EventLog.CreateEventSource(eventSource, eventLog) End If Dim eventMessage As String = String.Format("{0}: {1}: {2}", responseCode, ex.Message, System.Threading.Thread.CurrentPrincipal.Identity.Name) EventLog.WriteEntry(eventSource, eventMessage, EventLogEntryType.[Error]) End Sub Private Const eventSource As String = "ProductDataService" Private Const eventLog As String = "Application" End Class
[Visual C#] public class ProductDataService : DataService { ... protected override void HandleException(HandleExceptionArgs args) { logException(args.Exception, args.ResponseStatusCode.ToString()); base.HandleException(args); } private void logException(Exception ex, string responseCode) { if (!EventLog.SourceExists(eventSource)) { EventLog.CreateEventSource(eventSource, eventLog); } string eventMessage = string.Format("{0}: {1}: {2}", responseCode, ex.Message, System.Threading.Thread.CurrentPrincipal.Identity.Name); EventLog.WriteEntry(eventSource, eventMessage,
Querying Data by Using WCF Data Services
12-41
EventLogEntryType.Error); } private const string eventSource = "ProductDataService"; private const string eventLog = "Application"; }
When you build a client application, exceptions are passed across the network as serialized DataServiceException objects. The HandleException method handles all exceptions, not just DataServiceException exceptions. Therefore, in the HandleException method, if you want to pass information back to the client application, you must ensure that all exceptions are converted into DataServiceException objects so that the runtime can serialize them and handle them correctly. The Exception property of the args HandleExceptionArgs parameter is writable. If you detect an exception other than a DataServiceException exception in the HandleException method, you can construct a new DataServiceException object that wraps the exception, assign it to the Exception property of the args object, and then pass the args object as the parameter to the HandleException method of the base DataService class to propagate the exception to the client application. Note: If you create your own DataServiceException object, ensure that you also specify a valid and appropriate HTTP status code. HTTP status codes range from 100 to 599, and each code has a specific meaning. For example, do not create a DataServiceException object with HTTP status codes in the 200–299 range because these are intended to indicate successful operations. Similarly, status codes in the 100–199 range indicate informational messages that the Web server infrastructure uses to respond to protocol requests. Status codes in the 300–399 range indicate redirection messages that require a client application to take further action to resolve the request. The most common status codes are those in the 400–499 range, which indicate errors that are caused by bad client application requests, and the codes in the 500–599 range, which indicate server errors.
For security reasons, the details of any exceptions are not propagated to the client by default. However, for debugging purposes, you can include detailed information about the causes of an exception by setting the UseVerboseErrors property of the DataServiceConfiguration object to true. If an exception occurs before the data service has started (there is a problem with its configuration, for example), the HandleException method will not run. To debug problems such as this, you can temporarily define a WCF service behavior that sets the IncludeExceptionDetailInFaults property to true, either as part of the
12-42
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
WCF configuration in the Web.config file, or by applying the ServiceBehavior attribute to the data service, as shown in the following code example. [Visual Basic] Imports System.ServiceModel ... _ Public Class ProductDataService Inherits DataService(Of AdventureWorksEntities) ... End Class
[Visual C#] using System.ServiceModel; ... [ServiceBehavior(IncludeExceptionDetailInFaults=true)] public class ProductDataService : DataService { ... }
Important: If you set the IncludeExceptionDetailInFaults property to true for a data service, make sure that you disable this feature before you deploy the service in a production environment.
Question: If you don't override the HandleException method in your data service, what happens if an exception occurs?
Querying Data by Using WCF Data Services
12-43
Implementing Operations in a WCF Data Service
Key Points The primary purpose of a WCF Data Service is to provide access to data. However, you can also implement business operations that manipulate data. A WCF Data Service operation is simply a method of the data service that is visible to the REST infrastructure and that can be accessed by sending an HTTP GET, PUT, POST, or DELETE request. Data service operations that are accessed by using a GET request should be annotated with the WebGet attribute. These operations typically return data, although they may run some business logic that does not return a value. Operations that are accessed by using PUT, POST, or DELETE requests should be annotated with the WebInvoke attribute. These operations typically modify the data that the service uses in some way. Like entity sets, you must explicitly enable access to operations that a WCF data service exposes. You can perform this task by calling the SetServiceOperationAccessRule method of the DataServiceConfiguration object when you initialize the service. You specify the name of the operation, and the appropriate access rights.
12-44
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Note: Access rights for resources are described in detail later in this module.
A data service operation can take parameters and return one of the following data types: •
IEnumerable or IQueryable (where T represents an entity type in the service). If an operation returns an enumerable collection based on an entity type that the service recognizes, a client application can perform queries by specifying HTTP URIs in the manner shown in the previous topics in this lesson. Implementing an operation that returns an enumerable collection in this way gives you detailed control over the contents of the collection; it is the responsibility of your code to generate this collection, possibly based on information that the client application provides. The following code example shows an operation that retrieves products that have a color specified as a parameter. [Visual Basic] Public Class ProductDataService Inherits DataService(Of AdventureWorksEntities) Public Shared Sub InitializeService(ByVal config As DataServiceConfiguration) ... config.SetServiceOperationAccessRule("ProductsByColor", ServiceOperationRights.ReadMultiple) End Sub ... _ Public Function ProductsByColor(ByVal color As String) As IQueryable(Of Product) If Not [String].IsNullOrEmpty(color) Then Return From p In Me.CurrentDataSource.Products _ Where [String].Equals(p.Color, color) _ Select p Else Throw New ArgumentException("Color must be specified", "color") End If End Function End Class
Querying Data by Using WCF Data Services
12-45
[Visual C#] public class ProductDataService : DataService { public static void InitializeService( DataServiceConfiguration config) { ... config.SetServiceOperationAccessRule("ProductsByColor", ServiceOperationRights.ReadMultiple); } ... [WebGet] public IQueryable ProductsByColor(string color) { if (!String.IsNullOrEmpty(color)) { return from p in this.CurrentDataSource.Products where String.Equals(p.Color, color) select p; } else { throw new ArgumentException("Color must be specified", "color"); } } }
Note: You can access the data source for a WCF Data Service by using the CurrentDataSource property.
You can invoke this operation by specifying a URI such as this. Notice that you specify the parameter to the method by using an HTTP query string. http://MyHost/AdventureWorksSite/ProductDataService.svc/ProductsBuColo r?color='Red'
•
T (where T represents an entity type in the service). An operation can return a single instance of an entity. The following code example shows an operation that retrieves a product that has a specified ID. Notice that you should also annotate an operation that returns a scalar value rather than a collection with the SingleResult attribute.
12-46
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
[Visual Basic] Public Class ProductDataService Inherits DataService(Of AdventureWorksEntities) Public Shared Sub InitializeService(ByVal config As DataServiceConfiguration) ... config.SetServiceOperationAccessRule("ProductByID", ServiceOperationRights.ReadMultiple) End Sub ... _ _ Public Function ProductByID(ByVal id As Integer) As Product Dim prod As Product = (From p In Me.CurrentDataSource.Products _ Where p.ProductID = id _ Select p).[Single]() Return prod End Function End Class
[Visual C#] public class ProductDataService : DataService { public static void InitializeService( DataServiceConfiguration config) { ... config.SetServiceOperationAccessRule("ProductByID", ServiceOperationRights.ReadMultiple); } ... [WebGet] [SingleResult] public Product ProductByID(int id) { Product prod = (from p in this.CurrentDataSource.Products where p.ProductID == id select p).Single(); return prod; } }
Querying Data by Using WCF Data Services
•
12-47
A primitive value. The next code example shows an operation that retrieves a count of all products that have a specified color. [Visual Basic] Public Class ProductDataService Inherits DataService(Of AdventureWorksEntities) Public Shared Sub InitializeService(ByVal config As DataServiceConfiguration) ... config.SetServiceOperationAccessRule("NumProductsByColor", ServiceOperationRights.ReadMultiple) End Sub ... _ _ Public Function NumProductsByColor(ByVal color As String) As Integer If Not [String].IsNullOrEmpty(color) Then Return (From p In Me.CurrentDataSource.Products _ Where [String].Equals(p.Color, color) _ Select p).Count() Else Throw New ArgumentException("Color must be specified", "color") End If End Function End Class
[Visual C#] public class ProductDataService : DataService { public static void InitializeService( DataServiceConfiguration config) { ... config.SetServiceOperationAccessRule("NumProductsByColor", ServiceOperationRights.ReadMultiple); } ... [WebGet]
12-48
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
[SingleResult] public int NumProductsByColor(string color) { if (!String.IsNullOrEmpty(color)) { return (from p in this.CurrentDataSource.Products where String.Equals(p.Color, color) select p).Count(); } else { throw new ArgumentException("Color must be specified", "color"); } } }
•
void. Not all operations have to return a value.
Question: How do you specify that an operation in a WCF Data Service returns a scalar value rather than a collection?
Additional Reading For more information about implementing service operations in a WCF Data Service, see the Service Operations (WCF Data Services) page at http://go.microsoft.com/fwlink/?LinkID=194096.
Querying Data by Using WCF Data Services
12-49
Deploying a WCF Data Service
Key Points You can deploy a WCF Data Service in several ways, depending on how you have implemented the WCF Data Service.
Deploying a WCF Data Service in an ASP.NET Web Application If you have implemented the WCF Data Service in an ASP.NET Web application, you can use the Copy Web Site command. This command displays a dialog box that enables you to specify the target Web site and select the files and folders to copy to that Web site. You can also synchronize the target Web site with the source Web site if the files on the source Web site are newer.
X Copy files by using the Copy Web Site command 1.
In Solution Explorer, right-click the ASP.NET Web application, and then click Copy Web Site.
2.
In the Connections list, select the site to connect to as the remote site.
12-50
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
If the site that you want to connect to is not in the list, click Connect, and then use the Open Web Site dialog box to connect to the site that you want to copy files to or from. When the Copy Web Site tool opens the remote site, it examines the files on both sites and indicates their status (New, Unchanged, Changed, or Deleted). If there are two different versions of a file, one on the source site and one on the remote site, an arrow points from the newer version to the older version. Note: To see files that have been deleted, select the Show Deleted Files check box. The names of deleted files have a glyph next to them that indicates that they have been deleted.
3.
In the Source Web Site list box, select the files and folders that you want to copy.
4.
Click the copy button between the Source Web Site and Remote Web Site lists, using the button that indicates the direction in which you want to copy (you can also copy files from the target Web site back to the source Web site). To copy the recent version of a file to the site with the older version, click the synchronize button. The status pane at the bottom of the window displays the results of the copy operation.
Note: Files that have not changed are not copied.
Synchronization can detect situations that require you to indicate how it should proceed. The following table summarizes these conditions. Condition
Result
File has been deleted on a site.
Prompts you to indicate whether you want to delete the corresponding file in the other site.
A file has different timestamps on both sites. (The file has been added or edited at different times in both sites.)
Prompts you to indicate which version of the file you want to keep.
Querying Data by Using WCF Data Services
12-51
The Copy Web Site command is useful for manually copying the files for a Web application. You can also use the Publish Web Site Wizard, which compiles and copies the executable files and Web pages for your Web application to a target Web site, and configures the application. Note that the Publish Web Site Wizard does not copy the source code from any of the codebehind files to the target site; it only publishes the compiled code. To run the Publish Web Site Wizard, on the Build menu, click Publish Web Site.
Deploying a WCF Data Service in a WCF Service Application If you have implemented the WCF Data Service within a WCF service application, you can use the Create Package command to build a deployment package, or use the Publish command to publish the service to a Web site that supports one-click publishing. Both commands use the Package and Publish settings for the project to specify how to deploy the service. You configure the Package and Publish settings for a project by using the Package or Publish tab in the Properties window of the project. Alternatively, you can click the Package or Publish Settings command on the Project menu. The Package or Publish tabs enable you to specify that the files that make up the service should be compressed into a .zip file, and the name of the application and Web site that this file should be unzipped into when the service is deployed. You can also specify which files are included in the .zip file, that is, whether you include only the binaries and configuration files that are needed to run the application, or all files (including source files) that the project uses. Note: If you do not create a .zip file, the Create Package command instead creates a folder structure that contains all of your files when you generate the package.
If your service accesses a SQL Server database, you can include and edit the necessary connection strings by using the Deploy SQL tab. The Deploy SQL tab also enables you to include SQL scripts to configure the database and that are run when the service is deployed. The Create Package command generates the .zip file or folder structure by using the Package or Publish settings. You can run the Create Package command by right-clicking the service in Solution Explorer and clicking Create Package, or by using the Create Package command on the Project menu. The Create Package command also creates a command script that you can use to deploy the service. This command script uses MSDeploy to deploy the service, and
12-52
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
it reads the values that you specified for the various deployment settings from an XML file that the Create Package command also generates. You can deploy a package manually by copying the files that are generated by the Create Package command to the target server and running the deployment script. Alternatively, you can use the Publish Web Wizard to automate the package and deployment process. You run the Publish Web Wizard by right-clicking the WCF Service project in Solution Explorer and clicking Publish, or by using the Publish command on the Build menu. The Publish Web Wizard lets you select the destination and the publication method: •
Click MSDeploy to deploy the service to a Web server that supports one-click publishing.
•
Click FTP to copy the files for the service to a File Transfer Protocol (FTP) server.
•
Click File System to deploy the service to a local Web server.
•
Click FPSE to deploy the service by using Microsoft FrontPage Server Extensions.
The remaining deployment settings are gathered from the Package and Publish settings for the service. Question: You need to deploy a WCF Data Service on multiple Web servers. Each instance should be configured in the same way. What is the easiest way to perform this task and ensure consistency across Web servers?
Querying Data by Using WCF Data Services
12-53
Lesson 3
Consuming a WCF Data Service
You can connect to a WCF Data Service and perform queries by using a browser such as Internet Explorer. However, in many cases, you need to query and process data by using your own applications. You can build a client application that connects to a WCF Data Service and consumes the data that the service exposes by generating a client library. The client library acts as a proxy for the service, providing access to the data in the service through method calls. In this lesson, you will learn how to generate a client library for a WCF Data Service and use this client library to build a client application that can connect to the WCF Data Service.
Objectives After completing this lesson, you will be able to: •
Generate a client library that an application can use to connect to a WCF Data Service.
•
Query data from a WCF Data Service in a client application.
12-54
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
•
Filter data and apply projections in a client application.
•
Describe how the WCF Data Services client library handles associations between entities.
•
Catch and handle exceptions that a WCF Data Service raises in a client application.
•
Invoke an operation in a WCF Data Service.
•
Materialize data from a WCF Data Service into a custom type.
Querying Data by Using WCF Data Services
12-55
Generating the Client Library for a WCF Data Service
Key Points The client library for a WCF Data Service consists of a class that is derived from the DataServiceContext type that exposes one or more DataServiceQuery objects as properties. The name of this class is usually the same as the name of the ObjectContext object that is used by the entity model on which the WCF Data Service is based. For example, the ProductDataService WCF Data Service that was shown in the previous lesson uses an ObjectContext object called AdventureWorksEntities to connect to the underlying entity model, so the name of the DataServiceContext type that is generated for the client library is also AdventureWorksEntities. The DataServiceContext class performs a similar role to the ObjectContext class in the Entity Framework. A client application connects to the data source (in this case, a WCF Data Service) through a DataServiceContext object and fetches the data for the entities that the data service exposes by using the DataServiceQuery properties. Each DataServiceQuery property is a generic collection object that presents data from one of the underlying entities that provides the data for the WCF Data Service. In the ProductDataService WCF Data Service, the entity model provides access to the Product, ProductDocument, ProductListPriceHistory,
12-56
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
ProductModel, and ProductReview tables in the AdventureWorks database. The AdventureWorksEntities class (derived from the DataServiceContext class) has DataServiceQuery properties called Products, ProductDocuments, ProductListPriceHistories, ProductModels, and ProductReviews. The client library also provides definitions of the types that each DataServiceQuery collection contains (Product, ProductDocument, ProductListPriceHistory, ProductModel, and ProductReview). A client application can perform Language-Integrated Query (LINQ) queries against the DataServiceQuery collection properties, and the client library constructs the appropriate HTTP request to fetch the corresponding data. The WCF Data Service fetches the matching data and populates the DataServiceQuery collection. The client application can then iterate through this collection and retrieve the data for each item. You can generate the client library for a WCF Data Service by using the Add Service Reference dialog box in Visual Studio, or by using the WCF Data Service client utility from the command line.
Adding a Service Reference You can use the Add Service Reference dialog box in a client application. This dialog box enables you to specify the URL of the WCF Data Service to connect to. The dialog box sends a metadata query to the specified URL, and uses the response to generate the appropriate DataServiceContext class that contains the DataServiceQuery properties, and also the classes for each of the entities that the WCF Data Service has exposed. The returned metadata is stored in the client project as an .edmx file. This is not the same as an .edmx file that is generated by using the ADO.NET Entity Data Model Designer (it has a different format), but you can view this metadata file by using the XML editor or any text editor. Note: You can issue a metadata query from a browser by using the $metadata operator. For example, to query the metadata for the ProductDataService service, use the following query.
http://MyHost/AdventureWorksSite/ProductDataService.svc/$metadata
X Add a data service reference 1.
If the data service is not part of the solution and is not already running, start the data service and note the URI of the data service.
2.
Right-click the client project, and then select Add Service Reference.
Querying Data by Using WCF Data Services
3.
12-57
If the data service is part of the current solution, click Discover. Alternatively, if the data service is not part of the current solution, in the Address text box, type the base URL of the data service, and then click Go.
4.
Click OK.
Using the WCF Data Service Client Utility You can generate the client library for a WCF Data Service from the Visual Studio command prompt by using the DataSvcUtil command. The DataSvcUtil command provides several options, but as a minimum, you must specify the name of the code file to generate and the URL of the WCF Data Service. By default, the DataSvcUtil command generates C# code, but you can specify the /language option if you prefer to generate Microsoft Visual Basic® code. The following example generates a Visual Basic version of the client library for the ProductDataService WCF Data Service. The code is generated in a file called ProductClient.vb. You can then add the ProductClient.vb source file to your client application. Datasvcutil /out:ProductClient.vb /language:VB /uri:http:// MyHost/AdventureWorksSite/ProductDataService.svc
If you do not have direct access to the WCF Data Service, you can still generate the client library if you have an .edmx file or .csdl file that defines the conceptual model on which the WCF Data Service is based. The WCF Data Service developer can supply this file separately. To use an .edmx or .csdl file as the source for the client library, specify the /in option to the DataSvcUtil command, and provide the name of the file, as shown in the following code example. Datasvcutil /out:ProductClient.cs /in:ProductData.edmx
Question: What happens if the definition of a WCF Data Service changes after you have generated a client library for that service?
Additional Reading For more information about using the DataSvcUtil command, see the WCF Data Service Client Utility (DataSvcUtil.exe) page at http://go.microsoft.com/fwlink/?LinkId=196759.
12-58
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Querying Data in a Client Application
Key Points To retrieve data from a WCF Data Service by using the client library, a client application can perform the following tasks: 1.
Create an instance of the type derived from the DataServiceContext class in the client library, and connect to the WCF Data Service. The constructor for this class expects a Uri object that contains the address of the service.
2.
Retrieve data by querying the appropriate DataServiceQuery collection in the DataServiceContext object. When you query a DataServiceQuery collection, the client library constructs an HTTP request that specifies the resource and any criteria that is required. The query is transmitted to the WCF Data Service, and the data is returned and used to populate the DataServiceQuery object.
3.
Iterate through the items in the DataServiceQuery collection and process the objects that are returned.
The following code example connects to the ProductDataService WCF Data Service by using the AdventureWorksEntities type in the client library (this is the
Querying Data by Using WCF Data Services
12-59
class derived from DataServiceContext). The parameter to the constructor is the address of the service. The code then queries the Products DataServiceQuery property to fetch all products, and displays the name of each one. [Visual Basic] Dim context As New AdventureWorksEntities(New Uri("http://MyHost/AdventureWorksSite/ProductDataService.svc")) For Each product As Product In context.Products Console.WriteLine(product.Name) Next
[Visual C#] AdventureWorksEntities context = new AdventureWorksEntities(new Uri ("http://MyHost/AdventureWorksSite/ProductDataService.svc")); foreach (Product product in context.Products) { Console.WriteLine(product.Name); }
Paging Data When a client application performs a query, the number of items that are returned is governed by the page size of the WCF Data Service; if the page size that is specified for an entity set is 10, only the first 10 items will be retrieved by using the corresponding DataServiceQuery object. If you want to fetch additional pages, you can perform the query by using the Execute method of the DataServiceQuery object. The Execute method returns the data as an IEnumerable collection that you can cast as a QueryOperationResponse object. The QueryOperationResponse object contains the results of the query as before, but also provides a method called GetContinuation. The GetContinuation method returns a DataServiceQueryContinuation object that represents the URI for the next page of data, or null if there is no more data. You can pass the DataServiceQueryContinuation object to a subsequent call to the Execute method of the DataServiceQuery object to fetch the next page of data, return the results as another QueryOperationResponse object, and process the data in this page. You can repeat this process until the GetContinuation method returns a null value. The following code example shows how to read all of the Product data from the ProductDataService service by following this approach.
12-60
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
[Visual Basic] Dim context As New AdventureWorksEntities(New Uri("http://MyHost/AdventureWorksSite/ProductDataService.svc")) Dim continuationUri As DataServiceQueryContinuation(Of Product) = Nothing ' Fetch the first page of results Dim response As QueryOperationResponse(Of Product) = DirectCast(context.Products.Execute(), QueryOperationResponse(Of Product)) Do ' The first time around continuationUri will be null, ' and the first page has already been fetched ' before the loop started ' On subsequent iteration, fetch the next page ' by using the DataServiceQueryContinuation object If continuationUri IsNot Nothing Then response = context.Execute(continuationUri) End If ' Iterate through the results in the page ' and display the name of each product For Each product As Product In response Console.WriteLine(product.Name) Next ' Iterate until there are no more pages Loop While (continuationUri = response.GetContinuation()) IsNot Nothing
[Visual C#] AdventureWorksEntities context = new AdventureWorksEntities(new Uri ("http://MyHost/AdventureWorksSite/ProductDataService.svc")); DataServiceQueryContinuation continuationUri = null; // Fetch the first page of results QueryOperationResponse response = context.Products.Execute() as QueryOperationResponse;
Querying Data by Using WCF Data Services
12-61
do { // The first time around continuationUri will be null, // and the first page has already been fetched // before the loop started // On subsequent iteration, fetch the next page // by using the DataServiceQueryContinuation object if (continuationUri != null) { response = context.Execute(continuationUri); } // Iterate through the results in the page // and display the name of each product foreach (Product product in response) { Console.WriteLine(product.Name); } } // Iterate until there are no more pages while ((continuationUri = response.GetContinuation()) != null);
Note: When you fetch a page of data, you retrieve a snapshot of the data as it was at the time when the page was fetched. Another user may update the data between your application fetching a page and your application displaying the data for that page, and you will not see this update. However, if a user modifies data on a page before your application fetches that page, you will see the new data.
Counting Objects You can obtain a count of the number of objects that match a query by using the IncludeTotalCount method of the DataServiceQuery object. This method causes the client library to include the $count query option in the HTTP request that it sends to the WCF Data Service. You can obtain the value that is returned by reading the TotalCount property of the QueryOperationResponse object that is returned when you run the query. The following code example shows how to include a count of the number of products in a query; the changes from the previous example are highlighted in bold. Note that the count is the total number of matching objects, and not the number of objects that are retrieved so far if you are performing a paged query.
12-62
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
[Visual Basic] Dim context As New AdventureWorksEntities(New Uri("http://MyHost/AdventureWorksSite/ProductDataService.svc")) Dim continuationUri As DataServiceQueryContinuation(Of Product) = Nothing Dim response As QueryOperationResponse(Of Product) = DirectCast(context.Products.IncludeTotalCount().Execute(), QueryOperationResponse(Of Product))
Do If continuationUri IsNot Nothing Then response = context.Execute(continuationUri) End If For Each product As Product In response Console.WriteLine(product.Name) Next Loop While (continuationUri = response.GetContinuation()) IsNot Nothing Console.WriteLine("Number of products: {0}", response.TotalCount)
[Visual C#] AdventureWorksEntities context = new AdventureWorksEntities(new Uri ("http://MyHost/AdventureWorksSite/ProductDataService.svc")); DataServiceQueryContinuation continuationUri = null; QueryOperationResponse response = context.Products.IncludeTotalCount().Execute() as QueryOperationResponse; do { if (continuationUri != null) { response = context.Execute(continuationUri); }
Querying Data by Using WCF Data Services
12-63
foreach (Product product in response) { Console.WriteLine(product.Name); } } while ((continuationUri = response.GetContinuation()) != null); Console.WriteLine("Number of products: {0}", response.TotalCount);
Question: You have built a client application that fetches and displays a list of orders by querying a WCF Data Service. This application previously retrieved every order from the service, but recently you have noticed that it only displays the first 20. You have not made any changes to the application, and you know that there are at least 100 orders in the database. What might have changed that is causing this phenomenon?
Additional Reading For more information about handling paged content in a WCF Data Services client application, see the Paged Content section of the Loading Deferred Content (WCF Data Services) page at http://go.microsoft.com/fwlink/?LinkID=194098. For information about retrieving binary and streamed data in a WCF Data Services client application, see the Working with Binary Data (WCF Data Services) page at http://go.microsoft.com/fwlink/?LinkID=194099.
12-64
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Filtering and Projecting Data in a Client Application
Key Points You can filter data in a client application by using a LINQ query against the appropriate DataServiceQuery collection and specifying the relevant criteria. The client library converts the LINQ query into a URL that incorporates the equivalent $filter query option. You can also include a LINQ orderby clause, and the client library generates the equivalent $orderby query option. Note: The form of LINQ that the client library implements is known as LINQ to WCF Data Services. This version of LINQ automatically converts LINQ queries that are performed against a WCF Data Service into the equivalent HTTP queries.
The following code example shows a LINQ to WCF Data Services query that fetches all red products from the ProductDataService WCF Data Service. The data is retrieved in name order.
Querying Data by Using WCF Data Services
12-65
[Visual Basic] Dim context As New AdventureWorksEntities(New Uri("http://MyHost/AdventureWorksSite/ProductDataService.svc")) For Each product As Product In From p In context.Products _ Where p.Color = "Red" _ Order By p.Name _ Select p Console.WriteLine("Name: {0}, Color:{1}", product.Name, product.Color) Next
[Visual C#] AdventureWorksEntities context = new AdventureWorksEntities(new Uri ("http://MyHost/AdventureWorksSite/ProductDataService.svc")); foreach (Product product in from p in context.Products where p.Color == "Red" orderby p.Name select p) { Console.WriteLine("Name: {0}, Color:{1}", product.Name, product.Color); }
If the WCF Data Service uses paging, performing a LINQ to WCF Data Services query in this manner will only retrieve the first page of data. You should use a DataServiceQueryContinuation object to fetch each page of data in turn. The DataServiceQuery class provides the AddQueryOption method that you can use in this situation. By using the AddQueryOption method, you can specify a query option and a value for that option. The option is then appended to the HTTP query that the client library sends to the WCF Data Service. The name of the query option and the syntax for specifying a value for that option are the same as that used when you specify an HTTP query directly. If you need to specify more than one query option, you can invoke the AddQueryOption method multiple times. The next code example shows how to retrieve all red products in name order from a WCF Data Service that implements paging. Notice that you invoke the AddQueryOption method before calling Execute to generate the result set.
12-66
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
[Visual Basic] Dim context As New AdventureWorksEntities(New Uri("http://MyHost/AdventureWorksSite/ProductDataService.svc")) Dim continuationUri As DataServiceQueryContinuation(Of Product) = Nothing Dim response As QueryOperationResponse(Of Product) = DirectCast(context.Products _ .AddQueryOption("$filter", "Color eq 'Red'") _ .AddQueryOption("$orderby", "Name") _ .Execute(), QueryOperationResponse(Of Product)) Do If continuationUri IsNot Nothing Then response = context.Execute(continuationUri) End If For Each product As Product In response Console.WriteLine(product.Name) Next Loop While (continuationUri = response.GetContinuation()) IsNot Nothing
[Visual C#] AdventureWorksEntities context = new AdventureWorksEntities(new Uri ("http://MyHost/AdventureWorksSite/ProductDataService.svc")); DataServiceQueryContinuation continuationUri = null; QueryOperationResponse response = context.Products .AddQueryOption("$filter", "Color eq 'Red'") .AddQueryOption("$orderby", "Name") .Execute() as QueryOperationResponse; Do { if (continuationUri != null) { response = context.Execute(continuationUri); }
Querying Data by Using WCF Data Services
12-67
foreach (Product product in response) { Console.WriteLine(product.Name); } } while ((continuationUri = response.GetContinuation()) != null);
Projecting Data You can limit the fields that a query returns by specifying a select clause in a LINQ to WCF Data Services query and projecting the data onto an appropriate type. Queries that define a select clause in this way are translated by the client library into HTTP requests that use the $select query option in the request URI. The following code example shows a query that fetches only the ProductID, Name, and Color fields for products. [Visual Basic] Dim context As New AdventureWorksEntities(New Uri("http://MyHost/AdventureWorksSite/ProductDataService.svc")) For Each product In (From p In context.Products _ Where p.Color = "Red" _ Order By (p.Name) _ Select New With { .ID = p.ProductID, .Name = p.Name, .Color = p.Color })
Console.WriteLine("ID: {0}, Name: {1}, Color:{2}", product.ID, product.Name, product.Color) Next
[Visual C#] AdventureWorksEntities context = new AdventureWorksEntities(new Uri ("http://MyHost/AdventureWorksSite/ProductDataService.svc")); foreach (var product in from p in context.Products where p.Color == "Red" orderby p.Name select new { ID = p.ProductID, Name = p.Name,
12-68
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Color = p.Color }) { Console.WriteLine("ID: {0}, Name: {1}, Color:{2}", product.ID, product.Name, product.Color); }
Question: You have built a client application that successfully fetches and displays a list of orders by querying a WCF Data Service. You decide that you do not need every field of the orders objects that the query returns, so you apply a projection that fetches only the orderID and orderDate fields (these are valid fields). However, the client application now generates the following exception message. The $select query option cannot be used to define a projection when the MaxProtocolVersion of the data service is set to DataServiceProtocol.V1
When you talk with the developer of the WCF Data Service, she assures you that the MaxProtocolVersion property of the DataServiceBehavior object referenced by the DataServiceConfiguration object that is used to configure the service is set to DataServiceProtocolVersion.V2. What might be causing this problem?
Additional Reading For more information about using projections in a WCF Data Services client application, see How to: Project Query Results (WCF Data Services) page at http://go.microsoft.com/fwlink/?LinkID=194100.
Querying Data by Using WCF Data Services
12-69
Handling Associations Between Entities
Key Points By default, when you submit a query, only the data for the entity that is directly referenced by that query is returned. For example, if you fetch data by using the Products DataServiceQuery collection in the ProductDataService WCF Data Service, only products data is retrieved, even though products have relationships to other entities in the entity model that the WCF Data Service uses.
Implementing Eager Loading If an entity has related data in other entities, you can fetch the data from those entities by using the Expand method. This method causes LINQ to WCF Data Services to include the $expand option to automatically fetch the related data from these entities. You provide the list of related entities as a comma-delimited string. The following code example retrieves all products that have a productID greater than 100. The call to the Expand method specifies the ProductReviews table, so the LINQ to WCF Data Services query automatically fetches the product reviews for each matching product. These reviews are available through the ProductReviews collection property of the Product type. The example displays the
12-70
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
product ID and name of each product, and then prints the review date and the comments for each review for the product. [Visual Basic] Dim context As New AdventureWorksEntities(New Uri("http://MyHost/AdventureWorksSite/ProductDataService.svc")) For Each product As Product In From p In context.Products.Expand("ProductReviews") _ Where p.ProductID > 100 _ Select p Console.WriteLine("ID: {0}, Name: {1}", product.ProductID, product.Name) For Each review As ProductReview In product.ProductReviews Console.WriteLine("Date: {0}, Comments: {1}", review.ReviewDate, review.Comments) Next Next
[Visual C#] AdventureWorksEntities context = new AdventureWorksEntities(new Uri ("http://MyHost/AdventureWorksSite/ProductDataService.svc")); foreach (Product product in from p in context.Products.Expand("ProductReviews") where p.ProductID > 100 select p) { Console.WriteLine("ID: {0}, Name: {1}", product.ProductID, product.Name); foreach (ProductReview review in product.ProductReviews) { Console.WriteLine("Date: {0}, Comments: {1}", review.ReviewDate, review.Comments); } }
Note: LINQ to WCF Data Services does not support join operations; the following code example throws a NotSupportedException exception with the message "The method 'Join' is not supported."
Querying Data by Using WCF Data Services
12-71
[Visual Basic] For Each product In From p In context.Products _ Join r In context.ProductReviews On p.ProductID = r.ProductID _ Where p.ProductID > 100 _ Select New () ... Next
[Visual C#] foreach (var product in from p in context.Products join r in context.ProductReviews on p.ProductID equals r.ProductID where p.ProductID > 100 select new { ID = p.ProductID, Name = p.Name, r.ReviewDate, r.Comments }) { ... }
Implementing Explicit Loading Using the eager loading strategy that the Expand method implements causes the data for the specified related entities to be retrieved as part of the same request that fetches the primary data for the query. This approach is useful if you know that you will always need this related data, but it can be wasteful of bandwidth for the cases where you do not actually use these entities. As an alternative, you can use explicit loading. This strategy sends an additional query to the WCF Data Service that is requesting the related data for a specific object, but it has the advantage that it does not waste bandwidth by automatically fetching data that is not used. You can implement explicit loading by using the LoadProperty method of the DataServiceContext object. You call the LoadProperty method each time you require data that is related to a particular entity; you specify the entity and the name of the DataServiceQuery collection property that holds the related data. The following code example shows how to use the LoadProperty method to fetch the product reviews for a set of products.
12-72
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
[Visual Basic] Dim context As New AdventureWorksEntities(New Uri("http://MyHost/AdventureWorksSite/ProductDataService.svc")) For Each product As Product In From p In context.Products _ Where p.ProductID > 100 _ Select p Console.WriteLine("ID: {0}, Name: {1}", product.ProductID, product.Name) context.LoadProperty(product, "ProductReviews") For Each review As ProductReview In product.ProductReviews Console.WriteLine("Date: {0}, Comments: {1}", review.ReviewDate, review.Comments) Next Next
[Visual C#] AdventureWorksEntities context = new AdventureWorksEntities(new Uri ("http://MyHost/AdventureWorksSite/ProductDataService.svc")); foreach (Product product in from p in context.Products where p.ProductID > 100 select p) { Console.WriteLine("ID: {0}, Name: {1}", product.ProductID, product.Name); context.LoadProperty(product, "ProductReviews"); foreach (ProductReview review in product.ProductReviews) { Console.WriteLine("Date: {0}, Comments: {1}", review.ReviewDate, review.Comments); } }
Question: You are building a client application for a WCF Data Service that needs to fetch all orders and the invoices related to these orders. You have attempted to perform the LINQ to WCF Data Services query in the following code example.
Querying Data by Using WCF Data Services
[Visual Basic] For Each order In From o In context.Orders _ Join i In context.Invoices On o.OrderID = i.OrderID _ Select New () ... Next
[Visual C#] foreach (var order in from o in context.Orders join i in context.Invoices on o.OrderID equals i.OrderID select new { o.OrderID, i.InvoiceDate }) { ... }
This code compiles, but fails when it runs. Why?
12-73
12-74
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Handling WCF Data Service Exceptions in a Client Application
Key Points When a client application sends a request to a WCF Data Service, that request may fail for many reasons; possibly, the client application has attempted to access data that it does not have rights over, or it has tried to perform a query that requires functionality that the service has restricted. If the failure is due to the way in which the client application interacts with the service (as opposed to a failure caused by some other problem, such as a network failure when attempting to connect to the service), the service responds by throwing a DataServiceException exception, as described in the previous lesson in this module. The DataServiceException type is a serializable exception that is specifically designed to communicate the causes of a failure in a WCF Data Service. When the client library receives a DataServiceException exception, it actually deserializes it as a DataServiceClientException object, which it passes to your application. If your application was performing a query when the exception occurred, the DataServiceClientException exception is wrapped in a
Querying Data by Using WCF Data Services
12-75
DataServiceQueryException object, with the message "An error occurred while processing the request." You can access the DataServiceClientException exception that contains the reason for the exception by examining the InnerException property of the DataServiceQueryException object. Note: The amount of detail about the cause of the exception that is passed across the network and reported by the DataServiceClientException object is controlled by the UseVerboseErrors property of the DataServiceConfiguration object that is used to configure the WCF Data Service.
If a client application sends a request other than a query, the WCF Data Service can respond with a DataServiceRequestException exception. A client application should be prepared to catch the DataServiceQueryException type when it performs query operations, and the DataServiceRequestException exception when it performs other types of operations such as modifying data. A client application should also be prepared to catch the DataServiceClientException type to handle any other exceptions that the WCF Data Service throws when it performs other types of operations. Note: Modifying data in a WCF Data Service by using a client application is described in the next module.
Question: You are building a client application for a WCF Data Service that queries data. In your application, you catch and handle the DataServiceClientException exception whenever you submit a query, but your application periodically fails with an unhandled exception. You are certain that the error is being caused by the query operation, and that you are connecting successfully to the WCF Data Service. Why might errors that are thrown as a result of the query operation failing lead to unhandled exceptions in your application?
12-76
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Invoking WCF Data Service Operations
Key Points If a WCF Data Service implements data service operations, you can invoke these operations by using the Execute method of the DataServiceContext object in a client application. You specify a URI that contains the name of the operation and any parameters in the form of an HTTP query string. When you invoke a service operation, the value that the Execute method of the DataServiceContext object in the client library returns is an enumerable collection. If the service operation returns a single, scalar value, you should extract that value from the collection by using a method such as First. The ProductDataService service that was described in Lesson 1 exposes several service operations: •
ProductsByColor, which takes a string parameter called color and returns a collection of Product objects.
•
ProductByID, which takes an integer parameter called id and returns a single Product object.
Querying Data by Using WCF Data Services
•
12-77
NumProductsByColor, which takes a string parameter called color and returns an integer value.
You can invoke these operations from a client application as shown in the following code examples. [Visual Basic] Dim serviceAddress As String = "http://MyHost/AdventureWorksSite/ProductDataService.svc" Dim context As New AdventureWorksEntities(New Uri(serviceAddress)) ' Call the ProductsByColor service operation ' Provide an HTTP query string that specifies the color parameter For Each product As Product In context.Execute(Of Product)(New Uri("/ProductsByColor?color='Red'", UriKind.Relative)) Console.WriteLine(product.Name) ... Next ' Call the ProductByID service operation ' Provide an HTTP query string that specifies the id parameter ' The service operation returns a single value, so call First ' to extract this value from the enumerable collection generated ' by the client library Dim product100 As Product = context.Execute(Of Product)(New Uri("/ProductByID?id=316", UriKind.Relative)).First() ' Call the NumProductsByColor service operation ' Provide an HTTP query string that specifies the color parameter ' Call First to extract the scalar value from the enumerable ' collection generated by the client library Dim numberOfBlackProducts As Integer = context.Execute(Of Integer)(New Uri("/NumProductsByColor?color='Black'", UriKind.Relative)).First()
[Visual C#] string serviceAddress = "http://MyHost/AdventureWorksSite/ProductDataService.svc"; AdventureWorksEntities context = new AdventureWorksEntities(new Uri(serviceAddress)); // Call the ProductsByColor service operation // Provide an HTTP query string that specifies the color parameter foreach (Product product in context.Execute
12-78
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
(new Uri("/ProductsByColor?color='Red'", UriKind.Relative))) { Console.WriteLine(product.Name); ... } // Call the ProductByID service operation // Provide an HTTP query string that specifies the id parameter // The service operation returns a single value, so call First // to extract this value from the enumerable collection generated // by the client library Product product100 = context.Execute (new Uri("/ProductByID?id=316", UriKind.Relative)).First(); // Call the NumProductsByColor service operation // Provide an HTTP query string that specifies the color parameter // Call First to extract the scalar value from the enumerable // collection generated by the client library int numberOfBlackProducts = context.Execute (new Uri("/NumProductsByColor?color='Black'", UriKind.Relative)).First();
Invoking a Data Service Operation Asynchronously Some service operations may take a significant time to run. The DataServiceContext class provides an asynchronous version of the Execute method called BeginExecute that you can use to invoke long-running operations on a background thread in a client application. You can use the BeginExecute method to implement the Asynchronous Design pattern. The parameters to the BeginExecute method are the URI of the data service operation (including any parameters), a callback method to run when the operation completes, and a reference to the DataServiceContext object (passed as state information to the callback method). In the callback method, call the EndExecute method of the DataServiceContext object to obtain the results of the service operation. Note: The DataServiceContext class also provides an overloaded version of BeginExecute that enables you to specify a DataServiceQueryContinuation object as the first parameter. You can use this version to perform an operation that returns one or more pages of data.
The following code example shows how to run the ProductsByColor service operation on a separate thread by using the Asynchronous Design pattern.
Querying Data by Using WCF Data Services
12-79
[Visual Basic] Private Shared Sub OnProductsQueryComplete(ByVal result As IAsyncResult) Dim context As AdventureWorksEntities = TryCast(result.AsyncState, AdventureWorksEntities) Dim redProducts As IEnumerable(Of Product) = context.EndExecute(Of Product)(result) For Each product As Product In redProducts Console.WriteLine(product.Name) Next End Sub ... Dim serviceAddress As String = "http://MyHost/AdventureWorksSite/ProductDataService.svc" Dim context As New AdventureWorksEntities(New Uri(serviceAddress)) context.BeginExecute(Of Product)(New Uri("/ProductsByColor?color='Red'", UriKind.Relative), OnProductsQueryComplete, context) ...
[Visual C#] private static void OnProductsQueryComplete(IAsyncResult result) { AdventureWorksEntities context = result.AsyncState as AdventureWorksEntities; IEnumerable redProducts = context.EndExecute(result); foreach (Product product in redProducts) { Console.WriteLine(product.Name); } } ... string serviceAddress = "http://MyHost/AdventureWorksSite/ProductDataService.svc";
12-80
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
AdventureWorksEntities context = new AdventureWorksEntities(new Uri(serviceAddress)); context.BeginExecute( new Uri("/ProductsByColor?color='Red'", UriKind.Relative), OnProductsQueryComplete, context); ...
Question: If you attempt to invoke an operation asynchronously, but provide an invalid parameter, where will the exception occur in your application?
Additional Reading For more information about invoking a WCF Data Service operation asynchronously, see the Asynchronous Operations (WCF Data Services) page at http://go.microsoft.com/fwlink/?LinkID=194101.
Querying Data by Using WCF Data Services
12-81
Materializing Data from a WCF Data Service into a Custom Type
Key Points It may not always be feasible or desirable to generate the client library for a WCF Data Service automatically, by using Visual Studio tools. For example, if you already have a set of data classes to use in your client, you will not want to generate a new set. In these circumstances, you can retrieve and materialize data from a WCF Data Service into your own types that are defined by the client application. For example, you can retrieve products from the ProductsDataService service into the custom type in the following code example that is defined by a client application. [Visual Basic] Public Class LocalProduct Public Property ProductID As Integer Public Property Name As String Public Property ListPrice As Decimal End Class
12-82
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
[Visual C#] public class LocalProduct { public int ProductID { get; set; } public string Name { get; set; } public decimal ListPrice { get; set; } }
This type contains a subset of the fields that are defined by the Product entity that the data service uses. You can perform queries that fetch data into collections of this type by using a raw DataServiceContext object. The DataServiceContext type is located in the System.Data.Services.Client namespace. WCF Data Services implements a soft binding between the types that a data service implements and the types that a client application defines. By default, the client-side runtime checks that every field that is retrieved from the data service has a corresponding property in the data type that the client application uses, and throws an InvalidOperationException exception if one or more fields are missing. You can suppress this check by setting the IgnoreMissingProperties property of the DataServiceContext object to true. Note: If you specify a field in the local client type that has no corresponding member in the type that the data service defines, the member in the client type will be populated with the default value for the type of the data member.
The following code example shows how to invoke the ProductsByColor data service operation in the ProductDataService service and retrieve the matching products into LocalProduct objects. [Visual Basic] Imports System.Data.Service.Client ... Dim serviceAddress As String = "http://MyHost/AdventureWorksSite/ProductDataService.svc" Dim context As New DataServiceContext(New Uri(serviceAddress)) context.IgnoreMissingProperties = True Dim query As IEnumerable(Of LocalProduct) = context.Execute(Of LocalProduct)(New Uri("/ProductsByColor?color='Red'", UriKind.Relative))
Querying Data by Using WCF Data Services
12-83
For Each product As LocalProduct In query Console.WriteLine("{0}" & vbTab & "{1}" & vbTab & "{2}", product.ProductID, product.Name, product.ListPrice) Next
[Visual C#] using System.Data.Service.Client; ... string serviceAddress = "http://MyHost/AdventureWorksSite/ProductDataService.svc"; DataServiceContext context = new DataServiceContext( new Uri(serviceAddress)); context.IgnoreMissingProperties = true; IEnumerable query = context.Execute( new Uri("/ProductsByColor?color='Red'", UriKind.Relative)); foreach (LocalProduct product in query) { Console.WriteLine("{0}\t{1}\t{2}", product.ProductID, product.Name, product.ListPrice); }
Question: You are building a client application for a WCF Data Service that queries data. You have retrieved data into a custom type that the client application defines, but when you display the retrieved data, the value in one of the fields (which holds integer data) is always set to zero. What might be causing this?
12-84
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Lesson 4
Protecting Data and Operations in a WCF Data Service
The data and operations that a WCF Data Service exposes may be sensitive items, and in many cases, you should restrict access to these entities and operations to authenticated client applications. This lesson describes how you can use the features that WCF Data Services provides to help protect data, and summarizes some best practices for configuring and protecting a WCF Data Service endpoint to help prevent the possibility of unauthorized access and reduce the attack surface of a data service.
Objectives After completing this lesson, you will be able to: •
Protect a WCF Data Service.
•
Set entity and operation rights.
Querying Data by Using WCF Data Services
•
Use the query interceptor to restrict access to data.
•
Restrict access to operations.
12-85
12-86
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Protecting a WCF Data Service
Key Points By definition, Web applications allow users access to resources, such as those that a WCF Data Service exposes, through a Web server or other host environment. You should configure the host environment for the data service to: •
Guard resources against unauthorized access.
•
Restrict access levels by user or role.
•
Establish data integrity and confidentiality, providing a relatively secure environment for applications to consume your data service.
•
Establish control over how your application can gain access to restricted resources.
The security model that WCF Data Services implements depends on protection that the host environment provides. If you are hosting the data service in a Web application or Web service running under IIS, you should configure the Web application or Web service to enable authentication, and implement an appropriate level of access control.
Querying Data by Using WCF Data Services
12-87
You specify access to resources that a WCF Data Service exposes on a resource-byresource basis. The level of access that you grant automatically applies to all users who can access the site. If you need to provide different levels of access to different users, you can use a query interceptor, as described later in this lesson. A query interceptor can take advantage of the security credentials that are passed in the HTTP context to the data service, and you can programmatically decide whether a specific user or role should be allowed to access a resource. To protect requests that are submitted to a WCF data service, and the corresponding data that is returned by these requests, you can configure the data service and the Web service to use the HTTPS protocol and transport-level security. You can configure the WebHttpBinding binding that a data service uses to support HTTPS by specifying an appropriate WebHttpSecurityMode value. The following table summarizes the possible values that you can specify for the security mode. Value
Description
None
HTTP requests have no security applied.
Transport
Transport-level security is applied to HTTP requests.
TransportCredentialOnly
HTTP-based client authentication is provided, but requests are not protected.
The following code example shows how to configure the endpoint for the ProductDataService service to use transport-level security, and specify that users are authenticated by using certificates. ...
12-88
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
... ...
Question: How do you configure a WCF Data Service to implement message-level security?
Additional Reading For more information about configuring authorization and authentication for a Web application, see the Web Application Security at Run Time page at http://go.microsoft.com/fwlink/?LinkID=194102. For more information about configuring the WebHttpBinding binding, see the page at http://go.microsoft.com/fwlink/?LinkID=194103.
Querying Data by Using WCF Data Services
12-89
Granting Access to Entities
Key Points You have seen that, for security reasons, WCF Data Services do not automatically expose any resources, such as entity collections that the entity model implements. When a WCF Data Service starts running, the InitializeService method is passed a DataServiceConfiguration object that you can use to specify the security policy for the service. The SetEntitySetAccessRule method provides the means to grant and deny access to users of the WCF Data Service. The SetEntitySetAccessRule method takes two parameters: •
The name of the entity. This string can also contain the "*" wildcard character to indicate all entities. Note that you can either specify the name of an entity or the string "*"; you cannot combine "*" with other characters to form entity set name patterns. If you need to provide access to multiple entity sets, you must call the SetEntitySetAccessRule method for each entity set.
•
The access rights to grant to the entity. This is a value from the System.Data.Services.EntitySetRights enumeration. This enumeration defines various read and write access rights. You can combine entity set rights by using the bitwise or operator.
12-90
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
The following table summarizes the values in the EntitySetRights enumeration. Value
Description
None
Denial of all rights to access data.
ReadSingle
Authorization to read single data items.
ReadMultiple
Authorization to read sets of data.
WriteAppend
Authorization to create new data items in datasets.
WriteReplace
Authorization to replace data.
WriteDelete
Authorization to delete data items from datasets.
WriteMerge
Authorization to merge data.
AllRead
Authorization to read data.
AllWrite
Authorization to write data.
All
Authorization to create, read, update, and delete data.
The following code example grants access to the ProductDocuments entity in the ProductDataService service. The statement enables a client application to read single instances of the ProductDocuments entity from the data service. [Visual Basic] Public Class ProductDataService Inherits DataService(Of AdventureWorksEntities) Public Shared Sub InitializeService(ByVal config As DataServiceConfiguration) config.SetEntitySetAccessRule("ProductDocuments", EntitySetRights.ReadSingle) End Sub End Class
Querying Data by Using WCF Data Services
12-91
[Visual C#] public class ProductDataService: DataService { public static void InitializeService(DataServiceConfiguration config) { config.SetEntitySetAccessRule("ProductDocuments", EntitySetRights.ReadSingle); } }
If a client application performs a query that retrieves more than a single ProductDocuments entity, the query will fail with an HTTP "Forbidden" error. Question: You want to grant access to all entity sets that have a name that begins with the letter “P” in the ProductDataService service (such as Products, ProductDocuments, ProductModels, and so on) by using the statement in the following code example. config.SetEntitySetAccessRule("P*", EntitySetRights.ReadSingle)
However, when a client application attempts to query the Products entity set, the result is an exception. Why?
12-92
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Restricting Data Access by Using Query Interceptors
Key Points An interceptor enables you to inject custom logic into the request-processing pipeline that a data service implements. WCF Data Services supports two types of interceptors: •
A query interceptor, which runs when an HTTP GET request is received for a specified entity set.
•
A change interceptor, which runs when an HTTP request to update, insert, or delete data for a specified entity set is received.
You implement an interceptor by defining a method with a specified signature, and tagged with the QueryInterceptor or ChangeInterceptor attribute. This attribute also specifies the entity set to associate with the interceptor; when a client application sends a request to query data in this entity set, the appropriate query interceptor runs automatically. A query interceptor method takes no parameters and returns a lambda expression that determines which items in the entity set can be returned by the query results.
Querying Data by Using WCF Data Services
12-93
Note: This module concentrates on performing query operations. Change interceptors are described in a later module.
The lambda expression that is returned by a query interceptor should take a parameter that corresponds to an entity from the underlying entity set, and generate a Boolean value that indicates whether this item can be retrieved from this set. When the HTTP GET operation is performed, this filter is applied to the entity set, and only the entities that match the condition that this lambda expression specifies are fetched. The following code example shows a query interceptor for the Products entity set in the ProductDataService service. The query interceptor limits the entities that are returned to all products that have a list price that is at least twice the standard cost of the product. [Visual Basic] _ Public Function OnQueryProducts() As Expression(Of Func(Of Product, Boolean)) Return Function(p) p.ListPrice > 2 * p.StandardCost End Function
[Visual C#] [QueryInterceptor("Products")] public Expression OnQueryProducts() { return p => p.ListPrice > 2 * p.StandardCost; }
You can use a query interceptor to restrict the data that is available to different users and roles. The following code example shows how to enable all users who have the ProductsAdministrator role to retrieve all products, but deny access to all other users. Note: The static System.Web.HttpContext.Current property provides access to the HTTP context for the current request. The User property contains the user security information for the request, including the identity and roles that the user has. This feature depends on the Web site that is hosting the data service being configured to authenticate users correctly.
12-94
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
[Visual Basic] _ Public Function OnQueryProducts() As Expression(Of Func(Of Product, Boolean)) If HttpContext.Current.User.IsInRole("ProductsAdministrator") Then Return Function(p) True Else Return Function(p) False End If End Function
[Visual C#] [QueryInterceptor("Products")] public Expression OnQueryProducts() { if (HttpContext.Current.User.IsInRole("ProductsAdministrator")) { return p => true; } else { return p => false; } }
Question: You have added a query interceptor to a data service that limits access to entities in a specific entity set to users who have the EntityAdministrator role. However, when you test the data service by issuing queries from a Web browser, you find that all attempts to query the entity set return no items. You know that there are matching entities, and you are running the Web browser by using an account that is a member of the EntityAdministrator role. What might be causing the problem?
Additional Reading For more information about using query interceptors in a WCF Data Service, see the Interceptors (WCF Data Services) page at http://go.microsoft.com/fwlink/?LinkID=194104.
Querying Data by Using WCF Data Services
12-95
Granting Access to Operations
Key Points As is the case with entities collections, by default, a WCF Data Service does not provide access to any operations that it implements. You can grant access to operations in the InitializeService method when the data service starts running, by using the SetServiceOperationAccessRule method of the DataServiceConfiguration parameter. The SetServiceOperationAccessRule method is similar to the SetEntitySetAccessRule method that you use to grant or deny access to entity collections. The method takes two parameters: a string that specifies the name of the operation to grant access to (or "*" to specify all operations), and a value from the System.Data.Services.ServiceOperationRights enumeration. This enumeration contains values that enable you to specify the access rights to grant over the operation. The following table summarizes the values in the ServiceOperationRights enumeration.
12-96
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Value
Description
None
Denies authorization to access the service operation.
ReadSingle
Grants authorization to read a single data item by using the service operation.
ReadMultiple
Grants authorization to read multiple data items by using the service operation.
AllRead
Grants authorization to read single or multiple data items that the service operation deploys.
All
Grants all rights over the service operation.
OverrideEntitySetRights
Overrides entity set rights that are explicitly defined in the data service with the service operation rights. Use this value in combination with other ServiceOperationRights values.
Note: There are no explicit privileges for writing data. However, a service operation can insert, update, or delete entities if the user who is performing the operation has the appropriate access rights to the underlying data (if the WCF Data Service is hosted by a Web site that uses impersonation), or the WCF Data Service runs using an account that has sufficient access rights (if the Web site does not use impersonation).
If a service operation returns one or more entities from a specific entity set, you may also need to grant the appropriate access to that entity set by using the SetEntitySetAccessRights method. However, the OverrideEntitySetRights value enables you to override any entity set access rights. For example, if you grant EntitySetRights.ReadSingle access over an entity set, and a service operation retrieves a collection of entities from this entity set, the service operation will fail with an HTTP 403 (Forbidden) error when you attempt to run it. However, if you specify OverrideEntitySetRights when you grant access to the service operation, the access rule that is defined for the service operation will override the restriction that is applied to the entity set and the service operation will run successfully. The following code example shows how to grant access to the ProductsByColor service operation in the ProductDataService service. Note that the Products entity set has ReadSingle access, but the access rights that are granted to the ProductsByColor service operation override this limitation.
Querying Data by Using WCF Data Services
12-97
[Visual Basic] Public Shared Sub InitializeService(ByVal config As DataServiceConfiguration) ... config.SetEntitySetAccessRule("Products", EntitySetRights.ReadSingle) ... config.SetServiceOperationAccessRule("ProductsByColor", ServiceOperationRights.All Or ServiceOperationRights.OverrideEntitySetRights) ... End Sub
[Visual C#] public static void InitializeService(DataServiceConfiguration config) { ... config.SetEntitySetAccessRule( "Products", EntitySetRights.ReadSingle); ... config.SetServiceOperationAccessRule( "ProductsByColor", ServiceOperationRights.All | ServiceOperationRights.OverrideEntitySetRights); ... }
Note: You must grant some degree of access to the entity set that a service operation returns. If you do not call the SetEntitySetAccessRule method, or you specify a value of EntitySetRights.None, the ServiceOperationRights.OverrideEntitySetRights value has no effect and access to the underlying entity set is denied.
Paging and Interceptors If you have defined a page size for an entity set by using the SetEntitySetPageSize method, this page size still applies to entity sets that a service operation returns, regardless of whether you have specified the ServiceOperationRights.OverrideEntitySetRights value when you granted access to the service operation. Query interceptors are not invoked when you run a service operation, so you cannot use them to limit access to entities that a service operation returns.
12-98
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Question: You have created a service operation called ProductsByName that returns all products that have a name that matches a value that is specified as a parameter. You only want users to be able to access products by using this service operation, so you have decided not to grant access to the underlying Products entity set. Instead, you have simply specified the ServiceOperationRights.OverrideEntitySetRights value when you called the SetServiceOperationAccessRule method to grant access to the ProductsByName service operation, as shown in the following code example. What is wrong with this approach? [Visual Basic] Public Shared Sub InitializeService(ByVal config As DataServiceConfiguration) config.SetServiceOperationAccessRule("ProductsByName", ServiceOperationRights.OverrideEntitySetRights) End Sub
[Visual C#] public static void InitializeService(DataServiceConfiguration config) { config.SetServiceOperationAccessRule( "ProductsByName", ServiceOperationRights.OverrideEntitySetRights); }
Querying Data by Using WCF Data Services
12-99
Lab: Creating and Using WCF Data Services
Objectives After completing this lab, you will be able to: •
Expose data by using WCF Data Services.
•
Consume a WCF Data Service in a Web application.
•
Restrict access to the data that a WCF Data Service exposes.
•
Implement a business operation in a WCF Data Service.
Introduction In this exercise, you will build a WCF Data Service based on an entity model that provides access to data that is stored in the AdventureWorks database. You will test this data service by performing queries by using Internet Explorer, and then you will consume the data service in a client application. You will protect the data that the data service exposes and restrict access to the data for certain types of
12-100
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
users. Finally, you will add a service operation to the data service and invoke this operation from the client application.
Lab Setup For this lab, you will use the available virtual machine environment. Before you begin the lab, you must: •
Start the 10265A-GEN-DEV-12 virtual machine, and then log on by using the following credentials: •
User name: Student
•
Password: Pa$$w0rd
Querying Data by Using WCF Data Services
12-101
Lab Scenario
Adventure Works Cycles subcontracts the shipping of its products to various delivery companies. These companies need access to the Adventure Works order data so that they can collect and deliver the goods. You need to expose this information by using WCF Data Services and building a Web application that the delivery companies can use to access the data.
Exercise 1: Exposing Order Data as a WCF Data Service Scenario In this exercise, you will build a simple WCF Data Service that exposes the SalesOrderHeader, Contact, and Address tables from the AdventureWorks database by using an entity data model. You will see how to access the service from a Web browser, experiment with operators such as $select, and see how to navigate through the data. The data service will include exception handling to trap and log any exceptions that the service throws. The main tasks for this exercise are as follows:
12-102
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
1.
Prepare the environment for the lab.
2.
Prepare the AdventureWorks database for the lab.
3.
Open the starter project.
4.
Add a data service to the ShippingDataServiceSite project.
5.
Build and test the data service.
X Task 1: Prepare the environment for the lab 1.
Log on to the 10265A-GEN-DEV-12 virtual machine as Student with the password Pa$$w0rd.
2.
In the E:\Labfiles folder, run EnvSetup.bat as an administrator.
X Task 2: Prepare the AdventureWorks database for the lab •
In the E:\Labfiles folder, run AWReset.bat.
X Task 3: Open the starter project 1.
In the E:\Labfiles\Lab12\VB\Ex1\Starter or E:\Labfiles\Lab12\CS\Ex1\Starter folder, run ExSetup.bat as an administrator.
2.
Open Visual Studio 2010 as an administrator.
3.
Open the existing solution, ShippingDataServiceSite.sln, in the E:\Labfiles\Lab12\VB\Ex1\Starter or E:\Labfiles\Lab12\CS\Ex1\Starter folder.
X Task 4: Add a data service to the ShippingDataServiceSite project 1.
Add a WCF data service named ShippingDataService to the ShippingDataServiceSite project.
2.
In the Code Editor, add statements to bring the ShippingDataServiceLibrary and System.Diagnostics namespaces into scope.
3.
Add AdventureWorksEntities as the type of the DataService class: •
Locate the comment TODO: replace [[class name]] with your data class name for Visual Basic or TODO: put your data source class name here
Querying Data by Using WCF Data Services
12-103
for Visual C#, delete the comment, and then type AdventureWorksEntities •
4.
5.
If you are using Visual C#, locate the comment TODO: put your data source class name here, delete the comment, and then type AdventureWorksEntities
In the InitializeService method, write code that performs the following tasks: a.
Set the access permissions for the SalesOrderHeaders, Addresses, and Contacts entity sets to AllRead.
b.
Set the page size for the three entity sets to 10.
Write code that logs service exceptions to the event log. Your code must perform the following tasks: a.
Declare a private constant string named eventSource (_eventSource in Visual Basic) with a value of Orders Service.
b.
Declare a private constant string named eventLog (_eventLog in Visual Basic) with a value of Application.
c.
Define a private method called logException that takes an exception and a string as parameters, checks that the eventSource (_eventSource in Visual Basic) object exists, and then writes an entry to the event log.
d. Override the HandleException method to call the logException method, and then call the HandleException method of the base class. 6.
Save the ShippingDataService.svc file.
X Task 5: Build and test the data service 1.
Build the solution and correct any errors.
2.
Browse to the service by using Internet Explorer.
3.
Explore the service by using Internet Explorer:
Note: You must right-click anywhere on the page, and then click View Source to view the data in the following steps. Internet Explorer cannot display the XML data that the service returns.
12-104
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
a.
In Internet Explorer, visit http://localhost/ShippingDataServiceSite/ShippingDataService.svc /Contacts. Review the contact data.
b.
In Internet Explorer, visit http://localhost/ShippingDataServiceSite/ShippingDataService.svc /Contacts(7). Review the contact data.
c.
In Internet Explorer, visit http://localhost/ShippingDataServiceSite/ShippingDataService.svc /Contacts?$filter=LastName gt 'D'. Review the contact data.
4.
Close Internet Explorer.
5.
Close all instances of Notepad.
6.
Close the solution.
Exercise 2: Consuming a WCF Data Service Scenario In this exercise, you will modify a simple Web client application to consume the data service and display the data that is retrieved. This application enables users to query all of the data in the entities that the WCF Data Service exposes. The main tasks for this exercise are as follows: 1.
Open the starter project.
2.
Call the data service to retrieve SalesOrderHeader entities from a client Web application.
3.
Call the data service to retrieve the details of a specific SalesOrderHeader entity.
4.
Call the data service to retrieve the details of a specific Address entity.
5.
Call the data service to retrieve the details of a specific Contact entity.
6.
Build and test the client Web site.
X Task 1: Open the starter project 1.
In the E:\Labfiles\Lab12\VB\Ex2\Starter or E:\Labfiles\Lab12\CS\Ex2\Starter folder, run ExSetup.bat as an administrator.
Querying Data by Using WCF Data Services
2.
12-105
Open the existing solution, ShippingDataServiceSite.sln, in the E:\Labfiles\Lab12\VB\Ex2\Starter or E:\Labfiles\Lab12\CS\Ex2\Starter folder.
X Task 2: Call the data service to retrieve SalesOrderHeader entities from a client Web application 1.
Review the task list.
2.
Open the HomeController file by double-clicking the comment TODO: Ex2 Retrieve all SalesOrderHeaders with the chosen shipMethodID in the task list. This task is located in the SalesOrders method.
3.
In the SalesOrders method, immediately after the comment, write code that performs the following tasks:
4.
a.
Assign a new AdventureWorksEntities object to the context variable.
b.
Set the Credentials property of the context object to the current default network credentials.
c.
Define a LINQ query named orders that retrieves all of the SalesOrderHeader entities with a ShipMethodID property equal to the value of the shipMethodID variable.
Save the HomeController file.
X Task 3: Call the data service to retrieve the details of a specific SalesOrderHeader entity 1.
Review the task list.
2.
Open the HomeController file by double-clicking the comment TODO: Ex2 Return the SalesOrderHeader with SalesOrderID = id in the task list. This task is located in the Details method.
3.
In the Details method, immediately after the comment, write code that uses the AddQueryOption method of the SalesOrderHeaders entity set to filter the entity set and return the SalesOrderHeader entity with a SalesOrderID property equal to the value of the id variable.
4.
Save the HomeController file.
12-106
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
X Task 4: Call the data service to retrieve the details of a specific Address entity 1.
Review the task list.
2.
Open the HomeController file by double-clicking the comment TODO: Ex2 Return the Address with AddressID = id in the task list. This task is located in the AddressDetails method.
3.
In the AddressDetails method, immediately after the comment, write code that uses the AddQueryOption method of the Addresses entity set to filter the entity set and return the Address entity with an AddressID property equal to the value of the id variable.
4.
Save the HomeController file.
X Task 5: Call the data service to retrieve the details of a specific Contact entity 1.
Review the task list.
2.
Open the HomeController file by double-clicking the comment TODO: Ex2 Return the Contact with ContactID = id in the task list. This task is located in the ContactDetails method.
3.
In the ContactDetails method, immediately after the comment, write code that uses the AddQueryOption method of the Contacts entity set to filter the entity set and return the Contact entity with a ContactID property equal to the value of the id variable.
4.
Save the HomeController file.
X Task 6: Build and test the client Web site 1.
Build the solution and correct any errors.
2.
Check that the ShippingDetailsSite project is set as the StartUp project.
3.
Start the application in Debug mode.
4.
Explore the service by using Internet Explorer: a.
On the Adventure Works Delivery Information page, in the Show Orders by Shipping Method box, type 1 and then click Submit.
b.
On the SalesOrders page, click any of the Details hyperlinks.
Querying Data by Using WCF Data Services
c.
12-107
On the Details page, click one of the Contact Details hyperlinks.
5.
Close Internet Explorer.
6.
Close the solution.
Exercise 3: Restricting Access to Data That a WCF Data Service Exposes Scenario In this exercise, you will add a query interceptor so that only users with a specific security role can access sensitive data. You will also update the service to implement paging to reduce the number of items fetched by a request, and decrease the possibility of wasting bandwidth by performing unconstrained queries. The main tasks for this exercise are as follows: 1.
Open the starter project.
2.
Modify the data service to filter returned data based on the user's role membership.
3.
Modify the Web client application to support data paging.
4.
Build and test the data service and the client Web site.
X Task 1: Open the starter project 1.
In the E:\Labfiles\Lab12\VB\Ex3\Starter or E:\Labfiles\Lab12\CS\Ex3\Starter folder, run ExSetup.bat as an administrator.
2.
Open the existing solution, ShippingDataServiceSite.sln, in the E:\Labfiles\Lab12\VB\Ex3\Starter or E:\Labfiles\Lab12\CS\Ex3\Starter folder.
X Task 2: Modify the data service to filter returned data based on the user's role membership 1.
Review the task list.
12-108
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
2.
Open the ShippingDataService.svc file by double-clicking the comment TODO: Ex3 - Filter the query results based on localgroup membership in the task list. This task is located in the OnQuerySalesOrderHeaders method.
3.
Notice the QueryInterceptor attribute of the OnQuerySalesOrderHeaders method.
4.
In the OnQuerySalesOrderHeaders method, immediately after the comment, write code that performs the following tasks:
5.
a.
If the current user is in the WorldwideShipping role, allow the user to view all SalesOrderHeader entities with non-null ShipDate properties.
b.
If the current user is in the USShipping role, allow the user to view all SalesOrderHeader entities with non-null ShipDate properties and with the ShipMethodID property equal to 1, 2, or 4.
c.
Otherwise, return no SalesOrderHeader entities.
Save the ShippingDataService.svc file.
X Task 3: Modify the Web client application to support data paging 1.
Review the task list.
2.
Open the HomeController file by double-clicking the comment TODO: Ex3 Declare a QueryOperationResponse object in the task list. This task is located in the SalesOrders method.
3.
In the SalesOrders method, immediately after the comment, write code that declares a variable based on the QueryOperationResponse generic type, called response. Specify SalesOrderHeader as the type parameter, and initialize it to null.
4.
Locate the next comment TODO: Ex3 - Get the first page of SalesOrderHeader records in the SalesOrders method.
5.
In the SalesOrders method, immediately after the comment, write code that assigns the result of calling the Execute method on the SalesOrderHeaders entity set to the response variable.
6.
Locate the next comment TODO: Ex3 - Get the next page of SalesOrderHeader records in the SalesOrders method.
7.
In the SalesOrders method, immediately after the comment, write code that assigns the result of calling the Execute method on the SalesOrderHeader entity set to the response variable. Use the value of the next (_next in Visual
Querying Data by Using WCF Data Services
12-109
Basic) variable to instantiate a Uri object to pass as a parameter to the Execute method. 8.
Save the HomeController file.
X Task 4: Build and test the data service and the client Web site 1.
Build the solution and correct any errors.
2.
Check that the ShippingDetailsSite project is set as the StartUp project.
3.
Start the application in Debug mode.
4.
Test the application by using the three sets of credentials in the following table. User name
Password
Role
Bill
Pa$$w0rd
USShipping
James
Pa$$w0rd
None
Mary
Pa$$w0rd
WorldwideShipping
5.
Close Internet Explorer.
6.
Close the solution.
Exercise 4: Implementing a Business Operation in a WCF Data Service Scenario In this exercise, you will add a business operation to archive orders that were shipped more than a year ago to another table called ArchivedSalesOrderHeader. You will restrict access to this operation to users who are in the WorldwideShipping security role. You will modify the Web client application to invoke this operation asynchronously. The main tasks for this exercise are as follows: 1.
Open the starter project.
2.
Add a business operation to archive records in the data service.
12-110
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
3.
Call a business operation in the data service from a client Web application.
4.
Build and test the client Web site.
X Task 1: Open the starter project 1.
In the E:\Labfiles\Lab12\VB\Ex4\Starter or E:\Labfiles\Lab12\CS\Ex4\Starter folder, run ExSetup.bat as an administrator.
2.
Open the existing solution, ShippingDataServiceSite.sln, in the E:\Labfiles\Lab12\VB\Ex4\Starter or E:\Labfiles\Lab12\CS\Ex4\Starter folder.
X Task 2: Add a business operation to archive records in the data service 1.
Review the task list.
2.
Open the ShippingDataService.svc file by double-clicking the comment TODO: Ex4 - Configure Service Operation in the task list. This task is located in the InitializeService method.
3.
In the InitializeService method, immediately after the comment, write code that calls the SetServiceOperationAccessRule method of the config object and gives full permissions to the ArchiveSalesOrders function.
4.
Open the ShippingDataService.svc file by double-clicking the comment TODO: Ex4 - Define the ArchiveSalesOrders operation in the task list. This task is located in the ArchiveSalesOrders method.
5.
Write code that performs the following tasks: a.
Check the role membership of the current user. If the user is not in the WorldwideShipping role, throw an UnauthorizedAccessException exception.
b.
Calculate the archive date. This should be 365 days ago.
c.
Define a LINQ query that selects all of the SalesOrderHeader entities that are older than 365 days and have a Status property equal to 8.
d. For each of the SalesOrderHeader entities that the query selects, set the Status property to 8, and then create a new ArchivedSalesOrderHeader entity. e.
Save the changes.
Querying Data by Using WCF Data Services
6.
12-111
Save the ShippingDataService.svc file.
X Task 3: Call a business operation in the data service from a client Web application 1.
Review the task list.
2.
Open the HomeController file by double-clicking the comment TODO: Ex4 Define the ArchiveOrders ActionResult in the task list. This task is located in the ArchiveOrders method.
3.
Write code that performs the following tasks: a.
Create a new AsyncCallback object for the ArchiveCompleteCallback method.
b.
Create a new AdventureWorksEntities context object and assign the current network credentials to the Credentials property.
c.
Invoke the business operation asynchronously by using the static archiveUrl property of the Constants class.
d. Handle any DataServiceQueryException exceptions by throwing a new ApplicationException exception. e.
Return an ActionResult object by calling the Content method with a message Archive Complete as a parameter.
Note: ASP.NET Model-View-Controller (MVC) 1.0 does not directly support asynchronous action methods. Therefore, the code should use a blocking call to the EndExecute method instead of using the ArchiveCompleteCallBack method. ASP.NET MVC 2 will enable you to write real asynchronous action methods.
4.
Save the HomeController file.
X Task 4: Build and test the client Web site 1.
Build the solution and correct any errors.
2.
Check that the ShippingDetailsSite project is set as the StartUp project.
3.
Start the application in Debug mode.
4.
Explore the service by using Internet Explorer:
12-112
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
a.
On the Adventure Works Delivery Information page, click ArchiveShipping Orders.
b.
Wait until the Archive Complete message appears in the browser.
5.
Close Internet Explorer.
6.
Close the solution.
Querying Data by Using WCF Data Services
12-113
Lab Review
Review Questions 1.
What is the base type that provides the functionality that you can use to implement a WCF Data Service?
2.
In a WCF Data Service that implements transport-level security, how do you programmatically pass the credentials of a user who is running a client application to the service?
3.
How can you restrict the data that is visible to users of a data service based on their credentials?
4.
What attribute do you use to specify that a service operation retrieves data?
12-114
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Module Review and Takeaways
Review Questions 1.
You want to host a WCF Data Service in a custom application rather than by using IIS. Which type can you use to provide the hosting functionality in your application?
2.
What is the difference between the $select and $filter query options when you retrieve data from a WCF Data Service?
3.
If a WCF Data Service implements paging, how do you programmatically retrieve all of the pages of data for an entity set in a client application?
Best Practices Related to Querying Data by Using WCF Data Services Supplement or modify the following best practices for your own work situations: •
Protect a WCF Data Service by configuring transport-level security.
•
Use query interceptors to restrict access to entities to the users and roles that need to use those entities.
Querying Data by Using WCF Data Services
12-115
•
In a service operation, always check that the user or role that is requesting the operation has the appropriate privileges.
•
Configure appropriate page sizes for each entity in a WCF Data Service to prevent user requests from accidentally flooding the network with data, and to reduce the possibility of denial-of-service attacks by applications that deliberately request large volumes of data.
•
Limit use of the resource-intensive operations in a WCF Data Service. Configure the DataServiceConfiguration object to restrict use of query options such as $expand, $count, and $inlinecount.
•
Avoid enabling eager loading for large sets of related data.
•
Avoid using the "*" wildcard character as the parameter to the SetEntitySetAccessRule and SetServiceOperationAccessRule methods. Enable access to each entity and operation explicitly.
•
In a client application that consumes a WCF Data Service, always be prepared to handle DataServiceClientException, DataServiceQueryException, and DataServiceRequestException exceptions.
•
In a client application that calls service operations in a WCF Data Service, invoke these operations asynchronously.
Updating Data by Using WCF Data Services
13-1
Module 13 Updating Data by Using WCF Data Services Contents: Lesson 1: Creating, Updating, and Deleting Data in a WCF Data Service
13-3
Lesson 2: Preventing Unauthorized Updates and Improving Performance 13-22 Lesson 3: Using WCF Data Services with Nonrelational Data
13-30
Lab: Updating Data by Using WCF Data Services
13-42
13-2
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Module Overview
Many data-aware applications require more functionality than simply displaying data to a user. Windows® Communication Foundation (WCF) Data Services enables you to develop applications that support create, update, and delete functionality over the Internet or an intranet. This module describes how to use WCF Data Services to modify data. WCF Data Services use standard Internet protocols such as HTTP and the Atom Publishing Protocol to enable update access to data across the Internet or a corporate network.
Objectives After completing this module, you will be able to: •
Create, update, and delete data by using a WCF data service.
•
Control access to data-modification functionality, and improve performance by batching commands together.
•
Use WCF Data Services to access and modify nonrelational data.
Updating Data by Using WCF Data Services
13-3
Lesson 1
Creating, Updating, and Deleting Data in a WCF Data Service
When you modify data by using WCF Data Services, you first modify the data stored in the entities and then you submit the changes to the service. As with any multiuser data application, concurrency exceptions may be raised and you need to handle them appropriately in the client application. You also need to ensure that you maintain data integrity when modifying relational data. This lesson describes how to use WCF Data Services to create, update, and delete data. WCF Data Services follow the Representational State Transfer (REST) architectural model and use standard Web protocols; therefore, you can use a wide variety of client applications to access their functionality.
Objectives After completing this lesson, you will be able to: •
Describe how WCF Data Services implement create, update, and delete functionality.
13-4
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
•
Create tracked entities in a client application.
•
Create, update, and delete data by using the tracked entities in a client application.
•
Understand the exceptions that can be thrown when you send a request to a WCF data service.
•
Manage relationship links between entities in the client application.
•
Manage concurrency in a WCF data service.
Updating Data by Using WCF Data Services
13-5
WCF Data Services Data Modifications and the HTTP Protocol
Key Points You access a WCF data service to perform create, update, and delete (CUD) operations on data by using standard HTTP request messages. The WCF data service returns any results by using a standard HTTP response message. The following table shows how HTTP methods are mapped to CUD operations. HTTP method
CUD operation
HTTP POST
You use this HTTP method to create a new entity.
HTTP PUT
You can use either of these HTTP methods to update an existing entity.
HTTP MERGE HTTP DELETE
You use this method to delete an existing entity.
13-6
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Note: In the code examples in this lesson, the ProductDataService WCF data service provides access to product information in the AdventureWorks database. The ProductDataService WCF data service is hosted by a Web site with the URL http://MyHost/AdventureWorksSite.
Creating a New Entity To create a new entity, you send an HTTP POST message to the URI for the entity set to which you want to add the entity. For example, to create a new Product entity, you can use the URI in the following code example. http://MyHost/AdventureWorksSite/ProductDataService.svc/Products
The body of the HTTP POST message must include the entity data. You must encode this data by using either the Atom Publishing (AtomPub) Protocol format or the JavaScript Object Notation (JSON) format. You must provide values for all of the entity's properties except for those that support null values or are generated by the server. The body of the HTTP RESPONSE will include all of the entity's properties, including those that are generated by the server, and a URI that you can use to reference the entity that you have just created.
Performing Updates to an Entity by Using Replace-Based Semantics There are two ways to update an existing entity by using WCF Data Services. The first way uses replace-based semantics to perform the update. To update an existing entity, you send an HTTP PUT message to a URI that uniquely identifies an entity. For example, to modify the product with a ProductID value of 321, you can use the URI in the following code example. http://MyHost/AdventureWorksSite/ProductDataService.svc/Products(321)
If you send an HTTP PUT message, this will cause the server to replace the existing entity data with the data in the request message body. If any property values are missing from the request message, the server will replace them with a null or a default value.
Performing Updates to an Entity by Using Merge-Based Semantics The second way to update an entity by using WCF Data Services uses merge-based semantics.
Updating Data by Using WCF Data Services
13-7
To update an existing entity, you send an HTTP MERGE message to a URI that uniquely identifies an entity. For example, to modify the product with a ProductID value of 321, you can use the URI in the following code example. http://MyHost/AdventureWorksSite/ProductDataService.svc/Products(321)
If you send an HTTP MERGE message, this will cause the server to replace the existing entity data with the data in the request message body. You only need to send the properties that you are modifying, and if any property values are missing from the request message, the server will preserve the original values of those properties. Note: The HTTP MERGE method is an extension to the AtomPub Protocol.
Deleting an Entity To delete an existing entity, you send an HTTP DELETE message to a URI that uniquely identifies an entity. For example, to delete the product with a ProductID value of 321, you can use the URI in the following code example, http://MyHost/AdventureWorksSite/ProductDataService.svc/Products(321)
Inserting Related Entities You can insert several related entities at the same time by using an HTTP POST message. For example, you can insert a new product and its related product document by sending the data in the following code example in the body of the HTTP POST message. This example uses the JSON format. { ProductID:"987" Name: "Chainset" // Include additional property values here. ProductDocument : [ { ProductID: "987", DocumentID: "1187" ModifiedDate: " 2009-11-12T10:01:36.827" } ] }
13-8
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
You can also define relationships when you create a new entity. The following code example shows a URI that you can use to add a new review to the product with a ProductID value of 321. /Products(321)/ProductReview
Question: Must you include any data in the body of the HTTP request that you use to delete an entity?
Additional Reading For more information about the format of HTTP message bodies, see the Payload Formats (WCF Data Services) page at http://go.microsoft.com/fwlink/?LinkID=194105. For more information about the HTTP POST method, see the POST Method (WCF Data Services) page at http://go.microsoft.com/fwlink/?LinkID=194106. For more information about the HTTP MERGE method, see the MERGE Method (WCF Data Services) page at http://go.microsoft.com/fwlink/?LinkID=194107. For more information about the HTTP DELETE method, see the DELETE Method (WCF Data Services) page at http://go.microsoft.com/fwlink/?LinkID=194108.
Updating Data by Using WCF Data Services
13-9
Using the WCF Data Services Client Library
Key Points The previous module explained how to generate a WCF Data Services client library by adding a reference to your WCF data service in your client application project. This client library calls the REST-based application programming interface (API) of the WCF data service on your behalf by creating the appropriate HTTP request messages and by translating the incoming HTTP response messages. You use the DataServiceQuery class in your code to create queries that return typed entity objects. The DataServiceContext object caches these entity objects in typed entity sets. The DataServiceContext object tracks changes to the entity objects that your queries return, unless you specify a MergeOption value of NoTracking. The DataServiceContext object ensures that it has only one copy of an entity by using the unique id property of the entity. When your client application creates a new entity object, updates an existing entity object, or deletes an existing entity object, the DataServiceContext object records this change. When you call the SaveChanges method, the DataServiceContext
13-10
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
object generates and sends all of the HTTP requests that it needs to transmit the details of these changes to the WCF data service. Question: What is the parameter that is passed to the constructor of the DataServiceContext class?
Updating Data by Using WCF Data Services
13-11
Modifying Data by Using the WCF Data Services Client Library
Key Points You modify entity data by using the WCF Data Services client library in two stages. In the first stage, your application code modifies the entities that the DataServiceContext object tracks. In the second stage, your code calls the SaveChanges method of the DataServiceContext object to submit these changes to the WCF data service.
Creating a New Entity Object The following code example shows you how to create a new ProductReview entity object. When you add a data service reference, it generates a static Create method for each entity type, with a parameter for every nonnullable entity property. It also generates an AddTo method for every entity type. [Visual Basic] ' Instantiate the DataServiceContext object. Dim context As New AdventureWorksEntities(svcUri) ' Create a ProductReview object.
13-12
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Dim pr As ProductReview = ProductReview.CreateProductReview(0, 321, "John", DateTime.Now.[Date], "
[email protected]", 5, _ DateTime.Now) ' Add the ProductReview object to the ' DataServiceContext object. context.AddToProductReviews(pr)
[Visual C#] // Instantiate the DataServiceContext object. AdventureWorksEntities context = new AdventureWorksEntities(svcUri); // Create a ProductReview object. ProductReview pr = ProductReview.CreateProductReview(0, 321, "John", DateTime.Now.Date, "
[email protected]", 5, DateTime.Now); // Add the ProductReview object to the // DataServiceContext object. context.AddToProductReviews(pr);
Updating an Existing Entity Object The following code example shows you how to update an existing Product entity object. First, you retrieve the Product entity from the data service. Next, you modify the properties of the entity object, and then you call the UpdateObject method. [Visual Basic] ' Instantiate the DataServiceContext object. Dim context As New AdventureWorksEntities(svcUri) ' Retrieve a Product object. Dim product = (From p In context.Products _ Where p.ProductID = prodID _ Select p).[Single]() ' Make a change to the Product object. product.Color = "Red" ' Tell the DataServiceContext object about ' the update. context.UpdateObject(product)
Updating Data by Using WCF Data Services
13-13
[Visual C#] // Instantiate the DataServiceContext object. AdventureWorksEntities context = new AdventureWorksEntities(svcUri); // Retrieve a Product object. var product = (from p in context.Products where p.ProductID == prodID select p).Single(); // Make a change to the Product object. product.Color = "Red"; // Tell the DataServiceContext object about // the update. context.UpdateObject(product);
Attaching an Existing Entity to the DataServiceContext Object The following code example shows you how to update an entity object without first running a query to retrieve it. First, you use the Create method to instantiate a new entity object. Next, you attach it to the DataServiceContext object, and then you call the UpdateObject method. [Visual Basic] ' Instantiate the DataServiceContext object. Dim context As New AdventureWorksEntities(svcUri) ' Create a ProductReview object with ' the new property values. Dim pr As ProductReview = ProductReview.CreateProductReview(21, 321, "John Smith", DateTime.Now.[Date], "
[email protected]", 5, _ DateTime.Now) ' Tell the DataServiceContext object about ' the update. context.UpdateObject(pr)
[Visual C#] // Instantiate the DataServiceContext object. AdventureWorksEntities context = new AdventureWorksEntities(svcUri); // Create a ProductReview object with // the new property values. ProductReview pr = ProductReview.CreateProductReview(21,
13-14
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
321, "John Smith", DateTime.Now.Date, "
[email protected]", 5, DateTime.Now); // Tell the DataServiceContext object about // the update. context.UpdateObject(pr);
Deleting an Existing Entity Object The following code example shows you how to delete an existing Product entity object. First, you retrieve the Product entity from the data service, and then you call the DeleteObject method. [Visual Basic] ' Instantiate the DataServiceContext object. Dim context As New AdventureWorksEntities(svcUri) ' Retrieve a Product object. Dim product = (From p In context.Products _ Where p.ProductID = prodID _ Select p).[Single]() ' Delete the ProductReview object. context.DeleteObject(product)
[Visual C#] // Instantiate the DataServiceContext object. AdventureWorksEntities context = new AdventureWorksEntities(svcUri); // Retrieve a Product object. var product = (from p in context.Products where p.ProductID == prodID select p).Single(); // Delete the ProductReview object. context.DeleteObject(product);
Saving the Changes To persist the changes that you have made to the entity objects that the DataServiceContext object manages, you must call the SaveChanges method. The SaveChanges method generates and submits all of the necessary HTTP request messages to the WCF data service.
Updating Data by Using WCF Data Services
13-15
The SaveChanges method returns the results of all of the CUD operations that the DataServiceResponse object performs. This DataServiceResponse object contains a sequence of ChangeOperationResponse objects that contain a sequence of EntityDescriptor objects. For successful changes, the State property of the EntityDescriptor object is set to Unchanged. If the WCF data service generates any property values, for example, in a calculated column, the client library automatically updates the entity object with these values. Question: What is the advantage of attaching an existing entity object to the DataServiceContext object to perform an update?
13-16
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Handling Data-Modification Errors in the Client
Key Points When you modify entity data by using the WCF Data Services client library, the SaveChanges method returns a DataServiceResponse object with the results of the operations. There may be several results, because the SaveChanges method can process many CUD operations during a single call. For each failed operation, the client library sets the Error property of the EntityDescriptor object to a DataServiceRequestException object that contains the details of the error. By default, the SaveChanges method stops sending requests to the server as soon as it receives an error. It then throws a DataServiceRequestException exception that contains details of the error. If you pass the ContinueOnError value to the overloaded version of the SaveChanges method that takes a SaveChangesOption parameter, it will continue to send requests after it has received an error. In this case, you must check the DataServiceResponse object to discover the errors that have occurred.
Updating Data by Using WCF Data Services
13-17
Question: If you call the no-argument version of the SaveChanges method, and the WCF data service detects an error in one of the CUD operations, will the other CUD operations still run?
13-18
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Managing Entity Relationships
Key Points The DataServiceContext class tracks link objects in addition to entity objects in the client. When you add a new entity by using an AddTo method, you must also define any links that the entity has to other entities. The SaveChanges method will persist any links as relationships in the WCF data service. Note: You must define links between your entity objects on the client and define the navigation properties of your entity objects.
The following table describes the methods of the DataServiceContext class that you can use to manage your link objects. Method AddRelatedObject
Description This method creates a new bidirectional link between two related entity objects. Calling this method is equivalent to calling the AddLink method followed by the SetLink method to define both directions of the relationship. Call this method
Updating Data by Using WCF Data Services
Method
13-19
Description for any type of relationship.
AddLink
This method creates a new link between two related entity objects. Call this method when you add the object to a collection of objects at the "many" end of a one-to-many or many-to-many relationship, such as when you add a new product review to a product.
SetLink
This method creates a new link between two related entity objects. Call this method when you add the object as a reference at the zero-or-one end of a one-to-one or many-toone relationship, such as when you assign a customer to an order.
DeleteLink
This method marks a link that the ObjectContext object is tracking for deletion when you call the SaveChanges method. Use this method when you delete a related object or change a relationship by first deleting the link to an existing object and then adding a link to the new related object.
AttachLink
This method notifies the ObjectContext object of an existing link between two entity objects. The ObjectContext object assumes that this relationship already exists in the data service and does not try to create the link when you call the SaveChanges method. Use this method when you attach objects to a context and need to attach the link between the two. If you define a new relationship, you should use the AddLink and SetLink methods instead.
DetachLink
This method stops tracking the specified link in the context.
The following code example shows you how to add a product review to a product. [Visual Basic] ' Instantiate the DataServiceContext object. Dim context As New AdventureWorksEntities(svcUri) ' Get the product you want to add the review to. Dim selectedProduct = (From product In context.Products _ Where product.ProductID = productId _ Select product).Single() ' Create the new ProductReview object.
13-20
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Dim pr As ProductReview = ProductReview.CreateProductReview(0, productId, "John Smith", DateTime.Now.[Date], "
[email protected]", 5, DateTime.Now) pr.Comments = "Excellent!" ' Make sure that the links are set. context.AddRelatedObject(selectedProduct, "ProductReviews", pr) context.SetLink(pr, "Product", selectedProduct) ' Add the navigation properties. selectedProduct.ProductReviews.Add(pr) pr.Product = selectedProduct ' Save the changes. context.SaveChanges()
[Visual C#] // Instantiate the DataServiceContext object. AdventureWorksEntities context = new AdventureWorksEntities(svcUri); // Get the product you want to add the review to. var selectedProduct = (from product in context.Products where product.ProductID == productId select product).Single(); // Create the new ProductReview object. ProductReview pr = ProductReview.CreateProductReview(0, productId, "John Smith", DateTime.Now.Date, "
[email protected]", 5, DateTime.Now); pr.Comments = "Excellent!"; // Make sure that the links are set. context.AddRelatedObject(selectedProduct, "ProductReviews", pr); context.SetLink(pr, "Product", selectedProduct); // Add the navigation properties. selectedProduct.ProductReviews.Add(pr); pr.Product = selectedProduct; // Save the changes. context.SaveChanges();
Question: When you need to add an entity object at the "one" end of a one-tomany relationship, which Link method must you use?
Updating Data by Using WCF Data Services
13-21
Managing Concurrency
Key Points WCF Data Services implement an optimistic currency model. When you make a call to the SaveChanges method, the WCF data service will check whether another process on the server has modified the entity that you want to save. If it detects a conflict, the WCF data service returns an error to the client and makes no changes. The Entity Framework detects concurrency conflicts by using the ConcurrencyMode attribute. In an Entity Data Model (EDM), you set the ConcurrencyMode attribute for an entity property to Fixed. When the Entity Framework tries to modify the entity, it checks to see whether these properties have changed. A good example of such a property is a timestamp property that is updated whenever the entity is modified. Question: What strategies can you adopt to handle a concurrency conflict?
13-22
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Lesson 2
Preventing Unauthorized Updates and Improving Performance
Applications often need to include data validation during the update process. You can use change interceptors to insert additional logic that the WCF data service executes during the data-modification process to control access to the data. When you are modifying data, it can be more efficient to submit changes to the server in groups to reduce the number of round trips made. This lesson explains how to control access to update operations through configuration options and by using change interceptors. The lesson also explains how to batch data modifications together at the client to minimize the number of round trips to the service.
Updating Data by Using WCF Data Services
Objectives After completing this lesson, you will be able to: •
Control access to data-modification functionality in a WCF data service.
•
Batch data-modification operations together at the client.
13-23
13-24
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Preventing Unauthorized Updates
Key Points When the client application of a WCF data service enables a user to make changes to data, you must be able to control access to the service. You can use the DataServiceConfiguration object to set up some simple access rules, but for finegrained control, you must create change interceptors in your data service code.
Using the DataServiceConfiguration Object You can use the SetEntityAccessRule method to specify the operations that a user is permitted to perform on a specific entity type. The following table describes the values of the SetEntityRights enumeration that control data-modification rights. Name
Description
WriteAppend
Authorizes any user to create new entities in the entity set.
WriteReplace
Authorizes any user to replace entities in the entity set.
WriteDelete
Authorizes any user to delete entities in the entity set.
Updating Data by Using WCF Data Services
Name
13-25
Description
WriteMerge
Authorizes any user to merge changes into an entity in the entity set.
AllWrite
Authorizes any user to make any changes to an entity in the entity set.
All
Authorizes any user to make any changes to an entity and to read any entity in the entity set.
The following code example demonstrates how to give permissions to any user to read and update any contact entity. [Visual Basic] config.SetEntitySetAccessRule("SalesOrderHeaders", (EntitySetRights.AllRead Or EntitySetRights.WriteReplace Or EntitySetRights.WriteMerge))
[Visual C#] config.SetEntitySetAccessRule("SalesOrderHeaders", (EntitySetRights.AllRead | EntitySetRights.WriteReplace | EntitySetRights.WriteMerge));
Using Change Interceptors A change interceptor is a special method that informs you about the type of modification that the user wants to perform. Additionally, the method parameters enable you to identify the user and access the full details of the data that the user wants to modify. Methods that are change interceptors must meet the following criteria: •
You must decorate the method with the ChangeInterceptor attribute. This attribute specifies the entity set with which the change interceptor is associated.
•
The method must return void (Microsoft® Visual C#®) or nothing (Microsoft Visual Basic®).
•
The first parameter must be of a type that is compatible with the entity set that the attribute identifies. The data service uses the data that the client submits to populate this parameter.
13-26
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
•
The second parameter must be of type UpdateOperations.
The following code example shows how to check whether a user who attempts to delete a contact entity is in the AdventureWorksEmployees group. [Visual Basic] ' Use the ChangeInterceptor attribute. _ Public Sub OnDeleteContact(ByVal contact As Contact, ByVal operation As UpdateOperations) ' Check the operation. If operation = UpdateOperations.Delete Then ' Check the user's role membership. If Not HttpContext.Current.User.IsInRole("AdventureWorksEmployees") Then ' Throw the exception. Throw New DataServiceException(400, "You are not permitted to delete contacts") End If End If End Sub
[Visual C#] // Use the ChangeInterceptor attribute. [ChangeInterceptor("Contacts")] public void OnDeleteContact(Contact contact, UpdateOperations operation) { // Check the operation. if (operation == UpdateOperations.Delete) { // Check the user's role membership. if (!HttpContext.Current.User.IsInRole( "AdventureWorksEmployees")) { // Throw the exception. throw new DataServiceException(400, "You are not permitted to delete contacts"); } } }
You can use change interceptors to implement business logic during the datamodification process. The following code example shows how to set the value of
Updating Data by Using WCF Data Services
13-27
the EmailPromotion field if users provide an e-mail address when they create a new contact entity. [Visual Basic] ' Use the ChangeInterceptor attribute. _ Public Sub OnCreateContact(ByVal contact As Contact, ByVal operation As UpdateOperations) ' Check the operation. If operation = UpdateOperations.Add Then If contact.EmailAddress IsNot Nothing AndAlso contact.EmailPromotion = 0 Then contact.EmailPromotion = 1 End If End If End Sub
[Visual C#] // Use the ChangeInterceptor attribute. [ChangeInterceptor("Contacts")] public void OnCreateContact(Contact contact, UpdateOperations operation) { // Check the operation. if (operation == UpdateOperations.Add) { if (contact.EmailAddress != null && contact.EmailPromotion == 0) { contact.EmailPromotion = 1; } } }
Question: If you want to apply a single validation rule for both insert and update operations, do you need to define two separate change interceptor methods?
Additional Reading For more information about configuring data services, see the Configuring the Data Service (WCF Data Services) page at http://go.microsoft.com/fwlink/?LinkID=194109.
13-28
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Batching Multiple Operations
Key Points You use batch requests to submit a collection of data-modification requests, and queries, in a single HTTP request. This can reduce the number of round trips and improve the performance of the client application. Batches also behave like transactions, so either all of the operations in the batch succeed or they all fail. This can help with the design of data-integrity rules in the application. Each batch request results in a single HTTP response that contains the response results for all of the operations in the batch. If any individual operation fails, the batch response will contain just one response result for that failed operation. You can submit a collection of data-modification requests in a batch by calling the SaveChanges method with the SaveChangesOptions.Batch enumeration value as a parameter value. You can access the results of all of the submitted operations through the BatchHeaders property of the DataResponse object. The following code example demonstrates how to call the SaveChanges method and use a batch.
Updating Data by Using WCF Data Services
13-29
[Visual Basic] ' Send all of the changes as a single batch. Dim result As DataServiceResponse = context.SaveChanges(SaveChangesOptions.Batch)
[Visual C#] // Send all of the changes as a single batch. DataServiceResponse result = context.SaveChanges(SaveChangesOptions.Batch);
You can also execute batches asynchronously by using the BeginExecuteBatch and EndExecuteBatch methods. Question: When you call the SaveChanges method, can you combine the SaveChangesOptions.Batch and SaveChangesOptions.ContinueOnError enumeration values in the parameter value?
Additional Reading For more information about the format of the HTTP messages used to submit batches, see the Batch Requests (WCF Data Services) page at http://go.microsoft.com/fwlink/?LinkID=194110. For more information about asynchronous calls to WCF Data Services, see the Asynchronous Operations (WCF Data Services) page at http://go.microsoft.com/fwlink/?LinkID=194111.
13-30
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Lesson 3
Using WCF Data Services with Nonrelational Data
In addition to exposing relational data to your applications, WCF data services can provide access to nonrelational data such as binary data. You can then use custom classes in your WCF data service to represent your entity data. This lesson explains how to create WCF data services that expose nonrelational data to a client.
Objectives After completing this lesson, you will be able to: •
Create a WCF data service that uses custom classes to represent your entity data.
•
Describe the reflection provider and how you can use it to support data retrieval and data updates.
•
Create a WCF Data Service that supports streaming binary data.
Updating Data by Using WCF Data Services
13-31
Demonstration: Creating a WCF Data Service to Access Nonrelational Data
Key Points •
Create a WCF data service that uses nonrelational data.
Note: In this example, the service maintains the data in memory without the use of a database and makes it available as read-only data to a client application.
Demonstration Steps 1.
Log on to the 10265A-GEN-DEV-13 virtual machine as Student with the password Pa$$w0rd.
2.
In Windows Internet Explorer®, turn off the feed reading view.
3.
Start Microsoft Visual Studio® 2010.
4.
Open the AdventureWorks solution.
5.
Review the code.
13-32
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
6.
Add Get accessors for the two entity sets.
7.
Test the WCF data service.
8.
Close Internet Explorer, and then close Visual Studio.
Question: Describe a possible scenario where this type of WCF data service may be useful.
Updating Data by Using WCF Data Services
13-33
Using the Reflection Provider
Key Points In the previous demonstration, you saw how to create a WCF data service that exposed custom data classes as queryable entities. The WCF data service did this without the use of an EDM; instead, it inferred information about the data model from the classes that you used. The WCF data service infers the data model to use by using reflection. The following table describes the rules that it uses. Entity element
Description
Container
The container is identified as the class that exposes properties that return IQueryable instances. There is only one container in a namespace.
Set
Any property that returns an IQueryable interface identifies an entity set.
Type
The type T of the IQueryable property identifies the entity type.
Key
The DataServiceKey attribute identifies the key property of the entity.
13-34
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Entity element Property
Description If a property of the entity type is a primitive type, the property is treated as an entity property. If a property of an entity type is another entity type, the property is treated as the "one" end of a many-to-one or one-to-one relationship. If a property of the entity type is an IEnumerable instance of another entity type, the property is treated as the "many" end of a one-tomany or many-to-many relationship. If a property of the entity type is a value type, the property is treated as a complex type.
The Role of the IQueryable Interface You can use the reflection provider to expose data from any set of classes that you can access by using the IQueryable interface. For example, any collection that implements the IEnumerable interface exposes the IQueryable interface through the AsQueryable method. The Table class that Language-Integrated Query to Structured Query Language (LINQ to SQL) uses also implements the IQueryable interface.
Implementing the IUpdateable Interface The example in the previous demonstration used in-memory data that was lost when the application terminated. If the data that the WCF data service exposes has a backing store, you can enable data-modification functionality by implementing the IUpdateable interface. The following table describes the methods of the IUpdateable interface that you must implement. Method
Description
AddReferenceToCollection
This method adds an object to a collection of objects that a navigation property references.
ClearChanges
This method rolls back all pending changes to data.
CreateResource
This method creates a new entity object.
DeleteResource
This method deletes an entity object.
Updating Data by Using WCF Data Services
Method
13-35
Description
GetResource
This method retrieves an entity object.
GetValue
This method retrieves the value of an entity property.
RemoveReferenceFromCollection
This method removes an object from a collection of objects that a navigation property references.
ResetResource
This method updates an entity object.
ResolveResource
This method returns an entity object that is represented by an object instance.
SaveChanges
This method saves all pending changes to the backing store.
SetReference
This method sets a related object reference by using a navigation property.
SetValue
This method sets the value of an entity property.
Question: What characteristic must the property that you identify with the DataServiceKey attribute possess?
Additional Reading For more information about how to implement the IUpdateable interface, see the How to: Create a Data Service Using a LINQ to SQL Data Source (WCF Data Services) page at http://go.microsoft.com/fwlink/?LinkID=194112.
13-36
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Working with Binary Data
Key Points By default, WCF data services return binary properties of entity objects as base-64 encoded data in the response message. These messages can be large, which has a negative effect on the performance of the service. The Open Data Protocol (OData) includes a definition of a mechanism for streaming large amounts of binary data independently of the standard HTTP request and response messages. Entities can contain a media link entry that points to a separate media resource such as a video or an image. You can use this mechanism to retrieve and update binary data.
Enabling Binary Streaming for a WCF Data Service To implement a WCF data service that can stream binary data as a separate resource, you must perform the following steps: 1.
Add the HasStream attribute to the entity that includes the binary data that you want to stream in the EDM. If you use the reflection provider, you add the HasStreamAttribute attribute to the class that represents the entity type.
Updating Data by Using WCF Data Services
2.
13-37
Implement the IServiceProvider interface in your data service class. The following code example shows how to use this interface to retrieve an object that implements the IDataServiceStreamProvider interface.
[Visual Basic] Public Class AdventureWorksStreaming Inherits DataService(Of AdWorksEntities) Implements IServiceProvider Public Shared Sub InitializeService(ByVal config As DataServiceConfiguration) config.SetEntitySetAccessRule("Employees", EntitySetRights.All) End Sub #Region "Implement the IServiceProvider interface." Public Function GetService(ByVal serviceType As Type) As Object If serviceType Is GetType(IDataServiceStreamProvider) Then Return New AdWorksStreamProvider(Me.CurrentDataSource) End If Return Nothing End Function #End Region End Class
[Visual C#] public class AdventureWorksStreaming : DataService, IServiceProvider { public static void InitializeService(DataServiceConfiguration config) { config.SetEntitySetAccessRule("Employees", EntitySetRights.All); } #region Implement the IServiceProvider interface. public object GetService(Type serviceType) { if (serviceType == typeof(IDataServiceStreamProvider)) { return new AdWorksStreamProvider(this.CurrentDataSource); } return null; } #endregion }
13-38
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
3.
Define a class that implements the IDataServiceStreamProvider interface.
The following table describes the IDataServiceStreamProvider interface members. Name
Description
DeleteStream
The data service invokes this method to delete the corresponding media resource when its media link entry is deleted. When you implement the IDataServiceStreamProvider interface, this method contains the code that deletes the media resource that is associated with the supplied media link entry.
GetReadStream
The data service invokes this method to return a media resource as a stream. When you implement the IDataServiceStreamProvider interface, this method contains the code that provides a stream that the data service uses to return the media resource that is associated with the provided media link entry.
GetReadStreamUri
The data service invokes this method to return the URI that is used to request the media resource for the media link entry.
GetStreamContentType
The data service invokes this method to return the Content-Type value of the media resource that is associated with the specified media link entry.
GetStreamETag
The data service invokes this method to return the eTag of the data stream that is associated with the specified entity. You use this method to manage concurrency for the binary data. When this method returns null, the data service does not track concurrency.
GetWriteStream
The data service invokes this method to obtain the stream that is used when receiving the stream sent from the client. When you implement the
Updating Data by Using WCF Data Services
Name
13-39
Description IDataServiceStreamProvider interface, you must return a writable stream to which the data service writes the stream data that it receives. When you use a FileStream object, you can access the persisted binary data from the file system on the server before the interface instance is disposed of.
ResolveType
This method returns a namespace-qualified type name. This type name represents the type that the data service runtime must create for the media link entry that is associated with the data stream for the media resource.
StreamBufferSize
This property gets the size of the stream buffer.
Accessing Streamed Data in a WCF Data Service A WCF data service client application can access streamed binary data by using the GetReadStream and SetSaveStream methods of the DataServiceContext object. The following code example shows how to retrieve a bitmap image from a contact entity in the AdventureWorks database. [Visual Basic] Using response as DataServiceStreamResponse = context.GetReadStream(contacte, "image/bmp")) Using imageStream As New MemoryStream() Dim buffer As Byte() = New Byte(1000) {} Dim count As Integer = 0 ' Read the returned stream into the new memory stream. While response.Stream.CanRead And 0 < (count = response.Stream.Read(buffer, 0, buffer.Length)) imageStream.Write(buffer, 0, count) End While ' Use the returned bitmap stream. _Image = CreateImageFromStream(imageStream) End Using End Using
13-40
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
[Visual C#] using (DataServiceStreamResponse response = context.GetReadStream(contacte, "image/bmp")) { using (MemoryStream imageStream = new MemoryStream()) { byte[] buffer = new byte[1000]; int count = 0; // Read the returned stream into the new memory stream. while (response.Stream.CanRead && (0 < ( count = response.Stream.Read(buffer, 0, buffer.Length)))) { imageStream.Write(buffer, 0, count); } // Use the returned bitmap stream. image = CreateImageFromStream(imageStream); } }
The following code example shows how to save an image to a WCF data service. [Visual Basic] ' The stream image is associated with the contact entity. ' The stream will be closed when the SaveChanges method is called. context.SetSaveStream(contact, image, True, "image/bmp", String.Empty) ' Upload the binary stream to the data service. context.SaveChanges()
[Visual C#] // The stream image is associated with the contact entity. // The stream will be closed when the SaveChanges method is called. context.SetSaveStream(contact, image, true, "image/bmp", string.Empty); // Upload the binary stream to the data service. context.SaveChanges();
Updating Data by Using WCF Data Services
13-41
Question: Do you always need to implement the IServiceProvider and IDataServiceStreamProvider interfaces if you want your WCF data service to be able to handle binary data?
13-42
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Lab: Updating Data by Using WCF Data Services
Objectives After completing this lab, you will be able to: •
Update existing entities by using a WCF data service.
•
Create and delete entities by using a WCF data service.
•
Implement a change interceptor to protect data and prevent unauthorized insert, update, and delete operations.
Introduction In this lab, you will write code to enable a client application to update entities that a WCF data service exposes. You will modify the WCF data service to allow data to be created or deleted. Finally, you will write code to protect data by intercepting data service requests and applying business logic.
Updating Data by Using WCF Data Services
13-43
Lab Setup For this lab, you will use the available virtual machine environment. Before you begin the lab, you must: •
Start the 10265A-GEN-DEV-13 virtual machine, and then log on by using the following credentials: •
User name: Student
•
Password: Pa$$w0rd
13-44
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Lab Scenario
Delivery companies have expressed a need to be able to update delivery information remotely. You decide to modify the Web application to access the data service. You also decide to add update functionality to the WCF data service.
Exercise 1: Updating Entities by Using a WCF Data Service Scenario In this exercise, you will modify the Web client application to enable the user to update the status of orders. The Web application will handle any exceptions that the WCF data service throws. The main tasks for this exercise are as follows: 1.
Prepare the environment for the lab.
2.
Prepare the AdventureWorks database for the lab.
3.
Open the starter project.
4.
Implement a controller action that performs a status update.
Updating Data by Using WCF Data Services
5.
Enable modifications on the SalesOrderHeaders entity set.
6.
Test the ShippingDetailsSite Web application.
13-45
f Task 1: Prepare the environment for the lab 1.
Log on to the 10265A-GEN-DEV-13 virtual machine as Student with the password Pa$$w0rd.
2.
In the E:\Labfiles folder, run EnvSetup.bat as an administrator.
f Task 2: Prepare the AdventureWorks database for the lab •
In the E:\Labfiles folder, run AWReset.bat.
f Task 3: Open the starter project 1.
In the E:\Labfiles\Lab13\VB\Ex1\Starter or E:\Labfiles\Lab13\CS\Ex1\Starter folder, run ExSetup.bat as an administrator.
2.
Open Visual Studio 2010 as an administrator.
3.
Open the existing solution, ShippingDataServiceSite.sln, in the E:\Labfiles\Lab13\VB\Ex1\Starter or E:\Labfiles\Lab13\CS\Ex1\Starter folder.
f Task 4: Implement a controller action that performs a status update 1.
Review the task list.
2.
Open the HomeController file by double-clicking the comment TODO: Add action method to handle status update.
3.
Immediately after the comment, add a new method named HandleStatusUpdate that returns an ActionResult object and accepts the following parameters:
4.
a.
A byte parameter called StatusList.
b.
An integer parameter called id.
Make the HandleStatusUpdate method public, and then specify that if an exception is thrown, the Error view should be displayed.
13-46
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
5.
6.
In the HandleStatusUpdate method, write code that performs the following tasks: a.
Assign a new AdventureWorksEntities object to the context variable.
b.
Set the Credentials property of the context object to the current default network credentials.
c.
Define a LINQ query named salesOrderHeaderToChange that retrieves a single SalesOrderHeader object with a SalesOrderID property that is equal to the value of the id variable.
In the HandleStatusUpdate method, write code that performs the following tasks: a.
Set the Status property of the salesOrderHeaderToChange object to the value of the StatusList variable.
b.
Call the UpdateObject method of the context object, passing the salesOrderHeaderToChange object as a parameter.
c.
Handle any DataServiceRequestException exceptions by throwing a new ApplicationException exception.
d. Save the changes. e.
7.
Return an ActionResult object by calling the RedirectToAction method with "Details/" + id ("Details/" & id in Visual Basic) values as a parameter.
Save the HomeController file.
f Task 5: Enable modifications on the SalesOrderHeaders entity set 1.
Review the task list.
2.
Open the ShippingDataService file by double-clicking the TODO: Allow changes to SalesOrderHeaders task in the task list. This task is located in the InitializeService method.
3.
Immediately after the comment, modify the code that sets the access rule for the SalesOrderHeaders entity to allow WriteReplace access and WriteMerge access.
4.
Build the solution and correct any errors.
Updating Data by Using WCF Data Services
13-47
f Task 6: Test the ShippingDetailsSite Web application 1.
Check that the ShippingDetailsSite project is set as the startup project.
2.
Browse to the ShippingDetailsSite Web site by using Internet Explorer.
3.
Explore the service by using Internet Explorer.
4.
Close Internet Explorer.
5.
Close the solution:
Exercise 2: Creating and Deleting Entities by Using a WCF Data Service Scenario In this exercise, you will build a Windows Presentation Foundation (WPF) application to enable the user to add new contacts, modify contacts, and delete contacts. You will use batching to optimize network access and batch groups of updates together. You will also implement concurrency checking. The main tasks for this exercise are as follows: 1.
Open the starter project.
2.
Connect to the ShippingDataService data service.
3.
Call the data service to retrieve contacts in a specified range.
4.
Call the data service to retrieve the next page of contact results.
5.
Implement the method that adds, deletes, and updates contacts.
6.
Save the changes to the data context.
7.
Discard the changes to the data context.
8.
Enable write access on the Contacts entity set.
9.
Test the ContactManagement application.
f Task 1: Open the starter project 1.
In the E:\Labfiles\Lab13\CS\Ex2\Starter or E:\Labfiles\Lab13\VB\Ex2\Starter folder, run ExSetup.bat as an administrator.
13-48
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
2.
Open the existing solution, ContactManagement.sln, in the E:\Labfiles\Lab13\VB\Ex2\Starter\ContactManagement or E:\Labfiles\Lab13\CS\Ex2\Starter\ContactManagement folder.
f Task 2: Connect to the ShippingDataService data service 1.
Review the task list.
2.
Open the MainWindow file by double-clicking the TODO: Add the namespaces containing the types required to use a WCF Data Service task in the task list.
3.
In the Code Editor window, bring the System.Data.Services.Client and System.Net namespaces into scope.
4.
Locate the next comment in the MainWindow file by double-clicking the TODO: Add the namespace containing the types for the ShippingDataService task in the task list.
5.
Immediately after the comment, add a statement to bring the ContactManagement.ShippingDataService namespace into scope.
6.
Locate the next comment in the MainWindow file by double-clicking the TODO: Define a DataServiceCollection for holding Contact data task in the task list.
7.
Immediately after the comment, write code that declares a private field called contactInfo based on the DataServiceCollection generic type. Specify Contact as the type parameter for the DataServiceCollection type, and assign it the value null (Nothing in Visual Basic).
8.
Locate the next comment in the MainWindow file by double-clicking the TODO: Define an AdventureWorksEntities context for accessing the service task in the task list.
9.
Immediately after the comment, write code that declares a variable called context of type AdventureWorksEntities and assign it the value null.
10. Locate the next comment in the MainWindow file by double-clicking the TODO: Connect to the ShippingDataService task in the task list. 11. Immediately after the comment, write code that assigns the value that is returned by calling the ConnectToContext method to the context variable. 12. Locate the next comment in the MainWindow file by double-clicking the TODO: Connect to the ShippingDataService task in the task list.
Updating Data by Using WCF Data Services
13-49
13. Immediately after the comment, write code that creates a new AdventureWorksEntities object named context. 14. Locate the next comment in the MainWindow file by double-clicking the TODO: Set the credentials for accessing the service task in the task list. 15. Immediately after the comment, write code to set the Credentials property of the context object to the current default network credentials. 16. Locate the next comment in the MainWindow file by double-clicking the TODO: Return the connected context task in the task list. 17. Immediately after the comment, write code to return the context variable. 18. Save the MainWindow file.
f Task 3: Call the data service to retrieve contacts in a specified range 1.
Review the task list.
2.
Locate the next comment in the MainWindow file by double-clicking the TODO: Fetch all contacts in the range specified by the user on the form task in the task list. This task is located in the getContacts_Click method.
3.
In the getContacts_Click method, immediately after the comment, write code that performs the following tasks: a.
Assign the value of the contactIDFrom.Text property to an integer variable called lowerBound.
b.
Assign the value of the contactIDTo.Text property to an integer variable called upperBound.
c.
Create a DataServiceCollection object that the current context tracks, and assign it to the contactInfo variable by defining a LINQ query that retrieves an enumerable collection of Contact objects with a ContactID property greater than or equal to the value of the lowerBound variable and less than or equal to the upperBound variable.
d. Define a new ObservableCollection object of type Contact by using the contactInfo property as a parameter, and assign it to the this.contactsGrid.DataContext property (Me.contactsGrid.DataContext property in Visual Basic). e.
Handle any DataServiceQueryException exceptions or Exception exceptions by displaying a message box with the following text: Error Fetching Contacts.
13-50
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
4.
Save the MainWindow file.
f Task 4: Call the data service to retrieve the next page of contact results 1.
Review the task list.
2.
Locate the next comment in the MainWindow file by double-clicking the TODO: Retrieve the next page of Contact information and display it task in the task list. This task is located in the moreContacts_Click method.
3.
In the moreContacts_Click method, immediately after the comment, write code that performs the following tasks: a.
If the contactInfo.Continuation property is not null (Nothing in Visual Basic), create an enumerable collection of Contact objects by calling the Execute method of the context object, passing the contactInfo.Continuation.NextLinkUri property as a parameter. Pass this collection to the Load method of the contactInfo class.
b.
Define a new property based on the ObservableCollection generic type, specifying Contact as the type parameter. Pass the contactInfo property as a parameter, and assign this object to the current contactsGrid.DataContext property.
c.
Otherwise display a message box with the following text: No More Contacts.
d. Handle any DataServiceQueryException exceptions or Exception exceptions by displaying a message box with the following text: Error Fetching Next Page of Contacts. 4.
Save the MainWindow file.
f Task 5: Implement the method that adds, deletes, and updates contacts 1.
Review the task list.
2.
Locate the next comment in the MainWindow file by double-clicking the TODO: Add the new Contact to the context task in the task list. This task is located in the contactsGrid_PreviewKeyDown method.
3.
In the contactsGrid_PreviewKeyDown method, immediately after the comment, write code that performs the following tasks:
Updating Data by Using WCF Data Services
13-51
a.
If the newContact object is not null (Nothing in Visual Basic), add it to the context object by using the AddToContacts method.
b.
Insert the newContact object into the displayedContacts collection at the position that the contactsGrid.SelectedIndex property specifies.
4.
Locate the next comment in the MainWindow file by double-clicking the TODO: Remove the Contact from the context task in the task list.
5.
Immediately after the comment, write code to perform the following tasks: a.
Remove the currentContact object from the context.
b.
Remove the currentContact object from the displayedContacts collection.
6.
Locate the next comment in the MainWindow file by double-clicking the TODO: Update the Contact object in the context task in the task list.
7.
Immediately after the comment, write code to update the currentContact object by calling the UpdateObject method of the context object.
8.
Build the solution and correct any errors.
f Task 6: Save the changes to the data context 1.
Review the task list.
2.
Locate the next comment in the MainWindow file by double-clicking the TODO: Send all changes as a single batch task in the task list. This task is located in the saveChanges_Click method.
3.
Immediately after the comment, write code to perform the following tasks:
4.
a.
Define a DataServiceResponse object called result and assign this the value that is returned by calling the SaveChanges method of the context object, specifying the SaveChangesOptions.Batch enumeration as a parameter.
b.
Display a message box specifying Changes Saved as the text.
c.
Handle any DataServiceRequestException exceptions by displaying a message box with the following text: Error Saving Changes.
Build the solution and correct any errors.
13-52
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
f Task 7: Discard the changes to the data context 1.
Review the task list.
2.
Locate the next comment in the MainWindow file by double-clicking the TODO: Clear the context and fetch the Contact data from the Data Service again task in the task list. This task is located in the discardChanges_Click method.
3.
Immediately after the comment, write code to perform the following tasks:
4.
a.
Assign the value that is returned by calling the ConnectToContext method to the context parameter.
b.
Reload the contacts by raising the ClickEvent event object of the getContacts button.
Build the solution and correct any errors.
f Task 8: Enable write access on the Contacts entity set 1.
Review the task list.
2.
Open the ShippingDataService file by double-clicking the TODO: Enable Write access on the Contacts entity set task in the task list. This task is located in the InitializeService method.
3.
Immediately after the comment, modify the code that sets the access rule for the Contacts entity to allow AllWrite access and AllRead access.
4.
Build the solution and correct any errors.
f Task 9: Test the ContactManagement application 1.
Check that the ContactManagement application is set as the startup project.
2.
Start the application in Debug mode.
3.
In the Contact Management window, next to the Get Contacts button, in the box, type 1
4.
Press the TAB key, type 100 and then click Get Contacts.
5.
Click More to load more contacts. Repeat this until the No More Contacts message is displayed.
6.
In the No More Contacts dialog box, click OK.
7.
Edit an existing contact.
Updating Data by Using WCF Data Services
8.
Insert a new contact.
9.
Commit the changes to the database.
13-53
10. Delete a contact. 11. Commit the changes to the database. 12. Discard the changes and reload the contact data from the database. 13. Close the ContactManagement application. 14. Close the solution.
Exercise 3: Restricting Create, Update, and Delete Requests Scenario In this exercise, you will add change interceptors to the WCF data service to limit the operations that different users can perform and to apply business logic to update operations. Only users in the AdventureWorksEmployees roles can add, delete, and modify contacts. The main tasks for this exercise are as follows: 1.
Open the starter project.
2.
Set the credentials for accessing the ShippingData service.
3.
Define the change interceptors.
4.
Test the ContactManagement application.
f Task 1: Open the starter project 1.
In the E:\Labfiles\Lab13\VB\Ex3\Starter or E:\Labfiles\Lab13\CS\Ex3\Starter folder, run ExSetup.bat as an administrator.
2.
Open the existing solution, ContactManagement.sln, in the E:\Labfiles\Lab13\VB\Ex3\Starter\ContactManagement or E:\Labfiles\Lab13\CS\Ex3\Starter\ContactManagement folder.
f Task 2: Set the credentials for accessing the ShippingData service 1.
Review the task list.
13-54
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
2.
Open the MainWindow file by double-clicking the TODO: Set the credentials for accessing the service using the username and password specified on the form task in the task list. This task is located in the ConnectToContext method.
3.
In the ConnectToContext method, immediately after the comment, create a new NetworkCredentials object by using the userName.Text and password.Password properties as parameters. Assign this object to the Credentials property of the context object.
4.
Save the MainWindow file.
5.
Build the solution and correct any errors.
f Task 3: Define the change interceptors 1.
Review the task list.
2.
Open the ShippingDataService file by double-clicking the TODO: Define the change interceptor task in the task list.
3.
In the ShippingDataService class, immediately after the comment, write code that defines a new Change Interceptor method for the Contacts entity called OnChangeContacts.
4.
In the OnChangeContacts method, write code that throws a DataServiceException exception if the current user is not a member of the AdventureWorksEmployees group. Return a status code of 403 and a status message of "Cannot add, modify or delete contacts".
5.
In the OnChangeContacts method, write code that performs the following tasks only when a Change update operation is performed:
6.
a.
Define a LINQ query named oldemail that queries a new AdventureWorksEntities object and retrieves the string value from the EmailAddress property of the contact that the contact object specifies.
b.
If the EmailAddress property of the contact object is not null and the value of the oldEmail object is null, set the value of the contact object's EmailPromotion property to 1.
c.
If the value of the contact object's EmailAddress property is null, set the contact object's EmailPromotion property to 0.
Build the solution and correct any errors.
Updating Data by Using WCF Data Services
13-55
f Task 4: Test the ContactManagement application 1.
Check that the ContactManagement application is set as the startup project.
2.
Start the application in Debug mode.
3.
In the Contact Management window, next to the Get Contacts button, in the box, type 1
4.
Press the TAB key, type 100 and then click Get Contacts.
5.
In the Error Fetching Contacts dialog box, click Close.
6.
In the Contact Management window, in the User Name box, type Mary
7.
In the Password box, type Pa$$w0rd
8.
Click Get Contacts.
9.
Click More to load more contacts. Repeat this until the No More Contacts message is displayed.
10. In the No More Contacts dialog box, click OK. 11. Edit an existing contact. 12. Insert a new contact. 13. Commit the changes to the database. 14. Delete a contact. 15. Commit the changes to the database. 16. Edit an existing contact. 17. Discard the changes and reload the contact data from the database. 18. Close the ContactManagement application. 19. Close the solution, and then close Visual Studio.
13-56
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Lab Review
Review Questions 1.
How do you define access rights to entity data that a WCF data service exposes?
2.
In what circumstances is a DataServiceRequestException exception thrown?
3.
At what point during the request is a change interceptor invoked?
Updating Data by Using WCF Data Services
13-57
Module Review and Takeaways
Review Questions 1.
How do the HTTP methods map to the CUD operations when you use WCF Data Services?
2.
How can you limit access to a CUD operation by role membership in a WCF data service?
3.
How do you implement transactional behavior when performing CUD operations from a WCF data service client?
Best Practices Related to Updating Data by Using WCF Data Services Supplement or modify the following best practices for your own work situations: •
Control access to your WCF data service by using the DataServiceConfiguration object.
•
Use change interceptors to add role-based security to your WCF data services.
13-58
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
•
Use batches to improve the performance of your WCF Data Services application.
•
Use asynchronous operations to improve the responsiveness of the client application.
•
If your application uses large binary objects, use binary streaming in your application.
Using ADO.NET
14-1
Module 14 Using ADO.NET Contents: Lesson 1: Retrieving and Modifying Data by Using ADO.NET Commands
14-4
Lesson 2: Retrieving and Modifying Data by Using DataSets
14-34
Lesson 3: Managing Transactions and Concurrency in Multiuser Scenarios
14-58
Lab: Using ADO.NET
14-72
14-2
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Module Overview
ADO.NET is a highly flexible framework for building applications that require access to data held in a data source. The model that ADO.NET implements enables you to build applications that are independent of the underlying data source. ADO.NET underpins most of the other data access technologies provided with the Microsoft® .NET Framework. However, it is sometimes necessary to build applications that use ADO.NET directly rather than employing some of the higherlevel technologies, such as the Entity Framework. For example, you may need to support existing solutions that predate the Entity Framework, or you may not require the additional functionality that these higher-level technologies provide. Additionally, a good understanding of ADO.NET can help you to spot areas in your applications where you may be able to boost performance. This module introduces ADO.NET and explains how you can use it to develop scalable, high-performance, data-driven applications.
Using ADO.NET
Objectives After completing this module, you will be able to: •
Retrieve and update data by using ADO.NET commands and stored procedures.
•
Retrieve and update data by using DataSet objects.
•
Implement transactions and handle concurrency exceptions.
14-3
14-4
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Lesson 1
Retrieving and Modifying Data by Using ADO.NET Commands
ADO.NET provides a powerful, but simple framework for managing data in a data source such as a database. This lesson introduces ADO.NET and describes how to connect to a database and then use ADO.NET DbCommand objects to retrieve and update data.
Objectives After completing this lesson, you will be able to: •
Explain the purpose of ADO.NET data providers.
•
Configure a data provider and connect to a data source by using a DbConnection object.
•
Execute a command by using a DbCommand object.
•
Retrieve data by using a DbDataReader object.
Using ADO.NET
•
Create, update, and delete data by using a DbCommand object.
•
Query and maintain data by using stored procedures.
•
Handle special data types, such as binary large object (BLOB) and FILESTREAM data.
14-5
14-6
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Introduction to Data Providers
Key Points An ADO.NET application communicates with a data source by using an ADO.NET data provider. A data provider provides an abstraction for the details required to connect to and use different data sources, such as databases, XML files, or Open Database Connectivity (ODBC) sources. A data provider implements a programming model that enables an application to perform common operations, such as querying, inserting, updating, and deleting data, in a manner that is relatively independent of the underlying data source. All ADO.NET technologies (such as the Entity Framework, Language-Integrated Query to Structured Query Language (LINQ to SQL), LINQ to Entities, Entity SQL, and the EntityClient provider) connect to a data source by using a data provider. Many manufacturers supply data providers that are tuned for their specific database products, such as Microsoft SQL Server®. The .NET Framework includes the following data providers, although many other vendors supply data providers that you can download and install:
Using ADO.NET
•
.NET Framework Data Provider for SQL Server
•
.NET Framework Data Provider for OLE DB
•
.NET Framework Data Provider for ODBC
•
.NET Framework Data Provider for Oracle
•
The EntityClient Provider
14-7
All data providers contain four key objects: DbConnection, DbCommand, DbDataReader, and DbDataAdapter. Manufacturers provide specific implementations of these objects to access their database products. The .NET Framework Data Provider for SQL Server is defined in the System.Data.SqlClient namespace in the System.Data.dll assembly. Question: What is the purpose of a data provider?
Additional Reading For more information about data providers in the .NET Framework, see the .NET Framework Data Providers (ADO.NET) page http://go.microsoft.com/fwlink/?LinkID=194113.
14-8
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Connecting to a Data Source by Using a DbConnection Object
Key Points The DbConnection class establishes a connection to a specified data source. You populate the ConnectionString property of a DbConnection object with information that contains data source–specific information. You can then use the Open method to connect to the data source. It is good practice to store connection strings in the application configuration file, so you can modify them without rebuilding your applications. The following code example shows a typical connection string for connecting to the AdventureWorks SQL Server database.
Using ADO.NET
14-9
Important: If you build a Web application that uses ADO.NET, you can store the connection string in the Web.config file. However, you should ensure that you encrypt this string, because it may contain confidential information, such as the user name and the password to connect to the data source. You can use the aspnet_regiis command with the –pe switch to perform this task.
The DbConnection type is an abstract class. A manufacturer provides a productspecific implementation of this class. For example, the .NET Framework Data Provider for SQL Server contains the SqlConnection class. Additionally, the SqlConnection class provides a constructor that you can use to populate the ConnectionString property. The following code example shows how to use the SqlConnection object to open a connection to a SQL Server database. [Visual Basic] Using connection As New SqlConnection(connectionString) connection.Open() End Using
[Visual C#] using (SqlConnection connection = new SqlConnection(connectionString)) { connection.Open(); }
Note: It is good practice to use a using block to automatically close a connection and free any resources used.
Database connections are valuable resources. It takes time to connect to a database. ADO.NET optimizes the use of connections with a technique known as connection pooling. When you open a new connection, ADO.NET first attempts to retrieve an existing connection from the pool. When you close a connection, ADO.NET simply returns the connection to the pool but does not close it immediately. If you connect to the database again, the connection can be reused. This reduces the number of times that a new connection must be opened. Pooling connections can significantly enhance the performance and scalability of your application.
14-10
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Note: A connection pool is created for each connection that uses the same connection string. If you modify the connection string, ADO.NET creates a new connection pool. To maintain performance and gain maximum benefit from connection pools, you should not use different connection strings to connect to the same data source in an application.
Building the Connection String Dynamically It is best practice to use static connection strings held in an application configuration file, but there may be occasions when you must construct a connection string dynamically. The syntax for the connection string depends on the data provider. To assist you, ADO.NET defines the DbConnectionStringBuilder class, which data providers can extend. The SQL Server implementation of this class, SqlConnectionStringBuilder, provides a set of SQL Server–specific properties that you can set. It also provides the ConnectionString property, which returns a connection string that contains these properties in the correct format for connecting to a SQL Server database. The following code example shows how to use the SqlConnectionStringBuilder class to create a connection string that connects to the AdventureWorks database on the local instance of SQL Server by using Windows® integrated security. [Visual Basic] Dim builder As New SqlConnectionStringBuilder builder.DataSource = ".\SQLExpress" builder.InitialCatalog = "AdventureWorks" builder.IntegratedSecurity = True Dim connectionString As String = builder.ConnectionString
[Visual C#] SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(); builder.DataSource = ".\\SQLExpress"; builder.InitialCatalog = "AdventureWorks"; builder.IntegratedSecurity = true; string connectionString = builder.ConnectionString;
Question: Why should you use a using block when you open a connection to the database by using the DbConnection object?
Using ADO.NET
14-11
Additional Reading For information about connection events, see the Connection Events (ADO.NET) page at http://go.microsoft.com/fwlink/?LinkID=194114. For more information about establishing connections, see the Establishing the Connection (ADO.NET) page at http://go.microsoft.com/fwlink/?LinkID=194115.
14-12
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Executing Commands by Using a DbCommand Object
Key Points You use the DbCommand object to execute a command against a data source. Using a DbCommand object, you can perform a query; depending on the capabilities of the data provider, you can also insert, update, and delete data and invoke a stored procedure. The CommandText property of the DbCommand class specifies the command to run and the Connection property specifies the connection to use. Alternatively, you can use the CreateCommand factory method of the DbConnection class to create a new DbCommand object that is already associated with a connection. Depending on the nature of the operation that you want to perform, you can then use one of the following methods to run the command: •
ExecuteDataReader. This method assumes that the command specifies a query. The method runs the query and returns a DbDataReader object that your application can use to iterate through the results that the query returns.
•
ExecuteScalar. This method also assumes that the command specifies a query. However, in this case, the method returns a single value from the first column in the first row that is returned. This method is useful for performing simple
Using ADO.NET
14-13
queries that, for example, count the number of rows in a table that match a specified set of criteria or calculate the average value held in a column in a table. The value is returned as an object, so you may need to cast it to an appropriate type. •
ExecuteNonQuery. This method assumes that the command is not a query; it may be a command that performs an insert, update, or delete operation or runs a stored procedure. If the command specifies an insert, update, or delete operation, this method returns the number of rows affected in the database. For all other types of commands, this method returns the value –1.
Data providers are expected to supply their own implementations of this class. For example, if you use the SQL Server data provider, you should use the SqlCommand class. The SqlCommand class provides a constructor that enables you to specify the text of the command and a connection to use. The following code example shows how to create and run a SqlCommand object to perform a simple scalar query that returns the number of rows in the Person.Contact table in the AdventureWorks database. [Visual Basic] Dim builder As New SqlConnectionStringBuilder builder.DataSource = ".\SQLExpress" builder.InitialCatalog = "AdventureWorks" builder.IntegratedSecurity = True Using connection As New SqlConnection(builder.ConnectionString) connection.Open() Dim command As New SqlCommand ("SELECT COUNT(*) FROM Person.Contact", connection) Dim numPersons As Integer = command.ExecuteScalar() ... End Using
[Visual C#] SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(); builder.DataSource = ".\\SQLExpress"; builder.InitialCatalog = "AdventureWorks"; builder.IntegratedSecurity = true; using (SqlConnection connection = new SqlConnection(builder.ConnectionString)) { connection.Open(); SqlCommand command = new SqlCommand
14-14
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
("SELECT COUNT(*) FROM Person.Contact", connection); int numPersons = (int)command.ExecuteScalar(); ... }
Using Parameters with Commands You can construct the text for a command object dynamically based on user input. However, you must be careful that you do not make your application vulnerable to SQL injection attacks. If you must include data provided by the user in a command, the preferred technique is to use command parameters. A command can contain named placeholders. At run time, you can create a DbParameter object for each placeholder and provide a value that will be substituted when the command runs. You add each DbParameter object to the Parameters collection property of the DbCommand object. Each data provider has its own implementation of the DbParameter class. For example, the .NET Framework Data Provider for SQL Server has a class called SqlParameter. The constructor for the SqlParameter type enables you to specify the name of the parameter and the value to substitute (it defines other constructors as well). The following code example shows how to use a parameter that specifies a first name to match against when you retrieve contact information from the AdventureWorks database. Note that the SqlDbType property of the SqlParameter class specifies the type of the parameter by using types defined by SQL Server; the SqlDbType enumeration is defined in the System.Data namespace. Also notice that SQL Server uses the at sign (@) to indicate a named parameter in a SQL command. [Visual Basic] ... Using connection As New SqlConnection(builder.ConnectionString) connection.Open() Dim command As New SqlCommand ("SELECT COUNT(*) FROM Person.Contact WHERE FirstName LIKE @Name", connection) Dim nameParameter = New SqlParameter("Name", "A%") nameParameter.SqlDbType = SqlDbType.NVarChar command.Parameters.Add(nameParameter) Dim numPersons As Integer = command.ExecuteScalar() ... End Using
Using ADO.NET
14-15
[Visual C#] ... using (SqlConnection connection = new SqlConnection(builder.ConnectionString)) { connection.Open(); SqlCommand command = new SqlCommand ("SELECT COUNT(*) FROM Person.Contact WHERE FirstName LIKE @Name", connection); SqlParameter nameParameter = new SqlParameter("Name", "A%"); nameParameter.SqlDbType = SqlDbType.NVarChar; command.Parameters.Add(nameParameter); int numPersons = (int)command.ExecuteScalar(); ... }
Preparing Commands When you execute a command, the database server must perform a number of tasks; for example, it must parse the command and verify that it is syntactically correct. In addition, the database server may compile the command into an intermediate form and then insert any parameter values into the specified placeholders before it runs the command. When the command finishes, the compiled version of the command may be discarded. If you run the same command again with different parameter values, the database server repeats this work. If you repeatedly run the same commands with different parameter values, you can optimize this process by requesting that the database server saves the compiled version of the command internally. If your application runs the same command again, the database server can reuse the existing compiled version of the command and simply replace the parameter values. This technique is known as preparing the command, and it can greatly enhance the performance of repeated database operations. The DbCommand class provides the Prepare method to enable this behavior for data providers that support it (for data providers that do not support prepared commands, the Prepare method is a null operation). Important: When you prepare a command, the database server allocates resources to hold the compiled version of the command. Therefore, you must specify the maximum size of each parameter before you prepare a command. If you exceed this size for any parameter value when you run the command, the parameter value will be silently truncated to this length.
14-16
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
The following code example shows how to use the Prepare method. The example modifies the value of the SqlParameter object by setting the Value property. Note that the example sets the size of the nameParameter variable to 50 before it prepares the command. This is the maximum length of the FirstName column in the Person.Contact table. [Visual Basic] ... Using connection As New SqlConnection(builder.ConnectionString) connection.Open() Dim command As New SqlCommand ("SELECT COUNT(*) FROM Person.Contact WHERE FirstName LIKE @Name", connection) Dim nameParameter = New SqlParameter("Name", "A%") nameParameter.SqlDbType = SqlDbType.NVarChar nameParameter.Size = 50 command.Parameters.Add(nameParameter) command.Prepare() Dim numPersons As Integer = command.ExecuteScalar() ... nameParameter.Value = "Br%" numPersons = command.ExecuteScalar() ... End Using
[Visual C#] ... using (SqlConnection connection = new SqlConnection(builder.ConnectionString)) { connection.Open(); SqlCommand command = new SqlCommand ("SELECT COUNT(*) FROM Person.Contact WHERE FirstName LIKE @Name", connection); SqlParameter nameParameter = new SqlParameter("Name", "A%"); nameParameter.SqlDbType = SqlDbType.NVarChar; nameParameter.Size = 50; command.Parameters.Add(nameParameter); command.Prepare(); int numPersons = (int)command.ExecuteScalar(); ... nameParameter.Value = "Br%"; numPersons = (int)command.ExecuteScalar(); ... }
Using ADO.NET
14-17
Question: If you want to perform a query to calculate the average value of a numeric column in a table, which method should you use?
Additional Reading For information about running commands asynchronously, see the Asynchronous Operations (ADO.NET) page at http://go.microsoft.com/fwlink/?LinkID=194116. For information about writing secure dynamic SQL commands, see the Writing Secure Dynamic SQL in SQL Server (ADO.NET) page at http://go.microsoft.com/fwlink/?LinkID=194117.
14-18
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Retrieving Data by Using a DbDataReader Object
Key Points The DbDataReader class retrieves a forward-only, read-only stream of data from a specific data source. Data readers offer a lightweight and efficient way to retrieve data, and they are especially good for large amounts of data because data is not cached in memory. A DbDataReader object retrieves an unbuffered stream of data from a data source that enables you to process results sequentially. The ExecuteDataReader method of a DbCommand object runs a command against a data source and then creates a DbDataReader object that you can use to retrieve the results one row at a time. Initially, the DbDataReader is logically positioned before the first row of the results. You use the Read method to advance to the first row and extract the data. You can then invoke the Read method again to move to the next row, process this data, and repeat this cycle until there are no more rows available. The Read method returns a Boolean value that indicates whether there are any more rows; when the Read method returns false, you should not attempt to read any more data from the result set.
Using ADO.NET
14-19
The DbDataReader object also provides a number of properties that you can use to examine the state of the results. These properties include: •
HasRows. This is a Boolean property that indicates whether the command returned one or more rows.
•
FieldCount. This is an integer field that returns the number of fields in the current row.
•
RecordsAffected. This is an integer field that indicates the number of rows affected if the command was an insert, update, or delete operation.
You extract the data from a row by using one of the GetXXX methods of the DbDataReader object. There is a GetXXX method for each of the common data types supported by the .NET Framework; for example, the GetInt32 method returns a value as a 32-bit integer and the GetString method returns a value as a string. Each of the GetXXX methods expects a parameter that indicates which column to extract the data from in the current row. The columns are numbered starting with column 0. The SQL Server data provider includes an implementation of the DbDataReader class called SqlDataReader. This class includes additional GetSqlXXX methods to retrieve data by using the types that are available with SQL Server. The result set generated by a DbDataReader object can consume considerable resources, so when you have finished with a DbDataReader object, you should close it to release these resources. Use the Close method of the DbDataReader class. The following code example shows how to perform a query that retrieves information from the Person.Contact table in the AdventureWorks database and displays the results. [Visual Basic] Dim builder As New SqlConnectionStringBuilder builder.DataSource = ".\SQLExpress" builder.InitialCatalog = "AdventureWorks" builder.IntegratedSecurity = True Using connection As New SqlConnection(builder.ConnectionString) connection.Open() Dim command As New SqlCommand("SELECT ContactId, FirstName, LastName, ModifiedDate FROM Person.Contact", connection) Dim personReader As SqlDataReader = command.ExecuteReader() While personReader.Read() Dim contactId = personReader.GetInt32(0)
14-20
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Dim firstName = personReader.GetString(1) Dim lastName = personReader.GetString(2) Dim modifiedDate = personReader.GetDateTime(3) Console.WriteLine("{0} {1} {2} {3}", contactId, firstName, lastName, modifiedDate) End While personReader.Close() End Using
[Visual C#] SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(); builder.DataSource = ".\\SQLExpress"; builder.InitialCatalog = "AdventureWorks"; builder.IntegratedSecurity = true; using (SqlConnection connection = new SqlConnection(builder.ConnectionString)) { connection.Open(); SqlCommand command = new SqlCommand("SELECT ContactId, FirstName, LastName, ModifiedDate FROM Person.Contact", connection); SqlDataReader personReader = command.ExecuteReader(); while (personReader.Read()) { int contactId = personReader.GetInt32(0); string firstName = personReader.GetString(1); string lastName = personReader.GetString(2); DateTime modifiedDate = personReader.GetDateTime(3); Console.WriteLine("{0} {1} {2} {3}", contactId, firstName, lastName, modifiedDate); } personReader.Close(); }
Handling Exceptions When Retrieving Data The SQL Server data provider uses delayed construction for the majority of its types. This means that if there are problems with the data that is used to initialize an object, the error may occur at a place other than where you specified the data for the object. For example, when you create a SqlConnection object, you specify a connection string that contains properties that provide information to SQL Server
Using ADO.NET
14-21
about the connection you want to make. You do not know whether there is an error in the connection string until you call the SqlConnection.Open method; this is the point at which the connection string is passed to SQL Server. Similarly, when you construct a command or initialize the CommandText property, the command is not checked until SQL Server physically attempts to prepare it. Furthermore, the command is not executed when you call the ExecuteReader method but is instead executed only when you call the Read method for the first time. Therefore, any exception handling code that is designed to catch malformed commands should occur at the point where the Read method is called. The ExecuteReader method throws either an InvalidOperationException exception object or a SqlException exception object. An InvalidOperationException exception object is thrown if the connection is closed or if the command contains an invalid command (as opposed to a syntax error). The SqlException exception object is thrown if the data source returns errors while executing a command. Each SqlException exception object contains a collection of SqlError objects that you can use to obtain information about the cause of the error.
Handling Multiple Result Sets Some data sources, such as SQL Server, enable you to execute a DbCommand object that specifies a batch of commands. In this case, when you have reached the end of one result set, you can move to the next by using the NextResult method. This method returns true if there is another result set available or false otherwise. Note: To use multiple result sets with SQL Server, you must enable multiple active result sets (MARS) in the connection string when you connect to the database server. You can include the string MultipleActiveResultSets=True in the connection string, or if you use the SqlConnectionStringBuilder class to construct your connection string, you can set the MultipleActiveResultSets property to true.
Question: How can you determine how many rows a query will return?
Additional Reading For more information about working with DataReader objects, see the Retrieving Data Using a DataReader (ADO.NET) page at http://go.microsoft.com/fwlink/?LinkID=194118.
14-22
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Implementing Create, Update, and Delete Operations by Using a DbCommand Object
Key Points You use the DbCommand.ExecuteNonQuery method to perform create, update, and delete operations against a data source. If you use a data source, such as SQL Server, the SQL command can be the text of a SQL INSERT, UPDATE, or DELETE statement or the name of a stored procedure. The parameters that are passed to the DbCommand constructor specify the query and the connection to use. Note: You can use the ExecuteNonQuery method to run any valid command against a data source. If you use a database such as SQL Server, you can use the ExecuteNonQuery method to perform operations such as CREATE TABLE, DROP TABLE, or even CREATE DATABASE.
A command that you perform by using the ExecuteNonQuery method can be parameterized, and if you repeatedly perform the same operation on the same table but with different data, you can improve performance by using a prepared command.
Using ADO.NET
14-23
The return value of the ExecuteNonQuery method is an integer that indicates the number of rows affected by the command if it is an insert, update, or delete operation. If it is some other type of command, such as a stored procedure, the method returns –1 (this includes stored procedures that insert, update, or delete data). The following code example shows how you can use parameters with a SqlCommand object to insert a new product into the Product table in the AdventureWorks database. [Visual Basic] Using connection As New SqlConnection(builder.ConnectionString) Dim command As SqlCommand = connection.CreateCommand() command.CommandText = "INSERT INTO Product (ProductNumber, ProductName, Color)" + "VALUES(@ProductNumber,@ProductName, @Color)" command.Parameters.Add("@ProductNumber", SqlDbType.NVarChar, 50) command.Parameters.Add("@ProductName", SqlDbType.NVarChar, 50) command.Parameters.Add("@Color", SqlDbType.NVarChar, 15) command.Parameters("@ProductNumber").Value = product.ProductNumber command.Parameters("@ProductName").Value = product.Name command.Parameters("@Color").Value = product.Color connection.Open() Try Dim rows As Integer = command.ExecuteNonQuery() ... Catch sqlEx As SqlException ... End Try End Using
[Visual C#] using (sqlConnection connection = new SqlConnection(databaseConnectionString)) { SqlCommand command = connection.CreateCommand(); command.CommandText = "INSERT INTO Product (ProductNumber, ProductName, Color)" + "VALUES(@ProductNumber,@ProductName, @Color)"; command.Parameters.Add("@ProductNumber", SqlDbType.NVarChar, 50);
14-24
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
command.Parameters.Add("@ProductName", SqlDbType.NVarChar, 50); command.Parameters.Add("@Color", SqlDbType.NVarChar, 15); command.Parameters["@ProductNumber"].Value = product.ProductNumber; command.Parameters["@ProductName"].Value = product.Name; command.Parameters["@Color"].Value = product.Color; connection.Open(); try { int rows = command.ExecuteNonQuery(); ... } catch (SqlException sqlEx) { ... } }
Returning Data from a Command Although the ExecuteNonQuery method does not return any rows, it can return data as output parameters or return values. This feature is primarily useful if you call a stored procedure that returns data in this form. You indicate that a parameter is an output parameter by setting the ParameterDirection property to ParameterDirection.Output or ParameterDirection.ReturnValue. This feature is described in more detail in the next topic. Question: A stored procedure updates 500 rows in a database table. What value is returned by the ExecuteNonQuery method if it is used to invoke this stored procedure?
Using ADO.NET
14-25
Querying and Maintaining Data by Using Stored Procedures
Key Points Stored procedures offer many benefits, such as improved performance and increased security, maintainability, and abstraction, and you should use them whenever possible. The data providers included with ADO.NET make it easy for you to use stored procedures to perform queries and encapsulate complex insert, update, and delete operations that can span multiple tables. You can also define stored procedures that perform other operations, such as archiving data or some other business operation.
Querying Data by Using a Stored Procedure You can define stored procedures that perform query operations, and you can invoke these stored procedures in the same way as you run a query by using a DbDataReader object. The principle difference is that you specify the name of the stored procedure in the DbCommand object, and you should also set the CommandType property to CommandType.StoredProcedure.
14-26
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
In the following code example, the AdventureWorks database contains a stored procedure called uspGetManagerEmployees. This stored procedure takes a parameter called ManagerID that specifies the employee ID of a manager and returns a list of employees managed directly or indirectly by that manager. The following code example shows how to run this stored procedure and find all of the employees managed by employee 148. [Visual Basic] Using connection As New SqlConnection(connectionString) connection.Open() Dim command As New SqlCommand("uspGetManagerEmployees", connection) command.CommandType = CommandType.StoredProcedure Dim employeeIdParameter As New SqlParameter("ManagerID", 148) command.Parameters.Add(employeeIdParameter) Dim employeeReader As SqlDataReader = command.ExecuteReader() While employeeReader.Read() Dim employeeId As Integer = employeeReader.GetInt32(4) Dim firstName As String = employeeReader.GetString(5) Dim lastName As String = employeeReader.GetString(6) Console.WriteLine("{0} {1} {2}", employeeId, firstName, lastName) End While employeeReader.Close() End Using
[Visual C#] using (SqlConnection connection = new SqlConnection(connectionString)) { connection.Open(); SqlCommand command = new SqlCommand("uspGetManagerEmployees", connection); command.CommandType = CommandType.StoredProcedure; SqlParameter employeeIdParameter = new SqlParameter("ManagerID", 148); command.Parameters.Add(employeeIdParameter); SqlDataReader employeeReader = command.ExecuteReader();
Using ADO.NET
14-27
while (employeeReader.Read()) { int employeeId = employeeReader.GetInt32(4); string firstName = employeeReader.GetString(5); string lastName = employeeReader.GetString(6); Console.WriteLine("{0} {1} {2}", employeeId, firstName, lastName); } employeeReader.Close(); }
Invoking a Stored Procedures with Output Parameters Stored procedures can also return data in the form of output parameters or return values. This is common for stored procedures that perform a business operation and must indicate whether the operation was successful. As an example, the AdventureWorks database contains a stored procedure named uspLogError. This stored procedure records error information in a table called ErrorLog in the database and is used by many of the other stored procedures and triggers in the database. It provides an output parameter called ErrorLogID that contains the ID of the error message that is added to the ErrorLog table. The following code example shows how to invoke this stored procedure and catch the value passed back in the ErrorLogID output parameter. Notice that when you define an output parameter, you should specify the data type of the parameter. You can retrieve the value of the output parameter by using the SqlValue property. [Visual Basic] Using connection As New SqlConnection(builder.ConnectionString) connection.Open() Dim command As New SqlCommand("uspLogError", connection) command.CommandType = CommandType.StoredProcedure Dim errorLogIdParameter As New SqlParameter("@ErrorLogID", SqlDbType.Int) errorLogIdParameter.Direction = ParameterDirection.Output command.Parameters.Add(errorLogIdParameter) command.ExecuteNonQuery() Console.WriteLine("Value returned: {0}", errorLogIdParameter.SqlValue) End Using
14-28
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
[Visual C#] using (SqlConnection connection = new SqlConnection(builder.ConnectionString)) { connection.Open(); SqlCommand command = new SqlCommand("uspLogError", connection); command.CommandType = CommandType.StoredProcedure; SqlParameter errorLogIdParameter = new SqlParameter("@ErrorLogID", SqlDbType.Int); errorLogIdParameter.Direction = ParameterDirection.Output; command.Parameters.Add(errorLogIdParameter); command.ExecuteNonQuery(); Console.WriteLine("Value returned: {0}", errorLogIdParameter.SqlValue); }
Note: When you use SQL Server, ensure that your stored procedures do not use SET NOCOUNT ON. This will cause the returned affected row count to be zero, resulting in possible concurrency errors. Note: If you use the ExecuteReader method to run a stored procedure, any return values and output parameters are not accessible until the DataReader object is closed.
Question: What steps must you take to invoke a stored procedure by using a DbCommand object?
Additional Reading For more information about working with command parameters, see the Configuring Parameters and Parameter Data Types (ADO.NET) page at http://go.microsoft.com/fwlink/?LinkID=194119.
Using ADO.NET
14-29
Querying BLOB and FILESTREAM Data
Key Points Some data sources support BLOBs. BLOB data can be extremely large; up to several gigabytes (GB) with SQL Server. You can indicate that a column in a SQL Server table can hold BLOB data by using the varbinary(max) data type. However, you may not want to store the data for large columns associated with a table directly inside a database. For example, you may prefer to store this information in a separate set of files that are independent of the database to avoid possible fragmentation issues with the database, to take advantage of any caching provided by the file system to support fast read access, or to accommodate data that exceeds the current 2-GB limitation of the varbinary(max) type. To handle these situations, SQL Server 2008 provides the FILESTREAM modifier for the varbinary(max) data type. When you designate a varbinary(max) column as a FILESTREAM column, the data for the column is stored in a separate file rather than the database, and you can store up to 8 GB of data. SQL Server creates and maintains this file, but you can specify where SQL Server should store this file by defining a FILESTREAM filegroup for the database.
14-30
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Retrieving Large Data When you execute a query against a database, under normal circumstances, the DbDataReader object loads incoming data as a row as soon as the entire row is available. However, if a row contains a BLOB column, it may not be feasible to buffer this amount of data in memory, so you must handle it differently. If a command needs to query a BLOB column, you must modify the behavior of the DbDataReader object to retrieve data sequentially rather than by buffering each row. You achieve this by specifying the value CommandBehavior.SequentialAccess as a parameter to the ExecuteReader method. Rather than loading the entire row, sequential access enables the DbDataReader object to load data as a stream. You can then use the GetBytes or GetChars methods to read the data for any BLOB columns in chunks; you specify a byte location to start the read operation for a BLOB column, a buffer to store data, and the amount of data to read for the initial chunk. Subsequent calls to the GetBytes or GetChars methods can read the remaining chunks, until you have processed the entire column. The default behavior of the DbDataReader class enables you to access returned fields in any order until the next row is read. However, when you perform sequential access, you must read from the columns in the order they are returned, although you are not required to read each column. After you have read past a location in the returned stream of data, data at or before that location can no longer be read from the DbDataReader object. The following code example illustrates how to retrieve data sequentially from a SQL Server database by using a SqlDataReader object. The productLogo column in the Products table is a BLOB column. The code uses the GetBytes method to read the data from the productLogo column into a byte array called buffer. The data is read and processed in chunks of 10,000 bytes. [Visual Basic] ... Using connection As New SqlConnection(connectionString) Dim command As New SqlCommand( "SELECT productID, productLogo FROM products", connection) Dim Dim Dim Dim Dim
bufferSize As Integer = 10000 buffer(bufferSize) As Byte returnval As Long startIndex As Long = 0 prodID As String = ""
Using ADO.NET
14-31
connection.Open() Dim reader As SqlDataReader = command.ExecuteReader( CommandBehavior.SequentialAccess) While reader.Read() prodID = reader.GetString(0) ' Get the first chunk of BLOB data for this row. startIndex = 0 returnval = reader.GetBytes(1, startIndex, buffer, 0, bufferSize) While returnval = bufferSize ' Process the data in buffer. ... ' Get the next chunk of BLOB data. startIndex += bufferSize returnval = reader.GetBytes(1, startIndex, buffer, 0, bufferSize) End While End While reader.Close() End Using
[Visual C#] ... using (SqlConnection connection = new SqlConnection(connectionString)) { SqlCommand command = new SqlCommand( "SELECT productID, productLogo FROM products", connection); int bufferSize = 10000; byte[] buffer = new byte[bufferSize]; long returnval; long startIndex = 0; string prodID = ""; connection.Open(); SqlDataReader reader = command.ExecuteReader(
14-32
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
CommandBehavior.SequentialAccess); while (reader.Read()) { prodID = reader.GetString(0); // Get the first chunk of BLOB data for this row. startIndex = 0; returnval = reader.GetBytes(1, startIndex, buffer, 0, bufferSize); while (returnval == bufferSize) { // Process the data in buffer. ... // Get the next chunk of BLOB data. startIndex += bufferSize; returnval = reader.GetBytes(1, startIndex, buffer, 0, bufferSize); } } reader.Close(); }
Writing Large Data You can use a SqlCommand object to create a command that inserts or updates large object data in a SQL Server 2008 database. You should pass the large object data as parameters to the command when you execute it. You can insert large objects (BLOBs) and character large objects (CLOBs) into a database by using the following steps: 1.
Create a SqlCommand object with the appropriate SQL INSERT or UPDATE command.
2.
Pass the BLOB data as a parameter to the SqlCommand object, ensuring that you use the appropriate type.
Before you can pass these parameters, you must read the data, such as an image, into a suitable format that can be handled by ADO.NET. For example, you can write an image such as a GIF or JPEG as a stream of bytes, or you can write a document as a stream of characters. After you have filled the parameters, you can execute the command and upload the data to the database.
Using ADO.NET
14-33
Question: Is the order in which you read columns from a SqlDataReader object important when a result set contains BLOB data?
14-34
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Lesson 2
Retrieving and Modifying Data by Using DataSets
ADO.NET commands enable you to query and manage data, but sometimes you must be able to cache data in memory while maintaining the relational structure of this data. In this lesson you will learn how to use DataSet objects to achieve this goal. A DataSet object is a memory-resident representation of your data source and represents a complete set of data, including tables, constraints, and relationships. DataSet objects are useful if you need to support disconnected, distributed data scenarios.
Objectives After completing this lesson, you will be able to: •
Describe the DataSet object model.
•
Use DataSet objects to query data.
Using ADO.NET
•
Create a typed DataSet object.
•
Modify data by using a DataSet object.
•
Save changes made to data in a DataSet object back to the database.
14-35
14-36
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
What Is a DataSet Object?
Key Points A DataSet object provides an in-memory cache of data. The data in a DataSet object is held in an XML format that is independent of any underlying data source. The DataSet object enables an application to operate in a disconnected manner; an application can connect to a database, populate a DataSet object, and then disconnect. The application can then modify the data in the DataSet object and save the changes back to the database. When the data is saved, the application can reconnect to the database, store the changes, and then disconnect again.
The Structure of a DataSet Object A DataSet object can mirror the relational structure of tables in a database. A DataSet object contains one or more tables, each represented by a DataTable object, accessible through the Tables property of the DataSet object. A DataTable object typically holds data retrieved from a single table in a database. When you fill a DataSet object, each of the DataTable objects in the DataSet object is populated with data from the corresponding table in the database. A DataTable object contains zero or more rows, each represented by a DataRow object and accessible through the Rows property of the DataTable object. Each
Using ADO.NET
14-37
DataRow object can store both the original data for the row and the current data in the row if the application has modified it. When the changes in a DataSet object are saved back to the database, the DataSet object can use this information to generate the appropriate statement to update the corresponding row in the database. A DataRow object also stores information that indicates whether the row has been marked for deletion and should be removed from the database when changes are saved back to the database or is a new row that has just been created and should be inserted when changes are saved. A DataRow object contains the data for one or more columns. A column is modeled by a DataColumn object that holds meta-information about the underlying column in the database, such as its name and type. The DataTable object provides access to the DataColumn objects for the table through the Columns collection. You can define relationships between DataTable objects in a DataSet object by defining DataRelation objects Each DataRelation object relates one or more of the parent columns in one table to connected column(s) in a child table. The Relations collection property in a DataSet object contains the DataRelation objects defined for that DataSet object. A DataTable object can act as a binding source for Windows and Web client applications. UI controls, such as the Windows Presentation Foundation (WPF) DataGrid control, can bind to a DataSet object and display the information held in the DataTable objects in that DataSet object. However, sometimes you must customize the way in which data is displayed. For example, you may want to display only certain columns, display related data held in two or more DataTable objects, or perhaps just display aggregated summary information about the data in a DataTable object. To support this functionality, you can define a DataView object as part of a DataSet object and use it as a binding source for UI controls. A DataView object is similar to a view in a relational database; it does not hold any data of its own, but it can present data joined from one or more DataTable objects based on a query that you define.
Using DataSet Objects vs. DbDataReader Objects Deciding whether your application should use a DataSet object depends largely on the type of functionality your application requires. You should always use DataSet objects if your application requires any of the following features: •
Caching data locally so that you can manipulate it.
•
Passing data between tiers if you are using an XML Web service.
14-38
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
•
Dynamically binding data to UI controls, such as in WPF.
•
Combining data from multiple data sources.
•
Performing extensive processing on data without requiring an open connection to the data source.
A DataSet object can consume a considerable amount of memory, depending on the amount of data that you allow users to fetch, and it may require additional processing resources compared to a DbDataReader object. Therefore, if you do not require the functionality of a DataSet object, you should use a DbDataReader object, because this may significantly improve the performance of your application. Question: Identify an advantage of using a DataSet object.
Additional Reading For more information about creating DataRelation objects, see the Adding DataRelations (ADO.NET) page at http://go.microsoft.com/fwlink/?LinkID=194120. For more information about navigating data by using DataRelation objects, see the Navigating DataRelations (ADO.NET) page http://go.microsoft.com/fwlink/?LinkID=194121. For more information about the DataView class, see the DataViews (ADO.NET) page at http://go.microsoft.com/fwlink/?LinkID=194122.
Using ADO.NET
14-39
Populating a DataSet Object and Accessing Data
Key Points To populate a DataSet object, you use a DbDataAdapter object. A data adapter acts as a bridge between a DataSet object and the database. A data adapter is associated with commands that can query, insert, update, and delete data in a data source. It is also associated with a connection to that data source. Behind the scenes, a data adapter uses DbConnection, DbCommand, and DbDataReader objects. The DbDataAdapter class provides the Fill method that connects to the data source, issues the command to query data, reads the data by using a DbDataReader object, constructs the DataTable objects in a database, populates the DataRow and DataColumn collections for each DataTable object, and then disconnects from the database. You can then iterate through the Tables collection in a DataSet object and access the data in each row from the Rows collection. You can also obtain the metadata that describes the columns from the Columns collection.
14-40
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Note: If the connection that a data adapter uses is closed when the Fill method is called, the Fill method opens the connection and then closes it when the method finishes. If the connection is open when the Fill method is called, the connection is left open when the Fill method completes.
If you use SQL Server, you can employ an optimized version of the DbDataAdapter class called SqlDataAdapter. The following code example shows how to use a SqlDataAdapter object to fetch data from the Person.Contacts table in the AdventureWorks database into a DataSet object and then display the information retrieved. Notice that the Tables, Rows, and Columns collections provide indexers that enable you to access items in these collections either by index number or by using a string literal that specifies the name of the corresponding table or column in the original SQL SELECT statement. Additionally, you should notice that you no longer need to open or close the connection object; the DataAdapter object manages the connection for you automatically. [Visual Basic] Using connection As New SqlConnection(connectionString) Dim adapter As New SqlDataAdapter( _ "SELECT ContactID, FirstName, LastName FROM Person.Contact", connection) Dim contactDS As New DataSet() adapter.Fill(contactDS) For Each row As DataRow In contactDS.Tables(0).Rows Console.WriteLine("{0}: {1}" + vbNewLine + "{2}: {3}" + vbNewLine + "{4}: {5}" + vbNewLine, contactDS.Tables(0).Columns(0).ColumnName, row(0), contactDS.Tables(0).Columns(1).ColumnName, row(1), contactDS.Tables(0).Columns(2).ColumnName, row(2)) Next End Using
[Visual C#] using (SqlConnection connection = new SqlConnection(connectionString)) { SqlDataAdapter adapter = new SqlDataAdapter(
Using ADO.NET
14-41
"SELECT ContactID, FirstName, LastName FROM Person.Contact", connection); DataSet contactDS = new DataSet(); adapter.Fill(contactDS); foreach (DataRow row in contactDS.Tables[0].Rows) { Console.WriteLine("{0}: {1}\n{2}: {3}\n{4}: {5}\n", contactDS.Tables[0].Columns[0].ColumnName, row[0], contactDS.Tables[0].Columns[1].ColumnName, row[1], contactDS.Tables[0].Columns[2].ColumnName, row[2]); } }
Performing LINQ to DataSet Queries The previous examples have shown how to access data in a DataSet object by iterating through the Rows collection in a DataTable object. DataSet objects also support a form of LINQ called LINQ to DataSet that you can use to retrieve data. The DataTable class provides the AsEnumerable extension method, which generates an enumerable collection of rows. You can use the generic Field extension method defined for the DataRow type to specify criteria to match. The type parameter is the type of the column. The following code example shows how to use LINQ to DataSet to find all contacts with a last name of Smith. [Visual Basic] Using connection As New SqlConnection(builder.ConnectionString) Dim adapter As New SqlDataAdapter ( _ "SELECT ContactID, FirstName, LastName FROM Person.Contact", connection) Dim contactDS As New DataSet() adapter.Fill(contactDS) Dim contactsNamedSmith = _ From ct In contactDS.Tables(0).AsEnumerable() _ Where ct.Field(Of String)(2) = "Smith" _ Select ct For Each contact In contactsNamedSmith Console.WriteLine("{0}: {1}" + vbNewLine + "{2}: {3}" + vbNewLine + "{4}: {5}" + vbNewLine,
14-42
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
contactDS.Tables(0).Columns(0).ColumnName, contact(0), contactDS.Tables(0).Columns(1).ColumnName, contact(1), contactDS.Tables(0).Columns(2).ColumnName, contact(2)) Next End Using
[Visual C#] using (SqlConnection connection = new SqlConnection(builder.ConnectionString)) { SqlDataAdapter adapter = new SqlDataAdapter( "SELECT ContactID, FirstName, LastName FROM Person.Contact", connection); DataSet contactDS = new DataSet(); adapter.Fill(contactDS); var contactsNamedSmith = from ct in contactDS.Tables[0].AsEnumerable() where ct.Field(2) == "Smith" select ct; foreach (var contact in contactsNamedSmith) { Console.WriteLine("{0}: {1}\n{2}: {3}\n{4}: {5}\n", contactDS.Tables[0].Columns[0].ColumnName, contact[0], contactDS.Tables[0].Columns[1].ColumnName, contact[1], contactDS.Tables[0].Columns[2].ColumnName, contact[2]); } }
Question: Do you need to open the database connection before you use the Fill method of a DataAdapter object?
Additional Reading For more information about LINQ to DataSet, see the LINQ to DataSet page at http://go.microsoft.com/fwlink/?LinkID=194123. For more information about the DataTablesReader class, see the DataTablesReader (ADO.NET) page at http://go.microsoft.com/fwlink/?LinkID=194124.
Using ADO.NET
14-43
Using Typed and Untyped DataSet Classes
Key Points The default System.Data.DataSet class is an untyped DataSet class. You can easily create and populate instances of this type, but to access tables and columns in an untyped DataSet object, you must specify the names of tables and columns as string literals or by using a numeric index. If you specify an invalid index or string literal name, a run-time exception occurs. Microsoft Visual Studio® provides tools that enable you to create typed DataSet classes. A typed DataSet class contains nested properties and classes that provide type-safe access to the tables and columns. If you specify an invalid table name or column name, you obtain a compile-time error instead of a run-time exception. It is recommended that you use typed DataSet objects wherever possible. The following code example performs the same function as the previous example, except that it uses a typed DataSet class generated by using the Data Source Configuration Wizard provided with Visual Studio. Notice that the wizard generates a typed data adapter called ContactTableAdapter that has the command for querying the Person.Contact table built in. The Fill method of this adapter expects a typed DataTable object called ContactDataTable that has columns
14-44
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
based on the definition of the Contact.Person table. The type of rows in this table is ContactRow, and you can access the items in the Columns and Rows collections by specifying properties named after each column in the underlying database table. [Visual Basic] Using connection As New SqlConnection(connectionString) Dim adapter As New ContactsTableAdapters.ContactTableAdapter() Dim contactTable As New Contacts.ContactDataTable() adapter.Fill(contactTable) For Each row As Contacts.ContactRow In contactTable.Rows Console.WriteLine("{0}: {1}" + vbNewLine + "{2}: {3}" + vbNewLine + "{4}: {5}" + vbNewLine, contactTable.ContactIDColumn.ColumnName, row.ContactID, contactTable.FirstNameColumn.ColumnName, row.FirstName, contactTable.LastNameColumn.ColumnName, row.LastName) Next End Using
[Visual C#] using (SqlConnection connection = new SqlConnection(connectionString)) { ContactsTableAdapters.ContactTableAdapter adapter = new ContactsTableAdapters.ContactTableAdapter(); Contacts.ContactDataTable contactTable = new Contacts.ContactDataTable(); adapter.Fill(contactTable); foreach (Contacts.ContactRow row in contactTable.Rows) { Console.WriteLine("{0}: {1}\n{2}: {3}\n{4}: {5}\n", contactTable.ContactIDColumn.ColumnName, row.ContactID, contactTable.FirstNameColumn.ColumnName, row.FirstName, contactTable.LastNameColumn.ColumnName, row.LastName); } }
A DataTable class in a typed DataSet class also provides a method that enables you to retrieve rows by specifying a value for the primary key. This method is called FindByXXX, where XXX is the name of the primary key column in the database. The method returns the data as a typed DataRow object.
Using ADO.NET
14-45
Additionally, a typed DataTable class provides a Select method that you can use to retrieve all rows that match criteria specified as a string. The rows are returned as an untyped DataRow collection. The following code example shows how to use these methods with a typed DataSet class called Contacts to retrieve the data for the contact with an ID of 99 and all contacts with a last name of Smith. [Visual Basic] Dim contact As Contacts.ContactRow = contactTable.FindByContactID(99) Dim contactsNamedSmith() As DataRow = _ contactTable.Select("LastName = 'Smith'")
[Visual C#] Contacts.ContactRow contact = contactTable.FindByContactID(99); DataRow []contactsNamedSmith = contactTable.Select("LastName = 'Smith'");
Creating a Typed DataSet Class You can create a typed DataSet class in two ways: •
You can use the Data Source Configuration Wizard in Visual Studio. Visual Studio provides a DataSet template that generates the code for a typed DataSet class. When you define a DataSet class by using this template, you can use the Dataset Designer to graphically select a database, identity the tables, and select the columns that you want to use. You can also specify relationships between tables and define views based on queries and stored procedures.
•
You can use the XSD.EXE command-line tool. DataSet classes have an associated and well-defined XML schema that defines their structure. If you have an XML file that specifies the structure of a DataSet, you can use the XSD tool to generate the code for types that implement this structure. You can then add these types to your project. The following command-line example shows how to create a typed DataSet class by using the XSD tool. The /d switch specifies that the tool should generate code for a typed DataSet class, the /l flag specifies the source language for the code (Microsoft Visual C#® in this case), and SchemaFileName.xsd is the name of the file that contains the XML definition of the DataSet. The /eld flag specifies that the DataSet class should support LINQ to DataSet queries, and the /n flag specifies the namespace for the types that are generated. xsd.exe /d /l:CS SchemaFileName.xsd /eld /n:Schema.Namespace
14-46
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Question: Why should you always use typed DataSet classes if possible?
Using ADO.NET
14-47
Creating, Modifying, and Deleting Data by Using a DataSet Object
Key Points Creating, modifying, and deleting data by using a DataSet object is a two-step process: 1.
You add, update, or delete one or more DataRow objects in DataTable objects in the DataSet object.
2.
You use a data adapter to propagate all of the changes made to a DataSet object to the database as a batch.
Adding Rows to a DataSet To add a row to a DataSet object, you create a new DataRow object, populate it with the information required, and then add it to the DataRows collection for the appropriate DataTable object. To create a new row for an untyped DataSet object, use the NewRow method of the DataTable object that you want to insert the row into. The NewRow method
14-48
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
creates a new DataRow object that has the columns defined by the DataTable class. You can then populate this DataRow object with the data for the new row and add it to the DataRows collection for the DataTable object. To store data in an untyped DataRow object, you can either use indexer notation, or you can use the generic SetField method and specify the type of data in the row as the type parameter. The SetField method expects you to specify the field (as an index into the DataRow object, as a DataColumn object, or by using a string literal that contains the name of the field) and the value to assign to the field. Note: When you call the NewRow method, the TableNewRow event fires. You can use this method to populate the new row with default values before the NewRow method returns. When you add the new row to the Rows collection in the DataTable object, the RowChanging and RowChanged events fire. You can use these events to validate the data in the new row.
The following code example shows how to add a new row that contains details of a new contact to an untyped DataSet object. [Visual Basic] Dim contactDS As New DataSet() ... Dim newContact As DataRow = contactDS.Tables(0).NewRow() ' Indexer notation. Column 0 is the ContactID newContact(0) = 101 ' SetField with the name of the column newContact.SetField(Of String)("FirstName", "Paul") ' SetField with the index of the LastName column newContact.SetField(Of String)(2, "Alcorn")
[Visual C#] DataSet contactDS = new DataSet(); ... DataRow newContact = contactDS.Tables[0].NewRow(); // Indexer notation. Column 0 is the ContactID newContact[0] = 101;
Using ADO.NET
14-49
// SetField with the name of the column newContact.SetField("FirstName", "Paul"); // SetField with the index of the LastName column newContact.SetField(2, "Alcorn"); contactDS.Tables[0].Rows.Add(newContact);
DataTable objects in a typed DataSet object provide typed versions of the NewRow method, which generates a typed version of the DataRow object for the DataTable object. The names of the typed NewRow method and the typed DataRow class are based on the name of the table in the underlying database. For example, if you create a typed DataSet for the Contact table in the SQL Server AdventureWorks database, the NewRow method is called NewContactRow, and the DataRow type is called Contacts.ContactRow. Note: A typed DataTable class has typed versions of the RowChanging and RowChanged events. For example, in the typed Contacts DataSet class, these events are called ContactRowChanging and ContactRowChanged.
The following code example shows how to add a new row to the ContactDataTable object in the typed Contacts DataSet object. [Visual Basic] Dim contactTable As New Contacts.ContactDataTable() ... Dim newContact As Contacts.ContactRow = contactTable.NewContactRow() newContact.ContactID = 101 newContact.FirstName = "Paul" newContact.LastName = "Alcorn" contactTable.AddContactRow(newContact)
[Visual C#] Contacts.ContactDataTable contactTable = new Contacts.ContactDataTable(); ... Contacts.ContactRow newContact = contactTable.NewContactRow(); newContact.ContactID = 101; newContact.FirstName = "Paul"; newContact.LastName = "Alcorn"; contactTable.AddContactRow(newContact);
14-50
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
When you add a new row to a DataTable object, the RowState property of the row is set to Added. The DataAdapter class uses this information to generate an appropriate command to insert data into the database when you save the changes later.
Modifying Rows in a DataSet object To modify a row in an untyped DataSet object, find the DataRow object that corresponds to the row that you want to modify, and then make the necessary changes to the DataRow object. You can use indexer notation or the SetField method to assign new values to fields in a DataRow object. The following code example shows how to use a LINQ to DataSet query to locate the row for the contact with an ID of 99 in the typed and untyped DataSet objects used by the previous examples and then change the name of this contact. [Visual Basic] 'Untyped DataSet Dim contactDS As New DataSet() ... Dim contact99 As DataRow = _ (From ct In contactDS.Tables(0).AsEnumerable() Where ct.Field(Of Integer)(0) = 99 Select ct).First() contact99.SetField(Of String)("FirstName", "Amy") contact99.SetField(Of String)("LastName", "Alberts") 'Typed DataSet Dim contactTable As New Contacts.ContactDataTable() ... Dim contact99 = (From ct2 In contactTable Where ct2.ContactID = 98 Select ct2).First() contact99.FirstName = "Amy" contact99.LastName = "Albert"
[Visual C#] 'Untyped DatsSet DataSet contactDS = new DataSet(); ... DataRow contact99 = (from ct in contactDS.Tables[0].AsEnumerable()
Using ADO.NET
14-51
where ct.Field(0) == 99 select ct).First(); contact99.SetField("FirstName", "Amy"); contact99.SetField("LastName", "Alberts"); 'Typed DataSet Contacts.ContactDataTable contactTable = new Contacts.ContactDataTable(); ... var contact99 = (from ct2 in contactTable where ct2.ContactID == 98 select ct2).First(); contact99.FirstName = "Amy"; contact99.LastName = "Alberts";
Note: When you modify the data in an existing DataRow object that already belongs to the Rows collection in a DataTable object, the RowChanging and RowChanged events fire. If the DataTable object is a typed DataTable object, the corresponding typed events fire.
When you add a new row to a DataTable object, the RowState property of the row is set to Modified, and a copy of the original data for the row is stored with the DataRow object. The DataAdapter class uses this information to generate an appropriate command to update data in the database.
Deleting Rows from a DataSet Object To delete a row from an untyped DataSet object, use the Remove method of the Rows collection and specify the DataRow object to remove. To delete a row from a typed DataSet object, use the typed RemoveXXXRow method (where XXX is the name of the underlying database table) of the typed DataTable object, and specify the typed DataRow object to remove. The following code example shows how to delete the row for contact with an ID of 99 in the typed and untyped DataSet objects used by the previous code examples. [Visual Basic] 'Untyped DataSet Dim contactDS As New DataSet() ... Dim contact99 As DataRow = ...
14-52
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
contactDS.Tables(0).Rows.Remove(contact99) 'Typed DataSet Dim contactTable As New Contacts.ContactDataTable() ... Dim contact99 = ... contactTable.RemoveContactRow(contact99)
[Visual C#] 'Untyped DatsSet DataSet contactDS = new DataSet(); ... DataRow contact99 = ...; contactDS.Tables[0].Rows.Remove(contact99); 'Typed DataSet Contacts.ContactDataTable contactTable = new Contacts.ContactDataTable(); ... var contact99 = ...; contactTable.RemoveContactRow(contact99);
Note: When you remove a row from the Rows collection in an untyped DataTable object, the RowDeleting and RowDeleted events fire. If the DataTable object is a typed DataTable object, the corresponding typed events fire. For example, in the typed Contacts DataSet class, these events are called ContactRowDeleting and ContactRowDeleted.
When you add a new row to a DataTable object, the RowState property of the row is set to Deleted. The DataAdapter class uses this information to generate an appropriate command to delete data from the database. Question: How can you implement a set of default values for new rows in a DataTable object?
Additional Reading For more information about the DataTable events, see the Handling DataTable Events (ADO.NET) page http://go.microsoft.com/fwlink/?LinkID=194125.
Using ADO.NET
14-53
Validating Data and Saving Changes
Key Points When you have made changes to a DataSet object, you can save these changes back to the database.
Validating Changes in a DataSet Object You should not attempt to save changes made to a DataSet object until you have ensured that all of the changes you have made are valid. You can use the events raised when individual DataRow objects are created, added, updated, and modified to perform a limited amount of validation. However, you should not attempt to use these events to cross-validate a DataRow against other rows in the DataSet object, because the user may change the data in these rows later. When the user has changed the data and the application is about to save these changes, you can find out which data has changed by calling the GetChanges method of the DataSet object. This method returns a new DataSet object that contains only the changes that have been made. You can iterate through the rows in each DataTable object and perform any necessary cross-checks.
14-54
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Additionally, you can define business rules and integrity checks as part of a DataSet class in the form of Constraints. Prior to saving changes made to a DataSet object, you can determine whether any of these changes have validation errors by examining the Boolean HasErrors property of each DataTable object in the DataSet object that is returned by the GetChanges method. If this property is true for any DataTable object, call the GetErrors method on the DataTable object. The GetErrors method returns a collection of DataRow objects where validation errors have occurred. You can iterate through this collection and determine the cause of the error by calling the GetColumnsInError method. This method returns a collection of DataColumn objects that contain validation errors. Finally, you can determine the cause of the error by using the GetColumnError method and specifying the column that has the error. The GetColumnError method returns a string that contains the error message. You can attempt to resolve this error programmatically, or you can display the error and ask the user to make the necessary changes. You can clear the errors after you have corrected them by calling the ClearErrors method of the DataRow. Alternatively, you can discard all of the changes made to a DataSet object by using the RejectChanges method. The following code example uses the GetErrors method to return an array of DataRow objects that have errors. [Visual Basic] Private Sub PrintAllErrs(ByVal dataSet As DataSet) Dim Dim Dim Dim
rowsInError() As DataRow table As DataTable i As Integer column As DataColumn
For Each table In dataSet.Tables ' Test if the table has errors. If not, skip it. If table.HasErrors Then ' Get an array of all rows with errors. rowsInError = table.GetErrors() ' Print the error of each column in each row. For i = 0 To rowsInError.GetUpperBound(0) For Each column In table.Columns Console.WriteLine(column.ColumnName, _ rowsInError(i).GetColumnError(column)) Next ' Clear the row errors
Using ADO.NET
14-55
rowsInError(i).ClearErrors Next i End If Next End Sub
[Visual C#] private void PrintAllErrs(DataSet dataSet) { DataRow[] rowsInError; foreach(DataTable table in dataSet.Tables) { // Test if the table has errors. If not, skip it. if(table.HasErrors) { // Get an array of all rows with errors. rowsInError = table.GetErrors(); // Print the error of each column in each row. for(int i = 0; i < rowsInError.Length; i++) { foreach(DataColumn column in table.Columns) { Console.WriteLine(column.ColumnName + " " + rowsInError[i].GetColumnError(column)); } // Clear the row errors rowsInError[i].ClearErrors(); } } } }
Saving Changes to a Database You use the Update method of a DbDataAdapter object to physically save changes back to the database. The Update method expects a set of changes as a parameter, in the form of an array of DataRow objects, a DataTable object, or a DataSet object. The recommended approach is to specify the DataSet object returned by the GetChanges method, after any validation errors have been corrected. The Update method of the DbDataAdapter object iterates through the modified rows and generates the appropriate insert, update, and delete statements depending on the value of the RowState property. To perform these updates, the DataAdapter object must be provided with the appropriate commands. If you use
14-56
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
a SqlDataAdapter object to communicate with a SQL Server database, these commands take the form of parameterized SqlCommand objects that are attached to the DeleteCommand, InsertCommand, and UpdateCommand properties of the SqlDataAdapter object. The Update method replaces the parameters in each of these commands with the values in the fields in each DataRow object and then executes them. The following code examples show the SQL statements for the CommandText properties of SqlCommand objects for an untyped DataSet class that inserts, updates, and deletes rows from the Person.Contact table in the AdventureWorks database. -- Insert a new contact. -- The ContactID is an IDENTITY column and is generated by SQL Server INSERT INTO [Person].[Contact] ([FirstName], [LastName]) VALUES (@FirstName, @LastName) -- Update an existing contact. -- Use the ContactID to determine which row to update UPDATE [Person].[Contact] SET [FirstName] = @FirstName, [LastName = @LastName WHERE [ContactID] = @Original_ContactID -- Delete a contact. -- Use the ContactID to determine which row to delete DELETE FROM [Person].[Contact] WHERE [ContactID] = @Original_ContactID
Note: The DbDataAdapter class provides a property called UpdateBatchSize that you can use to send changes to the database server as a batch rather than individually. The default value for this property for the SqlDataAdapter class is 1, which effectively disables batching. You can also set it to 0, which removes any limits on the batch size and sends all update operations generated by the Update method as a single batch. However, you should use this value with caution; very large batches may consume considerable resources in the database server and affect performance.
If you use an untyped DataSet class, you must add these commands to the data adapter yourself, or you can use a SqlCommandBuilder object to generate them. If you use a typed DataSet class, the commands are generated automatically. Note: If an error occurs while the DataAdapter object is updating a row, an exception is thrown and execution of the update is discontinued. To continue the update operation without generating exceptions when an error is encountered, set the ContinueUpdateOnError property of the DataAdapter object to true before calling the
Using ADO.NET
14-57
Update method. You may also respond to errors on a per-row basis in the RowUpdated event of a DataAdapter object. To continue the update operation without generating an exception in the RowUpdated event, set the Status property of the RowUpdatedEventArgs object to Continue.
When the Update method has finished, you should call the AcceptChanges method of the DataSet object. This method removes all rows that have a RowState property value of Deleted from the DataSet object and resets the value of the RowState property for the remaining rows to Unchanged. Caution: If you fail to call the AcceptChanges method, a subsequent call to the Update method will attempt to repeat the changes that you made previously and may result in duplicated data in the database.
Question: Why is it important to validate the changes in a DataSet object before you save them?
Additional Reading For more information about validating data in datasets, see the Validating Data in Datasets page at http://go.microsoft.com/fwlink/?LinkID=194126. For more information about DataTable constraints, see the DataTable Constraints (ADO.NET) page at http://go.microsoft.com/fwlink/?LinkID=194127.
14-58
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Lesson 3
Managing Transactions and Concurrency in Multiuser Scenarios
Transaction management is an important element in any database application, because you must ensure that your application maintains the integrity of the database, and you must also optimize throughput. Additionally, you must consider how the application will manage potentially conflicting updates made by concurrent users, because this can also affect the integrity and performance of your solution. In this lesson, you will learn how ADO.NET enables you to create and manage transactions and how to handle concurrency violations.
Objectives After completing this lesson, you will be able to: •
Create transactions by using the SqlTransaction and TransactionScope classes.
Using ADO.NET
14-59
•
Describe the different transaction isolation levels available for working with DataSet objects.
•
Handle concurrency violations.
14-60
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Using Transactions
Key Points A transaction is a set of operations that should either succeed or fail as a unit. In transaction-processing terminology, either the transaction commits, in which case it saves all of the work performed and makes changes permanent, or it rolls back, in which case it undoes all of the work performed. For a transaction to commit, all participants (typically database servers) must guarantee that they can save any changes made to the databases they manage. Changes must persist even if the system crashes or some other unforeseen event occurs afterwards. If even a single participant fails to make this guarantee, the entire transaction fails, and all data modifications made during the transaction must be rolled back. ADO.NET and the .NET Framework provides support for local and distributed transactions. Local and distribution transactions are defined as follows: •
Local transactions. A local transaction applies to a single data source, such as a database or a message queue. It is common for these data sources to provide local transaction capabilities. Local transactions are controlled by the data source and are efficient and easy to manage.
Using ADO.NET
•
14-61
Distributed transactions. A distributed transaction spans multiple data sources. Distributed transactions enable you to incorporate several distinct operations, which occur on different systems, into an atomic action that either succeeds or fails completely.
Implementing Local Transactions A local transaction is a transaction that spans operations performed against a single database or other transactional resource. In ADO.NET, local transactions are controlled by the DbConnection object. You initiate a local transaction by invoking the BeginTransaction method, which returns a DbTransaction object that you can use to commit or roll back the updates, depending on the success of the transaction. Data providers that support transactions should implement their own version of this method and return their own DbTransaction object. For example, the BeginTransaction method of the SqlConnection class creates a SqlTransaction object that provides the transactional scope for SQL Server operations. The SqlTransaction class provides its own implementations of the Commit and Rollback methods, which commit or undo changes made to the SQL Server database during the transaction. The following code example performs a transaction that consists of two separate commands. The commands execute INSERT statements against the Production.Product table in the AdventureWorks database. If no exceptions are thrown, the transaction is committed. If an exception occurs or the connection is closed before the transaction completes, the transaction is rolled back: [Visual Basic] Using connection As New SqlConnection(builder.ConnectionString) connection.Open() ' Start a local transaction. Dim trans As SqlTransaction = connection.BeginTransaction() Dim command As SqlCommand = connection.CreateCommand() command.Transaction = trans Try command.CommandText = _ "INSERT INTO Production.Product(Name) VALUES('Widget')" command.ExecuteNonQuery() command.CommandText = _ "INSERT INTO Production.Product(Name) VALUES('Sprocket')" command.ExecuteNonQuery()
14-62
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
trans.Commit() Console.WriteLine("Both records were written to database.") Catch ex As SqlException Console.WriteLine(ex.Message) Try trans.Rollback() Catch exRollback As Exception Console.WriteLine(exRollback.Message) End Try End Try End Using
[Visual C#] using (SqlConnection connection = new SqlConnection(connectionString)) { connection.Open(); // Start a local transaction. SqlTransaction trans = connection.BeginTransaction(); SqlCommand command = connection.CreateCommand(); command.Transaction = trans; try { command.CommandText = "INSERT INTO Production.Product(Name) VALUES('Widget')"; command.ExecuteNonQuery(); command.CommandText = "INSERT INTO Production.Product(Name) VALUES('Sprocket')"; command.ExecuteNonQuery(); trans.Commit(); Console.WriteLine("Both records were written to database."); } catch (Exception ex) { Console.WriteLine(ex.Message);
Using ADO.NET
14-63
try { trans.Rollback(); } catch (Exception exRollback) { Console.WriteLine(exRollback.Message); } } }
If you use a DataSet object to maintain and update data, you may not have direct access to the DbConnection object that the Update method of the data adapter uses to update the database. However, you can still update data in a controlled manner by initiating a distributed transaction.
Implementing Distributed Transactions Distributed transactions are transactions that can span multiple data sources and other transactional resources. The .NET Framework provides extensive support for distributed transactions through the System.Transaction namespace. Note: Transactional resources do not have to be databases. For example, Message Queuing (also known as MSMQ) supports transactions, and you can use a distributed transaction to ensure that message queue send and receive operations are implemented as part of the same transaction as a database update operation performed by SQL Server.
You start a distributed transaction by creating a new TransactionScope object. You can then perform operations that are executed in the scope of the distributed transaction. These operations can involve one or more transactional resources that enlist into the transaction. The transaction remains active until the TransactionScope object is destroyed or you explicitly finish the transaction. When the transactional work has finished, you can call the Complete method of the TransactionScope object. The Complete method implements the two-phase commit protocol to ensure that all enlisted resources either commit or roll back together. If the TransactionScope object is destroyed before the Complete method is called, all transactional work is rolled back automatically. The following code example shows the typical use of a TransactionScope object.
14-64
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
[Visual Basic] Using ts As New TransactionScope() ' All work performed here is in the scope of the transaction. Try ' Perform operations. ... ' Commit the work performed by all operations. ts.Complete() Catch ex As ... ' Handle exceptions, but do not call Complete. ' The transaction will be rolled back automatically. ... End Try End Using ' The transaction is no longer active. ' Work performed here is not part of the transaction.
[Visual C#] using (TransactionScope ts = new TransactionScope()) { // All work performed here is in the scope of the transaction. try { // Perform operations. ... // Commit the work performed by all operations. ts.Complete(); } catch (...) { // Handle exceptions, but do not call Complete. // The transaction will be rolled back automatically. ... } } // The transaction is no longer active. // Work performed here is not part of the transaction.
The DbConnection class provides the EnlistTransaction method. A data provider can invoke this method, and all subsequent work performed through the specified connection forms part of the distributed transaction. If you use the SQL Server
Using ADO.NET
14-65
Data Provider for ADO.NET, you do not need to explicitly invoke the EnlistTransaction method, because any work performed by using a SqlConnection object is automatically enlisted if a TransactionScope object is active. Consequently, you can perform the updates for a DataSet object in a transaction simply by creating a TransactionScope object and calling the Update method in this transaction scope. Question: What happens if a TransactionScope object is destroyed without calling the Complete method?
Additional Reading For more information about the TransactionScope class, see the Implementing an Implicit Transaction using Transaction Scope page at http://go.microsoft.com/fwlink/?LinkID=194128.
14-66
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Understanding Isolation Levels
Key Points When you create a transaction by using an ADO.NET data provider or by using a TransactionScope object, you can specify the isolation level for that transaction. The isolation level determines the effect that this transaction has on other transactions while they are running and vice versa. Ideally, all transactions should be completely isolated and run concurrently without any impact on each other. However, a transaction frequently attempts to access data that is in use (or may be used) by another transaction. The transaction isolation level specifies what happens when this occurs. To reduce problems caused by two transactions that attempt to access the same data at the same time, many database management systems lock the data for the duration of the transaction. At one extreme, you can allow transactions to have unimpeded access to the database. This accessibility minimizes wait times for statements in the transactions but increases the risk of data corruption due to concurrent access.
Using ADO.NET
14-67
At the other extreme, you can specify that transactions are completely isolated from each other. The transactions are performed serially, one after the other. This isolation prevents the risk of data corruption due to concurrent access but increases wait times for statements in the transactions. Database management systems usually provide different types of locks and take out a lock appropriate to the type of access required. The transaction isolation level influences the locking strategy used by the connection that runs the transaction. It is important to choose an appropriate isolation level; you must prevent concurrency problems when multiple transactions access the same data but minimize the impact on performance.
Specifying the Isolation Level for a Transaction The DbTransaction abstract class defines the IsolationLevel property that you can use to specify the isolation level for the transaction. The System.Data namespace defines an enumeration type named IsolationLevel, which enables you to select an isolation level when you use a class that inherits from the DbTransaction class, such as the SqlTransaction class. You can specify the isolation level for a SqlTransaction object as a parameter to the BeginTransaction method that creates the object. You cannot change the isolation level for the transaction after you have set it. The System.Transactions namespace also defines an enumeration type named IsolationLevel. You can create a TransactionOptions object and set the IsolationLevel property to an item from this enumeration. You can then specify this TransactionOptions object when you create the TransactionScope object. The following table lists some of the commonly used values that you can assign to the IsolationLevel property on either type of transaction object. The more restrictive an isolation level, the better the data consistency, but the greater the impact on concurrency.
Isolation level enumeration value ReadUncommitted
Description This isolation level provides minimal protection between concurrent transactions. The transaction can access changes made by other transactions that have not yet been committed.
14-68
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Isolation level enumeration value ReadCommitted
Description The transaction only sees committed data. The transaction does not see uncommitted data updates. However, data read by this transaction can be modified or deleted by other concurrent transactions.
RepeatableRead
The transaction locks all rows that it reads in a query, so other transactions cannot update the data until the first transaction terminates. Other transactions can modify or insert rows that match the specification of data that this transaction reads. Consequently, if this transaction repeats the same query, it may obtain different data.
Serializable
Transactions are completely isolated from each other. This isolation level is frequently implemented by locking large numbers of rows or entire tables. This isolation level prevents access to uncommitted data; other transactions cannot modify or delete data that this transaction reads or add data that that this transaction may read if it repeats a query.
Important: The default isolation level for a TransactionScope object is Serializable. This provides the highest degree of safety, but it can cause contention because of locked resources, which has an impact on performance. If you use this isolation level, it is important that you keep your transactions as short as possible.
The following code example shows how to create a TransactionScope object with the RepeatableRead isolation level.
Using ADO.NET
14-69
[Visual Basic] Dim options As New TransactionOptions() options.IsolationLevel = _ System.Transactions.IsolationLevel.RepeatableRead Using ts = _ New TransactionScope(TransactionScopeOption.Required, options) ... End Using
[Visual C#] TransactionOptions options = new TransactionOptions(); options.IsolationLevel = System.Transactions.IsolationLevel.RepeatableRead; using (TransactionScope ts = new TransactionScope(TransactionScopeOption.Required, options)) { ... }
Question: What happens if your application attempts to read data currently being modified by another application and both applications use transactions?
14-70
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Handling Concurrency Violations
Key Points In a multiuser environment, the isolation level you select for a transaction has an impact on the possible concurrency errors that can occur. The following table lists the possible problems that can arise if several transactions attempt to access the same data at the same time. Concurrency error
Description
Dirty read
A transaction reads a row that a second transaction has updated but not committed. If the first transaction sees the updated data but the second transaction subsequently rolls back, the first transaction has seen data that never really existed.
Non-repeatable read
A transaction reads committed data. Another transaction then updates the data and commits. If the first transaction attempts to read the data again, it obtains
Using ADO.NET
Concurrency error
14-71
Description different values for the same row.
Phantom read
A transaction reads rows that match a search condition. Another transaction inserts, updates, or deletes rows in such a way as to modify the result set of the first transaction, and it then commits. If the first transaction repeats its read operation, it gets a different set of rows.
If you use a data adapter to send changes made to a DataSet object back to a database, a concurrency exception throws a DBConcurrencyException exception. The Row property of the DBConcurrencyException exception contains the DataRow object that caused the exception. If you perform updates manually by using SqlCommand objects, a concurrency violation throws a SqlException exception. Question: You attempt to save changes made to a DataSet object, but you receive a DBConcurrencyException exception. How can you find out which change has caused the exception?
14-72
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Lab: Using ADO.NET
Objectives After completing this lab, you will be able to: •
Use ADO.NET objects to connect to a SQL Server database, fetch data, and perform basic data modifications.
•
Display information retrieved by using ADO.NET commands.
•
Use an ADO.NET DataSet to fetch data and perform updates in a multiuser environment.
Introduction In this lab, you will connect to a SQL Server database by using an ADO.NET connection object and use ADO.NET objects to query and update data efficiently. You will also use a DataSet object to fetch data and cache it locally in an application, modify this data, and send the modifications back to a database.
Using ADO.NET
14-73
Lab Setup For this lab, you will use the available virtual machine environment. Before you begin the lab, you must: •
Start the 10265A-GEN-DEV-14 virtual machine, and then log on by using the following credentials: •
User name: Student
•
Password: Pa$$w0rd
14-74
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Lab Scenario
You have been asked to implement an application that will benefit from using ADO.NET to provide efficient access to a database. You will create a data access layer that queries and maintains the product list for the AdventureWorks database by using the data in the Product table. You will use ASP.NET to create a simple read-only test Web application that customers can use to browse products. You will then use a legacy Windows Forms application that enables employees to view, filter, and update product data to perform further tests.
Exercise 1: Using ADO.NET to Retrieve Read-Only Information Quickly and Perform Simple Data Modifications Scenario In this exercise, you will build a data access layer that uses an ADO.NET DataReader object to fetch product data from the AdventureWorks database.
Using ADO.NET
14-75
You will perform queries by using a collection of stored procedures rather than by dynamically constructing SQL SELECT statements in the data access layer. This will reduce the chances of SQL injection attacks. These stored procedures are: •
GetProductByID, which returns the product that matches the ID that is specified as a parameter
•
GetProductByColor, which returns a list of products that match the color that is specified as a parameter
•
GetProductByMaxListPrice, which returns a list of all products that have a list price that is less than or equal to the value that is specified as a parameter
•
GetAllProducts, which returns a list of all products in the database
You will use the following stored procedures to modify and remove products: •
UpdateProduct, which updates a specified product with data that is provided as parameters
•
DeleteProduct, which deletes the product with the specified ID
Note: Adding new products will not be not supported by this version of the data access layer.
You have been provided with a script that creates these stored procedures and the code for a class called ProductDataObject that exposes properties that match the columns returned by the queries in each of the stored procedures. You have also been provided with the definition of an interface called IProductDataAccessLayer that you will implement, which defines the functionality for the data access layer. The main tasks for this exercise are as follows: 1.
Prepare the AdventureWorks database for the lab.
2.
Open the starter project for this exercise.
3.
Create the ProductDataAccessLayer class.
4.
Implement the GetProductList method.
5.
Implement the GetProduct method.
6.
Implement the overloaded GetProductList methods.
7.
Implement the UpdateProduct method.
14-76
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
8.
Implement the DeleteProduct method.
9.
Create unit tests for the data access layer.
10. Test the data access layer.
X Task 1: Prepare the AdventureWorks database for the lab 1.
Log on to the 10265A-GEN-DEV-14 virtual machine as Student with the password Pa$$w0rd.
2.
In the E:\Labfiles folder, run AWReset.bat.
X Task 2: Open the starter project for this exercise 1.
Open Visual Studio 2010.
2.
Open the solution, Using ADO.NET.sln, in the E:\Labfiles\Lab14\VB\Ex1\Starter\Using ADO.NET or E:\Labfiles\Lab14\CS\Ex1\Starter\Using ADO.NET folder.
X Task 3: Create the ProductDataAccessLayer class 1.
Review the IProductDataAccessLayer interface. This is the interface that the ProductDataAccess class will implement. It defines the methods shown in the following code example.
[Visual Basic] Interface IproductDataAccessLayer ''' ''' ''' ''' '''
Get an Enumerable collection of ProductDataObjects An IEnumerable collection of ProductDataObjects
Function GetProductList() As List(Of ProductDataObject) ''' ''' ''' ''' ''' '''
Get an Enumerable collection of ProductDataObjects Only return products that match this color
Using ADO.NET
14-77
Function GetProductList(ByVal color As String) As List(Of ProductDataObject) ''' ''' ''' ''' ''' '''
Get an Enumerable collection of ProductDataObjects Only return products where the ListPrice is equal to or less that maxListPrice
Function GetProductList(ByVal maxListPrice As Decimal) As List(Of ProductDataObject) ''' ''' ''' ''' ''' '''
Get a single ProductDataObject by providing the ProductID The Int ProductID of the ProductDataObject to return A ProductDataObject
Function GetProduct(ByVal productID As Integer) As ProductDataObject ''' ''' ''' '''
Updates a ProductDataObject in the Database The ProductDataObject to update
Function UpdateProduct(ByVal product As ProductDataObject) As Boolean ''' ''' ''' '''
Deletes a single Product from the Database The ProjectDataObject to delete
Function DeleteProduct(ByVal product As ProductDataObject) As Boolean End Interface
[Visual C#] interface IProductDataAccessLayer { /// /// Get an Enumerable collection of ProductDataObjects objects. ///
14-78
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
/// An IEnumerable collection of /// ProductDataObjects. List GetProductList(); /// /// Get an Enumerable collection of ProductDataObjects. /// /// Only return products that match this /// color. /// List GetProductList(string color); /// /// Get an Enumerable collection of ProductDataObjects. /// /// Only return products where the /// ListPrice is equal to or less that maxListPrice. /// List GetProductList(decimal maxListPrice); /// /// Get a single ProductDataObject by providing the ProductID. /// /// The Int ProductID of the /// ProductDataObject to return. /// A ProductDataObject ProductDataObject GetProduct(int productID); /// /// Updates a ProductDataObject in the database. /// /// The ProductDataObject to update. bool UpdateProduct(ProductDataObject product); /// /// Deletes a single product from the database. /// /// The ProjectDataObject to delete. bool DeleteProduct(ProductDataObject product); }
2.
Review the ProductDataObject class. This is the type returned by the methods in the IProductDataAccess interface. The following code example shows this class.
Using ADO.NET
14-79
[Visual Basic] Public Class ProductDataObject Public Property ProductID As Integer Public Property Name As String Public Property ProductNumber As String Public Property Color As String Public Property ListPrice As Decimal Public Property ModifiedDate As DateTime End Class
[Visual C#] public class ProductDataObject { public int ProductID { get; set; } public string Name { get; set; } public string ProductNumber { get; set; } public string Color { get; set; } public Decimal ListPrice { get; set; } public DateTime ModifiedDate { get; set; } }
3.
In the DAL project, create a class named ProductDataAccessLayer.
4.
Modify the ProductDataAccessLayer class declaration to implement the IProductDataAccessLayer interface.
5.
If you are using Microsoft Visual Basic, generate the method stubs for each of the methods in the IProductDataAccessLayer interface by positioning the cursor after IProductDataAccessLayer and pressing ENTER. If you are using Visual C#, use the Implement Interface Wizard to generate method stubs for each of the methods in the IProductDataAccessLayer interface.
14-80
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
6.
Bring the System.Collections.Generic, System.Data, and System.Data.SqlClient namespaces into scope.
X Task 4: Implement the GetProductList method 1.
Review the Constants and AWDatabase classes. The Constants class contains a series of strings that specify the names of the stored procedures that the methods in the ProductDataAccess class will invoke. These stored procedures have already been created in the AdventureWorks database. The following code example shows this class.
[Visual Basic] Friend Class Constants Friend Const GetAllProducts As String = "productGetAllProducts" Friend Const GetProductByID As String = "productGetProductByID" Friend Const GetProductByColor As String = "productGetProductByColor" Friend Const GetProductByMaxListPrice As String = "productGetProductByMaxListPrice" Friend Const UpdateProduct As String = "productUpdateProduct" Friend Const DeleteProduct As String = "productDeleteProduct" End Class
[Visual C#] internal class Constants { internal const string GetAllProducts = "productGetAllProducts"; internal const string GetProductByID = "productGetProductByID"; internal const string GetProductByColor = "productGetProductByColor"; internal const string GetProductByMaxListPrice = "productGetProductByMaxListPrice"; internal const string UpdateProduct = "productUpdateProduct"; internal const string DeleteProduct = "productDeleteProduct"; }
The AWDatabase class is a singleton that retrieves the connection string that you will use to connect to the AdventureWorks database from the configuration file. The following code example shows this class.
Using ADO.NET
14-81
[Visual Basic] Friend Class AWDatabase ''' ''' The connection string for the AdventureWorks Database ''' Friend Shared DatabaseConnectionString As String = ConfigurationManager.ConnectionStrings("AdventureWorks").ConnectionStr ing ''' ''' Prevent object construction ''' Private Sub New() End Sub End Class
[Visual C#] internal class AWDatabase { /// /// The connection string for the AdventureWorks Database. /// internal static string DatabaseConnectionString = ConfigurationManager.ConnectionStrings["AdventureWorks"].ConnectionStr ing; /// /// Prevent object construction. /// private AWDatabase() { } }
2.
Return to the ProductDataAccessLayer class.
3.
Locate the GetProductList method, which accepts no parameters. If you are using Visual C#, remove the default method body inserted by Visual Studio that throws a NotImplementedException exception. Add code to instantiate a generic list of ProductDataObject objects, named products.
4.
In the GetProductList method, add a using code block that instantiates a new SqlConnection object named connection. The SqlConnection object should take the contents of the static field AWDatabase.DatabaseConnectionString
14-82
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
as a parameter. This is the connection string that contains the details necessary to connect to the AdventureWorks database. This using code block ensures that the SqlConnection object is correctly disposed of and the database connection is closed when it goes out of scope. 5.
In the using block, add code to perform the following tasks: a.
Create a SqlCommand object called getAllProducts. This should run the string that is specified by the GetAllProducts field in the Constants class and use the connection that is defined by the using statement.
b.
Set the CommandType property of the SqlCommand object to specify that the command runs a stored procedure.
c.
Open the connection to the database.
d. Create a SqlDataReader object called reader and use it to invoke the stored procedure. Specify that the SqlDataReader object should automatically close the connection after it has retrieved the data returned by the stored procedure. e.
Iterate through the results returned by the SqlDataReader object. For each row returned by the stored procedure, create a new ProductDataObject object. Populate the fields in the ProductDataObject object with the data returned by the SqlDataReader object, as shown in the following table, and add the ProductDataObject object to the products list.
Column number
Type of column
Can be null?
Field to populate
0
Int32
No
ProductID
1
String
No
Name
2
String
No
ProductNumber
3
String
Yes
Color
4
Decimal
No
Price
5
Date
Yes
ModifiedDate
6.
At the end of the GetProductList method, return the list of products.
7.
Build the solution and correct any errors.
Using ADO.NET
14-83
X Task 5: Implement the GetProduct method 1.
Locate the GetProduct method in the ProductDataAccessLayer class, which accepts an integer parameter that specifies a product ID. If you are using Visual C#. remove the default method body inserted by Visual Studio that throws a NotImplementedException exception.
2.
In the GetProduct method, add a ProductDataObject object named prod and assign it the value null.
3.
In the GetProduct method, add a using code block that instantiates a new SqlConnection object named connection. The SqlConnection object should use the static field AWDatabase.DatabaseConnectionString as a parameter.
4.
In the using block, add code to perform the following tasks: a.
Create a SqlCommand object called getProductsId. This should run the string that is specified by the GetProductByID field in the Constants class and use the connection that is defined by the using statement.
b.
Set the CommandType property of the SqlCommand object to specify that the command runs a stored procedure.
c.
Create a SqlParameter object for the productID parameter that is passed in. The GetProductByID method expects the product ID as a parameter. It returns all products that match this product ID.
d. Open the connection to the database. e.
Create a SqlDataReader object called reader and use it to invoke the stored procedure. Specify that the SqlDataReader object should automatically close the connection after it has retrieved the data returned by the stored procedure.
f.
Read the first row of the results returned by the SqlDataReader object. Instantiate the prod object as a new ProductDataObject object. Populate the fields in the ProductDataObject object with the data returned by the SqlDataReader object, as shown in the following table.
Column number
Type of column
Can be null?
Field to populate
0
Int32
No
ProductID
1
String
No
Name
2
String
No
ProductNumber
14-84
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Column number
Type of column
Can be null?
Field to populate
3
String
Yes
Color
4
Decimal
No
Price
5
Date
Yes
ModifiedDate
5.
At the end of the GetProductById method, return the prod object.
6.
Build the solution and correct any errors.
X Task 6: Implement the overloaded GetProductList methods 1.
Locate the GetProductList method, which accepts a string parameter that specifies the color to search for. If you are using Visual C#, remove the default method body inserted by Visual Studio that throws a NotImplementedException exception.
2.
Add code to instantiate a generic list of ProductDataObject objects, named products.
3.
In the GetProductList method, add code that creates a connection to the database by using the value that is specified by the static field AWDatabase.DatabaseConnectionString and then uses a SqlDataReader object to execute the stored procedure that is specified by the Constants.GetProductByColor string. This stored procedure expects a string parameter called color that indicates the color to match; create a SqlParameter object based on the parameter that is passed to the GetProductList method. Iterate through the results and fetch each matching product. Add the products to the products list, and return this list from the GetProductList method. The method should be similar to the original GetProductList method that takes no parameters.
4.
Locate the GetProductList method, which accepts a decimal parameter that specifies the maximum price to search for. If you are using Visual C#, remove the default method body inserted by Visual Studio that throws a NotImplementedException exception..
5.
In this version of the GetProductList method, add code to retrieve all products that have a price that is less than or equal to the price that is specified as the parameter. Use the stored procedure that is specified by the
Using ADO.NET
14-85
Constants.GetProductByMaxListPrice field. The stored procedure expects a parameter named maxListPrice. Create and return a list of all matching ProductDataObject objects. 6.
Build the solution and correct any errors.
X Task 7: Implement the UpdateProduct method 1.
Locate the UpdateProduct method. This method takes a ProductDataObject object as a parameter and returns a Boolean value. You will implement this method to update the specified product in the database with the information in this object and return true if the operation is successful or false otherwise.
2.
If you are using Visual C#, remove the default method body inserted by Visual Studio that throws a NotImplementedException exception.
3.
In the body of the method, add code to perform the following tasks: a.
Create a Boolean variable called result and initialize it to false. You will use this variable to indicate whether the delete was successful.
b.
Create a database connection object in a using block. Connect to the database by using the connection string that is specified in the AWDatabase.DatabaseConnectionString field.
c.
Create a SqlCommand object that can be used to execute the stored procedure that is specified by the Constants.UpdateProduct field.
d. Create SqlParameter objects with the names that are specified in the following table. Populate these SqlParameter objects with the data from the ProductDataObject object that is passed in to the method. Stored procedure parameter name ProductDataObject field to use @productID
ProductID
@name
Name
@productNumber
ProductNumber
@color
Color
@listPrice
ListPrice
e.
Add the parameters to the SqlCommand object.
14-86
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
4.
f.
Open the connection to the database.
g.
Execute the stored procedure by using the ExecuteNonQuery method of the SqlCommand object and set the value of the result variable to true. Note that if the update fails, a SqlException exception will be thrown.
h.
Catch the SqlException exception and set the value of the result variable to false.
i.
Return the result variable.
Build the solution and correct any errors.
X Task 8: Implement the DeleteProduct method 1.
Locate the DeleteProduct method. This method takes a ProductDataObject object as a parameter and returns a Boolean value. You will implement this method to delete the specified product from the database and return true if the operation is successful or false otherwise.
2.
In Visual C#, remove the default method body inserted by Visual Studio that throws a NotImplementedException exception.
3.
In the body of the method, add code to perform the following tasks: a.
Create a Boolean variable called result and initialize it to false. You will use this variable to indicate whether the delete was successful.
b.
Create a database connection object in a using block. Connect to the database by using the connection string that is specified in the AWDatabase.DatabaseConnectionString field.
c.
Create a SqlCommand object that can be used to execute the stored procedure that is specified by the Constants.DeleteProduct field.
d. Create a SqlParameter object with the details that are specified in the following table. Stored procedure parameter name ProductDataObject field to use @productID
ProductID
e.
Add the parameter to the SqlCommand object.
f.
Open the connection to the database.
Using ADO.NET
4.
14-87
g.
Execute the stored procedure by using the ExecuteNonQuery method of the SqlCommand object and set the value of result to true. Note that if the update fails, a SqlException exception will be thrown.
h.
Catch the SqlException exception and set the value of result to false.
i.
Return the result variable.
Build the solution and correct any errors.
X Task 9: Create unit tests for the data access layer 1.
In the DAL Unit Tests project, open the IProductDataAccessLayerTest class.
2.
Locate the CreateIProductDataAccessLayer method and remove the comment TODO: Instantiate an appropriate concrete class.
3.
Add code to perform the following tasks: a.
Create a new instance of the ProductDataAccessLayer class named target.
b.
Return the value of the target variable.
4.
Locate the GetProductListTest method.
5.
Remove the comment TODO and add code to perform the following tasks: a.
Create a variable of type IProductDataAccessLayer named target. Initialize the value of the target variable by invoking the CreateIProductDataAccessLayer method.
b.
Create a new instance of a generic list of ProductDataType objects, named expected. Initialize this list by invoking the static GetLocalProductList method. The GetLocalProductList method is part of the IProductDataAccessLayerTest class. It creates a list that contains a set of known values for testing purposes.
c.
Create another instance of a generic list of ProductDataType objects, named actual. Initialize this list by invoking the target.GetProductList method.
d. Iterate through the actual and expected lists, and verify that the items in each list are identical. Use the AreEqual method of the Assert class. 6.
Locate the GetProductByProductIDTest method.
14-88
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
7.
Remove the comment TODO and add code to perform the following tasks: a.
Create a variable of type IProductDataAccessLayer named target. Initialize the value of the target variable by invoking the CreateIProductDataAccessLayer method.
b.
Define an integer variable called productID and initialize it with the value 319.
c.
Create a variable of type ProductDataObject called expected. Initialize this object with the data for product 319 (use the productID variable) in the list returned by the GetLocalProductList method.
d. Create another variable of type ProductDataObject called actual. Initialize this object with the data returned from the GetProduct method of the target object. Pass the productID variable as the parameter to the GetProduct method. e.
Verify that the data in the actual and expected variables is the same. Use the AreEqual method of the Assert class.
8.
Locate the GetProductListByMaxPriceTest method.
9.
Remove the comment TODO and add code to test the version of the GetProductList method that expects a decimal parameter, specifying the maximum price to search for. Search for products that have a maximum price of 100. Follow the same general pattern as the GetProductListTest method.
10. Locate the GetProductListByColorTest method. 11. Remove the comment TODO and add code to test the version of the GetProductList method that expects a string parameter, specifying the color to search for. Search for products that have a color of "Silver". 12. Locate the UpdateProductTest method. 13. Remove the comment TODO and add code to test the UpdateProduct method by updating product 1; set the color to "Red" and the list price to 12.99. Verify that the UpdateProduct method returns true when it has successfully updated the product. 14. Locate the DeleteProductTest method. 15. Remove the comment TODO and add code to test the DeleteProduct method by deleting product 1. Verify that the DeleteProduct method returns true when it has successfully deleted the product. 16. Build the solution and correct any errors.
Using ADO.NET
14-89
X Task 10: Test the data access layer 1.
In the DAL Unit Tests project, run all unit tests and verify that they all pass.
2.
Close the solution.
Exercise 2: Developing the Product List Web Application Scenario In this exercise, you will build an ASP.NET test application to consume the ADO.NET data and to verify that the results are returned as expected. The main tasks for this exercise are as follows: 1.
Open the starter project for this exercise.
2.
Implement the BrowseColor controller action.
3.
Implement the BrowsePrice controller action.
4.
Implement the Products controller action.
5.
Implement the Details controller action.
6.
Test the data access layer by using the Web application.
X Task 1: Open the starter project for this exercise •
Open the solution, Using ADO.NET.sln, in the E:\Labfiles\Lab14\VB\Ex2\Starter\Using ADO.NET or E:\Labfiles\Lab14\CS\Ex2\Starter\Using ADO.NET folder. This solution contains an ASP.NET Model-View-Controller (MVC) Web application that you will use to test the ADO.NET data access layer that you built in the previous exercise.
X Task 2: Implement the BrowseColor controller action 1.
Review the task list.
2.
Open the HomeController file by double-clicking the comment TODO: Create a new instance of the ProductDataAccessLayer. This task is located in the BrowseColor method. The purpose of this method is to retrieve a list of all products that match the color that is specified by the user.
14-90
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
3.
Remove the comment TODO: Create a new instance of the ProductDataAccessLayer. and replace it with a statement that creates a new instance of the ProductDataAccessLayer class named dal.
4.
Locate the comment TODO: Create a list of products.
5.
Remove the comment and replace it with a statement that instantiates a generic list of ProductDataObject objects named products. Initialize this list with the value returned by the dal.GetProductList method, passing the value productResponse.Color as a parameter to this method.
6.
Locate the comment TODO: Return the Products view, passing the products list to the model context.
7.
Remove the comment and replace it with the following statement that returns from the method and passes the products collection to the Products view for display.
[Visual Basic] Return View("Products", products)
[Visual C#] return View("Products", products);
X Task 3: Implement the BrowsePrice controller action 1.
Review the task list.
2.
Locate the BrowsePrice method by double-clicking the first comment TODO: Create a new instance of the ProductDataAccessLayer in the task list. The purpose of this method is to retrieve a list of all products that have a list price not greater than the maximum price that is specified by the user.
3.
Remove the comment TODO: Create a new instance of the ProductDataAccessLayer and replace it with a statement that creates a new instance of the ProductDataAccessLayer class named dal.
4.
Locate the comment TODO: Create a list of products.
5.
Remove the comment and replace it with a statement that instantiates a generic list of ProductDataObject objects named products. Initialize this list with the value returned by the dal.GetProductList method, passing the value maxprice as a parameter to this method.
Using ADO.NET
14-91
6.
Locate the comment TODO: Return the Products view, passing the products list to the model context.
7.
Remove the comment and replace it with the following statement that returns from the method and passes the products collection to the Products view for display.
[Visual Basic] Return View("Products", products)
[Visual C#] return View("Products", products);
X Task 4: Implement the Products controller action 1.
Review the task list.
2.
Locate the Products method by double-clicking the first comment TODO: Create a new instance of the ProductDataAccessLayer in the task list. The purpose of this method is to retrieve a list of all products.
3.
Remove the comment TODO: Create a new instance of the ProductDataAccessLayer and replace it with a statement that creates a new instance of the ProductDataAccessLayer class named dal.
4.
Locate the comment TODO: Create a list of all products.
5.
Remove the comment and replace it with a statement that instantiates a generic list of ProductDataObject objects named products. Initialize this object with the data returned by the dal.GetProduct method. Pass the product ID that is passed as the parameter to this method to the GetProductList method.
6.
Locate the comment TODO: Return the Products view, passing the products list to the model context.
7.
Remove the comment and replace it with the following statement that returns from the method and passes the products collection to the current view for display.
14-92
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
[Visual Basic] Return View(products)
[Visual C#] return View(products);
X Task 5: Implement the Details controller action 1.
Review the task list.
2.
Locate the Details method by double-clicking the first comment TODO: Create a new instance of the ProductDataAccessLayer in the task list. The purpose of this method is to retrieve the details of the product that has the product ID that is specified by the user.
3.
Remove the comment TODO: Create a new instance of the ProductDataAccessLayer and replace it with a statement that creates a new instance of the ProductDataAccessLayer class named dal.
4.
Locate the comment TODO: Get the product specified by id.
5.
Remove the comment and replace it with a statement that creates a ProductDataObject object named product. Initialize this object with the value returned by the dal.GetProduct method, passing the value of id as a parameter to this method.
6.
Locate the comment TODO: Return the Details view, passing the products list to the model context.
7.
Remove the comment and replace it with the following statement that returns from the method and passes the product object to the current view for display.
[Visual Basic] Return View(product)
[Visual C#] return View(product);
Using ADO.NET
14-93
X Task 6: Test the data access layer by using the Web application 1.
Start the Test Application application. Windows Internet Explorer® starts and displays the Test Application page.
2.
On the Test Application page, click Browse all Products. Internet Explorer displays a list of the products in the AdventureWorks database.
3.
Click Home to return to the first page.
4.
In the Browse Products by Color box, type Silver and then click Browse. Internet Explorer displays a list of silver products.
5.
Click Details adjacent to the first product. Internet Explorer displays the details of the selected product.
6.
Click Home.
7.
In the Browse Products by Maximum Price box, type 100 and then click Browse. Internet Explorer displays a list of products where the list price is not greater than 100.
8.
Close Internet Explorer, and then return to Visual Studio.
9.
Close the solution.
Exercise 3: Enabling Data Modifications Scenario In this exercise, you will modify the data access layer to use an ADO.NET DataSet to maintain product and list price history data to enable employees to query, update, and filter product information. You will use a Windows Forms application to consume the data and to verify that the results are returned as expected. The data access layer will include full concurrency checking and use an appropriate strategy to handle conflicting updates. You will be provided with a skeleton version of the data access layer and will reimplement it to use a typed DataSet class. You will also be provided with a Windows Forms application that provides a simple user interface (UI). You will add the functionality that invokes the methods in the data access layer. You will
14-94
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
also run multiple instances of this Windows Forms application simultaneously to verify that the concurrency checking in the data access layer works as expected. The main tasks for this exercise are as follows: 1.
Open the starter project for this exercise.
2.
Create the ProductDataSet DataSet.
3.
Implement the GetProductList method.
4.
Implement the GetProduct method.
5.
Implement the overloaded GetProductList methods.
6.
Implement the UpdateProduct method.
7.
Implement the DeleteProduct method.
8.
Modify the test application.
9.
Test the data access layer by using the test application.
X Task 1: Open the starter project for this exercise •
Open the solution, Using ADO.NET.sln, in the E:\Labfiles\Lab14\VB\Ex3\Starter\Using ADO.NET or E:\Labfiles\Lab14\CS\Ex3\Starter\Using ADO.NET folder. This solution contains a new version of the data access layer code that you will implement by using DataSets and a Windows Forms application that you will use to test the data access layer code.
X Task 2: Create the ProductDataSet DataSet 1.
Add a new DataSet class named ProductDataSet to the DAL project.
2.
Add the Product(Production) table in the AdventureWorks database to the ProductDataSet.xsd file.
3.
Add the ProductListPriceHistory(Production) table to the ProductDataSet.xsd file.
4.
Save the ProductDataSet.xsd file, and then close it.
5.
Review the ProductDataProvider class. This class is a singleton that you can use to construct a ProductDataSet DataSet in a controlled manner.
Using ADO.NET
14-95
X Task 3: Implement the GetProductList method 1.
Review the task list.
2.
Open the ProductDataAccessLayer file by double-clicking the first comment TODO: Add code to return a list of all products. This task is located in the GetProductList method.
3.
Remove the comment and replace it with code that performs the following tasks: a.
Populate the ProductDataProvider.DataSet.Product DataTable class. The ProductDataAccessLayer class provides a table adapter called productDataAdapter that you can use to connect to the AdventureWorks database and retrieve product information. Use the Fill method of the productDataAdapter object.
b.
Create a new instance of a generic list of ProductDataObject objects, named products.
c.
Iterate through the DataRow collection in the ProductDataProvider.DataSet.Product DataTable and add the product found in each row to the products collection. The ProductDataAccessLayer class contains a helper method called BusinessObjectFromDataRow. You can use this method to construct a ProductDataObject object from a DataRow object that contains the data for a product. You pass the DataRow object as the parameter, and the method returns a ProductDataObject object.
d. Return the products collection.
X Task 4: Implement the GetProduct method 1.
Review the task list.
2.
In the Task View window, locate the comment TODO: Add code to return a single product. Double-click this comment to go to the GetProduct method.
3.
Remove the comment and replace it with code that performs the following tasks: a.
Populate the ProductDataProvider.DataSet.Product DataTable.
b.
Retrieve the DataRow object for the product from the ProductDataProvider.DataSet.Product DataTable object by using the
14-96
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
FindByProductID method of this DataTable object, passing the productID variable as a parameter. The FindByProductID method was generated by the Dataset Designer. c.
If the DataRow object is not null, construct a ProductDataObject object from the DataRow object by using the BusinessObjectFromDataRow helper method, and then return this ProductDataObject object. Otherwise, return a null value.
X Task 5: Implement the overloaded GetProductList methods 1.
Review the task list.
2.
In the Task View window, locate the comment TODO: Add code to return a list of products matching a given color. Double-click this comment to go to the GetProductList method that retrieves products that match a specified color.
3.
Remove the comment and replace it with code that performs the following tasks: a.
Populate the ProductDataProvider.DataSet.Product DataTable.
b.
Create a new instance of a generic list of ProductDataObject objects, named products.
c.
Define a LINQ to DataSet query that retrieves all rows from the ProductDataProvider.DataSet.Product DataTable where the Color column is not null and matches the color that is specified as the parameter to this method.
d. Iterate through the results returned by the LINQ to DataSet query and add each product found to the products collection. Use the BusinessObjectFromDataRow helper method to convert each DataRow object to a ProductDataObject object. e.
Return the products collection.
4.
In the Task View window, locate the comment TODO: Add code to return a list of products filtered on list price. Double-click this comment to go to the GetProductList method that retrieves products that have a list price not exceeding the price that is specified as the parameter to this method.
5.
Remove the comment and replace it with code that performs the following tasks:
Using ADO.NET
14-97
a.
Populate the ProductDataProvider.DataSet.Product DataTable object.
b.
Create a new instance of a generic list of ProductDataObject objects, named products.
c.
Define a LINQ to DataSet query that retrieves all rows from the ProductDataProvider.DataSet.Product DataTable object where the list price is not greater than the value that is specified as the parameter to this method.
d. Iterate through the results returned by the LINQ to DataSet query and add each product found to the products collection. Use the BusinessObjectFromDataRow helper method to convert each DataRow object to a ProductDataObject object. e.
Return the products collection.
X Task 6: Implement the UpdateProduct method 1.
Review the task list.
2.
In the Task View window, locate the comment TODO: Add code to update the product in the database. Double-click this comment to go to the UpdateProduct method.
3.
Review the code in the UpdateProduct method. Most of the code for this method has already been provided for you. This method updates a product in the Product table. If the product list price has changed, a new row is added to the ProductListPriceHistory table. This table maintains a list of all changes to a product's list price. If the update to the Product or ProductListPriceHistory tables fails, the transaction is rolled back. If a concurrency exception occurs, it is rethrown to the client application.
4.
Remove the comment and replace it with code that performs the following tasks: a.
Invoke the productAdapter.Update method. Pass row as the parameter.
b.
If the updatePrice variable is true, call the listPriceAdapter.Update method to update the list price history. You should use the listPriceAdapter.Update method and pass the ProductDataProvider.DataSet.ProductListPriceHistory DataTable object as the parameter.
14-98
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
X Task 7: Implement the DeleteProduct method 1.
Review the task list.
2.
In the Task View window, locate the comment TODO: Add code to delete a product from the database. Double-click this comment to go to the DeleteProduct method.
3.
Review the code in the DeleteProduct method. Most of the code for this method has already been provided for you. This deletes a product from the Product table along with any related rows in the ProductListPriceHistory table. If the updates to the Product or ProductListPriceHistory tables fail, the transaction is automatically rolled back. If a concurrency exception occurs, it is rethrown to the client application.
4.
Remove the comment and replace it with code that performs the following tasks: a.
Create a variable of type DataRelation named relation that refers to the relationship between the Product DataTable object and the ProductListPriceHistory DataTable object. You can find this relationship in the ChildRelations collection of the Product DataTable object. It has the name "FK_ProductListPriceHistory_Product_ProductID". When the code deletes a product, it will automatically delete all related price history rows to prevent any referential integrity problems. This strategy is known as a cascading delete.
b.
Create a DataRow variable called row that refers to the product being deleted in the Product DataTable. You can locate this product by using the FindByProductID method of the Product DataTable.
c.
Find all child rows in the ProductListPriceHistory DataTable object for the product identified by the row table, and store a reference to these child rows in the toRemove DataRow array. You can find these child rows by using the GetChildRows method of row object; specify relation as the parameter to this method.
d. Iterate through the rows in the toRemove array and mark them for deletion. e.
If there is at least one row in the toRemove array, delete all marked ProductListPriceHistory rows from the database. Use the Update method of the listPriceAdapter object and provide the toRemove array as the parameter to this method.
Using ADO.NET
5.
14-99
f.
Locate the DataRow object for the product to be deleted in the Product DataTable object and mark it for deletion.
g.
Delete the product from the database. Use the Update method of the productAdapter TableAdapter and specify the Product DataTable as the parameter.
h.
Commit the transaction and set the success variable to true. Note that if either of the Update methods fail, they will throw an exception, the transaction will be automatically rolled back, and the success variable will remain at its default value of false.
Build the project and correct any errors.
X Task 8: Modify the test application 1.
Review the task list.
2.
Open the Form1 file by double-clicking the comment TODO: Add code to get a product with a given product ID. This task is located in the btnSearchByProduct_Click method.
3.
Remove the comment and replace it with code to create a new ProductDataObject object named prod. Initialize the prod object with the value returned by the dal.GetProduct method, passing the prodID variable as a parameter.
4.
Locate the comment TODO: Add code to get a list of products matching a given color.
5.
Remove the comment and replace it with code to set the value of the products variable by calling the dal.GetProductList method. Use the txtColor.Text property as the parameter.
6.
Locate the comment TODO: Add code to get a list of products filtered by list price.
7.
Remove the comment and replace it with code to set the value of the products variable by calling the dal.GetProductList method and passing the maxPrice variable as a parameter.
8.
Locate the comment TODO: Add code to update products. This task is located in the btnSave_Click method and is run when the user clicks Save Changes on the form.
14-100
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
9.
Remove the comment and replace it with code to call the dal.UpdateProduct method. The UpdateProduct method expects a ProductDataObject object as a parameter; use the object created for each iteration of the enclosing loop.
10. Locate the comment TODO: Add code to delete products. 11. Remove the comment and replace it with code to invoke the dal.DeleteProduct method. The DeleteProduct method expects a ProductDataObject object as a parameter; use the object created for each iteration of the enclosing loop. 12. Build the project and correct any errors.
X Task 9: Test the data access layer by using the test application 1.
Start the Test Application application.
2.
In the Data Set Test Application window, click Load all Products. This should display a list of all of the products in the data grid.
3.
In the Data Set Test Application window, in the Product ID box, type 316 and then click Search By Product.
4.
In the Data Set Test Application window, in the data grid, in the Color box, type Gold and then click Save Changes.
5.
In the Data Set Test Application window, in the Color box, type Gold and then click Search By Color. You should see only one product listed in the data grid.
6.
Select the product and delete it.
7.
Verify that the product has been deleted.
8.
Close the solution.
Using ADO.NET
14-101
Lab Review
Review Questions 1.
Which class should you use to connect to a SQL Server database?
2.
Under what circumstances does a DBConcurrencyException exception occur when you update a database?
3.
How can you quickly check whether a DataSet object contains any referential integrity errors after you have made changes to rows?
4.
Which method of which object can you use to connect to a database, populate a DataSet object, and then disconnect again?
14-102
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Module Review and Takeaways
Review Questions 1.
What methods does the DbCommand class provide to execute operations against a data source? When should you use each of these methods?
2.
How do you persist changes to the database after you modify rows in a DataSet object?
3.
What is the difference between local and distributed transactions?
Best Practices Related to Retrieving and Modifying Data by Using ADO.NET Commands and DataSet Objects Supplement or modify the following best practices for your own work situations: •
You should use stored procedures to modify your data whenever possible.
•
You should use typed datasets whenever possible.
Using ADO.NET
14-103
•
When you make changes to a DataSet object, you should ensure that those changes are valid before you save to the database.
•
For performance reasons, you should prepare commands.
Using LINQ to SQL
15-1
Module 15 Using LINQ to SQL Contents: Lesson 1: Implementing a Logical Data Model by Using LINQ to SQL
15-4
Lesson 2: Managing Performance and Handling Concurrency
15-35
Lab: Using LINQ to SQL
15-54
15-2
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Module Overview
ADO.NET provides a mechanism that enables you to build applications that can query and maintain data that is held in a variety of sources in a database-agnostic manner. However, building applications by using ADO.NET requires that you are familiar with the Structured Query Language (SQL) language and features of the database management system that you are connecting to. Language-Integrated Query (LINQ) to SQL provides a higher-level abstraction for managing data that is held in a Microsoft® SQL Server® database, and is an ideal stepping stone for migrating ADO.NET applications toward the ADO.NET Entity Framework. LINQ to SQL is part of the ADO.NET family of technologies. It is based on services that the ADO.NET provider model provides. You can mix LINQ to SQL code with existing ADO.NET applications and move current ADO.NET solutions to LINQ to SQL quickly and easily. LINQ to SQL is an implementation of LINQ that provides a runtime infrastructure for managing relational data as objects. LINQ to SQL enables developers to deploy solutions by constructing object/relational mappings that can map classes to tables, views, stored procedures, and user-defined functions. LINQ to SQL works by translating LINQ queries into SQL commands for execution by the database
Using LINQ to SQL
15-3
and then translating the tabular results back into the structure, which is defined by a set of classes. LINQ to SQL automatically tracks changes to these objects as they are manipulated by the application. This module introduces LINQ to SQL and explains how you can use it to abstract the low-level details of ADO.NET queries by developing against a logical data model.
Objectives After completing this module, you will be able to: •
Design a logical data model by using LINQ to SQL.
•
Manage performance by using LINQ to SQL, and handle concurrency.
15-4
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Lesson 1
Implementing a Logical Data Model by Using LINQ to SQL
Most applications have a requirement to access and manipulate data. This data is often stored in relational databases. Unfortunately, there are large differences in the way that modern programming languages and relational databases represent and manipulate this data. Queries are often represented as text strings and passed through to the database for processing, as with the ADO.NET Command object. One drawback of this approach is that this data access logic cannot benefit from compile-time verification and design-time features such as Microsoft IntelliSense®. In addition, there are differences in the way that the data is represented. In an application, data is typically represented as objects, whereas in a database, rows are used. These differences can result in applications that are difficult to build and maintain. LINQ to SQL aims to solve these problems by providing a runtime infrastructure that translates LINQ queries into SQL commands for database execution and then translates the results into objects that are defined in the object model. LINQ to
Using LINQ to SQL
15-5
SQL works by mapping the relational database schema to Microsoft .NET Framework classes called entity classes. This lesson introduces the LINQ to SQL object model, and describes how to map relational database elements to entity classes. It also describes how you can use these classes to query and maintain data.
Objectives After completing this lesson, you will be able to: •
Describe the LINQ to SQL object model.
•
Explain how to model associations and relationships by using LINQ to SQL.
•
Use the Object Relational Designer (O/R Designer) to create a LINQ to SQL model.
•
Describe the purpose of the DataContext class.
•
Query a database by using LINQ to SQL.
•
Make and submit changes to a database by using LINQ to SQL.
•
Customize insert, update, and delete operations by using stored procedures with LINQ to SQL.
15-6
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
The LINQ to SQL Object Model
Key Points In LINQ to SQL, the data model of a relational database is mapped to a LINQ to SQL object model. The LINQ to SQL object model is a collection of entity classes and relationships that are based on the database schema. Entity classes are .NET Framework classes that are decorated with special attributes that bind the class and class members to the relational data model. The following table summarizes the most fundamental elements in the LINQ to SQL object model and their relationship to corresponding elements in the relational data model. LINQ to SQL object model
Relational data model
Entity class
Table
Class member
Column
Association
Foreign-key relationship
Using LINQ to SQL
LINQ to SQL object model Method
15-7
Relational data model Stored procedure or function
LINQ to SQL Entity Classes In LINQ to SQL, a database table is represented by an entity class. An entity class is a .NET Framework class that is annotated with attributes that associate the class with a database table. When you use LINQ to SQL to perform a query, it creates a collection of entity objects and populates them with data that is retrieved from the database. To associate a class with a table, you add the Table attribute, as the following code example shows. Note: The types and attributes that LINQ to SQL uses are implemented in the System.Data.Linq assembly, and are defined in the System.Data.Linq and System.Data.Linq.Mapping namespaces.
[Visual Basic] _ Public Class Employee Public EmployeeID As String ' ... Public Name As String End Class
[Visual C#] [Table(Name = "Staff")] public class Employee { public string EmployeeID; // ... public string Name; }
LINQ to SQL uses the table that the Table attribute specifies to help construct the appropriate query and insert, update, and delete commands to retrieve and
15-8
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
maintain data. The name of the table does not have to be the same as the name of the entity class.
Entity Class Members Fields and properties within an entity class are used to represent columns within a database table. To map a property or field to a table column, use the Column attribute, as the following code example shows. [Visual Basic] _ Public Class Employee Public EmployeeID As String Public Name As String Public DepartmentID As String ... End Class
[Visual C#] [Table(Name = "Staff")] public class Employee { [Column(IsPrimaryKey = true)] public string EmployeeID; [Column] public string Name; [Column] public string DepartmentID; ... }
Only fields and properties that are mapped to columns are persisted to or retrieved from the database. Any fields and properties that are not declared as columns are considered as transient parts of your application logic. The Column attribute has various properties that you can use to customize the members that represent columns. The most commonly used properties are:
Using LINQ to SQL
15-9
•
IsPrimaryKey. This property specifies that the column makes up part of the primary key. (If the table has a composite primary key that spans multiple columns, you should specify the IsPrimaryKey property for each corresponding column in the entity class.)
•
DbType. This property specifies the type of the underlying column in the database. In many cases, LINQ to SQL can detect and convert data in a column in the database to the type of the corresponding column in the entity class. However, in some situations, you may need to specify the data type mapping yourself.
•
CanBeNull. This property indicates whether the column in the database can contain a null value. The default value for the CanBeNull parameter is true.
Question: What happens if you omit the Column attribute for a field or property in an entity class?
Additional Reading For more information about the attributes that the LINQ to SQL object model provides for mapping tables and columns, see the TableAttribute Class page at http://go.microsoft.com/fwlink/?LinkID=194129, and the ColumnAttribute Class page at http://go.microsoft.com/fwlink/?LinkID=194130.
15-10
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Modeling Associations and Relationships
Key Points You can define database associations, such as primary-key to foreign-key relationships, by using the Association attribute. In LINQ to SQL, you represent database associations (such as foreign-key to primary-key relationships) by using properties that reference a set of entity objects that map to the related rows for an object. LINQ to SQL defines the generic EntitySet collection class for this purpose. It is good practice to make this collection private, but to implement a property that provides controlled access to the data in the collection. You annotate the property with the Association attribute to indicate that it is modeling a relationship between two sets of entities. The Storage property of the Association attribute indicates the EntitySet collection that actually provides the entity classes. For example, a Department entity might contain a property called Employees that provides access to a collection of entity objects, called employees, that represents the employees that belong to the department. You can apply the Association attribute to the Employee property, and specify how LINQ to SQL should identify which employees to fetch for a given department. You achieve this by using the
Using LINQ to SQL
15-11
OtherKey property of the Association attribute. In the following code example, the OtherKey property specifies the foreign-key column in the Employee table that refers to the Department table. When LINQ to SQL fetches the data for each department, it automatically fetches all of the employees who have a value in the DepartmentID column that matches the value in the primary-key column for the department. [Visual Basic] _ Public Class Department Public DepartmentID As String ... Private employees As EntitySet(Of Employee) Public Property Employees As EntitySet(Of Employee) Get Return Me.employees End Get Set(ByVal value As EntitySet(Of Employee)) Me.employees.Assign(value) End Set End Property End Class
[Visual C#] [Table(Name = "Department")] public class Department { [Column(IsPrimaryKey = true)] public string DepartmentID; ... private EntitySet employees; [Association(Storage = "employees", OtherKey = "DepartmentID")] public EntitySet Employees { get { return this.employees; } set { this.employees.Assign(value); } } }
15-12
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Note: You use the Assign method of the generic EntitySet class to populate an EntitySet collection.
You use the EntitySet generic class to implement a one-to-many relationship. LINQ to SQL also provides the EntityRef generic class, which you can use to model the inverse many-to-one relationship. Where EntitySet references a collection of entities, EntityRef references a single entity. In the following code example, the private department field in the Employee entity class is a reference to an instance of the Department entity class. The public Department property provides access to this reference. As with the one-to-many relationship shown previously, the Association attribute specifies how LINQ to SQL locates and populates the data for this property. The ThisKey property indicates which property in the Employee entity class LINQ to SQL should use to identify the department to reference for this employee. The OtherKey parameter specifies which property in the Department entity LINQ to SQL should match against the value for the ThisKey property. In this example, the Employee and Department entities are joined by using the EmpDeptID property in Employee and the DepartmentID property in Department. [Visual Basic] _ Public Class Employee Public EmployeeID As String Public EmpDeptID As String ' foreign key to Department table ... Private department As EntityRef(Of Department) Public Property Department As EntityRef(Of Department) Get Return Me.department.Entity End Get Set(ByVal value As EntityRef(Of Department)) Me.department.Entity = value End Set End Property End Class
Using LINQ to SQL
15-13
[Visual C#] [Table(Name = "Employee")] public class Employee { [Column(IsPrimaryKey = true)] public string EmployeeID; [Column] public string EmpDeptID; // foreign key to Department table ... private EntityRef department; [Association(Storage = "department", ThisKey = "EmpDeptID" OtherKey = "DepartmentID")] public EntityRef Department { get { return this.department.Entity; } set { this.department.Entity = value; } } }
Question: When should you use the EntityRef class and the EntitySet class when modeling a one-to-many relationship between entities?
Additional Reading For more information about using the Association attribute to model relationships between tables, see the How to: Map Database Relationships (LINQ to SQL) page at http://go.microsoft.com/fwlink/?LinkID=194131.
15-14
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Building a LINQ to SQL Object Model by Using the Object Relational Designer
Key Points You can create entity classes manually by writing your own code and annotating them with Table, Column, and Association attributes. However, Microsoft Visual Studio® 2010 provides the O/R Designer, which enables you to graphically create entity classes and relationships. You can create an object model from an existing database and use the model in its default state; you can also customize many aspects of the model and its behavior. The O/R Designer provides a visual design surface for creating LINQ to SQL entity classes and relationships. In addition to the entity classes, a DataContext class is created that communicates between the entity model and the database. Note: The O/R Designer also provides the ability to design inheritance relationships between entity classes. The O/R Designer only supports one-to-one relationship mappings between the entity model and the database, so a single entity class must map
Using LINQ to SQL
15-15
to a single database table. Mapping multiple tables to an entity class is not supported; however, you can map an entity class to a view that joins multiple related tables.
You can create entity classes, based on database tables or views, by dragging tables or views from Server Explorer into the O/R Designer window. The O/R Designer automatically generates the classes and applies the specific LINQ to SQL attributes to enable the LINQ to SQL functionality.
Building a New LINQ to SQL Object Model from an Existing Database To generate a LINQ to SQL object model, perform the following tasks: 1.
Add a new LINQ to SQL Classes item to your project. This creates a .dbml file that represents an empty DataContext class.
2.
Add entities by dragging tables and views from Server Explorer onto the O/R Designer design surface.
3.
If your database uses stored procedures or functions, you can also add these to the O/R Designer.
When the LINQ to SQL classes are saved, the entity classes are automatically generated. The following code example shows an example of the generated code. [Visual Basic] ... Partial Public Class AWModelDataContext Implements System.Data.Linq.DataContext ... Public ReadOnly Property Departments As System.Data.Linq.Table(Of Department) Get Return Me.GetTable(Of Department)() End Get End Property End Class
15-16
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Partial Public Class Department Implements System.Data.Linq.DataContext ... Private Private Private Private
_DepartmentID As Short _Name As String _GroupName As String _ModifiedDate As DateTime
... End Class
[Visual C#] ... [global::System.Data.Linq.Mapping.DatabaseAttribute(Name=”AdventureWor ks”)] public partial class AWModelDataContext : System.Data.Linq.DataContext { ... public System.Data.Linq.Table Departments { get { return this.GetTable(); } } } [global::System.Data.Linq.Mapping.TableAttribute(Name=”HumanResources. Department”)] public partial class Department : System.Data.Linq.DataContext { ... private short _DepartmentID; private string _Name; private string _GroupName; private DateTime _ModifiedDate; ... }
Code that the O/R Designer generates is regenerated whenever the entity classes and other objects on the design surface are modified. As a result, any code that is
Using LINQ to SQL
15-17
added to the generated code is typically overwritten when the regeneration takes place. Note: The O/R Designer can generate partial classes that enable you to add code that will not be overwritten. A common scenario in which you would use these partial classes is when adding data validation methods to the LINQ to SQL entity classes.
Question: When you want to perform validation on your entity class, where do you write your code?
Additional Reading For more information about using the O/R Designer, see the Walkthrough: Creating LINQ to SQL Classes (O/R Designer) page at http://go.microsoft.com/fwlink/?LinkID=194132.
15-18
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Demonstration: Creating a LINQ to SQL Object Model
Key Points •
Create a LINQ to SQL object model.
•
Review the generated code.
Demonstration Steps 1.
Log on to the 10265A-GEN-DEV-15 virtual machine as Student with the password Pa$$w0rd.
2.
Start Visual Studio 2010.
3.
Create a project named ObjectModelDemo in the E:\Demofiles\Demo1\Starter folder.
4.
Add a LINQ to SQL Classes item named AWModel.dbml to the solution.
5.
Create a model based on the local SQL Server Express AdventureWorks database, using the Contact and SalesOrderHeader tables.
6.
Save the AWModel.dbml file.
Using LINQ to SQL
7.
Review the SalesOrderHeader partial class.
8.
Review the AWModelDataContext partial class.
9.
Open the AWModel.designer.cs file, and then walk through the generated code.
10. Close the solution, and then close Visual Studio.
15-19
15-20
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
The DataContext Class
Key Points The DataContext class is responsible for connecting to the database and managing the relationship between the SQL object model’s entity classes and the tables that are contained in the database. It tracks the changes that are made and maintains an identity cache that guarantees that entities that are retrieved more than once are represented by using the same object instance. The DataContext object provides the channel by which you retrieve objects from the database and resubmit changes. You use it in the same way that you would use an ADO.NET Connection object. The purpose of the DataContext object is to translate your requests for objects into SQL queries that are made against the database and then assemble objects out of the results.
Connecting to a Database A DataContext object is intended to perform a single unit of work; it is lightweight and not expensive to create. Typically, a DataContext object is created at method scope. When you use a DataContext object to fetch or update data, it automatically opens a database connection, retrieves or modifies data, and then closes the connection when the operation is complete.
Using LINQ to SQL
15-21
The DataContext class constructor expects a connection string as a parameter, specifying the database that you want to use. This connection string is exactly the same as the connection string that you would use when connecting through an ADO.NET Connection object. (The DataContext class actually creates an ADO.NET connection behind the scenes.) The following code example shows how to create a new DataContext object. [Visual Basic] Dim db As New DataContext(connectionString)
[Visual C#] DataContext db = new DataContext(connectionString);
Using a Strongly Typed DataContext Class The O/R Designer generates a strongly typed DataContext class that exposes collections that contain each of the entity classes that your model defines. Wherever possible, you should create and use a strongly typed DataContext class in preference to the standard DataContext type. The following code example shows a strongly typed DataContext class for a model that references the Product and Order tables in a SQL Server database. [Visual Basic] Partial Public Class AdventureWorksContext Inherits DataContext Public Products As Table(Of Product) Public Orders As Table(Of Order) Public Sub New(ByVal connection As String) MyBase.New(connection) End Sub End Class
15-22
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
[Visual C#] public partial class AdventureWorksContext : DataContext { public Table Products; public Table Orders; public AdventureWorksContext(string connection): base(connection) {} ... }
Each database table is represented as a generic Table collection. Question: When you use a DataContext object to execute a query, what action should you take to close the database connection?
Additional Reading For more information about managing object identity in LINQ to SQL, see the Object Identity (LINQ to SQL) page at http://go.microsoft.com/fwlink/?LinkID=194133.
Using LINQ to SQL
15-23
Retrieving Data by Using LINQ to SQL
Key Points LINQ to SQL provides a mechanism for querying and manipulating the contents of a database. LINQ to SQL provides a high level of abstraction, removing the need for you to worry about the details of constructing an ADO.NET Command object, iterating through a result set that a DataReader object returns, or fetching data column by column by using the various GetXXX methods.
Fetching Data The DataContext object supports LINQ queries by implementing the standard query operators such as Where and Select. You can also use the generic GetTable method of the DataContext class to retrieve data. This method expects an entity class as its TEntity type parameter, constructs an enumerable collection based on this type, and returns the collection as a Table type. You can perform LINQ to SQL queries over this collection. The query that is shown in the following code example retrieves every row from the Product table in a SQL Server database, and then displays the data in the ProductName and UnitPrice columns for each row.
15-24
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
[Visual Basic] Dim db As New DataContext(connectionString) Dim products As Table(Of Product) = db.GetTable(Of products)() Dim productsQuery = From p In products Select p For Each product In productsQuery Console.WriteLine( "ID: {0}, Name: {1}, Supplier: {2}, Price: {3:C}", product.ProductID, product.ProductName, product.SupplierID, product.UnitPrice) Next
[Visual C#] DataContext db = new DataContext(connectionString); Table products = db.GetTable(); var productsQuery = from p in products select p; foreach (var product in productsQuery) { Console.WriteLine( "ID: {0}, Name: {1}, Supplier: {2}, Price: {3:C}", product.ProductID, product.ProductName, product.SupplierID, product.UnitPrice); }
Remember that the DataContext object controls the database connection automatically; it opens the connection immediately before it fetches the first row of data in the foreach (C#) or For Each (Visual Basic) statement, and then closes the connection after the last row has been retrieved. Note: It is important to understand that the data is fetched only when you iterate over the result set that a LINQ to SQL query has generated, not when you define the query. If you iterate over data by using the same LINQ to SQL query two or more times in succession, the automatic connection management that the DataContext object implements means that you not receive the same data each time. When you finish iterating, the connection is closed, allowing another user to change data. A subsequent iteration will therefore see the updated data.
Using LINQ to SQL
15-25
Fetching a Single Row When the foreach statement runs, the DataContext object constructs a SQL SELECT statement that retrieves all of the data from the Product table and populates the products collection. If you want to retrieve a single row in the Product table, you can call the Single method of the Product entity class. Single is an extension method that itself takes a method that identifies the row that you want to find and returns this row as an instance of the entity class (as opposed to a collection of rows in a Table collection). You can specify the method parameter as a lambda expression. If the lambda expression does not identify exactly one row, the Single method returns an InvalidOperationException exception. The following code example fetches the product that has the ProductID value of 27. The value that is returned is an instance of the Product class, and the Console.WriteLine statement prints the name of the product. As before, the DataContext object opens and closes the database connection automatically. [Visual Basic] Dim singleProduct As Product = products.Single( Function(p) p.ProductID = 27) Console.WriteLine("Name: {0}", singleProduct.ProductName)
[Visual C#] Product singleProduct = products.Single(p => p.ProductID == 27); Console.WriteLine("Name: {0}", singleProduct.ProductName);
Querying Across Relationships An object model that is defined by using the O/R Designer will define relationships between entities in all but the simplest applications. These relationships are implemented as described earlier, by using the Association attribute. You can use these associations in your LINQ to SQL queries to join tables together. For example, given the relationship between the Department and Employee entities that are shown in the following entity classes, you can easily define a LINQ to SQL query that fetches the details of every department that matches a specified criterion, together with the employees who work in those departments. The data in the Employee EntitySet collection is retrieved automatically and is accessible through the Employees property.
15-26
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
[Visual Basic] _ Partial Public Class Department _ Public DepartmentID As String ' ... Private employees As EntitySet(Of Employee) _ Public Property Employees As EntitySet(Of Employee) Get Return Me.employees End Get Set(ByVal value As EntitySet(Of Employee)) Me.employees.Assign(value) End Set End Property End Class ... ' Instantiate db as a strongly-typed DataContext object ... Dim departmentQuery = From d In db.Department Where ... Select d ' Fetch department and employee data For Each dept In departmentQuery Console.WriteLine( "Dept: {0}", dept.DepartmentID) For Each emp In dept.Employees Console.WriteLine( "Employee name: {0}", emp.EmployeeName) Next Next
Using LINQ to SQL
15-27
[Visual C#] [Table(Name = "Department")] public partial class Department { [Column(IsPrimaryKey = true)] public string DepartmentID; // ... private EntitySet employees; [Association(Storage = "employees", OtherKey = "DepartmentID")] public EntitySet Employees { get { return this.employees; } set { this.employees.Assign(value); } } } ... // Instantiate db as a strongly-typed DataContext object ... var departmentQuery = from d in db.Department where ... select d; // Fetch department and employee data foreach (var dept in departmentQuery) { Console.WriteLine( "Dept: {0}, dept.DepartmentID); foreach (var emp in dept.Employees) { Console.WriteLine( "Employee name: {0}", emp.EmployeeName); } }
Question: What happens when you use the Single method to try to return a single row of data, but there are no matching rows in the database?
Additional Reading For information about how LINQ queries use generic types in the .NET Framework, see the LINQ and Generic Types (C#) page at http://go.microsoft.com/fwlink/?LinkID=194134.
15-28
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Making Changes and Updating a Database
Key Points LINQ to SQL is designed to offer maximum flexibility when manipulating and persisting changes to the object model. After you have instantiated a collection of entity objects, either by retrieving them through a query to the DataContext object or by constructing them manually, you can use them in the same way as any other .NET Framework classes by setting the values of their properties. You can also add new instances of an entity to a Table collection that is managed by the DataContext object by using the InsertOnSubmit method, or remove instances from a Table collection by using the DeleteOnSubmit method. However, these changes are only made to inmemory objects. You can save changes back to the database by using the SubmitChanges method of the DataContext object, as the following code example shows.
Using LINQ to SQL
15-29
[Visual Basic] Dim db As New CustomDataContext(connectionString) ' Fetch a product Dim singleProduct As Product = db.Products.Single( Function(p) p.ProductID = 27) ' Modify the product singleProduct.UnitPrice = singleProduct.UnitPrice * 1.1 ... ' Add a new product Dim product As New Product() ... ' Populate the new Product entity db.Products.InsertOnSubmit(product) ' Delete a product Dim productToRemove As Product = db.Products.Single( Function(p) p.ProductID = 30) db.Products.DeleteOnSubmit(productToRemove) ' Save the changes to the database db.SubmitChanges()
[Visual C#] CustomDataContext db = new CustomDataContext(connectionString); // Fetch a product Product singleProduct = db.Products.Single(p => p.ProductID == 27); // Modify the product singleProduct.UnitPrice *= 1.1; ... // Add a new product Product product = new Product(); ... // Populate the new Product entity db.Products.InsertOnSubmit(product); // Delete a product Product productToRemove = db.Products.Single(p => p.ProductID == 30); db.Products.DeleteOnSubmit(productToRemove) // Save the changes to the database db.SubmitChanges();
15-30
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
When you call SubmitChanges, the DataContext object translates the changes that are made to all objects that are associated with the DataContext object into the equivalent SQL commands for inserting, updating, or deleting rows in the database. Before updating the database, the DataContext object creates a transaction that encompasses the changes (unless a TransactionScope object is already in scope). All of the changes are performed in the context of this transaction; if any of the operations fail, the transaction is aborted and all of the changes that were made are rolled back. However, the DataContext object will retain the details of all of the changes that were made, so it is possible to attempt to correct the problem and resubmit them by calling SubmitChanges again. The following code example shows a typical pattern for calling the SubmitChanges method. [Visual Basic] Dim db As New CustomDataContext(connectionString) ' Make changes to entity classes ... ' Save the changes to the database Try db.SubmitChanges() Catch e As Exception ' Correct the errors ... ' Try again db.SubmitChanges() End Try
[Visual C#] CustomDataContext db = new CustomDataContext(connectionString); // Make changes to entity classes ... // Save the changes to the database try { db.SubmitChanges(); } catch (Exception e) { // Correct the errors ... // Try again db.SubmitChanges(); }
Using LINQ to SQL
15-31
Note: The DataContext class also provides the Transaction property, which you can use to associate the DataContext object with a DbTransaction object. For example, you can start your own SqlTransaction object and assign it to the Transaction property of the DataContext object. When the DataContext object saves changes, it will use this transaction instead of creating its own. You can commit or roll back the transaction yourself, depending on the outcome.
The DataContext class also provides the Refresh method. Using this method, you can repopulate Table collections from the database and discard any changes that you have made. The following code example shows how to use this method. [Visual Basic] db.Refresh(RefreshMode.OverwriteCurrentValues, db.Products)
[Visual C#] db.Refresh(RefreshMode.OverwriteCurrentValues, db.Products);
The first parameter is a member of the System.Data.Linq.RefreshMode enumeration. Specifying the RefreshMode.OverwriteCurrentValues value forces the data to be refreshed from the database. The second parameter is the table to be refreshed. The Refresh method can take a params array as its second parameter, so you can provide a whole list of tables if you need to refresh more than one. Question: What happens if a database error occurs when you call the SubmitChanges method and the DataContext object has already started to update the database?
Additional Reading For more information about how LINQ to SQL tracks the changes made to objects, see the Object States and Change-Tracking (LINQ to SQL) page at http://go.microsoft.com/fwlink/?LinkID=194135.
15-32
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Customizing Insert, Update, and Delete Operations by Using Stored Procedures
Key Points By default, the logic to perform insert, update, and delete operations is provided by the LINQ to SQL runtime, which generates the corresponding SQL INSERT, UPDATE, and DELETE commands. These are based on the schema of the table and the changes to entities that are recorded in the DataContext object. If you need to customize the processing, or perform more extensive work when inserting, updating, or deleting rows, you can incorporate this logic into stored procedures in the database. You can also add stored procedures to the O/R Designer. The O/R Designer creates a wrapper method for each one and makes them available to your code as DataContext methods. You can also request that the DataContext object uses stored procedures rather than generating its own set of commands when you call the SubmitChanges method to update the database. To use a stored procedure to insert, update, or delete a database, the stored procedure must provide parameters that you can use to pass the data for the fields being updated or inserted, or that identify the row to be deleted. You use the Configure Behavior dialog box in the O/R Designer to specify that the
Using LINQ to SQL
15-33
DataContext object should use a stored procedure to update an entity class, and specify how to map the fields in the entity class to the parameters that the stored procedure exposes.
f Use stored procedures to update data for an entity class 1.
Using the O/R Designer, in Server Explorer or Database Explorer, expand Stored Procedures, and then locate the stored procedures that you want to use for the Insert, Update, and Delete commands of the entity class.
2.
Drag the stored procedure onto the O/R Designer. The stored procedure will be added to the methods pane as a DataContext method.
3.
Select the entity class for which you want to use the stored procedure for performing updates.
4.
In the Properties window, select the command to override (Insert, Update, or Delete).
5.
Click the ellipsis (...) next to the text Use Runtime.
6.
In the Configure Behavior dialog box, click Customize.
7.
In the Customize list, select the stored procedure to use.
8.
For each method argument, specify how the argument maps to fields in the entity class.
Note: By default, method arguments map to class properties when the names match. If changed property names no longer match between the table and the entity class, you might have to select the equivalent class property to map to if the designer cannot determine the correct mapping.
9.
Click OK.
To revert to using the default runtime logic for updates, click the ellipsis next to the Insert, Update, or Delete command in the Properties window, and then in the Configure Behavior dialog box, select Use runtime. Question: What benefit is there in using stored procedures to override the default behavior of the entity class?
15-34
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Additional Reading For information about extending LINQ to SQL classes and customizing the code generated by LINQ to SQL projects, see the Adding Business Logic By Using Partial Methods (LINQ to SQL) page at http://go.microsoft.com/fwlink/?LinkID=194136.
Using LINQ to SQL
15-35
Lesson 2
Managing Performance and Handling Concurrency
LINQ to SQL provides a powerful mechanism for querying and maintaining relational data in an object-oriented manner, but this power can come at a cost in terms of the computing resources that are required or the time that is taken to perform database operations. This lesson describes some common techniques for ensuring the performance of an application that uses LINQ to SQL, and describes how to handle concurrency conflicts when two or more users attempt to update the same data simultaneously.
Objectives After completing this lesson, you will be able to: •
Improve throughput and consistency by fetching data immediately.
•
Improve performance by filtering data, using read-only data, and using compiled LINQ to SQL queries.
15-36
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
•
Monitor the SQL commands that LINQ to SQL performs, and the change sets that are generated when updating data.
•
Manage change conflicts when saving changes to a database.
Using LINQ to SQL
15-37
Improving Throughput and Consistency by Using Immediate Loading
Key Points By default, LINQ to SQL retrieves the data from the database only when you request it, not when you define a LINQ to SQL query or create a Table collection. This is known as deferred fetching. In the following code example, which retrieves all of the products from the Product table, the productsQuery collection is populated only when the loop that fetches the data runs. This mode of operation matches that of LINQ when querying in-memory objects; you will always see the most up-to-date version of the data, even if the data changes after you have run the statement that creates the productsQuery enumerable collection. [Visual Basic] Dim products As Table(Of Product) = db.GetTable(Of Product)() For Each product As Product In products ... Next
15-38
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
[Visual C#] Table products = db.GetTable(); foreach (Product product in products) { ... }
When the loop starts, LINQ to SQL creates and runs a SQL SELECT statement that is derived from the LINQ to SQL query to create an ADO.NET DataReader object. Each iteration of the loop performs the necessary GetXXX methods to fetch the data for that row. After the final row has been fetched and processed by the loop, LINQ to SQL closes the database connection. Deferred fetching ensures that only the data that an application actually uses is retrieved from the database. However, the default isolation level that LINQ to SQL uses is ReadCommitted, so it is possible that another user may change data that the LINQ to SQL query identifies. Consequently, if the application iterates over the same data twice, it may see inconsistencies. This might be important if you are building reporting applications that require a consistent view of data. In addition, if you are accessing a database that is running on a remote instance of SQL Server, fetching data row by row does not make the best use of network bandwidth. In this scenario, you can fetch and cache all of the data in a single network request by forcing immediate evaluation of the LINQ to SQL query. You can do this by calling the ToList or ToArray extension methods, which fetch the data into a list or array when you define the LINQ to SQL query, as the following code example shows. [Visual Basic] Dim productsQuery = From p In products.ToList() Select p
[Visual C#] var productsQuery = from p in products.ToList() select p;
In this code example, productsQuery is now an enumerable list, populated with information from the Product table. When you iterate over the data, LINQ to SQL retrieves it from this list rather than sending individual fetch requests to the database.
Using LINQ to SQL
15-39
Associations and Deferred Loading Using ToList or ToArray to fetch data immediately does not apply to data that is referenced as EntitySet or EntityRef properties; even if you use ToList or ToArray, the data will still be fetched only when it is accessed. If you want to force LINQ to SQL to query and fetch referenced data immediately, you can set the LoadOptions property of the DataContext object, as the following code example shows. [Visual Basic] Dim db As new DataContext(...) Dim departments As Table(Of Department) = db.GetTable(Of Department)() Dim loadOptions As New DataLoadOptions() loadOptions.LoadWith(Of Department)(Function(e) e.Employees) db.LoadOptions = loadOptions Dim departmentsAndEmployees = From d In departments Select d
[Visual C#] DataContext db = new DataContext(...); Table departments = db.GetTable(); DataLoadOptions loadOptions = new DataLoadOptions(); loadOptions.LoadWith(e => e.Employees); db.LoadOptions = loadOptions; var departmentsAndEmployees = from d in departments select d;
The DataLoadOptions class provides the generic LoadWith method. By using this method, you can specify whether an EntitySet property in an instance should be loaded when the instance is populated. The parameter to the LoadWith method is another method, which you can supply as a lambda expression. The example that is shown in the code example causes the Employees property of each Department entity to be populated as soon as the data for each Department entity is fetched, rather than being deferred. If you specify the LoadOptions property of the DataContext object together with the ToList or ToArray extension method of a Table collection, LINQ to SQL will load into memory the entire collection, in addition to the data for the referenced properties for the entities in that collection, as soon as the DLINQ query is evaluated.
15-40
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Question: How can immediate loading help prevent inconsistencies in data?
Additional Reading For more information about the differences between deferred and immediate loading, see the Deferred versus Immediate Loading (LINQ to SQL) page at http://go.microsoft.com/fwlink/?LinkID=194137.
Using LINQ to SQL
15-41
Improving Performance with LINQ to SQL
Key Points You can use several techniques to optimize the performance of an application that uses LINQ to SQL. These techniques include: •
Filtering data. Only fetch the data that the application requires.
•
Using a read-only DataContext object. If an application is not going to modify data, do not involve the overhead of maintaining tracking information for entities.
•
Storing and reusing queries. If an application performs the same query frequently, but with different data values, use parameters and reuse the compiled version of the query each time.
Filtering Data You can filter data as a DataContext object fetches it by using the AssociateWith method of the DataLoadOptions class. The AssociateWith method can take a Table as a type parameter, and a lambda expression that filters data that is fetched
15-42
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
when using this type. The filter is applied to all queries that are performed against the specified table by using the DataContext object. The following code example shows a filter that is applied to the Employee entity class. When the LINQ to SQL query is performed by using the DataContext object that has the filter applied, the list of Employee objects that is generated is automatically filtered to include only those employees whose start date is not the current date. [Visual Basic] Dim db As New CustomDataContext(...) Dim dlo As New DataLoadOptions() dlo.AssociateWith(Of Employee)(Function(e) e.EmployeeStartDate DateTime.Today) db.LoadOptions = dlo Dim employeeQuery = From emp In db.Employees Where emp.EmployeeID > 100 Select emp
[Visual C#] CustomDataContext db = new CustomDataContext(...); DataLoadOptions dlo = new DataLoadOptions(); dlo.AssociateWith(e => e.EmployeeStartDate != DateTime.Today); db.LoadOptions = dlo; var employeeQuery = from emp in db.Employees where emp.EmployeeID > 100 select emp;
Any subsequent queries that are performed by using the DataContext object and that involve the Employee entity class will be filtered in the same way.
Using a Read-Only DataContext object If your application does not intend to modify retrieved data, it is possible to increase the performance of LINQ to SQL queries by retrieving read-only results. This technique prevents the DataContext object from tracking changes that have been made to your objects and the associated overheads that this tracking involves.
Using LINQ to SQL
15-43
To implement read-only processing, set the ObjectTrackingEnabled property to false, as the following code example shows. [Visual Basic] Dim db As New CustomDataContext(...) db.ObjectTrackingEnabled = False
[Visual C#] CustomDataContext db = new CustomDataContext (...); db.ObjectTrackingEnabled = false;
Storing and Reusing Queries If your application repeats the same queries, but with different data values, you can use the static Compile method of the CompiledQuery class to generate the expression tree for the query. You can parameterize the compiled query to specify values to use each time the query runs. The following code example creates a static method called EmployeesByDepartment that provides a compiled query that retrieves all employees who work in a department specified as a parameter. [Visual Basic] Public Shared EmployeesByDepartment As Func(Of CustomDataContext, String, IQueryable(Of Employee)) = CompiledQuery.Compile(Function(db As CustomDataContext, dept As String) _ From e In db.Employees _ Where e.EmpDeptID = dept _ Select e)
[Visual C#] public static Func EmployeesByDepartment = CompiledQuery.Compile((CustomDataContext db, string dept) => from e in db.Employees where e.EmpDeptID == dept select e);
The parameters to the Compile method are the DataContext object and the data to use to filter the results.
15-44
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
The following code example shows how to use a LINQ to SQL query that is compiled by the EmployeesByDepartment method. [Visual Basic] Dim db As CustomDataContext = CustomDataContext(...) ... Dim employeesInIT = EmployeesByDepartment(db, "IT") ... Dim employeesInMarketing = EmployeesByDepartment(db, "Marketing") ...
[Visual C#] CustomDataContext db = CustomDataContext(...); ... var employeesInIT = EmployeesByDepartment(db, "IT"); ... var employeesInMarketing = EmployeesByDepartment(db, "Marketing"); ... }
Question: If you do not intend to modify the data that a DataContext query retrieves, what can you do to improve the performance of your application?
Additional Reading For more information about improving performance by caching objects, see the Retrieving Objects from the Identity Cache (LINQ to SQL) page at http://go.microsoft.com/fwlink/?LinkID=194138.
Using LINQ to SQL
15-45
Examining SQL Commands and Change Sets
Key Points To help understand how queries and updates are processed, and to assist with debugging, you can view the SQL commands that LINQ to SQL generates when it updates the database. The DataContext class provides the Log property, which you can use to monitor the commands that it generates. The Log property is a TextWriter object, and the DataContext object writes the commands to this object before it runs them. The following code example sets the Log property to Console.Out, which causes the SQL commands to be displayed to the console. You could equally specify a TextWriter object that writes to a file to capture data permanently. [Visual Basic] Dim db As New CustomDataContext(...) db.Log = Console.Out Dim empQuery As IQueryable(Of Employee) = From staff In db.Employees Where emp.Department = "Sales" Select emp
15-46
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
For Each empObj As Employee In empQuery Console.WriteLine(empObj.Name) Next
[Visual C#] CustomDataContext db = new CustomDataContext(...); db.Log = Console.Out; IQueryable empQuery = from staff in db.Employees where emp.Department =="Sales" select emp; foreach(Employee empObj in empQuery) { Console.WriteLine(empObj.Name); }
The previous code example generates the following output. The first section shows the data from the Log property. SELECT [t0].[EmployeeID], [t0].[Name], [t0].[Department] FROM [dbo].[Staff] AS [t0] WHERE [t0].[Department] = @p0 -- @p0: Input String (Size = 5; Prec = 0; Scale = 0) [Sales] -- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 4.0.21006.1 Michael Linda Jillian ...
You can also examine the DbCommand object that a DataContext object uses to perform an operation by using the GetCommand method. The following code example uses the GetCommand method to return the DbCommand object that is associated with a LINQ to SQL query. [Visual Basic] Dim db As New CustomDataContext(...) Dim q = From emp In db.Employees Where emp.EmployeeID > 100 Select emp
Using LINQ to SQL
15-47
Console.WriteLine("Employees:") For Each employee In q Console.WriteLine("\t {0}", employee.LoginID) Next Dim dc As DbCommand = db.GetCommand(q) Console.WriteLine("\nCommand Text: \n{0}", dc.CommandText) Console.WriteLine("\nCommand Type: {0}", dc.CommandType) Console.WriteLine("\nConnection: {0}", dc.Connection) Console.ReadLine()
[Visual C#] CustomDataContext db = new CustomDataContext(...); var q = from emp in db.Employees where emp.EmployeeID > 100 select emp; Console.WriteLine("Employees:"); foreach (var employee in q) { Console.WriteLine("\t {0}",employee.LoginID); } DbCommand dc = db.GetCommand(q); Console.WriteLine("\nCommand Text: \n{0}",dc.CommandText); Console.WriteLine("\nCommand Type: {0}",dc.CommandType); Console.WriteLine("\nConnection: {0}",dc.Connection); Console.ReadLine();
Examining Change Sets If you have modified, added, or deleted rows in an entity class collection that a DataContext object manages, you can examine the changes by using the GetChangeSet method of the DataContext class. This method returns a ChangeSet object. A ChangeSet object contains the following three read-only collection properties: •
Deletes. The entities to be deleted when the changes are saved.
•
Inserts. The entities to be added when the changes are saved.
15-48
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
•
Updates. The entities to be updated when the changes are saved.
The ChangeSet class also provides a useful ToString method that renders a summary of the changes that have been made. The following code example displays the changes that have been recorded in a ChangeSet class for a DataContext object. [Visual Basic] Dim db As New CustomDataContext(...) Dim q = From emp In db.Employees Where emp.EmployeeID > 100 Select emp ... db.Employees.InsertOnSubmit(...) db.Employees.DeleteOnSubmit(...) ... Dim cs As ChangeSet = db.GetChangeSet() Console.Write("Total changes: {0}", cs)
[Visual C#] CustomDataContext db = new CustomDataContext(...); var q = from emp in db.Employees where emp.EmployeeID > 100 select emp; ... db.Employees.InsertOnSubmit(...); db.Employees.DeleteOnSubmit(...); ... ChangeSet cs = db.GetChangeSet(); Console.Write("Total changes: {0}", cs);
Question: Why might you set the Log property of the DataContext class?
Using LINQ to SQL
15-49
Handling Change Conflicts
Key Points When you fetch data by using LINQ to SQL, the data is buffered in the memory of your application. You can modify this data and use the SubmitChanges method of the DataContext object to save the changes back to the database. In a multiuser environment, another user may have queried, modified, and saved the same data, which may result in a lost update condition. The SubmitChanges method detects this condition and raises a ChangeConflictException exception, which you should be prepared to handle. When a ChangeConflictException exception arises, you can determine the cause of the conflict by examining the ChangeConflicts property of the DataContext object. This property is a collection that contains ObjectChangeConflict objects, which contain information about the reason for each conflict. There are two important properties in the ObjectChangeConflict class: •
The IsDeleted property is a Boolean value that indicates whether the conflict was caused by another user deleting the row that you were attempting to update.
15-50
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
•
The MemberConflicts property is a read-only collection of MemberChangeConflict objects.
The MemberChangeConflict class contains a further set of properties, including the current value of the data in your application, the current value of the data in the database, and the original value that you retrieved from the database. If you detect conflicts when you perform the SubmitChanges method, your application can examine the reason for each conflict and determine how to handle it. Depending on the nature of the application, you could even present information about the conflict to the user and let the user choose. To help you correct the problems that a conflict has caused, the ObjectChangeConflict class contains a method called Resolve. For each conflict in the ChangeConflicts collection property of the DataContext object, you can call the Resolve method and pass in a parameter that indicates your preferred resolution strategy. This parameter should be a member of the RefreshMode enumeration. You can specify the following values: •
RefreshMode.KeepCurrentValues. This value indicates that the data in memory should overwrite the conflicting changes in the database—the current user is the winner of the conflict.
•
RefreshMode.OverwriteCurrentValues. This value indicates that the data in the database should be used. The conflicting changes in memory will be overwritten with the values from the database—the current user is the loser of the conflict.
•
RefreshMode.KeepChanges. This value specifies what happens if two users update different columns in the same row. In this case, the changes that the other user has made to the other columns are merged with the changes that the current user has made in memory—both users are winners of the conflict.
The following code example shows a ChangeConflictException handler that displays conflicting data and resolves the conflict by using the RefreshMode.OverwriteCurrentValues option. [Visual Basic] Try db.SubmitChanges() Catch ex As ChangeConflictException For Each conflict As ObjectChangeConflict In db.ChangeConflicts
Using LINQ to SQL
For Each changeConflict As MemberChangeConflict In conflict.MemberConflicts Console.WriteLine("Conflict Details") Console.WriteLine( "Original value retrieved from database: {0}", changeConflict.OriginalValue.ToString()) Console.WriteLine("Current value in database: {0}", changeConflict.DatabaseValue.ToString()) Console.WriteLine("Current value in memory: {0}", changeConflict.CurrentValue.ToString()) Next conflict.Resolve(RefreshMode.OverwriteCurrentValues) Next End Try
[Visual C#] CustomDataContext db = new CustomDataContext(...); ... try { db.SubmitChanges(); } catch (ChangeConflictException) { foreach (ObjectChangeConflict conflict in db.ChangeConflicts) { foreach (MemberChangeConflict changeConflict in conflict.MemberConflicts) { Console.WriteLine("Conflict Details"); Console.WriteLine( "Original value retrieved from database: {0}", changeConflict.OriginalValue.ToString()); Console.WriteLine("Current value in database: {0}", changeConflict.DatabaseValue.ToString()); Console.WriteLine("Current value in memory: {0}", changeConflict.CurrentValue.ToString()); } conflict.Resolve(RefreshMode.OverwriteCurrentValues); } }
15-51
15-52
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Note: The ChangeConflicts collection of the DataContext class provides the ResolveAll method that lets you apply the same RefreshMode value to resolve all conflicts.
When you have resolved the conflicts, you should call the SubmitChanges method again to resubmit your changes. There is one potential issue with this technique as it currently stands: if the user has updated several rows, there could be more than one conflict. The ChangeConflictException exception is thrown the first time that a conflict is detected. You can handle it in the manner just described, but only one ObjectChangeConflict object will be set in the ChangeConflicts collection. When you call SubmitChanges again to send the resolved update to the database, another ChangeConflictException exception for the next conflict will arise, which you have to detect and handle. To help you, the SubmitChanges method is overloaded, so you can specify how to handle the ChangeConflictException exception. Calling SubmitChanges with a parameter value of ConflictMode.ContinueOnConflict indicates that the SubmitChanges method should try to perform all of the updates and only throw the ChangeConflictException exception at the end if one or more conflicts have occurred. The following code example shows how to call the overloaded method. [Visual Basic] db.SubmitChanges(ConflictMode.ContinueOnConflict)
[Visual C#] db.SubmitChanges(ConflictMode.ContinueOnConflict);
The code in your ChangeConflictException handler can then iterate through all of the items in the ObjectChangeConflict property of the DataContext object and resolve them all before calling SubmitChanges again. When you call SubmitChanges, you can also specify the parameter value of ConflictMode.FailOnFirstConflict. This is the default behavior and raises a ChangeConflictException exception as soon as the first conflict is detected. Question: Your application raises a ChangeConflictException exception. How can you determine the cause of the conflict?
Using LINQ to SQL
15-53
Additional Reading For more information about handling conflicting changes, see the How to: Detect and Resolve Conflicting Submissions (LINQ to SQL) page at http://go.microsoft.com/fwlink/?LinkID=194139.
15-54
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Lab: Using LINQ to SQL
Objectives After completing this lab, you will be able to: •
Use LINQ to SQL to build a data access layer.
•
Incorporate a stored procedure into a LINQ to SQL model.
•
Create a LINQ to SQL entity class that implements custom logic for presenting and validating data.
Introduction In this lab, you will develop a data access layer by using LINQ to SQL to fetch and manage data. You will use the O/R Designer to implement the object model. You will modify the object model to use a stored procedure to update data. Finally, you will create a custom entity class that provides additional logic for displaying and validating data.
Using LINQ to SQL
15-55
Lab Setup For this lab, you will use the available virtual machine environment. Before you begin the lab, you must: •
Start the 10265A-GEN-DEV-15 virtual machine, and then log on by using the following credentials: •
User name: Student
•
Password: Pa$$w0rd
15-56
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Lab Scenario
You decide that, instead of using ADO.NET for the applications that you developed in the previous lab, you should use a LINQ to SQL object model to decouple the database structure from the logical data model that your application uses.
Exercise 1: Using LINQ to SQL to Build a Data Access Layer Scenario In this exercise, you will use the O/R Designer to create a LINQ to SQL object model based on the Product table in the AdventureWorks database. You will modify the data access layer to use this object model rather than ADO.NET code. You will test the modified version of the data access layer by using the Web and Windows® Forms applications that you used in the previous lab. The main tasks for this exercise are as follows: 1.
Prepare the AdventureWorks database for the lab.
2.
Open the starter project for this exercise.
3.
Create the LINQ to SQL object model.
Using LINQ to SQL
4.
Implement the methods that retrieve data in the data access layer.
5.
Implement the methods that update data in the data access layer.
6.
Test the data access layer.
15-57
f Task 1: Prepare the AdventureWorks database for the lab 1.
Log on to the 10265A-GEN-DEV-15 virtual machine as Student with the password Pa$$w0rd.
2.
Run AWReset.bat in the E:\Labfiles folder.
f Task 2: Open the starter project for this exercise 1.
Open Visual Studio 2010.
2.
Open the existing solution, Using ADO.NET.sln, in the E:\Labfiles\Lab15\CS\Ex1\Starter\Using ADO.NET or E:\Labfiles\Lab15\VB\Ex1\Starter\Using ADO.NET folder.
f Task 3: Create the LINQ to SQL object model 1.
Add a LINQ to SQL object model called ProductDataModel.dbml to the DAL solution.
2.
Use Server Explorer to create a connection to the AdventureWorks database on the 10265A-GEN-DEV\SQLEXPRESS computer.
3.
Add the Product (Production) entity to the LINQ to SQL object model.
4.
In the O/R Designer, rename the Product entity as ProductDataObject.
5.
Remove all of the columns from the ProductDataObject entity except for the following: •
ProductID
•
Name
•
ProductNumber
•
Color
•
ListPrice
•
ModifiedDate
15-58
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
6.
Examine the code that the O/R Designer generates.
Note: If there is no option to expand ProductDataModel.dbml, ensure that the Show All Files button is switched on in Solution Explorer.
f Task 4: Implement the methods that retrieve data in the data access layer 1.
In Solution Explorer, in the DAL project, open the ProductDataAccessLayer code file.
2.
In the IProductDataAccessLayer Members region, implement the GetProductList method to use LINQ to SQL to retrieve all products from the database, populate a list of ProductDataObject objects, and then return this list. The code should perform the following tasks: a.
Create a new instance of the ProductDataModelDataContext object. This is a custom DataContext object that automatically connects to the AdventureWorks database.
Hint: Create the ProductDataModelDataContext object in a using statement and add the remaining code in this method to the body of the using statement. This will ensure that the ProductDataModelDataContext object is correctly disposed and its resources released when the method completes.
b.
3.
Retrieve the ProductDataObjects collection from the ProductDataModelDataContext object, convert it into a list of ProductDataObject objects, and then return this list.
Implement the GetProduct method. Use LINQ to SQL to retrieve the product with the product ID that matches the value that is passed as a parameter to this method, and then return this product. The code should perform the following tasks: a.
Create a new instance of the ProductDataModelDataContext object.
b.
Using the ProductDataObjects collection in the ProductDataModelDataContext object, find the product that matches the product ID that is specified as the parameter, and then return it.
Using LINQ to SQL
4.
15-59
Implement the GetProductList method that takes a string parameter called color. Use LINQ to SQL to retrieve a list of products with the specified color. The code should perform the following tasks:
5.
a.
Create a new instance of the ProductDataModelDataContext object.
b.
Define a LINQ query that finds all of the products that have the specified color from the ProductDataObjects collection in the ProductDataModelDataContext object. The color is specified as a string, so perform a case-insensitive match.
c.
Retrieve all of the matching products into a list, and then return this list.
Implement the GetProductList method that takes a decimal parameter called maxListPrice. Use LINQ to SQL to retrieve a list of products that have a list price that is less than or equal to this value. The code should perform the following tasks: a.
Create a new instance of the ProductDataModelDataContext object.
b.
Define a LINQ query that finds all of the products that have a price that falls within the specified range from the ProductDataObjects collection in the ProductDataModelDataContext object.
c.
Retrieve all of the matching products into a list, and then return this list.
f Task 5: Implement the methods that update data in the data access layer 1.
At the top of the ProductDataAccessLayer code file, bring the System.Data.Linq namespace into scope.
2.
Implement the UpdateProduct method. This method updates the database with the product information passed in as the parameter. The method returns true if the update is successful; otherwise, it returns false. The code should perform the following tasks: a.
Create a new instance of the ProductDataModelDataContext object.
b.
Retrieve the existing data for the product from the database from the ProductDataObjects collection of the ProductDataModelDataContext object.
c.
If this product still exists in the database, overwrite the data in the product entity object that you have just retrieved with the data in the product
15-60
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
object passed in as the method parameter. This action causes the changes to be recorded and tracked by the ProductDataObjects collection in the ProductDataModelDataContext object used to retrieve the product. If the product has been removed from the database, throw an exception with the message "The product has already been deleted. Reload the product list." Hint: Use the Single extension method of the ProductDataObjects collection to find the product in the collection. This method throws an InvalidOperationException exception if no matching product is found.
d. Using the ProductDataModelDataContext object, save the updated product back to the database. Specify that the transaction should roll back the first time that a concurrency conflict is detected.
3.
e.
If a ChangeConflictException exception occurs, iterate through all of the change conflict errors that are reported, resolve them by overwriting the changes made by the user with the latest data from the database, and then rethrow the exception.
f.
If the update is successful, return the value true. Otherwise, return the value false.
Implement the DeleteProduct method. This method removes the product passed in as the parameter from the database. The method returns true if the deletion is successful; otherwise, it returns false. The code should perform the following tasks: a.
Create a new instance of the ProductDataModelDataContext object.
b.
Retrieve the existing data for the product from the database from the ProductDataObjects collection of the ProductDataModelDataContext object.
c.
If this product still exists in the database, mark the product for deletion in the ProductDataObjects collection. If the product has already been removed from the database, throw an exception with the message "The product has already been deleted. Reload the product list."
d. Using the ProductDataModelDataContext object, save the updated product back to the database. Specify that the transaction should roll back the first time that a concurrency conflict is detected.
Using LINQ to SQL
4.
15-61
e.
If a ChangeConflictException exception occurs, iterate through all of the change conflict errors that are reported, resolve them by forcibly deleting the conflicting rows from the database, and then rethrow the exception.
f.
If the deletion is successful, return the value true; otherwise, return the value false.
Build the solution and correct any errors.
f Task 6: Test the data access layer 1.
Start the test application that is provided with the solution in Debug mode. This application is the Web application that you used in the previous lab.
2.
In the test application, click Browse all Products. Verify that the Products screen appears displaying a list of products from the database. This operation uses the GetProductList method in the data access layer.
3.
Click the Details link adjacent to a product, and then verify that the details for that product are displayed. This operation uses the GetProduct method in the data access layer.
4.
Click the Home link.
5.
In the Browse Products by Color box, type Black and then click Browse. Verify that a list of black products is displayed. This operation uses the overloaded GetProductList method that takes a string parameter in the data access layer.
6.
Click the Home link.
7.
In the Browse Products by Maximum Price box, type 25 and then click Browse. Verify that a list of products that have a list price less than or equal to 25 is displayed. This operation uses the overloaded GetProductList method that takes a decimal parameter in the data access layer.
8.
Close the test application and return to Visual Studio.
9.
Start the Windows Forms test application in Debug mode. This application is the Windows Forms application that you used in the previous lab.
10. In the Data Set Test Application window, click Load All Products. Verify that the data grid is populated with the details of products from the database.
15-62
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
This operation uses the GetProductList method in the data access layer. 11. In the Product ID box, type 316 and then click Search By Product. Verify that the details of product 316 are displayed. This operation uses the GetProduct method in the data access layer. 12. In the Color box, type Black and then click Search By Color. Verify that a list of black products is displayed. This should be the same list of products that was displayed by the Web application earlier. This operation uses the overloaded GetProductList method that takes a string parameter in the data access layer. 13. In the Maximum Price box, type 25 and then click Search By Price. Verify that a list of products with a price less than or equal to 25 is displayed. This operation uses the overloaded GetProductList method that takes a decimal parameter in the data access layer. 14. Click Delete Selected to remove the first item displayed in the data grid. 15. Change the Color field of any other product displayed in the data grid. 16. Click Save Changes. This button saves the changes to the database and then redisplays the entire list of products. Verify that the changes are saved and no exceptions are thrown. This operation uses the UpdateProduct and DeleteProduct methods in the data access layer. 17. Close the Data Set Test Application window, and then return to Visual Studio. 18. The DAL Unit Tests project contains the same unit tests that you used in the previous lab. Run all of the unit tests and verify that they all pass. 19. Close the solution.
Exercise 2: Updating a Database by Using a Stored Procedure Scenario In this exercise, you will modify the object model to use a stored procedure to update the Product table in the database. You will configure the ProductDataObject entity class to invoke this stored procedure when the DataContext object submits changes to the database.
Using LINQ to SQL
15-63
The main tasks for this exercise are as follows: 1.
Create the database objects for this exercise.
2.
Open the starter project for this exercise.
3.
Use a stored procedure to update data.
4.
Test the data access layer.
f Task 1: Create the database objects for this exercise 1.
Open the CustomUpdateProcedure.sql file in the E:\Labfiles\Lab15\CS\Ex2\SQL or E:\Labfiles\Lab15\VB\Ex2\SQL folder. This script adds a new table called ProductChangeHistory to the AdventureWorks database, and creates a stored procedure called productUpdateProduct. This stored procedure takes parameters that correspond to the columns in the Product entity class and uses them to update the Product table in the database. The stored procedure also adds a row to the ProductChangeHistory table, recording an audit trail of changes made to products.
2.
Run the script. Connect to the 10265A-GEN-DEV\SQLExpress SQL Server instance when prompted.
3.
Close the CustomUpdateProcedure.sql file.
f Task 2: Open the starter project for this exercise •
Open the existing solution, Using ADO.NET.sln, in the E:\Labfiles\Lab15\CS\Ex2\Starter\Using ADO.NET or E:\Labfiles\Lab15\VB\Ex2\Starter\Using ADO.NET folder. This solution is a copy of the completed solution from Exercise 1.
f Task 3: Use a stored procedure to update data 1.
Add the productUpdateProduct stored procedure in the AdventureWorks database to the LINQ to SQL object model. This action adds a method called productUpdateProduct to the DataContext object for the LINQ to SQL object model that an application can use to run the stored procedure.
15-64
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
2.
3.
Modify the ProductDataObject entity class to use the productUpdateProduct stored procedure to update data instead of using a SQL UPDATE statement by performing the following tasks: •
Configure the Update property of the ProductDataObject entity class and customize the update behavior to use the stored procedure.
•
Map the current values of the fields in the ProductDataObject entity class to the parameters passed to the stored procedure.
Build the solution and correct any errors.
f Task 4: Test the data access layer 1.
Open the Transact-SQL editor, and then connect to the 10265A-GENDEV\SQLExpress SQL Server instance.
2.
Query the Production.ProductChangeHistory table in the AdventureWorks database and verify that it contains no rows.
3.
Start the Windows Forms test application in Debug mode.
4.
In the Data Set Test Application window, click Load All Products.
5.
Change the Color field of any other product displayed in the data grid.
6.
Click Save Changes. Verify that the changes are saved and no exceptions are thrown.
7.
Close the Data Set Test Application window, and then return to Visual Studio.
8.
In the Transact-SQL Editor window, requery the Production.ProductChangeHistory table in the AdventureWorks database. The data displayed should include the original data for the product that you changed (the value in the Color column should be the original color).
9.
Close the solution.
Exercise 3: Building a Custom Entity Class Scenario In this exercise, you will replace the LINQ to SQL object model with a custom version of the ProductDataObject entity class that incorporates additional functionality for displaying error information when it is used as part of a user interface. The ProductDataObject class will implement the IDataErrorInfo
Using LINQ to SQL
15-65
interface, which provides custom error information that a user interface can bind to. The main tasks for this exercise are as follows: 1.
Open the starter project for this exercise.
2.
Create the entity class.
3.
Test the entity class.
f Task 1: Open the starter project for this exercise •
Open the existing solution, Using ADO.NET.sln, in the E:\Labfiles\Lab15\CS\Ex3\Starter\Using ADO.NET or E:\Labfiles\Lab15\VB\Ex3\Starter\Using ADO.NET folder. This solution contains a version of the data access layer that was not built by using an entity class or DataContext object that the O/R Designer generated. The methods in the data access layer use an ordinary DataContext object to manage collections of ProductDataObject entity objects. The ProductDataObject entity class has not yet been defined. You will implement this class in this exercise.
f Task 2: Create the entity class 1.
Add a new class called ProductDataObject to the DAL project.
2.
At the top of the ProductDataObject code file, bring the System.Data.Linq.Mapping and System.ComponentModel namespaces into scope.
3.
Mark the ProductDataObject class as an entity class that is associated with the AdventureWorks.Production.Product table.
4.
Make the ProductDataObject class public, and then specify that it implements the IDataErrorInfo interface.
5.
In the ProductDataObject class, add the following private fields: a.
An integer field called productID.
b.
A string field called name.
c.
A string field called productNumber.
d. A string field called color.
15-66
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
e.
A decimal field called listPrice.
f.
A DateTime field called modifiedDate.
6.
Add a property called ProductID that provides read and write access to the productID field. Mark this property as a primary-key column that does not allow null values. The name of the column in the database is ProductID.
7.
Add a property called Name that provides read and write access to the name field. In the get accessor, return the value of name in uppercase. Mark this property as a column that does not allow null values. The name of the column in the database is Name.
8.
Add a property called ProductNumber that provides read and write access to the productNumber field. Mark this property as a column that does not allow null values. The name of the column in the database is ProductNumber.
9.
Add a property called Color that provides read and write access to the color field. In the get accessor, return the value of color in uppercase if it is not null, but return a null value otherwise. In the set accessor, if the value specified is not null, convert it to uppercase before assigning it to the color field; otherwise, assign an empty string to the color field. Mark the property as a column that allows null values. The name of the column in the database is Color.
10. Add a property called ListPrice that provides read and write access to the listPrice field. Mark the property as a column that does not allow null values. The name of the column in the database is ListPrice. 11. Add a property called ModifiedDate that provides read and write access to the modifiedDate field. Mark this property as a column that does not allow null values. The name of the column in the database is ModifiedDate. 12. Add the indexer in the following code example to the ProductDataObject class. This indexer is part of the IDataErrorInfo interface. The get accessor takes the name of a column as a parameter, and returns a string that contains an error message if the specified column contains invalid data. The indexer returns an error message under the following circumstances: •
If the Name column is null or empty.
•
If the ProductNumber column is null or empty.
•
If the ListPrice column is less than 10.
Using LINQ to SQL
15-67
[Visual Basic] _ Public Class ProductDataObject Implements IDataErrorInfo ... #Region "IDataErrorInfo Members" Public Default ReadOnly Property Item( ByVal columnName As String) As String _ Implements IDataErrorInfo.Item Get If (columnName = "Name") AndAlso [String].IsNullOrEmpty(Name) Then Return "Name cannot be null" End If If (columnName = "ProductNumber") AndAlso [String].IsNullOrEmpty(ProductNumber) Then Return "Product Number cannot be null" End If If (columnName = "ListPrice") AndAlso listPrice < 10 Then Return "List Price must be at least 10.00" End If Return Nothing End Get End Property #End Region End Class
15-68
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
[Visual C#] [Table(Name = "AdventureWorks.Production.Product")] public class ProductDataObject : IDataErrorInfo { ... #region IDataErrorInfo Members public string this[string columnName] { get { if ((columnName == "Name") && String.IsNullOrEmpty(Name)) { return "Name cannot be null"; }
if ((columnName == "ProductNumber") && String.IsNullOrEmpty(ProductNumber)) { return "Product Number cannot be null"; }
if ((columnName == "ListPrice") && listPrice < 10) { return "List Price must be at least 10.00"; } return null; } } #endregion }
13. Add the Error property in the following code example to the ProductDataObject class. This property is also part of the IDataErrorInfo interface. This property is used to return an error message for the object. The ProductDataObject class does not require this data, so the property simply returns a null value.
Using LINQ to SQL
[Visual Basic] [Table(Name = "AdventureWorks.Production.Product")] public class ProductDataObject : IDataErrorInfo { ... #region IDataErrorInfo Members ... Public ReadOnly Property [Error]() As String _ Implements IDataErrorInfo.Error Get Return Nothing ' Not required End Get End Property #endregion }
[Visual C#] [Table(Name = "AdventureWorks.Production.Product")] public class ProductDataObject : IDataErrorInfo { ... #region IDataErrorInfo Members ... public string Error { get { return null; } // Not required } #endregion }
14. Build the solution and correct any errors.
f Task 3: Test the entity class 1.
Start the Windows Forms test application in Debug mode.
2.
In the Data Set Test Application window, click Load All Products.
15-69
15-70
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Verify that the Name and Color columns are converted to uppercase and that there are errors because the List Price of several products is less than 10. 3.
Modify a List Price so that it is above 10.00, and then click Save Changes. Verify that the error disappears.
4.
Change the value that you just modified back to 0.00, and then click Save Changes. Notice that the error returns.
5.
Close the Data Set Test Application window.
6.
Close Visual Studio.
Using LINQ to SQL
15-71
Lab Review
Review Questions 1.
How do you indicate that a field in an entity class maps to a column that allows null values?
2.
What is the purpose of the DataContext object?
3.
Which method can you use to examine any pending changes in objects that the DataContext object manages?
4.
What is the effect of setting the value of ObjectTrackingEnabled on the DataContext object to false?
15-72
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Module Review and Takeaways
Review Questions 1.
At what point does LINQ to SQL initiate the database connection and execute a query?
2.
How do you delete an entity class from the DataContext object and persist the change to the database?
3.
If you need to perform additional processing when you are inserting data into the database, what should you do?
Best Practices Related to Querying and Modifying Data by Using LINQ to SQL Supplement or modify the following best practices for your own work situations: •
You should always use a strongly typed DataContext class if possible.
•
You should only ever retrieve the data that your application needs.
Using LINQ to SQL
15-73
•
You should always be prepared to catch and handle database exceptions that the SubmitChanges method of the DataContext object raises.
•
You should add any validation or additional business logic to the partial classes that the object model provides.
15-74
Developing Data Access Solutions with Microsoft® Visual Studio® 2010
Course Evaluation
Your evaluation of this course will help Microsoft understand the quality of your learning experience. Please work with your training provider to access the course evaluation form. Microsoft will keep your answers to this survey private and confidential and will use your responses to improve your future learning experience. Your open and honest feedback is valuable and appreciated.
Lab 1: Analyzing Data Access Scenarios
L1-1
Introduction to Data Access Technologies
Lab 1: Analyzing Data Access Scenarios Exercise 1: Identifying Data Access Technologies Task 1: Identify the appropriate data access technology for a customer management application Scenario Adventure Works Cycles has a corporate database that contains customer information. Employees can browse and maintain customer data, but customers only have read access to the data. Employees use a Windows® Presentation Foundation (WPF) application to access their required data and the corporate network has no bandwidth issues. The corporate database is several years old and changes are made to the database structure twice a year. •
Given the scenario above, on a piece of paper, write down what you think is the most appropriate data access technology to solve the business problem: •
The data that is used in this application may be required in other applications in the future, so you decide to create the data access layer as an independent assembly that the WPF application consumes. You will use the Entity Framework to build the data access layer to insulate the logic and the user interface against future changes in the database structure. The entities that the data access layer uses will be initially created in the ADO.NET Entity Data Model Designer (Entity Designer) by importing the relevant database tables into a new model. You can then customize and extend the model for additional business requirements. You will use Language-Integrated Query (LINQ) to Entities, Entity SQL, or Entity Client for data access.
L1-2
Introduction to Data Access Technologies
Task 2: Identify the appropriate data access technology for an order management application Scenario Adventure Works Cycles has a requirement to enable salespeople to view and create orders during offsite meetings and add them to the database at a later time. Therefore, the data access layer needs to copy database content on the remote device to the server. •
Given the scenario above, on a piece of paper, write down what you think is the most appropriate data access technology to solve the business problem: •
In this scenario, there will be many users who need to access a central database from various locations and devices. Therefore, the data access layer will again reside in a separate tier on a server machine, enabling concurrent access by multiple client applications. The data access tier should be modeled by using an Entity Data Model (EDM) to insulate the design from the underlying database. You could use the mapping features of the Entity Framework to create the database, using LINQ to Entities to access it. If salespeople require read-only data, you could cache the data as local XML files to be read by using LINQ to XML. If salespeople need to create orders while they are disconnected from the data access tier, you could cache data in a local Microsoft® SQL Server® database and synchronize this with the server by using the Microsoft Sync Framework when the salespeople are reconnected later.
Task 3: Identify the appropriate data access technology for a delivery management application Scenario Adventure Works Cycles has agreed to provide an ASP.NET Model-View-Controller (MVC) Web application to delivery companies to query and maintain the delivery status of orders as they ship them. The application has to provide fast and responsive access to the database in a potentially low-bandwidth environment. Adventure Works Cycles has to produce a highly robust data access layer for this application in a very compressed time scale. •
Given the scenario above, on a piece of paper, write down what you think is the most appropriate data access technology to solve the business problem:
Lab 1: Analyzing Data Access Scenarios
•
L1-3
There will potentially be many users who need to access a central database from different locations. Therefore, you decide to develop a data access layer that resides in a separate tier on a server machine, allowing concurrent access by multiple client applications. The data access tier is best implemented by using WCF Data Services. In this way, the data service can be quickly developed and easily consumed by the Web application and any future Windows applications that may be developed.
Task 4: Identify the appropriate data access technology for a product management application Scenario Adventure Works Cycles previously developed what are now legacy applications that enable employees to browse and maintain a list of products, and enable customers to browse a list of products. The employee application is a Windows Forms application, and the customer application is a Web application. Both applications were built by using the Microsoft .NET Framework 2.0 or earlier. The underlying database structure is stable and has not been changed since it was first designed. Employees have no bandwidth limitations, although customers may do. 1.
Given the scenario above, on a piece of paper, write down what you think is the most appropriate data access technology to solve the business problem: •
2.
Two different applications will be using the data access layer, so you decide to implement the data access layer as a separate assembly that is distributed to both client applications. You also decide to implement the data access layer by using ADO.NET so that it supports the existing .NET Framework 2.0 applications and enables the data access layer to operate on the .NET Framework 4 with minimum changes. Using ADO.NET directly is also useful for building a solution that requires the fastest possible data access to a stable database model.
Discuss the benefits and drawbacks of each of your solutions with one of the other students.
Lab 2: Using Entity Data Models
L2-1
Building Entity Data Models
Lab 2: Using Entity Data Models Exercise 1: Generating an EDM from AdventureWorks Task 1: Prepare the AdventureWorks database for the lab 1.
Log on to the 10265A-GEN-DEV-02 virtual machine as Student with the password Pa$$w0rd.
2.
In the E:\Labfiles folder, run AWReset.bat: a.
Click Start, and then click Computer.
b.
Double-click Allfiles (E:), double-click Labfiles, and then double-click AWReset.bat.
c.
Wait for the batch file to finish running, and then close Windows® Explorer.
Task 2: Open the starter project 1.
Open Microsoft® Visual Studio® 2010: •
2.
Click Start, click All Programs, click Microsoft Visual Studio 2010, and then click Microsoft Visual Studio 2010.
Open the existing solution, DAL.sln, in the E:\Labfiles\Lab02\VB\Ex1\Starter\DAL or E:\Labfiles\Lab02\CS\Ex1\Starter\DAL folder: a.
In Visual Studio, on the File menu, point to Open, and then click Project/Solution.
b.
If you are using Microsoft Visual Basic®, in the Open Project dialog box, move to the E:\Labfiles\Lab02\VB\Ex1\Starter\DAL folder, click DAL.sln, and then click Open.
c.
If you are using Microsoft Visual C#®, in the Open Project dialog box, move to the E:\Labfiles\Lab02\CS\Ex1\Starter\DAL folder, click DAL.sln, and then click Open.
L2-2
Building Entity Data Models
Task 3: Create the AdventureWorks Entity Data Model •
Add a new ADO.NET Entity Data Model (EDM) to the DAL project. Generate the EDM from the AdventureWorks Microsoft SQL Server® database and create entities for the Contact, SalesOrderHeader, SalesTerritory, and StoreContact tables: a.
In Solution Explorer, right-click DAL, point to Add, and then click New Item.
b.
In the Add New Item - DAL dialog box, in the templates list, click ADO.NET Entity Data Model, in the Name box, type AdventureWorksEDM and then click Add.
c.
In the Entity Data Model Wizard, on the Choose Model Contents page, click Generate from database, and then click Next.
d. On the Choose Your Data Connection page, click New Connection. e.
In the Choose Data Source dialog box, in the Data source list, click Microsoft SQL Server, and then click Continue.
f.
In the Connection Properties dialog box, in the Server name box, type 10265A-GEN-DEV\SQLExpress
g.
In the Select or enter a database name box, enter AdventureWorks and then click OK.
h.
On the Choose Your Data Connection page, click Next.
i.
On the Choose Your Database Objects page, expand Tables, select the Contact (Person), SalesOrderHeader (Sales), SalesTerritory (Sales), and StoreContact (Sales) check boxes, and then click Finish.
Task 4: Review the AdventureWorks model 1.
Review the four entities shown in the Entity Designer pane and the associations between them: a.
In the Entity Designer pane, click the Contact entity, and then review the properties of the entity.
b.
Click the StoreContact entity, and then review the properties of the entity.
c.
Double-click the dotted line between the Contact and StoreContact entities.
Lab 2: Using Entity Data Models
L2-3
d. In the Referential Constraint dialog box, review the information, and then click Cancel. e. 2.
3.
Review the other entities and relationships in a similar way.
Open the Mapping Details pane and review the mappings for each entity in the model: a.
On the View menu, point to Other Windows, and then click Entity Data Model Mapping Details.
b.
In the Entity Designer pane, click an entity, and in the Mapping Details pane, review the mappings for the entity.
Close the Entity Designer pane: •
On the File menu, click Close.
Task 5: Modify the SalesTerritory entity by using XML 1.
2.
3.
Open the AdventureWorksEDM EDM in the XML Editor: a.
In Solution Explorer, right-click AdventureWorksEDM.edmx, and then click Open With.
b.
In the Open With - AdventureWorksEDM.edmx dialog box, click XML (Text) Editor, and then click OK.
Locate the conceptual schema definition language (CSDL) section of the model: a.
On the Edit menu, point to Find and Replace, and then click Quick Find.
b.
In the Find and Replace dialog box, in the Find what box, type CSDL and then click Find Next.
c.
Close the Find and Replace dialog box.
Locate the SalesTerritory entity in the CSDL section of the model: •
Scroll through the CSDL section until you find the following line of code.
4.
Find the Name property, and then change its Name attribute to TerritoryName: a.
Scroll through the CSDL section until you find the following line of code.
L2-4
Building Entity Data Models
b.
Change the Name attribute to TerritoryName. Your code should resemble the following code example.
5.
6.
Save the model and close the XML Editor window: a.
On the File menu, click Save AdventureWorksEDM.edmx.
b.
On the File menu, click Close.
Open AdventureWorksEDM.edmx in the Entity Designer pane, verify that the change has been made, and then change the property back to Name: a.
In Solution Explorer, right-click AdventureWorksEDM.edmx, and then click Open.
b.
Examine the SalesTerritory entity and verify that the second property in the list is now called TerritoryName.
c.
Right-click TerritoryName, and then click Rename.
d. Type Name and then press ENTER. 7.
Save and close the solution: a.
On the File menu, click Save All.
b.
On the File menu, click Close Solution.
Exercise 2: Adding Entities and Associations Task 1: Open the starter project •
Open the existing solution, DAL.sln, in the E:\Labfiles\Lab02\VB\Ex2\Starter\DAL or E:\Labfiles\Lab02\CS\Ex2\Starter\DAL folder: a.
In Visual Studio, on the File menu, point to Open, and then click Project/Solution.
b.
If you are using Visual Basic, in the Open Project dialog box, move to the E:\Labfiles\Lab02\VB\Ex2\Starter\DAL folder, click DAL.sln, and then click Open.
Lab 2: Using Entity Data Models
c.
L2-5
If you are using Visual C#, in the Open Project dialog box, move to the E:\Labfiles\Lab02\CS\Ex2\Starter\DAL folder, click DAL.sln, and then click Open.
Task 2: Add the Reward entity 1.
Open the AdventureWorksEDM model in the Entity Designer pane: •
2.
In Solution Explorer, right-click AdventureWorksEDM.edmx, and then click Open.
Add a new entity named Reward with a Key Property name of RewardID and an EntitySet name of Reward to the model: a.
In the Entity Designer pane, right-click anywhere in the white space, point to Add, and then click Entity.
b.
In the Add Entity dialog box, in the Entity name box, type Reward
c.
In the Entity Set box, type Reward
d. In the Property name box, type RewardID and then click OK. 3.
Add the scalar properties described in the following table to the entity. Property name
Type
Scale
Nullable Default Value
RewardType
String
not available
True
AW
RewardName
String
not available
False
(None)
NumberOfAirMilesRequired
Int32
not available
True
(None)
PointsPerAirMile
Int32
not available
True
(None)
Destination
String
not available
True
(None)
MoneyBackPerPoint
Decimal
2
True
(None)
NumberOfPointsRequired
Int32
not available
True
(None)
Product
String
not available
True
(None)
•
For each property, perform the following steps:
L2-6
Building Entity Data Models
i.
In the Reward entity, right-click the entity heading, point to Add, and then click Scalar Property.
ii.
Type the property name, and then press ENTER.
iii. Use the Properties pane to set the Type, Scale, Nullable, and Default Value properties.
Task 3: Add the RewardsClaimed entity 1.
Add a new entity named RewardsClaimed with a Key Property name of ClaimID and an Entity Set name of RewardsClaimed to the model: a.
In the Entity Designer pane, right-click anywhere in the white space, point to Add, and then click Entity.
b.
In the Add Entity dialog box, in the Entity name box, type RewardsClaimed
c.
In the Entity Set box, type RewardsClaimed
d. In the Property name box, type ClaimID and then click OK. 2.
Add the scalar property described in the following table to the entity. Property name PointsUsed
•
Type Int32
For the PointsUsed property, perform the following steps: i.
In the RewardsClaimed entity, right-click the entity heading, point to Add, and then click Scalar Property.
ii.
Type the property name, and then press ENTER.
iii. Use the Properties pane to set the Type property.
Task 4: Add the Reward entity to the RewardsClaimed association 1.
Add a one-to-many association between the Reward and RewardsClaimed entities: a.
In the Reward entity, right-click the entity heading, point to Add, and then click Association.
Lab 2: Using Entity Data Models
b. 2.
3.
L2-7
In the Add Association dialog box, review the default values, and then click OK.
In the RewardsClaimed entity, rename the new RewardRewardID property to RewardID: a.
In the RewardsClaimed entity, right-click RewardRewardID, and then click Rename.
b.
Type RewardID and then press ENTER.
In the Reward entity, rename the RewardsClaimeds navigation property to RewardsClaimed: a.
In the Reward entity, in the Navigation Properties section, right-click RewardsClaimeds, and then click Rename.
b.
Type RewardsClaimed and then press ENTER.
Task 5: Add the Contact entity to the RewardsClaimed association 1.
2.
3.
4.
Add a one-to-many association between the Contact and RewardsClaimed entities: a.
In the Contact entity, right-click the entity heading, point to Add, and then click Association.
b.
In the Add Association dialog box, change the right End Entity to RewardsClaimed, and then click OK.
Rename the new ContactContactID property in the RewardsClaimed entity to ContactID: a.
In the RewardsClaimed entity, right-click ContactContactID, and then click Rename.
b.
Type ContactID and then press ENTER.
Rename the Navigation Property in the Contact entity to RewardsClaimed: a.
In the Contact entity, in the Navigation Properties section, right-click RewardsClaimeds, and then click Rename.
b.
Type RewardsClaimed and then press ENTER.
Save and close the solution: a.
On the File menu, click Save All.
b.
On the File menu, click Close Solution.
L2-8
Building Entity Data Models
Exercise 3: Using the Generate Database Wizard Task 1: Open the starter project •
Open the existing solution, DAL.sln, in the E:\Labfiles\Lab02\VB\Ex3\Starter\DAL or E:\Labfiles\Lab02\CS\Ex3\Starter\DAL folder: a.
In Visual Studio, on the File menu, point to Open, and then click Project/Solution.
b.
If you are using Visual Basic, in the Open Project dialog box, move to the E:\Labfiles\Lab02\VB\Ex3\Starter\DAL folder, click DAL.sln, and then click Open.
c.
If you are using Visual C#, in the Open Project dialog box, move to the E:\Labfiles\Lab02\CS\Ex3\Starter\DAL folder, click DAL.sln, and then click Open.
Task 2: Modify the Database Schema Name property 1.
Open the AdventureWorksEDM model in the Entity Designer: •
In Solution Explorer, right-click AdventureWorksEDM.edmx, and then click Open.
Note: The Error List shows that the Reward and RewardsClaimed entities are not mapped. This is because you created the entities in the last exercise and have not yet mapped them to any database objects. You will resolve this error later in this exercise.
2.
Change the Database Schema Name property to Sales: a.
In the Entity Designer pane, click anywhere in the white space.
b.
In the Properties pane, click the Database Schema Name property, type Sales and then press ENTER.
Task 3: Generate Transact-SQL script of the model •
Run the Generate Database Wizard to script the AdventureWorksEDM model: a.
In the Entity Designer pane, right-click anywhere in the white space, and then click Generate Database from Model.
Lab 2: Using Entity Data Models
b.
In the Generate Database Wizard window, review the generated script, and then click Finish.
c.
In the SSDL/MSL Overwrite Warning dialog box, click Yes.
L2-9
Task 4: Modify the generated script 1.
2.
3.
Remove the Dropping existing FOREIGN KEY constraints section from the script: a.
In the script window, locate the comment that reads Dropping existing FOREIGN KEY constraints.
b.
Select all of the code from the beginning of the comment to the final GO in the section, and then press DELETE.
Remove the Dropping existing tables section from the script: a.
In the script window, locate the comment that reads Dropping existing tables.
b.
Select all of the code from the beginning of the comment to the final GO in the section, and then press DELETE.
Remove the code that creates the following existing tables in the database: Contacts, SalesOrderHeaders, SalesTerritories, and StoreContacts: a.
In the script window, locate the comment that reads Creating table 'Contacts'.
b.
Select all of the code from the beginning of the comment to the next GO, and then press DELETE.
c.
In the script window, locate the comment that reads Creating table 'SalesOrderHeaders'.
d. Select all of the code from the beginning of the comment to the next GO, and then press DELETE. e.
In the script window, locate the comment that reads Creating table 'SalesTerritories'.
f.
Select all of the code from the beginning of the comment to the next GO, and then press DELETE.
g.
In the script window, locate the comment that reads Creating table 'StoreContacts'.
L2-10
Building Entity Data Models
h. 4.
Select all of the code from the beginning of the comment to the next GO, and then press DELETE.
Remove the code that creates the existing primary keys on the following tables in the database: Contacts, SalesOrderHeaders, SalesTerritories, and StoreContacts: a.
In the script window, locate the comment that reads Creating primary key on [ContactID] in table 'Contacts'.
b.
Select all of the code from the beginning of the comment to the next GO, and then press DELETE.
c.
In the script window, locate the comment that reads Creating primary key on [SalesOrderID] in table 'SalesOrderHeaders'.
d. Select all of the code from the beginning of the comment to the next GO, and then press DELETE.
5.
e.
In the script window, locate the comment that reads Creating primary key on [TerritoryID] in table 'SalesTerritories'.
f.
Select all of the code from the beginning of the comment to the next GO, and then press DELETE.
g.
In the script window, locate the comment that reads Creating primary key on [CustomerID], [ContactID] in table 'StoreContacts'.
h.
Select all of the code from the beginning of the comment to the next GO, and then press DELETE.
Remove the code that creates the existing foreign keys on the following tables in the database: SalesOrderHeaders and StoreContacts: a.
In the script window, locate the comment that reads Creating foreign key on [ContactID] in table 'SalesOrderHeaders'.
b.
Select all of the code from the beginning of the comment to the next GO, and then press DELETE.
c.
In the script window, locate the comment that reads Creating foreign key on [ContactID] in table 'StoreContacts'.
d. Select all of the code from the beginning of the comment to the next GO, and then press DELETE. e.
In the script window, locate the comment that reads Creating foreign key on [TerritoryID] in table 'SalesOrderHeaders'.
Lab 2: Using Entity Data Models
f. 6.
L2-11
Select all of the code from the beginning of the comment to the next GO, and then press DELETE.
Modify the code that creates the foreign key on [ContactID] in the RewardsClaimed table to change the schema for the Contact table to Person and to singularize the table name to Contact: a.
In the script window, locate the comment that reads Creating foreign key on [ContactID] in table 'RewardsClaimed'.
b.
Modify the line that reads REFERENCES [Sales].[Contacts] to read REFERENCES [Person].[Contact].
At this point, your code should resemble the following code example. [Visual Basic] -- --------------------------------------------------- Date Created: 01/19/2010 08:38:09 -- Generated from EDMX file: E:\Labfiles\Lab02\VB\Ex3\Starter\DAL\DAL\AdventureWorksEDM.edmx -- -------------------------------------------------SET QUOTED_IDENTIFIER OFF; GO USE [AdventureWorks] GO IF SCHEMA_ID(N'Sales') IS NULL EXECUTE(N'CREATE SCHEMA [Sales]') GO -- --------------------------------------------------- Creating all tables -- --------------------------------------------------- Creating table 'Reward' CREATE TABLE [Sales].[Reward] ( [RewardID] int NOT NULL, [RewardType] nvarchar(max) NULL, [RewardName] nvarchar(max) NOT NULL, [NumberOfAirMilesRequired] int NULL, [PointsPerAirMile] int NULL, [Destination] nvarchar(max) NULL, [MoneyBackPerPoint] decimal(18,2) NULL, [NumberOfPointsRequired] int NULL, [Product] nvarchar(max) NULL ); GO -- Creating table 'RewardsClaimed' CREATE TABLE [Sales].[RewardsClaimed] ( [ClaimID] int NOT NULL,
L2-12
Building Entity Data Models
[PointsUsed] int NOT NULL, [RewardID] int NOT NULL, [ContactID] int NOT NULL ); GO -- --------------------------------------------------- Creating all PRIMARY KEY Constraints -- --------------------------------------------------- Creating primary key on [RewardID] in table 'Reward' ALTER TABLE [Sales].[Reward] ADD CONSTRAINT [PK_Reward] PRIMARY KEY CLUSTERED ([RewardID] ASC); GO -- Creating primary key on [ClaimID] in table 'RewardsClaimed' ALTER TABLE [Sales].[RewardsClaimed] ADD CONSTRAINT [PK_RewardsClaimed] PRIMARY KEY CLUSTERED ([ClaimID] ASC); GO -- --------------------------------------------------- Creating all FOREIGN KEY Constraints -- --------------------------------------------------- Creating foreign key on [RewardID] in table 'RewardsClaimed' ALTER TABLE [Sales].[RewardsClaimed] ADD CONSTRAINT [FK_RewardRewardsClaimed] FOREIGN KEY ([RewardID]) REFERENCES [Sales].[Reward] ([RewardID]) ON DELETE NO ACTION ON UPDATE NO ACTION; -- Creating non-clustered index for FOREIGN KEY 'FK_RewardRewardsClaimed' CREATE INDEX [IX_FK_RewardRewardsClaimed] ON [Sales].[RewardsClaimed] ([RewardID]); GO -- Creating foreign key on [ContactID] in table 'RewardsClaimed' ALTER TABLE [Sales].[RewardsClaimed] ADD CONSTRAINT [FK_ContactRewardsClaimed] FOREIGN KEY ([ContactID]) REFERENCES [Person].[Contact] ([ContactID]) ON DELETE NO ACTION ON UPDATE NO ACTION; -- Creating non-clustered index for FOREIGN KEY 'FK_ContactRewardsClaimed' CREATE INDEX [IX_FK_ContactRewardsClaimed] ON [Sales].[RewardsClaimed]
Lab 2: Using Entity Data Models
([ContactID]); GO -- --------------------------------------------------- Script has ended -- --------------------------------------------------
[Visual C#] -- --------------------------------------------------- Date Created: 01/19/2010 08:38:09 -- Generated from EDMX file: E:\Labfiles\Lab02\CS\Ex3\Starter\DAL\DAL\AdventureWorksEDM.edmx -- -------------------------------------------------SET QUOTED_IDENTIFIER OFF; GO USE [AdventureWorks] GO IF SCHEMA_ID(N'Sales') IS NULL EXECUTE(N'CREATE SCHEMA [Sales]') GO -- --------------------------------------------------- Creating all tables -- --------------------------------------------------- Creating table 'Reward' CREATE TABLE [Sales].[Reward] ( [RewardID] int NOT NULL, [RewardType] nvarchar(max) NULL, [RewardName] nvarchar(max) NOT NULL, [NumberOfAirMilesRequired] int NULL, [PointsPerAirMile] int NULL, [Destination] nvarchar(max) NULL, [MoneyBackPerPoint] decimal(18,2) NULL, [NumberOfPointsRequired] int NULL, [Product] nvarchar(max) NULL ); GO -- Creating table 'RewardsClaimed' CREATE TABLE [Sales].[RewardsClaimed] ( [ClaimID] int NOT NULL, [PointsUsed] int NOT NULL, [RewardID] int NOT NULL, [ContactID] int NOT NULL ); GO
L2-13
L2-14
Building Entity Data Models
-- --------------------------------------------------- Creating all PRIMARY KEY Constraints -- --------------------------------------------------- Creating primary key on [RewardID] in table 'Reward' ALTER TABLE [Sales].[Reward] ADD CONSTRAINT [PK_Reward] PRIMARY KEY CLUSTERED ([RewardID] ASC); GO -- Creating primary key on [ClaimID] in table 'RewardsClaimed' ALTER TABLE [Sales].[RewardsClaimed] ADD CONSTRAINT [PK_RewardsClaimed] PRIMARY KEY CLUSTERED ([ClaimID] ASC); GO -- --------------------------------------------------- Creating all FOREIGN KEY Constraints -- --------------------------------------------------- Creating foreign key on [RewardID] in table 'RewardsClaimed' ALTER TABLE [Sales].[RewardsClaimed] ADD CONSTRAINT [FK_RewardRewardsClaimed] FOREIGN KEY ([RewardID]) REFERENCES [Sales].[Reward] ([RewardID]) ON DELETE NO ACTION ON UPDATE NO ACTION; -- Creating non-clustered index for FOREIGN KEY 'FK_RewardRewardsClaimed' CREATE INDEX [IX_FK_RewardRewardsClaimed] ON [Sales].[RewardsClaimed] ([RewardID]); GO -- Creating foreign key on [ContactID] in table 'RewardsClaimed' ALTER TABLE [Sales].[RewardsClaimed] ADD CONSTRAINT [FK_ContactRewardsClaimed] FOREIGN KEY ([ContactID]) REFERENCES [Person].[Contact] ([ContactID]) ON DELETE NO ACTION ON UPDATE NO ACTION; -- Creating non-clustered index for FOREIGN KEY 'FK_ContactRewardsClaimed' CREATE INDEX [IX_FK_ContactRewardsClaimed] ON [Sales].[RewardsClaimed] ([ContactID]); GO -- --------------------------------------------------- Script has ended -- --------------------------------------------------
Lab 2: Using Entity Data Models
7.
L2-15
Validate and then execute the script against the 10265A-GEN-DEV \SQLExpress database engine: a.
On the Data menu, point to Transact-SQL Editor, and then click Validate SQL Syntax.
b.
In the Connect to Database Engine dialog box, in the Server name box, type 10265A-GEN-DEV\SQLExpress and then click Connect.
c.
Verify that the validation succeeded.
d. On the Data menu, point to Transact-SQL Editor, and then click Execute SQL. e. 8.
Verify that the script executed successfully.
Build the solution: •
On the Build menu, click Build Solution.
Note: The errors in the Error List have been cleared and the solution builds successfully because the Reward and RewardsClaimed entities are now mapped to the tables that you have just created.
9.
Save and close the solution: a.
On the File menu, click Save All.
b.
On the File menu, click Close Solution.
Exercise 4: Mapping Entities to Multiple Tables Task 1: Open the starter project •
Open the existing solution, DAL.sln, in the E:\Labfiles\Lab02\VB\Ex4\Starter\DAL or E:\Labfiles\Lab02\CS\Ex4\Starter\DAL folder: a.
In Visual Studio, on the File menu, point to Open, and then click Project/Solution.
b.
If you are using Visual Basic, in the Open Project dialog box, move to the E:\Labfiles\Lab02\VB\Ex4\Starter\DAL folder, click DAL.sln, and then click Open.
L2-16
Building Entity Data Models
c.
If you are using Visual C#, in the Open Project dialog box, move to the E:\Labfiles\Lab02\CS\Ex4\Starter\DAL folder, click DAL.sln, and then click Open.
Task 2: Create the InactiveStoreContact table 1.
2.
Open the InactiveStoreContact.sql script, in the E:\Labfiles\Lab02\VB\Ex4\Starter or E:\Labfiles\Lab02\CS\Ex4\Starter folder: a.
On the File menu, point to Open, and then click File.
b.
If you are using Visual Basic, in the Open Project dialog box, move to the E:\Labfiles\Lab02\VB\Ex4\Starter folder, click InactiveStoreContact.sql, and then click Open.
c.
If you are using Visual C#, in the Open Project dialog box, move to the E:\Labfiles\Lab02\CS\Ex4\Starter folder, click InactiveStoreContact.sql, and then click Open.
Validate and then execute the script against the 10265A-GEN-DEV \SQLExpress database engine: a.
On the Data menu, point to Transact-SQL Editor, and then click Validate SQL Syntax.
b.
In the Connect to Database Engine dialog box, in the Server name box, type 10265A-GEN-DEV\SQLExpress and then click Connect.
c.
Verify that the validation succeeded.
d. On the Data menu, point to Transact-SQL Editor, and then click Execute SQL. e.
Verify that the script executed successfully.
f.
On the File menu, click Close.
Task 3: Update the model from the database 1.
Open the AdventureWorksEDM model in the Entity Designer: •
2.
In Solution Explorer, right-click AdventureWorksEDM.edmx, and then click Open.
Update the model from the database to add the InactiveStoreContacts table to the model:
Lab 2: Using Entity Data Models
L2-17
a.
In the Entity Designer pane, right-click anywhere in the white space, and then click Update Model from Database.
b.
In the Update Wizard window, expand Tables, select the InactiveStoreContact (Sales) check box, and then click Finish.
Note: Due to an issue with the prerelease version of the Entity Designer, the mappings for the original table have been lost. To resolve this issue, it is necessary to delete the model from the project, re-create the model, and re-create any default values.
3.
4.
Delete the model from the project: a.
In Solution Explorer, right-click AdventureWorksEDM.edmx, and then click Delete.
b.
In the Microsoft Visual Studio dialog box, click OK.
Add a new ADO.NET EDM named AdventureWorksEDM.edmx to the DAL project. Generate the data model from the AdventureWorks database, and create entities for the Contact, InactiveStoreContact, Reward, RewardsClaimed, SalesOrderHeader, SalesTerritory, and StoreContact tables: a.
In Solution Explorer, right-click DAL, point to Add, and then click New Item.
b.
In the Add New Item - DAL dialog box, in the templates list, click ADO.NET Entity Data Model, in the Name box, type AdventureWorksEDM and then click Add.
c.
In the Entity Data Model Wizard, on the Choose Model Contents page, click Generate from database, and then click Next.
d. On the Choose Your Data Connection page, click Next. e.
5.
On the Choose Your Database Objects page, expand Tables, select the Contact (Person), InactiveStoreContact (Sales), Reward (Sales), RewardsClaimed (Sales), SalesOrderHeader (Sales), SalesTerritory (Sales), and StoreContact (Sales) check boxes, and then click Finish.
Change the Database Schema Name property to Sales: a.
In the Entity Designer pane, click anywhere in the white space.
b.
In the Properties pane, click the Database Schema Name property, type Sales and then press ENTER.
L2-18
Building Entity Data Models
6.
7.
Rename the Navigation Property in the Reward entity to RewardsClaimed: a.
In the Reward entity, in the Navigation Properties section, right-click RewardsClaimeds, and then click Rename.
b.
Type RewardsClaimed and then press ENTER.
Rename the Navigation Property in the Contact entity to RewardsClaimed: a.
In the Contact entity, in the Navigation Properties section, right-click RewardsClaimeds, and then click Rename.
b.
Type RewardsClaimed and then press ENTER.
Task 4: Map the StoreContact entity to the InactiveStoreContact table 1.
Open the Mapping Details pane, and then map the StoreContact entity to the InactiveStoreContact table: a.
In the Entity Designer pane, click the StoreContact entity.
b.
On the View menu, point to Other Windows, and then click Entity Data Model Mapping Details.
c.
In the Mapping Details pane, review the mappings for the entity.
d. In the Mapping Details pane, click , and then in the drop-down list, click InactiveStoreContact. e. 2.
In the Entity Designer pane, delete the InactiveStoreContact entity: •
3.
In the Entity Designer pane, in the InactiveStoreContact entity, rightclick the entity heading, and then click Delete.
Build the solution: •
4.
Review the automatic column mappings that have occurred because the column names in the entity and the table are the same.
On the Build menu, click Build Solution.
Save and close the solution: a.
On the File menu, click Save All.
b.
On the File menu, click Close Solution.
Lab 2: Using Entity Data Models
L2-19
Exercise 5: Implementing an Inheritance Hierarchy Task 1: Open the starter project •
Open the existing solution, DAL.sln, in the E:\Labfiles\Lab02\VB\Ex5\Starter\ DAL or E:\Labfiles\Lab02\CS\Ex5\Starter\DAL folder: a.
In Visual Studio, on the File menu, point to Open, and then click Project/Solution.
b.
If you are using Visual Basic, in the Open Project dialog box, move to the E:\Labfiles\Lab02\VB\Ex5\Starter\DAL folder, click DAL.sln, and then click Open.
c.
If you are using Visual C#, in the Open Project dialog box, move to the E:\Labfiles\Lab02\CS\Ex5\Starter\DAL folder, click DAL.sln, and then click Open.
Task 2: Add the AirMilesReward entity 1.
Open AdventureWorksEDM in the Entity Designer pane: •
2.
In Solution Explorer, right-click AdventureWorksEDM.edmx, and then click Open.
Add a new entity named AirMilesReward based on the Reward entity: a.
In the Entity Designer pane, right-click anywhere in the white space, point to Add, and then click Entity.
b.
In the Add Entity dialog box, in the Entity name box, type AirMilesReward
c.
In the Base type list, click Reward, and then click OK.
Task 3: Add the SupermarketReward entity •
Add a new entity named SupermarketReward based on the Reward entity: a.
In the Entity Designer pane, right-click anywhere in the white space, point to Add, and then click Entity.
b.
In the Add Entity dialog box, in the Entity name box, type SupermarketReward
c.
In the Base type list, click Reward, and then click OK.
L2-20
Building Entity Data Models
Task 4: Add the AdventureWorksReward entity •
Add a new entity named AdventureWorksReward based on the Reward entity: a.
In the Entity Designer pane, right-click anywhere in the white space, point to Add, and then click Entity.
b.
In the Add Entity dialog box, in the Entity name box, type AdventureWorksReward
c.
In the Base type list, click Reward, and then click OK.
Task 5: Map the new entities to the Reward table 1.
2.
3.
Open the Mapping Details pane, and then map the AirMilesReward entity to the Reward table: a.
In the Entity Designer pane, click the AirMilesReward entity.
b.
On the View menu, point to Other Windows, and then click Entity Data Model Mapping Details.
c.
In the Mapping Details pane, click , and then in the drop-down list, click Reward.
In the Mapping Details pane, map the SupermarketReward entity to the Reward table: a.
In the Entity Designer pane, click the SupermarketReward entity.
b.
In the Mapping Details pane, click , and then in the drop-down list, click Reward.
In the Mapping Details pane, map the AdventureWorksReward entity to the Reward table: a.
In the Entity Designer pane, click the AdventureWorksReward entity.
b.
In the Mapping Details pane, click , and then in the drop-down list, click Reward.
Task 6: Add conditions to the mappings 1.
Add a condition to the AirMilesReward mapping to specify that this entity should contain rewards only if the RewardType field is AM: a.
In the Entity Designer pane, click the AirMilesReward entity.
Lab 2: Using Entity Data Models
2.
3.
4.
5.
b.
In the Mapping Details pane, click , and then in the drop-down list, click RewardType.
c.
In the Value / Property column, click , type AM and then press ENTER.
Add a condition to the SupermarketReward mapping to specify that this entity should contain rewards only if the RewardType field is SM: a.
In the Entity Designer pane, click the SupermarketReward entity.
b.
In the Mapping Details pane, click , and then in the drop-down list, click RewardType.
c.
In the Value / Property column, click , type SM and then press ENTER.
Add a condition to the AdventureWorksReward mapping to specify that this entity should contain rewards only if the RewardType field is AW: a.
In the Entity Designer pane, click the AdventureWorksReward entity.
b.
In the Mapping Details pane, click , and then in the drop-down list, click RewardType.
c.
In the Value / Property column, click , type AW and then press ENTER.
Remove the RewardType property from the Reward entity: a.
In the Entity Designer pane, click the Reward entity.
b.
In the Properties list, right-click RewardType, and then click Delete.
Make the Reward entity abstract: a.
In the Entity Designer pane, click the Reward entity.
b.
In the Properties pane, click the Abstract property, and then in the dropdown list, click True.
Task 7: Assign properties to the new entities 1.
L2-21
Move the NumberOfAirMilesRequired property to the AirMilesReward entity: a.
In the Entity Designer pane, click the Reward entity.
b.
In the Properties list, right-click the NumberOfAirMilesRequired property, and then click Cut.
L2-22
Building Entity Data Models
c. 2.
3.
4.
5.
6.
7.
Right-click the AirMilesReward entity, and then click Paste.
Move the PointsPerAirMile property to the AirMilesReward entity: a.
In the Entity Designer pane, click the Reward entity.
b.
In the Properties list, right-click the PointsPerAirMile property, and then click Cut.
c.
Right-click the AirMilesReward entity, and then click Paste.
Move the Destination property to the AirMilesReward entity: a.
In the Entity Designer pane, click the Reward entity.
b.
In the Properties list, right-click the Destination property, and then click Cut.
c.
Right-click the AirMilesReward entity, and then click Paste.
Move the MoneyBackPerPoint property to the SupermarketReward entity: a.
In the Entity Designer pane, click the Reward entity.
b.
In the Properties list, right-click the MoneyBackPerPoint property, and then click Cut.
c.
Right-click the SupermarketReward entity, and then click Paste.
Move the NumberOfPointsRequired property to the AdventureWorksReward entity: a.
In the Entity Designer pane, click the Reward entity.
b.
In the Properties list, right-click the NumberOfPointsRequired property, and then click Cut.
c.
Right-click the AdventureWorksReward entity, and then click Paste.
Move the Product property to the AdventureWorksReward entity: a.
In the Entity Designer pane, click the Reward entity.
b.
In the Properties list, right-click the Product property, and then click Cut.
c.
Right-click the AdventureWorksReward entity, and then click Paste.
Map the AirMilesReward properties to the appropriate columns in the Reward table: a.
In the Entity Designer pane, click the AirMilesReward entity.
Lab 2: Using Entity Data Models
b.
8.
9.
L2-23
In the Mapping Details pane, map the NumberOfAirMilesRequired, PointsPerAirMile, and Destination properties to the matching columns in the database.
Map the SupermarketReward properties to the appropriate columns in the Reward table: a.
In the Entity Designer pane, click the SupermarketReward entity.
b.
In the Mapping Details pane, map the MoneyBackPerPoint property to the matching column in the database.
Map the AdventureWorksReward properties to the appropriate columns in the Reward table: a.
In the Entity Designer pane, click the AdventureWorksReward entity.
b.
In the Mapping Details pane, map the NumberOfPointsRequired and the Product properties to the matching columns in the database.
10. Build the solution: •
On the Build menu, click Build Solution.
11. Save and close the solution: a.
On the File menu, click Save All.
b.
On the File menu, click Close Solution.
Exercise 6: Using Stored Procedures Task 1: Open the starter project •
Open the existing solution, DAL.sln, in the E:\Labfiles\Lab02\VB\Ex6\Starter or E:\Labfiles\Lab02\CS\Ex6\Starter folder: a.
In Visual Studio, on the File menu, point to Open, and then click Project/Solution.
b.
If you are using Visual Basic, in the Open Project dialog box, move to the E:\Labfiles\Lab02\VB\Ex6\Starter folder, click DAL.sln, and then click Open.
c.
If you are using Visual C#, in the Open Project dialog box, move to the E:\Labfiles\Lab02\CS\Ex6\Starter folder, click DAL.sln, and then click Open.
L2-24
Building Entity Data Models
Task 2: Add the uspCountOrders stored procedure to the model 1.
Open AdventureWorksEDM in the Entity Designer pane: •
2.
In Solution Explorer, right-click AdventureWorksEDM.edmx, and then click Open.
Run the Update Wizard to add the uspCountOrders stored procedure to the model: a.
In the Entity Designer pane, right-click anywhere in the white space, and then click Update Model from Database.
b.
In the Update Wizard window, on the Choose Your Data Connection page, click Next.
c.
In the Update Wizard window, on the Choose Your Database Objects page, expand Stored Procedures, select the uspCountOrders (Sales) check box, and then click Finish.
Note: Four errors will appear in the Error List because you have updated the model from the database and the mappings for the inheritance hierarchy that you created in Exercise 5 have been lost. These errors will not impact this exercise, so you can ignore the errors and continue with the next task.
Task 3: Add a function import to the model 1.
Add a function import named CountOrders to the model. Map it to the uspCountOrders stored procedure and configure it to return a collection of scalar Int32 data types: a.
In the Entity Designer pane, right-click anywhere in the white space, point to Add, and then click Function Import.
b.
In the Add Function Import dialog box, in the Function Import Name box, type CountOrders
c.
In the Stored Procedure Name list, click uspCountOrders.
d. Click Get Column Information. e. 2.
Under Returns a Collection Of, click Scalars, in the type list, click Int32, and then click OK.
Build the solution:
Lab 2: Using Entity Data Models
• 3.
L2-25
On the Build menu, click Build Solution.
Save and close the solution: a.
On the File menu, click Save All.
b.
On the File menu, click Close Solution.
Exercise 7: Creating a Complex Type Task 1: Open the starter project •
Open the existing solution, DAL.sln, in the E:\Labfiles\Lab02\VB\Ex7\Starter or E:\Labfiles\Lab02\CS\Ex7\Starter folder: a.
In Visual Studio, on the File menu, point to Open, and then click Project/Solution.
b.
If you are using Visual Basic, in the Open Project dialog box, move to the E:\Labfiles\Lab02\VB\Ex7\Starter folder, click DAL.sln, and then click Open.
c.
If you are using Visual C#, in the Open Project dialog box, move to the E:\Labfiles\Lab02\CS\Ex7\Starter folder, click DAL.sln, and then click Open.
Task 2: Create the complex type 1.
Open AdventureWorksEDM in the Entity Designer pane: •
2.
In Solution Explorer, right-click AdventureWorksEDM.edmx, and then click Open.
In the SalesTerritory entity, create a complex type named LocationType from the Name, CountryRegionCode, and Group properties: a.
In the Entity Designer pane, click the SalesTerritory entity, and then select all of the following properties: Name, CountryRegionCode, and Group.
b.
Right-click the selected properties, and then click Refactor into New Complex Type.
c.
In the Model Browser pane, under Complex Types, type LocationType and then press ENTER.
L2-26
Building Entity Data Models
3.
4.
Change the name of the new complex property in the SalesTerritory entity to Location: a.
In the SalesTerritory entity, in the Properties list, right-click ComplexProperty, and then click Rename.
b.
Type Location and then press ENTER.
Build the solution: •
5.
On the Build menu, click Build Solution.
Save and close the solution, and then close Visual Studio: a.
On the File menu, click Save All.
b.
On the File menu, click Exit.
Lab 3: Querying Entity Data
L3-1
Querying Entity Data
Lab 3: Querying Entity Data Exercise 1: Retrieving All Contact Entities Task 1: Prepare the Adventure Works database for the lab 1.
Log on to the 10265A-GEN-DEV-03 virtual machine as Student with the password Pa$$w0rd.
2.
Run AWReset.bat in the E:\Labfiles folder: a.
Click Start, and then click Computer.
b.
Double-click Allfiles (E:), double-click Labfiles, and then double-click AWReset.bat.
c.
Wait for the batch file to finish running, and then close Windows® Explorer.
Task 2: Open the starter project for this exercise 1.
Open Microsoft® Visual Studio® 2010: •
2.
Click Start, click All Programs, click Microsoft Visual Studio 2010, and then click Microsoft Visual Studio 2010.
Open the existing solution, DAL.sln, in the E:\Labfiles\Lab03\VB\Ex1\Starter or E:\Labfiles\Lab03\CS\Ex1\Starter folder: a.
On the File menu, point to Open, and then click Project/Solution.
b.
If you are using Microsoft Visual Basic®, in the Open Project dialog box, move to the E:\Labfiles\Lab03\VB\Ex1\Starter folder, click DAL.sln, and then click Open.
c.
If you are using Microsoft Visual C#®, in the Open Project dialog box, move to the E:\Labfiles\Lab03\CS\Ex1\Starter folder, click DAL.sln, and then click Open.
Task 3: Add code to retrieve all of the contacts 1.
In Visual Studio, review the task list:
L3-2
Querying Entity Data
2.
a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
Open the DataAccessLayer code file by double-clicking the comment TODO: Ex1 - Retrieve all contacts item in the task list. This task is located in the first GetContactList method: •
3.
In the task list, double-click the TODO: Ex1 - Retrieve all contacts item.
Delete the existing code in the GetContactList method: •
4.
Select the code in the method, and then press DELETE.
Write code that performs the following tasks: a.
Check whether the entities variable is null. If it is, instantiate it as a new instance of the AdventureWorksEntities context object.
Note: The entities variable is a private field in the DataAccessLayer class. Your code should perform all data access operations by using this context object.
b.
Create and define a Language-Integrated Query (LINQ) to Entities query to select all Contact entities.
c.
Execute the query with MergeOption set to NoTracking.
d. Return the results. Your code should resemble the following code example. [Visual Basic] ''' ''' Returns a list of all the Contact entities from the database. ''' ''' Public Function GetContactList() As List(Of Contact) _ Implements IDataAccessLayer.GetContactList ' Check we have an ObjectContext If (entities Is Nothing) Then entities = _ New AdventureWorksEntities() ' Create a query from the entityset Dim contacts As ObjectQuery(Of Contact) = entities.Contacts ' Create detached objects
Lab 3: Querying Entity Data
contacts.MergeOption = MergeOption.NoTracking ' Define the query Dim query = From c In contacts Select c ' Execute the query Dim results As List(Of Contact) = query.ToList() ' Return the results in a List Return results End Function
[Visual C#] /// /// Returns a list of all the Contact entities from the database. /// /// public List GetContactList() { // Check we have an ObjectContext if (entities == null) entities = new AdventureWorksEntities(); // Create a query from the entityset ObjectQuery contacts = entities.Contacts; // Create detached objects contacts.MergeOption = MergeOption.NoTracking; // Define the query var query = from c in contacts select c; // Execute the query List results = query.ToList(); // Return the results in a List return results; }
5.
Save the DataAccessLayer code file: a.
If you are using Visual Basic, on the File menu, click Save DataAccessLayer.vb.
L3-3
L3-4
Querying Entity Data
b.
If you are using Visual C#, on the File menu, click Save DataAccessLayer.cs.
Task 4: Add a unit test to verify your code 1.
2.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
Open the DataAccessLayerTest code file by double-clicking the comment TODO: Ex1 - Create a unit test for GetContactList item in the task list. This task is located in the GetContactListTest method: •
3.
Delete the existing code in the GetContactListTest method: •
4.
In the task list, double-click the TODO: Ex1 - Create a unit test for GetContactList item.
Select the code in the method, and then press DELETE.
Write a unit test to compare the first 10 contacts returned by your query with the 10 contacts returned by the GetLocalCustomerListFirstTen method. Be sure to release all resources at the end of the test. Your code should resemble the following code example.
[Visual Basic] ''' '''A test for GetContactList ''' _ Public Sub GetContactListTest() Dim dal As New DataAccessLayer() Dim expected As List(Of Contact) = _ Me.GetLocalCustomerListFirstTen() Dim actual As List(Of Contact) = dal.GetContactList() For i As Integer = 0 To 9 Assert.AreEqual(expected(i).ContactID, actual(i).ContactID) Assert.AreEqual(expected(i).Title, actual(i).Title) Assert.AreEqual(expected(i).FirstName, actual(i).FirstName) Assert.AreEqual(expected(i).MiddleName, actual(i).MiddleName)
Lab 3: Querying Entity Data
L3-5
Assert.AreEqual(expected(i).LastName, actual(i).LastName) Assert.AreEqual(expected(i).EmailAddress, actual(i).EmailAddress) Assert.AreEqual(expected(i).Phone, actual(i).Phone) Assert.AreEqual(expected(i).CurrentPoints, actual(i).CurrentPoints) Next i dal.Dispose() End Sub
[Visual C#] /// ///A test for GetContactList /// [TestMethod()] public void GetContactListTest() { DataAccessLayer dal = new DataAccessLayer(); List expected = this.GetLocalCustomerListFirstTen(); List actual = dal.GetContactList(); for (int i = 0; i < 10; i++) { Assert.AreEqual(expected[i].ContactID, actual[i].ContactID); Assert.AreEqual(expected[i].Title, actual[i].Title); Assert.AreEqual(expected[i].FirstName, actual[i].FirstName); Assert.AreEqual(expected[i].MiddleName, actual[i].MiddleName); Assert.AreEqual(expected[i].LastName, actual[i].LastName); Assert.AreEqual(expected[i].EmailAddress, actual[i].EmailAddress); Assert.AreEqual(expected[i].Phone, actual[i].Phone); Assert.AreEqual(expected[i].CurrentPoints, actual[i].CurrentPoints); } dal.Dispose(); }
5.
Save the DataAccessLayerTest code file: a.
If you are using Visual Basic, on the File menu, click Save DataAccessLayerTest.vb.
L3-6
Querying Entity Data
b.
If you are using Visual C#, on the File menu, click Save DataAccessLayerTest.cs.
Task 5: Build and test the application 1.
Build the solution and correct any errors: •
2.
On the Build menu, click Build Solution.
Start the application in Debug mode: •
On the Debug menu, click Start Debugging.
3.
In the AdventureWorks Rewards window, click All Customers to load data from the entity model into the data grid. Verify that the application functions as expected.
4.
Close the application.
5.
Run all of the tests in the solution: •
On the Test menu, point to Run, and then click All Tests in Solution.
6.
Verify that all of the tests succeed, including GetContactListTest.
7.
Close the solution: •
On the File menu, click Close Solution.
Exercise 2: Retrieving Contact Entities by Using a Filter Task 1: Open the starter project for this exercise •
Open the existing solution, DAL.sln, in the E:\Labfiles\Lab03\VB\Ex2\Starter or E:\Labfiles\Lab03\CS\Ex2\Starter folder: a.
On the File menu, point to Open, and then click Project/Solution.
b.
If you are using Visual Basic, in the Open Project dialog box, move to the E:\Labfiles\Lab03\VB\Ex2\Starter folder, click DAL.sln, and then click Open.
c.
If you are using Visual C#, in the Open Project dialog box, move to the E:\Labfiles\Lab03\CS\Ex2\Starter folder, click DAL.sln, and then click Open.
Lab 3: Querying Entity Data
L3-7
Task 2: Add code to retrieve contacts by last name 1.
2.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
Open the DataAccessLayer code file by double-clicking the comment TODO: Ex2 - Retrieve contacts by last name item in the task list. This task is located in the second GetContactList method: •
3.
In the task list, double-click the TODO: Ex2 - Retrieve contacts by last name item.
Delete the existing code in the GetContactList method: •
4.
Select the code in the method, and then press DELETE.
Add code to the method that performs the following tasks: a.
Instantiate the entities context object if it is currently null.
b.
Create and define a LINQ to Entities query to retrieve Contact entities by last name.
c.
Execute the query with MergeOption set to NoTracking.
d. Return the results. Your code should resemble the following code example. [Visual Basic] ''' ''' Retrieve all contacts with a specified last name ''' ''' Name to search for ''' List of contacts Public Function GetContactList(ByVal name As String) As List(Of Contact) _ Implements IDataAccessLayer.GetContactList ' Check we have an ObjectContext If (entities Is Nothing) Then entities = _ New AdventureWorksEntities() ' Create a query from the entityset Dim contacts As ObjectQuery(Of Contact) = entities.Contacts ' Create detached objects
L3-8
Querying Entity Data
contacts.MergeOption = MergeOption.NoTracking ' Define the query Dim query = From c In contacts Where c.LastName = name Select c ' Execute the query Dim results As List(Of Contact) = query.ToList() ' Return the results in a List Return results End Function
[Visual C#] /// /// Retrieve all contacts with a specified last name /// /// Name to search for /// List of contacts public List GetContactList(string name) { // Check we have an ObjectContext if (entities == null) entities = new AdventureWorksEntities(); // Create a query from the entityset ObjectQuery contacts = entities.Contacts; // Create detached objects contacts.MergeOption = MergeOption.NoTracking; // Define the query var query = from c in contacts where c.LastName == name select c; // Execute the query List results = query.ToList(); // Return the results in a List return results; }
5.
Save the DataAccessLayer code file: a.
If you are using Visual Basic, on the File menu, click Save DataAccessLayer.vb.
Lab 3: Querying Entity Data
b.
L3-9
If you are using Visual C#, on the File menu, click Save DataAccessLayer.cs.
Task 3: Add a unit test to verify your code 1.
2.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
Open the DataAccessLayerTest code file by double-clicking the comment TODO: Ex2 - Create a unit test for GetContactList by last name item in the task list. This task is located in the GetAContactListTest method: •
3.
Delete the existing code in the GetAContactListTest method: •
4.
In the task list, double-click the TODO: Ex2 - Create a unit test for GetContactList by last name item.
Select the code in the method, and then press DELETE.
Write a unit test to compare the first contacts returned by your query, using “Adina” as the last name parameter, with the contacts returned by the GetLocalCustomerList method. Be sure to release all resources at the end of the test. Your code should resemble the following code example.
[Visual Basic] ''' '''A test for GetContactList ''' _ Public Sub GetAContactListTest() Dim Dim Dim Dim
dal As New DataAccessLayer() name As String = "Adina" expected As List(Of Contact) = Me.GetLocalCustomerList() actual As List(Of Contact) = dal.GetContactList(name)
For i As Integer = 0 To expected.Count - 1 Assert.AreEqual(expected(i).ContactID, actual(i).ContactID) Assert.AreEqual(expected(i).Title, actual(i).Title)
L3-10
Querying Entity Data
Assert.AreEqual(expected(i).FirstName, actual(i).FirstName) Assert.AreEqual(expected(i).MiddleName, actual(i).MiddleName) Assert.AreEqual(expected(i).LastName, actual(i).LastName) Assert.AreEqual(expected(i).EmailAddress, actual(i).EmailAddress) Assert.AreEqual(expected(i).Phone, actual(i).Phone) Assert.AreEqual(expected(i).CurrentPoints, actual(i).CurrentPoints) Next i dal.Dispose() End Sub
[Visual C#] /// ///A test for GetContactList /// [TestMethod()] public void GetAContactListTest() { DataAccessLayer dal = new DataAccessLayer(); string name = "Adina"; List expected = this.GetLocalCustomerList(); List actual = dal.GetContactList(name); for (int i = 0; i < expected.Count; i++) { Assert.AreEqual(expected[i].ContactID, actual[i].ContactID); Assert.AreEqual(expected[i].Title, actual[i].Title); Assert.AreEqual(expected[i].FirstName, actual[i].FirstName); Assert.AreEqual(expected[i].MiddleName, actual[i].MiddleName); Assert.AreEqual(expected[i].LastName, actual[i].LastName); Assert.AreEqual(expected[i].EmailAddress, actual[i].EmailAddress); Assert.AreEqual(expected[i].Phone, actual[i].Phone); Assert.AreEqual(expected[i].CurrentPoints, actual[i].CurrentPoints); } dal.Dispose(); }
5.
Save the DataAccessLayerTest code file: a.
If you are using Visual Basic, on the File menu, click Save DataAccessLayerTest.vb.
Lab 3: Querying Entity Data
b.
L3-11
If you are using Visual C#, on the File menu, click Save DataAccessLayerTest.cs.
Task 4: Build and test the application 1.
Build the solution and correct any errors: •
2.
On the Build menu, click Build Solution.
Start the application in Debug mode: •
On the Debug menu, click Start Debugging.
3.
In the AdventureWorks Rewards window, in the Name box, type Ward and then click Search. Verify that the application functions as expected and loads the correct contacts into the data grid.
4.
Close the application.
5.
Run all of the tests in the solution: •
On the Test menu, point to Run, and then click All Tests in Solution.
6.
Verify that all of the tests succeed, including GetAContactListTest.
7.
Close the solution: •
On the File menu, click Close Solution.
Exercise 3: Retrieving RewardsClaimed Entities Task 1: Open the starter project for this exercise •
Open the existing solution, DAL.sln, in the E:\Labfiles\Lab03\VB\Ex3\Starter or E:\Labfiles\Lab03\CS\Ex3\Starter folder: a.
On the File menu, point to Open, and then click Project/Solution.
b.
If you are using Visual Basic, in the Open Project dialog box, move to the E:\Labfiles\Lab03\VB\Ex3\Starter folder, click DAL.sln, and then click Open.
c.
If you are using Visual C#, in the Open Project dialog box, move to the E:\Labfiles\Lab03\CS\Ex3\Starter folder, click DAL.sln, and then click Open.
L3-12
Querying Entity Data
Task 2: Add code to retrieve rewards claimed by contact ID 1.
2.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
Open the DataAccessLayer code file by double-clicking the comment TODO: Ex3 - Retrieve rewards claimed by contact ID item in the task list. This task is located in the GetRewardsClaimedList method: •
3.
In the task list, double-click the TODO: Ex3 - Retrieve rewards claimed by contact ID item.
Delete the existing code in the GetRewardsClaimedList method: •
4.
Select the code in the method, and then press DELETE.
Add code that performs the following tasks: a.
Instantiate the entities context object if it is currently null.
b.
Create and define an Entity Structured Query Language (Entity SQL) query to retrieve RewardsClaimed entities by contact ID.
c.
Execute the query with MergeOption set to NoTracking.
d. Return the results. Your code should resemble the following code example. [Visual Basic] ''' ''' ''' ''' ''' '''
Get a list of RewardsClaimed for a contact. Use Entity SQL syntax. ContactID to search on List of RewardsClaimed
Public Function GetRewardsClaimedList(ByVal contactID As Integer) As List(Of RewardsClaimed) _ Implements IDataAccessLayer.GetRewardsClaimedList ' Check we have an ObjectContext If (entities Is Nothing) Then entities = _ New AdventureWorksEntities()
Lab 3: Querying Entity Data
L3-13
' Define the Entity SQL query Dim queryString As String = "SELECT VALUE r from " & "AdventureWorksEntities.RewardsClaimed as r " & "WHERE r.ContactID=@contactID" ' Create the query Dim query = New ObjectQuery(Of RewardsClaimed)(queryString, entities) ' Create detached objects query.MergeOption = MergeOption.NoTracking ' Add the parameters query.Parameters.Add(New ObjectParameter("contactID", contactID)) ' Execute the query Dim results As List(Of RewardsClaimed) = query.ToList() ' Return the results in a list Return results End Function
[Visual C#] /// /// /// /// /// ///
Get a list of RewardsClaimed for a contact. Use Entity SQL syntax. ContactID to search on List of RewardsClaimed
public List GetRewardsClaimedList(int contactID) { // Check we have an ObjectContext if (entities == null) entities = new AdventureWorksEntities(); // Define the Entity SQL query var queryString = @"SELECT VALUE r from AdventureWorksEntities.RewardsClaimed as r WHERE r.ContactID=@contactID"; // Create the query var query = new ObjectQuery(queryString, entities);
L3-14
Querying Entity Data
// Create detached objects query.MergeOption = MergeOption.NoTracking; // Add the parameters query.Parameters.Add(new ObjectParameter("contactID", contactID)); // Execute the query List results = query.ToList(); // Return the results in a list return results; }
5.
Save the DataAccessLayer code file: a.
If you are using Visual Basic, on the File menu, click Save DataAccessLayer.vb.
b.
If you are using Visual C#, on the File menu, click Save DataAccessLayer.cs.
Task 3: Add a unit test to verify your code 1.
2.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
Open the DataAccessLayerTest code file by double-clicking the comment TODO: Ex3 - Create a unit test for GetRewardsClaimedList item in the task list. This task is located in the GetRewardsClaimedListTest method: •
3.
Delete the existing code in the GetRewardsClaimedListTest method: •
4.
In the task list, double-click the TODO: Ex3 - Create a unit test for GetRewardsClaimedList item.
Select the code in the method, and then press DELETE.
Write a unit test to compare the RewardsClaimed entities returned by your query, using a contactID of 2 as the parameter value, with the RewardsClaimed entities returned by the GetLocalRewardsClaimedList method. Be sure to release all resources at the end of the test. Your code should resemble the following code example.
Lab 3: Querying Entity Data
L3-15
[Visual Basic] ''' '''A test for GetRewardsClaimedList ''' _ Public Sub GetRewardsClaimedListTest() Dim dal As New DataAccessLayer() Dim contactID As Integer = 2 Dim expected As List(Of RewardsClaimed) = _ Me.GetLocalRewardsClaimedList() Dim actual As List(Of RewardsClaimed) = _ dal.GetRewardsClaimedList(contactID)
For i As Integer = 0 To expected.Count - 1 Assert.AreEqual(expected(i).ClaimID, actual(i).ClaimID) Assert.AreEqual(expected(i).PointsUsed, actual(i).PointsUsed) Assert.AreEqual(expected(i).RewardID, actual(i).RewardID) Assert.AreEqual(expected(i).ContactID, actual(i).ContactID) Next i dal.Dispose() End Sub
[Visual C#] /// ///A test for GetRewardsClaimedList /// [TestMethod()] public void GetRewardsClaimedListTest() { DataAccessLayer dal = new DataAccessLayer(); int contactID = 2; List expected = this.GetLocalRewardsClaimedList(); List actual = dal.GetRewardsClaimedList(contactID);
L3-16
Querying Entity Data
for (int i = 0; i < expected.Count; i++) { Assert.AreEqual(expected[i].ClaimID, actual[i].ClaimID); Assert.AreEqual(expected[i].PointsUsed, actual[i].PointsUsed); Assert.AreEqual(expected[i].RewardID, actual[i].RewardID); Assert.AreEqual(expected[i].ContactID, actual[i].ContactID); } dal.Dispose(); }
5.
Save the DataAccessLayerTest code file: a.
If you are using Visual Basic, on the File menu, click Save DataAccessLayerTest.vb.
b.
If you are using Visual C#, on the File menu, click Save DataAccessLayerTest.cs.
Task 4: Build and test the application 1.
Build the solution and correct any errors: •
2.
On the Build menu, click Build Solution.
Start the application in Debug mode: •
On the Debug menu, click Start Debugging.
3.
In the AdventureWorks Rewards window, click All Customers to load contacts into the data grid. Then, click a contact in the data grid to display rewards claimed by that contact in the second data grid.
4.
Close the application.
5.
Run all of the tests in the solution: •
On the Test menu, point to Run, and then click All Tests in Solution.
6.
Verify that all of the tests succeed, including GetRewardsClaimedListTest.
7.
Close the solution: •
On the File menu, click Close Solution.
Lab 3: Querying Entity Data
L3-17
Exercise 4: Querying the Rewards Family of Entities Task 1: Open the starter project for this exercise •
Open the existing solution, DAL.sln, in the E:\Labfiles\Lab03\VB\Ex4\Starter or E:\Labfiles\Lab03\CS\Ex4\Starter folder: a.
On the File menu, point to Open, and then click Project/Solution.
b.
If you are using Visual Basic, in the Open Project dialog box, move to the E:\Labfiles\Lab03\VB\Ex4\Starter folder, click DAL.sln, and then click Open.
c.
If you are using Visual C#, in the Open Project dialog box, move to the E:\Labfiles\Lab03\CS\Ex4\Starter folder, click DAL.sln, and then click Open.
Task 2: Add code to retrieve reward details by reward ID 1.
2.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
Open the DataAccessLayer code file by double-clicking the comment TODO: Ex4 - Get a reward by reward ID item in the task list. This task is located in the GetReward method: •
3.
Delete the existing code in the GetReward method: •
4.
In the task list, double-click the TODO: Ex4 - Get a reward by reward ID item.
Select the code in the method, and then press DELETE.
Use the EntityConnection, EntityCommand, and EntityDataReader classes to connect to the entity model, retrieve the details of a reward by rewardID, and then return the result as a Reward entity. Set the MergeOption of the query to NoTracking. Your code should resemble the following code example.
L3-18
Querying Entity Data
[Visual Basic] ''' ''' ''' ''' ''' '''
Find Reward details given a RewardID. Implemented using an EntityCommand. RewardID to search for Single matching reward
Public Function GetReward(ByVal rewardID As Integer) As AdventureWorksReward _ Implements IDataAccessLayer.GetReward ' An empty AdventureWorksReward object Dim reward As New AdventureWorksReward() ' Create the EntityConnection Using connection As _ New EntityConnection("name=AdventureWorksEntities") ' Open the connection connection.Open() ' Define the query Dim queryString As String = "SELECT VALUE r FROM " & "OfType(AdventureWorksEntities.Rewards, " & "AdventureWorksModel.AdventureWorksReward) AS r WHERE " & "r.RewardID=@rewardID" ' Create the EntityCommand Using cmd = New EntityCommand(queryString, connection) ' Define the parameter Dim param = New EntityParameter() param.ParameterName = "rewardID" param.Value = rewardID cmd.Parameters.Add(param) ' Execute using an EntityReader Using reader = _ cmd.ExecuteReader(CommandBehavior.SequentialAccess) ' Process the results - there will only ' be a single Reward ' for any RewardID While reader.Read() reward.RewardID = reader.GetInt32(0) reward.RewardName = reader.GetString(1)
Lab 3: Querying Entity Data
L3-19
reward.NumberOfPointsRequired = reader.GetInt32(2) reward.Product = reader.GetString(3) End While End Using ' Be sure to close the connection connection.Close() End Using End Using ' Return the reward. Return reward End Function
[Visual C#] /// /// /// /// /// ///
Find Reward details given a RewardID. Implemented using an EntityCommand. RewardID to search for Single matching reward
public AdventureWorksReward GetReward(int rewardID) { // An empty AdventureWorksReward object AdventureWorksReward reward = new AdventureWorksReward(); // Create the EntityConnection using (EntityConnection connection = new EntityConnection("name=AdventureWorksEntities")) { // Open the connection connection.Open(); // Define the query var queryString = @"SELECT VALUE r FROM OfType(AdventureWorksEntities.Rewards, AdventureWorksModel.AdventureWorksReward) AS r WHERE r.RewardID=@rewardID"; // Create the EntityCommand using (EntityCommand cmd = new EntityCommand(queryString,
L3-20
Querying Entity Data
connection)) { // Define the parameter EntityParameter param = new EntityParameter(); param.ParameterName = "rewardID"; param.Value = rewardID; cmd.Parameters.Add(param); // Execute using an EntityReader using (EntityDataReader reader = cmd.ExecuteReader( CommandBehavior.SequentialAccess)) { // Process the results - there will only // be a single Reward // for any RewardID while (reader.Read()) { reward.RewardID = reader.GetInt32(0); reward.RewardName = reader.GetString(1); reward.NumberOfPointsRequired = reader.GetInt32(2); reward.Product = reader.GetString(3); } } // Be sure to close the connection connection.Close(); } } // Return the reward. return reward; }
5.
Save the DataAccessLayer code file: a.
If you are using Visual Basic, on the File menu, click Save DataAccessLayer.vb.
b.
If you are using Visual C#, on the File menu, click Save DataAccessLayer.cs.
Lab 3: Querying Entity Data
L3-21
Task 3: Add a unit test to verify your code 1.
2.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
Open the DataAccessLayerTest code file by double-clicking the comment TODO: Ex4 - Create a unit test for GetReward item in the task list. This task is located in the GetRewardTest method: •
3.
Delete the existing code in the GetRewardTest method: •
4.
In the task list, double-click the TODO: Ex4 - Create a unit test for GetReward item.
Select the code in the method, and then press DELETE.
Write a unit test to compare the Reward entity returned by your query, using a rewardID of 21 as the parameter value, with the Reward entity returned by the GetLocalRewardData method. Be sure to release all resources at the end of the test. Your code should resemble the following code example.
[Visual Basic] ''' '''A test for GetReward ''' _ Public Sub GetRewardTest() Dim Dim Dim Dim
dal As New DataAccessLayer() rewardID As Integer = 21 expected As AdventureWorksReward = Me.GetLocalRewardData() actual As AdventureWorksReward = dal.GetReward(rewardID)
Assert.AreEqual(expected.RewardID, actual.RewardID) Assert.AreEqual(expected.RewardName, actual.RewardName) Assert.AreEqual(expected.NumberOfPointsRequired, actual.NumberOfPointsRequired) Assert.AreEqual(expected.Product, actual.Product) dal.Dispose() End Sub
L3-22
Querying Entity Data
[Visual C#] /// ///A test for GetReward /// [TestMethod()] public void GetRewardTest() { DataAccessLayer dal = new DataAccessLayer(); int rewardID = 21; AdventureWorksReward expected = this.GetLocalRewardData(); AdventureWorksReward actual = dal.GetReward(rewardID); Assert.AreEqual(expected.RewardID, actual.RewardID); Assert.AreEqual(expected.RewardName, actual.RewardName); Assert.AreEqual(expected.NumberOfPointsRequired, actual.NumberOfPointsRequired); Assert.AreEqual(expected.Product, actual.Product); dal.Dispose(); }
5.
Save the DataAccessLayerTest code file: a.
If you are using Visual Basic, on the File menu, click Save DataAccessLayerTest.vb.
b.
If you are using Visual C#, on the File menu, click Save DataAccessLayerTest.cs.
Task 4: Build and test the application 1.
Build the solution and correct any errors: •
2.
On the Build menu, click Build Solution.
Start the application in Debug mode: •
On the Debug menu, click Start Debugging.
3.
In the AdventureWorks Rewards window, click All Customers to load contacts into the data grid. Click a contact in the data grid to display reward claims in the second data grid. Then, click a rewards claim in the second data grid to display the reward details on the form.
4.
Close the application.
5.
Run all of the tests in the solution: •
On the Test menu, point to Run, and then click All Tests in Solution.
Lab 3: Querying Entity Data
6.
Verify that all of the tests succeed, including GetRewardTest.
7.
Close the solution: •
L3-23
On the File menu, click Close Solution.
Exercise 5: Executing a Stored Procedure Task 1: Open the starter project for this exercise •
Open the existing solution, in the E:\Labfiles\Lab03\VB\Ex5\Starter or E:\Labfiles\Lab03\CS\Ex5\Starter folder: a.
On the File menu, point to Open, and then click Project/Solution.
b.
If you are using Visual Basic, in the Open Project dialog box, move to the E:\Labfiles\Lab03\VB\Ex5\Starter folder, click DAL.sln, and then click Open.
c.
If you are using Visual C#, in the Open Project dialog box, move to the E:\Labfiles\Lab03\CS\Ex5\Starter folder, click DAL.sln, and then click Open.
Task 2: Add code to retrieve the number of orders that a contact has placed 1.
2.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
Open the DataAccessLayer code file by double-clicking the comment TODO: Ex5 - Call the CountOrders stored procedure item in the task list. This task is located in the CountOrders method: •
3.
Delete the existing code in the CountOrders method: •
4.
In the task list, double-click the TODO: Ex5 - Call the CountOrders stored procedure item.
Select the code in the method, and then press DELETE.
Add code that performs the following tasks: a.
Instantiate the entities context object if it is currently null.
L3-24
Querying Entity Data
b.
Invoke the CountOrders method on the context object.
c.
Return the results.
Your code should resemble the following code example. [Visual Basic] ''' ''' Find the number of orders placed by a contact. ''' Invokes a function in the entity model, which in turn ''' invokes a stored procedure. ''' ''' ContactID to get count for ''' Number of orders Public Function CountOrders(ByVal contactID As Integer) As Integer _ Implements IDataAccessLayer.CountOrders ' Check we have an ObjectContext If (entities Is Nothing) Then entities = _ New AdventureWorksEntities() ' Invoke the stored procedure via the Entity model Return CType(entities.CountOrders(contactID).First(), Integer) End Function
[Visual C#] /// /// Find the number of orders placed by a contact. /// Invokes a function in the entity model, which in turn /// invokes a stored procedure. /// /// ContactID to get count for /// Number of orders public int CountOrders(int contactID) { // Check we have an ObjectContext if (entities == null) entities = new AdventureWorksEntities(); // Invoke the stored procedure via the Entity model return (int)entities.CountOrders(contactID).First(); }
5.
Save the DataAccessLayer code file: a.
If you are using Visual Basic, on the File menu, click Save DataAccessLayer.vb.
Lab 3: Querying Entity Data
b.
L3-25
If you are using Visual C#, on the File menu, click Save DataAccessLayer.cs.
Task 3: Add a unit test to verify your code 1.
2.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
Open the DataAccessLayerTest code file by double-clicking the comment TODO: Ex5 - Create a unit test for CountOrders item in the task list. This task is located in the CountOrdersTest method: •
3.
Delete the existing code in the CountOrdersTest method: •
4.
In the task list, double-click the TODO: Ex5 - Create a unit test for CountOrders item.
Select the code in the method, and then press DELETE.
Write a unit test to verify that the contact with a contact ID of 2 has placed four orders. Your code should resemble the following code example.
[Visual Basic] ''' '''A test for CountOrders ''' _ Public Sub CountOrdersTest() Dim Dim Dim Dim
dal As New DataAccessLayer() contactID As Integer = 2 expected As Integer = 4 actual As Integer = dal.CountOrders(contactID)
Assert.AreEqual(expected, actual) dal.Dispose() End Sub
L3-26
Querying Entity Data
[Visual C#] /// ///A test for CountOrders /// [TestMethod()] public void CountOrdersTest() { DataAccessLayer dal = new DataAccessLayer(); int contactID = 2; int expected = 4; int actual = dal.CountOrders(contactID); Assert.AreEqual(expected, actual); dal.Dispose(); }
5.
Save the DataAccessLayerTest code file: a.
If you are using Visual Basic, on the File menu, click Save DataAccessLayerTest.vb.
b.
If you are using Visual C#, on the File menu, click Save DataAccessLayerTest.cs.
Task 4: Build and test the application 1.
Build the solution and correct any errors: •
2.
On the Build menu, click Build Solution.
Start the application in Debug mode: •
On the Debug menu, click Start Debugging.
3.
In the AdventureWorks Rewards window, click All Customers to load contacts into the data grid. Then, click a contact in the data grid to display the number of orders placed by that contact on the form.
4.
Close the application.
5.
Run all of the tests in the solution: •
On the Test menu, point to Run, and then click All Tests in Solution.
6.
Verify that all of the tests succeed, including CountOrdersTest.
7.
Close Visual Studio: •
On the File menu, click Exit.
Lab 4: Creating, Updating, and Deleting Entity Data
L4-1
Creating, Updating, and Deleting Entity Data
Lab 4: Creating, Updating, and Deleting Entity Data Exercise 1: Maintaining Contact and Reward Data Task 1: Prepare the AdventureWorks database for the lab 1.
Log on to the 10265A-GEN-DEV-04 virtual machine as Student with the password Pa$$w0rd.
2.
In the E:\Labfiles folder, run AWReset.bat: a.
Click Start, and then click Computer.
b.
Double-click Allfiles (E:), double-click Labfiles, and then double-click AWReset.bat.
c.
Wait for the batch file to finish running, and then close Windows® Explorer.
Task 2: Open the starter project 1.
Open Microsoft® Visual Studio® 2010: •
2.
Click Start, click All Programs, click Microsoft Visual Studio 2010, and then click Microsoft Visual Studio 2010.
Open the existing solution, DAL.sln, in the E:\Labfiles\Lab04\VB\Ex1\Starter or E:\Labfiles\Lab04\CS\Ex1\Starter folder: a.
On the File menu, point to Open, and then click Project/Solution.
b.
If you are using Microsoft Visual Basic®, in the Open Project dialog box, move to the E:\Labfiles\Lab04\VB\Ex1\Starter folder, click DAL.sln, and then click Open.
c.
If you are using Microsoft Visual C#®, in the Open Project dialog box, move to the E:\Labfiles\Lab04\CS\Ex1\Starter folder, click DAL.sln, and then click Open.
L4-2
Creating, Updating, and Deleting Entity Data
Task 3: Add code to add a new contact 1.
2.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
Open the DataAccessLayer file by double-clicking the comment TODO: Ex1 Add a new Contact in the task list. This task is located in the AddContact method: •
3.
In the AddContact method, delete the existing code: •
4.
In the task list, double-click the comment TODO: Ex1 - Add a new Contact.
Select the code in the method, and then press DELETE.
Write code that performs the following tasks: a.
Check whether the entities variable is null, and if it is, instantiate it as a new instance of the AdventureWorksEntities context object.
Note: The entities variable is a private field in the DataAccessLayer class. Your code should perform all data access operations by using this context object.
b.
Encrypt the password in the contact passed as a parameter by calling the EncryptPassword method.
c.
Set the ModifiedDate property of the contact to the current date and time.
d. Assign a globally unique identifier (GUID) to the rowguid property of the contact. e.
Add the contact to the Contacts entity set.
f.
Save the changes to the database and return the new ContactID value.
g.
Handle any InvalidOperationException or UpdateException exceptions by throwing a new DALException exception.
Your code should resemble the following code example.
Lab 4: Creating, Updating, and Deleting Entity Data
[Visual Basic] ''' ''' Add a new Contact. ''' ''' The new Contact entity object to add. ''' The ContactID of the added contact. Public Function AddContact(ByVal contact As Contact) As Integer _ Implements IDataAccessLayer.AddContact ' Check you have an ObjectContext object. If (entities Is Nothing) Then entities = _ New AdventureWorksEntities() Try ' Encrypt the password in the detached object. EncryptPassword(contact) ' Set the time modified. contact.ModifiedDate = DateTime.Now ' Create a new GUID. contact.rowguid = Guid.NewGuid() ' Add the new contact to the entity set. entities.AddToContacts(contact) ' Save the changes to the database. entities.SaveChanges() ' ContactID is marked as an identity column in the model ' - so it will contain the ID of the added record. Return contact.ContactID Catch ex As InvalidOperationException Throw New DALException( "There was a problem adding the new Contact", ex) Catch ex As UpdateException Throw New DALException( "There was a problem saving the new Contact to the database", ex) End Try End Function
L4-3
L4-4
Creating, Updating, and Deleting Entity Data
[Visual C#] /// /// /// /// /// ///
Add a new Contact. The new Contact entity object to add. The ContactID of the added contact.
public int AddContact(Contact contact) { // Check you have an ObjectContext object. if (entities == null) entities = new AdventureWorksEntities(); try { // Encrypt the password in the detached object. EncryptPassword(contact); // Set the time modified. contact.ModifiedDate = DateTime.Now; // Create a new GUID. contact.rowguid = Guid.NewGuid(); // Add the new contact to the entity set. entities.AddToContacts(contact); // Save the changes to the database. entities.SaveChanges(); // ContactID is marked as an identity column in the model // - so it will contain the ID of the added record. return contact.ContactID; } catch (InvalidOperationException ex) { throw new DALException( "There was a problem adding the new Contact", ex); } catch (UpdateException ex) { throw new DALException( "There was a problem saving the new Contact to the database", ex); } }
Lab 4: Creating, Updating, and Deleting Entity Data
5.
L4-5
Save the DataAccessLayer code file: a.
If you are using Visual Basic, on the File menu, click Save DataAccessLayer.vb.
b.
If you are using Visual C#, on the File menu, click Save DataAccessLayer.cs.
Task 4: Add code to update a contact 1.
2.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
Open the DataAccessLayer file by double-clicking the comment TODO: Ex1 Modify an existing Contact in the task list. This task is located in the UpdateContact method: •
3.
In the UpdateContact method, delete the existing code: •
4.
In the task list, double-click the comment TODO: Ex1 - Modify an existing Contact.
Select the code in the method, and then press DELETE.
Write code that performs the following tasks: a.
Check whether the entities variable is null, and if it is, instantiate it as a new instance of the AdventureWorksEntities context object.
b.
Get the EntityKey property of the detached contact passed to the method. This detached contact contains the modified properties.
c.
Use the TryGetObjectByKey method to load the correct contact into the context.
d. Use the ApplyCurrentValues method to copy data from the detached contact passed as a parameter. e.
Save the changes to the database and return true.
f.
Handle any InvalidOperationException or UpdateException exceptions by throwing a new DALException exception.
Your code should resemble the following code example.
L4-6
Creating, Updating, and Deleting Entity Data
[Visual Basic] ''' ''' Modify an existing contact. ''' ''' A detached entity object containing the details to modify. Public Function UpdateContact(ByVal contact As Contact) As Boolean _ Implements IDataAccessLayer.UpdateContact ' Check you have an ObjectContext object. If (entities Is Nothing) Then entities = _ New AdventureWorksEntities() Try ' Get the key of the entity you are modifying. Dim key As EntityKey = entities.CreateEntityKey("Contacts", contact) Dim contactToModify As Object = Nothing ' Ensure that the entity to modify is loaded. If entities.TryGetObjectByKey(key, contactToModify) Then ' Copy all of the changes over from the ' detached contact entity. entities.ApplyCurrentValues(key.EntitySetName, contact) End If ' Save the changes to the database. entities.SaveChanges() Return True Catch ex As InvalidOperationException Throw New DALException("There was a problem changing the Contact", ex) Catch ex As UpdateException Throw New DALException("There was a problem saving the Contact to the database", ex) End Try End Function
Lab 4: Creating, Updating, and Deleting Entity Data
L4-7
[Visual C#] /// /// Modify an existing contact. /// /// A detached entity object containing the details to modify. public bool UpdateContact(Contact contact) { // Check you have an ObjectContext object. if (entities == null) entities = new AdventureWorksEntities(); try { // Get the key of the entity you are modifying. EntityKey key = entities.CreateEntityKey("Contacts", contact); object contactToModify; // Ensure that the entity to modify is loaded. if (entities.TryGetObjectByKey(key, out contactToModify)) { // Copy all of the changes over from the // detached contact entity. entities.ApplyCurrentValues(key.EntitySetName, contact); } // Save the changes to the database. entities.SaveChanges(); return true; } catch (InvalidOperationException ex) { throw new DALException("There was a problem changing the Contact", ex); } catch (UpdateException ex) { throw new DALException("There was a problem saving the Contact to the database", ex); } }
L4-8
Creating, Updating, and Deleting Entity Data
5.
Save the DataAccessLayer code file: a.
If you are using Visual Basic, on the File menu, click Save DataAccessLayer.vb.
b.
If you are using Visual C#, on the File menu, click Save DataAccessLayer.cs.
Task 5: Add code to delete a contact 1.
2.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
Open the DataAccessLayer file by double-clicking the comment TODO: Ex1 Delete an existing Contact in the task list. This task is located in the DeleteContact method: •
3.
In the DeleteContact method, delete the existing code: •
4.
In the task list, double-click the comment TODO: Ex1 - Delete an existing Contact.
Select the code in the method, and then press DELETE.
Write code that performs the following tasks: a.
Check whether the entities variable is null, and if it is, instantiate it as a new instance of the AdventureWorksEntities context object.
b.
Use the contactID variable passed as a parameter to create the EntityKey of the contact to delete.
c.
Use the TryGetObjectByKey method to load the correct contact into the context.
d. Delete all of the related RewardsClaimed entities belonging to the contact from the context. e.
Delete the contact from the context.
f.
Save all of the changes to the database and return true.
g.
Handle any InvalidOperationException or UpdateException exceptions by throwing a new DALException exception.
Your code should resemble the following code example.
Lab 4: Creating, Updating, and Deleting Entity Data
L4-9
[Visual Basic] ''' ''' Delete a contact. ''' ''' The ContactID of the contact to delete. ''' True if it succeeds. Public Function DeleteContact(ByVal contactID As Integer) As Boolean _ Implements IDataAccessLayer.DeleteContact ' Check you have an ObjectContext object. If (entities Is Nothing) Then entities = _ New AdventureWorksEntities() Try ' Get the key of the entity you are deleting. Dim key As New EntityKey( "AdventureWorksEntities.Contacts", "ContactID", contactID) Dim contactToDelete As Object = Nothing ' Ensure that the entity to delete is loaded. If entities.TryGetObjectByKey(key, contactToDelete) Then ' Delete all of the RewardsClaimed entities belonging ' to the contact. ' NOTE - an alternative would be to configure ' Cascade Delete in the EDM. For Each rewardClaimed In (DirectCast(contactToDelete, Contact).RewardsClaimed) entities.DeleteObject(rewardClaimed) Next ' Delete the contact. entities.DeleteObject(contactToDelete) End If ' Persist the deletions to the database. entities.SaveChanges() Return True Catch ex As InvalidOperationException Throw New DALException("There was a problem deleting the Contact", ex)
L4-10
Creating, Updating, and Deleting Entity Data
Catch ex As UpdateException Throw New DALException("There was a problem deleting the Contact from the database", ex) End Try End Function
[Visual C#] /// /// Delete a contact. /// /// The ContactID of the contact to /// delete. /// True if it succeeds. public bool DeleteContact(int contactID) { // Check you have an ObjectContext object. if (entities == null) entities = new AdventureWorksEntities(); try { // Get the key of the entity you are deleting. EntityKey key = new EntityKey( "AdventureWorksEntities.Contacts", "ContactID", contactID); object contactToDelete; // Ensure that the entity to delete is loaded. if (entities.TryGetObjectByKey(key, out contactToDelete)) { // Delete all of the RewardsClaimed entities belonging // to the contact. // NOTE - an alternative would be to configure // Cascade Delete in the EDM. foreach (var rewardClaimed in ((Contact)contactToDelete).RewardsClaimed) { entities.DeleteObject(rewardClaimed); } // Delete the contact. entities.DeleteObject(contactToDelete); }
Lab 4: Creating, Updating, and Deleting Entity Data
L4-11
// Persist the deletions to the database. entities.SaveChanges(); return true; } catch (InvalidOperationException ex) { throw new DALException("There was a problem deleting the Contact", ex); } catch (UpdateException ex) { throw new DALException("There was a problem deleting the Contact from the database", ex); } }
5.
Save the DataAccessLayer code file: a.
If you are using Visual Basic, on the File menu, click Save DataAccessLayer.vb.
b.
If you are using Visual C#, on the File menu, click Save DataAccessLayer.cs.
Task 6: Add unit tests to verify your code 1.
2.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
Open the DataAccessLayerTest file by double-clicking the comment TODO: Ex1 - Add a test for AddContact in the task list. This task is located in the AddContactTest method: •
3.
In the task list, double-click the comment TODO: Ex1 - Add a test for AddContact.
In the AddContactTest method, delete the existing code: •
Select the code in the method, and then press DELETE.
L4-12
Creating, Updating, and Deleting Entity Data
4.
Add a unit test to verify the behavior of the AddContact method. Use the CreateTestContact method to create a contact to add to the database, and use the GetContactById method to retrieve the contact from the database. Ensure that you remove the contact and release any resources at the end of the test. Your code should resemble the following code example.
[Visual Basic] ''' '''A test for AddContact ''' _ Public Sub AddContactTest() Dim target As New DataAccessLayer() ' Create a new contact and add it to the database. Dim contact As Contact = CreateTestContact() Dim contactId As Integer contactId = target.AddContact(contact) Dim expected As Contact = CreateTestContact() ' Get the saved contact back from the database. Dim actual As Contact = GetContactById(contactId) ' Check the saved data Assert.IsTrue(actual.ContactID > 0) Assert.AreEqual(expected.Title, actual.Title) Assert.AreEqual(expected.FirstName, actual.FirstName) Assert.AreEqual(expected.MiddleName, actual.MiddleName) Assert.AreEqual(expected.LastName, actual.LastName) Assert.AreEqual(expected.EmailAddress, actual.EmailAddress) Assert.AreEqual(expected.Phone, actual.Phone) Assert.AreEqual(expected.CurrentPoints, actual.CurrentPoints) Assert.AreNotEqual(expected.PasswordHash, actual.PasswordHash) Assert.AreNotEqual(expected.PasswordSalt, actual.PasswordSalt)
' Tidy up. If contactId > 0 Then target.DeleteContact(contactId) End If target.Dispose() End Sub
Lab 4: Creating, Updating, and Deleting Entity Data
L4-13
[Visual C#] /// ///A test for AddContact. /// [TestMethod()] public void AddContactTest() { DataAccessLayer target = new DataAccessLayer(); // Create a new contact and add it to the database. Contact contact = CreateTestContact(); int contactId; contactId = target.AddContact(contact); Contact expected = CreateTestContact(); // Get the saved contact back from the database. Contact actual = GetContactById(contactId); // Check the saved data Assert.IsTrue(actual.ContactID > 0); Assert.AreEqual(expected.Title, actual.Title); Assert.AreEqual(expected.FirstName, actual.FirstName); Assert.AreEqual(expected.MiddleName, actual.MiddleName); Assert.AreEqual(expected.LastName, actual.LastName); Assert.AreEqual(expected.EmailAddress, actual.EmailAddress); Assert.AreEqual(expected.Phone, actual.Phone); Assert.AreEqual(expected.CurrentPoints, actual.CurrentPoints); Assert.AreNotEqual(expected.PasswordHash, actual.PasswordHash); Assert.AreNotEqual(expected.PasswordSalt, actual.PasswordSalt);
// Tidy up. if (contactId > 0) target.DeleteContact(contactId); target.Dispose(); }
5.
Locate the ModifyContactTest method by double-clicking the comment TODO: Ex1 - Add a test for ModifyContact in the task list. This task is located in the ModifyContactTest method: •
6.
In the task list, double-click the comment TODO: Ex1 - Add a test for ModifyContact.
In the ModifyContactTest method, delete the existing code:
L4-14
Creating, Updating, and Deleting Entity Data
• 7.
Select the code in the method, and then press DELETE.
Add a unit test to verify the behavior of the ModifyContact method. Use the CreateTestContact method to create a contact to add to the database, which you can then modify, and use the GetContactById method to retrieve the contact from the database. Ensure that you remove the contact and release any resources at the end of the test. Your code should resemble the following code example.
[Visual Basic] ''' '''A test for ModifyContact ''' _ Public Sub ModifyContactTest() Dim target As New DataAccessLayer() Dim contact As Contact = CreateTestContact() Dim testId As Integer testId = target.AddContact(contact) ' Get a detached version of this contact. Dim expected As Contact = GetContactById(testId) ' Make some changes. expected.ContactID = testId expected.Title = "Mr." expected.FirstName = "Rupert" expected.MiddleName = "M." expected.LastName = "Beddoes" expected.EmailAddress = "
[email protected]" expected.Phone = "453-555-0166" expected.CurrentPoints = 1000 ' Save the changes. target.UpdateContact(expected) ' Check the results Dim actual As Contact = GetContactById(testId) Assert.AreEqual(expected.Title, actual.Title) Assert.AreEqual(expected.FirstName, actual.FirstName) Assert.AreEqual(expected.MiddleName, actual.MiddleName) Assert.AreEqual(expected.LastName, actual.LastName) Assert.AreEqual(expected.EmailAddress, actual.EmailAddress) Assert.AreEqual(expected.Phone, actual.Phone) Assert.AreEqual(expected.CurrentPoints, actual.CurrentPoints)
Lab 4: Creating, Updating, and Deleting Entity Data
' Tidy up. target.DeleteContact(testId) target.Dispose() End Sub
[Visual C#] /// ///A test for ModifyContact. /// [TestMethod()] public void ModifyContactTest() { DataAccessLayer target = new DataAccessLayer(); Contact contact = CreateTestContact(); int testId; testId = target.AddContact(contact); // Get a detached version of this contact. Contact expected = GetContactById(testId); // Make some changes. expected.ContactID = testId; expected.Title = "Mr."; expected.FirstName = "Rupert"; expected.MiddleName = "M."; expected.LastName = "Beddoes"; expected.EmailAddress = "
[email protected]"; expected.Phone = "453-555-0166"; expected.CurrentPoints = 1000; // Save the changes. target.UpdateContact(expected); // Check the results Contact actual = GetContactById(testId); Assert.AreEqual(expected.Title, actual.Title); Assert.AreEqual(expected.FirstName, actual.FirstName); Assert.AreEqual(expected.MiddleName, actual.MiddleName); Assert.AreEqual(expected.LastName, actual.LastName); Assert.AreEqual(expected.EmailAddress, actual.EmailAddress); Assert.AreEqual(expected.Phone, actual.Phone); Assert.AreEqual(expected.CurrentPoints, actual.CurrentPoints);
L4-15
L4-16
Creating, Updating, and Deleting Entity Data
// Tidy up. target.DeleteContact(testId); target.Dispose(); }
8.
Locate the DeleteContactTest method by double-clicking the comment TODO: Ex1 - Add a test for DeleteContact in the task list. This task is located in the DeleteContactTest method: •
9.
In the task list, double-click the comment TODO: Ex1 - Add a test for DeleteContact.
In the DeleteContactTest method, delete the existing code: •
Select the code in the method, and then press DELETE.
10. Add a unit test to verify the behavior of the DeleteContact method. Use the CreateTestContact method to create a contact to add to the database, which you can then delete, and use the GetContactById method to try to retrieve the contact from the database. Ensure that you release any resources at the end of the test. Your code should resemble the following code example. [Visual Basic] ''' '''A test for DeleteContact ''' _ Public Sub DeleteContactTest() Dim target As New DataAccessLayer() ' Add a test contact to the database. Dim testContact As Contact = CreateTestContact() Dim contactID As Integer = target.AddContact(testContact)
' Delete the contact. target.DeleteContact(contactID) ' Check that it has gone. Dim deleted As Contact = GetContactById(contactID) Assert.IsNull(deleted)
Lab 4: Creating, Updating, and Deleting Entity Data
L4-17
' Tidy up. target.Dispose() End Sub
[Visual C#] /// ///A test for DeleteContact. /// [TestMethod()] public void DeleteContactTest() { DataAccessLayer target = new DataAccessLayer(); // Add a test contact to the database. Contact testContact = CreateTestContact(); int contactID = target.AddContact(testContact); // Delete the contact. target.DeleteContact(contactID); // Check that it has gone. Contact deleted = GetContactById(contactID); Assert.IsNull(deleted); // Tidy up. target.Dispose(); }
11. Save the DataAccessLayerTest code file: a.
If you are using Visual Basic, on the File menu, click Save DataAccessLayerTest.vb.
b.
If you are using Visual C#, on the File menu, click Save DataAccessLayerTest.cs.
Task 7: Add code to add a new reward 1.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
L4-18
Creating, Updating, and Deleting Entity Data
2.
Open the DataAccessLayer file by double-clicking the comment TODO: Ex1 Add a new Reward in the task list. This task is located in the AddReward method: •
3.
In the task list, double-click the comment TODO: Ex1 - Add a new Reward.
In the AddReward method, delete the existing code: •
4.
Select the code in the method, and then press DELETE.
Write code that performs the following tasks: a.
Check whether the entities variable is null, and if it is, instantiate it as a new instance of the AdventureWorksEntities context object.
Note: The entities variable is a private field in the DataAccessLayer class. Your code should perform all data access operations by using this context object.
b.
Get the next available RewardID value by calling the GetNextRewardID method.
c.
Add the reward to the Rewards entity set.
d. Save the changes to the database and return the RewardID value. e.
Handle any InvalidOperationException or UpdateException exceptions by throwing a new DALException exception.
Your code should resemble the following code example. [Visual Basic] ''' ''' Adds a new reward. ''' ''' A new entity object containing the details of the Reward to add. ''' The RewardID of the added Reward. Public Function AddReward(ByVal reward As Reward) As Integer _ Implements IDataAccessLayer.AddReward ' Check you have an ObjectContext object. If (entities Is Nothing) Then entities = _ New AdventureWorksEntities() Try ' Get the next available rewardID.
Lab 4: Creating, Updating, and Deleting Entity Data
L4-19
reward.RewardID = GetNextRewardID() ' Add the reward to the entity set. entities.AddToRewards(reward) ' Save the changes to the database. entities.SaveChanges() Return reward.RewardID Catch ex As InvalidOperationException Throw New DALException("There was a problem adding the new Reward", ex) Catch ex As UpdateException Throw New DALException("There was a problem saving the new Reward to the database", ex) End Try End Function
[Visual C#] /// /// Adds a new reward. /// /// A new entity object containing the details of the Reward to add. /// The RewardID of the added Reward. public int AddReward(Reward reward) { // Check you have an ObjectContext object. if (entities == null) entities = new AdventureWorksEntities(); try { // Get the next available rewardID. reward.RewardID = GetNextRewardID(); // Add the reward to the entity set. entities.AddToRewards(reward); // Save the changes to the database. entities.SaveChanges(); return reward.RewardID;
L4-20
Creating, Updating, and Deleting Entity Data
} catch (InvalidOperationException ex) { throw new DALException("There was a problem adding the new Reward", ex); } catch (UpdateException ex) { throw new DALException("There was a problem saving the new Reward to the database", ex); } }
5.
Save the DataAccessLayer code file: a.
If you are using Visual Basic, on the File menu, click Save DataAccessLayer.vb.
b.
If you are using Visual C#, on the File menu, click Save DataAccessLayer.cs.
Task 8: Add code to update a reward 1.
2.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
Open the DataAccessLayer file by double-clicking the comment TODO: Ex1 Modify an existing Reward in the task list. This task is located in the UpdateReward method: •
3.
In the UpdateReward method, delete the existing code: •
4.
In the task list, double-click the comment TODO: Ex1 - Modify an existing Reward.
Select the code in the method, and then press DELETE.
Write code that performs the following tasks: a.
Check whether the entities variable is null, and if it is, instantiate it as a new instance of the AdventureWorksEntities context object.
b.
Get the EntityKey value of the detached contact passed to the method. This detached contact contains the modified properties.
Lab 4: Creating, Updating, and Deleting Entity Data
c.
L4-21
Use the TryGetObjectByKey method to load the correct contact into the context.
d. Use the ApplyCurrentValues method to copy data from the detached contact passed as a parameter. e.
Save the changes to the database and return true.
f.
Handle any InvalidOperationException or UpdateException exceptions by throwing a new DALException exception.
Your code should resemble the following code example. [Visual Basic] ''' ''' Modify an existing reward. ''' ''' A detached entity object containing the details to modify. Public Function UpdateReward(ByVal reward As Reward) As Integer _ Implements IDataAccessLayer.UpdateReward ' Check you have an ObjectContext object. If (entities Is Nothing) Then entities = _ New AdventureWorksEntities() Try ' Get the key of the entity you are modifying. Dim key As EntityKey = _ entities.CreateEntityKey("Rewards", reward) Dim rewardToModify As Object = Nothing ' Ensure that the entity to modify is loaded. If entities.TryGetObjectByKey(key, rewardToModify) Then ' Copy all of the changes over from the detached ' Reward entity. entities.ApplyCurrentValues(key.EntitySetName, reward) End If ' Save the changes to the database. entities.SaveChanges() Return True Catch ex As InvalidOperationException
L4-22
Creating, Updating, and Deleting Entity Data
Throw New DALException("There was a problem changing the Reward", ex) Catch ex As UpdateException Throw New DALException("There was a problem saving the Reward to the database", ex) End Try End Function
[Visual C#] /// /// Modify an existing reward. /// /// A detached entity object containing the details to modify. public bool UpdateReward(Reward reward) { // Check you have an ObjectContext object. if (entities == null) entities = new AdventureWorksEntities(); try { // Get the key of the entity you are modifying. EntityKey key = entities.CreateEntityKey("Rewards", reward); object rewardToModify; // Ensure that the entity to modify is loaded. if (entities.TryGetObjectByKey(key, out rewardToModify)) { // Copy all of the changes over from the detached // Reward entity. entities.ApplyCurrentValues(key.EntitySetName, reward); } // Save the changes to the database. entities.SaveChanges(); return true; }
Lab 4: Creating, Updating, and Deleting Entity Data
L4-23
catch (InvalidOperationException ex) { throw new DALException("There was a problem changing the Reward", ex); } catch (UpdateException ex) { throw new DALException("There was a problem saving the Reward to the database", ex); } }
5.
Save the DataAccessLayer code file: a.
If you are using Visual Basic, on the File menu, click Save DataAccessLayer.vb.
b.
If you are using Visual C#, on the File menu, click Save DataAccessLayer.cs.
Task 9: Add code to delete a reward 1.
2.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
Open the DataAccessLayer file by double-clicking the comment TODO: Ex1 Delete an existing Reward in the task list. This task is located in the DeleteReward method: •
3.
In the DeleteReward method, delete the existing code: •
4.
In the task list, double-click the comment TODO: Ex1 - Delete an existing Reward.
Select the code in the method, and then press DELETE.
Write code that performs the following tasks: a.
Check whether the entities variable is null, and if it is, instantiate it as a new instance of the AdventureWorksEntities context object.
b.
Use the rewardID value passed as a parameter to create the EntityKey object of the reward to delete.
L4-24
Creating, Updating, and Deleting Entity Data
c.
Use the TryGetObjectByKey method to load the correct reward into the context.
d. Delete the reward from the context. e.
Save the change to the database and return true.
f.
Handle any InvalidOperationException or UpdateException exceptions by throwing a new DALException exception.
Your code should resemble the following code example. [Visual Basic] ''' ''' Delete a reward. ''' ''' The RewardID of the reward to delete. Public Function DeleteReward(ByVal rewardID As Integer) As Boolean _ Implements IDataAccessLayer.DeleteReward ' Check you have an ObjectContext object. If (entities Is Nothing) Then entities = _ New AdventureWorksEntities() Try Dim rewardToDelete As Object = Nothing Dim key As EntityKey = New EntityKey( _ "AdventureWorksEntities.Rewards", "RewardID", rewardID) ' Ensure that the entity to modify is loaded. If entities.TryGetObjectByKey(key, rewardToDelete) Then ' Delete the object. ' This could fail because of referential ' integrity violations entities.DeleteObject(rewardToDelete) End If Return True Catch ex As InvalidOperationException
Lab 4: Creating, Updating, and Deleting Entity Data
Throw New DALException("There was a problem deleting the Reward", ex) Catch ex As UpdateException Throw New DALException("There was a problem deleting the Reward from the database", ex) End Try End Function
[Visual C#] /// /// Delete a reward. /// /// The RewardID of the reward to delete. public bool DeleteReward(int rewardID) { // Check you have an ObjectContext object. if (entities == null) entities = new AdventureWorksEntities(); try { object rewardToDelete = null; EntityKey key = new EntityKey("AdventureWorksEntities.Rewards", "RewardID", rewardID); // Ensure that the entity to modify is loaded. if (entities.TryGetObjectByKey(key, out rewardToDelete)) { // Delete the object. // This could fail because of referential // integrity violations entities.DeleteObject(rewardToDelete); } return true; } catch (InvalidOperationException ex) {
L4-25
L4-26
Creating, Updating, and Deleting Entity Data
throw new DALException("There was a problem deleting the Reward", ex); } catch (UpdateException ex) { throw new DALException("There was a problem deleting the Reward from the database", ex); } }
5.
Save the DataAccessLayer code file: a.
If you are using Visual Basic, on the File menu, click Save DataAccessLayer.vb.
b.
If you are using Visual C#, on the File menu, click Save DataAccessLayer.cs.
Task 10: Add unit tests to verify your code 1.
2.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
Open the DataAccessLayerTest file by double-clicking the comment TODO: Ex1 - Add a test for AddReward in the task list. This task is located in the AddRewardTest method: •
3.
In the AddRewardTest method, delete the existing code: •
4.
In the task list, double-click the comment TODO: Ex1 - Add a test for AddReward.
Select the code in the method, and then press DELETE.
Add a unit test to verify the behavior of the AddReward method. Use the CreateAdventureWorksRewardData and CreateSupermarketRewardData methods to create rewards to add to the database, and use the GetRewardById method to retrieve the rewards from the database. Ensure that you remove the rewards and release any resources at the end of the test. Your code should resemble the following code example.
Lab 4: Creating, Updating, and Deleting Entity Data
L4-27
[Visual Basic] ''' '''A test for AddReward ''' _ Public Sub AddRewardTest() Dim target As New DataAccessLayer() ' Create some test rewards Dim reward1 As AdventureWorksReward = _ CreateAdventureWorksRewardData() Dim reward2 As SupermarketReward = CreateSupermarketRewardData() Dim expected1 As AdventureWorksReward = _ CreateAdventureWorksRewardData() Dim expected2 As SupermarketReward = CreateSupermarketRewardData() ' Add the first one to the database. Dim id As Integer id = target.AddReward(reward1) ' Do some checks. Assert.IsTrue(id > 0) Dim actual As Reward = GetRewardById(id) Assert.IsInstanceOfType(actual, GetType(AdventureWorksReward)) ' Tidy up. target.DeleteReward(id) ' Add the second one to the database. id = target.AddReward(reward2) ' Do some tests. Assert.IsTrue(id > 0) actual = GetRewardById(id) Assert.IsInstanceOfType(actual, GetType(SupermarketReward)) ' Tidy up. target.DeleteReward(id) target.Dispose() End Sub
L4-28
Creating, Updating, and Deleting Entity Data
[Visual C#] /// ///A test for AddReward. /// [TestMethod()] public void AddRewardTest() { DataAccessLayer target = new DataAccessLayer(); // Create some test rewards AdventureWorksReward reward1 = CreateAdventureWorksRewardData(); SupermarketReward reward2 = CreateSupermarketRewardData(); AdventureWorksReward expected1 = CreateAdventureWorksRewardData(); SupermarketReward expected2 = CreateSupermarketRewardData(); // Add the first one to the database. int id; id = target.AddReward(reward1); // Do some checks. Assert.IsTrue(id > 0); Reward actual = GetRewardById(id); Assert.IsInstanceOfType(actual, typeof(AdventureWorksReward)); // Tidy up. target.DeleteReward(id); // Add the second one to the database. id = target.AddReward(reward2); // Do some tests. Assert.IsTrue(id > 0); actual = GetRewardById(id); Assert.IsInstanceOfType(actual, typeof(SupermarketReward)); // Tidy up. target.DeleteReward(id); target.Dispose(); }
5.
Locate the ModifyRewardTest method by double-clicking the comment TODO: Ex1 - Add a test for ModifyReward in the task list. This task is located in the ModifyRewardTest method: •
6.
In the task list, double-click the comment TODO: Ex1 - Add a test for ModifyReward.
In the ModifyRewardTest method, delete the existing code:
Lab 4: Creating, Updating, and Deleting Entity Data
• 7.
L4-29
Select the code in the method, and then press DELETE.
Add a unit test to verify the behavior of the ModifyReward method. Use the CreateSupermarketRewardData method to create a reward to add to the database, which you can then modify, and use the GetRewardById method to retrieve the reward from the database. Ensure that you remove the reward and release any resources at the end of the test. Your code should resemble the following code example.
[Visual Basic] ''' '''A test for ModifyReward ''' _ Public Sub ModifyRewardTest() Dim target As New DataAccessLayer() ' Create some test data. Dim testdata As Reward = CreateSupermarketRewardData() Dim testid As Integer = target.AddReward(testdata) ' Get a detached copy of the reward and modify. Dim expected As SupermarketReward = _ DirectCast(GetRewardById(testid), SupermarketReward) expected.MoneyBackPerPoint = 0.15 expected.RewardName = "Changed" target.UpdateReward(expected) ' Get the copy back from the database. Dim actual As SupermarketReward = _ DirectCast(GetRewardById(testid), SupermarketReward) ' Do some checks. Assert.AreEqual(expected.MoneyBackPerPoint, _ actual.MoneyBackPerPoint) Assert.AreEqual(expected.RewardName, actual.RewardName) ' Tidy up. target.DeleteReward(testid) target.Dispose() End Sub
L4-30
Creating, Updating, and Deleting Entity Data
[Visual C#] /// ///A test for ModifyReward. /// [TestMethod()] public void ModifyRewardTest() { DataAccessLayer target = new DataAccessLayer(); // Create some test data. Reward testdata = CreateSupermarketRewardData(); int testid = target.AddReward(testdata); // Get a detached copy of the reward and modify. SupermarketReward expected = (SupermarketReward)GetRewardById(testid); expected.MoneyBackPerPoint = 0.15m; expected.RewardName = "Changed"; target.UpdateReward(expected); // Get the copy back from the database. SupermarketReward actual = (SupermarketReward)GetRewardById(testid); // Do some checks. Assert.AreEqual(expected.MoneyBackPerPoint, actual.MoneyBackPerPoint); Assert.AreEqual(expected.RewardName, actual.RewardName); // Tidy up. target.DeleteReward(testid); target.Dispose(); }
8.
Locate the DeleteRewardTest method by double-clicking the comment TODO: Ex1 - Add a test for DeleteReward in the task list. This task is located in the DeleteRewardTest method: •
9.
In the task list, double-click the comment TODO: Ex1 - Add a test for DeleteReward.
In the DeleteRewardTest method, delete the existing code: •
Select the code in the method, and then press DELETE.
10. Add a unit test to verify the behavior of the DeleteReward method. Use the CreateAdventureWorksRewardData method to create a reward to add to the
Lab 4: Creating, Updating, and Deleting Entity Data
L4-31
database, which you can then delete. Ensure that you release any resources at the end of the test. Your code should resemble the following code example. [Visual Basic] ''' '''A test for DeleteReward ''' _ Public Sub DeleteRewardTest() Dim target As New DataAccessLayer() ' Add a test reward to the database. Dim reward As AdventureWorksReward = _ CreateAdventureWorksRewardData() Dim rewardID As Integer = target.AddReward(reward) ' Delete the contact. Dim actual As Boolean = target.DeleteReward(rewardID) ' Check that the delete was successful. Assert.IsTrue(actual) ' Tidy up. target.Dispose() End Sub
[Visual C#] /// ///A test for DeleteReward. /// [TestMethod()] public void DeleteRewardTest() { DataAccessLayer target = new DataAccessLayer(); // Add a test reward to the database. AdventureWorksReward reward = CreateAdventureWorksRewardData(); int rewardID = target.AddReward(reward);
L4-32
Creating, Updating, and Deleting Entity Data
// Delete the contact. bool actual = target.DeleteReward(rewardID); // Check that the delete was successful. Assert.IsTrue(actual); // Tidy up. target.Dispose(); }
11. Save the DataAccessLayerTest code file: a.
If you are using Visual Basic, on the File menu, click Save DataAccessLayerTest.vb.
b.
If you are using Visual C#, on the File menu, click Save DataAccessLayerTest.cs.
Task 11: Build and test the application 1.
Build the solution and correct any errors: •
2.
On the Build menu, click Build Solution.
Run all of the tests in the solution: •
On the Test menu, point to Run, and then click All Tests in Solution.
3.
Verify that all of the tests succeed.
4.
Start the application in Debug mode: •
On the Debug menu, click Start Debugging.
5.
In the AdventureWorks Rewards window, click All Customers to load data from the entity model into the data grid. Verify that you can add, modify, and delete contact data. Verify that you can add and modify Adventure Works reward data.
6.
Close the application.
7.
Close the solution: •
On the File menu, click Close Solution.
Lab 4: Creating, Updating, and Deleting Entity Data
L4-33
Exercise 2: Maintaining RewardsClaim Data Task 1: Open the starter project •
Open the existing solution, DAL.sln, in the E:\Labfiles\Lab04\Ex2\VB\Starter or E:\Labfiles\Lab04\Ex2\CS\Starter folder: a.
On the File menu, point to Open, and then click Project/Solution.
b.
If you are using Visual Basic, in the Open Project dialog box, move to the E:\Labfiles\Lab04\VB\Ex2\Starter folder, click DAL.sln, and then click Open.
c.
If you are using Visual C#, in the Open Project dialog box, move to the E:\Labfiles\Lab04\CS\Ex2\Starter folder, click DAL.sln, and then click Open.
Task 2: Add data modification stored procedures to your model 1.
Open the AdventureWorksEDM model in the Entity Designer. •
2.
3.
In Solution Explorer, right-click AdventureWorksEDM.edmx, and then click Open.
Run the Update Wizard to add the uspInsertRewardsClaim, uspUpdateRewardsClaim, and uspDeleteRewardsClaim stored procedures to the model: a.
In the Entity Designer pane, right-click anywhere in the white space, and then click Update Model from Database.
b.
In the Update Wizard window, on the Choose Your Database Objects page, on the Add tab, expand Stored Procedures, select the uspInsertRewardsClaim (Sales), uspUpdateRewardsClaim (Sales), and uspDeleteRewardsClaim (Sales) check boxes, and then click Finish.
Map the RewardsClaimed entity to the stored procedures in the following table. Function
Stored Procedure
Insert
uspInsertRewardsClaim
Update
uspUpdateRewardsClaim
Delete
uspDeleteRewardsClaim
L4-34
Creating, Updating, and Deleting Entity Data
a.
In the Entity Designer pane, right-click the RewardsClaimed entity, and then click Stored Procedure Mapping.
b.
Click , and then in the drop-down list, click uspInsertRewardsClaim.
c.
Click , and then in the drop-down list, click uspUpdateRewardsClaim.
d. Click , and then in the drop-down list, click uspDeleteRewardsClaim. 4.
Map the stored procedure parameters to the entity properties as shown in the following table. Stored Procedure
Property
uspInsertRewardsClaim
claimID : int
ClaimID : Int32
uspInsertRewardsClaim
pointsUsed : int
PointsUsed : Int32
uspInsertRewardsClaim
rewardID : int
RewardID : Int32
uspInsertRewardsClaim
contactID : int
ContactID : Int32
uspUpdateRewardsClaim
claimID : int
ClaimID : Int32
uspUpdateRewardsClaim
pointsUsed : int
PointsUsed : Int32
uspUpdateRewardsClaim
rewardID : int
RewardID : Int32
uspUpdateRewardsClaim
contactID : int
ContactID : Int32
uspDeleteRewardsClaim
claimID : int
ClaimID : Int32
•
5.
Parameter
For each parameter, perform the following steps: i.
Next to the parameter name, click the empty Property.
ii.
Click the correct property value.
Save the AdventureWorksEDM model: •
On the File menu, click Save AdventureWorksEDM.edmx.
Lab 4: Creating, Updating, and Deleting Entity Data
L4-35
Task 3: Add code to add a new RewardsClaim record 1.
2.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
Open the DataAccessLayer file by double-clicking the comment TODO: Ex2 Add a new RewardsClaimed entity in the task list. This task is located in the CreateRewardsClaim method: •
3.
In the CreateRewardsClaim method, delete the existing code: •
4.
In the task list, double-click the comment TODO: Ex2 - Add a new RewardsClaimed entity.
Select the code in the method, and then press DELETE.
Write code that performs the following tasks: a.
Check whether the entities variable is null, and if it is, instantiate it as a new instance of the AdventureWorksEntities context object.
Note: The entities variable is a private field in the DataAccessLayer class. Your code should perform all data access operations by using this context object.
b.
Set the ClaimID by calling the GetNextClaimID method.
c.
Add the claim to the RewardsClaimed entity set.
d. Decrement the associated contact’s points by the points used for the claim. e.
Save all of the changes to the database and return the new ClaimID.
f.
Handle any InvalidOperationException or UpdateException exceptions by throwing a new DALException exception.
Your code should resemble the following code example. [Visual Basic] ''' ''' Add a new RewardsClaimed entity ''' ''' The detached claim to add. ''' True if success Public Function CreateRewardsClaim(ByVal claim As RewardsClaimed) As Integer _
L4-36
Creating, Updating, and Deleting Entity Data
Implements IDataAccessLayer.CreateRewardsClaim ' Check you have an ObjectContext object. If entities Is Nothing Then entities = New _ AdventureWorksEntities() Try ' Get the next valid claim id. claim.ClaimID = GetNextClaimID() ' Add the RewardsClaimed to the entity set. entities.RewardsClaimed.AddObject(claim) ' Get the contact and decrement the points. Dim Contact As Contact = claim.Contact Contact.CurrentPoints = Contact.CurrentPoints – claim.PointsUsed ' Save all of the changes to the database. entities.SaveChanges() Return claim.ClaimID Catch ex As InvalidOperationException Throw New DALException("There was a problem creating the RewardClaim", ex) Catch ex As UpdateException Throw New DALException("There was a problem saving the RewardClaim to the database", ex) End Try End Function
[Visual C#] /// /// Add a new RewardsClaimed entity. /// /// The detached claim to add. /// True if success public int CreateRewardsClaim(RewardsClaimed claim) { // Check you have an ObjectContext object.
Lab 4: Creating, Updating, and Deleting Entity Data
L4-37
if (entities == null) entities = new AdventureWorksEntities(); try { // Get the next valid claim id. claim.ClaimID = GetNextClaimID(); // Add the RewardsClaimed to the entity set. entities.RewardsClaimed.AddObject(claim); // Get the contact and decrement the points. Contact contact = claim.Contact; contact.CurrentPoints -= claim.PointsUsed; // Save all of the changes to the database. entities.SaveChanges(); return claim.ClaimID; } catch (InvalidOperationException ex) { throw new DALException("There was a problem creating the RewardClaim", ex); } catch (UpdateException ex) { throw new DALException("There was a problem saving the RewardClaim to the database", ex); } }
5.
Save the DataAccessLayer code file: a.
If you are using Visual Basic, on the File menu, click Save DataAccessLayer.vb.
b.
If you are using Visual C#, on the File menu, click Save DataAccessLayer.cs.
Task 4: Add code to update a RewardsClaim record 1.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
L4-38
Creating, Updating, and Deleting Entity Data
2.
Open the DataAccessLayer file by double-clicking the comment TODO: Ex2 Update a RewardsClaimed entity in the task list. This task is located in the UpdateRewardsClaim method: •
3.
In the UpdateRewardsClaim method, delete the existing code: •
4.
In the task list, double-click the comment TODO: Ex2 - Update a RewardsClaimed entity.
Select the code in the method, and then press DELETE.
Write code that performs the following tasks: a.
Check whether the entities variable is null, and if it is, instantiate it as a new instance of the AdventureWorksEntities context object.
b.
Get the EntityKey property of the detached claim passed to the method. This detached claim contains the modified properties.
c.
Use the TryGetObjectByKey method to load the correct claim into the context.
d. Use the ApplyCurrentValues method to copy data from the detached contact passed as a parameter. e.
Adjust the points for the associated contact by the difference between the old and the new points for the claim.
f.
Save all of the changes to the database and return true.
g.
Handle any InvalidOperationException or UpdateException exceptions by throwing a new DALException exception.
Your code should resemble the following code example. [Visual Basic] ''' ''' Update a RewardsClaimed entity ''' ''' The detached RewardsClaimed entity with the new values ''' True on success Public Function UpdateRewardsClaim(ByVal rewardClaim As RewardsClaimed) As Boolean _ Implements IDataAccessLayer.UpdateRewardsClaim
Lab 4: Creating, Updating, and Deleting Entity Data
L4-39
' Check you have an ObjectContext object. If entities Is Nothing Then entities = _ New AdventureWorksEntities() Try ' Get the entity key you require. Dim key As EntityKey = rewardClaim.EntityKey Dim rewardClaimToModify As Object = Nothing ' Ensure that the entity to modify is loaded. If entities.TryGetObjectByKey(key, rewardClaimToModify) Then ' Get the old and new points ready to update the contact. Dim originalPoints As Integer = DirectCast(rewardClaimToModify, _ RewardsClaimed).PointsUsed Dim newPoints As Integer = rewardClaim.PointsUsed ' Copy all of the changes over from the ' detached RewardClaim entity. entities.ApplyCurrentValues(key.EntitySetName, rewardClaim) If originalPoints newPoints Then ' Update contacts points. DirectCast(rewardClaimToModify, _ RewardsClaimed).Contact.CurrentPoints = _ DirectCast(rewardClaimToModify, _ RewardsClaimed).Contact.CurrentPoints + originalPoints DirectCast(rewardClaimToModify, _ RewardsClaimed).Contact.CurrentPoints = _ DirectCast(rewardClaimToModify, _ RewardsClaimed).Contact.CurrentPoints + newPoints End If End If ' Save the changes to the database. entities.SaveChanges() Return True Catch ex As InvalidOperationException
L4-40
Creating, Updating, and Deleting Entity Data
Throw New DALException("There was a problem modifying the RewardClaim", ex) Catch ex As UpdateException Throw New DALException("There was a problem saving the RewardClaim changes to the database", ex) End Try End Function
[Visual C#] /// /// Update a RewardsClaimed entity. /// /// The detached RewardsClaimed entity with the new values /// True on success public bool UpdateRewardsClaim(RewardsClaimed rewardClaim) { // Check you have an ObjectContext object. if (entities == null) entities = new AdventureWorksEntities(); try { // Get the entity key you require. EntityKey key = rewardClaim.EntityKey; object rewardClaimToModify; // Ensure that the entity to modify is loaded. if (entities.TryGetObjectByKey(key, out rewardClaimToModify)) { // Get the old and new points ready to update the contact. int originalPoints = ((RewardsClaimed)rewardClaimToModify).PointsUsed; int newPoints = rewardClaim.PointsUsed; // Copy all of the changes over from the // detached RewardClaim entity. entities.ApplyCurrentValues(key.EntitySetName, rewardClaim); if (originalPoints != newPoints) { // Update contacts points.
Lab 4: Creating, Updating, and Deleting Entity Data
L4-41
((RewardsClaimed)rewardClaimToModify) .Contact.CurrentPoints += originalPoints; ((RewardsClaimed)rewardClaimToModify) .Contact.CurrentPoints -= newPoints; } } // Save the changes to the database. entities.SaveChanges(); return true; } catch (InvalidOperationException ex) { throw new DALException("There was a problem modifying the RewardClaim", ex); } catch (UpdateException ex) { throw new DALException("There was a problem saving the RewardClaim changes to the database", ex); } }
5.
Save the DataAccessLayer code file: a.
If you are using Visual Basic, on the File menu, click Save DataAccessLayer.vb.
b.
If you are using Visual C#, on the File menu, click Save DataAccessLayer.cs.
Task 5: Add code to delete a RewardsClaim record 1.
2.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
Open the DataAccessLayer file by double-clicking the comment TODO: Ex2 Delete a RewardsClaimed entity in the task list. This task is located in the DeleteRewardsClaim method: •
3.
In the task list, double-click the comment TODO: Ex2 - Delete a RewardsClaimed entity.
In the DeleteRewardsClaim method, delete the existing code:
L4-42
Creating, Updating, and Deleting Entity Data
• 4.
Select the code in the method, and then press DELETE.
Write code that performs the following tasks: a.
Check whether the entities variable is null, and if it is, instantiate it as a new instance of the AdventureWorksEntities context object.
b.
Use the claimID value passed as a parameter to create the EntityKey object of the claim to delete.
c.
Use the TryGetObjectByKey method to load the correct claim into the context.
d. Give the points of the claim back to the associated contact. e.
Delete the claim from the context.
f.
Save all of the changes to the database and return true.
g.
Handle any InvalidOperationException or UpdateException exceptions by throwing a new DALException exception.
Your code should resemble the following code example. [Visual Basic] ''' ''' Delete a RewardsClaimed entity ''' ''' The ClaimID of the entity to delete ''' True if success Public Function DeleteRewardsClaim(ByVal claimId As Integer) As Boolean _ Implements IDataAccessLayer.DeleteRewardsClaim ' Check you have an ObjectContext object. If entities Is Nothing Then entities = _ New AdventureWorksEntities() Try Dim rewardClaimToDelete As Object = Nothing ' Get the entity key of the claim to delete. Dim key As EntityKey = New EntityKey( _ "AdventureWorksEntities.RewardsClaimed", "ClaimID", _ claimId) ' Ensure that the entity to modify is loaded. If entities.TryGetObjectByKey(key, rewardClaimToDelete) Then ' Give the points back to the contact. DirectCast(rewardClaimToDelete, _ RewardsClaimed).Contact.CurrentPoints =
Lab 4: Creating, Updating, and Deleting Entity Data
L4-43
DirectCast(rewardClaimToDelete, _ RewardsClaimed).Contact.CurrentPoints + DirectCast(rewardClaimToDelete, _ RewardsClaimed).PointsUsed ' Delete the object. entities.DeleteObject(rewardClaimToDelete) End If ' Save the changes to the database. entities.SaveChanges() Return True Catch ex As InvalidOperationException Throw New DALException("There was a problem deleting the RewardsClaim", ex) Catch ex As UpdateException Throw New DALException("There was a problem deleting the RewardClaim from the database", ex) End Try End Function
[Visual C#] /// /// Delete a RewardsClaimed entity. /// /// The ClaimID of the entity to delete /// True if success public bool DeleteRewardsClaim(int claimId) { // Check you have an ObjectContext object. if (entities == null) entities = new AdventureWorksEntities(); try { object rewardClaimToDelete = null; // Get the entity key of the claim to delete. EntityKey key = new
L4-44
Creating, Updating, and Deleting Entity Data
EntityKey("AdventureWorksEntities.RewardsClaimed", "ClaimID", claimId); // Ensure that the entity to modify is loaded. if (entities.TryGetObjectByKey(key, out rewardClaimToDelete)) { // Give the points back to the contact. ((RewardsClaimed)rewardClaimToDelete) .Contact.CurrentPoints += ((RewardsClaimed)rewardClaimToDelete).PointsUsed; // Delete the object. entities.DeleteObject(rewardClaimToDelete); } // Save the changes to the database. entities.SaveChanges(); return true; } catch (InvalidOperationException ex) { throw new DALException("There was a problem deleting the RewardsClaim", ex); } catch (UpdateException ex) { throw new DALException("There was a problem deleting the RewardClaim from the database", ex); } }
5.
Save the DataAccessLayer code file: a.
If you are using Visual Basic, on the File menu, click Save DataAccessLayer.vb.
b.
If you are using Visual C#, on the File menu, click Save DataAccessLayer.cs.
Task 6: Add unit tests to verify your code 1.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
Lab 4: Creating, Updating, and Deleting Entity Data
b. 2.
In the task list, double-click the comment TODO: Ex2 - Add a test for CreateRewardsClaim.
In the CreateRewardsClaimTest method, delete the existing code: •
4.
If the task list is showing User Tasks, in the Categories list, click Comments.
Open the DataAccessLayerTest file by double-clicking the comment TODO: Ex2 - Add a test for CreateRewardsClaim in the task list. This task is located in the CreateRewardsClaimTest method: •
3.
L4-45
Select the code in the method, and then press DELETE.
Add a unit test to verify the behavior of the CreateRewardsClaim method. Use the CreateLocalClaim method to create a claim to add to the database, and use the GetRewardsClaimedByID method to retrieve the claim from the database. Ensure that you remove the rewards and release any resources at the end of the test. Your code should resemble the following code example.
[Visual Basic] ''' ''' A test for CreateRewardsClaim ''' _ Public Sub CreateRewardsClaimTest() Dim dal As New DataAccessLayer() ' Create a claim entity to insert. Dim claim As RewardsClaimed = Me.CreateLocalClaim() ' Insert the claim entity. Dim claimID As Integer = dal.CreateRewardsClaim(claim) ' Retrieve the claim entity. Dim actual As RewardsClaimed = GetRewardsClaimedByID(claimID) ' Do the test. Dim expected As RewardsClaimed = CreateLocalClaim() Assert.IsTrue(claimID > 0) Assert.AreEqual(expected.PointsUsed, actual.PointsUsed) Assert.AreEqual(expected.RewardID, actual.RewardID) Assert.AreEqual(expected.ContactID, actual.ContactID)
L4-46
Creating, Updating, and Deleting Entity Data
' Tidy up. Dim deleteResult As Boolean = _ dal.DeleteRewardsClaim(actual.ClaimID) dal.Dispose() End Sub
[Visual C#] /// /// A test for CreateRewardsClaim. /// [TestMethod()] public void CreateRewardsClaimTest() { DataAccessLayer dal = new DataAccessLayer(); // Create a claim entity to insert. RewardsClaimed claim = this.CreateLocalClaim(); // Insert the claim entity. int claimID = dal.CreateRewardsClaim(claim); // Retrieve the claim entity. RewardsClaimed actual = GetRewardsClaimedByID(claimID); // Do the test. RewardsClaimed expected = CreateLocalClaim(); Assert.IsTrue(claimID > 0); Assert.AreEqual(expected.PointsUsed, actual.PointsUsed); Assert.AreEqual(expected.RewardID, actual.RewardID); Assert.AreEqual(expected.ContactID, actual.ContactID); // Tidy up. bool deleteResult = dal.DeleteRewardsClaim(actual.ClaimID); dal.Dispose(); }
5.
Locate the UpdateRewardsClaimTest method by double-clicking the comment TODO: Ex2 - Add a test for UpdateRewardsClaim in the task list. This task is located in the UpdateRewardsClaimTest method: •
In the task list, double-click the comment TODO: Ex2 - Add a test for UpdateRewardsClaim.
Lab 4: Creating, Updating, and Deleting Entity Data
6.
In the UpdateRewardsClaimTest method, delete the existing code: •
7.
L4-47
Select the code in the method, then and press DELETE.
Add a unit test to verify the behavior of the UpdateRewardsClaim method. Use the CreateLocalClaim method to create a claim to add to the database, which you can then modify, and use the GetRewardsClaimedByID method to retrieve the claim from the database. Ensure that you remove the claim and release any resources at the end of the test. Your code should resemble the following code example.
[Visual Basic] ''' ''' A test for UpdateRewardsClaim ''' _ Public Sub UpdateRewardsClaimTest() Dim dal As New DataAccessLayer() ' Create a new claim entity to test. Dim claim As RewardsClaimed = Me.CreateLocalClaim() Dim claimID As Integer = dal.CreateRewardsClaim(claim) ' Retrieve detached version. Dim expected As RewardsClaimed = GetRewardsClaimedByID(claimID) ' Make some changes. expected.PointsUsed = 2000 expected.RewardID = 21 Dim updateResult As Boolean = dal.UpdateRewardsClaim(expected) ' Retrieve updated version. Dim actual As RewardsClaimed = GetRewardsClaimedByID(claimID) Assert.AreEqual(updateResult, True) Assert.AreEqual(expected.PointsUsed, actual.PointsUsed) Assert.AreEqual(expected.RewardID, actual.RewardID) Assert.AreEqual(expected.ContactID, actual.ContactID) ' Tidy up. Dim deleteResult As Boolean = dal.DeleteRewardsClaim(claimID) dal.Dispose() End Sub
L4-48
Creating, Updating, and Deleting Entity Data
[Visual C#] /// /// A test for UpdateRewardsClaim. /// [TestMethod()] public void UpdateRewardsClaimTest() { DataAccessLayer dal = new DataAccessLayer(); // Create a new claim entity to test. RewardsClaimed claim = this.CreateLocalClaim(); int claimID = dal.CreateRewardsClaim(claim); // Retrieve detached version. RewardsClaimed expected = GetRewardsClaimedByID(claimID); // Make some changes. expected.PointsUsed = 2000; expected.RewardID = 21; bool updateResult = dal.UpdateRewardsClaim(expected);
// Retrieve updated version. RewardsClaimed actual = GetRewardsClaimedByID(claimID); Assert.AreEqual(updateResult, true); Assert.AreEqual(expected.PointsUsed, actual.PointsUsed); Assert.AreEqual(expected.RewardID, actual.RewardID); Assert.AreEqual(expected.ContactID, actual.ContactID); // Tidy up. bool deleteResult = dal.DeleteRewardsClaim(claimID); dal.Dispose(); }
8.
Locate the DeleteRewardsClaimTest method by double-clicking the comment TODO: Ex2 - Add a test for DeleteRewardsClaim in the task list. This task is located in the DeleteRewardsClaimTest method: •
9.
In the task list, double-click the comment TODO: Ex2 - Add a test for DeleteRewardsClaim.
In the DeleteRewardsClaimTest method, delete the existing code: •
Select the code in the method, and then press DELETE.
Lab 4: Creating, Updating, and Deleting Entity Data
L4-49
10. Add a unit test to verify the behavior of the DeleteRewardsClaim method. Use the CreateLocalClaim method to create a claim to add to the database, which you can then delete. Ensure that you release any resources at the end of the test. Your code should resemble the following code example. [Visual Basic] ''' ''' A test for DeleteRewardsClaim ''' _ Public Sub DeleteRewardsClaimTest() Dim dal As New DataAccessLayer() ' Create a test record. Dim claim As RewardsClaimed = Me.CreateLocalClaim() Dim claimID As Integer = dal.CreateRewardsClaim(claim) ' Perform the delete. Dim deleteResult As Boolean = dal.DeleteRewardsClaim(claimID) ' Check the result. Assert.AreEqual(deleteResult, True) ' Tidy up. dal.Dispose() End Sub
[Visual C#] /// /// A test for DeleteRewardsClaim /// [TestMethod()] public void DeleteRewardsClaimTest() { DataAccessLayer dal = new DataAccessLayer(); // Create a test record. RewardsClaimed claim = this.CreateLocalClaim(); int claimID = dal.CreateRewardsClaim(claim); // Perform the delete. bool deleteResult = dal.DeleteRewardsClaim(claimID);
L4-50
Creating, Updating, and Deleting Entity Data
// Check the result. Assert.AreEqual(deleteResult, true); // Tidy up. dal.Dispose(); }
11. Save the DataAccessLayerTest code file: a.
If you are using Visual Basic, on the File menu, click Save DataAccessLayerTest.vb.
b.
If you are using Visual C#, on the File menu, click Save DataAccessLayerTest.cs.
Task 7: Build and test the application 1.
Build the solution and correct any errors: •
2.
On the Build menu, click Build Solution.
Run all of the tests in the solution: •
On the Test menu, point to Run, and then click All Tests in Solution.
3.
Verify that all of the tests succeed, including the GetContactListTest test.
4.
Start the application in Debug mode: •
On the Debug menu, click Start Debugging.
5.
In the AdventureWorks Rewards window, click All Customers to load data from the entity model into the data grid. Verify that you can add, modify, and delete claims and that the points for the contact are adjusted correctly.
6.
Close the application.
7.
Close the solution, and then close Visual Studio: a.
On the File menu, click Close Solution.
b.
On the File menu, click Exit.
Lab 5: Handling Multi-User Scenarios by Using Object Services
L5-1
Handling Multi-User Scenarios by Using Object Services
Lab 5: Handling Multi-User Scenarios by Using Object Services Exercise 1: Handling Concurrency of Rewards Claimed Data Task 1: Prepare the AdventureWorks database for the lab 1.
Log on to the 10265A-GEN-DEV-05 virtual machine as Student with the password Pa$$w0rd.
2.
In the E:\Labfiles folder, run AWReset.bat: a.
Click Start, and then click Computer.
b.
Double-click Allfiles (E:), double-click Labfiles, and then double-click AWReset.bat.
c.
Wait for the batch file to finish running, and then close Windows® Explorer.
Task 2: Open the starter project for this exercise 1.
Open Microsoft® Visual Studio® 2010: •
2.
Click Start, point to All Programs, click Microsoft Visual Studio 2010, and then click Microsoft Visual Studio 2010.
Open the existing solution, DAL.sln, in the E:\Labfiles\Lab05\VB\Ex1\Starter or E:\Labfiles\Lab05\CS\Ex1\Starter folder: a.
On the File menu, point to Open, and then click Project/Solution.
b.
If you are using Microsoft Visual Basic®, in the Open Project dialog box, move to the E:\Labfiles\Lab05\VB\Ex1\Starter folder, click DAL.sln, and then click Open.
c.
If you are using Microsoft Visual C#®, in the Open Project dialog box, move to the E:\Labfiles\Lab05\CS\Ex1\Starter folder, click DAL.sln, and then click Open.
L5-2
Handling Multi-User Scenarios by Using Object Services
Task 3: Set the concurrency behavior of the Contact and RewardsClaimed entities 1.
Open the AdventureWorksEDM model in the Entity Designer: •
2.
3.
4.
In Solution Explorer, right-click AdventureWorksEDM.edmx, and then click Open.
In the AdventureWorksEDM model, set the Concurrency Mode property of the ModifiedDate property of the Contact entity to Fixed: a.
In the Entity Designer pane, in the Contact entity, click ModifiedDate.
b.
In the Properties pane, click Concurrency Mode, and then select Fixed.
In the AdventureWorksEDM model, set the Concurrency Mode property of the TimeStamp property of the RewardsClaimed entity to Fixed: a.
In the Entity Designer pane, in the RewardsClaimed entity, click TimeStamp.
b.
In the Properties pane, click Concurrency Mode, and then select Fixed.
Save the AdventureWorks EDM model: •
On the File menu, click Save AdventureWorksEDM.edmx.
Task 4: Add code to set the ModifiedDate property of the contact 1.
2.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
Open the DataAccessLayer file by double-clicking the comment TODO: Ex1 Update the contact's Modified Date property item in the task list. This task is located in the UpdateContact method: •
3.
In the task list, double-click the TODO: Ex1 - Update the contact's Modified Date property item.
Immediately after the comment, add code that sets the ModifiedDate property of the contact being saved to the current date and time. Your code should resemble the following code example.
Lab 5: Handling Multi-User Scenarios by Using Object Services
L5-3
[Visual Basic] ' TODO: Ex1 - Update the contact's Modified Date property DirectCast(contactToModify, Contact).ModifiedDate = DateTime.Now
[Visual C#] // TODO: Ex1 - Update the contact's Modified Date property ((Contact)contactToModify).ModifiedDate = DateTime.Now;
4.
Save the DataAccessLayer file: •
If you are using Visual Basic, on the File menu, click Save DataAccessLayer.vb.
•
If you are using Visual C#, on the File menu, click Save DataAccessLayer.cs.
Task 5: Add code to handle OptimisticConcurrencyException exceptions in the CreateRewardsClaim method 1.
2.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
Open the DataAccessLayer file by double-clicking the comment TODO: Ex1 Handle the OptimisticConcurrencyException in CreateRewardsClaim item in the task list. This task is located in the CreateRewardsClaim method: •
3.
In the task list, double-click the TODO: Ex1 - Handle the OptimisticConcurrencyException in CreateRewardsClaim item.
Immediately after the comment, add a catch block to handle an OptimisticConcurrencyException exception. In the catch block, add code that performs the following tasks: a.
Refresh the contact with the current values from the database.
b.
Deduct the points used for the claim from the contact.
c.
Set the ModifiedDate property of the contact to the current date and time.
L5-4
Handling Multi-User Scenarios by Using Object Services
d. Save the changes to the database. e.
Refresh the contact and the claim with the current values from the database.
f.
Return the new claimID value.
Your code should resemble the following code example. [Visual Basic] ' TODO: Ex1 - Handle the OptimisticConcurrencyException in CreateRewardsClaim Catch ex As OptimisticConcurrencyException ' The contact could have been modified so ' get the latest version from the database. entities.Refresh(RefreshMode.StoreWins, claim.Contact) ' Deduct points from customer. claim.Contact.CurrentPoints = claim.Contact.CurrentPoints – claim.PointsUsed claim.Contact.ModifiedDate = DateTime.Now ' Try to save all the changes again. entities.SaveChanges() ' Make sure the correct datetime is in the context entities.Refresh(RefreshMode.StoreWins, claim.Contact) entities.Refresh(RefreshMode.StoreWins, claim) Return claim.ClaimID
[Visual C#] // TODO: Ex1 - Handle the OptimisticConcurrencyException in // CreateRewardsClaim catch (OptimisticConcurrencyException) { // The contact may have been modified, so // get the latest version from the database. entities.Refresh(RefreshMode.StoreWins, claim.Contact); // Deduct points from customer. claim.Contact.CurrentPoints -= claim.PointsUsed; claim.Contact.ModifiedDate = DateTime.Now;
Lab 5: Handling Multi-User Scenarios by Using Object Services
L5-5
// Try to save all of the changes again. entities.SaveChanges(); // Ensure that the correct datetime is in the context. entities.Refresh(RefreshMode.StoreWins, claim.Contact); entities.Refresh(RefreshMode.StoreWins, claim); return claim.ClaimID; }
4.
Save the DataAccessLayer file: •
If you are using Visual Basic, on the File menu, click Save DataAccessLayer.vb.
•
If you are using Visual C#, on the File menu, click Save DataAccessLayer.cs.
Task 6: Add code to handle OptimisticConcurrencyException exceptions in the UpdateRewardsClaim method 1.
2.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
Open the DataAccessLayer file by double-clicking the comment TODO: Ex1 Handle the OptimisticConcurrencyException in UpdateRewardsClaim item in the task list. This task is located in the UpdateRewardsClaim method: •
3.
In the task list, double-click the TODO: Ex1 - Handle the OptimisticConcurrencyException in UpdateRewardsClaim item.
Immediately after the comment, add a catch block to handle an OptimisticConcurrencyException exception. In the catch block, add code that performs the following tasks: a.
Refresh the contact with the current values from the database.
b.
Refresh the claim, keeping any changes made in the context.
c.
Deduct the points used for the original claim from the contact, and add the points used for the modified claim to the contact.
d. Set the ModifiedDate property of the contact to the current date and time.
L5-6
Handling Multi-User Scenarios by Using Object Services
e.
Save the changes to the database.
f.
Refresh the contact and the claim with the current values from the database.
g.
Return true.
Your code should resemble the following code example. [Visual Basic] ' TODO: Ex1 - Handle the OptimisticConcurrencyException in UpdateRewardsClaim Catch ex As OptimisticConcurrencyException ' The contact could have been modified ' so get the latest version of the contact from the database. entities.Refresh(RefreshMode.StoreWins, rewardClaimToModify.Contact) ' It's very unlikely the claim has been modified as we've ' only just refreshed it. entities.Refresh(RefreshMode.ClientWins, rewardClaimToModify) ' Adjust points for customer. rewardClaimToModify.Contact.CurrentPoints rewardClaimToModify.Contact.CurrentPoints rewardClaimToModify.Contact.CurrentPoints rewardClaimToModify.Contact.CurrentPoints
= + originalPoints = - newPoints
rewardClaimToModify.Contact.ModifiedDate = DateTime.Now ' Try to save all the changes again. entities.SaveChanges() ' Make sure the correct datetime is in the context entities.Refresh(RefreshMode.StoreWins, rewardClaimToModify.Contact) entities.Refresh(RefreshMode.StoreWins, rewardClaimToModify) Return True
Lab 5: Handling Multi-User Scenarios by Using Object Services
[Visual C#] // TODO: Ex1 - Handle the OptimisticConcurrencyException in // UpdateRewardsClaim catch (OptimisticConcurrencyException) { // The contact may have been modified, // so get the latest version of the contact from the database. entities.Refresh(RefreshMode.StoreWins, rewardClaimToModify.Contact); // It's very unlikely the claim has been modified, because you // have only just refreshed it. entities.Refresh(RefreshMode.ClientWins, rewardClaimToModify); // Adjust points for customer. rewardClaimToModify.Contact.CurrentPoints += originalPoints; rewardClaimToModify.Contact.CurrentPoints -= newPoints; rewardClaimToModify.Contact.ModifiedDate = DateTime.Now; // Try to save all of the changes again. entities.SaveChanges(); // Ensure that the correct datetime is in the context. entities.Refresh(RefreshMode.StoreWins, rewardClaimToModify.Contact); entities.Refresh(RefreshMode.StoreWins, rewardClaimToModify); return true; }
4.
Save the DataAccessLayer file: •
If you are using Visual Basic, on the File menu, click Save DataAccessLayer.vb.
•
If you are using Visual C#, on the File menu, click Save DataAccessLayer.cs.
L5-7
L5-8
Handling Multi-User Scenarios by Using Object Services
Task 7: Add code to handle OptimisticConcurrencyException exceptions in the DeleteRewardsClaim method 1.
2.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
Open the DataAccessLayer file by double-clicking the comment TODO: Ex1 Handle the OptimisticConcurrencyException in DeleteRewardsClaim item in the task list. This task is located in the DeleteRewardsClaim method: •
3.
In the task list, double-click the TODO: Ex1 - Handle the OptimisticConcurrencyException in DeleteRewardsClaim item.
Immediately after the comment, add a catch block to handle an OptimisticConcurrencyException exception. In the catch block, add code that performs the following tasks: a.
Refresh the contact with the current values from the database.
b.
Refresh the claim, keeping any changes made in the context.
c.
Add the points used for the deleted claim to the contact.
d. Set the ModifiedDate property of the contact to the current date and time. e.
Save the changes to the database.
f.
Refresh the contact with the current values from the database.
g.
Return true.
Your code should resemble the following code example. [Visual Basic] ' TODO: Ex1 - Handle the OptimisticConcurrencyException in DeleteRewardsClaim Catch ex As OptimisticConcurrencyException ' The contact could have been modified so ' get the latest version from the database. entities.Refresh(RefreshMode.StoreWins, relatedContact) ' Give the points back to the contact relatedContact.CurrentPoints = relatedContact.CurrentPoints + rewardClaimToDelete.PointsUsed relatedContact.ModifiedDate = DateTime.Now
Lab 5: Handling Multi-User Scenarios by Using Object Services
' Try to save all the changes again. entities.SaveChanges() ' Make sure the correct datetime is in the context entities.Refresh(RefreshMode.StoreWins, relatedContact) Return True
[Visual C#] // TODO: Ex1 - Handle the OptimisticConcurrencyException in // DeleteRewardsClaim catch (OptimisticConcurrencyException) { // The contact may have been modified, so // get the latest version from the database. entities.Refresh(RefreshMode.StoreWins, relatedContact); // Give the points back to the contact. relatedContact.CurrentPoints += rewardClaimToDelete.PointsUsed; relatedContact.ModifiedDate = DateTime.Now; // Try to save all of the changes again. entities.SaveChanges(); // Ensure that the correct datetime is in the context. entities.Refresh(RefreshMode.StoreWins, relatedContact); return true; }
4.
Save the DataAccessLayer file: •
If you are using Visual Basic, on the File menu, click Save DataAccessLayer.vb.
•
If you are using Visual C#, on the File menu, click Save DataAccessLayer.cs.
Task 8: Add unit tests to verify your code 1.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
L5-9
L5-10
Handling Multi-User Scenarios by Using Object Services
b. 2.
Open the DataAccessLayerTest file by double-clicking the comment TODO: Ex1 - Create a test to verify that CreateRewardsClaim handles concurrency issues item in the task list. This task is located in the CreateRewardsClaimConcurrencyTest method: •
3.
If the task list is showing User Tasks, in the Categories list, click Comments.
In the task list, double-click the TODO: Ex1 - Create a test to verify that CreateRewardsClaim handles concurrency issues item.
Using the comments in the CreateRewardsClaimConcurrencyTest method for guidance, write code that verifies the behavior of the CreateRewardsClaim method when two users modify the same contact while they are adding new claims to the database. Your code should resemble the following code example.
[Visual Basic] ''' ''' ''' '''
A test for CreateRewardsClaim when there is an OptimisticConcurrencyException
_ Public Sub CreateRewardsClaimConcurrencyTest() ' Create two instances of the DataAccessLayer ' to represent two users Dim user1 As New DataAccessLayer() Dim user2 As New DataAccessLayer() ' User1 creates a contact - this loads the contact ' into the context Dim contactID As Integer = user1.AddContact(CreateTestContact()) ' User2 adds a new claim - modifying the contact Dim claim As RewardsClaimed = CreateLocalClaim() claim.ContactID = contactID Dim claimID As Integer = user2.CreateRewardsClaim(claim) Dim actual1 As Contact = GetContactById(contactID) Assert.AreEqual(9000, actual1.CurrentPoints) ' User1 adds a new claim for the same contact ' - getting an OptimisticConcurrencyException Dim newClaim As RewardsClaimed = CreateLocalClaim() newClaim.ClaimID = claimID newClaim.ContactID = contactID
Lab 5: Handling Multi-User Scenarios by Using Object Services
newClaim.PointsUsed = 500 Dim newClaimID As Integer = user1.CreateRewardsClaim(newClaim) Dim actual2 As Contact = GetContactById(contactID) Assert.AreEqual(8500, actual2.CurrentPoints) ' Tidy up - delete the contact user1.DeleteContact(contactID) user1.Dispose() user2.Dispose() End Sub
[Visual C#] /// /// /// ///
A test for the CreateRewardsClaim method when there is an OptimisticConcurrencyException exception.
[TestMethod()] public void CreateRewardsClaimConcurrencyTest() { // Create two instances of the DataAccessLayer // to represent two users. DataAccessLayer user1 = new DataAccessLayer(); DataAccessLayer user2 = new DataAccessLayer(); // User1 creates a contact—this loads the contact // into the context. int contactID = user1.AddContact(CreateTestContact()); // User2 adds a new claim—modifying the contact. RewardsClaimed claim = CreateLocalClaim(); claim.ContactID = contactID; int claimID = user2.CreateRewardsClaim(claim); Contact actual1 = GetContactById(contactID); Assert.AreEqual(9000, actual1.CurrentPoints); // User1 adds a new claim for the same contact //—getting an OptimisticConcurrencyException exception. RewardsClaimed newClaim = CreateLocalClaim(); newClaim.ClaimID = claimID; newClaim.ContactID = contactID; newClaim.PointsUsed = 500; int newClaimID = user1.CreateRewardsClaim(newClaim); Contact actual2 = GetContactById(contactID); Assert.AreEqual(8500, actual2.CurrentPoints);
L5-11
L5-12
Handling Multi-User Scenarios by Using Object Services
// Tidy up—delete the contact. user1.DeleteContact(contactID); user1.Dispose(); user2.Dispose(); }
4.
Locate the UpdateRewardsClaimConcurrencyTest method by doubleclicking the comment TODO: Ex1 - Create a test to verify that UpdateRewardsClaim handles concurrency issues item in the task list. This task is located in the UpdateRewardsClaimConcurrencyTest method: •
5.
In the task list, double-click the TODO: Ex1 - Create a test to verify that UpdateRewardsClaim handles concurrency issues item.
Using the comments in the UpdateRewardsClaimConcurrencyTest method for guidance, write code that verifies the behavior of the UpdateRewardsClaim method when two users modify the same contact while they are updating claims to the database. Your code should resemble the following code example.
[Visual Basic] ''' ''' ''' '''
A test for UpdateRewardsClaim when there is an OptimisticConcurrencyException
_ Public Sub UpdateRewardsClaimConcurrencyTest() ' Create two instances of the DataAccessLayer ' to represent two users Dim user1 As New DataAccessLayer() Dim user2 As New DataAccessLayer() ' User1 creates a contact - this loads the ' contact into the context Dim contactID As Integer = user1.AddContact(CreateTestContact()) ' User2 adds a new claim - modifying the contact Dim claim = CreateLocalClaim() claim.ContactID = contactID Dim claimID As Integer = user2.CreateRewardsClaim(claim) Dim actual1 = GetContactById(contactID) Assert.AreEqual(9000, actual1.CurrentPoints)
Lab 5: Handling Multi-User Scenarios by Using Object Services
' User1 updates the claim for the same contact ' - getting an OptimisticConcurrencyException Dim modifiedClaim As RewardsClaimed = GetRewardsClaimedByID(claimID) modifiedClaim.ClaimID = claimID modifiedClaim.ContactID = contactID modifiedClaim.PointsUsed = 500 user1.UpdateRewardsClaim(modifiedClaim) Dim actual2 As Contact = GetContactById(contactID) Assert.AreEqual(9500, actual2.CurrentPoints) ' Tidy up - delete the contact user1.DeleteContact(contactID) user1.Dispose() user2.Dispose() End Sub
[Visual C#] /// /// /// ///
A test for the UpdateRewardsClaim method when there is an OptimisticConcurrencyException exception.
[TestMethod()] public void UpdateRewardsClaimConcurrencyTest() { // Create two instances of the DataAccessLayer // to represent two users. DataAccessLayer user1 = new DataAccessLayer(); DataAccessLayer user2 = new DataAccessLayer(); // User1 creates a contact—this loads the // contact into the context. int contactID = user1.AddContact(CreateTestContact()); // User2 adds a new claim—modifying the contact. RewardsClaimed claim = CreateLocalClaim(); claim.ContactID = contactID; int claimID = user2.CreateRewardsClaim(claim); Contact actual1 = GetContactById(contactID); Assert.AreEqual(9000, actual1.CurrentPoints);
L5-13
L5-14
Handling Multi-User Scenarios by Using Object Services
// User1 updates the claim for the same contact //—getting an OptimisticConcurrencyException exception. RewardsClaimed modifiedClaim = GetRewardsClaimedByID(claimID); modifiedClaim.ClaimID = claimID; modifiedClaim.ContactID = contactID; modifiedClaim.PointsUsed = 500; user1.UpdateRewardsClaim(modifiedClaim); Contact actual2 = GetContactById(contactID); Assert.AreEqual(9500, actual2.CurrentPoints); // Tidy up—delete the contact. user1.DeleteContact(contactID); user1.Dispose(); user2.Dispose(); }
6.
Locate the DeleteRewardsClaimConcurrencyTest method by double-clicking the comment TODO: Ex1 - Create a test to verify that DeleteRewardsClaim handles concurrency issues item in the task list. This task is located in the DeleteRewardsClaimConcurrencyTest method: •
7.
In the task list, double-click the TODO: Ex1 - Create a test to verify that DeleteRewardsClaim handles concurrency issues item.
Using the comments in the DeleteRewardsClaimConcurrencyTest method for guidance, write code that verifies the behavior of the DeleteRewardsClaim method when two users modify the same contact while they are inserting and deleting claims in the database. Your code should resemble the following code example.
[Visual Basic] ''' ''' A test for DeleteRewardsClaim ''' when there is an OptimisticConcurrencyException ''' _ Public Sub DeleteRewardsClaimConcurrencyTest() ' Create two instances of the DataAccessLayer ' to represent two users Dim user1 As New DataAccessLayer() Dim user2 As New DataAccessLayer() ' User1 creates a contact - this loads the ' contact into the context Dim contactID As Integer = user1.AddContact(CreateTestContact())
Lab 5: Handling Multi-User Scenarios by Using Object Services
' User2 adds a new claim - modifying the contact Dim claim As RewardsClaimed = CreateLocalClaim() claim.ContactID = contactID Dim claimID As Integer = user2.CreateRewardsClaim(claim) Dim actual1 As Contact = GetContactById(contactID) Assert.AreEqual(9000, actual1.CurrentPoints) ' User1 deletes the claim for the same contact ' - getting an OptimisticConcurrencyException user1.DeleteRewardsClaim(claimID) Dim actual2 As Contact = GetContactById(contactID) Assert.AreEqual(10000, actual2.CurrentPoints) ' Tidy up - delete the contact user1.DeleteContact(contactID) user1.Dispose() user2.Dispose() End Sub
[Visual C#] /// /// A test for the DeleteRewardsClaim method /// when there is an OptimisticConcurrencyException exception. /// [TestMethod()] public void DeleteRewardsClaimConcurrencyTest() { // Create two instances of the DataAccessLayer // to represent two users. DataAccessLayer user1 = new DataAccessLayer(); DataAccessLayer user2 = new DataAccessLayer(); // User1 creates a contact—this loads the // contact into the context. int contactID = user1.AddContact(CreateTestContact()); // User2 adds a new claim—modifying the contact. RewardsClaimed claim = CreateLocalClaim(); claim.ContactID = contactID; int claimID = user2.CreateRewardsClaim(claim); Contact actual1 = GetContactById(contactID); Assert.AreEqual(9000, actual1.CurrentPoints); // User1 deletes the claim for the same contact //—getting an OptimisticConcurrencyException exception.
L5-15
L5-16
Handling Multi-User Scenarios by Using Object Services
user1.DeleteRewardsClaim(claimID); Contact actual2 = GetContactById(contactID); Assert.AreEqual(10000, actual2.CurrentPoints); // Tidy up—delete the contact. user1.DeleteContact(contactID); user1.Dispose(); user2.Dispose(); }
8.
Save the DataAccessLayerTest file: •
If you are using Visual Basic, on the File menu, click Save DataAccessLayerTest.vb.
•
If you are using Visual C#, on the File menu, click Save DataAccessLayerTest.cs.
Task 9: Build and test the application 1.
Build the solution and correct any errors: •
2.
On the Build menu, click Build Solution.
Run all of the tests in the solution: •
On the Test menu, point to Run, and then click All Tests in Solution.
3.
Verify that all of the tests succeed, including the CreateRewardsClaimConcurrencyTest, UpdateRewardsClaimConcurrencyTest, and DeleteRewardsClaimConcurrencyTest tests.
4.
Close the solution: •
On the File menu, click Close Solution.
Exercise 2: Updating the RewardsClaimed and ArchivedRewardsClaimed Information by Using a Transaction Task 1: Open the starter project for this exercise •
Open the existing solution, DAL.sln, in the E:\Labfiles\Lab05\VB\Ex2\Starter or E:\Labfiles\Lab05\CS\Ex2\Starter folder:
Lab 5: Handling Multi-User Scenarios by Using Object Services
L5-17
a.
On the File menu, point to Open, and then click Project/Solution.
b.
If you are using Visual Basic, in the Open Project dialog box, move to the E:\Labfiles\Lab05\VB\Ex2\Starter folder, click DAL.sln, and then click Open.
c.
If you are using Visual C#, in the Open Project dialog box, move to the E:\Labfiles\Lab05\CS\Ex2\Starter folder, click DAL.sln, and then click Open.
Task 2: Create the AdventureWorksArchivedEDM Entity Data Model 1.
Add a new ADO.NET Entity Data Model to the DAL project. Generate the data model from the AdventureWorks Microsoft SQL Server® database and create entities for the ArchivedRewardsClaimed table: a.
In Solution Explorer, right-click DAL, point to Add, and then click New Item.
b.
In the Add New Item - DAL dialog box, in the Templates list, click ADO.NET Entity Data Model, in the Name box, type AdventureWorksArchivedEDM and then click Add.
c.
In the Entity Data Model Wizard, on the Choose Model Contents page, click Generate from database, and then click Next.
d. On the Choose Your Data Connection page, click New Connection. e.
In the Choose Data Source dialog box, in the Data source list, click Microsoft SQL Server, and then click Continue.
f.
In the Connection Properties dialog box, in the Server name box, type .\SQLEXPRESS and in the Select or enter a database name box, enter AdventureWorks and then click OK.
g.
On the Choose Your Data Connection page, in the Save entity connection settings in App.Config as box, type AdventureWorksArchivedEntities and then click Next.
h.
On the Choose Your Database Objects page, expand Tables, and then select the ArchivedRewardsClaimed (Sales) check box.
i.
Clear the Pluralize or singularize generated object names check box.
j.
In the Model Namespace box, type AdventureWorksArchivedModel and then click Finish.
L5-18
Handling Multi-User Scenarios by Using Object Services
2.
Copy the AdventureWorksArchivedEntities connection string from the App.Config file in the DAL project to the App.Config file in the DALTest project: a.
In Solution Explorer, in the DAL project, right-click App.Config, and then click Open.
b.
Select the whole line with the AdventureWorksArchivedEntities connection string, and on the Edit menu, click Copy.
c.
On the File menu, click Close.
d. In Solution Explorer, in the DALTest project, right-click App.Config, and then click Open. e.
Add a new blank line after the AdventureWorksEntities connection string, and on the Edit menu, click Paste. Your code should resemble the following code example.
f.
On the File menu, click Save App.Config.
g.
On the File menu, click Close.
Lab 5: Handling Multi-User Scenarios by Using Object Services
L5-19
Task 3: Modify the CreateRewardsClaim method to save a copy of the claim to the archive table as part of a transaction 1.
2.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
Open the DataAccessLayer file by double-clicking the comment TODO: Ex2 In CreateRewardsClaim, wrap SaveChanges in a distributed transaction that creates an ArchivedRewardsClaim item in the task list. This task is located in the CreateRewardsClaim method: •
3.
In the task list, double-click the TODO: Ex2 - In CreateRewardsClaim, wrap SaveChanges in a distributed transaction that creates an ArchivedRewardsClaim item.
Place the call to the SaveChanges method inside a new TransactionScope code block. In the TransactionScope code block, after the call to the SaveChanges method, add code to perform the following tasks: a.
Create a new AdventureWorksArchivedEntities context called archivedEntities.
b.
In the archivedEntities context, create a new ArchivedRewardsClaimed entity that contains a copy of the data in the RewardsClaimed entity.
c.
Add the new ArchivedRewardsClaimed entity to the ArchivedRewardsClaimed entity set.
d. Save all of the changes in the archivedEntities context. e.
At the end of the TransactionScope code block, call the Complete method of the TransactionScope object.
Your code should resemble the following code example. [Visual Basic] ' TODO: Ex2 - In CreateRewardsClaim, wrap SaveChanges in a distributed ' transaction that creates an ArchivedRewardsClaim ' Begin a distributed transaction. Using scope As New TransactionScope() ' Save all the changes to the database entities.SaveChanges()
L5-20
Handling Multi-User Scenarios by Using Object Services
' Use a different ObjectContext. Using archivedEntities As AdventureWorksArchivedEntities = New AdventureWorksArchivedEntities() ' Create archived rewards claim. Dim archivedClaim As ArchivedRewardsClaimed = ArchivedRewardsClaimed.CreateArchivedRewardsClaimed(0, claim.ClaimID, claim.PointsUsed, claim.RewardID, claim.ContactID, DateTime.Now) archivedEntities.AddToArchivedRewardsClaimed(archivedClaim) archivedEntities.SaveChanges() End Using ' The Complete method commits the transaction. ' If an exception is thrown, ' Complete is not called and the transaction is rolled back. scope.Complete() End Using
[Visual C#] // TODO: Ex2 - In CreateRewardsClaim, wrap SaveChanges // in a distributed transaction that creates an // ArchivedRewardsClaim // Begin a distributed transaction. using (TransactionScope scope = new TransactionScope()) { // Save all the changes to the database entities.SaveChanges(); // Use a different ObjectContext object. using (AdventureWorksArchivedEntities archivedEntities = new AdventureWorksArchivedEntities()) { // Create archived rewards claim. ArchivedRewardsClaimed archivedClaim = ArchivedRewardsClaimed.CreateArchivedRewardsClaimed(0, claim.ClaimID, claim.PointsUsed, claim.RewardID, claim.ContactID, DateTime.Now); archivedEntities.AddToArchivedRewardsClaimed(archivedClaim); archivedEntities.SaveChanges();
Lab 5: Handling Multi-User Scenarios by Using Object Services
L5-21
} // The Complete method commits the transaction. // If an exception is thrown, the Complete method is not called // and the transaction is rolled back. scope.Complete(); }
4.
Add code to the CreateRewardsClaim method to handle the TransactionAbortedException exception. To locate the place where you must add this code, double-click the comment TODO: Ex2 - In CreateRewardsClaim, handle TransactionAbortedException item in the task list: •
5.
In the task list, double-click the TODO: Ex2 - In CreateRewardsClaim, handle TransactionAbortedException item.
Add a catch block that traps TransactionAbortedException exceptions. In the catch block, refresh the contact and the claim from the database, and throw a new DALException exception to report the error. Your code should resemble the following code example.
[Visual Basic] ' TODO: Ex2 - In CreateRewardsClaim, handle ' TransactionAbortedException Catch ex As TransactionAbortedException entities.Refresh(RefreshMode.StoreWins, claim.Contact) entities.Refresh(RefreshMode.StoreWins, claim) Throw New DALException("Could not save RewardsClaim and " & "ArchivedRewardsClaim in transaction", ex)
[Visual C#] // TODO: Ex2 - In CreateRewardsClaim, handle // TransactionAbortedException catch (TransactionAbortedException ex) { entities.Refresh(RefreshMode.StoreWins, claim.Contact); entities.Refresh(RefreshMode.StoreWins, claim); throw new DALException("Could not save RewardsClaim and ArchivedRewardsClaim in transaction", ex); }
6.
Save the DataAccessLayer file:
L5-22
Handling Multi-User Scenarios by Using Object Services
•
If you are using Visual Basic, on the File menu, click Save DataAccessLayer.vb.
•
If you are using Visual C#, on the File menu, click Save DataAccessLayer.cs.
Task 4: Modify the UpdateRewardsClaim method to save a copy of the claim to the archive table as part of a transaction 1.
2.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
Open the DataAccessLayer file by double-clicking the comment TODO: Ex2 In UpdateRewardsClaim, wrap SaveChanges in a distributed transaction that creates an ArchivedRewardsClaim item in the task list. This task is located in the UpdateRewardsClaim method: •
3.
In the task list, double-click the TODO: Ex2 - In UpdateRewardsClaim, wrap SaveChanges in a distributed transaction that creates an ArchivedRewardsClaim item.
Place the call to the SaveChanges method inside a new TransactionScope code block. In the TransactionScope code block, after the call to the SaveChanges method, add code to perform the following tasks: a.
Create a new AdventureWorksArchivedEntities context called archivedEntities.
b.
In the archivedEntities context, create a new ArchivedRewardsClaimed entity that contains a copy of the data in the RewardsClaimed entity.
c.
Add the new ArchivedRewardsClaimed entity to the ArchivedRewardsClaimed entity set.
d. Save all of the changes in the archivedEntities context. e.
At the end of the TransactionScope code block, call the Complete method of the TransactionScope object.
Your code should resemble the following code example.
Lab 5: Handling Multi-User Scenarios by Using Object Services
L5-23
[Visual Basic] ' TODO: Ex2 - In UpdateRewardsClaim, wrap SaveChanges in a distributed transaction that creates an ArchivedRewardsClaim ' Begin a distributed transaction. Using scope = New TransactionScope() ' Save all the changes to the database entities.SaveChanges() ' Use a different ObjectContext. Using archivedEntities = New AdventureWorksArchivedEntities() ' Create archived rewards claim. Dim archivedClaim As ArchivedRewardsClaimed = ArchivedRewardsClaimed.CreateArchivedRewardsClaimed(0, rewardClaimToModify.ClaimID, rewardClaimToModify.PointsUsed, rewardClaimToModify.RewardID, rewardClaimToModify.ContactID, DateTime.Now) archivedEntities.AddToArchivedRewardsClaimed(archivedClaim) archivedEntities.SaveChanges() End Using ' The Complete method commits the transaction. ' If an exception is thrown, ' Complete is not called and the transaction is rolled back. scope.Complete() End Using
[Visual C#] // TODO: Ex2 - In UpdateRewardsClaim, wrap SaveChanges in a // distributed transaction that creates an ArchivedRewardsClaim // Begin a distributed transaction. using (TransactionScope scope = new TransactionScope()) { // Save all of the changes to the database. entities.SaveChanges(); // Use a different ObjectContext object. using (AdventureWorksArchivedEntities archivedEntities =
L5-24
Handling Multi-User Scenarios by Using Object Services
new AdventureWorksArchivedEntities()) { // Create an archived rewards claim. ArchivedRewardsClaimed archivedClaim = ArchivedRewardsClaimed.CreateArchivedRewardsClaimed(0, rewardClaimToModify.ClaimID, rewardClaimToModify.PointsUsed, rewardClaimToModify.RewardID, rewardClaimToModify.ContactID, DateTime.Now); archivedEntities.AddToArchivedRewardsClaimed(archivedClaim); archivedEntities.SaveChanges(); } // The Complete method commits the transaction. // If an exception is thrown, the Complete method is not called // and the transaction is rolled back. scope.Complete(); }
4.
Add code to the UpdateRewardsClaim method to handle the TransactionAbortedException exception. To locate the place where you must add this code, double-click the comment TODO: Ex2 - In UpdateRewardsClaim, handle TransactionAbortedException item in the task list: •
5.
In the task list, double-click the TODO: Ex2 - In UpdateRewardsClaim, handle TransactionAbortedException item.
Add a catch block that traps TransactionAbortedException exceptions. In the catch block, refresh the contact and the claim from the database, and throw a new DALException exception to report the error. Your code should resemble the following code example.
[Visual Basic] ' TODO: Ex2 - In UpdateRewardsClaim, handle ' TransactionAbortedException Catch ex As TransactionAbortedException entities.Refresh(RefreshMode.StoreWins, rewardClaimToModify.Contact) entities.Refresh(RefreshMode.StoreWins, rewardClaimToModify) Throw New DALException("Could not save RewardsClaim and " & "ArchivedRewardsClaim in transaction", ex)
Lab 5: Handling Multi-User Scenarios by Using Object Services
L5-25
[Visual C#] // TODO: Ex2 - In CreateRewardsClaim, handle // TransactionAbortedException catch (TransactionAbortedException ex) { entities.Refresh(RefreshMode.StoreWins, rewardClaimToModify.Contact); entities.Refresh(RefreshMode.StoreWins, rewardClaimToModify); throw new DALException("Could not save RewardsClaim and ArchivedRewardsClaim in transaction", ex); }
6.
Save the DataAccessLayer file: •
If you are using Visual Basic, on the File menu, click Save DataAccessLayer.vb.
•
If you are using Visual C#, on the File menu, click Save DataAccessLayer.cs.
Task 5: Modify the unit tests to verify your code 1.
2.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
Open the DataAccessLayerTest file by double-clicking the comment TODO: Ex2 - Count the archived claims before the insert item in the task list. This task is located in the CreateRewardsClaimTest method: •
3.
In the task list, double-click the TODO: Ex2 - Count the archived claims before the insert item.
Immediately after the comment, add code to count the number of archived claims by calling the CountArchivedRewardsClaimed method. Your code should resemble the following code example.
[Visual Basic] ' TODO: Ex 2 - Count the archived claims before the insert Dim start As Integer = CountArchivedRewardsClaimed()
L5-26
Handling Multi-User Scenarios by Using Object Services
[Visual C#] // TODO: Ex 2 - Count the archived claims before the insert int start = CountArchivedRewardsClaimed();
4.
Navigate to the next comment by double-clicking the comment TODO: Ex2 Count the archived claims after the insert and test item in the task list. This task is located in the CreateRewardsClaimTest method: •
5.
In the task list, double-click the TODO: Ex2 - Count the archived claims after the insert and test item.
Immediately after the comment, add code to count the number of archived claims by calling the CountArchivedRewardsClaimed method, and verify that the number of archived rewards has increased by one. Your code should resemble the following code example.
[Visual Basic] ' TODO: Ex 2 - Count the archived claims after the insert and test Dim finish As Integer = CountArchivedRewardsClaimed() Assert.AreEqual(start + 1, finish)
[Visual C#] // TODO: Ex 2 - Count the archived claims after the insert and test int finish = CountArchivedRewardsClaimed(); Assert.AreEqual(start + 1, finish);
6.
Navigate to the next comment by double-clicking the comment TODO: Ex2 Count the archived claims before the update item in the task list. This task is located in the UpdateRewardsClaimTest method: •
7.
In the task list, double-click the TODO: Ex2 - Count the archived claims before the update item.
Immediately after the comment, add code to count the number of archived claims by calling the CountArchivedRewardsClaimed method. Your code should resemble the following code example.
[Visual Basic] ' TODO: Ex 2 - Count the archived claims before the update Dim start As Integer = CountArchivedRewardsClaimed()
Lab 5: Handling Multi-User Scenarios by Using Object Services
L5-27
[Visual C#] // TODO: Ex 2 - Count the archived claims before the update int start = CountArchivedRewardsClaimed();
8.
Navigate to the next comment by double-clicking the comment TODO: Ex2 Count the archived claims after the update and test item in the task list. This task is located in the UpdateRewardsClaimTest method: •
9.
In the task list, double-click the TODO: Ex2 - Count the archived claims after the update and test item.
Immediately after the comment, add code to count the number of archived claims by calling the CountArchivedRewardsClaimed method, and verify that the number of archived rewards has increased by one. Your code should resemble the following code example.
[Visual Basic] ' TODO: Ex 2 - Count the archived claims after the update and test Dim finish As Integer = CountArchivedRewardsClaimed() Assert.AreEqual(start + 1, finish)
[Visual C#] // TODO: Ex 2 - Count the archived claims after the update and test int finish = CountArchivedRewardsClaimed(); Assert.AreEqual(start + 1, finish);
10. Save the DataAccessLayerTest file: •
If you are using Visual Basic, on the File menu, click Save DataAccessLayerTest.vb.
•
If you are using Visual C#, on the File menu, click Save DataAccessLayerTest.cs.
Task 6: Build and test the application 1.
Build the solution and correct any errors: •
2.
On the Build menu, click Build Solution.
Run all of the tests in the solution: •
On the Test menu, point to Run, and then click All Tests in Solution.
L5-28
Handling Multi-User Scenarios by Using Object Services
3.
Verify that all of the tests succeed, including the CreateRewardsClaimTest and UpdateRewardsClaimTest tests.
4.
Close the solution, and then close Visual Studio: •
On the File menu, click Close Solution.
•
On the File menu, click Exit.
Lab 6: Building Optimized Solutions by Using Object Services
L6-1
Building Optimized Solutions by Using Object Services
Lab 6: Building Optimized Solutions by Using Object Services Exercise 1: Improving the Performance of Query Operations Task 1: Prepare the AdventureWorks database for the lab 1.
Log on to the 10265A-GEN-DEV-06 virtual machine as Student with the password Pa$$w0rd.
2.
In the E:\Labfiles folder, run AWReset.bat: a.
Click Start, and then click Computer.
b.
Double-click Allfiles (E:), double-click Labfiles, and then double-click AWReset.bat.
c.
Wait for the batch file to finish running, and then close Windows® Explorer.
Task 2: Open the starter project for this exercise 1.
Open Microsoft® Visual Studio® 2010: •
2.
Click Start, point to All Programs, click Microsoft Visual Studio 2010, and then click Microsoft Visual Studio 2010.
Open the existing solution, DAL.sln, in the E:\Labfiles\Lab06\VB\Ex1\Starter or E:\Labfiles\Lab06\CS\Ex1\Starter folder: a.
On the File menu, point to Open, and then click Project/Solution.
b.
If you are using Microsoft Visual Basic®, in the Open Project dialog box, move to the E:\Labfiles\Lab06\VB\Ex1\Starter folder, click DAL.sln, and then click Open.
c.
If you are using Microsoft Visual C#®, in the Open Project dialog box, move to the E:\Labfiles\Lab06\CS\Ex1\Starter folder, click DAL.sln, and then click Open.
L6-2
Building Optimized Solutions by Using Object Services
Task 3: Print timing information during query execution 1.
2.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
Open the DataAccessLayer code file by double-clicking the comment TODO: Return all contacts with timings item in the task list. This task is located in the GetContactListDetail method: •
3.
Delete the comment in the GetContactListDetail method: •
4.
In the task list, double-click the TODO: Ex1 - Return all contacts with timings item.
Select the comment in the method, and then press DELETE.
Write code that performs the following tasks: a.
Instantiate and start two Stopwatch objects called totaltime and stagetime.
b.
Check whether the entities variable is null. If it is, instantiate it as a new instance of the AdventureWorksEntities context object.
Note: The entities variable is a private field in the DataAccessLayer class. Your code should perform all data access operations by using this context object.
c.
Print the value of the ElapsedTime property from the stagetime object, and then restart the Stopwatch.
d. Retrieve an ObjectQuery object from the context's Contacts property. e.
Print the value of the ElapsedTime property from the stagetime object, and then restart the Stopwatch.
f.
Define and execute a Language-Integrated Query (LINQ) query to retrieve all of the contacts from the ObjectQuery object, and then save the results to a List object.
g.
Print the value of the ElapsedTime property from the stagetime object, and then restart the Stopwatch.
h. Print the value of the ElapsedTime property from the totaltime object, and then restart the Stopwatch.
Lab 6: Building Optimized Solutions by Using Object Services
i.
L6-3
Return the List object.
Your code should resemble the following code example. [Visual Basic] Public Function GetContactListDetail() As List(Of Contact) Dim totaltime As Stopwatch = Stopwatch.StartNew() Dim stagetime As Stopwatch = Stopwatch.StartNew() ' Check you have an ObjectContext object. If entities Is Nothing Then entities = New AdventureWorksEntities() Console.WriteLine("Context Creation Time: " & vbTab & vbTab & "{0,6} ms", stagetime.ElapsedMilliseconds) stagetime.Restart() Dim contacts As ObjectQuery(Of Contact) = entities.Contacts Console.WriteLine("ObjectQuery Creation Time: " & vbTab & "{0,6} ms", stagetime.ElapsedMilliseconds) stagetime.Restart() Dim query = From c In contacts Select c Dim results As List(Of Contact) = query.ToList() Console.WriteLine("Query Run Time: " & vbTab & vbTab & "{0,6} ms", stagetime.ElapsedMilliseconds) stagetime.Restart() Console.WriteLine("Total Time: " & vbTab & vbTab & vbTab & "{0,6} ms", totaltime.ElapsedMilliseconds) Return results End Function
[Visual C#] public List GetContactListDetail() { Stopwatch totaltime = Stopwatch.StartNew(); Stopwatch stagetime = Stopwatch.StartNew(); // Check you have an ObjectContext object.
L6-4
Building Optimized Solutions by Using Object Services
if (entities == null) entities = new AdventureWorksEntities(); Console.WriteLine("Context Creation Time: \t\t{0,6} ms", stagetime.ElapsedMilliseconds); stagetime.Restart(); ObjectQuery contacts = entities.Contacts; Console.WriteLine("ObjectQuery Creation Time: \t{0,6} ms", stagetime.ElapsedMilliseconds); stagetime.Restart(); var query = from c in contacts select c; List results = query.ToList(); Console.WriteLine("Query Run Time: \t\t{0,6} ms", stagetime.ElapsedMilliseconds); stagetime.Restart(); Console.WriteLine("Total Time: \t\t\t{0,6} ms", totaltime.ElapsedMilliseconds); return results; }
5.
Save the DataAccessLayer code file: a.
If you are using Visual Basic, on the File menu, click Save DataAccessLayer.vb.
b.
If you are using Visual C#, on the File menu, click Save DataAccessLayer.cs.
Task 4: Add code to define a compiled LINQ query 1.
2.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
Open the DataAccessLayer code file by double-clicking the comment TODO: Define the compiled LINQ query item in the task list. This task is located in the DataAccessLayer class: •
In the task list, double-click the TODO: Ex1 - Define the compiled LINQ query item.
Lab 6: Building Optimized Solutions by Using Object Services
3.
L6-5
Immediately after the comment, add code to define a compiled LINQ query called compiledQuery by using a static function. The query should return all of the contact entities from the Entity Data Model (EDM). Your code should resemble the following code example.
[Visual Basic] Public Shared compiledQuery As Func(Of AdventureWorksEntities, ObjectQuery(Of Contact)) = System.Data.Objects.CompiledQuery.Compile(Of AdventureWorksEntities, ObjectQuery(Of Contact))(Function(entities) entities.Contacts)
[Visual C#] public static Func compiledQuery = CompiledQuery.Compile( entities => entities.Contacts);
4.
Save the DataAccessLayer code file: a.
If you are using Visual Basic, on the File menu, click Save DataAccessLayer.vb.
b.
If you are using Visual C#, on the File menu, click Save DataAccessLayer.cs.
Task 5: Add code to invoke the compiled LINQ query 1.
2.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
Open the DataAccessLayer code file by double-clicking the comment TODO: Retrieve all contacts using the compiled query item in the task list. This task is located in the GetContactListEntityCompiledLINQ method: •
3.
In the task list, double-click the TODO: Ex1 - Retrieve all contacts using the compiled query item.
Delete the comment in the GetContactListEntityCompiledLINQ method: •
Select the comment in the method, and then press DELETE.
L6-6
Building Optimized Solutions by Using Object Services
4.
Write code that performs the following tasks: a.
Check whether the entities variable is null. If it is, instantiate it as a new instance of the AdventureWorksEntities context object.
b.
Obtain an ObjectQuery object by invoking the compiled LINQ query.
c.
If the value of the NoTracking variable is true, set the ObjectQuery object's MergeOption property to NoTracking.
d. Return the contact entities in a List object. Your code should resemble the following code example. [Visual Basic] Public Function GetContactListEntityCompiledLINQ() As List(Of Contact) ' Check you have an ObjectContext object. If entities Is Nothing Then entities = New AdventureWorksEntities() Dim contacts As ObjectQuery(Of Contact) = compiledQuery.Invoke(entities) If NoTracking Then contacts.MergeOption = MergeOption.NoTracking Return contacts.ToList() End Function
[Visual C#] public List GetContactListEntityCompiledLINQ() { // Check you have an ObjectContext object. if (entities == null) entities = new AdventureWorksEntities(); ObjectQuery contacts = compiledQuery.Invoke(entities); if (NoTracking) contacts.MergeOption = MergeOption.NoTracking; return contacts.ToList(); }
5.
Save the DataAccessLayer code file: a.
If you are using Visual Basic, on the File menu, click Save DataAccessLayer.vb.
Lab 6: Building Optimized Solutions by Using Object Services
b.
L6-7
If you are using Visual C#, on the File menu, click Save DataAccessLayer.cs.
Task 6: Add code to retrieve all of the contact entities by using Entity SQL 1.
2.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
Open the DataAccessLayer code file by double-clicking the comment TODO: Ex1 - Retrieve all contacts using Entity SQL item in the task list. This task is located in the GetContactListEntityQuery method: •
3.
In the task list, double-click the TODO: Ex1 - Retrieve all contacts using Entity SQL item.
Delete the comment in the GetContactListEntityQuery method: •
4.
Select the comment in the method, and then press DELETE.
Write code that performs the following tasks: a.
Check whether the entities variable is null. If it is, instantiate it as a new instance of the AdventureWorksEntities context object.
b.
Obtain an ObjectQuery object by creating a query that uses Entity SQL to retrieve all of the contact entities from the EDM.
c.
If the value of the NoTracking variable is true, set the ObjectQuery object's MergeOption property to NoTracking.
d. Return the contact entities in a List object. Your code should resemble the following code example. [Visual Basic] Public Function GetContactListEntityQuery() As List(Of Contact) ' Check you have an ObjectContext object. If (Entities is Nothing) Then Entities = New AdventureWorksEntities() End If Dim contacts As ObjectQuery(Of Contact) = entities.CreateQuery(Of Contact)("Select value c from Contacts as c")
L6-8
Building Optimized Solutions by Using Object Services
If (NoTracking) Then contacts.MergeOption = MergeOption.NoTracking End If Return contacts.ToList() End Function
[Visual C#] public List GetContactListEntityQuery() { // Check you have an ObjectContext object. if (entities == null) entities = new AdventureWorksEntities(); ObjectQuery contacts = entities.CreateQuery("Select value c from Contacts as c"); if (NoTracking) contacts.MergeOption = MergeOption.NoTracking; return contacts.ToList(); }
5.
Save the DataAccessLayer code file: a.
If you are using Visual Basic, on the File menu, click Save DataAccessLayer.vb.
b.
If you are using Visual C#, on the File menu, click Save DataAccessLayer.cs.
Task 7: Modify the GetContactList method to check the NoTracking variable 1.
2.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
Open the DataAccessLayer code file by double-clicking the comment TODO: Ex1 - Check NoTracking item in the task list. This task is located in the GetContactList method: •
In the task list, double-click the TODO: Ex1 - Check NoTracking item.
Lab 6: Building Optimized Solutions by Using Object Services
3.
L6-9
Modify the line of code immediately below the comment to check whether the value of the NoTracking variable is true before you set the MergeOption property to NoTracking. Your code should resemble the following code example.
[Visual Basic] ' TODO: Ex1 - Check NoTracking If NoTracking Then contacts.MergeOption = MergeOption.NoTracking
[Visual C#] // TODO: Ex1 - Check NoTracking if (NoTracking) contacts.MergeOption = MergeOption.NoTracking;
4.
Save the DataAccessLayer code file: a.
If you are using Visual Basic, on the File menu, click Save DataAccessLayer.vb.
b.
If you are using Visual C#, on the File menu, click Save DataAccessLayer.cs.
Task 8: Build and test the application 1.
Build the solution and correct any errors: •
2.
Start the application in Debug mode: •
3.
On the Build menu, click Build Solution.
On the Debug menu, click Start Debugging.
Observe the timing results obtained by running the TimingTests application: a.
In the command window, on the Detailed Timing for GetContactList() page, make a note of the Total Time values, and then press ENTER.
b.
In the command window, on the Compare Implementations of GetContactList() page, write down the Average values, and then press ENTER.
L6-10
Building Optimized Solutions by Using Object Services
Task 9: Pre-generate views to improve query performance 1.
2.
3.
Open the AdventureWorksEDM.edmx file and change the Metadata Artifact Processing property to Copy to Output Directory: a.
In Solution Explorer, right-click AdventureWorksEDM.edmx, and then click Open.
b.
In the Model Browser window, click anywhere in the white space.
c.
In the Properties window, change the Metadata Artifact Processing property to Copy to Output Directory.
Save the AdventureWorksEDM.edmx file and build the solution: a.
On the File menu, click Save AdventureWorksEDM.edmx.
b.
On the Build menu, click Build Solution.
Add view generation to the DAL project by using EdmGen.exe to generate the views during the pre-build event: a.
In Solution Explorer, right-click the DAL project, and then click Properties.
b.
If you are using Visual Basic, on the Compile panel, click Build Events, add the following command to the Pre-build event command line, and then click OK.
"%windir%\Microsoft.NET\Framework\v4.0.30319\EdmGen.exe" /nologo /language:VB /mode:ViewGeneration "/inssdl:$(TargetDir)AdventureWorksEDM.ssdl" "/incsdl:$(TargetDir)AdventureWorksEDM.csdl" "/inmsl:$(TargetDir)AdventureWorksEDM.msl" "/outviews:$(ProjectDir)AdventureWorksEDM.Views.vb"
c.
If you are using Visual C#, on the Build Events panel, add the following command to the Pre-build event command line.
"%windir%\Microsoft.NET\Framework\v4.0.30319\EdmGen.exe" /nologo /language:CSharp /mode:ViewGeneration "/inssdl:$(TargetDir)AdventureWorksEDM.ssdl" "/incsdl:$(TargetDir)AdventureWorksEDM.csdl" "/inmsl:$(TargetDir)AdventureWorksEDM.msl" "/outviews:$(ProjectDir)AdventureWorksEDM.Views.cs"
Note: This command is one continuous line.
Lab 6: Building Optimized Solutions by Using Object Services
L6-11
d. On the File menu, click Save All. e. 4.
On the Build menu, click Build Solution.
Add the generated views to the project: a.
In Solution Explorer, right-click the DAL project, point to Add, and then click Existing Item.
b.
If you are using Visual Basic, in the Add Existing Item - DAL dialog box, click AdventureWorksEDM.Views.vb, and then click Add.
c.
If you are using Visual C#, in the Add Existing Item - DAL dialog box, click AdventureWorksEDM.Views.cs, and then click Add.
d. On the Build menu, click Build Solution. 5.
Update the connection strings in the TimingTests project to use the new metadata resources: a.
In Solution Explorer, in the TimingTests project, right-click App.Config, and then click Open.
b.
Comment out the Standard Connection String for AdventureWorksEntities, and uncomment the Lab 6 Connection String for pre-generated views. Your code should resemble the following code example.
-->
c. 6.
Start the application in Debug mode: •
7.
8.
On the File menu, click Save App.Config.
On the Debug menu, click Start Debugging.
Observe the timing results obtained by running the TimingTests application: a.
In the command window, on the Detailed Timing for GetContactList() page, write down the Total Time values, and then press ENTER.
b.
In the command window, on the Compare Implementations of GetContactList() page, write down the Average values, and then press ENTER.
Close the solution: •
On the File menu, click Close Solution.
Exercise 2: Improving the Performance of Update Operations Task 1: Open the starter project for this exercise •
Open the existing solution, DAL.sln, in the E:\Labfiles\Lab06\VB\Ex2\Starter or E:\Labfiles\Lab06\CS\Ex2\Starter folder: a.
On the File menu, point to Open, and then click Project/Solution.
b.
If you are using Visual Basic, in the Open Project dialog box, move to the E:\Labfiles\Lab06\VB\Ex2\Starter folder, click DAL.sln, and then click Open.
Lab 6: Building Optimized Solutions by Using Object Services
c.
L6-13
If you are using Visual C#, in the Open Project dialog box, move to the E:\Labfiles\Lab06\CS\Ex2\Starter folder, click DAL.sln, and then click Open.
Task 2: Modify the CreateRewardsClaim method to run asynchronously 1.
2.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
Open the DataAccessLayer code file by double-clicking the comment TODO: Ex2 - In CreateRewardsClaim, instantiate a BackgroundWorker item in the task list. This task is located in the CreateRewardsClaim method: •
3.
In the task list, double-click the TODO: Ex2 - In CreateRewardsClaim, instantiate a BackgroundWorker item.
Immediately after the comment, write code that performs the following tasks: a.
Instantiate a new BackgroundWorker object.
b.
Set the WorkerSupportsCancellation property to false.
c.
Set the WorkerReportsProgress property to false.
Your code should resemble the following code example. [Visual Basic] Dim bw As New BackgroundWorker() bw.WorkerSupportsCancellation = False bw.WorkerReportsProgress = False
[Visual C#] BackgroundWorker bw = new BackgroundWorker(); bw.WorkerSupportsCancellation = false; bw.WorkerReportsProgress = false;
4.
Locate the next comment by double-clicking the comment TODO: Ex2 - In CreateRewardsClaim, place existing code in DoWork item in the task list. This task is located in the CreateRewardsClaim method:
L6-14
Building Optimized Solutions by Using Object Services
• 5.
In the task list, double-click the TODO: Ex2 - In CreateRewardsClaim, place existing code in DoWork item.
Assign the existing code in the CreateRewardsClaim method to the BackgroundWorker object's DoWork event handler by using a lambda expression. Replace the two existing return statements with statements that assign the new claim to the Result property of the DoWork event's args parameter. Your code should resemble the following code example.
Note: New or modified code is highlighted in bold.
[Visual Basic] AddHandler bw.DoWork, Sub(o, args) ' Get an ObjectContext object. Using entities As New AdventureWorksEntities() Try ' Get the next valid claim id. claim.ClaimID = GetNextClaimID() ' Add the RewardsClaimed to the entity set. entities.RewardsClaimed.AddObject(claim) ' Get the contact and decrement the points. Dim contact As Contact = claim.Contact contact.CurrentPoints = contact.CurrentPoints - claim.PointsUsed contact.ModifiedDate = DateTime.Now ' Begin a distributed transaction. Using scope As New TransactionScope() ' Save all the changes to the database. entities.SaveChanges() ' Use a different ObjectContext object. Using archivedEntities As _ New AdventureWorksArchivedEntities() ' Create archived rewards claim. Dim archivedClaim As ArchivedRewardsClaimed = ArchivedRewardsClaimed.CreateArchivedRewardsClaimed(
Lab 6: Building Optimized Solutions by Using Object Services
L6-15
0, claim.ClaimID, claim.PointsUsed, claim.RewardID, claim.ContactID, DateTime.Now) archivedEntities.AddToArchivedRewardsClaimed( archivedClaim) archivedEntities.SaveChanges() End Using ' The Complete method commits the transaction. ' If an exception is thrown, Complete is ' not called and the transaction is rolled back. scope.Complete() End Using ' Make sure the correct datetime is in the context. entities.Refresh(RefreshMode.StoreWins, contact) entities.Refresh(RefreshMode.StoreWins, claim) args.Result = claim Catch ex As TransactionAbortedException entities.Refresh(RefreshMode.StoreWins, claim.Contact) entities.Refresh(RefreshMode.StoreWins, claim) Throw New DALException("Could not save " & _ "RewardsClaim and ArchivedRewardsClaim in " & _ "transaction", ex) Catch ex As OptimisticConcurrencyException ' The contact could have been modified so ' get the latest version from the database. entities.Refresh(RefreshMode.StoreWins, claim.Contact) ' Deduct points from customer. claim.Contact.CurrentPoints = claim.Contact.CurrentPoints - claim.PointsUsed claim.Contact.ModifiedDate = DateTime.Now ' Try to save all the changes again. entities.SaveChanges() ' Make sure the correct datetime is in the context. entities.Refresh(RefreshMode.StoreWins, claim.Contact) entities.Refresh(RefreshMode.StoreWins, claim) args.Result = claim
L6-16
Building Optimized Solutions by Using Object Services
Catch ex As InvalidOperationException Throw New DALException( "There was a problem creating the RewardClaim", ex) Catch ex As UpdateException Throw New DALException("There was a problem " & _ "saving the RewardClaim to the database", ex) End Try End Using End Sub
[Visual C#] bw.DoWork += (o, args) => { // Get an ObjectContext object. using (AdventureWorksEntities entities = new AdventureWorksEntities()) { try { // Get the next valid claim ID. claim.ClaimID = GetNextClaimID(); // Add the RewardsClaimed to the entity set. entities.RewardsClaimed.AddObject(claim); // Get the contact and decrement the points. Contact contact = claim.Contact; contact.CurrentPoints -= claim.PointsUsed; contact.ModifiedDate = DateTime.Now; // Begin a distributed transaction. using (TransactionScope scope = new TransactionScope()) { // Save all of the changes to the database. entities.SaveChanges(); // Use a different ObjectContext object. using (AdventureWorksArchivedEntities archivedEntities = new AdventureWorksArchivedEntities()) { // Create an archived rewards claim.
Lab 6: Building Optimized Solutions by Using Object Services
ArchivedRewardsClaimed archivedClaim = ArchivedRewardsClaimed.CreateArchivedRewardsClaimed (0, claim.ClaimID, claim.PointsUsed, claim.RewardID, claim.ContactID, DateTime.Now); archivedEntities.AddToArchivedRewardsClaimed( archivedClaim); archivedEntities.SaveChanges(); } // The Complete method commits the transaction. // If an exception is thrown, // the Complete method is not called, // and the transaction is rolled back. scope.Complete(); } // Make sure the correct datetime is in the context. entities.Refresh(RefreshMode.StoreWins, contact); entities.Refresh(RefreshMode.StoreWins, claim); args.Result = claim; } catch (TransactionAbortedException ex) { entities.Refresh(RefreshMode.StoreWins, claim.Contact); entities.Refresh(RefreshMode.StoreWins, claim); throw new DALException("Could not save RewardsClaim and ArchivedRewardsClaim in transaction", ex); } catch (OptimisticConcurrencyException) { // The contact may have been modified, so // get the latest version from the database. entities.Refresh(RefreshMode.StoreWins, claim.Contact); // Deduct points from customer. claim.Contact.CurrentPoints -= claim.PointsUsed; claim.Contact.ModifiedDate = DateTime.Now; // Try to save all of the changes again. entities.SaveChanges(); // Make sure the correct datetime is in the context. entities.Refresh(RefreshMode.StoreWins, claim.Contact); entities.Refresh(RefreshMode.StoreWins, claim); args.Result = claim;
L6-17
L6-18
Building Optimized Solutions by Using Object Services
} catch (InvalidOperationException ex) { throw new DALException( "There was a problem creating the RewardClaim", ex); } catch (UpdateException ex) { throw new DALException("There was a problem saving the RewardClaim to the database", ex); } } };
6.
Locate the next comment by double-clicking the comment TODO: Ex2 - In CreateRewardsClaim, implement the BackgroundWorkers RunWorkerCompleted event item in the task list. This task is located in the CreateRewardsClaim method: •
7.
In the task list, double-click the TODO: Ex2 - In CreateRewardsClaim, implement the BackgroundWorkers RunWorkerCompleted event item.
Use a lambda expression to implement the BackgroundWorker object's RunWorkerCompleted event handler. If there were errors in the DoWork event, call the OnDataModificationCompleted method with false as the first parameter, the error message as the second parameter, and -1 as the third parameter. If the DoWork event completed without errors, call the OnDataModificationCompleted method with true as the first parameter, a success message as the second parameter, and the claimID property of the new claim as the third parameter. Your code should resemble the following code example.
[Visual Basic] AddHandler bw.RunWorkerCompleted, Sub(o, args) If args.Error IsNot Nothing Then OnDataModificationCompleted(False, args.Error.Message, -1) Else OnDataModificationCompleted(True, "Created new RewardsClaimed with ID: " & DirectCast(args.Result, RewardsClaimed).ClaimID, DirectCast(args.Result, RewardsClaimed).ClaimID) End If End Sub
Lab 6: Building Optimized Solutions by Using Object Services
L6-19
[Visual C#] bw.RunWorkerCompleted += (o, args) => { if (args.Error != null) OnDataModificationCompleted(false, args.Error.Message, -1); else OnDataModificationCompleted(true, "Created new RewardsClaimed with ID: " & ((RewardsClaimed)args.Result).ClaimID, ((RewardsClaimed)args.Result).ClaimID); };
8.
Locate the next comment by double-clicking the comment TODO: Ex2 - In CreateRewardsClaim, start the BackgroundWorker item in the task list. This task is located in the CreateRewardsClaim method: •
9.
In the task list, double-click the TODO: Ex2 - In CreateRewardsClaim, start the BackgroundWorker item.
Immediately after the comment, add code to start the BackgroundWorker component running asynchronously. Your code should resemble the following code example.
[Visual Basic] bw.RunWorkerAsync()
[Visual C#] bw.RunWorkerAsync();
10. Save the DataAccessLayer code file: a.
If you are using Visual Basic, on the File menu, click Save DataAccessLayer.vb.
b.
If you are using Visual C#, on the File menu, click Save DataAccessLayer.cs.
Task 3: Modify the UpdateRewardsClaim method to run asynchronously 1.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
L6-20
Building Optimized Solutions by Using Object Services
b. 2.
Open the DataAccessLayer code file by double-clicking the comment TODO: Ex2 - In UpdateRewardsClaim, instantiate a BackgroundWorker item in the task list. This task is located in the UpdateRewardsClaim method: •
3.
If the task list is showing User Tasks, in the Categories list, click Comments.
In the task list, double-click the TODO: Ex2 - In UpdateRewardsClaim, instantiate a BackgroundWorker item.
Immediately after the comment, write code that performs the following tasks: a.
Instantiate a new BackgroundWorker object.
b.
Set the WorkerSupportsCancellation property to false.
c.
Set the WorkerReportsProgress property to false.
Your code should resemble the following code example. [Visual Basic] Dim bw As New BackgroundWorker() bw.WorkerSupportsCancellation = False bw.WorkerReportsProgress = False
[Visual C#] BackgroundWorker bw = new BackgroundWorker(); bw.WorkerSupportsCancellation = false; bw.WorkerReportsProgress = false;
4.
Locate the next comment by double-clicking the comment TODO: Ex2 - In UpdateRewardsClaim, place existing code in DoWork item in the task list. This task is located in the UpdateRewardsClaim method: •
5.
In the task list, double-click the TODO: Ex2 - In UpdateRewardsClaim, place existing code in DoWork item.
Assign the existing code in the UpdateRewardsClaim method to the BackgroundWorker object's DoWork event handler by using a lambda expression. Replace the two existing return statements with statements that assign the updated claim to the Result property of the DoWork event's args parameter. Your code should resemble the following code example.
Note: New or modified code is highlighted in bold.
Lab 6: Building Optimized Solutions by Using Object Services
L6-21
[Visual Basic] AddHandler bw.DoWork, Sub(o, args) Dim originalPoints As Integer = 0 Dim newPoints As Integer = 0 Dim rewardClaimToModify As RewardsClaimed = Nothing ' Get an ObjectContext object. Using entities As New AdventureWorksEntities() Try ' Get the entity key you need. Dim key As EntityKey = rewardClaim.EntityKey Dim objectClaim As Object = Nothing ' Make sure that the entity to modify is loaded. If entities.TryGetObjectByKey(key, objectClaim) Then rewardClaimToModify = DirectCast(objectClaim, RewardsClaimed) ' Make sure you're working with ' the latest version of the claim. entities.Refresh( RefreshMode.StoreWins, rewardClaimToModify) ' Get the old and new points ready ' to update the contact. originalPoints = rewardClaimToModify.PointsUsed newPoints = rewardClaim.PointsUsed ' Copy all the changes over from ' the detached RewardClaim entity. entities.ApplyCurrentValues( key.EntitySetName, rewardClaim) If originalPoints newPoints Then ' Update conatcts points. rewardClaimToModify.Contact.CurrentPoints = rewardClaimToModify.Contact.CurrentPoints + originalPoints rewardClaimToModify.Contact.CurrentPoints = rewardClaimToModify.Contact.CurrentPoints -
L6-22
Building Optimized Solutions by Using Object Services
newPoints rewardClaimToModify.Contact.ModifiedDate = DateTime.Now End If End If ' Begin a distributed transaction. Using scope As New TransactionScope() ' Save all the changes to the database. entities.SaveChanges() ' Use a different ObjectContext. Using archivedEntities As New _ AdventureWorksArchivedEntities() ' Create archived rewards claim. Dim archivedClaim As ArchivedRewardsClaimed = ArchivedRewardsClaimed.CreateArchivedRewardsClaimed( 0, rewardClaimToModify.ClaimID, rewardClaimToModify.PointsUsed, rewardClaimToModify.RewardID, rewardClaimToModify.ContactID, DateTime.Now) archivedEntities.AddToArchivedRewardsClaimed( archivedClaim) archivedEntities.SaveChanges() End Using ' The Complete method commits the transaction. ' If an exception is thrown, ' Complete is not called and the ' transaction is rolled back. scope.Complete() End Using ' Make sure the correct datetime is in the context. entities.Refresh(RefreshMode.StoreWins, rewardClaimToModify.Contact) entities.Refresh(RefreshMode.StoreWins, rewardClaimToModify) args.Result = rewardClaimToModify
Lab 6: Building Optimized Solutions by Using Object Services
Catch ex As TransactionAbortedException entities.Refresh(RefreshMode.StoreWins, rewardClaimToModify.Contact) entities.Refresh(RefreshMode.StoreWins, rewardClaimToModify) Throw New DALException("Could not save " & _ "RewardsClaim and ArchivedRewardsClaim in " & _ "transaction", ex) Catch ex As OptimisticConcurrencyException ' The contact could have been modified ' so get the latest version of the ' contact from the database. entities.Refresh(RefreshMode.StoreWins, rewardClaimToModify.Contact) ' It's very unlikely the claim has been modified ' as you've only just refreshed it. entities.Refresh(RefreshMode.ClientWins, rewardClaimToModify) ' Adjust points for customer. rewardClaimToModify.Contact.CurrentPoints = rewardClaimToModify.Contact.CurrentPoints + originalPoints rewardClaimToModify.Contact.CurrentPoints = rewardClaimToModify.Contact.CurrentPoints newPoints rewardClaimToModify.Contact.ModifiedDate = DateTime.Now ' Try to save all the changes again. entities.SaveChanges() ' Make sure the correct datetime is in the context. entities.Refresh(RefreshMode.StoreWins, rewardClaimToModify.Contact) entities.Refresh(RefreshMode.StoreWins, rewardClaimToModify) args.Result = rewardClaimToModify Catch ex As InvalidOperationException Throw New DALException("There was a problem " & _
L6-23
L6-24
Building Optimized Solutions by Using Object Services
"modifying the RewardClaim", ex) Catch ex As UpdateException Throw New DALException("There was a problem " & _ "saving the RewardClaim changes to the " & _ "database", ex) End Try End Using End Sub
[Visual C#] bw.DoWork += (o, args) => { int originalPoints = 0; int newPoints = 0; RewardsClaimed rewardClaimToModify = null; // Get an an ObjectContext object. using (AdventureWorksEntities entities = new AdventureWorksEntities()) { try { // Get the entity key you need. EntityKey key = rewardClaim.EntityKey; object objectClaim = null; // Make sure that the entity to modify is loaded. if (entities.TryGetObjectByKey(key, out objectClaim)) { rewardClaimToModify = (RewardsClaimed)objectClaim; // Make sure you are working with the latest version // of the claim. entities.Refresh(RefreshMode.StoreWins, rewardClaimToModify); // Get the old and new points ready to update the // contact. originalPoints = rewardClaimToModify.PointsUsed; newPoints = rewardClaim.PointsUsed;
Lab 6: Building Optimized Solutions by Using Object Services
L6-25
// Copy all of the changes over from the detached // RewardClaim entity. entities.ApplyCurrentValues(key.EntitySetName, rewardClaim); if (originalPoints != newPoints) { // Update contacts points. rewardClaimToModify.Contact.CurrentPoints += originalPoints; rewardClaimToModify.Contact.CurrentPoints -= newPoints; rewardClaimToModify.Contact.ModifiedDate = DateTime.Now; } } // Begin a distributed transaction. using (TransactionScope scope = new TransactionScope()) { // Save all of the changes to the database. entities.SaveChanges(); // Use a different ObjectContext object. using (AdventureWorksArchivedEntities archivedEntities = new AdventureWorksArchivedEntities()) { // Create an archived rewards claim. ArchivedRewardsClaimed archivedClaim = ArchivedRewardsClaimed.CreateArchivedRewardsClaimed(0, rewardClaimToModify.ClaimID, rewardClaimToModify.PointsUsed, rewardClaimToModify.RewardID, rewardClaimToModify.ContactID, DateTime.Now); archivedEntities.AddToArchivedRewardsClaimed( archivedClaim); archivedEntities.SaveChanges(); } // The Complete method commits the transaction. // If an exception is thrown, // the Complete method is not called, // and the transaction is rolled back. scope.Complete(); } // Make sure the correct datetime is in the context. entities.Refresh(RefreshMode.StoreWins,
L6-26
Building Optimized Solutions by Using Object Services
rewardClaimToModify.Contact); entities.Refresh(RefreshMode.StoreWins, rewardClaimToModify); args.Result = rewardClaimToModify; } catch (TransactionAbortedException ex) { entities.Refresh(RefreshMode.StoreWins, rewardClaimToModify.Contact); entities.Refresh(RefreshMode.StoreWins, rewardClaimToModify); throw new DALException("Could not save RewardsClaim and ArchivedRewardsClaim in transaction", ex); } catch (OptimisticConcurrencyException) { // The contact may have been modified, // so get the latest version of the contact // from the database. entities.Refresh(RefreshMode.StoreWins, rewardClaimToModify.Contact); // It is very unlikely the claim has been modified // because you have only just refreshed it. entities.Refresh(RefreshMode.ClientWins, rewardClaimToModify); // Adjust points for customer. rewardClaimToModify.Contact.CurrentPoints += originalPoints; rewardClaimToModify.Contact.CurrentPoints -= newPoints; rewardClaimToModify.Contact.ModifiedDate = DateTime.Now; // Try to save all of the changes again. entities.SaveChanges(); // Make sure the correct datetime is in the context. entities.Refresh(RefreshMode.StoreWins, rewardClaimToModify.Contact); entities.Refresh(RefreshMode.StoreWins, rewardClaimToModify); args.Result = rewardClaimToModify; } catch (InvalidOperationException ex) { throw new DALException("There was a problem modifying the
Lab 6: Building Optimized Solutions by Using Object Services
L6-27
RewardClaim", ex); } catch (UpdateException ex) { throw new DALException("There was a problem saving the RewardClaim changes to the database", ex); } } };
6.
Locate the next comment by double-clicking the comment TODO: Ex2 - In UpdateRewardsClaim, implement the BackgroundWorkers RunWorkerCompleted event item in the task list. This task is located in the UpdateRewardsClaim method: •
7.
In the task list, double-click the TODO: Ex2 - In UpdateRewardsClaim, implement the BackgroundWorkers RunWorkerCompleted event item.
Use a lambda expression to implement the BackgroundWorker object's RunWorkerCompleted event handler. If there were errors in the DoWork event, call the OnDataModificationCompleted method with false as the first parameter, the error message as the second parameter, and -1 as the third parameter. If the DoWork event completed without errors, call the OnDataModificationCompleted method with true as the first parameter, a success message as the second parameter, and the claimID property of the updated claim as the third parameter. Your code should resemble the following code example.
[Visual Basic] AddHandler bw.RunWorkerCompleted, Sub(o, args) If args.Error IsNot Nothing Then OnDataModificationCompleted(False, args.Error.Message, -1) Else OnDataModificationCompleted(True, "Updated RewardsClaimed with ID: " & DirectCast(args.Result, RewardsClaimed).ClaimID, DirectCast(args.Result, RewardsClaimed).ClaimID) End If End Sub
L6-28
Building Optimized Solutions by Using Object Services
[Visual C#] bw.RunWorkerCompleted += (o, args) => { if (args.Error != null) OnDataModificationCompleted(false, args.Error.Message, -1); else OnDataModificationCompleted(true, "Updated RewardsClaimed with ID: " & ((RewardsClaimed)args.Result).ClaimID, ((RewardsClaimed)args.Result).ClaimID); };
8.
Locate the next comment TODO: Ex2 - In UpdateRewardsClaim, start the BackgroundWorker item in the task list. This task is located in the UpdateRewardsClaim method: •
9.
In the task list, double-click the TODO: Ex2 - In UpdateRewardsClaim, start the BackgroundWorker item.
Immediately after the comment, add code to start the BackgroundWorker component running asynchronously. Your code should resemble the following code example.
[Visual Basic] bw.RunWorkerAsync()
[Visual C#] bw.RunWorkerAsync();
10. Save the DataAccessLayer code file: a.
If you are using Visual Basic, on the File menu, click Save DataAccessLayer.vb.
b.
If you are using Visual C#, on the File menu, click Save DataAccessLayer.cs.
Task 4: Modify the DeleteRewardsClaim method to run asynchronously 1.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
Lab 6: Building Optimized Solutions by Using Object Services
b. 2.
If the task list is showing User Tasks, in the Categories list, click Comments.
Open the DataAccessLayer code file by double-clicking the comment TODO: Ex2 - In DeleteRewardsClaim, instantiate a BackgroundWorker item in the task list. This task is located in the DeleteRewardsClaim method: •
3.
L6-29
In the task list, double-click the TODO: Ex2 - In DeleteRewardsClaim, instantiate a BackgroundWorker item.
Immediately after the comment, write code that performs the following tasks: a.
Instantiate a new BackgroundWorker object.
b.
Set the WorkerSupportsCancellation property to false.
c.
Set the WorkerReportsProgress property to false.
Your code should resemble the following code example. [Visual Basic] Dim bw As New BackgroundWorker() bw.WorkerSupportsCancellation = False bw.WorkerReportsProgress = False
[Visual C#] BackgroundWorker bw = new BackgroundWorker(); bw.WorkerSupportsCancellation = false; bw.WorkerReportsProgress = false;
4.
Locate the next comment by double-clicking the comment TODO: Ex2 - In DeleteRewardsClaim, place existing code in DoWork item in the task list. This task is located in the DeleteRewardsClaim method: •
5.
In the task list, double-click the TODO: Ex2 - In DeleteRewardsClaim, place existing code in DoWork item.
Assign the existing code in the DeleteRewardsClaim method to the BackgroundWorker object's DoWork event handler by using a lambda expression. Delete the two existing return statements. Your code should resemble the following code example.
Note: New or modified code is highlighted in bold.
L6-30
Building Optimized Solutions by Using Object Services
[Visual Basic] AddHandler bw.DoWork, Sub(o, args) Dim rewardClaimToDelete As RewardsClaimed = Nothing Dim relatedContact As Contact = Nothing ' Get an ObjectContext object. Using entities As New AdventureWorksEntities() Try ' Get the entity key of the claim to delete. Dim key As New EntityKey( "AdventureWorksEntities.RewardsClaimed", "ClaimID", claimId) Dim objectClaim As Object = Nothing ' Make sure that the entity to modify is loaded. If entities.TryGetObjectByKey(key, objectClaim) Then rewardClaimToDelete = DirectCast( objectClaim, RewardsClaimed) ' Make sure you're working with ' the latest version of the claim. entities.Refresh(RefreshMode.StoreWins, rewardClaimToDelete) ' Give the points back to the contact. relatedContact = rewardClaimToDelete.Contact relatedContact.CurrentPoints = relatedContact.CurrentPoints + rewardClaimToDelete.PointsUsed relatedContact.ModifiedDate = DateTime.Now ' Delete the object. entities.DeleteObject(rewardClaimToDelete) End If ' Save the changes to the database. entities.SaveChanges() ' Make sure the correct datetime is in the context. entities.Refresh(RefreshMode.StoreWins, relatedContact) Catch ex As OptimisticConcurrencyException
Lab 6: Building Optimized Solutions by Using Object Services
L6-31
' The contact could have been modified so ' get the latest version from the database. entities.Refresh( RefreshMode.StoreWins, relatedContact) ' Give the points back to the contact. relatedContact.CurrentPoints = relatedContact.CurrentPoints + rewardClaimToDelete.PointsUsed relatedContact.ModifiedDate = DateTime.Now ' Try to save all the changes again. entities.SaveChanges() ' Make sure the correct datetime is in the context. entities.Refresh( RefreshMode.StoreWins, relatedContact) Catch ex As InvalidOperationException Throw New DALException("There was a problem " & _ "deleting the RewardsClaim", ex) Catch ex As UpdateException Throw New DALException("There was a problem " & _ "deleting the RewardClaim from the database", ex) End Try End Using End Sub
[Visual C#] bw.DoWork += (o, args) => { RewardsClaimed rewardClaimToDelete = null; Contact relatedContact = null; // Get an ObjectContext object. using (AdventureWorksEntities entities = new AdventureWorksEntities()) { try { // Get the entity key of the claim to delete. EntityKey key =
L6-32
Building Optimized Solutions by Using Object Services
new EntityKey("AdventureWorksEntities.RewardsClaimed", "ClaimID", claimId); object objectClaim = null; // Make sure that the entity to modify is loaded. if (entities.TryGetObjectByKey(key, out objectClaim)) { rewardClaimToDelete = (RewardsClaimed)objectClaim; // Make sure you are working with the latest version // of the claim. entities.Refresh(RefreshMode.StoreWins, rewardClaimToDelete); // Give the points back to the contact. relatedContact = rewardClaimToDelete.Contact; relatedContact.CurrentPoints += rewardClaimToDelete.PointsUsed; relatedContact.ModifiedDate = DateTime.Now; // Delete the object. entities.DeleteObject(rewardClaimToDelete); } // Save the changes to the database. entities.SaveChanges(); // Make sure the correct datetime is in the context. entities.Refresh(RefreshMode.StoreWins, relatedContact); } catch (OptimisticConcurrencyException) { // The contact may have been modified, so // get the latest version from the database. entities.Refresh(RefreshMode.StoreWins, relatedContact); // Give the points back to the contact. relatedContact.CurrentPoints += rewardClaimToDelete.PointsUsed; relatedContact.ModifiedDate = DateTime.Now; // Try to save all of the changes again. entities.SaveChanges(); // Make sure the correct datetime is in the context. entities.Refresh(RefreshMode.StoreWins, relatedContact); } catch (InvalidOperationException ex) { throw new DALException( "There was a problem deleting the RewardsClaim", ex); }
Lab 6: Building Optimized Solutions by Using Object Services
L6-33
catch (UpdateException ex) { throw new DALException("There was a problem deleting the RewardClaim from the database", ex); } } };
6.
Locate the next comment by double-clicking the comment TODO: Ex2 - In DeleteRewardsClaim, implement the BackgroundWorkers RunWorkerCompleted event item in the task list. This task is located in the DeleteRewardsClaim method: •
7.
In the task list, double-click the TODO: Ex2 - In DeleteRewardsClaim, implement the BackgroundWorkers RunWorkerCompleted event item.
Use a lambda expression to implement the BackgroundWorker object's RunWorkerCompleted event handler. If there were errors in the DoWork event, call the OnDataModificationCompleted method with false as the first parameter, the error message as the second parameter, and -1 as the third parameter. If the DoWork event completed without errors, call the OnDataModificationCompleted method with true as the first parameter, a success message as the second parameter, and the claimID property of the deleted claim as the third parameter. Your code should resemble the following code example.
[Visual Basic] AddHandler bw.RunWorkerCompleted, Sub(o, args) If args.Error IsNot Nothing Then OnDataModificationCompleted(False, args.Error.Message, -1) Else OnDataModificationCompleted(True, "Deleted RewardsClaimed with ID: " & claimId, claimId) End If End Sub
[Visual C#] bw.RunWorkerCompleted += (o, args) => { if (args.Error != null) OnDataModificationCompleted(false, args.Error.Message, -1); else
L6-34
Building Optimized Solutions by Using Object Services
OnDataModificationCompleted(true, "Deleted RewardsClaimed with ID: " & claimId, claimId); };
8.
Locate the next comment by double-clicking the comment TODO: Ex2 - In DeleteRewardsClaim, start the BackgroundWorker item in the task list. This task is located in the DeleteRewardsClaim method: •
9.
In the task list, double-click the TODO: Ex2 - In DeleteRewardsClaim, start the BackgroundWorker item.
Immediately after the comment, add code to start the BackgroundWorker component running asynchronously. Your code should resemble the following code example.
[Visual Basic] bw.RunWorkerAsync()
[Visual C#] bw.RunWorkerAsync();
10. Save the DataAccessLayer code file: a.
If you are using Visual Basic, on the File menu, click Save DataAccessLayer.vb.
b.
If you are using Visual C#, on the File menu, click Save DataAccessLayer.cs.
Task 5: Modify your unit tests to verify your asynchronous code 1.
2.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
Open the DataAccessLayerTest code file by double-clicking the comment TODO: Ex2 - In CreateRewardsClaimTest, call CreateRewardsClaim item in the task list. This task is located in the CreateRewardsClaimTest method: •
In the task list, double-click the TODO: Ex2 - In CreateRewardsClaimTest, call CreateRewardsClaim item.
Lab 6: Building Optimized Solutions by Using Object Services
L6-35
3.
Review the existing code in the CreateRewardsClaimTest method.
4.
Immediately after the comment, call the CreateRewardsClaim method in the data access layer, passing the test claim called claim as a parameter. Your code should resemble the following code example.
[Visual Basic] dal.CreateRewardsClaim(claim)
[Visual C#] dal.CreateRewardsClaim(claim);
5.
Locate the next TODO comment in the CreateRewardsClaimTest method by double-clicking the comment TODO: Ex2 - In CreateRewardsClaimTest, check the values retrieved from the database item in the task list: •
6.
In the task list, double-click the TODO: Ex2 - In CreateRewardsClaimTest, check the values retrieved from the database item.
Immediately after the comment, add code to check that the property values of the claim object match those of the lastClaim object. Your code should resemble the following code example.
[Visual Basic] ' Check that it was correctly saved. Assert.AreEqual(createResult, True) Assert.AreEqual(claim.ClaimID, lastClaim.ClaimID) Assert.AreEqual(claim.PointsUsed, lastClaim.PointsUsed) Assert.AreEqual(claim.RewardID, lastClaim.RewardID) Assert.AreEqual(claim.ContactID, lastClaim.ContactID)
[Visual C#] // Check that it was correctly saved. Assert.AreEqual(createResult, true); Assert.AreEqual(claim.ClaimID, lastClaim.ClaimID); Assert.AreEqual(claim.PointsUsed, lastClaim.PointsUsed); Assert.AreEqual(claim.RewardID, lastClaim.RewardID); Assert.AreEqual(claim.ContactID, lastClaim.ContactID);
L6-36
Building Optimized Solutions by Using Object Services
7.
Locate the first TODO comment in the UpdateRewardsClaimTest method by double-clicking the comment TODO: Ex2 - In UpdateRewardsClaimTest, modify the claim and save the changes item in the task list: •
In the task list, double-click the TODO: Ex2 - In UpdateRewardsClaimTest, modify the claim and save the changes item.
8.
Review the existing code in the UpdateRewardsClaimTest method.
9.
Immediately after the comment, modify the PointsUsed and RewardID properties of the claim object, and call the UpdateRewardsClaim method in the data access layer, passing the test claim called claim as a parameter. Your code should resemble the following code example.
[Visual Basic] ' Modify the claim. claim.PointsUsed = 2000 claim.RewardID = 21 ' Save the changes dal.UpdateRewardsClaim(claim)
[Visual C#] // Modify the claim. claim.PointsUsed = 2000; claim.RewardID = 21; // Save the changes dal.UpdateRewardsClaim(claim);
10. Locate the next TODO comment in the UpdateRewardsClaimTest method by double-clicking the comment TODO: Ex2 - In UpdateRewardsClaimTest, check the values retrieved from the database item in the task list: •
In the task list, double-click the TODO: Ex2 - In UpdateRewardsClaimTest, check the values retrieved from the database item.
11. Immediately after the comment, add code to check that the property values of the claim object match those of the updatedClaim object and that the value of the updateResult variable is true. Your code should resemble the following code example.
Lab 6: Building Optimized Solutions by Using Object Services
L6-37
[Visual Basic] ' Check that the changes were saved correctly. Assert.AreEqual(updateResult, True) Assert.AreEqual(claim.ClaimID, updatedClaim.ClaimID) Assert.AreEqual(claim.PointsUsed, updatedClaim.PointsUsed) Assert.AreEqual(claim.RewardID, updatedClaim.RewardID) Assert.AreEqual(claim.ContactID, updatedClaim.ContactID)
[Visual C#] // Check that the changes were saved correctly. Assert.AreEqual(updateResult, true); Assert.AreEqual(claim.ClaimID, updatedClaim.ClaimID); Assert.AreEqual(claim.PointsUsed, updatedClaim.PointsUsed); Assert.AreEqual(claim.RewardID, updatedClaim.RewardID); Assert.AreEqual(claim.ContactID, updatedClaim.ContactID);
12. Locate the first TODO comment in the DeleteRewardsClaimTest method by double-clicking the TODO: Ex2 - In DeleteRewardsClaimTest, delete the claim item in the task list: •
In the task list, double-click the TODO: Ex2 - In DeleteRewardsClaimTest, delete the claim item.
13. Review the existing code in the DeleteRewardsClaimTest method. 14. Immediately after the comment, call the DeleteRewardsClaim method in the data access layer, passing the claim object's ClaimID property as a parameter. Your code should resemble the following code example. [Visual Basic] ' Delete the claim. DAL.DeleteRewardsClaim(claim.ClaimID)
[Visual C#] // Delete the claim. dal.DeleteRewardsClaim(claim.ClaimID);
15. Locate the next TODO comment in the DeleteRewardsClaimTest method by double-clicking the comment TODO: Ex2 - In DeleteRewardsClaimTest, check the delete succeeded item in the task list:
L6-38
Building Optimized Solutions by Using Object Services
•
In the task list, double-click the TODO: Ex2 - In DeleteRewardsClaimTest, check the delete succeeded item.
16. Immediately after the comment, add code to check that the value of the deleteResult variable is true. Your code should resemble the following code example. [Visual Basic] ' Check the delete worked. Assert.AreEqual(deleteResult, True)
[Visual C#] // Check the delete worked. Assert.AreEqual(deleteResult, true);
17. Save the DataAccessLayerTest code file: a.
If you are using Visual Basic, on the File menu, click Save DataAccessLayerTest.vb.
b.
If you are using Visual C#, on the File menu, click Save DataAccessLayerTest.cs.
Task 6: Build and test the application 1.
Build the solution and correct any errors: •
2.
On the Build menu, click Build Solution.
Run all of the tests in the solution: •
On the Test menu, point to Run, and then click All Tests in Solution.
3.
Verify that all of the tests succeed.
4.
Start the Customer Rewards application in Debug mode: •
On the Debug menu, click Start Debugging.
5.
In the AdventureWorks Rewards window, click All Customers to load data from the entity model into the data grid. Verify that you can add, modify, and delete claims and that the contact’s points are adjusted correctly.
6.
Close the application.
7.
Close the solution, and then close Visual Studio:
Lab 6: Building Optimized Solutions by Using Object Services
a.
On the File menu, click Close Solution.
b.
On the File menu, click Exit.
L6-39
Lab 7: Customizing Entities and Building Custom Entity Classes
L7-1
Customizing Entities and Building Custom Entity Classes
Lab 7: Customizing Entities and Building Custom Entity Classes Exercise 1: Using a Template to Add Custom Functionality to Entity Classes Task 1: Prepare the AdventureWorks database for the lab 1.
Log on to the 10265A-GEN-DEV-07 virtual machine as Student with the password Pa$$w0rd.
2.
Run AWReset.bat in the E:\Labfiles folder: a.
Click Start, and then click Computer.
b.
Double-click Allfiles (E:), double-click Labfiles, and then double-click AWReset.bat.
c.
Wait for the batch file to finish running, and then close Windows® Explorer.
Task 2: Open the starter project for this exercise 1.
Open Microsoft® Visual Studio® 2010: •
2.
Click Start, click All Programs, click Microsoft Visual Studio 2010, and then click Microsoft Visual Studio 2010.
Open the existing solution, DAL.sln, in the E:\Labfiles\Lab07\VB\Ex1\Starter or E:\Labfiles\Lab07\CS\Ex1\Starter folder: a.
On the File menu, point to Open, and then click Project/Solution.
b.
If you are using Microsoft Visual Basic®, in the Open Project dialog box, move to the E:\Labfiles\Lab07\VB\Ex1\Starter folder, click DAL.sln, and then click Open.
c.
If you are using Microsoft Visual C#®, in the Open Project dialog box, move to the E:\Labfiles\Lab07\CS\Ex1\Starter folder, click DAL.sln, and then click Open.
L7-2
Customizing Entities and Building Custom Entity Classes
Task 3: Create the IValidate class 1.
2.
Add a new interface named IValidate to the DAL project: a.
In Solution Explorer, right-click DAL, point to Add, and then click New Item.
b.
In the Add New Item - DAL dialog box, in the Templates list, click Interface.
c.
In the Name box, type IValidate and then click Add.
Modify the interface definition to make it public, and add a void method named Validate that takes no arguments. a.
If you are using Visual Basic, in IValidate.vb, replace both occurrences of Class with Interface, and in the body of the class, type Sub Validate();
b.
If you are using Visual C#, in IValidate.cs, replace class with public interface, and in the body of the class, type void Validate(); Your code should resemble the following code example.
[Visual Basic] Public Interface IValidate Sub Validate() End Interface
[Visual C#] public interface IValidate { void Validate(); }
Task 4: Create the template 1.
Add an ADO.NET EntityObject Generator item named AWModel.tt to the DAL project: a.
In Solution Explorer, expand DAL, right-click AdventureWorksEDM.edmx, and then click Open.
b.
In the Entity Designer pane, right-click anywhere in the white space, and then click Add Code Generation Item.
Lab 7: Customizing Entities and Building Custom Entity Classes
c.
L7-3
In the Add New Item - DAL dialog box, in the templates list, click ADO.NET EntityObject Generator.
d. In the Name box, type AWModel.tt and then click Add. e. 2.
If the Security Warning dialog box appears, select the Do not show this message again check box, and then click OK.
Open AdventureWorksEDM.Designer.vb or AdventureWorksEDM.Designer.cs and review the comment that it contains: a.
In Solution Explorer, expand AdventureWorksEDM.edmx. If you are using Visual Basic, right-click AdventureWorksEDM.Designer.vb, and then click Open. If you are using Visual C#, right-click AdventureWorksEDM.Designer.cs, and then click Open.
b.
Review the comment at the beginning of the file.
c.
On the File menu, click Close.
Task 5: Customize the template 1.
In AWModel.tt, locate the line of code that begins : a.
In Solution Explorer, right-click AWModel.tt, and then click Open.
b.
On the Edit menu, point to Find and Replace, and then click Quick Find.
c.
In the Find and Replace dialog box, in the Find what box, type and then click Find Next.
d. Close the Find and Replace dialog box. 2.
3.
Edit the line of code to make every entity object implement the IValidate interface: •
If you are using Visual Basic, you will see the next line begins with Inherits. On the line below this, type Implements IValidate
•
If you are using Visual C#, at the end of the line of code, type, IValidate (including the comma)
Within the body of this section, declare a partial void method named OnValidate that takes no arguments: a.
If you are using Visual Basic, position your cursor after Implements IValidate, and then press ENTER.
L7-4
Customizing Entities and Building Custom Entity Classes
b.
If you are using Visual C#, position your cursor after the brace on the following line, and then press ENTER. Your code should resemble the following code example.
[Visual Basic] Private Partial Sub OnValidate() End Sub
[Visual C#] partial void OnValidate();
4.
Immediately after the statement that declares the OnValidate method, implement the IValidate.Validate method. Inside this method, call the OnValidate method that you have just declared. Your code should resemble the following code example.
[Visual Basic] Overloads Sub Validate() Implements IValidate.Validate OnValidate() End Sub
[Visual C#] void IValidate.Validate() { OnValidate(); }
5.
If you are using Visual Basic, you must also manually adjust the namespace to match the rest of the project. a.
Locate the line of code that begins Dim namespaceName As String. This code is located near the top of AWModel.tt.
b.
Replace this line with the following code.
Dim namespaceName As String = "DAL"
Lab 7: Customizing Entities and Building Custom Entity Classes
L7-5
Task 6: View the generated code •
Build the solution, and then review the generated code in each class in AWModel.vb or AWModel.cs: a.
On the Build menu, click Build Solution.
b.
In Solution Explorer, expand AWModel.tt. If you are using Visual Basic, right-click AWModel.vb, and then click Open. If you are using Visual C#, right-click AWModel.cs, and then click Open.
c.
On the Edit menu, point to Find and Replace, and then click Quick Find.
d. In the Find and Replace dialog box, in the Find what box, type IValidate and then click Find Next. e.
Close the Find and Replace dialog box.
f.
Review the generated code in the class.
Task 7: Implement the OnValidate method 1.
2.
Add a new class named ContactExtension to the DAL project: a.
In Solution Explorer, right-click DAL, point to Add, and then click New Item.
b.
In the Add New Item - DAL dialog box, in the Templates list, click Class.
c.
In the Name box, type ContactExtension and then click Add.
Modify the class definition to define the class as a public partial class for the Contact class. Your code should resemble the following code example.
[Visual Basic] Namespace DAL Partial Public Class Contact End Class End Namespace
L7-6
Customizing Entities and Building Custom Entity Classes
[Visual C#] namespace DAL { public partial class Contact { } }
3.
Add a void method named OnValidate to the class. If you are using Visual C#, this method should be declared partial. Your code should resemble the following code example.
[Visual Basic] Partial Public Class Contact Private Sub OnValidate() End Sub End Class
[Visual C#] public partial class Contact { partial void OnValidate() { } }
4.
Add code to the OnValidate method to throw a DALValidationException exception in each of the following scenarios: •
If the CurrentPoints property is set to a negative value.
•
If the EmailAddress property does not contain an @ symbol.
•
If the EmailAddress property does not contain a period.
Your code should resemble the following code example.
Lab 7: Customizing Entities and Building Custom Entity Classes
L7-7
[Visual Basic] Namespace DAL Partial Public Class Contact Private Sub OnValidate() ' Validation rules for contacts If CurrentPoints < 0 Then Throw New DALValidationException( "Customers cannot have negative points total") End If If EmailAddress.Contains("@") = False Then Throw New DALValidationException( "E-mail address must contain an @ symbol") End If If EmailAddress.Contains(".") = False Then Throw New DALValidationException( "E-mail address must contain a period") End If End Sub End Class End Namespace
[Visual C#] partial void OnValidate() { // Validation rules for contacts if (CurrentPoints < 0) { throw new DALValidationException("Customers cannot have negative points total"); } if (EmailAddress.Contains("@") == false) { throw new DALValidationException ("E-mail address must contain an @ symbol"); }
L7-8
Customizing Entities and Building Custom Entity Classes
if (EmailAddress.Contains(".") == false) { throw new DALValidationException ("E-mail address must contain a period"); } }
Task 8: Modify the DAL code to validate the data 1.
In the DataAccessLayer class, modify the UpdateContact method to call the Validate method before saving changes to the object: a.
If you are using Visual Basic, in Solution Explorer, right-click DataAccessLayer.vb, and then click Open.
b.
If you are using Visual C#, in Solution Explorer, right-click DataAccessLayer.cs, and then click Open.
c.
Locate the UpdateContact method and call the Validate method before saving changes. Your code should resemble the following code example.
[Visual Basic] ' Validate the changes DirectCast(contactToModify, IValidate).Validate() ' Save the changes to the database entities.SaveChanges()
[Visual C#] // Validate the changes ((IValidate)contactToModify).Validate(); // Save the changes to the database entities.SaveChanges();
2.
In the DataAccessLayer class, modify the AddContact method to call the Validate method before saving changes to the object: •
Locate the AddContact method and call the Validate method before saving changes. Your code should resemble the following code example.
Lab 7: Customizing Entities and Building Custom Entity Classes
L7-9
[Visual Basic] ' Validate the changes DirectCast(contact, IValidate).Validate() ' Save the changes to the database entities.SaveChanges()
[Visual C#] // Validate the changes ((IValidate)contact).Validate(); // Save the changes to the database entities.SaveChanges();
3.
In the CustomerRewards project, in the MainWindow.xaml.vb or MainWindow.xaml.cs class, in the contacts_MouseDoubleClick event, refresh the contacts list: a.
In Solution Explorer, expand CustomerRewards, expand MainWindow.xaml. If you are using Visual Basic, right-click MainWindow.xaml.vb, and then click Open. If you are using Visual C#, right-click MainWindow.xaml.cs, and then click Open.
b.
Locate the catch block in the contacts_MouseDoubleClick method.
c.
Add code to refresh the data. Your code should resemble the following code example.
[Visual Basic] Catch ex As Exception MessageBox.Show(String.Format("Exception occurred: {0}", ex.Message)) Me.RefreshContacts() End Try
[Visual C#] catch (Exception ex) { MessageBox.Show(string.Format("Exception occurred: {0}", ex.Message)); this.RefreshContacts(); }
L7-10
Customizing Entities and Building Custom Entity Classes
Task 9: Add unit tests to verify your code 1.
2.
Review the task list: a.
On the View menu, click Task List.
b.
In the Task List pane, in the Categories list, click Comments.
Open the DataAccessLayerTest code file by double-clicking the comment TODO: Ex1 - Add a test for AddContact when there is a CurrentPoints validation exception item in the task list. This task is located in the AddContactCurrentPointsValidationTest method: •
3.
In the task list, double-click the TODO: Ex1 - Add a test for AddContact when there is a CurrentPoints validation exception item.
Add an ExpectedException attribute to the method for the DALValidationException type: Your code should resemble the following code example.
[Visual Basic] _ _ Public Sub AddContactCurrentPointsValidationTest()
[Visual C#] [TestMethod()] [ExpectedException(typeof(DALValidationException))] public void AddContactCurrentPointsValidationTest()
4.
Delete the comment in the AddContactCurrentPointsValidationTest method: •
5.
Select the comment in the method, and then press DELETE.
Add a unit test to create a Contact object, set the CurrentPoints property of the Contact object to an invalid value, and then add the contact to the database. Be sure to release all resources at the end of the test. Your code should resemble the following code example.
[Visual Basic] _ _ Public Sub AddContactCurrentPointsValidationTest() Dim target As New DataAccessLayer()
Lab 7: Customizing Entities and Building Custom Entity Classes
L7-11
' Create a new contact and set the CurrentPoints ' property to an invalid value Dim contact As Contact = CreateTestContact() contact.CurrentPoints = -5 ' Add the new contact to the database Dim contactId As Integer = 0 contactId = target.AddContact(contact) ' Tidy up If contactId > 0 Then target.DeleteContact(contactId) End If target.Dispose() End Sub
[Visual C#] [TestMethod()] [ExpectedException(typeof(DALValidationException))] public void AddContactCurrentPointsValidationTest() { DataAccessLayer target = new DataAccessLayer(); // Create a new contact and set the CurrentPoints property to an invalid value Contact contact = CreateTestContact(); contact.CurrentPoints = -5; // Add the new contact to the database int contactId = 0; contactId = target.AddContact(contact); // Tidy up if (contactId > 0) target.DeleteContact(contactId); target.Dispose(); }
6.
Locate the AddContactAtSymbolValidationTest method by double-clicking the comment TODO: Ex1 - Add a test for AddContact when there is a missing @ sign in the e-mail address validation exception item in the task list:
L7-12
Customizing Entities and Building Custom Entity Classes
•
7.
In the task list, double-click the TODO: Ex1 - Add a test for AddContact when there is a missing @ sign in the e-mail address validation exception item.
Add an ExpectedException attribute to the method for the DALValidationException type. Your code should resemble the following code example.
[Visual Basic] _ _ Public Sub AddContactAtSymbolValidationTest()
[Visual C#] [TestMethod()] [ExpectedException(typeof(DALValidationException))] public void AddContactAtSymbolValidationTest()
8.
Delete the comment in the AddContactAtSymbolValidationTest method: •
9.
Select the comment in the method, and then press DELETE.
Add a unit test to create a Contact object, set the EmailAddress property of the Contact object to an invalid value with a missing @ symbol, and then add the contact to the database. Be sure to release all resources at the end of the test. Your code should resemble the following code example.
[Visual Basic] _ _ Public Sub AddContactAtSymbolValidationTest() Dim target As New DataAccessLayer() ' Create a new contact and set the EmailAddress property ' to an invalid value Dim contact As Contact = CreateTestContact() contact.EmailAddress = "ronald1adventure-works.com" ' Add the new contact to the database Dim contactId As Integer = 0 contactId = target.AddContact(contact)
Lab 7: Customizing Entities and Building Custom Entity Classes
L7-13
' Tidy up If contactId > 0 Then target.DeleteContact(contactId) End If target.Dispose() End Sub
[Visual C#] [TestMethod()] [ExpectedException(typeof(DALValidationException))] public void AddContactAtSymbolValidationTest() { DataAccessLayer target = new DataAccessLayer(); // Create a new contact and set the EmailAddress property to an invalid value Contact contact = CreateTestContact(); contact.EmailAddress = "ronald1adventure-works.com"; // Add the new contact to the database int contactId = 0; contactId = target.AddContact(contact); // Tidy up if (contactId > 0) target.DeleteContact(contactId); target.Dispose(); }
10. Locate the AddContactPeriodValidationTest method by double-clicking the comment TODO: Ex1 - Add a test for AddContact when there is a missing period in the e-mail address validation exception item in the task list: •
In the task list, double-click the TODO: Ex1 - Add a test for AddContact when there is a missing period in the e-mail address validation exception item.
11. Add an ExpectedException attribute to the method for the DALValidationException type. Your code should resemble the following code example.
L7-14
Customizing Entities and Building Custom Entity Classes
[Visual Basic] _ _ Public Sub AddContactPeriodValidationTest()
[Visual C#] [TestMethod()] [ExpectedException(typeof(DALValidationException))] public void AddContactPeriodValidationTest()
12. Delete the comment in the AddContactPeriodValidationTest method: •
Select the comment in the method, and then press DELETE.
13. Add a unit test to create a Contact object, set the EmailAddress property of the Contact object to an invalid value with a missing period, and then add the contact to the database. Be sure to release all resources at the end of the test. Your code should resemble the following code example. [Visual Basic] _ _ Public Sub AddContactPeriodValidationTest() Dim target As New DataAccessLayer() ' Create a new contact and set the EmailAddress property ' to an invalid value Dim contact As Contact = CreateTestContact() contact.EmailAddress = "ronald1@adventure-workscom" ' Add the new contact to the database Dim contactId As Integer = 0 contactId = target.AddContact(contact) ' Tidy up If contactId > 0 Then target.DeleteContact(contactId) End If target.Dispose() End Sub
Lab 7: Customizing Entities and Building Custom Entity Classes
L7-15
[Visual C#] [TestMethod()] [ExpectedException(typeof(DALValidationException))] public void AddContactPeriodValidationTest() { DataAccessLayer target = new DataAccessLayer(); // Create a new contact and set the EmailAddress property to an invalid value Contact contact = CreateTestContact(); contact.EmailAddress = "ronald1@adventure-workscom"; // Add the new contact to the database int contactId = 0; contactId = target.AddContact(contact); // Tidy up if (contactId > 0) target.DeleteContact(contactId); target.Dispose(); }
14. Locate the UpdateContactCurrentPointsValidationTest method by doubleclicking the comment TODO: Ex1 - Add a test for UpdateContact when there is a CurrentPoints validation exception item in the task list: •
In the task list, double-click the TODO: Ex1 - Add a test for UpdateContact when there is a CurrentPoints validation exception item.
15. Add an ExpectedException attribute to the method for the DALValidationException type. Your code should resemble the following code example. [Visual Basic] _ _ Public Sub UpdateContactCurrentPointsValidationTest()
[Visual C#] [TestMethod()] [ExpectedException(typeof(DALValidationException))] public void UpdateContactCurrentPointsValidationTest()
L7-16
Customizing Entities and Building Custom Entity Classes
16. Delete the comment in the UpdateContactCurrentPointsValidationTest method: •
Select the comment in the method, and then press DELETE.
17. Add a unit test to create a Contact object, retrieve that contact from the database, set the CurrentPoints property of that Contact object to an invalid value, and then update the contact in the database. Be sure to release all resources at the end of the test. Your code should resemble the following code example. [Visual Basic] _ _ Public Sub UpdateContactCurrentPointsValidationTest() Dim target As New DataAccessLayer() ' Create a test contact and then get a detached version ' of the contact Dim contact As Contact = CreateTestContact() Dim testId As Integer = target.AddContact(contact) contact = GetContactById(testId) ' Set the CurrentPoints property to an invalid value contact.CurrentPoints = -10 ' Save the changes target.UpdateContact(contact) ' Tidy up target.DeleteContact(testId) target.Dispose() End Sub
[Visual C#] [TestMethod()] [ExpectedException(typeof(DALValidationException))] public void UpdateContactCurrentPointsValidationTest() { DataAccessLayer target = new DataAccessLayer(); // Create a test contact and then get a detached version of the contact Contact contact = CreateTestContact();
Lab 7: Customizing Entities and Building Custom Entity Classes
L7-17
int testId = target.AddContact(contact); contact = GetContactById(testId); // Set the CurrentPoints property to an invalid value contact.CurrentPoints = -10; // Save the changes target.UpdateContact(contact); // Tidy up target.DeleteContact(testId); target.Dispose(); }
18. Locate the UpdateContactAtSymbolValidationTest method by doubleclicking the comment TODO: Ex1 - Add a test for UpdateContact when there is a missing @ sign in the e-mail address validation exception item in the task list: •
In the task list, double-click the TODO: Ex1 - Add a test for UpdateContact when there is a missing @ sign in the e-mail address validation exception item.
19. Add an ExpectedException attribute to the method for the DALValidationException type. Your code should resemble the following code example. [Visual Basic] _ _ Public Sub UpdateContactAtSymbolValidationTest()
[Visual C#] [TestMethod()] [ExpectedException(typeof(DALValidationException))] public void UpdateContactAtSymbolValidationTest()
20. Delete the comment in the UpdateContactAtSymbolValidationTest method: •
Select the comment in the method, and then press DELETE.
21. Add a unit test to create a Contact object, retrieve that contact from the database, set the EmailAddress property of the Contact object to an invalid
L7-18
Customizing Entities and Building Custom Entity Classes
value with a missing @ symbol, and then update the contact in the database. Be sure to release all resources at the end of the test. Your code should resemble the following code example. [Visual Basic] _ _ Public Sub UpdateContactAtSymbolValidationTest() Dim target As New DataAccessLayer() ' Create a test contact and then get a detached version ' of the contact Dim contact As Contact = CreateTestContact() Dim testId As Integer = target.AddContact(contact) contact = GetContactById(testId) ' Set the EmailAddress property to an invalid value contact.EmailAddress = "ronald1adventure-works.com" ' Save the changes target.UpdateContact(contact) ' Tidy up target.DeleteContact(testId) target.Dispose() End Sub
[Visual C#] [TestMethod()] [ExpectedException(typeof(DALValidationException))] public void UpdateContactAtSymbolValidationTest() { DataAccessLayer target = new DataAccessLayer(); // Create a test contact and then get a detached version of the contact Contact contact = CreateTestContact(); int testId = target.AddContact(contact); contact = GetContactById(testId); // Set the EmailAddress property to an invalid value contact.EmailAddress = "ronald1adventure-works.com";
Lab 7: Customizing Entities and Building Custom Entity Classes
L7-19
// Save the changes target.UpdateContact(contact); // Tidy up target.DeleteContact(testId); target.Dispose(); }
22. Locate the UpdateContactPeriodValidationTest method by double-clicking the comment TODO: Ex1 - Add a test for UpdateContact when there is a missing period in the e-mail address validation exception item in the task list: •
In the task list, double-click the TODO: Ex1 - Add a test for UpdateContact when there is a missing period in the e-mail address validation exception item.
23. Add an ExpectedException attribute to the method for the DALValidationException type. Your code should resemble the following code example. [Visual Basic] _ _ Public Sub UpdateContactPeriodValidationTest()
[Visual C#] [TestMethod()] [ExpectedException(typeof(DALValidationException))] public void UpdateContactPeriodValidationTest()
24. Delete the comment in the UpdateContactPeriodValidationTest method: •
Select the comment in the method, and then press DELETE.
25. Add a unit test to create a Contact object, retrieve that contact from the database , set the EmailAddress property of the Contact object to an invalid value with a missing period, and then add the contact to the database. Be sure to release all resources at the end of the test. Your code should resemble the following code example.
L7-20
Customizing Entities and Building Custom Entity Classes
[Visual Basic] _ _ Public Sub UpdateContactPeriodValidationTest() Dim target As New DataAccessLayer() ' Create a test contact and then get a detached version ' of the contact Dim contact As Contact = CreateTestContact() Dim testId As Integer = target.AddContact(contact) contact = GetContactById(testId) ' Set the EmailAddress property to an invalid value contact.EmailAddress = "ronald1@adventure-workscom" ' Save the changes target.UpdateContact(contact) ' Tidy up target.DeleteContact(testId) target.Dispose() End Sub
[Visual C#] [TestMethod()] [ExpectedException(typeof(DALValidationException))] public void UpdateContactPeriodValidationTest() { DataAccessLayer target = new DataAccessLayer(); // Create a test contact and then get a detached version of the contact Contact contact = CreateTestContact(); int testId = target.AddContact(contact); contact = GetContactById(testId); // Set the EmailAddress property to an invalid value contact.EmailAddress = "ronald1@adventure-workscom"; // Save the changes target.UpdateContact(contact); // Tidy up target.DeleteContact(testId); target.Dispose(); }
Lab 7: Customizing Entities and Building Custom Entity Classes
L7-21
26. Save the DataAccessLayerTest code file: •
If you are using Visual Basic, on the File menu, click Save DataAccessLayerTest.vb.
•
If you are using Visual C#, on the File menu, click Save DataAccessLayerTest.cs.
Task 10: Build and test the application 1.
Build the solution and correct any errors: •
2.
On the Build menu, click Build Solution.
Start the application in Debug mode. •
On the Debug menu, click Start Debugging.
3.
In the AdventureWorks Rewards window, click All Customers to load data from the entity model into the data grid. Verify that the application functions as expected.
4.
Close the application.
5.
Run all of the tests in the solution: •
On the Test menu, point to Run, and then click All Tests in Solution.
6.
Verify that all of the tests succeed.
7.
Close the solution: •
On the File menu, click Close Solution.
Exercise 2: Creating Custom Entity Classes Task 1: Open the starter project for this exercise 1.
Open the DAL solution in the E:\Labfiles\Lab07\VB\Ex2\Starter or E:\Labfiles\Lab07\CS\Ex2\Starter folder: a.
In Visual Studio, on the File menu, point to Open, and then click Project/Solution:
b.
If you are using Visual Basic, in the Open Project dialog box, move to the E:\Labfiles\Lab07\VB\Ex2\Starter folder, click DAL.sln, and then click Open.
L7-22
Customizing Entities and Building Custom Entity Classes
c.
2.
If you are using Visual C#, in the Open Project dialog box, move to the E:\Labfiles\Lab07\CS\Ex2\Starter folder, click DAL.sln, and then click Open.
If you are using Visual C#, if a Problem Loading message is displayed, on the Build menu, click Rebuild Solution, and then in the designer pane, click Reload the designer.
Task 2: Remove the existing Contact class from the DAL project •
Open AdventureWorksEDM.Designer.vb or AdventureWorksEDM.Designer.cs, and then in the Entities region, comment out all of the Contact partial class: a.
In Solution Explorer, expand AdventureWorksEDM.edmx. If you are using Visual Basic, right-click AdventureWorksEDM.Designer.vb, and then click Open. If you are using Visual C#, right-click AdventureWorksEDM.Designer.cs, and then click Open.
b.
In the code pane, scroll through the code until you find the #region defined as Entities, and then position your insertion point after the region definition.
c.
On the Edit menu, point to Find and Replace, and then click Quick Find.
d. In the Find and Replace dialog box, in the Find what box, type Contact e.
Expand Find options, select the Match whole word check box, and then click Find Next.
f.
Close the Find and Replace dialog box.
g.
Select all of the Contact partial class, on the Edit menu, point to Advanced, and then click Comment Selection.
Task 3: Add the existing business class to the DAL project 1.
Add the businessLogicCustomer.cs or businessLogicCustomer.vb file in the E:\Labfiles\Lab07\CS\Ex2\Starter or E:\Labfiles\Lab07\VB\Ex2\Starter folder to the DAL project: a.
In Solution Explorer, right-click DAL, point to Add, and then click Existing Item.
Lab 7: Customizing Entities and Building Custom Entity Classes
2.
L7-23
b.
If you are using Visual Basic, in the Add Existing Item - DAL dialog box, move to the E:\Labfiles\Lab07\VB\Ex2\Starter folder, click businessLogicCustomer.vb, and then click Add.
c.
If you are using Visual C#, in the Add Existing Item - DAL dialog box, move to the E:\Labfiles\Lab07\CS\Ex2\Starter folder, click businessLogicCustomer.cs, and then click Add.
Rename the businessLogicCustomer class file to Contact.cs or Contact.vb, and the class to Contact: a.
If you are using Visual Basic, in Solution Explorer, right-click businessLogicCustomer.vb, and then click Rename.
b.
If you are using Visual C#, in Solution Explorer, right-click businessLogicCustomer.cs, and then click Rename.
c.
If you are using Visual Basic, type Contact.vb and then press ENTER.
d. If you are using Visual C#, type Contact.cs and then press ENTER. e.
If you are using Visual C#, in the Microsoft Visual Studio dialog box, click Yes.
f.
If you are using Visual Basic, right-click Contact.vb, and then click Open. Locate the line of code that begins Public Class businessLogicCustomer, and replace it with Public Class Contact.
Task 4: Modify the business class to operate as an entity class 1.
In Contact.cs or Contact.vb, bring the following namespaces into scope: •
System.Data
•
System.Data.Objects.DataClasses
•
System.Data.Metadata.Edm
a.
If you are using Visual Basic, in Solution Explorer, right-click Contact.vb, and then click Open.
b.
If you are using Visual C#, in Solution Explorer, right-click Contact.cs, and then click Open.
c.
At the top of the code file, add statements to bring the namespaces into scope. Your code should resemble the following code example.
L7-24
Customizing Entities and Building Custom Entity Classes
[Visual Basic] ' TODO: Add using statements for System.Data, System.Data.Objects.DataClasses, and System.Data.Metadata.Edm Imports System.Data Imports System.Data.Objects.DataClasses Imports System.Data.Metadata.Edm
[Visual C#] // TODO: Add using statements for System.Data, System.Data.Objects.DataClasses, and System.Data.Metadata.Edm using System.Data; using System.Data.Objects.DataClasses; using System.Data.Metadata.Edm;
2.
In the Contact class, modify the class definition to inherit from EntityObject. Your code should resemble the following code example.
[Visual Basic] Public Class Contact Inherits EntityObject
[Visual C#] public class Contact : EntityObject
3.
In the Contact class, use the EdmEntityType attribute to link the class to the Contact entity in the AdventureWorksModel namespace. Your code should resemble the following code example.
[Visual Basic] _ Public Class Contact Inherits EntityObject
[Visual C#] [EdmEntityType(NamespaceName = "AdventureWorksModel", Name = "Contact")] public class Contact : EntityObject
Lab 7: Customizing Entities and Building Custom Entity Classes
4.
L7-25
Use the EdmScalarProperty attribute to configure the entity properties in the class, as the following table shows. Property name
EntityKeyProperty
IsNullable
ContactID
true
false
NameStyle
false
false
Title
false
true
FirstName
false
false
MiddleName
false
true
LastName
false
false
Suffix
false
true
EmailAddress
false
true
EmailPromotion
false
false
Phone
false
true
PasswordHash
false
false
PasswordSalt
false
false
AdditionalContactInfo
false
true
rowguid
false
false
ModifiedDate
false
false
CurrentPoints
false
false
Your code should resemble the following code example. [Visual Basic] _ Public Property ContactID As Int32 Get Return _ContactID End Get Set(ByVal value As Int32) If _ContactID value Then
L7-26
Customizing Entities and Building Custom Entity Classes
_ContactID = value End If End Set End Property _ Public Property NameStyle As Boolean Get Return _NameStyle End Get Set(ByVal value As Boolean) _NameStyle = value End Set End Property _ Public Property Title As String Get Return _Title End Get Set(ByVal value As String) _Title = value End Set End Property _ Public Property FirstName As String Get Return _FirstName End Get Set(ByVal value As String) _FirstName = value End Set End Property _ Public Property MiddleName As String Get Return _MiddleName End Get Set(ByVal value As String) _MiddleName = value End Set End Property _ Public Property LastName As String Get Return _LastName
Lab 7: Customizing Entities and Building Custom Entity Classes
End Get Set(ByVal value As String) _LastName = value End Set End Property _ Public Property Suffix As String Get Return _Suffix End Get Set(ByVal value As String) _Suffix = value End Set End Property _ Public Property EmailAddress As String Get Return _EmailAddress End Get Set(ByVal value As String) If value.Contains("@") = False Then Throw New DALValidationException("E-mail address must contain an @ symbol.") End If If value.Contains(".") = False Then Throw New DALValidationException("E-mail address must contain a period.") End If _EmailAddress = value End Set End Property _ Public Property EmailPromotion As Int32 Get Return _EmailPromotion End Get Set(ByVal value As Int32) _EmailPromotion = value End Set End Property _ Public Property Phone As String Get Return _Phone End Get
L7-27
L7-28
Customizing Entities and Building Custom Entity Classes
Set(ByVal value As String) _Phone = value End Set End Property _ Public Property PasswordHash As String Get Return _PasswordHash End Get Set(ByVal value As String) _PasswordHash = value End Set End Property _ Public Property PasswordSalt As String Get Return _PasswordSalt End Get Set(ByVal value As String) _PasswordSalt = value End Set End Property _ Public Property AdditionalContactInfo As String Get Return _AdditionalContactInfo End Get Set(ByVal value As String) _AdditionalContactInfo = value End Set End Property _ Public Property rowguid As Guid Get Return _rowguid End Get Set(ByVal value As Guid) _rowguid = value End Set End Property _ Public Property ModifiedDate As DateTime Get Return _ModifiedDate
Lab 7: Customizing Entities and Building Custom Entity Classes
L7-29
End Get Set(ByVal value As DateTime) If value > DateTime.Now Then Throw New DALValidationException("Modified date must not be in the future.") End If _ModifiedDate = value End Set End Property _ Public Property CurrentPoints As Int32 Get Return _CurrentPoints End Get Set(ByVal value As Int32) If value < 0 Then Throw New DALValidationException("Customers cannot have a negative points balance.") End If _CurrentPoints = value End Set End Property
[Visual C#] [EdmScalarProperty(EntityKeyProperty = true, IsNullable = false)] public Int32 ContactID { get { return _ContactID; } set { if (_ContactID != value) { _ContactID = value; } } } [EdmScalarProperty(EntityKeyProperty = false, IsNullable = false)] public Boolean NameStyle { get { return _NameStyle;
L7-30
Customizing Entities and Building Custom Entity Classes
} set { _NameStyle = value; } } [EdmScalarProperty(EntityKeyProperty = false, IsNullable = true)] public String Title { get { return _Title; } set { _Title = value; } } [EdmScalarProperty(EntityKeyProperty = false, IsNullable = false)] public String FirstName { get { return _FirstName; } set { _FirstName = value; } } [EdmScalarProperty(EntityKeyProperty = false, IsNullable = true)] public String MiddleName { get { return _MiddleName; } set { _MiddleName = value; } } [EdmScalarProperty(EntityKeyProperty = false, IsNullable = false)]
Lab 7: Customizing Entities and Building Custom Entity Classes
public String LastName { get { return _LastName; } set { _LastName = value; } } [EdmScalarProperty(EntityKeyProperty = false, IsNullable = true)] public String Suffix { get { return _Suffix; } set { _Suffix = value; } } [EdmScalarProperty(EntityKeyProperty = false, IsNullable = true)] public String EmailAddress { get { return _EmailAddress; } set { if (value.Contains("@") == false) { throw new DALValidationException("E-mail address must contain an @ symbol."); } if (value.Contains(".") == false) { throw new DALValidationException("E-mail address must contain a period."); } _EmailAddress = value; } }
L7-31
L7-32
Customizing Entities and Building Custom Entity Classes
[EdmScalarProperty(EntityKeyProperty = false, IsNullable = false)] public Int32 EmailPromotion { get { return _EmailPromotion; } set { _EmailPromotion = value; } } [EdmScalarProperty(EntityKeyProperty = false, IsNullable = true)] public String Phone { get { return _Phone; } set { _Phone = value; } } [EdmScalarProperty(EntityKeyProperty = false, IsNullable = false)] public String PasswordHash { get { return _PasswordHash; } set { _PasswordHash = value; } } [EdmScalarProperty(EntityKeyProperty = false, IsNullable = false)] public String PasswordSalt { get { return _PasswordSalt; } set { _PasswordSalt = value;
Lab 7: Customizing Entities and Building Custom Entity Classes
L7-33
} } [EdmScalarProperty(EntityKeyProperty = false, IsNullable = true)] public String AdditionalContactInfo { get { return _AdditionalContactInfo; } set { _AdditionalContactInfo = value; } } [EdmScalarProperty(EntityKeyProperty = false, IsNullable = false)] public Guid rowguid { get { return _rowguid; } set { _rowguid = value; } } [EdmScalarProperty(EntityKeyProperty = false, IsNullable = false)] public DateTime ModifiedDate { get { return _ModifiedDate; } set { if (value > DateTime.Now) { throw new DALValidationException("Modified date must not be in the future."); } _ModifiedDate = value; } } [EdmScalarProperty(EntityKeyProperty = false, IsNullable = false)] public Int32 CurrentPoints {
L7-34
Customizing Entities and Building Custom Entity Classes
get { return _CurrentPoints; } set { if (value < 0) { throw new DALValidationException("Customers cannot have a negative points balance."); } _CurrentPoints = value; } }
5.
Modify the Set statements for each property to notify the change tracker when a property change is pending and then completed, and to use the SetValidValue method of the StructuralObject object to change the property value. Your code should resemble the following code example.
[Visual Basic] _ Public Property ContactID As Int32 Get Return _ContactID End Get Set(ByVal value As Int32) If _ContactID value Then ReportPropertyChanging("ContactID") _ContactID = StructuralObject.SetValidValue(value) ReportPropertyChanged("ContactID") End If End Set End Property _ Public Property NameStyle As Boolean Get Return _NameStyle End Get Set(ByVal value As Boolean) ReportPropertyChanging("NameStyle") _NameStyle = StructuralObject.SetValidValue(value) ReportPropertyChanged("NameStyle") End Set
Lab 7: Customizing Entities and Building Custom Entity Classes
End Property _ Public Property Title As String Get Return _Title End Get Set(ByVal value As String) ReportPropertyChanging("Title") _Title = StructuralObject.SetValidValue(value, True) ReportPropertyChanged("Title") End Set End Property _ Public Property FirstName As String Get Return _FirstName End Get Set(ByVal value As String) ReportPropertyChanging("FirstName") _FirstName = StructuralObject.SetValidValue(value, True) ReportPropertyChanged("FirstName") End Set End Property _ Public Property MiddleName As String Get Return _MiddleName End Get Set(ByVal value As String) ReportPropertyChanging("MiddleName") _MiddleName = StructuralObject.SetValidValue(value, True) ReportPropertyChanged("MiddleName") End Set End Property _ Public Property LastName As String Get Return _LastName End Get Set(ByVal value As String) ReportPropertyChanging("LastName") _LastName = StructuralObject.SetValidValue(value, False) ReportPropertyChanged("LastName") End Set End Property
L7-35
L7-36
Customizing Entities and Building Custom Entity Classes
_ Public Property Suffix As String Get Return _Suffix End Get Set(ByVal value As String) ReportPropertyChanging("Suffix") _Suffix = StructuralObject.SetValidValue(value, True) ReportPropertyChanged("Suffix") End Set End Property _ Public Property EmailAddress As String Get Return _EmailAddress End Get Set(ByVal value As String) If value.Contains("@") = False Then Throw New DALValidationException("E-mail address must contain an @ symbol.") End If If value.Contains(".") = False Then Throw New DALValidationException("E-mail address must contain a period.") End If ReportPropertyChanging("EmailAddress") _EmailAddress = StructuralObject.SetValidValue(value, True) ReportPropertyChanged("EmailAddress") End Set End Property _ Public Property EmailPromotion As Int32 Get Return _EmailPromotion End Get Set(ByVal value As Int32) ReportPropertyChanging("EmailPromotion") _EmailPromotion = StructuralObject.SetValidValue(value) ReportPropertyChanged("EmailPromotion") End Set End Property _ Public Property Phone As String Get Return _Phone End Get
Lab 7: Customizing Entities and Building Custom Entity Classes
L7-37
Set(ByVal value As String) ReportPropertyChanging("Phone") _Phone = StructuralObject.SetValidValue(value, True) ReportPropertyChanged("Phone") End Set End Property _ Public Property PasswordHash As String Get Return _PasswordHash End Get Set(ByVal value As String) ReportPropertyChanging("PasswordHash") _PasswordHash = StructuralObject.SetValidValue(value, False) ReportPropertyChanged("PasswordHash") End Set End Property _ Public Property PasswordSalt As String Get Return _PasswordSalt End Get Set(ByVal value As String) ReportPropertyChanging("PasswordSalt") _PasswordSalt = StructuralObject.SetValidValue(value, False) ReportPropertyChanged("PasswordSalt") End Set End Property _ Public Property AdditionalContactInfo As String Get Return _AdditionalContactInfo End Get Set(ByVal value As String) ReportPropertyChanging("AdditionalContactInfo") _AdditionalContactInfo = StructuralObject.SetValidValue(value, True) ReportPropertyChanged("AdditionalContactInfo") End Set End Property _ Public Property rowguid As Guid Get Return _rowguid End Get
L7-38
Customizing Entities and Building Custom Entity Classes
Set(ByVal value As Guid) ReportPropertyChanging("rowguid") _rowguid = StructuralObject.SetValidValue(value) ReportPropertyChanged("rowguid") End Set End Property _ Public Property ModifiedDate As DateTime Get Return _ModifiedDate End Get Set(ByVal value As DateTime) If value > DateTime.Now Then Throw New DALValidationException("Modified date must not be in the future.") End If ReportPropertyChanging("ModifiedDate") _ModifiedDate = StructuralObject.SetValidValue(value) ReportPropertyChanged("ModifiedDate") End Set End Property _ Public Property CurrentPoints As Int32 Get Return _CurrentPoints End Get Set(ByVal value As Int32) If value < 0 Then Throw New DALValidationException("Customers cannot have a negative points balance.") End If ReportPropertyChanging("CurrentPoints") _CurrentPoints = StructuralObject.SetValidValue(value) ReportPropertyChanged("CurrentPoints") End Set End Property
[Visual C#] [EdmScalarPropertyAttribute(EntityKeyProperty = true, IsNullable = false)] public Int32 ContactID { get { return _ContactID;
Lab 7: Customizing Entities and Building Custom Entity Classes
} set { if (_ContactID != value) { ReportPropertyChanging("ContactID"); _ContactID = StructuralObject.SetValidValue(value); ReportPropertyChanged("ContactID"); } } } [EdmScalarPropertyAttribute(EntityKeyProperty = false, IsNullable = false)] public Boolean NameStyle { get { return _NameStyle; } set { ReportPropertyChanging("NameStyle"); _NameStyle = StructuralObject.SetValidValue(value); ReportPropertyChanged("NameStyle"); } } [EdmScalarPropertyAttribute(EntityKeyProperty = false, IsNullable = true)] public String Title { get { return _Title; } set { ReportPropertyChanging("Title"); _Title = StructuralObject.SetValidValue(value, true); ReportPropertyChanged("Title"); } } [EdmScalarPropertyAttribute(EntityKeyProperty = false, IsNullable = true)] public String FirstName { get
L7-39
L7-40
Customizing Entities and Building Custom Entity Classes
{ return _FirstName; } set { ReportPropertyChanging("FirstName"); _FirstName = StructuralObject.SetValidValue(value, true); ReportPropertyChanged("FirstName"); } } [EdmScalarPropertyAttribute(EntityKeyProperty = false, IsNullable = true)] public String MiddleName { get { return _MiddleName; } set { ReportPropertyChanging("MiddleName"); _MiddleName = StructuralObject.SetValidValue(value, true); ReportPropertyChanged("MiddleName"); } } [EdmScalarPropertyAttribute(EntityKeyProperty = false, IsNullable = false)] public String LastName { get { return _LastName; } set { ReportPropertyChanging("LastName"); _LastName = StructuralObject.SetValidValue(value, false); ReportPropertyChanged("LastName"); } } [EdmScalarPropertyAttribute(EntityKeyProperty = false, IsNullable = true)] public String Suffix { get {
Lab 7: Customizing Entities and Building Custom Entity Classes
L7-41
return _Suffix; } set { ReportPropertyChanging("Suffix"); _Suffix = StructuralObject.SetValidValue(value, true); ReportPropertyChanged("Suffix"); } } [EdmScalarPropertyAttribute(EntityKeyProperty = false, IsNullable = true)] public String EmailAddress { get { return _EmailAddress; } set { if (value.Contains("@") == false) { throw new DALValidationException("E-mail address must contain an @ symbol."); } if (value.Contains(".") == false) { throw new DALValidationException("E-mail address must contain a period."); } ReportPropertyChanging("EmailAddress"); _EmailAddress = StructuralObject.SetValidValue(value, true); ReportPropertyChanged("EmailAddress"); } } [EdmScalarPropertyAttribute(EntityKeyProperty = false, IsNullable = false)] public Int32 EmailPromotion { get { return _EmailPromotion; } set { ReportPropertyChanging("EmailPromotion"); _EmailPromotion = StructuralObject.SetValidValue(value); ReportPropertyChanged("EmailPromotion"); }
L7-42
Customizing Entities and Building Custom Entity Classes
} [EdmScalarPropertyAttribute(EntityKeyProperty = false, IsNullable = true)] public String Phone { get { return _Phone; } set { ReportPropertyChanging("Phone"); _Phone = StructuralObject.SetValidValue(value, true); ReportPropertyChanged("Phone"); } } [EdmScalarPropertyAttribute(EntityKeyProperty = false, IsNullable = false)] public String PasswordHash { get { return _PasswordHash; } set { ReportPropertyChanging("PasswordHash"); _PasswordHash = StructuralObject.SetValidValue(value, false); ReportPropertyChanged("PasswordHash"); } } [EdmScalarPropertyAttribute(EntityKeyProperty = false, IsNullable = false)] public String PasswordSalt { get { return _PasswordSalt; } set { ReportPropertyChanging("PasswordSalt"); _PasswordSalt = StructuralObject.SetValidValue(value, false); ReportPropertyChanged("PasswordSalt"); } }
Lab 7: Customizing Entities and Building Custom Entity Classes
L7-43
[EdmScalarPropertyAttribute(EntityKeyProperty = false, IsNullable = true)] public String AdditionalContactInfo { get { return _AdditionalContactInfo; } set { ReportPropertyChanging("AdditionalContactInfo"); _AdditionalContactInfo = StructuralObject.SetValidValue(value, true); ReportPropertyChanged("AdditionalContactInfo"); } } [EdmScalarPropertyAttribute(EntityKeyProperty = false, IsNullable = false)] public Guid rowguid { get { return _rowguid; } set { ReportPropertyChanging("rowguid"); _rowguid = StructuralObject.SetValidValue(value); ReportPropertyChanged("rowguid"); } } [EdmScalarPropertyAttribute(EntityKeyProperty = false, IsNullable = false)] public DateTime ModifiedDate { get { return _ModifiedDate; } set { if (value > DateTime.Now) { throw new DALValidationException("Modified date must not be in the future."); } ReportPropertyChanging("ModifiedDate");
L7-44
Customizing Entities and Building Custom Entity Classes
_ModifiedDate = StructuralObject.SetValidValue(value); ReportPropertyChanged("ModifiedDate"); } } [EdmScalarPropertyAttribute(EntityKeyProperty = false, IsNullable = false)] public Int32 CurrentPoints { get { return _CurrentPoints; } set { if (value < 0) { throw new DALValidationException("Customers cannot have a negative points balance."); } ReportPropertyChanging("CurrentPoints"); _CurrentPoints = StructuralObject.SetValidValue(value); ReportPropertyChanged("CurrentPoints"); } }
6.
Add navigation properties to link the Contact entity to the SalesOrderHeader, StoreContact, and RewardsClaimed entities. Your code should resemble the following code example.
[Visual Basic] 'TODO: Add navigation properties for the links to the other entities in the model. _ Public Property SalesOrderHeaders() As EntityCollection(Of SalesOrderHeader) Get Return DirectCast(Me, IEntityWithRelationships).RelationshipManager.GetRelatedCollection(Of SalesOrderHeader)("AdventureWorksModel.FK_SalesOrderHeader_Contact_Con tactID", "SalesOrderHeader") End Get Set(ByVal value As EntityCollection(Of SalesOrderHeader)) If (value IsNot Nothing) Then DirectCast(Me, IEntityWithRelationships).RelationshipManager.InitializeRelatedCollect
Lab 7: Customizing Entities and Building Custom Entity Classes
L7-45
ion(Of SalesOrderHeader)("AdventureWorksModel.FK_SalesOrderHeader_Contact_Con tactID", "SalesOrderHeader", value) End If End Set End Property _ Public Property StoreContacts() As EntityCollection(Of StoreContact) Get Return DirectCast(Me, IEntityWithRelationships).RelationshipManager.GetRelatedCollection(Of StoreContact)("AdventureWorksModel.FK_StoreContact_Contact_ContactID", "StoreContact") End Get Set(ByVal value As EntityCollection(Of StoreContact)) If (value IsNot Nothing) Then DirectCast(Me, IEntityWithRelationships).RelationshipManager.InitializeRelatedCollect ion(Of StoreContact)("AdventureWorksModel.FK_StoreContact_Contact_ContactID", "StoreContact", value) End If End Set End Property _ Public Property RewardsClaimed() As EntityCollection(Of RewardsClaimed) Get Return DirectCast(Me, IEntityWithRelationships).RelationshipManager.GetRelatedCollection(Of RewardsClaimed)("AdventureWorksModel.FK_ContactRewardsClaimed", "RewardsClaimed") End Get Set(ByVal value As EntityCollection(Of RewardsClaimed)) If (value IsNot Nothing) Then DirectCast(Me, IEntityWithRelationships).RelationshipManager.InitializeRelatedCollect ion(Of RewardsClaimed)("AdventureWorksModel.FK_ContactRewardsClaimed", "RewardsClaimed", value) End If End Set End Property
L7-46
Customizing Entities and Building Custom Entity Classes
[Visual C#] // Add navigation properties for the links to the other entities in the model. [EdmRelationshipNavigationProperty("AdventureWorksModel", "FK_SalesOrderHeader_Contact_ContactID", "SalesOrderHeader")] public EntityCollection SalesOrderHeaders { get { return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedCollect ion("AdventureWorksModel.FK_SalesOrderHeader_Contact _ContactID", "SalesOrderHeader"); } set { if ((value != null)) { ((IEntityWithRelationships)this).RelationshipManager.InitializeRelated Collection("AdventureWorksModel.FK_SalesOrderHeader_ Contact_ContactID", "SalesOrderHeader", value); } } } [EdmRelationshipNavigationProperty("AdventureWorksModel", "FK_StoreContact_Contact_ContactID", "StoreContact")] public EntityCollection StoreContacts { get { return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedCollect ion("AdventureWorksModel.FK_StoreContact_Contact_Contact ID", "StoreContact"); } set { if ((value != null)) { ((IEntityWithRelationships)this).RelationshipManager.InitializeRelated Collection("AdventureWorksModel.FK_StoreContact_Contact_ ContactID", "StoreContact", value); } } } [EdmRelationshipNavigationProperty("AdventureWorksModel", "FK_ContactRewardsClaimed", "RewardsClaimed")] public EntityCollection RewardsClaimed
Lab 7: Customizing Entities and Building Custom Entity Classes
L7-47
{ get { return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedCollect ion("AdventureWorksModel.FK_ContactRewardsClaimed", "RewardsClaimed"); } set { if ((value != null)) { ((IEntityWithRelationships)this).RelationshipManager.InitializeRelated Collection("AdventureWorksModel.FK_ContactRewardsClaim ed", "RewardsClaimed", value); } } }
7.
If you are using Visual C#, build the solution and correct any errors: •
On the Build menu, click Build Solution.
Task 5: Alter the AdventureWorksEDM.Designer.vb file to reflect the new Contact class (for Visual Basic only) 1.
Open the AdventureWorksEDM.Designer.vb file: •
2.
In Solution Explorer, expand AdventureWorksEDM.edmx, and then double-click AdventureWorksEDM.Designer.vb.
At the top of the file, if it is not already present, add a statement to bring the DAL namespace into scope. Your code should resemble the following code example.
[Visual Basic] Imports DAL
3.
Update code that references AdventureWorksModel.Contact to reference DAL.Contact: a.
Examine the code located in the EDM Relationship Metadata region, located near the top of the file.
b.
Update the AdventureWorksModel.Contact references to DAL.Contact.
L7-48
Customizing Entities and Building Custom Entity Classes
Your code should resemble the following code example. #Region "EDM Relationship Metadata" #End Region
4.
Build the solution and correct any errors: •
On the Build menu, click Build Solution.
Task 6: Modify the user interface to catch the validation exception 1.
In CustomerWindow.xaml or CustomerAddWindow.xaml, add the ExceptionValidationRule rule to the binding validation rules for the CurrentPoints text box to catch the validation exception: a.
If you are using Visual Basic, in Solution Explorer, right-click CustomerAddWindow.xaml, and then click Open.
Lab 7: Customizing Entities and Building Custom Entity Classes
b.
If you are using Visual C#, in Solution Explorer, right-click CustomerWindow.xaml, and then click Open.
c.
In the Design pane, click the CurrentPoints text box.
L7-49
d. In the XAML pane, locate the element for the text box. e.
Inside this element, add an element to ensure that exceptions are caught during the update of the bound property. Your code should resemble the following code example.
2.
In CustomerWindow.xaml or CustomerAddWindow.xaml, add the binding exception validation rule for the EmailAddress text box to catch the validation exception and change the Style attribute of the text box to display errors: a.
In the Design pane, click the Email text box.
b.
In the XAML pane, locate the element for the text box.
c.
Add a Style attribute to display the error.
d. Modify the element so that it contains a element to describe the validation rules that it should use. Your code should resemble the following code example.
L7-50
Customizing Entities and Building Custom Entity Classes
Task 7: Build and test the application 1.
Build the solution and correct any errors: •
2.
On the Build menu, click Build Solution.
Start the application without debugging: •
On the Debug menu, click Start Without Debugging.
3.
In the AdventureWorks Rewards window, click All Customers to load data from the entity model into the data grid. Verify that the application functions as expected.
4.
Close the application.
5.
Run all of the tests in the solution: •
On the Test menu, point to Run, and then click All Tests in Solution.
6.
Verify that all of the tests succeed.
7.
Save and close the solution, and then close Visual Studio: a.
On the File menu, click Save All.
b.
On the File menu, click Close Solution.
c.
On the File menu, click Exit.
Lab 8: Using POCO Classes with the Entity Framework
L8-1
Using POCO Classes with the Entity Framework
Lab 8: Using POCO Classes with the Entity Framework Exercise 1: Using POCO Classes Task 1: Prepare the AdventureWorks database for the lab 1.
Log on to the 10265A-GEN-DEV-08 virtual machine as Student with the password Pa$$w0rd.
2.
In the E:\Labfiles folder, run AWReset.bat: a.
Click Start, and then click Computer.
b.
Double-click Allfiles (E:), double-click Labfiles, and then double-click AWReset.bat.
c.
Wait for the batch file to finish running, and then close Windows® Explorer.
Task 2: Open the starter project for this exercise 1.
Open Microsoft® Visual Studio® 2010: •
2.
Click Start, click All Programs, click Microsoft Visual Studio 2010, and then click Microsoft Visual Studio 2010.
Open the existing solution, DAL.sln, in the E:\Labfiles\Lab08\CS\Ex1\Starter or E:\Labfiles\Lab08\VB\Ex1\Starter folder: a.
On the File menu, point to Open, and then click Project/Solution.
b.
If you are using Microsoft Visual Basic®, in the Open Project dialog box, move to the E:\Labfiles\Lab08\VB\Ex1\Starter folder, click DAL.sln, and then click Open.
c.
If you are using Microsoft Visual C#®, in the Open Project dialog box, move to the E:\Labfiles\Lab08\CS\Ex1\Starter folder, click DAL.sln, and then click Open.
L8-2
Using POCO Classes with the Entity Framework
Task 3: Disable object layer generation for your EDM 1.
Open the AdventureWorks EDM model in the ADO.NET Entity Data Model Designer (Entity Designer): •
2.
3.
In Solution Explorer, right-click AdventureWorksEDM.edmx, and then click Open.
In the AdventureWorks EDM model, set the Code Generation Strategy property to None: a.
In the Entity Designer pane, right-click the designer surface, and then click Properties.
b.
In the Properties pane, click Code Generation Strategy, and then in the drop-down list, click None.
Save the AdventureWorks EDM model: •
On the File menu, click Save AdventureWorksEDM.edmx.
Task 4: Implement a custom ObjectContext class 1.
2.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
Open the AdventureWorksContext file by double-clicking the TODO: Ex1 Add a constructor task in the task list. This task is located in the AdventureWorksContext class: •
3.
In the task list, double-click the TODO: Ex1 - Add a constructor task.
Immediately after the comment, add a no-argument constructor that enables automatic lazy loading. The constructor should invoke the base class constructor passing the strings "name=AdventureWorksEntities" and "AdventureWorksEntities" as parameters. Your code should resemble the following code example.
Lab 8: Using POCO Classes with the Entity Framework
L8-3
[Visual Basic] Public Sub New() MyBase.New("name=AdventureWorksEntities", "AdventureWorksEntities") Me.ContextOptions.LazyLoadingEnabled = True End Sub
[Visual C#] public AdventureWorksContext() : base("name=AdventureWorksEntities", "AdventureWorksEntities") { this.ContextOptions.LazyLoadingEnabled = true; }
4.
Locate the next comment in the AdventureWorksContext file by doubleclicking the TODO: Ex1 - Define the Contacts entity set task in the task list. This task is located in the AdventureWorksContext class: •
5.
In the task list, double-click the TODO: Ex1 - Define the Contacts entity set task.
Immediately after the comment, add a read-only property called Contacts based on the ObjectSet generic type. Specify Contact as the type parameter for the ObjectSet type. Create the ObjectSet object if it does not exist by calling the CreateObjectSet method in the base class, and then save the ObjectSet object in a private field. Your code should resemble the following code example.
[Visual Basic] Public ReadOnly Property Contacts As ObjectSet(Of Contact) Get If _Contacts Is Nothing Then _Contacts = MyBase.CreateObjectSet(Of Contact)("Contacts") End If Return _Contacts End Get End Property Private _Contacts As ObjectSet(Of Contact)
L8-4
Using POCO Classes with the Entity Framework
[Visual C#] public ObjectSet Contacts { get { if (_Contacts == null) { _Contacts = base.CreateObjectSet("Contacts"); } return _Contacts; } } private ObjectSet _Contacts;
6.
Locate the next comment in the AdventureWorksContext file by doubleclicking the TODO: Ex1 - Define the RewardsClaimed entity set task in the task list. This task is located in the AdventureWorksContext class: •
7.
In the task list, double-click the TODO: Ex1 - Define the RewardsClaimed entity set task.
Immediately after the comment, add a read-only property called RewardsClaimed based on the ObjectSet generic type. Specify RewardsClaimed as the type parameter for the ObjectSet type. Create the ObjectSet object if it does not exist by calling the CreateObjectSet method in the base class, and then save the ObjectSet object in a private field. Your code should resemble the following code example.
[Visual Basic] Public ReadOnly Property RewardsClaimed As ObjectSet(Of RewardsClaimed) Get If _RewardsClaimed Is Nothing Then _RewardsClaimed = MyBase.CreateObjectSet(Of RewardsClaimed)("RewardsClaimed") End If Return _RewardsClaimed End Get End Property Private _RewardsClaimed As ObjectSet(Of RewardsClaimed)
Lab 8: Using POCO Classes with the Entity Framework
L8-5
[Visual C#] public ObjectSet RewardsClaimed { get { if (_RewardsClaimed == null) { _RewardsClaimed = base.CreateObjectSet("RewardsClaimed"); } return _RewardsClaimed; } } private ObjectSet _RewardsClaimed;
8.
Locate the next comment in the AdventureWorksContext file by doubleclicking the TODO: Ex1 - Define the Rewards entity set task in the task list. This task is located in the AdventureWorksContext class: •
9.
In the task list, double-click the TODO: Ex1 - Define the Rewards entity set task.
Immediately after the comment, add a read-only property called Rewards based on the ObjectSet generic type. Specify Rewards as the type parameter for the ObjectSet type. Create the ObjectSet object if it does not exist by calling the CreateObjectSet method in the base class, and then save the ObjectSet object in a private field. Your code should resemble the following code example.
[Visual Basic] Public ReadOnly Property Rewards As ObjectSet(Of Reward) Get If _Rewards Is Nothing Then _Rewards = MyBase.CreateObjectSet(Of Reward)("Rewards") End If Return _Rewards End Get End Property Private _Rewards As ObjectSet(Of Reward)
L8-6
Using POCO Classes with the Entity Framework
[Visual C#] public ObjectSet Rewards { get { if (_Rewards == null) { _Rewards = base.CreateObjectSet("Rewards"); } return _Rewards; } } private ObjectSet _Rewards;
10. Locate the next comment in the AdventureWorksContext file by doubleclicking the TODO: Ex1 - Define the AddToContacts method task in the task list. This task is located in the AdventureWorksContext class: •
In the task list, double-click the TODO: Ex1 - Define the AddToContacts method task.
11. Immediately after the comment, add a void method called AddToContacts that takes a contact entity as a parameter. The method should call the AddObject method in the base class to add the contact entity to the contacts entity set. Your code should resemble the following code example. [Visual Basic] Public Sub AddToContacts(ByVal contact As Contact) MyBase.AddObject("Contacts", contact) End Sub
[Visual C#] public void AddToContacts(Contact contact) { base.AddObject("Contacts", contact); }
Lab 8: Using POCO Classes with the Entity Framework
L8-7
12. Locate the next comment in the AdventureWorksContext file by doubleclicking the TODO: Ex1 - Define the AddToRewards method task in the task list. This task is located in the AdventureWorksContext class: •
In the task list, double-click the TODO: Ex1 - Define the AddToRewards method task.
13. Immediately after the comment, add a void method called AddToRewards that takes a reward entity as a parameter. The method should call the AddObject method in the base class to add the reward entity to the rewards entity set. Your code should resemble the following code example. [Visual Basic] Public Sub AddToRewards(ByVal reward As Reward) MyBase.AddObject("Rewards", reward) End Sub
[Visual C#] public void AddToRewards(Reward reward) { base.AddObject("Rewards", reward); }
14. Save the AdventureWorksContext file: a.
If you are using Visual Basic, on the File menu, click Save AdventureWorksContext.vb.
b.
If you are using Visual C#, on the File menu, click Save AdventureWorksContext.cs.
Task 5: Complete the RewardsClaimed class in the AdventureWorks project 1.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
L8-8
Using POCO Classes with the Entity Framework
2.
Open the RewardsClaimed file by double-clicking the TODO: Ex1 - Add virtual public accessors for every RewardsClaimed entity property task in the task list. This task is located in the RewardsClaimed class: •
3.
In the task list, double-click the TODO: Ex1 - Add virtual public accessors for every RewardsClaimed entity property task.
Immediately after the comment, add a virtual public property for every entity property of the RewardsClaimed entity object in the Entity Data Model (EDM). Your code should resemble the following code example.
[Visual Basic] Public Overridable Public Overridable Public Overridable Public Overridable Public Overridable
[Visual C#] public virtual public virtual public virtual public virtual public virtual
4.
ClaimID As Integer PointsUsed As Integer RewardID As Integer ContactID As Integer TimeStamp As DateTime
int ClaimID { get; set; } int PointsUsed { get; set; } int RewardID { get; set; } int ContactID { get; set; } DateTime TimeStamp { get; set; }
Locate the next comment in the RewardsClaimed file by double-clicking the TODO: Ex1 - Add virtual public accessors for every RewardsClaimed navigation property task in the task list. This task is located in the RewardsClaimed class: •
5.
Property Property Property Property Property
In the task list, double-click the TODO: Ex1 - Add virtual public accessors for every RewardsClaimed navigation property task.
Immediately after the comment, add a virtual public property for every navigation property of the RewardsClaimed entity object in the EDM. Your code should resemble the following code example.
[Visual Basic] Public Overridable Property Reward As Reward Public Overridable Property Contact As Contact
Lab 8: Using POCO Classes with the Entity Framework
L8-9
[Visual C#] public virtual Reward Reward { get; set; } public virtual Contact Contact { get; set; }
6.
Save the RewardsClaimed file: a.
If you are using Visual Basic, on the File menu, click Save RewardsClaimed.vb.
b.
If you are using Visual C#, on the File menu, click Save RewardsClaimed.cs.
Task 6: Modify the data access layer to work with the new POCO classes 1.
2.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
Open the DataAccessLayer file by double-clicking the TODO: Ex1 - Add a using clause for the AdventureWorks namespace task in the task list. This task is located near the top of the DataAccessLayer file: •
3.
In the task list, double-click the TODO: Ex1 - Add a using clause for the AdventureWorks namespace task.
Immediately after the comment, add a using statement for the AdventureWorks namespace. Your code should resemble the following code example.
[Visual Basic] Imports AdventureWorks
[Visual C#] using AdventureWorks;
4.
Locate the next comment in the DataAccessLayer file by double-clicking the TODO: Ex1 - Use the custom ObjectContext class task in the task list. This task is located in the SetContext method:
L8-10
Using POCO Classes with the Entity Framework
• 5.
In the task list, double-click the TODO: Ex1 - Use the custom ObjectContext class task.
Immediately after the comment, modify the next line of code to use the AdventureWorksContext class instead of the AdventureWorksEntities class. Your code should resemble the following code example.
[Visual Basic] If entities Is Nothing Then entities = New AdventureWorksContext() End If
[Visual C#] if (entities == null) entities = new AdventureWorksContext();
6.
Locate the next comment in the DataAccessLayer file by double-clicking the TODO: Ex1 - Create a new contact by using the CreateObject method task in the task list. This task is located in the AddContact method: •
7.
In the task list, double-click the TODO: Ex1 - Create a new contact by using the CreateObject method task.
Immediately after the comment, add code that creates a new contact entity by calling the CreateObject method. Then, use the Copy method of the contact object to copy the values from the parameter passed to the AddContact method. Your code should resemble the following code example.
[Visual Basic] Dim contact As Contact = entities.CreateObject(Of Contact)() contact.Copy(pcontact)
[Visual C#] Contact contact = entities.CreateObject(); contact.Copy(pcontact);
8.
Locate the next comment in the DataAccessLayer file by double-clicking the TODO: Ex1 - Create a new reward by using the CreateObject method task in the task list. This task is located in the AddReward method:
Lab 8: Using POCO Classes with the Entity Framework
• 9.
L8-11
In the task list, double-click the TODO: Ex1 - Create a new reward by using the CreateObject method task.
Immediately after the comment, add code that creates a new reward entity by calling the CreateObject method. Then, use the Copy method of the reward object to copy the values from the parameter passed to the AddReward method. You must check the type of reward passed as a parameter to the AddReward method (AdventureWorksReward, SupermarketReward, or AirMilesReward), and then create the correct reward type. Your code should resemble the following code example.
[Visual Basic] Dim reward As Reward = Nothing If TypeOf preward Is AdventureWorksReward Then reward = entities.CreateObject(Of AdventureWorksReward)() DirectCast(reward, AdventureWorksReward).Copy(DirectCast(preward, AdventureWorksReward)) ElseIf TypeOf preward Is SupermarketReward Then reward = entities.CreateObject(Of SupermarketReward)() DirectCast(reward, SupermarketReward).Copy(DirectCast(preward, SupermarketReward)) ElseIf TypeOf preward Is AirMilesReward Then reward = entities.CreateObject(Of AirMilesReward)() DirectCast(reward, AirMilesReward).Copy(DirectCast(preward, AirMilesReward)) Else Throw New InvalidOperationException("Unrecognized Reward Type") End If
[Visual C#] Reward reward = null; if (preward is AdventureWorksReward) { reward = entities.CreateObject(); ((AdventureWorksReward)reward).Copy( (AdventureWorksReward)preward); }
L8-12
Using POCO Classes with the Entity Framework
else if (preward is SupermarketReward) { reward = entities.CreateObject(); ((SupermarketReward)reward).Copy((SupermarketReward)preward); } else if (preward is AirMilesReward) { reward = entities.CreateObject(); ((AirMilesReward)reward).Copy((AirMilesReward)preward); } else { throw new InvalidOperationException("Unrecognized Reward Type"); }
10. Locate the next comment in the DataAccessLayer file by double-clicking the TODO: Ex1 - Create a new claim by using the CreateObject method task in the task list. This task is located in the CreateRewardsClaim method: •
In the task list, double-click the TODO: Ex1 - Create a new claim by using the CreateObject method task.
11. Immediately after the comment, add code that creates a new RewardsClaimed entity by calling the CreateObject method. Then, use the Copy method of the RewardsClaimed object to copy the values from the parameter passed to the CreateRewardsClaim method. Your code should resemble the following code example. [Visual Basic] Dim claim As RewardsClaimed = entities.CreateObject(Of RewardsClaimed)() claim.Copy(pclaim)
[Visual C#] RewardsClaimed claim = entities.CreateObject(); claim.Copy(pclaim);
12. Save the DataAccessLayer file: a.
If you are using Visual Basic, on the File menu, click Save DataAccessLayer.vb.
Lab 8: Using POCO Classes with the Entity Framework
b.
L8-13
If you are using Visual C#, on the File menu, click Save DataAccessLayer.cs.
Task 7: Build and test the application 1.
Build the solution and correct any errors: •
2.
On the Build menu, click Build Solution.
Run all of the tests in the solution: •
On the Test menu, point to Run, and then click All Tests in Solution.
3.
Verify that all of the tests succeed.
4.
Start the application in Debug mode: •
5.
On the Debug menu, click Start Debugging.
In the AdventureWorks Rewards window, click All Customers to load data from the entity model into the data grid. Verify that you can add, search for, and delete customer data and that you can add and delete claim data: a.
Click Create Customer, complete all the fields on the Add new customer form with your own data. Make sure that your customer has at least 10000 points, and then click Add.
b.
In the Name text box, type the last name of the customer you added in the previous step, and then click Search.
c.
In the data grid, click the customer that you added, click Create Claim, select a reward, and then click OK.
d. Click the claim that you added in the previous step, and then click Delete Claim. e.
Click the customer that you added, and then click Delete Customer.
Note: If you try to delete a contact that has reward data, you may see an exception thrown.
6.
Close the application.
7.
Close the solution: •
On the File menu, click Close Solution.
L8-14
Using POCO Classes with the Entity Framework
8.
Reset the AdventureWorks Database. In the E:\Labfiles folder, run AWReset.bat: a.
Click Start, and then click Computer.
b.
Double-click Allfiles (E:), double-click Labfiles, and then double-click AWReset.bat.
c.
Wait for the batch file to finish running, and then close Windows Explorer.
Exercise 2: Extending Your POCO Classes Task 1: Open the starter project for this exercise •
Open the existing solution, DAL.sln, in the E:\Labfiles\Lab08\CS\Ex2\Starter or E:\Labfiles\Lab08\VB\Ex2\Starter folder: a.
On the File menu, point to Open, and then click Project/Solution.
b.
If you are using Visual Basic, in the Open Project dialog box, move to the E:\Labfiles\Lab08\VB\Ex2\Starter folder, click DAL.sln, and then click Open.
c.
If you are using Visual C#, in the Open Project dialog box, move to the E:\Labfiles\Lab08\CS\Ex2\Starter folder, click DAL.sln, and then click Open.
Task 2: Add business operations to the Contact class 1.
2.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
Open the Contact file by double-clicking the TODO: Ex2 - Create the Salt and Hash task in the task list. This task is located in the Password property: •
3.
In the task list, double-click the TODO: Ex2 - Create the Salt and Hash task.
Immediately after the comment, generate a value for the PasswordSalt property by calling the static CreateSalt method of the Hashing class with a parameter value of 5. Then, generate a value for the PasswordHash property
Lab 8: Using POCO Classes with the Entity Framework
L8-15
by calling the CreatePasswordHash method of the Hashing class, passing the password and PasswordSalt value as parameters. Your code should resemble the following code example. [Visual Basic] Me.PasswordSalt = Hashing.CreateSalt(5) Me.PasswordHash = Hashing.CreatePasswordHash(value, Me.PasswordSalt)
[Visual C#] this.PasswordSalt = Hashing.CreateSalt(5); this.PasswordHash = Hashing.CreatePasswordHash(value, this.PasswordSalt);
4.
Locate the next comment in the Contact file by double-clicking the TODO: Ex2 - Implement the AddRewardClaim method task in the task list. This task is located in the AddRewardClaim method: •
5.
In the task list, double-click the TODO: Ex2 - Implement the AddRewardClaim method task.
Immediately after the comment, add code to decrement the CurrentPoints property by the value of the PointsUsed property of the claim object, set the ModifiedDate property to the current date and time, and then add the claim object to the _rewardsClaimed list. Your code should resemble the following code example.
[Visual Basic] Me.CurrentPoints = Me.CurrentPoints - claim.PointsUsed Me.ModifiedDate = DateTime.Now Me._rewardsClaimed.Add(claim)
[Visual C#] this.CurrentPoints -= claim.PointsUsed; this.ModifiedDate = DateTime.Now; this._rewardsClaimed.Add(claim);
6.
Locate the next comment in the Contact file by double-clicking the TODO: Ex2 - Implement the RemoveRewardClaim method task in the task list. This task is located in the RemoveRewardClaim method:
L8-16
Using POCO Classes with the Entity Framework
• 7.
In the task list, double-click the TODO: Ex2 - Implement the RemoveRewardClaim method task.
Immediately after the comment, add code to increment the CurrentPoints property by the value of the PointsUsed property of the claim object, set the ModifiedDate property to the current date and time, and then add the claim object to the _rewardsClaimed list. Your code should resemble the following code example.
[Visual Basic] Me.CurrentPoints = Me.CurrentPoints + claim.PointsUsed Me.ModifiedDate = DateTime.Now Me._rewardsClaimed.Remove(claim)
[Visual C#] this.CurrentPoints += claim.PointsUsed; this.ModifiedDate = DateTime.Now; this._rewardsClaimed.Remove(claim);
8.
Save the Contact file: a.
If you are using Visual Basic, on the File menu, click Save Contact.vb.
b.
If you are using Visual C#, on the File menu, click Save Contact.cs.
Task 3: Add a business operation to the RewardsClaimed class 1.
2.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
Open the RewardsClaimed file by double-clicking the TODO: Ex2 Implement the ModifyClaim method task in the task list. This task is located in the ModifyClaim method: •
3.
In the task list, double-click the TODO: Ex2 - Implement the ModifyClaim method task.
Immediately after the comment, add code to perform the following tasks: a.
Increment the CurrentPoints property of the Contact property by the value of the PointsUsed property of the current claim object.
Lab 8: Using POCO Classes with the Entity Framework
L8-17
b.
Decrement the CurrentPoints property of the Contact property by the value of the pointsUsed parameter.
c.
Assign the rewardID parameter to the RewardID property.
d. Assign the pointsUsed parameter to the PointsUsed property. Your code should resemble the following code example. [Visual Basic] Me.Contact.CurrentPoints = Me.Contact.CurrentPoints + Me.PointsUsed Me.Contact.CurrentPoints = Me.Contact.CurrentPoints - pointsUsed Me.RewardID = rewardID Me.PointsUsed = pointsUsed
[Visual C#] this.Contact.CurrentPoints += this.PointsUsed; this.Contact.CurrentPoints -= pointsUsed; this.RewardID = rewardID; this.PointsUsed = pointsUsed;
4.
Save the RewardsClaimed file: a.
If you are using Visual Basic, on the File menu, click Save RewardsClaimed.vb.
b.
If you are using Visual C#, on the File menu, click Save RewardsClaimed.cs.
Task 4: Modify the data access layer to work with your new POCO entities 1.
2.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
Open the DataAccessLayer file by double-clicking the TODO: Ex2 - Delete the call to the EncryptPassword method task in the task list. This task is located in the AddContact method: •
In the task list, double-click the TODO: Ex2 - Delete the call to the EncryptPassword method task.
L8-18
Using POCO Classes with the Entity Framework
3.
The Contact class now handles password encryption. Delete the line of code after the comment that calls the EncryptPassword method. Your code should resemble the following code example.
[Visual Basic] ' TODO: Ex2 - Delete the call to the EncryptPassword method. ' Encrypt the password in the detached object. ' Set the time modified. contact.ModifiedDate = DateTime.Now
[Visual C#] // TODO: Ex2 - Delete the call to the EncryptPassword method. // Encrypt the password in the detached object. // Set the time modified. contact.ModifiedDate = DateTime.Now;
4.
Locate the next comment in the DataAccessLayer file by double-clicking the TODO: Ex2 - Remove the EncryptPassword method task in the task list. This task is located in the DataAccessLayer class: •
5.
In the task list, double-click the TODO: Ex2 - Remove the EncryptPassword method task.
The password encryption functionality is now in the AdventureWorks project. Delete the whole of the EncryptPassword method from the DataAccessLayer class. Your code should resemble the following code example.
[Visual Basic] ' TODO: Ex2 - Remove the EncryptPassword method. #End Region
[Visual C#] // TODO: Ex2 - Remove the EncryptPassword method. #endregion
Lab 8: Using POCO Classes with the Entity Framework
6.
Locate the next comment in the DataAccessLayer file by double-clicking the TODO: Ex2 - Make sure that all the claims are loaded task in the task list. This task is located in the DeleteContact method: •
7.
L8-19
In the task list, double-click the TODO: Ex2 - Make sure that all the claims are loaded task.
The new plain-old CLR object (POCO) classes do not support automatic lazy loading. Immediately after the comment, add code to load all of the claims that are related to the contact by using the LoadProperty method.
Note: If you are using Visual Basic, refer to the RewardsClaimed navigation property by name by supplying a string as a parameter. If you are using Visual C#, you can use a lambda that identifies the objects in the RewardsClaimed property for the contact.
Your code should resemble the following code example. [Visual Basic] entities.LoadProperty(DirectCast(contactToDelete, Contact), "RewardsClaimed")
[Visual C#] entities.LoadProperty((Contact)contactToDelete, c => c.RewardsClaimed);
8.
Locate the next comment in the DataAccessLayer file by double-clicking the TODO: Ex2 - Load the contact and then call the AddRewardClaim method task in the task list. This task is located in the CreateRewardsClaim method: •
9.
In the task list, double-click the TODO: Ex2 - Load the contact and then call the AddRewardClaim method task.
Immediately after the comment, add code to perform the following tasks: a.
Create an EntityKey object for the contact associated with the claim.
b.
Use the TryGetObjectByKey method to load the contact entity.
c.
Use the AddRewardClaim method to add the claim to the contact.
Your code should resemble the following code example.
L8-20
Using POCO Classes with the Entity Framework
[Visual Basic] ' Get the entity key you need. Dim key As New EntityKey("AdventureWorksEntities.Contacts", "ContactID", claim.ContactID) Dim contact As Object = Nothing ' Make sure that the entity to modify is loaded. If entities.TryGetObjectByKey(key, contact) Then DirectCast(contact, Contact).AddRewardClaim(claim) End If
[Visual C#] // Get the entity key you need. EntityKey key = new EntityKey("AdventureWorksEntities.Contacts", "ContactID", claim.ContactID); object contact = null; // Make sure that the entity to modify is loaded. if (entities.TryGetObjectByKey(key, out contact)) { ((Contact)contact).AddRewardClaim(claim); }
10. Locate the next comment in the DataAccessLayer file by double-clicking the TODO: Ex2 - Remove the claim before you refresh the contact task in the task list. This task is located in the CreateRewardsClaim method: •
In the task list, double-click the TODO: Ex2 - Remove the claim before you refresh the contact task.
11. Immediately after the comment, add code to remove the claim from the contact by calling the RemoveRewardClaim method. Your code should resemble the following code example. [Visual Basic] claim.Contact.RemoveRewardClaim(claim)
[Visual C#] claim.Contact.RemoveRewardClaim(claim);
Lab 8: Using POCO Classes with the Entity Framework
L8-21
12. Locate the next comment in the DataAccessLayer file by double-clicking the TODO: Ex2 - Use the AddRewardClaim method task in the task list. This task is located in the CreateRewardsClaim method: •
In the task list, double-click the TODO: Ex2 - Use the AddRewardClaim method task.
13. Immediately after the comment, add code to add the claim to the contact by calling the AddRewardClaim method on the Contact property of the claim variable. Your code should resemble the following code example. [Visual Basic] claim.Contact.AddRewardClaim(claim)
[Visual C#] claim.Contact.AddRewardClaim(claim);
14. Locate the next comment in the DataAccessLayer file by double-clicking the TODO: Ex2 - Use the ModifyClaim business method task in the task list. This task is located in the UpdateRewardsClaim method: •
In the task list, double-click the TODO: Ex2 - Use the ModifyClaim business method task.
15. Immediately after the comment, add code to call the ModifyClaim method. Your code should resemble the following code example. [Visual Basic] rewardClaimToModify.ModifyClaim(rewardClaim.RewardID, rewardClaim.PointsUsed)
[Visual C#] rewardClaimToModify.ModifyClaim(rewardClaim.RewardID, rewardClaim.PointsUsed);
16. Locate the next comment in the DataAccessLayer file by double-clicking the TODO: Ex2 - Give the original points back to the Contact task in the task list. This task is located in the UpdateRewardsClaim method:
L8-22
Using POCO Classes with the Entity Framework
•
In the task list, double-click the TODO: Ex2 - Give the original points back to the Contact task.
17. Immediately after the comment, add code to call the ModifyClaim method, passing the originalPoints variable as the second parameter. Your code should resemble the following code example. [Visual Basic] rewardClaimToModify.ModifyClaim(rewardClaim.RewardID, originalPoints)
[Visual C#] rewardClaimToModify.ModifyClaim(rewardClaim.RewardID, originalPoints);
18. Locate the next comment in the DataAccessLayer file by double-clicking the TODO: Ex2 - Use the ModifyClaim method to give the points to the contact task in the task list. This task is located in the UpdateRewardsClaim method: •
In the task list, double-click the TODO: Ex2 - Use the ModifyClaim method to give the points to the contact task.
19. Immediately after the comment, add code to call the ModifyClaim method, passing the RewardID property of the rewardClaim object as the first parameter and the PointsUsed property of the rewardClaim object as the second parameter. Your code should resemble the following code example. [Visual Basic] rewardClaimToModify.ModifyClaim(rewardClaim.RewardID, rewardClaim.PointsUsed)
[Visual C#] rewardClaimToModify.ModifyClaim(rewardClaim.RewardID, rewardClaim.PointsUsed);
20. Locate the next comment in the DataAccessLayer file by double-clicking the TODO: Ex2 - Use the RemoveRewardClaim method task in the task list. This task is located in the DeleteRewardsClaim method: •
In the task list, double-click the TODO: Ex2 - Use the RemoveRewardClaim method task.
Lab 8: Using POCO Classes with the Entity Framework
L8-23
21. Immediately after the comment, add code to call the RemoveRewardClaim method of the relatedContact object, passing the rewardClaimToDelete object as a parameter. Your code should resemble the following code example. [Visual Basic] relatedContact.RemoveRewardClaim(rewardClaimToDelete)
[Visual C#] relatedContact.RemoveRewardClaim(rewardClaimToDelete);
22. Locate the next comment in the DataAccessLayer file by double-clicking the TODO: Ex2 - Add the claim back while you refresh the contact task in the task list. This task is located in the DeleteRewardsClaim method: •
In the task list, double-click the TODO: Ex2 - Add the claim back while you refresh the contact task.
23. Immediately after the comment, add code to call the AddRewardClaim method of the relatedContact object, passing the rewardClaimToDelete object as a parameter. Your code should resemble the following code example. [Visual Basic] relatedContact.AddRewardClaim(rewardClaimToDelete)
[Visual C#] relatedContact.AddRewardClaim(rewardClaimToDelete);
24. Locate the next comment in the DataAccessLayer file by double-clicking the TODO: Ex2 - Use the RemoveRewardClaim method again task in the task list. This task is located in the DeleteRewardsClaim method: •
In the task list, double-click the TODO: Ex2 - Use the RemoveRewardClaim method again task.
25. Immediately after the comment, add code to call the RemoveRewardClaim method of the relatedContact object, passing the rewardClaimToDelete object as a parameter. Your code should resemble the following code example.
L8-24
Using POCO Classes with the Entity Framework
[Visual Basic] relatedContact.RemoveRewardClaim(rewardClaimToDelete)
[Visual C#] relatedContact.RemoveRewardClaim(rewardClaimToDelete);
26. Save the DataAccessLayer file: a.
If you are using Visual Basic, on the File menu, click Save DataAccessLayer.vb.
b.
If you are using Visual C#, on the File menu, click Save DataAccessLayer.cs.
Task 5: Build and test the application 1.
Build the solution and correct any errors: •
2.
On the Build menu, click Build Solution.
Run all of the tests in the solution: •
On the Test menu, point to Run, and then click All Tests in Solution.
3.
Verify that all of the tests succeed.
4.
Start the application in Debug mode: •
5.
On the Debug menu, click Start Debugging.
In the AdventureWorks Rewards window, click All Customers to load data from the entity model into the data grid. Verify that you can add, modify, and delete contact data. Verify that you can add, modify, and delete Adventure Works reward data: a.
Click Create Customer, complete all the fields on the Add new customer form with your own data (note that the password field shows blank when you move to another field). Make sure that your customer has at least 10000 points, and then click Add.
b.
Use the Search button to find the customer you added in the previous step.
c.
Click Create Claim, select a reward, and then click OK.
Lab 8: Using POCO Classes with the Entity Framework
L8-25
d. Use the Delete Claim button to delete the claim you added in the previous step. e.
Use the Delete Customer button to delete the customer you added in step a.
Note: If you try to delete a contact that has reward data, you will see an exception thrown.
6.
Close the application.
7.
Close the solution, and then close Visual Studio: a.
On the File menu, click Close Solution.
b.
On the File menu, click Exit.
Lab 9: Building an N-Tier Solution by Using the Entity Framework
L9-1
Building an N-Tier Solution by Using the Entity Framework
Lab 9: Building an N-Tier Solution by Using the Entity Framework Exercise 1: Creating the Contacts and Orders Data Access Tier Task 1: Prepare the environment for the lab 1.
Log on to the 10265A-GEN-DEV-09 virtual machine as Student with the password Pa$$w0rd.
2.
In the E:\Labfiles folder, run EnvSetup.bat as an administrator. This file configures Internet Information Services(IIS) and creates the required users and groups: a.
Click Start, and then click Computer.
b.
Double-click Allfiles (E:), and then double-click Labfiles.
c.
Right-click EnvSetup.bat, and then click Run as administrator.
d. In the User Account Control dialog box, in the Password box, type Pa$$w0rd and then click Yes. e.
Wait for the batch file to finish running.
Task 2: Prepare the AdventureWorks database for the lab •
In the E:\Labfiles folder, run AWReset.bat: a.
In the E:\Labfiles folder, double-click AWReset.bat.
b.
Wait for the batch file to finish running.
Task 3: Open the starter project 1.
In the E:\Labfiles\Lab09\VB\Ex1\Starter folder (if you are using Microsoft® Visual Basic®), or E:\Labfiles\Lab09\CS\Ex1\Starter folder (if you are using
L9-2
Building an N-Tier Solution by Using the Entity Framework
Microsoft Visual C#®), run ExSetup.bat as an administrator. This script adds the required virtual directories to IIS: a.
If you are using Visual Basic, in Windows® Explorer, double-click Lab09, double-click VB, double-click Ex1, and then double-click Starter.
b.
If you are using Visual C#, in Windows Explorer, double-click Lab09, double-click CS, double-click Ex1, and then double-click Starter.
c.
Right-click ExSetup.bat, and then click Run as administrator.
d. In the User Account Control dialog box, in the Password box, type Pa$$w0rd and then click Yes. e. 2.
3.
Wait for the batch file to finish running, and then close Windows Explorer.
Open Microsoft Visual Studio® 2010 as an administrator: a.
Click Start, point to All Programs, click Microsoft Visual Studio 2010, right-click Microsoft Visual Studio 2010, and then click Run as administrator.
b.
In the User Account Control dialog box, in the Password box, type Pa$$w0rd and then click Yes.
Open the existing solution, OrdersDAL.sln, in the E:\Labfiles\Lab09\VB\Ex1\Starter\OrdersDAL or E:\Labfiles\Lab09\CS\Ex1\Starter\OrdersDAL folder: a.
On the File menu, point to Open, and then click Project/Solution.
b.
If you are using Visual Basic, in the Open Project dialog box, move to the E:\Labfiles\Lab09\VB\Ex1\Starter\OrdersDAL folder, click OrdersDAL.sln, and then click Open.
c.
If you are using Visual C#, in the Open Project dialog box, move to the E:\Labfiles\Lab09\CS\Ex1\Starter\OrdersDAL folder, click OrdersDAL.sln, and then click Open.
Task 4: Create the OrdersDAL class library 1.
Add a new Class Library project named OrdersDAL to the solution: a.
On the File menu, point to Add, and then click New Project.
b.
If you are using Visual Basic, in the Add New Project dialog box, in the Installed Templates list, click Visual Basic.
Lab 9: Building an N-Tier Solution by Using the Entity Framework
c.
L9-3
If you are using Visual C#, in the Add New Project dialog box, in the Installed Templates list, click Visual C#.
d. In the list of templates, click Class Library. e. 2.
3.
In the Name box, type OrdersDAL and then click OK.
Delete the Class1 file: a.
If you are using Visual Basic, in Solution Explorer, right-click Class1.vb, and then click Delete.
b.
If you are using Visual C#, in Solution Explorer, right-click Class1.cs, and then click Delete.
c.
In the Microsoft Visual Studio dialog box, click OK.
Add a new ADO.NET Entity Data Model (EDM) named AdventureWorksModel to the OrdersDAL project. Generate the EDM from the AdventureWorks Microsoft SQL Server® database and create entities for the Contact, SalesOrderHeader, and SalesOrderDetail tables: a.
In Solution Explorer, right-click OrdersDAL, point to Add, and then click New Item.
b.
In the Add New Item - OrdersDAL dialog box, in the list of templates, click ADO.NET Entity Data Model, in the Name box, type AdventureWorksModel.edmx and then click Add.
c.
In the Entity Data Model Wizard, on the Choose Model Contents page, click Generate from database, and then click Next.
d. On the Choose Your Data Connection page, click New Connection. e.
In the Choose Data Source dialog box, in the Data source list, click Microsoft SQL Server, and then click Continue.
f.
In the Connection Properties dialog box, in the Server name box, type 10265A-GEN-DEV\SQLExpress
g.
In the Select or enter a database name box, enter AdventureWorks, and then click OK.
h.
On the Choose Your Data Connection page, click Next.
i.
On the Choose Your Database Objects page, expand Tables, select the Contact (Person), SalesOrderHeader (Sales), and SalesOrderDetail (Sales) check boxes, and then click Finish.
L9-4
Building an N-Tier Solution by Using the Entity Framework
4.
Modify the EDM to generate self-tracking entities by adding the ADO.NET SelfTracking Entity Generator code generation item to the EDM. Name the code generation item AdventureWorksModel.tt: a.
In the AdventureWorksModel.edmx window, right-click the design surface, and then click Add Code Generation Item.
b.
In the Add New Item – OrdersDAL dialog box, in the templates list, click ADO.NET Self-Tracking Entity Generator.
c.
In the Name box, type AdventureWorksModel.tt and then click Add.
d. In the Security Warning dialog box, click OK. 5.
Build the OrdersDAL project and correct any errors:
Important: Only build the OrdersDAL project. The OrderManagement project will not build successfully because it is not yet complete.
•
In Solution Explorer, right-click OrdersDAL, and then click Build. Correct any errors.
Task 5: Create the OrdersClientLibrary class library 1.
2.
3.
Add a new Class Library project named OrdersClientLibrary to the solution: a.
On the File menu, point to Add, and then click New Project.
b.
In the Add New Project dialog box, in the templates list, click Class Library.
c.
In the Name box, type OrdersClientLibrary and then click OK.
Delete the Class1 file: a.
If you are using Visual Basic, in Solution Explorer, right-click Class1.vb, and then click Delete.
b.
If you are using Visual C#, in Solution Explorer, right-click Class1.cs, and then click Delete.
c.
In the Microsoft Visual Studio dialog box, click OK.
Add a reference to the System.Runtime.Serialization assembly: a.
In Solution Explorer, right-click OrdersClientLibrary, and then click Add Reference.
Lab 9: Building an N-Tier Solution by Using the Entity Framework
b. 4.
L9-5
In the Add Reference dialog box, on the .NET tab, click System.Runtime.Serialization, and then click OK.
If you are using Visual Basic, change the Root namespace property of the OrdersClientLibrary project to OrdersDAL. a.
In Solution Explorer, right-click OrdersClientLibrary, and then click Properties.
b.
In the OrdersClientLibary window, click the Application tab.
c.
In the Root namespace box, delete the text OrdersClientLibrary, and type OrdersDAL
d. On the File menu, click Close. 5.
6.
7.
8.
Move the AdventureWorksModel.tt file to the OrdersClientLibrary project: a.
In Solution Explorer, expand the OrdersDAL project, right-click AdventureWorksModel.tt, and then click Cut.
b.
In Solution Explorer, right-click the OrdersClientLibrary project, and then click Paste.
Add a reference to the OrdersClientLibrary assembly to the OrdersDAL project: a.
In Solution Explorer, right-click OrdersDAL, and then click Add Reference.
b.
In the Add Reference dialog box, on the Projects tab, click OrdersClientLibrary, and then click OK.
Add a class named AdditionalMethods to the OrdersClientLibrary project: a.
In Solution Explorer, right-click the OrdersClientLibrary project, point to Add, and then click Class.
b.
In the Add New Item - OrdersClientLibrary dialog box, in the list of templates, click Class.
c.
In the Name box, type AdditionalMethods and then click Add.
In the AdditionalMethods class, perform the following tasks: a.
Remove the AdditionalMethods class declaration.
b.
If you are using Visual C#, change the namespace to OrdersDAL.
c.
Create a public partial class named SalesOrderHeader.
d. Create a public partial class named SalesOrderDetail.
L9-6
Building an N-Tier Solution by Using the Entity Framework
Your code should resemble the following code example. [Visual Basic] Public Partial Class SalesOrderHeader End Class Public Partial Class SalesOrderDetail End Class
[Visual C#] namespace OrdersDAL { public partial class SalesOrderHeader { } public partial class SalesOrderDetail { } }
9.
In the partial SalesOrderHeader class, add code to overwrite the ToString method by returning a string that contains the SalesOrderID, ContactID, AccountNumber, OrderDate, PurchaseOrderNumber, and TotalDue properties from the current object.
Your code should resemble the following code example. [Visual Basic] Partial Public Class SalesOrderHeader Public Overrides Function ToString() As String Return String.Format("Order: {0}" & vbTab & "Contact: {1}" & vbTab & "Account: {2}" & vbTab & "Date: {3}" & vbTab & "PO: {4}" & vbTab & "Total: {5}", Me.SalesOrderID, Me.ContactID, Me.AccountNumber,
Lab 9: Building an N-Tier Solution by Using the Entity Framework
Me.OrderDate, Me.PurchaseOrderNumber, Me.TotalDue) End Function End Class
[Visual C#] public partial class SalesOrderHeader { public override string ToString() { return string.Format("Order: {0}\tContact: {1}\tAccount: {2}\tDate: {3}\tPO: {4}\tTotal: {5}", this.SalesOrderID, this.ContactID, this.AccountNumber, this.OrderDate, this.PurchaseOrderNumber, this.TotalDue); } }
10. In the partial SalesOrderDetail class, add code to overwrite the ToString method by returning the ProductID, OrderQty, UnitPrice, UnitPriceDiscount, and LineTotal properties of the current object. Your code should resemble the following code example. [Visual Basic] Partial Public Class SalesOrderDetail Public Overrides Function ToString() As String Return String.Format("Product: {0}" + vbTab & "Qty: {1}" & vbTab & "Price: {2}" & vbTab & "Discount: {3}" & vbTab & "Line Cost: {4}", Me.ProductID, Me.OrderQty, Me.UnitPrice, Me.UnitPriceDiscount, Me.LineTotal) End Function End Class
[Visual C#] public partial class SalesOrderDetail { public override string ToString() {
L9-7
L9-8
Building an N-Tier Solution by Using the Entity Framework
return string.Format("Product: {0}\tQty: {1}\tPrice: {2}\tDiscount:{3}\tLine Cost: {4}", this.ProductID, this.OrderQty, this.UnitPrice, this.UnitPriceDiscount, this.LineTotal); } }
11. Build the OrdersClientLibrary project and correct any errors: Important: Only build the OrdersClientLibrary project. The OrderManagement project will not build successfully because it is not yet complete.
•
In Solution Explorer, right-click OrdersClientLibrary, and then click Build. Correct any errors.
Task 6: Create the IOrdersService interface 1.
2.
3.
4.
Add a new Class Library project named OrdersService to the solution: a.
On the File menu, point to Add, and then click New Project.
b.
In the Add New Project dialog box, in the list of templates, click Class Library.
c.
In the Name box, type OrdersService and then click OK.
Delete the Class1 file: a.
If you are using Visual Basic, in Solution Explorer, right-click Class1.vb, and then click Delete.
b.
If you are using Visual C#, in Solution Explorer, right-click Class1.cs, and then click Delete.
c.
In the Microsoft Visual Studio dialog box, click OK.
Create an interface named IOrdersService in the OrdersService project: a.
In Solution Explorer, right-click OrdersService, point to Add, and then click New Item.
b.
In the Add New Item - OrdersService dialog box, in the templates list, click Interface.
c.
In the Name box, type IOrdersService and then click Add.
Add a reference to the OrdersClientLibrary assembly:
Lab 9: Building an N-Tier Solution by Using the Entity Framework
5.
6.
7.
8.
9.
a.
In Solution Explorer, right-click OrdersService, and then click Add Reference.
b.
In the Add Reference dialog box, on the Projects tab, click OrdersClientLibrary, and then click OK.
L9-9
Add a reference to the OrdersDAL assembly: a.
In Solution Explorer, right-click OrdersService, and then click Add Reference.
b.
In the Add Reference dialog box, on the Projects tab, click OrdersDAL, and then click OK.
Add a reference to the System.ServiceModel assembly: a.
In Solution Explorer, right-click OrdersService, and then click Add Reference.
b.
In the Add Reference dialog box, on the .NET tab, click System.ServiceModel, and then click OK.
Add a reference to the System.Runtime.Serialization assembly: a.
In Solution Explorer, right-click OrdersService, and then click Add Reference.
b.
In the Add Reference dialog box, on the .NET tab, click System.Runtime.Serialization, and then click OK.
Add a reference to the System.Data.Entity assembly: a.
In Solution Explorer, right-click OrdersService, and then click Add Reference.
b.
In the Add Reference dialog box, on the .NET tab, click System.Data.Entity, and then click OK.
In the IOrdersService code file, add code to bring the System.Runtime.Serialization, System.ServiceModel, and OrdersDAL namespaces into scope. Your code should resemble the following code example.
[Visual Basic] Imports System.Runtime.Serialization Imports System.ServiceModel Imports OrdersDAL
L9-10
Building an N-Tier Solution by Using the Entity Framework
Public Interface IOrdersService End Interface
[Visual C#] using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Runtime.Serialization; using System.ServiceModel; using OrdersDAL; namespace OrdersService { interface IOrdersService { } }
10. In the IOrdersService file, before the interface declaration, write code to perform the following tasks: a.
Add a new class called ServiceFault. Prefix the class with the DataContract attribute.
b.
Add a public string field to the ServiceFault class called ExceptionType. Prefix the field with the DataMember attribute.
c.
Add a public string field to the ServiceFault class called ExceptionMessage. Prefix the field with the DataMember attribute.
Your code should resemble the following code example. [Visual Basic] Public Class ServiceFault Public ExceptionType As String Public ExceptionMessage As String End Class
Lab 9: Building an N-Tier Solution by Using the Entity Framework
L9-11
[Visual C#] [DataContract] public class ServiceFault { [DataMember] public string ExceptionType; [DataMember] public string ExceptionMessage; }
11. In the IOrdersService file, perform the following tasks: a.
Make the IOrdersService interface public (if it is not already public).
b.
Specify that the IOrdersService interface defines a service contract called OrdersWebService. Use http://microsoft.com as the Web service namespace.
Your code should resemble the following code example. [Visual Basic] Public Interface IOrdersService End Interface
[Visual C#] [ServiceContract (Name="OrdersWebService", Namespace="http://microsoft.com")] public interface IOrdersService { }
12. In the IOrdersService interface, perform the following tasks: a.
Define a method called GetContactDetails that returns an object of type Contact and accepts an integer named contactID as a parameter.
b.
Specify that the GetContactDetails method is Web service operation that is part of the OrdersWebService service contract.
L9-12
Building an N-Tier Solution by Using the Entity Framework
c.
Specify that the GetContactDetails method returns a message of type ServiceFault if an exception occurs.
Your code should resemble the following code example. [Visual Basic] Public Interface IOrdersService Function GetContactDetails(ByVal contactID As Integer) As Contact End Interface
[Visual C#] public interface IOrdersService { [FaultContract(typeof(ServiceFault))] [OperationContract] Contact GetContactDetails(int contactID); }
13. In the IOrdersService interface, perform the following tasks: a.
Define a method called GetAllContactsInRange that returns a generic IEnumerable object of with a type parameter of Contact, and accepts an integer named lowerBound and an integer named upperBound as parameters.
b.
Specify that the GetAllContactsInRange method is Web service operation that is part of the OrdersWebService service contract.
c.
Specify that the GetAllContactsInRange method returns a message of type ServiceFault if an exception occurs.
Your code should resemble the following code example. [Visual Basic] Public Interface IOrdersService ...
Lab 9: Building an N-Tier Solution by Using the Entity Framework
L9-13
Function GetAllContactsInRange(ByVal lowerBound As Integer, ByVal upperBound As Integer) _ As IEnumerable(Of Contact) End Interface
[Visual C#] public interface IOrdersService { ... [FaultContract(typeof(ServiceFault))] [OperationContract] IEnumerable GetAllContactsInRange( int lowerBound, int upperBound); }
14. In the IOrdersService interface, perform the following tasks: a.
Define a method called GetOrderDetails that returns an object of type SalesOrderHeader and accepts an integer named orderID as a parameter.
b.
Specify that the GetOrderDetails method is a Web service operation that is part of the OrdersWebService service contract.
c.
Specify that the GetOrderDetails method returns a message of type ServiceFault if an exception occurs.
Your code should resemble the following code example. [Visual Basic] Public Interface IOrdersService ... Function GetOrderDetails(ByVal orderID As Integer) _ As SalesOrderHeader End Interface
L9-14
Building an N-Tier Solution by Using the Entity Framework
[Visual C#] public interface IOrdersService { ... [FaultContract(typeof(ServiceFault))] [OperationContract] SalesOrderHeader GetOrderDetails(int orderID); }
15. In the IOrdersService interface, perform the following tasks: a.
Define a method called GetOrdersForContact that returns a generic IEnumerable object of with a type parameter of SalesOrderHeader and accepts an integer named contactID as a parameter.
b.
Specify that the GetOrdersForContact method is a Web service operation that is part of the OrdersWebService service contract.
c.
Specify that the GetOrdersForContact method returns a message of type ServiceFault if an exception occurs.
Your code should resemble the following code example. [Visual Basic] Public Interface IOrdersService ... Function GetOrdersForContact(ByVal contactID As Integer) _ As IEnumerable(Of SalesOrderHeader) End Interface
[Visual C#] public interface IOrdersService { ... [FaultContract(typeof(ServiceFault))] [OperationContract] IEnumerable GetOrdersForContact(int contactID); }
16. In the IOrdersService interface, perform the following tasks:
Lab 9: Building an N-Tier Solution by Using the Entity Framework
L9-15
a.
Define a method called GetOrdersForProduct that returns a generic IEnumerable object of with a type parameter of SalesOrderHeader and accepts an integer named productID as a parameter.
b.
Specify that the GetOrdersForProduct method is a Web service operation that is part of the OrdersWebService service contract.
c.
Specify that the GetOrdersForProduct method returns a message of type ServiceFault if an exception occurs.
Your code should resemble the following code example. [Visual Basic] Public Interface IOrdersService ... Function GetOrdersForProduct(ByVal productID As Integer) _ As IEnumerable(Of SalesOrderHeader) End Interface
[Visual C#] public interface IOrdersService { ... [FaultContract(typeof(ServiceFault))] [OperationContract] IEnumerable GetOrdersForProduct(int productID); }
17. In the IOrdersService interface, perform the following tasks: a.
Define a method called GetAllOrdersInRange that returns a generic IEnumerable object with a type parameter of SalesOrderHeader and accepts an integer named lowerBound and an integer named upperBound as parameters.
b.
Specify that the GetAllOrdersInRange method is a Web service operation that is part of the OrdersWebService service contract.
c.
Specify that the GetAllOrdersInRange method returns a message of type ServiceFault if an exception occurs.
L9-16
Building an N-Tier Solution by Using the Entity Framework
Your code should resemble the following code example. [Visual Basic] Public Interface IOrdersService ... Function GetAllOrdersInRange(ByVal lowerBound As Integer, ByVal upperBound As Integer) _ As IEnumerable(Of SalesOrderHeader) End Interface
[Visual C#] public interface IOrdersService { ... [FaultContract(typeof(ServiceFault))] [OperationContract] IEnumerable GetAllOrdersInRange( int lowerBound, int upperBound); }
18. Build the project and correct any errors: Important: Only build the OrdersService project. The OrderManagement project will not build successfully because it is not yet complete.
•
In Solution Explorer, right-click OrdersService, and then click Build. Correct any errors.
Task 7: Implement exception handling and logging 1.
Create a class named OrdersServiceImpl in the OrdersService project: In Solution Explorer, right-click OrdersService, point to Add, and then click Class. In the Add New Item - OrdersService dialog box, in the list of templates, click Class.
Lab 9: Building an N-Tier Solution by Using the Entity Framework
L9-17
In the Name box, type OrdersServiceImpl and then click Add. 2.
In the OrdersServiceImpl code file, write code to bring the following namespaces into scope: •
System.Text
Note: If you are using Visual C#, the System.Text namespace is already in scope and there is no need to add it again.
•
System.ServiceModel
•
System.Diagnostics
•
System.Threading
•
OrdersDAL
•
System.Security.Permissions
Your completed code should resemble the following code example. [Visual Imports Imports Imports Imports Imports Imports
Basic] System.Text System.ServiceModel System.Diagnostics System.Threading OrdersDAL System.Security.Permissions
...
[Visual C#] using System; using System.Collections.Generic; using System.Linq; using System.Text using using using using using ...
System.ServiceModel; System.Diagnostics; System.Threading; OrdersDAL; System.Security.Permissions;
L9-18
Building an N-Tier Solution by Using the Entity Framework
3.
In the OrdersServiceImpl code file, perform the following tasks: a.
Prefix the OrdersWebService class with the ServiceBehavior attribute, specify the name of the service as OrdersWebService, and set the namespace to http://microsoft.com.
b.
Set the InstanceContextMode property of the ServiceBehavior attribute to PerCall.
c.
Set the ConcurrencyMode property of the ServiceBehavior attribute to Multiple.
d. Declare the OrdersServiceImpl class as a public class (if it is not already public) that implements the IOrdersService interface. Your completed code should resemble the following code example. [Visual Basic] Public Class OrdersServiceImpl Implements IOrdersService End Class
[Visual C#] [ServiceBehavior(Name ="OrdersWebService", Namespace = "http://microsoft.com", InstanceContextMode = InstanceContextMode.PerCall, ConcurrencyMode = ConcurrencyMode.Multiple)] public class OrdersServiceImpl : IOrdersService { }
4.
In the OrdersServiceImpl class, write code to perform the following tasks: a.
Declare a constant integer named maxContactsCount and assign it a value of 500.
b.
Declare a constant integer named maxOrdersCount and assign it a value of 400.
Lab 9: Building an N-Tier Solution by Using the Entity Framework
c.
L9-19
Declare a constant string named eventSource and assign it the value "Orders Service".
d. Declare a constant string named eventLog and assign it the value "Application". Your code should resemble the following code example. [Visual Basic] Public Class OrdersServiceImpl Implements IOrdersService Private Const maxContactsCount As Integer = 500 Private Const maxOrdersCount As Integer = 400 Private Const eventSource As String = "Orders Service" Private Const eventLog As String = "Application" End Class
[Visual C#] public class OrdersServiceImpl : IOrdersService { private const int maxContactsCount = 500; private const int maxOrdersCount = 400; private const string eventSource = "Orders Service"; private const string eventLog = "Application"; }
5.
In the OrdersServiceImpl class, write code to perform the following tasks: a.
Create a new private method named logException that accepts an Exception object named ex and a string object named eventName as parameters. This method should not return a value.
b.
In the logException method, verify that the event source identified by the eventSource constant exists by calling the SourceExists method of the EventLog class in the System.Diagnostics namespace. If the event source does not exist, create it by calling the CreateEventSource method of the EventLog class, specifying the eventSource and eventLog constants as parameters.
L9-20
Building an N-Tier Solution by Using the Entity Framework
c.
Declare a string named eventMessage and assign it a value by combining the eventName variable, the Message property of the ex variable, and the value of the Thread.CurrentPrincipal.Identity.Name property.
d. Call the WriteEntry method of the EventLog class, passing the eventSource and eventMessage variables as parameters and specifying Error as the EventLogEntryType parameter. Your code should resemble the following code example. [Visual Basic] Private Sub logException(ByVal ex As Exception, ByVal eventName As String) If Not System.Diagnostics.EventLog.SourceExists(eventSource) Then System.Diagnostics.EventLog.CreateEventSource( eventSource, eventLog) End If Dim eventMessage As String = String.Format("{0}: {1}: {2}:", eventName, ex.Message, Thread.CurrentPrincipal.Identity.Name) System.Diagnostics.EventLog.WriteEntry(eventSource, eventMessage, EventLogEntryType.Error) End Sub
[Visual C#] private void logException(Exception ex, string eventName) { if (!EventLog.SourceExists(eventSource)) { EventLog.CreateEventSource(eventSource, eventLog); } string eventMessage = string.Format("{0}: {1}: {2}:", eventName, ex.Message, Thread.CurrentPrincipal.Identity.Name); EventLog.WriteEntry(eventSource, eventMessage, EventLogEntryType.Error); }
6.
In the OrdersServiceImpl class, create a new private method named handleException that accepts the following parameters: •
An Exception object named ex.
Lab 9: Building an N-Tier Solution by Using the Entity Framework
L9-21
•
A string parameter named operationName.
•
An optional integer parameter named operationData with a default value of 0.
Your code should resemble the following code example. [Visual Basic] Private Sub handleException(ByVal ex As Exception, ByVal operationName As String, Optional ByVal operationData As Integer = 0) End Sub
[Visual C#] private void handleException(Exception ex, string operationName, int operationData =0) { }
7.
In the handleException method, write code to perform the following tasks: a.
Create a new StringBuilder object named eventMessageBuilder.
b.
Append the message "Failure in {0}" to the eventMessageBuilder object, and specify the value of the operationName parameter as the {0} placeholder.
c.
If the value of the operationData variable is not equal to 0, append the value of the operationData parameter to the eventMessageBuilder object.
d. Call the logException method, passing the ex object and the string value of the eventMessageBuilder object as parameters. Your code should resemble the following code example. [Visual Basic] Private Sub handleException(ByVal ex As Exception, ByVal operationName As String, Optional ByVal operationData As Integer = 0) Dim eventMessageBuilder As New StringBuilder()
L9-22
Building an N-Tier Solution by Using the Entity Framework
eventMessageBuilder.Append( String.Format("Failure in {0}", operationName)) If operationData 0 Then eventMessageBuilder.Append(String.Format(":Data {0}", operationData)) End If Dim eventMessage As String = eventMessageBuilder.ToString() logException(ex,eventMessage) End Sub
[Visual C#] private void handleException(Exception ex, string operationName, int operationData =0) { StringBuilder eventMessageBuilder = new StringBuilder(); eventMessageBuilder.Append( string.Format("Failure in {0}", operationName)); if (operationData !=0) { eventMessageBuilder.Append(string.Format(":Data {0}", operationData)); } string eventMessage = eventMessageBuilder.ToString(); logException(ex,eventMessage); }
8.
In the handleException method, add code to perform the following tasks: a.
If the ex object is of type ApplicationException, create a new ServiceFault object named sf. Set the ExceptionType property to the type of the ex object, and set the ExceptionMessage property by using Message property of the ex object.
b.
Throw a new FaultException exception, specifying the sf object and the string value of the eventMessageBuilder object as parameters.
c.
If the ex object is some other type of exception, create a new ServiceFault object named sf, and set the ExceptionType property to the type of the ex object and the value of the ExceptionMessage property to the string "Exception occurred fetching data".
Lab 9: Building an N-Tier Solution by Using the Entity Framework
L9-23
d. Throw a new FaultException exception. Pass the value of the sf object and the message "Failure in {0}" where the {0} placeholder is the value of the operationName variable as parameters to the constructor. Your code should resemble the following code example. [Visual Basic] Private Sub handleException(ByVal ex As Exception, ByVal operationName As String, Optional ByVal operationData As Integer = 0) ... If TypeOf ex Is ApplicationException Then Dim sf As New ServiceFault() With { .ExceptionType = ex.GetType().ToString(), .ExceptionMessage = ex.Message } Throw New FaultException(Of ServiceFault)( sf, eventMessageBuilder.ToString()) Else Dim sf As New ServiceFault() With { .ExceptionType = ex.GetType.ToString(), .ExceptionMessage = "Exception occurred fetching data" } Throw New FaultException(Of ServiceFault)( sf, String.Format("Failure in {0}", operationName)) End If End Sub
[Visual C#] private void handleException(Exception ex, string operationName, int operationData =0) { ... if (ex is ApplicationException) { ServiceFault sf = new ServiceFault { ExceptionType = ex.GetType().ToString(), ExceptionMessage = ex.Message }; throw new FaultException(
L9-24
Building an N-Tier Solution by Using the Entity Framework
sf,eventMessageBuilder.ToString()); } else { ServiceFault sf = new ServiceFault { ExceptionType = ex.GetType().ToString(), ExceptionMessage = "Exception occurred fetching data" }; throw new FaultException( sf,string.Format("Failure in {0}", operationName)); } }
9.
Save the OrdersServiceImpl file: a.
If you are using Visual Basic, on the File menu, click Save OrdersServiceImpl.vb.
b.
If you are using Visual C#, on the File menu, click Save OrdersServiceImpl.cs.
Task 8: Implement the IOrdersService interface 1.
In the OrdersServiceImpl class, generate stub methods for each of the items in the IOrdersService interface: a.
If you are using Visual Basic, in the OrdersServiceImpl class declaration, move to the end of the line that contains the code Implements IOrdersService, and then press ENTER. Method stubs for each of the methods in the IOrdersService interface will be added to the end of the class.
b.
If you are using Visual C#, in the OrdersServiceImpl class declaration, right-click IOrdersService, point to Implement Interface, and then click Implement Interface. Method stubs for each of the methods in the IOrdersService interface will be added to the end of the class.
2.
Locate the GetContactDetails method. This method takes an integer value as a parameter and returns a Contact object.
3.
If you are using Visual C#, delete the default method body that throws a NotImplementedException exception.
4.
In the body of the method, add code to perform the following tasks:
Lab 9: Building an N-Tier Solution by Using the Entity Framework
L9-25
a.
Create a Contact object named contact and assign it the value null (Nothing in Visual Basic).
b.
Create a new AdventureWorksEntities object.
c.
Define a Language-Integrated Query (LINQ) query named matchingContacts that retrieves all of the Contact entities by using the AdventureWorksEntities object, with a ContactID property that is equal to the value of the contactID variable.
d. If there is at least one Contact object in the matchingContacts collection, return it; otherwise, return a null (Nothing in Visual Basic) value. If there is more than one matching contact, return the first one found. Note: The ContactID should be unique, so there should only be at most one matching contact. However, it is good practice to write defensive code just in case a database administrator amends the structure of the Contact table in the database and creates a different key column.
e.
Handle any exceptions by calling the handleException method; pass the Exception object, the method name, and the contactID variable as parameters before returning a null (Nothing in Visual Basic) value.
Your code should resemble the following code example. [Visual Basic] Public Function GetContactDetails(ByVal contactID As Integer) As OrdersDAL.Contact Implements IOrdersService.GetContactDetails Dim contact As Contact = Nothing Try Using context As New AdventureWorksEntities() Dim matchingContacts As IEnumerable(Of Contact) = From c In context.Contacts Where c.ContactID = contactID Select c If matchingContacts.Count() > 0 Then contact = matchingContacts.First End If Return contact
L9-26
Building an N-Tier Solution by Using the Entity Framework
End Using Catch ex As Exception Me.handleException(ex, "GetContactDetails", contactID) Return Nothing End Try End Function
[Visual C#] public Contact GetContactDetails(int contactID) { Contact contact = null; try { using (AdventureWorksEntities context = new AdventureWorksEntities()) { IEnumerable matchingContacts = from c in context.Contacts where c.ContactID == contactID select c; if (matchingContacts.Count() > 0) { contact = matchingContacts.First(); } return contact; } } catch (Exception ex) { this.handleException(ex, "GetContactDetails", contactID); return null; } }
5.
Locate the GetAllContactsInRange method. This method takes two integer values, lowerBound and upperBound, as parameters and returns an IEnumerable list of Contact objects.
6.
If you are using Visual C#, delete the default method body that throws a NotImplementedException exception.
Lab 9: Building an N-Tier Solution by Using the Entity Framework
7.
L9-27
In the body of the method, add code to perform the following tasks: a.
Create an IEnumerable object named contacts by using the Contact type as the type parameter, and assign it the value null (Nothing in Visual Basic).
b.
Create a new AdventureWorksEntities object.
c.
Define a LINQ query that retrieves all of the Contact entities where the ContactID property is between the values of the lowerBound and upperBound variables. The result of this query should be assigned to the contacts object.
d. If the number of objects in the contacts collection is greater than or equal to the value of the maxContactsCount constant, throw a new ApplicationException exception with the message "Too many contacts". e.
Return the contacts collection as a generic List object. If you are using Visual C#, specify the Contact type as the type parameter for the List object.
f.
Handle any exceptions by calling the handleException method; pass the exception object and the method name as parameters before returning null (Nothing in Visual Basic).
Your code should resemble the following code example. [Visual Basic] Public Function GetAllContactsInRange(ByVal lowerBound As Integer, ByVal upperBound As Integer) As System.Collections.Generic.IEnumerable(Of OrdersDAL.Contact) Implements IOrdersService.GetAllContactsInRange Dim contacts As IEnumerable(Of Contact) = Nothing Try Using context As New AdventureWorksEntities() contacts = From c In context.Contacts Where c.ContactID >= lowerBound And c.ContactID = maxContactsCount Then Throw New ApplicationException("Too many contacts") End If
L9-28
Building an N-Tier Solution by Using the Entity Framework
Return contacts.ToList() End Using Catch ex As Exception Me.handleException(ex, "GetAllContacts") Return Nothing End Try End Function
[Visual C#] public IEnumerable GetAllContactsInRange(int lowerBound, int upperBound) { IEnumerable contacts = null; try { using (AdventureWorksEntities context = new AdventureWorksEntities()) { contacts = from c in context.Contacts where c.ContactID >= lowerBound && c.ContactID = maxContactsCount) { throw new ApplicationException("Too many contacts"); } return contacts.ToList(); } } catch (Exception ex) { this.handleException(ex, "GetAllContacts"); return null; } }
8.
Locate the GetOrderDetails method. This method takes an integer value as a parameter and returns a SalesOrderHeader object.
Lab 9: Building an N-Tier Solution by Using the Entity Framework
9.
L9-29
If you are using Visual C#, delete the default method that throws a NotImplementedException exception.
10. In the body of the method, add code to perform the following tasks: a.
Create a SalesOrderHeader object named order and assign it the value null (Nothing in Visual Basic).
b.
Create a new AdventureWorksEntities object.
c.
Define a LINQ query named matchingOrders that retrieves all of the SalesOrderHeader entities and the related SalesOrderDetail entities where the SalesOrderID property matches the value in the orderID variable.
d. If the number of objects in the matchingOrders collection is greater than zero, return the first SalesOrderHeader object in the collection; otherwise, return null (Nothing in Visual Basic). e.
Handle any Exception exceptions by calling the handleException method; pass the exception object, the method name, and the orderID variable as parameters before returning null (Nothing in Visual Basic).
Your code should resemble the following code example. [Visual Basic] Public Function GetOrderDetails(ByVal orderID As Integer) As OrdersDAL.SalesOrderHeader Implements IOrdersService.GetOrderDetails Dim order As SalesOrderHeader = Nothing Try Using context = New AdventureWorksEntities Dim matchingOrders As IEnumerable(Of SalesOrderHeader) = From o In context.SalesOrderHeaders.Include( "SalesOrderDetails") Where o.SalesOrderID = orderID Select o If matchingOrders.Count() > 0 Then order = matchingOrders.First() End If Return order End Using
L9-30
Building an N-Tier Solution by Using the Entity Framework
Catch ex As Exception Me.handleException(ex, "GetOrderDetails", orderID) Return Nothing End Try End Function
[Visual C#] public SalesOrderHeader GetOrderDetails(int orderID) { SalesOrderHeader order = null; try { using (AdventureWorksEntities context = new AdventureWorksEntities()) { IEnumerable matchingOrders = from o in context.SalesOrderHeaders.Include( "SalesOrderDetails") where o.SalesOrderID == orderID select o; if (matchingOrders.Count() > 0) { order = matchingOrders.First(); } return order; } } catch (Exception ex) { this.handleException(ex, "GetOrderDetails", orderID); return null; } }
11. Locate the GetOrdersForContact method. This method takes an integer value as a parameter and returns an IEnumerable list of SalesOrderHeader objects. 12. If you are using Visual C#, delete the default method body that throws a NotImplementedException exception. 13. In the body of the method, add code to perform the following tasks:
Lab 9: Building an N-Tier Solution by Using the Entity Framework
L9-31
a.
Create an IEnumerable object named orders by using the SalesOrderHeader type as the type parameter, and assign it the value null (Nothing in Visual Basic).
b.
Create a new AdventureWorksEntities object.
c.
Define a LINQ query that retrieves all of the SalesOrderHeader entities and related SalesOrderDetail entities where the ContactID property matches the value in the contactID variable. Assign the result of this query to the orders object.
d. If the number of objects in the orders collection is greater than or equal to the value of the maxOrdersCount constant, throw a new ApplicationException exception with the message "Too many orders". e.
Return the orders collection as a generic List object. If you are using Visual C#, specify the SalesOrderHeader type as the type parameter for the List object.
f.
Handle any exceptions by calling the handleException method; pass the exception object, the method name, and the contactID variable as parameters before returning null (Nothing in Visual Basic).
Your code should resemble the following code example. [Visual Basic] Public Function GetOrdersForContact(ByVal contactID As Integer) As System.Collections.Generic.IEnumerable(Of OrdersDAL.SalesOrderHeader) Implements IOrdersService.GetOrdersForContact Dim orders As IEnumerable(Of SalesOrderHeader) = Nothing Try Using context = New AdventureWorksEntities() orders = From o In context.SalesOrderHeaders.Include( "SalesOrderDetails") Where o.ContactID = contactID Select o If orders.Count >= maxOrdersCount Then Throw New ApplicationException("Too many orders") End If Return orders.ToList() End Using
L9-32
Building an N-Tier Solution by Using the Entity Framework
Catch ex As Exception Me.handleException(ex, "GetOrdersForContact", contactID) Return Nothing End Try End Function
[Visual C#] public IEnumerable GetOrdersForContact(int contactID) { IEnumerable orders = null; try { using (AdventureWorksEntities context = new AdventureWorksEntities()) { orders = from o in context.SalesOrderHeaders.Include( "SalesOrderDetails") where o.ContactID == contactID select o; if (orders.Count() >= maxOrdersCount) { throw new ApplicationException("Too many orders"); } return orders.ToList(); }
}
} catch (Exception ex) { this.handleException(ex, "GetOrdersForContact", contactID); return null; }
14. Locate the GetOrdersForProduct method. This method takes an integer value as a parameter and returns an IEnumerable list of SalesOrderHeader objects. 15. If you are using Visual C#, delete the default method body that throws a NotImplementedException exception. 16. In the body of the method, add code to perform the following tasks:
Lab 9: Building an N-Tier Solution by Using the Entity Framework
L9-33
a.
Create an IEnumerable object named orders by using the SalesOrderHeader type as the type parameter, and assign it the value null (Nothing in Visual Basic).
b.
Create a new AdventureWorksEntities object.
c.
Define a LINQ query that retrieves all of the SalesOrderHeader entities and related SalesOrderDetail entities where the ProductID property of at least one of the SalesOrderDetail entities for the SalesOrderHeader entity matches the productID variable. Assign the result of this query to the orders object.
Note: The SalesOrderDetail records for an order specify the products being ordered. An order can have one or more SalesOrderDetail records. To find all orders for a specific product, you must find all SalesOrderDetail records that match the product and return the SalesOrderHeader objects that reference these SalesOrderDetail records.
d. If the number of objects in the orders collection is greater than or equal to the value of the maxOrdersCount constant, throw a new ApplicationException exception with the message "Too many orders". e.
Return the orders collection as a generic List object. If you are using Visual C#, specify the SalesOrderHeader type as the type parameter for the List object.
f.
Handle any exceptions by calling the handleException method; pass the exception object, and the method name as parameters before returning null (Nothing in Visual Basic).
Your code should resemble the following code example. [Visual Basic] Public Function GetOrdersForProduct(ByVal productID As Integer) As System.Collections.Generic.IEnumerable(Of OrdersDAL.SalesOrderHeader) Implements IOrdersService.GetOrdersForProduct Dim orders As IEnumerable(Of SalesOrderHeader) = Nothing Try Using context = New AdventureWorksEntities() orders = From o In context.SalesOrderHeaders.Include( "SalesOrderDetails") Where o.SalesOrderDetails.Any(
L9-34
Building an N-Tier Solution by Using the Entity Framework
Function(d) d.ProductID = productID) Select o If orders.Count() >= maxOrdersCount Then Throw New ApplicationException("Too many orders") End If Return orders.ToList() End Using Catch ex As Exception Me.handleException(ex, "GetOrdersForProduct") Return Nothing End Try End Function
[Visual C#] public IEnumerable GetOrdersForProduct(int productID) { IEnumerable orders = null; try { using (AdventureWorksEntities context = new AdventureWorksEntities()) { orders = from o in context.SalesOrderHeaders.Include( "SalesOrderDetails") where o.SalesOrderDetails.Any( d => d.ProductID == productID) select o; if (orders.Count() >= maxOrdersCount) { throw new ApplicationException("Too many orders"); } return orders.ToList(); } }
Lab 9: Building an N-Tier Solution by Using the Entity Framework
L9-35
catch (Exception ex) { this.handleException(ex, "GetOrdersForProduct"); return null; } }
17. Locate the GetAllOrdersInRange method. This method takes two integer values, lowerBound and upperBound, as parameters and returns an IEnumerable list of Contact objects. 18. If you are using Visual C#, delete the default method body that throws a NotImplementedException exception. 19. In the body of the method, add code to perform the following tasks: a.
Create an IEnumerable object named orders by using the SalesOrderHeader type as the type parameter, and assign it the value null (Nothing in Visual Basic).
b.
Create a new AdventureWorksEntities object.
c.
Define a LINQ query that retrieves all of the SalesOrderHeader entities and related SalesOrderDetails entities where the value in the SalesOrderID property is between the lowerBound and upperBound variables. The result of this query should be assigned to the orders object.
d. If the number of objects in the orders collection is greater than or equal to the value of the maxOrdersCount constant, throw a new ApplicationException exception with the message "Too many orders". e.
Return the orders collection as a generic List object. If you are using Visual C#, specify the SalesOrderHeader type as the type parameter for the List object.
f.
Handle any exceptions by calling the handleException method; pass the exception object, and the method name as parameters before returning null (Nothing in Visual Basic).
Your code should resemble the following code example. [Visual Basic] Public Function GetAllOrdersInRange(ByVal lowerBound As Integer, ByVal upperBound As Integer) As System.Collections.Generic.IEnumerable(Of OrdersDAL.SalesOrderHeader) Implements IOrdersService.GetAllOrdersInRange
L9-36
Building an N-Tier Solution by Using the Entity Framework
Dim orders As IEnumerable(Of SalesOrderHeader) = Nothing Try Using context = New AdventureWorksEntities orders = From o In context.SalesOrderHeaders.Include( "SalesOrderDetails") Where o.SalesOrderID >= lowerBound And o.SalesOrderID = maxOrdersCount Then Throw New ApplicationException("Too many orders") End If Return orders.ToList() End Using Catch ex As Exception Me.handleException(ex, "GetAllOrdersInRange") Return Nothing End Try End Function
[Visual C#] public IEnumerable GetAllOrdersInRange(int lowerBound, int upperBound) { IEnumerable orders = null; try { using (AdventureWorksEntities context = new AdventureWorksEntities()) { orders = from o in context.SalesOrderHeaders.Include( "SalesOrderDetails") where o.SalesOrderID >= lowerBound && o.SalesOrderID = maxOrdersCount) { throw new ApplicationException("Too many orders"); }
Lab 9: Building an N-Tier Solution by Using the Entity Framework
L9-37
return orders.ToList(); } } catch (Exception ex) { this.handleException(ex, "GetAllOrdersInRange"); return null; } }
20. Build the OrdersService project and correct any errors: Important: Only build the OrdersService project. The OrderManagement project will not build successfully because it is not yet complete.
•
In Solution Explorer, right-click OrdersService, and then click Build. Correct any errors.
Task 9: Create the OrdersWebService Web service 1.
Add a new empty ASP.NET Web Application project named OrdersWebService to the solution: a.
On the File menu, point to Add, and then click New Project.
b.
In the Add New Project dialog box, in the Installed Templates pane click Web.
c.
In the list of templates, click ASP.NET Empty Web Application.
d. In the Name box, type OrdersWebService and then click OK. 2.
Configure the project to use the local IIS Web server with the URL http://localhost/OrdersWebService: a.
In Solution Explorer, right-click OrdersWebService, and then click Properties.
b.
In the OrdersWebService window, click the Web tab, and then click Use Local IIS Web server.
c.
In the Project Url box, check that the URL is set to http://localhost/OrdersWebService.
d. On the File menu, click Close.
L9-38
Building an N-Tier Solution by Using the Entity Framework
3.
4.
5.
6.
7.
8.
Add a new WCF service called OrdersWebService.svc to the OrdersWebService project: a.
In Solution Explorer, right-click OrdersWebService, point to Add, and then click New Item.
b.
In the Add New Item - OrdersWebService dialog box, in the list of templates, click WCF Service.
c.
In the Name box, type OrdersWebService.svc and then click Add.
Delete the IOrdersWebService code file from the OrdersWebService project: a.
If you are using Visual Basic, in Solution Explorer, right-click IOrdersWebService.vb, and then click Delete.
b.
If you are using Visual C#, in Solution Explorer, right-click IOrdersWebService.cs, and then click Delete.
c.
In the Microsoft Visual Studio dialog box, click OK.
Add a reference to the OrdersClientLibrary assembly: a.
In Solution Explorer, right-click OrdersWebService, and then click Add Reference.
b.
In the Add Reference dialog box, on the Projects tab, click OrdersClientLibrary, and then click OK.
Add a reference to the OrdersDAL assembly: a.
In Solution Explorer, right-click OrdersWebService, and then click Add Reference.
b.
In the Add Reference dialog box, on the Projects tab, click OrdersDAL, and then click OK.
Add a reference to the OrdersService assembly: a.
In Solution Explorer, right-click OrdersWebService, and then click Add Reference.
b.
In the Add Reference dialog box, on the Projects tab, click OrdersService, and then click OK.
Delete the OrdersWebService code-behind file: a.
In the Solution Explorer toolbar, click Show All Files.
b.
In Solution Explorer, expand OrdersWebService.svc.
Lab 9: Building an N-Tier Solution by Using the Entity Framework
c.
L9-39
If you are using Visual Basic, right-click OrdersWebService.svc.vb, and then click Delete.
d. If you are using Visual C#, right-click OrdersWebService.svc.cs, and then click Delete. e. 9.
In the Microsoft Visual Studio dialog box, click OK.
In the OrdersWebService.svc file, delete the existing markup code: a.
In Solution Explorer, right-click OrdersWebService.svc, and then click Open.
b.
In the OrdersWebService.svc window, delete the existing markup code.
10. In the OrdersWebService.svc file, add markup statements that perform the following tasks: a.
Create a ServiceHost instance and set the Service property to point to the OrdersServiceImpl service that is defined in the OrdersService project.
b.
Specify that this service is located in the OrdersService assembly.
Your code should resemble the following code example.
Note: Visual Studio reports that it cannot find the OrdersService assembly. This warning will disappear when you build the project and you can safely ignore it.
11. Delete the Web.config file from the OrdersWebService project: a.
In Solution Explorer, right-click Web.config, and then click Delete.
b.
In the Microsoft Visual Studio dialog box, click OK.
12. Add the existing Web.config file from the E:\Labfiles\Lab09\CS\Ex1\Starter or E:\Labfiles\Lab09\VB\Ex1\Starter folder to the OrdersWebService project: a.
In Solution Explorer, right-click OrdersWebService, point to Add, and then click Existing Item.
b.
If you are using Visual Basic, in the Add Existing Item OrdersWebService dialog box, move to the E:\Labfiles\Lab09\VB\Ex1\Starter folder, click web.config, and then click Add.
L9-40
Building an N-Tier Solution by Using the Entity Framework
c.
If you are using Visual C#, in the Add Existing Item - OrdersWebService dialog box, move to the E:\Labfiles\Lab09\CS\Ex1\Starter folder, click web.config, and then click Add.
13. Build the OrdersWebService project and correct any errors: Important: Only build the OrdersWebService project. The OrderManagement project will not build successfully because it is not yet complete.
•
In Solution Explorer, right-click OrdersWebService, and then click Build. Correct any errors.
Task 10: Configure the OrderManagement application 1.
2.
In the OrderManagement project, add a reference to the OrdersClientLibrary assembly: a.
In Solution Explorer, right-click OrderManagement, and then click Add Reference.
b.
In the Add Reference dialog box, on the Projects tab, click OrdersClientLibrary, and then click OK.
Add a service reference to the OrdersWebService service to the OrderManagement application. Generate the service reference in the OWService namespace. The URL of the OrdersWebService is http://localhost/OrdersWebService/OrdersWebService.svc. Use System.Collections.Generic.List as the collection type and ensure that you reuse types in referenced assemblies: a.
In Solution Explorer, right-click OrderManagement, and then click Add Service Reference.
b.
In the Add Service Reference dialog box, click Discover.
c.
In the Namespace box, type OWService and then click Advanced.
d. In the Service Reference Settings dialog box, in the Collection type list, click System.Collections.Generic.List. e.
Select the Reuse types in referenced assemblies check box, and then click Reuse types in all referenced assemblies.
f.
In the Service Reference Settings dialog box, click OK.
g.
In the Add Service Reference dialog box, click OK.
Lab 9: Building an N-Tier Solution by Using the Entity Framework
3.
L9-41
In the app.config file, modify the definition of the wsHttpBinding binding by increasing the maxBufferPoolSize property to 5242880 and the maxReceivedMessageSize property to 5242880: a.
In Solution Explorer, in the OrderManagement project, right-click app.config, and then click Open.
b.
In the app.config window, in the wsHttpBinding configuration section, change the value of the maxBufferPoolSize property to 5242880.
c.
In the wsHttpBinding configuration section, change the value of the maxReceivedMessageSize property to 5242880.
Your code should resemble the following code example. -1 Then result = True End If Return result End Function
Lab 11: Building Occasionally Connected Solutions
L11-7
[Visual C#] // Check that the Web service is available. Return True if it is, false otherwise. private bool CheckWebServiceExists(OrdersWebServiceClient service) { bool result = false; byte[] response; System.Net.WebClient client = new System.Net.WebClient(); try { response = client.DownloadData( service.Endpoint.Address.Uri.AbsoluteUri); } catch (Exception) { return result; } string str = Encoding.UTF8.GetString(response); if (str.IndexOf("xml") > -1) result = true; return result; }
Task 5: Write code to cache the contacts 1.
Locate the next comment in the code file behind the OrderManagementWindow.xaml window by double-clicking the first TODO: Check whether the Web service is still operational task in the task list. . This task is located in the getContacts_Click method: •
2.
In the task list, double-click the first TODO: Check whether the Web service is still operational task.
Immediately after the comment, add a call to the CheckWebServiceExists method. If the service does not exist, call the getContactsFromLocalCache method and return from the method. Your code should resemble the following code example.
[Visual Basic] If CheckWebServiceExists(service) = False Then Me.getContactsFromLocalCache(rangeFrom, rangeTo) return End If
L11-8
Building Occasionally Connected Solutions
[Visual C#] if (false == CheckWebServiceExists(service)) { this.getContactsFromLocalCache(rangeFrom, rangeTo); return; }
3.
Locate the next comment in the code file behind the OrderManagementWindow.xaml window by double-clicking the TODO: Cache the contacts task in the task list: •
4.
In the task list, double-click the TODO: Cache the contacts task. This task is located in the getContacts_Click method.
Immediately after the comment, add a call to the SaveContactsToLocalCache method, passing the contacts object. Your code should resemble the following code example.
[Visual Basic] If contacts IsNot Nothing Then Me.SaveContactsToLocalCache(contacts) End If
[Visual C#] if (contacts != null) { this.SaveContactsToLocalCache(contacts); }
5.
Locate the next comment in the code file behind the OrderManagementWindow.xaml window by double-clicking the TODO: Get contact information from the local cache file task in the task list: •
6.
In the task list, double-click the TODO: Get contact information from the local cache file task.
Delete the existing code in this method, and then add code that performs the following tasks:
Lab 11: Building Occasionally Connected Solutions
L11-9
a.
Instantiate a new instance of the list of contacts and then update the message in the statusMessage status bar item with the text "Fetching contacts …"
b.
If the contacts XML file exists, call the LoadContactsFromLocalCache method, display the data in the contactsGrid grid, display the number of contacts retrieved in the numContactRows label, and then update the message in the statusMessage status bar item with the text "Ready".
c.
If there are no cached contacts, update the message in the statusMessage status bar item with the text "No cached data available".
Your code should resemble the following code example. [Visual Basic] ' Get contact information from the local cache file Private Sub getContactsFromLocalCache(ByVal rangeFrom As Integer, ByVal rangeTo As Integer) Dim contacts As List(Of Contact) = Nothing Me.statusMessage.Content = "Fetching contacts ..." Try If File.Exists(contactsFile) Then contacts = Me.LoadContactsFromLocalCache( rangeFrom, rangeTo) ' Display the data. Me.contactsGrid.DataContext = contacts Me.numContactRows.Content = String.Format("Rows: {0}", If(contacts IsNot Nothing, contacts.Count(), 0)) Me.statusMessage.Content = "Ready" End If If contacts Is Nothing Or contacts.Count() = 0 Then Me.statusMessage.Content = "No cached data available" Me.contactsGrid.DataContext = Nothing End If Catch ex As Exception MessageBox.Show(String.Format("Exception occurred: {0}", ex.Message)) End Try End Sub
L11-10
Building Occasionally Connected Solutions
[Visual C#] // Get contact information from the local cache file private void getContactsFromLocalCache(int rangeFrom, int rangeTo) { List contacts = null; this.statusMessage.Content = "Fetching contacts ..."; try { if (File.Exists(contactsFile)) { contacts = this.LoadContactsFromLocalCache( rangeFrom, rangeTo); // Display the data. this.contactsGrid.DataContext = contacts; this.numContactRows.Content = string.Format( "Rows: {0}", contacts != null ? contacts.Count() : 0); this.statusMessage.Content = "Ready"; } if (contacts == null || contacts.Count() == 0) { this.statusMessage.Content = "No cached data available"; this.contactsGrid.DataContext = null; } } catch (Exception ex) { MessageBox.Show(string.Format( "Exception occurred: {0}", ex.Message)); } }
7.
Locate the next comment in the code file behind the OrderManagementWindow.xaml window by double-clicking the TODO: Save contact information to the local cache file task in the task list: •
8.
In the task list, double-click the TODO: Save contact information to the local cache file task.
Delete the existing code in this method, and then add code that performs the following tasks: a.
Construct an XElement object that contains the data in the contacts object that is passed to the method.
b.
Save the XElement object to the file specified by the contactsFile variable.
Lab 11: Building Occasionally Connected Solutions
Your code should resemble the following code example. [Visual Basic] ' Save contact information to the local cache file Private Sub SaveContactsToLocalCache(ByVal contacts As List(Of Contact)) Dim xml As XElement = Nothing Try xml = New XElement("Contacts", From c In contacts Select New XElement("Contact", New XElement("ContactID", c.ContactID), New XElement("Title", c.Title), New XElement("FirstName", c.FirstName), New XElement("MiddleName", c.MiddleName), New XElement("LastName", c.LastName), New XElement("EmailAddress", c.EmailAddress), New XElement("Phone", c.Phone))) Catch ex As Exception MessageBox.Show(String.Format("Exception occurred: {0}", ex.Message)) End Try Try xml.Save(contactsFile) Catch ex As Exception MessageBox.Show(String.Format("Exception occurred: {0}", ex.Message)) End Try End Sub
[Visual C#] // Save contact information to the local cache file private void SaveContactsToLocalCache(List contacts) { XElement xml = null; try { xml = new XElement("Contacts", from c in contacts
L11-11
L11-12
Building Occasionally Connected Solutions
select new XElement("Contact", new XElement("ContactID", c.ContactID), new XElement("Title", c.Title), new XElement("FirstName", c.FirstName), new XElement("MiddleName", c.MiddleName), new XElement("LastName", c.LastName), new XElement("EmailAddress", c.EmailAddress), new XElement("Phone", c.Phone))); } catch (Exception ex) { MessageBox.Show(string.Format("Exception occurred: {0}", ex.Message)); } try { xml.Save(contactsFile); } catch (Exception ex) { MessageBox.Show(string.Format("Exception occurred: {0}", ex.Message)); } }
9.
Locate the next comment in the code file behind the OrderManagementWindow.xaml window by double-clicking the TODO: Helper method. Read contact information from the local cache file task in the task list: •
In the task list, double-click the TODO: Helper method. Read contact information from the local cache file task.
10. Delete the existing code in this method, and then add code that performs the following tasks: a.
Load the contents of the contacts file into an XDocument object.
b.
Convert the XML into a list of Contact objects and return this list.
Your code should resemble the following code example. [Visual Basic] ' Helper method. Read contact information from the local cache file Private Function LoadContactsFromLocalCache(ByVal rangeFrom As Integer, ByVal rangeTo As Integer) As List(Of Contact)
Lab 11: Building Occasionally Connected Solutions
L11-13
Dim doc As XDocument = Nothing Try doc = XDocument.Load(contactsFile) Catch ex As Exception MessageBox.Show(String.Format( "Exception occurred: {0}", ex.Message)) End Try Dim query = From contact In doc.Descendants("Contact") Where Convert.ToInt32(contact.Element("ContactID").Value) _ >= rangeFrom And Convert.ToInt32(contact.Element("ContactID").Value) _ = rangeFrom && Convert.ToInt32(contact.Element("ContactID").Value) o.ShipDate != null; } else if (HttpContext.Current.User.IsInRole("USShipping")) { // Just US Shipping methods return o => o.ShipDate != null && (o.ShipMethodID == 1 || o.ShipMethodID == 2 || o.ShipMethodID == 4);
L12-16
Creating and Using WCF Data Services
} else { // Nothing at all return o => o.ShipMethodID == -1; } }
5.
Save the ShippingDataService.svc file: a.
If you are using Visual Basic, on the File menu, click Save ShippingDataService.svc.vb.
b.
If you are using Visual C#, on the File menu, click Save ShippingDataService.svc.cs.
Task 3: Modify the Web client application to support data paging 1.
2.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
Open the HomeController file by double-clicking the comment TODO: Ex3 Declare a QueryOperationResponse object in the task list. This task is located in the SalesOrders method: •
3.
In the task list, double-click the comment TODO: Ex3 - Declare a QueryOperationResponse object.
In the SalesOrders method, immediately after the comment, write code that declares a variable based on the QueryOperationResponse generic type, called response. Specify SalesOrderHeader as the type parameter, and initialize it to null. Your code should resemble the following code example.
[Visual Basic] ' Handle result of query execution and paging Dim response As QueryOperationResponse(Of SalesOrderHeader) = Nothing
Lab 12: Creating and Using WCF Data Services
L12-17
[Visual C#] // Handle result of query execution and paging QueryOperationResponse response = null;
4.
Locate the next comment TODO: Ex3 - Get the first page of SalesOrderHeader records in the SalesOrders method: •
5.
In the task list, double-click the comment TODO: Ex3 - Get the first page of SalesOrderHeader records.
In the SalesOrders method, immediately after the comment, write code that assigns the result of calling the Execute method on the SalesOrderHeaders entity set to the response variable. Your code should resemble the following code example.
[Visual Basic] response = DirectCast(context.SalesOrderHeaders.Execute(), QueryOperationResponse(Of SalesOrderHeader))
[Visual C#] response = context.SalesOrderHeaders.Execute() as QueryOperationResponse;
6.
Locate the next comment TODO: Ex3 - Get the next page of SalesOrderHeader records in the SalesOrders method: •
7.
In the task list, double-click the comment TODO: Ex3 - Get the next page of SalesOrderHeader records.
In the SalesOrders method, immediately after the comment, write code that assigns the result of calling the Execute method on the SalesOrderHeader entity set to the response variable. Use the value of the next (_next in Visual Basic) variable to instantiate a Uri object to pass as a parameter to the Execute method. Your code should resemble the following code example.
[Visual Basic] response = DirectCast(context.Execute(Of SalesOrderHeader)(New Uri(_next)), QueryOperationResponse(Of SalesOrderHeader))
L12-18
Creating and Using WCF Data Services
[Visual C#] response = context.Execute(new Uri(next)) as QueryOperationResponse;
8.
Save the HomeController file: a.
If you are using Visual Basic, on the File menu, click Save Controllers\HomeController.vb.
b.
If you are using Visual C#, on the File menu, click Save Controllers\HomeController.cs.
Task 4: Build and test the data service and the client Web site 1.
Build the solution and correct any errors: •
2.
Check that the ShippingDetailsSite project is set as the StartUp project: •
3.
In Solution Explorer, if the ShippingDetailsSite project is not highlighted in bold text, right-click the ShippingDetailsSite project, and then click Set as StartUp Project.
Start the application in Debug mode: •
4.
On the Build menu, click Build Solution.
On the Debug menu, click Start Debugging.
Test the application by using the three sets of credentials in the following table. User name
Password
Role
Bill
Pa$$w0rd
USShipping
James
Pa$$w0rd
None
Mary
Pa$$w0rd
WorldwideShipping
5.
Close Internet Explorer.
6.
Close the solution: •
On the File menu, click Close Solution.
Lab 12: Creating and Using WCF Data Services
L12-19
Exercise 4: Implementing a Business Operation in a WCF Data Service Task 1: Open the starter project 1.
In the E:\Labfiles\Lab12\VB\Ex4\Starter or E:\Labfiles\Lab12\CS\Ex4\Starter folder, run ExSetup.bat as an administrator: a.
Click Start, and then click Computer.
b.
Double-click Allfiles (E:), double-click Labfiles, double-click Lab12, double-click VB or CS, double-click Ex4, and then double-click Starter.
c.
Right-click ExSetup.bat, and then click Run as administrator.
d. In the User Account Control dialog box, in the Password box, type Pa$$w0rd and then click Yes. e. 2.
Wait for the batch file to finish running, and then close Windows Explorer.
Open the existing solution, ShippingDataServiceSite.sln, in the E:\Labfiles\Lab12\VB\Ex4\Starter or E:\Labfiles\Lab12\CS\Ex4\Starter folder: a.
In Visual Studio, on the File menu, point to Open, and then click Project/Solution.
b.
In the Open Project dialog box, move to the E:\Labfiles\Lab12\VB\Ex4\Starter or E:\Labfiles\Lab12\CS\Ex4\Starter folder, click ShippingDataServiceSite.sln, and then click Open.
Task 2: Add a business operation to archive records in the data service 1.
2.
Review the task list: a.
If the task list is not already visible, on the View menu, click Task List.
b.
If the task list is showing User Tasks, in the Categories list, click Comments.
Open the ShippingDataService.svc file by double-clicking the comment TODO: Ex4 - Configure Service Operation in the task list. This task is located in the InitializeService method:
L12-20
Creating and Using WCF Data Services
• 3.
In the task list, double-click the comment TODO: Ex4 - Configure Service Operation.
In the InitializeService method, immediately after the comment, write code that calls the SetServiceOperationAccessRule method of the config object and gives full permissions to the ArchiveSalesOrders function. Your code should resemble the following code example.
[Visual Basic] config.SetServiceOperationAccessRule("ArchiveSalesOrders", ServiceOperationRights.All)
[Visual C#] config.SetServiceOperationAccessRule("ArchiveSalesOrders", ServiceOperationRights.All);
4.
Open the ShippingDataService.svc file by double-clicking the comment TODO: Ex4 - Define the ArchiveSalesOrders operation in the task list. This task is located in the ArchiveSalesOrders method: •
5.
In the task list, double-click the comment TODO: Ex4 - Define the ArchiveSalesOrders operation.
Write code that performs the following tasks: a.
Check the role membership of the current user. If the user is not in the WorldwideShipping role, throw an UnauthorizedAccessException exception.
b.
Calculate the archive date. This should be 365 days ago.
c.
Define a LINQ query that selects all of the SalesOrderHeader entities that are older than 365 days and have a Status property equal to 8.
d. For each of the SalesOrderHeader entities that the query selects, set the Status property to 8, and then create a new ArchivedSalesOrderHeader entity. e.
Save the changes.
Your code should resemble the following code example.
Lab 12: Creating and Using WCF Data Services
L12-21
[Visual Basic] ''' ''' A business operation only availably to users in the "WorldWideShipping" role ''' Used to archive orders shipped over a year ago to the ArchivedSaleOrderHeader table ''' _ Public Sub ArchiveSalesOrders() ' Check the role membership. If Not HttpContext.Current.User.IsInRole("WorldwideShipping") Then Throw New UnauthorizedAccessException("You are not permitted to archive the data") End If ' Calculate the Archive date. Dim archiveDate As DateTime = DateTime.Now.Subtract( New TimeSpan(365, 0, 0, 0, 0)) ' Define a query to select the records to archive. Dim oldOrders = From old In Me.CurrentDataSource.SalesOrderHeaders Where old.ShipDate = lowerBound And contact.ContactID = lowerBound && contact.ContactID