RESTier vNext: Modernize to ASP.NET Core, OData 8.x, and .NET 8/9/10#776
RESTier vNext: Modernize to ASP.NET Core, OData 8.x, and .NET 8/9/10#776jspuij wants to merge 613 commits into
Conversation
|
To start the conversation. Some opinionated choices have been made regarding the service registration and the new constructor signature for ApiBase which both eliminates the ServiceLocator anti-pattern and provides better extensibility. Service chaining has been decoupled from the underlying DI container and does not use reflection anymore (but an interface) to accomplish the chaining. The branch has switched to xUnit and FluentAssertions to better align with the other OData projects. BreakDance is still being used to setup the tests and generate API surfaces. Full OData.NET and AspNetCoreOdata support. dotnet 8, 9 and 10, although we are still waiting on the RTM versions of OData.NET and AspNetCoreOdata for dotnet 10, so it's forward compatibility for now. I might add some other fixes here and there for issues to this branch, while it's being reviewed. Full disclosure: The first part of this migration was done by hand with a little help of copilot. The last month I've been using Claude Code extensively. @robertmclaws shall we work together on this? What do you think. @gathogojr I see that you're involved now as well. Let's discuss. I've done previous upgrades of RESTier as well. |
|
@jspuij I just dropped you an email. Let's catch up this week and talk about it. |
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… project Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…trip EF6's DbGeography.FromText requires the native Microsoft.SqlServer.Types assembly which is not available on macOS/Linux. The three geometry- exercising tests are guarded with xUnit v3 SkipUnless so they execute on Windows CI (where the native types are present) and are skipped cleanly on all other platforms. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…RID, DbGeometry, errors) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…Spatial project Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-> NTS Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-type inference Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tputHelper, Assert.*) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…, publish to Azure DevOps
Replace unconditional [TestMethod, Ignore] with [TestMethod] + Assert.Inconclusive guard inside Round_trips_LineString and Round_trips_Polygon so the tests run (and can pass) on machines where SqlServerSpatial160.dll is loadable, while still reporting Skipped on machines where the native binary is absent (macOS, CI without the binary). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…r tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rTrackingTests MSTest does not call Dispose() automatically; the method existed but was never invoked, leaving LibraryContext instances to GC. Swap to [TestCleanup] Cleanup() so the context is deterministically disposed after every test. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rcerTrackingTests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…maphore Adds SharedLocalDbLock — a named OS semaphore (Windows-only, no-op on macOS/Linux) — and AssemblyHooks classes for the five assemblies that share access to the LocalDB-backed LibraryContext / MarvelContext databases (Tests.AspNetCore, Tests.AspNetCore.NSwag, Tests.AspNetCore.Swagger, Tests.AspNetCore.Versioning, Tests.EntityFramework). Also adds the missing Tests.Shared ProjectReference to the three assemblies that lacked it. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ootprint These three assemblies use Microsoft.AspNetCore.TestHost in-process and have no reference to LibraryContext, RestierTestHelpers, UseSqlServer, or MSSQLLocalDB. The SharedLocalDbLock semaphore added in f46e65a is unnecessary here; only Tests.AspNetCore and Tests.EntityFramework genuinely touch LocalDB and retain their AssemblyHooks. Also removes the now-unused Tests.Shared ProjectReference from the three csproj files. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… tests Guard Acquire() against a second call in the same process (would leak the semaphore handle and self-deadlock via a re-entrant WaitOne on a count=1 semaphore). Add AssemblyHooks to Tests.EntityFrameworkCore, which shares the same LibraryContext_*_EFCore LocalDB database as Tests.AspNetCore's EFCore feature tests and must therefore also serialise via the named semaphore. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
xUnit→MSTest migration follow-ups: restore conditional skips on 6 spatial tests, replace IDisposable with [TestCleanup] in EFQuerySourcerTrackingTests (EF6+EFCore), and add cross-process SharedLocalDbLock for the 3 assemblies that share LocalDB databases (AspNetCore, EntityFramework, EntityFrameworkCore). # Conflicts: # test/Microsoft.Restier.Tests.AspNetCore/FeatureTests/ActionTests.cs
The DotNetCoreCLI@2 test task automatically appends --results-directory $(Agent.TempDirectory)/TestResults when publishTestResults: true is set. Passing it explicitly in arguments caused 'Option --results-directory expects a single argument but 2 were provided' and failed every test job. The auto-added path is the same one ReportGenerator's -reports: glob expects, so removing the explicit line leaves coverage collection unchanged.
After dropping the explicit --results-directory, the DotNetCoreCLI@2 task auto-adds its own results directory which may not be exactly $(Agent.TempDirectory)/TestResults — so the previous narrow glob $(Agent.TempDirectory)/TestResults/**/coverage.cobertura.xml found nothing and ReportGenerator exited non-zero. Broaden the glob to $(Agent.TempDirectory)/**/coverage.cobertura.xml, and prepend a directory listing so the next failure (if any) is diagnosable from the log. Tests pass cleanly on the previous run (3279 passed, 0 failed, 12 skipped), so this is purely a coverage-report-discovery fix.
…'s new Adds a "What's New in 2.0 (Beta)" section enumerating the vNext highlights (modern targets, endpoint routing, API versioning, OpenAPI, deep operations, keyless views, spatial types, multi-tenancy, authorization attributes, AsNoTracking default, magical operations, dynamic routing, deferred materialization, conformance options, validation options, MSTest). Updates Supported Platforms to drop Classic ASP.NET and pre-8.0 .NET targets, calling out that the 1.x line remains on NuGet but is no longer actively developed. Rewrites the Restier Components list to add NSwag, Versioning, and the two Spatial providers, and refines the existing entries for accuracy.
|
@robertmclaws I'm about as far as we discussed. Let me know what you think, or let's catch up in a call to discuss. Thanks for your patience and cooperation so far. |
Summary
This PR is the cumulative result of the RESTier vNext effort, shipping as 2.0.0-beta — a substantial rework on top of the 1.2 baseline. The headline themes:
RestierRouteOptions) for all per-route configuration.[BoundOperation]/[UnboundOperation]methods register themselves.@odata.bind, multi-tenancy, and lower-camelCase JSON.Full release notes:
src/Microsoft.Restier.Docs/release-notes/2-0-0-beta.md. Full diff:v1.2.0...feature/vnext.Platform updates
net8.0,net9.0,net10.0. .NET Framework 4.8 is no longer supported. Restier 1.x is still maintained for .NET Framework consumers.Microsoft.OData.Core/Microsoft.OData.Edm8.x,Microsoft.AspNetCore.OData9.x,Microsoft.OData.ModelBuilder2.x.Breaking changes
AddRestierRouteoverloads collapsed to the options-bag formThe old
AddRestierRouteoverload set (taking individualAction<IServiceCollection>and validation knobs) has been replaced with a single options-bag form:The bag exposes
DeepOperations,Conformance,Validation,UseRestierBatching, andNamingConvention. The versioning package'sAddVersionwas updated to the same shape.Endpoint routing only —
MapRestier()replaces legacy conventionsThe old convention-based routing infrastructure (
RestierRouteConvention,RestierControllerRouteConvention, …) has been deleted. Routes are now registered through ASP.NET Core endpoint routing viaendpoints.MapRestier(), which wires aRestierRouteValueTransformerthat dynamically parses OData paths and dispatches intoRestierController. The marker typeRestierRouteMarkeridentifies route containers as Restier-owned.Query validation: bag-only, no more DI registration of
ODataValidationSettingsPer-route query validation knobs (
MaxTop,MaxSkip,MaxExpansionDepth,MaxAnyAllExpressionDepth,MaxOrderByNodeCount,MaxNodeCount) now live onRestierRouteOptions.Validation— and the bag is the only configuration channel.ODataValidationSettingsis no longer a route-DI service. Consumers must resolveRestierValidationOptionsfrom the route container instead.ODataValidationSettingsinside theAddRestierRouteservice callback throwsInvalidOperationExceptionat startup with a migration message.The only place
MaxTopcan still appear twice — bag and globalODataOptions.SetMaxTop(...)— emits aTrace.TraceWarningif the two values disagree. Closes #684, #719, #751.OperationContext.GetParameterValueFuncis now presence-awareChanged from
Func<string, object>toFunc<string, (bool Present, object Value)>. ThePresentflag istruewhen the parameter name appears in the request, even if the supplied value isnull— necessary to distinguish "URL omitted the parameter" from "URL suppliedp=null" for default substitution and explicit-null semantics on the same parameter.Affects custom
RestierControllersubclasses that construct their owngetParaValueFunc, and any code that constructsOperationContextdirectly.GET queries no longer change-track entities (rolled forward from 1.2)
GET queries execute with change tracking disabled by default (EF Core:
AsNoTrackingWithIdentityResolution; EF6:AsNoTrackingwith a cycle-aware fallback). The submit pipeline and internal lookups are unaffected. Opt back in viarestierOpts.TrackingBehavior = RestierEFTrackingBehavior.TrackAll.IExpandCycleDetectoris now a first-class core service and the controller computes aHasRecursiveExpandhint onQueryRequest.Magical operations
[BoundOperation]/[UnboundOperation]-decorated methods are now fully self-registering. Highlights:ComplexType,EntityType(when keyed), orEnumTypewithout manual model-builder work (#651).Nullable<T>, compiler defaults,[DefaultValue], and the new[Optional]attribute — produce the correctEdmOptionalParametershape and default literal. The runtime substitutes declared defaults on URL-omitted parameters; explicit?p=nullon a nullable parameter passes null. Non-nullable value-type parameters without[Optional]or[DefaultValue]are rejected at startup (#656).[Operation]registration no longer creates an EDM duplicate; the manual registration wins and aTrace.TraceWarningsurfaces the conflict (#652).[Obsolete]on a method emitsCore.V1.Revisions { Kind = Deprecated }, round-tripping into OpenAPI'sdeprecatedfield.[Description]annotatesEdmOperationParameterwithCore.V1.Description.Closes the operation-related items of #750.
Keyless views
Auto-generated keyless-view function imports flow through the normal RESTier query pipeline:
EdmComplexTypeand surfaced as aFunctionImport(GET /Service/MyView()).IQueryExpressionSourcerprojecting the keyless type.OnFilter<View>convention methods fire. Visibility isprotectedorprotected internal, matching the entity-set contract.IQueryExpressionAuthorizerregistrations see view GET requests.RestierEFOptions.TrackingBehaviorapplies to keyless-view reads.DELETE/PUT/PATCHon a function import returns405 Method Not Allowed.Convention name corrected from the V1 docs'
OnFiltering<View>toOnFilter<View>(matches the entity-set convention viaConventionBasedMethodNameFactory.GetEntitySetMethodName).IOperationFilterdoes not fire for view requests — useIQueryExpressionProcessororOnFilter<View>. Closes #741.Authorization metadata on API surfaces
[AllowAnonymous],[Authorize],[Authorize(Policy = …)],[Authorize(Roles = …)], and[Authorize(AuthenticationSchemes = …)]are now honored on:ApiBasesubclass itself (scoping every route the API serves).A new
RestierAuthorizationMetadataPolicypropagates the discovered attributes onto the matched endpoint soAuthorizationMiddlewareruns against them before the request reaches RESTier. RESTier-level convention authorization (Can{Operation}{Target},IChangeSetItemAuthorizer) continues to layer on top.API versioning — new package:
Microsoft.Restier.AspNetCore.VersioningURL-segment API versioning built on
Asp.Versioning. Each version is a distinctApiBasesubclass at its own route prefix, with its own EDM,$metadata, and OpenAPI document.RestierApiVersionSegmentFormatters(Major,MajorMinor) control howApiVersionbecomes a URL segment.[ApiVersion]attributes on each API class are discovered byApiVersionAttributeReader.UseRestierVersionHeadersmiddleware emitsapi-supported-versionsandapi-deprecated-versionsresponse headers.$batchrouting is supported.Sunsetand explicit base-prefix overrides.NSwag integration — new package:
Microsoft.Restier.AspNetCore.NSwagA first-class NSwag integration that ports the Restier OpenAPI document generator onto the NSwag pipeline:
AddRestierNSwag(settings => …)service registration.UseRestierOpenApi()middleware serving the OpenAPI document at/openapi/v1.json(path configurable; honorsSunset).UseRestierReDoc()andUseRestierNSwagUI()for ReDoc and the NSwag UI, both registry-aware and listing user-registered NSwag documents in the dropdown.RestierControllerApiExplorerConventionexcludesRestierControllerfrom plain MVCApiExplorerdiscovery — hand-written controllers stay isolated from the Restier doc.NSwag is the recommended integration for new projects; the Swashbuckle-based
Microsoft.Restier.AspNetCore.Swaggerpackage remains supported.OpenAPI annotations from .NET attributes
Restier scans CLR types for standard .NET attributes and emits the matching OData vocabulary annotations into
$metadata. NSwag and Swagger surface them in the generated OpenAPI document with no extra configuration:[Description]Core.V1.Description[DatabaseGenerated]Core.V1.ComputedreadOnly[ReadOnly(true)]Core.V1.Immutable[Obsolete](operation)Core.V1.Revisions { Kind = Deprecated }deprecated: true[Range]Validation.Min/Validation.Maxminimum/maximum[RegularExpression]Validation.PatternpatternCore.V1.ComputedandCore.V1.Immutableare not metadata-only — the submit pipeline reads them to drop request-body properties before the change set is applied.Spatial types — new packages
Round-tripping
Microsoft.Spatialtypes through EF6 and EF Core, plus server-side translation of ODatageo.*filter functions:Microsoft.Restier.EntityFramework.SpatialwiresMicrosoft.SpatialtoSystem.Data.Entity.Spatial.DbGeography/DbGeometryviaDbSpatialConverterandDbSpatialModelMetadataProvider. Register withservices.AddRestierSpatial().Microsoft.Restier.EntityFrameworkCore.SpatialwiresMicrosoft.Spatialto NetTopologySuite viaNtsSpatialConverterand column-type inference (NtsSpatialModelMetadataProvider).[Spatial]opts CLR properties into the EDMGeography/Geometryprimitive types.RestierSpatialFilterBindertranslatesgeo.intersects,geo.distance, andgeo.lengthto provider methods/properties so EF Core can push them to the database.SridPrefixHelpersmediates the SQL Server WKT dialect (SRID=4326;…) when needed.ODataOptions.TimeZoneis propagated to the filter binder (fixes #704).Closes #673.
Multi-tenancy guide and middleware support
Restier's per-route scoped DI plus EF Core's runtime
DbContextOptionsconfiguration are enough to build a DB-per-tenant SaaS service from oneApiBasesubclass:PathSegmentTenantResolutionMiddlewarereads the tenant id from the URL, validates it against anIConnectionStringProvider, and populates a scopedITenantContext.AddDbContextfactory bridges back viaIHttpContextAccessorto pick the right connection string at request time.@odata.contextpreserves the tenant prefix viaPathBase.No changes to RESTier itself are required — this is shipped as a guide with an end-to-end integration test fixture.
Deep insert / deep update /
@odata.bindPOST and PATCH bodies can now express nested writes:
DeepOperationExtractorwalks the request payload into aDataModificationItemtree (withBindReferencenodes for@odata.bind).DeepUpdateClassifierdecides — per nested property — whether to insert, update, link, or unlink (with FK update / relationship removal where applicable).DefaultChangeSetInitializerand the EF6/EFCore initializers handle the new tree shape and emit400for relationship constraint violations (DbUpdateException).RestierRouteOptions.DeepOperations.MaxDepthcaps nesting depth (default5; set to0to disable).4.01is required for nested PATCH bodies; non-4.01 requests get a clear error.$ContentIdreferences in$batchchange sets are resolved via the newChangeSetDependencyResolver, with the whole batch enlisted in aTransactionScope(#762).Lower-camelCase JSON
RestierRouteOptions.NamingConvention = RestierNamingConvention.CamelCasetransforms property names end-to-end:$metadataand query response payloads use camelCase property names.If-Match/If-None-Matchheaders normalize to CLR names before reaching the submit pipeline (EdmClrPropertyMapper).RestierResourceDeserializeraccepts camelCase enum literals and validates them against the CLR enum.Closes #549.
Conformance toggle:
StrictMissingParentForCollectionsA collection-valued navigation property whose parent entity doesn't exist (e.g.
/Books(missing)/Reviews) historically returned200 OK { "value": [] }. Withbag.Conformance.StrictMissingParentForCollections = trueit returns404 Not Foundper OData v4 Part 1 §9.1.5 / §11.2.6, at the cost of one extra parent-existence query per request. The toggle also extends to$count(#735).Other notable changes
DefaultQueryExecutorandEFQueryExecutorno longer materialize collections eagerly; the controller adds 404 detection for key-based requests against missing resources, andEFChangeSetInitializer.FindResourcematerializes explicitly when needed.OnFilterfor single navigation properties. Interceptors now fire for single navigation references inside$expand(#519).OData-Version: 4.01gating with a clear error message when a request uses a 4.01-only construct on a 4.0 endpoint.$filterpath segment.RestierQueryBuilderhandlesFilterSegmentso/Books/$filter(Year gt 2000)/$countworks end-to-end.Microsoft.Restier.Samples.Postgres.AspNetCore) — a vnext-style sample wired to PostgreSQL via EF Core, including a keyless-view example.api-reference/tree is regenerated from XML doc comments on build.Testing infrastructure
src/totest/.Tests.Legacy,Tests.Breakdance,Tests.AspNet,Tests.AspNetCorePlusEF6).Tests.Shared,Tests.Shared.EntityFramework,Tests.Shared.EntityFrameworkCore.dotnet user-secrets. Thread-safe seeding prevents race conditions in parallel runs.InternalsVisibleToauto-configured from source to matching test project.Test plan
dotnet build RESTier.slnxsucceeds on all target frameworks (net8.0,net9.0,net10.0)dotnet test RESTier.slnx— all tests pass (xUnit v3)dotnet user-secrets)CamelCasemodeOnFilter<View>, 405 for write methods)@odata.bindintegration tests pass; nested PATCH requiresOData-Version: 4.01$ContentIdreference resolution works inside$batchchange setsapi-supported-versionsheaders/openapi/v1.json; ReDoc and NSwag UI rendergeo.intersects,geo.distance,geo.length) translate against EF Core + NetTopologySuitedotnet build src/Microsoft.Restier.Docs/Microsoft.Restier.Docs.docsprojregeneratesapi-reference/anddocs.json