AdoNetShop/README.md

59 KiB
Raw Blame History

AdoNetShop — лабораторная работа по технологии ADO.NET

Что это за проект. Это небольшой учебный интернет-магазин, написанный на C# (.NET 8). Всё общение с базой данных Microsoft SQL Server выполняется вручную через ADO.NET (пакет Microsoft.Data.SqlClient). В проекте нет Entity Framework, Dapper, NHibernate и любых других ORM — нам важно увидеть, как приложение общается с БД «на низком уровне».


Содержание

  1. Что такое ADO.NET и зачем он нужен
  2. Что такое Provider (провайдер данных)
  3. Основные классы 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 — транзакции
  4. Connected vs Disconnected: в чём разница
  5. Структура проекта
  6. Структура базы данных
  7. Как установить SQL Server
  8. Как создать базу данных и выполнить SQL-скрипты
  9. Как изменить строку подключения (Connection String)
  10. Как запустить проект
  11. Что умеет программа (демонстрация преподавателю)
  12. Обработка ошибок и валидация
  13. Какие файлы нужно изучить в первую очередь
  14. Частые проблемы и их решения

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.

Зачем это нужно:

  1. Прямой контроль над SQL. Вы сами пишете запрос — значит, вы можете использовать всё, что умеет конкретная СУБД: индексы, хинты, JOIN, оконные функции, OUTPUT, временные таблицы. Никакой ORM не даст вам такой свободы без «хаков».
  2. Производительность. ORM делает много «умной» работы (отслеживание изменений, ленивая загрузка, кэширование), которая часто не нужна. Прямой ADO.NET — это минимальные накладные расходы. Для учебных и для production-задач вродя отчётов ADO.NET часто быстрее.
  3. Понимание того, как всё работает на самом деле. Даже если потом вы будете писать на Entity Framework, понимание ADO.NET помогает понять, что ORM делает под капотом.
  4. Не зависимость от лишних зависимостей. 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();

Три важных правила:

  1. Создать — new SqlConnection(connectionString). Сам по себе объект ещё не подключён, это просто «заготовка».
  2. Открыть — Open() / OpenAsync(). Только после этого можно выполнять команды. Строка подключения читается именно в этот момент (ленивое подключение).
  3. Закрыть/освободить — 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 (автономный) подход работает иначе:

  1. SqlDataAdapter получает команду с запросом.
  2. Сам открывает подключение (если оно закрыто), выполняет SELECT и забирает весь результат в память.
  3. Заполняет DataTable (или DataSet) — таблицу, которая живёт в памяти приложения.
  4. Сам закрывает подключение (если открывал его).

После этого вы работаете с 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 (самый простой для учёбы)

  1. Скачайте SQL Server Express с официального сайта Microsoft: https://www.microsoft.com/ru-ru/products/sql-server/sql-server-express-downloads
  2. При установке отметьте Basic (базовый) тип установки.
  3. В мастере установки обязательно включите средства управления — SQL Server Management Tools (SSMS). Это программа, в которой вы будете выполнять SQL-скрипты.
  4. После установки запустите SQL Server Configuration Manager и проверьте, что служба SQL Server (SQLEXPRESS) запущена (состояние «Running»).
  5. Откройте 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 (рекомендуется для учебной работы)

  1. Запустите SQL Server Management Studio.
  2. В поле «Тип сервера» выберите Database Engine.
  3. В поле «Имя сервера» введите localhost (или .\SQLEXPRESS для Express, или localhost\SQLEXPRESS — этот вариант подходит всегда).
  4. Аутентификация: Windows Authentication — вводить логин и пароль не нужно.
  5. Нажмите 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 создаёт базу только если её ещё нет;
  • 02 создаёт таблицы только если их нет;
  • 03 сначала удаляет прежние данные, а затем подставляет свои значения вместе с Id (через SET IDENTITY_INSERT), поэтому результат всегда одинаковый: товары Id = 1…6, клиенты Id = 1…2;
  • 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            — именованный экземпляр основного сервера

Если используется LocalDB (лёгкая версия SQL Server для разработки)

LocalDB — это «облегчённый» SQL Server, который запускается одной командой и не требует установки службы. Он удобен тем, что не занимает ресурсы и не мешает другим программам.

Запуск экземпляра (по умолчанию он называется MSSQLLocalDB):

sqllocaldb start MSSQLLocalDB

Строка подключения для него:

"DefaultConnection": "Server=(localdb)\\MSSQLLocalDB;Database=AdoNetShop;Trusted_Connection=True;TrustServerCertificate=True;"

В файле appsettings.json строка по умолчанию — Server=localhost. Если у вас LocalDB, замените её на вариант выше.

Если нужна аутентификация 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

  1. Откройте файл решения AdoNetShop.slnx (или AdoNetShop.csproj).
  2. Убедитесь, что в качестве стартового проекта выбран AdoNetShop.
  3. Нажмите Ctrl + F5 (запуск без отладки) или F5 (с отладкой).
  4. Для отладки поставьте точки останова в 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
12. Демонстрация ROLLBACK транзакции
0. Выход
================================

Если .NET 8 не установлен. Проект собирается под net8.0, но в .csproj включён параметр <RollForward>LatestMajor</RollForward>. Это значит: если на компьютере установлена только более новая версия .NET (9, 10, 11…), приложение всё равно запустится на ней. Если же ошибка всё-таки появилась, либо поставьте .NET 8 SDK, либо смените в AdoNetShop.csproj строку <TargetFramework>net8.0</TargetFramework> на установленную у вас версию.

Кодировка SQL-файлов. Все скрипты в папке database сохранены в UTF-8 с BOM (метка порядка байтов в начале файла). Благодаря этому кириллица корректно читается и в SSMS, и в Visual Studio, и в командной строке (sqlcmd -f 65001 -i файл.sql). Если вы пересохраните скрипт в другой кодировке — русские названия могут превратиться в «кракозябры».


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. Добавьте товар 1 в количестве 2 — заказ создастся успешно.
  3. Добавьте товар, которого на складе мало, в количестве 9999.
  4. Программа выведет ошибку «Недостаточно товара на складе».
  5. Откройте 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. Демонстрация ROLLBACK → наглядный откат транзакции ★

Второй сценарий, который обязательно стоит показать преподавателю. Метод OrderRepository.CreateOrderWithRollbackDemoAsync() делает ровно то, что описано в учебнике про транзакции:

  1. Открывает транзакцию (BeginTransactionAsync).
  2. Создаёт заказ.
  3. Успешно списывает 2 штуки первого товара.
  4. Затем «падает» с ошибкой (искусственное исключение).
  5. Выполняет RollbackAsync().
  6. Читает остатки заново и показывает их в консоли.

Что увидит преподаватель: первое списание прошло успешно, но после отката остатки такие же, как были до операции, а заказ в базе отсутствует. Вот эту картинку и нужно доказать: «даже если часть операций внутри транзакции успела выполниться, откат возвращает базу в исходное состояние».


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.
  • CreateOrderWithRollbackDemoAsync() — наглядная демонстрация отката для защиты: часть операций успевает выполниться, потом происходит ошибка, и RollbackAsync() возвращает базу в исходное состояние.

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
You must install or update .NET to run this application На компьютере нет runtime .NET 8 Установите .NET 8 SDK или проверьте, что в .csproj есть <RollForward>LatestMajor</RollForward>
Вместо русских букв «кракозябры» Скрипт сохранён не в UTF-8 Не пересохраняйте .sql в ANSI-кодировке либо запускайте sqlcmd -f 65001 -i файл.sql
Ошибка FOREIGN KEY при повторном запуске 03_insert_data.sql Ручная вставка данных без IDENTITY_INSERT Запустите скрипт заново: он сам удаляет старые данные и подставляет свои Id

Краткая памятка: 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
13 Commit / Rollback CreateOrderWithTransactionAsync
14 Наглядный откат (отмена выполненных операций) CreateOrderWithRollbackDemoAsync

Проект создан в учебных целях для лабораторной работы по технологии ADO.NET. ADO.NET используется как основной и единственный способ работы с базой данных.