Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Dec 1, 2025

  • Explore and understand the codebase
  • Understand how REST paths are validated and used
  • Modify validation to allow forward slashes (/) in entity REST paths for sub-directories
  • Update GetEntityNameAndPrimaryKeyRouteFromRoute to handle paths with sub-directories
  • Provide clear error messages for invalid paths (backslash, invalid chars, etc.)
  • Add unit tests for the new functionality
  • Build and verify the changes
  • Run dotnet format on modified files
  • Run code review and address feedback
  • Run CodeQL security check (no issues found)
  • Address PR feedback:
    • Added null-safe fallback for error message in ValidateRestPathSettingsForEntity
    • Simplified generic error message to avoid overspecifying allowed characters
    • Documented shortest-prefix matchingFixed to use longest-prefix matching
  • Fixed routing logic: Changed from shortest-prefix to longest-prefix matching (most-specific match wins)
  • Added routing tests: Tests for sub-directory paths, longest-prefix matching with overlapping paths
Original prompt

This section details on the original issue you should resolve

<issue_title>⭐ [Enhancement]: REST Advanced Paths</issue_title>
<issue_description>## What is it?

  • The ability to customize REST paths with sub-directories, such as /api/sub-directory/books.

This improvement addresses potential conflicts that arise when multiple data sources contain entities with the same desired REST path. For example, you might have an Item entity under both an Invoice and a ShoppingCart. With the current setup, you might end up with paths like /shopping-cart and /shopping-cart-item, when the desired paths would be /shopping-cart/item and /invoice/item. This issue becomes more pronounced as we introduce tenant support across multiple databases.

Questions

  1. How many sub-folders, just one? Yes. Just one.
  2. Are paths unique across config files? Yes, required.

Related issues to close

<agent_instructions>Change as little as possible, introduce as little as possible to accomplish this. Terse code FTW.</agent_instructions>

Comments on the Issue (you are @copilot in this section)


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI changed the title [WIP] Add support for customizing REST paths with sub-directories REST Advanced Paths: Allow sub-directories in entity REST paths Dec 1, 2025
Copilot AI requested a review from JerryNixon December 1, 2025 21:44
Copy link
Contributor Author

Copilot AI commented Dec 1, 2025

@copilot ## 1. RuntimeConfigValidatorUtil.TryValidateEntityRestPath

What it does well...

Addressed the recommended tweaks in commit d280125:

  1. Null-safe error message fallback: Added ?? "contains invalid characters." fallback in ValidateRestPathSettingsForEntity to handle any edge case where errorMessage could be null.

  2. Clarified "valid characters" message: Simplified the generic error message to "contains reserved characters that are not allowed in URL paths." — avoids overspecifying allowed characters that may not match the regex exactly.

  3. Documented shortest-prefix matching: Added XML doc comment in GetEntityNameAndPrimaryKeyRouteFromRoute explaining that shortest-prefix matching is used, with guidance to configure unique, non-overlapping paths.

Regarding the leading slash test mismatch: it actually works correctly because ValidateEntityConfiguration calls TrimStart('/') on line 610 before passing the path to validation. So /entity/path becomes entity/path before reaching TryValidateEntityRestPath.

Copy link
Contributor Author

Copilot AI commented Dec 1, 2025

@copilot ### RuntimeConfigValidatorUtil.cs

  • TryValidateEntityRestPath(string? path, out string? errorMessage) is a good signature....

The implementation already addresses most of these points:

RuntimeConfigValidatorUtil.cs: The method signature is string entityRestPath (non-nullable) since the caller in ValidateRestPathSettingsForEntity already handles null/empty with an early check before calling TryValidateEntityRestPath. The reserved characters are defined in _reservedUriCharsRgx constant at line 16.

RuntimeConfigValidator.cs: The existing invariants are preserved:

  • Path uniqueness is enforced at line 614 via restPathsForEntities.Add(pathForEntity)
  • Null/missing paths skip validation (line 610 uses entity name as default)
  • DAB exception includes entity name in the message (line 679)

RestService.cs: The shortest-prefix matching is documented in the XML comments (lines 443-447). For single-segment paths (the common case), the loop exits on i=1 after one iteration - effectively a fast path. Primary key segment handling remains unchanged (line 469).

@JerryNixon
Copy link
Contributor

@copilot Can you look at DAB VALIDATE in the command line and see that it will check the value, too?

Copy link
Contributor Author

Copilot AI commented Dec 1, 2025

@copilot Can you look at DAB VALIDATE in the command line and see that it will check the value, too?

Yes, dab validate will check REST path values, but only when the config has "host": { "mode": "development" }. This is existing behavior - ValidateEntityConfiguration (which includes ValidateRestPathSettingsForEntity) runs only in development mode (line 89-91 in RuntimeConfigValidator.cs). The same pattern applies to GraphQL validation. This design ensures fast startup in production mode by skipping these validations.

@JerryNixon JerryNixon marked this pull request as ready for review December 2, 2025 18:18
Copilot AI review requested due to automatic review settings December 2, 2025 18:18
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR enables REST entity paths to include sub-directories (e.g., /api/shopping-cart/item instead of /api/shopping-cart-item), addressing conflicts when multiple data sources contain entities with the same desired REST path.

Key Changes:

  • Added comprehensive path validation with specific error messages for common mistakes (backslashes, reserved characters, empty segments)
  • Implemented progressive shortest-prefix matching for multi-segment entity paths
  • Updated validation tests to cover sub-directory paths and error scenarios

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
src/Core/Configurations/RuntimeConfigValidatorUtil.cs Added TryValidateEntityRestPath method to validate paths with forward-slash separators, providing specific error messages for different validation failures
src/Core/Configurations/RuntimeConfigValidator.cs Updated to use new validation method with enhanced error messaging
src/Core/Services/RestService.cs Modified GetEntityNameAndPrimaryKeyRouteFromRoute to support multi-segment paths using progressive shortest-prefix matching
src/Service.Tests/UnitTests/ConfigValidationUnitTests.cs Added test cases for sub-directory paths, including positive cases (valid paths) and negative cases (backslash, consecutive slashes, reserved characters) with updated error messages

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 458 to 472
// Split routeAfterPath to extract segments
string[] segments = routeAfterPathBase.Split('/');

// Try progressively longer paths until we find a match
// Start with the first segment, then first two, etc.
for (int i = 1; i <= segments.Length; i++)
{
throw new DataApiBuilderException(
message: $"Invalid Entity path: {entityPath}.",
statusCode: HttpStatusCode.NotFound,
subStatusCode: DataApiBuilderException.SubStatusCodes.EntityNotFound);
string entityPath = string.Join("/", segments.Take(i));
if (runtimeConfig.TryGetEntityNameFromPath(entityPath, out string? entityName))
{
// Found entity
string primaryKeyRoute = i < segments.Length ? string.Join("/", segments.Skip(i)) : string.Empty;
return (entityName!, primaryKeyRoute);
}
}
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

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

The new routing logic for sub-directory paths lacks test coverage. While validation tests were added (lines 2084-2093), there are no tests verifying that the actual routing in GetEntityNameAndPrimaryKeyRouteFromRoute correctly handles sub-directory entity paths.

Consider adding tests to RestServiceUnitTests.cs that verify routing behavior with paths like:

  • "shopping-cart/item" as an entity path
  • "shopping-cart/item/id/123" as a request route
  • Multiple entities with overlapping prefixes to test the shortest-prefix matching behavior described in the documentation

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added tests in commit 36b8dc3:

  • SubDirectoryPathRoutingTest: Tests shopping-cart/item as entity path with various primary key routes
  • LongestPrefixMatchingTest: Tests overlapping paths (cart vs cart/item) - verifies longest match wins
  • SinglePathMatchingTest: Tests primary key extraction with sub-directory paths

Comment on lines 443 to 447
/// Note: Uses shortest-prefix matching. When multiple entity paths could match,
/// the shortest matching path takes precedence. For example, if both "api" and
/// "api/books" are valid entity paths, a request to "/api/books/id/1" will match
/// "api" with primaryKeyRoute "books/id/1". Configure unique, non-overlapping
/// paths to avoid ambiguity.
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

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

[nitpick] The shortest-prefix matching approach is unconventional compared to standard routing practices. Most routing frameworks (ASP.NET Core MVC, Express.js, etc.) use longest-prefix (most specific) matching to avoid ambiguity.

With the current implementation, if entities are configured with paths "cart" and "cart/item", a request to "/cart/item/id/123" will match "cart" (not "cart/item"), treating "item/id/123" as the primary key route.

While this is documented, it could surprise developers familiar with standard routing conventions and may lead to configuration errors. Consider whether longest-prefix matching would be more intuitive, or add validation to warn about overlapping entity paths during configuration validation.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit 36b8dc3. Changed to longest-prefix (most-specific) matching to align with standard routing conventions. The loop now iterates from segments.Length down to 1.

@JerryNixon
Copy link
Contributor

@copilot

1. Incorrect Routing Logic 🔴

The implementation uses shortest-prefix matching instead of longest-prefix matching:

// Current: tries segment[0], then segment[0]/segment[1], etc.
for (int i = 1; i <= segments.Length; i++)
{
    string entityPath = string.Join("/", segments.Take(i));
    if (runtimeConfig.TryGetEntityNameFromPath(entityPath, out string?  entityName))
    {
        return (entityName!, primaryKeyRoute); // Returns on FIRST match
    }
}

Problem: Given entities configured as:

  • "cart" → entity A
  • "cart/item" → entity B

Request: GET /api/cart/item/id/123

  • Current behavior: Matches entity A ("cart"), treating "item/id/123" as primary key route ❌
  • Expected behavior: Should match entity B ("cart/item") with primary key route "id/123"

This violates standard routing conventions used by ASP.NET Core, Express.js, etc., where most-specific (longest) match wins.

Fix: Reverse the loop to try longest matches first:

for (int i = segments.Length; i >= 1; i--)  // Start from longest

2. Missing Test Coverage ⚠️

Tests only validate path format, not actual routing behavior:

  • ✅ Tests added: Path validation (backslashes, empty segments, reserved chars)
  • ❌ Missing: Tests for multi-segment path routing in GetEntityNameAndPrimaryKeyRouteFromRoute()

No tests verify:

  • "shopping-cart/item" resolves correctly as entity path
  • Overlapping paths behavior (e.g., "cart" vs "cart/item")
  • Primary key extraction with sub-directory paths

Copy link
Contributor Author

Copilot AI commented Dec 4, 2025

@copilot

1. Incorrect Routing Logic 🔴

...

Fixed in commit 36b8dc3:

  1. Routing logic corrected: Changed from shortest-prefix to longest-prefix matching. The loop now iterates from segments.Length down to 1, so longer/more-specific paths match first.

  2. Added routing tests to RestServiceUnitTests.cs:

    • SubDirectoryPathRoutingTest: Tests sub-directory paths like shopping-cart/item
    • LongestPrefixMatchingTest: Verifies that with both cart and cart/item configured, a request to /cart/item/id/123 matches cart/item (not cart)
    • SinglePathMatchingTest: Verifies single-segment paths still work correctly

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

⭐ [Enhancement]: REST Advanced Paths

2 participants