Skip to content

Feature/topology#474

Merged
NielsPilgaard merged 14 commits into
mainfrom
feature/topology
Dec 23, 2025
Merged

Feature/topology#474
NielsPilgaard merged 14 commits into
mainfrom
feature/topology

Conversation

@NielsPilgaard

Copy link
Copy Markdown
Owner

No description provided.

@coderabbitai

coderabbitai Bot commented Dec 22, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds NetTopologySuite geography support: new nullable Point Location properties on UserProfile/Group/Post, a LocationService and migration service, refactors search filtering to use geographic distance, updates DB configuration and migrations, and adjusts UI/profile pages to capture and migrate location data.

Changes

Cohort / File(s) Summary
Dependencies & CI
src/shared/Jordnaer.Shared/Jordnaer.Shared.csproj, src/web/Jordnaer/Jordnaer.csproj, .github/workflows/website_frontend_ci.yml
Added NetTopologySuite and Microsoft.EntityFrameworkCore.SqlServer.NetTopologySuite package references; updated Playwright env var keys (double-underscore format).
Domain Model
src/shared/Jordnaer.Shared/Database/UserProfile.cs, src/shared/Jordnaer.Shared/Database/Group.cs, src/shared/Jordnaer.Shared/Database/Post.cs
Added nullable Location property (NetTopologySuite.Geometries.Point?) to UserProfile, Group, and Post.
Database & Migrations
src/web/Jordnaer/Migrations/20251222211350_Add_Location_Geography_Column.cs, ...Designer.cs, JordnaerDbContextModelSnapshot.cs, migration_script.sql
New EF Core migration adding nullable geography Location column to UserProfiles, Posts, Groups; updated model snapshot and SQL migration script.
EF Core Configuration
src/web/Jordnaer/Extensions/WebApplicationBuilderExtensions.cs, tests/web/Jordnaer.Tests/Infrastructure/SqlServerContainer.cs
Enabled NetTopologySuite in SQL Server provider (UseSqlServer(..., o => o.UseNetTopologySuite())) for app and tests.
Location Feature & Services
src/web/Jordnaer/Features/Profile/LocationService.cs, src/web/Jordnaer/Features/Profile/WebApplicationBuilderExtensions.cs
Added ILocationService + LocationService (address/zip → Point) and registered service in DI.
One-time Migration Worker
src/web/Jordnaer/Features/Profile/LocationMigrationService.cs
New BackgroundService that batches resolution of zip-based records to populate Location for users, groups, and posts via ILocationService.
Search Refactor (zip→geography)
src/web/Jordnaer/Features/UserSearch/UserSearchService.cs, src/web/Jordnaer/Features/GroupSearch/GroupSearchService.cs, src/web/Jordnaer/Features/PostSearch/PostSearchService.cs, src/web/Jordnaer/Features/UserSearch/QueryableUserProfileExtensions.cs
Replaced IZipCodeService with ILocationService; changed filtering/ordering from zip-set based to geography-based distance checks (Point, IsWithinDistance/Distance) and adjusted query projections/pagination.
Profile UI & Persistence
src/web/Jordnaer/Pages/Profile/MyProfile.razor, src/web/Jordnaer/Features/Profile/ProfileService.cs, src/web/Jordnaer/Features/Profile/AddressAutoComplete.razor
Added UI to capture address/ZIP-based location, handlers to resolve location via LocationService, persisted Location in ProfileService; added address autocomplete component.
Delete Account / Account UI Changes
src/web/Jordnaer/Components/Account/Pages/Manage/DeletePersonalData.razor, src/web/Jordnaer/Components/Account/Pages/Manage/PersonalData.razor, src/web/Jordnaer/Components/Account/Pages/ConfirmEmail.razor, src/web/Jordnaer/Components/Account/Shared/ManageNavMenu.razor, src/web/Jordnaer/Pages/Home/DeleteUserPage.razor, src/web/Jordnaer/Features/Profile/DeleteUserComponent.razor
Reworked delete-personal-data flow into a simplified delete-user flow with new page /InitializeDeleteUser; removed delete action from PersonalData; added navigation entry; adjusted ConfirmEmail redirect behavior and related UI markup.
Tests
tests/web/Jordnaer.Tests/UserSearch/UserSearchServiceTests.cs
Updated tests to mock ILocationService.GetLocationFromZipCodeAsync and assert on Location (Point) usage; tests and test SQL config enable NetTopologySuite.
Misc / Cleanup
tasks/02-nettopologysuite-migration.md, src/web/Jordnaer/Features/Profile/EditChildProfileTabs.razor
Removed migration planning doc; minor UI formatting/icon tweaks.

Sequence Diagram(s)

sequenceDiagram
    participant Client as User / UI
    participant App as Web App (SearchService)
    participant LocSvc as LocationService (ILocationService)
    participant DataFors as DataForsyningenClient
    participant DB as Database (EF Core + SQL Server w/ NetTopologySuite)

    Client->>App: Submit search (address or zip + radius)
    App->>LocSvc: GetLocationFromAddressAsync / GetLocationFromZipCodeAsync
    LocSvc->>DataFors: GetAddressesWithAutoComplete / GetZipCodesWithAutoComplete
    DataFors-->>LocSvc: Address/Zip results (coords)
    LocSvc->>LocSvc: Parse coords → Point (EPSG:4326)
    LocSvc-->>App: LocationResult (Point, Zip, City)

    App->>DB: Query entities WHERE Location IS WITHIN distance(radius) ORDER BY Location.Distance
    DB-->>App: Filtered, distance-ordered results
    App-->>Client: Return search results (users/groups/posts)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 2 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.69% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'Feature/topology' is overly vague and does not clearly describe the main changes. While it hints at a topology-related feature, it uses generic naming that fails to convey the specific purpose of integrating NetTopologySuite for geolocation functionality across multiple entities. Revise the title to be more specific and descriptive, such as 'Add geolocation support with NetTopologySuite integration' or 'Implement location-based features using geography data types'.
Description check ❓ Inconclusive No pull request description was provided by the author. While descriptions are not strictly required, the absence of any context makes it difficult to understand the scope, rationale, or migration strategy for this significant architectural change involving geolocation features. Add a detailed description explaining the purpose of the changes, the migration strategy for existing data, and the new geolocation capabilities being introduced.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/topology

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (6)
src/shared/Jordnaer.Shared/Database/Post.cs (1)

25-26: Consider adding a spatial index for location-based queries.

If the Location property will be used for distance-based filtering (as indicated in the PR summary), a spatial index on this column would significantly improve query performance. This is typically configured in the EF Core model builder or migration.

Example in your DbContext's OnModelCreating:

modelBuilder.Entity<Post>()
    .HasIndex(p => p.Location)
    .HasMethod("GEOGRAPHY");
src/web/Jordnaer/Pages/Home/DeleteUserPage.razor (1)

1-5: Consider revising route naming and sitemap inclusion.

Two observations:

  1. Route naming: /InitializeDeleteUser is unconventional. Consider a more REST-like path such as /account/delete or /settings/delete-account for consistency with typical web conventions.

  2. Sitemap inclusion: The [Sitemap] attribute will expose this delete page to search engines. Typically, account management/deletion pages should not be indexed. Consider removing the sitemap attribute.

🔎 Suggested changes
-@page "/InitializeDeleteUser"
+@page "/account/delete"

 @layout MainLayout

-@attribute [Sitemap]
src/web/Jordnaer/Migrations/migration_script.sql (1)

727-758: Consider adding spatial indexes for the Location columns.

The migration adds geography columns to enable location-based distance filtering, but no spatial indexes are created. Without indexes, IsWithinDistance queries will perform full table scans, which can significantly degrade performance as data grows.

Example spatial index creation
IF NOT EXISTS (
    SELECT * FROM [__EFMigrationsHistory]
    WHERE [MigrationId] = N'20251222211350_Add_Location_Geography_Column'
)
BEGIN
    CREATE SPATIAL INDEX [SPATIAL_UserProfiles_Location] ON [UserProfiles] ([Location]);
    CREATE SPATIAL INDEX [SPATIAL_Posts_Location] ON [Posts] ([Location]);
    CREATE SPATIAL INDEX [SPATIAL_Groups_Location] ON [Groups] ([Location]);
END;
src/web/Jordnaer/Features/UserSearch/UserSearchService.cs (1)

49-51: Missing AsNoTracking() for read-only query.

The GetUsersAsync method performs a read-only search but doesn't apply AsNoTracking(). This is already correctly used in GetUsersByNameAsync (line 37). As per coding guidelines, read-only queries should use AsNoTracking().

Proposed fix
 var users = context.UserProfiles
              .Where(user => !string.IsNullOrEmpty(user.UserName))
-             .AsQueryable();
+             .AsNoTracking()
+             .AsQueryable();
src/web/Jordnaer/Features/Profile/LocationMigrationService.cs (1)

111-116: Consider skipping delay after the last batch.

The delay is unnecessary after processing the final batch, adding up to 3 extra seconds (one per entity type) to migration time.

🔎 Proposed fix for MigrateUserProfiles (apply similarly to other methods)
 				// Delay between batches to avoid rate limiting
 				if (cancellationToken.IsCancellationRequested)
 					break;

-				await Task.Delay(DelayBetweenBatchesMs, cancellationToken);
+				// Only delay if there are more batches to process
+				if (batches.Skip(1).Any())
+				{
+					await Task.Delay(DelayBetweenBatchesMs, cancellationToken);
+				}

Alternatively, convert batches to a list and use an index check, or restructure with a for loop.

src/web/Jordnaer/Pages/Profile/MyProfile.razor (1)

316-319: Consider enhancing toggle UX with data preservation hints.

The toggle correctly switches input modes, and the data in each state variable (_addressLocation, _zipCodeLocation) is preserved. However, users might be confused when their input visually disappears after toggling and reappears when toggling back.

Consider adding a confirmation dialog or tooltip explaining that switching modes preserves their current input, or consider extracting/populating the zip code from the address when switching modes.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 646a426 and 04d67a3.

📒 Files selected for processing (31)
  • .github/workflows/website_frontend_ci.yml
  • src/shared/Jordnaer.Shared/Database/Group.cs
  • src/shared/Jordnaer.Shared/Database/Post.cs
  • src/shared/Jordnaer.Shared/Database/UserProfile.cs
  • src/shared/Jordnaer.Shared/Jordnaer.Shared.csproj
  • src/web/Jordnaer/Components/Account/Pages/ConfirmEmail.razor
  • src/web/Jordnaer/Components/Account/Pages/Manage/DeletePersonalData.razor
  • src/web/Jordnaer/Components/Account/Pages/Manage/PersonalData.razor
  • src/web/Jordnaer/Components/Account/Shared/ManageNavMenu.razor
  • src/web/Jordnaer/Extensions/WebApplicationBuilderExtensions.cs
  • src/web/Jordnaer/Features/GroupSearch/GroupSearchService.cs
  • src/web/Jordnaer/Features/PostSearch/PostSearchService.cs
  • src/web/Jordnaer/Features/Profile/AddressAutoComplete.razor
  • src/web/Jordnaer/Features/Profile/DeleteUserComponent.razor
  • src/web/Jordnaer/Features/Profile/EditChildProfileTabs.razor
  • src/web/Jordnaer/Features/Profile/LocationMigrationService.cs
  • src/web/Jordnaer/Features/Profile/LocationService.cs
  • src/web/Jordnaer/Features/Profile/ProfileService.cs
  • src/web/Jordnaer/Features/Profile/WebApplicationBuilderExtensions.cs
  • src/web/Jordnaer/Features/UserSearch/QueryableUserProfileExtensions.cs
  • src/web/Jordnaer/Features/UserSearch/UserSearchService.cs
  • src/web/Jordnaer/Jordnaer.csproj
  • src/web/Jordnaer/Migrations/20251222211350_Add_Location_Geography_Column.Designer.cs
  • src/web/Jordnaer/Migrations/20251222211350_Add_Location_Geography_Column.cs
  • src/web/Jordnaer/Migrations/JordnaerDbContextModelSnapshot.cs
  • src/web/Jordnaer/Migrations/migration_script.sql
  • src/web/Jordnaer/Pages/Home/DeleteUserPage.razor
  • src/web/Jordnaer/Pages/Profile/MyProfile.razor
  • tasks/02-nettopologysuite-migration.md
  • tests/web/Jordnaer.Tests/Infrastructure/SqlServerContainer.cs
  • tests/web/Jordnaer.Tests/UserSearch/UserSearchServiceTests.cs
💤 Files with no reviewable changes (1)
  • tasks/02-nettopologysuite-migration.md
🧰 Additional context used
📓 Path-based instructions (4)
**/Features/**/WebApplicationBuilderExtensions.cs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

DI setup must be in WebApplicationBuilderExtensions (named AddYourFeatureServices()) called from Program.cs

Files:

  • src/web/Jordnaer/Features/Profile/WebApplicationBuilderExtensions.cs
src/web/**/*.cs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

src/web/**/*.cs: Always use 'await using var context = await contextFactory.CreateDbContextAsync()' for scoped DbContext access
Use AsNoTracking() for read-only queries in EF Core
Use OneOf<T, TError> for explicit error handling instead of exceptions or null returns
Use IHubContext for broadcasting SignalR messages to specific user groups
Use custom logger extensions like logger.LogFunctionBegan() and include diagnostic context with diagnosticContext.Set()
Publish messages using 'await publishEndpoint.Publish(/* ... */, cancellationToken)'
Use FluentValidation for input validation
Use IFeatureManager for feature flags
Use custom RateLimitExtensions for rate limiting
Apply NetEscapades.AspNetCore.SecurityHeaders for HTTP security headers
Use SixLabors.ImageSharp for image processing
Use Markdig for Markdown parsing
Use Azure Communication Email service for email sending
Use Azure Blob Storage for storing profile pictures and attachments
Use DSFAPI for Danish civil registry search functionality
Support OAuth authentication with Google, Microsoft, and Facebook providers

Files:

  • src/web/Jordnaer/Features/Profile/WebApplicationBuilderExtensions.cs
  • src/web/Jordnaer/Features/Profile/ProfileService.cs
  • src/web/Jordnaer/Features/PostSearch/PostSearchService.cs
  • src/web/Jordnaer/Features/GroupSearch/GroupSearchService.cs
  • src/web/Jordnaer/Features/UserSearch/UserSearchService.cs
  • src/web/Jordnaer/Migrations/20251222211350_Add_Location_Geography_Column.cs
  • src/web/Jordnaer/Migrations/JordnaerDbContextModelSnapshot.cs
  • src/web/Jordnaer/Features/UserSearch/QueryableUserProfileExtensions.cs
  • src/web/Jordnaer/Extensions/WebApplicationBuilderExtensions.cs
  • src/web/Jordnaer/Migrations/20251222211350_Add_Location_Geography_Column.Designer.cs
  • src/web/Jordnaer/Features/Profile/LocationMigrationService.cs
  • src/web/Jordnaer/Features/Profile/LocationService.cs
tests/web/**/*.cs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use NSubstitute for mocking in unit tests

Files:

  • tests/web/Jordnaer.Tests/Infrastructure/SqlServerContainer.cs
  • tests/web/Jordnaer.Tests/UserSearch/UserSearchServiceTests.cs
src/web/**/Components/**/*.razor

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Razor components should use MudBlazor v8.11.0 components for UI

Files:

  • src/web/Jordnaer/Components/Account/Pages/ConfirmEmail.razor
  • src/web/Jordnaer/Components/Account/Pages/Manage/DeletePersonalData.razor
  • src/web/Jordnaer/Components/Account/Pages/Manage/PersonalData.razor
  • src/web/Jordnaer/Components/Account/Shared/ManageNavMenu.razor
🧠 Learnings (6)
📚 Learning: 2025-12-20T19:25:14.338Z
Learnt from: CR
Repo: NielsPilgaard/Jordnaer PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T19:25:14.338Z
Learning: Applies to **/Features/**/WebApplicationBuilderExtensions.cs : DI setup must be in WebApplicationBuilderExtensions (named AddYourFeatureServices()) called from Program.cs

Applied to files:

  • src/web/Jordnaer/Features/Profile/WebApplicationBuilderExtensions.cs
  • src/web/Jordnaer/Extensions/WebApplicationBuilderExtensions.cs
📚 Learning: 2025-12-20T19:25:14.338Z
Learnt from: CR
Repo: NielsPilgaard/Jordnaer PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T19:25:14.338Z
Learning: Applies to src/web/**/*.cs : Always use 'await using var context = await contextFactory.CreateDbContextAsync()' for scoped DbContext access

Applied to files:

  • tests/web/Jordnaer.Tests/Infrastructure/SqlServerContainer.cs
  • src/web/Jordnaer/Extensions/WebApplicationBuilderExtensions.cs
📚 Learning: 2025-12-20T19:25:14.338Z
Learnt from: CR
Repo: NielsPilgaard/Jordnaer PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T19:25:14.338Z
Learning: Applies to src/web/**/Components/**/*.razor : Razor components should use MudBlazor v8.11.0 components for UI

Applied to files:

  • src/web/Jordnaer/Features/Profile/EditChildProfileTabs.razor
  • src/web/Jordnaer/Features/Profile/DeleteUserComponent.razor
📚 Learning: 2025-12-20T19:25:14.338Z
Learnt from: CR
Repo: NielsPilgaard/Jordnaer PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T19:25:14.338Z
Learning: Applies to src/web/**/*.cs : Use DSFAPI for Danish civil registry search functionality

Applied to files:

  • src/web/Jordnaer/Features/UserSearch/UserSearchService.cs
📚 Learning: 2025-12-20T19:25:14.338Z
Learnt from: CR
Repo: NielsPilgaard/Jordnaer PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T19:25:14.338Z
Learning: Applies to tests/web/**/*.cs : Use NSubstitute for mocking in unit tests

Applied to files:

  • tests/web/Jordnaer.Tests/UserSearch/UserSearchServiceTests.cs
📚 Learning: 2025-03-25T07:56:30.061Z
Learnt from: NielsPilgaard
Repo: NielsPilgaard/Jordnaer PR: 421
File: src/web/Jordnaer/Features/Posts/CreatePostComponent.razor:79-83
Timestamp: 2025-03-25T07:56:30.061Z
Learning: In the Post model, ZipCode and City fields are designed to be nullable in the database, eliminating the need for explicit null checks before assignment.

Applied to files:

  • src/shared/Jordnaer.Shared/Database/Post.cs
🧬 Code graph analysis (4)
src/web/Jordnaer/Features/Profile/WebApplicationBuilderExtensions.cs (1)
src/web/Jordnaer/Features/Profile/LocationService.cs (1)
  • LocationService (31-118)
src/web/Jordnaer/Features/GroupSearch/GroupSearchService.cs (2)
src/shared/Jordnaer.Shared/Groups/GroupSlim.cs (1)
  • GroupSlim (3-20)
src/shared/Jordnaer.Shared/Groups/GroupSearchResult.cs (1)
  • GroupSearchResult (3-7)
src/web/Jordnaer/Features/UserSearch/UserSearchService.cs (3)
src/web/Jordnaer/Features/UserSearch/QueryableUserProfileExtensions.cs (1)
  • Task (10-38)
src/shared/Jordnaer.Shared/UserSearch/UserDto.cs (1)
  • UserDto (3-17)
src/shared/Jordnaer.Shared/UserSearch/ChildDto.cs (1)
  • ChildDto (3-10)
src/web/Jordnaer/Features/Profile/LocationService.cs (3)
src/web/Jordnaer/Features/PostSearch/PostSearchService.cs (2)
  • Task (17-61)
  • Task (63-88)
src/web/Jordnaer/Features/UserSearch/QueryableUserProfileExtensions.cs (1)
  • Task (10-38)
src/web/Jordnaer/Features/UserSearch/UserSearchService.cs (4)
  • Task (11-11)
  • Task (12-12)
  • Task (20-41)
  • Task (43-95)
🔇 Additional comments (38)
src/web/Jordnaer/Components/Account/Pages/ConfirmEmail.razor (1)

57-57: Verify the post-confirmation redirect change aligns with UX expectations.

The default redirect after email confirmation has changed from "/profile" to "/" (home page). While the AI summary indicates this aligns with updated routing expectations, please confirm this UX change is intentional and that users landing on the home page (instead of their profile) after email confirmation provides a better experience.

src/web/Jordnaer/Features/Profile/EditChildProfileTabs.razor (1)

5-9: LGTM! Formatting improvements and better icon choice.

The changes are formatting refinements that improve readability. The icon change from DeleteForever to Close is more semantically appropriate for closing a tab rather than implying permanent deletion.

Also applies to: 24-28, 30-36, 63-78

src/web/Jordnaer/Components/Account/Shared/ManageNavMenu.razor (1)

49-54: LGTM!

The new navigation item for user deletion is properly integrated with consistent styling and follows the existing pattern.

src/web/Jordnaer/Components/Account/Pages/Manage/PersonalData.razor (1)

15-20: LGTM!

The simplified UI focuses solely on data download, with deletion functionality appropriately moved to a dedicated page. This separation of concerns improves the user experience.

src/web/Jordnaer/Features/Profile/DeleteUserComponent.razor (1)

7-10: LGTM!

The formatting changes improve code readability without altering behavior. The component correctly uses MudBlazor components as per coding guidelines.

Also applies to: 28-39

src/web/Jordnaer/Features/Profile/AddressAutoComplete.razor (1)

1-17: LGTM!

The MudAutocomplete configuration is well-designed with appropriate debouncing, minimum character requirements, and user-friendly features like progress indicators and clearable input. This aligns with MudBlazor v8.11.0 coding guidelines.

src/web/Jordnaer/Features/Profile/WebApplicationBuilderExtensions.cs (1)

12-12: LGTM!

The scoped lifetime for ILocationService is appropriate for per-request usage in location resolution workflows.

src/shared/Jordnaer.Shared/Database/Group.cs (1)

27-28: Verify spatial indexing for the Location property.

Similar to UserProfile, the Group Location property should have a spatial index configured for efficient geography-based queries. Ensure this is set up in the EF Core mapping or migration.

The spatial index verification script for UserProfile will also check Group indexing.

.github/workflows/website_frontend_ci.yml (1)

37-39: No action needed. Configuration binding is correctly implemented.

The double underscore, __, is supported by all platforms and is automatically replaced by a colon, :. The test code uses .NET's AddEnvironmentVariables() which automatically handles this conversion, and the PlaywrightOptions class properties (BaseUrl, Username, Password) correctly align with the hierarchical structure created by the double underscore naming convention in the workflow environment variables.

tests/web/Jordnaer.Tests/Infrastructure/SqlServerContainer.cs (1)

14-18: LGTM!

The test infrastructure correctly mirrors the production database configuration by enabling UseNetTopologySuite(). This ensures that tests can work with the new Point geography types added to entities.

src/web/Jordnaer/Features/Profile/ProfileService.cs (1)

96-96: LGTM!

The Location property is correctly propagated during profile updates, following the established pattern for other profile fields. The implementation aligns with the coding guidelines for DbContext access and OneOf error handling.

src/web/Jordnaer/Pages/Home/DeleteUserPage.razor (1)

9-24: LGTM!

The page structure follows established patterns with MudBlazor components. The warning text appropriately emphasizes the permanent nature of account deletion.

src/shared/Jordnaer.Shared/Jordnaer.Shared.csproj (1)

16-16: NetTopologySuite 2.6.0 has no external dependencies and is pure managed code, making it compatible with Blazor WebAssembly. The shared project can safely reference this package alongside the browser platform declaration.

src/web/Jordnaer/Extensions/WebApplicationBuilderExtensions.cs (1)

136-137: No action required. The configuration using UseSqlServer with UseNetTopologySuite() is the correct approach. UseNetTopologySuite cannot be combined with UseAzureSql, so the switch from UseAzureSql to UseSqlServer was necessary to support spatial data. While UseAzureSql provides Azure-specific optimizations, it is incompatible with NetTopologySuite support.

src/web/Jordnaer/Migrations/20251222211350_Add_Location_Geography_Column.cs (1)

1-49: Migration structure looks correct.

The migration properly adds nullable Point columns with geography type to all three tables and includes reversible Down logic. Consider adding spatial indexes (as noted in the SQL script review) for query performance.

src/web/Jordnaer/Migrations/JordnaerDbContextModelSnapshot.cs (1)

8-8: Model snapshot correctly reflects the new Location properties.

The auto-generated snapshot properly includes the NetTopologySuite.Geometries using directive and adds Location properties with geography column type to Group, Post, and UserProfile entities.

Also applies to: 246-248, 363-365, 491-493

src/web/Jordnaer/Features/UserSearch/UserSearchService.cs (2)

15-17: Location service integration looks good.

The switch from IZipCodeService to ILocationService for geography-based filtering is well integrated. The ApplyLocationFilter extension method properly handles the async location resolution.

Also applies to: 56-61


68-94: Improved query structure with direct projection.

The refactored query now correctly:

  • Counts before pagination
  • Projects directly to UserDto in the database query
  • Includes nested projections for Categories and Children

This approach is more efficient than materializing entities first.

tests/web/Jordnaer.Tests/UserSearch/UserSearchServiceTests.cs (2)

19-30: Test setup correctly uses NSubstitute for mocking.

The test properly uses Substitute.For<ILocationService>() as per coding guidelines for mocking in unit tests.


239-249: No issues found. The test code correctly constructs LocationResult(aarhusPoint, zipCode, "Aarhus") with compatible types: the Point from NetTopologySuite.Geometries matches the Location parameter, the int zipCode matches the int? parameter, and the string city matches the string? parameter. The test properly uses NSubstitute for mocking per coding guidelines.

Likely an incorrect or invalid review comment.

src/web/Jordnaer/Features/PostSearch/PostSearchService.cs (2)

63-88: Location filter implementation is correct.

The method properly:

  • Returns early if location or radius is not provided
  • Handles null searchLocation gracefully
  • Converts radius from kilometers to meters
  • Uses IsWithinDistance for efficient SQL Server geography filtering

12-15: Service correctly migrated to use ILocationService.

The dependency injection change from IZipCodeService to ILocationService aligns with the PR's geography-based approach.

src/web/Jordnaer/Migrations/20251222211350_Add_Location_Geography_Column.Designer.cs (1)

1-895: Auto-generated migration designer is correctly scaffolded.

This file is auto-generated by EF Core and correctly reflects the target model state including the new Location geography columns on Group, Post, and UserProfile entities.

src/web/Jordnaer/Features/Profile/LocationMigrationService.cs (1)

24-56: LGTM on the migration orchestration structure.

The error handling approach is appropriate for a non-critical background migration. Gracefully logging errors without crashing the application ensures resilience.

src/web/Jordnaer/Features/UserSearch/QueryableUserProfileExtensions.cs (1)

9-37: LGTM on the location filter refactor.

The migration from zip-code-based filtering to geography-based distance checks is well-implemented. The pattern is consistent with GroupSearchService and PostSearchService shown in the relevant snippets.

src/web/Jordnaer/Features/GroupSearch/GroupSearchService.cs (2)

14-56: LGTM on the group search implementation.

The service follows the coding guidelines: uses AsNoTracking() for read-only queries, projects directly to GroupSlim DTO, and properly uses the context factory pattern.


61-87: LGTM on the location filter logic.

The geography-based filtering pattern is consistent with the other search services, and the distance calculation is correct (kilometers to meters conversion).

src/web/Jordnaer/Components/Account/Pages/Manage/DeletePersonalData.razor (1)

5-19: LGTM on the simplified delete user flow.

The UI is clean and provides a clear warning to users. The MudBlazor components are used appropriately per the coding guidelines.

src/web/Jordnaer/Features/Profile/LocationService.cs (4)

6-6: LGTM on the LocationResult record.

Clean and immutable record type for returning geocoding results.


31-35: LGTM on the static GeometryFactory.

GeometryFactory is thread-safe, and using SRID 4326 (WGS84) is correct for latitude/longitude coordinates.


37-76: LGTM on GetLocationFromAddressAsync.

Good defensive programming with null checks and appropriate warning-level logging for failures. The coordinate order (X=longitude, Y=latitude) is correctly documented.


78-117: LGTM on GetLocationFromZipCodeAsync.

Consistent pattern with the address method. The comments about coordinate order help future maintainers understand the DataForsyningen API conventions.

src/web/Jordnaer/Pages/Profile/MyProfile.razor (6)

2-2: LGTM! Geography support integration looks correct.

The NetTopologySuite using statement and ILocationService injection align well with the PR's objective to enable location-based distance filtering.

Also applies to: 7-7


71-123: Well-structured location UI with clear privacy controls.

The location section provides a good user experience with:

  • Clear toggle between address and zip-code-only modes
  • Visual indicators (icons and colors) for privacy levels
  • Informative privacy notice explaining data visibility

229-255: Good privacy-aware location handling.

Both event handlers properly:

  • Clear all location fields when input is empty
  • Handle service failures with user-friendly error messages
  • Explicitly null the Address field in zip-code mode (line 273) to maintain privacy

Also applies to: 257-283


186-189: State field declarations look correct.

The location-related state fields have appropriate types and default values for managing the dual-mode location input.


296-299: Remove unnecessary null check — CategorySelector prevents null assignment by design.

The _selectedCategoryNames variable cannot be null after initialization at line 224. The CategorySelector component's SelectedCategoriesChanged method (lines 35-37) ensures its Categories parameter is always set to either an empty array [] or a populated array, never null. The null check at line 298 is therefore redundant and can be simplified to:

_userProfile.Categories = _categories
.Where(c => _selectedCategoryNames.Contains(c.Name))
.ToList();

Likely an incorrect or invalid review comment.


209-219: Both location initialization variables should be populated independently to prevent empty inputs when toggling modes.

The else-if logic at line 215 means that if a profile has both Address and ZipCode/City populated (possible during migrations or data inconsistencies), only _addressLocation initializes and _zipCodeLocation remains an empty string. When the user later toggles to zip-code mode, the input field would be blank despite the profile containing valid zip/city data.

Initialize both variables from the profile data regardless, since the mode toggle at line 318 doesn't refresh their values:

_addressLocation = _userProfile.Address ?? string.Empty;
_zipCodeLocation = (_userProfile.ZipCode.HasValue && _userProfile.City is not null)
    ? $"{_userProfile.ZipCode} {_userProfile.City}"
    : string.Empty;
_useZipCodeOnly = _userProfile.Address is null && _userProfile.ZipCode.HasValue;

Comment thread src/shared/Jordnaer.Shared/Database/UserProfile.cs
Comment thread src/web/Jordnaer/Features/Profile/AddressAutoComplete.razor
Comment thread src/web/Jordnaer/Features/Profile/LocationMigrationService.cs Outdated
Comment thread src/web/Jordnaer/Jordnaer.csproj
@github-project-automation github-project-automation Bot moved this from Todo to In Progress in Jordnaer Community Website Dec 22, 2025
@NielsPilgaard NielsPilgaard merged commit e24aae7 into main Dec 23, 2025
1 of 2 checks passed
@NielsPilgaard NielsPilgaard deleted the feature/topology branch December 23, 2025 07:27
@github-project-automation github-project-automation Bot moved this from In Progress to Done in Jordnaer Community Website Dec 23, 2025

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/web/Jordnaer/Features/Profile/LocationMigrationService.cs (1)

90-93: Consider batch updates for better performance.

The current implementation updates records individually with separate ExecuteSqlRawAsync calls, resulting in one database round-trip per record. While this works correctly and uses proper parameterization, consider these alternatives for better performance:

  1. Batch raw SQL updates: Accumulate multiple UPDATE statements and execute them together within a transaction.
  2. Use EF Core entities: Load entities with tracking, update the Location property, and call SaveChangesAsync once per batch.

Example with EF Core:

// Load entities with tracking
var users = await context.UserProfiles
    .Where(u => u.ZipCode != null && u.City != null && u.Location == null)
    .Take(BatchSize)
    .ToListAsync(cancellationToken);

foreach (var user in users)
{
    var locationResult = await locationService.GetLocationFromZipCodeAsync(...);
    if (locationResult != null)
        user.Location = locationResult.Location;
}

await context.SaveChangesAsync(cancellationToken); // Batch commit

The current approach is acceptable for an infrequent migration, but batching would reduce database load.

Also applies to: 158-161, 225-228

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 04d67a3 and 2aa92cd.

📒 Files selected for processing (1)
  • src/web/Jordnaer/Features/Profile/LocationMigrationService.cs
🧰 Additional context used
📓 Path-based instructions (1)
src/web/**/*.cs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

src/web/**/*.cs: Always use 'await using var context = await contextFactory.CreateDbContextAsync()' for scoped DbContext access
Use AsNoTracking() for read-only queries in EF Core
Use OneOf<T, TError> for explicit error handling instead of exceptions or null returns
Use IHubContext for broadcasting SignalR messages to specific user groups
Use custom logger extensions like logger.LogFunctionBegan() and include diagnostic context with diagnosticContext.Set()
Publish messages using 'await publishEndpoint.Publish(/* ... */, cancellationToken)'
Use FluentValidation for input validation
Use IFeatureManager for feature flags
Use custom RateLimitExtensions for rate limiting
Apply NetEscapades.AspNetCore.SecurityHeaders for HTTP security headers
Use SixLabors.ImageSharp for image processing
Use Markdig for Markdown parsing
Use Azure Communication Email service for email sending
Use Azure Blob Storage for storing profile pictures and attachments
Use DSFAPI for Danish civil registry search functionality
Support OAuth authentication with Google, Microsoft, and Facebook providers

Files:

  • src/web/Jordnaer/Features/Profile/LocationMigrationService.cs
🧠 Learnings (1)
📚 Learning: 2025-12-20T19:25:14.348Z
Learnt from: CR
Repo: NielsPilgaard/Jordnaer PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T19:25:14.348Z
Learning: Applies to src/web/**/*.cs : Always use 'await using var context = await contextFactory.CreateDbContextAsync()' for scoped DbContext access

Applied to files:

  • src/web/Jordnaer/Features/Profile/LocationMigrationService.cs

Comment on lines +7 to +11
/// <summary>
/// One-time migration service that fetches location data from DataForsyningen for existing users with zip codes.
/// Converts ZipCode + City to Location Point geometry by calling the external API.
/// Runs once on application startup and marks completion to avoid re-running.
/// </summary>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -name "LocationMigrationService.cs" -type f 2>/dev/null | head -20

Repository: NielsPilgaard/Jordnaer

Length of output: 129


🏁 Script executed:

fd "LocationMigrationService.cs" | head -5

Repository: NielsPilgaard/Jordnaer

Length of output: 127


🏁 Script executed:

cat -n src/web/Jordnaer/Features/Profile/LocationMigrationService.cs

Repository: NielsPilgaard/Jordnaer

Length of output: 9251


Documentation claims "one-time" but has no completion tracking mechanism.

The documentation states this is a "one-time migration" that "marks completion to avoid re-running," but the implementation contains no completion tracking (no database flag, configuration, or feature flag). When the application restarts, ExecuteAsync will execute again and query the database to find records where Location is null.

While this is idempotent in practice (already-migrated records won't be re-migrated), the documentation misleads developers into thinking the service won't execute again. If the intent is to run only once across all future deployments, add persistent state tracking to prevent re-execution. If the intent is to handle records added after initial deployment (continuous migration), update the documentation to reflect that behavior.

🤖 Prompt for AI Agents
In src/web/Jordnaer/Features/Profile/LocationMigrationService.cs around lines 7
to 11, the XML doc claims this is a "one-time migration" that "marks completion
to avoid re-running" but the implementation lacks any persistent completion
tracking; update the code to either (A) implement persistent state (eg. add a
MigrationStatus table/flag or an AppSettings key and check/set it before
running, so ExecuteAsync exits if marked complete) or (B) change the
documentation to state this service is idempotent and runs on every startup to
backfill missing Location values; pick one approach and implement the matching
change (persist a completion marker and set it after the first successful run,
or update the summary/comments to reflect continuous/backfill behavior).

private const int BatchSize = 50; // Process in batches to avoid overloading DataForsyningen API
private const int DelayBetweenBatchesMs = 1000; // 1 second delay between batches

private readonly ILocationService locationService = null!;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Incomplete fix causes NullReferenceException.

The previous resource leak fix is incomplete. The field locationService on line 19 is initialized to null!, but the Migrate methods access this field directly (lines 86, 154, 221) instead of receiving the resolved service from line 28 as a parameter. This will cause a NullReferenceException at runtime.

🔎 Proposed fix: Remove field and pass locationService as parameter
-	private readonly ILocationService locationService = null!;
-
 	protected override async Task ExecuteAsync(CancellationToken stoppingToken)
 	{
 		try
 		{
 			await using var scope = serviceScopeFactory.CreateAsyncScope();
 
 			var contextFactory = scope.ServiceProvider.GetRequiredService<IDbContextFactory<JordnaerDbContext>>();
 			var locationService = scope.ServiceProvider.GetRequiredService<ILocationService>();
 			await using var context = await contextFactory.CreateDbContextAsync(stoppingToken);
 
 			logger.LogInformation("Starting location migration from ZipCode to Point geometry");
 
 			// Migrate UserProfiles
-			var userProfilesUpdated = await MigrateUserProfiles(context, stoppingToken);
+			var userProfilesUpdated = await MigrateUserProfiles(context, locationService, stoppingToken);
 			logger.LogInformation("Migrated {Count} user profiles", userProfilesUpdated);
 
 			// Migrate Groups
-			var groupsUpdated = await MigrateGroups(context, stoppingToken);
+			var groupsUpdated = await MigrateGroups(context, locationService, stoppingToken);
 			logger.LogInformation("Migrated {Count} groups", groupsUpdated);
 
 			// Migrate Posts
-			var postsUpdated = await MigratePosts(context, stoppingToken);
+			var postsUpdated = await MigratePosts(context, locationService, stoppingToken);
 			logger.LogInformation("Migrated {Count} posts", postsUpdated);

Then update each Migrate method signature to accept ILocationService locationService as a parameter:

-	private async Task<int> MigrateUserProfiles(JordnaerDbContext context, CancellationToken cancellationToken)
+	private async Task<int> MigrateUserProfiles(JordnaerDbContext context, ILocationService locationService, CancellationToken cancellationToken)
-	private async Task<int> MigrateGroups(JordnaerDbContext context, CancellationToken cancellationToken)
+	private async Task<int> MigrateGroups(JordnaerDbContext context, ILocationService locationService, CancellationToken cancellationToken)
-	private async Task<int> MigratePosts(JordnaerDbContext context, CancellationToken cancellationToken)
+	private async Task<int> MigratePosts(JordnaerDbContext context, ILocationService locationService, CancellationToken cancellationToken)

Also applies to: 28-28, 34-34, 38-38, 42-42

🤖 Prompt for AI Agents
In src/web/Jordnaer/Features/Profile/LocationMigrationService.cs around lines
19, 28, 34, 38, 42 and where Migrate methods use locationService (lines ~86,
154, 221): the field private readonly ILocationService locationService = null!
causes a NullReferenceException because methods still use the field instead of
the resolved service; remove the class-level field entirely, update each Migrate
method signature to accept ILocationService locationService as a parameter,
change all internal uses to the parameter, and update every call site to pass
the resolved service instance obtained at line 28 (or its equivalent) so the
service is injected and no null field remains.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

1 participant