diff --git a/MADE.NET.sln b/MADE.NET.sln
index 37709251..a4a61427 100644
--- a/MADE.NET.sln
+++ b/MADE.NET.sln
@@ -51,6 +51,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MADE.Data.Serialization", "
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MADE.Data.Serialization.Tests", "tests\MADE.Data.Serialization.Tests\MADE.Data.Serialization.Tests.csproj", "{7D789D04-A010-4F11-91AD-B1B94A23BAE0}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MADE.Data.EFCore.Tests", "tests\MADE.Data.EFCore.Tests\MADE.Data.EFCore.Tests.csproj", "{7ECFE1EE-A42A-495E-BBB1-A34C95F70413}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Ad-Hoc|Any CPU = Ad-Hoc|Any CPU
@@ -1315,6 +1317,62 @@ Global
{7D789D04-A010-4F11-91AD-B1B94A23BAE0}.Release|x64.Build.0 = Release|Any CPU
{7D789D04-A010-4F11-91AD-B1B94A23BAE0}.Release|x86.ActiveCfg = Release|Any CPU
{7D789D04-A010-4F11-91AD-B1B94A23BAE0}.Release|x86.Build.0 = Release|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Ad-Hoc|ARM64.ActiveCfg = Debug|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Ad-Hoc|ARM64.Build.0 = Debug|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Ad-Hoc|x64.Build.0 = Debug|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Ad-Hoc|x86.Build.0 = Debug|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.AppStore|Any CPU.Build.0 = Debug|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.AppStore|ARM.ActiveCfg = Debug|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.AppStore|ARM.Build.0 = Debug|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.AppStore|ARM64.ActiveCfg = Debug|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.AppStore|ARM64.Build.0 = Debug|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.AppStore|iPhone.Build.0 = Debug|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.AppStore|x64.ActiveCfg = Debug|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.AppStore|x64.Build.0 = Debug|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.AppStore|x86.ActiveCfg = Debug|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.AppStore|x86.Build.0 = Debug|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Debug|ARM.ActiveCfg = Debug|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Debug|ARM.Build.0 = Debug|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Debug|iPhone.Build.0 = Debug|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Debug|x64.Build.0 = Debug|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Debug|x86.Build.0 = Debug|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Release|ARM.ActiveCfg = Release|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Release|ARM.Build.0 = Release|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Release|ARM64.Build.0 = Release|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Release|iPhone.ActiveCfg = Release|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Release|iPhone.Build.0 = Release|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Release|x64.ActiveCfg = Release|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Release|x64.Build.0 = Release|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Release|x86.ActiveCfg = Release|Any CPU
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1342,6 +1400,7 @@ Global
{D8C1B3CC-B5BA-4946-944E-D898AB4DFF6E} = {69149D0F-BB09-411B-88F0-A1E845058D70}
{0ACCC377-5FA5-47D9-B6EF-7936F1038B90} = {01380FB8-F8A7-4416-AABA-5407574B7723}
{7D789D04-A010-4F11-91AD-B1B94A23BAE0} = {69149D0F-BB09-411B-88F0-A1E845058D70}
+ {7ECFE1EE-A42A-495E-BBB1-A34C95F70413} = {69149D0F-BB09-411B-88F0-A1E845058D70}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3921AD86-E6C0-4436-8880-2D9EDFAD6151}
diff --git a/src/MADE.Data.EFCore/EntityBase{TKey}.cs b/src/MADE.Data.EFCore/EntityBase{TKey}.cs
new file mode 100644
index 00000000..1cb1f4dd
--- /dev/null
+++ b/src/MADE.Data.EFCore/EntityBase{TKey}.cs
@@ -0,0 +1,31 @@
+// MADE Apps licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace MADE.Data.EFCore
+{
+ using System;
+ using System.ComponentModel.DataAnnotations.Schema;
+
+ ///
+ /// Defines a base definition for an entity.
+ ///
+ /// The type of unique identifier for the entity.
+ public abstract class EntityBase : IEntityBase
+ {
+ ///
+ /// Gets or sets the identifier of the entity.
+ ///
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public TKey Id { get; set; }
+
+ ///
+ /// Gets or sets the date of the entity's creation.
+ ///
+ public DateTime CreatedDate { get; set; }
+
+ ///
+ /// Gets or sets the date of the entity's last update.
+ ///
+ public DateTime? UpdatedDate { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/MADE.Data.EFCore/Extensions/DbContextExtensions.cs b/src/MADE.Data.EFCore/Extensions/DbContextExtensions.cs
index a85504cd..31a38706 100644
--- a/src/MADE.Data.EFCore/Extensions/DbContextExtensions.cs
+++ b/src/MADE.Data.EFCore/Extensions/DbContextExtensions.cs
@@ -64,14 +64,14 @@ public static void SetEntityDates(this DbContext context)
IEnumerable entries = context.ChangeTracker
.Entries()
.Where(
- entry => entry.Entity is IEntityBase &&
+ entry => entry.Entity is IDatedEntity &&
entry.State is EntityState.Added or EntityState.Modified);
DateTime now = DateTime.UtcNow;
foreach (EntityEntry entry in entries)
{
- var entity = (IEntityBase)entry.Entity;
+ var entity = (IDatedEntity)entry.Entity;
entity.UpdatedDate = now;
if (entry.State == EntityState.Added && entity.CreatedDate == DateTime.MinValue)
diff --git a/src/MADE.Data.EFCore/Extensions/EntityBaseExtensions.cs b/src/MADE.Data.EFCore/Extensions/EntityBaseExtensions.cs
index 926d3247..8ae0f13d 100644
--- a/src/MADE.Data.EFCore/Extensions/EntityBaseExtensions.cs
+++ b/src/MADE.Data.EFCore/Extensions/EntityBaseExtensions.cs
@@ -3,6 +3,7 @@
namespace MADE.Data.EFCore.Extensions
{
+ using System;
using MADE.Data.EFCore.Converters;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
@@ -19,6 +20,20 @@ public static class EntityBaseExtensions
/// The entity type builder.
public static EntityTypeBuilder Configure(this EntityTypeBuilder builder)
where TEntity : class, IEntityBase
+ {
+ builder.ConfigureWithKey();
+ return builder;
+ }
+
+ ///
+ /// Configures the default properties of an entity.
+ ///
+ /// The type of entity to configure.
+ /// The type of unique identifier for the entity.
+ /// The entity type builder associated with the entity.
+ /// The entity type builder.
+ public static EntityTypeBuilder ConfigureWithKey(this EntityTypeBuilder builder)
+ where TEntity : class, IEntityBase
{
builder.HasKey(e => e.Id);
builder.ConfigureDateProperties();
@@ -33,7 +48,7 @@ public static EntityTypeBuilder Configure(this EntityTypeBuild
/// The entity type builder.
public static EntityTypeBuilder ConfigureDateProperties(
this EntityTypeBuilder builder)
- where TEntity : class, IEntityBase
+ where TEntity : class, IDatedEntity
{
builder.Property(x => x.CreatedDate).IsUtc();
builder.Property(x => x.UpdatedDate).IsUtc();
diff --git a/src/MADE.Data.EFCore/Extensions/QueryableExtensions.cs b/src/MADE.Data.EFCore/Extensions/QueryableExtensions.cs
index 714613e0..244bd0b2 100644
--- a/src/MADE.Data.EFCore/Extensions/QueryableExtensions.cs
+++ b/src/MADE.Data.EFCore/Extensions/QueryableExtensions.cs
@@ -34,9 +34,12 @@ public static IQueryable Page(this IQueryable query, int page, int page
/// The ordered query.
public static IQueryable OrderBy(this IQueryable query, string sortName, bool sortDesc)
{
- return string.IsNullOrWhiteSpace(sortName)
- ? query
- : (!sortDesc ? query.AddOrAppendOrderBy(sortName) : query.AddOrAppendOrderByDescending(sortName));
+ if (string.IsNullOrWhiteSpace(sortName))
+ {
+ return query;
+ }
+
+ return !sortDesc ? query.AddOrAppendOrderBy(sortName) : query.AddOrAppendOrderByDescending(sortName);
}
}
}
\ No newline at end of file
diff --git a/src/MADE.Data.EFCore/IDatedEntity.cs b/src/MADE.Data.EFCore/IDatedEntity.cs
new file mode 100644
index 00000000..684b927a
--- /dev/null
+++ b/src/MADE.Data.EFCore/IDatedEntity.cs
@@ -0,0 +1,23 @@
+// MADE Apps licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace MADE.Data.EFCore
+{
+ using System;
+
+ ///
+ /// Defines a base definition for an entity with defined created and updated date.
+ ///
+ public interface IDatedEntity
+ {
+ ///
+ /// Gets or sets the date of the entity's creation.
+ ///
+ DateTime CreatedDate { get; set; }
+
+ ///
+ /// Gets or sets the date of the entity's last update.
+ ///
+ DateTime? UpdatedDate { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/MADE.Data.EFCore/IEntityBase.cs b/src/MADE.Data.EFCore/IEntityBase.cs
index 457295e5..2f526e6a 100644
--- a/src/MADE.Data.EFCore/IEntityBase.cs
+++ b/src/MADE.Data.EFCore/IEntityBase.cs
@@ -8,21 +8,7 @@ namespace MADE.Data.EFCore
///
/// Defines a base definition for an entity.
///
- public interface IEntityBase
+ public interface IEntityBase : IEntityBase
{
- ///
- /// Gets or sets the identifier of the entity.
- ///
- Guid Id { get; set; }
-
- ///
- /// Gets or sets the date of the entity's creation.
- ///
- DateTime CreatedDate { get; set; }
-
- ///
- /// Gets or sets the date of the entity's last update.
- ///
- DateTime? UpdatedDate { get; set; }
}
}
\ No newline at end of file
diff --git a/src/MADE.Data.EFCore/IEntityBase{TKey}.cs b/src/MADE.Data.EFCore/IEntityBase{TKey}.cs
new file mode 100644
index 00000000..c3e30083
--- /dev/null
+++ b/src/MADE.Data.EFCore/IEntityBase{TKey}.cs
@@ -0,0 +1,17 @@
+// MADE Apps licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace MADE.Data.EFCore
+{
+ ///
+ /// Defines a base definition for an entity with a defined primary key type.
+ ///
+ /// The type of unique identifier for the entity.
+ public interface IEntityBase : IDatedEntity
+ {
+ ///
+ /// Gets or sets the identifier of the entity.
+ ///
+ TKey Id { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/tests/MADE.Data.EFCore.Tests/Data/TestDbContext.cs b/tests/MADE.Data.EFCore.Tests/Data/TestDbContext.cs
new file mode 100644
index 00000000..47034b4f
--- /dev/null
+++ b/tests/MADE.Data.EFCore.Tests/Data/TestDbContext.cs
@@ -0,0 +1,72 @@
+namespace MADE.Data.EFCore.Tests.Data
+{
+ using System.Diagnostics.CodeAnalysis;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Converters;
+ using Extensions;
+ using Microsoft.EntityFrameworkCore;
+ using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+ [ExcludeFromCodeCoverage]
+ public class TestDbContext : DbContext
+ {
+ public DbSet Entities { get; set; }
+
+ public DbSet KeyEntities { get; set; }
+
+ public TestDbContext(DbContextOptions options) : base(options) { }
+
+ public static TestDbContext CreateInMemoryContext(string dbName)
+ {
+ var optionsBuilder = new DbContextOptionsBuilder();
+ DbContextOptions options = optionsBuilder.UseInMemoryDatabase(dbName).Options;
+ return new TestDbContext(options);
+ }
+
+ public override Task SaveChangesAsync(CancellationToken cancellationToken = default)
+ {
+ this.SetEntityDates();
+ return base.SaveChangesAsync(cancellationToken);
+ }
+
+ public override Task SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
+ {
+ this.SetEntityDates();
+ return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
+ }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.HasDefaultSchema("dbo");
+ modelBuilder.ApplyConfigurationsFromAssembly(typeof(TestDbContext).Assembly);
+ modelBuilder.ApplyUtcDateTimeConverter();
+ }
+ }
+
+ public class TestEntity : EntityBase
+ {
+ public string Name { get; set; }
+ }
+
+ public class TestKeyedEntity : EntityBase
+ {
+ public string Name { get; set; }
+ }
+
+ public class TestEntityTypeConfiguration : IEntityTypeConfiguration
+ {
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.Configure();
+ }
+ }
+
+ public class TestKeyedEntityTypeConfiguration : IEntityTypeConfiguration
+ {
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.ConfigureWithKey();
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/MADE.Data.EFCore.Tests/MADE.Data.EFCore.Tests.csproj b/tests/MADE.Data.EFCore.Tests/MADE.Data.EFCore.Tests.csproj
new file mode 100644
index 00000000..b75e50bb
--- /dev/null
+++ b/tests/MADE.Data.EFCore.Tests/MADE.Data.EFCore.Tests.csproj
@@ -0,0 +1,27 @@
+
+
+
+ net6.0
+ false
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/MADE.Data.EFCore.Tests/Tests/EntityBaseTests.cs b/tests/MADE.Data.EFCore.Tests/Tests/EntityBaseTests.cs
new file mode 100644
index 00000000..e5bd47ce
--- /dev/null
+++ b/tests/MADE.Data.EFCore.Tests/Tests/EntityBaseTests.cs
@@ -0,0 +1,56 @@
+namespace MADE.Data.EFCore.Tests.Tests
+{
+ using System;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Threading.Tasks;
+ using Data;
+ using Extensions;
+ using NUnit.Framework;
+ using Shouldly;
+
+ [TestFixture]
+ [ExcludeFromCodeCoverage]
+ public class EntityBaseTests
+ {
+ public class WhenSavingToDbContext
+ {
+ [Test]
+ public async Task ShouldSetEntityBaseDates()
+ {
+ // Arrange
+ var dbContext = TestDbContext.CreateInMemoryContext("ShouldSetEntityBaseDates");
+
+ var entity = new TestEntity { Id = Guid.NewGuid(), Name = "Test" };
+
+ await dbContext.AddAsync(entity);
+
+ // Act
+ await dbContext.TrySaveChangesAsync();
+
+ // Assert
+ entity.CreatedDate.ShouldBeInRange(DateTime.UtcNow.AddSeconds(-1), DateTime.UtcNow.AddSeconds(1));
+ entity.UpdatedDate.ShouldNotBeNull();
+ entity.UpdatedDate.Value.ShouldBeInRange(DateTime.UtcNow.AddSeconds(-1), DateTime.UtcNow.AddSeconds(1));
+ }
+
+ [Test]
+ public async Task ShouldSetKeyedEntityBaseDates()
+ {
+ // Arrange
+ var dbContext = TestDbContext.CreateInMemoryContext("ShouldSetKeyedEntityBaseDates");
+
+ var entity = new TestKeyedEntity { Id = 1, Name = "Test" };
+
+ await dbContext.AddAsync(entity);
+
+ // Act
+ await dbContext.TrySaveChangesAsync();
+
+ // Assert
+ entity.CreatedDate.ShouldBeInRange(DateTime.UtcNow.AddSeconds(-1), DateTime.UtcNow.AddSeconds(1));
+ entity.UpdatedDate.ShouldNotBeNull();
+ entity.UpdatedDate.Value.ShouldBeInRange(DateTime.UtcNow.AddSeconds(-1), DateTime.UtcNow.AddSeconds(1));
+ }
+ }
+ }
+}
\ No newline at end of file