C# и базы данных: Dapper vs EF Core 9, работа с NoSQL (Redis, MongoDB)

Введение

В современной разработке на 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# приложениях — это не вопрос «что лучше», а вопрос «что больше подходит для конкретной задачи».