Some Best Practices for Entity Framework Core, my opinion
March 19, 2025Entity Framework Core (EF Core) is a powerful ORM (Object-Relational Mapper) for .NET applications that simplifies database interactions. However, to build efficient, scalable, and maintainable applications, following best practices is crucial.
In this blog post, we’ll explore best practices for EF Core, covering topics like:
- Primary Keys (PK) & Foreign Keys (FK)
- Annotations & Fluent API
- Inverse Properties for Navigation
- Optimizing Performance for Queries
- Best Security Practices for APIs
Defining Primary and Foreign Keys
A well-structured database starts with proper primary and foreign key definitions.
Primary Key (PK) Best Practices
- Always use a unique identifier (
int
,GUID
, etc.). - Use
Key
annotation or Fluent API to explicitly mark the PK. - Prefer auto-incremented IDs (
Identity
in SQL Server,Serial
in PostgreSQL).
Example: Defining a Primary Key
public class Product
{
[Key] // Explicitly marking as PK
public int Id { get; set; }
[Required]
[MaxLength(100)]
public string Name { get; set; }
}
Foreign Key (FK) Relationships
Foreign Keys define relationships between entities and ensure referential integrity.
Example: Defining a Foreign Key
public class Order
{
[Key]
public int Id { get; set; }
[Required]
public int ProductId { get; set; } // Foreign Key to Product
[ForeignKey("ProductId")] // Explicitly defining FK
public Product? Product { get; set; }
}
Best Practice: Always use [ForeignKey]
or Fluent API to explicitly define relationships.
Using Annotations for Data Validation
EF Core provides Data Annotations to enforce constraints on your entities.
Example: Using Data Annotations
public class Customer
{
[Key]
public int Id { get; set; }
[Required] // Ensures Name is NOT NULL
[MaxLength(200)] // Limits Name to 200 chars
public string Name { get; set; }
[EmailAddress] // Ensures valid email format
public string Email { get; set; }
}
Best Practice: Always use annotations like [Required]
, [MaxLength]
, [EmailAddress]
to enforce constraints at the database level.
Inverse Properties & Navigation Properties
Why Use Inverse Properties?
EF Core automatically determines relationships, but explicitly defining Inverse Properties improves performance and readability.
Why Use ICollection<T>
Instead of List<T>
?
ICollection<T>
is more flexible thanList<T>
because EF Core handles lazy loading more efficiently.- When defining navigation properties, using
ICollection<T>
ensures better proxy support for EF Core. List<T>
has a concrete implementation, which can cause unintended behavior when EF Core proxies are generated dynamically.
Example: One-to-Many Relationship with Inverse Property
public class Customer
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
// One Customer can have multiple Orders
[InverseProperty("Customer")]
public ICollection<Order> Orders { get; set; } = new HashSet<Order>();
}
public class Order
{
[Key]
public int Id { get; set; }
public int CustomerId { get; set; }
[ForeignKey("CustomerId")]
public Customer Customer { get; set; } // Navigation Property
}
Best Practice: Always use [InverseProperty]
when there are multiple relationships between the same entities to avoid ambiguity.
Optimizing Query Performance in EF Core
Handling database queries efficiently is crucial for high-performance APIs.
Best Practices for Query Performance
Use AsNoTracking() for Read-Only Queries
var products = dbContext.Products.AsNoTracking().ToList();
Why?
- Improves performance by disabling change tracking.
- Ideal for read-heavy operations.
Use Eager Loading to Avoid N+1 Queries
var customerNames = dbContext.Customers.Select(c => new { c.Id, c.Name }).ToList();
Why?
- Reduces unnecessary data loading.
- Improves API response times.
Securing EF Core APIs
Security is critical when exposing database interactions via an API. Here are the best practices:
Secure API Endpoints with Authentication & Authorization
Use JWT (JSON Web Tokens) or OAuth 2.0 to authenticate users before accessing EF Core.
[Authorize]
[HttpGet("orders")]
public IActionResult GetOrders()
{
var orders = _dbContext.Orders.Include(o => o.Customer).ToList();
return Ok(orders);
}
Why?
- Prevents unauthorized access.
- Ensures only authenticated users can retrieve data.
Prevent SQL Injection with Parameterized Queries
EF Core already prevents SQL injection, but never concatenate user input in queries.
var customer = _dbContext.Customers.Where(c => c.Email == userInput).FirstOrDefault();
Why?
- Protects against malicious SQL injection attacks.
- Ensures data integrity.
Implement Rate Limiting to Prevent DDoS Attacks
Use ASP.NET Rate Limiting Middleware to protect EF Core queries from excessive requests.
services.AddRateLimiter(options =>
{
options.GlobalLimiter = RateLimitPartition.GetFixedWindowLimiter(
partitionKey: context => context.Connection.RemoteIpAddress.ToString(),
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 10,
Window = TimeSpan.FromSeconds(60)
});
});
Why?
- Prevents API abuse by limiting requests per user.
- Enhances overall system stability.
Conclusion
Following EF Core best practices ensures your application is scalable, maintainable, and performant.
Key Takeaways:
- Use proper PKs & FKs for relationships.
- Use annotations to enforce validation.
- Optimize queries using
AsNoTracking()
,Include()
, andSelect()
. - Secure APIs with authentication, rate limiting, and best security practices.