Entity Framework Anti-Patterns

In an effort to make Entity Framework easier to learn, online tutorials tend to repeat two patterns that I now consider anti-patterns. Typically, these patterns are described in the context of web applications, with SQL Server database backends.

Sharing classes between the presentation layer and the domain layer

In the first anti-pattern, a set of domain classes are created, representing the entities within the business domain. For example:

public class Author
{
    public int AuthorId { get; set; }
    public string Name { get; set; }
    public bool IsAdmin { get; set; }

    public List<Post> Posts { get; } = new();
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    
    public Author Author { get; set; }
}

Entity Framework is configured to use these classes:

public class BloggingContext : DbContext
{
    public DbSet<Author> Authors { get; set; }
    public DbSet<Post> Posts { get; set; }    
}

Finally, the classes are re-used as API models:

[HttpGet("{postId}")]
public async Task<ActionResult> GetPost(int postId)
{
    var post = await _context.Posts.FindAsync(postId);

    if (post == null)
    {
        return NotFound();
    }

    // Send a serialised domain object directly to the user
    return Ok(post);
}

Thus a single set of classes is shared between two layers of the application.

This pattern has two key disadvantages:

a) Over-posting attacks

When data is received by the controllers, and materialised back into the shared classes by the model binder, there is the potential for a user to send more fields than are required or allowed. This is known as an over-posting attack. Using our Author example again:

public class Author
{
    public int AuthorId { get; set; }

    // This property can be edited by a user
    public string Name { get; set; }
    
    // This property should never be edited by a user
    public bool IsAdmin { get; set; }

    public List<Post> Posts { get; } = new();
}

The user is allowed to update the Name property, but is not allowed to update IsAdmin. However the model binder can not make automatically make that distinction and it will bind all available incoming data.

[HttpPut("{authorId}")]
public async Task<ActionResult> UpdateAuthor(int authorId, Author updatedAuthor)
{
    // Can we trust that updatedAuthor is valid here?
    // What if the user sent { "IsAdmin": true }?
}

There are a number of workarounds for this issue, including the BindAttribute Class, the BindNeverAttribute Class, and writing validation logic into the controller actions.

b) API design is directly tied to domain design

Only the simplest software has an API that directly mirrors the underlying domain design, which makes this pattern a dead-end for real-world business applications.

Anemic domain model

In the second anti-pattern, both domain classes and API models are created (dodging the issues described above), but the domain specific business logic is not added to the domain objects. Instead, it is added to the API controller actions, or to service classes which accept domain objects and modify them.

[HttpPut("{authorId}")]
public async Task<ActionResult> UpdateAuthor(int authorId, UpdateAuthor updateAuthor)
{
    var author =  await _context.Authors.FindAsync(authorId);

    // Business logic to update author lives in a service, not in the Author class
    _authorUpdateService.Update(author, updateAuthor);

    await _context.SaveChangesAsync();
    return NoContent();
}

The domain classes (which are really just collections of properties) are known as anemic domain models, and they are the antithesis of object-orientated programming, which requires data and behaviour to be combined:

Object-oriented programming is a programming paradigm based on the concept of objects, which are self-contained units that combine data (attributes or properties) and behaviour (methods or functions).

What to use instead

First, create domain classes that encapsulate data and behaviour, and configure Entity Framework to use them.

public class Author
{
    // Use getter-only properties to preserve integrity of domain
    public int AuthorId { get; }
    public string Name { get; private set; }
    public bool IsAdmin { get; }

    // Modify domain via methods
    public void UpdateName(string name)
    {
        // Enforce business rules 
        if (this.IsAdmin)
        {
            throw new InvalidOperationException("You cannot update an admin's name.");
        }

        this.Name = name;
    }

    // Abstract dependencies on external services like email or file storage with interfaces
    public void EmailAuthor(IEmailService emailService)
    {
        ...
    }
}

Next, create API models specific to the requirements of the API users.

public class AuthorModel
{   
    // API was designed to use "DisplayName" instead of "Name"
    public string DisplayName { get; set; }
}

Then, write a mapper class or extension methods that converts domain objects to/from API models.

public AuthorModel ToApiModel(this Author author)
{    
    return new AuthorModel()
    {
        DisplayName = author.Name;
    };
}

Finally, write lightweight controller actions.

[HttpGet("{authorId}")]
public async Task<ActionResult> GetAuthor(int authorId)
{
    // Load domain
    var author =  await _context.Authors.FindAsync(authorId);

    // Map and return
    return Ok(author.ToApiModel());
}

[HttpPut("{authorId}")]
public async Task<ActionResult> UpdateAuthor(int authorId, UpdateAuthor updateAuthor)
{
    // Load domain
    var author =  await _context.Authors.FindAsync(authorId);

    // Modify domain
    author.UpdateName(updateAuthor.Name);

    // Save domain
    await _context.SaveChangesAsync();
    return NoContent();
}

Conclusion

My preferred pattern may initially seem underwhelming, and some readers might dismiss it as “obvious”, but it is rarely mentioned in tutorials despite its ability to enable proper object-oriented programming.