From d65678eb00696ce1acac0471def1789c8354105e Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Thu, 28 Dec 2023 12:00:08 +1000 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=2070=20sentinel=20values=20(#78)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added SentinelValues.csproj * Added sentinel values --- EfCoreSamples.sln | 6 ++++++ SentinelValues/Account.cs | 22 ++++++++++++++++++++++ SentinelValues/ApplicationDbContext.cs | 20 ++++++++++++++++++++ SentinelValues/Program.cs | 24 ++++++++++++++++++++++++ SentinelValues/SentinelValues.csproj | 10 ++++++++++ SentinelValues/readme.md | 15 +++++++++++++++ 6 files changed, 97 insertions(+) create mode 100644 SentinelValues/Account.cs create mode 100644 SentinelValues/ApplicationDbContext.cs create mode 100644 SentinelValues/Program.cs create mode 100644 SentinelValues/SentinelValues.csproj create mode 100644 SentinelValues/readme.md diff --git a/EfCoreSamples.sln b/EfCoreSamples.sln index 48253cf..12e8ed9 100644 --- a/EfCoreSamples.sln +++ b/EfCoreSamples.sln @@ -66,6 +66,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnhancedBulkUpdateAndDelete EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HierarchyIds", "HierarchyIds\HierarchyIds.csproj", "{B18EB327-BF8D-4D61-B9F1-8392A9678F55}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SentinelValues", "SentinelValues\SentinelValues.csproj", "{EFE525A3-2D62-4A00-B354-12E1D0BE247D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -188,6 +190,10 @@ Global {B18EB327-BF8D-4D61-B9F1-8392A9678F55}.Debug|Any CPU.Build.0 = Debug|Any CPU {B18EB327-BF8D-4D61-B9F1-8392A9678F55}.Release|Any CPU.ActiveCfg = Release|Any CPU {B18EB327-BF8D-4D61-B9F1-8392A9678F55}.Release|Any CPU.Build.0 = Release|Any CPU + {EFE525A3-2D62-4A00-B354-12E1D0BE247D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EFE525A3-2D62-4A00-B354-12E1D0BE247D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EFE525A3-2D62-4A00-B354-12E1D0BE247D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EFE525A3-2D62-4A00-B354-12E1D0BE247D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/SentinelValues/Account.cs b/SentinelValues/Account.cs new file mode 100644 index 0000000..9ab0399 --- /dev/null +++ b/SentinelValues/Account.cs @@ -0,0 +1,22 @@ +namespace SentinelValues; + +public class Account +{ + public int Id { get; private set; } + + // One way to use sentinel values is to use a nullable backing field + private int? _balance; + public int Balance + { + get => _balance ?? 100; + set => _balance = value; + } + + // Another way to use sentinel values is to use a sentinel value that's been configured in ApplicationDbContext + public int Credits { get; set; } = -1; + + public override string ToString() + { + return $"Id: {Id}, Balance: {Balance}, Credits: {Credits}"; + } +} \ No newline at end of file diff --git a/SentinelValues/ApplicationDbContext.cs b/SentinelValues/ApplicationDbContext.cs new file mode 100644 index 0000000..6d776dd --- /dev/null +++ b/SentinelValues/ApplicationDbContext.cs @@ -0,0 +1,20 @@ +using Common; +using Microsoft.EntityFrameworkCore; + +namespace SentinelValues; + +public class ApplicationDbContext : DbContext +{ + public DbSet Accounts => Set(); + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder + .UseSqlServer(DbConnectionFactory.Create("SentinelValues")); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().Property(a => a.Credits).HasDefaultValue(10).HasSentinel(-1); + } +} \ No newline at end of file diff --git a/SentinelValues/Program.cs b/SentinelValues/Program.cs new file mode 100644 index 0000000..2388baf --- /dev/null +++ b/SentinelValues/Program.cs @@ -0,0 +1,24 @@ +using Microsoft.EntityFrameworkCore; +using SentinelValues; + +Console.WriteLine("Sentinel Values Sample"); + +using var db = new ApplicationDbContext(); +db.Database.EnsureDeleted(); +db.Database.EnsureCreated(); + +var accounts = new List +{ + new(), + new() { Balance = 0, Credits = 0 }, // NOTE: Without Sentinel values this would use the defaults of Balance=100, and Credits=10. + new() { Balance = 100, Credits = 10 }, +}; + +db.Accounts.AddRange(accounts); +db.SaveChanges(); + +var allAccounts = db.Accounts.ToList(); +Console.WriteLine("All Account"); +allAccounts.ForEach(Console.WriteLine); + +Console.ReadLine(); \ No newline at end of file diff --git a/SentinelValues/SentinelValues.csproj b/SentinelValues/SentinelValues.csproj new file mode 100644 index 0000000..2f4fc77 --- /dev/null +++ b/SentinelValues/SentinelValues.csproj @@ -0,0 +1,10 @@ + + + + Exe + net8.0 + enable + enable + + + diff --git a/SentinelValues/readme.md b/SentinelValues/readme.md new file mode 100644 index 0000000..e706ad5 --- /dev/null +++ b/SentinelValues/readme.md @@ -0,0 +1,15 @@ +# Sentinel Values and Database Defaults + +EF Core can configure SQL Server to use Database defaults. For this to work, EF needs to know when NOT to send a value to the DB so that the DB can use the default value. It does this by using the `default` value of the .NET CLR type This works well for reference types, but not value types. + +However, in some cases the CLR default value is a value valid to insert. For example, a default makes sense when creating a record. But what if you want to create a record with the default CLR value? Previously you couldn't. This is where Sentinel Values come in. + +## Use Cases + +- Inserting rows with default CLR values when the DB has a default value +- Correct EF Core behavior when using boolean `default` and `enum` default values +- Overriding defaults for other value types such as `int`, `DateTime`, etc. + +## Resources + +- [EF Core Docs | Sentinel Values and Database Defaults](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-8.0/whatsnew#database-defaults-for-enums)