65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 EntityFramework 自动执行图操作

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.52/5 (9投票s)

2016年11月10日

CPOL

5分钟阅读

viewsIcon

13004

downloadIcon

148

使用一行代码自动为 EntityFramework Code-First 定义实体图的状态。

引言

首先,不要被文章的长度吓到,它的主要部分是一个简单的示例,也已作为源代码添加。如果你使用 **EntityFramework** 有一段时间了,你可能遇到过这样的问题:应该插入还是更新实体。当然,EntityFramework 有一个很好的特性可以自动跟踪实体更改,但这仅限于当前上下文中的实体。此外,即使 **AutoDetectChanges** 在这种情况下工作,它也有自己的问题,比如性能问题。如果我们从其他来源(例如不同的服务、网页等)获取数据,我们需要将它们映射到我们自己的模型,然后保存到数据库,那么 **AutoDetectChanges** 就无法帮助我们,我们将不得不手动定义状态,例如

  1. 查询数据库以检查对象是否存在于数据库中
  2. 如果不存在,则将其添加到上下文中
  3. 如果存在,则将 **State** 设置为 **Modified**

在代码中,最简单的形式如下

Person person = GetPersonFromService();
if(context.Persons.Any(m => m.RegNumber == person.RegNumber)
    context.Entry(company).State = EntityState.Modified;
else
    context.Entry(company).State = EntityState.Added;

虽然这可行,但在实体存在的情况下,它会更新实体的所有属性,这肯定不是我们想要的。现在,想象一下你有几十个模型,你必须为所有模型做同样的事情,你必须考虑层级结构,对子属性做同样的事情等等。我们可以预期会遇到问题,事实上,当我们试图实现类似的东西时,会遇到更多的问题。但关于问题的讨论就到这里,让我们看看如何轻松地解决这个问题。

先决条件

  • 此 API 应与 EntityFramewok Code-First 一起使用
  • 你应该熟悉 Fluent API(快速介绍)。
  • 安装 **Ma.EntityFramewok.GraphManager** nuget 包
  • 在模型中添加显式的外键属性。如果我们不添加显式属性并将其配置为外键,则 Entity Framework 会为我们创建它。但是,我们必须自己创建它。
public class Post
{
    // Foreign key to Blog must exist
    public int BlogID { get; set; }
    public Blog Blog { get; set; }
}
  • 此外,如果你有多对多关系,你应该为第三个表创建模型。例如,如果你有 *Student* 和 *Course* 模型,并且它们之间存在多对多关系,你也应该为关联的第三个表(很可能是 *StudentCourse*)创建模型。然后 *Student 和 StudentCourse、Course 和 StudentCourse* 模型将具有一对多关系。这在我们想更改哪些学生参加哪些课程(添加、更新或删除)时会有很大帮助。

特点

  • 轻松自动定义实体图的状态。
  • 使用不仅是主键,还包括配置的唯一键来定义状态。
  • 简单和复杂的唯一键。
  • 仅为已更改的属性发送更新查询。
  • 根据主键和唯一键处理实体重复。
  • 熟悉的 Fluent API 风格映射。
  • 额外的自定义选项,用于不更新某些不应更改的属性。
  • 自动定义状态后的手动操作。

用法

使用 **GraphManager** 非常简单。这是一个快速教程

  1. 你的映射类应继承 **ExtendedEntityTypeConfiguration<TEntity>**,其中 *TEntity* 是你要为其配置映射的实体的类型。为了能够做到这一点,你必须在*using*部分添加 **Ma.EntityFramework.GraphManager.CustomMappings** 命名空间。请记住,如果你不需要任何自定义映射(*例如唯一键、不更新的属性等*),则不必继承此类。自动状态定义即使没有它也应该能正常工作。
  2. 在你想添加或更新实体的地方,将 **Ma.EntityFramework.GraphManager** 添加到你的*using*部分。
  3. 只需一行代码即可定义整个图的状态:**context.AddOrUpdate(entity);**

就是这样,不需要其他任何东西。这一行代码将为你完成所有事情。很棒,不是吗?!

示例

现在,让我们一起实现和测试它。

步骤 1:创建应用程序

首先,创建一个名为 *Ma.EntityFramework.GraphManager.Sample* 的新控制台应用程序。从 Nuget 安装 **Ma.EntityFramework.GraphManager** 包。在应用程序中添加名为 *Concerete* 和 *Models* 的文件夹。将以下连接字符串添加到 **App.config** 文件(*如果你的计算机上的数据库版本不同,请不要忘记修改它以匹配正确的版本*)

<connectionStrings>
    <add connectionString="Data Source=(localdb)\v11.0;Initial Catalog=GraphManager.Sample;Integrated Security=True" 
         name="GraphManagerSample" providerName="System.Data.SqlClient" />
  </connectionStrings>

步骤 2:添加模型

在 Models 文件夹中添加两个子文件夹,名称分别为 **Enums** 和 **Entities**。将 **ContactType.cs** 添加到 Enums 文件夹。

public enum ContactType
{
    NotSPecified,
    Mobile, Phone, Email, Fax
}

现在我们将把 **Company**、**Person** 和 **Contact** 实体及其映射添加到 Entities 文件夹中。

Company.cs
public class Company
{
    public int CompanyID { get; set; }
    public string Name { get; set; }
    public DateTime EstablishmentDate { get; set; }
    public int EstablisherID { get; set; }        

    // Navigation properties
    public Person Establisher { get; set; }        
    public ICollection<Contact> Contacts { get; set; }
} 

internal class CompanyMap
    : ExtendedEntityTypeConfiguration<Company>
{
    public CompanyMap()
    {
        // Primary key

        // Properties
        Property(m => m.Name)
            .IsRequired()
            .HasMaxLength(150);

        // Table & column mappings

        // Custom mappings

        // Relationship mappings
        HasRequired(m => m.Establisher)
            .WithMany()
            .HasForeignKey(m => m.EstablisherID)
            .WillCascadeOnDelete(false);
    }
}  
Person.cs
public class Person
{
    private string registrationNumber;

    public int PersonID { get; set; } 
    public string RegistrationNumber
    {
        get { return registrationNumber; }
        set
        {
            if (!string.IsNullOrEmpty(value))
                registrationNumber = value.ToUpper();
        }
    }
    public string Surname { get; set; }
    public string FirstName { get; set; }
    public string Patronymic { get; set; }
    public DateTime BirthDate { get; set; }

    // Navigation properties
}

internal class PersonMap
    : ExtendedEntityTypeConfiguration<Person>
{
    public PersonMap()
    {
        // Primary key

        // Properties
        Property(m => m.RegistrationNumber)
            .HasMaxLength(7)
            .IsFixedLength();

        Property(m => m.Surname)
            .HasMaxLength(30);

        Property(m => m.FirstName)
            .HasMaxLength(30);

        Property(m => m.Patronymic)
            .HasMaxLength(30);

        // Table & column mappings

        // Custom mappings
        HasUnique(m => m.RegistrationNumber);

        // Relationship mappings
    }
}
Contact.cs
public class Contact
{
    public int ContactID { get; set; }
    public ContactType TypeId { get; set; }
    public string Value { get; set; }
    public int CompanyID { get; set; }

    // Navigation properties
    public Company Company { get; set; }
}

internal class ContactMap
    : ExtendedEntityTypeConfiguration<Contact>
{
    public ContactMap()
    {
        // Primary key

        // Properties
        Property(m => m.Value)
            .IsRequired()
            .HasMaxLength(20);

        // Table & column mappings

        // Custom mappings
        HasUnique(m => new { m.TypeId, m.Value, m.CompanyID });

        // Relationship mappings
    }
}

让我们花点时间仔细检查映射

  • 尽管 *Comapny* 没有任何*自定义映射*,但我们为了保持一致性而继承了 ExtendedEntityTypeConfiguration 的所有映射。
  • 我们根据*EstablisherID*关联了 Comany 和 person,因为它不遵循*外键约定*。
  • 我们将*RegistrationNumber*标记为 *Person* 模型上的唯一属性。
  • 我们将*TypeId、Value 和 ComanyID*的组合标记为 *Contact* 模型上的唯一属性。
  • 对于其他所有内容,我们依赖于约定(如果您有兴趣,请参阅此链接了解更多信息)。

步骤 3:添加数据提供程序

将 **DataProvider** 类添加到 *Concrete* 文件夹。此类的唯一目的是提供测试数据。

public class DataProvider
{
    public Company GetFirstCompany()
    {
        return new Company
        {
            Name = "First LLC",
            EstablishmentDate = new DateTime(2010, 10, 10)
        };
    }

    public Company GetSecondCompany()
    {
        return new Company
        {
            Name = "Second LLC",
            EstablishmentDate = new DateTime(2008, 08, 08)
        };
    }

    public Person GetFirstPerson()
    {
        return new Person
        {
            RegistrationNumber = "12A53BC",
            FirstName = "Roxie",
            Surname = "Solomon",
            Patronymic = "C",
            BirthDate = new DateTime(1990, 1, 25)
        };
    }

    public Person GetSecondPerson()
    {
        return new Person
        {
            RegistrationNumber = "72AC5F2",
            FirstName = "Paul",
            Surname = "Lewis",
            Patronymic = "M",
            BirthDate = new DateTime(1990, 1, 25)
        };
    }

    public Contact GetFirstContact()
    {
        return new Contact
        {
            TypeId = ContactType.Mobile,
            Value = "123456789"
        };
    }

    public Contact GetSecondContact()
    {
        return new Contact
        {
            TypeId = ContactType.Email,
            Value = "contact@company.com"
        };
    }
}

步骤 4:创建 DbContext

我们需要创建 DbContext,重写 **OnModelCreating** 方法并将配置添加到 model builder。我们不会*逐个*添加映射类,而是使用一些反射来获取所有配置并将它们自动添加。将 **GraphManagerSampleContext** 添加到 Concrete 文件夹。

public class GraphManagerSampleContext
    : DbContext
{
    public GraphManagerSampleContext()
        : base("Name=GraphManagerSample")
    {
        // Disable auto detect changes
        Configuration.AutoDetectChangesEnabled = false;
    }

    public DbSet<Company> CompanySet { get; set; }
    public DbSet<Person> PersonSet { get; set; }
    public DbSet<Contact> ContactSet { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        /// Get all mappings which inherits either EntityTypeConfiguration<>
        /// or ExtendedEntityTypeConfiguration<>
        IEnumerable<Type> mappingClasses = Assembly
            .GetExecutingAssembly()
            .GetTypes()
            .Where(m => m.IsClass
                && !m.ContainsGenericParameters
                && !m.IsAbstract
                && !m.IsGenericType
                && m.BaseType != null
                && m.BaseType.IsGenericType
                && (m.BaseType.GetGenericTypeDefinition().Equals(typeof(ExtendedEntityTypeConfiguration<>))
                    || m.BaseType.GetGenericTypeDefinition().Equals(typeof(EntityTypeConfiguration<>))));

        // Add all mappings to configuration list
        foreach (Type mappingClass in mappingClasses)
        {
            dynamic mappingInstance = Activator.CreateInstance(mappingClass);
            modelBuilder.Configurations.Add(mappingInstance);
        }

        // Call base method
        base.OnModelCreating(modelBuilder);
    }
}

步骤 5:让我们测试一下!

现在,您已准备好随意进行测试。例如,我们可以添加新的公司

DataProvider dataProvider = new DataProvider();

/// Initialize new companies, set establisher and contact
Company firstCompany = dataProvider.GetFirstCompany();
firstCompany.Establisher = dataProvider.GetFirstPerson();
firstCompany.Contacts = new List<Contact>
{
    dataProvider.GetFirstContact()
};

Company secondComapny = dataProvider.GetSecondCompany();
secondComapny.Establisher = dataProvider.GetSecondPerson();
secondComapny.Contacts = new List<Contact>
{
    dataProvider.GetSecondContact()
};

using (var context = new GraphManagerSampleContext())
{
    context.AddOrUpdate(firstCompany);
    context.AddOrUpdate(secondComapny);
    context.SaveChanges();
}

我们可以更改第一家公司*Establiser*,*而无需处理键*

using (var context = new GraphManagerSampleContext())
{
    // Get company according to name
    DataProvider dataProvider = new DataProvider();
    string firstCompanyName = dataProvider
        .GetFirstCompany()
        .Name;

    Company firstCompany = context
        .CompanySet
        .FirstOrDefault(m => m.Name == firstCompanyName);

    // Exit if company not found
    if (firstCompany == null)
        return;

    /// Person has unique filed so we should not need an ID
    firstCompany.Establisher = dataProvider.GetSecondPerson();

    context.AddOrUpdate(firstCompany);
    context.SaveChanges();
}

请注意,在此代码片段中,我们根据公司*名称*获取公司以获取其 ID。但是,如果我们将*Name*属性在*Company*模型上定义为唯一,我们将不需要这样做,Establisher 将根据提供的公司名称进行更新。您可以在 **CompanyMap** 中将*Name*属性设置为唯一,然后自行检查。

我们可以更新人员姓名

using (var context = new GraphManagerSampleContext())
{
    DataProvider dataProvider = new DataProvider();
    Person secondPerson = dataProvider.GetSecondPerson();

    secondPerson.FirstName = "Timothy";
    context.AddOrUpdate(secondPerson);
    context.SaveChanges();
}

您可以下载提供的示例代码并进行操作,或者创建更复杂的示例并根据需要进行测试。此应用程序中只有三个模型,想象一下在拥有数十个模型和复杂层级结构的应用程序中它会有多大用处。

备注

  • 请务必查看此 API 的 GitHub 页面
  • 链接到 Nuget 包。
  • 我将在 GitHub 的 Wiki 部分为这个 API 编写文档。所以请尽快查看。
  • 如果您对此 API 有任何建议或问题,请随时与我联系。
© . All rights reserved.