Related Links
This document was copied from MSDN here Application Architecture for .NET: Designing Applications and Services Angela Crocker, Andy Olsen, and Edward Jezierski August 2002 Applies to: Summary: Learn how to best expose your data to Microsoft .NET applications and how to implement an effective strategy for passing data between the tiers in a distributed application. (65 printed pages) DownloadDownload Designing Data Tier Components and Passing Data Through Tiers in .pdf format. ContentsIntroduction
IntroductionWhen designing a distributed application, you need to decide how to access and represent the business data associated with your application. This document provides guidance to help you choose the most appropriate way of exposing, persisting and passing that data through the tiers of an application. Figure 1 depicts the common tiers of a distributed application. This document distinguishes between business data and the business processes that use the data; the business process tier is discussed only where needed for clarification. Likewise, the presentation tier is discussed only where there are direct implications for the way data is represented, such as the way Microsoft® ASP.NET Web pages expose business data. Figure 1 introduces two new terms: data access logic components and business entity components. These terms are described later in this document.
Figure 1. Accessing and representing data in a distributed application Most applications store data in relational databases. While there are other options for storing data, this document focuses on how .NET applications interact with relational databases, and does not specifically discuss how to interact with data held in other data stores such as flat files or non-relational databases. This document makes a clear distinction between persistence logic and the data itself. The reasons for separating persistence logic from the data include the following:
To attain the distinction between persistence logic and the data itself, this document proposes two different component types.
Data Access Logic ComponentsA Data Access Logic Component provides methods to perform the following tasks upon a database, on behalf of the caller:
The methods that perform the preceding tasks are often called "CRUD" methods, where CRUD is an acronym based on the first letter of each task. The Data Access Logic Component also has methods to implement business logic against the database. For example, a Data Access Logic Component might have a method to find the highest-selling product in a catalog for this month. Typically, a Data Access Logic Component accesses a single database and encapsulates the data-related operations for a single table or a group of related tables in the database. For example, you might define one Data Access Logic Component to deal with the Customer and Address tables in a database, and another Data Access Logic Component to deal with the Orders and OrderDetails tables. The design decisions for mapping data access logic components to database tables are discussed later in this document. Representing Business EntitiesEach Data Access Logic Component deals with a specific type of business entity. For example, the Customer Data Access Logic Component deals with Customer business entities. There are many different ways to represent business entities, depending on factors such as the following:
This document outlines the advantages and disadvantages of the following implementation options:
Technical ConsiderationsFigure 2 shows some of the technical considerations that influence the implementation strategy for data access logic components and business entities. This document addresses each of these technical considerations and provides recommendations.
Figure 2. Technical considerations that influence the design of data access logic components and business entities Mapping Relational Data to Business EntitiesDatabases typically contain many tables, with relationships implemented by primary keys and foreign keys in these tables. When you define business entities to represent this data in your .NET application, you must decide how to map these tables to business entities. Consider the hypothetical retailer's database shown in Figure 3.
Figure 3. Hypothetical table relationships in a relational database The following table summarizes the types of relationships in the example database.
When you define business entities to model the information in the database, consider how you will use the information in your application. Identify the core business entities that encapsulate your application's functionality, rather than defining a separate business entity for each table. Typical operations in the hypothetical retailer's application are as follows:
To fulfill these application requirements, there are three logical business entities that the application will handle: a Customer, an Order and a Product. For each business entity, a separate Data Access Logic Component will be defined as follows:
Figure 4 illustrates the relationships between the data access logic components and the tables that they represent in the database.
Figure 4. Defining data access logic components to expose relational data to .NET applications For a description of how to implement data access logic components, see Implementing Data Access Logic Components later in this document. Recommendations for Mapping Relational Data to Business EntitiesTo map relational data to business entities, consider the following recommendations:
Implementing Data Access Logic ComponentsA Data Access Logic Component is a stateless class, meaning that all messages exchanged can be interpreted independently. No state is held between calls. The Data Access Logic Component provides methods for accessing one or more related tables in a single database, or in some instances, multiple databases as in the case of horizontal database partitioning. Typically, the methods in a Data Access Logic Component invoke stored procedures to perform their operations. One of the key goals of data access logic components is to hide the invocation and format idiosyncrasies of the database from the calling application. Data access logic components provide an encapsulated data-access service to these applications. Specifically, data access logic components handle the following implementation details:
Some of these issues are explored in more detail later in this section. Application Scenarios for Data Access Logic ComponentsFigure 5 shows how a Data Access Logic Component can be called from a variety of application types, including Windows Forms applications, ASP.NET applications, XML Web services and business processes. These calls might be local or remote, depending on how you deploy your applications. Figure 5. Application scenarios for data access logic components (click thumbnail for larger image)
Implementing Data Access Logic Component ClassesData access logic components use ADO.NET to execute SQL statements or call stored procedures. For an example of a Data Access Logic Component class, see How to Define a Data Access Logic Component Class in the appendix. If your application contains multiple data access logic components, you can simplify the implementation of Data Access Logic Component classes by using a data access helper component. This component can help manage database connections, execute SQL commands and cache parameters. The data access logic components still encapsulate the logic required to access the specific business data, whereas the data access helper component centralizes data access API development and data connection configuration, thereby helping to reduce code duplication. Microsoft provides the Data Access Application Block for .NET, which can be used as a generic data access helper component in your applications when you use Microsoft SQL Server™ databases. Figure 6 shows how to use the data access helper component to help implement data access logic components.
Figure 6. Implementing data access logic components by using the data access helper component If there are utility functions that are common to all of your data access logic components, you can define a base class for data access logic components to inherit from and extend. Design your Data Access Logic Component classes to provide a consistent interface for different types of clients. If you design the Data Access Logic Component to be compatible with the requirements of your current and potential business process tier implementation, you can reduce the number of additional interfaces, facades or mapping layers that you must implement. To support a diverse range of business processes and applications, consider the following techniques to pass data to and from Data Access Logic Component methods:
The following sections present the options for passing business entity data to and from your data access logic components, in addition to the advantages and disadvantages of each approach. This information will help you to make an informed choice based on your specific application scenario. Passing Scalar Values As Inputs and OutputsThe advantages of this option are as follows:
The disadvantages of this option are as follows:
Passing XML Strings As Inputs and OutputsThe advantages of this option are as follows:
The disadvantages of this option are as follows:
Passing DataSets As Inputs and OutputsThe advantages of this option are as follows:
The disadvantages of this option are as follows:
Passing Custom Business Entity Components As Inputs and OutputsThe advantages of this option are as follows:
The disadvantages of this option are as follows:
Returning Data Readers As OutputsThe advantage of this option is as follows:
The disadvantage of this option is as follows:
Using Stored Procedures in Conjunction with Data Access Logic ComponentsYou can use stored procedures to perform many of the data access tasks supported by data access logic components. Advantages
Despite the advantages listed above, there are some situations where the use of stored procedures is not recommended or may be infeasible. Disadvantages
Recommendations for Using Stored Procedures with Data Access Logic ComponentsConsider the following recommendations for using stored procedures in conjunction with data access logic components:
Managing Locking and ConcurrencySome applications take the "Last in Wins" approach when it comes to updating data in a database. With the "Last in Wins" approach, the database is updated, and no effort is made to compare updates against the original record, potentially overwriting any changes made by other users since the records were last refreshed. However, at times it is important for the application to determine if the data has been changed since it was initially read, before performing the update. Data access logic components implement the code to manage locking and concurrency. There are two ways to manage locking and concurrency:
Using Pessimistic ConcurrencyPessimistic concurrency is primarily used in environments where there is heavy contention for data, and where the cost of protecting data through locks is less than the cost of rolling back transactions if concurrency conflicts occur. Pessimistic concurrency is best implemented when lock times will be short, as in programmatic processing of records. Pessimistic concurrency requires a persistent connection to the database and is not a scalable option when users are interacting with data, because records might be locked for relatively large periods of time. Using Optimistic ConcurrencyOptimistic concurrency is appropriate in environments where there is low contention for data, or where read-only access to data is required. Optimistic concurrency improves database performance by reducing the amount of locking required, thereby reducing the load on the database server. Optimistic concurrency is used extensively in .NET to address the needs of mobile and disconnected applications, where locking data rows for prolonged periods of time would be infeasible. Also, maintaining record locks requires a persistent connection to the database server, which is not possible in disconnected applications. Testing for Optimistic Concurrency ViolationsThere are several ways to test for optimistic concurrency violations:
Manually Implementing Optimistic ConcurrencyConsider the following SQL query: SELECT Column1, Column2, Column3 FROM Table1 To test for an optimistic concurrency violation when updating a row in Table1, issue the following UPDATE statement: UPDATE Table1 Set Column1 = @NewValueColumn1, Set Column2 = @NewValueColumn2, Set Column3 = @NewValueColumn3 WHERE Column1 = @OldValueColumn1 AND Column2 = @OldValueColumn2 AND Column3 = @OldValueColumn3 If the original values match the values in the database, the update is performed. If a value has been modified, the update will not modify the row because the WHERE clause will not find a match. You can use a variation of this technique and apply a WHERE clause only to specific columns, resulting in data being overwritten unless particular fields have been updated since they were last queried. Note Always return a value that uniquely identifies a row in your query, such as a primary key, to use in your WHERE clause of the UPDATE statement. This ensures that the UPDATE statement updates the correct row or rows. If a column at your data source allows nulls, you may need to extend your WHERE clause to check for a matching null reference in your local table and at the data source. For example, the following UPDATE statement verifies that a null reference in the local row still matches a null reference at the data source, or that the value in the local row still matches the value at the data source: UPDATE Table1 Set Column1 = @NewColumn1Value WHERE (@OldColumn1Value IS NULL AND Column1 IS NULL) OR Column1 = @OldColumn1Value Using Data Adapters and DataSets to Implement Optimistic ConcurrencyThe DataAdapter.RowUpdated event can be used in conjunction with the techniques described earlier to provide notification to your application of optimistic concurrency violations. The RowUpdated event occurs after each attempt to update a modified row from a DataSet. You can use the RowUpdated event to add special handling code, including processing when an exception occurs, adding custom error information and adding retry logic. The RowUpdated event handler receives a RowUpdatedEventArgs object, which has a RecordsAffected property that indicates how many rows were affected by an update command for a modified row in a table. If you set the update command to test for optimistic concurrency, the RecordsAffected property will be 0 when an optimistic concurrency violation occurs. Set the RowUpdatedEventArgs.Status property to indicate how to proceed; for example, set the property to UpdateStatus.SkipCurrentRow to skip updating the current row, but to continue updating the other rows in the update command. For more information about the RowUpdated event, see Working with DataAdapter Events. An alternative way to test for concurrency errors with a data adapter is to set the DataAdapter.ContinueUpdateOnError property to true before you call the Update method. When the update is completed, call the GetErrors method on the DataTable object to determine which rows have errors. Then, use the RowError property on these rows to find specific error details. For more information about how to process row errors, see Adding and Reading Row Error Information. The following code sample shows how the Customer Data Access Logic Component can check for concurrency violations. This example assumes that the client has retrieved a DataSet, made changes to the data, and then passed the DataSet to the UpdateCustomer method on the Data Access Logic Component. The UpdateCustomer method will invoke the following stored procedure to update the appropriate customer record; the stored procedure updates the record only if the customer ID and company name have not already been modified: CREATE PROCEDURE CustomerUpdate { @CompanyName varchar(30), @oldCustomerID varchar(10), @oldCompanyName varchar(30) } AS UPDATE Customers Set CompanyName = @CompanyName WHERE CustomerID = @oldCustomerID AND CompanyName = @oldCompanyName GO Inside the UpdateCustomer method, the following code sample sets the UpdateCommand property of a data adapter to test for optimistic concurrency, and then uses the RowUpdated event to test for optimistic concurrency violations. If an optimistic concurrency violation is encountered, the application indicates the violation by setting the RowError of the row for which the update was issued. Note that the parameter values passed to the WHERE clause of the UPDATE command are mapped to the Original values of the respective columns in the DataSet. // UpdateCustomer method in the CustomerDALC class public void UpdateCustomer(DataSet dsCustomer) { // Connect to the Northwind database SqlConnection cnNorthwind = new SqlConnection( "Data source=localhost;Integrated security=SSPI;Initial Catalog=northwind"); // Create a Data Adapter to access the Customers table in Northwind SqlDataAdapter da = new SqlDataAdapter(); // Set the Data Adapter's UPDATE command, to call the "UpdateCustomer" stored procedure da.UpdateCommand = new SqlCommand("CustomerUpdate", cnNorthwind); da.UpdateCommand.CommandType = CommandType.StoredProcedure; // Add two parameters to the Data Adapter's UPDATE command, to specify information // for the WHERE clause (to facilitate checking for optimistic concurrency violation) da.UpdateCommand.Parameters.Add("@CompanyName", SqlDbType.NVarChar, 30, "CompanyName"); // Specify the original value of CustomerID as the first WHERE clause parameter SqlParameter myParm = da.UpdateCommand.Parameters.Add( "@oldCustomerID", SqlDbType.NChar, 5, "CustomerID"); myParm.SourceVersion = DataRowVersion.Original; // Specify the original value of CustomerName as the second WHERE clause parameter myParm = da.UpdateCommand.Parameters.Add( "@oldCompanyName", SqlDbType.NVarChar, 30, "CompanyName"); myParm.SourceVersion = DataRowVersion.Original; // Add a handler for Row Update events da.RowUpdated += new SqlRowUpdatedEventHandler(OnRowUpdated); // Update the database da.Update(ds, "Customers"); foreach (DataRow myRow in ds.Tables["Customers"].Rows) { if (myRow.HasErrors) Console.WriteLine(myRow[0] + " " + myRow.RowError); } } // Method to handle Row Update events. If you register the event but do not handle it, // a SQL exception is thrown. protected static void OnRowUpdated(object sender, SqlRowUpdatedEventArgs args) { if (args.RecordsAffected == 0) { args.Row.RowError = "Optimistic Concurrency Violation Encountered"; args.Status = UpdateStatus.SkipCurrentRow; } } When executing multiple SQL statements within one SQL Server stored procedure, you may, for performance reasons, choose to use the SET NOCOUNT ON option. This option prevents SQL Server from returning a message to the client after completion of each statement, thus reducing network traffic. However, you will not be able to check the RecordsAffected property as demonstrated in the previous code sample. The RecordsAffected property will always be–1. An alternative is to return the @@ROWCOUNT function (or specify it as an output parameter) in your stored procedure; @@ROWCOUNT contains the record count for the last statement completed in your stored procedure and is updated even when SET NOCOUNT ON is used. Therefore, if the last SQL statement executed in your stored procedure is the actual UPDATE statement and you specify the @@ROWCOUNT as a return value, you can modify application code as follows: // Add another parameter to Data Adapter's UPDATE command, to accept the return value. // You can name it anything you want. myParm = da.UpdateCommand.Parameters.Add("@RowCount", SqlDbType.Int); myParm.Direction = ParameterDirection.ReturnValue; // Modify the OnRowUpdated method, to check the value of this parameter // instead of the RecordsAffected property. protected static void OnRowUpdated(object sender, SqlRowUpdatedEventArgs args) { if (args.Command.Parameters["@RowCount"].Value == 0) { args.Row.RowError = "Optimistic Concurrency Violation Encountered"; args.Status = UpdateStatus.SkipCurrentRow; } } COM InteroperabilityIf you want your Data Access Logic Component class to be callable from COM clients, the recommended approach is to define your data access logic components by using the preceding guidelines and provide a wrapper component. However, if you want COM clients to be able to access your data access logic components, consider the following recommendations:
For more information about COM interoperability, see the Microsoft .NET/COM Migration and Interoperability guide. Implementing Business EntitiesBusiness entities exhibit the following characteristics:
As mentioned earlier in this document, there are various ways to represent business entities in your application, ranging from a data-centric model to a more object-oriented representation:
The sections that follow describe how to represent business entity data in each of these formats. To help you decide the most appropriate representation for business entities in your particular circumstances, the sections describe how to perform the following tasks for each business entity format:
The sections also consider the suitability of each business entity representation in terms of nonfunctional requirements, including performance, efficiency, scalability, and extensibility. Representing Business Entities as XMLThe following example shows how to represent a simple business entity as XML. The business entity consists of a single product. <?xml version="1.0"?> <Product xmlns="urn:aUniqueNamespace"> <ProductID>1</ProductID> <ProductName>Chai</ProductName> <QuantityPerUnit>10 boxes x 20 bags</QuantityPerUnit> <UnitPrice>18.00</UnitPrice> <UnitsInStock>39</UnitsInStock> <UnitsOnOrder>0</UnitsOnOrder> <ReorderLevel>10</ReorderLevel> </Product> For more information, see How to Use XML to Represent Collections and Hierarchies of Data in the appendix. When you use XML to represent business entity data, consider the following guidelines:
For more information and performance considerations, see Performance Comparison: Data Access Techniques. The advantages of representing business entities as XML are as follows:
The disadvantages of representing business entities as XML are as follows:
Representing Business Entities As a Generic DataSetA generic DataSet is an instance of the DataSet class, which is defined in the System.Data namespace in ADO.NET. A DataSet object contains one or more DataTable objects to represent information that the Data Access Logic Component retrieves from the database. Figure 7 shows a generic DataSet object for the Product business entity. The DataSet object has a single DataTable to hold information for products. The DataTable has a UniqueConstraint object to denote the ProductID column as a primary key. The DataTable and UniqueConstraint objects are created when the DataSet is created in the Data Access Logic Component.
Figure 7. Generic DataSet for the Product business entity Figure 8 shows a generic DataSet object for the Order business entity. This DataSet object has two DataTable objects to hold information for the orders and order details, respectively. Each DataTable has a corresponding UniqueConstraint object to identify the primary key in that table. Additionally, the DataSet has a Relation object to associate order details with orders.
Figure 8. Generic DataSet for the Order business entity The following code shows how to retrieve a generic DataSet from a Data Access Logic Component, bind the DataSet to a DataGrid control, and then pass the DataSet to the Data Access Logic Component to save data changes: // Create a ProductDALC object ProductDALC dalcProduct = new ProductDALC(); // Call a method on ProductDALC to get a DataSet containing information for all products DataSet dsProducts = dalcProduct.GetProducts(); // Use DataSet in the client. For example, bind the DataSet to user interface controls dataGrid1.DataSource = dsProducts.Tables[0].DefaultView; dataGrid1.DataBind(); // When you are ready, pass the updated DataSet to the ProductDALC to save the changes // back to the database dalcProduct.UpdateProducts(dsProducts); You can also query and modify the tables, constraints, and relations in a DataSet at run time. For more information, see Creating and Using DataSets. The advantages of representing business entities as a generic DataSet are as follows:
The disadvantages of representing business entities as a generic DataSet are as follows:
Representing Business Entities As a Typed DataSetA typed DataSet is a class that contains strongly typed methods, properties, and type definitions to expose the data and metadata in a DataSet. For an example of how to create a typed DataSet, see How to Create a Typed DataSet in the appendix. The following lists describe the advantages and disadvantages of typed DataSets, compared with generic DataSets. Note that typed DataSets exhibit approximately the same instantiation and marshalling performance as generic DataSets. The advantages of representing business entities as a typed DataSet are as follows:
The disadvantages of representing business entities as a typed DataSet are as follows:
Defining Custom Business Entity ComponentsCustom classes that represent business entities typically contain the following members:
Figure 9 shows how custom entity classes are used. Note that the entity class has no knowledge of the Data Access Logic Component or the underlying database; all database access is performed by the Data Access Logic Component to centralize data access policies and business logic. Also, the way in which you pass business entity data through the tiers is not directly tied to the format of your business entity representation; for example, you can represent business entities locally as objects, yet choose a different approach (such as scalar values or XML) to pass business entity data to a different tier. Figure 9. The role of custom business entity components (click thumbnail for larger image)
Recommendations for Defining Custom Business Entity ComponentsTo help you implement your custom entity components, consider the following recommendations:
The advantages of defining a custom entity are as follows:
The disadvantages of defining a custom entity are as follows:
Defining Custom Business Entity Components with CRUD BehaviorsWhen you define a custom entity, you can provide methods to completely encapsulate the CRUD operations on the underlying Data Access Logic Component. This is the more traditional object-oriented approach, and may be appropriate for complex object domains. The client application no longer accesses the Data Access Logic Component class directly. Instead, the client application creates an entity component and calls CRUD methods on the entity component. These methods forward to the underlying Data Access Logic Component. Figure 10 shows the role of custom entity classes with CRUD behaviors. Figure 10. The role of custom business entity components with CRUD behaviors (click thumbnail for larger image)
The advantages of defining custom entity classes with CRUD behaviors are as follows:
The disadvantages of defining custom entity classes with CRUD behaviors are as follows:
Recommendations for Representing Data and Passing Data Through TiersThe way in which you represent data throughout your application, and the way in which you pass that data through the tiers, do not necessarily need to be the same. However, having a consistent and limited set of formats yields performance and maintenance benefits that reduce your need for additional translation layers. The data format that you use should depend on your specific application requirements and how you want to work with the data. There is no universal way to represent your data, especially because many of today's applications are required to support multiple callers. However, it is recommended that you follow these general guidelines:
TransactionsMost of today's applications need to support transactions for maintaining the integrity of a system's data. There are several approaches to transaction management; however, each approach fits into one of two basic programming models:
This section provides guidance and recommendations to help you implement transaction support in data access logic components and business entity components. For examples and a more in-depth discussion of transactions in .NET, see the .NET Data Access Architecture Guide. Implementing TransactionsIn most circumstances, the root of the transaction is the business process rather than a Data Access Logic Component or a Business Entity Component. The reason is that business processes typically require transactions that span multiple business entities, not just a single business entity. However, situations may arise where you need to perform transactional operations on a single business entity without the assistance of a higher-level business process. For example, to add a new customer to the database discussed earlier, you must perform the following operations:
Both of these operations must succeed, or the customer will not be added to the database. If the Customer business entity will never be a part of a larger business process that will initiate the transaction, use manual transactions within the Customer business entity. Manual transactions are significantly faster than automatic transactions because they do not require any interprocess communication with the Microsoft Distributed Transaction Coordinator (DTC). Figure 11 shows how to decide whether to use manual transactions or automatic transactions. Due to the overhead of COM+ transactions, the recommended approach is to push the transaction to the database and control the transactional behavior in stored procedures where possible.
Figure 11. Deciding how to implement transactions Note If you are calling from an ASP.NET-based client, and there is no business process to initiate the transaction, you may be tempted to start the transaction in the ASP.NET code-behind. This is not a good design; you should never initiate a transaction from an ASP.NET-based client. Instead, separate the presentation of the data from the business process. Performance is also an issue due to issues such as network latency, because this is the most common layer to be physically deployed on another tier. Recommendations for Using Manual Transactions in Data Access Logic ComponentsWhen you implement manual transactions in data access logic components, consider the following recommendations:
Recommendations for Using Automatic Transactions in Data Access Logic ComponentsDespite the overhead associated with COM+ transactions, automatic transactions provide a simpler programming model than manual transactions, and are necessary when your transactions span multiple distributed data sources as they work in conjunction with the DTC. If you implement automatic transactions in data access logic components, consider the following recommendations:
The following code sample shows how to support automatic transactions in a Data Access Logic Component class: using System.EnterpriseServices; [Transaction(TransactionOption.Supported)] public class CustomerDALC : ServicedComponent { ... } If you use automatic transactions, your data access logic components should vote in transactions to indicate whether the operation succeeded or failed. To vote implicitly, annotate your methods by using the AutoComplete attribute and throw an exception if the operation fails. To vote explicitly, call the SetComplete or SetAbort method on the ContextUtil class. For more information about automatic transactions, see "Using Automatic Transactions" in the .NET Data Access Architecture Guide. Using Automatic Transactions in Business Entity ComponentsIf you implement custom business entity components that have behaviors, you can use automatic transactions to specify the transactional behavior of these objects. The recommendations for using automatic transactions to specify the transactional behavior of business entity components are the same as the previously listed recommendations for implementing automatic transactions in data access logic components. Note If the Business Entity Component does not contain any business logic that requires it to vote in the transaction, it can ignore the transaction context altogether. The custom Business Entity Component does not need to inherit from ServicedComponent; the transaction context will still flow, but the entity component will ignore the context. ValidationsYou can perform data validation at many tiers in your application. Different types of validation are appropriate in each tier:
There are two general kinds of validation:
At times, you may want to implement an out-of-band aggregator or a transformer process. This approach may be useful if the validations and transformations change often, but it incurs a performance penalty. For example, if an ISV wanted to use the same components to support two versions of a database schema, you could create a separate component to perform validation and transformations between the two database schema versions. How to Validate XML by Using an XSD SchemaTo validate an XML document against an XSD schema, take the following steps:
How to Validate Data in Property Accessors in Business Entity ComponentsThe following code fragment shows how to perform simple validation in property accessors in a custom entity. If the validation test fails, you can throw an exception to indicate the nature of the problem. You can also use regular expressions within your set property accessors to validate specific data and formats. public class ProductDALC { Â… public short ReorderLevel { get { return reorderLevel; } } set { if (value < 0) { throw new ArgumentOutOfRangeException("ReorderLevel cannot be negative."); } reorderLevel = value; } // Plus other members in the ProductDALC class... } Exception ManagementWhen errors occur in .NET applications, the general advice is to throw exceptions rather than return error values from your methods. This advice has implications for the way you write data access logic components and business entity components. There are two general kinds of exceptions that will occur:
Recommendations for Managing Exceptions in a Data Access Logic ComponentData access logic components should propagate exceptions, and should wrap exception types only if doing so makes the exception easier for the client to manage. Wrapping the exceptions in two main exception types (technical and business) helps exception handling structure and exception publishing logic for the potentially diverse callers. Your application should publish exception information. Technical exceptions can be published to a log that is monitored by administrators or by a Windows Management Instrumentation (WMI) monitoring tool like Microsoft Operations Manager. Business exceptions can be published to an application-specific log. In general, allow the exceptions to propagate from your Data Access Logic Component and be published by the caller so that you have the entire context of the exception. The following example illustrates these recommendations: public class CustomerDALC { public void UpdateCustomer(Dataset aCustomer) { try { // Update the customer in the database... } catch (SqlException se) { // Catch the exception and wrap, and rethrow throw new DataAccessException("Database is unavailable", se); } finally { // Cleanup code } } } Recommendations for Managing Exceptions in Business Entity ComponentsBusiness entity components should propagate exceptions to callers. Business entity components may also raise exceptions if they are performing validation, or if the caller tries to perform an operation without providing all the required data for the operation. The following example illustrates how a business entity component can raise an exception. In this example, the Update method throws an exception if the customer's first name is not provided: public class CustomerEntity { public void Update() { // Check that the user has provided the required data. In this case a first name for // the customer if (FirstName == "" ) { // Throw a new application exception that you have defined throw new MyArgumentException("You must provide a First Name.); } ... } } For more information about dealing with exceptions in .NET applications, see Exception Management in .NET. You can inherit your custom technical exceptions and custom business exceptions from the ApplicationException class or the BaseApplicationException class provided in the Exception Management Application Block. Authorization and SecurityThis section explains how security applies to your data access logic components and your business entity components. The .NET common language runtime uses permissions objects to implement its mechanism for enforcing restrictions on managed code. There are three kinds of permissions objects, each with a specific purpose:
Managed code can discover the identity or the role of a principal by using a Principal object, which contains a reference to an Identity object. It might be helpful to compare identity and principal objects to familiar concepts like user and group accounts. In the .NET Framework, identity objects represent users, whereas roles represent memberships and security contexts. The principal object encapsulates both an identity object and a role. Applications in the .NET Framework grant rights to the principal object based on its identity or, more commonly, its role membership. For more information about permissions and security in .NET, see Key Security Concepts. Recommendations for Security in Data Access Logic ComponentsData access logic components are designed to be used by other application components, and are the last place in your application code where you can implement security before the caller has access to your data. Often, data access logic components can rely on the security context set by the caller. However, there are some situations where the Data Access Logic Component should perform its own authorization checks to determine whether a principal is allowed to perform a requested action. Authorization occurs after authentication and uses information about the principal's identity and roles to determine what resources the principal can access. Perform authorization checks at the Data Access Logic Component level if you need to:
After you define identity and principal objects, there are three different ways to perform role-based security checks:
The following code sample shows how to use PrincipalPermissionAttribute to specify a declarative role-based security check on a method in a Data Access Logic Component class: using System; using System.Security.Permissions; public class CustomerDALC { public CustomerDALC() { } // Use PrincipalPermissionAttribute to demand that the caller of this method // has an identity named "MyUser", and belongs to the "Administrator" role. [PrincipalPermissionAttribute(SecurityAction.Demand, Name="MyUser", Role="Administrator")] public void DeleteCustomer(string customerID) { // Delete customer code here } } The following code shows how to create a principal object that has the required identity and role, so that you can call the DeleteCustomer method on a CustomerDALC object: using System; using System.Security.Principal; using System.Threading; public class MainClass { public static int Main(string[] args) { Console.Write("User Name: "); string UserName = Console.ReadLine(); Console.Write("Password: "); string Password = Console.ReadLine(); if (Password == "password" && UserName == "MyUser") { // Create a generic identity with the name "MyUser" GenericIdentity MyIdentity = new GenericIdentity("MyUser"); // Create roles String[] MyString = {"Administrator", "User"}; // Create a generic principal GenericPrincipal MyPrincipal = new GenericPrincipal(MyIdentity, MyString); // Set this thread's current principal, for use in role-based security Thread.CurrentPrincipal = MyPrincipal; } // Create a CustomerDALC object, and try to call its DeleteCustomer method. // This will only succeed if the current principal's identity and role are OK. CustomerDALC c = new CustomerDALC(); c.DeleteCustomer("VINET"); } } Windows AuthenticationIdeally, you should use Windows Authentication, rather than using SQL Server Authentication, when you connect to the database. However, you should use service accounts and avoid impersonating through to the database, because this will impede connection pooling. Connection pooling requires identical connection strings; if you try to open the database by using different connection strings, you will create separate connection pools, which will limit scalability. For more information about Windows Authentication and connection pooling, see the "Managing Database Connections" section in the .NET Data Access Architecture Guide. Secure Communication RecommendationsTo achieve secure communication between calling applications and data access logic components, consider the following recommendations:
Recommendations for Security in Business Entity ComponentsIf you implement your business entities as data structures (such as XML or DataSets), you do not need to implement security checks. However, if you implement your business entities as custom business entity components with CRUD operations, consider the following recommendations:
DeploymentThis section provides recommendations to help you decide how to deploy data access logic components and business entity components. Deploying Data Access Logic ComponentsThere are two ways to deploy data access logic components:
Deploying Business EntitiesBusiness entities are used at many different tiers in your application. Depending on how you implement your business entities, you may need to deploy them to multiple locations if your application spans physical tiers. The following list describes how to deploy business entities in different implementation scenarios:
AppendixHow to Define a
Data Access Logic Component Class How to Define a Data Access Logic Component ClassThe code that follows is a sample definition of a class named CustomerDALC, which is the Data Access Logic Component class for Customer business entities. The CustomerDALC class implements the CRUD operations for the Customer business entity and provides additional methods to encapsulate business logic for this object. public class CustomerDALC { private string conn_string; public CustomerDALC() { // Acquire the connection string from a secure or encrypted location // and assign to conn_string } public CustomerDataSet GetCustomer(string id) { // Code to retrieve a typed DataSet containing Customer data } public string CreateCustomer(string name, string address, string city, string state, string zip) { // Code to create a new customer in the database, based on the scalar // parameters passed into this method. // Return the customerID from this method. } public void UpdateCustomer(CustomerDataSet updatedCustomer) { // Code to update the database, based on the Customer data sent in as a parameter // of type CustomerDataSet } public void DeleteCustomer(string id) { // Code to delete the customer with the specified ID } public DataSet GetCustomersWhoPurchasedProduct(int productID) { // Code to retrieve customers using a generic DataSet, because this method is not // retrieving all the information associated with a customer } } How to Use XML to Represent Collections and Hierarchies of DataThe following example shows how to represent collections and hierarchies of data in an XML document. The XML document represents a single order made by a customer; note that the <OrderDetails> element holds a collection of order detail information for the order. <Order xmlns="urn:aUniqueNamespace"> <OrderID>10248</OrderID> <CustomerID>VINET</CustomerID> <OrderDate>1996-07-04</OrderDate> <ShippedDate>1996-07-16</ShippedDate> <OrderDetails> <OrderDetail> <ProductID>11</ProductID> <UnitPrice>14.00</UnitPrice> <Quantity>12</Quantity> </OrderDetail> <OrderDetail> <ProductID>42</ProductID> <UnitPrice>9.80</UnitPrice> <Quantity>10</Quantity> </OrderDetail> <OrderDetail> <ProductID>72</ProductID> <UnitPrice>34.80</UnitPrice> <Quantity>5</Quantity> </OrderDetail> </OrderDetails> </Order> How to Apply a Style Sheet Programmatically in a .NET ApplicationTo apply a style sheet programmatically in a .NET application, take the following steps:
How to Create a Typed DataSetYou can use typed DataSets to represent business entities. There are several ways to create a typed DataSet:
Note It is also possible to define a typed DataSet programmatically by inheriting from the DataSet and defining methods, properties, and nested classes to represent the structure of the DataSet. The easiest way to do this is to create a typed DataSet by using one of the procedures that follow, and then use this typed DataSet class as the basis for your own typed DataSet classes in the future. Creating a Typed DataSet by Using a Data AdapterTo create a typed DataSet by using a data adapter, follow these steps:
Creating a Typed DataSet from an XSD Schema FileTo create a typed DataSet from an XSD schema file by using Visual Studio .NET, follow these steps:
Creating a Typed DataSet by Using the XSD Schema Definition Tool (xsd.exe)The XML Schema Definition tool can generate a typed DataSet from an XSD schema file, an XDR schema file, or an XML instance document. The following command uses an XSD schema file named XsdSchemaFile.xsd to generate a typed DataSet in a Visual C# source file named XsdSchemaFile.cs in the current directory: xsd /dataset /language:C# XsdSchemaFile.xsd For more information, see Generating a Strongly Typed DataSet. How to Define a Business Entity ComponentThe following example shows how to define a custom entity class for the Product business entity: public class ProductEntity { // Private fields, to hold the state of the Product entity private int productID; private string productName; private string quantityPerUnit; private decimal unitPrice; private short unitsInStock; private short unitsOnOrder; private short reorderLevel; // Public properties, to expose the state of the entity public int ProductID { get { return productID; } set { productID = value; } } public string ProductName { get { return productName; } set { productName = value; } } public string QuantityPerUnit { get { return quantityPerUnit; } set { quantityPerUnit = value; } } public decimal UnitPrice { get { return unitPrice; } set { unitPrice = value; } } public short UnitsInStock { get { return unitsInStock; } set { unitsInStock = value; } } public short UnitsOnOrder { get { return unitsOnOrder; } set { unitsOnOrder = value; } } public short ReorderLevel { get { return reorderLevel; } set { reorderLevel = value; } } // Methods and properties to perform localized processing public void IncreaseUnitPriceBy(decimal amount) { unitPrice += amount; } public short UnitsAboveReorderLevel { get { return (short)(unitsInStock - reorderLevel); } } public string StockStatus { get { return "In stock: " + unitsInStock + ", on order: " + unitsOnOrder; } } } How to Represent Collections and Hierarchies of Data in a Business Entity ComponentThe following example shows how to define a custom entity class for the Order business entity. Each order comprises many order items; these order items are stored in a DataSet in the OrderEntity class. public class OrderEntity { // Private fields, to hold the order information private int orderID; private string customerID; private DateTime orderDate; private DateTime shippedDate; // Private field, to hold the order details information private DataSet orderDetails; // Public properties, to expose the order information public int OrderID { get { return orderID; } set { orderID = value; } } public string CustomerID { get { return customerID; } set { customerID = value; } } public DateTime OrderDate { get { return orderDate; } set { orderDate = value; } } public DateTime ShippedDate { get { return shippedDate; } set { shippedDate = value; } } // Public property, to expose the order details information public DataSet OrderDetails { get { return orderDetails; } set { orderDetails = value; } } // Additional method, to simplify access to order details information public bool IsProductOrdered(int productID) { // Primary key column must be defined in DataTable DataRow row = orderDetails.Tables[0].Rows.Find(productID); if (row != null) return true; else return false; } // Additional property, to simplify access to order details information public int NumberOfOrderItems { get { return orderDetails.Tables[0].Rows.Count; } } } Note the following points concerning the OrderEntity class:
How to Bind Business Entity Components to User-Interface ControlsYou can bind user-interface controls to custom entities in Windows Forms and in ASP.NET applications. There are two possible scenarios:
How to Expose Events in a Business Entity ComponentCustom entities can raise events when the business entity state is modified. These events are useful for rich client, user-interface design because data can be refreshed wherever it is being displayed. The following code sample shows how to raise business entity-related events in the OrderEntity class: // Define a common event class for all business entity events public class EntityEventArgs : EventArgs { // Define event members, to provide information about the event } // Define a delegate specifying the signature for business entity-related events public delegate void EntityEventHandler(Object source, EntityEventArgs e); // Define a custom Entity class that raises events when the business entity state changes public class OrderEntity { // Define 'before' and 'after' events for business entity state changes public event EntityEventHandler BeforeChange, AfterChange; // Private fields, to hold the business entity's state private int orderID; private int customerID; private DateTime orderDate; private DateTime shippedDate; private DataSet orderDetails; // Public properties, to expose the business entity's state public int OrderID { get { return orderID; } set { BeforeChange(this, new EntityEventArgs()); // Raise a 'before' event orderID = value; AfterChange(this, new EntityEventArgs()); // Raise an 'after' event } } public int CustomerID { get { return customerID; } set { BeforeChange(this, new EntityEventArgs()); // Raise a 'before' event customerID = value; AfterChange(this, new EntityEventArgs()); // Raise an 'after' event } } public DateTime OrderDate { get { return orderDate; } set { BeforeChange(this, new EntityEventArgs()); // Raise a 'before' event orderDate = value; AfterChange(this, new EntityEventArgs()); // Raise an 'after' event } } public DateTime ShippedDate { get { return shippedDate; } set { BeforeChange(this, new EntityEventArgs()); // Raise a 'before' event shippedDate = value; AfterChange(this, new EntityEventArgs()); // Raise an 'after' event } } // Additional members, as required ... } Note the following points concerning the preceding code:
How to Serialize Business Entity Components to XML FormatThis section discusses the following issues:
Using XmlSerializer to Serialize a Custom Entity ObjectThe following code sample shows how to use the XmlSerializer class to serialize an OrderEntity object to XML format: using System.Xml.Serialization; // This namespace contains the XmlSerializer class ... // Create an XmlSerializer object, to serialize OrderEntity-type objects XmlSerializer serializer = new XmlSerializer(typeof(OrderEntity)); // Serialize an OrderEntity object to an XML file named "MyXmlOrderEntity.xml" TextWriter writer = new StreamWriter("MyXmlOrderEntity.xml"); serializer.Serialize(writer, order); writer.Close(); Serializing Objects in XML Web ServicesThe following code sample shows how to write an XML Web service that uses custom entity objects: namespace MyWebService { [WebService(Namespace="urn:MyWebServiceNamespace")] public class OrderWS : System.Web.Services.WebService { [WebMethod] public OrderEntity GetOrder(int orderID) { // Create an OrderDALC object. OrderDALC dalcOrder = new OrderDALC(); // Use dalcOrder to get an OrderEntity object for the specified order ID. // This code assumes the OrderDALC class has a method named GetOrder, // which takes an Order ID as a parameter and returns an OrderEntity object // containing all the data for this order. OrderEntity order = dalcOrder.GetOrder(10248); // Return the OrderEntity object. The object will be serialized automatically. return order; } [WebMethod] public void UpdateOrder(OrderEntity order) { // Create an OrderDALC object. OrderDALC dalcOrder = new OrderDALC(); // Use dalcOrder to save the OrderEntity object's data to the database. // This code assumes the OrderDALC class has a method named UpdateOrder, // which receives an OrderEntity object and saves the data to the database. dalcOrder.UpdateOrder(order); } Note the following points concerning the preceding code:
Default XML Format of a Serialized Custom Entity ObjectThe following XML document shows the default XML serialization format for OrderEntity objects: <?xml version="1.0" encoding="utf-8"?> <OrderEntity xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <OrderID>10248</OrderID> <CustomerID>VINET</CustomerID> <OrderDate>1996-07-04T00:00:00.0000000+01:00</OrderDate> <OrderDetails> ... see below ... </OrderDetails> <ShippedDate>1996-07-16T00:00:00.0000000+01:00</ShippedDate> </OrderEntity> The preceding document illustrates the default rules for XML serialization:
The OrderDetails property in the OrderEntity class is a DataSet. DataSets provide built-in support for XML serialization. The OrderDetails DataSet is serialized as follows: <OrderDetails> <xs:schema id="NewDataSet" xmlns="" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> <xs:element name="NewDataSet" msdata:IsDataSet="true" msdata:Locale="en- UK"> <xs:complexType> <xs:choice maxOccurs="unbounded"> <xs:element name="OrderDetails"> <xs:complexType> <xs:sequence> <xs:element name="OrderID" type="xs:int" minOccurs="0" /> <xs:element name="ProductID" type="xs:int" minOccurs="0" /> <xs:element name="UnitPrice" type="xs:decimal" minOccurs="0" /> <xs:element name="Quantity" type="xs:short" minOccurs="0" /> </xs:sequence> </xs:complexType> </xs:element> </xs:choice> </xs:complexType> </xs:element> </xs:schema> <diffgr:diffgram xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1"> <NewDataSet> <OrderDetails diffgr:id="OrderDetails1" msdata:rowOrder="0" diffgr:hasChanges="inserted"> <OrderID>10248</OrderID> <ProductID>11</ProductID> <UnitPrice>14</UnitPrice> <Quantity>12</Quantity> </OrderDetails> <OrderDetails diffgr:id="OrderDetails2" msdata:rowOrder="1" diffgr:hasChanges="inserted"> <OrderID>10248</OrderID> <ProductID>42</ProductID> <UnitPrice>9.8</UnitPrice> <Quantity>10</Quantity> </OrderDetails> <OrderDetails diffgr:id="OrderDetails3" msdata:rowOrder="2" diffgr:hasChanges="inserted"> <OrderID>10248</OrderID> <ProductID>72</ProductID> <UnitPrice>34.8</UnitPrice> <Quantity>5</Quantity> </OrderDetails> </NewDataSet> </diffgr:diffgram> </OrderDetails> Note the following points concerning the serialization of DataSets:
Controlling the XML Format of a Serialized Custom Entity ObjectYou can use .NET attributes in your custom entity class to control how the properties and fields are serialized to XML. Consider the following revised version of the OrderEntity class: [XmlRoot(ElementName="Order", Namespace="urn:MyNamespace")] public class OrderEntity { [XmlAttribute(AttributeName="ID")] public int OrderID { ...get and set code, as before... } [XmlAttribute(AttributeName="CustID")] public string CustomerID { ...get and set code, as before... } [XmlElement(ElementName="Ordered")] public DateTime OrderDate { ...get and set code, as before... } public DataSet OrderDetails { ...get and set code, as before... } [XmlElement(ElementName="Shipped") public DateTime ShippedDate { ...get and set code, as before... } // Additional members, as required... }
<?xml version="1.0" encoding="utf-8" ?> <Order ID="10248" CustID="VINET" xmlns="urn:MyNamespace" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <Ordered>1996-07-04T00:00:00.0000000+01:00</Ordered> <OrderDetails> ...details same as before... </OrderDetails> <Shipped>1996-07-16T00:00:00.0000000+01:00</Shipped> </Order> For more information about how to use attributes to control XML serialization, see Attributes that Control XML Serialization. How to Serialize Business Entity Components to SOAP FormatThe following code sample shows how to use the SoapFormatter class to serialize an OrderEntity object to SOAP format. SOAP serialization also occurs (implicitly) when you pass an object to or from an XML Web service by using the SOAP protocol, and when you pass an object to or from a Remoting server by using an HTTP remoting channel. In addition, you can specify SOAP formatting when you use the TCP remoting channel. using System.Runtime.Serialization.Formatters.Soap; // For the SoapFormatter class ... // Create a SoapFormatter object, to serialize OrderEntity-type objects SoapFormatter formatter = new SoapFormatter(); // Serialize an OrderEntity object to a SOAP (XML) file named "MySoapOrderEntity.xml" FileStream stream = File.Create("MySoapOrderEntity.xml"); formatter.Serialize(stream, order); stream.Close(); To use SOAP serialization for custom entity components, you must annotate your entity class by using the Serializable attribute, as shown in the following code: [Serializable] public class OrderEntity { // Members, as before If you want to customize the SOAP format generated during serialization, your entity class must implement the ISerializable interface. You must provide a GetObjectData method for the SoapFormatter to call during serialization, and you must provide a special constructor for the SoapFormatter to call to recreate the object during deserialization. The following code demonstrates the use of the ISerializable interface, the GetObjectData method, and the special constructor: using System.Runtime.Serialization; // For ISerializable interface, and related types ... [Serializable] public class OrderEntity : ISerializable { // Serialization function, called by the SoapFormatter during serialization void ISerializable.GetObjectData(SerializationInfo info, StreamingContext ctxt) { // Add each field to the SerializationInfo object info.AddValue("OrderID", orderID); // Additional code, as required... } // Deserialization constructor, called by the SoapFormatter during deserialization public OrderEntity(SerializationInfo info, StreamingContext ctxt) { // Deserialize from the SerializationInfo object to the OrderEntity fields orderID = (int)info.GetValue("OrderID", typeof(int)); // Additional code, as required... } // Other members, as before... } For more information about customized SOAP serialization, see Basic Serialization. How to Serialize Business Entity Components to Binary FormatThe following code sample shows how to use the BinaryFormatter class to serialize an OrderEntity object to binary format. Binary serialization also occurs (implicitly) when you pass an object to or from a Remoting server by using a TCP remoting channel. In addition, to improve performance, you can specify binary formatting when you use the HTTP remoting channel. using System.Runtime.Serialization.Formatters.Binary; // For the BinaryFormatter class ... // Create a BinaryFormatter object, to serialize OrderEntity-type objects BinaryFormatter formatter = new BinaryFormatter(); // Serialize an OrderEntity object to a binary file named "MyBinaryOrderEntity.dat" FileStream stream = File.Create("MyBinaryOrderEntity.dat"); formatter.Serialize(stream, order); stream.Close(); To use binary serialization for custom entity objects, you must annotate your custom entity class by using the Serializable attribute. To customize the binary format generated during serialization, your custom entity class must implement the ISerializable interface. The coding details in both scenarios are the same as for SOAP serialization. For more information about binary serialization, see Binary Serialization. CollaboratorsMany thanks to the following contributors and reviewers: Luca Bolognese, Mike Pizzo, Keith Short, Martin Petersen-Frey (PSS), Pablo De Grande, Bernard Chen (Sapient), Dimitris Georgakopoulos (Sapient), Kenny Jones, Chris Brooks, Lance Hendrix, Pradyumna Siddhartha, Franco A. Ceruti (VBNext), Diego Gonzalez (Lagash), and Chris Schoon. Thanks, also, to the content team: Chris Sfanos, Angela Crocker, Andy Olsen, and Sharon Smith. |