重新整理 .net core 實踐篇——— endpoint[四十七]

語言: CN / TW / HK

前言

簡單整理一些endpoint的一些東西,主要是介紹一個這個endpoint是什麼。

正文

endpoint 從表面意思是端點的意思,也就是說比如客戶端的某一個action 是一個點,那麼服務端的action也是一個點,這個端點的意義更加具體,而不是服務端和客戶端這麼泛指。

比如說客戶端的action請求使用者信心,那麼服務端的action就是GetUserInfo,那麼endpoint 在這裡是什麼意思呢?是GetUserInfo的抽象,或者是GetUserInfo的描述符。

那麼netcore 對endpoint的描述是什麼呢?

派生 Microsoft.AspNetCore.Routing.RouteEndpoint

RouteEndpoint 是:

那麼來看一下:

app.UseRouting();

程式碼為:

public static IApplicationBuilder UseRouting(this IApplicationBuilder builder)
{
  if (builder == null)
	throw new ArgumentNullException(nameof (builder));
  EndpointRoutingApplicationBuilderExtensions.VerifyRoutingServicesAreRegistered(builder);
  DefaultEndpointRouteBuilder endpointRouteBuilder = new DefaultEndpointRouteBuilder(builder);
  builder.Properties["__EndpointRouteBuilder"] = (object) endpointRouteBuilder;
  return builder.UseMiddleware<EndpointRoutingMiddleware>((object) endpointRouteBuilder);
}

VerifyRoutingServicesAreRegistered 主要驗證路由標記服務是否注入了:

private static void VerifyRoutingServicesAreRegistered(IApplicationBuilder app)
{
  if (app.ApplicationServices.GetService(typeof (RoutingMarkerService)) == null)
	throw new InvalidOperationException(Resources.FormatUnableToFindServices((object) "IServiceCollection", (object) "AddRouting", (object) "ConfigureServices(...)"));
}

然後可以檢視一下DefaultEndpointRouteBuilder:

internal class DefaultEndpointRouteBuilder : IEndpointRouteBuilder
  {
    public DefaultEndpointRouteBuilder(IApplicationBuilder applicationBuilder)
    {
      IApplicationBuilder applicationBuilder1 = applicationBuilder;
      if (applicationBuilder1 == null)
        throw new ArgumentNullException(nameof (applicationBuilder));
      this.ApplicationBuilder = applicationBuilder1;
      this.DataSources = (ICollection<EndpointDataSource>) new List<EndpointDataSource>();
    }

    public IApplicationBuilder ApplicationBuilder { get; }

    public IApplicationBuilder CreateApplicationBuilder()
    {
      return this.ApplicationBuilder.New();
    }

    public ICollection<EndpointDataSource> DataSources { get; }

    public IServiceProvider ServiceProvider
    {
      get
      {
        return this.ApplicationBuilder.ApplicationServices;
      }
    }
  }

我們知道builder 是用來構造某個東西的,從名字上來看是用來構造DefaultEndpointRoute。

建設者,一般分為兩種,一種是內部就有構建方法,另一種是構建方法在材料之中或者操作機器之中。

這個怎麼說呢?比如說一個構建者相當於一個工人,那麼這個工人可能只帶材料去完成一個小屋。也可能構建者本身沒有帶材料,那麼可能材料之中包含了製作方法(比如泡麵)或者機器中包含了製作方法比如榨汁機。

然後來看一下中介軟體EndpointRoutingMiddleware:

public EndpointRoutingMiddleware(
  MatcherFactory matcherFactory,
  ILogger<EndpointRoutingMiddleware> logger,
  IEndpointRouteBuilder endpointRouteBuilder,
  DiagnosticListener diagnosticListener,
  RequestDelegate next)
{
  if (endpointRouteBuilder == null)
	throw new ArgumentNullException(nameof (endpointRouteBuilder));
  MatcherFactory matcherFactory1 = matcherFactory;
  if (matcherFactory1 == null)
	throw new ArgumentNullException(nameof (matcherFactory));
  this._matcherFactory = matcherFactory1;
  ILogger<EndpointRoutingMiddleware> logger1 = logger;
  if (logger1 == null)
	throw new ArgumentNullException(nameof (logger));
  this._logger = (ILogger) logger1;
  DiagnosticListener diagnosticListener1 = diagnosticListener;
  if (diagnosticListener1 == null)
	throw new ArgumentNullException(nameof (diagnosticListener));
  this._diagnosticListener = diagnosticListener1;
  RequestDelegate requestDelegate = next;
  if (requestDelegate == null)
	throw new ArgumentNullException(nameof (next));
  this._next = requestDelegate;
  this._endpointDataSource = (EndpointDataSource) new CompositeEndpointDataSource((IEnumerable<EndpointDataSource>) endpointRouteBuilder.DataSources);
}

裡面就做一些判斷,是否服務注入了。然後值得關注的是幾個新鮮事物了,比如MatcherFactory、DiagnosticListener、CompositeEndpointDataSource。

把這些都看一下吧。

internal abstract class MatcherFactory
{
   public abstract Matcher CreateMatcher(EndpointDataSource dataSource);
}

CreateMatcher 通過某個端點資源,來建立一個Matcher。

這裡猜測,是這樣子的,一般來說客戶端把某個路由都比作某個資源,也就是uri,這裡抽象成EndpointDataSource,那麼這個匹配器的作用是:

Attempts to asynchronously select an <see cref="Endpoint"/> for the current request.

也就是匹配出Endpoint。

那麼再看一下DiagnosticListener,DiagnosticListener 原始碼就不看了,因為其實一個系統類,也就是system下面的類,比較複雜,直接看其描述就會。

DiagnosticListener 是一個 NotificationSource,這意味著返回的結果可用於記錄通知,但它也有 Subscribe 方法,因此可以任意轉發通知。 因此,其工作是將生成的作業從製造者轉發到所有偵聽器 (多轉換) 。 通常情況下,不應 DiagnosticListener 使用,而是使用預設設定,以便通知儘可能公共。

是一個監聽作用的,模式是訂閱模式哈。 https://docs.microsoft.com/zh-cn/dotnet/api/system.diagnostics.diagnosticlistener?view=net-6.0 有興趣可以去看一下。

最後這個看一下CompositeEndpointDataSource,表面意思是綜合性EndpointDataSource,繼承自EndpointDataSource。

public CompositeEndpointDataSource(IEnumerable<EndpointDataSource> endpointDataSources) : this()
{
	_dataSources = new List<EndpointDataSource>();

	foreach (var dataSource in endpointDataSources)
	{
		_dataSources.Add(dataSource);
	}
}

是用來管理endpointDataSources的,暫且不看其作用。

public Task Invoke(HttpContext httpContext)
{
  Endpoint endpoint = httpContext.GetEndpoint();
  if (endpoint != null)
  {
	EndpointRoutingMiddleware.Log.MatchSkipped(this._logger, endpoint);
	return this._next(httpContext);
  }
  Task<Matcher> matcherTask = this.InitializeAsync();
  if (!matcherTask.IsCompletedSuccessfully)
	return AwaitMatcher(this, httpContext, matcherTask);
  Task matchTask = matcherTask.Result.MatchAsync(httpContext);
  return !matchTask.IsCompletedSuccessfully ? AwaitMatch(this, httpContext, matchTask) : this.SetRoutingAndContinue(httpContext);

  async Task AwaitMatcher(
	EndpointRoutingMiddleware middleware,
	HttpContext httpContext,
	Task<Matcher> matcherTask)
  {
	await (await matcherTask).MatchAsync(httpContext);
	await middleware.SetRoutingAndContinue(httpContext);
  }

  async Task AwaitMatch(
	EndpointRoutingMiddleware middleware,
	HttpContext httpContext,
	Task matchTask)
  {
	await matchTask;
	await middleware.SetRoutingAndContinue(httpContext);
  }
}

一段一段看:

Endpoint endpoint = httpContext.GetEndpoint();
if (endpoint != null)
{
EndpointRoutingMiddleware.Log.MatchSkipped(this._logger, endpoint);
return this._next(httpContext);
}

如果有endpoint,就直接執行下一個中介軟體。

Task<Matcher> matcherTask = this.InitializeAsync();

看InitializeAsync。

private Task<Matcher> InitializeAsync()
{
	var initializationTask = _initializationTask;
	if (initializationTask != null)
	{
		return initializationTask;
	}

	return InitializeCoreAsync();
}

接著看:InitializeCoreAsync

private Task<Matcher> InitializeCoreAsync()
{
	var initialization = new TaskCompletionSource<Matcher>(TaskCreationOptions.RunContinuationsAsynchronously);
	var initializationTask = Interlocked.CompareExchange(ref _initializationTask, initialization.Task, null);
	if (initializationTask != null)
	{
		// This thread lost the race, join the existing task.
		return initializationTask;
	}

	// This thread won the race, do the initialization.
	try
	{
		var matcher = _matcherFactory.CreateMatcher(_endpointDataSource);

		// Now replace the initialization task with one created with the default execution context.
		// This is important because capturing the execution context will leak memory in ASP.NET Core.
		using (ExecutionContext.SuppressFlow())
		{
			_initializationTask = Task.FromResult(matcher);
		}

		// Complete the task, this will unblock any requests that came in while initializing.
		initialization.SetResult(matcher);
		return initialization.Task;
	}
	catch (Exception ex)
	{
		// Allow initialization to occur again. Since DataSources can change, it's possible
		// for the developer to correct the data causing the failure.
		_initializationTask = null;

		// Complete the task, this will throw for any requests that came in while initializing.
		initialization.SetException(ex);
		return initialization.Task;
	}
}

這裡面作用就是建立matcher,值得注意的是這一句程式碼:var matcher = _matcherFactory.CreateMatcher(_endpointDataSource);

建立匹配器的資源物件是_endpointDataSource。

this._endpointDataSource = (EndpointDataSource) new CompositeEndpointDataSource((IEnumerable<EndpointDataSource>) endpointRouteBuilder.DataSources);

這個_endpointDataSource 並不是值某一個endpointDataSource,而是全部的endpointDataSource,這一點前面就有介紹CompositeEndpointDataSource。

然後看最後一段:

if (!matcherTask.IsCompletedSuccessfully)
	return AwaitMatcher(this, httpContext, matcherTask);
  Task matchTask = matcherTask.Result.MatchAsync(httpContext);
  return !matchTask.IsCompletedSuccessfully ? AwaitMatch(this, httpContext, matchTask) : this.SetRoutingAndContinue(httpContext);

  async Task AwaitMatcher(
	EndpointRoutingMiddleware middleware,
	HttpContext httpContext,
	Task<Matcher> matcherTask)
  {
	await (await matcherTask).MatchAsync(httpContext);
	await middleware.SetRoutingAndContinue(httpContext);
  }

  async Task AwaitMatch(
	EndpointRoutingMiddleware middleware,
	HttpContext httpContext,
	Task matchTask)
  {
	await matchTask;
	await middleware.SetRoutingAndContinue(httpContext);
  }

這裡面其實就表達一個意思哈,如果task完成了,然後就直接執行下一步,如果沒有完成就await,總之是要執行MatchAsync然後再執行SetRoutingAndContinue。

internal abstract class Matcher
{
	/// <summary>
	/// Attempts to asynchronously select an <see cref="Endpoint"/> for the current request.
	/// </summary>
	/// <param name="httpContext">The <see cref="HttpContext"/> associated with the current request.</param>
	/// <returns>A <see cref="Task"/> which represents the asynchronous completion of the operation.</returns>
	public abstract Task MatchAsync(HttpContext httpContext);
}

MatchAsync 前面也提到過就是匹配出Endpoint的。

然後SetRoutingAndContinue:

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private Task SetRoutingAndContinue(HttpContext httpContext)
{
	// If there was no mutation of the endpoint then log failure
	var endpoint = httpContext.GetEndpoint();
	if (endpoint == null)
	{
		Log.MatchFailure(_logger);
	}
	else
	{
		// Raise an event if the route matched
		if (_diagnosticListener.IsEnabled() && _diagnosticListener.IsEnabled(DiagnosticsEndpointMatchedKey))
		{
			// We're just going to send the HttpContext since it has all of the relevant information
			_diagnosticListener.Write(DiagnosticsEndpointMatchedKey, httpContext);
		}

		Log.MatchSuccess(_logger, endpoint);
	}

	return _next(httpContext);
}

這裡沒有匹配到也沒有終結哈,為什麼這麼做呢?因為匹配不到,下一個中介軟體可以做到如果沒有endpoint,那麼就指明做什麼事情,不要一下子寫死。

如何如果匹配成功了,那麼傳送一個事件,這個事件是DiagnosticsEndpointMatchedKey,然後列印匹配成功的一些log了。

private const string DiagnosticsEndpointMatchedKey = "Microsoft.AspNetCore.Routing.EndpointMatched";

那麼這裡我們可以監聽做一些事情,看專案需求了。

其實看到這裡有兩點疑問了Matcher的具體實現和EndpointDataSource 是怎麼來的。

先看一下Matcher吧,這個肯定要在路由服務中去查看了。

public static IServiceCollection AddRouting(
	this IServiceCollection services,
	Action<RouteOptions> configureOptions)
{
	if (services == null)
	{
		throw new ArgumentNullException(nameof(services));
	}

	if (configureOptions == null)
	{
		throw new ArgumentNullException(nameof(configureOptions));
	}

	services.Configure(configureOptions);
	services.AddRouting();

	return services;
}

然後在AddRouting 中檢視,這裡面非常多,這裡我直接放出這個的依賴注入:

services.TryAddSingleton<MatcherFactory, DfaMatcherFactory>();

那麼這裡進入看DfaMatcherFactory。

public override Matcher CreateMatcher(EndpointDataSource dataSource)
{
	if (dataSource == null)
	{
		throw new ArgumentNullException(nameof(dataSource));
	}

	// Creates a tracking entry in DI to stop listening for change events
	// when the services are disposed.
	var lifetime = _services.GetRequiredService<DataSourceDependentMatcher.Lifetime>();

	return new DataSourceDependentMatcher(dataSource, lifetime, () =>
	{
		return _services.GetRequiredService<DfaMatcherBuilder>();
	});
}

檢視DataSourceDependentMatcher.Lifetime:

public sealed class Lifetime : IDisposable
{
	private readonly object _lock = new object();
	private DataSourceDependentCache<Matcher>? _cache;
	private bool _disposed;

	public DataSourceDependentCache<Matcher>? Cache
	{
		get => _cache;
		set
		{
			lock (_lock)
			{
				if (_disposed)
				{
					value?.Dispose();
				}

				_cache = value;
			}
		}
	}

	public void Dispose()
	{
		lock (_lock)
		{
			_cache?.Dispose();
			_cache = null;

			_disposed = true;
		}
	}
}

Lifetime 是生命週期的意思,裡面實現的比較簡單,單純從功能上看似乎只是快取,但是其之所以取這個名字是因為Dispose,在Lifetime結束的時候帶走了DataSourceDependentCache ? _cache。

為了讓大家更清晰一寫,DataSourceDependentCache,將其標紅。

然後CreateMatcher 建立了DataSourceDependentMatcher。

return new DataSourceDependentMatcher(dataSource, lifetime, () =>
{
	return _services.GetRequiredService<DfaMatcherBuilder>();
});

看一下DataSourceDependentMatcher:

public DataSourceDependentMatcher(
	EndpointDataSource dataSource,
	Lifetime lifetime,
	Func<MatcherBuilder> matcherBuilderFactory)
{
	_matcherBuilderFactory = matcherBuilderFactory;

	_cache = new DataSourceDependentCache<Matcher>(dataSource, CreateMatcher);
	_cache.EnsureInitialized();

	// This will Dispose the cache when the lifetime is disposed, this allows
	// the service provider to manage the lifetime of the cache.
	lifetime.Cache = _cache;
}

這裡建立了一個DataSourceDependentCache。

public DataSourceDependentCache(EndpointDataSource dataSource, Func<IReadOnlyList<Endpoint>, T> initialize)
{
	if (dataSource == null)
	{
		throw new ArgumentNullException(nameof(dataSource));
	}

	if (initialize == null)
	{
		throw new ArgumentNullException(nameof(initialize));
	}

	_dataSource = dataSource;
	_initializeCore = initialize;

	_initializer = Initialize;
	_initializerWithState = (state) => Initialize();
	_lock = new object();
}

// Note that we don't lock here, and think about that in the context of a 'push'. So when data gets 'pushed'
// we start computing a new state, but we're still able to perform operations on the old state until we've
// processed the update.
public T Value => _value;

public T EnsureInitialized()
{
	return LazyInitializer.EnsureInitialized<T>(ref _value, ref _initialized, ref _lock, _initializer);
}

private T Initialize()
{
	lock (_lock)
	{
		var changeToken = _dataSource.GetChangeToken();
		_value = _initializeCore(_dataSource.Endpoints);

		// Don't resubscribe if we're already disposed.
		if (_disposed)
		{
			return _value;
		}

		_disposable = changeToken.RegisterChangeCallback(_initializerWithState, null);
		return _value;
	}
}

這裡Initialize可以看到呼叫了_initializeCore,這個_initializeCore就是傳遞進來的DataSourceDependentMatcher的CreateMatcher,如下:

private Matcher CreateMatcher(IReadOnlyList<Endpoint> endpoints)
{
	var builder = _matcherBuilderFactory();
	for (var i = 0; i < endpoints.Count; i++)
	{
		// By design we only look at RouteEndpoint here. It's possible to
		// register other endpoint types, which are non-routable, and it's
		// ok that we won't route to them.
		if (endpoints[i] is RouteEndpoint endpoint && endpoint.Metadata.GetMetadata<ISuppressMatchingMetadata>()?.SuppressMatching != true)
		{
			builder.AddEndpoint(endpoint);
		}
	}

	return builder.Build();
}

這裡可以看到這裡endpoint 必須帶有ISuppressMatchingMetadata,否則將不會被匹配。

這個_matcherBuilderFactory 是:

也就是:

看一下builder:

public override Matcher Build()
{
#if DEBUG
	var includeLabel = true;
#else
	var includeLabel = false;
#endif

	var root = BuildDfaTree(includeLabel);

	// State count is the number of nodes plus an exit state
	var stateCount = 1;
	var maxSegmentCount = 0;
	root.Visit((node) =>
	{
		stateCount++;
		maxSegmentCount = Math.Max(maxSegmentCount, node.PathDepth);
	});
	_stateIndex = 0;

	// The max segment count is the maximum path-node-depth +1. We need
	// the +1 to capture any additional content after the 'last' segment.
	maxSegmentCount++;

	var states = new DfaState[stateCount];
	var exitDestination = stateCount - 1;
	AddNode(root, states, exitDestination);

	// The root state only has a jump table.
	states[exitDestination] = new DfaState(
		Array.Empty<Candidate>(),
		Array.Empty<IEndpointSelectorPolicy>(),
		JumpTableBuilder.Build(exitDestination, exitDestination, null),
		null);

	return new DfaMatcher(_loggerFactory.CreateLogger<DfaMatcher>(), _selector, states, maxSegmentCount);
}

BuildDfaTree 這個是一個演算法哈,這裡就不介紹哈,是dfa 演算法,這裡理解為將endpoint建立為一顆數,有利於匹配就好。

最後返回了一個return new DfaMatcher(_loggerFactory.CreateLogger (), _selector, states, maxSegmentCount);

DfaMatcher 就是endpoint匹配器。

那麼進去看DfaMatcher 匹配方法MatchAsync。

public sealed override Task MatchAsync(HttpContext httpContext)
{
	if (httpContext == null)
	{
		throw new ArgumentNullException(nameof(httpContext));
	}

	// All of the logging we do here is at level debug, so we can get away with doing a single check.
	var log = _logger.IsEnabled(LogLevel.Debug);

	// The sequence of actions we take is optimized to avoid doing expensive work
	// like creating substrings, creating route value dictionaries, and calling
	// into policies like versioning.
	var path = httpContext.Request.Path.Value!;

	// First tokenize the path into series of segments.
	Span<PathSegment> buffer = stackalloc PathSegment[_maxSegmentCount];
	var count = FastPathTokenizer.Tokenize(path, buffer);
	var segments = buffer.Slice(0, count);

	// FindCandidateSet will process the DFA and return a candidate set. This does
	// some preliminary matching of the URL (mostly the literal segments).
	var (candidates, policies) = FindCandidateSet(httpContext, path, segments);
	var candidateCount = candidates.Length;
	if (candidateCount == 0)
	{
		if (log)
		{
			Logger.CandidatesNotFound(_logger, path);
		}

		return Task.CompletedTask;
	}

	if (log)
	{
		Logger.CandidatesFound(_logger, path, candidates);
	}

	var policyCount = policies.Length;

	// This is a fast path for single candidate, 0 policies and default selector
	if (candidateCount == 1 && policyCount == 0 && _isDefaultEndpointSelector)
	{
		ref readonly var candidate = ref candidates[0];

		// Just strict path matching (no route values)
		if (candidate.Flags == Candidate.CandidateFlags.None)
		{
			httpContext.SetEndpoint(candidate.Endpoint);

			// We're done
			return Task.CompletedTask;
		}
	}

	// At this point we have a candidate set, defined as a list of endpoints in
	// priority order.
	//
	// We don't yet know that any candidate can be considered a match, because
	// we haven't processed things like route constraints and complex segments.
	//
	// Now we'll iterate each endpoint to capture route values, process constraints,
	// and process complex segments.

	// `candidates` has all of our internal state that we use to process the
	// set of endpoints before we call the EndpointSelector.
	//
	// `candidateSet` is the mutable state that we pass to the EndpointSelector.
	var candidateState = new CandidateState[candidateCount];

	for (var i = 0; i < candidateCount; i++)
	{
		// PERF: using ref here to avoid copying around big structs.
		//
		// Reminder!
		// candidate: readonly data about the endpoint and how to match
		// state: mutable storarge for our processing
		ref readonly var candidate = ref candidates[i];
		ref var state = ref candidateState[i];
		state = new CandidateState(candidate.Endpoint, candidate.Score);

		var flags = candidate.Flags;

		// First process all of the parameters and defaults.
		if ((flags & Candidate.CandidateFlags.HasSlots) != 0)
		{
			// The Slots array has the default values of the route values in it.
			//
			// We want to create a new array for the route values based on Slots
			// as a prototype.
			var prototype = candidate.Slots;
			var slots = new KeyValuePair<string, object?>[prototype.Length];

			if ((flags & Candidate.CandidateFlags.HasDefaults) != 0)
			{
				Array.Copy(prototype, 0, slots, 0, prototype.Length);
			}

			if ((flags & Candidate.CandidateFlags.HasCaptures) != 0)
			{
				ProcessCaptures(slots, candidate.Captures, path, segments);
			}

			if ((flags & Candidate.CandidateFlags.HasCatchAll) != 0)
			{
				ProcessCatchAll(slots, candidate.CatchAll, path, segments);
			}

			state.Values = RouteValueDictionary.FromArray(slots);
		}

		// Now that we have the route values, we need to process complex segments.
		// Complex segments go through an old API that requires a fully-materialized
		// route value dictionary.
		var isMatch = true;
		if ((flags & Candidate.CandidateFlags.HasComplexSegments) != 0)
		{
			state.Values ??= new RouteValueDictionary();
			if (!ProcessComplexSegments(candidate.Endpoint, candidate.ComplexSegments, path, segments, state.Values))
			{
				CandidateSet.SetValidity(ref state, false);
				isMatch = false;
			}
		}

		if ((flags & Candidate.CandidateFlags.HasConstraints) != 0)
		{
			state.Values ??= new RouteValueDictionary();
			if (!ProcessConstraints(candidate.Endpoint, candidate.Constraints, httpContext, state.Values))
			{
				CandidateSet.SetValidity(ref state, false);
				isMatch = false;
			}
		}

		if (log)
		{
			if (isMatch)
			{
				Logger.CandidateValid(_logger, path, candidate.Endpoint);
			}
			else
			{
				Logger.CandidateNotValid(_logger, path, candidate.Endpoint);
			}
		}
	}

	if (policyCount == 0 && _isDefaultEndpointSelector)
	{
		// Fast path that avoids allocating the candidate set.
		//
		// We can use this when there are no policies and we're using the default selector.
		DefaultEndpointSelector.Select(httpContext, candidateState);
		return Task.CompletedTask;
	}
	else if (policyCount == 0)
	{
		// Fast path that avoids a state machine.
		//
		// We can use this when there are no policies and a non-default selector.
		return _selector.SelectAsync(httpContext, new CandidateSet(candidateState));
	}

	return SelectEndpointWithPoliciesAsync(httpContext, policies, new CandidateSet(candidateState));
}

這一段程式碼我看了一下,就是通過一些列的判斷,來設定httpcontext 的 Endpoint,這個有興趣可以看一下.

這裡提一下上面這個_selector:

services.TryAddSingleton<EndpointSelector, DefaultEndpointSelector>();

看一下DefaultEndpointSelector:

internal static void Select(HttpContext httpContext, CandidateState[] candidateState)
{
	// Fast path: We can specialize for trivial numbers of candidates since there can
	// be no ambiguities
	switch (candidateState.Length)
	{
		case 0:
			{
				// Do nothing
				break;
			}

		case 1:
			{
				ref var state = ref candidateState[0];
				if (CandidateSet.IsValidCandidate(ref state))
				{
					httpContext.SetEndpoint(state.Endpoint);
					httpContext.Request.RouteValues = state.Values!;
				}

				break;
			}

		default:
			{
				// Slow path: There's more than one candidate (to say nothing of validity) so we
				// have to process for ambiguities.
				ProcessFinalCandidates(httpContext, candidateState);
				break;
			}
	}
}

private static void ProcessFinalCandidates(
	HttpContext httpContext,
	CandidateState[] candidateState)
{
	Endpoint? endpoint = null;
	RouteValueDictionary? values = null;
	int? foundScore = null;
	for (var i = 0; i < candidateState.Length; i++)
	{
		ref var state = ref candidateState[i];
		if (!CandidateSet.IsValidCandidate(ref state))
		{
			continue;
		}

		if (foundScore == null)
		{
			// This is the first match we've seen - speculatively assign it.
			endpoint = state.Endpoint;
			values = state.Values;
			foundScore = state.Score;
		}
		else if (foundScore < state.Score)
		{
			// This candidate is lower priority than the one we've seen
			// so far, we can stop.
			//
			// Don't worry about the 'null < state.Score' case, it returns false.
			break;
		}
		else if (foundScore == state.Score)
		{
			// This is the second match we've found of the same score, so there
			// must be an ambiguity.
			//
			// Don't worry about the 'null == state.Score' case, it returns false.

			ReportAmbiguity(candidateState);

			// Unreachable, ReportAmbiguity always throws.
			throw new NotSupportedException();
		}
	}

	if (endpoint != null)
	{
		httpContext.SetEndpoint(endpoint);
		httpContext.Request.RouteValues = values!;
	}
}

這裡可以看一下Select。

如果候選項是0,那麼跳過這個就沒什麼好說的。

如果候選項是1,那麼就設定這個的endpoint。

如果匹配到多個,如果兩個相同得分相同的,就會丟擲異常,否則就是最後一個,也就是說我們不能設定完全相同的路由。

然後對於匹配策略而言呢,是:

感興趣可以看一下。

HttpMethodMatcherPolicy 這個是匹配405的。

HostMatcherPolicy 這個是用來匹配host的,如果不符合匹配到的endpoint,會設定驗證不通過。

下一節把app.UseEndpoints 介紹一下。