mirror of
https://github.com/wiremock/WireMock.Net.git
synced 2026-04-28 19:58:09 +02:00
* Move ScenarioState to Abstractions and add IScenarioStateStore interface ScenarioState is moved to the Abstractions project so it can be referenced by the new IScenarioStateStore interface. The interface defines the contract for storing and retrieving scenario states, enabling distributed implementations. * Add InMemoryScenarioStateStore default implementation Wraps ConcurrentDictionary with OrdinalIgnoreCase comparer, preserving exact current behavior. The Update method encapsulates read-modify-write so distributed implementations can make it atomic. * Wire IScenarioStateStore into middleware options, settings, and consumers Replace direct ConcurrentDictionary<string, ScenarioState> usage with IScenarioStateStore across all consumer files. The store is injectable via WireMockServerSettings.ScenarioStateStore, defaulting to the InMemoryScenarioStateStore for backward compatibility. * Add FileBasedScenarioStateStore for persistent scenario state In-memory ConcurrentDictionary backed by JSON file persistence in __admin/scenarios/. Reads from cache, mutations write through to disk. Constructor loads existing state from disk on startup. * Make ScenarioStateStore non-nullable with default InMemoryScenarioStateStore Move InMemoryScenarioStateStore from WireMock.Net.Minimal to WireMock.Net.Shared so it lives alongside WireMockServerSettings. This allows WireMockServerSettings.ScenarioStateStore to be non-nullable with a default value, following the same pattern as DefaultJsonSerializer. The null-coalescing fallback in WireMockMiddlewareOptionsHelper is no longer needed.
101 lines
3.8 KiB
C#
101 lines
3.8 KiB
C#
// Copyright © WireMock.Net
|
|
|
|
using System.Linq;
|
|
using Stef.Validation;
|
|
using WireMock.Extensions;
|
|
using WireMock.Services;
|
|
|
|
namespace WireMock.Owin;
|
|
|
|
internal class MappingMatcher(IWireMockMiddlewareOptions options, IRandomizerDoubleBetween0And1 randomizerDoubleBetween0And1) : IMappingMatcher
|
|
{
|
|
private readonly IWireMockMiddlewareOptions _options = Guard.NotNull(options);
|
|
private readonly IRandomizerDoubleBetween0And1 _randomizerDoubleBetween0And1 = Guard.NotNull(randomizerDoubleBetween0And1);
|
|
|
|
public (MappingMatcherResult? Match, MappingMatcherResult? Partial) FindBestMatch(RequestMessage request)
|
|
{
|
|
Guard.NotNull(request);
|
|
|
|
var possibleMappings = new List<MappingMatcherResult>();
|
|
|
|
var mappings = _options.Mappings.Values
|
|
.Where(m => m.TimeSettings.IsValid())
|
|
.Where(m => m.Probability is null || _randomizerDoubleBetween0And1.Generate() <= m.Probability)
|
|
.ToArray();
|
|
|
|
foreach (var mapping in mappings)
|
|
{
|
|
try
|
|
{
|
|
var nextState = GetNextState(mapping);
|
|
|
|
var mappingMatcherResult = new MappingMatcherResult(mapping, mapping.GetRequestMatchResult(request, nextState));
|
|
|
|
var exceptions = mappingMatcherResult.RequestMatchResult.MatchDetails
|
|
.Where(md => md.Exception != null)
|
|
.Select(md => md.Exception!)
|
|
.ToArray();
|
|
|
|
if (exceptions.Length == 0)
|
|
{
|
|
possibleMappings.Add(mappingMatcherResult);
|
|
}
|
|
else if (!request.AbsolutePath.StartsWith("/__admin", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
foreach (var ex in exceptions)
|
|
{
|
|
LogException(mapping, ex);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogException(mapping, ex);
|
|
}
|
|
}
|
|
|
|
var partialMatches = possibleMappings
|
|
.Where(pm => (pm.Mapping.IsAdminInterface && pm.RequestMatchResult.IsPerfectMatch) || !pm.Mapping.IsAdminInterface)
|
|
.OrderBy(m => m.RequestMatchResult)
|
|
.ThenBy(m => m.RequestMatchResult.TotalNumber)
|
|
.ThenBy(m => m.Mapping.Priority)
|
|
.ThenByDescending(m => m.Mapping.Probability)
|
|
.ThenByDescending(m => m.Mapping.UpdatedAt)
|
|
.Where(pm => pm.RequestMatchResult.AverageTotalScore > 0.0)
|
|
.ToArray();
|
|
var partialMatch = partialMatches.FirstOrDefault();
|
|
|
|
if (_options.AllowPartialMapping == true)
|
|
{
|
|
return (partialMatch, partialMatch);
|
|
}
|
|
|
|
var match = possibleMappings
|
|
.Where(m => m.RequestMatchResult.IsPerfectMatch)
|
|
.OrderBy(m => m.Mapping.Priority)
|
|
.ThenBy(m => m.RequestMatchResult)
|
|
.ThenBy(m => m.RequestMatchResult.TotalNumber)
|
|
.ThenByDescending(m => m.Mapping.Probability)
|
|
.ThenByDescending(m => m.Mapping.UpdatedAt)
|
|
.FirstOrDefault();
|
|
|
|
return (match, partialMatch);
|
|
}
|
|
|
|
private void LogException(IMapping mapping, Exception ex)
|
|
{
|
|
_options.Logger.Error($"Getting a Request MatchResult for Mapping '{mapping.Guid}' failed. This mapping will not be evaluated. Exception: {ex}");
|
|
}
|
|
|
|
private string? GetNextState(IMapping mapping)
|
|
{
|
|
// If the mapping does not have a scenario or the store does not contain this scenario,
|
|
// just return null to indicate that there is no next state.
|
|
if (mapping.Scenario == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return _options.ScenarioStateStore.TryGet(mapping.Scenario, out var state) ? state.NextState : null;
|
|
}
|
|
} |