diff --git a/examples/WireMock.Net.Console.NETCoreApp/__admin/mappings/791a3f31-6946-4ce7-8e6f-0237c7443275.json b/examples/WireMock.Net.Console.NETCoreApp/__admin/mappings/791a3f31-6946-4ce7-8e6f-0237c7443275.json
index f360d25d..35a1c17b 100644
--- a/examples/WireMock.Net.Console.NETCoreApp/__admin/mappings/791a3f31-6946-4ce7-8e6f-0237c7443275.json
+++ b/examples/WireMock.Net.Console.NETCoreApp/__admin/mappings/791a3f31-6946-4ce7-8e6f-0237c7443275.json
@@ -27,7 +27,7 @@
},
"UseTransformer": false,
"Headers": {
- "Date": "Wed, 25 Oct 2017 18:57:40 GMT",
+ "Date": "Wed, 27 Oct 2017 18:57:40 GMT",
"Alt-Svc": "quic=\":443\"; ma=2592000; v=\"39,38,37,35\"",
"Referrer-Policy": "no-referrer",
"Connection": "close"
diff --git a/examples/WireMock.Net.ConsoleApplication/MainApp.cs b/examples/WireMock.Net.ConsoleApplication/MainApp.cs
index c5a23ffa..6549b171 100644
--- a/examples/WireMock.Net.ConsoleApplication/MainApp.cs
+++ b/examples/WireMock.Net.ConsoleApplication/MainApp.cs
@@ -21,6 +21,7 @@ namespace WireMock.Net.ConsoleApplication
Urls = new[] { url1, url2, url3 },
StartAdminInterface = true,
ReadStaticMappings = true,
+ WatchStaticMappings = true,
//ProxyAndRecordSettings = new ProxyAndRecordSettings
//{
// SaveMapping = true
diff --git a/src/WireMock.Net.StandAlone/StandAloneApp.cs b/src/WireMock.Net.StandAlone/StandAloneApp.cs
index a2b365cd..aedea35d 100644
--- a/src/WireMock.Net.StandAlone/StandAloneApp.cs
+++ b/src/WireMock.Net.StandAlone/StandAloneApp.cs
@@ -43,6 +43,7 @@ namespace WireMock.Net.StandAlone
{
StartAdminInterface = parser.GetBoolValue("StartAdminInterface", true),
ReadStaticMappings = parser.GetBoolValue("ReadStaticMappings"),
+ WatchStaticMappings = parser.GetBoolValue("WatchStaticMappings"),
AllowPartialMapping = parser.GetBoolValue("AllowPartialMapping", true),
AdminUsername = parser.GetStringValue("AdminUsername"),
AdminPassword = parser.GetStringValue("AdminPassword"),
diff --git a/src/WireMock.Net/Admin/Mappings/MappingModel.cs b/src/WireMock.Net/Admin/Mappings/MappingModel.cs
index 62ddd6d8..200b5297 100644
--- a/src/WireMock.Net/Admin/Mappings/MappingModel.cs
+++ b/src/WireMock.Net/Admin/Mappings/MappingModel.cs
@@ -10,29 +10,20 @@ namespace WireMock.Admin.Mappings
///
/// Gets or sets the unique identifier.
///
- ///
- /// The unique identifier.
- ///
public Guid? Guid { get; set; }
///
- /// Gets or sets the unique title.
- ///
- ///
/// The unique title.
- ///
+ ///
public string Title { get; set; }
///
- /// Gets or sets the priority.
- ///
- ///
/// The priority.
- ///
+ ///
public int? Priority { get; set; }
///
- /// Scenario.
+ /// The Scenario.
///
public string Scenario { get; set; }
@@ -48,19 +39,13 @@ namespace WireMock.Admin.Mappings
public object SetStateTo { get; set; }
///
- /// Gets or sets the request.
- ///
- ///
/// The request.
- ///
+ ///
public RequestModel Request { get; set; }
///
- /// Gets or sets the response.
- ///
- ///
/// The response.
- ///
+ ///
public ResponseModel Response { get; set; }
}
}
\ No newline at end of file
diff --git a/src/WireMock.Net/Mapping.cs b/src/WireMock.Net/Mapping.cs
index 72d9a136..d0d9b352 100644
--- a/src/WireMock.Net/Mapping.cs
+++ b/src/WireMock.Net/Mapping.cs
@@ -20,6 +20,11 @@ namespace WireMock
///
public string Title { get; }
+ ///
+ /// The full filename path for this mapping (only defined for static mappings).
+ ///
+ public string Path { get; set; }
+
///
/// Gets the priority.
///
@@ -63,17 +68,19 @@ namespace WireMock
/// Initializes a new instance of the class.
///
/// The unique identifier.
- /// The unique title (can be null_.
+ /// The unique title (can be null).
+ /// The full file path from this mapping title (can be null).
/// The request matcher.
/// The provider.
/// The priority for this mapping.
/// The scenario. [Optional]
/// State in which the current mapping can occur. [Optional]
/// The next state which will occur after the current mapping execution. [Optional]
- public Mapping(Guid guid, [CanBeNull] string title, IRequestMatcher requestMatcher, IResponseProvider provider, int priority, [CanBeNull] string scenario, [CanBeNull] object executionConditionState, [CanBeNull] object nextState)
+ public Mapping(Guid guid, [CanBeNull] string title, [CanBeNull] string path, IRequestMatcher requestMatcher, IResponseProvider provider, int priority, [CanBeNull] string scenario, [CanBeNull] object executionConditionState, [CanBeNull] object nextState)
{
Guid = guid;
Title = title;
+ Path = path;
RequestMatcher = requestMatcher;
Provider = provider;
Priority = priority;
diff --git a/src/WireMock.Net/Server/FluentMockServer.Admin.cs b/src/WireMock.Net/Server/FluentMockServer.Admin.cs
index 73c71c1f..46928b6b 100644
--- a/src/WireMock.Net/Server/FluentMockServer.Admin.cs
+++ b/src/WireMock.Net/Server/FluentMockServer.Admin.cs
@@ -43,46 +43,7 @@ namespace WireMock.Server
NullValueHandling = NullValueHandling.Ignore,
};
- ///
- /// Reads the static mappings from a folder.
- ///
- /// The optional folder. If not defined, use \__admin\mappings\
- [PublicAPI]
- public void ReadStaticMappings([CanBeNull] string folder = null)
- {
- if (folder == null)
- folder = Path.Combine(Directory.GetCurrentDirectory(), AdminMappingsFolder);
-
- if (!Directory.Exists(folder))
- return;
-
- foreach (string filename in Directory.EnumerateFiles(folder).OrderBy(f => f))
- {
- ReadStaticMapping(filename);
- }
- }
-
- ///
- /// Reads the static mapping.
- ///
- /// The filename.
- [PublicAPI]
- public void ReadStaticMapping([NotNull] string filename)
- {
- Check.NotNull(filename, nameof(filename));
-
- string filenameWithoutExtension = Path.GetFileNameWithoutExtension(filename);
-
- if (Guid.TryParse(filenameWithoutExtension, out var guidFromFilename))
- {
- DeserializeAndAddMapping(File.ReadAllText(filename), guidFromFilename);
- }
- else
- {
- DeserializeAndAddMapping(File.ReadAllText(filename));
- }
- }
-
+ #region InitAdmin
private void InitAdmin()
{
// __admin/settings
@@ -129,6 +90,96 @@ namespace WireMock.Server
// __admin/scenarios/reset
Given(Request.Create().WithPath(AdminScenarios + "/reset").UsingPost()).RespondWith(new DynamicResponseProvider(ScenariosReset));
}
+ #endregion
+
+ #region StaticMappings
+ ///
+ /// Reads the static mappings from a folder.
+ ///
+ /// The optional folder. If not defined, use \__admin\mappings\
+ [PublicAPI]
+ public void ReadStaticMappings([CanBeNull] string folder = null)
+ {
+ if (folder == null)
+ {
+ folder = Path.Combine(Directory.GetCurrentDirectory(), AdminMappingsFolder);
+ }
+
+ if (!Directory.Exists(folder))
+ {
+ return;
+ }
+
+ foreach (string filename in Directory.EnumerateFiles(folder).OrderBy(f => f))
+ {
+ ReadStaticMappingAndAddOrUpdate(filename);
+ }
+ }
+
+ ///
+ /// Watches the static mappings for changes.
+ ///
+ /// The optional folder. If not defined, use \__admin\mappings\
+ [PublicAPI]
+ public void WatchStaticMappings([CanBeNull] string folder = null)
+ {
+ if (folder == null)
+ {
+ folder = Path.Combine(Directory.GetCurrentDirectory(), AdminMappingsFolder);
+ }
+
+ if (!Directory.Exists(folder))
+ {
+ return;
+ }
+
+ var watcher = new EnhancedFileSystemWatcher(folder, "*.json", 500);
+ watcher.Created += (sender, args) =>
+ {
+ ReadStaticMappingAndAddOrUpdate(args.FullPath);
+ };
+ watcher.Changed += (sender, args) =>
+ {
+ ReadStaticMappingAndAddOrUpdate(args.FullPath);
+ };
+ watcher.Deleted += (sender, args) =>
+ {
+ string filenameWithoutExtension = Path.GetFileNameWithoutExtension(args.FullPath);
+
+ if (Guid.TryParse(filenameWithoutExtension, out var guidFromFilename))
+ {
+ DeleteMapping(guidFromFilename);
+ }
+ else
+ {
+ DeleteMapping(args.FullPath);
+ }
+ };
+
+ watcher.EnableRaisingEvents = true;
+ }
+
+ ///
+ /// Reads a static mapping file and adds or updates the mapping.
+ ///
+ /// The path.
+ [PublicAPI]
+ public void ReadStaticMappingAndAddOrUpdate([NotNull] string path)
+ {
+ Check.NotNull(path, nameof(path));
+
+ string filenameWithoutExtension = Path.GetFileNameWithoutExtension(path);
+
+ if (Guid.TryParse(filenameWithoutExtension, out Guid guidFromFilename))
+ {
+ DeserializeAndAddOrUpdateMapping(FileHelper.ReadAllText(path), guidFromFilename, path);
+ }
+ else
+ {
+ DeserializeAndAddOrUpdateMapping(FileHelper.ReadAllText(path), null, path);
+ }
+ }
+ #endregion
#region Proxy and Record
private HttpClient _httpClientForProxy;
@@ -185,7 +236,7 @@ namespace WireMock.Server
var response = Response.Create(responseMessage);
- return new Mapping(Guid.NewGuid(), string.Empty, request, response, 0, null, null, null);
+ return new Mapping(Guid.NewGuid(), string.Empty, null, request, response, 0, null, null, null);
}
#endregion
@@ -228,7 +279,9 @@ namespace WireMock.Server
var mapping = Mappings.FirstOrDefault(m => !m.IsAdminInterface && m.Guid == guid);
if (mapping == null)
+ {
return new ResponseMessage { StatusCode = 404, Body = "Mapping not found" };
+ }
var model = MappingConverter.ToMappingModel(mapping);
@@ -238,23 +291,8 @@ namespace WireMock.Server
private ResponseMessage MappingPut(RequestMessage requestMessage)
{
Guid guid = Guid.Parse(requestMessage.Path.TrimStart(AdminMappings.ToCharArray()));
- var mappingModel = JsonConvert.DeserializeObject(requestMessage.Body);
- if (mappingModel.Request == null)
- return new ResponseMessage { StatusCode = 400, Body = "Request missing" };
-
- if (mappingModel.Response == null)
- return new ResponseMessage { StatusCode = 400, Body = "Response missing" };
-
- var requestBuilder = InitRequestBuilder(mappingModel.Request);
- var responseBuilder = InitResponseBuilder(mappingModel.Response);
-
- IRespondWithAProvider respondProvider = Given(requestBuilder).WithGuid(guid);
-
- if (!string.IsNullOrEmpty(mappingModel.Title))
- respondProvider = respondProvider.WithTitle(mappingModel.Title);
-
- respondProvider.RespondWith(responseBuilder);
+ DeserializeAndAddOrUpdateMapping(requestMessage.Body, guid);
return new ResponseMessage { Body = "Mapping added or updated" };
}
@@ -264,7 +302,9 @@ namespace WireMock.Server
Guid guid = Guid.Parse(requestMessage.Path.Substring(AdminMappings.Length + 1));
if (DeleteMapping(guid))
+ {
return new ResponseMessage { Body = "Mapping removed" };
+ }
return new ResponseMessage { Body = "Mapping not found" };
}
@@ -285,7 +325,9 @@ namespace WireMock.Server
{
string folder = Path.Combine(Directory.GetCurrentDirectory(), AdminMappingsFolder);
if (!Directory.Exists(folder))
+ {
Directory.CreateDirectory(folder);
+ }
var model = MappingConverter.ToMappingModel(mapping);
string json = JsonConvert.SerializeObject(model, _settings);
@@ -315,7 +357,7 @@ namespace WireMock.Server
{
try
{
- DeserializeAndAddMapping(requestMessage.Body);
+ DeserializeAndAddOrUpdateMapping(requestMessage.Body);
}
catch (ArgumentException a)
{
@@ -329,7 +371,7 @@ namespace WireMock.Server
return new ResponseMessage { StatusCode = 201, Body = "Mapping added" };
}
- private void DeserializeAndAddMapping(string json, Guid? guid = null)
+ private void DeserializeAndAddOrUpdateMapping(string json, Guid? guid = null, string path = null)
{
var mappingModel = JsonConvert.DeserializeObject(json);
@@ -351,11 +393,20 @@ namespace WireMock.Server
respondProvider = respondProvider.WithGuid(mappingModel.Guid.Value);
}
+ if (path != null)
+ {
+ respondProvider = respondProvider.WithPath(path);
+ }
+
if (!string.IsNullOrEmpty(mappingModel.Title))
+ {
respondProvider = respondProvider.WithTitle(mappingModel.Title);
+ }
if (mappingModel.Priority != null)
+ {
respondProvider = respondProvider.AtPriority(mappingModel.Priority.Value);
+ }
if (mappingModel.Scenario != null)
{
@@ -688,7 +739,7 @@ namespace WireMock.Server
{
Body = JsonConvert.SerializeObject(result, _settings),
StatusCode = 200,
- Headers = new Dictionary> { { "Content-Type", new WireMockList("application/json") } }
+ Headers = new Dictionary> { { HttpKnownHeaderNames.ContentType, new WireMockList("application/json") } }
};
}
diff --git a/src/WireMock.Net/Server/FluentMockServer.cs b/src/WireMock.Net/Server/FluentMockServer.cs
index b2e55cc3..3ab9838c 100644
--- a/src/WireMock.Net/Server/FluentMockServer.cs
+++ b/src/WireMock.Net/Server/FluentMockServer.cs
@@ -13,6 +13,7 @@ using WireMock.RequestBuilders;
using WireMock.Settings;
using WireMock.Validation;
using WireMock.Owin;
+using WireMock.Serialization;
namespace WireMock.Server
{
@@ -202,6 +203,11 @@ namespace WireMock.Server
ReadStaticMappings();
}
+ if (settings.WatchStaticMappings == true)
+ {
+ WatchStaticMappings();
+ }
+
if (settings.ProxyAndRecordSettings != null)
{
InitProxyAndRecord(settings.ProxyAndRecordSettings);
@@ -274,7 +280,18 @@ namespace WireMock.Server
public bool DeleteMapping(Guid guid)
{
// Check a mapping exists with the same GUID, if so, remove it.
- var existingMapping = _options.Mappings.FirstOrDefault(m => m.Guid == guid);
+ return DeleteMapping(m => m.Guid == guid);
+ }
+
+ private bool DeleteMapping(string path)
+ {
+ // Check a mapping exists with the same path, if so, remove it.
+ return DeleteMapping(m => string.Equals(m.Path, path, StringComparison.OrdinalIgnoreCase));
+ }
+
+ private bool DeleteMapping(Func predicate)
+ {
+ var existingMapping = _options.Mappings.FirstOrDefault(predicate);
if (existingMapping != null)
{
_options.Mappings.Remove(existingMapping);
diff --git a/src/WireMock.Net/Server/IRespondWithAProvider.cs b/src/WireMock.Net/Server/IRespondWithAProvider.cs
index 72486ac9..fd10623e 100644
--- a/src/WireMock.Net/Server/IRespondWithAProvider.cs
+++ b/src/WireMock.Net/Server/IRespondWithAProvider.cs
@@ -21,6 +21,13 @@ namespace WireMock.Server
/// The .
IRespondWithAProvider WithTitle(string title);
+ ///
+ /// Define the full filepath for this mapping.
+ ///
+ /// The full filepath.
+ /// The .
+ IRespondWithAProvider WithPath(string path);
+
///
/// Define a unique identifier for this mapping.
///
diff --git a/src/WireMock.Net/Server/RespondWithAProvider.cs b/src/WireMock.Net/Server/RespondWithAProvider.cs
index 77546c68..bc7f50f2 100644
--- a/src/WireMock.Net/Server/RespondWithAProvider.cs
+++ b/src/WireMock.Net/Server/RespondWithAProvider.cs
@@ -11,18 +11,11 @@ namespace WireMock.Server
private int _priority;
private Guid? _guid;
private string _title;
+ private string _path;
private object _executionConditionState;
private object _nextState;
private string _scenario;
-
- ///
- /// The _registration callback.
- ///
private readonly RegistrationCallback _registrationCallback;
-
- ///
- /// The _request matcher.
- ///
private readonly IRequestMatcher _requestMatcher;
///
@@ -39,30 +32,20 @@ namespace WireMock.Server
///
/// The respond with.
///
- ///
- /// The provider.
- ///
+ /// The provider.
public void RespondWith(IResponseProvider provider)
{
var mappingGuid = _guid ?? Guid.NewGuid();
- _registrationCallback(new Mapping(mappingGuid, _title, _requestMatcher, provider, _priority, _scenario, _executionConditionState, _nextState));
+ _registrationCallback(new Mapping(mappingGuid, _title, _path, _requestMatcher, provider, _priority, _scenario, _executionConditionState, _nextState));
}
- ///
- /// Define a unique identifier for this mapping.
- ///
- /// The unique identifier.
- /// The .
+ ///
public IRespondWithAProvider WithGuid(string guid)
{
return WithGuid(Guid.Parse(guid));
}
- ///
- /// Define a unique identifier for this mapping.
- ///
- /// The unique identifier.
- /// The .
+ ///
public IRespondWithAProvider WithGuid(Guid guid)
{
_guid = guid;
@@ -70,11 +53,7 @@ namespace WireMock.Server
return this;
}
- ///
- /// Define a unique identifier for this mapping.
- ///
- /// The unique identifier.
- /// The .
+ ///
public IRespondWithAProvider WithTitle(string title)
{
_title = title;
@@ -82,11 +61,15 @@ namespace WireMock.Server
return this;
}
- ///
- /// Define the priority for this mapping.
- ///
- /// The priority.
- /// The .
+ ///
+ public IRespondWithAProvider WithPath(string path)
+ {
+ _path = path;
+
+ return this;
+ }
+
+ ///
public IRespondWithAProvider AtPriority(int priority)
{
_priority = priority;
@@ -94,6 +77,7 @@ namespace WireMock.Server
return this;
}
+ ///
public IRespondWithAProvider InScenario(string scenario)
{
_scenario = scenario;
@@ -101,6 +85,7 @@ namespace WireMock.Server
return this;
}
+ ///
public IRespondWithAProvider WhenStateIs(object state)
{
if (string.IsNullOrEmpty(_scenario))
@@ -118,6 +103,7 @@ namespace WireMock.Server
return this;
}
+ ///
public IRespondWithAProvider WillSetStateTo(object state)
{
if (string.IsNullOrEmpty(_scenario))
diff --git a/src/WireMock.Net/Settings/FluentMockServerSettings.cs b/src/WireMock.Net/Settings/FluentMockServerSettings.cs
index d526eafb..2784e620 100644
--- a/src/WireMock.Net/Settings/FluentMockServerSettings.cs
+++ b/src/WireMock.Net/Settings/FluentMockServerSettings.cs
@@ -25,6 +25,10 @@ namespace WireMock.Settings
[PublicAPI]
public bool? ReadStaticMappings { get; set; }
+ ///
+ [PublicAPI]
+ public bool? WatchStaticMappings { get; set; }
+
///
[PublicAPI]
public IProxyAndRecordSettings ProxyAndRecordSettings { get; set; }
diff --git a/src/WireMock.Net/Settings/IFluentMockServerSettings.cs b/src/WireMock.Net/Settings/IFluentMockServerSettings.cs
index 92cebb7a..3ad5ba59 100644
--- a/src/WireMock.Net/Settings/IFluentMockServerSettings.cs
+++ b/src/WireMock.Net/Settings/IFluentMockServerSettings.cs
@@ -28,6 +28,11 @@ namespace WireMock.Settings
///
bool? ReadStaticMappings { get; set; }
+ ///
+ /// Watch the static mapping files + folder for changes when running.
+ ///
+ bool? WatchStaticMappings { get; set; }
+
///
/// Gets or sets if the proxy and record settings.
///
diff --git a/src/WireMock.Net/Util/FileHelper.cs b/src/WireMock.Net/Util/FileHelper.cs
new file mode 100644
index 00000000..18c0aad2
--- /dev/null
+++ b/src/WireMock.Net/Util/FileHelper.cs
@@ -0,0 +1,29 @@
+using System.IO;
+using System.Threading;
+
+namespace WireMock.Util
+{
+ internal static class FileHelper
+ {
+ private const int NumberOfRetries = 3;
+ private const int DelayOnRetry = 500;
+
+ public static string ReadAllText(string path)
+ {
+ for (int i = 1; i <= NumberOfRetries; ++i)
+ {
+ try
+ {
+ return File.ReadAllText(path);
+ }
+ catch
+ {
+ // You may check error code to filter some exceptions, not every error can be recovered.
+ Thread.Sleep(DelayOnRetry);
+ }
+ }
+
+ throw new IOException();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/WireMock.Net/WireMock.Net.csproj b/src/WireMock.Net/WireMock.Net.csproj
index 7884a767..e85b9cb2 100644
--- a/src/WireMock.Net/WireMock.Net.csproj
+++ b/src/WireMock.Net/WireMock.Net.csproj
@@ -33,7 +33,7 @@
-
+
All
@@ -41,6 +41,7 @@
+
diff --git a/test/WireMock.Net.Tests/FluentMockServerTests.cs b/test/WireMock.Net.Tests/FluentMockServerTests.cs
index db45d7af..91431a5c 100644
--- a/test/WireMock.Net.Tests/FluentMockServerTests.cs
+++ b/test/WireMock.Net.Tests/FluentMockServerTests.cs
@@ -47,7 +47,7 @@ namespace WireMock.Net.Tests
_server = FluentMockServer.Start();
string folder = Path.Combine(GetCurrentFolder(), "__admin", "mappings", "documentdb_root.json");
- _server.ReadStaticMapping(folder);
+ _server.ReadStaticMappingAndAddOrUpdate(folder);
var mappings = _server.Mappings.ToArray();
Check.That(mappings).HasSize(1);
@@ -65,7 +65,7 @@ namespace WireMock.Net.Tests
_server = FluentMockServer.Start();
string folder = Path.Combine(GetCurrentFolder(), "__admin", "mappings", guid + ".json");
- _server.ReadStaticMapping(folder);
+ _server.ReadStaticMappingAndAddOrUpdate(folder);
var mappings = _server.Mappings.ToArray();
Check.That(mappings).HasSize(1);