Внедрение зависимостей с помощью Entity Framework 5 Database First. Начиная?

Я рассматриваю создание приложения VB.NET 11 WPF MVVM с использованием Entity Framework 5 и Database First (подключение к SQL Server 2008 R2).

Я выбрал базу данных в первую очередь, так как я переношу существующее решение на WPF MVVM, где база данных, конечно, уже существует.

Я хотел бы начать использовать внедрение зависимостей, чтобы я мог проводить модульное тестирование как можно большей части своего кода.

Кажется, я не могу найти четкое и краткое описание того, как использовать внедрение зависимостей с EF DB-First и, в частности, с vb.net. Хотя я уверен, что даже пример C # будет в порядке.

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

До сих пор я создал решение и его проекты следующим образом;

  • DBAccess. В нем нет ничего, кроме моего файла .edmx и небольшого мода, позволяющего передавать ConnectionString конструктору.
  • DBControl. Здесь находятся различные классы, которые я использую для обеспечения слоя между моим EDMX и моими моделями представления. В частности, я заполняю сложные типы (которые я создал с помощью дизайнера) здесь для отображения «более дружественных» данных через пользовательский интерфейс, а также для преобразования этих «дружественных» сложных типов в сопоставленные объекты для сохранения/обновления. У меня есть один класс на таблицу в моей базе данных. Каждый с двумя методами "FetchFriendlyRecords" (один принимает фильтры) и методом "AddUpdateFriendlyRecord". Я создал интерфейс для каждого класса. Каждый класс принимает DbContext в своем конструкторе, и я просто передаю свой DBContext из проекта DBAccess.
  • MainUI — в нем размещены мои слои MVVM и ссылки на каждый класс в проекте DBControl, чтобы обеспечить привязку данных и т. д.

Я видел предположения, что вместо того, чтобы тратить время на написание сложного решения для модульного тестирования с помощью EF, проще создать твердую фиктивную базу данных с заполненными тестовыми данными и просто указать код на фиктивную базу данных, а не на живи один. Однако я бы предпочел иметь возможность создать решение в памяти, которое будет работать без какой-либо необходимости запускать SQL Server.

Любая помощь будет отличной, в том числе сказать мне, что я все делаю неправильно!!

Обновление:

Я принял решение, предоставленное Полом Кирби ниже, и, как я полагаю, создал «своего рода» шаблон репозитория.

Я создал интерфейс;

Public Interface IFriendlyRepository(Of T)
        ReadOnly Property FriendlyRecords As ObservableCollection(Of T)
        Function GetFilteredFriendlyRecords(predicates As List(of Func(Of T, Boolean))) As ObservableCollection(Of T)
        Function AddEditFriendlyRecord(ByVal RecordToSave As T) As EntityException
        Sub SaveData()
End Interface

Затем я реализовал этот интерфейс для каждого класса;

Namespace Repositories
    Public Class clsCurrenciesRepository
        Implements Interfaces.IFriendlyRepository(Of CriticalPathDB.FriendlyCurrencies)

        Private _DBContext As CriticalPathEntities                          'The Data Context

        Public Sub New(ByVal Context As DbContext)   
            _DBContext = Context
        End Sub

        Public ReadOnly Property FriendlyRecords As ObservableCollection(Of FriendlyCurrencies) Implements Interfaces.IFriendlyRepository(Of CriticalPathDB.FriendlyCurrencies).FriendlyRecords
            Get
                ' We need to convert the results of a Linq to SQL stored procedure to a list,
                ' otherwise we get an error stating that the query cannot be enumerated twice!
                Dim Query = (From Currencies In _DBContext.Currencies.ToList
                             Group Join CreationUsers In _DBContext.Users.ToList
                             On Currencies.CreationUserCode Equals CreationUsers.User_Code Into JoinedCreationUsers = Group
                             From CreationUsers In JoinedCreationUsers.DefaultIfEmpty
                             Group Join UpdateUsers In _DBContext.Users.ToList
                             On Currencies.LastUpdateUserCode Equals UpdateUsers.User_Code Into JoinedUpdateUsers = Group
                             From UpdateUsers In JoinedUpdateUsers.DefaultIfEmpty
                             Where (Currencies.Deleted = False Or Currencies.Deleted Is Nothing)
                             Order By Currencies.NAME
                             Select New FriendlyCurrencies With {.Currency_Code = Currencies.Currency_Code,
                                                                .NAME = Currencies.NAME,
                                                                .Rate = Currencies.Rate,
                                                                .CreatedBy = If(Currencies.CreationUserCode Is Nothing, "", CreationUsers.First_Name & " " & CreationUsers.Last_Name),
                                                                .CreationDate = Currencies.CreationDate,
                                                                .CreationUserCode = Currencies.CreationUserCode,
                                                                .Deleted = Currencies.Deleted,
                                                                .LastUpdateDate = Currencies.LastUpdateDate,
                                                                .LastUpdatedBy = If(Currencies.LastUpdateUserCode Is Nothing, "", UpdateUsers.First_Name & " " & UpdateUsers.Last_Name),
                                                                .LastUpdateUserCode = Currencies.LastUpdateUserCode}).ToList

                Return New ObservableCollection(Of FriendlyCurrencies)(Query)
            End Get
        End Property

        Public Function GetFilteredFriendlyRecords(predicates As List(of Func(Of FriendlyCurrencies, Boolean))) As ObservableCollection(Of FriendlyCurrencies) Implements Interfaces.IFriendlyRepository(Of CriticalPathDB.FriendlyCurrencies).GetFilteredFriendlyRecords
            Dim ReturnQuery = FriendlyRecords.ToList

            For Each Predicate As Func(Of FriendlyCurrencies, Boolean) In predicates
                If Predicate IsNot Nothing Then
                    ReturnQuery = ReturnQuery.Where(Predicate).ToList
                End If
            Next
            Return New ObservableCollection(Of FriendlyCurrencies)(ReturnQuery)
        End Function

        Public Function AddEditFriendlyRecord(ByVal RecordToSave As FriendlyCurrencies) As EntityException Implements Interfaces.IFriendlyRepository(Of CriticalPathDB.FriendlyCurrencies).AddEditFriendlyRecord

            Dim dbCurrency As New Currency
            ' Check if this Staff Member Exists
            Dim query = From c In _DBContext.Currencies
                        Where c.Currency_Code = RecordToSave.Currency_Code
                        Select c

            ' If Asset exists, then edit.
            If query.Count > 0 Then
                dbCurrency = query.FirstOrDefault
            Else
                'Do Nothing
            End If

            dbCurrency.Currency_Code = RecordToSave.Currency_Code
            dbCurrency.NAME = RecordToSave.NAME
            dbCurrency.CreationDate = RecordToSave.CreationDate
            dbCurrency.CreationUserCode = RecordToSave.CreationUserCode
            dbCurrency.LastUpdateDate = RecordToSave.LastUpdateDate
            dbCurrency.LastUpdateUserCode = RecordToSave.LastUpdateUserCode
            dbCurrency.Deleted = RecordToSave.Deleted

            ' Save Asset Object to Database
            If query.Count > 0 Then
                ' If Asset exists, then edit.
                Try
                    '_dbContext.SaveChanges           'We could save here but it's generally bad practice
                Catch ex As EntityException
                    Return ex
                End Try
            Else
                Try
                    _DBContext.Currencies.Add(dbCurrency)
                    '_dbContext.SaveChanges           'We could save here but it's generally bad practice
                Catch ex As EntityException
                    Return ex
                End Try
            End If
            Return Nothing
        End Function

        Public Sub SaveData() Implements Interfaces.IFriendlyRepository(Of CriticalPathDB.FriendlyCurrencies).SaveData
            _DBContext.SaveChanges()
        End Sub
    End Class
End Namespace

Я использовал внедрение конструктора, чтобы вставить dbContext в класс.

Я надеялся смоделировать поддельный dbContext, используя существующий контекст и инструмент модульного тестирования "Effort". .

Однако, похоже, я не могу заставить это работать.

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

Затем я создаю dbContext, ссылающийся на тестовую базу данных, заполняю ее тестовыми данными и тестирую их.

В качестве примечания, я буду реорганизовывать свой метод «Добавить/редактировать» для работы с реальной базовой сущностью, а не с моей «дружественной» сложной версией, это был самый простой метод в то время.


person PGallagher    schedule 11.10.2012    source источник


Ответы (1)


Если вы сначала работаете с БД, я бы посоветовал вот что.

  1. Откройте файл .edmx, щелкните правой кнопкой мыши любое пустое место и выберите «Добавить элемент генерации кода».
  2. В области «Онлайн-шаблоны» найдите «EF 5.x DbContext Generator for VB».
  3. Дайте имя файлу .tt, нажмите «Добавить». Это изменит способ создания вашим файлом .edmx резервного кода, чтобы все ваши сущности были POCO, что упрощает общее тестирование, сохраняя отключение основной логики от EF.

После того, как вы это сделаете, вы, вероятно, захотите изучить что-то вроде шаблона Unit of Work. Вот краткий пример кода, я объясню его позже.

public interface IUnitOfWork
{
    IDbSet<Location> Locations { get; }
    void Commit();
}

public class EFUnitOfWork : IUnitOfWork
{
    private readonly YourGeneratedDbContext _context;

    public EFUnitOfWork(string connectionString)
    {
        _context = new YourGeneratedDbContext();
    }

    public IDbSet<Location> Locations
    {
        get { return _context.Locations; }
    }

    public void Commit()
    {
        _context.SaveChanges();
    }
}

Это базовая единица работы, которая в качестве примера предоставляет некоторый список местоположений (извините, что это на C#, но я плохо знаю VB).

Обратите внимание, что он предоставляет объекты IDbSet — вот тут-то и начинается магия. Если в вашем проекте DBAccess вы используете эту единицу работы или шаблон репозитория, чтобы скрыть EF, и поскольку он реализует интерфейс и возвращает объекты IDbSet, в любом месте, которое нуждается в том, чтобы ваши данные могли иметь этот конструктор IUnitOfWork, внедренный с DI, и замененный фиктивной версией, которая возвращает фиктивные объекты IDbSet (в конце концов, они просто IQueryables), когда вам нужно выполнить модульное тестирование.

Вы можете обнаружить, что с генерацией POCO в этом новом шаблоне вы даже можете отказаться от большей части работы, которую вы выполняете в своем проекте DBControl.

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

person Paul Kirby    schedule 12.10.2012
comment
Большое спасибо за это ... Я добавил генерацию кода и удалил существующий файл .tt (который, конечно, генерировал ошибки) ... Сейчас я поиграю и вернусь к вам ... Спасибо еще раз! - person PGallagher; 13.10.2012
comment
Просто быстрый вопрос... Нужно ли мне тогда добавлять IDbSet для всех моих сущностей в этот один файл класса? Включая функции для получения моих дружественных данных? Что я хотел бы сделать, так это протестировать эти функции на фиктивном источнике данных. - person PGallagher; 13.10.2012
comment
Обычно я так и делаю, да. Ваша единица работы должна скомпрометировать все типы объектов, которые вам могут понадобиться в одной конкретной высокоуровневой операции. Конечно, у вас также может быть несколько разных единиц работы. В качестве примера, возможно, у вас есть единица работы для операции, которая позволяет искать пользователей по адресу (поэтому вам нужны объекты Location и User), а другая — для операции, которая вставляет пользователей и их разрешения (объекты User и Permission). . В основном это зависит от контекста того, что вы делаете. - person Paul Kirby; 15.10.2012
comment
Еще раз спасибо за это ... Ваше решение работает ... и теперь я развил его, чтобы (почти) включить использование Effort ... Для насмешки над моей базой данных. Как только я закончу его, я опубликую свое решение в качестве еще одного ответа! - person PGallagher; 16.10.2012