В моем проекте C # у меня был запрос с .ToList(); в конце:

public List<Product> ListAllProducts()
{
    List<Product> products = db.Products
        .Where(...)
        .Select(p => new Product()
        {
            ProductId = p.ProductId,
            ...
        })
        .ToList();

    return products;
}

Время от времени я получал ошибку EntityException: "The underlying provider failed on Open.". Я сделал следующее обходное решение на основе ответов, которые я нашел здесь, на SO

public List<Product> ListAllProducts()
{
    try
    {
        // When accessing a lot of Database Rows the following error might occur:
        // EntityException: "The underlying provider failed on Open." With an inner TimeoutExpiredException.
        // So, we use a new db so it's less likely to occur
        using(MyDbContext usingDb = new MyDbContext())
        {
            // and also set the CommandTimeout to 2 min, instead of the default 30 sec, just in case
            ((IObjectContextAdapter)usingDb).ObjectContext.CommandTimeout = 120;

            List<Product> products = usingDb.Products
                .Where(...)
                .Select(p => new Product()
                {
                    ProductId = p.ProductId,
                    ...
                })
                .ToList();

            return products;
        }
    }
    // and also have a try-catch in case the error stil occurres,
    // since we don't want the entire website to crash
    catch (EntityException ex)
    {
        Console.WriteLine("An error has occured: " + ex.StackTrace);
        return new List<Product>();
    }
}

Пока что это работает, и я получаю свои данные каждый раз (и на всякий случай я также добавил try-catch).

Сейчас я занимаюсь UnitTesting, и у меня возникла проблема. У меня есть MockDbContext в моем модуле UnitTest, но поскольку я использую using(...), в котором используется немодельная версия, мои модульные тесты не работают. Как я могу проверить это, используя MockDbContext вместо контекста по умолчанию?


РЕДАКТИРОВАТЬ:

Я только что наткнулся на этот пост о модуле UnitTesting Mock DbContext . Так что мне кажется, что мне действительно нужно полностью передать это моему методу или просто не использовать using(MyDbContext usingDb = new MyDbContext()), а вместо этого только временно увеличить CommandTimeout и использовать try-catch. Неужели другого выхода нет? Или есть лучший способ исправить / обойти ошибку, которую я получаю, без using(MyDbContext usingDb = new MyDbContext()) целиком?


РЕДАКТИРОВАТЬ 2:

Чтобы прояснить мою ситуацию:

У меня есть класс MyDbContext и интерфейс IMyDbContext.

В каждом из моих контроллеров есть следующее:

public class ProductController : Controller
{
    private IMyDbContext _db;

    public ProductController(IMyDbContext db)
    {
        this._db = db;
    }

    ... // Methods that use this "_db"

    public List<Product> ListAllProducts()
    {
        try
        {
            using(MyDbContext usingDb = new MyDbContext())
            {
                ... // Query that uses "usingDb"
            }
        }
        catch(...)
        {
            ...
        }
    }
}

В моем обычном коде я создаю такой экземпляр:

ProductController controller = new ProductController(new MyDbContext());

В моем коде UnitTest я создаю такой экземпляр:

ProductController controller = new ProductController(this.GetTestDb());

// With GetTestDb something like this:
private IMyDbContext GetTestDb()
{
    var memoryProducts = new Product { ... };
    ...

    var mockDB = new Mock<FakeMyDbContext>();
    mockDb.Setup(m => m.Products).Returns(memoryProducts);
    ...
    return mockDB.Object;
}

Все работает отлично, как в моем обычном проекте, так и в моем UnitTest, за исключением этой части using(MyDbContext usingDb = new MyDbContext()) в ListAll-методах.

2
Kevin Cruijssen 31 Июл 2014 в 18:13

2 ответа

Лучший ответ

Хотя для облегчения тестирования желательно разложить код на множители, наличие ветвей кода, которые выполняются только в рамках теста, не помогает. На самом деле это помещает код конфигурации тестовой среды в ваш живой код.

Что вам нужно, так это еще одна абстракция для создания DbContext. Вы должны передать делегата, который создает DbContext:

public List<Product> ListAllProducts(Func<DbContext> dbFunc)
{
    using(MyDbContext usingDb = dbFunc())
    {
        // ...
    }
}

И теперь, когда вы больше не передаете тяжелый объект DbContext, вы можете повторно преобразовать параметр метода в конструктор класса.

public class ProductDAL
{
    private Func<DbContext> _dbFunc;

    public ProductDAL(Func<DbContext> dbFunc)
    {
        _dbFunc = dbFunc;
    }

    public List<Product> ListAllProducts()
    {
        using(MyDbContext usingDb = _dbFunc())
        {
            // ...
        }
    }
}

И изюминка в том, что теперь вы можете контролировать, какой DbContext использовать, включая строку подключения, извлеченную из файла конфигурации, в вашем контейнере IOC и никогда не беспокоиться об этом где-либо еще в коде.

2
Keith Payne 31 Июл 2014 в 18:35
Использование делегата похоже на использование интерфейса, который у меня есть прямо сейчас. У меня есть MyDbContext-класс и IMyDbContext-интерфейс, и в моем ProductController я использую: private IMyDbContext db; public ProductController(IMyDbContext _db){ this.db = _db; } Должен ли я вместо этого изменить это на делегат? Похоже, он может решить мою проблему, но я хочу убедиться, прежде чем менять его ВЕЗДЕ в моем коде.
 – 
Kevin Cruijssen
31 Июл 2014 в 18:47
Да, вы должны изменить его на делегата. Причина в том, что вы хотите, чтобы ваши DbContext были недолговечными.
 – 
Keith Payne
31 Июл 2014 в 19:16
Хорошо, спасибо, тогда я сделаю это завтра для всех контроллеров. Я приму ваш ответ, как только все заработает при использовании делегата. Выглядит очень многообещающе.
 – 
Kevin Cruijssen
31 Июл 2014 в 19:19
1
Еще одно замечание: если вы измените его на Func, вам придется либо наследовать IDisposable в IMyDbContext, либо вам придется привести возвращаемое значение Func к (IDisposable) в операторе использования. Оба они менее чем оптимальны, и лучшей альтернативой становится инкапсуляция делегата в класс типа построителя, который сам является одноразовым и удаляет DbContext по мере его удаления.
 – 
Keith Payne
31 Июл 2014 в 19:19

По моему опыту, трудно получить надлежащее модульное тестирование без структурирования кода, чтобы он был удобен для модульного тестирования, но вы можете попробовать сделать класс универсальным, чтобы его можно было создать в любом контексте.

public class SomeClass<T> where T: MyDbContext, new()
{
    public List<Product> ListAllProducts()
    {
        using(MyDbContext usingDb = new T())
        {
            ((IObjectContextAdapter)usingDb).ObjectContext.CommandTimeout = 120;
            List<Product> products = usingDb.Products.ToList();
            return products;
        }
    }
}


// real-code:
var foo = new SomeClass<MyDbContext>();

// test
var bar = new SomeClass<MockContext>();
0
Kenned 31 Июл 2014 в 18:37