54 KiB
AdoNetShop — лабораторная работа по технологии ADO.NET
Что это за проект. Это небольшой учебный интернет-магазин, написанный на C# (.NET 8).
Всё общение с базой данных Microsoft SQL Server выполняется вручную через ADO.NET
(пакет Microsoft.Data.SqlClient). В проекте нет Entity Framework, Dapper, NHibernate
и любых других ORM — нам важно увидеть, как приложение общается с БД «на низком уровне».
Содержание
- Что такое ADO.NET и зачем он нужен
- Что такое Provider (провайдер данных)
- Основные классы ADO.NET, которые используются в проекте
- 3.1
SqlConnection— подключение к базе - 3.2
SqlCommand— команда (запрос) - 3.3
SqlParameter— параметры и защита от SQL Injection - 3.4
SqlDataReader— чтение результатов (connected) - 3.5
SqlDataAdapterиDataTable— disconnected - 3.6
SqlTransaction— транзакции
- 3.1
- Connected vs Disconnected: в чём разница
- Структура проекта
- Структура базы данных
- Как установить SQL Server
- Как создать базу данных и выполнить SQL-скрипты
- Как изменить строку подключения (Connection String)
- Как запустить проект
- Что умеет программа (демонстрация преподавателю)
- Обработка ошибок и валидация
- Какие файлы нужно изучить в первую очередь
- Частые проблемы и их решения
1. Что такое ADO.NET и зачем он нужен
ADO.NET (ActiveX Data Objects .NET) — это набор классов, входящих в состав .NET, который позволяет приложению на C# общаться с источниками данных: реляционными базами данных, XML-файлами и т.д.
Проще всего представить ADO.NET так:
┌──────────────────────┐ ADO.NET ┌──────────────────────┐
│ Приложение на C# │ ◄──────────────────► │ SQL Server │
│ (ваш код) │ команды и результаты │ (данные) │
└──────────────────────┘ └──────────────────────┘
Приложение не «знает» SQL Server напрямую. Между ними стоит слой ADO.NET, который переводит вызовы C# (открыть соединение, выполнить запрос, прочитать результат) в сетевые сообщения протокола SQL Server.
Зачем это нужно:
- Прямой контроль над SQL. Вы сами пишете запрос — значит, вы можете использовать
всё, что умеет конкретная СУБД: индексы, хинты,
JOIN, оконные функции,OUTPUT, временные таблицы. Никакой ORM не даст вам такой свободы без «хаков». - Производительность. ORM делает много «умной» работы (отслеживание изменений, ленивая загрузка, кэширование), которая часто не нужна. Прямой ADO.NET — это минимальные накладные расходы. Для учебных и для production-задач вродя отчётов ADO.NET часто быстрее.
- Понимание того, как всё работает на самом деле. Даже если потом вы будете писать на Entity Framework, понимание ADO.NET помогает понять, что ORM делает под капотом.
- Не зависимость от лишних зависимостей. ADO.NET — часть платформы .NET, нужен только один NuGet-пакет провайдера.
2. Что такое Provider (провайдер данных)
ADO.NET — это набор интерфейсов и абстракций, но он не умеет сам ходить в базу. Для каждой СУБД существует конкретная реализация — провайдер данных (Data Provider).
| СУБД | Провайдер (пакет NuGet) |
|---|---|
| Microsoft SQL Server | Microsoft.Data.SqlClient |
| PostgreSQL | Npgsql |
| MySQL / MariaDB | MySqlConnector |
| Oracle | Oracle.ManagedDataAccess |
| SQLite | Microsoft.Data.Sqlite |
В нашем проекте используется Microsoft.Data.SqlClient — официальный провайдер
для SQL Server. Именно он предоставляет классы SqlConnection, SqlCommand,
SqlDataReader, SqlDataAdapter, SqlTransaction.
Зачем провайдер вынесен в отдельный пакет, а не встроен в .NET? Разработчики .NET не могут включить в платформу драйверы ко всем существующим СУБД — это слишком много кода и лицензионные ограничения. Поэтому каждый провайдер — отдельный NuGet-пакет, который подключается одной строкой в
.csproj:
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.0" />
3. Основные классы ADO.NET, используемые в проекте
3.1 SqlConnection — подключение к базе
SqlConnection — это объект, представляющий физическое соединение с SQL Server.
Создаётся из строки подключения (connection string), которая содержит всю необходимую информацию: адрес сервера, имя базы, способ аутентификации.
// Файл: AdoNetShop/Data/DbConnectionFactory.cs
await using var connection = new SqlConnection(connectionString);
// Открываем физическое подключение к SQL Server
await connection.OpenAsync();
Три важных правила:
- Создать —
new SqlConnection(connectionString). Сам по себе объект ещё не подключён, это просто «заготовка». - Открыть —
Open()/OpenAsync(). Только после этого можно выполнять команды. Строка подключения читается именно в этот момент (ленивое подключение). - Закрыть/освободить —
Close()/Dispose(). Подключения — ограниченный ресурс (пул ограничен по числу соединений), поэтому их обязательно нужно закрывать.
В проекте мы нигде не вызываем Close() вручную, а всегда используем await using:
await using var connection = _connectionFactory.CreateConnection();
await connection.OpenAsync();
// ... работа с базой ...
// при выходе из блока соединение автоматически закроется и освободится,
// даже если внутри произошло исключение
using / await using — это конструкция языка C#, которая гарантирует вызов
Dispose(). Для SqlConnection, SqlCommand, SqlDataReader это обязательно,
потому что они используют неуправляемые ресурсы.
3.2 SqlCommand — команда (запрос)
SqlCommand — это объект, который содержит что именно мы хотим сделать с базой:
SQL-запрос, имя хранимой процедуры или текст команды.
await using var command = new SqlCommand(
"SELECT Id, Name, Price FROM Products WHERE CategoryId = @CategoryId",
connection);
Три способа выполнить команду:
| Метод | Что возвращает | Когда использовать |
|---|---|---|
ExecuteReaderAsync() |
SqlDataReader |
SELECT — когда нужно прочитать много строк |
ExecuteScalarAsync() |
одно значение (object) |
SELECT COUNT(*), SELECT SCOPE_IDENTITY() |
ExecuteNonQueryAsync() |
число затронутых строк (int) |
INSERT, UPDATE, DELETE |
Примеры из проекта (AdoNetShop/Data/ProductRepository.cs):
// ExecuteScalarAsync — получить Id только что вставленного товара
// (SCOPE_IDENTITY() возвращает Id, который сгенерировала база для новой строки)
string query = @"
INSERT INTO Products (Name, Description, Price, StockQuantity, CategoryId)
VALUES (@Name, @Description, @Price, @StockQuantity, @CategoryId);
SELECT SCOPE_IDENTITY();";
var newId = await command.ExecuteScalarAsync();
return Convert.ToInt32(newId);
// ExecuteNonQueryAsync — обновить товар, вернётся количество затронутых строк
int rowsAffected = await command.ExecuteNonQueryAsync();
return rowsAffected > 0;
3.3 SqlParameter — параметры и защита от SQL Injection
Это самая важная тема для защиты лабораторной.
Правило №1 при работе с любой БД: никогда не собирайте SQL-запрос склейкой строк.
❌ НЕЛЬЗЯ так — это SQL Injection:
// ОПАСНО! Не делайте так никогда.
string query = "SELECT * FROM Products WHERE Name = '" + name + "'";
Почему это опасно: если пользователь введёт в поле имя товара
' OR '1'='1
то запрос превратится в SELECT * FROM Products WHERE Name = '' OR '1'='1' —
вернётся вся таблица. Это и есть SQL Injection: злоумышленник может подставить
'; DROP TABLE Products; -- и удалить таблицу.
✅ ПРАВИЛЬНО так — использовать параметры:
// Безопасно. Значение не «вклеивается» в текст запроса,
// база получает его как отдельный типизированный параметр.
command.Parameters.Add("@Id", SqlDbType.Int).Value = id;
Что здесь важно:
@Id— имя параметра в SQL-тексте, оно не является значением.SqlDbType.Int— явно указываем SQL-тип. Это правильнее, чемAddWithValue, потому чтоAddWithValueпередаёт .NET-тип, а SQL Server может неправильно его интерпретировать (например, объект станетnvarchar, и запрос по числовому полю будет работать медленнее или вообще иначе).SqlClientотправляет значение и тип отдельно от текста запроса, поэтому вставить произвольный SQL через значение параметра невозможно.
Также обратите внимание, что текст SQL в проекте всегда пишется константой
(через @"..." — многострочная строка), а пользовательские данные подставляются
только через Parameters. Поиск по коду не должен находить конкатенацию
пользовательских данных с SQL.
3.4 SqlDataReader — чтение результатов запроса
SqlDataReader — «читатель» результата SELECT. Он работает в режиме
forward-only (только вперёд) и read-only (только чтение).
Что это значит на практике:
- Данные приходят по одной строке, а не сразу все. Это экономит память — можно обработать миллион строк, не загружая их в память целиком.
- Нельзя вернуться на предыдущую строку (нельзя «отмотать назад»).
- Пока
readerоткрыт, нельзя выполнять другие команды на том же подключении.
Пример из проекта:
// Выполняем SELECT и получаем потоковое чтение результата
await using var reader = await command.ExecuteReaderAsync();
// пока reader.ReadAsync() возвращает true — читаем очередную строку
while (await reader.ReadAsync())
{
int id = reader.GetInt32(reader.GetOrdinal("Id"));
string name = reader.GetString(reader.GetOrdinal("Name"));
// Если в поле может быть NULL, проверяем это отдельно
string? description = reader.IsDBNull(reader.GetOrdinal("Description"))
? null
: reader.GetString(reader.GetOrdinal("Description"));
products.Add(new Product { Id = id, Name = name, Description = description });
}
Методы GetInt32, GetString, GetDecimal читают значение нужного типа.
GetOrdinal("Name") возвращает номер колонки по её названию — читать по номеру
(reader.GetString(0)) тоже можно, но по названию понятнее и безопаснее
(если порядок колонок в запросе изменится, код продолжит работать).
3.5 SqlDataAdapter и DataTable — disconnected (автономный) режим
Мы уже посмотрели на connected подход: подключение открыто, SqlDataReader
читает строки по одной. Он быстрый, но подключение занято всё время чтения.
Disconnected (автономный) подход работает иначе:
SqlDataAdapterполучает команду с запросом.- Сам открывает подключение (если оно закрыто), выполняет
SELECTи забирает весь результат в память. - Заполняет
DataTable(илиDataSet) — таблицу, которая живёт в памяти приложения. - Сам закрывает подключение (если открывал его).
После этого вы работаете с DataTable как с обычной таблицей в памяти,
а база данных полностью свободна и не ждёт вас.
Пример из проекта (AdoNetShop/Data/ProductRepository.cs):
public async Task<DataTable> GetProductsTableAsync()
{
await using var connection = _connectionFactory.CreateConnection();
string query = "SELECT Id, Name, Price, StockQuantity FROM Products";
await using var command = new SqlCommand(query, connection);
// SqlDataAdapter — «мост» между источником данных и DataTable
using var adapter = new SqlDataAdapter(command);
var table = new DataTable("Products");
// Fill() сам откроет подключение, выполнит запрос,
// заполнит DataTable и закроет подключение.
// Fill() синхронный, поэтому оборачиваем его в Task.Run.
await Task.Run(() => adapter.Fill(table));
return table;
}
А дальше в Program.cs мы работаем с таблицей без базы данных:
DataTable table = await service.GetProductsTableAsync();
foreach (DataRow row in table.Rows) // никакого подключения к БД уже нет
{
int id = (int)row["Id"];
string name = (string)row["Name"];
decimal price = (decimal)row["Price"];
Console.WriteLine($"{id}\t{name}\t{price:C}");
}
Когда что использовать?
SqlDataReader— когда данных много (тысячи строк) и нужна экономия памяти и максимальная скорость.SqlDataAdapter+DataTable— когда данных немного и с ними нужно удобно работать: сортировать, фильтровать, показывать в отчёте, передавать дальше.
3.6 SqlTransaction — транзакция
Транзакция — это группа SQL-операций, которые выполняются как одно целое: всё или ничего.
Классический пример из нашего проекта — создание заказа. Это несколько операций:
1. Создать заказ в таблице Orders
2. Для каждого товара заказа:
2.1 Уменьшить остаток в таблице Products
2.2 Добавить позицию в таблицу OrderItems
3. Зафиксировать (COMMIT)
Если на шаге 2.1 не хватает товара, то нельзя оставлять базу в таком состоянии: заказ создан, а половина позиций — нет. Нужно отменить всё. Именно это и обеспечивает транзакция.
Реализация в проекте (AdoNetShop/Data/OrderRepository.cs):
await using var connection = _connectionFactory.CreateConnection();
await connection.OpenAsync();
// 1) Начинаем транзакцию
await using var transaction = (SqlTransaction)await connection.BeginTransactionAsync();
try
{
// 2) Создаём заказ
await using var insertOrder = new SqlCommand(insertOrderQuery, connection, transaction);
insertOrder.Parameters.Add("@CustomerId", SqlDbType.Int).Value = order.CustomerId;
int newOrderId = Convert.ToInt32(await insertOrder.ExecuteScalarAsync());
foreach (var item in order.Items)
{
// 3) Уменьшаем остаток. В условии — проверка, что товара хватает:
// WHERE Id = @ProductId AND StockQuantity >= @Quantity
await using var updateStock = new SqlCommand(updateStockQuery, connection, transaction);
updateStock.Parameters.Add("@ProductId", SqlDbType.Int).Value = item.ProductId;
updateStock.Parameters.Add("@Quantity", SqlDbType.Int).Value = item.Quantity;
int rowsAffected = await updateStock.ExecuteNonQueryAsync();
if (rowsAffected == 0)
{
// Товара не хватает (или его нет). Бросаем исключение —
// код перейдёт в catch и сделает откат всей транзакции.
throw new Exception($"Недостаточно товара на складе для ProductId: {item.ProductId}");
}
// 4) Добавляем позицию заказа
await using var insertItem = new SqlCommand(insertItemQuery, connection, transaction);
// ... параметры ...
await insertItem.ExecuteNonQueryAsync();
}
// 5) Всё прошло успешно — фиксируем результат
await transaction.CommitAsync();
return newOrderId;
}
catch (Exception ex)
{
// 6) Любая ошибка — откатываем все изменения
await transaction.RollbackAsync();
throw new Exception($"Ошибка при создании заказа. Транзакция отменена. Детали: {ex.Message}", ex);
}
Обратите внимание на важную деталь: каждая команда внутри транзакции получает
третий параметр — transaction. Если забыть его передать, SQL Server выдаст ошибку
«Invalid operation. The transaction associated with this command is not the connection's
active transaction».
4. Connected vs Disconnected: в чём разница
| Connected (подключенный) | Disconnected (автономный) | |
|---|---|---|
| Что используем | SqlConnection + SqlCommand + SqlDataReader |
SqlDataAdapter + DataTable / DataSet |
| Когда открыто подключение | Всё время, пока читаем данные | Только на момент загрузки данных |
| Как читаем | По одной строке (ReadAsync()) |
Сразу таблица целиком |
| Расход памяти | Минимальный (строка за строкой) | Вся выборка в памяти |
| Скорость на больших объёмах | Выше | Ниже (из-за накладных расходов на загрузку) |
| Можно работать без БД после загрузки | Нет | Да |
| Где в проекте | GetAllAsync(), GetByIdAsync(), CreateOrderWithTransactionAsync() |
GetProductsTableAsync() (пункт меню 10) |
Простое правило: данных много → SqlDataReader; данных мало и нужно удобно
работать в памяти → SqlDataAdapter + DataTable.
5. Структура проекта
Artem/
│
├── README.md ← этот файл (теория + инструкции)
├── .gitignore
├── AdoNetShop.slnx ← файл решения Visual Studio / Rider
│
├── AdoNetShop/ ← сам проект C#
│ ├── AdoNetShop.csproj ← файл проекта, здесь подключается Microsoft.Data.SqlClient
│ ├── Program.cs ← консольное меню и сценарии демонстрации
│ ├── appsettings.json ← строка подключения к БД (НЕ в коде!)
│ │
│ ├── Models/ ← простые классы предметной области (POCO)
│ │ ├── Product.cs ← Id, Name, Description, Price, StockQuantity, CategoryId
│ │ ├── Category.cs ← Id, Name
│ │ ├── Customer.cs ← Id, Name, Email, Phone
│ │ ├── Order.cs ← Id, CustomerId, OrderDate, Status, TotalAmount
│ │ └── OrderItem.cs ← Id, OrderId, ProductId, Quantity, Price
│ │
│ ├── Data/ ← ВЕСЬ РАБОТА С БАЗОЙ ЗДЕСЬ (ADO.NET)
│ │ ├── DbConnectionFactory.cs ← создание SqlConnection из строки подключения
│ │ ├── ProductRepository.cs ← CRUD товаров + SqlDataReader + SqlDataAdapter + Stored Procedure
│ │ ├── CategoryRepository.cs ← чтение категорий
│ │ ├── CustomerRepository.cs ← чтение клиентов
│ │ └── OrderRepository.cs ← создание заказа в транзакции (Commit / Rollback)
│ │
│ └── Services/
│ └── ShopService.cs ← небольшая прослойка: проверки и «прокси» к репозиториям
│
└── database/ ← SQL-скрипты (выполняются по порядку)
├── 01_create_database.sql ← создание базы данных AdoNetShop
├── 02_create_tables.sql ← таблицы, ключи, ограничения CHECK, индексы
├── 03_insert_data.sql ← тестовые данные
├── 04_queries.sql ← примеры запросов
├── 05_procedures.sql ← хранимые процедуры
└── 06_test.sql ← проверочные запросы (выполнять вручную)
Почему такая структура? Это классическое разделение ответственности:
- Models — «что есть в магазине». Никакой логики, только данные.
- Data (Repository) — «как получить и сохранить данные». Здесь живёт
весь ADO.NET:
SqlConnection,SqlCommand,SqlDataReader, транзакции. - Services — «что можно сделать с магазином». Здесь простые проверки данных перед сохранением. Бизнес-логика намеренно не усложнена.
- Program — «как это показываем пользователю». Консольное меню.
Repository (репозиторий) — просто класс, в котором собраны SQL-запросы к одной таблице. Это удобно: когда понадобится поменять SQL, вы открываете один файл.
6. Структура базы данных
Categories (категории)
Id INT IDENTITY PRIMARY KEY
Name NVARCHAR(100) NOT NULL
▲
│ FK Products.CategoryId
│
Products (товары) ─┐
Id INT IDENTITY PRIMARY KEY│ 1 товар ──► много позиций заказа
Name NVARCHAR(200) NOT NULL │
Description NVARCHAR(MAX) NULL │
Price DECIMAL(18,2) NOT NULL CHECK (Price > 0)
StockQuantity INT NOT NULL CHECK (StockQuantity >= 0)
CategoryId INT NOT NULL │
▲ │
│ │ FK OrderItems.ProductId
│ │
Customers (клиенты) │
Id INT IDENTITY PRIMARY KEY │
Name NVARCHAR(100) NOT NULL │
Email NVARCHAR(100) NOT NULL UNIQUE │
Phone NVARCHAR(20) NULL │
▲ │
│ FK Orders.CustomerId │
│ │
Orders (заказы) ─────────────────────┘
Id INT IDENTITY PRIMARY KEY
CustomerId INT NOT NULL
OrderDate DATETIME2 NOT NULL DEFAULT GETDATE()
Status NVARCHAR(50) NOT NULL DEFAULT 'New'
TotalAmount DECIMAL(18,2) NOT NULL CHECK (TotalAmount >= 0)
OrderItems (позиции заказа)
Id INT IDENTITY PRIMARY KEY
OrderId INT NOT NULL FK Orders(Id) ON DELETE CASCADE
ProductId INT NOT NULL FK Products(Id)
Quantity INT NOT NULL CHECK (Quantity > 0)
Price DECIMAL(18,2) NOT NULL CHECK (Price > 0)
Что здесь важно и почему:
IDENTITY— автоинкремент: база сама генерируетId, приложение его не придумывает.PRIMARY KEY— гарантирует уникальность строк.FOREIGN KEY— гарантирует, что нельзя создать заказ для несуществующего клиента или позицию для несуществующего товара. База защищает целостность данных.CHECK— ограничение на значения: цена не может быть отрицательной, количество в заказе — нулевым или отрицательным.UNIQUEнаCustomers.Email— нельзя завести двух клиентов с одним email.INDEXна внешних ключах (IX_Products_CategoryId,IX_Orders_CustomerId,IX_OrderItems_OrderId,IX_OrderItems_ProductId) — ускоряютJOINи поиск.DECIMAL(18,2)для денег — неFLOATи неDOUBLE: это единственный тип, который хранит денежные значения без ошибок округления.NVARCHAR, а неVARCHAR—NVARCHARподдерживает кириллицу и любые языки.
7. Как установить SQL Server
Есть три бесплатных варианта — выбирайте любой:
Вариант А: SQL Server Express (самый простой для учёбы)
- Скачайте SQL Server Express с официального сайта Microsoft: https://www.microsoft.com/ru-ru/products/sql-server/sql-server-express-downloads
- При установке отметьте Basic (базовый) тип установки.
- В мастере установки обязательно включите средства управления — SQL Server Management Tools (SSMS). Это программа, в которой вы будете выполнять SQL-скрипты.
- После установки запустите SQL Server Configuration Manager и проверьте, что служба SQL Server (SQLEXPRESS) запущена (состояние «Running»).
- Откройте SSMS и подключитесь к серверу. Сервер для Express по умолчанию
называется
.\SQLEXPRESS.
Вариант Б: Developer Edition (полная версия, бесплатно для разработки)
Скачивается на той же странице Microsoft. После установки сервер по умолчанию
называется localhost (или имя вашего компьютера).
Вариант В: Docker (если установлен Docker)
docker run -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=Your_password123!" -p 1433:1433 -d mcr.microsoft.com/mssql/server:2022-latest
Важно: SQL Server — это отдельная программа-сервер. Пока он не установлен и не запущен, приложение не сможет подключиться, даже если код написан правильно.
8. Как создать базу данных и выполнить SQL-скрипты
Способ 1: через SSMS (рекомендуется для учебной работы)
- Запустите SQL Server Management Studio.
- В поле «Тип сервера» выберите Database Engine.
- В поле «Имя сервера» введите
localhost(или.\SQLEXPRESSдля Express, илиlocalhost\SQLEXPRESS— этот вариант подходит всегда). - Аутентификация: Windows Authentication — вводить логин и пароль не нужно.
- Нажмите Connect. Если увидели окно с деревом серверов — вы подключились.
Дальше откройте каждый файл из папки database через File → Open → File
и нажмите Execute (или клавишу F5). Порядок важен, выполняйте строго
по номерам:
| Шаг | Файл | Что делает |
|---|---|---|
| 1 | 01_create_database.sql |
Создаёт базу данных AdoNetShop |
| 2 | 02_create_tables.sql |
Создаёт таблицы, ключи, ограничения, индексы |
| 3 | 03_insert_data.sql |
Заполняет таблицы тестовыми данными |
| 4 | 04_queries.sql |
Примеры запросов (для понимания, можно не выполнять) |
| 5 | 05_procedures.sql |
Создаёт хранимые процедуры |
| 6 | 06_test.sql |
Проверочные запросы (для самопроверки, можно не выполнять) |
Скрипты написаны «безопасно»: 01 использует IF NOT EXISTS, 02 создаёт
таблицы только если их нет, 05 удаляет процедуру перед созданием. Поэтому
скрипты можно запускать повторно — ошибки «уже существует» не будет.
Способ 2: через командную строку (sqlcmd)
# Способ с Windows-аутентификацией
sqlcmd -S localhost -E -i "database\01_create_database.sql"
sqlcmd -S localhost -E -d AdoNetShop -i "database\02_create_tables.sql"
sqlcmd -S localhost -E -d AdoNetShop -i "database\03_insert_data.sql"
# Для SQL Server Express
sqlcmd -S .\SQLEXPRESS -E -i "database\01_create_database.sql"
Проверка, что всё создалось
В SSMS подключитесь к базе AdoNetShop (в списке слева) и выполните:
SELECT * FROM Categories;
SELECT * FROM Products;
SELECT * FROM Customers;
Вы должны увидеть: 3 категории, 6 товаров, 2 клиентов.
9. Как изменить строку подключения (Connection String)
Строка подключения не зашита в код. Она лежит в файле
AdoNetShop/appsettings.json:
{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=AdoNetShop;Trusted_Connection=True;TrustServerCertificate=True;"
}
}
Читает её класс DbConnectionFactory (AdoNetShop/Data/DbConnectionFactory.cs):
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.Build();
_connectionString = configuration.GetConnectionString("DefaultConnection");
Что означают параметры строки подключения
| Параметр | Значение в примере | Что это |
|---|---|---|
Server |
localhost |
Адрес сервера SQL Server |
Database |
AdoNetShop |
Имя базы данных |
Trusted_Connection |
True |
Использовать Windows-аутентификацию (логин/пароль не нужны) |
TrustServerCertificate |
True |
Доверять самоподписанному SSL-сертификату SQL Server (нужно для свежих версий Microsoft.Data.SqlClient) |
Если SQL Server работает как именованный экземпляр
Так бывает после установки SQL Server Express — сервер называется SQLEXPRESS.
В строке подключения именованный экземпляр пишется через обратный слэш \:
"DefaultConnection": "Server=.\\SQLEXPRESS;Database=AdoNetShop;Trusted_Connection=True;TrustServerCertificate=True;"
Про обратный слэш и JSON. В JSON символ
\— начало escape-последовательности, поэтому обратный слэш записывается двойным:\\. В строке подключения это означает один-единственный обратный слэш.
Варианты, которые тоже работают:
Server=localhost\SQLEXPRESS — если хотите быть явными
Server=(local)\SQLEXPRESS — эквивалент
Server=.\MSSQLSERVER — именованный экземпляр основного сервера
Если нужна аутентификация SQL Server (логин и пароль)
"DefaultConnection": "Server=localhost;Database=AdoNetShop;User Id=sa;Password=ТвойПароль;TrustServerCertificate=True;"
Внимание: пароль в
appsettings.json— это «учебный» вариант. В настоящем проекте такие данные хранят в переменных окружения или в защищённом хранилище секретов. Для лабораторной работы достаточноappsettings.json— главное, что пароль не зашит в исходный код, а вынесен в конфигурацию.
Как проверить, что подключение работает
# В SSMS: Server → Connect → Database Engine, имя сервера как в appsettings.json
# Либо в командной строке проекта (Visual Studio / Rider → Terminal):
cd AdoNetShop
dotnet run
Если в меню выбрать пункт 1. Список товаров и вы увидите таблицу с товарами —
подключение работает корректно.
10. Как запустить проект
Что нужно
- .NET 8 SDK — проверьте командой
dotnet --version - Запущенный SQL Server (см. раздел 7)
- Выполненные скрипты
01,02,03,05(см. раздел 8) - Правильная строка подключения в
appsettings.json(см. раздел 9)
Запуск из командной строки
cd AdoNetShop
# Восстановить пакеты (обычно делается автоматически)
dotnet restore
# Собрать проект
dotnet build
# Запустить
dotnet run
Запуск из Visual Studio
- Откройте файл решения
AdoNetShop.slnx(илиAdoNetShop.csproj). - Убедитесь, что в качестве стартового проекта выбран
AdoNetShop. - Нажмите
Ctrl + F5(запуск без отладки) илиF5(с отладкой). - Для отладки поставьте точки останова в
ProductRepository.GetAllAsyncиOrderRepository.CreateOrderWithTransactionAsync— очень полезно для защиты.
Запуск из Rider
Откройте папку проекта в Rider — он сам определит .csproj. Нажмите Shift + F10.
Что должно появиться
================================
ADO.NET SHOP
================================
1. Список товаров
2. Найти товар по ID
3. Добавить товар
4. Изменить товар
5. Удалить товар
6. Список категорий
7. Список клиенты
8. Создать заказ (Демонстрация Transaction)
9. Просмотреть заказы
10. Демонстрация DataAdapter (Disconnected mode)
11. Демонстрация Stored Procedures
0. Выход
================================
11. Что умеет программа (демонстрация преподавателю)
Каждый пункт меню — это отдельная демонстрация элемента ADO.NET.
Пункт 1. Список товаров → SqlDataReader + ExecuteReaderAsync
Вызывает ProductRepository.GetAllAsync(). Показывает потоковое чтение
результата SELECT через SqlDataReader.
Что говорить: «Открывается соединение, выполняется SELECT, и результат
читается построчно объектом SqlDataReader, пока ReadAsync() возвращает true».
Пункт 2. Найти товар по ID → параметризованный SqlCommand
Вызывает GetByIdAsync(id). Демонстрирует WHERE Id = @Id + SqlParameter.
Что говорить: «Пользовательский ввод никогда не подставляется в текст запроса через конкатенацию — только через параметры. Это защита от SQL Injection».
Пункт 3. Добавить товар → ExecuteScalarAsync
Вызывает CreateAsync(product). Вставляет строку и получает сгенерированный
базой Id через SELECT SCOPE_IDENTITY().
Что говорить: «ExecuteScalarAsync возвращает одно значение — в нашем случае
это Id нового товара, который сгенерировал IDENTITY».
Пункт 4. Изменить товар → ExecuteNonQueryAsync
Вызывает UpdateAsync(product). Возвращает количество затронутых строк.
Пункт 5. Удалить товар → ExecuteNonQueryAsync
Вызывает DeleteAsync(id). Обратите внимание: если на товар ссылаются строки
в OrderItems, база не даст удалить такой товар (Foreign Key). Это тоже можно
показать как защиту целостности данных.
Пункт 6. Категории / Пункт 7. Клиенты → простые SELECT
Чтение справочных таблиц, которые нужны, чтобы заполнить выпадающие списки при добавлении товара и оформлении заказа.
Пункт 8. Создать заказ → SqlTransaction, Commit, Rollback ★
Главная демонстрация лабораторной. Создаёт заказ целиком в одной транзакции.
Как проверить откат:
- Выберите клиента (например,
1). - Добавьте товар
1в количестве2— заказ создастся успешно. - Добавьте товар, которого на складе мало, в количестве
9999. - Программа выведет ошибку «Недостаточно товара на складе».
- Откройте SSMS и выполните
SELECT * FROM Orders;— нового заказа не будет. Транзакция откатила всё: и заказ, и позиции, и изменения остатков.
Что говорить: «Транзакция гарантирует принцип "все или ничего". Нельзя получить ситуацию, когда заказ создан, а товары к нему — нет».
Пункт 9. Просмотреть заказы → чтение результата
Показывает созданные заказы: клиент, сумма, дата, статус.
Пункт 10. DataAdapter demo → SqlDataAdapter + DataTable (disconnected)
Вызывает GetProductsTableAsync(). Показывает, что данные загрузились
в таблицу в памяти и с ними можно работать без подключения к базе.
Что говорить: «SqlDataAdapter сам открыл подключение, скачал данные в
DataTable и закрыл подключение. Дальше мы работаем с обычной таблицей в памяти —
база данных в этот момент свободна для других пользователей».
Пункт 11. Stored Procedures demo → CommandType.StoredProcedure
Вызывает хранимые процедуры dbo.GetProducts и dbo.UpdateProductPrice.
Как проверить обработку ошибки: введите цену -5 — процедура вызовет
RAISERROR, приложение перехватит SqlException и покажет сообщение.
Что говорить: «Хранимые процедуры — это SQL-код, который хранится на сервере. Из C# мы вызываем их по имени, а не пишем запрос текстом. Основная бизнес-логика при этом остаётся в приложении — процедуры здесь только для демонстрации».
12. Обработка ошибок и валидация
Проверка пользовательского ввода (в Program.cs и ShopService.cs)
| Что проверяем | Где |
|---|---|
| ID — целое число | int.TryParse(...) перед вызовом репозитория |
| Цена больше нуля | ShopService.CreateProductAsync, Product.Price |
| Количество на складе не отрицательное | ShopService.CreateProductAsync, StockQuantity |
| Email не пустой | при добавлении клиента |
| Количество товара в заказе больше нуля | при оформлении заказа |
| Товар существует и его хватает | ShopService.CreateOrderAsync до открытия транзакции |
Пример:
if (product.Price <= 0)
throw new ArgumentException("Price must be greater than 0");
if (product.StockQuantity < 0)
throw new ArgumentException("Stock cannot be negative");
Обработка ошибок в приложении
В главном цикле Program.cs все пункты меню выполняются внутри try/catch:
try
{
switch (input) { /* ... */ }
}
catch (Exception ex)
{
Console.WriteLine($"[ОШИБКА]: {ex.Message}");
if (ex.InnerException != null)
Console.WriteLine($"[ДЕТАЛИ]: {ex.InnerException.Message}");
}
Такой подход выбран сознательно: для учебного проекта исключения не скрываются молча — пользователь видит, что произошло. В продакшене ошибки логировали бы в файл, а пользователю показывали бы нейтральное сообщение.
Дополнительно ошибки от самой СУБД обрабатываются отдельно. SqlException —
это исключение SQL Server, в нём есть номер ошибки и текст от сервера:
catch (SqlException ex)
{
Console.WriteLine($"Ошибка SQL Server (код {ex.Number}): {ex.Message}");
return false;
}
13. Какие файлы нужно изучить в первую очередь
Если преподаватель спросит «покажи, где у тебя ADO.NET», открывайте эти файлы в таком порядке:
1. AdoNetShop/Data/DbConnectionFactory.cs — подключение
Класс, который создаёт SqlConnection. Здесь видно, что строка подключения
берётся из appsettings.json.
2. AdoNetShop/Data/ProductRepository.cs — основные элементы ADO.NET
Самый важный файл лабораторной. В нём собрано всё сразу:
| Метод | Что показывает |
|---|---|
GetAllAsync() |
SqlConnection.OpenAsync(), SqlCommand, ExecuteReaderAsync(), SqlDataReader |
GetByIdAsync(id) |
SqlParameter, параметризованный запрос |
CreateAsync(product) |
ExecuteScalarAsync(), SCOPE_IDENTITY() |
UpdateAsync(product) |
ExecuteNonQueryAsync() для UPDATE |
DeleteAsync(id) |
ExecuteNonQueryAsync() для DELETE |
GetProductsTableAsync() |
SqlDataAdapter + DataTable (disconnected) |
GetProductsByStoredProcedureAsync(...) |
CommandType.StoredProcedure |
UpdatePriceViaStoredProcedureAsync(...) |
StoredProcedure + UPDATE, SqlException |
3. AdoNetShop/Data/OrderRepository.cs — транзакция
Метод CreateOrderWithTransactionAsync(): BeginTransactionAsync() → CommitAsync()
/ RollbackAsync(). Показывает, что все команды внутри транзакции получают
третий параметр transaction.
4. AdoNetShop/appsettings.json — конфигурация
Строка подключения вынесена из кода. Покажите преподавателю, что в исходниках нет ни логинов, ни паролей.
5. database/02_create_tables.sql — схема БД
PRIMARY KEY, FOREIGN KEY, CHECK, индексы. Свяжите таблицы с классами
из папки Models/.
6. database/05_procedures.sql — хранимые процедуры
Три процедуры, которые вызываются из ProductRepository.
14. Частые проблемы и их решения
| Симптом | Причина | Что сделать |
|---|---|---|
A network-related or instance-specific error occurred |
SQL Server не запущен или неверный адрес сервера | Запустите службу SQL Server. Проверьте Server в appsettings.json. Для Express должно быть .\SQLEXPRESS |
Login failed for user |
Неверный режим аутентификации | Попробуйте Trusted_Connection=True (Windows Auth) вместо User Id=sa;Password=... |
Certificate is not trusted |
Не настроен SSL-сертификат SQL Server | Добавьте TrustServerCertificate=True в строку подключения |
Cannot open database "AdoNetShop" |
База не создана | Выполните 01_create_database.sql в SSMS |
Invalid object name 'Products' |
Таблицы не созданы | Выполните 02_create_tables.sql |
Could not find file appsettings.json |
Запустили из другой папки | Запускайте команду из папки AdoNetShop, где лежит .csproj |
The model ... context has been changed |
Изменили модель после миграции | Не применимо к этому проекту — здесь нет EF и миграций |
Ошибка PK_... при INSERT |
Попытка вставить дубликат | Например, два клиента с одинаковым Email (UNIQUE) |
| Не компилируется проект | Не установлен .NET 8 SDK | Установите SDK и выполните dotnet restore |
Краткая памятка: 12 элементов ADO.NET, которые демонстрирует проект
| # | Элемент ADO.NET | Где найти |
|---|---|---|
| 1 | SqlConnection (создание) |
Data/DbConnectionFactory.cs |
| 2 | OpenAsync() / Close() / using |
все репозитории |
| 3 | SqlCommand |
Data/ProductRepository.cs |
| 4 | ExecuteReaderAsync() |
GetAllAsync |
| 5 | ExecuteScalarAsync() |
CreateAsync |
| 6 | ExecuteNonQueryAsync() |
UpdateAsync, DeleteAsync |
| 7 | SqlParameter |
GetByIdAsync, CreateAsync |
| 8 | SqlDataReader |
GetAllAsync |
| 9 | SqlDataAdapter + DataTable |
GetProductsTableAsync |
| 10 | SqlTransaction, Commit, Rollback |
Data/OrderRepository.cs |
| 11 | CommandType.StoredProcedure |
GetProductsByStoredProcedureAsync |
| 12 | SqlException |
UpdatePriceViaStoredProcedureAsync |
Проект создан в учебных целях для лабораторной работы по технологии ADO.NET. ADO.NET используется как основной и единственный способ работы с базой данных.