Skip to content

[Task] Middleware contract validation #20

Description

@rian-be

Summary

Extend tools/PluginContractValidator to verify that plugin contributed middleware is structurally valid before the host starts.

A plugin middleware is declared by type (PluginMiddleware.MiddlewareType); the host activates it. The validator does not impose a single uniform shape it distinguishes three distinct middleware models and validates each according to its own rules:

  1. Convention middleware - MiddlewareType following the ASP.NET Core middleware convention.
  2. AuthKitMiddlewareBase - an AuthKit specific base class.
  3. IAuthKitMiddleware - an AuthKit specific, DI aware interface.

Only the convention model requires RequestDelegate constructor parameter; the two AuthKit models are activated by the host from the request service provider and must not be forced into the ASP.NET Core constructor convention. Non conforming middleware must be reported explicitly so failures are caught at startup validation rather than at request time.

There is no Factory or PluginMiddlewareInstance in the contract the validator only inspects the declared MiddlewareType and which AuthKit model (if any) it satisfies.

Goal

Ensure that every PluginMiddleware registration (C2), every IAuthKitMiddleware implementation (C5), and every AuthKitMiddlewareBase subclass (C4) is structurally valid.

The validator must produce actionable diagnostics that name:

  • the owning plugin,
  • the offending middleware type,
  • and the specific contract violation.

Background

ASP.NET Core middleware is commonly expressed as convention based: a type with (RequestDelegate next, ...deps) constructor and an Invoke/InvokeAsync method. The plugin contract additionally introduces two AuthKit specific models AuthKitMiddlewareBase (C4) and IAuthKitMiddleware (C5) which are NOT the ASP.NET Core IMiddleware interface. The validator must accept all valid forms and reject invalid ones without forcing the ASP.NET Core convention onto the AuthKit abstractions.

Scope

C6. Middleware contract validation

For each PluginMiddleware, the validator classifies MiddlewareType into one of the three models applying explicit precedence and rejecting ambiguous combinations and validates accordingly:

Model 1. Convention middleware (MiddlewareType)

  • MiddlewareType is public, concrete, non abstract, generic.
  • exposes public constructor whose first parameter is RequestDelegate (remaining parameters are injectable dependencies),
  • exposes public method InvokeAsync(HttpContext, RequestDelegate) or Invoke(HttpContext, RequestDelegate) returning Task (never void).

Model 2. AuthKitMiddlewareBase (MiddlewareType)

  • MiddlewareType is public, concrete, non abstract, generic.
  • inherits AuthKitMiddlewareBase,
  • overrides InvokeAsync(HttpContext, RequestDelegate).
  • constructor is compatible with host DI activation (host activates from the request service provider), it does not require RequestDelegate` constructor parameter. Final dependency resolvability is confirmed by the host DI container at startup, not by this structural validator.

Model 3. IAuthKitMiddleware (MiddlewareType)

  • MiddlewareType is public, concrete, non abstract, generic.
  • implements IAuthKitMiddleware,
  • provides InvokeAsync(HttpContext, RequestDelegate).
  • constructor is compatible with host DI activation (host activates from the request service provider), it does not require RequestDelegate constructor parameter. Final dependency resolvability is confirmed by the host DI container at startup, not by this structural validator.

A MiddlewareType must not simultaneously inherit AuthKitMiddlewareBase and implement IAuthKitMiddleware such type is rejected as ambiguous the host should not have to guess which lifecycle to apply. If type matches more than one AuthKit model, validation fails.

A MiddlewareType that matches none of the three models is rejected.


C6. Middleware Contract Validation

Proposed Checks

For each plugin:
  For each PluginMiddleware entry:
    MiddlewareType must be non null (contract API already non nullable;
      guarded at reflection time too)
    type must be public, concrete, non abstract, non generic

    if type inherits AuthKitMiddlewareBase AND implements IAuthKitMiddleware:
        -> ERROR: ambiguous middleware model (both AuthKit models)
    else if type inherits AuthKitMiddlewareBase:
        -> Model 2: requires valid instance InvokeAsync override
           constructor compatible with host DI activation
           (final resolvability confirmed by host DI container at startup)
    else if type implements IAuthKitMiddleware:
        -> Model 3: requires instance InvokeAsync(HttpContext, RequestDelegate)
           constructor compatible with host DI activation
           (final resolvability confirmed by host DI container at startup)
    else:
        -> Model 1 (convention):
             requires public ctor with first param RequestDelegate
             requires instance InvokeAsync/Invoke(HttpContext, RequestDelegate) returning Task
             (errors if requirements not met)

Error Reporting

Failures must be explicit and model aware, for example:

Plugin 'DevTokens' middleware 'MyMiddleware' is invalid:
  does not implement AuthKitMiddlewareBase or IAuthKitMiddleware,
  and its constructor does not take RequestDelegate as the first parameter.

or

Plugin 'X' declares PluginMiddleware with a null MiddlewareType.

The validator returns non zero / [FAIL] result so startup or CI fails fast.

Edge Cases

  • Accepted method signatures (convention model): exactly Task InvokeAsync(HttpContext, RequestDelegate) or Task Invoke(HttpContext, RequestDelegate). A void returning method is not accepted.
  • Constructor parameter order (convention model): only the first parameter must be RequestDelegate; the rest are injectable dependencies.
  • Multiple constructors (convention model): the validator must identify at least one valid activation constructor whose first parameter is RequestDelegate. If more than one such constructor exists and the host cannot deterministically select one, validation fails (final resolvability is still confirmed by the DI container at startup).
  • DI based models (Model 2 / Model 3): the constructor is validated only as compatible with host DI activation (public, concrete, with injectable parameters); a RequestDelegate constructor parameter is not required and must not be demanded. Final dependency resolvability is confirmed by the host DI container at startup, not by this structural validator.
  • Generic types (both open MyMiddleware<T> and closed MyMiddleware<int>) are rejected the contract does not support generic middleware; this keeps the validator, plugin loader, and diagnostics simple.
  • Abstract types and interfaces supplied as MiddlewareType are rejected.
  • Static invocation methods are rejected. Invoke/InvokeAsync must be instance methods, static method is not valid middleware invocation entry point (the override requirement for AuthKitMiddlewareBase already implies this, but it is checked explicitly).
  • Null MiddlewareType: already invalid at the API level (non nullable record member) the validator still guards against it defensively.

Host Behavior

The host must run the validator during startup (and in CI) and must not start with non conforming middleware contract. A configuration may downgrade specific findings to warnings only if explicitly allowed by the host policy by default, structural middleware errors are fatal. Final dependency resolvability of DI based middleware is confirmed by the DI container at startup.


Backward Compatibility

This sub issue is additive.

  • Existing plugins without middleware are unaffected.
  • Existing valid middleware continues to validate as [PASS].
  • DevTokens must continue to pass validation.
  • The validator itself is extended, not replaced.

Validation

C6. Middleware Contract Validation

  • Validator classifies each MiddlewareType into one of the three models.
  • Convention middleware: MiddlewareType concrete/public/non generic, public ctor with first param RequestDelegate, InvokeAsync/Invoke returning Task (no void).
  • AuthKitMiddlewareBase subclasses validate via instance InvokeAsync override; constructor need not take RequestDelegate.
  • IAuthKitMiddleware implementations validate via instance InvokeAsync; constructor compatible with host DI activation, need not take RequestDelegate.
  • A type inheriting both AuthKitMiddlewareBase and IAuthKitMiddleware is rejected as ambiguous.
  • Static Invoke/InvokeAsync methods are rejected.
  • Types matching no model are rejected with an explicit message.
  • Generic/abstract/invalid types are rejected with explicit messages.
  • Errors name the plugin and the offending type.
  • Non conforming middleware fails validation (or is warned per policy).

Integration

  • dotnet build AuthKit.Plugins.Abstractions succeeds.
  • tools/PluginContractValidator on DevTokens → [PASS].
  • A deliberately broken sample plugin fails validation explicitly.
  • Validator runs in CI.

Acceptance Criteria

  • Plugin contributed middleware is validated at contract-check time.
  • Valid convention based, base class, and interface based middleware all pass.
  • Type based PluginMiddleware registrations are validated.
  • A type inheriting both AuthKitMiddlewareBase and IAuthKitMiddleware is rejected as ambiguous.
  • Static Invoke/InvokeAsync methods are rejected.
  • Invalid middleware is rejected with clear message naming the plugin and type:
    • convention middleware missing RequestDelegate constructor or Task returning Invoke/InvokeAsync,
    • AuthKitMiddlewareBase / IAuthKitMiddleware types missing the required InvokeAsync,
    • generic/abstract types, or types matching no supported model.
  • The validator fails fast on structural errors.
  • DevTokens continues to pass.
  • Existing plugins remain unaffected.

Non Goals

This sub issue does not include:

  • gRPC interceptor validation (see C7),
  • pipeline ordering logic (see C1–C5),
  • runtime middleware behavior,
  • enforcing single uniform constructor shape across all three models,
  • or changing the PluginContractValidator output format beyond what is needed for clear diagnostics.
Pinned by rian-be

Activity

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

Metadata

Metadata

Assignees

Labels

P1Core operationadditiveAdditive, non-breaking changearea/abstractionsAuthKit.Plugins.Abstractions contractcontractChanges the plugin contractsub-taskChild task of an epic

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions