Object-Relational Mapping in the Microsoft World by Benjamin Day Benjamin Day Consulting, Inc.
-
Upload
heather-skinner -
Category
Documents
-
view
226 -
download
0
Transcript of Object-Relational Mapping in the Microsoft World by Benjamin Day Benjamin Day Consulting, Inc.
Object-Relational Mapping in the Microsoft World
by Benjamin DayBenjamin Day Consulting, Inc.
About the speaker
• Owner, Benjamin Day Consulting, Inc. Email: [email protected] Web: http://www.benday.com Blog: http://blog.benday.com
• Trainer Visual Studio Team System, Team Foundation Server
• Microsoft MVP for C#• Microsoft VSTS/TFS Customer Advisory Council• Leader of Beantown.NET INETA User Group
Agenda
• Overview of “the problem”• Brief discussion of the options• LINQ to SQL• NHibernate• Entity Framework
THE PROBLEM
The Problem
• Mismatch between objects and relational databases Objects = inheritance, polymorphism, composition Database = rows of data with relationships to other rows of data
One Table Per Class
• The class looks like the table• Great candidate for a typed dataset
Simple Works nicely with DataAdapter because of RowState
Limitations of Typed Dataset Route• Tight coupling with the database• Inheritance is difficult• How do you validate data?
Null, data type, and length check validation is handled Serious validation lives outside of the dataset not very object-oriented
• The ‘object’ route is better for validation Validation goes in the “set” property
Object - DataRow Hybrid
• Every object wraps a typed DataRow• Properties control access to columns on the DataRow
Facilitates more complex validation
• DataRow becomes a data transfer object Still works nicely for System.Data integration
Primitive Obsession
• Validation in the get / set properties is ok but is phone number validation really the responsibility of the Person class?
• James Shore’s “Primitive Obsession” Too many plain scalar values Phone number isn’t really just
a string http://www.jamesshore.com/Blog/
Coarse-Grained vs. Fine-Grained Object Model
• Fine-grained = More object-oriented• Data and properties are split into actual responsibilities
coarse-grained fine-grained
But how do you save it?
• Four classes go to one table?• Five instances go to one table?
Inheritance, Collections, and Relationships
• Employee is a Person• Employee has Underlings• Employee has a Supervisor
Limitations of Object - DataRow Hybrid• Inheritance >1 DataRow at once
One for base class, one for descendent• Which instance of DataSet is the object using?
Complicates saves Client has to worry about the internal state of the object
• How to save parent-child relationships? Employee has FK to a Supervisor Supervisor needs to be saved
first• Objects have ability to modify internal state of other objects
wrapping the same DataSet bugs & encapsulation• Number of records in the DataSet tends to grow rather than
shrink How to clear out old, unused records in the DataSet? How do you
know when they’re old?
Object-Only Model
• No more DataRows• Classes are member variables, methods and properties• Clean “Domain Model” pattern
Business tier worries about itself Decoupled from the database
• Straightforward inheritance, polymorphism
Complications in the Object-Only Model
• Data access (“Mapper”) tier has to “adapt” data and structures from the database into populated business objects and vice versa
• How to manage IsDirty for INSERT vs UPDATE? Adding IsDirty logic to domain objects is a violation of the “separation of
concerns”
• Concurrency management?• Transaction management?
Do you really want to solve these problems?
• Problems are solvable• Everybody writes their own solution• Persistence is a distraction from solving The Real Business
Problem
AVAILABLE OPTIONS
Options
• LINQ to SQL• Entity Framework• NHibernate• Wilson OR Mapper• LLBLGen• And more…
LINQ to SQL
• Available in Visual Studio 2008• “Better typed dataset”
LINQ to SQL: Pros / Cons
Pros• You don’t have to write
data access code• Good for Rapid
Application Prototyping• Integrated into Visual
Studio• Designer support• Will generate the
database schema for you
Cons• SQL Server only• Limited inheritance
modeling (Table Per Hierarchy only)
• Closely tied to the database
• Unusual way of handling stored procedures
• Generates code
Entity Framework
• Microsoft’s first, real ORM solution (well…I guess this depends on who you talk to)
• In beta• Rumored to be released with Visual Studio 2008 sp1
Entity Framework: Pros / Cons
Pros• Full-featured• Not open source• Integrated into Visual
Studio• Designer support
(eventually)• Support for non-SQL
Server databases (eventually)
• Supports LINQ• Support Table-per-Type
Cons• Still in beta• Not open source• Version 1
NHibernate
• Full-featured ORM solution• Open source• Based on Java’s Hibernate framework• Uses XML to map tables/columns to objects/properties
NHibernate: Pros / Cons
Pros• Rich selection of
mappings• Inheritance modelling• Polymorphic queries• Established solution with
lots of current users• Support for multiple
database vendors• Open source• Free
Cons• Open source• Free• Not from Microsoft• 3rd party library• Limited to zero GUI
support• Currently doesn’t support
LINQ …but does have HQL
LINQ TO SQL
Using the LINQ to SQL O/R Designer
• Define classes• Set up inheritance
LINQ to SQL vs. Typed DataSets
• My $0.02 – LINQ to SQL is the new Typed DataSet
LINQ to SQL vs. LINQ to DataSets
• DataSets in VS2008 are query-able with LINQ• DataTable’s object model has changed • DataTable now extends from TypeTableBase<T>• TypedTableBase<T> extends DataTable
• Backwards compatible
• No special way to fill the DataSet/DataTable with LINQ to SQL• Still uses DataAdapter
LINQ to SQL vs. Raw SQL Access
• Hopefully you don’t do this…• The data access strategy of masochists everywhere
LINQ to SQL vs. NHibernate• NHibernate is LINQ to SQL’s older, more successful
cousin• 5+ years in .NET & Java
• Full-featured ORM framework• Maps tables/columns to classes/properties• Uses xml mapping files or attributes
• Comprehensive inheritance modeling• Has LINQ-like object query syntax
• HQL – Hibernate Query Language
• Multi-vendor database support• Oracle, SQLServer (2000 & 2005), MySql, Sybase, etc
• Lets you focus on the business problem rather than persistence
Using LINQ to SQL with Unit Tests
• When testing database code, database must be in a known state
• Easiest way: Wipe the database between tests DataContext.DeleteDatabase() DataContext.CreateDatabase() Database schema always in sync with code
• Harder way: Unit test manages transaction Rollback at end of unit test
LINQ to SQL in an n-tier Application
• Common BaseClass• Hooking into save events
Auto-updating: ModifiedDate, ModifiedBy
• Keeping your code organized with the “Service Layer” pattern
Common Business Base Class
• Each business class should probably have similar fields Id ModifiedDate, ModifiedBy CreateDate, CreatedBy
• Bummer: LINQ to SQL isn’t great at this (NHibernate does this effortlessly) Mapped columns must be defined on the concrete
Code Demo
• Introduce a common business base class
Code Demo
• Implement the partials and auto-populate the base class properties
Auto-update base class propertiesfrom DataContext
• Generated DataContext & other objects are partial classes• Generated code gives you partial methods on DataContext
for each object InsertXxx(), UpdateXxx(), DeleteXxx()
• Create your own partial class and create your own implementation of the method
• Don’t forget to call ExecuteDynamicInsert(), ExecuteDynamicUpdate() or
ExecuteDynamicDelete()
Other fun stuff with the partial methods
• Your partial implementations wipe out the LINQ to SQL default implementation
• (Who cares? This is boring.)• You could put your own implementation that uses stored
procedures in your partials!
Service Layer Pattern
From “Patterns Of Enterprise Application Architecture”by Martin Fowler, Randy Stafford, et al.Chapter 9
“Defines an application’s boundary with a layer of services that establishes a set of available operations and coordinates the application’s response in each operation.”
-Randy Stafford
Why Service Layer?
• Formally defines supported business tier operations (aka methods)
• Methods provide ideal target for unit testing• Keeps code organized
Code review: anything complex not in the service layer refactor Keeps code out of the UI
• Isolates the Domain Model (business) objects Minimize usage of the Domain Model objects outside of the Business tier
Service Layer in LINQ?
• CRUD operations for each business object • Any specialized “get” operations
Centralized place for any custom from-where-select’s
• Factory methods
• Create a BusinessFacade<T>
ENTITY FRAMEWORK
Entity Framework Overview
• Still in beta• Official release with VS2008 sp1• Trying to be more than just an ORM• 3 layers
Conceptual Model (classes) Storage / Logical Model (table definitions) Mapping layer
NHIBERNATE
What NHibernate isn’t
• “Hibernate” has nothing to do with Windows Hibernate• Not object serialization• Not a code generator
NHibernate To The Rescue
• .NET port of the Java-based Hibernate Object-Relational Framework NHibernate 1.2 is (roughly) Hibernate 2.1 + some features of
Hibernate 3.0• “Hibernate's goal is to relieve the developer from 95 percent of
common data persistence related programming tasks.” Facilitates saves and retrieves of objects
• “Hibernate provides transparent persistence, the only requirement for a persistent class is a no-argument constructor.” Keeps your objects clean
• Open-source, free software under LGPL http://www.jboss.org/opensource/lgpl/faq
• Ported by Tom Barrett, Mike Doerfler, Peter Smulovics, and Sergey Koshcheyev
What is it?
• Object Relational Mapping Framework• XML-based• Mapping files
*.hbm.xml Classes/Properties Tables/Columns 1+ mapping files
• hibernate.cfg.xml Database Dialect: SQL Server, Oracle, MySQL, etc. Which assemblies to manage Which mapping files to use
What it will do for you.
• Make your life easier• Simplify your data access• Allow you to focus on your business tier
hibernate.cfg.xml<?xml version="1.0" encoding="utf-8" ?><hibernate-configuration xmlns="urn:nhibernate-configuration-2.0" > <session-factory name="NHibernate.Test">
<!–- Writes all SQL to Console --> <property name="show_sql">true</property> <property
name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
<property name="query.substitutions">true 1, false 0, yes 'Y', no 'N'</property><!–- SQL SERVER -->
<property name="connection.connection_string">Server=localhost\sql2005;initial catalog=bugs;User ID=sa;Password=sa_password;Min Pool Size=2</property>
<property name="dialect">NHibernate.Dialect.MsSql2000Dialect</property> <property name="connection.driver_class“
>NHibernate.Driver.SqlClientDriver</property> <!-- mapped assemblies --> <mapping assembly="NHibernateResearch.Business" /> </session-factory></hibernate-configuration>
Mapping files (*.hbm.xml)<?xml version="1.0" encoding="utf-8" ?><hibernate-mapping xmlns="urn:nhibernate-mapping-2.0"> <class
name="NHibernateResearch.Business.Person, NHibernateResearch.Business" table="Person">
<id name="PersonId" type="System.Int32" unsaved-value="0"> <generator class="native" /> </id> <timestamp name="LastModified" />
<property name="FirstName" not-null="true"></property> <property name="LastName" not-null="true"></property> </class></hibernate-mapping>
<class>
• Simplest mapping• Maps a class to a database table• Attributes
“name” = name of the persistent class Fully qualified class name, assembly name (no “.dll”)
“table” = name of the table
• Required element <id> = defines object identity
• Common elements <timestamp>, <version> for optimistic concurrency <property> for mapping class properties to database columns
<id>
<id name="PersonId" type="System.Int32" unsaved-value="0"> <generator class="native" /></id>
• Required element of <class> mapping • Used to establish object identity, database primary key• Attributes
“name” – name of the property “column” – (optional) name of the table column “type” – (optional) data type for the property “unsaved-value” – used to determine INSERT vs. UPDATE
• For “identity” columns, specify a <generator> class=“native” uses int identity column (SQL Server) or sequence (Oracle) class=“guid” generates guid keys
• For non-”identity” columns use <composite-id> This approach is strongly discouraged
<property>
<property name="LastName" not-null="true"></property> <property name="Html"> <column name="HtmlContent" sql-type="text" not-null="true"></column></property>
• Child element to <class>• Used to map a property to a database column• Attributes
“name” – name of the property “column” – (optional) name of the database column “not-null” – (optional) describes the nullability of the database column
defaults to nullable “type” – (optional) datatype for the property
• Optional <column> element describes information about the database column “sql-type” attribute – override default column datatype “length” attribute – override default column length
Mapping the Person class
<class name="NHibernateResearch.Business.Person, NHibernateResearch.Business" table="Person">
<id name="PersonId" type="System.Int32" unsaved-value="0"> <generator class="native" /> </id> <property name="FirstName" not-null="true"></property> <property name="LastName" not-null="true"></property> <property name="Email" not-null="true"></property> <property name="HomePhone" not-null="true"></property> <property name="WorkPhone" not-null="true"></property></class>
The NHibernate Session
• Main point of contact ISession interface
• SessionFactory.OpenSession() • Objects are associated the ISession
Lazy loading
• Save, delete, and retrieve operations
Saving
• SaveUpdate() Person person1 = new Person(); // createperson1.Name.FirstName = "firstname1"; // set propertiesperson1.Name.LastName = "lastname1";session.SaveOrUpdate(person1);session.Flush();session.Close();
Transactions
• Call BeginTransaction() on the session
public virtual void Save(object item){ ITransaction tx = null;
try { tx = m_session.BeginTransaction();
m_session.SaveOrUpdate(item);
tx.Commit(); } catch (Exception ex) { if (tx != null) tx.Rollback();
throw ex; }}
Retrieving
• Query / HQL syntax HQL = Hibernate Query Language SQL-like queries against the object model Use for more complex queries (JOINs)
• Criteria syntax Build the query programmatically
• NHibernate Allows Polymorphic Querying
HQL Sample•Find Employees by Supervisor’s Name
HQL Sample
public IList FindSupervisorEmployees(string firstName, string lastName)
{ string hqlQuery = @"
from Person p where p.Supervisor.FirstName = :firstName and p.Supervisor.LastName = :lastName";
IQuery query = Session.CreateQuery(hqlQuery); query.SetParameter("firstName", firstName); query.SetParameter("lastName", lastName);
return query.List();}
ICriteria: Load All By Type
• Gets an IList of objects by type or interfacepublic IList GetList(Type type){
ICriteria criteria = m_session.CreateCriteria(type);return criteria.List();
}
• Polymorphic queries Piano : MusicalInstrument Flute : MusicalInstrument CreateCriteria(typeof(MusicalInstrument))
Returns mix of Piano and Flute objects
ICriteria: Load By Property Value
• Gets a list of objects by type where a property has a certain value
public IList Get(Type type, string propertyName, object propertyValue)
{ ICriteria criteria = m_session.CreateCriteria(type);
criteria.Add(Expression.Eq(propertyName, propertyValue));
IList list = criteria.List();
return list; }
Deleting
• Session.Delete(object instance)
Making Person More Fine-grained
coarse-grained
fine-grained
fine-grained
Mapping Fine-grained Person
<component><component name="Name"
class="NHibernateResearch.Business.Name, NHibernateResearch.Business">
<property name="FirstName" not-null="true"></property> <property name="LastName" not-null="true"></property></component>
• Used to map columns in a table to properties on a different class
• Object does not have it’s own “identity” No primary key, no <id> Only exists in relation to the containing class
Cannot save an instance of “Name” by itself to the database
Mapping Person Using <component><class name="NHibernateResearch.Business.Person, NHibernateResearch.Business"
table="Person"> <id name="PersonId" unsaved-value="0"> <generator class="native" /> </id> <component name="Name" class="NHibernateResearch.Business.Name,
NHibernateResearch.Business"> <property name="FirstName" not-null="true"></property> <property name="LastName" not-null="true"></property> </component>
<component name="Email" class="NHibernateResearch.Business.Email, NHibernateResearch.Business">
<property name="Address" column="EmailAddress" not-null="true"></property> </component>
<component name="WorkPhone" class="NHibernateResearch.Business.Phone, NHibernateResearch.Business">
<property name="Number" column="WorkPhone" not-null="true"></property> </component>
<component name="HomePhone" class="NHibernateResearch.Business.Phone, NHibernateResearch.Business">
<property name="Number" column="HomePhone" not-null="true"></property> </component></class>
Concurrency Columns
• Child elements of <class>• Must be declared immediately after the <id>• <timestamp> – uses date/time data to manage concurrency• <version> – uses a numeric version id
Mapping Inheritance• Employee is a Person
<joined-subclass><class name="NHibernateResearch.Business.Person, NHibernateResearch.Business" table="Person"> <id name="PersonId" unsaved-value="0"> <generator class="native" /> </id> ... <joined-subclass name="NHibernateResearch.Business.Employee, NHibernateResearch.Business"
table="Employee"> <key column="EmployeeId"/> <property name="Title" not-null="true" /> <many-to-one name="Supervisor" column="SupervisorId" not-null="false" cascade="save-
update"></many-to-one> <bag name="Underlings" lazy="true" inverse="true" cascade="save-update"> <key column="SupervisorId"></key> <one-to-many
class="NHibernateResearch.Business.Employee, NHibernateResearch.Business" ></one-to-many>
</bag> </joined-subclass></class>
• Subclass gets its own table for its properties/columns• <joined-subclass> element goes inside of the superclass’ <class> or <joined-subclass>
element• <key> element defines the name of the column to use to join from the superclass’ <id>
(Person.PersonId Employee.EmployeeId)
Mapping Associations & Collections• Supervisor is an Employee• Employee has Underlings
Associations
• Associations define relationships between classes• NHibernate uses the association mappings to
automatically store and retrieve related objects• <one-to-one> – file has one owner• <one-to-many> – directory has many files• <many-to-one> – sub-directory has one parent
directory (many sub-directories, one parent)• <many-to-many> – Many users, many roles
intersection of a user and a role
<many-to-one>, <one-to-many> <joined-subclass name="NHibernateResearch.Business.Employee, NHibernateResearch.Business" table="Employee"> <key column="EmployeeId"/> <property name="Title" not-null="true" />
<many-to-one name="Supervisor" column="SupervisorId" not-null="false" cascade="save-update”></many-to-one>
<bag name="Underlings" lazy="true" inverse="true" cascade="save-update"> <key column="SupervisorId"></key> <one-to-many
class="NHibernateResearch.Business.Employee, NHibernateResearch.Business“ />
</bag> </joined-subclass>
• SupervisorId is a foreign-key to the supervisor’s EmployeeId• Many-to-one turns the SupervisorId into an instance of Employee• not-null=“false” means that the SupervisorId is nullable• cascade=“save-update” tells NHibernate to check the many-to-one relationship for
changes when an INSERT or UPDATE is requested• The association syntax is also used to populate collections of objects• <bag> populates an IList of Employee objects for the current employee
select * from employee where supervisorId=@currentEmployeeId
Collections
Mapping Element
Interface Implementation Description
<list> IList ArrayList Ordered collection
<bag> IList ArrayList Unordered collection, allows duplicates
<map> IDictionary Hashtable Dictionary (key/value pairs)
• Use associations to store collections of reference types– <one-to-many>, <many-to-many>
• Use <element> tag to store collections of value types– Similar syntax to <property>
Collections: <bag>Collection of objects
<bag name="Children" lazy="true" cascade="all"><key column="ParentId"></key><one-to-many class="classname"></one-to-many>
</bag>
Collection of values
<bag name="Children" lazy="true" cascade="all"><key column="ParentId"></key><element column=“columnName“ type="System.Int32"/>
</bag>
• System.Collections.IList• If collection of values, allows duplicates• If collection of objects, duplicates get eaten• By default, items in the collection same order as they in the tables
“order-by” attribute available to do database sorts on data
Collections: <list><list name="Children" lazy="true" cascade="all">
<key column="ParentId"></key><index column="indexValue"></index><one-to-many class=“classname" ></one-to-many>
</list>
• System.Collections.IList• Uses an numeric, zero-based index column for sorting• Missing index values become null
child0.Index = 0;// no child with Index of 1child1.Index = 2;
parent.Children.Add(child0);parent.Children.Add(child1);
session.Save(parent);
// reload the parent
parent.Children[0] child0;parent.Children[1] null;parent.Children[2] child1;
Collections: <map>
<map name="Children" cascade="all"> <key column="ParentId"></key> <index column="indexval" type="System.Int32"></index> <one-to-many class="classname" /></map>
• System.Collections.IDictionary• Unsorted collection uses Hashtable• Collection of key/value pairs• Use <index> for a value type key• Use <index-many-to-many> for an object key
<index-many-to-many column="ItemId" class="classname" />
• If sorted, collection uses SortedList “order-by” attribute available to do database sorts on data “sort” attribute for sorting using an implementation of IComparer
Collections
• Lazy loading lazy = “true” Expose collections interface not concrete class
ArrayList IList Hashtable IDictionary
Lazy-load proxy loads on first access• “inverse” attribute for bi-directionality
“child” has a reference back to the parent Employee has Supervisor Supervisor has Employees
• “cascade” attribute
The “cascade” attribute
• Controls when changes to child objects are saved• Options:
“none” – no cascading saves/deletes “save-update” – cascade for INSERTs and UPDATEs “delete” – cascade on DELETEs only “all” – cascade on INSERT, UPDATE, DELETE “all-delete-orphan” – same as all, automatically delete
any child objects when the parent’s collection
NHibernate, Collections, and Generics• Current release is NHibernate 1.2.1• Now supports generics
Nullable Columns & ValueTypes
• Nullable columns should be avoided even if you don’t use NHibernate
• Sometimes you need them• Sometimes you’re stuck with them (legacy databases)• Under .NET 2.0 – Use the “?” syntax for the nullable
properties on your classes
Auditing Info
• Auditing info common to all tables / classes CreateDate,
CreatedBy LastModified,
LastModifiedBy
• Base class functionality
• How to know when to update the values?
ILifecycle
• Allows class to perform actions at certain times during the NHibernate “lifecycle” of the instance
• OnSave(), OnUpdate(), OnDelete(), OnLoad()
• Slightly “pollutes” object model with NHibernate specific code
• Now is deprecated IInterceptor
Code Demo
• Refactor Person to extend BusinessBase• Implement auditing on Person
Intellisense for NHibernate
• Eliminate the tedium• Eliminate the potential errors• Learn what else is in there• Download the source• Schemas in “src\NHibernate”
nhibernate-configuration-2.0.xsd nhibernate-mapping-2.0.xsd nhibernate-generic.xsd
• Copy to Visual Studio’s “Schemas” directory “c:\program files\Microsoft Visual Studio 8\Xml\Schemas”
• Restart Visual Studio
Contains(), Evict(), Lock(), Refresh()• Methods on NHibernate Session• Contains(object) – Queries the session to
determine if it is managing the supplied object Is the object persistent for the session
• Evict(object) – Remove an object from the session’s control
• Lock(object, LockMode) – Request that the object becomes persistent for the session
• Refresh(object) – Update the object with the current data from the database
“assembly” & “namespace”
• Attributes on <hibernate-mapping>• Specifies default
Assembly name Namespace
• This name="Com.Benday.ContentManagement.Business.DomainModel.Folder, Com.Benday.ContentManagement.Business”
name=“Folder”
Simplify Data Access With Generics• Persistent objects operations are almost identical
ISession Save(), Get(), Delete() ICriteria GetBy***()
• Leads to code duplication in your façade (aka Factory, Finder, Data Access) classes
• Use .NET 2.0 generics to Simplify Gain compile-time type safety
Code Walkthrough
• Com.Benday.NHibernate• BusinessFacade<>
Mapping: <query>
• Allows you to write HQL complex queries and store it separately from the code
• Assigned a unique name• Accessed through ISession.GetNamedQuery()• Executed via IQuery.List()
Mapping: <sql-query>
• Write SQL queries against the native database• Still brings back persistent objects• ISession.GetNamedQuery(), IQuery.List()• <return class=“classname” alias=“table alias” />• Table alias must be in “{ }”
Design Strategies
• Model to schema Create the object model first, then create mappings Have NHibernate generate the database schema Allows you to be much more database independent Concentrate on creating a good object model and less about storage This is the best to start learning NHibernate
-Write some code, write some mappings -Export the schema through NHibernate -Look at the database tables and FK relationships and see if it’s what you
expected• Schema to model
Database first, then classes, then mappings Legacy development More difficult, more about the needs of the database and less focus on the object
model
Create Database Using SchemaExport • NHibernate.Tool.hbm2ddl namespace• Reads hbm’s and classes creates database schema• Reads the database dialect generates for your db (MySql, Sql
Server, Oracle, etc etc)public static void ExportSchema(){ Configuration config = new Configuration(); config.Configure();
SchemaExport exporter = new SchemaExport(config);
exporter.Drop(true, true); exporter.Create(true, true);}
• Great for unit testing always have a clean db db model always in sync with the hbm’s
Code Demo
• Use BusinessFacade<> • Use schema export from unit tests• Create an fixture base class• Create a PersonFixture• Create a Person class• Play around with [TestInitialize] & [TestCleanup]
Mappings: Create a UNIQUE database constraint
• “unique-key” attribute on the <column> mapping• Set the same value on all properties that should
participate in the key
Benjamin Day• Consultant, Trainer• Architecture, Development, Project Coaching• Microsoft VSTS Customer Advisory Council• MVP for C#• Leader of Beantown .NET User Group• VSLive, O’Reilly Conferences• Team System, Team Foundation Server,
NHibernate, C#, ASP.NET, TDD, WCF• http://blog.benday.com• [email protected]