Entity Framework - realestateexplorer/SystemDesign GitHub Wiki
how to configure Entity Framework Core
Of course! Configuring Entity Framework Core involves a few key steps, from setting up the database connection to defining your data model.
This comprehensive guide will walk you through the entire process, from basic setup to more advanced configurations.
- DbContext Configuration: Telling EF Core which database to connect to (e.g., SQL Server, PostgreSQL, SQLite) and providing the connection string. This is typically done with Dependency Injection.
- Model Configuration: Defining your entities (tables), their properties (columns), and the relationships between them (keys, foreign keys).
- Runtime Behavior Configuration: Adjusting how EF Core behaves, such as logging, query tracking, and connection resiliency.
First, you need to install the necessary packages. You'll always need a provider package for your specific database and the design package for tooling (like migrations).
-
For SQL Server:
dotnet add package Microsoft.EntityFrameworkCore.SqlServer dotnet add package Microsoft.EntityFrameworkCore.Design
-
For PostgreSQL:
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL dotnet add package Microsoft.EntityFrameworkCore.Design
-
For SQLite:
dotnet add package Microsoft.EntityFrameworkCore.Sqlite dotnet add package Microsoft.EntityFrameworkCore.Design
-
For In-Memory (testing):
dotnet add package Microsoft.EntityFrameworkCore.InMemory
The DbContext
is the heart of EF Core. It represents a session with the database and allows you to query and save data. Your entity classes are simple C# objects (POCOs) that represent the tables in your database.
Example Entities:
// Models/Blog.cs
public class Blog
{
public int BlogId { get; set; } // Will become the Primary Key by convention
public string Url { get; set; }
public List<Post> Posts { get; set; } = new List<Post>(); // Navigation property
}
// Models/Post.cs
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int BlogId { get; set; } // Foreign Key
public Blog Blog { get; set; } // Navigation property
}
The DbContext:
using Microsoft.EntityFrameworkCore;
// Data/BlogContext.cs
public class BlogContext : DbContext
{
// The constructor is essential for dependency injection
public BlogContext(DbContextOptions<BlogContext> options) : base(options)
{
}
// Each DbSet represents a table in the database
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
// This is where you can configure the model using the Fluent API (see Step 4)
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Fluent API configuration goes here
}
}
This is the most common and recommended way to configure EF Core, especially in an ASP.NET Core application.
-
Add Connection String to
appsettings.json
:{ "ConnectionStrings": { "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=MyBlogDb;Trusted_Connection=True;" }, // ... other settings }
-
Register the DbContext in
Program.cs
(.NET 6+) orStartup.cs
:This tells your application how to create instances of your
BlogContext
.In
Program.cs
(.NET 6+ Minimal API style):using Microsoft.EntityFrameworkCore; using YourApp.Data; // Replace with the namespace of your DbContext var builder = WebApplication.CreateBuilder(args); // 1. Get the connection string var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); // 2. Add the DbContext to the services container builder.Services.AddDbContext<BlogContext>(options => options.UseSqlServer(connectionString)); // UseSqlServer, UseNpgsql, UseSqlite, etc. // ... Add other services ... var app = builder.Build(); // ... Configure the HTTP request pipeline ... app.Run();
The AddDbContext
method registers your DbContext
with a scoped lifetime by default, which is the correct lifetime for a web application (one instance per HTTP request).
EF Core uses a set of conventions to build the model, but you often need to customize it. There are two primary ways to do this: Data Annotations and the Fluent API.
Best Practice: Use the Fluent API for most configurations. It is more powerful, flexible, and keeps your entity classes clean (POCOs). Data Annotations are fine for simple validation rules.
This method is quick and easy for simple configurations.
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
public class Post
{
[Key] // Explicitly set as Primary Key
public int PostId { get; set; }
[Required] // Makes the column NOT NULL
[MaxLength(200)] // Sets the max length (e.g., NVARCHAR(200))
public string Title { get; set; }
[Column("PostBody", TypeName = "ntext")] // Custom column name and type
public string Content { get; set; }
// Foreign Key configuration
public int BlogId { get; set; }
[ForeignKey("BlogId")]
public Blog Blog { get; set; }
}
The Fluent API provides fine-grained control over your model configuration in a single, centralized place.
// Inside your BlogContext class
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Configure the Blog entity
modelBuilder.Entity<Blog>(entity =>
{
// Set the primary key
entity.HasKey(b => b.BlogId);
// Configure a property
entity.Property(b => b.Url)
.IsRequired()
.HasMaxLength(500);
// Define the one-to-many relationship with Post
entity.HasMany(b => b.Posts) // A Blog has many Posts
.WithOne(p => p.Blog) // A Post has one Blog
.HasForeignKey(p => p.BlogId); // The foreign key is BlogId on Post
});
// Configure the Post entity
modelBuilder.Entity<Post>(entity =>
{
entity.ToTable("Posts"); // Set the table name explicitly
entity.HasKey(p => p.PostId);
entity.Property(p => p.Title)
.IsRequired()
.HasMaxLength(200);
entity.Property(p => p.Content)
.HasColumnName("PostBody")
.HasColumnType("ntext");
});
}
For larger models, your OnModelCreating
method can get crowded. You can split the configuration into separate classes.
-
Create a configuration class for each entity:
// Data/Configuration/BlogConfiguration.cs public class BlogConfiguration : IEntityTypeConfiguration<Blog> { public void Configure(EntityTypeBuilder<Blog> builder) { builder.HasKey(b => b.BlogId); builder.Property(b => b.Url) .IsRequired() .HasMaxLength(500); builder.HasMany(b => b.Posts) .WithOne(p => p.Blog) .HasForeignKey(p => p.BlogId); } }
-
Apply these configurations in
OnModelCreating
:// Inside your BlogContext class protected override void OnModelCreating(ModelBuilder modelBuilder) { // Apply a single configuration modelBuilder.ApplyConfiguration(new BlogConfiguration()); // Or, apply all configurations from an assembly automatically // modelBuilder.ApplyConfigurationsFromAssembly(typeof(BlogContext).Assembly); }
You can configure additional behaviors in the AddDbContext
call.
builder.Services.AddDbContext<BlogContext>(options =>
{
options.UseSqlServer(
connectionString,
sqlServerOptions =>
{
// Enable connection resiliency for cloud databases like Azure SQL
sqlServerOptions.EnableRetryOnFailure(
maxRetryCount: 5,
maxRetryDelay: TimeSpan.FromSeconds(30),
errorNumbersToAdd: null);
});
// Log SQL queries to the console (great for debugging)
options.LogTo(Console.WriteLine, LogLevel.Information);
// For better performance in read-only scenarios, disable tracking globally
// options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
});
-
Install Packages:
Microsoft.EntityFrameworkCore.SqlServer
andMicrosoft.EntityFrameworkCore.Design
. -
Create Models: Define your C# entity classes (e.g.,
Blog
,Post
). -
Create DbContext: Create a class that inherits from
DbContext
(e.g.,BlogContext
). -
Add Connection String: Put your database connection string in
appsettings.json
. -
Configure DI: Use
builder.Services.AddDbContext<...>()
inProgram.cs
. -
Configure Model (Recommended): Use the Fluent API inside
OnModelCreating
or, even better, in separateIEntityTypeConfiguration
classes. -
Create Migrations: Run
dotnet ef migrations add InitialCreate
in the terminal. -
Apply Migrations: Run
dotnet ef database update
to create/update your database schema.
what is Fluent API
Excellent question! The Fluent API is a central concept in Entity Framework Core. It's one of the primary ways you configure your data model, and it's generally considered the most powerful and flexible method.
In short, the Fluent API is a way to configure your EF Core model using a chain of C# method calls. The name "fluent" comes from the way these methods are chained together, creating a readable, sentence-like structure.
This configuration happens inside the OnModelCreating
method of your DbContext
. You are given a ModelBuilder
object, which is the entry point for all your configurations.
Here is the basic structure:
public class BlogContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
// The Fluent API lives here!
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Example: "For the Blog entity, the primary key is the BlogId property."
modelBuilder.Entity<Blog>()
.HasKey(b => b.BlogId);
// Example: "For the Post entity, the Title property is required and has a max length of 200."
modelBuilder.Entity<Post>()
.Property(p => p.Title)
.IsRequired()
.HasMaxLength(200);
}
}
EF Core provides two ways to configure the model: Data Annotations (attributes like [Key]
, [Required]
) and the Fluent API. While Data Annotations are simpler for basic cases, the Fluent API is superior for several reasons:
- More Powerful: There are many configurations that can only be done using the Fluent API. For example, configuring shadow properties, owned entity types, or complex index definitions.
-
Keeps Models Clean (POCOs): Your entity classes (e.g.,
Blog
,Post
) remain "Plain Old C# Objects" (POCOs). They don't need any attributes or dependencies on Entity Framework. This is excellent for separating your domain logic from your persistence logic. -
Centralized Configuration: All of your database mapping logic is located in one place (
OnModelCreating
or related configuration classes). You don't have to hunt through multiple entity files to see how the database is structured. -
Better Separation of Concerns: Your domain model (
Blog.cs
) describes the business entity, while your Fluent API configuration describes how that entity is stored in the database. These are two separate concerns.
Feature | Fluent API | Data Annotations |
---|---|---|
Location |
OnModelCreating in DbContext
|
Attributes on entity classes |
Power | Very High. Can configure everything. | Limited. Covers common scenarios. |
Clean Models | Yes. Keeps entities free of EF attributes. | No. Clutters domain models. |
Centralization | Yes. All config is in one place. | No. Config is scattered across files. |
Readability | Can be very readable and "fluent". | Easy to see a single property's rules. |
Best For | All non-trivial applications. | Quick prototypes, simple validation rules. |
Let's explore some of the most common configuration tasks you'll perform with the Fluent API.
modelBuilder.Entity<Post>(entity =>
{
// Map the entity to a specific table name
entity.ToTable("BlogPosts");
// Set the primary key
entity.HasKey(p => p.PostId);
// Create an index on the Url property and make it unique
entity.HasIndex(p => p.Url).IsUnique();
});
modelBuilder.Entity<Post>(entity =>
{
// Make a property required (NOT NULL in the database)
entity.Property(p => p.Title)
.IsRequired();
// Set a maximum length (e.g., NVARCHAR(200))
entity.Property(p => p.Title)
.HasMaxLength(200);
// Change the column name and database type
entity.Property(p => p.Content)
.HasColumnName("PostBody")
.HasColumnType("ntext");
// Set a default value in the database
entity.Property(p => p.PublishedOn)
.HasDefaultValueSql("GETDATE()"); // SQL Server specific
});
Let's say we have Blog
and Post
entities. A Blog
has many Posts
, and a Post
belongs to one Blog
.
// Inside OnModelCreating...
// Configure the One-to-Many relationship
modelBuilder.Entity<Blog>()
.HasMany(b => b.Posts) // A Blog has many Posts...
.WithOne(p => p.Blog) // ...and a Post has one Blog...
.HasForeignKey(p => p.BlogId); // ...using the BlogId property as the foreign key.
This is far more explicit and powerful than trying to let conventions or annotations figure it out, especially for complex relationships.
A shadow property is a property that is not defined on your .NET entity class but exists in the EF Core model and is mapped to a database column. This is impossible with Data Annotations.
// Add a "LastUpdated" shadow property to the Post entity
modelBuilder.Entity<Post>()
.Property<DateTime>("LastUpdated");
You can then access this property through EF Core's Change Tracker API.
For larger applications, the OnModelCreating
method can become extremely large. The best practice is to separate the configuration for each entity into its own class using the IEntityTypeConfiguration<T>
interface.
-
Create a Configuration Class:
// Data/Configurations/PostConfiguration.cs public class PostConfiguration : IEntityTypeConfiguration<Post> { public void Configure(EntityTypeBuilder<Post> builder) { builder.ToTable("BlogPosts"); builder.HasKey(p => p.PostId); builder.Property(p => p.Title) .IsRequired() .HasMaxLength(200); // ... other configurations for the Post entity } }
-
Apply the Configuration in
OnModelCreating
:// Inside your DbContext's OnModelCreating method protected override void OnModelCreating(ModelBuilder modelBuilder) { // Apply a single configuration modelBuilder.ApplyConfiguration(new PostConfiguration()); modelBuilder.ApplyConfiguration(new BlogConfiguration()); // OR, even better, apply all configurations from an assembly at once // modelBuilder.ApplyConfigurationsFromAssembly(typeof(BlogContext).Assembly); }
The Fluent API is the de facto standard for configuring EF Core models in professional applications. It provides maximum power, promotes a clean domain model, and centralizes your persistence logic, making your application more maintainable and robust.
provide examples of CRUD operation for Entity Framework and Azure Cosmos DB in C#
Of course! Here are detailed examples of CRUD (Create, Read, Update, Delete) operations for Azure Cosmos DB using Entity Framework Core in C#.
This guide will walk you through setting up a simple console application to demonstrate these operations.
- .NET SDK installed (e.g., .NET 6, .NET 7, or .NET 8).
-
An Azure Cosmos DB Account.
- Make sure you are using the NoSQL API (formerly Core SQL API).
- Have your Endpoint URI and Primary Key ready.
- A code editor like Visual Studio or VS Code.
-
Create a new C# Console Application.
dotnet new console -n EfCosmosCrud cd EfCosmosCrud
-
Install the EF Core Cosmos DB provider package.
dotnet add package Microsoft.EntityFrameworkCore.Cosmos
This is the C# class that will represent the documents in your Cosmos DB container. For this example, we'll create an Order
class.
Key Points for Cosmos DB Models:
- You must have a property named
id
(lowercase) of typestring
. EF Core will map this to Cosmos DB's requiredid
field. - You must define a partition key for performance and scalability.
Create a new file named Order.cs
:
// Order.cs
using Newtonsoft.Json;
public class Order
{
// The unique ID for the document. Mapped to 'id' in Cosmos DB.
[JsonProperty("id")]
public Guid Id { get; set; }
// This property will be used as the Partition Key.
public string CustomerName { get; set; } = string.Empty;
public string ShippingAddress { get; set; } = string.Empty;
public decimal TotalPrice { get; set; }
public DateTime OrderDate { get; set; }
// We must have a ToString() to make our object readable in the console.
public override string ToString()
{
return $"Order [Id: {Id}, Customer: {CustomerName}, Price: {TotalPrice:C}, Date: {OrderDate.ToShortDateString()}]";
}
}
Note: Using [JsonProperty("id")]
on a Guid
property is a common pattern to let EF Core manage the required lowercase id
string property in the background.
The DbContext
is the bridge between your C# code and the database. It's where you configure the connection and define your data sets (DbSet<T>
).
Create a new file named OrderContext.cs
:
// OrderContext.cs
using Microsoft.EntityFrameworkCore;
public class OrderContext : DbContext
{
// Represents the collection/container of Orders in the database.
public DbSet<Order> Orders { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// --- IMPORTANT ---
// Replace with your own Cosmos DB Endpoint and Primary Key.
// In a real application, use a secure configuration method (e.g., appsettings.json, Azure Key Vault).
string endpoint = "YOUR_COSMOS_DB_ENDPOINT";
string key = "YOUR_COSMOS_DB_KEY";
string databaseName = "EFCosmosDemoDb";
optionsBuilder.UseCosmos(endpoint, key, databaseName);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Configure the Order entity
modelBuilder.Entity<Order>(entity =>
{
// Set the container name
entity.ToContainer("Orders");
// Define the partition key
entity.HasPartitionKey(o => o.CustomerName);
// By default, EF Core adds a "discriminator" property to distinguish between
// different entity types in the same container. Since we only have one type,
// we can disable it. This is a good practice for single-model containers.
entity.HasNoDiscriminator();
});
}
}
Now, let's put everything together in your Program.cs
file to perform the CRUD operations.
// Program.cs
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
using System.Threading.Tasks;
public class Program
{
public static async Task Main(string[] args)
{
Console.WriteLine("Entity Framework Core with Cosmos DB - CRUD Demo");
Console.WriteLine("==============================================");
// DbContext is a lightweight unit of work. Create and dispose it for each operation.
// The 'using' statement handles disposal automatically.
using var context = new OrderContext();
// Ensure the database and container are created. Great for demos, not for production.
// In production, you'd use Infrastructure as Code (Bicep, ARM, Terraform).
await context.Database.EnsureCreatedAsync();
Console.WriteLine("Database and container are ready.\n");
// We'll store the ID of the created order to use it in other operations.
Guid createdOrderId = Guid.Empty;
string customerName = "John Doe"; // This is our partition key value
// === 1. CREATE (Add a new Order) ===
Console.WriteLine("--- 1. CREATE Operation ---");
var newOrder = new Order
{
Id = Guid.NewGuid(),
CustomerName = customerName,
ShippingAddress = "123 Main St, Anytown, USA",
TotalPrice = 199.99m,
OrderDate = DateTime.UtcNow
};
await context.Orders.AddAsync(newOrder);
await context.SaveChangesAsync();
createdOrderId = newOrder.Id;
Console.WriteLine($"Created new order: {newOrder}\n");
// === 2. READ Operations ===
Console.WriteLine("--- 2. READ Operations ---");
// A. Read a single item (most efficient way)
// FindAsync is optimized to use the ID and Partition Key for a fast point-read.
Console.WriteLine("Reading a single order by ID and Partition Key...");
var foundOrder = await context.Orders.FindAsync(createdOrderId, customerName);
if (foundOrder != null)
{
Console.WriteLine($"Found order: {foundOrder}\n");
}
else
{
Console.WriteLine("Order not found.\n");
}
// B. Read multiple items using a LINQ query
Console.WriteLine("Reading all orders for customer 'John Doe'...");
var customerOrders = await context.Orders
.Where(o => o.CustomerName == customerName) // This query includes the partition key, making it efficient.
.ToListAsync();
Console.WriteLine($"Found {customerOrders.Count} orders for {customerName}:");
foreach (var order in customerOrders)
{
Console.WriteLine($"- {order}");
}
Console.WriteLine();
// === 3. UPDATE Operation ===
Console.WriteLine("--- 3. UPDATE Operation ---");
var orderToUpdate = await context.Orders.FindAsync(createdOrderId, customerName);
if (orderToUpdate != null)
{
Console.WriteLine($"Updating order {orderToUpdate.Id}...");
orderToUpdate.ShippingAddress = "456 Oak Ave, Sometown, USA"; // Change a property
// EF Core's change tracker automatically detects the modification.
await context.SaveChangesAsync(); // This single call persists the change.
Console.WriteLine($"Updated order: {orderToUpdate}\n");
}
// === 4. DELETE Operation ===
Console.WriteLine("--- 4. DELETE Operation ---");
var orderToDelete = await context.Orders.FindAsync(createdOrderId, customerName);
if (orderToDelete != null)
{
Console.WriteLine($"Deleting order {orderToDelete.Id}...");
context.Orders.Remove(orderToDelete); // Mark the entity for deletion.
await context.SaveChangesAsync(); // Commit the deletion to the database.
Console.WriteLine("Order deleted successfully.\n");
}
// Verify deletion
var deletedOrder = await context.Orders.FindAsync(createdOrderId, customerName);
Console.WriteLine(deletedOrder == null
? "Verification successful: Order no longer exists."
: "Verification failed: Order still exists.");
}
}
- Replace
"YOUR_COSMOS_DB_ENDPOINT"
and"YOUR_COSMOS_DB_KEY"
inOrderContext.cs
with your actual credentials. - Open your terminal in the project directory.
- Run the application:
dotnet run
You will see a console output that walks through each CRUD step, confirming the creation, reading, updating, and final deletion of the order document in your Cosmos DB container.
Entity Framework Core with Cosmos DB - CRUD Demo
==============================================
Database and container are ready.
--- 1. CREATE Operation ---
Created new order: Order [Id: a1b2c3d4..., Customer: John Doe, Price: $199.99, Date: 10/27/2023]
--- 2. READ Operations ---
Reading a single order by ID and Partition Key...
Found order: Order [Id: a1b2c3d4..., Customer: John Doe, Price: $199.99, Date: 10/27/2023]
Reading all orders for customer 'John Doe'...
Found 1 orders for John Doe:
- Order [Id: a1b2c3d4..., Customer: John Doe, Price: $199.99, Date: 10/27/2023]
--- 3. UPDATE Operation ---
Updating order a1b2c3d4...
Updated order: Order [Id: a1b2c3d4..., Customer: John Doe, Price: $199.99, Date: 10/27/2023]
--- 4. DELETE Operation ---
Deleting order a1b2c3d4...
Order deleted successfully.
Verification successful: Order no longer exists.
provide examples of CRUD operation for Entity Framework and Azure Cosmos DB in C#
Of course! Here are detailed examples of CRUD (Create, Read, Update, Delete) operations for Azure Cosmos DB using Entity Framework Core in C#.
This guide will walk you through setting up a simple console application to demonstrate these operations.
- .NET SDK installed (e.g., .NET 6, .NET 7, or .NET 8).
-
An Azure Cosmos DB Account.
- Make sure you are using the NoSQL API (formerly Core SQL API).
- Have your Endpoint URI and Primary Key ready.
- A code editor like Visual Studio or VS Code.
-
Create a new C# Console Application.
dotnet new console -n EfCosmosCrud cd EfCosmosCrud
-
Install the EF Core Cosmos DB provider package.
dotnet add package Microsoft.EntityFrameworkCore.Cosmos
This is the C# class that will represent the documents in your Cosmos DB container. For this example, we'll create an Order
class.
Key Points for Cosmos DB Models:
- You must have a property named
id
(lowercase) of typestring
. EF Core will map this to Cosmos DB's requiredid
field. - You must define a partition key for performance and scalability.
Create a new file named Order.cs
:
// Order.cs
using Newtonsoft.Json;
public class Order
{
// The unique ID for the document. Mapped to 'id' in Cosmos DB.
[JsonProperty("id")]
public Guid Id { get; set; }
// This property will be used as the Partition Key.
public string CustomerName { get; set; } = string.Empty;
public string ShippingAddress { get; set; } = string.Empty;
public decimal TotalPrice { get; set; }
public DateTime OrderDate { get; set; }
// We must have a ToString() to make our object readable in the console.
public override string ToString()
{
return $"Order [Id: {Id}, Customer: {CustomerName}, Price: {TotalPrice:C}, Date: {OrderDate.ToShortDateString()}]";
}
}
Note: Using [JsonProperty("id")]
on a Guid
property is a common pattern to let EF Core manage the required lowercase id
string property in the background.
The DbContext
is the bridge between your C# code and the database. It's where you configure the connection and define your data sets (DbSet<T>
).
Create a new file named OrderContext.cs
:
// OrderContext.cs
using Microsoft.EntityFrameworkCore;
public class OrderContext : DbContext
{
// Represents the collection/container of Orders in the database.
public DbSet<Order> Orders { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// --- IMPORTANT ---
// Replace with your own Cosmos DB Endpoint and Primary Key.
// In a real application, use a secure configuration method (e.g., appsettings.json, Azure Key Vault).
string endpoint = "YOUR_COSMOS_DB_ENDPOINT";
string key = "YOUR_COSMOS_DB_KEY";
string databaseName = "EFCosmosDemoDb";
optionsBuilder.UseCosmos(endpoint, key, databaseName);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Configure the Order entity
modelBuilder.Entity<Order>(entity =>
{
// Set the container name
entity.ToContainer("Orders");
// Define the partition key
entity.HasPartitionKey(o => o.CustomerName);
// By default, EF Core adds a "discriminator" property to distinguish between
// different entity types in the same container. Since we only have one type,
// we can disable it. This is a good practice for single-model containers.
entity.HasNoDiscriminator();
});
}
}
Now, let's put everything together in your Program.cs
file to perform the CRUD operations.
// Program.cs
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
using System.Threading.Tasks;
public class Program
{
public static async Task Main(string[] args)
{
Console.WriteLine("Entity Framework Core with Cosmos DB - CRUD Demo");
Console.WriteLine("==============================================");
// DbContext is a lightweight unit of work. Create and dispose it for each operation.
// The 'using' statement handles disposal automatically.
using var context = new OrderContext();
// Ensure the database and container are created. Great for demos, not for production.
// In production, you'd use Infrastructure as Code (Bicep, ARM, Terraform).
await context.Database.EnsureCreatedAsync();
Console.WriteLine("Database and container are ready.\n");
// We'll store the ID of the created order to use it in other operations.
Guid createdOrderId = Guid.Empty;
string customerName = "John Doe"; // This is our partition key value
// === 1. CREATE (Add a new Order) ===
Console.WriteLine("--- 1. CREATE Operation ---");
var newOrder = new Order
{
Id = Guid.NewGuid(),
CustomerName = customerName,
ShippingAddress = "123 Main St, Anytown, USA",
TotalPrice = 199.99m,
OrderDate = DateTime.UtcNow
};
await context.Orders.AddAsync(newOrder);
await context.SaveChangesAsync();
createdOrderId = newOrder.Id;
Console.WriteLine($"Created new order: {newOrder}\n");
// === 2. READ Operations ===
Console.WriteLine("--- 2. READ Operations ---");
// A. Read a single item (most efficient way)
// FindAsync is optimized to use the ID and Partition Key for a fast point-read.
Console.WriteLine("Reading a single order by ID and Partition Key...");
var foundOrder = await context.Orders.FindAsync(createdOrderId, customerName);
if (foundOrder != null)
{
Console.WriteLine($"Found order: {foundOrder}\n");
}
else
{
Console.WriteLine("Order not found.\n");
}
// B. Read multiple items using a LINQ query
Console.WriteLine("Reading all orders for customer 'John Doe'...");
var customerOrders = await context.Orders
.Where(o => o.CustomerName == customerName) // This query includes the partition key, making it efficient.
.ToListAsync();
Console.WriteLine($"Found {customerOrders.Count} orders for {customerName}:");
foreach (var order in customerOrders)
{
Console.WriteLine($"- {order}");
}
Console.WriteLine();
// === 3. UPDATE Operation ===
Console.WriteLine("--- 3. UPDATE Operation ---");
var orderToUpdate = await context.Orders.FindAsync(createdOrderId, customerName);
if (orderToUpdate != null)
{
Console.WriteLine($"Updating order {orderToUpdate.Id}...");
orderToUpdate.ShippingAddress = "456 Oak Ave, Sometown, USA"; // Change a property
// EF Core's change tracker automatically detects the modification.
await context.SaveChangesAsync(); // This single call persists the change.
Console.WriteLine($"Updated order: {orderToUpdate}\n");
}
// === 4. DELETE Operation ===
Console.WriteLine("--- 4. DELETE Operation ---");
var orderToDelete = await context.Orders.FindAsync(createdOrderId, customerName);
if (orderToDelete != null)
{
Console.WriteLine($"Deleting order {orderToDelete.Id}...");
context.Orders.Remove(orderToDelete); // Mark the entity for deletion.
await context.SaveChangesAsync(); // Commit the deletion to the database.
Console.WriteLine("Order deleted successfully.\n");
}
// Verify deletion
var deletedOrder = await context.Orders.FindAsync(createdOrderId, customerName);
Console.WriteLine(deletedOrder == null
? "Verification successful: Order no longer exists."
: "Verification failed: Order still exists.");
}
}
- Replace
"YOUR_COSMOS_DB_ENDPOINT"
and"YOUR_COSMOS_DB_KEY"
inOrderContext.cs
with your actual credentials. - Open your terminal in the project directory.
- Run the application:
dotnet run
You will see a console output that walks through each CRUD step, confirming the creation, reading, updating, and final deletion of the order document in your Cosmos DB container.
Entity Framework Core with Cosmos DB - CRUD Demo
==============================================
Database and container are ready.
--- 1. CREATE Operation ---
Created new order: Order [Id: a1b2c3d4..., Customer: John Doe, Price: $199.99, Date: 10/27/2023]
--- 2. READ Operations ---
Reading a single order by ID and Partition Key...
Found order: Order [Id: a1b2c3d4..., Customer: John Doe, Price: $199.99, Date: 10/27/2023]
Reading all orders for customer 'John Doe'...
Found 1 orders for John Doe:
- Order [Id: a1b2c3d4..., Customer: John Doe, Price: $199.99, Date: 10/27/2023]
--- 3. UPDATE Operation ---
Updating order a1b2c3d4...
Updated order: Order [Id: a1b2c3d4..., Customer: John Doe, Price: $199.99, Date: 10/27/2023]
--- 4. DELETE Operation ---
Deleting order a1b2c3d4...
Order deleted successfully.
Verification successful: Order no longer exists.
explain NoSQL API in examples above
Excellent question. This is a key concept to understand.
The NoSQL API (formerly called the Core SQL API) in Azure Cosmos DB is the default and most widely used interface. The name can be a bit confusing. Let's break down what it means and how it's reflected in the C# examples.
Think of it as "Not only SQL, with a SQL-like query language."
It's a document database. This means:
- Data is stored in JSON documents: Instead of rows and columns in a table, your data is stored as flexible JSON objects.
- Schema-on-read: Unlike a traditional SQL database where you must define a rigid table schema upfront, a document database is more flexible. You can have documents with different structures within the same collection (called a "container").
- SQL-like query language: You can query these JSON documents using a familiar, SQL-like syntax. This is the "SQL" part of the original name and a major selling point.
Let's connect these concepts directly to the C# code you saw.
The Order
class you defined is a direct representation of the structure of the JSON document that will be stored in Cosmos DB.
Your C# Class (Order.cs
):
public class Order
{
[JsonProperty("id")]
public Guid Id { get; set; } // Will become the 'id' field in JSON
public string CustomerName { get; set; }
public string ShippingAddress { get; set; }
public decimal TotalPrice { get; set; }
public DateTime OrderDate { get; set; }
}
What EF Core and Cosmos DB store (the JSON Document): When you save an instance of this class, it gets serialized into a JSON document that looks like this in the database:
{
"id": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
"CustomerName": "John Doe",
"ShippingAddress": "123 Main St, Anytown, USA",
"TotalPrice": 199.99,
"OrderDate": "2023-10-27T10:30:00Z",
"_rid": "...", // System-generated properties
"_self": "...",
"_etag": "...",
"_attachments": "...",
"_ts": 1698394200
}
The NoSQL API is all about managing these JSON documents.
In the NoSQL world, you don't have "tables"; you have "collections" or, in Cosmos DB's terminology, containers. A container is simply a logical grouping of your JSON documents.
Your C# Code (OrderContext.cs
):
public DbSet<Order> Orders { get; set; } // This represents the container
// ... in OnModelCreating ...
modelBuilder.Entity<Order>().ToContainer("Orders"); // Explicitly names the container
This code tells EF Core: "I want to work with a container named Orders
, and the documents inside it will have the structure of my Order
class."
This is the most powerful feature. You get to write expressive, type-safe C# LINQ queries, and EF Core translates them into the Cosmos DB SQL-like query language for you.
Your C# LINQ Query (Program.cs
):
var customerOrders = await context.Orders
.Where(o => o.CustomerName == "John Doe" && o.TotalPrice > 100)
.ToListAsync();
What EF Core translates this into (Cosmos DB SQL Query): Behind the scenes, EF Core generates a query string that it sends to the NoSQL API. The query looks very similar to standard SQL:
SELECT *
FROM c
WHERE c.CustomerName = 'John Doe' AND c.TotalPrice > 100
(Here, c
is just an alias for the documents in the container, similar to how you'd alias a table in SQL).
The NoSQL API engine executes this query against the JSON documents in your container and returns the results, which EF Core then deserializes back into a List<Order>
.
The NoSQL API model is built for massive scale, and two properties are critical for this:
-
id
: A unique identifier for a document within a logical partition. The[JsonProperty("id")]
attribute maps yourGuid
property to this required lowercase string field. - Partition Key: This is the most important property for performance and scalability. It determines how Cosmos DB physically distributes your data across different servers.
Your C# Code (OrderContext.cs
):
// Defines which property in your document is the partition key
entity.HasPartitionKey(o => o.CustomerName);
By choosing CustomerName
as the partition key, you are telling Cosmos DB to group all orders for the same customer together physically.
Why this matters for the API:
-
Efficient Reads: When you perform a "point read" using both the
id
and the partition key value, it's the fastest and cheapest operation possible. This is whatFindAsync
does.// This is a highly efficient "point read" var foundOrder = await context.Orders.FindAsync(createdOrderId, customerName);
-
Efficient Queries: Queries that include the partition key in the
WHERE
clause (like our LINQ example) are very efficient because Cosmos DB knows exactly which physical server(s) to search. These are called "in-partition" queries. -
Cross-Partition Queries: If you were to query without the partition key (e.g.,
context.Orders.Where(o => o.TotalPrice > 500)
), Cosmos DB would have to "fan out" the query to all physical partitions, making it slower and more expensive.
C# / EF Core Concept | Cosmos DB NoSQL API Concept | Explanation |
---|---|---|
class Order { ... } |
JSON Document | Your C# object model defines the structure of the data. |
DbSet<Order> |
Container | A collection that holds all your Order JSON documents. |
Where(o => ...) |
SQL-like Query | Your LINQ query is translated into a SQL-like query string. |
[JsonProperty("id")] |
id property |
The unique identifier for a document within a partition. |
HasPartitionKey(...) |
Partition Key | The property used to distribute data for scale and performance. |
In short, the NoSQL API provides a document-oriented database model that you can interact with using a familiar SQL-like syntax. The Entity Framework Core provider acts as a brilliant abstraction layer, allowing you to work with this model using standard C# classes and LINQ queries.
explain NoSQL API in examples above
Excellent question. This is a key concept to understand.
The NoSQL API (formerly called the Core SQL API) in Azure Cosmos DB is the default and most widely used interface. The name can be a bit confusing. Let's break down what it means and how it's reflected in the C# examples.
Think of it as "Not only SQL, with a SQL-like query language."
It's a document database. This means:
- Data is stored in JSON documents: Instead of rows and columns in a table, your data is stored as flexible JSON objects.
- Schema-on-read: Unlike a traditional SQL database where you must define a rigid table schema upfront, a document database is more flexible. You can have documents with different structures within the same collection (called a "container").
- SQL-like query language: You can query these JSON documents using a familiar, SQL-like syntax. This is the "SQL" part of the original name and a major selling point.
Let's connect these concepts directly to the C# code you saw.
The Order
class you defined is a direct representation of the structure of the JSON document that will be stored in Cosmos DB.
Your C# Class (Order.cs
):
public class Order
{
[JsonProperty("id")]
public Guid Id { get; set; } // Will become the 'id' field in JSON
public string CustomerName { get; set; }
public string ShippingAddress { get; set; }
public decimal TotalPrice { get; set; }
public DateTime OrderDate { get; set; }
}
What EF Core and Cosmos DB store (the JSON Document): When you save an instance of this class, it gets serialized into a JSON document that looks like this in the database:
{
"id": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
"CustomerName": "John Doe",
"ShippingAddress": "123 Main St, Anytown, USA",
"TotalPrice": 199.99,
"OrderDate": "2023-10-27T10:30:00Z",
"_rid": "...", // System-generated properties
"_self": "...",
"_etag": "...",
"_attachments": "...",
"_ts": 1698394200
}
The NoSQL API is all about managing these JSON documents.
In the NoSQL world, you don't have "tables"; you have "collections" or, in Cosmos DB's terminology, containers. A container is simply a logical grouping of your JSON documents.
Your C# Code (OrderContext.cs
):
public DbSet<Order> Orders { get; set; } // This represents the container
// ... in OnModelCreating ...
modelBuilder.Entity<Order>().ToContainer("Orders"); // Explicitly names the container
This code tells EF Core: "I want to work with a container named Orders
, and the documents inside it will have the structure of my Order
class."
This is the most powerful feature. You get to write expressive, type-safe C# LINQ queries, and EF Core translates them into the Cosmos DB SQL-like query language for you.
Your C# LINQ Query (Program.cs
):
var customerOrders = await context.Orders
.Where(o => o.CustomerName == "John Doe" && o.TotalPrice > 100)
.ToListAsync();
What EF Core translates this into (Cosmos DB SQL Query): Behind the scenes, EF Core generates a query string that it sends to the NoSQL API. The query looks very similar to standard SQL:
SELECT *
FROM c
WHERE c.CustomerName = 'John Doe' AND c.TotalPrice > 100
(Here, c
is just an alias for the documents in the container, similar to how you'd alias a table in SQL).
The NoSQL API engine executes this query against the JSON documents in your container and returns the results, which EF Core then deserializes back into a List<Order>
.
The NoSQL API model is built for massive scale, and two properties are critical for this:
-
id
: A unique identifier for a document within a logical partition. The[JsonProperty("id")]
attribute maps yourGuid
property to this required lowercase string field. - Partition Key: This is the most important property for performance and scalability. It determines how Cosmos DB physically distributes your data across different servers.
Your C# Code (OrderContext.cs
):
// Defines which property in your document is the partition key
entity.HasPartitionKey(o => o.CustomerName);
By choosing CustomerName
as the partition key, you are telling Cosmos DB to group all orders for the same customer together physically.
Why this matters for the API:
-
Efficient Reads: When you perform a "point read" using both the
id
and the partition key value, it's the fastest and cheapest operation possible. This is whatFindAsync
does.// This is a highly efficient "point read" var foundOrder = await context.Orders.FindAsync(createdOrderId, customerName);
-
Efficient Queries: Queries that include the partition key in the
WHERE
clause (like our LINQ example) are very efficient because Cosmos DB knows exactly which physical server(s) to search. These are called "in-partition" queries. -
Cross-Partition Queries: If you were to query without the partition key (e.g.,
context.Orders.Where(o => o.TotalPrice > 500)
), Cosmos DB would have to "fan out" the query to all physical partitions, making it slower and more expensive.
C# / EF Core Concept | Cosmos DB NoSQL API Concept | Explanation |
---|---|---|
class Order { ... } |
JSON Document | Your C# object model defines the structure of the data. |
DbSet<Order> |
Container | A collection that holds all your Order JSON documents. |
Where(o => ...) |
SQL-like Query | Your LINQ query is translated into a SQL-like query string. |
[JsonProperty("id")] |
id property |
The unique identifier for a document within a partition. |
HasPartitionKey(...) |
Partition Key | The property used to distribute data for scale and performance. |
In short, the NoSQL API provides a document-oriented database model that you can interact with using a familiar SQL-like syntax. The Entity Framework Core provider acts as a brilliant abstraction layer, allowing you to work with this model using standard C# classes and LINQ queries.