Click here to Skip to main content
15,868,016 members
Articles / Programming Languages / C#

Automatic graph operations using EntityFramework

Rate me:
Please Sign up or sign in to vote.
4.80/5 (13 votes)
10 Nov 2016CPOL5 min read 12.2K   125   8  
Automatically define state of entity graph for EntityFramework Code-First using a single line of code.

Introduction

First of all, don't get overwhelmed by length of article, the main part of it easy example which is also added as source code. If you are using EntityFramework for a while you probably faced with the problem that wheter you should insert or update entity. Of course, EntityFramework has great feature to track entity changes automatically, but if only they live in the current context. Additionally, even if AutoDetectChanges works in this case, it has its own problems, such as performance issues. If we get our data from other sources (i.e. different services, web pages and etc.), we need to map them to our own models and then save them to database, then AutoDetectChanges will not be able to help us, we will have to define state manually, like:

  1. Query database to check if object exists in the database
  2. If not add it to context
  3. If yes set State to Modified

In code, in its simplest form it will be something like:

C#
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;

Although this works, it will update all properties of the entity in case it exists, and it is not what we want for sure. Now, imagine that you have dozens of models, you have to to same thing for all of them, you have to consider hierarchies, do same thing for child properties and so on. We can count on problems, indeed we face more when we try to implement something like this. But that is enough talking about the problems, Lets see how can we solve this problem easily.

Prerequisite

  • This API should be used with EntityFramewok Code-First
  • You should be familiar with Fluent API (quick introduction).
  • Install Ma.EntityFramewok.GraphManager nuget package.
  • Add explicit foreign key properties to your models. If we do not add explicit property and configure it as foreign key, then entity framework will create it for us. But, we have to create it ourselves.
public class Post
{
    // Foreign key to Blog must exist
    public int BlogID { get; set; }
    public Blog Blog { get; set; }
}
  • Additionally, if you have many-to-many relationshipsyou should create model for third tableFor example, if you have Student and Course models, and if there is many-to-many relation between them, you should also create model for relating third table, which porbably will be StudentCourse. Then Student and StudentCourse, Course and StudentCourse models will have one-to-many relationships. This will help a lot when we want to change which Students attend which Courses (add, update or delete.).

Features

  • Automatically define state of entity graph with ease.
  • Use not only primary keys, but also configured unique keys to define state.
  • Simple and complex unique keys.
  • Send update query only for changed properties.
  • Handle entity duplications according to primary and unique keys.
  • Familiar Fluent API style mappings.
  • Additional customization options to not update certain properties which shouldnot change.
  • Manual operations after automatic state define.

Usage

Using GraphManager is very easy. Here is quick walkthrough:

  1. Your mapping classes should intherit ExtendedEntityTypeConfiguration<TEntity>, where TEntity is type of entity which you are configuring mappings for. To be able to do so, you have to add Ma.EntityFramework.GraphManager.CustomMappings namespace to unsings section. Remember that, you do not have to inherit this class if you do not need any custom mappings (i.e. unique keys, not updated properties and etc.). Automatic state defining should still work without this.
  2. Add Ma.EntityFramework.GraphManager to your usings section where you want to add or update entities.
  3. Define state of whole graph using just one line: context.AddOrUpdate(entity);

That is it, nothing else is required. The one line of code, will do it all for you. Lovely, isn't it?!

Example

Now, lets implemt and test it together.

Step 1: Create application

First of all, create new console application called Ma.EntityFramework.GraphManager.Sample. Install Ma.EntityFramework.GraphManager package from Nuget. Add folders named Concerete and Models to the application. Add following connection sting to the App.config file (do not forget to change it match correct database version if its different on your computer) :

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

Step 2: Add models

Add two subfloders to the Models folder, with names of Enums and Entities. Add ContactType.cs to the Enums folder.

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

Now we are going to add Company, Person and Contact entities and their mappings to Entities folder. 

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
    }
}

Let's take a minute two examine mappings closely:

  • Despite that Comapny does not have any custom mapping we have inherited all mappings from ExtendedEntityTypeConfiguration for consistency.
  • We related Comany and person according to EstablisherID as it does not follow foreign key conventions.
  • We have marked RegistrationNumber as unique property for Person model.
  • We have marked combination of TypeId, Value and ComanyID as unique for Contact model.
  • For everything else, we rely on conventions (See this link for more info if you are interested).

Step 3: Add data provider

Add DataProvider class to the Concrete folder. The only purpose of this class is to provide test data.

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"
        };
    }
}

Step 4: Create DbContext

We need to create DdContext, override OnModelCreating method and add configurations to model builder. Instead of adding mapping classes one-by-one we will use a little reflection to get all configurations and add them automatically. Add GraphManagerSampleContext to Concrete folder.

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);
    }
}

Step 5: Let's test it!

Now, you are all set to freely test it as you want. For example we can add new companies:

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();
}

We can change Establiser of first comapny without dealing with keys:

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();
}

Note that in this code snippet, we get comapny according to its name to get ID of comapny. But if defined Name property Company model as unique, we will not need to do this, Establisher will be updad according to provided company name. You can set Name property as unique in the CompanyMap and check for yourself.

We can update name of person:

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

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

You can download provided sample code and play on it, or create more complex samples and test it as you want. There is just three models in this application, imagine how it can be useful in an application with dozens of models and complex hierarchies.

Remarks

  • Be sure to check out GitHub page of this API.
  • Link to Nuget package.
  • I'm going to write documentation for this API at the Wiki section of GitHub. So be sure to check this out soon.
  • Feel free to contact me according to this API in case you have any suggestion or question.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Azerbaijan Azerbaijan
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --