Compare commits
1 Commits
main
..
27b040882c
| Author | SHA1 | Date | |
|---|---|---|---|
| 27b040882c |
@@ -1,63 +0,0 @@
|
|||||||
###############################################################################
|
|
||||||
# Set default behavior to automatically normalize line endings.
|
|
||||||
###############################################################################
|
|
||||||
* text=auto
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
# Set default behavior for command prompt diff.
|
|
||||||
#
|
|
||||||
# This is need for earlier builds of msysgit that does not have it on by
|
|
||||||
# default for csharp files.
|
|
||||||
# Note: This is only used by command line
|
|
||||||
###############################################################################
|
|
||||||
#*.cs diff=csharp
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
# Set the merge driver for project and solution files
|
|
||||||
#
|
|
||||||
# Merging from the command prompt will add diff markers to the files if there
|
|
||||||
# are conflicts (Merging from VS is not affected by the settings below, in VS
|
|
||||||
# the diff markers are never inserted). Diff markers may cause the following
|
|
||||||
# file extensions to fail to load in VS. An alternative would be to treat
|
|
||||||
# these files as binary and thus will always conflict and require user
|
|
||||||
# intervention with every merge. To do so, just uncomment the entries below
|
|
||||||
###############################################################################
|
|
||||||
#*.sln merge=binary
|
|
||||||
#*.csproj merge=binary
|
|
||||||
#*.vbproj merge=binary
|
|
||||||
#*.vcxproj merge=binary
|
|
||||||
#*.vcproj merge=binary
|
|
||||||
#*.dbproj merge=binary
|
|
||||||
#*.fsproj merge=binary
|
|
||||||
#*.lsproj merge=binary
|
|
||||||
#*.wixproj merge=binary
|
|
||||||
#*.modelproj merge=binary
|
|
||||||
#*.sqlproj merge=binary
|
|
||||||
#*.wwaproj merge=binary
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
# behavior for image files
|
|
||||||
#
|
|
||||||
# image files are treated as binary by default.
|
|
||||||
###############################################################################
|
|
||||||
#*.jpg binary
|
|
||||||
#*.png binary
|
|
||||||
#*.gif binary
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
# diff behavior for common document formats
|
|
||||||
#
|
|
||||||
# Convert binary document formats to text before diffing them. This feature
|
|
||||||
# is only available from the command line. Turn it on by uncommenting the
|
|
||||||
# entries below.
|
|
||||||
###############################################################################
|
|
||||||
#*.doc diff=astextplain
|
|
||||||
#*.DOC diff=astextplain
|
|
||||||
#*.docx diff=astextplain
|
|
||||||
#*.DOCX diff=astextplain
|
|
||||||
#*.dot diff=astextplain
|
|
||||||
#*.DOT diff=astextplain
|
|
||||||
#*.pdf diff=astextplain
|
|
||||||
#*.PDF diff=astextplain
|
|
||||||
#*.rtf diff=astextplain
|
|
||||||
#*.RTF diff=astextplain
|
|
||||||
-363
@@ -1,363 +0,0 @@
|
|||||||
## Ignore Visual Studio temporary files, build results, and
|
|
||||||
## files generated by popular Visual Studio add-ons.
|
|
||||||
##
|
|
||||||
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
|
||||||
|
|
||||||
# User-specific files
|
|
||||||
*.rsuser
|
|
||||||
*.suo
|
|
||||||
*.user
|
|
||||||
*.userosscache
|
|
||||||
*.sln.docstates
|
|
||||||
|
|
||||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
|
||||||
*.userprefs
|
|
||||||
|
|
||||||
# Mono auto generated files
|
|
||||||
mono_crash.*
|
|
||||||
|
|
||||||
# Build results
|
|
||||||
[Dd]ebug/
|
|
||||||
[Dd]ebugPublic/
|
|
||||||
[Rr]elease/
|
|
||||||
[Rr]eleases/
|
|
||||||
x64/
|
|
||||||
x86/
|
|
||||||
[Ww][Ii][Nn]32/
|
|
||||||
[Aa][Rr][Mm]/
|
|
||||||
[Aa][Rr][Mm]64/
|
|
||||||
bld/
|
|
||||||
[Bb]in/
|
|
||||||
[Oo]bj/
|
|
||||||
[Oo]ut/
|
|
||||||
[Ll]og/
|
|
||||||
[Ll]ogs/
|
|
||||||
|
|
||||||
# Visual Studio 2015/2017 cache/options directory
|
|
||||||
.vs/
|
|
||||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
|
||||||
#wwwroot/
|
|
||||||
|
|
||||||
# Visual Studio 2017 auto generated files
|
|
||||||
Generated\ Files/
|
|
||||||
|
|
||||||
# MSTest test Results
|
|
||||||
[Tt]est[Rr]esult*/
|
|
||||||
[Bb]uild[Ll]og.*
|
|
||||||
|
|
||||||
# NUnit
|
|
||||||
*.VisualState.xml
|
|
||||||
TestResult.xml
|
|
||||||
nunit-*.xml
|
|
||||||
|
|
||||||
# Build Results of an ATL Project
|
|
||||||
[Dd]ebugPS/
|
|
||||||
[Rr]eleasePS/
|
|
||||||
dlldata.c
|
|
||||||
|
|
||||||
# Benchmark Results
|
|
||||||
BenchmarkDotNet.Artifacts/
|
|
||||||
|
|
||||||
# .NET Core
|
|
||||||
project.lock.json
|
|
||||||
project.fragment.lock.json
|
|
||||||
artifacts/
|
|
||||||
|
|
||||||
# ASP.NET Scaffolding
|
|
||||||
ScaffoldingReadMe.txt
|
|
||||||
|
|
||||||
# StyleCop
|
|
||||||
StyleCopReport.xml
|
|
||||||
|
|
||||||
# Files built by Visual Studio
|
|
||||||
*_i.c
|
|
||||||
*_p.c
|
|
||||||
*_h.h
|
|
||||||
*.ilk
|
|
||||||
*.meta
|
|
||||||
*.obj
|
|
||||||
*.iobj
|
|
||||||
*.pch
|
|
||||||
*.pdb
|
|
||||||
*.ipdb
|
|
||||||
*.pgc
|
|
||||||
*.pgd
|
|
||||||
*.rsp
|
|
||||||
*.sbr
|
|
||||||
*.tlb
|
|
||||||
*.tli
|
|
||||||
*.tlh
|
|
||||||
*.tmp
|
|
||||||
*.tmp_proj
|
|
||||||
*_wpftmp.csproj
|
|
||||||
*.log
|
|
||||||
*.vspscc
|
|
||||||
*.vssscc
|
|
||||||
.builds
|
|
||||||
*.pidb
|
|
||||||
*.svclog
|
|
||||||
*.scc
|
|
||||||
|
|
||||||
# Chutzpah Test files
|
|
||||||
_Chutzpah*
|
|
||||||
|
|
||||||
# Visual C++ cache files
|
|
||||||
ipch/
|
|
||||||
*.aps
|
|
||||||
*.ncb
|
|
||||||
*.opendb
|
|
||||||
*.opensdf
|
|
||||||
*.sdf
|
|
||||||
*.cachefile
|
|
||||||
*.VC.db
|
|
||||||
*.VC.VC.opendb
|
|
||||||
|
|
||||||
# Visual Studio profiler
|
|
||||||
*.psess
|
|
||||||
*.vsp
|
|
||||||
*.vspx
|
|
||||||
*.sap
|
|
||||||
|
|
||||||
# Visual Studio Trace Files
|
|
||||||
*.e2e
|
|
||||||
|
|
||||||
# TFS 2012 Local Workspace
|
|
||||||
$tf/
|
|
||||||
|
|
||||||
# Guidance Automation Toolkit
|
|
||||||
*.gpState
|
|
||||||
|
|
||||||
# ReSharper is a .NET coding add-in
|
|
||||||
_ReSharper*/
|
|
||||||
*.[Rr]e[Ss]harper
|
|
||||||
*.DotSettings.user
|
|
||||||
|
|
||||||
# TeamCity is a build add-in
|
|
||||||
_TeamCity*
|
|
||||||
|
|
||||||
# DotCover is a Code Coverage Tool
|
|
||||||
*.dotCover
|
|
||||||
|
|
||||||
# AxoCover is a Code Coverage Tool
|
|
||||||
.axoCover/*
|
|
||||||
!.axoCover/settings.json
|
|
||||||
|
|
||||||
# Coverlet is a free, cross platform Code Coverage Tool
|
|
||||||
coverage*.json
|
|
||||||
coverage*.xml
|
|
||||||
coverage*.info
|
|
||||||
|
|
||||||
# Visual Studio code coverage results
|
|
||||||
*.coverage
|
|
||||||
*.coveragexml
|
|
||||||
|
|
||||||
# NCrunch
|
|
||||||
_NCrunch_*
|
|
||||||
.*crunch*.local.xml
|
|
||||||
nCrunchTemp_*
|
|
||||||
|
|
||||||
# MightyMoose
|
|
||||||
*.mm.*
|
|
||||||
AutoTest.Net/
|
|
||||||
|
|
||||||
# Web workbench (sass)
|
|
||||||
.sass-cache/
|
|
||||||
|
|
||||||
# Installshield output folder
|
|
||||||
[Ee]xpress/
|
|
||||||
|
|
||||||
# DocProject is a documentation generator add-in
|
|
||||||
DocProject/buildhelp/
|
|
||||||
DocProject/Help/*.HxT
|
|
||||||
DocProject/Help/*.HxC
|
|
||||||
DocProject/Help/*.hhc
|
|
||||||
DocProject/Help/*.hhk
|
|
||||||
DocProject/Help/*.hhp
|
|
||||||
DocProject/Help/Html2
|
|
||||||
DocProject/Help/html
|
|
||||||
|
|
||||||
# Click-Once directory
|
|
||||||
publish/
|
|
||||||
|
|
||||||
# Publish Web Output
|
|
||||||
*.[Pp]ublish.xml
|
|
||||||
*.azurePubxml
|
|
||||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
|
||||||
# but database connection strings (with potential passwords) will be unencrypted
|
|
||||||
*.pubxml
|
|
||||||
*.publishproj
|
|
||||||
|
|
||||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
|
||||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
|
||||||
# in these scripts will be unencrypted
|
|
||||||
PublishScripts/
|
|
||||||
|
|
||||||
# NuGet Packages
|
|
||||||
*.nupkg
|
|
||||||
# NuGet Symbol Packages
|
|
||||||
*.snupkg
|
|
||||||
# The packages folder can be ignored because of Package Restore
|
|
||||||
**/[Pp]ackages/*
|
|
||||||
# except build/, which is used as an MSBuild target.
|
|
||||||
!**/[Pp]ackages/build/
|
|
||||||
# Uncomment if necessary however generally it will be regenerated when needed
|
|
||||||
#!**/[Pp]ackages/repositories.config
|
|
||||||
# NuGet v3's project.json files produces more ignorable files
|
|
||||||
*.nuget.props
|
|
||||||
*.nuget.targets
|
|
||||||
|
|
||||||
# Microsoft Azure Build Output
|
|
||||||
csx/
|
|
||||||
*.build.csdef
|
|
||||||
|
|
||||||
# Microsoft Azure Emulator
|
|
||||||
ecf/
|
|
||||||
rcf/
|
|
||||||
|
|
||||||
# Windows Store app package directories and files
|
|
||||||
AppPackages/
|
|
||||||
BundleArtifacts/
|
|
||||||
Package.StoreAssociation.xml
|
|
||||||
_pkginfo.txt
|
|
||||||
*.appx
|
|
||||||
*.appxbundle
|
|
||||||
*.appxupload
|
|
||||||
|
|
||||||
# Visual Studio cache files
|
|
||||||
# files ending in .cache can be ignored
|
|
||||||
*.[Cc]ache
|
|
||||||
# but keep track of directories ending in .cache
|
|
||||||
!?*.[Cc]ache/
|
|
||||||
|
|
||||||
# Others
|
|
||||||
ClientBin/
|
|
||||||
~$*
|
|
||||||
*~
|
|
||||||
*.dbmdl
|
|
||||||
*.dbproj.schemaview
|
|
||||||
*.jfm
|
|
||||||
*.pfx
|
|
||||||
*.publishsettings
|
|
||||||
orleans.codegen.cs
|
|
||||||
|
|
||||||
# Including strong name files can present a security risk
|
|
||||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
|
||||||
#*.snk
|
|
||||||
|
|
||||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
|
||||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
|
||||||
#bower_components/
|
|
||||||
|
|
||||||
# RIA/Silverlight projects
|
|
||||||
Generated_Code/
|
|
||||||
|
|
||||||
# Backup & report files from converting an old project file
|
|
||||||
# to a newer Visual Studio version. Backup files are not needed,
|
|
||||||
# because we have git ;-)
|
|
||||||
_UpgradeReport_Files/
|
|
||||||
Backup*/
|
|
||||||
UpgradeLog*.XML
|
|
||||||
UpgradeLog*.htm
|
|
||||||
ServiceFabricBackup/
|
|
||||||
*.rptproj.bak
|
|
||||||
|
|
||||||
# SQL Server files
|
|
||||||
*.mdf
|
|
||||||
*.ldf
|
|
||||||
*.ndf
|
|
||||||
|
|
||||||
# Business Intelligence projects
|
|
||||||
*.rdl.data
|
|
||||||
*.bim.layout
|
|
||||||
*.bim_*.settings
|
|
||||||
*.rptproj.rsuser
|
|
||||||
*- [Bb]ackup.rdl
|
|
||||||
*- [Bb]ackup ([0-9]).rdl
|
|
||||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
|
||||||
|
|
||||||
# Microsoft Fakes
|
|
||||||
FakesAssemblies/
|
|
||||||
|
|
||||||
# GhostDoc plugin setting file
|
|
||||||
*.GhostDoc.xml
|
|
||||||
|
|
||||||
# Node.js Tools for Visual Studio
|
|
||||||
.ntvs_analysis.dat
|
|
||||||
node_modules/
|
|
||||||
|
|
||||||
# Visual Studio 6 build log
|
|
||||||
*.plg
|
|
||||||
|
|
||||||
# Visual Studio 6 workspace options file
|
|
||||||
*.opt
|
|
||||||
|
|
||||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
|
||||||
*.vbw
|
|
||||||
|
|
||||||
# Visual Studio LightSwitch build output
|
|
||||||
**/*.HTMLClient/GeneratedArtifacts
|
|
||||||
**/*.DesktopClient/GeneratedArtifacts
|
|
||||||
**/*.DesktopClient/ModelManifest.xml
|
|
||||||
**/*.Server/GeneratedArtifacts
|
|
||||||
**/*.Server/ModelManifest.xml
|
|
||||||
_Pvt_Extensions
|
|
||||||
|
|
||||||
# Paket dependency manager
|
|
||||||
.paket/paket.exe
|
|
||||||
paket-files/
|
|
||||||
|
|
||||||
# FAKE - F# Make
|
|
||||||
.fake/
|
|
||||||
|
|
||||||
# CodeRush personal settings
|
|
||||||
.cr/personal
|
|
||||||
|
|
||||||
# Python Tools for Visual Studio (PTVS)
|
|
||||||
__pycache__/
|
|
||||||
*.pyc
|
|
||||||
|
|
||||||
# Cake - Uncomment if you are using it
|
|
||||||
# tools/**
|
|
||||||
# !tools/packages.config
|
|
||||||
|
|
||||||
# Tabs Studio
|
|
||||||
*.tss
|
|
||||||
|
|
||||||
# Telerik's JustMock configuration file
|
|
||||||
*.jmconfig
|
|
||||||
|
|
||||||
# BizTalk build output
|
|
||||||
*.btp.cs
|
|
||||||
*.btm.cs
|
|
||||||
*.odx.cs
|
|
||||||
*.xsd.cs
|
|
||||||
|
|
||||||
# OpenCover UI analysis results
|
|
||||||
OpenCover/
|
|
||||||
|
|
||||||
# Azure Stream Analytics local run output
|
|
||||||
ASALocalRun/
|
|
||||||
|
|
||||||
# MSBuild Binary and Structured Log
|
|
||||||
*.binlog
|
|
||||||
|
|
||||||
# NVidia Nsight GPU debugger configuration file
|
|
||||||
*.nvuser
|
|
||||||
|
|
||||||
# MFractors (Xamarin productivity tool) working folder
|
|
||||||
.mfractor/
|
|
||||||
|
|
||||||
# Local History for Visual Studio
|
|
||||||
.localhistory/
|
|
||||||
|
|
||||||
# BeatPulse healthcheck temp database
|
|
||||||
healthchecksdb
|
|
||||||
|
|
||||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
|
||||||
MigrationBackup/
|
|
||||||
|
|
||||||
# Ionide (cross platform F# VS Code tools) working folder
|
|
||||||
.ionide/
|
|
||||||
|
|
||||||
# Fody - auto-generated XML schema
|
|
||||||
FodyWeavers.xsd
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 moarten
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||||
|
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
||||||
|
following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||||
|
portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||||
|
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
||||||
|
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||||
|
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
**/.classpath
|
|
||||||
**/.dockerignore
|
|
||||||
**/.env
|
|
||||||
**/.git
|
|
||||||
**/.gitignore
|
|
||||||
**/.project
|
|
||||||
**/.settings
|
|
||||||
**/.toolstarget
|
|
||||||
**/.vs
|
|
||||||
**/.vscode
|
|
||||||
**/*.*proj.user
|
|
||||||
**/*.dbmdl
|
|
||||||
**/*.jfm
|
|
||||||
**/azds.yaml
|
|
||||||
**/bin
|
|
||||||
**/charts
|
|
||||||
**/docker-compose*
|
|
||||||
**/Dockerfile*
|
|
||||||
**/node_modules
|
|
||||||
**/npm-debug.log
|
|
||||||
**/obj
|
|
||||||
**/secrets.dev.yaml
|
|
||||||
**/values.dev.yaml
|
|
||||||
LICENSE
|
|
||||||
README.md
|
|
||||||
!**/.gitignore
|
|
||||||
!.git/HEAD
|
|
||||||
!.git/config
|
|
||||||
!.git/packed-refs
|
|
||||||
!.git/refs/heads/**
|
|
||||||
-205
@@ -1,205 +0,0 @@
|
|||||||
---
|
|
||||||
name: "C# Expert"
|
|
||||||
description: An agent designed to assist with software development tasks for .NET projects.
|
|
||||||
# version: 2026-01-20a
|
|
||||||
---
|
|
||||||
|
|
||||||
You are an expert C#/.NET developer. You help with .NET tasks by giving clean, well-designed, error-free, fast, secure, readable, and maintainable code that follows .NET conventions. You also give insights, best practices, general software design tips, and testing best practices.
|
|
||||||
|
|
||||||
You are familiar with the currently released .NET and C# versions (for example, up to .NET 10 and C# 14 at the time of writing). (Refer to https://learn.microsoft.com/en-us/dotnet/core/whats-new
|
|
||||||
and https://learn.microsoft.com/en-us/dotnet/csharp/whats-new for details.)
|
|
||||||
|
|
||||||
When invoked:
|
|
||||||
|
|
||||||
- Understand the user's .NET task and context
|
|
||||||
- Propose clean, organized solutions that follow .NET conventions
|
|
||||||
- Cover security (authentication, authorization, data protection)
|
|
||||||
- Use and explain patterns: Async/Await, Dependency Injection, Unit of Work, CQRS, Gang of Four
|
|
||||||
- Apply SOLID principles
|
|
||||||
- Plan and write tests (TDD/BDD) with xUnit, NUnit, or MSTest
|
|
||||||
- Improve performance (memory, async code, data access)
|
|
||||||
|
|
||||||
# General C# Development
|
|
||||||
|
|
||||||
- Follow the project's own conventions first, then common C# conventions.
|
|
||||||
- Keep naming, formatting, and project structure consistent.
|
|
||||||
|
|
||||||
## Code Design Rules
|
|
||||||
|
|
||||||
- DON'T add interfaces/abstractions unless used for external dependencies or testing.
|
|
||||||
- Don't wrap existing abstractions.
|
|
||||||
- Don't default to `public`. Least-exposure rule: `private` > `internal` > `protected` > `public`
|
|
||||||
- Keep names consistent; pick one style (e.g., `WithHostPort` or `WithBrowserPort`) and stick to it.
|
|
||||||
- Don't edit auto-generated code (`/api/*.cs`, `*.g.cs`, `// <auto-generated>`).
|
|
||||||
- Comments explain **why**, not what.
|
|
||||||
- Don't add unused methods/params.
|
|
||||||
- When fixing one method, check siblings for the same issue.
|
|
||||||
- Reuse existing methods as much as possible
|
|
||||||
- Add comments when adding public methods
|
|
||||||
- Move user-facing strings (e.g., AnalyzeAndConfirmNuGetConfigChanges) into resource files. Keep error/help text localizable.
|
|
||||||
|
|
||||||
## Error Handling & Edge Cases
|
|
||||||
|
|
||||||
- **Null checks**: use `ArgumentNullException.ThrowIfNull(x)`; for strings use `string.IsNullOrWhiteSpace(x)`; guard early. Avoid blanket `!`.
|
|
||||||
- **Exceptions**: choose precise types (e.g., `ArgumentException`, `InvalidOperationException`); don't throw or catch base Exception.
|
|
||||||
- **No silent catches**: don't swallow errors; log and rethrow or let them bubble.
|
|
||||||
|
|
||||||
## Goals for .NET Applications
|
|
||||||
|
|
||||||
### Productivity
|
|
||||||
|
|
||||||
- Prefer modern C# (file-scoped ns, raw """ strings, switch expr, ranges/indices, async streams) when TFM allows.
|
|
||||||
- Keep diffs small; reuse code; avoid new layers unless needed.
|
|
||||||
- Be IDE-friendly (go-to-def, rename, quick fixes work).
|
|
||||||
|
|
||||||
### Production-ready
|
|
||||||
|
|
||||||
- Secure by default (no secrets; input validate; least privilege).
|
|
||||||
- Resilient I/O (timeouts; retry with backoff when it fits).
|
|
||||||
- Structured logging with scopes; useful context; no log spam.
|
|
||||||
- Use precise exceptions; don’t swallow; keep cause/context.
|
|
||||||
|
|
||||||
### Performance
|
|
||||||
|
|
||||||
- Simple first; optimize hot paths when measured.
|
|
||||||
- Stream large payloads; avoid extra allocs.
|
|
||||||
- Use Span/Memory/pooling when it matters.
|
|
||||||
- Async end-to-end; no sync-over-async.
|
|
||||||
|
|
||||||
### Cloud-native / cloud-ready
|
|
||||||
|
|
||||||
- Cross-platform; guard OS-specific APIs.
|
|
||||||
- Diagnostics: health/ready when it fits; metrics + traces.
|
|
||||||
- Observability: ILogger + OpenTelemetry hooks.
|
|
||||||
- 12-factor: config from env; avoid stateful singletons.
|
|
||||||
|
|
||||||
# .NET quick checklist
|
|
||||||
|
|
||||||
## Do first
|
|
||||||
|
|
||||||
- Read TFM + C# version.
|
|
||||||
- Check `global.json` SDK.
|
|
||||||
|
|
||||||
## Initial check
|
|
||||||
|
|
||||||
- App type: web / desktop / console / lib.
|
|
||||||
- Packages (and multi-targeting).
|
|
||||||
- Nullable on? (`<Nullable>enable</Nullable>` / `#nullable enable`)
|
|
||||||
- Repo config: `Directory.Build.*`, `Directory.Packages.props`.
|
|
||||||
|
|
||||||
## C# version
|
|
||||||
|
|
||||||
- **Don't** set C# newer than TFM default.
|
|
||||||
- C# 14 (NET 10+): extension members; `field` accessor; implicit `Span<T>` conv; `?.=`; `nameof` with unbound generic; lambda param mods w/o types; partial ctors/events; user-defined compound assign.
|
|
||||||
|
|
||||||
## Build
|
|
||||||
|
|
||||||
- .NET 5+: `dotnet build`, `dotnet publish`.
|
|
||||||
- .NET Framework: May use `MSBuild` directly or require Visual Studio
|
|
||||||
- Look for custom targets/scripts: `Directory.Build.targets`, `build.cmd/.sh`, `Build.ps1`.
|
|
||||||
|
|
||||||
## Good practice
|
|
||||||
|
|
||||||
- Always compile or check docs first if there is unfamiliar syntax. Don't try to correct the syntax if code can compile.
|
|
||||||
- Don't change TFM, SDK, or `<LangVersion>` unless asked.
|
|
||||||
|
|
||||||
# Async Programming Best Practices
|
|
||||||
|
|
||||||
- **Naming:** all async methods end with `Async` (incl. CLI handlers).
|
|
||||||
- **Always await:** no fire-and-forget; if timing out, **cancel the work**.
|
|
||||||
- **Cancellation end-to-end:** accept a `CancellationToken`, pass it through, call `ThrowIfCancellationRequested()` in loops, make delays cancelable (`Task.Delay(ms, ct)`).
|
|
||||||
- **Timeouts:** use linked `CancellationTokenSource` + `CancelAfter` (or `WhenAny` **and** cancel the pending task).
|
|
||||||
- **Context:** use `ConfigureAwait(false)` in helper/library code; omit in app entry/UI.
|
|
||||||
- **Stream JSON:** `GetAsync(..., ResponseHeadersRead)` → `ReadAsStreamAsync` → `JsonDocument.ParseAsync`; avoid `ReadAsStringAsync` when large.
|
|
||||||
- **Exit code on cancel:** return non-zero (e.g., `130`).
|
|
||||||
- **`ValueTask`:** use only when measured to help; default to `Task`.
|
|
||||||
- **Async dispose:** prefer `await using` for async resources; keep streams/readers properly owned.
|
|
||||||
- **No pointless wrappers:** don’t add `async/await` if you just return the task.
|
|
||||||
|
|
||||||
## Immutability
|
|
||||||
|
|
||||||
- Prefer records to classes for DTOs
|
|
||||||
|
|
||||||
# Testing best practices
|
|
||||||
|
|
||||||
## Test structure
|
|
||||||
|
|
||||||
- Separate test project: **`[ProjectName].Tests`**.
|
|
||||||
- Mirror classes: `CatDoor` -> `CatDoorTests`.
|
|
||||||
- Name tests by behavior: `WhenCatMeowsThenCatDoorOpens`.
|
|
||||||
- Follow existing naming conventions.
|
|
||||||
- Use **public instance** classes; avoid **static** fields.
|
|
||||||
- No branching/conditionals inside tests.
|
|
||||||
|
|
||||||
## Unit Tests
|
|
||||||
|
|
||||||
- One behavior per test;
|
|
||||||
- Avoid Unicode symbols.
|
|
||||||
- Follow the Arrange-Act-Assert (AAA) pattern
|
|
||||||
- Use clear assertions that verify the outcome expressed by the test name
|
|
||||||
- Avoid using multiple assertions in one test method. In this case, prefer multiple tests.
|
|
||||||
- When testing multiple preconditions, write a test for each
|
|
||||||
- When testing multiple outcomes for one precondition, use parameterized tests
|
|
||||||
- Tests should be able to run in any order or in parallel
|
|
||||||
- Avoid disk I/O; if needed, randomize paths, don't clean up, log file locations.
|
|
||||||
- Test through **public APIs**; don't change visibility; avoid `InternalsVisibleTo`.
|
|
||||||
- Require tests for new/changed **public APIs**.
|
|
||||||
- Assert specific values and edge cases, not vague outcomes.
|
|
||||||
|
|
||||||
## Test workflow
|
|
||||||
|
|
||||||
### Run Test Command
|
|
||||||
|
|
||||||
- Look for custom targets/scripts: `Directory.Build.targets`, `test.ps1/.cmd/.sh`
|
|
||||||
- .NET Framework: May use `vstest.console.exe` directly or require Visual Studio Test Explorer
|
|
||||||
- Work on only one test until it passes. Then run other tests to ensure nothing has been broken.
|
|
||||||
|
|
||||||
### Code coverage (dotnet-coverage)
|
|
||||||
|
|
||||||
- **Tool (one-time):**
|
|
||||||
bash
|
|
||||||
`dotnet tool install -g dotnet-coverage`
|
|
||||||
- **Run locally (every time add/modify tests):**
|
|
||||||
bash
|
|
||||||
`dotnet-coverage collect -f cobertura -o coverage.cobertura.xml dotnet test`
|
|
||||||
|
|
||||||
## Test framework-specific guidance
|
|
||||||
|
|
||||||
- **Use the framework already in the solution** (xUnit/NUnit/MSTest) for new tests.
|
|
||||||
|
|
||||||
### xUnit
|
|
||||||
|
|
||||||
- Packages: `Microsoft.NET.Test.Sdk`, `xunit`, `xunit.runner.visualstudio`
|
|
||||||
- No class attribute; use `[Fact]`
|
|
||||||
- Parameterized tests: `[Theory]` with `[InlineData]`
|
|
||||||
- Setup/teardown: constructor and `IDisposable`
|
|
||||||
|
|
||||||
### xUnit v3
|
|
||||||
|
|
||||||
- Packages: `xunit.v3`, `xunit.runner.visualstudio` 3.x, `Microsoft.NET.Test.Sdk`
|
|
||||||
- `ITestOutputHelper` and `[Theory]` are in `Xunit`
|
|
||||||
|
|
||||||
### NUnit
|
|
||||||
|
|
||||||
- Packages: `Microsoft.NET.Test.Sdk`, `NUnit`, `NUnit3TestAdapter`
|
|
||||||
- Class `[TestFixture]`, test `[Test]`
|
|
||||||
- Parameterized tests: **use `[TestCase]`**
|
|
||||||
|
|
||||||
### MSTest
|
|
||||||
|
|
||||||
- Class `[TestClass]`, test `[TestMethod]`
|
|
||||||
- Setup/teardown: `[TestInitialize]`, `[TestCleanup]`
|
|
||||||
- Parameterized tests: **use `[TestMethod]` + `[DataRow]`**
|
|
||||||
|
|
||||||
### Assertions
|
|
||||||
|
|
||||||
- If **FluentAssertions/AwesomeAssertions** are already used, prefer them.
|
|
||||||
- Otherwise, use the framework’s asserts.
|
|
||||||
- Use `Throws/ThrowsAsync` (or MSTest `Assert.ThrowsException`) for exceptions.
|
|
||||||
|
|
||||||
## Mocking
|
|
||||||
|
|
||||||
- Avoid mocks/Fakes if possible
|
|
||||||
- External dependencies can be mocked. Never mock code whose implementation is part of the solution under test.
|
|
||||||
- Try to verify that the outputs (e.g. return values, exceptions) of the mock match the outputs of the dependency. You can write a test for this but leave it marked as skipped/explicit so that developers can verify it later.
|
|
||||||
|
|
||||||
Vendored
-81
@@ -1,81 +0,0 @@
|
|||||||
---
|
|
||||||
description: 'Debug your application to find and fix a bug'
|
|
||||||
name: 'Debug Mode Instructions'
|
|
||||||
tools: ['edit/editFiles', 'search', 'execute/getTerminalOutput', 'execute/runInTerminal', 'read/terminalLastCommand', 'read/terminalSelection', 'search/usages', 'read/problems', 'execute/testFailure', 'web/fetch', 'web/githubRepo', 'execute/runTests']
|
|
||||||
---
|
|
||||||
|
|
||||||
# Debug Mode Instructions
|
|
||||||
|
|
||||||
You are in debug mode. Your primary objective is to systematically identify, analyze, and resolve bugs in the developer's application. Follow this structured debugging process:
|
|
||||||
|
|
||||||
## Phase 1: Problem Assessment
|
|
||||||
|
|
||||||
1. **Gather Context**: Understand the current issue by:
|
|
||||||
- Reading error messages, stack traces, or failure reports
|
|
||||||
- Examining the codebase structure and recent changes
|
|
||||||
- Identifying the expected vs actual behavior
|
|
||||||
- Reviewing relevant test files and their failures
|
|
||||||
|
|
||||||
2. **Reproduce the Bug**: Before making any changes:
|
|
||||||
- Run the application or tests to confirm the issue
|
|
||||||
- Document the exact steps to reproduce the problem
|
|
||||||
- Capture error outputs, logs, or unexpected behaviors
|
|
||||||
- Provide a clear bug report to the developer with:
|
|
||||||
- Steps to reproduce
|
|
||||||
- Expected behavior
|
|
||||||
- Actual behavior
|
|
||||||
- Error messages/stack traces
|
|
||||||
- Environment details
|
|
||||||
|
|
||||||
## Phase 2: Investigation
|
|
||||||
|
|
||||||
3. **Root Cause Analysis**:
|
|
||||||
- Trace the code execution path leading to the bug
|
|
||||||
- Examine variable states, data flows, and control logic
|
|
||||||
- Check for common issues: null references, off-by-one errors, race conditions, incorrect assumptions
|
|
||||||
- Use search and usages tools to understand how affected components interact
|
|
||||||
- Review git history for recent changes that might have introduced the bug
|
|
||||||
|
|
||||||
4. **Hypothesis Formation**:
|
|
||||||
- Form specific hypotheses about what's causing the issue
|
|
||||||
- Prioritize hypotheses based on likelihood and impact
|
|
||||||
- Plan verification steps for each hypothesis
|
|
||||||
|
|
||||||
## Phase 3: Resolution
|
|
||||||
|
|
||||||
5. **Implement Fix**:
|
|
||||||
- Make targeted, minimal changes to address the root cause
|
|
||||||
- Ensure changes follow existing code patterns and conventions
|
|
||||||
- Add defensive programming practices where appropriate
|
|
||||||
- Consider edge cases and potential side effects
|
|
||||||
|
|
||||||
6. **Verification**:
|
|
||||||
- Run tests to verify the fix resolves the issue
|
|
||||||
- Execute the original reproduction steps to confirm resolution
|
|
||||||
- Run broader test suites to ensure no regressions
|
|
||||||
- Test edge cases related to the fix
|
|
||||||
|
|
||||||
## Phase 4: Quality Assurance
|
|
||||||
7. **Code Quality**:
|
|
||||||
- Review the fix for code quality and maintainability
|
|
||||||
- Add or update tests to prevent regression
|
|
||||||
- Update documentation if necessary
|
|
||||||
- Consider if similar bugs might exist elsewhere in the codebase
|
|
||||||
|
|
||||||
8. **Final Report**:
|
|
||||||
- Summarize what was fixed and how
|
|
||||||
- Explain the root cause
|
|
||||||
- Document any preventive measures taken
|
|
||||||
- Suggest improvements to prevent similar issues
|
|
||||||
|
|
||||||
## Debugging Guidelines
|
|
||||||
- **Be Systematic**: Follow the phases methodically, don't jump to solutions
|
|
||||||
- **Document Everything**: Keep detailed records of findings and attempts
|
|
||||||
- **Think Incrementally**: Make small, testable changes rather than large refactors
|
|
||||||
- **Consider Context**: Understand the broader system impact of changes
|
|
||||||
- **Communicate Clearly**: Provide regular updates on progress and findings
|
|
||||||
- **Stay Focused**: Address the specific bug without unnecessary changes
|
|
||||||
- **Test Thoroughly**: Verify fixes work in various scenarios and environments
|
|
||||||
|
|
||||||
Remember: Always reproduce and understand the bug before attempting to fix it. A well-understood problem is half solved.
|
|
||||||
|
|
||||||
-25
@@ -1,25 +0,0 @@
|
|||||||
---
|
|
||||||
description: "Provide expert .NET software engineering guidance using modern software design patterns."
|
|
||||||
name: "Expert .NET software engineer mode instructions"
|
|
||||||
tools: ["changes", "codebase", "edit/editFiles", "extensions", "fetch", "findTestFiles", "githubRepo", "new", "openSimpleBrowser", "problems", "runCommands", "runNotebooks", "runTasks", "runTests", "search", "searchResults", "terminalLastCommand", "terminalSelection", "testFailure", "usages", "vscodeAPI", "microsoft.docs.mcp"]
|
|
||||||
---
|
|
||||||
|
|
||||||
# Expert .NET software engineer mode instructions
|
|
||||||
|
|
||||||
You are in expert software engineer mode. Your task is to provide expert software engineering guidance using modern software design patterns as if you were a leader in the field.
|
|
||||||
|
|
||||||
You will provide:
|
|
||||||
|
|
||||||
- insights, best practices and recommendations for .NET software engineering as if you were Anders Hejlsberg, the original architect of C# and a key figure in the development of .NET as well as Mads Torgersen, the lead designer of C#.
|
|
||||||
- general software engineering guidance and best-practices, clean code and modern software design, as if you were Robert C. Martin (Uncle Bob), a renowned software engineer and author of "Clean Code" and "The Clean Coder".
|
|
||||||
- DevOps and CI/CD best practices, as if you were Jez Humble, co-author of "Continuous Delivery" and "The DevOps Handbook".
|
|
||||||
- Testing and test automation best practices, as if you were Kent Beck, the creator of Extreme Programming (XP) and a pioneer in Test-Driven Development (TDD).
|
|
||||||
|
|
||||||
For .NET-specific guidance, focus on the following areas:
|
|
||||||
|
|
||||||
- **Design Patterns**: Use and explain modern design patterns such as Async/Await, Dependency Injection, Repository Pattern, Unit of Work, CQRS, Event Sourcing and of course the Gang of Four patterns.
|
|
||||||
- **SOLID Principles**: Emphasize the importance of SOLID principles in software design, ensuring that code is maintainable, scalable, and testable.
|
|
||||||
- **Testing**: Advocate for Test-Driven Development (TDD) and Behavior-Driven Development (BDD) practices, using frameworks like xUnit, NUnit, or MSTest.
|
|
||||||
- **Performance**: Provide insights on performance optimization techniques, including memory management, asynchronous programming, and efficient data access patterns.
|
|
||||||
- **Security**: Highlight best practices for securing .NET applications, including authentication, authorization, and data protection.
|
|
||||||
|
|
||||||
-20
@@ -1,20 +0,0 @@
|
|||||||
---
|
|
||||||
description: "Work with PostgreSQL databases using the PostgreSQL extension."
|
|
||||||
name: "PostgreSQL Database Administrator"
|
|
||||||
tools: ["codebase", "edit/editFiles", "githubRepo", "extensions", "runCommands", "database", "pgsql_bulkLoadCsv", "pgsql_connect", "pgsql_describeCsv", "pgsql_disconnect", "pgsql_listDatabases", "pgsql_listServers", "pgsql_modifyDatabase", "pgsql_open_script", "pgsql_query", "pgsql_visualizeSchema"]
|
|
||||||
---
|
|
||||||
|
|
||||||
# PostgreSQL Database Administrator
|
|
||||||
|
|
||||||
Before running any tools, use #extensions to ensure that `ms-ossdata.vscode-pgsql` is installed and enabled. This extension provides the necessary tools to interact with PostgreSQL databases. If it is not installed, ask the user to install it before continuing.
|
|
||||||
|
|
||||||
You are a PostgreSQL Database Administrator (DBA) with expertise in managing and maintaining PostgreSQL database systems. You can perform tasks such as:
|
|
||||||
|
|
||||||
- Creating and managing databases
|
|
||||||
- Writing and optimizing SQL queries
|
|
||||||
- Performing database backups and restores
|
|
||||||
- Monitoring database performance
|
|
||||||
- Implementing security measures
|
|
||||||
|
|
||||||
You have access to various tools that allow you to interact with databases, execute queries, and manage database configurations. **Always** use the tools to inspect the database, do not look into the codebase.
|
|
||||||
|
|
||||||
-162
@@ -1,162 +0,0 @@
|
|||||||
---
|
|
||||||
name: 'SE: Security'
|
|
||||||
description: 'Security-focused code review specialist with OWASP Top 10, Zero Trust, LLM security, and enterprise security standards'
|
|
||||||
model: GPT-5
|
|
||||||
tools: ['codebase', 'edit/editFiles', 'search', 'problems']
|
|
||||||
---
|
|
||||||
|
|
||||||
# Security Reviewer
|
|
||||||
|
|
||||||
Prevent production security failures through comprehensive security review.
|
|
||||||
|
|
||||||
## Your Mission
|
|
||||||
|
|
||||||
Review code for security vulnerabilities with focus on OWASP Top 10, Zero Trust principles, and AI/ML security (LLM and ML specific threats).
|
|
||||||
|
|
||||||
## Step 0: Create Targeted Review Plan
|
|
||||||
|
|
||||||
**Analyze what you're reviewing:**
|
|
||||||
|
|
||||||
1. **Code type?**
|
|
||||||
- Web API → OWASP Top 10
|
|
||||||
- AI/LLM integration → OWASP LLM Top 10
|
|
||||||
- ML model code → OWASP ML Security
|
|
||||||
- Authentication → Access control, crypto
|
|
||||||
|
|
||||||
2. **Risk level?**
|
|
||||||
- High: Payment, auth, AI models, admin
|
|
||||||
- Medium: User data, external APIs
|
|
||||||
- Low: UI components, utilities
|
|
||||||
|
|
||||||
3. **Business constraints?**
|
|
||||||
- Performance critical → Prioritize performance checks
|
|
||||||
- Security sensitive → Deep security review
|
|
||||||
- Rapid prototype → Critical security only
|
|
||||||
|
|
||||||
### Create Review Plan:
|
|
||||||
Select 3-5 most relevant check categories based on context.
|
|
||||||
|
|
||||||
## Step 1: OWASP Top 10 Security Review
|
|
||||||
|
|
||||||
**A01 - Broken Access Control:**
|
|
||||||
```python
|
|
||||||
# VULNERABILITY
|
|
||||||
@app.route('/user/<user_id>/profile')
|
|
||||||
def get_profile(user_id):
|
|
||||||
return User.get(user_id).to_json()
|
|
||||||
|
|
||||||
# SECURE
|
|
||||||
@app.route('/user/<user_id>/profile')
|
|
||||||
@require_auth
|
|
||||||
def get_profile(user_id):
|
|
||||||
if not current_user.can_access_user(user_id):
|
|
||||||
abort(403)
|
|
||||||
return User.get(user_id).to_json()
|
|
||||||
```
|
|
||||||
|
|
||||||
**A02 - Cryptographic Failures:**
|
|
||||||
```python
|
|
||||||
# VULNERABILITY
|
|
||||||
password_hash = hashlib.md5(password.encode()).hexdigest()
|
|
||||||
|
|
||||||
# SECURE
|
|
||||||
from werkzeug.security import generate_password_hash
|
|
||||||
password_hash = generate_password_hash(password, method='scrypt')
|
|
||||||
```
|
|
||||||
|
|
||||||
**A03 - Injection Attacks:**
|
|
||||||
```python
|
|
||||||
# VULNERABILITY
|
|
||||||
query = f"SELECT * FROM users WHERE id = {user_id}"
|
|
||||||
|
|
||||||
# SECURE
|
|
||||||
query = "SELECT * FROM users WHERE id = %s"
|
|
||||||
cursor.execute(query, (user_id,))
|
|
||||||
```
|
|
||||||
|
|
||||||
## Step 1.5: OWASP LLM Top 10 (AI Systems)
|
|
||||||
|
|
||||||
**LLM01 - Prompt Injection:**
|
|
||||||
```python
|
|
||||||
# VULNERABILITY
|
|
||||||
prompt = f"Summarize: {user_input}"
|
|
||||||
return llm.complete(prompt)
|
|
||||||
|
|
||||||
# SECURE
|
|
||||||
sanitized = sanitize_input(user_input)
|
|
||||||
prompt = f"""Task: Summarize only.
|
|
||||||
Content: {sanitized}
|
|
||||||
Response:"""
|
|
||||||
return llm.complete(prompt, max_tokens=500)
|
|
||||||
```
|
|
||||||
|
|
||||||
**LLM06 - Information Disclosure:**
|
|
||||||
```python
|
|
||||||
# VULNERABILITY
|
|
||||||
response = llm.complete(f"Context: {sensitive_data}")
|
|
||||||
|
|
||||||
# SECURE
|
|
||||||
sanitized_context = remove_pii(context)
|
|
||||||
response = llm.complete(f"Context: {sanitized_context}")
|
|
||||||
filtered = filter_sensitive_output(response)
|
|
||||||
return filtered
|
|
||||||
```
|
|
||||||
|
|
||||||
## Step 2: Zero Trust Implementation
|
|
||||||
|
|
||||||
**Never Trust, Always Verify:**
|
|
||||||
```python
|
|
||||||
# VULNERABILITY
|
|
||||||
def internal_api(data):
|
|
||||||
return process(data)
|
|
||||||
|
|
||||||
# ZERO TRUST
|
|
||||||
def internal_api(data, auth_token):
|
|
||||||
if not verify_service_token(auth_token):
|
|
||||||
raise UnauthorizedError()
|
|
||||||
if not validate_request(data):
|
|
||||||
raise ValidationError()
|
|
||||||
return process(data)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Step 3: Reliability
|
|
||||||
|
|
||||||
**External Calls:**
|
|
||||||
```python
|
|
||||||
# VULNERABILITY
|
|
||||||
response = requests.get(api_url)
|
|
||||||
|
|
||||||
# SECURE
|
|
||||||
for attempt in range(3):
|
|
||||||
try:
|
|
||||||
response = requests.get(api_url, timeout=30, verify=True)
|
|
||||||
if response.status_code == 200:
|
|
||||||
break
|
|
||||||
except requests.RequestException as e:
|
|
||||||
logger.warning(f'Attempt {attempt + 1} failed: {e}')
|
|
||||||
time.sleep(2 ** attempt)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Document Creation
|
|
||||||
|
|
||||||
### After Every Review, CREATE:
|
|
||||||
**Code Review Report** - Save to `docs/code-review/[date]-[component]-review.md`
|
|
||||||
- Include specific code examples and fixes
|
|
||||||
- Tag priority levels
|
|
||||||
- Document security findings
|
|
||||||
|
|
||||||
### Report Format:
|
|
||||||
```markdown
|
|
||||||
# Code Review: [Component]
|
|
||||||
**Ready for Production**: [Yes/No]
|
|
||||||
**Critical Issues**: [count]
|
|
||||||
|
|
||||||
## Priority 1 (Must Fix) ⛔
|
|
||||||
- [specific issue with fix]
|
|
||||||
|
|
||||||
## Recommended Changes
|
|
||||||
[code examples]
|
|
||||||
```
|
|
||||||
|
|
||||||
Remember: Goal is enterprise-grade code that is secure, maintainable, and compliant.
|
|
||||||
|
|
||||||
-86
@@ -1,86 +0,0 @@
|
|||||||
---
|
|
||||||
description: 'Orchestrates comprehensive test generation using Research-Plan-Implement pipeline. Use when asked to generate tests, write unit tests, improve test coverage, or add tests.'
|
|
||||||
name: 'Polyglot Test Generator'
|
|
||||||
---
|
|
||||||
|
|
||||||
# Test Generator Agent
|
|
||||||
|
|
||||||
You coordinate test generation using the Research-Plan-Implement (RPI) pipeline. You are polyglot - you work with any programming language.
|
|
||||||
|
|
||||||
## Pipeline Overview
|
|
||||||
|
|
||||||
1. **Research** - Understand the codebase structure, testing patterns, and what needs testing
|
|
||||||
2. **Plan** - Create a phased test implementation plan
|
|
||||||
3. **Implement** - Execute the plan phase by phase, with verification
|
|
||||||
|
|
||||||
## Workflow
|
|
||||||
|
|
||||||
### Step 1: Clarify the Request
|
|
||||||
|
|
||||||
First, understand what the user wants:
|
|
||||||
- What scope? (entire project, specific files, specific classes)
|
|
||||||
- Any priority areas?
|
|
||||||
- Any testing framework preferences?
|
|
||||||
|
|
||||||
If the request is clear (e.g., "generate tests for this project"), proceed directly.
|
|
||||||
|
|
||||||
### Step 2: Research Phase
|
|
||||||
|
|
||||||
Call the `polyglot-test-researcher` subagent to analyze the codebase:
|
|
||||||
|
|
||||||
```
|
|
||||||
runSubagent({
|
|
||||||
agent: "polyglot-test-researcher",
|
|
||||||
prompt: "Research the codebase at [PATH] for test generation. Identify: project structure, existing tests, source files to test, testing framework, build/test commands."
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
The researcher will create `.testagent/research.md` with findings.
|
|
||||||
|
|
||||||
### Step 3: Planning Phase
|
|
||||||
|
|
||||||
Call the `polyglot-test-planner` subagent to create the test plan:
|
|
||||||
|
|
||||||
```
|
|
||||||
runSubagent({
|
|
||||||
agent: "polyglot-test-planner",
|
|
||||||
prompt: "Create a test implementation plan based on the research at .testagent/research.md. Create phased approach with specific files and test cases."
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
The planner will create `.testagent/plan.md` with phases.
|
|
||||||
|
|
||||||
### Step 4: Implementation Phase
|
|
||||||
|
|
||||||
Read the plan and execute each phase by calling the `polyglot-test-implementer` subagent:
|
|
||||||
|
|
||||||
```
|
|
||||||
runSubagent({
|
|
||||||
agent: "polyglot-test-implementer",
|
|
||||||
prompt: "Implement Phase N from .testagent/plan.md: [phase description]. Ensure tests compile and pass."
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
Call the implementer ONCE PER PHASE, sequentially. Wait for each phase to complete before starting the next.
|
|
||||||
|
|
||||||
### Step 5: Report Results
|
|
||||||
|
|
||||||
After all phases are complete:
|
|
||||||
- Summarize tests created
|
|
||||||
- Report any failures or issues
|
|
||||||
- Suggest next steps if needed
|
|
||||||
|
|
||||||
## State Management
|
|
||||||
|
|
||||||
All state is stored in `.testagent/` folder in the workspace:
|
|
||||||
- `.testagent/research.md` - Research findings
|
|
||||||
- `.testagent/plan.md` - Implementation plan
|
|
||||||
- `.testagent/status.md` - Progress tracking (optional)
|
|
||||||
|
|
||||||
## Important Rules
|
|
||||||
|
|
||||||
1. **Sequential phases** - Always complete one phase before starting the next
|
|
||||||
2. **Polyglot** - Detect the language and use appropriate patterns
|
|
||||||
3. **Verify** - Each phase should result in compiling, passing tests
|
|
||||||
4. **Don't skip** - If a phase fails, report it rather than skipping
|
|
||||||
|
|
||||||
-124
@@ -1,124 +0,0 @@
|
|||||||
# Copilot Instructions for Lutra
|
|
||||||
|
|
||||||
Use these standards when generating or modifying code in this repository.
|
|
||||||
|
|
||||||
## Project overview
|
|
||||||
|
|
||||||
- Target framework: .NET 10 (`net10.0`)
|
|
||||||
- Architecture: Clean Architecture
|
|
||||||
- Solution layers:
|
|
||||||
- `Lutra.Domain`
|
|
||||||
- `Lutra.Application`
|
|
||||||
- `Lutra.Infrastructure.Sql`
|
|
||||||
- `Lutra.API`
|
|
||||||
- `Lutra.AppHost`
|
|
||||||
- `Lutra.Infrastructure.Migrator`
|
|
||||||
- `Lutra.Application.UnitTests` — xUnit unit tests for Application handlers
|
|
||||||
- integration tests for API behavior (`Lutra.API.IntegrationTests`)
|
|
||||||
- Database: PostgreSQL via EF Core
|
|
||||||
- Orchestration: .NET Aspire
|
|
||||||
- Messaging pattern: `Cortex.Mediator` for CQRS handling
|
|
||||||
|
|
||||||
## General standards
|
|
||||||
|
|
||||||
- Prefer small, focused changes.
|
|
||||||
- Follow existing naming, formatting, and folder conventions.
|
|
||||||
- Keep dependencies pointing inward:
|
|
||||||
- Domain depends on nothing
|
|
||||||
- Application depends on Domain
|
|
||||||
- Infrastructure depends on Application and Domain
|
|
||||||
- API depends on Application and Infrastructure
|
|
||||||
- AppHost and Migrator are host projects only
|
|
||||||
- Use nullable reference types correctly and keep implicit usings compatible with the project style.
|
|
||||||
|
|
||||||
## Clean Architecture guidance
|
|
||||||
|
|
||||||
Use Jason Taylor’s CleanArchitecture project as a reference for intent and structure, but adapt to this codebase’s actual implementation.
|
|
||||||
|
|
||||||
Where the reference template uses MediatR, this project uses `Cortex.Mediator`.
|
|
||||||
The established test stack is **xUnit** with **FluentAssertions** — both are already in use and should be used for all new tests.
|
|
||||||
Add FluentValidation, AutoMapper, Shouldly, Moq, or Respawn only if the repository already uses them or the change clearly requires them.
|
|
||||||
|
|
||||||
## Domain layer rules
|
|
||||||
|
|
||||||
- Keep entities simple and focused on business state.
|
|
||||||
- Use `BaseEntity` as the shared base type when appropriate.
|
|
||||||
- Preserve the current entity style:
|
|
||||||
- `Id` as `Guid`
|
|
||||||
- audit fields like `CreatedAt`, `ModifiedAt`, `DeletedAt`
|
|
||||||
- `IsDeleted` as a derived property
|
|
||||||
- Keep validation and persistence concerns out of Domain unless they are intrinsic business rules.
|
|
||||||
|
|
||||||
## Application layer rules
|
|
||||||
|
|
||||||
- Put use cases in feature folders.
|
|
||||||
- Follow the existing CQRS split:
|
|
||||||
- `FeatureName.cs` as the partial entry point
|
|
||||||
- `FeatureName.Query.cs` or `FeatureName.Command.cs`
|
|
||||||
- `FeatureName.Handler.cs`
|
|
||||||
- `FeatureName.Response.cs`
|
|
||||||
- Use `Cortex.Mediator` request and handler interfaces.
|
|
||||||
- Keep handlers focused on orchestration and application logic.
|
|
||||||
- Use `ILutraDbContext` rather than referencing the concrete DbContext directly when possible.
|
|
||||||
- Shared model types (DTOs, enums) that are not use-case-specific live in `Models/<FeatureArea>/` (e.g., `Models/Verspakketten/`, `Models/Supermarkten/`).
|
|
||||||
|
|
||||||
## Infrastructure rules
|
|
||||||
|
|
||||||
- Put EF Core implementation, migrations, and database wiring in `Lutra.Infrastructure.Sql`.
|
|
||||||
- Keep the DbContext implementation aligned with `ILutraDbContext`.
|
|
||||||
- If package versions must be pinned for runtime compatibility, pin them explicitly in the infrastructure project.
|
|
||||||
- Be cautious with `PrivateAssets="all"` on design-time packages because they do not flow dependency constraints to downstream projects.
|
|
||||||
|
|
||||||
## API layer rules
|
|
||||||
|
|
||||||
- Keep controllers thin.
|
|
||||||
- Controllers should delegate to Application use cases through `IMediator`.
|
|
||||||
- Keep `Program.cs` focused on DI and middleware setup.
|
|
||||||
|
|
||||||
## Aspire and migration rules
|
|
||||||
|
|
||||||
- Keep `Lutra.AppHost` responsible for orchestration only.
|
|
||||||
- Preserve the persistent PostgreSQL container setup unless a change explicitly requires otherwise.
|
|
||||||
- Keep the migrator as a one-shot project that applies migrations on startup.
|
|
||||||
- Ensure migration-related changes do not break runtime assembly/version consistency.
|
|
||||||
|
|
||||||
## Naming and code style
|
|
||||||
|
|
||||||
- Use English for general code names (variables, method names, namespaces, folders) and use Dutch only for domain-specific enums, types, and files that reflect business concepts (e.g., `Verspakketten`, `Supermarkt`, `Beoordeling`).
|
|
||||||
- Keep class and file names consistent with the feature name.
|
|
||||||
- Use sealed types where the project already does.
|
|
||||||
- Prefer explicit `required` members where the current codebase uses them.
|
|
||||||
- Always use **file-scoped namespaces** (`namespace Foo.Bar;`), never block-scoped (`namespace Foo.Bar { }`).
|
|
||||||
- Preserve current indentation and brace style in each file.
|
|
||||||
|
|
||||||
## Testing guidance
|
|
||||||
|
|
||||||
- If tests exist, update or add tests alongside behavior changes.
|
|
||||||
- Follow the reference template's intent for test coverage:
|
|
||||||
- unit tests for business logic (`Lutra.Application.UnitTests`)
|
|
||||||
- integration tests for API behavior (`Lutra.API.IntegrationTests`)
|
|
||||||
- Test framework: **xUnit** (`xunit` v2 + `xunit.runner.visualstudio`).
|
|
||||||
- Assertions: **FluentAssertions**.
|
|
||||||
- Integration tests use **SQLite** (via `Microsoft.EntityFrameworkCore.Sqlite`) as the in-process test database -- not PostgreSQL. Use `LutraApiFactory` and `IntegrationTestBase` as the base infrastructure.
|
|
||||||
- Keep tests readable and focused on behavior.
|
|
||||||
|
|
||||||
## When making changes
|
|
||||||
|
|
||||||
- Read the relevant file before editing.
|
|
||||||
- Make the smallest change that solves the problem.
|
|
||||||
- Reuse existing patterns from the repo instead of inventing new ones.
|
|
||||||
- Run or request a build/test verification after changes when appropriate.
|
|
||||||
- Avoid broad refactors unless the user asks for them.
|
|
||||||
|
|
||||||
## Available agents
|
|
||||||
|
|
||||||
Specialist agents for common tasks are in `.github/agents/`. Invoke them when the task matches:
|
|
||||||
|
|
||||||
| Agent file | When to use |
|
|
||||||
|---|---|
|
|
||||||
| `csharp-expert.agent.md` | C# code quality, async patterns, LINQ, nullability, records, performance |
|
|
||||||
| `dotnet-expert.agent.md` | .NET architecture, CQRS, DI, SOLID, general engineering guidance |
|
|
||||||
| `security-reviewer.agent.md` | API security review, OWASP Top 10, access control, injection risks |
|
|
||||||
| `debug.agent.md` | Diagnosing and fixing bugs systematically |
|
|
||||||
| `postgresql-dba.agent.md` | PostgreSQL queries, schema, migrations, performance |
|
|
||||||
| `test-generator.agent.md` | Generating unit, integration, or functional tests for any layer |
|
|
||||||
Generated
-13
@@ -1,13 +0,0 @@
|
|||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
# Rider ignored files
|
|
||||||
/contentModel.xml
|
|
||||||
/projectSettingsUpdater.xml
|
|
||||||
/modules.xml
|
|
||||||
/.idea.Lutra.iml
|
|
||||||
# Editor-based HTTP Client requests
|
|
||||||
/httpRequests/
|
|
||||||
# Datasource local storage ignored files
|
|
||||||
/dataSources/
|
|
||||||
/dataSources.local.xml
|
|
||||||
-8
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="UserContentModel">
|
|
||||||
<attachedFolders />
|
|
||||||
<explicitIncludes />
|
|
||||||
<explicitExcludes />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
Generated
-6
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
using System.Net;
|
|
||||||
using System.Net.Http.Json;
|
|
||||||
using FluentAssertions;
|
|
||||||
using Lutra.API.IntegrationTests.Infrastructure;
|
|
||||||
using Lutra.Application.Supermarkten;
|
|
||||||
using Lutra.Domain.Entities;
|
|
||||||
|
|
||||||
namespace Lutra.API.IntegrationTests.Controllers;
|
|
||||||
|
|
||||||
public class SupermarktenControllerTests(LutraApiFactory factory)
|
|
||||||
: IntegrationTestBase(factory)
|
|
||||||
{
|
|
||||||
// ── GET /api/supermarkten ─────────────────────────────────────────────────
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Get_ReturnsOk_WithEmptyList_WhenNoDataExists()
|
|
||||||
{
|
|
||||||
var response = await Client.GetAsync("/api/supermarkten");
|
|
||||||
|
|
||||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
||||||
var body = await response.Content.ReadFromJsonAsync<GetSupermarkten.Response>();
|
|
||||||
body.Should().NotBeNull();
|
|
||||||
body!.Supermarkten.Should().BeEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Get_ReturnsSeededSupermarkt()
|
|
||||||
{
|
|
||||||
await SeedAsync(new Supermarkt
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(), Naam = "Albert Heijn",
|
|
||||||
CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
|
|
||||||
var response = await Client.GetAsync("/api/supermarkten");
|
|
||||||
|
|
||||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
||||||
var body = await response.Content.ReadFromJsonAsync<GetSupermarkten.Response>();
|
|
||||||
body!.Supermarkten.Should().HaveCount(1);
|
|
||||||
body.Supermarkten.First().Naam.Should().Be("Albert Heijn");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Get_ReturnsAllSeededSupermarkten()
|
|
||||||
{
|
|
||||||
await SeedAsync(new Supermarkt
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(), Naam = "Albert Heijn",
|
|
||||||
CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
await SeedAsync(new Supermarkt
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(), Naam = "Jumbo",
|
|
||||||
CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
await SeedAsync(new Supermarkt
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(), Naam = "Picnic",
|
|
||||||
CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
|
|
||||||
var response = await Client.GetAsync("/api/supermarkten");
|
|
||||||
|
|
||||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
||||||
var body = await response.Content.ReadFromJsonAsync<GetSupermarkten.Response>();
|
|
||||||
body!.Supermarkten.Should().HaveCount(3);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── GET /api/supermarkten — pagination ────────────────────────────────────
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Get_Pagination_ReturnsCorrectPage()
|
|
||||||
{
|
|
||||||
await SeedAsync(new Supermarkt
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(), Naam = "Albert Heijn",
|
|
||||||
CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
await SeedAsync(new Supermarkt
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(), Naam = "Jumbo",
|
|
||||||
CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
await SeedAsync(new Supermarkt
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(), Naam = "Picnic",
|
|
||||||
CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
|
|
||||||
var response = await Client.GetAsync("/api/supermarkten?skip=1&take=1");
|
|
||||||
|
|
||||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
||||||
var body = await response.Content.ReadFromJsonAsync<GetSupermarkten.Response>();
|
|
||||||
body!.Supermarkten.Should().HaveCount(1);
|
|
||||||
// Handler orders by Naam ascending, so skip=1 skips "Albert Heijn" and returns "Jumbo".
|
|
||||||
body.Supermarkten.First().Naam.Should().Be("Jumbo");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Get_Pagination_ReturnsEmptyList_WhenSkipExceedsTotalCount()
|
|
||||||
{
|
|
||||||
await SeedAsync(new Supermarkt
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(), Naam = "Albert Heijn",
|
|
||||||
CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
|
|
||||||
var response = await Client.GetAsync("/api/supermarkten?skip=10&take=50");
|
|
||||||
|
|
||||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
||||||
var body = await response.Content.ReadFromJsonAsync<GetSupermarkten.Response>();
|
|
||||||
body!.Supermarkten.Should().BeEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── GET /api/supermarkten — sorting ───────────────────────────────────────
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Get_ReturnsSupermarkten_OrderedByNaamAscending()
|
|
||||||
{
|
|
||||||
await SeedAsync(new Supermarkt
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(), Naam = "Picnic",
|
|
||||||
CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
await SeedAsync(new Supermarkt
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(), Naam = "Albert Heijn",
|
|
||||||
CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
|
|
||||||
var response = await Client.GetAsync("/api/supermarkten");
|
|
||||||
|
|
||||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
||||||
var body = await response.Content.ReadFromJsonAsync<GetSupermarkten.Response>();
|
|
||||||
var namen = body!.Supermarkten.Select(s => s.Naam).ToList();
|
|
||||||
namen.Should().BeInAscendingOrder();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Soft-delete behaviour ─────────────────────────────────────────────────
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Get_DoesNotReturn_SoftDeletedSupermarkt()
|
|
||||||
{
|
|
||||||
await SeedAsync(new Supermarkt
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(), Naam = "Verwijderd",
|
|
||||||
CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow,
|
|
||||||
DeletedAt = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
|
|
||||||
var response = await Client.GetAsync("/api/supermarkten");
|
|
||||||
|
|
||||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
||||||
var body = await response.Content.ReadFromJsonAsync<GetSupermarkten.Response>();
|
|
||||||
body!.Supermarkten.Should().BeEmpty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,324 +0,0 @@
|
|||||||
using System.Net;
|
|
||||||
using System.Net.Http.Json;
|
|
||||||
using FluentAssertions;
|
|
||||||
using Lutra.API.IntegrationTests.Infrastructure;
|
|
||||||
using Lutra.API.Requests;
|
|
||||||
using Lutra.Application.Verspakketten;
|
|
||||||
using Lutra.Domain.Entities;
|
|
||||||
|
|
||||||
namespace Lutra.API.IntegrationTests.Controllers;
|
|
||||||
|
|
||||||
public class VerspakkettenControllerTests(LutraApiFactory factory)
|
|
||||||
: IntegrationTestBase(factory)
|
|
||||||
{
|
|
||||||
// ── GET /api/verspakketten ────────────────────────────────────────────────
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Get_ReturnsOk_WithEmptyList_WhenNoDataExists()
|
|
||||||
{
|
|
||||||
var response = await Client.GetAsync("/api/verspakketten");
|
|
||||||
|
|
||||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
||||||
var body = await response.Content.ReadFromJsonAsync<GetVerspakketten.Response>();
|
|
||||||
body.Should().NotBeNull();
|
|
||||||
body!.Verspakketten.Should().BeEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Get_ReturnsSeededVerspakket()
|
|
||||||
{
|
|
||||||
var supermarkt = await SeedAsync(new Supermarkt
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(), Naam = "AH",
|
|
||||||
CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
var verspakket = new Verspakket
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(), Naam = "Lente Pakket", AantalPersonen = 2,
|
|
||||||
SupermarktId = supermarkt.Id,
|
|
||||||
CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
verspakket.AddFoto(new VerspakketFoto
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(),
|
|
||||||
Data = [1, 2, 3],
|
|
||||||
IsMainImage = true,
|
|
||||||
VerspakketId = verspakket.Id,
|
|
||||||
CreatedAt = DateTime.UtcNow,
|
|
||||||
ModifiedAt = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
verspakket.AddFoto(new VerspakketFoto
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(),
|
|
||||||
Data = [4, 5, 6],
|
|
||||||
IsMainImage = false,
|
|
||||||
VerspakketId = verspakket.Id,
|
|
||||||
CreatedAt = DateTime.UtcNow,
|
|
||||||
ModifiedAt = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
await SeedAsync(verspakket);
|
|
||||||
|
|
||||||
var response = await Client.GetAsync("/api/verspakketten");
|
|
||||||
|
|
||||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
||||||
var body = await response.Content.ReadFromJsonAsync<GetVerspakketten.Response>();
|
|
||||||
body!.Verspakketten.Should().HaveCount(1);
|
|
||||||
body.Verspakketten.First().Naam.Should().Be("Lente Pakket");
|
|
||||||
body.Verspakketten.First().Foto.Should().NotBeNull();
|
|
||||||
body.Verspakketten.First().Foto!.IsMainImage.Should().BeTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── GET /api/verspakketten/{id} ───────────────────────────────────────────
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetById_ReturnsNotFound_WhenVerspakketDoesNotExist()
|
|
||||||
{
|
|
||||||
var response = await Client.GetAsync($"/api/verspakketten/{Guid.NewGuid()}");
|
|
||||||
|
|
||||||
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetById_ReturnsVerspakket_WhenItExists()
|
|
||||||
{
|
|
||||||
var supermarkt = await SeedAsync(new Supermarkt
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(), Naam = "Jumbo",
|
|
||||||
CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
var verspakket = await SeedAsync(new Verspakket
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(), Naam = "Zomer Pakket", AantalPersonen = 4,
|
|
||||||
SupermarktId = supermarkt.Id,
|
|
||||||
CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
|
|
||||||
var response = await Client.GetAsync($"/api/verspakketten/{verspakket.Id}");
|
|
||||||
|
|
||||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
||||||
var body = await response.Content.ReadFromJsonAsync<GetVerspakket.Response>();
|
|
||||||
body!.Verspakket.Should().NotBeNull();
|
|
||||||
body.Verspakket!.Naam.Should().Be("Zomer Pakket");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── POST /api/verspakketten ───────────────────────────────────────────────
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Post_CreatesVerspakket_AndReturns201()
|
|
||||||
{
|
|
||||||
var supermarkt = await SeedAsync(new Supermarkt
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(), Naam = "Picnic",
|
|
||||||
CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
|
|
||||||
var request = new CreateVerspakketRequest("Herfst Pakket", 1499, 3, supermarkt.Id);
|
|
||||||
var response = await Client.PostAsJsonAsync("/api/verspakketten", request);
|
|
||||||
|
|
||||||
response.StatusCode.Should().Be(HttpStatusCode.Created);
|
|
||||||
var body = await response.Content.ReadFromJsonAsync<CreateVerspakket.Response>();
|
|
||||||
body!.Id.Should().NotBeEmpty();
|
|
||||||
response.Headers.Location.Should().NotBeNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Post_CreatesVerspakket_WithBeoordeling_AndReturns201()
|
|
||||||
{
|
|
||||||
var supermarkt = await SeedAsync(new Supermarkt
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(), Naam = "Picnic",
|
|
||||||
CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
|
|
||||||
var command = new CreateVerspakket.Command(
|
|
||||||
"Herfst Pakket",
|
|
||||||
1499,
|
|
||||||
3,
|
|
||||||
supermarkt.Id,
|
|
||||||
new Lutra.Application.Models.Verspakketten.Beoordeling
|
|
||||||
{
|
|
||||||
CijferSmaak = 9,
|
|
||||||
CijferBereiden = 8,
|
|
||||||
Aanbevolen = true,
|
|
||||||
Tekst = "Heel goed"
|
|
||||||
});
|
|
||||||
|
|
||||||
var response = await Client.PostAsJsonAsync("/api/verspakketten", command);
|
|
||||||
|
|
||||||
response.StatusCode.Should().Be(HttpStatusCode.Created);
|
|
||||||
var body = await response.Content.ReadFromJsonAsync<CreateVerspakket.Response>();
|
|
||||||
body!.Id.Should().NotBeEmpty();
|
|
||||||
|
|
||||||
var created = await Client.GetFromJsonAsync<GetVerspakket.Response>($"/api/verspakketten/{body.Id}");
|
|
||||||
created!.Verspakket.Beoordelingen.Should().ContainSingle();
|
|
||||||
created.Verspakket.Beoordelingen!.Single().CijferSmaak.Should().Be(9);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Post_ReturnsBadRequest_WhenSupermarktDoesNotExist()
|
|
||||||
{
|
|
||||||
var request = new CreateVerspakketRequest("Winter Pakket", 999, 2, Guid.NewGuid());
|
|
||||||
var response = await Client.PostAsJsonAsync("/api/verspakketten", request);
|
|
||||||
|
|
||||||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── PUT /api/verspakketten/{id} ───────────────────────────────────────────
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Update_ReturnsNoContent_WhenVerspakketExists()
|
|
||||||
{
|
|
||||||
var supermarkt = await SeedAsync(new Supermarkt
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(), Naam = "AH",
|
|
||||||
CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
var verspakket = await SeedAsync(new Verspakket
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(), Naam = "Oud Pakket", AantalPersonen = 2,
|
|
||||||
SupermarktId = supermarkt.Id,
|
|
||||||
CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
|
|
||||||
var request = new UpdateVerspakketRequest("Nieuw Pakket", 1999, 3, supermarkt.Id);
|
|
||||||
var response = await Client.PutAsJsonAsync($"/api/verspakketten/{verspakket.Id}", request);
|
|
||||||
|
|
||||||
response.StatusCode.Should().Be(HttpStatusCode.NoContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Update_ReturnsNotFound_WhenVerspakketDoesNotExist()
|
|
||||||
{
|
|
||||||
var supermarkt = await SeedAsync(new Supermarkt
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(), Naam = "AH",
|
|
||||||
CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
|
|
||||||
var request = new UpdateVerspakketRequest("Pakket", 999, 2, supermarkt.Id);
|
|
||||||
var response = await Client.PutAsJsonAsync($"/api/verspakketten/{Guid.NewGuid()}", request);
|
|
||||||
|
|
||||||
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Update_ReturnsBadRequest_WhenSupermarktDoesNotExist()
|
|
||||||
{
|
|
||||||
var supermarkt = await SeedAsync(new Supermarkt
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(), Naam = "AH",
|
|
||||||
CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
var verspakket = await SeedAsync(new Verspakket
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(), Naam = "Pakket", AantalPersonen = 2,
|
|
||||||
SupermarktId = supermarkt.Id,
|
|
||||||
CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
|
|
||||||
var request = new UpdateVerspakketRequest("Pakket", 999, 2, Guid.NewGuid());
|
|
||||||
var response = await Client.PutAsJsonAsync($"/api/verspakketten/{verspakket.Id}", request);
|
|
||||||
|
|
||||||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── GET /api/verspakketten — pagination & sorting ─────────────────────────
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Get_Pagination_ReturnsCorrectPage()
|
|
||||||
{
|
|
||||||
var supermarkt = await SeedAsync(new Supermarkt
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(), Naam = "AH",
|
|
||||||
CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
await SeedAsync(new Verspakket
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(), Naam = "Aardappel Pakket", AantalPersonen = 2,
|
|
||||||
SupermarktId = supermarkt.Id,
|
|
||||||
CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
await SeedAsync(new Verspakket
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(), Naam = "Broccoli Pakket", AantalPersonen = 2,
|
|
||||||
SupermarktId = supermarkt.Id,
|
|
||||||
CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
await SeedAsync(new Verspakket
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(), Naam = "Courgette Pakket", AantalPersonen = 2,
|
|
||||||
SupermarktId = supermarkt.Id,
|
|
||||||
CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
|
|
||||||
var response = await Client.GetAsync("/api/verspakketten?skip=1&take=1");
|
|
||||||
|
|
||||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
||||||
var body = await response.Content.ReadFromJsonAsync<GetVerspakketten.Response>();
|
|
||||||
body!.Verspakketten.Should().HaveCount(1);
|
|
||||||
body.Verspakketten.First().Naam.Should().Be("Broccoli Pakket");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Get_SortDescending_ReturnsItemsInReverseOrder()
|
|
||||||
{
|
|
||||||
var supermarkt = await SeedAsync(new Supermarkt
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(), Naam = "AH",
|
|
||||||
CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
await SeedAsync(new Verspakket
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(), Naam = "Aardappel Pakket", AantalPersonen = 2,
|
|
||||||
SupermarktId = supermarkt.Id,
|
|
||||||
CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
await SeedAsync(new Verspakket
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(), Naam = "Zomerpakket", AantalPersonen = 2,
|
|
||||||
SupermarktId = supermarkt.Id,
|
|
||||||
CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
|
|
||||||
var response = await Client.GetAsync("/api/verspakketten?sortDirection=Descending");
|
|
||||||
|
|
||||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
||||||
var body = await response.Content.ReadFromJsonAsync<GetVerspakketten.Response>();
|
|
||||||
body!.Verspakketten.First().Naam.Should().Be("Zomerpakket");
|
|
||||||
body.Verspakketten.Last().Naam.Should().Be("Aardappel Pakket");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── POST /api/verspakketten/{id}/beoordelingen ────────────────────────────
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task AddBeoordeling_ReturnsBadRequest_WhenVerspakketDoesNotExist()
|
|
||||||
{
|
|
||||||
var command = new AddBeoordeling.Command(Guid.NewGuid(), 8, 7, true, "Heerlijk!");
|
|
||||||
var response = await Client.PostAsJsonAsync($"/api/verspakketten/{command.VerspakketId}/beoordelingen", command);
|
|
||||||
|
|
||||||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task AddBeoordeling_Returns201_WhenVerspakketExists()
|
|
||||||
{
|
|
||||||
var supermarkt = await SeedAsync(new Supermarkt
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(), Naam = "Lidl",
|
|
||||||
CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
var verspakket = await SeedAsync(new Verspakket
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(), Naam = "Basis Pakket", AantalPersonen = 2,
|
|
||||||
SupermarktId = supermarkt.Id,
|
|
||||||
CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
|
|
||||||
var command = new AddBeoordeling.Command(verspakket.Id, 8, 7, true, "Heerlijk!");
|
|
||||||
var response = await Client.PostAsJsonAsync($"/api/verspakketten/{verspakket.Id}/beoordelingen", command);
|
|
||||||
|
|
||||||
response.StatusCode.Should().Be(HttpStatusCode.Created);
|
|
||||||
var body = await response.Content.ReadFromJsonAsync<AddBeoordeling.Response>();
|
|
||||||
body!.Id.Should().NotBeEmpty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
using Lutra.Application.Interfaces;
|
|
||||||
using Lutra.Infrastructure.Sql;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
|
|
||||||
namespace Lutra.API.IntegrationTests.Infrastructure;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Base class for integration tests. Provides a shared factory and helper methods
|
|
||||||
/// to seed and reset the database between tests.
|
|
||||||
/// </summary>
|
|
||||||
public abstract class IntegrationTestBase : IClassFixture<LutraApiFactory>, IAsyncLifetime
|
|
||||||
{
|
|
||||||
protected readonly LutraApiFactory Factory;
|
|
||||||
protected readonly HttpClient Client;
|
|
||||||
|
|
||||||
protected IntegrationTestBase(LutraApiFactory factory)
|
|
||||||
{
|
|
||||||
Factory = factory;
|
|
||||||
Client = factory.CreateClient();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Seed data or perform setup before each test.</summary>
|
|
||||||
public virtual ValueTask InitializeAsync()
|
|
||||||
{
|
|
||||||
Factory.EnsureSchemaCreated();
|
|
||||||
return ValueTask.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Reset database state after each test.</summary>
|
|
||||||
public async ValueTask DisposeAsync()
|
|
||||||
{
|
|
||||||
using var scope = Factory.Services.CreateScope();
|
|
||||||
var db = scope.ServiceProvider.GetRequiredService<ILutraDbContext>() as LutraDbContext;
|
|
||||||
if (db is not null)
|
|
||||||
{
|
|
||||||
db.Beoordelingen.RemoveRange(db.Beoordelingen);
|
|
||||||
db.Verspaketten.RemoveRange(db.Verspaketten);
|
|
||||||
db.Supermarkten.RemoveRange(db.Supermarkten);
|
|
||||||
await db.SaveChangesAsync(CancellationToken.None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async ValueTask<T> SeedAsync<T>(T entity) where T : class
|
|
||||||
{
|
|
||||||
using var scope = Factory.Services.CreateScope();
|
|
||||||
var db = scope.ServiceProvider.GetRequiredService<ILutraDbContext>() as LutraDbContext;
|
|
||||||
db!.Set<T>().Add(entity);
|
|
||||||
await db.SaveChangesAsync(CancellationToken.None);
|
|
||||||
return entity;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
using Lutra.Application.Interfaces;
|
|
||||||
using Lutra.Infrastructure.Sql;
|
|
||||||
using Microsoft.AspNetCore.Hosting;
|
|
||||||
using Microsoft.AspNetCore.Mvc.Testing;
|
|
||||||
using Microsoft.Data.Sqlite;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
|
||||||
|
|
||||||
namespace Lutra.API.IntegrationTests.Infrastructure;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Custom WebApplicationFactory that replaces the PostgreSQL database with SQLite in-memory
|
|
||||||
/// so that integration tests can run without a live database server.
|
|
||||||
/// A single SqliteConnection is kept open for the lifetime of the factory so that
|
|
||||||
/// all DI scopes share the same in-memory database.
|
|
||||||
/// </summary>
|
|
||||||
public class LutraApiFactory : WebApplicationFactory<Program>
|
|
||||||
{
|
|
||||||
// Opened immediately so it is ready when ConfigureWebHost runs.
|
|
||||||
private readonly SqliteConnection _connection = new("Data Source=:memory:");
|
|
||||||
private bool _schemaCreated;
|
|
||||||
|
|
||||||
public LutraApiFactory()
|
|
||||||
{
|
|
||||||
_connection.Open();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Ensures the SQLite schema is created. Call once before the first test.</summary>
|
|
||||||
public void EnsureSchemaCreated()
|
|
||||||
{
|
|
||||||
if (_schemaCreated) return;
|
|
||||||
|
|
||||||
using var scope = Services.CreateScope();
|
|
||||||
var db = (LutraDbContext)scope.ServiceProvider.GetRequiredService<ILutraDbContext>();
|
|
||||||
db.Database.EnsureCreated();
|
|
||||||
_schemaCreated = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
|
||||||
{
|
|
||||||
builder.ConfigureServices(services =>
|
|
||||||
{
|
|
||||||
// EF Core 10 stores provider configuration in IDbContextOptionsConfiguration<T>
|
|
||||||
// descriptors (one per AddDbContext call). All four registration types must be
|
|
||||||
// removed so neither Npgsql options nor its provider services survive into the
|
|
||||||
// SQLite registration.
|
|
||||||
services.RemoveAll<ILutraDbContext>();
|
|
||||||
services.RemoveAll<LutraDbContext>();
|
|
||||||
services.RemoveAll<DbContextOptions<LutraDbContext>>();
|
|
||||||
services.RemoveAll<DbContextOptions>();
|
|
||||||
services.RemoveAll(typeof(IDbContextOptionsConfiguration<LutraDbContext>));
|
|
||||||
|
|
||||||
// Register SQLite using the shared open connection.
|
|
||||||
services.AddDbContext<ILutraDbContext, LutraDbContext>(options =>
|
|
||||||
options.UseSqlite(_connection));
|
|
||||||
});
|
|
||||||
|
|
||||||
builder.UseEnvironment("Testing");
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
base.Dispose(disposing);
|
|
||||||
if (disposing)
|
|
||||||
_connection.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<IsPackable>false</IsPackable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="coverlet.collector" Version="10.0.0">
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="FluentAssertions" Version="8.9.0" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.7" />
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.7" />
|
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.4.0" />
|
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="xunit.v3" Version="3.2.2" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Using Include="Xunit" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\Lutra.API\Lutra.API.csproj" />
|
|
||||||
<ProjectReference Include="..\Lutra.Infrastructure\Lutra.Infrastructure.Sql.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace Lutra.API.Controllers;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Exposes a lightweight health check for the API.
|
|
||||||
/// </summary>
|
|
||||||
[ApiController]
|
|
||||||
[Route("health")]
|
|
||||||
[Produces("application/json")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
public class HealthController : ControllerBase
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the API health status.
|
|
||||||
/// </summary>
|
|
||||||
[HttpGet]
|
|
||||||
public IActionResult Index()
|
|
||||||
{
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
using Cortex.Mediator;
|
|
||||||
using Lutra.Application.Supermarkten;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace Lutra.API.Controllers;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Provides endpoints for Supermarkt-related operations.
|
|
||||||
/// </summary>
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/supermarkten")]
|
|
||||||
[Produces("application/json")]
|
|
||||||
public class SupermarktenController(IMediator mediator) : ControllerBase
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a page of supermarkten.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="skip">The number of items to skip.</param>
|
|
||||||
/// <param name="take">The maximum number of items to return.</param>
|
|
||||||
/// <returns>The requested supermarkt page.</returns>
|
|
||||||
[HttpGet]
|
|
||||||
[ProducesResponseType(typeof(GetSupermarkten.Response), StatusCodes.Status200OK)]
|
|
||||||
public async Task<GetSupermarkten.Response> Get(int skip = 0, int take = 50)
|
|
||||||
{
|
|
||||||
return await mediator.SendQueryAsync<GetSupermarkten.Query, GetSupermarkten.Response>(new GetSupermarkten.Query(skip, take));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
using Cortex.Mediator;
|
|
||||||
using Lutra.API.Requests;
|
|
||||||
using Lutra.Application.Verspakketten;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace Lutra.API.Controllers;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Provides access to verspakket resources.
|
|
||||||
/// </summary>
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/verspakketten")]
|
|
||||||
[Produces("application/json")]
|
|
||||||
public class VerspakkettenController(IMediator mediator) : ControllerBase
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a page of verspakketten.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="skip">The number of items to skip. Default: 0.</param>
|
|
||||||
/// <param name="take">The maximum number of items to return. Default: 50.</param>
|
|
||||||
/// <param name="sortField">The field to sort by: Naam, PrijsInCenten, AverageCijferSmaak, or AverageCijferBereiden. Default: Naam.</param>
|
|
||||||
/// <param name="sortDirection">The sort direction: Ascending or Descending. Default: Ascending.</param>
|
|
||||||
/// <returns>The requested verspakket page.</returns>
|
|
||||||
[HttpGet]
|
|
||||||
[ProducesResponseType(typeof(GetVerspakketten.Response), StatusCodes.Status200OK)]
|
|
||||||
public async Task<GetVerspakketten.Response> Get(
|
|
||||||
int skip = 0,
|
|
||||||
int take = 50,
|
|
||||||
VerspakketSortField sortField = VerspakketSortField.Naam,
|
|
||||||
SortDirection sortDirection = SortDirection.Ascending)
|
|
||||||
{
|
|
||||||
return await mediator.SendQueryAsync<GetVerspakketten.Query, GetVerspakketten.Response>(
|
|
||||||
new GetVerspakketten.Query(skip, take, sortField, sortDirection));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a specific verspakket by ID.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="id">The verspakket ID.</param>
|
|
||||||
/// <returns>Returns 200 OK with the verspakket when found, or 404 Not Found when not found.</returns>
|
|
||||||
[HttpGet("{id:guid}")]
|
|
||||||
[ProducesResponseType(typeof(GetVerspakket.Response), StatusCodes.Status200OK)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
||||||
public async Task<ActionResult<GetVerspakket.Response?>> GetById(Guid id)
|
|
||||||
{
|
|
||||||
var result = await mediator.SendQueryAsync<GetVerspakket.Query, GetVerspakket.Response?>(new GetVerspakket.Query(id));
|
|
||||||
if (result?.Verspakket == null)
|
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new verspakket.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">The verspakket values to create.</param>
|
|
||||||
/// <returns>Returns 201 Created with the created verspakket identifier.</returns>
|
|
||||||
[HttpPost]
|
|
||||||
[ProducesResponseType(typeof(CreateVerspakket.Response), StatusCodes.Status201Created)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
|
||||||
public async Task<ActionResult<CreateVerspakket.Response>> Post([FromBody] CreateVerspakketRequest request)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var beoordeling = request.Beoordeling is null
|
|
||||||
? null
|
|
||||||
: new Application.Models.Verspakketten.Beoordeling
|
|
||||||
{
|
|
||||||
CijferSmaak = request.Beoordeling.CijferSmaak,
|
|
||||||
CijferBereiden = request.Beoordeling.CijferBereiden,
|
|
||||||
Aanbevolen = request.Beoordeling.Aanbevolen,
|
|
||||||
Tekst = request.Beoordeling.Tekst
|
|
||||||
};
|
|
||||||
|
|
||||||
var fotos = request.Fotos?
|
|
||||||
.Select(f => new Application.Models.Verspakketten.VerspakketFoto(f.Base64Data, f.IsMainImage))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var command = new CreateVerspakket.Command(
|
|
||||||
request.Naam,
|
|
||||||
request.PrijsInCenten,
|
|
||||||
request.AantalPersonen,
|
|
||||||
request.SupermarktId,
|
|
||||||
beoordeling,
|
|
||||||
fotos);
|
|
||||||
|
|
||||||
var result = await mediator.SendCommandAsync<CreateVerspakket.Command, CreateVerspakket.Response>(command);
|
|
||||||
|
|
||||||
return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
|
|
||||||
}
|
|
||||||
catch (InvalidOperationException ex)
|
|
||||||
{
|
|
||||||
return BadRequest(ex.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Updates an existing verspakket with the provided values.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="id">The verspakket identifier.</param>
|
|
||||||
/// <param name="request">The updated verspakket values.</param>
|
|
||||||
/// <returns>
|
|
||||||
/// Returns 204 No Content when the update succeeds.
|
|
||||||
/// Returns 404 Not Found when the specified verspakket does not exist.
|
|
||||||
/// </returns>
|
|
||||||
[HttpPut("{id:guid}")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
||||||
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateVerspakketRequest request)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var fotos = request.Fotos?
|
|
||||||
.Select(f => new Application.Models.Verspakketten.VerspakketFoto(f.Base64Data, f.IsMainImage))
|
|
||||||
.ToList();
|
|
||||||
var command = new UpdateVerspakket.Command(id, request.Naam, request.PrijsInCenten, request.AantalPersonen, request.SupermarktId, fotos);
|
|
||||||
await mediator.SendCommandAsync<UpdateVerspakket.Command, UpdateVerspakket.Response>(command);
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
catch (ArgumentException ex)
|
|
||||||
{
|
|
||||||
return BadRequest(ex.Message);
|
|
||||||
}
|
|
||||||
catch (InvalidOperationException ex) when (ex.Message.StartsWith($"Verspakket with id '{id}'"))
|
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
catch (InvalidOperationException ex)
|
|
||||||
{
|
|
||||||
return BadRequest(ex.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Adds a beoordeling to an existing verspakket.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="id">The verspakket ID.</param>
|
|
||||||
/// <param name="request">The beoordeling values to add.</param>
|
|
||||||
/// <returns>Returns 201 Created with the created beoordeling identifier.</returns>
|
|
||||||
[HttpPost("{id:guid}/beoordelingen")]
|
|
||||||
[ProducesResponseType(typeof(AddBeoordeling.Response), StatusCodes.Status201Created)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
|
||||||
public async Task<ActionResult<AddBeoordeling.Response>> AddBeoordeling(Guid id, [FromBody] AddBeoordelingRequest request)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var command = new AddBeoordeling.Command(id, request.CijferSmaak, request.CijferBereiden, request.Aanbevolen, request.Tekst);
|
|
||||||
var result = await mediator.SendCommandAsync<AddBeoordeling.Command, AddBeoordeling.Response>(command);
|
|
||||||
|
|
||||||
return CreatedAtAction(nameof(GetById), new { id }, result);
|
|
||||||
}
|
|
||||||
catch (InvalidOperationException ex)
|
|
||||||
{
|
|
||||||
return BadRequest(ex.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
# See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
|
|
||||||
|
|
||||||
# This stage is used when running from VS in fast mode (Default for Debug configuration)
|
|
||||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
|
|
||||||
USER $APP_UID
|
|
||||||
WORKDIR /app
|
|
||||||
EXPOSE 8080
|
|
||||||
EXPOSE 8081
|
|
||||||
|
|
||||||
|
|
||||||
# This stage is used to build the service project
|
|
||||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
|
||||||
ARG BUILD_CONFIGURATION=Release
|
|
||||||
WORKDIR /src
|
|
||||||
COPY ["Lutra.API/Lutra.API.csproj", "Lutra.API/"]
|
|
||||||
RUN dotnet restore "./Lutra.API/Lutra.API.csproj"
|
|
||||||
COPY . .
|
|
||||||
WORKDIR "/src/Lutra.API"
|
|
||||||
RUN dotnet build "./Lutra.API.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
|
||||||
|
|
||||||
# This stage is used to publish the service project to be copied to the final stage
|
|
||||||
FROM build AS publish
|
|
||||||
ARG BUILD_CONFIGURATION=Release
|
|
||||||
RUN dotnet publish "./Lutra.API.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
|
||||||
|
|
||||||
# This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration)
|
|
||||||
FROM base AS final
|
|
||||||
WORKDIR /app
|
|
||||||
COPY --from=publish /app/publish .
|
|
||||||
ENTRYPOINT ["dotnet", "Lutra.API.dll"]
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
|
||||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
|
||||||
<UserSecretsId>c9bd54b0-d347-4e93-bcea-ed5e98a71d5c</UserSecretsId>
|
|
||||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Cortex.Mediator" Version="3.1.2" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.7" />
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.7">
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.23.0" />
|
|
||||||
<PackageReference Include="Scalar.AspNetCore" Version="2.14.4" />
|
|
||||||
<ProjectReference Include="..\Lutra.Infrastructure\Lutra.Infrastructure.Sql.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\Lutra.Application\Lutra.Application.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
@Lutra.API_HostAddress = http://localhost:5037
|
|
||||||
|
|
||||||
GET {{Lutra.API_HostAddress}}
|
|
||||||
Accept: application/json
|
|
||||||
|
|
||||||
###
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
|
|
||||||
using Cortex.Mediator.DependencyInjection;
|
|
||||||
using Lutra.Application.Verspakketten;
|
|
||||||
using Lutra.Application.Interfaces;
|
|
||||||
using Lutra.Infrastructure.Sql;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Scalar.AspNetCore;
|
|
||||||
|
|
||||||
namespace Lutra.API
|
|
||||||
{
|
|
||||||
public class Program
|
|
||||||
{
|
|
||||||
public static void Main(string[] args)
|
|
||||||
{
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
|
||||||
|
|
||||||
builder.Services.AddCors(options =>
|
|
||||||
{
|
|
||||||
options.AddPolicy("AllowLocalDevelopment", policy =>
|
|
||||||
policy.SetIsOriginAllowed(origin =>
|
|
||||||
{
|
|
||||||
if (!Uri.TryCreate(origin, UriKind.Absolute, out var uri))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return uri.Host is "localhost" or "127.0.0.1" or "[::1]";
|
|
||||||
})
|
|
||||||
.AllowAnyHeader()
|
|
||||||
.AllowAnyMethod());
|
|
||||||
});
|
|
||||||
|
|
||||||
builder.Services.AddCortexMediator(
|
|
||||||
handlerAssemblyMarkerTypes: [typeof(Program), typeof(GetVerspakketten)],
|
|
||||||
options => options.AddDefaultBehaviors()
|
|
||||||
);
|
|
||||||
|
|
||||||
builder.Services.AddDbContext<ILutraDbContext, LutraDbContext>(options =>
|
|
||||||
options.UseNpgsql(builder.Configuration.GetConnectionString("LutraDb")));
|
|
||||||
|
|
||||||
builder.Services.AddControllers();
|
|
||||||
builder.Services.AddOpenApi();
|
|
||||||
|
|
||||||
|
|
||||||
var app = builder.Build();
|
|
||||||
|
|
||||||
if (app.Environment.IsDevelopment())
|
|
||||||
{
|
|
||||||
app.MapOpenApi();
|
|
||||||
app.MapScalarApiReference();
|
|
||||||
app.MapGet("/", () => Results.Redirect("/scalar/v1")).ExcludeFromDescription();
|
|
||||||
}
|
|
||||||
|
|
||||||
app.UseHttpsRedirection();
|
|
||||||
|
|
||||||
app.UseCors("AllowLocalDevelopment");
|
|
||||||
|
|
||||||
app.UseAuthorization();
|
|
||||||
|
|
||||||
|
|
||||||
app.MapControllers();
|
|
||||||
|
|
||||||
app.Run();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"profiles": {
|
|
||||||
"http": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
|
||||||
},
|
|
||||||
"dotnetRunMessages": true,
|
|
||||||
"applicationUrl": "http://localhost:5037"
|
|
||||||
},
|
|
||||||
"https": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
|
||||||
},
|
|
||||||
"dotnetRunMessages": true,
|
|
||||||
"applicationUrl": "https://localhost:7013;http://localhost:5037"
|
|
||||||
},
|
|
||||||
"Container (Dockerfile)": {
|
|
||||||
"commandName": "Docker",
|
|
||||||
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_HTTPS_PORTS": "8081",
|
|
||||||
"ASPNETCORE_HTTP_PORTS": "8080"
|
|
||||||
},
|
|
||||||
"publishAllPorts": true,
|
|
||||||
"useSSL": true
|
|
||||||
},
|
|
||||||
"IIS Express": {
|
|
||||||
"commandName": "IISExpress",
|
|
||||||
"launchBrowser": true,
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
|
||||||
"iisSettings": {
|
|
||||||
"windowsAuthentication": false,
|
|
||||||
"anonymousAuthentication": true,
|
|
||||||
"iisExpress": {
|
|
||||||
"applicationUrl": "http://localhost:50177/",
|
|
||||||
"sslPort": 44385
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace Lutra.API.Requests;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents the data required to add a beoordeling to a verspakket.
|
|
||||||
/// </summary>
|
|
||||||
public sealed record AddBeoordelingRequest(
|
|
||||||
[Range(1, 10)] int CijferSmaak,
|
|
||||||
[Range(1, 10)] int CijferBereiden,
|
|
||||||
bool Aanbevolen,
|
|
||||||
[MaxLength(1024)] string? Tekst);
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace Lutra.API.Requests;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents the data required to create a verspakket.
|
|
||||||
/// </summary>
|
|
||||||
public sealed record CreateVerspakketRequest(
|
|
||||||
[Required, MaxLength(50)] string Naam,
|
|
||||||
[Range(0, int.MaxValue)] int? PrijsInCenten,
|
|
||||||
[Range(1, 10)] int AantalPersonen,
|
|
||||||
[Required] Guid SupermarktId,
|
|
||||||
AddBeoordelingRequest? Beoordeling = null,
|
|
||||||
IReadOnlyList<VerspakketFotoRequest>? Fotos = null);
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace Lutra.API.Requests;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents the data required to update a verspakket.
|
|
||||||
/// </summary>
|
|
||||||
public sealed record UpdateVerspakketRequest(
|
|
||||||
[Required, MaxLength(50)] string Naam,
|
|
||||||
[Range(0, int.MaxValue)] int PrijsInCenten,
|
|
||||||
[Range(1, 10)] int AantalPersonen,
|
|
||||||
[Required] Guid SupermarktId,
|
|
||||||
IReadOnlyList<VerspakketFotoRequest>? Fotos = null);
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace Lutra.API.Requests;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a foto in a request, encoded as base64.
|
|
||||||
/// </summary>
|
|
||||||
public sealed record VerspakketFotoRequest(
|
|
||||||
[Required] string Base64Data,
|
|
||||||
bool IsMainImage);
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"ConnectionStrings": {
|
|
||||||
"LutraDb": "Host=localhost;Database=lutra;Username=postgres;Password=<set-locally>"
|
|
||||||
},
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ConnectionStrings": {
|
|
||||||
"LutraDb": ""
|
|
||||||
},
|
|
||||||
"AllowedHosts": "*"
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
var builder = DistributedApplication.CreateBuilder(args);
|
|
||||||
|
|
||||||
var postgres = builder.AddPostgres("postgres")
|
|
||||||
.WithDataVolume()
|
|
||||||
.WithLifetime(ContainerLifetime.Persistent);
|
|
||||||
|
|
||||||
var db = postgres
|
|
||||||
.AddDatabase("LutraDb");
|
|
||||||
|
|
||||||
var migrator = builder.AddProject<Projects.Lutra_Infrastructure_Migrator>("dbmigrator")
|
|
||||||
.WithReference(db)
|
|
||||||
.WaitFor(db);
|
|
||||||
|
|
||||||
var apiService = builder.AddProject<Projects.Lutra_API>("apiservice")
|
|
||||||
.WithHttpHealthCheck("/health")
|
|
||||||
.WithReference(db)
|
|
||||||
.WaitForCompletion(migrator);
|
|
||||||
|
|
||||||
builder.Build().Run();
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<Sdk Name="Aspire.AppHost.Sdk" Version="9.5.0" />
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<OutputType>Exe</OutputType>
|
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<UserSecretsId>cb3bf793-12ac-4e3d-9aeb-c2e3cb36a9fe</UserSecretsId>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Aspire.Hosting.AppHost" Version="13.2.3" />
|
|
||||||
<PackageReference Include="Aspire.Hosting.PostgreSQL" Version="13.2.3" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\Lutra.API\Lutra.API.csproj" />
|
|
||||||
<ProjectReference Include="..\Lutra.Infrastructure.Migrator\Lutra.Infrastructure.Migrator.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
|
||||||
"profiles": {
|
|
||||||
"https": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"dotnetRunMessages": true,
|
|
||||||
"launchBrowser": true,
|
|
||||||
"applicationUrl": "https://localhost:17126;http://localhost:15299",
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
|
||||||
"DOTNET_ENVIRONMENT": "Development",
|
|
||||||
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21049",
|
|
||||||
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22271"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"http": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"dotnetRunMessages": true,
|
|
||||||
"launchBrowser": true,
|
|
||||||
"applicationUrl": "http://localhost:15299",
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
|
||||||
"DOTNET_ENVIRONMENT": "Development",
|
|
||||||
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19176",
|
|
||||||
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20117"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning",
|
|
||||||
"Aspire.Hosting.Dcp": "Warning"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<IsPackable>false</IsPackable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="coverlet.collector" Version="10.0.0">
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="FluentAssertions" Version="8.9.0" />
|
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.4.0" />
|
|
||||||
<PackageReference Include="Moq" Version="4.20.72" />
|
|
||||||
<PackageReference Include="Moq.EntityFrameworkCore" Version="10.0.0.2" />
|
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="xunit.v3" Version="3.2.2" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Using Include="Xunit" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\Lutra.Application\Lutra.Application.csproj" />
|
|
||||||
<ProjectReference Include="..\Lutra.Domain\Lutra.Domain.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
using FluentAssertions;
|
|
||||||
using Lutra.Application.Interfaces;
|
|
||||||
using Lutra.Application.Verspakketten;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Moq;
|
|
||||||
using Moq.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace Lutra.Application.UnitTests.Verspakketten;
|
|
||||||
|
|
||||||
public class AddBeoordelingHandlerTests
|
|
||||||
{
|
|
||||||
private readonly Mock<ILutraDbContext> _contextMock;
|
|
||||||
private readonly AddBeoordeling.Handler _handler;
|
|
||||||
|
|
||||||
public AddBeoordelingHandlerTests()
|
|
||||||
{
|
|
||||||
_contextMock = new Mock<ILutraDbContext>();
|
|
||||||
_handler = new AddBeoordeling.Handler(_contextMock.Object);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Handle_VerspakketExists_AddsBeoordeling()
|
|
||||||
{
|
|
||||||
var verspakketId = Guid.NewGuid();
|
|
||||||
var verspakketten = new List<Domain.Entities.Verspakket>
|
|
||||||
{
|
|
||||||
new() { Id = verspakketId, Naam = "Test", AantalPersonen = 2, SupermarktId = Guid.NewGuid(), CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow }
|
|
||||||
};
|
|
||||||
|
|
||||||
_contextMock.Setup(c => c.Verspaketten).ReturnsDbSet(verspakketten);
|
|
||||||
_contextMock.Setup(c => c.Beoordelingen).ReturnsDbSet(new List<Domain.Entities.Beoordeling>());
|
|
||||||
_contextMock.Setup(c => c.SaveChangesAsync(It.IsAny<CancellationToken>())).ReturnsAsync(1);
|
|
||||||
|
|
||||||
var command = new AddBeoordeling.Command(verspakketId, 8, 7, true, "Lekker!");
|
|
||||||
|
|
||||||
var result = await _handler.Handle(command, CancellationToken.None);
|
|
||||||
|
|
||||||
result.Id.Should().NotBeEmpty();
|
|
||||||
_contextMock.Verify(c => c.SaveChangesAsync(It.IsAny<CancellationToken>()), Times.Once);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Handle_VerspakketNotFound_ThrowsInvalidOperationException()
|
|
||||||
{
|
|
||||||
_contextMock.Setup(c => c.Verspaketten).ReturnsDbSet(new List<Domain.Entities.Verspakket>());
|
|
||||||
|
|
||||||
var command = new AddBeoordeling.Command(Guid.NewGuid(), 8, 7, true, null);
|
|
||||||
|
|
||||||
var act = () => _handler.Handle(command, CancellationToken.None);
|
|
||||||
|
|
||||||
await act.Should().ThrowAsync<InvalidOperationException>()
|
|
||||||
.WithMessage("*was not found*");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Handle_VerspakketDeleted_ThrowsInvalidOperationException()
|
|
||||||
{
|
|
||||||
var verspakketId = Guid.NewGuid();
|
|
||||||
var verspakketten = new List<Domain.Entities.Verspakket>
|
|
||||||
{
|
|
||||||
new() { Id = verspakketId, Naam = "Deleted", AantalPersonen = 2, SupermarktId = Guid.NewGuid(), CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow, DeletedAt = DateTime.UtcNow }
|
|
||||||
};
|
|
||||||
|
|
||||||
_contextMock.Setup(c => c.Verspaketten).ReturnsDbSet(verspakketten);
|
|
||||||
|
|
||||||
var command = new AddBeoordeling.Command(verspakketId, 8, 7, true, null);
|
|
||||||
|
|
||||||
var act = () => _handler.Handle(command, CancellationToken.None);
|
|
||||||
|
|
||||||
await act.Should().ThrowAsync<InvalidOperationException>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
using FluentAssertions;
|
|
||||||
using Lutra.Application.Interfaces;
|
|
||||||
using Lutra.Application.Models.Verspakketten;
|
|
||||||
using Lutra.Application.Verspakketten;
|
|
||||||
using Moq;
|
|
||||||
using Moq.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace Lutra.Application.UnitTests.Verspakketten;
|
|
||||||
|
|
||||||
public class CreateVerspakketHandlerTests
|
|
||||||
{
|
|
||||||
private readonly Mock<ILutraDbContext> _contextMock;
|
|
||||||
private readonly CreateVerspakket.Handler _handler;
|
|
||||||
|
|
||||||
public CreateVerspakketHandlerTests()
|
|
||||||
{
|
|
||||||
_contextMock = new Mock<ILutraDbContext>();
|
|
||||||
_handler = new CreateVerspakket.Handler(_contextMock.Object);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Handle_SupermarktExists_CreatesVerspakket()
|
|
||||||
{
|
|
||||||
var supermarktId = Guid.NewGuid();
|
|
||||||
var supermarkten = new List<Domain.Entities.Supermarkt>
|
|
||||||
{
|
|
||||||
new() { Id = supermarktId, Naam = "AH", CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow }
|
|
||||||
};
|
|
||||||
|
|
||||||
_contextMock.Setup(c => c.Supermarkten).ReturnsDbSet(supermarkten);
|
|
||||||
_contextMock.Setup(c => c.Verspaketten).ReturnsDbSet(new List<Domain.Entities.Verspakket>());
|
|
||||||
_contextMock.Setup(c => c.SaveChangesAsync(It.IsAny<CancellationToken>())).ReturnsAsync(1);
|
|
||||||
|
|
||||||
var command = new CreateVerspakket.Command("Lente Pakket", 1299, 2, supermarktId, null);
|
|
||||||
|
|
||||||
var result = await _handler.Handle(command, CancellationToken.None);
|
|
||||||
|
|
||||||
result.Id.Should().NotBeEmpty();
|
|
||||||
_contextMock.Verify(c => c.SaveChangesAsync(It.IsAny<CancellationToken>()), Times.Once);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Handle_SupermarktNotFound_ThrowsInvalidOperationException()
|
|
||||||
{
|
|
||||||
_contextMock.Setup(c => c.Supermarkten).ReturnsDbSet(new List<Domain.Entities.Supermarkt>());
|
|
||||||
|
|
||||||
var command = new CreateVerspakket.Command("Lente Pakket", 1299, 2, Guid.NewGuid(), null);
|
|
||||||
|
|
||||||
var act = () => _handler.Handle(command, CancellationToken.None);
|
|
||||||
|
|
||||||
await act.Should().ThrowAsync<InvalidOperationException>()
|
|
||||||
.WithMessage("*was not found*");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Handle_CreatesVerspakketWithCorrectProperties()
|
|
||||||
{
|
|
||||||
var supermarktId = Guid.NewGuid();
|
|
||||||
var supermarkten = new List<Domain.Entities.Supermarkt>
|
|
||||||
{
|
|
||||||
new() { Id = supermarktId, Naam = "Jumbo", CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow }
|
|
||||||
};
|
|
||||||
|
|
||||||
Domain.Entities.Verspakket? savedVerspakket = null;
|
|
||||||
_contextMock.Setup(c => c.Supermarkten).ReturnsDbSet(supermarkten);
|
|
||||||
_contextMock.Setup(c => c.Verspaketten).ReturnsDbSet(new List<Domain.Entities.Verspakket>());
|
|
||||||
_contextMock
|
|
||||||
.Setup(c => c.Verspaketten.AddAsync(It.IsAny<Domain.Entities.Verspakket>(), It.IsAny<CancellationToken>()))
|
|
||||||
.Callback<Domain.Entities.Verspakket, CancellationToken>((v, _) => savedVerspakket = v);
|
|
||||||
_contextMock.Setup(c => c.SaveChangesAsync(It.IsAny<CancellationToken>())).ReturnsAsync(1);
|
|
||||||
|
|
||||||
var command = new CreateVerspakket.Command("Zomer Pakket", 999, 4, supermarktId, null);
|
|
||||||
|
|
||||||
await _handler.Handle(command, CancellationToken.None);
|
|
||||||
|
|
||||||
savedVerspakket.Should().NotBeNull();
|
|
||||||
savedVerspakket!.Naam.Should().Be("Zomer Pakket");
|
|
||||||
savedVerspakket.PrijsInCenten.Should().Be(999);
|
|
||||||
savedVerspakket.AantalPersonen.Should().Be(4);
|
|
||||||
savedVerspakket.SupermarktId.Should().Be(supermarktId);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Handle_WithBeoordeling_CreatesVerspakketWithBeoordeling()
|
|
||||||
{
|
|
||||||
var supermarktId = Guid.NewGuid();
|
|
||||||
var supermarkten = new List<Domain.Entities.Supermarkt>
|
|
||||||
{
|
|
||||||
new() { Id = supermarktId, Naam = "Jumbo", CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow }
|
|
||||||
};
|
|
||||||
|
|
||||||
Domain.Entities.Verspakket? savedVerspakket = null;
|
|
||||||
_contextMock.Setup(c => c.Supermarkten).ReturnsDbSet(supermarkten);
|
|
||||||
_contextMock.Setup(c => c.Verspaketten).ReturnsDbSet(new List<Domain.Entities.Verspakket>());
|
|
||||||
_contextMock
|
|
||||||
.Setup(c => c.Verspaketten.AddAsync(It.IsAny<Domain.Entities.Verspakket>(), It.IsAny<CancellationToken>()))
|
|
||||||
.Callback<Domain.Entities.Verspakket, CancellationToken>((v, _) => savedVerspakket = v);
|
|
||||||
_contextMock.Setup(c => c.SaveChangesAsync(It.IsAny<CancellationToken>())).ReturnsAsync(1);
|
|
||||||
|
|
||||||
var command = new CreateVerspakket.Command(
|
|
||||||
"Zomer Pakket",
|
|
||||||
999,
|
|
||||||
4,
|
|
||||||
supermarktId,
|
|
||||||
new Beoordeling
|
|
||||||
{
|
|
||||||
CijferSmaak = 8,
|
|
||||||
CijferBereiden = 7,
|
|
||||||
Aanbevolen = true,
|
|
||||||
Tekst = "Lekker"
|
|
||||||
});
|
|
||||||
|
|
||||||
await _handler.Handle(command, CancellationToken.None);
|
|
||||||
|
|
||||||
savedVerspakket.Should().NotBeNull();
|
|
||||||
savedVerspakket!.Beoordelingen.Should().ContainSingle();
|
|
||||||
var beoordeling = savedVerspakket.Beoordelingen.Single();
|
|
||||||
beoordeling.CijferSmaak.Should().Be(8);
|
|
||||||
beoordeling.CijferBereiden.Should().Be(7);
|
|
||||||
beoordeling.Aanbevolen.Should().BeTrue();
|
|
||||||
beoordeling.Tekst.Should().Be("Lekker");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-99
@@ -1,99 +0,0 @@
|
|||||||
using FluentAssertions;
|
|
||||||
using Lutra.Application.Interfaces;
|
|
||||||
using Lutra.Application.Models.Verspakketten;
|
|
||||||
using Lutra.Application.Verspakketten;
|
|
||||||
using Moq;
|
|
||||||
using Moq.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace Lutra.Application.UnitTests.Verspakketten;
|
|
||||||
|
|
||||||
public class CreateVerspakketWithFotosHandlerTests
|
|
||||||
{
|
|
||||||
private readonly Mock<ILutraDbContext> _contextMock;
|
|
||||||
private readonly CreateVerspakket.Handler _handler;
|
|
||||||
|
|
||||||
// 1x1 white PNG as base64
|
|
||||||
private const string ValidBase64Png = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI6QAAAABJRU5ErkJggg==";
|
|
||||||
|
|
||||||
public CreateVerspakketWithFotosHandlerTests()
|
|
||||||
{
|
|
||||||
_contextMock = new Mock<ILutraDbContext>();
|
|
||||||
_handler = new CreateVerspakket.Handler(_contextMock.Object);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetupContext(Guid supermarktId)
|
|
||||||
{
|
|
||||||
var supermarkten = new List<Domain.Entities.Supermarkt>
|
|
||||||
{
|
|
||||||
new() { Id = supermarktId, Naam = "AH", CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow }
|
|
||||||
};
|
|
||||||
_contextMock.Setup(c => c.Supermarkten).ReturnsDbSet(supermarkten);
|
|
||||||
_contextMock.Setup(c => c.Verspaketten).ReturnsDbSet(new List<Domain.Entities.Verspakket>());
|
|
||||||
_contextMock.Setup(c => c.SaveChangesAsync(It.IsAny<CancellationToken>())).ReturnsAsync(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Handle_WithFotos_CreatesVerspakketWithFotos()
|
|
||||||
{
|
|
||||||
var supermarktId = Guid.NewGuid();
|
|
||||||
SetupContext(supermarktId);
|
|
||||||
|
|
||||||
Domain.Entities.Verspakket? saved = null;
|
|
||||||
_contextMock
|
|
||||||
.Setup(c => c.Verspaketten.AddAsync(It.IsAny<Domain.Entities.Verspakket>(), It.IsAny<CancellationToken>()))
|
|
||||||
.Callback<Domain.Entities.Verspakket, CancellationToken>((v, _) => saved = v);
|
|
||||||
|
|
||||||
var fotos = new List<VerspakketFoto>
|
|
||||||
{
|
|
||||||
new(ValidBase64Png, IsMainImage: true),
|
|
||||||
new(ValidBase64Png, IsMainImage: false)
|
|
||||||
};
|
|
||||||
|
|
||||||
var command = new CreateVerspakket.Command("Lente Pakket", 999, 2, supermarktId, null, fotos);
|
|
||||||
|
|
||||||
await _handler.Handle(command, CancellationToken.None);
|
|
||||||
|
|
||||||
saved.Should().NotBeNull();
|
|
||||||
saved!.Fotos.Should().HaveCount(2);
|
|
||||||
saved.Fotos.Count(f => f.IsMainImage).Should().Be(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Handle_WithoutFotos_CreatesVerspakketWithNoFotos()
|
|
||||||
{
|
|
||||||
var supermarktId = Guid.NewGuid();
|
|
||||||
SetupContext(supermarktId);
|
|
||||||
|
|
||||||
Domain.Entities.Verspakket? saved = null;
|
|
||||||
_contextMock
|
|
||||||
.Setup(c => c.Verspaketten.AddAsync(It.IsAny<Domain.Entities.Verspakket>(), It.IsAny<CancellationToken>()))
|
|
||||||
.Callback<Domain.Entities.Verspakket, CancellationToken>((v, _) => saved = v);
|
|
||||||
|
|
||||||
var command = new CreateVerspakket.Command("Herfst Pakket", 799, 2, supermarktId, null);
|
|
||||||
|
|
||||||
await _handler.Handle(command, CancellationToken.None);
|
|
||||||
|
|
||||||
saved.Should().NotBeNull();
|
|
||||||
saved!.Fotos.Should().BeEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Handle_FotoBase64Decoded_StoresCorrectBytes()
|
|
||||||
{
|
|
||||||
var supermarktId = Guid.NewGuid();
|
|
||||||
SetupContext(supermarktId);
|
|
||||||
|
|
||||||
Domain.Entities.Verspakket? saved = null;
|
|
||||||
_contextMock
|
|
||||||
.Setup(c => c.Verspaketten.AddAsync(It.IsAny<Domain.Entities.Verspakket>(), It.IsAny<CancellationToken>()))
|
|
||||||
.Callback<Domain.Entities.Verspakket, CancellationToken>((v, _) => saved = v);
|
|
||||||
|
|
||||||
var fotos = new List<VerspakketFoto> { new(ValidBase64Png, IsMainImage: false) };
|
|
||||||
var command = new CreateVerspakket.Command("Pakket", null, 1, supermarktId, null, fotos);
|
|
||||||
|
|
||||||
await _handler.Handle(command, CancellationToken.None);
|
|
||||||
|
|
||||||
var foto = saved!.Fotos.Single();
|
|
||||||
foto.Data.Should().BeEquivalentTo(Convert.FromBase64String(ValidBase64Png));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
using FluentAssertions;
|
|
||||||
using Lutra.Application.Interfaces;
|
|
||||||
using Lutra.Application.Models.Verspakketten;
|
|
||||||
using Lutra.Application.Verspakketten;
|
|
||||||
using Moq;
|
|
||||||
using Moq.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace Lutra.Application.UnitTests.Verspakketten;
|
|
||||||
|
|
||||||
public class UpdateVerspakketHandlerTests
|
|
||||||
{
|
|
||||||
private readonly Mock<ILutraDbContext> _contextMock;
|
|
||||||
private readonly UpdateVerspakket.Handler _handler;
|
|
||||||
|
|
||||||
private const string ValidBase64Png = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI6QAAAABJRU5ErkJggg==";
|
|
||||||
|
|
||||||
public UpdateVerspakketHandlerTests()
|
|
||||||
{
|
|
||||||
_contextMock = new Mock<ILutraDbContext>();
|
|
||||||
_handler = new UpdateVerspakket.Handler(_contextMock.Object);
|
|
||||||
}
|
|
||||||
|
|
||||||
private (Guid verspakketId, Guid supermarktId) SetupContext(
|
|
||||||
List<Domain.Entities.VerspakketFoto>? existingFotos = null)
|
|
||||||
{
|
|
||||||
var supermarktId = Guid.NewGuid();
|
|
||||||
var verspakketId = Guid.NewGuid();
|
|
||||||
|
|
||||||
var supermarkten = new List<Domain.Entities.Supermarkt>
|
|
||||||
{
|
|
||||||
new() { Id = supermarktId, Naam = "AH", CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow }
|
|
||||||
};
|
|
||||||
|
|
||||||
var verspakket = new Domain.Entities.Verspakket
|
|
||||||
{
|
|
||||||
Id = verspakketId,
|
|
||||||
Naam = "Oud Pakket",
|
|
||||||
PrijsInCenten = 500,
|
|
||||||
AantalPersonen = 2,
|
|
||||||
SupermarktId = supermarktId,
|
|
||||||
CreatedAt = DateTime.UtcNow,
|
|
||||||
ModifiedAt = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (var foto in existingFotos ?? [])
|
|
||||||
verspakket.AddFoto(foto);
|
|
||||||
|
|
||||||
_contextMock.Setup(c => c.Supermarkten).ReturnsDbSet(supermarkten);
|
|
||||||
_contextMock.Setup(c => c.Verspaketten).ReturnsDbSet(new List<Domain.Entities.Verspakket> { verspakket });
|
|
||||||
_contextMock.Setup(c => c.VerspakketFotos).ReturnsDbSet(existingFotos ?? []);
|
|
||||||
_contextMock.Setup(c => c.SaveChangesAsync(It.IsAny<CancellationToken>())).ReturnsAsync(1);
|
|
||||||
|
|
||||||
return (verspakketId, supermarktId);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Handle_WithoutFotos_UpdatesFieldsOnly()
|
|
||||||
{
|
|
||||||
var (verspakketId, supermarktId) = SetupContext();
|
|
||||||
|
|
||||||
var command = new UpdateVerspakket.Command(verspakketId, "Nieuw Pakket", 1200, 4, supermarktId);
|
|
||||||
|
|
||||||
var result = await _handler.Handle(command, CancellationToken.None);
|
|
||||||
|
|
||||||
result.Should().NotBeNull();
|
|
||||||
_contextMock.Verify(c => c.SaveChangesAsync(It.IsAny<CancellationToken>()), Times.Once);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Handle_WithFotos_ReplacesFotos()
|
|
||||||
{
|
|
||||||
var oldFotoId = Guid.NewGuid();
|
|
||||||
var existingFotos = new List<Domain.Entities.VerspakketFoto>
|
|
||||||
{
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Id = oldFotoId,
|
|
||||||
Data = [0x00],
|
|
||||||
IsMainImage = true,
|
|
||||||
VerspakketId = Guid.NewGuid(),
|
|
||||||
CreatedAt = DateTime.UtcNow,
|
|
||||||
ModifiedAt = DateTime.UtcNow
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var (verspakketId, supermarktId) = SetupContext(existingFotos);
|
|
||||||
|
|
||||||
var newFotos = new List<VerspakketFoto>
|
|
||||||
{
|
|
||||||
new(ValidBase64Png, IsMainImage: true),
|
|
||||||
new(ValidBase64Png, IsMainImage: false)
|
|
||||||
};
|
|
||||||
|
|
||||||
var command = new UpdateVerspakket.Command(verspakketId, "Pakket", 999, 2, supermarktId, newFotos);
|
|
||||||
|
|
||||||
await _handler.Handle(command, CancellationToken.None);
|
|
||||||
|
|
||||||
_contextMock.Verify(c => c.SaveChangesAsync(It.IsAny<CancellationToken>()), Times.Once);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Handle_NullFotos_DoesNotTouchFotos()
|
|
||||||
{
|
|
||||||
var (verspakketId, supermarktId) = SetupContext();
|
|
||||||
|
|
||||||
var command = new UpdateVerspakket.Command(verspakketId, "Pakket", 800, 3, supermarktId, null);
|
|
||||||
|
|
||||||
await _handler.Handle(command, CancellationToken.None);
|
|
||||||
|
|
||||||
// VerspakketFotos.RemoveRange should NOT be called when Fotos is null
|
|
||||||
_contextMock.Verify(
|
|
||||||
c => c.VerspakketFotos.RemoveRange(It.IsAny<IEnumerable<Domain.Entities.VerspakketFoto>>()),
|
|
||||||
Times.Never);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Handle_VerspakketNotFound_ThrowsInvalidOperationException()
|
|
||||||
{
|
|
||||||
var supermarktId = Guid.NewGuid();
|
|
||||||
_contextMock.Setup(c => c.Verspaketten).ReturnsDbSet(new List<Domain.Entities.Verspakket>());
|
|
||||||
_contextMock.Setup(c => c.Supermarkten).ReturnsDbSet(new List<Domain.Entities.Supermarkt>());
|
|
||||||
|
|
||||||
var command = new UpdateVerspakket.Command(Guid.NewGuid(), "Pakket", 800, 2, supermarktId);
|
|
||||||
|
|
||||||
var act = () => _handler.Handle(command, CancellationToken.None);
|
|
||||||
|
|
||||||
await act.Should().ThrowAsync<InvalidOperationException>().WithMessage("*was not found*");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Handle_InvalidAantalPersonen_ThrowsArgumentException()
|
|
||||||
{
|
|
||||||
var (verspakketId, supermarktId) = SetupContext();
|
|
||||||
|
|
||||||
var command = new UpdateVerspakket.Command(verspakketId, "Pakket", 800, 0, supermarktId);
|
|
||||||
|
|
||||||
var act = () => _handler.Handle(command, CancellationToken.None);
|
|
||||||
|
|
||||||
await act.Should().ThrowAsync<ArgumentException>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
using Lutra.Domain.Entities;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace Lutra.Application.Interfaces;
|
|
||||||
|
|
||||||
public interface ILutraDbContext
|
|
||||||
{
|
|
||||||
DbSet<Supermarkt> Supermarkten { get; }
|
|
||||||
|
|
||||||
DbSet<Beoordeling> Beoordelingen { get; }
|
|
||||||
|
|
||||||
DbSet<VerspakketFoto> VerspakketFotos { get; }
|
|
||||||
|
|
||||||
DbSet<Verspakket> Verspaketten { get; }
|
|
||||||
|
|
||||||
Task<int> SaveChangesAsync(CancellationToken cancellationToken);
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Cortex.Mediator" Version="3.1.2" />
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.7" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\Lutra.Domain\Lutra.Domain.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace Lutra.Application.Models.Supermarkten;
|
|
||||||
|
|
||||||
public record Supermarkt
|
|
||||||
{
|
|
||||||
public required Guid Id { get; init; }
|
|
||||||
|
|
||||||
public required string Naam { get; init; }
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
namespace Lutra.Application.Models.Verspakketten;
|
|
||||||
|
|
||||||
public record Beoordeling
|
|
||||||
{
|
|
||||||
public required int CijferSmaak { get; init; }
|
|
||||||
|
|
||||||
public required int CijferBereiden { get; init; }
|
|
||||||
|
|
||||||
public required bool Aanbevolen { get; init; }
|
|
||||||
|
|
||||||
public string? Tekst { get; init; }
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
using Lutra.Application.Models.Supermarkten;
|
|
||||||
|
|
||||||
namespace Lutra.Application.Models.Verspakketten;
|
|
||||||
|
|
||||||
public record Verspakket
|
|
||||||
{
|
|
||||||
public required Guid Id { get; init; }
|
|
||||||
|
|
||||||
public required string Naam { get; init; }
|
|
||||||
|
|
||||||
public int? PrijsInCenten { get; init; }
|
|
||||||
|
|
||||||
public int AantalPersonen { get; init; }
|
|
||||||
|
|
||||||
public double? AverageCijferSmaak { get; init; }
|
|
||||||
|
|
||||||
public double? AverageCijferBereiden { get; init; }
|
|
||||||
|
|
||||||
public Beoordeling[]? Beoordelingen { get; init; }
|
|
||||||
|
|
||||||
public VerspakketFotoResponse[]? Fotos { get; init; }
|
|
||||||
|
|
||||||
public Supermarkt? Supermarkt { get; init; }
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
namespace Lutra.Application.Models.Verspakketten;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a foto to associate with a verspakket.
|
|
||||||
/// </summary>
|
|
||||||
public sealed record VerspakketFoto(
|
|
||||||
/// <summary>Base64-encoded image data.</summary>
|
|
||||||
string Base64Data,
|
|
||||||
bool IsMainImage);
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace Lutra.Application.Models.Verspakketten;
|
|
||||||
|
|
||||||
public sealed record VerspakketFotoResponse(
|
|
||||||
Guid Id,
|
|
||||||
/// <summary>Base64-encoded image data.</summary>
|
|
||||||
string Base64Data,
|
|
||||||
bool IsMainImage);
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
using Lutra.Application.Models.Supermarkten;
|
|
||||||
|
|
||||||
namespace Lutra.Application.Models.Verspakketten;
|
|
||||||
|
|
||||||
public record VerspakketSummary
|
|
||||||
{
|
|
||||||
public required Guid Id { get; init; }
|
|
||||||
|
|
||||||
public required string Naam { get; init; }
|
|
||||||
|
|
||||||
public int? PrijsInCenten { get; init; }
|
|
||||||
|
|
||||||
public int AantalPersonen { get; init; }
|
|
||||||
|
|
||||||
public double? AverageCijferSmaak { get; init; }
|
|
||||||
|
|
||||||
public double? AverageCijferBereiden { get; init; }
|
|
||||||
|
|
||||||
public VerspakketFotoResponse? Foto { get; init; }
|
|
||||||
|
|
||||||
public Supermarkt? Supermarkt { get; init; }
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
using Cortex.Mediator.Queries;
|
|
||||||
using Lutra.Application.Interfaces;
|
|
||||||
using Lutra.Application.Models.Supermarkten;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace Lutra.Application.Supermarkten;
|
|
||||||
|
|
||||||
public sealed partial class GetSupermarkten
|
|
||||||
{
|
|
||||||
public sealed class Handler(ILutraDbContext context) : IQueryHandler<Query, Response>
|
|
||||||
{
|
|
||||||
public async Task<Response> Handle(Query request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var supermarkten = await context.Supermarkten
|
|
||||||
.AsNoTracking()
|
|
||||||
.Where(w => w.DeletedAt == null)
|
|
||||||
.OrderBy(s => s.Naam)
|
|
||||||
.Skip(request.Skip)
|
|
||||||
.Take(request.Take)
|
|
||||||
.Select(s => new Supermarkt
|
|
||||||
{
|
|
||||||
Id = s.Id,
|
|
||||||
Naam = s.Naam
|
|
||||||
})
|
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
return new Response { Supermarkten = supermarkten };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
using Cortex.Mediator.Queries;
|
|
||||||
|
|
||||||
namespace Lutra.Application.Supermarkten;
|
|
||||||
|
|
||||||
public sealed partial class GetSupermarkten
|
|
||||||
{
|
|
||||||
public record Query(int Skip, int Take) : IQuery<Response>;
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
using Lutra.Application.Models.Supermarkten;
|
|
||||||
|
|
||||||
namespace Lutra.Application.Supermarkten;
|
|
||||||
|
|
||||||
public sealed partial class GetSupermarkten
|
|
||||||
{
|
|
||||||
public sealed record Response
|
|
||||||
{
|
|
||||||
public required IEnumerable<Supermarkt> Supermarkten { get; init; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
namespace Lutra.Application.Supermarkten;
|
|
||||||
|
|
||||||
public sealed partial class GetSupermarkten { }
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
using Cortex.Mediator.Commands;
|
|
||||||
|
|
||||||
namespace Lutra.Application.Verspakketten;
|
|
||||||
|
|
||||||
public sealed partial class AddBeoordeling
|
|
||||||
{
|
|
||||||
public sealed record Command(
|
|
||||||
Guid VerspakketId,
|
|
||||||
int CijferSmaak,
|
|
||||||
int CijferBereiden,
|
|
||||||
bool Aanbevolen,
|
|
||||||
string? Tekst) : ICommand<Response>;
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
using Cortex.Mediator.Commands;
|
|
||||||
using Lutra.Application.Interfaces;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace Lutra.Application.Verspakketten;
|
|
||||||
|
|
||||||
public sealed partial class AddBeoordeling
|
|
||||||
{
|
|
||||||
public sealed class Handler(ILutraDbContext context) : ICommandHandler<Command, Response>
|
|
||||||
{
|
|
||||||
public async Task<Response> Handle(Command request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var verspakketExists = await context.Verspaketten
|
|
||||||
.AsNoTracking()
|
|
||||||
.AnyAsync(v => v.Id == request.VerspakketId && v.DeletedAt == null, cancellationToken);
|
|
||||||
|
|
||||||
if (!verspakketExists)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Verspakket with id '{request.VerspakketId}' was not found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
|
||||||
var beoordeling = new Domain.Entities.Beoordeling
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(),
|
|
||||||
CijferSmaak = request.CijferSmaak,
|
|
||||||
CijferBereiden = request.CijferBereiden,
|
|
||||||
Aanbevolen = request.Aanbevolen,
|
|
||||||
Tekst = request.Tekst,
|
|
||||||
VerspakketId = request.VerspakketId,
|
|
||||||
CreatedAt = now,
|
|
||||||
ModifiedAt = now
|
|
||||||
};
|
|
||||||
|
|
||||||
await context.Beoordelingen.AddAsync(beoordeling, cancellationToken);
|
|
||||||
await context.SaveChangesAsync(cancellationToken);
|
|
||||||
|
|
||||||
return new Response { Id = beoordeling.Id };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
namespace Lutra.Application.Verspakketten;
|
|
||||||
|
|
||||||
public sealed partial class AddBeoordeling
|
|
||||||
{
|
|
||||||
public sealed record Response
|
|
||||||
{
|
|
||||||
public required Guid Id { get; init; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
namespace Lutra.Application.Verspakketten;
|
|
||||||
|
|
||||||
public sealed partial class AddBeoordeling { }
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
using Cortex.Mediator.Commands;
|
|
||||||
using Lutra.Application.Models.Verspakketten;
|
|
||||||
|
|
||||||
namespace Lutra.Application.Verspakketten;
|
|
||||||
|
|
||||||
public sealed partial class CreateVerspakket
|
|
||||||
{
|
|
||||||
public sealed record Command(
|
|
||||||
string Naam,
|
|
||||||
int? PrijsInCenten,
|
|
||||||
int AantalPersonen,
|
|
||||||
Guid SupermarktId,
|
|
||||||
Beoordeling? Beoordeling,
|
|
||||||
IReadOnlyList<VerspakketFoto>? Fotos = null) : ICommand<Response>;
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
using Cortex.Mediator.Commands;
|
|
||||||
using Lutra.Application.Interfaces;
|
|
||||||
using Lutra.Application.Models.Verspakketten;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace Lutra.Application.Verspakketten;
|
|
||||||
|
|
||||||
public sealed partial class CreateVerspakket
|
|
||||||
{
|
|
||||||
public sealed class Handler(ILutraDbContext context) : ICommandHandler<Command, Response>
|
|
||||||
{
|
|
||||||
public async Task<Response> Handle(Command request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var supermarktExists = await context.Supermarkten
|
|
||||||
.AsNoTracking()
|
|
||||||
.AnyAsync(s => s.Id == request.SupermarktId, cancellationToken);
|
|
||||||
|
|
||||||
if (!supermarktExists)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Supermarkt with id '{request.SupermarktId}' was not found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
|
||||||
var verspakket = new Domain.Entities.Verspakket
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(),
|
|
||||||
Naam = request.Naam,
|
|
||||||
PrijsInCenten = request.PrijsInCenten,
|
|
||||||
AantalPersonen = request.AantalPersonen,
|
|
||||||
SupermarktId = request.SupermarktId,
|
|
||||||
CreatedAt = now,
|
|
||||||
ModifiedAt = now
|
|
||||||
};
|
|
||||||
|
|
||||||
if (request.Beoordeling is not null)
|
|
||||||
{
|
|
||||||
verspakket.AddBeoordeling(new Domain.Entities.Beoordeling
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(),
|
|
||||||
CijferSmaak = request.Beoordeling.CijferSmaak,
|
|
||||||
CijferBereiden = request.Beoordeling.CijferBereiden,
|
|
||||||
Aanbevolen = request.Beoordeling.Aanbevolen,
|
|
||||||
Tekst = request.Beoordeling.Tekst,
|
|
||||||
VerspakketId = verspakket.Id,
|
|
||||||
CreatedAt = now,
|
|
||||||
ModifiedAt = now
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.Fotos is { Count: > 0 })
|
|
||||||
{
|
|
||||||
foreach (var foto in request.Fotos)
|
|
||||||
{
|
|
||||||
verspakket.AddFoto(new Domain.Entities.VerspakketFoto
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(),
|
|
||||||
Data = Convert.FromBase64String(foto.Base64Data),
|
|
||||||
IsMainImage = foto.IsMainImage,
|
|
||||||
VerspakketId = verspakket.Id,
|
|
||||||
CreatedAt = now,
|
|
||||||
ModifiedAt = now
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await context.Verspaketten.AddAsync(verspakket, cancellationToken);
|
|
||||||
await context.SaveChangesAsync(cancellationToken);
|
|
||||||
|
|
||||||
return new Response { Id = verspakket.Id };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
namespace Lutra.Application.Verspakketten;
|
|
||||||
|
|
||||||
public sealed partial class CreateVerspakket
|
|
||||||
{
|
|
||||||
public sealed record Response
|
|
||||||
{
|
|
||||||
public required Guid Id { get; init; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
namespace Lutra.Application.Verspakketten;
|
|
||||||
|
|
||||||
public sealed partial class CreateVerspakket { }
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
using Cortex.Mediator.Queries;
|
|
||||||
using Lutra.Application.Interfaces;
|
|
||||||
using Lutra.Application.Models.Supermarkten;
|
|
||||||
using Lutra.Application.Models.Verspakketten;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace Lutra.Application.Verspakketten;
|
|
||||||
|
|
||||||
public sealed partial class GetVerspakket
|
|
||||||
{
|
|
||||||
public sealed class Handler(ILutraDbContext context) : IQueryHandler<Query, Response?>
|
|
||||||
{
|
|
||||||
public async Task<Response?> Handle(Query request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var verspakket = await context.Verspaketten
|
|
||||||
.AsNoTracking()
|
|
||||||
.Where(v => v.Id == request.Id)
|
|
||||||
.Select(v => new Verspakket
|
|
||||||
{
|
|
||||||
Id = v.Id,
|
|
||||||
Naam = v.Naam,
|
|
||||||
PrijsInCenten = v.PrijsInCenten,
|
|
||||||
AantalPersonen = v.AantalPersonen,
|
|
||||||
AverageCijferSmaak = v.Beoordelingen.Any() ? v.Beoordelingen.Average(b => (double)b.CijferSmaak) : null,
|
|
||||||
AverageCijferBereiden = v.Beoordelingen.Any() ? v.Beoordelingen.Average(b => (double)b.CijferBereiden) : null,
|
|
||||||
Beoordelingen = v.Beoordelingen
|
|
||||||
.Select(b => new Beoordeling
|
|
||||||
{
|
|
||||||
CijferSmaak = b.CijferSmaak,
|
|
||||||
CijferBereiden = b.CijferBereiden,
|
|
||||||
Aanbevolen = b.Aanbevolen,
|
|
||||||
Tekst = b.Tekst
|
|
||||||
})
|
|
||||||
.ToArray(),
|
|
||||||
Supermarkt = new Supermarkt
|
|
||||||
{
|
|
||||||
Id = v.Supermarkt.Id,
|
|
||||||
Naam = v.Supermarkt.Naam
|
|
||||||
},
|
|
||||||
Fotos = v.Fotos
|
|
||||||
.Select(f => new VerspakketFotoResponse(
|
|
||||||
f.Id,
|
|
||||||
Convert.ToBase64String(f.Data),
|
|
||||||
f.IsMainImage))
|
|
||||||
.ToArray()
|
|
||||||
})
|
|
||||||
.SingleOrDefaultAsync(cancellationToken);
|
|
||||||
|
|
||||||
return verspakket is null ? null : new Response { Verspakket = verspakket };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
using Cortex.Mediator.Queries;
|
|
||||||
|
|
||||||
namespace Lutra.Application.Verspakketten;
|
|
||||||
|
|
||||||
public sealed partial class GetVerspakket
|
|
||||||
{
|
|
||||||
public record Query(Guid Id) : IQuery<Response?>;
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
using Lutra.Application.Models.Verspakketten;
|
|
||||||
|
|
||||||
namespace Lutra.Application.Verspakketten;
|
|
||||||
|
|
||||||
public sealed partial class GetVerspakket
|
|
||||||
{
|
|
||||||
public sealed record Response
|
|
||||||
{
|
|
||||||
public required Verspakket Verspakket { get; init; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
namespace Lutra.Application.Verspakketten;
|
|
||||||
|
|
||||||
public sealed partial class GetVerspakket { }
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
using Cortex.Mediator.Queries;
|
|
||||||
using Lutra.Application.Interfaces;
|
|
||||||
using Lutra.Application.Models.Supermarkten;
|
|
||||||
using Lutra.Application.Models.Verspakketten;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace Lutra.Application.Verspakketten;
|
|
||||||
|
|
||||||
public sealed partial class GetVerspakketten
|
|
||||||
{
|
|
||||||
public sealed class Handler(ILutraDbContext context) : IQueryHandler<Query, Response>
|
|
||||||
{
|
|
||||||
public async Task<Response> Handle(Query request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var query = context.Verspaketten
|
|
||||||
.Where(w => w.DeletedAt == null)
|
|
||||||
.AsNoTracking();
|
|
||||||
|
|
||||||
// Apply sort before pagination so the database handles ordering efficiently.
|
|
||||||
IOrderedQueryable<Domain.Entities.Verspakket> sorted = request.SortField switch
|
|
||||||
{
|
|
||||||
VerspakketSortField.AverageCijferSmaak =>
|
|
||||||
request.SortDirection == SortDirection.Ascending
|
|
||||||
? query.OrderBy(v => v.Beoordelingen.Average(b => (double)b.CijferSmaak))
|
|
||||||
: query.OrderByDescending(v => v.Beoordelingen.Average(b => (double)b.CijferSmaak)),
|
|
||||||
VerspakketSortField.AverageCijferBereiden =>
|
|
||||||
request.SortDirection == SortDirection.Ascending
|
|
||||||
? query.OrderBy(v => v.Beoordelingen.Average(b => (double)b.CijferBereiden))
|
|
||||||
: query.OrderByDescending(v => v.Beoordelingen.Average(b => (double)b.CijferBereiden)),
|
|
||||||
VerspakketSortField.PrijsInCenten =>
|
|
||||||
request.SortDirection == SortDirection.Ascending
|
|
||||||
? query.OrderBy(v => v.PrijsInCenten)
|
|
||||||
: query.OrderByDescending(v => v.PrijsInCenten),
|
|
||||||
VerspakketSortField.Naam or _ =>
|
|
||||||
request.SortDirection == SortDirection.Ascending
|
|
||||||
? query.OrderBy(v => v.Naam)
|
|
||||||
: query.OrderByDescending(v => v.Naam),
|
|
||||||
};
|
|
||||||
|
|
||||||
var verspakketten = await sorted
|
|
||||||
.Skip(request.Skip)
|
|
||||||
.Take(request.Take)
|
|
||||||
.Select(v => new VerspakketSummary
|
|
||||||
{
|
|
||||||
Id = v.Id,
|
|
||||||
Naam = v.Naam,
|
|
||||||
PrijsInCenten = v.PrijsInCenten,
|
|
||||||
AantalPersonen = v.AantalPersonen,
|
|
||||||
AverageCijferSmaak = v.Beoordelingen.Any() ? v.Beoordelingen.Average(b => (double)b.CijferSmaak) : null,
|
|
||||||
AverageCijferBereiden = v.Beoordelingen.Any() ? v.Beoordelingen.Average(b => (double)b.CijferBereiden) : null,
|
|
||||||
Supermarkt = new Supermarkt
|
|
||||||
{
|
|
||||||
Id = v.Supermarkt.Id,
|
|
||||||
Naam = v.Supermarkt.Naam
|
|
||||||
},
|
|
||||||
Foto = v.Fotos
|
|
||||||
.Where(f => f.IsMainImage)
|
|
||||||
.Select(f => new VerspakketFotoResponse(
|
|
||||||
f.Id,
|
|
||||||
Convert.ToBase64String(f.Data),
|
|
||||||
f.IsMainImage))
|
|
||||||
.SingleOrDefault()
|
|
||||||
})
|
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
return new Response { Verspakketten = verspakketten };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
using Cortex.Mediator.Queries;
|
|
||||||
|
|
||||||
namespace Lutra.Application.Verspakketten;
|
|
||||||
|
|
||||||
public sealed partial class GetVerspakketten
|
|
||||||
{
|
|
||||||
public record Query(
|
|
||||||
int Skip,
|
|
||||||
int Take,
|
|
||||||
VerspakketSortField SortField = VerspakketSortField.Naam,
|
|
||||||
SortDirection SortDirection = SortDirection.Ascending) : IQuery<Response>;
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
using Lutra.Application.Models.Verspakketten;
|
|
||||||
|
|
||||||
namespace Lutra.Application.Verspakketten;
|
|
||||||
|
|
||||||
public sealed partial class GetVerspakketten
|
|
||||||
{
|
|
||||||
public sealed record Response
|
|
||||||
{
|
|
||||||
public required IEnumerable<VerspakketSummary> Verspakketten { get; init; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
namespace Lutra.Application.Verspakketten;
|
|
||||||
|
|
||||||
public sealed partial class GetVerspakketten { }
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace Lutra.Application.Verspakketten;
|
|
||||||
|
|
||||||
public enum SortDirection
|
|
||||||
{
|
|
||||||
Ascending,
|
|
||||||
Descending
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
using Cortex.Mediator.Commands;
|
|
||||||
using Lutra.Application.Models.Verspakketten;
|
|
||||||
|
|
||||||
namespace Lutra.Application.Verspakketten;
|
|
||||||
|
|
||||||
public sealed partial class UpdateVerspakket
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Updates an existing verspakket.
|
|
||||||
/// </summary>
|
|
||||||
public sealed record Command(
|
|
||||||
Guid Id,
|
|
||||||
string Naam,
|
|
||||||
int PrijsInCenten,
|
|
||||||
int AantalPersonen,
|
|
||||||
Guid SupermarktId,
|
|
||||||
IReadOnlyList<VerspakketFoto>? Fotos = null) : ICommand<Response>;
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
using Cortex.Mediator.Commands;
|
|
||||||
using Lutra.Application.Interfaces;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace Lutra.Application.Verspakketten;
|
|
||||||
|
|
||||||
public sealed partial class UpdateVerspakket
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Handles update requests for verspakketten.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class Handler(ILutraDbContext context) : ICommandHandler<Command, Response>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Updates an existing verspakket.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">The update command.</param>
|
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
|
||||||
/// <returns>An empty response.</returns>
|
|
||||||
public async Task<Response> Handle(Command request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(request.Naam))
|
|
||||||
throw new ArgumentException("Naam mag niet leeg zijn.", nameof(request.Naam));
|
|
||||||
|
|
||||||
if (request.Naam.Length > 50)
|
|
||||||
throw new ArgumentException("Naam mag maximaal 50 tekens bevatten.", nameof(request.Naam));
|
|
||||||
|
|
||||||
if (request.PrijsInCenten < 0)
|
|
||||||
throw new ArgumentException("PrijsInCenten mag niet negatief zijn.", nameof(request.PrijsInCenten));
|
|
||||||
|
|
||||||
if (request.AantalPersonen is < 1 or > 10)
|
|
||||||
throw new ArgumentException("AantalPersonen moet tussen 1 en 10 liggen.", nameof(request.AantalPersonen));
|
|
||||||
|
|
||||||
var verspakket = await context.Verspaketten
|
|
||||||
.Include(v => v.Fotos)
|
|
||||||
.FirstOrDefaultAsync(v => v.Id == request.Id && v.DeletedAt == null, cancellationToken);
|
|
||||||
|
|
||||||
if (verspakket is null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Verspakket with id '{request.Id}' was not found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var supermarktExists = await context.Supermarkten
|
|
||||||
.AsNoTracking()
|
|
||||||
.AnyAsync(s => s.Id == request.SupermarktId && s.DeletedAt == null, cancellationToken);
|
|
||||||
|
|
||||||
if (!supermarktExists)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Supermarkt with id '{request.SupermarktId}' was not found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
verspakket.Naam = request.Naam;
|
|
||||||
verspakket.PrijsInCenten = request.PrijsInCenten;
|
|
||||||
verspakket.AantalPersonen = request.AantalPersonen;
|
|
||||||
verspakket.SupermarktId = request.SupermarktId;
|
|
||||||
verspakket.ModifiedAt = DateTime.UtcNow;
|
|
||||||
|
|
||||||
if (request.Fotos is not null)
|
|
||||||
{
|
|
||||||
// Replace all existing fotos
|
|
||||||
foreach (var existing in verspakket.Fotos.ToList())
|
|
||||||
verspakket.RemoveFoto(existing.Id);
|
|
||||||
|
|
||||||
context.VerspakketFotos.RemoveRange(
|
|
||||||
await context.VerspakketFotos
|
|
||||||
.Where(f => f.VerspakketId == request.Id)
|
|
||||||
.ToListAsync(cancellationToken));
|
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
|
||||||
foreach (var foto in request.Fotos)
|
|
||||||
{
|
|
||||||
verspakket.AddFoto(new Domain.Entities.VerspakketFoto
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(),
|
|
||||||
Data = Convert.FromBase64String(foto.Base64Data),
|
|
||||||
IsMainImage = foto.IsMainImage,
|
|
||||||
VerspakketId = verspakket.Id,
|
|
||||||
CreatedAt = now,
|
|
||||||
ModifiedAt = now
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await context.SaveChangesAsync(cancellationToken);
|
|
||||||
|
|
||||||
return new Response();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
namespace Lutra.Application.Verspakketten;
|
|
||||||
|
|
||||||
public sealed partial class UpdateVerspakket
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Represents the result of an update verspakket operation.
|
|
||||||
/// </summary>
|
|
||||||
public sealed record Response;
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
namespace Lutra.Application.Verspakketten;
|
|
||||||
|
|
||||||
public sealed partial class UpdateVerspakket;
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
namespace Lutra.Application.Verspakketten;
|
|
||||||
|
|
||||||
public enum VerspakketSortField
|
|
||||||
{
|
|
||||||
Naam,
|
|
||||||
PrijsInCenten,
|
|
||||||
AverageCijferSmaak,
|
|
||||||
AverageCijferBereiden
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
namespace Lutra.Domain.Entities;
|
|
||||||
|
|
||||||
public abstract class BaseEntity
|
|
||||||
{
|
|
||||||
public Guid Id { get; set; }
|
|
||||||
|
|
||||||
public DateTime CreatedAt { get; set; }
|
|
||||||
|
|
||||||
public DateTime ModifiedAt { get; set; }
|
|
||||||
|
|
||||||
public DateTime? DeletedAt { get; set; }
|
|
||||||
|
|
||||||
public bool IsDeleted => DeletedAt.HasValue;
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace Lutra.Domain.Entities;
|
|
||||||
|
|
||||||
public class Beoordeling : BaseEntity
|
|
||||||
{
|
|
||||||
[Range(1, 10)]
|
|
||||||
public required int CijferSmaak { get; set; }
|
|
||||||
|
|
||||||
[Range(1, 10)]
|
|
||||||
public required int CijferBereiden { get; set; }
|
|
||||||
|
|
||||||
public required bool Aanbevolen { get; set; }
|
|
||||||
|
|
||||||
[MaxLength(1024)]
|
|
||||||
public string? Tekst { get; set; }
|
|
||||||
|
|
||||||
public required Guid VerspakketId { get; set; }
|
|
||||||
|
|
||||||
public virtual Verspakket Verspakket { get; set; } = null!;
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace Lutra.Domain.Entities;
|
|
||||||
|
|
||||||
public class Supermarkt : BaseEntity
|
|
||||||
{
|
|
||||||
[MaxLength(50)]
|
|
||||||
public required string Naam { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace Lutra.Domain.Entities;
|
|
||||||
|
|
||||||
public class Verspakket : BaseEntity
|
|
||||||
{
|
|
||||||
private readonly List<Beoordeling> _beoordelingen = [];
|
|
||||||
private readonly List<VerspakketFoto> _fotos = [];
|
|
||||||
|
|
||||||
[MaxLength(50)]
|
|
||||||
public required string Naam { get; set; }
|
|
||||||
|
|
||||||
[Range(0, int.MaxValue)]
|
|
||||||
public int? PrijsInCenten { get; set; }
|
|
||||||
|
|
||||||
[Range(1, 10)]
|
|
||||||
public required int AantalPersonen { get; set; }
|
|
||||||
|
|
||||||
public required Guid SupermarktId { get; set; }
|
|
||||||
|
|
||||||
public virtual Supermarkt Supermarkt { get; set; } = null!;
|
|
||||||
|
|
||||||
public IReadOnlyCollection<Beoordeling> Beoordelingen => _beoordelingen.AsReadOnly();
|
|
||||||
|
|
||||||
public IReadOnlyCollection<VerspakketFoto> Fotos => _fotos.AsReadOnly();
|
|
||||||
|
|
||||||
public void AddBeoordeling(Beoordeling beoordeling)
|
|
||||||
{
|
|
||||||
_beoordelingen.Add(beoordeling);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddFoto(VerspakketFoto foto)
|
|
||||||
{
|
|
||||||
_fotos.Add(foto);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool RemoveBeoordeling(Guid id)
|
|
||||||
{
|
|
||||||
var beoordeling = _beoordelingen.Find(b => b.Id == id);
|
|
||||||
if (beoordeling is null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
_beoordelingen.Remove(beoordeling);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool RemoveFoto(Guid id)
|
|
||||||
{
|
|
||||||
var foto = _fotos.Find(f => f.Id == id);
|
|
||||||
if (foto is null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
_fotos.Remove(foto);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
namespace Lutra.Domain.Entities;
|
|
||||||
|
|
||||||
public class VerspakketFoto : BaseEntity
|
|
||||||
{
|
|
||||||
public required byte[] Data { get; set; }
|
|
||||||
|
|
||||||
public required bool IsMainImage { get; set; }
|
|
||||||
|
|
||||||
public required Guid VerspakketId { get; set; }
|
|
||||||
|
|
||||||
public virtual Verspakket Verspakket { get; set; } = null!;
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<OutputType>Exe</OutputType>
|
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.7" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.7" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\Lutra.Infrastructure\Lutra.Infrastructure.Sql.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
using Lutra.Domain.Entities;
|
|
||||||
using Lutra.Infrastructure.Sql;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
|
|
||||||
var builder = Host.CreateApplicationBuilder(args);
|
|
||||||
|
|
||||||
builder.Services.AddDbContext<LutraDbContext>(options =>
|
|
||||||
options.UseNpgsql(builder.Configuration.GetConnectionString("LutraDb")));
|
|
||||||
|
|
||||||
using var host = builder.Build();
|
|
||||||
using var scope = host.Services.CreateScope();
|
|
||||||
|
|
||||||
var dbContext = scope.ServiceProvider.GetRequiredService<LutraDbContext>();
|
|
||||||
await dbContext.Database.MigrateAsync();
|
|
||||||
|
|
||||||
if (!await dbContext.Supermarkten.AnyAsync())
|
|
||||||
{
|
|
||||||
var createdAt = DateTime.UtcNow;
|
|
||||||
dbContext.Supermarkten.AddRange(
|
|
||||||
CreateSupermarkt("Albert Heijn", createdAt),
|
|
||||||
CreateSupermarkt("Jumbo", createdAt),
|
|
||||||
CreateSupermarkt("Poiesz", createdAt),
|
|
||||||
CreateSupermarkt("Lidl", createdAt));
|
|
||||||
|
|
||||||
await dbContext.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
static Supermarkt CreateSupermarkt(string naam, DateTime createdAt)
|
|
||||||
{
|
|
||||||
return new Supermarkt
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(),
|
|
||||||
Naam = naam,
|
|
||||||
CreatedAt = createdAt,
|
|
||||||
ModifiedAt = createdAt
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.7" />
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.7">
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.7" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="10.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0" />
|
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\Lutra.Application\Lutra.Application.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
using Lutra.Application.Interfaces;
|
|
||||||
using Lutra.Domain.Entities;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace Lutra.Infrastructure.Sql;
|
|
||||||
|
|
||||||
public class LutraDbContext : DbContext, ILutraDbContext
|
|
||||||
{
|
|
||||||
public LutraDbContext(DbContextOptions<LutraDbContext> options)
|
|
||||||
: base(options)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public DbSet<Supermarkt> Supermarkten => Set<Supermarkt>();
|
|
||||||
|
|
||||||
public DbSet<Beoordeling> Beoordelingen => Set<Beoordeling>();
|
|
||||||
|
|
||||||
public DbSet<VerspakketFoto> VerspakketFotos => Set<VerspakketFoto>();
|
|
||||||
|
|
||||||
public DbSet<Verspakket> Verspaketten => Set<Verspakket>();
|
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
base.OnModelCreating(modelBuilder);
|
|
||||||
|
|
||||||
// Global soft-delete filter: exclude logically deleted entities from all queries.
|
|
||||||
modelBuilder.Entity<Beoordeling>().HasQueryFilter(b => !b.IsDeleted);
|
|
||||||
modelBuilder.Entity<VerspakketFoto>().HasQueryFilter(f => !f.IsDeleted);
|
|
||||||
modelBuilder.Entity<Supermarkt>().HasQueryFilter(s => !s.IsDeleted);
|
|
||||||
|
|
||||||
modelBuilder.Entity<Verspakket>(b =>
|
|
||||||
{
|
|
||||||
b.HasQueryFilter(v => !v.IsDeleted);
|
|
||||||
|
|
||||||
b.HasMany(v => v.Beoordelingen)
|
|
||||||
.WithOne()
|
|
||||||
.HasForeignKey(beo => beo.VerspakketId)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasMany(v => v.Fotos)
|
|
||||||
.WithOne()
|
|
||||||
.HasForeignKey(foto => foto.VerspakketId)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.ToTable(t =>
|
|
||||||
{
|
|
||||||
t.HasCheckConstraint("CK_Verspaketten_AantalPersonen", "\"AantalPersonen\" BETWEEN 1 AND 10");
|
|
||||||
t.HasCheckConstraint("CK_Verspaketten_PrijsInCenten", "\"PrijsInCenten\" IS NULL OR \"PrijsInCenten\" >= 0");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Populates audit fields on tracked entities before persisting.
|
|
||||||
/// </summary>
|
|
||||||
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
var now = DateTime.UtcNow;
|
|
||||||
|
|
||||||
foreach (var entry in ChangeTracker.Entries<BaseEntity>())
|
|
||||||
{
|
|
||||||
switch (entry.State)
|
|
||||||
{
|
|
||||||
case EntityState.Added:
|
|
||||||
entry.Entity.CreatedAt = now;
|
|
||||||
entry.Entity.ModifiedAt = now;
|
|
||||||
break;
|
|
||||||
case EntityState.Modified:
|
|
||||||
entry.Entity.ModifiedAt = now;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return base.SaveChangesAsync(cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Design;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
|
|
||||||
namespace Lutra.Infrastructure.Sql;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Design-time factory used by EF Core tools (dotnet ef migrations) so that
|
|
||||||
/// no startup project or live database connection is required when running migrations.
|
|
||||||
/// Reads ConnectionStrings:LutraDb from the API project's appsettings.json.
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class LutraDbContextFactory : IDesignTimeDbContextFactory<LutraDbContext>
|
|
||||||
{
|
|
||||||
private const string ConnectionStringName = "LutraDb";
|
|
||||||
private const string ConnectionStringEnvironmentVariableName = "ConnectionStrings__LutraDb";
|
|
||||||
|
|
||||||
public LutraDbContext CreateDbContext(string[] args)
|
|
||||||
{
|
|
||||||
var apiProjectPath = GetApiProjectPath();
|
|
||||||
|
|
||||||
var configuration = new ConfigurationBuilder()
|
|
||||||
.SetBasePath(apiProjectPath)
|
|
||||||
.AddJsonFile("appsettings.json", optional: false)
|
|
||||||
.AddJsonFile("appsettings.Development.json", optional: true)
|
|
||||||
.AddEnvironmentVariables()
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
var connectionString = configuration.GetConnectionString(ConnectionStringName);
|
|
||||||
if (string.IsNullOrWhiteSpace(connectionString) || connectionString.Contains("<set-locally>", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"ConnectionStrings:{ConnectionStringName} is not configured. Set it in appsettings.Development.json or via {ConnectionStringEnvironmentVariableName}.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var options = new DbContextOptionsBuilder<LutraDbContext>()
|
|
||||||
.UseNpgsql(connectionString)
|
|
||||||
.Options;
|
|
||||||
|
|
||||||
return new LutraDbContext(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetApiProjectPath()
|
|
||||||
{
|
|
||||||
var currentDirectory = new DirectoryInfo(Directory.GetCurrentDirectory());
|
|
||||||
|
|
||||||
while (currentDirectory is not null)
|
|
||||||
{
|
|
||||||
var apiProjectPath = Path.Combine(currentDirectory.FullName, "Lutra.API");
|
|
||||||
if (Directory.Exists(apiProjectPath))
|
|
||||||
{
|
|
||||||
return apiProjectPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentDirectory = currentDirectory.Parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new InvalidOperationException("Could not locate the Lutra.API project directory.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-149
@@ -1,149 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using Lutra.Infrastructure.Sql;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Lutra.Infrastructure.Sql.Migrations
|
|
||||||
{
|
|
||||||
[DbContext(typeof(LutraDbContext))]
|
|
||||||
[Migration("20260326190730_InitialCreate")]
|
|
||||||
partial class InitialCreate
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder
|
|
||||||
.HasAnnotation("ProductVersion", "10.0.5")
|
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
|
||||||
|
|
||||||
modelBuilder.Entity("Lutra.Domain.Entities.Beoordeling", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<bool>("Aanbevolen")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<int>("CijferBereiden")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("CijferSmaak")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("DeletedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<DateTime>("ModifiedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Tekst")
|
|
||||||
.HasMaxLength(1024)
|
|
||||||
.HasColumnType("character varying(1024)");
|
|
||||||
|
|
||||||
b.Property<Guid?>("VerspakketId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("VerspakketId");
|
|
||||||
|
|
||||||
b.ToTable("Beoordelingen");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Lutra.Domain.Entities.Supermarkt", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("DeletedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<DateTime>("ModifiedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Naam")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(50)
|
|
||||||
.HasColumnType("character varying(50)");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("Supermarkten");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Lutra.Domain.Entities.Verspakket", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<int>("AantalPersonen")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("DeletedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<DateTime>("ModifiedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Naam")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(50)
|
|
||||||
.HasColumnType("character varying(50)");
|
|
||||||
|
|
||||||
b.Property<Guid>("SupermarktId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("SupermarktId");
|
|
||||||
|
|
||||||
b.ToTable("Verspaketten");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Lutra.Domain.Entities.Beoordeling", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Lutra.Domain.Entities.Verspakket", null)
|
|
||||||
.WithMany("Beoordelingen")
|
|
||||||
.HasForeignKey("VerspakketId");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Lutra.Domain.Entities.Verspakket", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Lutra.Domain.Entities.Supermarkt", "Supermarkt")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("SupermarktId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Supermarkt");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Lutra.Domain.Entities.Verspakket", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Beoordelingen");
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Lutra.Infrastructure.Sql.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class InitialCreate : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Supermarkten",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
|
||||||
Naam = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
|
||||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
|
||||||
ModifiedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
|
||||||
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Supermarkten", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Verspaketten",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
|
||||||
Naam = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
|
||||||
AantalPersonen = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
SupermarktId = table.Column<Guid>(type: "uuid", nullable: false),
|
|
||||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
|
||||||
ModifiedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
|
||||||
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Verspaketten", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_Verspaketten_Supermarkten_SupermarktId",
|
|
||||||
column: x => x.SupermarktId,
|
|
||||||
principalTable: "Supermarkten",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Beoordelingen",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
|
||||||
CijferSmaak = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
CijferBereiden = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
Aanbevolen = table.Column<bool>(type: "boolean", nullable: false),
|
|
||||||
Tekst = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
|
|
||||||
VerspakketId = table.Column<Guid>(type: "uuid", nullable: true),
|
|
||||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
|
||||||
ModifiedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
|
||||||
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Beoordelingen", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_Beoordelingen_Verspaketten_VerspakketId",
|
|
||||||
column: x => x.VerspakketId,
|
|
||||||
principalTable: "Verspaketten",
|
|
||||||
principalColumn: "Id");
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Beoordelingen_VerspakketId",
|
|
||||||
table: "Beoordelingen",
|
|
||||||
column: "VerspakketId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Verspaketten_SupermarktId",
|
|
||||||
table: "Verspaketten",
|
|
||||||
column: "SupermarktId");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Beoordelingen");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Verspaketten");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Supermarkten");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Generated
-151
@@ -1,151 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using Lutra.Infrastructure.Sql;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Lutra.Infrastructure.Sql.Migrations
|
|
||||||
{
|
|
||||||
[DbContext(typeof(LutraDbContext))]
|
|
||||||
[Migration("20260409194456_MakeVerspakketAggregateRoot")]
|
|
||||||
partial class MakeVerspakketAggregateRoot
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder
|
|
||||||
.HasAnnotation("ProductVersion", "10.0.5")
|
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
|
||||||
|
|
||||||
modelBuilder.Entity("Lutra.Domain.Entities.Beoordeling", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<bool>("Aanbevolen")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<int>("CijferBereiden")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("CijferSmaak")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("DeletedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<DateTime>("ModifiedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Tekst")
|
|
||||||
.HasMaxLength(1024)
|
|
||||||
.HasColumnType("character varying(1024)");
|
|
||||||
|
|
||||||
b.Property<Guid>("VerspakketId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("VerspakketId");
|
|
||||||
|
|
||||||
b.ToTable("Beoordelingen", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Lutra.Domain.Entities.Supermarkt", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("DeletedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<DateTime>("ModifiedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Naam")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(50)
|
|
||||||
.HasColumnType("character varying(50)");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("Supermarkten");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Lutra.Domain.Entities.Verspakket", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<int>("AantalPersonen")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("DeletedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<DateTime>("ModifiedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Naam")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(50)
|
|
||||||
.HasColumnType("character varying(50)");
|
|
||||||
|
|
||||||
b.Property<Guid>("SupermarktId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("SupermarktId");
|
|
||||||
|
|
||||||
b.ToTable("Verspaketten");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Lutra.Domain.Entities.Beoordeling", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Lutra.Domain.Entities.Verspakket", null)
|
|
||||||
.WithMany("Beoordelingen")
|
|
||||||
.HasForeignKey("VerspakketId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Lutra.Domain.Entities.Verspakket", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Lutra.Domain.Entities.Supermarkt", "Supermarkt")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("SupermarktId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Supermarkt");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Lutra.Domain.Entities.Verspakket", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Beoordelingen");
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Lutra.Infrastructure.Sql.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class MakeVerspakketAggregateRoot : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropForeignKey(
|
|
||||||
name: "FK_Beoordelingen_Verspaketten_VerspakketId",
|
|
||||||
table: "Beoordelingen");
|
|
||||||
|
|
||||||
migrationBuilder.AlterColumn<Guid>(
|
|
||||||
name: "VerspakketId",
|
|
||||||
table: "Beoordelingen",
|
|
||||||
type: "uuid",
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
|
|
||||||
oldClrType: typeof(Guid),
|
|
||||||
oldType: "uuid",
|
|
||||||
oldNullable: true);
|
|
||||||
|
|
||||||
migrationBuilder.AddForeignKey(
|
|
||||||
name: "FK_Beoordelingen_Verspaketten_VerspakketId",
|
|
||||||
table: "Beoordelingen",
|
|
||||||
column: "VerspakketId",
|
|
||||||
principalTable: "Verspaketten",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropForeignKey(
|
|
||||||
name: "FK_Beoordelingen_Verspaketten_VerspakketId",
|
|
||||||
table: "Beoordelingen");
|
|
||||||
|
|
||||||
migrationBuilder.AlterColumn<Guid>(
|
|
||||||
name: "VerspakketId",
|
|
||||||
table: "Beoordelingen",
|
|
||||||
type: "uuid",
|
|
||||||
nullable: true,
|
|
||||||
oldClrType: typeof(Guid),
|
|
||||||
oldType: "uuid");
|
|
||||||
|
|
||||||
migrationBuilder.AddForeignKey(
|
|
||||||
name: "FK_Beoordelingen_Verspaketten_VerspakketId",
|
|
||||||
table: "Beoordelingen",
|
|
||||||
column: "VerspakketId",
|
|
||||||
principalTable: "Verspaketten",
|
|
||||||
principalColumn: "Id");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Generated
-154
@@ -1,154 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using Lutra.Infrastructure.Sql;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Lutra.Infrastructure.Sql.Migrations
|
|
||||||
{
|
|
||||||
[DbContext(typeof(LutraDbContext))]
|
|
||||||
[Migration("20260414193437_AddPrijsInCentenToVerspakket")]
|
|
||||||
partial class AddPrijsInCentenToVerspakket
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder
|
|
||||||
.HasAnnotation("ProductVersion", "10.0.5")
|
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
|
||||||
|
|
||||||
modelBuilder.Entity("Lutra.Domain.Entities.Beoordeling", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<bool>("Aanbevolen")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<int>("CijferBereiden")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("CijferSmaak")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("DeletedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<DateTime>("ModifiedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Tekst")
|
|
||||||
.HasMaxLength(1024)
|
|
||||||
.HasColumnType("character varying(1024)");
|
|
||||||
|
|
||||||
b.Property<Guid>("VerspakketId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("VerspakketId");
|
|
||||||
|
|
||||||
b.ToTable("Beoordelingen", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Lutra.Domain.Entities.Supermarkt", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("DeletedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<DateTime>("ModifiedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Naam")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(50)
|
|
||||||
.HasColumnType("character varying(50)");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("Supermarkten");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Lutra.Domain.Entities.Verspakket", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<int>("AantalPersonen")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("DeletedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<DateTime>("ModifiedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Naam")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(50)
|
|
||||||
.HasColumnType("character varying(50)");
|
|
||||||
|
|
||||||
b.Property<int?>("PrijsInCenten")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<Guid>("SupermarktId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("SupermarktId");
|
|
||||||
|
|
||||||
b.ToTable("Verspaketten");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Lutra.Domain.Entities.Beoordeling", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Lutra.Domain.Entities.Verspakket", null)
|
|
||||||
.WithMany("Beoordelingen")
|
|
||||||
.HasForeignKey("VerspakketId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Lutra.Domain.Entities.Verspakket", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Lutra.Domain.Entities.Supermarkt", "Supermarkt")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("SupermarktId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Supermarkt");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Lutra.Domain.Entities.Verspakket", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Beoordelingen");
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Lutra.Infrastructure.Sql.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class AddPrijsInCentenToVerspakket : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.AddColumn<int>(
|
|
||||||
name: "PrijsInCenten",
|
|
||||||
table: "Verspaketten",
|
|
||||||
type: "integer",
|
|
||||||
nullable: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "PrijsInCenten",
|
|
||||||
table: "Verspaketten");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Generated
-157
@@ -1,157 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using Lutra.Infrastructure.Sql;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Lutra.Infrastructure.Sql.Migrations
|
|
||||||
{
|
|
||||||
[DbContext(typeof(LutraDbContext))]
|
|
||||||
[Migration("20260418173428_AddAantalPersonenCheckConstraint")]
|
|
||||||
partial class AddAantalPersonenCheckConstraint
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder
|
|
||||||
.HasAnnotation("ProductVersion", "10.0.6")
|
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
|
||||||
|
|
||||||
modelBuilder.Entity("Lutra.Domain.Entities.Beoordeling", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<bool>("Aanbevolen")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<int>("CijferBereiden")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("CijferSmaak")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("DeletedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<DateTime>("ModifiedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Tekst")
|
|
||||||
.HasMaxLength(1024)
|
|
||||||
.HasColumnType("character varying(1024)");
|
|
||||||
|
|
||||||
b.Property<Guid>("VerspakketId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("VerspakketId");
|
|
||||||
|
|
||||||
b.ToTable("Beoordelingen", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Lutra.Domain.Entities.Supermarkt", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("DeletedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<DateTime>("ModifiedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Naam")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(50)
|
|
||||||
.HasColumnType("character varying(50)");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("Supermarkten");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Lutra.Domain.Entities.Verspakket", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<int>("AantalPersonen")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("DeletedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<DateTime>("ModifiedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Naam")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(50)
|
|
||||||
.HasColumnType("character varying(50)");
|
|
||||||
|
|
||||||
b.Property<int?>("PrijsInCenten")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<Guid>("SupermarktId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("SupermarktId");
|
|
||||||
|
|
||||||
b.ToTable("Verspaketten", t =>
|
|
||||||
{
|
|
||||||
t.HasCheckConstraint("CK_Verspaketten_AantalPersonen", "\"AantalPersonen\" BETWEEN 1 AND 10");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Lutra.Domain.Entities.Beoordeling", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Lutra.Domain.Entities.Verspakket", null)
|
|
||||||
.WithMany("Beoordelingen")
|
|
||||||
.HasForeignKey("VerspakketId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Lutra.Domain.Entities.Verspakket", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Lutra.Domain.Entities.Supermarkt", "Supermarkt")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("SupermarktId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Supermarkt");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Lutra.Domain.Entities.Verspakket", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Beoordelingen");
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-27
@@ -1,27 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Lutra.Infrastructure.Sql.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class AddAantalPersonenCheckConstraint : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.AddCheckConstraint(
|
|
||||||
name: "CK_Verspaketten_AantalPersonen",
|
|
||||||
table: "Verspaketten",
|
|
||||||
sql: "\"AantalPersonen\" BETWEEN 1 AND 10");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropCheckConstraint(
|
|
||||||
name: "CK_Verspaketten_AantalPersonen",
|
|
||||||
table: "Verspaketten");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Generated
-159
@@ -1,159 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using Lutra.Infrastructure.Sql;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Lutra.Infrastructure.Sql.Migrations
|
|
||||||
{
|
|
||||||
[DbContext(typeof(LutraDbContext))]
|
|
||||||
[Migration("20260418174149_AddPrijsInCentenCheckConstraint")]
|
|
||||||
partial class AddPrijsInCentenCheckConstraint
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder
|
|
||||||
.HasAnnotation("ProductVersion", "10.0.6")
|
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
|
||||||
|
|
||||||
modelBuilder.Entity("Lutra.Domain.Entities.Beoordeling", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<bool>("Aanbevolen")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<int>("CijferBereiden")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("CijferSmaak")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("DeletedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<DateTime>("ModifiedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Tekst")
|
|
||||||
.HasMaxLength(1024)
|
|
||||||
.HasColumnType("character varying(1024)");
|
|
||||||
|
|
||||||
b.Property<Guid>("VerspakketId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("VerspakketId");
|
|
||||||
|
|
||||||
b.ToTable("Beoordelingen", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Lutra.Domain.Entities.Supermarkt", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("DeletedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<DateTime>("ModifiedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Naam")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(50)
|
|
||||||
.HasColumnType("character varying(50)");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("Supermarkten");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Lutra.Domain.Entities.Verspakket", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<int>("AantalPersonen")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("DeletedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<DateTime>("ModifiedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Naam")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(50)
|
|
||||||
.HasColumnType("character varying(50)");
|
|
||||||
|
|
||||||
b.Property<int?>("PrijsInCenten")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<Guid>("SupermarktId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("SupermarktId");
|
|
||||||
|
|
||||||
b.ToTable("Verspaketten", t =>
|
|
||||||
{
|
|
||||||
t.HasCheckConstraint("CK_Verspaketten_AantalPersonen", "\"AantalPersonen\" BETWEEN 1 AND 10");
|
|
||||||
|
|
||||||
t.HasCheckConstraint("CK_Verspaketten_PrijsInCenten", "\"PrijsInCenten\" IS NULL OR \"PrijsInCenten\" >= 0");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Lutra.Domain.Entities.Beoordeling", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Lutra.Domain.Entities.Verspakket", null)
|
|
||||||
.WithMany("Beoordelingen")
|
|
||||||
.HasForeignKey("VerspakketId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Lutra.Domain.Entities.Verspakket", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Lutra.Domain.Entities.Supermarkt", "Supermarkt")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("SupermarktId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Supermarkt");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Lutra.Domain.Entities.Verspakket", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Beoordelingen");
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-27
@@ -1,27 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Lutra.Infrastructure.Sql.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class AddPrijsInCentenCheckConstraint : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.AddCheckConstraint(
|
|
||||||
name: "CK_Verspaketten_PrijsInCenten",
|
|
||||||
table: "Verspaketten",
|
|
||||||
sql: "\"PrijsInCenten\" IS NULL OR \"PrijsInCenten\" >= 0");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropCheckConstraint(
|
|
||||||
name: "CK_Verspaketten_PrijsInCenten",
|
|
||||||
table: "Verspaketten");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Lutra.Infrastructure.Sql.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class AddVerspakketFotos : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "VerspakketFotos",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
|
||||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
|
||||||
ModifiedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
|
||||||
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
|
||||||
Data = table.Column<byte[]>(type: "bytea", nullable: false),
|
|
||||||
VerspakketId = table.Column<Guid>(type: "uuid", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_VerspakketFotos", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_VerspakketFotos_Verspaketten_VerspakketId",
|
|
||||||
column: x => x.VerspakketId,
|
|
||||||
principalTable: "Verspaketten",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_VerspakketFotos_VerspakketId",
|
|
||||||
table: "VerspakketFotos",
|
|
||||||
column: "VerspakketId");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "VerspakketFotos");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Generated
-201
@@ -1,201 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using Lutra.Infrastructure.Sql;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Lutra.Infrastructure.Sql.Migrations
|
|
||||||
{
|
|
||||||
[DbContext(typeof(LutraDbContext))]
|
|
||||||
[Migration("20260425121000_AddIsMainImageToVerspakketFoto")]
|
|
||||||
partial class AddIsMainImageToVerspakketFoto
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder
|
|
||||||
.HasAnnotation("ProductVersion", "10.0.6")
|
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
|
||||||
|
|
||||||
modelBuilder.Entity("Lutra.Domain.Entities.Beoordeling", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<bool>("Aanbevolen")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<int>("CijferBereiden")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("CijferSmaak")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("DeletedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<DateTime>("ModifiedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Tekst")
|
|
||||||
.HasMaxLength(1024)
|
|
||||||
.HasColumnType("character varying(1024)");
|
|
||||||
|
|
||||||
b.Property<Guid>("VerspakketId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("VerspakketId");
|
|
||||||
|
|
||||||
b.ToTable("Beoordelingen", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Lutra.Domain.Entities.Supermarkt", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("DeletedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<DateTime>("ModifiedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Naam")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(50)
|
|
||||||
.HasColumnType("character varying(50)");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("Supermarkten");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Lutra.Domain.Entities.Verspakket", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<int>("AantalPersonen")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("DeletedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<DateTime>("ModifiedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Naam")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(50)
|
|
||||||
.HasColumnType("character varying(50)");
|
|
||||||
|
|
||||||
b.Property<int?>("PrijsInCenten")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<Guid>("SupermarktId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("SupermarktId");
|
|
||||||
|
|
||||||
b.ToTable("Verspaketten", t =>
|
|
||||||
{
|
|
||||||
t.HasCheckConstraint("CK_Verspaketten_AantalPersonen", "\"AantalPersonen\" BETWEEN 1 AND 10");
|
|
||||||
|
|
||||||
t.HasCheckConstraint("CK_Verspaketten_PrijsInCenten", "\"PrijsInCenten\" IS NULL OR \"PrijsInCenten\" >= 0");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Lutra.Domain.Entities.VerspakketFoto", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<byte[]>("Data")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("bytea");
|
|
||||||
|
|
||||||
b.Property<bool>("IsMainImage")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("DeletedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<DateTime>("ModifiedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<Guid>("VerspakketId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("VerspakketId");
|
|
||||||
|
|
||||||
b.ToTable("VerspakketFotos");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Lutra.Domain.Entities.Beoordeling", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Lutra.Domain.Entities.Verspakket", null)
|
|
||||||
.WithMany("Beoordelingen")
|
|
||||||
.HasForeignKey("VerspakketId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Lutra.Domain.Entities.Verspakket", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Lutra.Domain.Entities.Supermarkt", "Supermarkt")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("SupermarktId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Supermarkt");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Lutra.Domain.Entities.VerspakketFoto", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Lutra.Domain.Entities.Verspakket", null)
|
|
||||||
.WithMany("Fotos")
|
|
||||||
.HasForeignKey("VerspakketId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Lutra.Domain.Entities.Verspakket", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Beoordelingen");
|
|
||||||
b.Navigation("Fotos");
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user