This document contains an extensive list of medium to advanced interview questions for Entity Framework Core, designed to test a deep understanding of its features, performance considerations, and internal workings.
Performance Optimization
1. What is the difference between IQueryable<T>
and IEnumerable<T>
in the context of EF Core? Why is it so important? Answer: This is a fundamental concept. IQueryable<T>
represents a query that has not yet been executed. It builds an expression tree that EF Core can translate into a SQL query. The query is only sent to the database when the IQueryable
is materialized (e.g., by calling .ToList()
, .FirstOrDefault()
, .ToArray()
, or iterating over it).
IEnumerable<T>
, on the other hand, represents an in-memory collection. If you convert an EF Core query to an IEnumerable
(e.g., with .AsEnumerable()
), you will pull the entire table from the database into memory before applying subsequent filters like .Where()
or .Take()
. This can lead to massive performance issues.
Bad (fetches all users):
var users = context.Users .AsEnumerable() // Fetches all users from the DB into memory .Where(u => u.IsActive) // Filters in-memory .ToList();
Good (filters in the database):
var users = context.Users .Where(u => u.IsActive) // WHERE clause is added to the SQL query .ToList(); // Executes the filtered query on the DB
2. Explain the purpose of AsNoTracking()
. When should and shouldn’t you use it? Answer: AsNoTracking()
tells EF Core not to track the results of a query. The change tracker is what allows EF Core to know which entities have been modified, added, or deleted so it can generate the correct UPDATE
, INSERT
, or DELETE
statements when SaveChanges()
is called.
- Use
AsNoTracking()
for read-only scenarios. If you are querying data just to display it (e.g., on a webpage, in a report) and have no intention of modifying it in the currentDbContext
scope, usingAsNoTracking()
provides a significant performance boost. It avoids the overhead of setting up change tracking information for each entity. - Do NOT use
AsNoTracking()
if you intend to update the fetched entities within the sameDbContext
instance. EF Core won’t know about your changes, andSaveChanges()
will do nothing.
// Good for a read-only list var products = await context.Products .AsNoTracking() .Where(p => p.IsFeatured) .ToListAsync();
3. What is the difference between AsNoTracking()
and AsNoTrackingWithIdentityResolution()
? Answer: AsNoTracking()
is faster but can lead to duplicate objects. If your query returns the same entity multiple times (e.g., a product that appears in multiple categories in a joined query), AsNoTracking()
will create a distinct object instance for each appearance.
AsNoTrackingWithIdentityResolution()
is a compromise. It does not track changes, but it does perform identity resolution. This means it will ensure that for a given primary key, only one object instance is created, even if it appears multiple times in the result set. This prevents duplicates but has a slightly higher overhead than the standard AsNoTracking()
.
4. How can you prevent the “N+1” query problem in EF Core? Answer: The N+1 problem occurs when you fetch a list of parent entities (1 query) and then loop through them, accessing a navigation property that triggers a separate database query for each parent (N queries). This is a classic example of lazy loading causing performance issues.
You can prevent it using eager loading with the Include()
and ThenInclude()
methods.
Bad (N+1 Problem):
// Assumes lazy loading is enabled. // 1 query to get blogs var blogs = await context.Blogs.ToListAsync(); // N queries - one for each blog to get its posts foreach (var blog in blogs) { // This line triggers a separate query for each blog var posts = blog.Posts; Console.WriteLine($"Blog '{blog.Url}' has {posts.Count} posts."); }
Good (Eager Loading):
// 1 query to get blogs AND their posts using a JOIN var blogs = await context.Blogs .Include(blog => blog.Posts) .ToListAsync(); foreach (var blog in blogs) { // No extra query is triggered here var posts = blog.Posts; Console.WriteLine($"Blog '{blog.Url}' has {posts.Count} posts."); }
5. What are split queries (AsSplitQuery
) and when are they useful? Answer: When you use Include()
on multiple one-to-many relationships, EF Core generates a single SQL query with multiple JOIN
s. This can lead to a “cartesian explosion,” where the database returns a huge amount of redundant parent data.
AsSplitQuery()
tells EF Core to break the single query into multiple SQL queries. The first query fetches the primary entities (e.g., Blogs
), and subsequent queries fetch the related data (e.g., one query for Posts
, another for Tags
). EF Core then stitches the data together in memory. This is often much more efficient than a single, massive query.
var blogs = await context.Blogs .Include(b => b.Posts) .Include(b => b.Contributors) .AsSplitQuery() // Generates 3 separate queries instead of one big one .ToListAsync();
6. Explain DbContext
pooling (AddDbContextPool
). What are its benefits and potential pitfalls? Answer: DbContext
pooling is a performance optimization for applications that frequently create and dispose of DbContext
instances, like ASP.NET Core apps. Instead of creating a new DbContext
object from scratch for each request, the framework maintains a pool of reusable instances. When a request needs a context, it gets one from the pool. When the request is done, the context’s state is reset, and it’s returned to the pool.
- Benefit: Reduces the overhead of object allocation and initialization, improving request throughput.
- Pitfall: Because the instance is reused, you must be careful not to hold onto state in your
DbContext
class via member variables. TheOnConfiguring
method is only called once when the instance is first created, not every time it’s leased from the pool.
7. What are compiled queries and why would you use them? Answer: A compiled query allows you to pre-compile a LINQ query into a delegate that can be executed more efficiently. Normally, EF Core has to parse the LINQ expression tree and generate the SQL for every query execution. A compiled query does this work only once.
They are most useful for high-performance scenarios where the same query structure is executed many times with different parameters.
private static readonly Func<MyDbContext, int, Task<Product>> _getProductById = EF.CompileAsyncQuery((MyDbContext context, int id) => context.Products.FirstOrDefault(p => p.Id == id)); // Usage: var product = await _getProductById(context, 123);
8. How do the new ExecuteUpdate
and ExecuteDelete
methods in EF7+ improve performance? Answer: Before EF7, to update or delete multiple entities, you had to load them into the DbContext
first, modify them, and then call SaveChanges()
. This involved querying the data, tracking changes, and then sending individual UPDATE
or DELETE
statements.
ExecuteUpdate
and ExecuteDelete
perform a batch operation directly in the database without loading entities into the change tracker. This is vastly more efficient for bulk operations.
// Old way: Load, modify, save var blogsToUpdate = await context.Blogs.Where(b => b.Rating < 3).ToListAsync(); foreach(var blog in blogsToUpdate) { blog.IsArchived = true; } await context.SaveChangesAsync(); // New, efficient way: await context.Blogs .Where(b => b.Rating < 3) .ExecuteUpdateAsync(s => s.SetProperty(b => b.IsArchived, true));
Advanced Querying & LINQ
9. What are Global Query Filters and how would you use them for a soft-delete or multi-tenancy scenario? Answer: A Global Query Filter is a LINQ query predicate applied automatically to all queries for a specific entity type. It’s defined in OnModelCreating
.
For soft-delete, you can define a filter that automatically excludes entities marked as IsDeleted
.
modelBuilder.Entity<Post>().HasQueryFilter(p => !p.IsDeleted);
Now, any query for Post
entities will implicitly have WHERE IsDeleted = 0
appended.
For multi-tenancy, you can filter by a TenantId
. This requires the DbContext
to have access to the current tenant’s ID.
public class MyDbContext : DbContext { public int TenantId { get; set; } // ... protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Order>().HasQueryFilter(o => o.TenantId == this.TenantId); } }
10. How can you temporarily disable a Global Query Filter? Answer: You can use the IgnoreQueryFilters()
operator on a specific query to bypass any global filters defined on the model. This is useful for administrative tasks, like viewing or restoring soft-deleted items.
// This query will include soft-deleted posts var allPostsIncludingDeleted = await context.Posts .IgnoreQueryFilters() .ToListAsync();
11. Explain how to use Raw SQL queries. What is the difference between FromSqlRaw
and FromSqlInterpolated
? Answer: EF Core allows you to drop down to raw SQL when a query is too complex for LINQ.
FromSqlRaw
: Executes a raw SQL string. It is vulnerable to SQL injection if you concatenate user input directly into the string. You must parameterize it manually.FromSqlInterpolated
: Uses C# interpolated string syntax ($""
) to create parameterized queries, which is the safe and recommended approach. EF Core automatically converts the interpolated values intoDbParameter
objects.
Safe (FromSqlInterpolated
):
var user = "johndoe"; var blogs = await context.Blogs .FromSqlInterpolated($"EXECUTE dbo.GetMostPopularBlogsForUser {user}") .ToListAsync();
Unsafe (if not parameterized):
var user = "johndoe"; // from user input // DANGEROUS - vulnerable to SQL injection var blogs = await context.Blogs .FromSqlRaw("EXECUTE dbo.GetMostPopularBlogsForUser '" + user + "'") .ToListAsync();
12. Can you use Include()
after a FromSql
method? Answer: Yes, but only if the raw SQL query is composable. This generally means it should be a SELECT
statement from a table (SELECT * FROM Blogs
). If your raw SQL calls a stored procedure, it is typically not composable, and attempting to add Include()
will throw an exception.
13. What are Shadow Properties? Give a practical use case. Answer: A shadow property is a property that is defined in the EF Core model for an entity but does not exist on the .NET class itself. Its value and state are maintained entirely by the Change Tracker.
A common use case is for foreign key properties. You might have Blog
and Post
classes with a navigation property, but you don’t want to expose the BlogId
foreign key property on the Post
class to keep your domain model clean. EF Core can create BlogId
as a shadow property by convention.
Another use case is for auditing, like a LastUpdated
timestamp that is managed by the database or application infrastructure, not the domain model.
// In OnModelCreating modelBuilder.Entity<Blog>() .Property<DateTime>("LastUpdated"); // Accessing it via the ChangeTracker context.Entry(myBlog).Property("LastUpdated").CurrentValue = DateTime.UtcNow;
14. How can you query against a shadow property using LINQ? Answer: You use the static EF.Property
method.
var recentlyUpdatedBlogs = await context.Blogs .OrderByDescending(b => EF.Property<DateTime>(b, "LastUpdated")) .ToListAsync();
15. What are Temporal Tables and how does EF Core support them? Answer: Temporal Tables are a feature in SQL Server (and other databases) that automatically keeps a history of data changes in a separate history table. When a row is updated or deleted, the old version is stored in the history table with a start and end validity time.
EF Core provides first-class support. You can configure an entity to be backed by a temporal table with a single line in OnModelCreating
.
modelBuilder.Entity<Employee>() .ToTable("Employees", b => b.IsTemporal());
EF Core then provides methods to query the data at a specific point in time, or to view all historical changes.
var employeeOnDate = await context.Employees .TemporalAsOf(new DateTime(2021, 1, 1)) .SingleOrDefaultAsync(e => e.Id == 1);
16. How do you map a query to a keyless entity type (formerly query types)? Answer: Keyless entity types are useful for mapping to database views or the results of stored procedures that don’t have a primary key. You configure them in OnModelCreating
using .HasNoKey()
and can map them to a view with .ToView()
.
// The class doesn't need a key property public class BlogPostsCount { public string BlogName { get; set; } public int PostCount { get; set; } } // In OnModelCreating modelBuilder.Entity<BlogPostsCount>(eb => { eb.HasNoKey(); eb.ToView("View_BlogPostCounts"); // Maps to a database view }); // Querying it var counts = await context.Set<BlogPostsCount>().ToListAsync();
Change Tracking, State Management & Concurrency
17. Describe the different EntityState
values and their lifecycle. Answer:
Detached
: The entity is not being tracked by theDbContext
. This is the state of an object you’ve just created withnew
but haven’t added to the context.Added
: The entity is new and marked to be inserted into the database whenSaveChanges()
is called. An entity enters this state when passed tocontext.Add()
.Unchanged
: The entity exists in the database and has not been modified since it was queried. All entities returned from a tracking query start in this state.Modified
: The entity has been modified in some way. The change tracker detects changes to property values and moves the entity fromUnchanged
toModified
. It will be updated whenSaveChanges()
is called.Deleted
: The entity is marked to be deleted from the database. An entity enters this state when passed tocontext.Remove()
.
18. What is the difference between DbContext.Add()
, DbContext.Attach()
, and DbContext.Update()
? Answer:
Add()
: Tells the context that the entity is new. It puts the entity (and any other reachable, untracked entities) into theAdded
state. It will always result in anINSERT
statement.Attach()
: Attaches an existing entity (that you might have created outside the context, e.g., from a web request) to the change tracker in theUnchanged
state. It assumes the entity already exists in the database and is unmodified. No database operation will happen onSaveChanges()
unless you then modify the entity.Update()
: Attaches an existing entity and marks its state asModified
. This will result in anUPDATE
statement for all its properties, whether they changed or not. It’s a blunt instrument.
19. How does DetectChanges()
work internally? When might you need to call it manually? Answer: DetectChanges()
is the process where the DbContext
scans all tracked entities and compares their current property values against a snapshot that was taken when the entity was first loaded or attached. If any differences are found, the entity’s state is set to Modified
, and the specific properties are marked as modified.
EF Core calls DetectChanges()
automatically at certain times (e.g., before SaveChanges()
, before returning results from DbSet.Find()
). You rarely need to call it manually. One scenario is if you’ve made changes and want to see the state of the change tracker before calling SaveChanges()
, perhaps for logging or debugging.
20. Explain optimistic concurrency and how to implement it in EF Core. Answer: Optimistic concurrency is a strategy to handle situations where multiple users might try to edit the same data at the same time. Instead of locking the data (pessimistic concurrency), you allow conflicts to happen and then detect and resolve them.
The implementation involves a concurrency token—a property whose value is checked during an UPDATE
or DELETE
operation.
Implementation:
- Add a property to your entity and mark it with the
[ConcurrencyCheck]
attribute, or for a more robust solution, use arowversion
/timestamp
column with the[Timestamp]
attribute.public class Product { public int Id { get; set; } public string Name { get; set; } [Timestamp] public byte[] RowVersion { get; set; } }
- When EF Core generates an
UPDATE
statement, it will include the concurrency token in theWHERE
clause.UPDATE [Products] SET [Name] = @p0 WHERE [Id] = @p1 AND [RowVersion] = @p2;
- If another user has modified the row in the meantime, the
RowVersion
won’t match, theWHERE
clause will find 0 rows to update, andSaveChanges()
will throw aDbUpdateConcurrencyException
.
21. How do you handle a DbUpdateConcurrencyException
? Answer: You must catch
the exception and implement a resolution strategy. This typically involves:
- Notifying the user that their change failed because the data has been updated by someone else.
- Providing them with three options:
- “Force Overwrite” (Client Wins): Reload the original entity to get the new concurrency token, re-apply the user’s changes, and try saving again.
- “Discard My Changes” (Database Wins): Reload the entity from the database to show the user the current values.
- “Merge”: Present the user with their proposed values and the current database values and let them decide which values to keep.
catch (DbUpdateConcurrencyException ex) { var entry = ex.Entries.Single(); var databaseValues = await entry.GetDatabaseValuesAsync(); var clientValues = entry.CurrentValues; // Logic to show user the conflict and get resolution // ... // Example: Database wins await entry.ReloadAsync(); }
22. What is a disconnected entity, and what is the main challenge when updating one? Answer: A disconnected entity is an entity instance that was retrieved from a DbContext
, sent to a client (like a web browser in a hidden form or a client application), and then sent back to the server to be updated in a new DbContext
instance.
The challenge is that the new DbContext
doesn’t know which properties were changed. If you simply use context.Update(product)
, EF will mark all properties as modified and generate an UPDATE
statement that sets every column, which can be inefficient and overwrite changes made by other users between the read and the write.
23. How can you efficiently update a disconnected entity? Answer: The best approach is to:
- Fetch the original entity from the database in the new context.
- Use
Entry(original).CurrentValues.SetValues(disconnectedEntity)
to copy the scalar values from the disconnected object to the tracked original object. - Call
SaveChanges()
.
This way, EF Core’s change tracker can compare the original and updated values and will only generate UPDATE
statements for the properties that actually changed.
public async Task UpdateProduct(Product productFromRequest) { var productInDb = await _context.Products.FindAsync(productFromRequest.Id); if (productInDb != null) { _context.Entry(productInDb).CurrentValues.SetValues(productFromRequest); await _context.SaveChangesAsync(); } }
Database, Schema & Migrations
24. What is the difference between Table-per-Hierarchy (TPH) and Table-per-Type (TPT) inheritance mapping? Answer:
- TPH (Default): All classes in an inheritance hierarchy are mapped to a single database table. A “discriminator” column is used to identify which type each row represents. It’s simple and generally performs best for queries.
- TPT: Each class in the hierarchy gets its own table. The base class table contains common properties, and derived class tables contain properties specific to that type, with a foreign key linking back to the base table. This results in a more normalized schema but requires
JOIN
s for most queries, which can be slower.
25. How do you configure TPT inheritance? Answer: You use the ToTable()
method in OnModelCreating
.
modelBuilder.Entity<Blog>().ToTable("Blogs"); modelBuilder.Entity<RssBlog>().ToTable("RssBlogs"); // TPT configuration
26. What are Owned Entity Types? How do they differ from Complex Types in EF6? Answer: Owned Entity Types allow you to model entities that can only exist as properties of another “owner” entity. They don’t have their own primary key and are intrinsically part of the owner. A classic example is an Address
class, which might be a property on both Customer
and Supplier
.
EF Core will map the owned type’s properties as columns in the owner’s table. They are conceptually similar to Complex Types in EF6 but are more powerful, as they can contain references to other entities.
// In Customer entity public Address ShippingAddress { get; set; } // In OnModelCreating modelBuilder.Entity<Customer>().OwnsOne(c => c.ShippingAddress);
27. What are Value Converters and what is a good use case? Answer: Value Converters allow you to transform a property’s value when reading from or writing to the database. This lets you use custom domain types that don’t have a natural database mapping.
A great use case is for strongly-typed IDs or for converting an enum
to and from a string value in the database instead of its integer value, making the data more readable.
// Example: Storing an enum as a string var converter = new ValueConverter<StatusEnum, string>( v => v.ToString(), v => (StatusEnum)Enum.Parse(typeof(StatusEnum), v)); modelBuilder .Entity<Order>() .Property(e => e.Status) .HasConversion(converter);
28. What does it mean to create an idempotent migration script? Answer: An idempotent script is one that can be run multiple times without causing errors or changing the result after the first successful execution. EF Core’s Script-Migration
command generates a script that uses the __EFMigrationsHistory
table to check if a migration has already been applied. However, for deployment, you might want a script that includes IF EXISTS
checks to be even more robust, ensuring it can be safely run against databases in various states.
29. How can you apply migrations at runtime? Is this a good practice for production? Answer: You can call context.Database.Migrate()
in your application’s startup code.
This is generally not recommended for production environments.
- It requires the application to have elevated database permissions (to alter schema).
- If you have multiple instances of your application starting up at the same time (in a web farm or container environment), they could all try to run the migration simultaneously, leading to race conditions and failures.
- If a migration fails, it can leave the application in a non-functional state.
The best practice is to apply migrations as a separate step in your deployment pipeline, using dotnet ef database update
or by generating an SQL script.
30. How would you manage migrations for multiple database providers (e.g., SQL Server and PostgreSQL)? Answer: You can use conditional logic in your OnConfiguring
and OnModelCreating
methods based on the provider being used. For migrations, a common pattern is to place provider-specific migrations in different folders and have separate DbContext
designs for each provider if the differences are significant.
A more advanced approach is to use a separate migrations assembly for each provider. You can specify this when adding migrations: dotnet ef migrations add InitialCreate -o Data/Migrations/SqlServer -c MyDbContext
dotnet ef migrations add InitialCreate -o Data/Migrations/Postgres -c MyDbContext
31. What are Value-Generated properties? Give examples. Answer: These are properties for which the database generates a value.
ValueGeneratedOnAdd
: The value is generated when a row is first inserted. The classic example is an identity primary key (Id
).ValueGeneratedOnAddOrUpdate
: The value is generated on insert or update. The classic example is a computed column or arowversion
/timestamp
for concurrency.ValueGeneratedNever
: The database never generates a value. The application is always expected to supply it.
Internals, Architecture & Design Patterns
32. Is the Repository Pattern still necessary when using EF Core? Discuss the pros and cons. Answer: This is a classic debate.
- Argument Against (The “DbSet is a Repository” view):
DbSet<T>
already acts like a generic repository. It exposes methods likeAdd
,Remove
, and LINQ for querying. Adding another repository layer on top of it can be seen as an unnecessary abstraction that just adds boilerplate code without providing much value.DbContext
itself acts as a Unit of Work. - Argument For (The “Decoupling” view):
- Decoupling: A repository can insulate your application/domain layer from EF Core. If you ever needed to switch your data access technology (e.g., to Dapper, or a different ORM), you would only need to change the repository implementation, not your business logic.
- Testing: It makes unit testing easier. You can mock the repository interface (
IRepository<T>
) instead of having to mockDbSet<T>
andDbContext
, which can be complex. - Centralized Logic: You can centralize complex, data-access-specific queries in one place instead of scattering them throughout your application.
Conclusion: For simple CRUD applications, using DbContext
directly is often fine. For large, complex applications with a rich domain model where decoupling and testability are paramount, the repository pattern can still be a valuable tool.
33. What are Interceptors and what are they used for? Answer: Interceptors allow you to intercept, modify, or suppress EF Core operations. They provide low-level hooks into the framework. Use cases include:
DbCommandInterceptor
: Modifying the SQL command just before it’s executed. Useful for logging, adding query hints, or changing the command text.DbConnectionInterceptor
: Intercepting operations on the database connection, likeOpen()
orClose()
.SaveChangesInterceptor
: Running logic before or afterSaveChanges()
is called. Useful for automatically setting audit properties likeDateCreated
orDateModified
.
public class AuditingSaveChangesInterceptor : SaveChangesInterceptor { public override InterceptionResult<int> SavingChanges( DbContextEventData eventData, InterceptionResult<int> result) { var entries = eventData.Context.ChangeTracker.Entries<IAuditable>(); foreach (var entry in entries) { if (entry.State == EntityState.Added) { entry.Entity.DateCreated = DateTime.UtcNow; } if (entry.State == EntityState.Added || entry.State == EntityState.Modified) { entry.Entity.DateModified = DateTime.UtcNow; } } return base.SavingChanges(eventData, result); } }
34. Explain the role of IModelCacheKeyFactory
. Answer: This is an advanced service. EF Core builds an internal model of your entities, relationships, and mappings. This process is expensive, so EF Core caches this model by default. The IModelCacheKeyFactory
is the service responsible for creating the cache key for a given DbContext
instance.
You might need to implement a custom IModelCacheKeyFactory
if some runtime parameter affects the shape of your model. The classic example is in a multi-tenant application where the model itself might change based on the current tenant (e.g., a property is mapped to a different column). You would create a cache key that includes the TenantId
to ensure each tenant gets its own cached model.
35. How does SaveChanges()
ensure atomicity? Answer: By default, SaveChanges()
wraps all the generated INSERT
, UPDATE
, and DELETE
statements for a single call into a database transaction. If any single part of the operation fails, the entire transaction is rolled back, and the database is left in its original state. This ensures that your changes are applied atomically (all or nothing).
36. Can you explicitly control transactions in EF Core? Answer: Yes. You can use DbContext.Database.BeginTransaction()
to create a transaction object. This gives you explicit control to Commit()
or Rollback()
the transaction. This is useful when you need to coordinate operations across multiple SaveChanges()
calls or even with other non-EF Core database work.
using (var transaction = context.Database.BeginTransaction()) { try { context.Blogs.Add(new Blog { Url = "http://newblog.com" }); await context.SaveChangesAsync(); // Perform some other operation await context.Database.ExecuteSqlRawAsync("UPDATE dbo.Counters SET BlogCount = BlogCount + 1"); await transaction.CommitAsync(); } catch (Exception) { await transaction.RollbackAsync(); } }
37. What is connection resiliency and how do you configure an execution strategy? Answer: Connection resiliency is the ability for an application to automatically retry database commands that fail due to transient (temporary) errors, such as network glitches or brief database unavailability common in cloud environments.
EF Core allows you to configure an execution strategy. The strategy defines the retry logic. You configure it in OnConfiguring
or AddDbContext
.
services.AddDbContext<MyDbContext>(options => options.UseSqlServer(connectionString, sqlServerOptionsAction: sqlOptions => { sqlOptions.EnableRetryOnFailure( maxRetryCount: 5, maxDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); }));
This tells EF Core to retry a failed command up to 5 times with an exponential backoff delay.
38. What is a “property bag” entity type? Answer: This is an entity type that uses a dictionary-like object (specifically Dictionary<string, object>
) to store its property values, instead of standard .NET properties on a class. It’s a way to have a very dynamic entity schema. They are configured as shared-type entity types.
39. How would you map a C# 9 record
as an entity? Answer: Records can be used as entities. However, because they are immutable by default, you need to be careful. For EF Core to track changes, properties need setters. You can use init
-only setters. For EF Core to create instances when querying, it needs a parameterless constructor.
public record Person { public int Id { get; init; } public string Name { get; init; } // Required for EF Core materialization private Person() { } public Person(int id, string name) => (Id, Name) = (id, name); }
40. Explain the purpose of the [Keyless]
attribute. Answer: It’s a data annotation equivalent of the Fluent API’s .HasNoKey()
. It marks a class as a keyless entity type, meaning it’s intended to be used for query results that don’t have a primary key, like from a database view.
41. How does EF Core handle many-to-many relationships without an explicit join entity? Answer: Since EF Core 5.0, it supports “skip navigations.” You can define collection navigation properties on both sides of the relationship, and EF Core will create the join table in the background by convention.
public class Post { public int Id { get; set; } public string Title { get; set; } public ICollection<Tag> Tags { get; set; } // Skip navigation } public class Tag { public int Id { get; set; } public string Name { get; set; } public ICollection<Post> Posts { get; set; } // Skip navigation } // In OnModelCreating modelBuilder .Entity<Post>() .HasMany(p => p.Tags) .WithMany(t => t.Posts);
EF Core will automatically create a PostTag
join table in the database.
42. How can you configure the backing field for a property? Answer: By default, EF Core uses the auto-generated backing field for a property. You can configure a custom backing field using the Fluent API. This is useful if you want to have logic in your property getter/setter or if your field name doesn’t follow convention.
public class Blog { private string _url; public int Id { get; set; } public string Url { get { return _url; } set { _url = value.ToLower(); } // Example logic } } // In OnModelCreating modelBuilder.Entity<Blog>() .Property(b => b.Url) .HasField("_url");
43. What is table splitting? Answer: Table splitting is a technique where you map multiple entity types to a single table. This is useful when two entities are always used together and share the same primary key. For example, you could have a Product
and a ProductDetails
entity that are both stored in the Products
table.
modelBuilder.Entity<Product>(pb => { pb.ToTable("Products"); pb.HasOne(p => p.Details).WithOne(d => d.Product) .HasForeignKey<ProductDetails>(d => d.Id); }); modelBuilder.Entity<ProductDetails>(pb => { pb.ToTable("Products"); // Mapped to the same table });
44. How can you seed data in EF Core? Answer: You use the HasData
method in OnModelCreating
. This data is applied during migrations. It’s useful for lookup data or initial user accounts. The key advantage is that the data is managed along with the schema in migrations.
modelBuilder.Entity<Status>().HasData( new Status { Id = 1, Name = "Pending" }, new Status { Id = 2, Name = "Approved" }, new Status { Id = 3, Name = "Rejected" } );
45. What is the difference between Find()
and FirstOrDefault()
? Answer:
DbSet.Find(key)
: This method is specifically designed to find an entity by its primary key. It has a special behavior: it will first look for the entity in the change tracker. If the entity is already loaded and tracked, it will be returned immediately without a database query. If it’s not found in the tracker, it will then query the database.FirstOrDefault(predicate)
: This is a standard LINQ method. It always executes a query against the database. It finds the first element that satisfies the given predicate (theWhere
condition).
Use Find()
when you have the primary key and want the most efficient way to get an entity that might already be in memory.
46. How can you get the generated SQL for a LINQ query without executing it? Answer: You can use the ToQueryString()
extension method on an IQueryable
. This is extremely useful for debugging and understanding how EF Core translates your LINQ to SQL.
var query = context.Blogs.Where(b => b.Rating > 4); var sql = query.ToQueryString(); Console.WriteLine(sql);
47. What are database collations and how can you configure them in EF Core? Answer: A collation defines the rules for how string data is sorted and compared in a database (e.g., case-sensitivity, accent-sensitivity). You can configure a collation for a specific column or for the entire database.
// For the whole database modelBuilder.UseCollation("SQL_Latin1_General_CP1_CI_AS"); // For a specific property modelBuilder.Entity<User>() .Property(u => u.Username) .UseCollation("SQL_Latin1_General_CP1_CS_AS"); // Case-Sensitive
48. How do you map an entity to a table with a different name or schema? Answer: Use the ToTable("TableName", "SchemaName")
method in OnModelCreating
or the [Table("TableName", Schema = "SchemaName")]
attribute.
// Fluent API modelBuilder.Entity<Product>().ToTable("Products", schema: "inventory"); // Attribute [Table("Products", Schema = "inventory")] public class Product { /* ... */ }
49. What is a computed column? How do you map it? Answer: A computed column is a column in a database whose value is calculated from other columns in the same table. You can map a property to a computed column using HasComputedColumnSql()
.
// FullName is computed in the database modelBuilder.Entity<Person>() .Property(p => p.FullName) .HasComputedColumnSql("[LastName] + ', ' + [FirstName]");
50. How can you call a database scalar function in a LINQ query? Answer: You first need to register the function with EF Core in OnModelCreating
, then you can use it in your LINQ queries.
// 1. Define a static method stub in your code public static class MyDbFunctions { public static int WordCount(string s) => throw new NotImplementedException(); } // 2. Register it in OnModelCreating modelBuilder.HasDbFunction(typeof(MyDbFunctions) .GetMethod(nameof(MyDbFunctions.WordCount), new[] { typeof(string) })) .HasName("fn_WordCount"); // The actual DB function name // 3. Use it in a LINQ query var blogs = await context.Blogs .Where(b => MyDbFunctions.WordCount(b.Content) > 1000) .ToListAsync();
51. Explain the purpose of OnConfiguring
vs. AddDbContext
. Answer:
OnConfiguring
: This is a method you can override inside yourDbContext
class. It’s used to configure the context, most commonly to set the database provider and connection string. It’s a simple way to configure the context but tightly couples the context to its configuration.AddDbContext
/AddDbContextPool
: This is the method used in an ASP.NET Core application’sStartup.cs
orProgram.cs
to register theDbContext
with the dependency injection container. This is the preferred modern approach because it decouples the context from its configuration. The configuration (like the connection string) is provided from the outside, making the application more flexible and testable.