Initial commit

This commit is contained in:
Mark J Price 2022-03-05 15:45:55 +00:00
parent 01d6ccf414
commit dd097904c2
54 changed files with 37154 additions and 0 deletions

View file

@ -0,0 +1,24 @@
using System.ComponentModel.DataAnnotations.Schema; // [Column]
namespace Packt.Shared;
public class Category
{
// these properties map to columns in the database
public int CategoryId { get; set; }
public string? CategoryName { get; set; }
[Column(TypeName = "ntext")]
public string? Description { get; set; }
// defines a navigation property for related rows
public virtual ICollection<Product> Products { get; set; }
public Category()
{
// to enable developers to add products to a Category we must
// initialize the navigation property to an empty collection
Products = new HashSet<Product>();
}
}

View file

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Using Include="System.Console" Static="true" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.0-*" />
</ItemGroup>
<ItemGroup>
<None Update="Northwind.db">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View file

@ -0,0 +1,47 @@
using Microsoft.EntityFrameworkCore; // DbContext, DbContextOptionsBuilder
using Microsoft.EntityFrameworkCore.Diagnostics;
namespace Packt.Shared;
// this manages the connection to the database
public class Northwind : DbContext
{
// these properties map to tables in the database
public DbSet<Category>? Categories { get; set; }
public DbSet<Product>? Products { get; set; }
protected override void OnConfiguring(
DbContextOptionsBuilder optionsBuilder)
{
string path = Path.Combine(
Environment.CurrentDirectory, "Northwind.db");
string connection = $"Filename={path}";
ConsoleColor previousColor = ForegroundColor;
ForegroundColor = ConsoleColor.DarkYellow;
WriteLine($"Connection: {connection}");
ForegroundColor = previousColor;
optionsBuilder.UseSqlite(connection);
}
protected override void OnModelCreating(
ModelBuilder modelBuilder)
{
// example of using Fluent API instead of attributes
// to limit the length of a category name to 15
modelBuilder.Entity<Category>()
.Property(category => category.CategoryName)
.IsRequired() // NOT NULL
.HasMaxLength(15);
if (Database.ProviderName?.Contains("Sqlite") ?? false)
{
// added to "fix" the lack of decimal support in SQLite
modelBuilder.Entity<Product>()
.Property(product => product.Cost)
.HasConversion<double>();
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,26 @@
using System.ComponentModel.DataAnnotations; // [Required], [StringLength]
using System.ComponentModel.DataAnnotations.Schema; // [Column]
namespace Packt.Shared;
public class Product
{
public int ProductId { get; set; } // primary key
[Required]
[StringLength(40)]
public string ProductName { get; set; } = null!;
[Column("UnitPrice", TypeName = "money")]
public decimal? Cost { get; set; } // property name != column name
[Column("UnitsInStock")]
public short? Stock { get; set; }
public bool Discontinued { get; set; }
// these two define the foreign key relationship
// to the Categories table
public int CategoryId { get; set; }
public virtual Category Category { get; set; } = null!;
}

View file

@ -0,0 +1,163 @@
using System.Xml; // XmlWriter
using System.Text.Json; // Utf8JsonWriter, JsonWriterOptions
using Packt.Shared; // Category, Product
using static System.IO.Path;
using static System.Environment;
// we want to easily show the difference between outputting
// XML using elements or attributes so we will define a
// delegate to reference the two different methods.
delegate void WriteDataDelegate(string name, string? value);
partial class Program
{
static void GenerateXmlFile(
IQueryable<Category> categories, bool useAttributes = true)
{
string which = useAttributes ? "attibutes" : "elements";
string xmlFile = $"categories-and-products-using-{which}.xml";
using (FileStream xmlStream = File.Create(
Combine(CurrentDirectory, xmlFile)))
{
using (XmlWriter xml = XmlWriter.Create(xmlStream,
new XmlWriterSettings { Indent = true }))
{
WriteDataDelegate writeMethod;
if (useAttributes)
{
writeMethod = xml.WriteAttributeString;
}
else // use elements
{
writeMethod = xml.WriteElementString;
}
xml.WriteStartDocument();
xml.WriteStartElement("categories");
foreach (Category c in categories)
{
xml.WriteStartElement("category");
writeMethod("id", c.CategoryId.ToString());
writeMethod("name", c.CategoryName);
writeMethod("desc", c.Description);
writeMethod("product_count", c.Products.Count.ToString());
xml.WriteStartElement("products");
foreach (Product p in c.Products)
{
xml.WriteStartElement("product");
writeMethod("id", p.ProductId.ToString());
writeMethod("name", p.ProductName);
writeMethod("cost", p.Cost is null ? "0" : p.Cost.Value.ToString());
writeMethod("stock", p.Stock.ToString());
writeMethod("discontinued", p.Discontinued.ToString());
xml.WriteEndElement(); // </product>
}
xml.WriteEndElement(); // </products>
xml.WriteEndElement(); // </category>
}
xml.WriteEndElement(); // </categories>
}
}
WriteLine("{0} contains {1:N0} bytes.",
arg0: xmlFile,
arg1: new FileInfo(xmlFile).Length);
}
static void GenerateCsvFile(IQueryable<Category> categories)
{
string csvFile = "categories-and-products.csv";
using (FileStream csvStream = File.Create(Combine(CurrentDirectory, csvFile)))
{
using (StreamWriter csv = new(csvStream))
{
csv.WriteLine("CategoryId,CategoryName,Description,ProductId,ProductName,Cost,Stock,Discontinued");
foreach (Category c in categories)
{
foreach (Product p in c.Products)
{
csv.Write("{0},\"{1}\",\"{2}\",",
arg0: c.CategoryId,
arg1: c.CategoryName,
arg2: c.Description);
csv.Write("{0},\"{1}\",{2},",
arg0: p.ProductId,
arg1: p.ProductName,
arg2: p.Cost is null ? 0 : p.Cost.Value);
csv.WriteLine("{0},{1}",
arg0: p.Stock,
arg1: p.Discontinued);
}
}
}
}
WriteLine("{0} contains {1:N0} bytes.",
arg0: csvFile,
arg1: new FileInfo(csvFile).Length);
}
static void GenerateJsonFile(IQueryable<Category> categories)
{
string jsonFile = "categories-and-products.json";
using (FileStream jsonStream = File.Create(Combine(CurrentDirectory, jsonFile)))
{
using (Utf8JsonWriter json = new(jsonStream,
new JsonWriterOptions { Indented = true }))
{
json.WriteStartObject();
json.WriteStartArray("categories");
foreach (Category c in categories)
{
json.WriteStartObject();
json.WriteNumber("id", c.CategoryId);
json.WriteString("name", c.CategoryName);
json.WriteString("desc", c.Description);
json.WriteNumber("product_count", c.Products.Count);
json.WriteStartArray("products");
foreach (Product p in c.Products)
{
json.WriteStartObject();
json.WriteNumber("id", p.ProductId);
json.WriteString("name", p.ProductName);
json.WriteNumber("cost", p.Cost is null ? 0 : p.Cost.Value);
json.WriteNumber("stock", p.Stock is null ? 0 : p.Stock.Value);
json.WriteBoolean("discontinued", p.Discontinued);
json.WriteEndObject(); // product
}
json.WriteEndArray(); // products
json.WriteEndObject(); // category
}
json.WriteEndArray(); // categories
json.WriteEndObject();
}
}
WriteLine("{0} contains {1:N0} bytes.",
arg0: jsonFile,
arg1: new FileInfo(jsonFile).Length);
}
}

View file

@ -0,0 +1,23 @@
using Microsoft.EntityFrameworkCore; // Include extension method
using Packt.Shared; // Northwind, Category, Product
WriteLine("Creating four files containing serialized categories and products.");
using (Northwind db = new())
{
// a query to get all categories and their related products
IQueryable<Category>? categories = db.Categories?.Include(c => c.Products);
if (categories is null)
{
WriteLine("No categories found.");
return;
}
GenerateXmlFile(categories);
GenerateXmlFile(categories, useAttributes: false);
GenerateCsvFile(categories);
GenerateJsonFile(categories);
WriteLine($"Current directory: {Environment.CurrentDirectory}");
}

View file

@ -0,0 +1,37 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.2.32210.308
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkingWithEFCore", "WorkingWithEFCore\WorkingWithEFCore.csproj", "{4D815158-DA92-4AD6-A417-BEC800167D1E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoursesAndStudents", "CoursesAndStudents\CoursesAndStudents.csproj", "{B4E2819E-6E1B-4314-BBC4-80253EC7764E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ch10Ex02DataSerialization", "Ch10Ex02DataSerialization\Ch10Ex02DataSerialization.csproj", "{C9B3475A-66BB-48DC-B0A7-643A415A3D51}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{4D815158-DA92-4AD6-A417-BEC800167D1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4D815158-DA92-4AD6-A417-BEC800167D1E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4D815158-DA92-4AD6-A417-BEC800167D1E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4D815158-DA92-4AD6-A417-BEC800167D1E}.Release|Any CPU.Build.0 = Release|Any CPU
{B4E2819E-6E1B-4314-BBC4-80253EC7764E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B4E2819E-6E1B-4314-BBC4-80253EC7764E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B4E2819E-6E1B-4314-BBC4-80253EC7764E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B4E2819E-6E1B-4314-BBC4-80253EC7764E}.Release|Any CPU.Build.0 = Release|Any CPU
{C9B3475A-66BB-48DC-B0A7-643A415A3D51}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C9B3475A-66BB-48DC-B0A7-643A415A3D51}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C9B3475A-66BB-48DC-B0A7-643A415A3D51}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C9B3475A-66BB-48DC-B0A7-643A415A3D51}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FFE356D0-B615-4575-B486-42CBA73ED30C}
EndGlobalSection
EndGlobal

View file

@ -0,0 +1,91 @@
using Microsoft.EntityFrameworkCore; // DbContext, DbSet<T>
namespace Packt.Shared;
public class Academy : DbContext
{
public DbSet<Student>? Students { get; set; }
public DbSet<Course>? Courses { get; set; }
protected override void OnConfiguring(
DbContextOptionsBuilder optionsBuilder)
{
string path = Path.Combine(
Environment.CurrentDirectory, "Academy.db");
string connection = $"Filename={path}";
// string connection = @"Data Source=.;Initial Catalog=Academy;Integrated Security=true;MultipleActiveResultSets=true;";
WriteLine($"Connection: {connection}");
optionsBuilder.UseSqlite(connection);
// optionsBuilder.UseSqlServer(connection);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Fluent API validation rules
modelBuilder.Entity<Student>()
.Property(s => s.LastName).HasMaxLength(30).IsRequired();
// populate database with sample data
Student alice = new()
{
StudentId = 1,
FirstName = "Alice",
LastName = "Jones"
};
Student bob = new()
{
StudentId = 2,
FirstName = "Bob",
LastName = "Smith"
};
Student cecilia = new()
{
StudentId = 3,
FirstName = "Cecilia",
LastName = "Ramirez"
};
Course csharp = new()
{
CourseId = 1,
Title = "C# 10 and .NET 6"
};
Course webdev = new()
{
CourseId = 2,
Title = "Web Development"
};
Course python = new()
{
CourseId = 3,
Title = "Python for Beginners"
};
modelBuilder.Entity<Student>()
.HasData(alice, bob, cecilia);
modelBuilder.Entity<Course>()
.HasData(csharp, webdev, python);
modelBuilder.Entity<Course>()
.HasMany(c => c.Students)
.WithMany(s => s.Courses)
.UsingEntity(e => e.HasData(
// all students signed up for C# course
new { CoursesCourseId = 1, StudentsStudentId = 1 },
new { CoursesCourseId = 1, StudentsStudentId = 2 },
new { CoursesCourseId = 1, StudentsStudentId = 3 },
// only Bob signed up for Web Dev
new { CoursesCourseId = 2, StudentsStudentId = 2 },
// only Cecilia signed up for Python
new { CoursesCourseId = 3, StudentsStudentId = 3 }
));
}
}

View file

@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
namespace Packt.Shared;
public class Course
{
public int CourseId { get; set; }
[Required]
[StringLength(60)]
public string? Title { get; set; }
public ICollection<Student>? Students { get; set; }
}

View file

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Using Include="System.Console" Static="true" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.0-*" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.0-*" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,25 @@
using Microsoft.EntityFrameworkCore; // for GenerateCreateScript()
using Packt.Shared; // Academy
using (Academy a = new())
{
bool deleted = await a.Database.EnsureDeletedAsync();
WriteLine($"Database deleted: {deleted}");
bool created = await a.Database.EnsureCreatedAsync();
WriteLine($"Database created: {created}");
WriteLine("SQL script used to create database:");
WriteLine(a.Database.GenerateCreateScript());
foreach (Student s in a.Students.Include(s => s.Courses))
{
WriteLine("{0} {1} attends the following {2} courses:",
s.FirstName, s.LastName, s.Courses.Count);
foreach (Course c in s.Courses)
{
WriteLine($" {c.Title}");
}
}
}

View file

@ -0,0 +1,9 @@
namespace Packt.Shared;
public class Student
{
public int StudentId { get; set; }
public string? FirstName { get; set; }
public string? LastName { get; set; }
public ICollection<Course>? Courses { get; set; }
}

View file

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace WorkingWithEFCore.AutoGen
{
[Index("CategoryName", Name = "CategoryName")]
public partial class Category
{
public Category()
{
Products = new HashSet<Product>();
}
[Key]
public long CategoryId { get; set; }
[Column(TypeName = "nvarchar (15)")]
public string CategoryName { get; set; } = null!;
[Column(TypeName = "ntext")]
public string? Description { get; set; }
[Column(TypeName = "image")]
public byte[]? Picture { get; set; }
[InverseProperty("Category")]
public virtual ICollection<Product> Products { get; set; }
}
}

View file

@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
namespace WorkingWithEFCore.AutoGen
{
public partial class Northwind : DbContext
{
public Northwind()
{
}
public Northwind(DbContextOptions<Northwind> options)
: base(options)
{
}
public virtual DbSet<Category> Categories { get; set; } = null!;
public virtual DbSet<Product> Products { get; set; } = null!;
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see http://go.microsoft.com/fwlink/?LinkId=723263.
optionsBuilder.UseSqlite("Filename=Northwind.db");
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Category>(entity =>
{
entity.Property(e => e.CategoryId).ValueGeneratedNever();
});
modelBuilder.Entity<Product>(entity =>
{
entity.Property(e => e.ProductId).ValueGeneratedNever();
entity.Property(e => e.Discontinued).HasDefaultValueSql("0");
entity.Property(e => e.ReorderLevel).HasDefaultValueSql("0");
entity.Property(e => e.UnitPrice).HasDefaultValueSql("0");
entity.Property(e => e.UnitsInStock).HasDefaultValueSql("0");
entity.Property(e => e.UnitsOnOrder).HasDefaultValueSql("0");
});
OnModelCreatingPartial(modelBuilder);
}
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}
}

View file

@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace WorkingWithEFCore.AutoGen
{
[Index("CategoryId", Name = "CategoriesProducts")]
[Index("CategoryId", Name = "CategoryId")]
[Index("ProductName", Name = "ProductName")]
[Index("SupplierId", Name = "SupplierId")]
[Index("SupplierId", Name = "SuppliersProducts")]
public partial class Product
{
[Key]
public long ProductId { get; set; }
[Column(TypeName = "nvarchar (40)")]
public string ProductName { get; set; } = null!;
[Column(TypeName = "int")]
public long? SupplierId { get; set; }
[Column(TypeName = "int")]
public long? CategoryId { get; set; }
[Column(TypeName = "nvarchar (20)")]
public string? QuantityPerUnit { get; set; }
[Column(TypeName = "money")]
public byte[]? UnitPrice { get; set; }
[Column(TypeName = "smallint")]
public long? UnitsInStock { get; set; }
[Column(TypeName = "smallint")]
public long? UnitsOnOrder { get; set; }
[Column(TypeName = "smallint")]
public long? ReorderLevel { get; set; }
[Column(TypeName = "bit")]
public byte[] Discontinued { get; set; } = null!;
[ForeignKey("CategoryId")]
[InverseProperty("Products")]
public virtual Category? Category { get; set; }
}
}

View file

@ -0,0 +1,24 @@
using System.ComponentModel.DataAnnotations.Schema; // [Column]
namespace Packt.Shared;
public class Category
{
// these properties map to columns in the database
public int CategoryId { get; set; }
public string? CategoryName { get; set; }
[Column(TypeName = "ntext")]
public string? Description { get; set; }
// defines a navigation property for related rows
public virtual ICollection<Product> Products { get; set; }
public Category()
{
// to enable developers to add products to a Category we must
// initialize the navigation property to an empty collection
Products = new HashSet<Product>();
}
}

View file

@ -0,0 +1,57 @@
using Microsoft.EntityFrameworkCore; // DbContext, DbContextOptionsBuilder
using Microsoft.EntityFrameworkCore.Diagnostics;
namespace Packt.Shared;
// this manages the connection to the database
public class Northwind : DbContext
{
// these properties map to tables in the database
public DbSet<Category>? Categories { get; set; }
public DbSet<Product>? Products { get; set; }
protected override void OnConfiguring(
DbContextOptionsBuilder optionsBuilder)
{
string path = Path.Combine(
Environment.CurrentDirectory, "Northwind.db");
string connection = $"Filename={path}";
ConsoleColor previousColor = ForegroundColor;
ForegroundColor = ConsoleColor.DarkYellow;
WriteLine($"Connection: {connection}");
ForegroundColor = previousColor;
optionsBuilder.UseSqlite(connection);
optionsBuilder.LogTo(WriteLine, // Console
new[] { RelationalEventId.CommandExecuting })
.EnableSensitiveDataLogging();
optionsBuilder.UseLazyLoadingProxies();
}
protected override void OnModelCreating(
ModelBuilder modelBuilder)
{
// example of using Fluent API instead of attributes
// to limit the length of a category name to 15
modelBuilder.Entity<Category>()
.Property(category => category.CategoryName)
.IsRequired() // NOT NULL
.HasMaxLength(15);
if (Database.ProviderName?.Contains("Sqlite") ?? false)
{
// added to "fix" the lack of decimal support in SQLite
modelBuilder.Entity<Product>()
.Property(product => product.Cost)
.HasConversion<double>();
}
// global filter to remove discontinued products
modelBuilder.Entity<Product>()
.HasQueryFilter(p => !p.Discontinued);
}
}

Binary file not shown.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,26 @@
using System.ComponentModel.DataAnnotations; // [Required], [StringLength]
using System.ComponentModel.DataAnnotations.Schema; // [Column]
namespace Packt.Shared;
public class Product
{
public int ProductId { get; set; } // primary key
[Required]
[StringLength(40)]
public string ProductName { get; set; } = null!;
[Column("UnitPrice", TypeName = "money")]
public decimal? Cost { get; set; } // property name != column name
[Column("UnitsInStock")]
public short? Stock { get; set; }
public bool Discontinued { get; set; }
// these two define the foreign key relationship
// to the Categories table
public int CategoryId { get; set; }
public virtual Category Category { get; set; } = null!;
}

View file

@ -0,0 +1,28 @@
partial class Program
{
static void SectionTitle(string title)
{
ConsoleColor previousColor = ForegroundColor;
ForegroundColor = ConsoleColor.Yellow;
WriteLine("*");
WriteLine($"* {title}");
WriteLine("*");
ForegroundColor = previousColor;
}
static void Fail(string message)
{
ConsoleColor previousColor = ForegroundColor;
ForegroundColor = ConsoleColor.Red;
WriteLine($"Fail > {message}");
ForegroundColor = previousColor;
}
static void Info(string message)
{
ConsoleColor previousColor = ForegroundColor;
ForegroundColor = ConsoleColor.Cyan;
WriteLine($"Info > {message}");
ForegroundColor = previousColor;
}
}

View file

@ -0,0 +1,107 @@
using Microsoft.EntityFrameworkCore.ChangeTracking; // EntityEntry<T>
using Microsoft.EntityFrameworkCore.Storage; // IDbContextTransaction
using Packt.Shared;
partial class Program
{
static void ListProducts(int? productIdToHighlight = null)
{
using (Northwind db = new())
{
if ((db.Products is null) || (db.Products.Count() == 0))
{
Fail("There are no products.");
return;
}
WriteLine("| {0,-3} | {1,-35} | {2,8} | {3,5} | {4} |",
"Id", "Product Name", "Cost", "Stock", "Disc.");
foreach (Product p in db.Products)
{
ConsoleColor previousColor = ForegroundColor;
if (productIdToHighlight == p.ProductId)
{
ForegroundColor = ConsoleColor.Green;
}
WriteLine("| {0:000} | {1,-35} | {2,8:$#,##0.00} | {3,5} | {4} |",
p.ProductId, p.ProductName, p.Cost, p.Stock, p.Discontinued);
ForegroundColor = previousColor;
}
}
}
static (int affected, int productId) AddProduct(
int categoryId, string productName, decimal? price)
{
using (Northwind db = new())
{
Product p = new()
{
CategoryId = categoryId,
ProductName = productName,
Cost = price,
Stock = 72
};
// set product as added in change tracking
EntityEntry<Product> entity = db.Products.Add(p);
WriteLine($"State: {entity.State}, ProductId: {p.ProductId}");
// save tracked change to database
int affected = db.SaveChanges();
WriteLine($"State: {entity.State}, ProductId: {p.ProductId}");
return (affected, p.ProductId);
}
}
static (int affected, int productId) IncreaseProductPrice(
string productNameStartsWith, decimal amount)
{
using (Northwind db = new())
{
// get first product whose name starts with name
Product updateProduct = db.Products.First(
p => p.ProductName.StartsWith(productNameStartsWith));
updateProduct.Cost += amount;
int affected = db.SaveChanges();
return (affected, updateProduct.ProductId);
}
}
static int DeleteProducts(string productNameStartsWith)
{
using (Northwind db = new())
{
using (IDbContextTransaction t = db.Database.BeginTransaction())
{
WriteLine("Transaction isolation level: {0}",
arg0: t.GetDbTransaction().IsolationLevel);
IQueryable<Product>? products = db.Products?.Where(
p => p.ProductName.StartsWith(productNameStartsWith));
if (products is null)
{
WriteLine("No products found to delete.");
return 0;
}
else
{
db.Products.RemoveRange(products);
}
int affected = db.SaveChanges();
t.Commit();
return affected;
}
}
}
}

View file

@ -0,0 +1,199 @@
using Microsoft.EntityFrameworkCore; // Include extension method
using Microsoft.EntityFrameworkCore.ChangeTracking; // CollectionEntry
using Packt.Shared; // Northwind, Category, Product
partial class Program
{
static void QueryingCategories()
{
using (Northwind db = new())
{
SectionTitle("Categories and how many products they have:");
// a query to get all categories and their related products
IQueryable<Category>? categories;
// = db.Categories;
// .Include(c => c.Products);
db.ChangeTracker.LazyLoadingEnabled = false;
Write("Enable eager loading? (Y/N): ");
bool eagerLoading = (ReadKey(intercept: true).Key == ConsoleKey.Y);
bool explicitLoading = false;
WriteLine();
if (eagerLoading)
{
categories = db.Categories?.Include(c => c.Products);
}
else
{
categories = db.Categories;
Write("Enable explicit loading? (Y/N): ");
explicitLoading = (ReadKey(intercept: true).Key == ConsoleKey.Y);
WriteLine();
}
if (categories is null)
{
Fail("No categories found.");
return;
}
// execute query and enumerate results
foreach (Category c in categories)
{
if (explicitLoading)
{
Write($"Explicitly load products for {c.CategoryName}? (Y/N): ");
ConsoleKeyInfo key = ReadKey(intercept: true);
WriteLine();
if (key.Key == ConsoleKey.Y)
{
CollectionEntry<Category, Product> products =
db.Entry(c).Collection(c2 => c2.Products);
if (!products.IsLoaded) products.Load();
}
}
WriteLine($"{c.CategoryName} has {c.Products.Count} products.");
}
}
}
static void FilteredIncludes()
{
using (Northwind db = new())
{
SectionTitle("Products with a minimum number of units in stock.");
string? input;
int stock;
do
{
Write("Enter a minimum for units in stock: ");
input = ReadLine();
} while (!int.TryParse(input, out stock));
IQueryable<Category>? categories = db.Categories?
.Include(c => c.Products.Where(p => p.Stock >= stock));
if (categories is null)
{
Fail("No categories found.");
return;
}
Info($"ToQueryString: {categories.ToQueryString()}");
foreach (Category c in categories)
{
WriteLine($"{c.CategoryName} has {c.Products.Count} products with a minimum of {stock} units in stock.");
foreach (Product p in c.Products)
{
WriteLine($" {p.ProductName} has {p.Stock} units in stock.");
}
}
}
}
static void QueryingProducts()
{
using (Northwind db = new())
{
SectionTitle("Products that cost more than a price, highest at top.");
string? input;
decimal price;
do
{
Write("Enter a product price: ");
input = ReadLine();
} while (!decimal.TryParse(input, out price));
IQueryable<Product>? products = db.Products?
.Where(product => product.Cost > price)
.OrderByDescending(product => product.Cost);
if (products is null)
{
Fail("No products found.");
return;
}
Info($"ToQueryString: {products.ToQueryString()}");
foreach (Product p in products)
{
WriteLine(
"{0}: {1} costs {2:$#,##0.00} and has {3} in stock.",
p.ProductId, p.ProductName, p.Cost, p.Stock);
}
}
}
static void QueryingWithLike()
{
using (Northwind db = new())
{
SectionTitle("Pattern matching with LIKE.");
Write("Enter part of a product name: ");
string? input = ReadLine();
if (string.IsNullOrWhiteSpace(input))
{
Fail("You did not enter part of a product name.");
return;
}
IQueryable<Product>? products = db.Products?
.Where(p => EF.Functions.Like(p.ProductName, $"%{input}%"));
if (products is null)
{
Fail("No products found.");
return;
}
foreach (Product p in products)
{
WriteLine("{0} has {1} units in stock. Discontinued? {2}",
p.ProductName, p.Stock, p.Discontinued);
}
}
}
static void GetRandomProduct()
{
using (Northwind db = new())
{
SectionTitle("Get a random product.");
int? rowCount = db.Products?.Count();
if (rowCount == null)
{
Fail("Products table is empty.");
return;
}
Product? p = db.Products?.FirstOrDefault(
p => p.ProductId == (int)(EF.Functions.Random() * rowCount));
if (p == null)
{
Fail("Product not found.");
return;
}
WriteLine($"Random product: {p.ProductId} {p.ProductName}");
}
}
}

View file

@ -0,0 +1,48 @@
using Packt.Shared;
/*
Northwind db = new();
WriteLine($"Provider: {db.Database.ProviderName}");
*/
// QueryingCategories();
//FilteredIncludes();
//QueryingProducts();
//QueryingWithLike();
//GetRandomProduct();
var resultAdd = AddProduct(categoryId: 6,
productName: "Bob's Burgers", price: 500M);
if (resultAdd.affected == 1)
{
WriteLine("Add product successful.");
}
ListProducts(productIdToHighlight: resultAdd.productId);
var resultUpdate = IncreaseProductPrice(
productNameStartsWith: "Bob", amount: 20M);
if (resultUpdate.affected == 1)
{
WriteLine("Increase product price successful.");
}
ListProducts(productIdToHighlight: resultUpdate.productId);
WriteLine("About to delete all products whose name starts with Bob.");
Write("Press Enter to continue: ");
if (ReadKey(intercept: true).Key == ConsoleKey.Enter)
{
int deleted = DeleteProducts(productNameStartsWith: "Bob");
WriteLine($"{deleted} product(s) were deleted.");
}
else
{
WriteLine("Delete was cancelled.");
}

View file

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Using Include="System.Console" Static="true" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.0-*" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.0-*" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="7.0.0-*" />
</ItemGroup>
<ItemGroup>
<None Update="Northwind.db">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View file

@ -0,0 +1,24 @@
using System.ComponentModel.DataAnnotations.Schema; // [Column]
namespace Packt.Shared;
public class Category
{
// these properties map to columns in the database
public int CategoryId { get; set; }
public string? CategoryName { get; set; }
[Column(TypeName = "ntext")]
public string? Description { get; set; }
// defines a navigation property for related rows
public virtual ICollection<Product> Products { get; set; }
public Category()
{
// to enable developers to add products to a Category we must
// initialize the navigation property to an empty collection
Products = new HashSet<Product>();
}
}

View file

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Using Include="System.Console" Static="true" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.0-*" />
</ItemGroup>
<ItemGroup>
<None Update="Northwind.db">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View file

@ -0,0 +1,47 @@
using Microsoft.EntityFrameworkCore; // DbContext, DbContextOptionsBuilder
using Microsoft.EntityFrameworkCore.Diagnostics;
namespace Packt.Shared;
// this manages the connection to the database
public class Northwind : DbContext
{
// these properties map to tables in the database
public DbSet<Category>? Categories { get; set; }
public DbSet<Product>? Products { get; set; }
protected override void OnConfiguring(
DbContextOptionsBuilder optionsBuilder)
{
string path = Path.Combine(
Environment.CurrentDirectory, "Northwind.db");
string connection = $"Filename={path}";
ConsoleColor previousColor = ForegroundColor;
ForegroundColor = ConsoleColor.DarkYellow;
WriteLine($"Connection: {connection}");
ForegroundColor = previousColor;
optionsBuilder.UseSqlite(connection);
}
protected override void OnModelCreating(
ModelBuilder modelBuilder)
{
// example of using Fluent API instead of attributes
// to limit the length of a category name to 15
modelBuilder.Entity<Category>()
.Property(category => category.CategoryName)
.IsRequired() // NOT NULL
.HasMaxLength(15);
if (Database.ProviderName?.Contains("Sqlite") ?? false)
{
// added to "fix" the lack of decimal support in SQLite
modelBuilder.Entity<Product>()
.Property(product => product.Cost)
.HasConversion<double>();
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,26 @@
using System.ComponentModel.DataAnnotations; // [Required], [StringLength]
using System.ComponentModel.DataAnnotations.Schema; // [Column]
namespace Packt.Shared;
public class Product
{
public int ProductId { get; set; } // primary key
[Required]
[StringLength(40)]
public string ProductName { get; set; } = null!;
[Column("UnitPrice", TypeName = "money")]
public decimal? Cost { get; set; } // property name != column name
[Column("UnitsInStock")]
public short? Stock { get; set; }
public bool Discontinued { get; set; }
// these two define the foreign key relationship
// to the Categories table
public int CategoryId { get; set; }
public virtual Category Category { get; set; } = null!;
}

View file

@ -0,0 +1,163 @@
using System.Xml; // XmlWriter
using System.Text.Json; // Utf8JsonWriter, JsonWriterOptions
using Packt.Shared; // Category, Product
using static System.IO.Path;
using static System.Environment;
// we want to easily show the difference between outputting
// XML using elements or attributes so we will define a
// delegate to reference the two different methods.
delegate void WriteDataDelegate(string name, string? value);
partial class Program
{
static void GenerateXmlFile(
IQueryable<Category> categories, bool useAttributes = true)
{
string which = useAttributes ? "attibutes" : "elements";
string xmlFile = $"categories-and-products-using-{which}.xml";
using (FileStream xmlStream = File.Create(
Combine(CurrentDirectory, xmlFile)))
{
using (XmlWriter xml = XmlWriter.Create(xmlStream,
new XmlWriterSettings { Indent = true }))
{
WriteDataDelegate writeMethod;
if (useAttributes)
{
writeMethod = xml.WriteAttributeString;
}
else // use elements
{
writeMethod = xml.WriteElementString;
}
xml.WriteStartDocument();
xml.WriteStartElement("categories");
foreach (Category c in categories)
{
xml.WriteStartElement("category");
writeMethod("id", c.CategoryId.ToString());
writeMethod("name", c.CategoryName);
writeMethod("desc", c.Description);
writeMethod("product_count", c.Products.Count.ToString());
xml.WriteStartElement("products");
foreach (Product p in c.Products)
{
xml.WriteStartElement("product");
writeMethod("id", p.ProductId.ToString());
writeMethod("name", p.ProductName);
writeMethod("cost", p.Cost is null ? "0" : p.Cost.Value.ToString());
writeMethod("stock", p.Stock.ToString());
writeMethod("discontinued", p.Discontinued.ToString());
xml.WriteEndElement(); // </product>
}
xml.WriteEndElement(); // </products>
xml.WriteEndElement(); // </category>
}
xml.WriteEndElement(); // </categories>
}
}
WriteLine("{0} contains {1:N0} bytes.",
arg0: xmlFile,
arg1: new FileInfo(xmlFile).Length);
}
static void GenerateCsvFile(IQueryable<Category> categories)
{
string csvFile = "categories-and-products.csv";
using (FileStream csvStream = File.Create(Combine(CurrentDirectory, csvFile)))
{
using (StreamWriter csv = new(csvStream))
{
csv.WriteLine("CategoryId,CategoryName,Description,ProductId,ProductName,Cost,Stock,Discontinued");
foreach (Category c in categories)
{
foreach (Product p in c.Products)
{
csv.Write("{0},\"{1}\",\"{2}\",",
arg0: c.CategoryId,
arg1: c.CategoryName,
arg2: c.Description);
csv.Write("{0},\"{1}\",{2},",
arg0: p.ProductId,
arg1: p.ProductName,
arg2: p.Cost is null ? 0 : p.Cost.Value);
csv.WriteLine("{0},{1}",
arg0: p.Stock,
arg1: p.Discontinued);
}
}
}
}
WriteLine("{0} contains {1:N0} bytes.",
arg0: csvFile,
arg1: new FileInfo(csvFile).Length);
}
static void GenerateJsonFile(IQueryable<Category> categories)
{
string jsonFile = "categories-and-products.json";
using (FileStream jsonStream = File.Create(Combine(CurrentDirectory, jsonFile)))
{
using (Utf8JsonWriter json = new(jsonStream,
new JsonWriterOptions { Indented = true }))
{
json.WriteStartObject();
json.WriteStartArray("categories");
foreach (Category c in categories)
{
json.WriteStartObject();
json.WriteNumber("id", c.CategoryId);
json.WriteString("name", c.CategoryName);
json.WriteString("desc", c.Description);
json.WriteNumber("product_count", c.Products.Count);
json.WriteStartArray("products");
foreach (Product p in c.Products)
{
json.WriteStartObject();
json.WriteNumber("id", p.ProductId);
json.WriteString("name", p.ProductName);
json.WriteNumber("cost", p.Cost is null ? 0 : p.Cost.Value);
json.WriteNumber("stock", p.Stock is null ? 0 : p.Stock.Value);
json.WriteBoolean("discontinued", p.Discontinued);
json.WriteEndObject(); // product
}
json.WriteEndArray(); // products
json.WriteEndObject(); // category
}
json.WriteEndArray(); // categories
json.WriteEndObject();
}
}
WriteLine("{0} contains {1:N0} bytes.",
arg0: jsonFile,
arg1: new FileInfo(jsonFile).Length);
}
}

View file

@ -0,0 +1,23 @@
using Microsoft.EntityFrameworkCore; // Include extension method
using Packt.Shared; // Northwind, Category, Product
WriteLine("Creating four files containing serialized categories and products.");
using (Northwind db = new())
{
// a query to get all categories and their related products
IQueryable<Category>? categories = db.Categories?.Include(c => c.Products);
if (categories is null)
{
WriteLine("No categories found.");
return;
}
GenerateXmlFile(categories);
GenerateXmlFile(categories, useAttributes: false);
GenerateCsvFile(categories);
GenerateJsonFile(categories);
WriteLine($"Current directory: {Environment.CurrentDirectory}");
}

View file

@ -0,0 +1,13 @@
{
"folders": [
{
"path": "WorkingWithEFCore"
},
{
"path": "CoursesAndStudents"
},
{
"path": "Ch10Ex02DataSerialization"
}
]
}

View file

@ -0,0 +1,91 @@
using Microsoft.EntityFrameworkCore; // DbContext, DbSet<T>
namespace Packt.Shared;
public class Academy : DbContext
{
public DbSet<Student>? Students { get; set; }
public DbSet<Course>? Courses { get; set; }
protected override void OnConfiguring(
DbContextOptionsBuilder optionsBuilder)
{
string path = Path.Combine(
Environment.CurrentDirectory, "Academy.db");
string connection = $"Filename={path}";
// string connection = @"Data Source=.;Initial Catalog=Academy;Integrated Security=true;MultipleActiveResultSets=true;";
WriteLine($"Connection: {connection}");
optionsBuilder.UseSqlite(connection);
// optionsBuilder.UseSqlServer(connection);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Fluent API validation rules
modelBuilder.Entity<Student>()
.Property(s => s.LastName).HasMaxLength(30).IsRequired();
// populate database with sample data
Student alice = new()
{
StudentId = 1,
FirstName = "Alice",
LastName = "Jones"
};
Student bob = new()
{
StudentId = 2,
FirstName = "Bob",
LastName = "Smith"
};
Student cecilia = new()
{
StudentId = 3,
FirstName = "Cecilia",
LastName = "Ramirez"
};
Course csharp = new()
{
CourseId = 1,
Title = "C# 10 and .NET 6"
};
Course webdev = new()
{
CourseId = 2,
Title = "Web Development"
};
Course python = new()
{
CourseId = 3,
Title = "Python for Beginners"
};
modelBuilder.Entity<Student>()
.HasData(alice, bob, cecilia);
modelBuilder.Entity<Course>()
.HasData(csharp, webdev, python);
modelBuilder.Entity<Course>()
.HasMany(c => c.Students)
.WithMany(s => s.Courses)
.UsingEntity(e => e.HasData(
// all students signed up for C# course
new { CoursesCourseId = 1, StudentsStudentId = 1 },
new { CoursesCourseId = 1, StudentsStudentId = 2 },
new { CoursesCourseId = 1, StudentsStudentId = 3 },
// only Bob signed up for Web Dev
new { CoursesCourseId = 2, StudentsStudentId = 2 },
// only Cecilia signed up for Python
new { CoursesCourseId = 3, StudentsStudentId = 3 }
));
}
}

View file

@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
namespace Packt.Shared;
public class Course
{
public int CourseId { get; set; }
[Required]
[StringLength(60)]
public string? Title { get; set; }
public ICollection<Student>? Students { get; set; }
}

View file

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Using Include="System.Console" Static="true" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.0-*" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.0-*" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,25 @@
using Microsoft.EntityFrameworkCore; // for GenerateCreateScript()
using Packt.Shared; // Academy
using (Academy a = new())
{
bool deleted = await a.Database.EnsureDeletedAsync();
WriteLine($"Database deleted: {deleted}");
bool created = await a.Database.EnsureCreatedAsync();
WriteLine($"Database created: {created}");
WriteLine("SQL script used to create database:");
WriteLine(a.Database.GenerateCreateScript());
foreach (Student s in a.Students.Include(s => s.Courses))
{
WriteLine("{0} {1} attends the following {2} courses:",
s.FirstName, s.LastName, s.Courses.Count);
foreach (Course c in s.Courses)
{
WriteLine($" {c.Title}");
}
}
}

View file

@ -0,0 +1,9 @@
namespace Packt.Shared;
public class Student
{
public int StudentId { get; set; }
public string? FirstName { get; set; }
public string? LastName { get; set; }
public ICollection<Course>? Courses { get; set; }
}

View file

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace WorkingWithEFCore.AutoGen
{
[Index("CategoryName", Name = "CategoryName")]
public partial class Category
{
public Category()
{
Products = new HashSet<Product>();
}
[Key]
public long CategoryId { get; set; }
[Column(TypeName = "nvarchar (15)")]
public string CategoryName { get; set; } = null!;
[Column(TypeName = "ntext")]
public string? Description { get; set; }
[Column(TypeName = "image")]
public byte[]? Picture { get; set; }
[InverseProperty("Category")]
public virtual ICollection<Product> Products { get; set; }
}
}

View file

@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
namespace WorkingWithEFCore.AutoGen
{
public partial class Northwind : DbContext
{
public Northwind()
{
}
public Northwind(DbContextOptions<Northwind> options)
: base(options)
{
}
public virtual DbSet<Category> Categories { get; set; } = null!;
public virtual DbSet<Product> Products { get; set; } = null!;
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see http://go.microsoft.com/fwlink/?LinkId=723263.
optionsBuilder.UseSqlite("Filename=Northwind.db");
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Category>(entity =>
{
entity.Property(e => e.CategoryId).ValueGeneratedNever();
});
modelBuilder.Entity<Product>(entity =>
{
entity.Property(e => e.ProductId).ValueGeneratedNever();
entity.Property(e => e.Discontinued).HasDefaultValueSql("0");
entity.Property(e => e.ReorderLevel).HasDefaultValueSql("0");
entity.Property(e => e.UnitPrice).HasDefaultValueSql("0");
entity.Property(e => e.UnitsInStock).HasDefaultValueSql("0");
entity.Property(e => e.UnitsOnOrder).HasDefaultValueSql("0");
});
OnModelCreatingPartial(modelBuilder);
}
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}
}

View file

@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace WorkingWithEFCore.AutoGen
{
[Index("CategoryId", Name = "CategoriesProducts")]
[Index("CategoryId", Name = "CategoryId")]
[Index("ProductName", Name = "ProductName")]
[Index("SupplierId", Name = "SupplierId")]
[Index("SupplierId", Name = "SuppliersProducts")]
public partial class Product
{
[Key]
public long ProductId { get; set; }
[Column(TypeName = "nvarchar (40)")]
public string ProductName { get; set; } = null!;
[Column(TypeName = "int")]
public long? SupplierId { get; set; }
[Column(TypeName = "int")]
public long? CategoryId { get; set; }
[Column(TypeName = "nvarchar (20)")]
public string? QuantityPerUnit { get; set; }
[Column(TypeName = "money")]
public byte[]? UnitPrice { get; set; }
[Column(TypeName = "smallint")]
public long? UnitsInStock { get; set; }
[Column(TypeName = "smallint")]
public long? UnitsOnOrder { get; set; }
[Column(TypeName = "smallint")]
public long? ReorderLevel { get; set; }
[Column(TypeName = "bit")]
public byte[] Discontinued { get; set; } = null!;
[ForeignKey("CategoryId")]
[InverseProperty("Products")]
public virtual Category? Category { get; set; }
}
}

View file

@ -0,0 +1,24 @@
using System.ComponentModel.DataAnnotations.Schema; // [Column]
namespace Packt.Shared;
public class Category
{
// these properties map to columns in the database
public int CategoryId { get; set; }
public string? CategoryName { get; set; }
[Column(TypeName = "ntext")]
public string? Description { get; set; }
// defines a navigation property for related rows
public virtual ICollection<Product> Products { get; set; }
public Category()
{
// to enable developers to add products to a Category we must
// initialize the navigation property to an empty collection
Products = new HashSet<Product>();
}
}

View file

@ -0,0 +1,57 @@
using Microsoft.EntityFrameworkCore; // DbContext, DbContextOptionsBuilder
using Microsoft.EntityFrameworkCore.Diagnostics;
namespace Packt.Shared;
// this manages the connection to the database
public class Northwind : DbContext
{
// these properties map to tables in the database
public DbSet<Category>? Categories { get; set; }
public DbSet<Product>? Products { get; set; }
protected override void OnConfiguring(
DbContextOptionsBuilder optionsBuilder)
{
string path = Path.Combine(
Environment.CurrentDirectory, "Northwind.db");
string connection = $"Filename={path}";
ConsoleColor previousColor = ForegroundColor;
ForegroundColor = ConsoleColor.DarkYellow;
WriteLine($"Connection: {connection}");
ForegroundColor = previousColor;
optionsBuilder.UseSqlite(connection);
optionsBuilder.LogTo(WriteLine, // Console
new[] { RelationalEventId.CommandExecuting })
.EnableSensitiveDataLogging();
optionsBuilder.UseLazyLoadingProxies();
}
protected override void OnModelCreating(
ModelBuilder modelBuilder)
{
// example of using Fluent API instead of attributes
// to limit the length of a category name to 15
modelBuilder.Entity<Category>()
.Property(category => category.CategoryName)
.IsRequired() // NOT NULL
.HasMaxLength(15);
if (Database.ProviderName?.Contains("Sqlite") ?? false)
{
// added to "fix" the lack of decimal support in SQLite
modelBuilder.Entity<Product>()
.Property(product => product.Cost)
.HasConversion<double>();
}
// global filter to remove discontinued products
modelBuilder.Entity<Product>()
.HasQueryFilter(p => !p.Discontinued);
}
}

Binary file not shown.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,26 @@
using System.ComponentModel.DataAnnotations; // [Required], [StringLength]
using System.ComponentModel.DataAnnotations.Schema; // [Column]
namespace Packt.Shared;
public class Product
{
public int ProductId { get; set; } // primary key
[Required]
[StringLength(40)]
public string ProductName { get; set; } = null!;
[Column("UnitPrice", TypeName = "money")]
public decimal? Cost { get; set; } // property name != column name
[Column("UnitsInStock")]
public short? Stock { get; set; }
public bool Discontinued { get; set; }
// these two define the foreign key relationship
// to the Categories table
public int CategoryId { get; set; }
public virtual Category Category { get; set; } = null!;
}

View file

@ -0,0 +1,28 @@
partial class Program
{
static void SectionTitle(string title)
{
ConsoleColor previousColor = ForegroundColor;
ForegroundColor = ConsoleColor.Yellow;
WriteLine("*");
WriteLine($"* {title}");
WriteLine("*");
ForegroundColor = previousColor;
}
static void Fail(string message)
{
ConsoleColor previousColor = ForegroundColor;
ForegroundColor = ConsoleColor.Red;
WriteLine($"Fail > {message}");
ForegroundColor = previousColor;
}
static void Info(string message)
{
ConsoleColor previousColor = ForegroundColor;
ForegroundColor = ConsoleColor.Cyan;
WriteLine($"Info > {message}");
ForegroundColor = previousColor;
}
}

View file

@ -0,0 +1,107 @@
using Microsoft.EntityFrameworkCore.ChangeTracking; // EntityEntry<T>
using Microsoft.EntityFrameworkCore.Storage; // IDbContextTransaction
using Packt.Shared;
partial class Program
{
static void ListProducts(int? productIdToHighlight = null)
{
using (Northwind db = new())
{
if ((db.Products is null) || (db.Products.Count() == 0))
{
Fail("There are no products.");
return;
}
WriteLine("| {0,-3} | {1,-35} | {2,8} | {3,5} | {4} |",
"Id", "Product Name", "Cost", "Stock", "Disc.");
foreach (Product p in db.Products)
{
ConsoleColor previousColor = ForegroundColor;
if (productIdToHighlight == p.ProductId)
{
ForegroundColor = ConsoleColor.Green;
}
WriteLine("| {0:000} | {1,-35} | {2,8:$#,##0.00} | {3,5} | {4} |",
p.ProductId, p.ProductName, p.Cost, p.Stock, p.Discontinued);
ForegroundColor = previousColor;
}
}
}
static (int affected, int productId) AddProduct(
int categoryId, string productName, decimal? price)
{
using (Northwind db = new())
{
Product p = new()
{
CategoryId = categoryId,
ProductName = productName,
Cost = price,
Stock = 72
};
// set product as added in change tracking
EntityEntry<Product> entity = db.Products.Add(p);
WriteLine($"State: {entity.State}, ProductId: {p.ProductId}");
// save tracked change to database
int affected = db.SaveChanges();
WriteLine($"State: {entity.State}, ProductId: {p.ProductId}");
return (affected, p.ProductId);
}
}
static (int affected, int productId) IncreaseProductPrice(
string productNameStartsWith, decimal amount)
{
using (Northwind db = new())
{
// get first product whose name starts with name
Product updateProduct = db.Products.First(
p => p.ProductName.StartsWith(productNameStartsWith));
updateProduct.Cost += amount;
int affected = db.SaveChanges();
return (affected, updateProduct.ProductId);
}
}
static int DeleteProducts(string productNameStartsWith)
{
using (Northwind db = new())
{
using (IDbContextTransaction t = db.Database.BeginTransaction())
{
WriteLine("Transaction isolation level: {0}",
arg0: t.GetDbTransaction().IsolationLevel);
IQueryable<Product>? products = db.Products?.Where(
p => p.ProductName.StartsWith(productNameStartsWith));
if (products is null)
{
WriteLine("No products found to delete.");
return 0;
}
else
{
db.Products.RemoveRange(products);
}
int affected = db.SaveChanges();
t.Commit();
return affected;
}
}
}
}

View file

@ -0,0 +1,199 @@
using Microsoft.EntityFrameworkCore; // Include extension method
using Microsoft.EntityFrameworkCore.ChangeTracking; // CollectionEntry
using Packt.Shared; // Northwind, Category, Product
partial class Program
{
static void QueryingCategories()
{
using (Northwind db = new())
{
SectionTitle("Categories and how many products they have:");
// a query to get all categories and their related products
IQueryable<Category>? categories;
// = db.Categories;
// .Include(c => c.Products);
db.ChangeTracker.LazyLoadingEnabled = false;
Write("Enable eager loading? (Y/N): ");
bool eagerLoading = (ReadKey(intercept: true).Key == ConsoleKey.Y);
bool explicitLoading = false;
WriteLine();
if (eagerLoading)
{
categories = db.Categories?.Include(c => c.Products);
}
else
{
categories = db.Categories;
Write("Enable explicit loading? (Y/N): ");
explicitLoading = (ReadKey(intercept: true).Key == ConsoleKey.Y);
WriteLine();
}
if (categories is null)
{
Fail("No categories found.");
return;
}
// execute query and enumerate results
foreach (Category c in categories)
{
if (explicitLoading)
{
Write($"Explicitly load products for {c.CategoryName}? (Y/N): ");
ConsoleKeyInfo key = ReadKey(intercept: true);
WriteLine();
if (key.Key == ConsoleKey.Y)
{
CollectionEntry<Category, Product> products =
db.Entry(c).Collection(c2 => c2.Products);
if (!products.IsLoaded) products.Load();
}
}
WriteLine($"{c.CategoryName} has {c.Products.Count} products.");
}
}
}
static void FilteredIncludes()
{
using (Northwind db = new())
{
SectionTitle("Products with a minimum number of units in stock.");
string? input;
int stock;
do
{
Write("Enter a minimum for units in stock: ");
input = ReadLine();
} while (!int.TryParse(input, out stock));
IQueryable<Category>? categories = db.Categories?
.Include(c => c.Products.Where(p => p.Stock >= stock));
if (categories is null)
{
Fail("No categories found.");
return;
}
Info($"ToQueryString: {categories.ToQueryString()}");
foreach (Category c in categories)
{
WriteLine($"{c.CategoryName} has {c.Products.Count} products with a minimum of {stock} units in stock.");
foreach (Product p in c.Products)
{
WriteLine($" {p.ProductName} has {p.Stock} units in stock.");
}
}
}
}
static void QueryingProducts()
{
using (Northwind db = new())
{
SectionTitle("Products that cost more than a price, highest at top.");
string? input;
decimal price;
do
{
Write("Enter a product price: ");
input = ReadLine();
} while (!decimal.TryParse(input, out price));
IQueryable<Product>? products = db.Products?
.Where(product => product.Cost > price)
.OrderByDescending(product => product.Cost);
if (products is null)
{
Fail("No products found.");
return;
}
Info($"ToQueryString: {products.ToQueryString()}");
foreach (Product p in products)
{
WriteLine(
"{0}: {1} costs {2:$#,##0.00} and has {3} in stock.",
p.ProductId, p.ProductName, p.Cost, p.Stock);
}
}
}
static void QueryingWithLike()
{
using (Northwind db = new())
{
SectionTitle("Pattern matching with LIKE.");
Write("Enter part of a product name: ");
string? input = ReadLine();
if (string.IsNullOrWhiteSpace(input))
{
Fail("You did not enter part of a product name.");
return;
}
IQueryable<Product>? products = db.Products?
.Where(p => EF.Functions.Like(p.ProductName, $"%{input}%"));
if (products is null)
{
Fail("No products found.");
return;
}
foreach (Product p in products)
{
WriteLine("{0} has {1} units in stock. Discontinued? {2}",
p.ProductName, p.Stock, p.Discontinued);
}
}
}
static void GetRandomProduct()
{
using (Northwind db = new())
{
SectionTitle("Get a random product.");
int? rowCount = db.Products?.Count();
if (rowCount == null)
{
Fail("Products table is empty.");
return;
}
Product? p = db.Products?.FirstOrDefault(
p => p.ProductId == (int)(EF.Functions.Random() * rowCount));
if (p == null)
{
Fail("Product not found.");
return;
}
WriteLine($"Random product: {p.ProductId} {p.ProductName}");
}
}
}

View file

@ -0,0 +1,48 @@
using Packt.Shared;
/*
Northwind db = new();
WriteLine($"Provider: {db.Database.ProviderName}");
*/
// QueryingCategories();
//FilteredIncludes();
//QueryingProducts();
//QueryingWithLike();
//GetRandomProduct();
var resultAdd = AddProduct(categoryId: 6,
productName: "Bob's Burgers", price: 500M);
if (resultAdd.affected == 1)
{
WriteLine("Add product successful.");
}
ListProducts(productIdToHighlight: resultAdd.productId);
var resultUpdate = IncreaseProductPrice(
productNameStartsWith: "Bob", amount: 20M);
if (resultUpdate.affected == 1)
{
WriteLine("Increase product price successful.");
}
ListProducts(productIdToHighlight: resultUpdate.productId);
WriteLine("About to delete all products whose name starts with Bob.");
Write("Press Enter to continue: ");
if (ReadKey(intercept: true).Key == ConsoleKey.Enter)
{
int deleted = DeleteProducts(productNameStartsWith: "Bob");
WriteLine($"{deleted} product(s) were deleted.");
}
else
{
WriteLine("Delete was cancelled.");
}

View file

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Using Include="System.Console" Static="true" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.0-*" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.0-*" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="7.0.0-*" />
</ItemGroup>
<ItemGroup>
<None Update="Northwind.db">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>