Skip to content

Instantly share code, notes, and snippets.

@sandord
Last active June 12, 2018 20:56
Show Gist options
  • Save sandord/c5b041d8ed77a5d15d7434a2af72c8e9 to your computer and use it in GitHub Desktop.
Save sandord/c5b041d8ed77a5d15d7434a2af72c8e9 to your computer and use it in GitHub Desktop.
Entity Framework 6 Tips & Tricks

EF6 Tips & Tricks

Guidance

Entity classes

Parameterless constructor

Each entity must have a parameterless constructor.

publicEntity() { }

Example in conjunction with a public constructor taking arguments:

protected Entity() { }

public Entity(string name) : this()
{
    Name = name;
}

The protected constructor forces application code to call the constructor that has arguments.

Identity

The identity property can have private setter to prevent modification.

public int Id { get; private set; }

Read-only navigation properties

public virtual Dependency Dependency { get; private set; }

public Entity(Dependency dependency)
{
    Dependency = dependency ?? throw new ArgumentNullException(nameof(dependency));
}

Encapsulated navigation properties

public virtual Dependency Dependency { get; private set; }

// This method exists because changing the value through the property directly would require the
// property to have a null check in the setter (since it must not be null), which will lead to
// problems with Entity Framework when deleting instances of this class (EF sets relations to
// null after deleting them).
public void SetDependency(Dependency dependency) =>
    Dependency = dependency ?? throw new ArgumentNullException(nameof(dependency));

Encapsulated value properties

private string _name;

public string Name
{
    get => _name;
    set => _name = !string.IsNullOrEmpty(value) ? value
        : throw new ArgumentException("Null or empty string.", nameof(Name));
}

Collection properties

public virtual List<Dependency> Dependencies { get; private set; } = new List<Dependency>();

In addition, we can provide modification methods but only if they are used to provide any validation value. Otherwise they're useless ceremony since Entity Framework doesn't natively support read-only collections anyway.

So, it's the programmer's responsibility not to modify the collection property directly if modification methods are available for the collection.

It is possbible to achieve the effect of read-only collections but the plumbing and side effects are probably not worth it. See https://lostechies.com/jimmybogard/2014/04/29/domain-modeling-with-entity-framework-scorecard and https://ardalis.com/exposing-private-collection-properties-to-entity-framework for some background.

public void AddDependency(Depedency dependency)
{
    if (dependency == null) throw new ArgumentNullException(nameof(dependency));
    
    // TODO: Validate state and input.
    
    Dependencies.Add(dependency);
}

public void RemoveDependency(Depedency dependency)
{
    if (dependency == null) throw new ArgumentNullException(nameof(dependency));
    
    // TODO: Validate state and input.

    Dependencies.Remove(dependency);
}

Mapping

Mapping conventions

The default mapping conventions are fine in most situations.

The following convention is quite useful though:

modelBuilder
    .Properties()
    .Where(n => n.Name == "Id")
    .Configure(n => n.IsKey());

Mapping overrides

Most entity relations need some additional mapping instructions.

Example 1

modelBuilder.Entity<Player>()
    .HasRequired(n => n.Team)
    .WithMany(n => n.Players)
    // Prohibit deletion of a team that has players associated with it.
    .WillCascadeOnDelete(false);

Example 2

modelBuilder.Entity<Customer>()
    .HasMany(n => n.Subscriptions)
    .WithRequired(n => n.Customer)
    // When deleting a customer, all of its subscriptions are deleted along with it.
    .WillCascadeOnDelete(true);

Querying

Projections

Usually, the following pattern is a good approach to efficient querying. By projecting the data you want into an anonymous object, the generated SQL will only select the data you really want.

var query =
    from customer in dbContext.Customers
    orderby customer.Name
    select new
    {
        customer.Name,
        LogoId = customer.Logo.Id
    };
    
var totalCount = await query.CountAsync();

var items = await
    (from n in query // Apply pagination if you like, by doing: .Skip(offset).Limit(limit)
        .ToListAsync()
     select new CustomerModel
     {
        Name = n.Name,
        LogoUrl = new Uri($"/logo{n.LogoId}.png")
     });

As you can see, there are actually two projections performed. The second one converts the selected data to a model suitable for further processing. Since some operations (such as the composition of LogoUrl) can't be parsed by the Entity Framework Linq provider, we'll have to do them in memory. The second projection (select new CustomerModel) is done after the ToListAsync() call and therefore performed in memory.

Inner joins

Inner joins can be made by simply adding more from clauses, as long as you apply a join condition (where...) or otherwise you'll end up with a cross join.

from order in dbContext.Orders 
from user in dbContext.Users
where n => n.Key == order.UserKey
select new
{
    OrderNumber = order.OrderNumber,
    UserName = user.Name
}

Outer joins

The simplest way to express an outer join is to use DefaultIfEmpty(). You'll have to resort to the fluent notation (Linq extension methods) for your where statement though.

from order in dbContext.Orders 
from user in dbContext.Users.Where(n => n.Key == order.UserKey).DefaultIfEmpty()
select new
{
    OrderNumber = order.OrderNumber,
    UserName = user != null ? user.Name : ""
}

Aggregation

Rather than

from order in dbContext.Orders
select new
{
    TotalAmount = order.Lines.Sum(n => n.Amount)
}

you should use:

from order in dbContext.Orders
select new
{
    TotalAmount = order.Lines.Select(n => n.Amount).DefaultIfEmpty().Sum()
}

because otherwise, Entity Framework may throw the following exception: The cast to value type 'System.Int32' failed because the materialized value is null. Either the result type's generic parameter or the query must use a nullable type. This does make sense if you realize that the result of the Sum() may return null for an order that has no lines. The DefaultIfEmpty() call mitigates that problem by returning a collection with a single item (the value '0' in this case, which is the default value for int).

Testing

See https://msdn.microsoft.com/en-us/library/dn314429(v=vs.113).aspx

var contextMock = new Mock<DbContext>();

var customers = new List<Customer>() { new Customer { Name = "Customer1" } };
contextMock.Setup(n => n.Customers).Returns(TestDbHelper.CreateDbSetMock(customers).Object);

// Now use contextMock.Object for testing...
   

using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
using Moq;

public static class TestDbHelper
{
    public static Mock<DbSet<T>> CreateDbSetMock<T>(List<T> entities)
        where T : class
    {
        var dbSetMock = new Mock<DbSet<T>>();
        var queryable = entities.AsQueryable();

        dbSetMock.As<IDbAsyncEnumerable<T>>().Setup(m => m.GetAsyncEnumerator()).Returns(new TestDbAsyncEnumerator<T>(queryable.GetEnumerator()));
        dbSetMock.As<IQueryable<T>>().Setup(m => m.Provider).Returns(new TestDbAsyncQueryProvider<T>(queryable.Provider));
        dbSetMock.As<IQueryable<T>>().Setup(m => m.Expression).Returns(queryable.Expression);
        dbSetMock.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(queryable.ElementType);
        dbSetMock.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(queryable.GetEnumerator);

        dbSetMock.Setup(n => n.Add(It.IsAny<T>())).Returns<T>(n => n).Callback<T>(obj =>
        {
            entities.Add(obj);
            queryable = entities.AsQueryable();
        });

        return dbSetMock;
    }
}

public class TestDbAsyncQueryProvider<TEntity> : IDbAsyncQueryProvider
{
    private readonly IQueryProvider _inner;

    public TestDbAsyncQueryProvider(IQueryProvider inner)
    {
        _inner = inner;
    }

    public IQueryable CreateQuery(Expression expression) => new TestDbAsyncEnumerable<TEntity>(expression);
    public IQueryable<TElement> CreateQuery<TElement>(Expression expression) => new TestDbAsyncEnumerable<TElement>(expression);
    public object Execute(Expression expression) => _inner.Execute(expression);
    public TResult Execute<TResult>(Expression expression) => _inner.Execute<TResult>(expression);
    public Task<object> ExecuteAsync(Expression expression, CancellationToken cancellationToken) => Task.FromResult(Execute(expression));
    public Task<TResult> ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken) => Task.FromResult(Execute<TResult>(expression));
}

public class TestDbAsyncEnumerable<T> : EnumerableQuery<T>, IDbAsyncEnumerable<T>, IQueryable<T>
{
    public TestDbAsyncEnumerable(IEnumerable<T> enumerable) : base(enumerable) { }
    public TestDbAsyncEnumerable(Expression expression) : base(expression) { }
    public IDbAsyncEnumerator<T> GetAsyncEnumerator() => new TestDbAsyncEnumerator<T>(this.AsEnumerable().GetEnumerator());
    
    IDbAsyncEnumerator IDbAsyncEnumerable.GetAsyncEnumerator() => GetAsyncEnumerator();
    IQueryProvider IQueryable.Provider => new TestDbAsyncQueryProvider<T>(this);
}

public class TestDbAsyncEnumerator<T> : IDbAsyncEnumerator<T>
{
    private readonly IEnumerator<T> _inner;

    public TestDbAsyncEnumerator(IEnumerator<T> inner) => _inner = inner;
    public void Dispose() => _inner.Dispose();
    public Task<bool> MoveNextAsync(CancellationToken cancellationToken) => Task.FromResult(_inner.MoveNext());
    public T Current => _inner.Current;

    object IDbAsyncEnumerator.Current => Current;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment