Data Access Application Block for .NET

Data Access Application Block for .NET

Chris Brooks, Graeme Malcolm, Alex Mackman, Edward Jezierski, Jason Hogg, Diego Gonzalez (Lagash), Pablo Cibraro (Lagash) and Julian Cantore (Lagash)
Microsoft Corporation

April 2002

Updated June 2003

Summary: The Data Access Application Block is a .NET component that contains optimized data access code that will help you call stored procedures and issue SQL text commands against a SQL Server database. It returns SqlDataReader, DataSet, and XmlReader objects. You can use it as a building block in your own .NET-based application to reduce the amount of custom code you need to create, test, and maintain. The download provides full Visual C# and Visual Basic .NET source code and comprehensive documentation.

Note that this document refers to some functionality that is only available in the 2.0 release of the Data Access Application Block.

Introduction

Are you involved in the design and development of data access code for .NET-based applications? Have you ever felt that you write the same data access code again and again? Have you wrapped data access code in helper functions that let you call a stored procedure in one line? If so, the Microsoft® Data Access Application Block for .NET is for you.

The Data Access Application Block encapsulates performance and resource management best practices for accessing Microsoft SQL Server™ databases. It can easily be used as a building block in your own .NET-based application. If you use it, you will reduce the amount of custom code you need to create, test, and maintain.

Specifically, the Data Access Application Block helps you:

·                     Call stored procedures or SQL text commands.

·                     Specify parameter details.

·                     Return SqlDataReader, DataSet, or XmlReader objects.

·                     Use strongly typed DataSets.

For example, in an application that references the Data Access Application Block, you can call a stored procedure and generate a DataSet in a single line of code, as follows:

[Visual Basic]
    
    
Dim ds As DataSet = SqlHelper.ExecuteDataset( _
    
    
      connectionString, _
    
    
      CommandType.StoredProcedure, _
    
    
      "getProductsByCategory", _
    
    
      new SqlParameter("@CategoryID", categoryID))
    
    
 
    
    
[C#]
    
    
DataSet ds = SqlHelper.ExecuteDataset( 
    
    
      connectionString,
    
    
      CommandType.StoredProcedure,
    
    
      "getProductsByCategory",
    
    
      new SqlParameter("@CategoryID", categoryID)); 
    
    
    
    
    
  
    
    

Note: This Application Block for .NET design is based on reviews of successful .NET-based applications. It is provided as source code that you can use as-is or customized for your application. It is not an indication of future direction for Microsoft ADO.NET libraries, which are built for fine-grained control of data access behavior in a wide range of use cases. Future releases of ADO.NET may address this scenario with a different model.

The remainder of this overview is divided into the following sections:

What Does the Data Access Application Block Include?

Downloading and Installing the Data Access Application

Using the Data Access Application Block

Internal Design

Frequently Asked Questions

Feedback and Support

More Information

Collaborators

What Does the Data Access Application Block Include?

The source code for the Data Access Application Block is provided, together with Quick Start sample applications that you can use to test its functionality. The Data Access Application Block also includes comprehensive documentation to help you work with and learn about the code provided.

The Visual Studio .NET Projects

Microsoft Visual Basic® .NET and Microsoft Visual C#® source code is supplied for the Data Access Application Block, together with a Quick Start Samples client application in each language that you can use to test common scenarios. This helps increase your understanding of how the Data Access Application Block works. You are also free to customize the source code to suit your requirements.

The Visual Basic and C# Microsoft.ApplicationBlocks.Data projects can each be compiled to produce an assembly named Microsoft.ApplicationBlocks.Data.dll. This assembly includes a class called SqlHelper, which contains the core functionality for executing database commands, and a second class called SqlhelperParameterCache, which provides parameter discovery and caching functionality.

The Documentation

The documentation for the Data Access Application Block includes the following main sections:

·                     Developing Applications with the Data Access Application Block. This section includes quick start samples that cover a range of common use cases. These will help you start to use the Data Access Application Block quickly and easily.

·                     Design and Implementation of the Data Access Application Block. This section includes background design philosophy information that provides insights into the design and implementation of the Data Access Application Block.

·                     Deployment and Operations. This section includes installation information that covers deployment and update options, as well as security related information.

·                     Reference. This is a comprehensive API reference section that explains the classes and interfaces which comprise the Data Access Application Block.

System Requirements

To run version 2.0 of Data Access Application Block, you need the following:

·                     Microsoft Windows® 2000, Windows XP Professional, or Windows 2003 operating system

·                     Microsoft .NET Framework Software Development Kit (SDK), version 1.1, version 1.1

·                     Microsoft Visual Studio® 2003 development system

·                     A database server running SQL Server 7.0 or later

To run the 1.0 version of the Data Access Application Block, you need the following:

·                     Microsoft Windows 2000 or Windows XP Professional

·                     The RTM version of the .NET Framework SDK

·                     The RTM version of Visual Studio .NET (recommended but not required)

·                     A database server running SQL Server 7.0 or later

Downloading and Installing the Data Access Application Block

A Windows Installer file containing the signed Data Access Application Block assembly and comprehensive documentation is available.

The install process creates a Microsoft Application Blocks for .NET submenu on your Programs menu. On the Microsoft Application Blocks for .NET submenu, there is a Data Access submenu that includes options to launch the documentation and to launch the Data Access Application Block Visual Studio.NET solution.

You can download version 1.0 of the Data Access Application Block from the Microsoft Download Center.

You can download version 2.0 of the Data Access Application Block from the Microsoft Download Center

Using the Data Access Application Block

This section discusses how to use the Data Access Application Block to execute database commands and manage parameters. The main elements of the Data Access Application Block are illustrated in Figure 1.

Figure 1. Data Access Application Block

The SqlHelper class provides a set of static methods that you can use to execute a variety of different command types against a SQL Server database.

The SqlHelperParameterCache class provides command parameter caching functionality used to improve performance. This is used internally by a number of the Execute methods (specifically, the overloads that are designed to execute only stored procedures). It can also be used directly by the data access client to cache specific parameter sets for specific commands.

Executing Commands with the SqlHelper Class

The SqlHelper class provides 13 Shared (Visual Basic) or static (C#) methods as shown in the diagram above. Each of the methods implemented provides a consistent set of overloads. This provides a well defined pattern for executing a command by using the SqlHelper class, while giving developers the necessary level of flexibility in how they choose to access data. The overloads provided for each method support different method arguments, so developers can decide how connection, transaction, and parameter information should be passed. All of the methods implemented in the class support the following overloads:

[Visual Basic]
    
    
Execute* (ByVal connection As SqlConnection, _
    
    
          ByVal commandType As CommandType, _
    
    
          ByVal CommandText As String)
    
    
 
    
    
Execute* (ByVal connection As SqlConnection, _
    
    
          ByVal commandType As CommandType, _
    
    
          ByVal commandText As String, _
    
    
          ByVal ParamArray commandParameters() As SqlParameter)
    
    
 
    
    
Execute* (ByVal connection As SqlConnection, ByVal spName As String, _
    
    
          ByVal ParamArray parameterValues() As Object)
    
    
 
    
    
Execute* (ByVal transaction As SqlTransaction, _
    
    
          ByVal commandType As CommandType, _
    
    
          ByVal commandText As String)
    
    
 
    
    
Execute* (ByVal transaction As SqlTransaction, _
    
    
          ByVal commandType As CommandType, _
    
    
          ByVal commandText As String, _
    
    
          ByVal ParamArray commandParameters() As SqlParameter)
    
    
 
    
    
Execute* (ByVal transaction As SqlTransaction, _
    
    
          ByVal spName As String, _
    
    
          ByVal ParamArray parameterValues() As Object)
    
    
 
    
    
[C#]
    
    
Execute* (SqlConnection connection, CommandType commandType, 
    
    
          string commandText)
    
    
 
    
    
Execute* (SqlConnection connection, CommandType commandType,
    
    
          string commandText, params SqlParameter[] commandParameters)
    
    
 
    
    
Execute* (SqlConnection connection, string spName, 
    
    
          params object[] parameterValues)
    
    
 
    
    
Execute* (SqlConnection connection, 
    
    
          CommandType commandType, string commandText)
    
    
 
    
    
Execute* (SqlConnection connection,
    
    
          CommandType commandType, string commandText, 
    
    
          params SqlParameter[] commandParameters)
    
    
 
    
    
Execute* (SqlConnection connection,
    
    
          string spName, params object[] parameterValues)
    
    
  
    
    
  
    
    

In addition to these overloads, all methods other than ExecuteXmlReader, UpdateDataset, and CreateCommand provide overloads to allow connection information to be passed as a connection string, rather than as a connection object, as shown in the following method signatures:

[Visual Basic]
    
    
Execute* (ByVal connectionString As String, _
    
    
          ByVal commandType As CommandType, _
    
    
          ByVal commandText As String)
    
    
 
    
    
Execute* (ByVal connectionString As String, _
    
    
          ByVal commandType As CommandType, _
    
    
          ByVal commandText As String, _
    
    
          ByVal ParamArray commandParameters() As SqlParameter)
    
    
 
    
    
Execute* (ByVal connectionString As String, ByVal spName As String, _
    
    
          ByVal ParamArray parameterValues() As Object)
    
    
 
    
    
[C#]
    
    
Execute* (string connectionString, CommandType commandType, 
    
    
          string commandText)
    
    
 
    
    
Execute* (string connectionString, CommandType commandType, 
    
    
          string commandText, 
    
    
          params SqlParameter[] commandParameters)
    
    
 
    
    
Execute* (string connectionString, string spName, 
    
    
          params object[] parameterValues)
    
    
  
    
    
  
    
    

Note   The ExecuteXmlReader does not support a connection string because—unlike SqlDataReader objects—an XmlReader object does not provide a way to close connections automatically when the XmlReader is closed. Clients that passed a connection string would have no way of closing the connection object associated with the XmlReader when they had finished with it. UpdateDataset uses an existing connection, and CreateCommand uses a SqlConnection object.

You can write code that uses any of the SqlHelper class methods simply by referencing the Data Access Application Block assembly and importing the Microsoft.ApplicationBlocks.Data namespace, as illustrated in the following code sample.

[Visual Basic]
    
    
Imports Microsoft.ApplicationBlocks.Data
    
    
 
    
    
[C#]
    
    
using Microsoft.ApplicationBlocks.Data;
    
    
  
    
    
  
    
    

After the namespace has been imported, you can call any of the Execute* methods, as illustrated in the following code sample.

[Visual Basic]
    
    
Dim ds As DataSet = SqlHelper.ExecuteDataset( _
    
    
      "SERVER=(local);DATABASE=Northwind;INTEGRATED SECURITY=True;", _    
    
    
      CommandType.Text, "SELECT * FROM Products")
    
    
 
    
    
[C#]
    
    
DataSet ds = SqlHelper.ExecuteDataset( 
    
    
  "SERVER=DataServer;DATABASE=Northwind;INTEGRATED SECURITY=sspi;", 
    
    
  CommandType.Text, "SELECT * FROM Products");
    
    
  
    
    
  
    
    

Managing Parameters with the SqlHelperParameterCache Class

The SqlHelperParameterCache class provides three public shared methods that can be used to manage parameters. These methods are:

·                     CacheParameterSet. Used to store an array of SqlParameters in the cache.

·                     GetCachedParameterSet. Used to retrieve a copy of a cached parameter array.

·                     GetSpParameterSet. An overloaded method used to retrieve the appropriate parameters for a specified stored procedure by querying the database once and then caching the results for future queries.

Caching and Retrieving Parameters

An array of SqlParameter objects can be cached by using the CacheParameterSet method. This method creates a key by concatenating the connection string and command text, and then stores the parameter array in the Hashtable.

To retrieve the parameter from the cache, the GetCachedParameterSet method is used. This method returns an array of SqlParameter objects initialized with the names, directions, data types, and so on, of the parameters in the cache corresponding to the connection string and command text passed to the method.

Note   The connection string used as a key for the parameter set is matched using a simple string comparison. The connection string used to retrieve parameters from GetCachedParameterSet must be absolutely identical to the connection string used to store those parameters with CacheParameterSet. A syntactically different connection string, even if semantically equivalent, will not result in an exact match.

The following code shows how parameters for a Transact-SQL statement can be cached and retrieved by using the SqlHelperParameterCache class.

[Visual Basic]
    
    
'Initialize the connection string and command text
    
    
'These will form the key used to store and retrieve the parameters
    
    
Const CONN_STRING As String = _
    
    
  "SERVER=(local); DATABASE=Northwind; INTEGRATED SECURITY=True;"
    
    
Dim sql As String = _
    
    
         "SELECT ProductName FROM Products WHERE CategoryID=@Cat " + _
    
    
         "AND SupplierID = @Sup"
    
    
 
    
    
'Cache the parameters
    
    
Dim paramsToStore(1) As SqlParameter
    
    
paramsToStore(0) = New SqlParameter("@Cat", SqlDbType.Int)
    
    
paramsToStore(1) = New SqlParameter("@Sup", SqlDbType.Int)
    
    
SqlHelperParameterCache.CacheParameterSet(CONN_STRING, sql, _
    
    
                                          paramsToStore)
    
    
 
    
    
'Retrieve the parameters from the cache
    
    
Dim storedParams(1) As SqlParameter
    
    
storedParams = SqlHelperParameterCache.GetCachedParameterSet(
    
    
                                             CONN_STRING, sql)
    
    
storedParams(0).Value = 2
    
    
storedParams(1).Value = 3
    
    
 
    
    
'Use the parameters in a command
    
    
Dim ds As DataSet
    
    
ds = SqlHelper.ExecuteDataset(CONN_STRING, CommandType.Text, sql, _
    
    
                              storedParams)
    
    
 
    
    
[C#]
    
    
// Initialize the connection string and command text
    
    
// These will form the key used to store and retrieve the parameters
    
    
const string CONN_STRING =
    
    
  "SERVER=(local); DATABASE=Northwind; INTEGRATED SECURITY=True;";
    
    
string spName = 
    
    
"SELECT ProductName FROM Products WHERE CategoryID=@Cat " + 
    
    
                                  "AND SupplierID = @Sup";
    
    
 
    
    
//Cache the parameters
    
    
SqlParameter[] paramsToStore = new SqlParameter[2];
    
    
paramsToStore[0] = New SqlParameter("@Cat", SqlDbType.Int);
    
    
paramsToStore[1] = New SqlParameter("@Sup", SqlDbType.Int);
    
    
SqlHelperParameterCache.CacheParameterSet(CONN_STRING, sql, 
    
    
                                          paramsToStore);
    
    
 
    
    
//Retrieve the parameters from the cache
    
    
SqlParameter storedParams = new SqlParameter[2];
    
    
storedParams = SqlHelperParameterCache.GetCachedParameterSet(
    
    
                                             CONN_STRING, sql);
    
    
storedParams(0).Value = 2;
    
    
storedParams(1).Value = 3;
    
    
 
    
    
//Use the parameters in a command
    
    
DataSet ds;
    
    
ds = SqlHelper.ExecuteDataset(CONN_STRING, 
    
    
                              CommandType.StoredProcedure,
    
    
                              sql, storedParams);
    
    
  
    
    
  
    
    
Retrieving Stored Procedure Parameters

SqlHelperParameterCache also provides a way to retrieve an array of parameters for a specific stored procedure. An overloaded method named GetSpParameterSet with two implementations provides this functionality. This method attempts to retrieve the parameters for the specified stored procedure from the cache. If the parameters are not cached, they are retrieved internally using the .NET SqlCommandBuilder class and added to the cache for subsequent requests. The appropriate parameter settings are then assigned for each parameter, before the parameters are returned in an array to the client. The following code shows how the parameters for the SalesByCategory stored procedure in the Northwind database can be retrieved.

[Visual Basic]
    
    
'Initialize the connection string and command text
    
    
'These will form the key used to store and retrieve the parameters
    
    
Const CONN_STRING As String = _
    
    
  "SERVER=(local); DATABASE=Northwind; INTEGRATED SECURITY=True;"
    
    
Dim spName As String = "SalesByCategory"
    
    
 
    
    
'Retrieve the parameters
    
    
Dim storedParams(1) As SqlParameter
    
    
storedParams = SqlHelperParameterCache.GetSpParameterSet(CONN_STRING, spName)
    
    
storedParams(0).Value = "Beverages"
    
    
storedParams(1).Value = "1997"
    
    
 
    
    
'Use the parameters in a command
    
    
Dim ds As DataSet
    
    
ds = SqlHelper.ExecuteDataset(CONN_STRING, _
    
    
                              CommandType.StoredProcedure, _
    
    
                              spName, storedParams)
    
    
 
    
    
[C#]
    
    
// Initialize the connection string and command text
    
    
/ These will form the key used to store and retrieve the parameters
    
    
const string CONN_STRING = 
    
    
  "SERVER=(local); DATABASE=Northwind; INTEGRATED SECURITY=True;";
    
    
string spName = "SalesByCategory";
    
    
 
    
    
// Retrieve the parameters
    
    
SqlParameter storedParams = new SqlParameter[2];
    
    
storedParams = SqlHelperParameterCache.GetSpParameterSet(CONN_STRING, 
    
    
                                                         spName);
    
    
storedParams[0].Value = "Beverages";
    
    
storedParams[1].Value = "1997";
    
    
 
    
    
//Use the parameters in a command
    
    
DataSet ds;
    
    
ds = SqlHelper.ExecuteDataset(CONN_STRING, CommandType.StoredProcedure,
    
    
                              spName, storedParams);
    
    
  
    
    
  
    
    

Internal Design

The Data Access Application Block includes the full source code and a comprehensive guide to its design. In this section, the main implementation details are described.

SqlHelper Class Implementation Details

The SqlHelper class is designed to encapsulate data access functionality through a set of static methods. Because it is not designed to be inherited from or instantiated, the class is declared as a non-inheritable class with a private constructor.

Each of the methods implemented in the SqlHelper class provides a consistent set of overloads. This provides a well defined pattern for executing a command by using the SqlHelper class, while giving developers the necessary level of flexibility in how they choose to access data. The overloads provided for each method support different method arguments, so developers can decide how connection, transaction, and parameter information should be passed. The methods implemented in the SqlHelper class are:

·                     ExecuteNonQuery. This method is used to execute commands that do not return any rows or values. They are generally used to perform database updates, but they can also be used to return output parameters from stored procedures.

·                     ExecuteReader. This method is used to return a SqlDataReader object that contains the resultset returned by a command.

·                     ExecuteDataset. This method returns a DataSet object that contains the resultset returned by a command.

·                     ExecuteScalar. This method returns a single value. The value is always the first column of the first row returned by the command.

·                     ExecuteXmlReader. This method returns an XML fragment from a FOR XML query.

·                     FillDataset. This method is similar to ExecuteDataset, except that a pre-existing DataSet can be passed in, allowing additional tables to be added.

·                     UpdateDataset. This method updates a DataSet using an existing connection and user-specified update commands. It is typically used with CreateCommand.

·                     CreateCommand. This method simplifies the creation of a SQL command object by allowing a stored procedure and optional parameters to be provided. This method is typically used with UpdateDataset.

·                     ExecuteNonQueryTypedParams. This method executes a non-query operation using a data row instead of parameters.

·                     ExecuteDatasetTypedParams. This method executes a DataSet creation operation, using a data row instead of parameters.

·                     ExecuteReaderTypedParams. This method returns a data reader using a data row instead of parameters.

·                     ExecuteScalarTypedParams. This method returns a scalar using a data row instead of parameters.

·                     ExecuteXmlReaderTypedParams. This method executes an XmlReader using a data row instead of parameters.

In addition to the public methods, the SqlHelper class includes a number of private functions, which are used to manage parameters and prepare commands for execution. Regardless of the method implementation called by the client, all commands are executed by using a SqlCommand object. Before this SqlCommand object can be executed, any parameters must be added to its Parameters collection, and the Connection, CommandType, CommandText, and Transaction properties must be set appropriately. The private functions in the SqlHelper class are primarily designed to provide a consistent way to execute commands against a SQL Server database, regardless of the overloaded method implementation called by the client application. The private utility functions in the SqlHelper class are:

·                     AttachParameters. A function used to attach any necessary SqlParameter objects to the SqlCommand being executed.

·                     AssignParameterValues. A function used to assign values to the SqlParameter objects.

·                     PrepareCommand. A function used to initialize the properties of the command, such as its connection, transaction context, and so on.

·                     ExecuteReader. This private implementation of ExecuteReader is used to open a SqlDataReader object with the appropriate CommandBehavior to manage the lifetime of the connection associated with the reader most efficiently.

SqlHelperParameterCache Class Implementation Details

Parameter arrays are cached in a private Hashtable. Internally, the parameters retrieved from the cache are copied so that the client application can change parameter values, and so on, without affecting the cached parameter arrays. A private shared function named CloneParameters is used for this purpose.

Frequently Asked Questions

What's new in this release?

The 2.0 release of the Data Access Application Block includes the following new features:

·                     Support for strongly typed DataSets with the FillDataset method

·                     Support for committing updates to a DataSet back to the database

·                     Additional helper methods with support for DataRow type parameters

·                     Minor bug fixes

The RTM release of the Data Access Application Block includes the following new features and changes from the Beta 2.0 release:

·                     Transactional overloads of SqlHelper class methods no longer require a SqlConnection parameter. In this release, the connection information is derived from the SqlTransaction object, thus eliminating the need to include a SqlConnection object parameter in the method signature.

·                     The GetSpParameterSet method now uses the ADO.NET CommandBuilder class's DeriveParameters method to ascertain the parameters required by a stored procedure. This is more efficient than the technique used in the beta 2.0 release, where the information was retrieved by directly querying the database.

Can I use XCOPY deployment to deploy the Data Access Application Block assemblies?

Yes. After it is compiled, the Microsoft,ApplicationBlocks.Data.dll assembly can be deployed using xcopy.

When should I use the ExecuteDataset method and when should I use the ExecuteReader method?

The real question here is when should you return multiple rows of data in a DataSet object, and when should you use a DataReader. The answer depends on the needs of your particular application and your priorities in terms of flexibility versus raw performance. A DataSet gives you a flexible, disconnected, relational view of your data while a DataReader provides an extremely high performance, read only, forward only cursor.

If you use a parameter array to return output values, be aware that you must extract the values (using the SqlParameter Value property) after you close the SqlDataReader object.

For a comprehensive comparison of DataSets and DataReaders, see the Data Access Architecture Guide at http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnbda/html/daag.asp.

When should I use the Execute*TypedParams methods?

These methods are designed to capitalize on the support for strongly typed DataSets. They allow you to pass in an entire typed data row as a parameter for a stored procedure, rather than an array of all the parameters that would normally map to all the fields of the table.

How do I use ExecuteDataset to return a DataSet containing multiple tables?

You can retrieve a DataSet containing multiple tables by creating a stored procedure that returns multiple rowsets (either by executing multiple SELECT statements or by making nested calls to other stored procedures), and executing it using the ExecuteDataset method.

For example, suppose you have the following stored procedures in your database.

CREATE PROCEDURE GetCategories
    
    
AS
    
    
SELECT * FROM Categories
    
    
GO
    
    
CREATE PROCEDURE GetProducts
    
    
AS
    
    
SELECT * FROM Products
    
    
  
    
    
  
    
    

You could create a master stored procedure that makes nested calls to these procedures, as illustrated in the following code sample.

CREATE PROCEDURE GetCategoriesAndProducts
    
    
AS
    
    
BEGIN
    
    
  EXEC GetCategories
    
    
  EXEC GetProducts
    
    
END
    
    
  
    
    
  
    
    

Executing this master stored procedure with the ExecuteDataset method returns a single DataSet containing two tables—one containing the category data and the other containing the product data.

Note   The ExecuteDataset method does not provide a way to assign custom names to the tables returned. The first table is always numbered 0 and named Table, the second is numbered 1 and named Table1, and so on.

Are there any other application blocks?

The Data Access Application Block is one of several Application Blocks that are being released. These Application Blocks solve the common problems that developers face from one project to the next. They can be plugged into .NET-based applications quickly and easily.

Feedback and Support

The Application Blocks for .NET are designed to jumpstart development of .NET distributed applications. The sample code and documentation is provided "as-is." Support is available through Microsoft Product Support for a fee.

To learn more about .NET best practices, please visit the patterns & practices Web page.

To participate in an online collaborative development environment on this topic, join the GotDotNet workspace: Microsoft Patterns & Practices Data Access for .NET Workspace. Please share your Data Access Block questions, suggestions, and customizations with the community in this workspace.

Questions? Comments? Suggestions? For feedback on the content of this article, please e-mail us at devfdbck@microsoft.com.

More Information

The Data Access Application Block is designed and developed based on the best practices and general design principles discussed in the .NET Data Access Architecture Guide at http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnbda/html/daag.asp. Read this guide to learn more about data access.

Collaborators

Many thanks to the following contributors and reviewers: Susan Warren, Brad Abrams, Andy Dunn, Michael Day, Mark Ashton, Gregory Leake, Steve Busby, Kenny Jones, David Schleifer, Pablo Castro, Michael Pizzo, Paul Andrew, Cihan Biyikoglu, Eugenio Pace, Roger Lamb, Nick Takos, Andrew Roubin (Vorsite Corp.), Jeffrey Richter (Wintellect), Bernard Chen (Sapient), and Matt Drucker (Turner Broadcasting).

Thanks, also, to the content team: Tina Burden (Entirenet), Shylender Ramamurthy (Infosys Technologies Ltd), Filiberto Selvas Patino, and Roberta Leibovitz and Colin Campbell (Modeled Computation LLC)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值