| AdoNetShop | ||
| database | ||
| .gitignore | ||
| AdoNetShop.slnx | ||
| README.md | ||
AdoNetShop — лабораторная работа по ADO.NET
Небольшой интернет-магазин на C#, который работает с Microsoft SQL Server через ADO.NET. ORM здесь не используются: ни Entity Framework, ни Dapper. Все запросы к базе написаны вручную, поэтому в коде видно, как приложение общается с базой данных.
Что нужно для запуска
- .NET 8 SDK. Проверить:
dotnet --version - Microsoft SQL Server (Developer, Express или LocalDB).
- SQL Server Management Studio (SSMS) — программа, в которой выполняют SQL-скрипты.
Как запустить
Шаг 1. Создайте базу данных.
Откройте SSMS, подключитесь к своему серверу и выполните файлы из папки database
строго по порядку:
| Файл | Что делает |
|---|---|
01_create_database.sql |
создаёт базу AdoNetShop |
02_create_tables.sql |
создаёт таблицы, ключи, ограничения, индексы |
03_insert_data.sql |
добавляет тестовые данные |
05_procedures.sql |
создаёт хранимые процедуры |
Файлы 04_queries.sql и 06_test.sql — это примеры запросов для проверки.
Их можно не выполнять.
Все скрипты можно запускать повторно: старые данные удаляются, новые добавляются с теми же номерами.
Шаг 2. Проверьте строку подключения.
Файл AdoNetShop/appsettings.json:
{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=AdoNetShop;Trusted_Connection=True;TrustServerCertificate=True;"
}
}
Если сервер называется иначе, поменяйте только параметр Server:
Server=localhost обычный SQL Server
Server=.\\SQLEXPRESS SQL Server Express
Server=(localdb)\\MSSQLLocalDB LocalDB
В JSON обратный слэш пишется дважды: \\ вместо \.
Подробнее про каждый параметр строки подключения — в разделе ниже.
Шаг 3. Запустите приложение.
cd AdoNetShop
dotnet run
Шаг 4. Пользуйтесь меню.
Если база создана неправильно или сервер выключен, программа не сломается: она покажет понятное сообщение об ошибке и продолжит работать.
Что такое ADO.NET
ADO.NET — это набор классов, через которые программа на C# общается с базой данных. Без него приложение не знает, как отправить запрос в SQL Server и как получить ответ.
Схема выглядит так:
Приложение на C# <--> ADO.NET <--> SQL Server
наш код переводчик где лежат данные
Зачем он нужен
- Можно писать любой SQL, какой поддерживает база. ORM так не умеет без обходных путей.
- Работает быстро: нет лишних проверок и отслеживания изменений, которые делают ORM.
- Показывает, как устроена работа с базой на самом деле. Любой ORM внутри делает почти то же самое, только прячет это от вас.
Что такое провайдер
ADO.NET сам не ходит в базу. Для каждой СУБД есть отдельная библиотека — это и есть провайдер:
| База | Провайдер |
|---|---|
| SQL Server | Microsoft.Data.SqlClient |
| PostgreSQL | Npgsql |
| MySQL | MySqlConnector |
| SQLite | Microsoft.Data.Sqlite |
В нашем проекте используется Microsoft.Data.SqlClient. Он подключается одной
строкой в файле проекта:
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.0" />
Главные классы ADO.NET
SqlConnection — подключение к базе
SqlConnection — это подключение к серверу. Создаётся из строки подключения.
await using var connection = new SqlConnection(connectionString);
// Открываем подключение
await connection.OpenAsync();
Подключение нужно открыть перед запросом и закрыть после. Закрывать вручную не нужно:
await using сделает это сам, даже если внутри случилась ошибка.
Создаёт подключения класс Data/DbConnectionFactory.cs. Он читает строку подключения
из appsettings.json, поэтому в коде нет ни логинов, ни паролей.
SqlCommand — команда
SqlCommand хранит сам запрос: текст SQL или имя хранимой процедуры.
await using var command = new SqlCommand("SELECT Id, Name FROM Products", connection);
Выполнить команду можно тремя способами:
| Способ | Что делает | Когда нужен |
|---|---|---|
ExecuteReaderAsync() |
возвращает строки результата | когда читаем SELECT |
ExecuteScalarAsync() |
возвращает одно значение | когда нужно одно число, например новый Id |
ExecuteNonQueryAsync() |
возвращает число изменённых строк | для INSERT, UPDATE, DELETE |
SqlParameter — параметры и защита от SQL Injection
Пользовательский ввод нельзя вставлять в текст запроса склейкой строк.
Так делать нельзя:
// Неправильно
string query = "SELECT * FROM Products WHERE Name = '" + name + "'";
Если пользователь введёт ' OR '1'='1, запрос станет
WHERE Name = '' OR '1'='1' и вернёт все товары. Ещё хуже — можно подставить
'; DROP TABLE Products; -- и удалить таблицу. Это и называется SQL Injection.
Правильно — передавать значения через параметры:
// Правильно
command.Parameters.Add("@Id", SqlDbType.Int).Value = id;
Значение уходит на сервер отдельно от текста запроса, поэтому подставить свой SQL
через него невозможно. SqlDbType.Int указывает тип заранее: так сервер понимает
данные быстрее и правильнее, чем при AddWithValue.
Во всём проекте текст запроса — это неизменяемая строка, а вставляются только параметры. Ни одного такого склеивания в коде нет.
SqlDataReader — чтение строк
SqlDataReader возвращает строки по одной. Он работает только вперёд, поэтому
подходит для больших таблиц: память не переполняется.
// Выполняем SELECT и читаем результат построчно
await using var reader = await command.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
string name = reader.GetString(reader.GetOrdinal("Name"));
decimal price = reader.GetDecimal(reader.GetOrdinal("Price"));
// Если поле может быть пустым, проверяем это
string? description = reader.IsDBNull(reader.GetOrdinal("Description"))
? null
: reader.GetString(reader.GetOrdinal("Description"));
}
ReadAsync() возвращает true, пока есть строки. Пока открыт reader, другие
команды на этом же подключении выполнять нельзя.
SqlDataAdapter и DataTable — работа без базы
Иногда удобно забрать все данные в память и закрыть подключение. Это делает
SqlDataAdapter:
await using var connection = _connectionFactory.CreateConnection();
await using var command = new SqlCommand("SELECT Id, Name, Price FROM Products", connection);
using var adapter = new SqlDataAdapter(command);
var table = new DataTable();
// Сам открывает подключение, забирает данные и закрывает подключение
await Task.Run(() => adapter.Fill(table));
Fill() — синхронный метод, поэтому он обёрнут в Task.Run.
Дальше с таблицей можно работать как с обычной таблицей в памяти. База при этом свободна и не ждёт:
foreach (DataRow row in table.Rows)
{
int id = (int)row["Id"];
string name = (string)row["Name"];
decimal price = (decimal)row["Price"];
Console.WriteLine($"{id} {name} {price:C}");
}
SqlTransaction — всё или ничего
Транзакция — это несколько запросов, которые выполняются вместе. Если хотя бы один провалился, отменяются все.
Создание заказа как раз такой случай. Заказ состоит из нескольких действий:
создать заказ
для каждого товара:
уменьшить остаток на складе
добавить позицию в заказ
Если второго товара не оказалось на складе, нельзя оставлять базу в таком виде: заказ есть, а позиций нет. Транзакция отменяет всё целиком.
await using var transaction = (SqlTransaction)await connection.BeginTransactionAsync();
try
{
// создаём заказ
// списываем остатки и добавляем позиции
await transaction.CommitAsync(); // всё прошло успешно
}
catch (Exception ex)
{
await transaction.RollbackAsync(); // отменяем все изменения
throw;
}
Важная деталь: каждая команда внутри транзакции получает третий параметр — саму транзакцию. Если его забыть, сервер вернёт ошибку.
await using var command = new SqlCommand(query, connection, transaction);
Connected и Disconnected — разница
| Connected | Disconnected | |
|---|---|---|
| Классы | SqlConnection, SqlCommand, SqlDataReader |
SqlDataAdapter, DataTable |
| Подключение | открыто всё время чтения | открыто только на время загрузки |
| Чтение | по одной строке | сразу вся таблица |
| Память | тратится мало | тратится на всю выборку |
| Когда применять | много данных | мало данных и нужна удобная работа с ними |
Простое правило: данных много — берём SqlDataReader, данных мало и нужно
покрутить их в памяти — берём SqlDataAdapter.
Структура проекта
Artem/
├── README.md
├── AdoNetShop.slnx файл решения
│
├── AdoNetShop/ сам проект
│ ├── Program.cs консольное меню
│ ├── appsettings.json строка подключения
│ ├── AdoNetShop.csproj файл проекта
│ │
│ ├── Models/ данные магазина, без логики
│ │ ├── Product.cs
│ │ ├── Category.cs
│ │ ├── Customer.cs
│ │ ├── Order.cs
│ │ └── OrderItem.cs
│ │
│ ├── Data/ вся работа с базой
│ │ ├── DbConnectionFactory.cs создаёт SqlConnection
│ │ ├── ProductRepository.cs CRUD товаров, DataAdapter, процедуры
│ │ ├── CategoryRepository.cs чтение категорий
│ │ ├── CustomerRepository.cs чтение клиентов
│ │ └── OrderRepository.cs заказ в транзакции
│ │
│ └── Services/
│ └── ShopService.cs простые проверки перед сохранением
│
└── database/ скрипты для SQL Server
├── 01_create_database.sql
├── 02_create_tables.sql
├── 03_insert_data.sql
├── 04_queries.sql
├── 05_procedures.sql
└── 06_test.sql
Разделение простое: Models — что есть в магазине, Data — как это лежит в базе,
Services — что с этим можно сделать, Program — как это показываем пользователю.
Таблицы в базе
| Таблица | Что хранит |
|---|---|
Categories |
категории товаров: ноутбуки, смартфоны, аксессуары |
Products |
товары: название, описание, цена, остаток, категория |
Customers |
клиенты: имя, email, телефон |
Orders |
заказы: кто, когда, статус, сумма |
OrderItems |
что именно заказали: товар, количество, цена |
Что настроено в таблицах и зачем:
IDENTITY— база сама выдаётId.PRIMARY KEY— строки не повторяются.FOREIGN KEY— нельзя создать заказ для несуществующего клиента.CHECK— цена больше нуля, количество в заказе больше нуля.UNIQUE— нельзя завести двух клиентов с одним email.INDEXна внешних ключах — быстрее работаютJOIN.DECIMAL(18,2)для денег — неFLOAT, потому что он даёт ошибки округления.NVARCHARвместоVARCHAR— чтобы работала кириллица.
Что умеет программа
В меню 12 пунктов. Каждый показывает свой элемент ADO.NET.
| Пункт | Что делает | Что показывает |
|---|---|---|
| 1 | Список товаров | SqlDataReader, ExecuteReaderAsync |
| 2 | Найти товар по номеру | SqlParameter, параметризованный запрос |
| 3 | Добавить товар | ExecuteScalarAsync, получение нового Id |
| 4 | Изменить товар | ExecuteNonQueryAsync для UPDATE |
| 5 | Удалить товар | ExecuteNonQueryAsync для DELETE |
| 6 | Список категорий | простой SELECT |
| 7 | Список клиентов | простой SELECT |
| 8 | Создать заказ | транзакция: Commit или Rollback |
| 9 | Список заказов | чтение результата запроса |
| 10 | DataAdapter | SqlDataAdapter и DataTable |
| 11 | Stored Procedures | CommandType.StoredProcedure, ошибка RAISERROR |
| 12 | Откат транзакции | наглядный Rollback |
Пункт 8. Заказ и транзакция
Программа спрашивает клиента и список товаров, затем в одной транзакции создаёт заказ, списывает остатки и добавляет позиции. Если товара не хватает, база остаётся без изменений: заказ не появляется, остатки прежние.
Пункт 12. Наглядный откат
Этот пункт специально для защиты. Метод CreateOrderWithRollbackDemoAsync()
делает так:
- Открывает транзакцию.
- Создаёт заказ.
- Успешно списывает две штуки первого товара.
- Падает с ошибкой.
- Выполняет
Rollback. - Показывает остатки заново.
В консоли видно: списание прошло успешно, но после отката остатки такие же, как были до операции. Это и доказывает главное свойство транзакции.
Пункт 11. Хранимые процедуры
Вызываются процедуры dbo.GetProducts, dbo.GetProductById, dbo.UpdateProductPrice.
Если ввести цену -5, процедура вернёт ошибку через RAISERROR, а программа
покажет её как SqlException.
Бизнес-логика остаётся в приложении. Процедуры здесь только для демонстрации, что ADO.NET умеет их вызывать.
Проверка ввода и ошибок
Перед сохранением проверяется:
- номер введён как число;
- цена больше нуля;
- остаток не отрицательный;
- количество в заказе больше нуля;
- товар существует и его хватает;
- email не пустой.
Ошибки не скрываются: в консоль выводится сообщение, а для SQL Server показывается
код ошибки. В Program.cs главный цикл оборачивает каждый пункт в try/catch:
catch (Exception ex)
{
Console.WriteLine($"[ОШИБКА]: {ex.Message}");
if (ex.InnerException != null)
Console.WriteLine($"[ДЕТАЛИ]: {ex.InnerException.Message}");
}
Для учебной работы такой подход удобен: видно, что произошло. В настоящем проекте подробности писали бы в лог, а пользователю показывали короткое сообщение.
Что показать преподавателю
Data/DbConnectionFactory.cs— подключение и строка из конфига.Data/ProductRepository.cs— три способа выполнить команду, параметры,SqlDataReader,SqlDataAdapter, вызов процедуры.Data/OrderRepository.cs— транзакция: успешный заказ и откат.- Пункты меню 8, 10, 11, 12 — прямо во время защиты.
Если что-то не работает
| Сообщение | Причина | Что делать |
|---|---|---|
| Сервер не найден или недоступен | SQL Server не запущен | Запустите службу SQL Server |
| Не удалось открыть соединение | неверное имя экземпляра | для Express замените Server на .\\SQLEXPRESS |
| Login failed for user | неверный способ входа | проверьте Trusted_Connection=True |
| Недоверенный сертификат | нет SSL-сертификата | добавьте TrustServerCertificate=True |
| Cannot open database "AdoNetShop" | база не создана | выполните 01_create_database.sql |
| Invalid object name 'Products' | таблиц нет | выполните 02_create_tables.sql |
| Could not find file appsettings.json | запустили не из той папки | запускайте из папки AdoNetShop |
| Требуется .NET, которого нет | нет runtime .NET 8 | установите .NET 8 SDK |
Повторный запуск 03_insert_data.sql даёт ошибку FK |
данные вставлялись вручную | запустите скрипт заново, он чистит таблицы сам |
Как менять строку подключения
Строка лежит в AdoNetShop/appsettings.json. Разбор по параметрам:
| Параметр | Значение | Зачем |
|---|---|---|
Server |
localhost |
адрес сервера |
Database |
AdoNetShop |
имя базы |
Trusted_Connection |
True |
вход через Windows, без логина и пароля |
TrustServerCertificate |
True |
доверять сертификату локального сервера |
Если нужен вход по логину и паролю:
Server=localhost;Database=AdoNetShop;User Id=sa;Password=Пароль;TrustServerCertificate=True;
Пароль в файле конфигурации — это вариант для учёбы. В настоящем проекте такие данные хранят в переменных окружения. Важно, что в коде пароля нет.
Замечание про кодировку
SQL-файлы сохранены в UTF-8 с меткой в начале файла, поэтому кириллица правильно читается в SSMS, Visual Studio и в командной строке:
sqlcmd -S localhost -E -f 65001 -i database\01_create_database.sql
Если пересохранить файл в другой кодировке, русские названия превратятся в нечитаемый текст.
Проект собран под net8.0. Если на компьютере стоит только .NET 9, 10 или новее,
приложение всё равно запустится: в .csproj включён RollForward. Если понадобится,
строку <TargetFramework>net8.0</TargetFramework> можно поменять на установленную версию.