SQLite Entity Framework (EF) makes database operations more natural for .NET developers by providing an object-oriented way to work with data. When combined with SQLite, it creates a lightweight, file-based database solution that works well for applications of various sizes.
SQLite Entity Framework provides developers with a robust ORM solution that requires minimal setup, supports code-first and database-first approaches, and works seamlessly with .NET applications while maintaining high performance for local data storage needs.
Let’s explore how to integrate and use SQLite with Entity Framework in practical applications.
Getting Started with SQLite and Entity Framework
Setup and Installation
First, add the required NuGet packages to your .NET project:
// Using Package Manager Console
Install-Package Microsoft.EntityFrameworkCore.Sqlite
Install-Package Microsoft.EntityFrameworkCore.Tools
Creating Your First DbContext
The DbContext class serves as the primary way to interact with your database:
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
public class RetailContext : DbContext
{
public DbSet<Product> Products { get; set; }
public DbSet<Category> Categories { get; set; }
public DbSet<Customer> Customers { get; set; }
public DbSet<Order> Orders { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlite("Data Source=retail.db");
}
}
public class Product
{
public int ProductId { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public int CategoryId { get; set; }
public Category Category { get; set; }
}
public class Category
{
public int CategoryId { get; set; }
public string Name { get; set; }
public List<Product> Products { get; set; }
}
public class Customer
{
public int CustomerId { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public List<Order> Orders { get; set; }
}
public class Order
{
public int OrderId { get; set; }
public DateTime OrderDate { get; set; }
public int CustomerId { get; set; }
public Customer Customer { get; set; }
public decimal TotalAmount { get; set; }
}
Creating the Database
Generate and run the migration to create your database:
// Using Package Manager Console
Add-Migration InitialCreate
Update-Database
Basic CRUD Operations
Creating Records
using (var context = new RetailContext())
{
// Add categories
var electronics = new Category { Name = "Electronics" };
var clothing = new Category { Name = "Clothing" };
context.Categories.AddRange(electronics, clothing);
context.SaveChanges();
// Add products
var laptop = new Product {
Name = "Laptop",
Price = 899.99m,
CategoryId = electronics.CategoryId
};
var tShirt = new Product {
Name = "T-Shirt",
Price = 19.99m,
CategoryId = clothing.CategoryId
};
context.Products.AddRange(laptop, tShirt);
// Add a customer
var customer = new Customer {
Name = "Alex Smith",
Email = "[email protected]"
};
context.Customers.Add(customer);
context.SaveChanges();
// Add an order
var order = new Order {
OrderDate = DateTime.Now,
CustomerId = customer.CustomerId,
TotalAmount = laptop.Price
};
context.Orders.Add(order);
context.SaveChanges();
}
Sample Output:
Added 2 categories
Added 2 products
Added 1 customer
Added 1 order
Reading Records
using (var context = new RetailContext())
{
// Get all products
var allProducts = context.Products.ToList();
Console.WriteLine("All Products:");
foreach (var p in allProducts)
{
Console.WriteLine($"- {p.Name}: ${p.Price}");
}
// Get products with their categories
var productsWithCategories = context.Products
.Include(p => p.Category)
.ToList();
Console.WriteLine("\nProducts with Categories:");
foreach (var p in productsWithCategories)
{
Console.WriteLine($"- {p.Name} (Category: {p.Category.Name})");
}
// Get orders with customer information
var ordersWithCustomers = context.Orders
.Include(o => o.Customer)
.ToList();
Console.WriteLine("\nOrders:");
foreach (var o in ordersWithCustomers)
{
Console.WriteLine($"- Order #{o.OrderId} by {o.Customer.Name} for ${o.TotalAmount}");
}
}
Sample Output:
All Products:
- Laptop: $899.99
- T-Shirt: $19.99
Products with Categories:
- Laptop (Category: Electronics)
- T-Shirt (Category: Clothing)
Orders:
- Order #1 by Alex Smith for $899.99
Updating Records
using (var context = new RetailContext())
{
// Find a product
var laptop = context.Products
.FirstOrDefault(p => p.Name == "Laptop");
if (laptop != null)
{
// Update the price
laptop.Price = 849.99m;
context.SaveChanges();
Console.WriteLine($"Updated {laptop.Name} price to ${laptop.Price}");
}
// Update multiple records
var electronics = context.Categories
.Include(c => c.Products)
.FirstOrDefault(c => c.Name == "Electronics");
if (electronics != null && electronics.Products != null)
{
foreach (var product in electronics.Products)
{
// Apply 10% discount on all electronics
product.Price = product.Price * 0.9m;
}
context.SaveChanges();
Console.WriteLine("Applied 10% discount to all electronics");
}
}
Sample Output:
Updated Laptop price to $849.99
Applied 10% discount to all electronics
Deleting Records
using (var context = new RetailContext())
{
// Find a product to delete
var tShirt = context.Products
.FirstOrDefault(p => p.Name == "T-Shirt");
if (tShirt != null)
{
context.Products.Remove(tShirt);
context.SaveChanges();
Console.WriteLine($"Deleted product: {tShirt.Name}");
}
}
Sample Output:
Deleted product: T-Shirt
Advanced Features
Working with Migrations
Managing schema changes:
// Add a new property to Product
public class Product
{
// Existing properties...
public int StockQuantity { get; set; }
}
// In Package Manager Console
Add-Migration AddProductStock
Update-Database
Querying with LINQ
using (var context = new RetailContext())
{
// Get products with price > $500
var expensiveProducts = context.Products
.Where(p => p.Price > 500)
.OrderByDescending(p => p.Price)
.ToList();
Console.WriteLine("Expensive Products:");
foreach (var p in expensiveProducts)
{
Console.WriteLine($"- {p.Name}: ${p.Price}");
}
// Group products by category
var productsByCategory = context.Products
.Include(p => p.Category)
.GroupBy(p => p.Category.Name)
.Select(g => new {
CategoryName = g.Key,
ProductCount = g.Count(),
AveragePrice = g.Average(p => p.Price)
})
.ToList();
Console.WriteLine("\nProduct Stats by Category:");
foreach (var group in productsByCategory)
{
Console.WriteLine($"- {group.CategoryName}: {group.ProductCount} products, Avg price: ${group.AveragePrice:F2}");
}
}
Sample Output:
Expensive Products:
- Laptop: $764.99
Product Stats by Category:
- Electronics: 1 products, Avg price: $764.99
Real-World Example: Retail Analytics Application
Let’s build a more complete example focusing on a retail analytics scenario:
// Add sales data model
public class SalesRecord
{
public int SalesRecordId { get; set; }
public int ProductId { get; set; }
public Product Product { get; set; }
public int Quantity { get; set; }
public decimal SalePrice { get; set; }
public DateTime SaleDate { get; set; }
public string Region { get; set; }
}
// Update context
public class RetailContext : DbContext
{
// Existing DbSets...
public DbSet<SalesRecord> SalesRecords { get; set; }
// Configuration...
}
Seeding Sales Data
using (var context = new RetailContext())
{
// Get products
var products = context.Products.ToList();
if (products.Count > 0)
{
var random = new Random();
var regions = new[] { "North", "South", "East", "West" };
var salesRecords = new List<SalesRecord>();
// Generate 100 random sales records
for (int i = 0; i < 100; i++)
{
var product = products[random.Next(products.Count)];
var salesRecord = new SalesRecord
{
ProductId = product.ProductId,
Quantity = random.Next(1, 10),
SalePrice = product.Price * (1 - (decimal)random.NextDouble() * 0.2m), // Random discount up to 20%
SaleDate = DateTime.Now.AddDays(-random.Next(1, 60)), // Random date in the past 60 days
Region = regions[random.Next(regions.Length)]
};
salesRecords.Add(salesRecord);
}
context.SalesRecords.AddRange(salesRecords);
context.SaveChanges();
Console.WriteLine($"Added {salesRecords.Count} sales records");
}
}
Running Analytics Queries
using (var context = new RetailContext())
{
// Sales by region
var salesByRegion = context.SalesRecords
.GroupBy(s => s.Region)
.Select(g => new {
Region = g.Key,
TotalSales = g.Sum(s => s.Quantity * s.SalePrice),
UnitsSold = g.Sum(s => s.Quantity)
})
.OrderByDescending(r => r.TotalSales)
.ToList();
Console.WriteLine("Sales by Region:");
foreach (var region in salesByRegion)
{
Console.WriteLine($"- {region.Region}: ${region.TotalSales:F2} ({region.UnitsSold} units)");
}
// Sales trend by month
var salesByMonth = context.SalesRecords
.GroupBy(s => new { Month = s.SaleDate.Month, Year = s.SaleDate.Year })
.Select(g => new {
Period = $"{g.Key.Year}-{g.Key.Month:D2}",
TotalSales = g.Sum(s => s.Quantity * s.SalePrice)
})
.OrderBy(s => s.Period)
.ToList();
Console.WriteLine("\nSales Trend by Month:");
foreach (var month in salesByMonth)
{
Console.WriteLine($"- {month.Period}: ${month.TotalSales:F2}");
}
// Top 5 best-selling products
var topProducts = context.SalesRecords
.GroupBy(s => s.ProductId)
.Select(g => new {
ProductId = g.Key,
UnitsSold = g.Sum(s => s.Quantity),
Revenue = g.Sum(s => s.Quantity * s.SalePrice)
})
.OrderByDescending(p => p.UnitsSold)
.Take(5)
.Join(
context.Products,
s => s.ProductId,
p => p.ProductId,
(s, p) => new {
ProductName = p.Name,
UnitsSold = s.UnitsSold,
Revenue = s.Revenue
}
)
.ToList();
Console.WriteLine("\nTop 5 Best-Selling Products:");
foreach (var product in topProducts)
{
Console.WriteLine($"- {product.ProductName}: {product.UnitsSold} units (${product.Revenue:F2})");
}
}
Sample Output:
Sales by Region:
- West: $18,476.32 (241 units)
- East: $15,921.88 (207 units)
- North: $12,843.56 (165 units)
- South: $11,267.24 (147 units)
Sales Trend by Month:
- 2025-02: $19,823.45
- 2025-03: $24,876.32
- 2025-04: $13,809.23
Top 5 Best-Selling Products:
- Laptop: 183 units ($137,546.87)
- Smartphone: 159 units ($95,281.54)
- Tablet: 143 units ($64,307.28)
- Wireless Headphones: 118 units ($14,051.22)
- External SSD: 98 units ($8,624.59)
Configuration Options Comparison
Feature | Code-First Approach | Database-First Approach |
---|---|---|
Schema Definition | Defined in C# classes | Extracted from existing database |
Migration Control | Explicit migrations with code | Auto-generated from database changes |
Development Speed | Faster for new projects | Faster for existing databases |
Version Control | Schema stored in code | Requires extra steps to track schema |
Learning Curve | Lower for .NET developers | Requires SQL knowledge |
Connection String | "Data Source=retail.db" | "Data Source=retail.db" |
Setup Complexity | Lower | Higher |
Recommended For | New applications, small to medium size | Legacy integration, large databases |
Performance Optimization
SQLite Entity Framework works well for many applications, but here are some optimization techniques:
// Disable change tracking for read-only operations
using (var context = new RetailContext())
{
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
var products = context.Products.ToList();
Console.WriteLine($"Retrieved {products.Count} products with tracking disabled");
}
// Batching multiple operations
using (var context = new RetailContext())
{
// Add 1000 products in a single SaveChanges call
for (int i = 1; i <= 1000; i++)
{
context.Products.Add(new Product
{
Name = $"Batch Product {i}",
Price = 9.99m,
CategoryId = 1
});
}
var start = DateTime.Now;
context.SaveChanges();
var end = DateTime.Now;
Console.WriteLine($"Added 1000 products in {(end - start).TotalMilliseconds:F2} ms");
}
Sample Output:
Retrieved 1002 products with tracking disabled
Added 1000 products in 123.45 ms
Common Issues and Solutions
Connection Management
// Proper connection management with using statement
using (var context = new RetailContext())
{
// Operations here...
}
// Connection automatically disposed
// With dependency injection (ASP.NET Core)
services.AddDbContext<RetailContext>(options =>
options.UseSqlite(Configuration.GetConnectionString("RetailDatabase")));
Concurrency Handling
public class Product
{
// Existing properties...
[Timestamp]
public byte[] RowVersion { get; set; }
}
// Using concurrency tokens
try
{
using (var context = new RetailContext())
{
var product = context.Products.Find(1);
product.Price = 999.99m;
context.SaveChanges();
}
}
catch (DbUpdateConcurrencyException ex)
{
Console.WriteLine("Concurrency conflict detected. The record was modified by another user.");
}
Conclusion
SQLite Entity Framework combines the simplicity of SQLite with the power of Entity Framework to create a practical data access solution. Through practical examples from a retail database, we’ve seen how to set up the database, perform CRUD operations, run analytics queries, and optimize performance.
For projects that need a lightweight database with minimal setup, SQLite Entity Framework delivers an excellent balance of features, performance, and ease of use. Whether building a desktop application, a simple web app, or a prototype before moving to a larger database system, the combination provides a solid foundation for development.