Compare commits

...

5 Commits

Author SHA1 Message Date
moarten
0acc9a1f8e Added endpoints for specific verspakketten, added sorting for All Verspakketten endpoint 2026-04-14 21:47:46 +02:00
moarten
7bce78aa0c Get and return stuff from the database 2026-04-10 22:33:31 +02:00
moarten
87d15b99ed More AI changes 2026-04-09 22:35:54 +02:00
moarten
569cfbb85d Added postgres docker db for local development 2026-04-04 22:26:57 +02:00
moarten
5697cba8d4 AI did some stuff 2026-03-26 21:40:11 +01:00
43 changed files with 1942 additions and 34 deletions

View File

@@ -0,0 +1,205 @@
---
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; dont 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:** dont 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 frameworks 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.

81
Lutra/.github/agents/debug.agent.md vendored Normal file
View File

@@ -0,0 +1,81 @@
---
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.

View File

@@ -0,0 +1,25 @@
---
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.

View File

@@ -0,0 +1,20 @@
---
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.

View File

@@ -0,0 +1,162 @@
---
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.

View File

@@ -0,0 +1,86 @@
---
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

117
Lutra/.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,117 @@
# 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`
- 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 Taylors CleanArchitecture project as a reference for intent and structure, but adapt to this codebases actual implementation.
Where the reference template uses MediatR, this project uses `Cortex.Mediator`.
Add FluentValidation, AutoMapper, NUnit, 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.
## 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.
- Preserve current indentation and brace style in each file.
## Testing guidance
- If tests exist, update or add tests alongside behavior changes.
- Follow the reference templates intent for test coverage:
- unit tests for business logic
- integration tests for infrastructure and data access
- functional tests for API behavior
- 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 |

View File

@@ -1,12 +1,20 @@
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace Lutra.API.Controllers; namespace Lutra.API.Controllers;
/// <summary>
/// Exposes a lightweight health check for the API.
/// </summary>
[ApiController] [ApiController]
[Route("health")] [Route("health")]
public class HealthController : Controller [Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)]
public class HealthController : ControllerBase
{ {
/// <summary>
/// Returns the API health status.
/// </summary>
[HttpGet]
public IActionResult Index() public IActionResult Index()
{ {
return Ok(); return Ok();

View File

@@ -0,0 +1,32 @@
using Cortex.Mediator;
using Lutra.Application.Supermarkten;
using Lutra.Application.Verspakketten;
using Microsoft.AspNetCore.Mvc;
namespace Lutra.API.Controllers;
/// <summary>
/// Provides a dedicated endpoint group for Supermarkt-related operations.
/// </summary>
/// <remarks>
/// This controller is intentionally empty for now. Endpoints will be added when the Supermarkt
/// use cases are implemented.
/// </remarks>
[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 verspakket 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));
}
}

View File

@@ -4,14 +4,51 @@ using Microsoft.AspNetCore.Mvc;
namespace Lutra.API.Controllers namespace Lutra.API.Controllers
{ {
/// <summary>
/// Provides read-only access to verspakket resources.
/// </summary>
[ApiController] [ApiController]
[Route("api/verspakketten")] [Route("api/verspakketten")]
[Produces("application/json")]
public class VerspakkettenController(IMediator mediator) : ControllerBase 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] [HttpGet]
public async Task<GetVerspakketten.Response> GetAll(int skip = 0, int take = 50) [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)); 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);
} }
} }
} }

View File

@@ -1,9 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
<UserSecretsId>c9bd54b0-d347-4e93-bcea-ed5e98a71d5c</UserSecretsId> <UserSecretsId>c9bd54b0-d347-4e93-bcea-ed5e98a71d5c</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS> <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup> </PropertyGroup>
@@ -11,7 +13,13 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Cortex.Mediator" Version="3.1.2" /> <PackageReference Include="Cortex.Mediator" Version="3.1.2" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.5" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.5">
<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="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.23.0" />
<PackageReference Include="Scalar.AspNetCore" Version="2.13.20" />
<ProjectReference Include="..\Lutra.Infrastructure\Lutra.Infrastructure.Sql.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -1,6 +1,10 @@
using Cortex.Mediator.DependencyInjection; using Cortex.Mediator.DependencyInjection;
using Lutra.Application.Verspakketten; using Lutra.Application.Verspakketten;
using Lutra.Application.Interfaces;
using Lutra.Infrastructure.Sql;
using Microsoft.EntityFrameworkCore;
using Scalar.AspNetCore;
namespace Lutra.API namespace Lutra.API
{ {
@@ -15,17 +19,20 @@ namespace Lutra.API
options => options.AddDefaultBehaviors() options => options.AddDefaultBehaviors()
); );
builder.Services.AddDbContext<ILutraDbContext, LutraDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("LutraDb")));
builder.Services.AddControllers(); builder.Services.AddControllers();
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi(); builder.Services.AddOpenApi();
var app = builder.Build(); var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment()) if (app.Environment.IsDevelopment())
{ {
app.MapOpenApi(); app.MapOpenApi();
app.MapScalarApiReference();
app.MapGet("/", () => Results.Redirect("/scalar/v1")).ExcludeFromDescription();
} }
app.UseHttpsRedirection(); app.UseHttpsRedirection();

View File

@@ -5,5 +5,8 @@
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Warning"
} }
}, },
"ConnectionStrings": {
"LutraDb": "Host=db.m91.nl;Username=user;Password=password;Database=Lutra"
},
"AllowedHosts": "*" "AllowedHosts": "*"
} }

View File

@@ -1,13 +1,19 @@
var builder = DistributedApplication.CreateBuilder(args); var builder = DistributedApplication.CreateBuilder(args);
var sql = builder.AddSqlServer("sql") var postgres = builder.AddPostgres("postgres")
.WithDataVolume()
.WithLifetime(ContainerLifetime.Persistent); .WithLifetime(ContainerLifetime.Persistent);
var db = sql.AddDatabase("database", "Lutra"); 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") var apiService = builder.AddProject<Projects.Lutra_API>("apiservice")
.WithHttpHealthCheck("/health") .WithHttpHealthCheck("/health")
.WithReference(db) .WithReference(db)
.WaitFor(db); .WaitForCompletion(migrator);
builder.Build().Run(); builder.Build().Run();

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<Sdk Name="Aspire.AppHost.Sdk" Version="9.5.0" /> <Sdk Name="Aspire.AppHost.Sdk" Version="9.5.0" />
@@ -11,12 +11,13 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" Version="13.1.3" /> <PackageReference Include="Aspire.Hosting.AppHost" Version="13.2.2" />
<PackageReference Include="Aspire.Hosting.SqlServer" Version="13.1.3" /> <PackageReference Include="Aspire.Hosting.PostgreSQL" Version="13.2.2" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Lutra.API\Lutra.API.csproj" /> <ProjectReference Include="..\Lutra.API\Lutra.API.csproj" />
<ProjectReference Include="..\Lutra.Infrastructure.Migrator\Lutra.Infrastructure.Migrator.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -2,6 +2,6 @@
{ {
public record Supermarkt public record Supermarkt
{ {
public required string Name { get; init; } public required string Naam { get; init; }
} }
} }

View File

@@ -0,0 +1,26 @@
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()
.OrderBy(s => s.Naam)
.Skip(request.Skip)
.Take(request.Take)
.Select(s => new Supermarkt { Naam = s.Naam })
.ToListAsync(cancellationToken);
return new Response { Supermarkten = supermarkten };
}
}
}
}

View File

@@ -0,0 +1,9 @@
using Cortex.Mediator.Queries;
namespace Lutra.Application.Supermarkten
{
public sealed partial class GetSupermarkten
{
public record Query(int Skip, int Take) : IQuery<Response>;
}
}

View File

@@ -0,0 +1,12 @@
using Lutra.Application.Models.Supermarkten;
namespace Lutra.Application.Supermarkten
{
public sealed partial class GetSupermarkten
{
public sealed class Response
{
public required IEnumerable<Supermarkt> Supermarkten { get; set; }
}
}
}

View File

@@ -0,0 +1,4 @@
namespace Lutra.Application.Supermarkten
{
public sealed partial class GetSupermarkten { }
}

View File

@@ -0,0 +1,42 @@
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
{
Naam = v.Naam,
PrijsInCenten = v.PrijsInCenten,
Beoordelingen = v.Beoordelingen
.Select(b => new Beoordeling
{
CijferSmaak = b.CijferSmaak,
CijferBereiden = b.CijferBereiden,
Aanbevolen = b.Aanbevolen,
Tekst = b.Tekst
})
.ToArray(),
Supermarkt = new Supermarkt
{
Naam = v.Supermarkt.Naam
}
})
.SingleOrDefaultAsync(cancellationToken);
return verspakket is null ? null : new Response { Verspakket = verspakket };
}
}
}
}

View File

@@ -0,0 +1,9 @@
using Cortex.Mediator.Queries;
namespace Lutra.Application.Verspakketten
{
public sealed partial class GetVerspakket
{
public record Query(Guid Id) : IQuery<Response?>;
}
}

View File

@@ -0,0 +1,12 @@
using Lutra.Application.Models.Verspakketten;
namespace Lutra.Application.Verspakketten
{
public sealed partial class GetVerspakket
{
public sealed class Response
{
public required Verspakket Verspakket { get; set; }
}
}
}

View File

@@ -0,0 +1,4 @@
namespace Lutra.Application.Verspakketten
{
public sealed partial class GetVerspakket { }
}

View File

@@ -1,14 +1,64 @@
using Cortex.Mediator.Queries; 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 namespace Lutra.Application.Verspakketten
{ {
public sealed partial class GetVerspakketten public sealed partial class GetVerspakketten
{ {
public sealed class Handler : IQueryHandler<Query, Response> public sealed class Handler(ILutraDbContext context) : IQueryHandler<Query, Response>
{ {
public async Task<Response> Handle(Query request, CancellationToken cancellationToken) public async Task<Response> Handle(Query request, CancellationToken cancellationToken)
{ {
return new Response { Verspakketten = [] }; var query = context.Verspaketten.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 Verspakket
{
Naam = v.Naam,
PrijsInCenten = v.PrijsInCenten,
Beoordelingen = v.Beoordelingen
.Select(b => new Beoordeling
{
CijferSmaak = b.CijferSmaak,
CijferBereiden = b.CijferBereiden,
Aanbevolen = b.Aanbevolen,
Tekst = b.Tekst
})
.ToArray(),
Supermarkt = new Supermarkt
{
Naam = v.Supermarkt.Naam
}
})
.ToListAsync(cancellationToken);
return new Response { Verspakketten = verspakketten };
} }
} }
} }

View File

@@ -1,9 +1,13 @@
using Cortex.Mediator.Queries; using Cortex.Mediator.Queries;
namespace Lutra.Application.Verspakketten namespace Lutra.Application.Verspakketten
{ {
public sealed partial class GetVerspakketten public sealed partial class GetVerspakketten
{ {
public record Query(int Skip, int Take) : IQuery<Response>; public record Query(
int Skip,
int Take,
VerspakketSortField SortField = VerspakketSortField.Naam,
SortDirection SortDirection = SortDirection.Ascending) : IQuery<Response>;
} }
} }

View File

@@ -0,0 +1,7 @@
namespace Lutra.Application.Verspakketten;
public enum SortDirection
{
Ascending,
Descending
}

View File

@@ -0,0 +1,9 @@
namespace Lutra.Application.Verspakketten;
public enum VerspakketSortField
{
Naam,
PrijsInCenten,
AverageCijferSmaak,
AverageCijferBereiden
}

View File

@@ -14,4 +14,6 @@ public class Beoordeling : BaseEntity
[MaxLength(1024)] [MaxLength(1024)]
public string? Tekst { get; set; } public string? Tekst { get; set; }
public required Guid VerspakketId { get; set; }
} }

View File

@@ -4,17 +4,34 @@ namespace Lutra.Domain.Entities;
public class Verspakket : BaseEntity public class Verspakket : BaseEntity
{ {
private readonly List<Beoordeling> _beoordelingen = [];
[MaxLength(50)] [MaxLength(50)]
public required string Naam { get; set; } public required string Naam { get; set; }
public int? PrijsInCenten { get; set; }
[Range(1, 10)] [Range(1, 10)]
public int AantalPersonen { get; set; } public int AantalPersonen { get; set; }
public required Guid SupermarktId { get; set; } public required Guid SupermarktId { get; set; }
public required virtual Beoordeling[]? Beoordelingen { get; set; }
public required virtual Supermarkt Supermarkt { get; set; } public required virtual Supermarkt Supermarkt { get; set; }
}
public IReadOnlyCollection<Beoordeling> Beoordelingen => _beoordelingen.AsReadOnly();
public void AddBeoordeling(Beoordeling beoordeling)
{
_beoordelingen.Add(beoordeling);
}
public bool RemoveBeoordeling(Guid id)
{
var beoordeling = _beoordelingen.Find(b => b.Id == id);
if (beoordeling is null)
return false;
_beoordelingen.Remove(beoordeling);
return true;
}
}

View File

@@ -0,0 +1,19 @@
<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.5" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.5" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Lutra.Infrastructure\Lutra.Infrastructure.Sql.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,40 @@
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
};
}

View File

@@ -8,6 +8,12 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.5" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.5" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -4,14 +4,33 @@ using Microsoft.EntityFrameworkCore;
namespace Lutra.Infrastructure.Sql; namespace Lutra.Infrastructure.Sql;
public class LutraDbContext : ILutraDbContext public class LutraDbContext : DbContext, ILutraDbContext
{ {
public DbSet<Supermarkt> Supermarkten => throw new NotImplementedException(); public LutraDbContext(DbContextOptions<LutraDbContext> options)
: base(options)
public DbSet<Verspakket> Verspaketten => throw new NotImplementedException();
public Task<int> SaveChangesAsync(CancellationToken cancellationToken)
{ {
throw new NotImplementedException(); }
public DbSet<Supermarkt> Supermarkten => Set<Supermarkt>();
public DbSet<Verspakket> Verspaketten => Set<Verspakket>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Beoordeling>()
.ToTable("Beoordelingen");
modelBuilder.Entity<Verspakket>()
.HasMany(v => v.Beoordelingen)
.WithOne()
.HasForeignKey(b => b.VerspakketId)
.IsRequired();
}
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
return base.SaveChangesAsync(cancellationToken);
} }
} }

View File

@@ -0,0 +1,20 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
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.
/// </summary>
internal sealed class LutraDbContextFactory : IDesignTimeDbContextFactory<LutraDbContext>
{
public LutraDbContext CreateDbContext(string[] args)
{
var options = new DbContextOptionsBuilder<LutraDbContext>()
.UseNpgsql("Host=localhost;Database=lutra;Username=postgres;Password=postgres")
.Options;
return new LutraDbContext(options);
}
}

View File

@@ -0,0 +1,149 @@
// <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
}
}
}

View File

@@ -0,0 +1,100 @@
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");
}
}
}

View File

@@ -0,0 +1,151 @@
// <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
}
}
}

View File

@@ -0,0 +1,60 @@
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");
}
}
}

View File

@@ -0,0 +1,154 @@
// <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
}
}
}

View File

@@ -0,0 +1,28 @@
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");
}
}
}

View File

@@ -0,0 +1,151 @@
// <auto-generated />
using System;
using Lutra.Infrastructure.Sql;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Lutra.Infrastructure.Sql.Migrations
{
[DbContext(typeof(LutraDbContext))]
partial class LutraDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(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
}
}
}

View File

@@ -13,6 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lutra.Infrastructure.Sql",
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lutra.AppHost", "Lutra.AppHost\Lutra.AppHost.csproj", "{B9B41350-EB18-4564-81AB-C16F3E5A0CBB}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lutra.AppHost", "Lutra.AppHost\Lutra.AppHost.csproj", "{B9B41350-EB18-4564-81AB-C16F3E5A0CBB}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lutra.Infrastructure.Migrator", "Lutra.Infrastructure.Migrator\Lutra.Infrastructure.Migrator.csproj", "{6582486C-CEB7-78BF-D043-E0C3C0D6A923}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -39,6 +41,10 @@ Global
{B9B41350-EB18-4564-81AB-C16F3E5A0CBB}.Debug|Any CPU.Build.0 = Debug|Any CPU {B9B41350-EB18-4564-81AB-C16F3E5A0CBB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B9B41350-EB18-4564-81AB-C16F3E5A0CBB}.Release|Any CPU.ActiveCfg = Release|Any CPU {B9B41350-EB18-4564-81AB-C16F3E5A0CBB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B9B41350-EB18-4564-81AB-C16F3E5A0CBB}.Release|Any CPU.Build.0 = Release|Any CPU {B9B41350-EB18-4564-81AB-C16F3E5A0CBB}.Release|Any CPU.Build.0 = Release|Any CPU
{6582486C-CEB7-78BF-D043-E0C3C0D6A923}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6582486C-CEB7-78BF-D043-E0C3C0D6A923}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6582486C-CEB7-78BF-D043-E0C3C0D6A923}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6582486C-CEB7-78BF-D043-E0C3C0D6A923}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE