Compare commits

..

No commits in common. "9abe9d3db36261db19a1215ffc0ea7fd2428e023" and "4eef0f94324730f06f8b20f1aea04fd82c4e08d3" have entirely different histories.

23 changed files with 2 additions and 2282 deletions

23
.gitignore vendored
View File

@ -1,23 +0,0 @@
## Build results
bin/
obj/
[Dd]ebug/
[Rr]elease/
*.user
*.suo
*.userosscache
*.sln.docstates
## NuGet packages
*.nupkg
*.snupkg
packages/
## IDE
.vs/
.idea/
*.swp
## OS
Thumbs.db
.DS_Store

View File

@ -1,3 +0,0 @@
<Solution>
<Project Path="AdoNetShop/AdoNetShop.csproj" />
</Solution>

View File

@ -1,23 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@ -1,38 +0,0 @@
using System.Data;
using AdoNetShop.Models;
using Microsoft.Data.SqlClient;
namespace AdoNetShop.Data;
public class CategoryRepository
{
private readonly DbConnectionFactory _connectionFactory;
public CategoryRepository(DbConnectionFactory connectionFactory)
{
_connectionFactory = connectionFactory;
}
public async Task<List<Category>> GetAllAsync()
{
var categories = new List<Category>();
await using var connection = _connectionFactory.CreateConnection();
await connection.OpenAsync();
string query = "SELECT Id, Name FROM Categories";
await using var command = new SqlCommand(query, connection);
await using var reader = await command.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
categories.Add(new Category
{
Id = reader.GetInt32(0),
Name = reader.GetString(1)
});
}
return categories;
}
}

View File

@ -1,40 +0,0 @@
using System.Data;
using AdoNetShop.Models;
using Microsoft.Data.SqlClient;
namespace AdoNetShop.Data;
public class CustomerRepository
{
private readonly DbConnectionFactory _connectionFactory;
public CustomerRepository(DbConnectionFactory connectionFactory)
{
_connectionFactory = connectionFactory;
}
public async Task<List<Customer>> GetAllAsync()
{
var customers = new List<Customer>();
await using var connection = _connectionFactory.CreateConnection();
await connection.OpenAsync();
string query = "SELECT Id, Name, Email, Phone FROM Customers";
await using var command = new SqlCommand(query, connection);
await using var reader = await command.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
customers.Add(new Customer
{
Id = reader.GetInt32(0),
Name = reader.GetString(1),
Email = reader.GetString(2),
Phone = reader.IsDBNull(3) ? null : reader.GetString(3)
});
}
return customers;
}
}

View File

@ -1,22 +0,0 @@
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
namespace AdoNetShop.Data;
public class DbConnectionFactory
{
private readonly string _connectionString;
public DbConnectionFactory(IConfiguration configuration)
{
// Получаем строку подключения из конфигурации (appsettings.json)
_connectionString = configuration.GetConnectionString("DefaultConnection")
?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
}
// Фабричный метод для создания и настройки подключения
public SqlConnection CreateConnection()
{
return new SqlConnection(_connectionString);
}
}

View File

@ -1,117 +0,0 @@
using System.Data;
using AdoNetShop.Models;
using Microsoft.Data.SqlClient;
namespace AdoNetShop.Data;
public class OrderRepository
{
private readonly DbConnectionFactory _connectionFactory;
public OrderRepository(DbConnectionFactory connectionFactory)
{
_connectionFactory = connectionFactory;
}
// Демонстрация транзакции (Создание заказа)
public async Task<int> CreateOrderWithTransactionAsync(Order order)
{
await using var connection = _connectionFactory.CreateConnection();
await connection.OpenAsync();
// 1. Начинаем транзакцию
await using var transaction = (SqlTransaction)await connection.BeginTransactionAsync();
try
{
// 2. Создаем сам заказ
string insertOrderQuery = @"
INSERT INTO Orders (CustomerId, OrderDate, Status, TotalAmount)
VALUES (@CustomerId, @OrderDate, @Status, @TotalAmount);
SELECT SCOPE_IDENTITY();";
await using var insertOrderCommand = new SqlCommand(insertOrderQuery, connection, transaction);
insertOrderCommand.Parameters.Add("@CustomerId", SqlDbType.Int).Value = order.CustomerId;
insertOrderCommand.Parameters.Add("@OrderDate", SqlDbType.DateTime2).Value = order.OrderDate;
insertOrderCommand.Parameters.Add("@Status", SqlDbType.NVarChar, 50).Value = order.Status;
insertOrderCommand.Parameters.Add("@TotalAmount", SqlDbType.Decimal).Value = order.TotalAmount;
var newOrderIdObj = await insertOrderCommand.ExecuteScalarAsync();
if (newOrderIdObj == null) throw new Exception("Failed to create order.");
int newOrderId = Convert.ToInt32(newOrderIdObj);
// 3. Добавляем товары заказа и обновляем остатки
string insertItemQuery = @"
INSERT INTO OrderItems (OrderId, ProductId, Quantity, Price)
VALUES (@OrderId, @ProductId, @Quantity, @Price)";
string updateStockQuery = @"
UPDATE Products
SET StockQuantity = StockQuantity - @Quantity
WHERE Id = @ProductId AND StockQuantity >= @Quantity";
foreach (var item in order.Items)
{
// Проверка наличия и уменьшение остатка
await using var updateStockCommand = new SqlCommand(updateStockQuery, connection, transaction);
updateStockCommand.Parameters.Add("@ProductId", SqlDbType.Int).Value = item.ProductId;
updateStockCommand.Parameters.Add("@Quantity", SqlDbType.Int).Value = item.Quantity;
int rowsAffected = await updateStockCommand.ExecuteNonQueryAsync();
if (rowsAffected == 0)
{
// Если обновление не удалось (недостаточно товара или неверный ID),
// генерируем исключение. Это приведет к переходу в блок catch и откату транзакции.
throw new Exception($"Недостаточно товара на складе для ProductId: {item.ProductId}");
}
// Добавление позиции заказа
await using var insertItemCommand = new SqlCommand(insertItemQuery, connection, transaction);
insertItemCommand.Parameters.Add("@OrderId", SqlDbType.Int).Value = newOrderId;
insertItemCommand.Parameters.Add("@ProductId", SqlDbType.Int).Value = item.ProductId;
insertItemCommand.Parameters.Add("@Quantity", SqlDbType.Int).Value = item.Quantity;
insertItemCommand.Parameters.Add("@Price", SqlDbType.Decimal).Value = item.Price;
await insertItemCommand.ExecuteNonQueryAsync();
}
// 4. Если всё прошло успешно, фиксируем транзакцию
await transaction.CommitAsync();
return newOrderId;
}
catch (Exception ex)
{
// 5. При возникновении любой ошибки откатываем транзакцию
// Все изменения (созданный заказ, измененные остатки) будут отменены
await transaction.RollbackAsync();
// Пробрасываем ошибку дальше для обработки на уровне UI/Сервиса
throw new Exception($"Ошибка при создании заказа. Транзакция отменена. Детали: {ex.Message}", ex);
}
}
// Получение списка заказов (простая реализация)
public async Task<List<Order>> GetAllOrdersBasicAsync()
{
var orders = new List<Order>();
await using var connection = _connectionFactory.CreateConnection();
await connection.OpenAsync();
string query = "SELECT Id, CustomerId, OrderDate, Status, TotalAmount FROM Orders";
await using var command = new SqlCommand(query, connection);
await using var reader = await command.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
orders.Add(new Order
{
Id = reader.GetInt32(0),
CustomerId = reader.GetInt32(1),
OrderDate = reader.GetDateTime(2),
Status = reader.GetString(3),
TotalAmount = reader.GetDecimal(4)
});
}
return orders;
}
}

View File

@ -1,232 +0,0 @@
using System.Data;
using AdoNetShop.Models;
using Microsoft.Data.SqlClient;
namespace AdoNetShop.Data;
public class ProductRepository
{
private readonly DbConnectionFactory _connectionFactory;
public ProductRepository(DbConnectionFactory connectionFactory)
{
_connectionFactory = connectionFactory;
}
// CRUD: Read All (Получение списка)
public async Task<List<Product>> GetAllAsync()
{
var products = new List<Product>();
// Используем await using для автоматического освобождения ресурсов
await using var connection = _connectionFactory.CreateConnection();
await connection.OpenAsync(); // Открываем подключение
string query = "SELECT Id, Name, Description, Price, StockQuantity, CategoryId FROM Products";
await using var command = new SqlCommand(query, connection);
// SqlDataReader обеспечивает потоковое чтение результатов (forward-only)
await using var reader = await command.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
products.Add(new Product
{
Id = reader.GetInt32(reader.GetOrdinal("Id")),
Name = reader.GetString(reader.GetOrdinal("Name")),
// Обработка возможного NULL значения для Description
Description = reader.IsDBNull(reader.GetOrdinal("Description")) ? null : reader.GetString(reader.GetOrdinal("Description")),
Price = reader.GetDecimal(reader.GetOrdinal("Price")),
StockQuantity = reader.GetInt32(reader.GetOrdinal("StockQuantity")),
CategoryId = reader.GetInt32(reader.GetOrdinal("CategoryId"))
});
}
return products;
}
// CRUD: Read One (Получение одного товара по ID)
public async Task<Product?> GetByIdAsync(int id)
{
await using var connection = _connectionFactory.CreateConnection();
await connection.OpenAsync();
// Использование параметров для предотвращения SQL Injections
string query = "SELECT Id, Name, Description, Price, StockQuantity, CategoryId FROM Products WHERE Id = @Id";
await using var command = new SqlCommand(query, connection);
command.Parameters.Add("@Id", SqlDbType.Int).Value = id;
await using var reader = await command.ExecuteReaderAsync();
if (await reader.ReadAsync())
{
return new Product
{
Id = reader.GetInt32(reader.GetOrdinal("Id")),
Name = reader.GetString(reader.GetOrdinal("Name")),
Description = reader.IsDBNull(reader.GetOrdinal("Description")) ? null : reader.GetString(reader.GetOrdinal("Description")),
Price = reader.GetDecimal(reader.GetOrdinal("Price")),
StockQuantity = reader.GetInt32(reader.GetOrdinal("StockQuantity")),
CategoryId = reader.GetInt32(reader.GetOrdinal("CategoryId"))
};
}
return null; // Если товар не найден
}
// CRUD: Create (Добавление)
public async Task<int> CreateAsync(Product product)
{
await using var connection = _connectionFactory.CreateConnection();
await connection.OpenAsync();
// Используем OUTPUT INSERTED.Id или SCOPE_IDENTITY() для получения сгенерированного Id
string query = @"
INSERT INTO Products (Name, Description, Price, StockQuantity, CategoryId)
VALUES (@Name, @Description, @Price, @StockQuantity, @CategoryId);
SELECT SCOPE_IDENTITY();";
await using var command = new SqlCommand(query, connection);
// Явное указание типов параметров (хорошая практика)
command.Parameters.Add("@Name", SqlDbType.NVarChar, 200).Value = product.Name;
command.Parameters.Add("@Description", SqlDbType.NVarChar, -1).Value = (object?)product.Description ?? DBNull.Value;
command.Parameters.Add("@Price", SqlDbType.Decimal).Value = product.Price;
command.Parameters.Add("@StockQuantity", SqlDbType.Int).Value = product.StockQuantity;
command.Parameters.Add("@CategoryId", SqlDbType.Int).Value = product.CategoryId;
// ExecuteScalarAsync используется для получения единственного значения (в данном случае Id)
var newId = await command.ExecuteScalarAsync();
return Convert.ToInt32(newId);
}
// CRUD: Update (Обновление)
public async Task<bool> UpdateAsync(Product product)
{
await using var connection = _connectionFactory.CreateConnection();
await connection.OpenAsync();
string query = @"
UPDATE Products
SET Name = @Name, Description = @Description, Price = @Price,
StockQuantity = @StockQuantity, CategoryId = @CategoryId
WHERE Id = @Id";
await using var command = new SqlCommand(query, connection);
command.Parameters.Add("@Id", SqlDbType.Int).Value = product.Id;
command.Parameters.Add("@Name", SqlDbType.NVarChar, 200).Value = product.Name;
command.Parameters.Add("@Description", SqlDbType.NVarChar, -1).Value = (object?)product.Description ?? DBNull.Value;
command.Parameters.Add("@Price", SqlDbType.Decimal).Value = product.Price;
command.Parameters.Add("@StockQuantity", SqlDbType.Int).Value = product.StockQuantity;
command.Parameters.Add("@CategoryId", SqlDbType.Int).Value = product.CategoryId;
// ExecuteNonQueryAsync используется для запросов INSERT/UPDATE/DELETE
int rowsAffected = await command.ExecuteNonQueryAsync();
return rowsAffected > 0;
}
// CRUD: Delete (Удаление)
public async Task<bool> DeleteAsync(int id)
{
await using var connection = _connectionFactory.CreateConnection();
await connection.OpenAsync();
string query = "DELETE FROM Products WHERE Id = @Id";
await using var command = new SqlCommand(query, connection);
command.Parameters.Add("@Id", SqlDbType.Int).Value = id;
int rowsAffected = await command.ExecuteNonQueryAsync();
return rowsAffected > 0;
}
// Демонстрация DataAdapter и автономной модели (Disconnected Mode)
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 служит мостом между БД и DataSet/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;
}
// ============ Вызов хранимой процедуры (CommandType.StoredProcedure) ============
// Демонстрация получения данных через хранимую процедуру dbo.GetProducts.
// В SQL передаётся не текст запроса, а ИМЯ процедуры.
public async Task<List<Product>> GetProductsByStoredProcedureAsync(int? categoryId)
{
var products = new List<Product>();
await using var connection = _connectionFactory.CreateConnection();
await connection.OpenAsync();
// Главное отличие от обычного запроса: CommandType.StoredProcedure + текст = имя процедуры
await using var command = new SqlCommand("dbo.GetProducts", connection)
{
CommandType = CommandType.StoredProcedure
};
// Параметры хранимой процедуры передаются точно так же, как параметры запроса.
// SQL Server сам подставит значения вместо @CategoryId внутри процедуры.
command.Parameters.Add("@CategoryId", SqlDbType.Int).Value = (object?)categoryId ?? DBNull.Value;
await using var reader = await command.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
products.Add(new Product
{
Id = reader.GetInt32(0),
Name = reader.GetString(1),
Description = reader.IsDBNull(2) ? null : reader.GetString(2),
Price = reader.GetDecimal(3),
StockQuantity = reader.GetInt32(4),
CategoryId = reader.GetInt32(5)
});
}
return products;
}
// Вызов процедуры, которая изменяет данные, и получение числа затронутых строк.
// Здесь же обрабатывается ошибка, которую процедура отдаёт через RAISERROR.
public async Task<bool> UpdatePriceViaStoredProcedureAsync(int productId, decimal newPrice)
{
await using var connection = _connectionFactory.CreateConnection();
await connection.OpenAsync();
await using var command = new SqlCommand("dbo.UpdateProductPrice", connection)
{
CommandType = CommandType.StoredProcedure
};
command.Parameters.Add("@ProductId", SqlDbType.Int).Value = productId;
command.Parameters.Add("@NewPrice", SqlDbType.Decimal).Value = newPrice;
try
{
int rowsAffected = await command.ExecuteNonQueryAsync();
return rowsAffected > 0;
}
catch (SqlException ex)
{
// SqlException - это исключение самого SQL Server.
// В нём есть номер ошибки (Number) и текст сообщения от сервера.
Console.WriteLine($"Ошибка SQL Server (код {ex.Number}): {ex.Message}");
return false;
}
}
}

View File

@ -1,7 +0,0 @@
namespace AdoNetShop.Models;
public class Category
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
}

View File

@ -1,9 +0,0 @@
namespace AdoNetShop.Models;
public class Customer
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string? Phone { get; set; }
}

View File

@ -1,12 +0,0 @@
namespace AdoNetShop.Models;
public class Order
{
public int Id { get; set; }
public int CustomerId { get; set; }
public DateTime OrderDate { get; set; }
public string Status { get; set; } = "New";
public decimal TotalAmount { get; set; }
public List<OrderItem> Items { get; set; } = new List<OrderItem>();
}

View File

@ -1,10 +0,0 @@
namespace AdoNetShop.Models;
public class OrderItem
{
public int Id { get; set; }
public int OrderId { get; set; }
public int ProductId { get; set; }
public int Quantity { get; set; }
public decimal Price { get; set; }
}

View File

@ -1,14 +0,0 @@
namespace AdoNetShop.Models;
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public decimal Price { get; set; }
public int StockQuantity { get; set; }
public int CategoryId { get; set; }
// Навигационное свойство (заполняется опционально)
public Category? Category { get; set; }
}

View File

@ -1,346 +0,0 @@
using System.Data;
using AdoNetShop.Data;
using AdoNetShop.Models;
using AdoNetShop.Services;
using Microsoft.Extensions.Configuration;
namespace AdoNetShop;
class Program
{
static async Task Main(string[] args)
{
// 1. Настройка конфигурации для чтения appsettings.json
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.Build();
// 2. "Ручной" Dependency Injection
var connectionFactory = new DbConnectionFactory(configuration);
var productRepo = new ProductRepository(connectionFactory);
var categoryRepo = new CategoryRepository(connectionFactory);
var customerRepo = new CustomerRepository(connectionFactory);
var orderRepo = new OrderRepository(connectionFactory);
var shopService = new ShopService(productRepo, categoryRepo, customerRepo, orderRepo);
// 3. Главный цикл приложения
bool exit = false;
while (!exit)
{
Console.Clear();
Console.WriteLine("================================");
Console.WriteLine(" ADO.NET SHOP");
Console.WriteLine("================================");
Console.WriteLine("1. Список товаров");
Console.WriteLine("2. Найти товар по ID");
Console.WriteLine("3. Добавить товар");
Console.WriteLine("4. Изменить товар");
Console.WriteLine("5. Удалить товар");
Console.WriteLine("6. Список категорий");
Console.WriteLine("7. Список клиентов");
Console.WriteLine("8. Создать заказ (Демонстрация Transaction)");
Console.WriteLine("9. Просмотреть заказы");
Console.WriteLine("10. Демонстрация DataAdapter (Disconnected mode)");
Console.WriteLine("11. Демонстрация Stored Procedures");
Console.WriteLine("0. Выход");
Console.WriteLine("================================");
Console.Write("Выберите действие: ");
var input = Console.ReadLine();
try
{
switch (input)
{
case "1":
await ShowAllProducts(shopService);
break;
case "2":
await FindProduct(shopService);
break;
case "3":
await AddProduct(shopService);
break;
case "4":
await UpdateProduct(shopService);
break;
case "5":
await DeleteProduct(shopService);
break;
case "6":
await ShowAllCategories(shopService);
break;
case "7":
await ShowAllCustomers(shopService);
break;
case "8":
await CreateOrder(shopService);
break;
case "9":
await ShowAllOrders(shopService);
break;
case "10":
await DemoDataAdapter(shopService);
break;
case "11":
await DemoStoredProcedures(shopService);
break;
case "0":
exit = true;
break;
default:
Console.WriteLine("Неверный ввод.");
break;
}
}
catch (Exception ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"\n[ОШИБКА]: {ex.Message}");
if(ex.InnerException != null)
{
Console.WriteLine($"[ДЕТАЛИ]: {ex.InnerException.Message}");
}
Console.ResetColor();
}
if (!exit)
{
Console.WriteLine("\nНажмите любую клавишу для продолжения...");
Console.ReadKey();
}
}
}
private static async Task ShowAllProducts(ShopService service)
{
Console.WriteLine("\n--- Список товаров ---");
var products = await service.GetAllProductsAsync();
foreach (var p in products)
{
Console.WriteLine($"[{p.Id}] {p.Name} | Цена: {p.Price:C} | Остаток: {p.StockQuantity} | Кат: {p.CategoryId}");
}
}
private static async Task FindProduct(ShopService service)
{
Console.Write("\nВведите ID товара: ");
if (int.TryParse(Console.ReadLine(), out int id))
{
var product = await service.GetProductByIdAsync(id);
if (product != null)
{
Console.WriteLine($"Найдено: [{product.Id}] {product.Name} - {product.Description} | {product.Price:C} | {product.StockQuantity} шт.");
}
else
{
Console.WriteLine("Товар не найден.");
}
}
else
{
Console.WriteLine("Некорректный ID.");
}
}
private static async Task AddProduct(ShopService service)
{
Console.WriteLine("\n--- Добавление товара ---");
Console.Write("Название: ");
string name = Console.ReadLine() ?? "";
Console.Write("Описание: ");
string desc = Console.ReadLine() ?? "";
Console.Write("Цена: ");
if (!decimal.TryParse(Console.ReadLine(), out decimal price)) throw new ArgumentException("Неверный формат цены.");
Console.Write("Количество: ");
if (!int.TryParse(Console.ReadLine(), out int quantity)) throw new ArgumentException("Неверный формат количества.");
Console.Write("ID Категории: ");
if (!int.TryParse(Console.ReadLine(), out int catId)) throw new ArgumentException("Неверный формат ID категории.");
var product = new Product
{
Name = name,
Description = desc,
Price = price,
StockQuantity = quantity,
CategoryId = catId
};
int newId = await service.CreateProductAsync(product);
Console.WriteLine($"Товар успешно добавлен. Новый ID: {newId}");
}
private static async Task UpdateProduct(ShopService service)
{
Console.Write("\nВведите ID товара для обновления: ");
if (!int.TryParse(Console.ReadLine(), out int id)) return;
var product = await service.GetProductByIdAsync(id);
if (product == null)
{
Console.WriteLine("Товар не найден.");
return;
}
Console.WriteLine($"Текущее название: {product.Name}");
Console.Write("Новое название (оставьте пустым, чтобы не менять): ");
string name = Console.ReadLine() ?? "";
if (!string.IsNullOrWhiteSpace(name)) product.Name = name;
Console.WriteLine($"Текущая цена: {product.Price}");
Console.Write("Новая цена (оставьте пустым, чтобы не менять): ");
string priceStr = Console.ReadLine() ?? "";
if (!string.IsNullOrWhiteSpace(priceStr) && decimal.TryParse(priceStr, out decimal price)) product.Price = price;
bool updated = await service.UpdateProductAsync(product);
Console.WriteLine(updated ? "Товар обновлен." : "Не удалось обновить товар.");
}
private static async Task DeleteProduct(ShopService service)
{
Console.Write("\nВведите ID товара для удаления: ");
if (int.TryParse(Console.ReadLine(), out int id))
{
bool deleted = await service.DeleteProductAsync(id);
Console.WriteLine(deleted ? "Товар удален." : "Товар не найден или не может быть удален.");
}
}
private static async Task ShowAllCategories(ShopService service)
{
Console.WriteLine("\n--- Категории ---");
var cats = await service.GetAllCategoriesAsync();
foreach (var c in cats) Console.WriteLine($"[{c.Id}] {c.Name}");
}
private static async Task ShowAllCustomers(ShopService service)
{
Console.WriteLine("\n--- Клиенты ---");
var customers = await service.GetAllCustomersAsync();
foreach (var c in customers) Console.WriteLine($"[{c.Id}] {c.Name} ({c.Email})");
}
private static async Task CreateOrder(ShopService service)
{
Console.WriteLine("\n--- Создание заказа ---");
await ShowAllCustomers(service);
Console.Write("Введите ID клиента: ");
if (!int.TryParse(Console.ReadLine(), out int customerId)) return;
var items = new List<(int ProductId, int Quantity)>();
while (true)
{
Console.Write("Введите ID товара (или 0 для завершения): ");
if (!int.TryParse(Console.ReadLine(), out int productId)) continue;
if (productId == 0) break;
Console.Write("Введите количество: ");
if (!int.TryParse(Console.ReadLine(), out int quantity) || quantity <= 0)
{
Console.WriteLine("Неверное количество.");
continue;
}
items.Add((productId, quantity));
}
if (items.Count == 0)
{
Console.WriteLine("Заказ отменен (нет товаров).");
return;
}
Console.WriteLine("Попытка создать заказ в БД (Транзакция)...");
int orderId = await service.CreateOrderAsync(customerId, items);
Console.WriteLine($"Заказ успешно создан! ID: {orderId}");
}
private static async Task ShowAllOrders(ShopService service)
{
Console.WriteLine("\n--- Заказы ---");
var orders = await service.GetAllOrdersAsync();
foreach (var o in orders)
{
Console.WriteLine($"[{o.Id}] Клиент: {o.CustomerId} | Сумма: {o.TotalAmount:C} | Дата: {o.OrderDate:g} | Статус: {o.Status}");
}
}
private static async Task DemoDataAdapter(ShopService service)
{
Console.WriteLine("\n--- Демонстрация DataAdapter (Disconnected mode) ---");
Console.WriteLine("Запрашиваем данные через DataAdapter.Fill()...");
DataTable table = await service.GetProductsTableAsync();
Console.WriteLine($"Получено строк: {table.Rows.Count}");
Console.WriteLine("Подключение к БД уже закрыто. Данные находятся в памяти (DataTable).\n");
Console.WriteLine("Id\tName\t\t\tPrice");
Console.WriteLine("--------------------------------------------------");
// Чтение данных из автономного DataTable
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,-20}\t{price:C}");
}
}
// Демонстрация вызова хранимых процедур через ADO.NET
private static async Task DemoStoredProcedures(ShopService service)
{
Console.WriteLine("\n--- Демонстрация Stored Procedures ---");
await ShowAllCategories(service);
Console.Write("Введите Id категории (0 = все товары): ");
if (!int.TryParse(Console.ReadLine(), out int categoryId))
{
Console.WriteLine("Некорректный ввод.");
return;
}
// Вызов dbo.GetProducts. В C# мы передаём имя процедуры, а не SQL-запрос.
Console.WriteLine($"\nВызываем хранимую процедуру dbo.GetProducts @CategoryId = {(categoryId == 0 ? "NULL" : categoryId.ToString())}...");
var products = await service.GetProductsByStoredProcedureAsync(categoryId == 0 ? null : categoryId);
Console.WriteLine($"Процедура вернула товаров: {products.Count}");
foreach (var p in products)
{
Console.WriteLine($" [{p.Id}] {p.Name} — {p.Price:C} (остаток: {p.StockQuantity})");
}
if (products.Count == 0)
{
Console.WriteLine("\nНет данных для изменения цены.");
return;
}
Console.WriteLine($"\nВызываем dbo.UpdateProductPrice для товара [{products[0].Id}]: {products[0].Name}");
Console.Write("Новая цена (введите -5, чтобы увидеть ошибку от SQL Server): ");
if (!decimal.TryParse(Console.ReadLine(), out decimal newPrice))
{
Console.WriteLine("Некорректная цена.");
return;
}
// Процедура сама проверит цену и вернёт ошибку через RAISERROR.
// Мы её перехватываем как SqlException и показываем пользователю.
bool updated = await service.UpdatePriceViaStoredProcedureAsync(products[0].Id, newPrice);
Console.WriteLine(updated
? $"Цена товара [{products[0].Id}] успешно изменена на {newPrice:C}"
: "Цена не была изменена (смотрите сообщение об ошибке выше).");
}
}

View File

@ -1,99 +0,0 @@
using AdoNetShop.Data;
using AdoNetShop.Models;
namespace AdoNetShop.Services;
// Сервисы могут содержать дополнительную бизнес-логику,
// но в рамках этой лабы они выступают простыми прокси к репозиториям.
public class ShopService
{
private readonly ProductRepository _productRepository;
private readonly CategoryRepository _categoryRepository;
private readonly CustomerRepository _customerRepository;
private readonly OrderRepository _orderRepository;
public ShopService(
ProductRepository productRepository,
CategoryRepository categoryRepository,
CustomerRepository customerRepository,
OrderRepository orderRepository)
{
_productRepository = productRepository;
_categoryRepository = categoryRepository;
_customerRepository = customerRepository;
_orderRepository = orderRepository;
}
public Task<List<Product>> GetAllProductsAsync() => _productRepository.GetAllAsync();
public Task<Product?> GetProductByIdAsync(int id) => _productRepository.GetByIdAsync(id);
public async Task<int> CreateProductAsync(Product product)
{
// Пример простой валидации на уровне сервиса
if (product.Price <= 0) throw new ArgumentException("Price must be greater than 0");
if (product.StockQuantity < 0) throw new ArgumentException("Stock cannot be negative");
return await _productRepository.CreateAsync(product);
}
public Task<bool> UpdateProductAsync(Product product) => _productRepository.UpdateAsync(product);
public Task<bool> DeleteProductAsync(int id) => _productRepository.DeleteAsync(id);
public Task<List<Category>> GetAllCategoriesAsync() => _categoryRepository.GetAllAsync();
public Task<List<Customer>> GetAllCustomersAsync() => _customerRepository.GetAllAsync();
// Прокси-методы для демонстрации хранимых процедур
public Task<List<Product>> GetProductsByStoredProcedureAsync(int? categoryId)
=> _productRepository.GetProductsByStoredProcedureAsync(categoryId);
public Task<bool> UpdatePriceViaStoredProcedureAsync(int productId, decimal newPrice)
=> _productRepository.UpdatePriceViaStoredProcedureAsync(productId, newPrice);
public async Task<int> CreateOrderAsync(int customerId, List<(int ProductId, int Quantity)> requestedItems)
{
// 1. Формируем заказ
var order = new Order
{
CustomerId = customerId,
OrderDate = DateTime.Now,
Status = "Created",
Items = new List<OrderItem>()
};
decimal totalAmount = 0;
// 2. Получаем актуальные цены товаров
foreach (var reqItem in requestedItems)
{
var product = await _productRepository.GetByIdAsync(reqItem.ProductId);
if (product == null)
{
throw new ArgumentException($"Product with ID {reqItem.ProductId} not found.");
}
if(product.StockQuantity < reqItem.Quantity)
{
throw new ArgumentException($"Not enough stock for product '{product.Name}'. Available: {product.StockQuantity}, Requested: {reqItem.Quantity}");
}
var orderItem = new OrderItem
{
ProductId = reqItem.ProductId,
Quantity = reqItem.Quantity,
Price = product.Price
};
order.Items.Add(orderItem);
totalAmount += (reqItem.Quantity * product.Price);
}
order.TotalAmount = totalAmount;
// 3. Вызываем репозиторий для сохранения (внутри будет транзакция)
return await _orderRepository.CreateOrderWithTransactionAsync(order);
}
public Task<List<Order>> GetAllOrdersAsync() => _orderRepository.GetAllOrdersBasicAsync();
public Task<System.Data.DataTable> GetProductsTableAsync() => _productRepository.GetProductsTableAsync();
}

View File

@ -1,5 +0,0 @@
{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=AdoNetShop;Trusted_Connection=True;TrustServerCertificate=True;"
}
}

971
README.md
View File

@ -1,970 +1,3 @@
# AdoNetShop — лабораторная работа по технологии ADO.NET
# AdoNetShop
**Что это за проект.** Это небольшой учебный интернет-магазин, написанный на C# (.NET 8).
Всё общение с базой данных Microsoft SQL Server выполняется **вручную через ADO.NET**
(пакет `Microsoft.Data.SqlClient`). В проекте **нет** Entity Framework, Dapper, NHibernate
и любых других ORM — нам важно увидеть, как приложение общается с БД «на низком уровне».
---
## Содержание
1. [Что такое ADO.NET и зачем он нужен](#1-что-такое-adonet-и-зачем-он-нужен)
2. [Что такое Provider (провайдер данных)](#2-что-такое-provider-провайдер-данных)
3. [Основные классы ADO.NET, которые используются в проекте](#3-основные-классы-adonet-используемые-в-проекте)
- 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: в чём разница](#4-connected-vs-disconnected-в-чём-разница)
5. [Структура проекта](#5-структура-проекта)
6. [Структура базы данных](#6-структура-базы-данных)
7. [Как установить SQL Server](#7-как-установить-sql-server)
8. [Как создать базу данных и выполнить SQL-скрипты](#8-как-создать-базу-данных-и-выполнить-sql-скрипты)
9. [Как изменить строку подключения (Connection String)](#9-как-изменить-строку-подключения-connection-string)
10. [Как запустить проект](#10-как-запустить-проект)
11. [Что умеет программа (демонстрация преподавателю)](#11-что-умеет-программа-демонстрация-преподавателю)
12. [Обработка ошибок и валидация](#12-обработка-ошибок-и-валидация)
13. [Какие файлы нужно изучить в первую очередь](#13-какие-файлы-нужно-изучить-в-первую-очередь)
14. [Частые проблемы и их решения](#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`:
```xml
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.0" />
```
---
## 3. Основные классы ADO.NET, используемые в проекте
### 3.1 `SqlConnection` — подключение к базе
`SqlConnection` — это объект, представляющий **физическое соединение** с SQL Server.
Создаётся из **строки подключения** (connection string), которая содержит всю
необходимую информацию: адрес сервера, имя базы, способ аутентификации.
```csharp
// Файл: 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`:
```csharp
await using var connection = _connectionFactory.CreateConnection();
await connection.OpenAsync();
// ... работа с базой ...
// при выходе из блока соединение автоматически закроется и освободится,
// даже если внутри произошло исключение
```
`using` / `await using` — это конструкция языка C#, которая гарантирует вызов
`Dispose()`. Для `SqlConnection`, `SqlCommand`, `SqlDataReader` это обязательно,
потому что они используют неуправляемые ресурсы.
---
### 3.2 `SqlCommand` — команда (запрос)
`SqlCommand` — это объект, который содержит **что именно** мы хотим сделать с базой:
SQL-запрос, имя хранимой процедуры или текст команды.
```csharp
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`):
```csharp
// 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:
```csharp
// ОПАСНО! Не делайте так никогда.
string query = "SELECT * FROM Products WHERE Name = '" + name + "'";
```
Почему это опасно: если пользователь введёт в поле имя товара
```
' OR '1'='1
```
то запрос превратится в `SELECT * FROM Products WHERE Name = '' OR '1'='1'` —
вернётся вся таблица. Это и есть **SQL Injection**: злоумышленник может подставить
`'; DROP TABLE Products; --` и удалить таблицу.
✅ **ПРАВИЛЬНО** так — использовать параметры:
```csharp
// Безопасно. Значение не «вклеивается» в текст запроса,
// база получает его как отдельный типизированный параметр.
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` открыт, **нельзя** выполнять другие команды на том же подключении.
Пример из проекта:
```csharp
// Выполняем 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`):
```csharp
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` мы работаем с таблицей **без базы данных**:
```csharp
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`):
```csharp
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)
```bash
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` использует `IF NOT EXISTS`, `02` создаёт
таблицы только если их нет, `05` удаляет процедуру перед созданием. Поэтому
скрипты можно запускать повторно — ошибки «уже существует» не будет.
### Способ 2: через командную строку (sqlcmd)
```bash
# Способ с 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` (в списке слева) и выполните:
```sql
SELECT * FROM Categories;
SELECT * FROM Products;
SELECT * FROM Customers;
```
Вы должны увидеть: 3 категории, 6 товаров, 2 клиентов.
---
## 9. Как изменить строку подключения (Connection String)
Строка подключения **не зашита в код**. Она лежит в файле
`AdoNetShop/appsettings.json`:
```json
{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=AdoNetShop;Trusted_Connection=True;TrustServerCertificate=True;"
}
}
```
Читает её класс `DbConnectionFactory` (`AdoNetShop/Data/DbConnectionFactory.cs`):
```csharp
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`.
В строке подключения именованный экземпляр пишется через обратный слэш `\`:
```json
"DefaultConnection": "Server=.\\SQLEXPRESS;Database=AdoNetShop;Trusted_Connection=True;TrustServerCertificate=True;"
```
> **Про обратный слэш и JSON.** В JSON символ `\` — начало escape-последовательности,
> поэтому обратный слэш записывается **двойным**: `\\`.
> В строке подключения это означает один-единственный обратный слэш.
Варианты, которые тоже работают:
```
Server=localhost\SQLEXPRESS — если хотите быть явными
Server=(local)\SQLEXPRESS — эквивалент
Server=.\MSSQLSERVER — именованный экземпляр основного сервера
```
### Если нужна аутентификация SQL Server (логин и пароль)
```json
"DefaultConnection": "Server=localhost;Database=AdoNetShop;User Id=sa;Password=ТвойПароль;TrustServerCertificate=True;"
```
> Внимание: пароль в `appsettings.json` — это «учебный» вариант. В настоящем проекте
> такие данные хранят в переменных окружения или в защищённом хранилище секретов.
> Для лабораторной работы достаточно `appsettings.json` — главное, что пароль
> **не зашит в исходный код**, а вынесен в конфигурацию.
### Как проверить, что подключение работает
```bash
# В SSMS: Server → Connect → Database Engine, имя сервера как в appsettings.json
# Либо в командной строке проекта (Visual Studio / Rider → Terminal):
cd AdoNetShop
dotnet run
```
Если в меню выбрать пункт `1. Список товаров` и вы увидите таблицу с товарами —
подключение работает корректно.
---
## 10. Как запустить проект
### Что нужно
* [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) — проверьте командой `dotnet --version`
* Запущенный SQL Server (см. раздел 7)
* Выполненные скрипты `01`, `02`, `03`, `05` (см. раздел 8)
* Правильная строка подключения в `appsettings.json` (см. раздел 9)
### Запуск из командной строки
```bash
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
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. Добавьте товар `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. Обработка ошибок и валидация
### Проверка пользовательского ввода (в `Program.cs` и `ShopService.cs`)
| Что проверяем | Где |
|---|---|
| ID — целое число | `int.TryParse(...)` перед вызовом репозитория |
| Цена больше нуля | `ShopService.CreateProductAsync`, `Product.Price` |
| Количество на складе не отрицательное | `ShopService.CreateProductAsync`, `StockQuantity` |
| Email не пустой | при добавлении клиента |
| Количество товара в заказе больше нуля | при оформлении заказа |
| Товар существует и его хватает | `ShopService.CreateOrderAsync` до открытия транзакции |
Пример:
```csharp
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`:
```csharp
try
{
switch (input) { /* ... */ }
}
catch (Exception ex)
{
Console.WriteLine($"[ОШИБКА]: {ex.Message}");
if (ex.InnerException != null)
Console.WriteLine($"[ДЕТАЛИ]: {ex.InnerException.Message}");
}
```
Такой подход выбран сознательно: **для учебного проекта исключения не скрываются
молча** — пользователь видит, что произошло. В продакшене ошибки логировали бы
в файл, а пользователю показывали бы нейтральное сообщение.
Дополнительно ошибки от самой СУБД обрабатываются отдельно. `SqlException` —
это исключение SQL Server, в нём есть номер ошибки и текст от сервера:
```csharp
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 используется как основной и единственный способ работы с базой данных.*
??????? ???????????? ?????? ?? ?????????? ADO.NET (C# + Microsoft SQL Server, ??? ORM)

View File

@ -1,11 +0,0 @@
-- 01_create_database.sql
-- Скрипт для создания базы данных
USE master;
GO
IF NOT EXISTS (SELECT name FROM sys.databases WHERE name = N'AdoNetShop')
BEGIN
CREATE DATABASE [AdoNetShop];
END
GO

View File

@ -1,85 +0,0 @@
-- 02_create_tables.sql
-- Скрипт для создания таблиц
USE [AdoNetShop];
GO
-- Таблица Categories
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'Categories')
BEGIN
CREATE TABLE Categories (
Id INT IDENTITY(1,1) PRIMARY KEY,
Name NVARCHAR(100) NOT NULL
);
END
GO
-- Таблица Products
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'Products')
BEGIN
CREATE TABLE Products (
Id INT IDENTITY(1,1) PRIMARY KEY,
Name NVARCHAR(200) NOT NULL,
Description NVARCHAR(MAX) NULL,
Price DECIMAL(18, 2) NOT NULL CHECK (Price > 0),
StockQuantity INT NOT NULL DEFAULT 0 CHECK (StockQuantity >= 0),
CategoryId INT NOT NULL,
FOREIGN KEY (CategoryId) REFERENCES Categories(Id)
);
END
GO
-- Таблица Customers
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'Customers')
BEGIN
CREATE TABLE Customers (
Id INT IDENTITY(1,1) PRIMARY KEY,
Name NVARCHAR(100) NOT NULL,
Email NVARCHAR(100) NOT NULL UNIQUE,
Phone NVARCHAR(20) NULL
);
END
GO
-- Таблица Orders
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'Orders')
BEGIN
CREATE TABLE Orders (
Id INT IDENTITY(1,1) 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 DEFAULT 0 CHECK (TotalAmount >= 0),
FOREIGN KEY (CustomerId) REFERENCES Customers(Id)
);
END
GO
-- Таблица OrderItems
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'OrderItems')
BEGIN
CREATE TABLE OrderItems (
Id INT IDENTITY(1,1) PRIMARY KEY,
OrderId INT NOT NULL,
ProductId INT NOT NULL,
Quantity INT NOT NULL CHECK (Quantity > 0),
Price DECIMAL(18, 2) NOT NULL CHECK (Price > 0),
FOREIGN KEY (OrderId) REFERENCES Orders(Id) ON DELETE CASCADE,
FOREIGN KEY (ProductId) REFERENCES Products(Id)
);
END
GO
-- Индексы для внешних ключей (улучшают производительность JOIN'ов)
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_Products_CategoryId')
CREATE INDEX IX_Products_CategoryId ON Products(CategoryId);
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_Orders_CustomerId')
CREATE INDEX IX_Orders_CustomerId ON Orders(CustomerId);
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_OrderItems_OrderId')
CREATE INDEX IX_OrderItems_OrderId ON OrderItems(OrderId);
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_OrderItems_ProductId')
CREATE INDEX IX_OrderItems_ProductId ON OrderItems(ProductId);
GO

View File

@ -1,42 +0,0 @@
-- 03_insert_data.sql
-- Скрипт для вставки тестовых данных
USE [AdoNetShop];
GO
-- Очистка таблиц перед вставкой (опционально, для чистоты эксперимента)
-- Отключаем проверку внешних ключей
EXEC sp_MSForEachTable 'ALTER TABLE ? NOCHECK CONSTRAINT ALL'
EXEC sp_MSForEachTable 'DELETE FROM ?'
EXEC sp_MSForEachTable 'ALTER TABLE ? WITH CHECK CHECK CONSTRAINT ALL'
-- Сброс identity
DBCC CHECKIDENT ('Categories', RESEED, 0);
DBCC CHECKIDENT ('Products', RESEED, 0);
DBCC CHECKIDENT ('Customers', RESEED, 0);
DBCC CHECKIDENT ('Orders', RESEED, 0);
DBCC CHECKIDENT ('OrderItems', RESEED, 0);
GO
-- Добавляем категории
INSERT INTO Categories (Name) VALUES
('Laptops'),
('Smartphones'),
('Accessories');
-- Добавляем товары
INSERT INTO Products (Name, Description, Price, StockQuantity, CategoryId) VALUES
('Laptop Dell XPS 15', 'High performance laptop', 1500.00, 10, 1),
('MacBook Pro 14', 'Apple laptop with M2 chip', 1999.99, 5, 1),
('iPhone 14 Pro', 'Latest Apple smartphone', 999.00, 20, 2),
('Samsung Galaxy S23', 'Flagship Android phone', 899.50, 15, 2),
('Wireless Mouse', 'Ergonomic wireless mouse', 25.00, 50, 3),
('USB-C Hub', 'Multi-port adapter', 45.00, 30, 3);
-- Добавляем клиентов
INSERT INTO Customers (Name, Email, Phone) VALUES
('Ivan Ivanov', 'ivan@example.com', '+1234567890'),
('Petr Petrov', 'petr@example.com', '+0987654321');
-- Заказы добавлять не будем, создадим их из кода, чтобы протестировать транзакции
GO

View File

@ -1,28 +0,0 @@
-- 04_queries.sql
-- Примеры запросов
USE [AdoNetShop];
GO
-- Получить все товары с названиями категорий
SELECT
p.Id,
p.Name,
p.Price,
p.StockQuantity,
c.Name AS CategoryName
FROM Products p
JOIN Categories c ON p.CategoryId = c.Id;
-- Получить все заказы клиента с деталями
SELECT
o.Id AS OrderId,
o.OrderDate,
o.TotalAmount,
p.Name AS ProductName,
oi.Quantity,
oi.Price
FROM Orders o
JOIN OrderItems oi ON o.Id = oi.OrderId
JOIN Products p ON oi.ProductId = p.Id
WHERE o.CustomerId = 1;

View File

@ -1,73 +0,0 @@
-- 05_procedures.sql
-- Хранимые процедуры (Stored Procedures).
-- ВАЖНО: основная бизнес-логика остаётся в C# (ADO.NET).
-- Здесь только простые процедуры, чтобы показать, как ADO.NET
-- умеет вызывать хранимую процедуру через CommandType.StoredProcedure.
USE [AdoNetShop];
GO
-- 1) Получить все товары (или только товары одной категории).
-- Если @CategoryId = NULL, возвращаются все товары.
IF OBJECT_ID('dbo.GetProducts', 'P') IS NOT NULL
DROP PROCEDURE dbo.GetProducts;
GO
CREATE PROCEDURE dbo.GetProducts
@CategoryId INT = NULL
AS
BEGIN
SET NOCOUNT ON;
SELECT Id, Name, Description, Price, StockQuantity, CategoryId
FROM Products
WHERE @CategoryId IS NULL OR CategoryId = @CategoryId
ORDER BY Name;
END
GO
-- 2) Получить один товар по идентификатору.
IF OBJECT_ID('dbo.GetProductById', 'P') IS NOT NULL
DROP PROCEDURE dbo.GetProductById;
GO
CREATE PROCEDURE dbo.GetProductById
@ProductId INT
AS
BEGIN
SET NOCOUNT ON;
SELECT Id, Name, Description, Price, StockQuantity, CategoryId
FROM Products
WHERE Id = @ProductId;
END
GO
-- 3) Изменить цену товара с проверкой входных данных.
-- Простой пример того, как часть проверки можно вынести на сервер БД.
IF OBJECT_ID('dbo.UpdateProductPrice', 'P') IS NOT NULL
DROP PROCEDURE dbo.UpdateProductPrice;
GO
CREATE PROCEDURE dbo.UpdateProductPrice
@ProductId INT,
@NewPrice DECIMAL(18, 2)
AS
BEGIN
SET NOCOUNT ON;
IF @NewPrice <= 0
BEGIN
-- RAISERROR: сами мы формируем сообщение об ошибке и отдаём его клиенту
RAISERROR ('Цена товара должна быть больше нуля.', 16, 1);
RETURN;
END
UPDATE Products
SET Price = @NewPrice
WHERE Id = @ProductId;
IF @@ROWCOUNT = 0
RAISERROR ('Товар с таким Id не найден.', 16, 1);
END
GO

View File

@ -1,74 +0,0 @@
-- 06_test.sql
-- Проверочные запросы. Выполняйте вручную после 01-05 скриптов.
USE [AdoNetShop];
GO
-- ============ Таблицы ============
SELECT * FROM Categories;
SELECT * FROM Products;
SELECT * FROM Customers;
SELECT * FROM Orders;
SELECT * FROM OrderItems;
-- ============ Примеры SELECT-запросов из приложения ============
-- 1. Все товары (демонстрация SqlDataReader)
SELECT Id, Name, Description, Price, StockQuantity, CategoryId
FROM Products;
GO
-- 2. Один товар по Id (демонстрация параметризованного SqlCommand)
DECLARE @Id INT = 1;
SELECT Id, Name, Description, Price, StockQuantity, CategoryId
FROM Products
WHERE Id = @Id;
GO
-- 3. Товары с названием категории (JOIN)
SELECT p.Id, p.Name, p.Price, p.StockQuantity, c.Name AS CategoryName
FROM Products p
JOIN Categories c ON p.CategoryId = c.Id
ORDER BY p.Name;
GO
-- ============ Проверка хранимых процедур ============
-- Все товары
EXEC dbo.GetProducts;
GO
-- Товары только 1-й категории
EXEC dbo.GetProducts @CategoryId = 1;
GO
-- Один товар
EXEC dbo.GetProductById @ProductId = 1;
GO
-- Успешное изменение цены
EXEC dbo.UpdateProductPrice @ProductId = 1, @NewPrice = 1450.00;
SELECT Id, Name, Price FROM Products WHERE Id = 1;
GO
-- Ожидаемая ошибка: цена <= 0 (процедура сама вызовет RAISERROR)
EXEC dbo.UpdateProductPrice @ProductId = 1, @NewPrice = -10.00;
GO
-- ============ Проверка ограничений (CHECK) ============
-- Ожидаемая ошибка: Price > 0
INSERT INTO Products (Name, Price, StockQuantity, CategoryId)
VALUES ('Тестовый товар', -5, 1, 1);
GO
-- Ожидаемая ошибка: StockQuantity >= 0
UPDATE Products SET StockQuantity = -1 WHERE Id = 1;
GO
-- ============ Проверка целостности связей (FOREIGN KEY) ============
-- Ожидаемая ошибка: категории 999 не существует
INSERT INTO Products (Name, Price, StockQuantity, CategoryId)
VALUES ('Товар без категории', 100, 1, 999);
GO