-
Book Overview & Buying
-
Table Of Contents
Apps and Services with .NET 7
By :
Microsoft.Data.SqlClient provides database connectivity to SQL Server for .NET applications. It is known as the Microsoft ADO.NET driver for SQL Server and Azure SQL Database.
You can find the GitHub repository for ADO.NET at the following link: https://github.com/dotnet/SqlClient.
The Microsoft.Data.SqlClient package supports the following .NET platforms:
ADO.NET defines abstract types that represent minimal objects for working with data, like DbConnection, DbCommand, and DbDataReader. Database software manufacturers can inherit from and provide specific implementations that are optimized for and expose additional features for their database. Microsoft has done this for SQL Server. The most important types with their most used members are shown in the following table:
|
Type |
Properties |
Methods |
Description |
|
|
|
|
Manage the connection to the database. |
|
|
|
|
Build a valid connection string for a SQL Server database. After setting all the relevant individual properties, get the |
|
|
|
|
Configure the command to execute. |
|
|
|
Configure a parameter for a command. |
|
|
|
|
|
Process the result set from executing a query. |
SqlConnection has two useful events: StateChange and InfoMessage.
All the ExecuteXxx methods will execute any command. The one you use depends on what you expect to get back:
SELECT statement that returns a result set, then call ExecuteReader to execute the command. This method returns a DbDataReader-derived object for reading row-by-row through the result set.SELECT statement, then it is more efficient to call ExecuteNonQuery. This method returns an integer for the number of rows affected.SELECT statement that returns XML because it uses the AS XML command, then call ExecuteXmlReader to execute the command.First, we will create a console app project for working with ADO.NET:
Chapter02.consoleChapter02Northwind.Console.SqlClientGood Practice: For all the projects that you create for this book, keep your root path short and avoid using # in your folder and file names, or you might see compiler errors like RSG002: TargetPath not specified for additional file. For example, do not use C:\My C# projects\ as your root path!
Microsoft.Data.SqlClient, and statically and globally import System.Console, as shown highlighted in the following markup:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
<Using Include="System.Console" Static="true" />
</ItemGroup>
</Project>
Program.EventHandlers.cs, and modify its contents to define methods that will act as event handlers for a database connection state change by showing the original and current states, and for when the database sends an InfoMessage, as shown in the following code:
using Microsoft.Data.SqlClient; // SqlInfoMessageEventArgs
using System.Data; // StateChangeEventArgs
partial class Program
{
static void Connection_StateChange(object sender, StateChangeEventArgs e)
{
ConsoleColor previousColor = ForegroundColor;
ForegroundColor = ConsoleColor.DarkYellow;
WriteLine($"State change from {e.OriginalState} to {e.CurrentState}.");
ForegroundColor = previousColor;
}
static void Connection_InfoMessage(object sender, SqlInfoMessageEventArgs e)
{
ConsoleColor previousColor = ForegroundColor;
ForegroundColor = ConsoleColor.DarkBlue;
WriteLine($"Info: {e.Message}.");
foreach(SqlError error in e.Errors)
{
WriteLine($" Error: {error.Message}.");
}
ForegroundColor = previousColor;
}
}
Program.cs, delete the existing statements. Add statements to connect to SQL Server locally, to Azure SQL Database, or to SQL Edge, using either SQL authentication with a user ID and password or Windows Authentication without a user ID and password, as shown in the following code:
using Microsoft.Data.SqlClient; // SqlConnection and so on
SqlConnectionStringBuilder builder = new();
builder.InitialCatalog = "Northwind";
builder.MultipleActiveResultSets = true;
builder.Encrypt = true;
builder.TrustServerCertificate = true;
builder.ConnectTimeout = 10;
WriteLine("Connect to:");
WriteLine(" 1 - SQL Server on local machine");
WriteLine(" 2 - Azure SQL Database");
WriteLine(" 3 – Azure SQL Edge");
WriteLine();
Write("Press a key: ");
ConsoleKey key = ReadKey().Key;
WriteLine(); WriteLine();
if (key is ConsoleKey.D1 or ConsoleKey.NumPad1)
{
builder.DataSource = "."; // Local SQL Server
// @".\net7book"; // Local SQL Server with an instance name
}
else if (key is ConsoleKey.D2 or ConsoleKey.NumPad2)
{
builder.DataSource = // Azure SQL Database
"tcp:apps-services-net7.database.windows.net,1433";
}
else if (key is ConsoleKey.D3 or ConsoleKey.NumPad3)
{
builder.DataSource = "tcp:127.0.0.1,1433"; // Azure SQL Edge
}
else
{
WriteLine("No data source selected.");
return;
}
WriteLine("Authenticate using:");
WriteLine(" 1 – Windows Integrated Security");
WriteLine(" 2 – SQL Login, for example, sa");
WriteLine();
Write("Press a key: ");
key = ReadKey().Key;
WriteLine(); WriteLine();
if (key is ConsoleKey.D1 or ConsoleKey.NumPad1)
{
builder.IntegratedSecurity = true;
}
else if (key is ConsoleKey.D2 or ConsoleKey.NumPad2)
{
builder.UserID = "sa"; // Azure SQL Edge
// "markjprice"; // change to your username
Write("Enter your SQL Server password: ");
string? password = ReadLine();
if (string.IsNullOrWhiteSpace(password))
{
WriteLine("Password cannot be empty or null.");
return;
}
builder.Password = password;
builder.PersistSecurityInfo = false;
}
else
{
WriteLine("No authentication selected.");
return;
}
SqlConnection connection = new(builder.ConnectionString);
WriteLine(connection.ConnectionString);
WriteLine();
connection.StateChange += Connection_StateChange;
connection.InfoMessage += Connection_InfoMessage;
try
{
WriteLine("Opening connection. Please wait up to {0} seconds...",
builder.ConnectTimeout);
WriteLine();
connection.Open();
WriteLine($"SQL Server version: {connection.ServerVersion}");
connection.StatisticsEnabled = true;
}
catch (SqlException ex)
{
WriteLine($"SQL exception: {ex.Message}");
return;
}
connection.Close();
Connect to:
1 - SQL Server on local machine
2 - Azure SQL Database
3 - Azure SQL Edge
Press a key: 1
Authenticate using:
1 - Windows Integrated Security
2 - SQL Login, for example, sa
Press a key: 1
Data Source=.;Initial Catalog=Northwind;Integrated Security=True;Multiple Active Result Sets=True;Connect Timeout=10;Encrypt=True;Trust Server Certificate=True
Opening connection. Please wait up to 10 seconds...
State change from Closed to Open.
SQL Server version: 15.00.2095
State change from Open to Closed.
The following steps show the experience when connecting to Azure SQL Database or Azure SQL Edge, which require a username and password. If you are connecting to a local SQL Server using Windows Integrated Security, then you will not need to enter a password.
Connect to:
1 - SQL Server on local machine
2 - Azure SQL Database
3 - Azure SQL Edge
Press a key: 3
Authenticate using:
1 - Windows Integrated Security
2 - SQL Login, for example, sa
Press a key: 2
Enter your SQL Server password: s3cret-Ninja
State change from Closed to Open.
SQL Server version: 15.00.0041
State change from Open to Closed.
Enter your SQL Server password: silly-ninja
SQL exception: Login failed for user 'sa'.
Program.cs, change the server name to something wrong.SQL exception: A network-related or instance-specific error occurred while establishing a connection to SQL Server. The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server is configured to allow remote connections. (provider: TCP Provider, error: 0 - No such host is known.)
When opening a SQL Server connection, the default timeout is 30 seconds for server connection problems, so be patient! We changed the timeout to 10 seconds to avoid having to wait so long.
Now that we have a successful connection to the SQL Server database, we can run commands and process the results using a data reader.
Program.cs, import the namespace for working with ADO.NET command types, as shown in the following code:
using System.Data; // CommandType
Products table, executes it, and outputs the product IDs, names, and prices using a data reader, as shown in the following code:
SqlCommand cmd = connection.CreateCommand();
cmd.CommandType = CommandType.Text;
cmd.CommandText = "SELECT ProductId, ProductName, UnitPrice FROM Products";
SqlDataReader r = cmd.ExecuteReader();
WriteLine("----------------------------------------------------------");
WriteLine("| {0,5} | {1,-35} | {2,8} |", "Id", "Name", "Price");
WriteLine("----------------------------------------------------------");
while (r.Read())
{
WriteLine("| {0,5} | {1,-35} | {2,8:C} |",
r.GetInt32("ProductId"),
r.GetString("ProductName"),
r.GetDecimal("UnitPrice"));
}
WriteLine("----------------------------------------------------------");
r.Close();
We format the unit price using the C format which uses your OS current culture to format currency values. My output uses £ because I am in the UK. You will learn how to control the current culture in Chapter 7, Handling Dates, Times, and Internationalization.
----------------------------------------------------------
| Id | Name | Price |
----------------------------------------------------------
| 1 | Chai | £18.00 |
| 2 | Chang | £19.00 |
...
| 76 | Lakkalikööri | £18.00 |
| 77 | Original Frankfurter grüne Soße | £13.00 |
----------------------------------------------------------
Program.cs, modify the SQL statement to define a parameter for the unit price and use it to filter the results to products that cost more than that unit price, as shown highlighted in the following code:
Write("Enter a unit price: ");
string? priceText = ReadLine();
if(!decimal.TryParse(priceText, out decimal price))
{
WriteLine("You must enter a valid unit price.");
return;
}
SqlCommand cmd = connection.CreateCommand();
cmd.CommandType = CommandType.Text;
cmd.CommandText = "SELECT ProductId, ProductName, UnitPrice FROM Products"
+ " WHERE UnitPrice > @price";
cmd.Parameters.AddWithValue("price", price);
50, and note the results, as shown in the following partial output:
Enter a unit price: 50
----------------------------------------------------------
| Id | Name | Price |
----------------------------------------------------------
| 9 | Mishi Kobe Niku | £97.00 |
| 18 | Carnarvon Tigers | £62.50 |
| 20 | Sir Rodney's Marmalade | £81.00 |
| 29 | Thüringer Rostbratwurst | £123.79 |
| 38 | Côte de Blaye | £263.50 |
| 51 | Manjimup Dried Apples | £53.00 |
| 59 | Raclette Courdavault | £55.00 |
----------------------------------------------------------
You can improve the responsiveness of data access code by making it asynchronous. You will see more details of how asynchronous operations work in Chapter 4, Benchmarking Performance, Multitasking, and Concurrency. For now, just enter the code as instructed.
Let’s see how to change the statements to work asynchronously:
Program.cs, change the statement to open the connection to make it asynchronous, as shown in the following code:
await connection.OpenAsync();
Program.cs, change the statement to execute the command to make it asynchronous, as shown in the following code:
SqlDataReader r = await cmd.ExecuteReaderAsync();
Program.cs, change the statements to read the next row and get the field values to make them asynchronous, as shown in the following code:
while (await r.ReadAsync())
{
WriteLine("| {0,5} | {1,-35} | {2,8:C} |",
await r.GetFieldValueAsync<int>("ProductId"),
await r.GetFieldValueAsync<string>("ProductName"),
await r.GetFieldValueAsync<decimal>("UnitPrice"));
}
Program.cs, change the statements to close the data reader and connection to make them asynchronous, as shown in the following code:
await r.CloseAsync();
await connection.CloseAsync();
If you need to execute the same query or another SQL statement multiple times, it is best to create a stored procedure, often with parameters, so that it can be precompiled and optimized. Parameters have a direction to indicate if they are inputs, outputs, or return values.
Let’s see an example that uses all three types of direction:
GetExpensiveProducts with two parameters, an input parameter for the minimum unit price and an output parameter for the row count of matching products, as shown in the following code:
CREATE PROCEDURE [dbo].[GetExpensiveProducts]
@price money,
@count int OUT
AS
SELECT @count = COUNT(*)
FROM Products
WHERE UnitPrice > @price
SELECT *
FROM Products
WHERE UnitPrice > @price
RETURN 0
The stored procedure uses two SELECT statements. The first sets the @count output parameter to a count of the matching product rows. The second returns the matching product rows.

Figure 2.12: Parameters of the GetExpensiveProducts stored procedure
Program.cs, add statements to allow the user to choose between running the text command and the stored procedure. Add statements defining the stored procedure and its parameters, and then execute the command, as shown highlighted in the following code:
SqlCommand cmd = connection.CreateCommand();
WriteLine("Execute command using:");
WriteLine(" 1 - Text");
WriteLine(" 2 - Stored Procedure");
WriteLine();
Write("Press a key: ");
key = ReadKey().Key;
WriteLine(); WriteLine();
SqlParameter p1, p2 = new(), p3 = new();
if (key is ConsoleKey.D1 or ConsoleKey.NumPad1)
{
cmd.CommandType = CommandType.Text;
cmd.CommandText = "SELECT ProductId, ProductName, UnitPrice FROM Products"
+ " WHERE UnitPrice > @price";
cmd.Parameters.AddWithValue("price", price);
}
else if (key is ConsoleKey.D2 or ConsoleKey.NumPad2)
{
cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandText = "GetExpensiveProducts";
p1 = new()
{
ParameterName = "price",
SqlDbType = SqlDbType.Money,
SqlValue = price
};
p2 = new()
{
Direction = ParameterDirection.Output,
ParameterName = "count",
SqlDbType = SqlDbType.Int
};
p3 = new()
{
Direction= ParameterDirection.ReturnValue,
ParameterName = "rv",
SqlDbType = SqlDbType.Int
};
cmd.Parameters.Add(p1);
cmd.Parameters.Add(p2);
cmd.Parameters.Add(p3);
}
SqlDataReader r = await cmd.ExecuteReaderAsync();
await r.CloseAsync();
WriteLine($"Output count: {p2.Value}");
WriteLine($"Return value: {p3.Value}");
await connection.CloseAsync();
If a stored procedure returns result sets as well as parameters, then the data reader for the result sets must be closed before the parameters can be read.
60, as shown in the following output:
Enter a unit price: 60
Execute command using:
1 - Text
2 - Stored Procedure
Press a key: 2
----------------------------------------------------------
| Id | Name | Price |
----------------------------------------------------------
| 9 | Mishi Kobe Niku | £97.00 |
| 18 | Carnarvon Tigers | £62.50 |
| 20 | Sir Rodney's Marmalade | £81.00 |
| 29 | Thüringer Rostbratwurst | £123.79 |
| 38 | Côte de Blaye | £263.50 |
----------------------------------------------------------
Output count: 5
Return value: 0
State change from Open to Closed.
Change the font size
Change margin width
Change background colour