Skip to content

Add optional ISoapEndpointRegistry for SOAP endpoint discovery - #1198

Open
frankyjquintero wants to merge 1 commit into
DigDes:developfrom
frankyjquintero:feature/1190-soap-endpoint-registry
Open

frankyjquintero wants to merge 1 commit into
DigDes:developfrom
frankyjquintero:feature/1190-soap-endpoint-registry

Conversation

@frankyjquintero

Copy link
Copy Markdown

Add optional ISoapEndpointRegistry for SOAP endpoint discovery

Closes #1190

Summary

SoapCore only records its endpoints as a side effect of UseSoapEndpoint. In endpoint-routing mode EndpointDataSource shows four routes per service (Path, /$metadata, /mex, /{methodName}), only one of them named SoapCore, and none of them carries the service type, the serializer or the SOAP version. In middleware mode the endpoints do not appear in EndpointDataSource at all. Anything that needs to list the SOAP services of a host (a catalog, a gateway, per-endpoint diagnostics) has to guess from the route table.

This PR adds an opt-in, SOAP-aware registry of the endpoints configured with UseSoapEndpoint, in both hosting modes. It follows the proposal in #1190 and the feedback from @andersjonsson ("a reasonable place to start"), and it includes the README section he asked for.

When no registry is registered, SoapCore behaves exactly as before.

What changes

New public API:

namespace SoapCore.Extensibility
{
	public interface ISoapEndpointRegistry
	{
		void Register(SoapEndpointInfo endpoint);
		IReadOnlyList<SoapEndpointInfo> GetAll();
	}
}

namespace SoapCore
{
	public class SoapEndpointInfo
	{
		public string Path { get; set; }
		public Type ServiceType { get; set; }
		public Type MessageType { get; set; }                       // T_MESSAGE, CustomMessage by default
		public SoapSerializer Serializer { get; set; }
		public Type CustomSerializerType { get; set; }              // UseCustomSerializer<T>(), or null
		public bool CaseInsensitivePath { get; set; }
		public bool GenerateSoapActionWithoutContractName { get; set; }
		public IReadOnlyList<MessageVersion> MessageVersions { get; set; }   // distinct versions of the configured encoders
	}

	public static class SoapEndpointExtensions
	{
		public static IServiceCollection AddSoapEndpointRegistry(this IServiceCollection serviceCollection);
	}
}
services.AddSoapCore();
services.AddSoapEndpointRegistry();   // default thread-safe, in-memory registry

// after the endpoints are configured:
var endpoints = app.Services.GetRequiredService<ISoapEndpointRegistry>().GetAll();

Implementation notes:

  • Two integration points. All 21 UseSoapEndpoint overloads end in one of two methods, the IApplicationBuilder one and the IEndpointRouteBuilder one. Both call a small private helper that resolves ISoapEndpointRegistry from the root provider and, if present, registers one entry. In endpoint-routing mode the four mapped routes produce one entry.
  • Opt-in, no overhead when absent. Without a registry the cost is one GetService per UseSoapEndpoint call at startup and no allocation (the snapshot is only built when a registry exists). AddSoapCore() is unchanged.
  • Value snapshot. SoapEndpointInfo copies values instead of holding the mutable SoapOptions, because the middleware mutates EncoderOptions later (EncoderOptions ??= ...). To keep the snapshot and the middleware aligned, the default encoder list now comes from one internal helper, SoapEncoderOptions.CreateDefaults(), used in both places (a one-line, behaviour-neutral change in SoapEndpointMiddleware).
  • DefaultSoapEndpointRegistry is internal, reachable through AddSoapEndpointRegistry() (TryAddSingleton, so a registry the user already registered is never replaced). It is copy-on-write: registration takes a lock and publishes a new read-only array, GetAll() is a lock-free read, and a snapshot taken earlier never changes. Duplicates are kept on purpose: the same path can appear in mutually exclusive UseWhen branches.
  • Order and semantics. Entries are recorded while UseSoapEndpoint runs, in call order. The recorded Path is the one as configured: prefixes added around the endpoint with app.Map(...) or UsePathBase(...) are not included (documented in the README).

Backward compatibility

  • No existing public signature changes. The only addition to an existing public type is the new extension method AddSoapEndpointRegistry().
  • No change to routes, endpoint metadata or DisplayName.
  • No per-request work.
  • Builds for all four targets (netstandard2.0, netstandard2.1, netcoreapp3.1, net8.0) with no new warnings. The endpoint-routing hook sits inside the existing #if NETCOREAPP3_0_OR_GREATER block.

Tests

New tests in src/SoapCore.Tests/EndpointRegistry (TestServer, same pattern as the existing integration tests):

Test What it checks
MiddlewareMode_RecordsSnapshotWithSoapCoreDefaults Middleware mode is recorded, with the SoapCore defaults applied before the middleware is built
EndpointRouting_RecordsOneEntryPerCallInCallOrder 3 endpoints give 3 entries (not 12), in call order, with CaseInsensitivePath, GenerateSoapActionWithoutContractName and distinct MessageVersions
SamePathInExclusiveBranches_RecordsBothEntriesInCallOrder The same path in two UseWhen branches is recorded twice, not deduplicated
UserRegisteredRegistry_TakesPrecedenceAndReceivesEntries (both modes) A user registry wins over AddSoapEndpointRegistry() and receives the entries
MapBranch_RecordsPathRelativeToBranch The documented app.Map limitation
AddSoapCore_DoesNotEnableRegistry Opt-in: no registry without AddSoapEndpointRegistry()
AddSoapEndpointRegistry_RegistersOneSingleton TryAdd and singleton lifetime
DefaultRegistry_ValidatesAndReturnsReadOnlySnapshots Null check, read-only snapshots that do not change, order and duplicates kept

Full suite: dotnet test src/SoapCore.Tests passes (302 existing tests plus the 9 new test cases).

Documentation

New README section, "Discovering SOAP endpoints with ISoapEndpointRegistry": motivation, setup, a JSON catalog sample that works on every target framework, the SoapEndpointInfo fields, and the notes above (singleton requirement, call order, snapshot semantics, when to read the registry, the app.Map/UsePathBase limitation, what is not recorded).

Not in this PR

Kept out on purpose to keep the change small and easy to review:

  • Endpoint metadata on the mapped routes and discovery through EndpointDataSource (the "future extension" of the issue).
  • Operation and binding DTOs, GET/POST flags, hosting mode.
  • A prefix or logical name for catalogs (would solve app.Map, UsePathBase and MapGroup).
  • A <Version> bump, which I leave to the maintainers.

Questions for the maintainers

  1. Should DefaultSoapEndpointRegistry stay internal (only reachable through AddSoapEndpointRegistry()), or be public like DefaultOperationInvoker? I kept it internal because making it public later is not a breaking change, the opposite is.
  2. SoapEndpointInfo has public setters, like SoapOptions and the object initializer in the proposal. Would you prefer get-only properties with a constructor?
  3. For a follow-up that exposes endpoints through EndpointDataSource: should the metadata always be attached to the routes, or be opt-in through a SoapCoreOptions flag?

Checklist

Permite el listado programático de los servicios SOAP configurados con UseSoapEndpoint.
Resuelve la limitación de que EndpointDataSource de ASP.NET Core no expone detalles suficientes para los servicios SoapCore. Esto facilita la creación de catálogos de servicios, gateways y herramientas de observabilidad.
La funcionalidad es opt-in, thread-safe y registra un snapshot de la configuración del endpoint.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Proposal: Add Optional ISoapEndpointRegistry for Explicit SOAP Endpoint Discovery

1 participant