Fix category shadow foreign keys and .NET 10 Docker build context
Build MoneyMap image / build-and-push (push) Failing after 7s
Build MoneyMap image / build-and-push (push) Failing after 7s
This commit is contained in:
@@ -0,0 +1,28 @@
|
|||||||
|
# Build with the repository root as context.
|
||||||
|
.git
|
||||||
|
**/bin
|
||||||
|
**/obj
|
||||||
|
**/.vs
|
||||||
|
**/.idea
|
||||||
|
**/TestResults
|
||||||
|
**/*.user
|
||||||
|
**/*.suo
|
||||||
|
**/*.log
|
||||||
|
**/node_modules
|
||||||
|
|
||||||
|
# Runtime configuration and credentials must be supplied outside the image.
|
||||||
|
**/appsettings*.json
|
||||||
|
**/secrets.json
|
||||||
|
**/.env
|
||||||
|
**/.env.*
|
||||||
|
**/*.pfx
|
||||||
|
**/*.pem
|
||||||
|
**/*.key
|
||||||
|
|
||||||
|
# Local receipts, database files, and backups are not application assets.
|
||||||
|
**/receipts
|
||||||
|
**/*.db
|
||||||
|
**/*.db-*
|
||||||
|
**/*.mdf
|
||||||
|
**/*.ldf
|
||||||
|
**/*.bak
|
||||||
@@ -7,7 +7,8 @@ on:
|
|||||||
- "MoneyMap/**"
|
- "MoneyMap/**"
|
||||||
- "MoneyMap.Core/**"
|
- "MoneyMap.Core/**"
|
||||||
- "MoneyMap.Mcp/**"
|
- "MoneyMap.Mcp/**"
|
||||||
- "Dockerfile"
|
- "MoneyMap.sln"
|
||||||
|
- ".dockerignore"
|
||||||
- ".gitea/workflows/build-moneymap.yml"
|
- ".gitea/workflows/build-moneymap.yml"
|
||||||
workflow_dispatch: {}
|
workflow_dispatch: {}
|
||||||
|
|
||||||
@@ -21,7 +22,7 @@ jobs:
|
|||||||
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.thecozycat.net -u "${{ gitea.actor }}" --password-stdin
|
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.thecozycat.net -u "${{ gitea.actor }}" --password-stdin
|
||||||
|
|
||||||
- name: Build image
|
- name: Build image
|
||||||
run: docker build -f MoneyMap/Dockerfile -t git.thecozycat.net/${{ gitea.repository_owner }}/moneymap:latest -t git.thecozycat.net/${{ gitea.repository_owner }}/moneymap:${{ gitea.sha }} MoneyMap/
|
run: docker build -f MoneyMap/Dockerfile -t git.thecozycat.net/${{ gitea.repository_owner }}/moneymap:latest -t git.thecozycat.net/${{ gitea.repository_owner }}/moneymap:${{ gitea.sha }} .
|
||||||
|
|
||||||
- name: Push image
|
- name: Push image
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -1801,6 +1801,34 @@ var importer = new TransactionImporter(mockDb.Object, mockCardResolver.Object);
|
|||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
|
### Docker image build (.NET 10)
|
||||||
|
|
||||||
|
Run from the repository root (not `MoneyMap/`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -f MoneyMap/Dockerfile -t moneymap:local .
|
||||||
|
```
|
||||||
|
|
||||||
|
The Dockerfile uses the .NET 10 SDK and ASP.NET Core 10 runtime. The root
|
||||||
|
context is required to copy `MoneyMap.sln` and the shared `MoneyMap.Core`
|
||||||
|
project. The root `.dockerignore` excludes local build output, receipts,
|
||||||
|
database files, and all `appsettings*.json`/credential files. Supply runtime
|
||||||
|
configuration separately via environment variables (for example
|
||||||
|
`ConnectionStrings__MoneyMapDb`, `OPENAI_API_KEY`) or a mounted
|
||||||
|
configuration file; credentials are not baked into the image. Building does
|
||||||
|
not start the application or apply migrations.
|
||||||
|
|
||||||
|
### Category relationship mapping
|
||||||
|
|
||||||
|
`Transaction.Category` and `CategoryMapping.Category` remain string category
|
||||||
|
names. The corresponding collections on `Category` are `[NotMapped]` so EF
|
||||||
|
cannot infer nonexistent `CategoryId` shadow foreign keys. The real
|
||||||
|
`RecurringPlace.CategoryId` and parent/child category relationships remain
|
||||||
|
mapped. This is a model correction only, with no database schema change or
|
||||||
|
migration required. SQL Server model/query regression tests compile both
|
||||||
|
receipt duplicate-check queries without connecting to a database.
|
||||||
|
|
||||||
|
|
||||||
### Database Migration
|
### Database Migration
|
||||||
```bash
|
```bash
|
||||||
# Create migration
|
# Create migration
|
||||||
|
|||||||
@@ -25,7 +25,11 @@ public class Category
|
|||||||
public ICollection<Category> Children { get; set; } = new List<Category>();
|
public ICollection<Category> Children { get; set; } = new List<Category>();
|
||||||
|
|
||||||
public ICollection<RecurringPlace> RecurringPlaces { get; set; } = new List<RecurringPlace>();
|
public ICollection<RecurringPlace> RecurringPlaces { get; set; } = new List<RecurringPlace>();
|
||||||
|
// These entities store category names, not Category foreign keys. Mapping these
|
||||||
|
// collections would make EF invent CategoryId columns absent from the schema.
|
||||||
|
[NotMapped]
|
||||||
public ICollection<CategoryMapping> CategoryMappings { get; set; } = new List<CategoryMapping>();
|
public ICollection<CategoryMapping> CategoryMappings { get; set; } = new List<CategoryMapping>();
|
||||||
|
[NotMapped]
|
||||||
public ICollection<Transaction> Transactions { get; set; } = new List<Transaction>();
|
public ICollection<Transaction> Transactions { get; set; } = new List<Transaction>();
|
||||||
|
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using MoneyMap.Data;
|
||||||
|
using MoneyMap.Models;
|
||||||
|
|
||||||
|
namespace MoneyMap.Tests.Data;
|
||||||
|
|
||||||
|
public class CategoryModelTests
|
||||||
|
{
|
||||||
|
// Model/query compilation only: never opens a connection or reads application configuration.
|
||||||
|
private static MoneyMapContext CreateContext() => new(
|
||||||
|
new DbContextOptionsBuilder<MoneyMapContext>()
|
||||||
|
.UseSqlServer("Server=localhost;Database=ModelOnly;Integrated Security=true;TrustServerCertificate=true")
|
||||||
|
.Options);
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(typeof(Transaction))]
|
||||||
|
[InlineData(typeof(CategoryMapping))]
|
||||||
|
public void StringCategoryEntities_DoNotHaveCategoryForeignKeys(Type entityType)
|
||||||
|
{
|
||||||
|
using var context = CreateContext();
|
||||||
|
var entity = context.Model.FindEntityType(entityType)!;
|
||||||
|
|
||||||
|
Assert.Null(entity.FindProperty("CategoryId"));
|
||||||
|
Assert.DoesNotContain(entity.GetProperties(), p => p.IsShadowProperty());
|
||||||
|
Assert.Equal(typeof(string), entity.FindProperty("Category")!.ClrType);
|
||||||
|
Assert.DoesNotContain(entity.GetForeignKeys(), fk => fk.PrincipalEntityType.ClrType == typeof(Category));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(true)]
|
||||||
|
[InlineData(false)]
|
||||||
|
public void ReceiptDuplicateQueries_DoNotSelectCategoryId(bool matchHash)
|
||||||
|
{
|
||||||
|
using var context = CreateContext();
|
||||||
|
var query = context.Receipts.Include(r => r.Transaction);
|
||||||
|
var sql = (matchHash
|
||||||
|
? query.Where(r => r.FileHashSha256 == "test-hash")
|
||||||
|
: query.Where(r => r.FileName == "receipt.png" && r.FileSizeBytes == 8))
|
||||||
|
.ToQueryString();
|
||||||
|
|
||||||
|
Assert.Contains("LEFT JOIN [Transactions]", sql);
|
||||||
|
Assert.Contains("[Category]", sql);
|
||||||
|
Assert.DoesNotContain("[CategoryId]", sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CategoryMappingQuery_DoesNotSelectCategoryId()
|
||||||
|
{
|
||||||
|
using var context = CreateContext();
|
||||||
|
var sql = context.CategoryMappings.ToQueryString();
|
||||||
|
Assert.Contains("[Category]", sql);
|
||||||
|
Assert.DoesNotContain("[CategoryId]", sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IntendedCategoryRelationships_ArePreserved()
|
||||||
|
{
|
||||||
|
using var context = CreateContext();
|
||||||
|
var recurringPlace = context.Model.FindEntityType(typeof(RecurringPlace))!;
|
||||||
|
Assert.Contains(recurringPlace.GetForeignKeys(), fk =>
|
||||||
|
fk.PrincipalEntityType.ClrType == typeof(Category) &&
|
||||||
|
fk.Properties.Single().Name == "CategoryId" && !fk.Properties.Single().IsShadowProperty());
|
||||||
|
var category = context.Model.FindEntityType(typeof(Category))!;
|
||||||
|
Assert.Contains(category.GetForeignKeys(), fk =>
|
||||||
|
fk.PrincipalEntityType.ClrType == typeof(Category) &&
|
||||||
|
fk.Properties.Single().Name == "ParentCategoryId");
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
# Build stage
|
# Build stage
|
||||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
# Copy solution and project files for restore
|
# Copy solution and project files for restore
|
||||||
@@ -19,7 +19,7 @@ RUN libman restore
|
|||||||
RUN dotnet publish -c Release -o /app/publish
|
RUN dotnet publish -c Release -o /app/publish
|
||||||
|
|
||||||
# Runtime stage
|
# Runtime stage
|
||||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
|
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install ImageMagick dependencies for Magick.NET
|
# Install ImageMagick dependencies for Magick.NET
|
||||||
|
|||||||
Reference in New Issue
Block a user