diff --git a/content/.gitignore b/content/.gitignore
index 8361e8e..aef0b94 100644
--- a/content/.gitignore
+++ b/content/.gitignore
@@ -9,6 +9,9 @@ coverage/
# https://github.com/antfu/unplugin-vue-components/issues/611
components.d.ts
+# Vite temporary compiled config files.
+vite.config.ts.timestamp*
+
# User-specific files
*.suo
*.user
diff --git a/content/.vscode/extensions.json b/content/.vscode/extensions.json
index 9505d30..3c500cb 100644
--- a/content/.vscode/extensions.json
+++ b/content/.vscode/extensions.json
@@ -7,7 +7,6 @@
"streetsidesoftware.code-spell-checker",
"dbaeumer.vscode-eslint",
"vue.volar",
- "vue.vscode-typescript-vue-plugin",
"esbenp.prettier-vscode",
"antfu.goto-alias"
],
diff --git a/content/Coalesce.Starter.Vue.Data.Test/Coalesce.Starter.Vue.Data.Test.csproj b/content/Coalesce.Starter.Vue.Data.Test/Coalesce.Starter.Vue.Data.Test.csproj
new file mode 100644
index 0000000..e253b59
--- /dev/null
+++ b/content/Coalesce.Starter.Vue.Data.Test/Coalesce.Starter.Vue.Data.Test.csproj
@@ -0,0 +1,35 @@
+
+
+
+ net8.0
+
+ false
+ true
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/content/Coalesce.Starter.Vue.Data.Test/UnitTest1.cs b/content/Coalesce.Starter.Vue.Data.Test/UnitTest1.cs
new file mode 100644
index 0000000..d8128f0
--- /dev/null
+++ b/content/Coalesce.Starter.Vue.Data.Test/UnitTest1.cs
@@ -0,0 +1,27 @@
+namespace Coalesce.Starter.Vue.Data.Test;
+
+public class UnitTest1 : TestBase
+{
+ [Fact]
+ public void Test1()
+ {
+ // Arrange
+ var widget1 = new Widget { Name = "Gnoam Sprecklesprocket", Category = WidgetCategory.Sprecklesprockets };
+ Db.Add(widget1);
+ Db.SaveChanges();
+
+ RefreshServices();
+
+ // Act
+ var widget2 = Db.Widgets.Single();
+
+ // Assert
+ Assert.Equal(WidgetCategory.Sprecklesprockets, widget2.Category);
+
+ // After calling RefreshServices, we have a different DbContext instance
+ // and so we'll get a different entity instance.
+ Assert.NotEqual(widget1, widget2);
+
+
+ }
+}
\ No newline at end of file
diff --git a/content/Coalesce.Starter.Vue.Data.Test/Utilities/AppDbContextForSqlite.cs b/content/Coalesce.Starter.Vue.Data.Test/Utilities/AppDbContextForSqlite.cs
new file mode 100644
index 0000000..729ddaa
--- /dev/null
+++ b/content/Coalesce.Starter.Vue.Data.Test/Utilities/AppDbContextForSqlite.cs
@@ -0,0 +1,25 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+namespace Coalesce.Starter.Vue.Data.Test;
+
+public class AppDbContextForSqlite(DbContextOptions options) : AppDbContext(options)
+{
+ protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
+ {
+ // SQLite does not have proper support for DateTimeOffset via Entity Framework Core, see the limitations
+ // here: https://docs.microsoft.com/en-us/ef/core/providers/sqlite/limitations#query-limitations
+ configurationBuilder.Properties().HaveConversion();
+ configurationBuilder.Properties().HaveConversion();
+ }
+
+ public class DateOnlyToDateTimeConverter : ValueConverter
+ {
+ public DateOnlyToDateTimeConverter()
+ : base(
+ dateOnly => dateOnly.ToDateTime(new TimeOnly()),
+ dateTime => DateOnly.FromDateTime(dateTime)
+ )
+ { }
+ }
+}
diff --git a/content/Coalesce.Starter.Vue.Data.Test/Utilities/AssertionExtensions.cs b/content/Coalesce.Starter.Vue.Data.Test/Utilities/AssertionExtensions.cs
new file mode 100644
index 0000000..c789a64
--- /dev/null
+++ b/content/Coalesce.Starter.Vue.Data.Test/Utilities/AssertionExtensions.cs
@@ -0,0 +1,72 @@
+using IntelliTect.Coalesce.Models;
+
+namespace Coalesce.Starter.Vue.Data.Test;
+
+public static class AssertionExtensions
+{
+ ///
+ /// Asserts that the result was a failure.
+ ///
+ public static void AssertError(this ApiResult result)
+ {
+ Assert.False(result.WasSuccessful);
+ }
+
+ ///
+ /// Asserts that the result was a failure.
+ ///
+ /// Expected error message.
+ public static void AssertError(this ApiResult result, string message)
+ {
+ result.AssertError();
+ Assert.Equal(message, result.Message);
+ }
+
+ ///
+ /// Asserts that the result was successful.
+ ///
+ public static void AssertSuccess(this ApiResult result, string? message = null)
+ {
+ // Returns a more useful assertion error than only checking WasSuccessful.
+ Assert.Equal(message, result.Message);
+ Assert.True(result.WasSuccessful);
+ }
+
+ ///
+ /// Asserts that the result was successful.
+ ///
+ public static T AssertSuccess(this ItemResult result)
+ {
+ Assert.Null(result.Message);
+ Assert.True(result.WasSuccessful);
+ return result.Object ?? throw new ArgumentException("Sucessful result unexpectedly returned null object");
+ }
+
+ ///
+ /// Asserts that the result was successful.
+ ///
+ public static async Task AssertSuccess(this Task> resultTask)
+ {
+ var result = await resultTask;
+ return result.AssertSuccess();
+ }
+
+ ///
+ /// Asserts that the result was successful.
+ ///
+ public static async Task AssertSuccess(this Task resultTask)
+ {
+ var result = await resultTask;
+ Assert.True(result.WasSuccessful);
+ }
+
+ ///
+ /// Asserts that the result was successful.
+ ///
+ /// Expected value on the result.
+ public static void AssertSuccess(this ItemResult result, T expectedValue)
+ {
+ result.AssertSuccess();
+ Assert.Equal(expectedValue, result.Object);
+ }
+}
diff --git a/content/Coalesce.Starter.Vue.Data.Test/Utilities/SqliteDatabaseFixture.cs b/content/Coalesce.Starter.Vue.Data.Test/Utilities/SqliteDatabaseFixture.cs
new file mode 100644
index 0000000..4a661fe
--- /dev/null
+++ b/content/Coalesce.Starter.Vue.Data.Test/Utilities/SqliteDatabaseFixture.cs
@@ -0,0 +1,52 @@
+using Microsoft.Data.Sqlite;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+
+namespace Coalesce.Starter.Vue.Data.Test;
+
+public class SqliteDatabaseFixture : IDisposable
+{
+ private readonly SqliteConnection _HoldOpenConnection;
+
+ public DbContextOptions Options { get; }
+
+ private static readonly ILoggerFactory _LoggerFac = LoggerFactory.Create(b =>
+ {
+ b.SetMinimumLevel(LogLevel.Error);
+ b.AddConsole();
+ });
+
+ public SqliteDatabaseFixture()
+ {
+ // Use a unique database instance per test.
+ var connString = $"Data Source=A{Guid.NewGuid():N};Mode=Memory;Cache=Shared";
+
+ // Per https://docs.microsoft.com/en-us/dotnet/standard/data/sqlite/in-memory-databases#shareable-in-memory-databases,
+ // a connection must be kept open in order to preserve a particular in-memory SQLite instance.
+ // EF doesn't hold connections open necessarily, so we'll do this ourselves.
+ _HoldOpenConnection = new SqliteConnection(connString);
+ _HoldOpenConnection.Open();
+
+ var dbOptionBuilder = new DbContextOptionsBuilder();
+ dbOptionBuilder.UseSqlite(connString);
+ dbOptionBuilder.UseLoggerFactory(_LoggerFac);
+ dbOptionBuilder.EnableDetailedErrors(true);
+ dbOptionBuilder.EnableSensitiveDataLogging(true);
+
+ Options = dbOptionBuilder.Options;
+ using var db = new AppDbContextForSqlite(Options);
+
+ db.Database.EnsureCreated();
+ Seed();
+ }
+
+ public void Seed()
+ {
+ // Seed baseline test data, if desired.
+ }
+
+ public void Dispose()
+ {
+ _HoldOpenConnection.Dispose();
+ }
+}
diff --git a/content/Coalesce.Starter.Vue.Data.Test/Utilities/TestBase.cs b/content/Coalesce.Starter.Vue.Data.Test/Utilities/TestBase.cs
new file mode 100644
index 0000000..4f19995
--- /dev/null
+++ b/content/Coalesce.Starter.Vue.Data.Test/Utilities/TestBase.cs
@@ -0,0 +1,115 @@
+using IntelliTect.Coalesce;
+using IntelliTect.Coalesce.TypeDefinition;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Caching.Memory;
+using Moq;
+using Moq.AutoMock;
+using System.Security.Claims;
+
+namespace Coalesce.Starter.Vue.Data.Test;
+
+public class TestBase : IDisposable
+{
+ protected SqliteDatabaseFixture DbFixture { get; }
+
+ private MockerScope? _CurrentMocker;
+
+ protected AutoMocker Mocker => _CurrentMocker ?? throw new InvalidOperationException("The current mocker has been disposed.");
+
+ protected AppDbContext Db => Mocker.Get();
+
+ protected ClaimsPrincipal CurrentUser { get; set; } = new();
+
+ public TestBase()
+ {
+ ReflectionRepository.Global.AddAssembly();
+
+ DbFixture = new SqliteDatabaseFixture();
+
+ _CurrentMocker = BeginMockScope(standalone: false);
+ }
+
+ public class MockerScope : AutoMocker, IDisposable
+ {
+ private readonly TestBase? _Parent;
+
+ public MockerScope(TestBase? parent) : base(MockBehavior.Loose)
+ {
+ _Parent = parent;
+ if (parent != null) parent._CurrentMocker = this;
+ }
+
+ public void Dispose()
+ {
+ if (_Parent == null) return;
+ Interlocked.CompareExchange(ref _Parent._CurrentMocker, null, this);
+ this.AsDisposable().Dispose();
+ }
+ }
+
+ ///
+ ///
+ /// Create a new , allowing for new instances of all services
+ /// to be obtained - especially a new .
+ /// Persistence mechanisms are maintained, including the same .
+ ///
+ ///
+ /// Doing this allows you to verify that test setup steps haven't polluted the state
+ /// of services in such a way that will cause the test to behave differently than it would
+ /// in a real scenario.
+ ///
+ ///
+ /// For example, you should call this after setting up test data, and also in multi-step tests
+ /// where the steps would normally be performed by separate web requests/background jobs/etc.
+ ///
+ ///
+ protected void RefreshServices()
+ {
+ _CurrentMocker?.Dispose();
+ BeginMockScope(standalone: false);
+ }
+
+ ///
+ /// Create an standalone mocker instance that can be used to acquire services.
+ /// Usually you should use the current mock scope in ,
+ /// resetting it with as needed.
+ /// Only create an independent scope for operations like parallel processing
+ /// (an unusual thing to be doing in unit tests).
+ ///
+ protected MockerScope BeginMockScope() => BeginMockScope(true);
+
+ private MockerScope BeginMockScope(bool standalone = false)
+ {
+ var mocker = new MockerScope(standalone ? null : this);
+ var db = new AppDbContextForSqlite(DbFixture.Options);
+ mocker.Use(DbFixture.Options);
+ mocker.Use(db);
+
+ mocker.Use>(new CrudContext(
+ db,
+ () => CurrentUser
+ ));
+
+ mocker.GetMock>()
+ .Setup(x => x.CreateDbContextAsync(It.IsAny()))
+ .ReturnsAsync(() => new AppDbContextForSqlite(DbFixture.Options));
+
+ mocker.GetMock>()
+ .Setup(x => x.CreateDbContext())
+ .Returns(() => new AppDbContextForSqlite(DbFixture.Options));
+
+ mocker.Use(new MemoryCache(new MemoryCacheOptions()));
+
+ // Register additional services required by tests,
+ // preferring real implementations where possible
+ // in order to improve test fidelity.
+
+ return mocker;
+ }
+
+ public virtual void Dispose()
+ {
+ _CurrentMocker?.Dispose();
+ DbFixture.Dispose();
+ }
+}
diff --git a/content/Coalesce.Starter.Vue.Data/Coalesce.Starter.Vue.Data.csproj b/content/Coalesce.Starter.Vue.Data/Coalesce.Starter.Vue.Data.csproj
index e44f2cd..470b983 100644
--- a/content/Coalesce.Starter.Vue.Data/Coalesce.Starter.Vue.Data.csproj
+++ b/content/Coalesce.Starter.Vue.Data/Coalesce.Starter.Vue.Data.csproj
@@ -4,12 +4,11 @@
-
-
-
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/content/Coalesce.Starter.Vue.Data/Models/Widget.cs b/content/Coalesce.Starter.Vue.Data/Models/Widget.cs
index ee24fd2..8be167f 100644
--- a/content/Coalesce.Starter.Vue.Data/Models/Widget.cs
+++ b/content/Coalesce.Starter.Vue.Data/Models/Widget.cs
@@ -16,5 +16,4 @@ public enum WidgetCategory
Whizbangs,
Sprecklesprockets,
Discombobulators,
-
}
diff --git a/content/Coalesce.Starter.Vue.Web/.vscode/extensions.json b/content/Coalesce.Starter.Vue.Web/.vscode/extensions.json
index 9505d30..3c500cb 100644
--- a/content/Coalesce.Starter.Vue.Web/.vscode/extensions.json
+++ b/content/Coalesce.Starter.Vue.Web/.vscode/extensions.json
@@ -7,7 +7,6 @@
"streetsidesoftware.code-spell-checker",
"dbaeumer.vscode-eslint",
"vue.volar",
- "vue.vscode-typescript-vue-plugin",
"esbenp.prettier-vscode",
"antfu.goto-alias"
],
diff --git a/content/Coalesce.Starter.Vue.Web/Coalesce.Starter.Vue.Web.csproj b/content/Coalesce.Starter.Vue.Web/Coalesce.Starter.Vue.Web.csproj
index 440012d..ff687da 100644
--- a/content/Coalesce.Starter.Vue.Web/Coalesce.Starter.Vue.Web.csproj
+++ b/content/Coalesce.Starter.Vue.Web/Coalesce.Starter.Vue.Web.csproj
@@ -9,7 +9,7 @@
-
+
diff --git a/content/Coalesce.Starter.Vue.Web/src/components/HelloWorld.vue b/content/Coalesce.Starter.Vue.Web/src/components/HelloWorld.vue
index 672df74..b956b2e 100644
--- a/content/Coalesce.Starter.Vue.Web/src/components/HelloWorld.vue
+++ b/content/Coalesce.Starter.Vue.Web/src/components/HelloWorld.vue
@@ -128,21 +128,9 @@
- Vue Language Features (Volar)
+ Vue - Official
-
-
- TypeScript Vue Plugin (Volar)
-
- (alternatively, use Volar
- takeover mode)
-
true
true
4.9
+
+ 4.0.0-alpha.20240502.1
diff --git a/content/Directory.Packages.props b/content/Directory.Packages.props
deleted file mode 100644
index 974f891..0000000
--- a/content/Directory.Packages.props
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
- true
- false
-
- 4.0.0-alpha.20231221.1
-
-
-
-
-
-
-
-
-