Введение
В современной разработке на C# выбор инструментов для работы с данными — это стратегическое решение, влияющее на производительность, масштабируемость и поддерживаемость приложения. Между легковесным микро-ORM Dapper, полнофункциональным Entity Framework Core 9 и разнообразными NoSQL решениями (Redis для кэширования, MongoDB для документ-ориентированного хранения) существует четкое разделение обязанностей. Каждый инструмент решает свои задачи: Dapper предлагает максимальную скорость и контроль над SQL, EF Core обеспечивает богатую функциональность и абстракцию, а NoSQL базы данных решают проблемы специфичных сценариев, где реляционная модель неэффективна.
Dapper: микро-ORM для максимальной производительности
1.1. Основы и философия Dapper
1.1.1. Установка и базовое использование
// Установка: dotnet add package Dapper using Dapper; using System.Data; using Microsoft.Data.SqlClient; public class UserRepository { private readonly IDbConnection _connection; public UserRepository(string connectionString) { _connection = new SqlConnection(connectionString); } // Базовый запрос с параметрами public async Task<User> GetUserAsync(int id) { const string sql = "SELECT * FROM Users WHERE Id = @Id"; return await _connection.QueryFirstOrDefaultAsync<User>( sql, new { Id = id } // Анонимный объект для параметров ); } // Запрос с несколькими результатами public async Task<IEnumerable<User>> GetUsersByRoleAsync(string role) { const string sql = @" SELECT u.*, r.Name as RoleName FROM Users u INNER JOIN Roles r ON u.RoleId = r.Id WHERE r.Name = @Role"; return await _connection.QueryAsync<User, Role, User>( sql, (user, role) => { user.Role = role; return user; }, new { Role = role }, splitOn: "RoleName" // Разделитель для маппинга ); } } public class User { public int Id { get; set; } public string Name { get; set; } public string Email { get; set; } public Role Role { get; set; } } public class Role { public int Id { get; set; } public string Name { get; set; } }
1.2. Продвинутые возможности Dapper
1.2.1. Пакетные операции и транзакции
public class OrderRepository { private readonly IDbConnection _connection; public async Task<int> CreateOrderWithTransactionAsync(Order order, List<OrderItem> items) { // Использование транзакций using var transaction = _connection.BeginTransaction(); try { // Вставка заказа const string orderSql = @" INSERT INTO Orders (CustomerId, OrderDate, TotalAmount) OUTPUT INSERTED.Id VALUES (@CustomerId, @OrderDate, @TotalAmount)"; int orderId = await _connection.ExecuteScalarAsync<int>( orderSql, order, transaction ); // Пакетная вставка товаров const string itemsSql = @" INSERT INTO OrderItems (OrderId, ProductId, Quantity, Price) VALUES (@OrderId, @ProductId, @Quantity, @Price)"; // Подготовка данных для пакетной вставки var itemsData = items.Select(item => new { OrderId = orderId, item.ProductId, item.Quantity, item.Price }); await _connection.ExecuteAsync( itemsSql, itemsData, transaction ); transaction.Commit(); return orderId; } catch { transaction.Rollback(); throw; } } // Множественные результаты в одном запросе public async Task<(Order Order, List<OrderItem> Items)> GetOrderWithItemsAsync(int orderId) { const string sql = @" SELECT * FROM Orders WHERE Id = @OrderId; SELECT * FROM OrderItems WHERE OrderId = @OrderId; "; using var multi = await _connection.QueryMultipleAsync( sql, new { OrderId = orderId } ); var order = await multi.ReadFirstOrDefaultAsync<Order>(); var items = (await multi.ReadAsync<OrderItem>()).ToList(); return (order, items); } }
1.2.2. Кастомный маппинг и производительность
// Регистрация кастомных мапперов public class CustomTypeHandler : SqlMapper.TypeHandler<DateTime> { public override void SetValue(IDbDataParameter parameter, DateTime value) { parameter.Value = value.ToUniversalTime(); } public override DateTime Parse(object value) { return DateTime.SpecifyKind((DateTime)value, DateTimeKind.Utc); } } // Инициализация при запуске приложения SqlMapper.AddTypeHandler(new CustomTypeHandler()); // Использование Dapper.Contrib для простых CRUD операций using Dapper.Contrib.Extensions; [Table("Products")] public class Product { [Key] public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } [Computed] public decimal PriceWithTax => Price * 1.2m; } public class ProductRepository { public async Task<Product> GetAsync(int id) { return await _connection.GetAsync<Product>(id); } public async Task<int> InsertAsync(Product product) { return await _connection.InsertAsync(product); } public async Task<bool> UpdateAsync(Product product) { return await _connection.UpdateAsync(product); } } // Бенчмарк производительности // Dapper: ~100,000 записей/сек // EF Core: ~50,000 записей/сек (без AsNoTracking)
Entity Framework Core 9: современный ORM с богатой функциональностью
2.1. Новые возможности EF Core 9
2.1.1. Улучшенная производительность и новые операторы
// Установка: dotnet add package Microsoft.EntityFrameworkCore.SqlServer using Microsoft.EntityFrameworkCore; public class ApplicationDbContext : DbContext { public DbSet<Product> Products { get; set; } public DbSet<Category> Categories { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer( "Server=(localdb)\\mssqllocaldb;Database=ShopDB;Trusted_Connection=True", options => options.EnableRetryOnFailure(5, TimeSpan.FromSeconds(10), null) ); // Включение чувствительных данных в логи (только для разработки) optionsBuilder.EnableSensitiveDataLogging(); } protected override void OnModelCreating(ModelBuilder modelBuilder) { // Конфигурация через Fluent API modelBuilder.Entity<Product>() .HasKey(p => p.Id); modelBuilder.Entity<Product>() .HasIndex(p => p.Name) .IsUnique(); modelBuilder.Entity<Product>() .Property(p => p.Price) .HasPrecision(18, 2); // Комплексные индексы modelBuilder.Entity<Product>() .HasIndex(p => new { p.CategoryId, p.CreatedDate }) .IsDescending(false, true); // Триггеры для аудита modelBuilder.Entity<Product>() .ToTable(tb => tb.HasTrigger("ProductAuditTrigger")); } } // Использование новых операторов LINQ в EF Core 9 public async Task<List<Product>> GetFilteredProductsAsync(string search, decimal? minPrice) { var query = _context.Products.AsNoTracking(); // Новый оператор: Where с несколькими предикатами if (!string.IsNullOrEmpty(search)) query = query.Where(p => EF.Functions.Like(p.Name, $"%{search}%")); if (minPrice.HasValue) query = query.Where(p => p.Price >= minPrice.Value); // ExecuteUpdate для массового обновления (без SELECT) await query.ExecuteUpdateAsync(p => p.SetProperty(x => x.LastSearched, DateTime.UtcNow)); // ExecuteDelete для массового удаления await query.Where(p => p.IsDeleted) .ExecuteDeleteAsync(); // Новые агрегатные функции var stats = await query .GroupBy(p => p.CategoryId) .Select(g => new { CategoryId = g.Key, Count = g.Count(), AveragePrice = g.Average(p => p.Price), TotalStock = g.Sum(p => p.StockQuantity), // Новое в EF Core 9: статистические функции PriceStdDev = EF.Functions.StandardDeviation(g.Select(p => p.Price)) }) .ToListAsync(); return await query.ToListAsync(); }
2.2. Комплексные сценарии и производительность
2.2.1. Наследование и полиморфизм в EF Core
// TPT (Table Per Type) стратегия наследования [Table("People")] public abstract class Person { public int Id { get; set; } public string Name { get; set; } public string Email { get; set; } } [Table("Employees")] public class Employee : Person { public string EmployeeCode { get; set; } public decimal Salary { get; set; } public DateTime HireDate { get; set; } } [Table("Customers")] public class Customer : Person { public string CustomerNumber { get; set; } public decimal CreditLimit { get; set; } public DateTime RegistrationDate { get; set; } } // Конфигурация наследования protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Person>() .UseTptMappingStrategy(); modelBuilder.Entity<Employee>(); modelBuilder.Entity<Customer>(); } // Полиморфные запросы public async Task<List<Person>> GetAllPeopleAsync() { return await _context.People .Include(p => p.Addresses) .ToListAsync(); } // Дискриминатор для TPH (Table Per Hierarchy) modelBuilder.Entity<Person>() .HasDiscriminator<string>("PersonType") .HasValue<Employee>("Employee") .HasValue<Customer>("Customer");
2.2.2. Оптимизация производительности и мониторинг
public class OptimizedProductService { private readonly ApplicationDbContext _context; private readonly ILogger<OptimizedProductService> _logger; public async Task<ProductDto> GetProductWithOptimizationsAsync(int id) { // 1. Использование AsNoTracking для read-only операций var product = await _context.Products .AsNoTracking() .Include(p => p.Category) .Include(p => p.Supplier) .FirstOrDefaultAsync(p => p.Id == id); // 2. Split Queries для избежания Cartesian Explosion var orderDetails = await _context.Orders .Where(o => o.ProductId == id) .AsSplitQuery() .Include(o => o.Customer) .Include(o => o.ShippingInfo) .Take(100) .ToListAsync(); // 3. Compiled Queries для часто выполняемых запросов private static readonly Func<ApplicationDbContext, int, Task<Product>> GetProductByIdQuery = EF.CompileAsyncQuery( (ApplicationDbContext context, int productId) => context.Products .AsNoTracking() .FirstOrDefault(p => p.Id == productId)); var compiledResult = await GetProductByIdQuery(_context, id); // 4. Мониторинг производительности _context.Database.SetCommandTimeout(30); // Таймаут 30 секунд var connection = _context.Database.GetDbConnection(); _logger.LogInformation("Connection state: {State}", connection.State); // 5. Использование Bulk Operations для массовых вставок using var transaction = await _context.Database.BeginTransactionAsync(); try { await _context.BulkInsertAsync(products); // Используя библиотеку EFCore.BulkExtensions await transaction.CommitAsync(); } catch { await transaction.RollbackAsync(); throw; } return MapToDto(product); } }
2.3. Материализованные представления и кэширование
// Определение материализованного представления [Keyless] public class ProductSalesSummary { public int ProductId { get; set; } public string ProductName { get; set; } public int TotalSold { get; set; } public decimal TotalRevenue { get; set; } public DateTime LastSaleDate { get; set; } } // Использование в контексте public DbSet<ProductSalesSummary> ProductSalesSummaries { get; set; } // Запрос к материализованному представлению public async Task<List<ProductSalesSummary>> GetTopSellingProductsAsync(int topCount) { return await _context.ProductSalesSummaries .OrderByDescending(p => p.TotalRevenue) .Take(topCount) .ToListAsync(); } // Кэширование второго уровня с Redis services.AddDbContext<ApplicationDbContext>(options => { options.UseSqlServer(connectionString); options.UseLazyLoadingProxies(); options.UseSecondLevelCache(); // Используя библиотеку EFCoreSecondLevelCache }); // Конфигурация кэша services.AddEFSecondLevelCache(options => { options.UseMemoryCacheProvider() .CacheQueriesContainingTypes( CacheExpirationMode.Absolute, TimeSpan.FromMinutes(10), typeof(Product), typeof(Category)); options.UseRedisCacheProvider( Configuration.GetConnectionString("Redis"), "EFCache_"); // Префикс для ключей });
Redis: высокопроизводительный кэш и хранилище ключ-значение
3.1. Основы работы с Redis в C#
3.1.1. Установка и базовые операции
// Установка: dotnet add package StackExchange.Redis using StackExchange.Redis; public class RedisService : IAsyncDisposable { private readonly IConnectionMultiplexer _redis; private readonly IDatabase _database; private readonly IServer _server; public RedisService(string connectionString) { // Подключение к Redis _redis = ConnectionMultiplexer.Connect( $"{connectionString},abortConnect=false,syncTimeout=5000"); _database = _redis.GetDatabase(); _server = _redis.GetServer(_redis.GetEndPoints().First()); } // Базовые операции public async Task<string> GetStringAsync(string key) { return await _database.StringGetAsync(key); } public async Task SetStringAsync(string key, string value, TimeSpan? expiry = null) { await _database.StringSetAsync(key, value, expiry); } public async Task<bool> DeleteAsync(string key) { return await _database.KeyDeleteAsync(key); } // Операции с хэшами public async Task SetHashAsync(string key, HashEntry[] entries) { await _database.HashSetAsync(key, entries); } public async Task<HashEntry[]> GetHashAsync(string key) { return await _database.HashGetAllAsync(key); } public async Task<RedisValue> GetHashFieldAsync(string key, string field) { return await _database.HashGetAsync(key, field); } public async ValueTask DisposeAsync() { await _redis.CloseAsync(); _redis.Dispose(); } }
3.2. Продвинутые сценарии использования Redis
3.2.1. Кэширование и инвалидация
public class DistributedCacheService { private readonly IDatabase _redis; private readonly JsonSerializerSettings _serializerSettings; public DistributedCacheService(IConnectionMultiplexer redis) { _redis = redis.GetDatabase(); _serializerSettings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Auto }; } // Кэширование с зависимостями public async Task<T> GetOrSetAsync<T>( string key, Func<Task<T>> factory, TimeSpan expiry, params string[] dependencyKeys) { // Проверка кэша var cached = await _redis.StringGetAsync(key); if (!cached.IsNull) { return JsonConvert.DeserializeObject<T>(cached, _serializerSettings); } // Проверка зависимостей bool dependenciesValid = true; foreach (var depKey in dependencyKeys) { if (!await _redis.KeyExistsAsync($"{depKey}_version")) { dependenciesValid = false; break; } } if (!dependenciesValid) { // Инвалидация кэша await _redis.KeyDeleteAsync(key); } else { // Получение данных из фабрики var data = await factory(); // Кэширование var serialized = JsonConvert.SerializeObject(data, _serializerSettings); await _redis.StringSetAsync(key, serialized, expiry); // Установка зависимостей foreach (var depKey in dependencyKeys) { await _redis.StringSetAsync( $"{key}_dep_{depKey}", await _redis.StringGetAsync($"{depKey}_version")); } return data; } return await factory(); } // Инвалидация кэша по паттерну public async Task InvalidatePatternAsync(string pattern) { var keys = _redis.Multiplexer.GetServer(_redis.Multiplexer.GetEndPoints().First()) .Keys(pattern: $"*{pattern}*") .ToArray(); if (keys.Any()) { await _redis.KeyDeleteAsync(keys.Select(k => (RedisKey)k).ToArray()); } } // Pub/Sub для инвалидации в кластере public async Task PublishInvalidationAsync(string channel, string key) { var subscriber = _redis.Multiplexer.GetSubscriber(); await subscriber.PublishAsync(channel, key); } public async Task SubscribeToInvalidationAsync(string channel, Action<string> handler) { var subscriber = _redis.Multiplexer.GetSubscriber(); await subscriber.SubscribeAsync(channel, (_, key) => handler(key)); } }
3.2.2. Redis как основное хранилище
public class RedisSessionStore { private readonly IDatabase _redis; public async Task<Session> CreateSessionAsync(int userId, Dictionary<string, string> data) { var sessionId = Guid.NewGuid().ToString(); var sessionKey = $"session:{sessionId}"; var userSessionsKey = $"user_sessions:{userId}"; // Сохранение данных сессии var entries = data.Select(kv => new HashEntry(kv.Key, kv.Value)).ToArray(); await _redis.HashSetAsync(sessionKey, entries); // Установка TTL await _redis.KeyExpireAsync(sessionKey, TimeSpan.FromHours(2)); // Добавление в список сессий пользователя await _redis.SetAddAsync(userSessionsKey, sessionId); await _redis.KeyExpireAsync(userSessionsKey, TimeSpan.FromHours(2)); return new Session(sessionId, DateTime.UtcNow.AddHours(2)); } public async Task InvalidateUserSessionsAsync(int userId) { var userSessionsKey = $"user_sessions:{userId}"; var sessionIds = await _redis.SetMembersAsync(userSessionsKey); var tasks = new List<Task>(); foreach (var sessionId in sessionIds) { tasks.Add(_redis.KeyDeleteAsync($"session:{sessionId}")); } tasks.Add(_redis.KeyDeleteAsync(userSessionsKey)); await Task.WhenAll(tasks); } } // Использование Redis Streams для событий public class RedisEventStore { private readonly IDatabase _redis; public async Task PublishEventAsync(string stream, string eventType, object data) { var entries = new NameValueEntry[] { new("type", eventType), new("data", JsonConvert.SerializeObject(data)), new("timestamp", DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString()), new("source", Environment.MachineName) }; await _redis.StreamAddAsync(stream, entries, maxLength: 10000); } public async Task<IEnumerable<Event>> ReadEventsAsync( string stream, string lastId = "0-0", int count = 100) { var entries = await _redis.StreamReadAsync(stream, lastId, count); return entries.Select(e => new Event { Id = e.Id, Type = e.Values.FirstOrDefault(v => v.Name == "type").Value, Data = JsonConvert.DeserializeObject<dynamic>( e.Values.FirstOrDefault(v => v.Name == "data").Value), Timestamp = DateTimeOffset.FromUnixTimeMilliseconds( long.Parse(e.Values.FirstOrDefault(v => v.Name == "timestamp").Value)) }); } }
MongoDB: документ-ориентированная NoSQL база данных
4.1. Основы работы с MongoDB в C#
4.1.1. Установка и CRUD операции
// Установка: dotnet add package MongoDB.Driver using MongoDB.Driver; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; [BsonIgnoreExtraElements] public class Product { [BsonId] [BsonRepresentation(BsonType.ObjectId)] public string Id { get; set; } public string Name { get; set; } [BsonElement("price")] public decimal Price { get; set; } [BsonElement("category")] public string Category { get; set; } [BsonElement("attributes")] public Dictionary<string, object> Attributes { get; set; } = new(); [BsonElement("tags")] public List<string> Tags { get; set; } = new(); [BsonElement("created_at")] public DateTime CreatedAt { get; set; } [BsonElement("updated_at")] public DateTime UpdatedAt { get; set; } } public class MongoProductRepository { private readonly IMongoCollection<Product> _products; public MongoProductRepository(string connectionString, string databaseName) { var client = new MongoClient(connectionString); var database = client.GetDatabase(databaseName); _products = database.GetCollection<Product>("products"); // Создание индексов CreateIndexes(); } private void CreateIndexes() { // Составной индекс var keys = Builders<Product>.IndexKeys .Ascending(p => p.Category) .Descending(p => p.CreatedAt); var indexOptions = new CreateIndexOptions { Name = "category_created_at", Background = true }; _products.Indexes.CreateOne( new CreateIndexModel<Product>(keys, indexOptions)); // Текстовый индекс для полнотекстового поиска var textIndexKeys = Builders<Product>.IndexKeys .Text(p => p.Name) .Text(p => p.Tags); _products.Indexes.CreateOne( new CreateIndexModel<Product>(textIndexKeys)); } // CRUD операции public async Task<Product> GetByIdAsync(string id) { var filter = Builders<Product>.Filter.Eq(p => p.Id, id); return await _products.Find(filter).FirstOrDefaultAsync(); } public async Task<List<Product>> GetByCategoryAsync(string category, int page = 1, int pageSize = 20) { var filter = Builders<Product>.Filter.Eq(p => p.Category, category); var sort = Builders<Product>.Sort.Descending(p => p.CreatedAt); return await _products.Find(filter) .Sort(sort) .Skip((page - 1) * pageSize) .Limit(pageSize) .ToListAsync(); } public async Task<Product> CreateAsync(Product product) { product.Id = ObjectId.GenerateNewId().ToString(); product.CreatedAt = DateTime.UtcNow; product.UpdatedAt = DateTime.UtcNow; await _products.InsertOneAsync(product); return product; } public async Task<bool> UpdateAsync(string id, UpdateDefinition<Product> update) { var filter = Builders<Product>.Filter.Eq(p => p.Id, id); update = update.Set(p => p.UpdatedAt, DateTime.UtcNow); var result = await _products.UpdateOneAsync(filter, update); return result.ModifiedCount > 0; } public async Task<bool> DeleteAsync(string id) { var filter = Builders<Product>.Filter.Eq(p => p.Id, id); var result = await _products.DeleteOneAsync(filter); return result.DeletedCount > 0; } }
4.2. Продвинутые запросы и агрегации
4.2.1. Агрегационные пайплайны
public class MongoAnalyticsService { private readonly IMongoCollection<Order> _orders; public async Task<SalesReport> GetSalesReportAsync(DateTime from, DateTime to) { var pipeline = new BsonDocument[] { // Stage 1: Фильтрация по дате new BsonDocument("$match", new BsonDocument { ["order_date"] = new BsonDocument { ["$gte"] = from, ["$lte"] = to } }), // Stage 2: Разворачивание товаров new BsonDocument("$unwind", "$items"), // Stage 3: Группировка по категории new BsonDocument("$group", new BsonDocument { ["_id"] = "$items.category", ["total_quantity"] = new BsonDocument("$sum", "$items.quantity"), ["total_revenue"] = new BsonDocument("$sum", new BsonDocument { ["$multiply"] = new BsonArray { "$items.price", "$items.quantity" } }), ["average_price"] = new BsonDocument("$avg", "$items.price"), ["unique_products"] = new BsonDocument("$addToSet", "$items.product_id") }), // Stage 4: Проекция new BsonDocument("$project", new BsonDocument { ["category"] = "$_id", ["total_quantity"] = 1, ["total_revenue"] = 1, ["average_price"] = 1, ["product_count"] = new BsonDocument("$size", "$unique_products"), ["_id"] = 0 }), // Stage 5: Сортировка new BsonDocument("$sort", new BsonDocument("total_revenue", -1)) }; var results = await _orders.Aggregate<BsonDocument>(pipeline).ToListAsync(); return new SalesReport { Period = $"{from:yyyy-MM-dd} to {to:yyyy-MM-dd}", Categories = results.Select(doc => new CategorySales { Category = doc["category"].AsString, TotalQuantity = doc["total_quantity"].AsInt32, TotalRevenue = doc["total_revenue"].AsDecimal, AveragePrice = doc["average_price"].AsDecimal, ProductCount = doc["product_count"].AsInt32 }).ToList() }; } // Текстовый поиск public async Task<List<Product>> SearchProductsAsync(string query) { var filter = Builders<Product>.Filter.Text(query); var sort = Builders<Product>.Sort .MetaTextScore("textScore") .Descending(p => p.CreatedAt); return await _products.Find(filter) .Sort(sort) .Limit(50) .ToListAsync(); } // Геопространственные запросы public async Task<List<Store>> FindNearbyStoresAsync(double latitude, double longitude, double radiusKm) { var point = new GeoJsonPoint<GeoJson2DCoordinates>( new GeoJson2DCoordinates(longitude, latitude)); var filter = Builders<Store>.Filter.NearSphere( s => s.Location, point, radiusKm * 1000); // Конвертация в метры return await _stores.Find(filter).Limit(20).ToListAsync(); } } // Модель для геоданных public class Store { [BsonId] public ObjectId Id { get; set; } public string Name { get; set; } [BsonElement("location")] public GeoJsonPoint<GeoJson2DCoordinates> Location { get; set; } [BsonElement("address")] public string Address { get; set; } }
4.3. Транзакции и репликация
public class MongoTransactionService { private readonly IMongoClient _client; private readonly IMongoDatabase _database; public async Task<bool> ProcessOrderTransactionAsync(Order order) { using var session = await _client.StartSessionAsync(); session.StartTransaction(); try { var orders = _database.GetCollection<Order>("orders"); var inventory = _database.GetCollection<Inventory>("inventory"); // 1. Создание заказа await orders.InsertOneAsync(session, order); // 2. Обновление инвентаря для каждого товара foreach (var item in order.Items) { var filter = Builders<Inventory>.Filter.And( Builders<Inventory>.Filter.Eq(i => i.ProductId, item.ProductId), Builders<Inventory>.Filter.Gte(i => i.Quantity, item.Quantity) ); var update = Builders<Inventory>.Update .Inc(i => i.Quantity, -item.Quantity) .Set(i => i.LastUpdated, DateTime.UtcNow); var result = await inventory.UpdateOneAsync( session, filter, update); if (result.ModifiedCount == 0) { throw new InsufficientInventoryException( $"Недостаточно товара: {item.ProductId}"); } } // 3. Запись в журнал var auditLog = new AuditLog { Action = "ORDER_CREATED", EntityId = order.Id.ToString(), Timestamp = DateTime.UtcNow, Details = new BsonDocument { ["order_total"] = order.TotalAmount, ["item_count"] = order.Items.Count } }; var auditCollection = _database.GetCollection<AuditLog>("audit_logs"); await auditCollection.InsertOneAsync(session, auditLog); // Коммит транзакции await session.CommitTransactionAsync(); return true; } catch (Exception ex) { await session.AbortTransactionAsync(); throw new OrderProcessingException("Ошибка обработки заказа", ex); } } }
5. Гибридный подход: комбинирование технологий
5.1. Оптимальная архитектура для разных сценариев
public class HybridDataService { private readonly ApplicationDbContext _dbContext; private readonly RedisService _redis; private readonly MongoProductRepository _mongoProducts; private readonly ILogger<HybridDataService> _logger; public HybridDataService( ApplicationDbContext dbContext, RedisService redis, MongoProductRepository mongoProducts) { _dbContext = dbContext; _redis = redis; _mongoProducts = mongoProducts; } // Сценарий 1: Чтение продукта с кэшированием public async Task<ProductDto> GetProductWithCacheAsync(int productId) { var cacheKey = $"product:{productId}"; // Попытка получить из кэша var cached = await _redis.GetStringAsync(cacheKey); if (!string.IsNullOrEmpty(cached)) { return JsonConvert.DeserializeObject<ProductDto>(cached); } // Получение из SQL (EF Core) var product = await _dbContext.Products .AsNoTracking() .Include(p => p.Category) .FirstOrDefaultAsync(p => p.Id == productId); if (product == null) { // Попытка получить из MongoDB var mongoProduct = await _mongoProducts.GetByIdAsync(productId.ToString()); if (mongoProduct != null) { product = MapToEntity(mongoProduct); } } if (product == null) return null; var dto = MapToDto(product); // Кэширование на 5 минут await _redis.SetStringAsync( cacheKey, JsonConvert.SerializeObject(dto), TimeSpan.FromMinutes(5)); return dto; } // Сценарий 2: Поиск с использованием подходящей БД public async Task<SearchResult> SearchProductsAsync(SearchRequest request) { if (request.UseFullTextSearch && request.Query.Length > 3) { // Использование MongoDB для полнотекстового поиска var mongoResults = await _mongoProducts.SearchProductsAsync(request.Query); return new SearchResult { Source = "MongoDB", Products = mongoResults.Select(MapToDto).ToList(), Total = mongoResults.Count }; } else { // Использование SQL для структурированного поиска var query = _dbContext.Products.AsNoTracking(); if (!string.IsNullOrEmpty(request.Category)) query = query.Where(p => p.Category.Name == request.Category); if (request.MinPrice.HasValue) query = query.Where(p => p.Price >= request.MinPrice.Value); if (request.MaxPrice.HasValue) query = query.Where(p => p.Price <= request.MaxPrice.Value); var sqlResults = await query .OrderBy(p => p.Name) .Skip((request.Page - 1) * request.PageSize) .Take(request.PageSize) .ToListAsync(); return new SearchResult { Source = "SQL Server", Products = sqlResults.Select(MapToDto).ToList(), Total = await query.CountAsync() }; } } // Сценарий 3: Аналитика с использованием агрегаций public async Task<AnalyticsReport> GetAnalyticsAsync(DateTime from, DateTime to) { // Для сложных агрегаций используем MongoDB var salesReport = await _mongoAnalytics.GetSalesReportAsync(from, to); // Для транзакционных данных используем SQL var transactionStats = await _dbContext.Orders .Where(o => o.OrderDate >= from && o.OrderDate <= to) .GroupBy(o => o.Status) .Select(g => new { Status = g.Key, Count = g.Count(), Total = g.Sum(o => o.TotalAmount) }) .ToListAsync(); // Кэшируем результат в Redis на 1 час var report = new AnalyticsReport { Sales = salesReport, Transactions = transactionStats, GeneratedAt = DateTime.UtcNow }; var cacheKey = $"analytics:{from:yyyyMMdd}:{to:yyyyMMdd}"; await _redis.SetStringAsync( cacheKey, JsonConvert.SerializeObject(report), TimeSpan.FromHours(1)); return report; } }
Заключение
Выбор инструментов для работы с данными в C# приложениях — это не вопрос «что лучше», а вопрос «что больше подходит для конкретной задачи».

