diff --git a/ZeroLevel.Discovery/App.config b/ZeroLevel.Discovery/App.config
new file mode 100644
index 0000000..430fe22
--- /dev/null
+++ b/ZeroLevel.Discovery/App.config
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ZeroLevel.Discovery/Controllers/BaseController.cs b/ZeroLevel.Discovery/Controllers/BaseController.cs
new file mode 100644
index 0000000..03c8d5e
--- /dev/null
+++ b/ZeroLevel.Discovery/Controllers/BaseController.cs
@@ -0,0 +1,95 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Web.Http;
+using ZeroLevel.Services.Web;
+
+namespace ZeroLevel.Discovery
+{
+ public abstract class BaseController : ApiController
+ {
+ #region Responce create helpers
+ public static HttpResponseMessage BadRequestAnswer(HttpRequestMessage request, string message)
+ {
+ return request.CreateSelfDestroyingResponse(HttpStatusCode.BadRequest,
+ message.Replace("\r", " ").Replace("\n", " "));
+ }
+
+ public static HttpResponseMessage BadRequestAnswer(HttpRequestMessage request, Exception ex)
+ {
+ return request.CreateSelfDestroyingResponse(HttpStatusCode.BadRequest,
+ ex.Message.Replace("\r", " ").Replace("\n", " "));
+ }
+
+ public static HttpResponseMessage SuccessAnswer(HttpRequestMessage request)
+ {
+ return request.CreateSelfDestroyingResponse(HttpStatusCode.OK);
+ }
+
+ public static HttpResponseMessage NotFoundAnswer(HttpRequestMessage request, string message)
+ {
+ return request.CreateSelfDestroyingResponse(HttpStatusCode.Conflict,
+ message.Replace("\r", " ").Replace("\n", " "));
+ }
+
+ public static HttpResponseMessage HttpActionResult(HttpRequestMessage request, Func responseBuilder)
+ {
+ try
+ {
+ return request.CreateSelfDestroyingResponse(responseBuilder(), HttpStatusCode.OK);
+ }
+ catch (KeyNotFoundException knfEx)
+ {
+ return NotFoundAnswer(request, knfEx.Message);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Request {0} fault", request.RequestUri.PathAndQuery);
+ return BadRequestAnswer(request, ex);
+ }
+ }
+
+ protected static DateTime? ParseDateTime(string line)
+ {
+ var dateParts = line.Split('.', '/', '\\', '-').Select(p => p.Trim()).ToArray();
+ if (dateParts.Last().Length == 4)
+ {
+ dateParts = dateParts.Reverse().ToArray();
+ }
+ if (dateParts.First().Length != 4) return null;
+ int year, month = 1, day = 1;
+ if (false == int.TryParse(dateParts.First(), out year))
+ {
+ return null;
+ }
+ if (dateParts.Count() > 1)
+ {
+ if (false == int.TryParse(dateParts[1], out month))
+ {
+ return null;
+ }
+ }
+ if (dateParts.Count() > 2)
+ {
+ if (false == int.TryParse(dateParts[2], out day))
+ {
+ return null;
+ }
+ }
+ return new DateTime(year, month, day);
+ }
+
+ protected static String GetParameter(HttpRequestMessage request, string name)
+ {
+ var keys = UrlUtility.ParseQueryString(request.RequestUri.Query);
+ if (keys.ContainsKey(name))
+ {
+ return keys[name];
+ }
+ return null;
+ }
+ #endregion
+ }
+}
diff --git a/ZeroLevel.Discovery/Controllers/HttpRequestMessagesExtensions.cs b/ZeroLevel.Discovery/Controllers/HttpRequestMessagesExtensions.cs
new file mode 100644
index 0000000..36353f8
--- /dev/null
+++ b/ZeroLevel.Discovery/Controllers/HttpRequestMessagesExtensions.cs
@@ -0,0 +1,72 @@
+using System.Net;
+using System.Net.Http;
+
+namespace ZeroLevel.Discovery
+{
+ public static class HttpRequestMessagesExtensions
+ {
+ private const string HttpContext = "MS_HttpContext";
+ private const string RemoteEndpointMessage = "System.ServiceModel.Channels.RemoteEndpointMessageProperty";
+ private const string OwinContext = "MS_OwinContext";
+
+ public static string GetClientIpAddress(HttpRequestMessage request)
+ {
+ //Web-hosting
+ if (request.Properties.ContainsKey(HttpContext))
+ {
+ dynamic ctx = request.Properties[HttpContext];
+ if (ctx != null)
+ {
+ return ctx.Request.UserHostAddress;
+ }
+ }
+ //Self-hosting
+ if (request.Properties.ContainsKey(RemoteEndpointMessage))
+ {
+ dynamic remoteEndpoint = request.Properties[RemoteEndpointMessage];
+ if (remoteEndpoint != null)
+ {
+ return remoteEndpoint.Address;
+ }
+ }
+ //Owin-hosting
+ if (request.Properties.ContainsKey(OwinContext))
+ {
+ dynamic ctx = request.Properties[OwinContext];
+ if (ctx != null)
+ {
+ return ctx.Request.RemoteIpAddress;
+ }
+ }
+ return null;
+ }
+
+ public static HttpResponseMessage CreateSelfDestroyingResponse(this HttpRequestMessage request, HttpStatusCode code = HttpStatusCode.OK)
+ {
+ var response = request.CreateResponse(code);
+ request.RegisterForDispose(response);
+ return response;
+ }
+
+ public static HttpResponseMessage CreateSelfDestroyingResponse(this HttpRequestMessage request, T val, HttpStatusCode code = HttpStatusCode.OK)
+ {
+ var response = request.CreateResponse(code, val);
+ request.RegisterForDispose(response);
+ return response;
+ }
+
+ public static HttpResponseMessage CreateSelfDestroyingResponse(this HttpRequestMessage request, HttpStatusCode code, string reasonPhrase)
+ {
+ var response = request.CreateResponse(code);
+ response.ReasonPhrase = reasonPhrase;
+ request.RegisterForDispose(response);
+ return response;
+ }
+
+ public static HttpResponseMessage CreateSelfDestroyingResponse(this HttpRequestMessage request, HttpResponseMessage response)
+ {
+ request.RegisterForDispose(response);
+ return response;
+ }
+ }
+}
diff --git a/ZeroLevel.Discovery/Controllers/RoutesController.cs b/ZeroLevel.Discovery/Controllers/RoutesController.cs
new file mode 100644
index 0000000..af65ac5
--- /dev/null
+++ b/ZeroLevel.Discovery/Controllers/RoutesController.cs
@@ -0,0 +1,55 @@
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Threading.Tasks;
+using System.Web.Http;
+using System.Web.Http.Description;
+using ZeroLevel.Models;
+using ZeroLevel.Network.Microservices;
+
+namespace ZeroLevel.Discovery
+{
+ public class RoutesController :
+ BaseController
+ {
+ [HttpGet]
+ [Route("favicon.ico")]
+ public HttpResponseMessage favicon(HttpRequestMessage request)
+ {
+ return null;
+ }
+
+ [HttpGet]
+ [Route("api/v0/routes")]
+ [ResponseType(typeof(IEnumerable))]
+ public HttpResponseMessage GetRoutes(HttpRequestMessage request)
+ {
+ try
+ {
+ return request.CreateSelfDestroyingResponse(Injector.Default.Resolve().Get());
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Error with read records");
+ return BadRequestAnswer(request, ex);
+ }
+ }
+
+ [HttpPost]
+ [Route("api/v0/routes")]
+ [ResponseType(typeof(InvokeResult))]
+ public HttpResponseMessage AddRoute(HttpRequestMessage request, MicroserviceInfo service)
+ {
+ try
+ {
+ var ir = Injector.Default.Resolve().Append(service);
+ return request.CreateSelfDestroyingResponse(ir);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Error with append endpoint");
+ return BadRequestAnswer(request, ex);
+ }
+ }
+ }
+}
diff --git a/ZeroLevel.Discovery/DiscoveryService.cs b/ZeroLevel.Discovery/DiscoveryService.cs
new file mode 100644
index 0000000..7ad6ff1
--- /dev/null
+++ b/ZeroLevel.Discovery/DiscoveryService.cs
@@ -0,0 +1,32 @@
+using ZeroLevel.Services.Applications;
+
+namespace ZeroLevel.Discovery
+{
+ public sealed class DiscoveryService
+ : BaseWindowsService, IZeroService
+ {
+ public DiscoveryService()
+ : base("Discovery")
+ {
+ }
+
+ public override void PauseAction()
+ {
+ }
+
+ public override void ResumeAction()
+ {
+ }
+
+ public override void StartAction()
+ {
+ Injector.Default.Register(new RouteTable());
+ var port = Configuration.Default.First("port");
+ Startup.StartWebPanel(port, false);
+ }
+
+ public override void StopAction()
+ {
+ }
+ }
+}
diff --git a/ZeroLevel.Discovery/ExchangeTransportFactory.cs b/ZeroLevel.Discovery/ExchangeTransportFactory.cs
new file mode 100644
index 0000000..76864fe
--- /dev/null
+++ b/ZeroLevel.Discovery/ExchangeTransportFactory.cs
@@ -0,0 +1,110 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Net;
+using System.Reflection;
+using ZeroLevel.Network.Microservices;
+using ZeroLevel.Services.Network;
+using ZeroLevel.Services.Network.Contract;
+
+namespace ZeroLevel.Microservices
+{
+ internal static class ExchangeTransportFactory
+ {
+ private static readonly Dictionary _customServers = new Dictionary();
+ private static readonly Dictionary _customClients = new Dictionary();
+ private static readonly ConcurrentDictionary _clientInstances = new ConcurrentDictionary();
+
+ ///
+ /// Сканирование указанной сборки для поиска типов реализующих интерфейсы
+ /// IExchangeServer или IExchangeClient
+ ///
+ internal static void ScanAndRegisterCustomTransport(Assembly asm)
+ {
+ foreach (var type in asm.GetExportedTypes())
+ {
+ var serverAttr = type.GetCustomAttribute();
+ if (serverAttr != null &&
+ string.IsNullOrWhiteSpace(serverAttr.Protocol) == false &&
+ typeof(IZObservableServer).IsAssignableFrom(type))
+ {
+ _customServers[serverAttr.Protocol] = type;
+ }
+ var clientAttr = type.GetCustomAttribute();
+ if (clientAttr != null &&
+ string.IsNullOrWhiteSpace(clientAttr.Protocol) == false &&
+ typeof(IZTransport).IsAssignableFrom(type))
+ {
+ _customClients[clientAttr.Protocol] = type;
+ }
+ }
+ }
+
+ ///
+ /// Создает сервер для приема сообщений по указанному протоколу
+ ///
+ /// Протокол
+ /// Сервер
+ internal static ExService GetServer(string protocol)
+ {
+ ExService instance = null;
+ if (protocol.Equals("socket", StringComparison.OrdinalIgnoreCase))
+ {
+ instance = new ExService(new ZExSocketObservableServer(new System.Net.IPEndPoint(IPAddress.Any, IPFinder.GetFreeTcpPort())));
+ }
+ else
+ {
+ var key = protocol.Trim().ToLowerInvariant();
+ if (_customServers.ContainsKey(key))
+ {
+ instance = new ExService((IZObservableServer)Activator.CreateInstance(_customServers[key]));
+ }
+ }
+ if (instance != null)
+ {
+ return instance;
+ }
+ throw new NotSupportedException($"Protocol {protocol} not supported");
+ }
+
+ ///
+ /// Создает клиента для обращений к серверу по указанному протоколу
+ ///
+ /// Протокол
+ /// Адрес сервера
+ /// Клиент
+ internal static ExClient GetClient(string protocol, string endpoint)
+ {
+ ExClient instance = null;
+ var cachee_key = $"{protocol}:{endpoint}";
+ if (_clientInstances.ContainsKey(cachee_key))
+ {
+ instance = _clientInstances[cachee_key];
+ if (instance.Status == ZTransportStatus.Working)
+ {
+ return instance;
+ }
+ _clientInstances.TryRemove(cachee_key, out instance);
+ instance = null;
+ }
+ if (protocol.Equals("socket", StringComparison.OrdinalIgnoreCase))
+ {
+ instance = new ExClient(new ZSocketClient(SocketExtensions.CreateIPEndPoint(endpoint)));
+ }
+ else
+ {
+ var key = protocol.Trim().ToLowerInvariant();
+ if (_customClients.ContainsKey(key))
+ {
+ instance = new ExClient((IZTransport)Activator.CreateInstance(_customClients[key], new object[] { endpoint }));
+ }
+ }
+ if (instance != null)
+ {
+ _clientInstances[cachee_key] = instance;
+ return instance;
+ }
+ throw new NotSupportedException($"Protocol {protocol} not supported");
+ }
+ }
+}
diff --git a/ZeroLevel.Discovery/Program.cs b/ZeroLevel.Discovery/Program.cs
new file mode 100644
index 0000000..bd8b524
--- /dev/null
+++ b/ZeroLevel.Discovery/Program.cs
@@ -0,0 +1,10 @@
+namespace ZeroLevel.Discovery
+{
+ class Program
+ {
+ static void Main(string[] args)
+ {
+ Bootstrap.Startup(args);
+ }
+ }
+}
diff --git a/ZeroLevel.Discovery/Properties/AssemblyInfo.cs b/ZeroLevel.Discovery/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..53517ec
--- /dev/null
+++ b/ZeroLevel.Discovery/Properties/AssemblyInfo.cs
@@ -0,0 +1,36 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("ZeroLevel.Discovery")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("ZeroLevel.Discovery")]
+[assembly: AssemblyCopyright("Copyright © 2019")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("4f55b23f-938c-4da2-b6dc-b6bc66d36073")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/ZeroLevel.Discovery/RouteTable.cs b/ZeroLevel.Discovery/RouteTable.cs
new file mode 100644
index 0000000..535ae63
--- /dev/null
+++ b/ZeroLevel.Discovery/RouteTable.cs
@@ -0,0 +1,246 @@
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Threading;
+using ZeroLevel.Microservices;
+using ZeroLevel.Models;
+using ZeroLevel.Network.Microservices;
+
+namespace ZeroLevel.Discovery
+{
+ public class RouteTable
+ : IDisposable
+ {
+ private readonly Dictionary _table = new Dictionary();
+ private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
+
+ public RouteTable()
+ {
+ Load();
+ Sheduller.RemindEvery(TimeSpan.FromSeconds(10), Heartbeat);
+ }
+
+ #region Snapshot
+ private static readonly object _snapshot_lock = new object();
+
+ private void Save()
+ {
+ string snapshot;
+ _lock.EnterReadLock();
+ try
+ {
+ snapshot = JsonConvert.SerializeObject(_table);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Fault make snapshot");
+ return;
+ }
+ finally
+ {
+ _lock.ExitReadLock();
+ }
+ try
+ {
+ var snapshot_path = Path.Combine(Configuration.BaseDirectory, "snapshot.snp");
+ lock (_snapshot_lock)
+ {
+ File.WriteAllText(snapshot_path, snapshot);
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Fault save shapshot");
+ }
+ }
+
+ private void Load()
+ {
+ try
+ {
+ var path = Path.Combine(Configuration.BaseDirectory, "snapshot.snp");
+ if (File.Exists(path))
+ {
+ var snapshot = File.ReadAllText(path);
+ if (string.IsNullOrWhiteSpace(snapshot) == false)
+ {
+ var restored = JsonConvert.DeserializeObject>(snapshot);
+ _lock.EnterWriteLock();
+ try
+ {
+ _table.Clear();
+ foreach (var r in restored)
+ {
+ _table.Add(r.Key, r.Value);
+ }
+ }
+ finally
+ {
+ _lock.ExitWriteLock();
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Fault load snapshot");
+ }
+ }
+ #endregion
+
+ private bool Ping(string protocol, string endpoint, string msg)
+ {
+ try
+ {
+ using (var client = ExchangeTransportFactory.GetClient(protocol, endpoint))
+ {
+ return client.Status == Services.Network.ZTransportStatus.Working;
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"[RouteTable] Fault ping endpoint {endpoint}, protocol {protocol}");
+ return false;
+ }
+ }
+
+ private void Heartbeat(long taskid)
+ {
+ try
+ {
+ var removeEntities = new Dictionary>();
+ _lock.EnterReadLock();
+ try
+ {
+ foreach (var pair in _table)
+ {
+ var endpointsToRemove = new List();
+ foreach (var e in pair.Value.Endpoints)
+ {
+ if (Ping(e.Protocol, e.Endpoint, "HELLO") == false)
+ {
+ if (false == removeEntities.ContainsKey(pair.Key))
+ {
+ removeEntities.Add(pair.Key, new List());
+ }
+ removeEntities[pair.Key].Add(e);
+ }
+ }
+ }
+ }
+ finally
+ {
+ _lock.ExitReadLock();
+ }
+ _lock.EnterWriteLock();
+ try
+ {
+ foreach (var pair in removeEntities)
+ {
+ foreach (var ep in pair.Value)
+ {
+ _table[pair.Key].Endpoints.Remove(ep);
+ }
+ }
+ var badKeys = _table.Where(f => f.Value.Endpoints.Count == 0)
+ .Select(pair => pair.Key)
+ .ToList();
+ foreach (var badKey in badKeys)
+ {
+ _table.Remove(badKey);
+ }
+ }
+ finally
+ {
+ _lock.ExitWriteLock();
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Fault heartbeat");
+ }
+ Save();
+ }
+
+ public InvokeResult Append(MicroserviceInfo serviceInfo)
+ {
+ InvokeResult result = null;
+ if (Ping(serviceInfo.Protocol, serviceInfo.Endpoint, serviceInfo.ServiceKey))
+ {
+ var key = $"{serviceInfo.ServiceGroup}:{serviceInfo.ServiceType}:{serviceInfo.ServiceKey.Trim().ToLowerInvariant()}";
+ _lock.EnterWriteLock();
+ try
+ {
+ if (false == _table.ContainsKey(key))
+ {
+ _table.Add(key, new ServiceEndpointsInfo
+ {
+ ServiceKey = serviceInfo.ServiceKey,
+ Version = serviceInfo.Version,
+ ServiceGroup = serviceInfo.ServiceGroup,
+ ServiceType = serviceInfo.ServiceType,
+ Endpoints = new List()
+ });
+ _table[key].Endpoints.Add(new ServiceEndpointInfo
+ {
+ Endpoint = serviceInfo.Endpoint,
+ Protocol = serviceInfo.Protocol
+ });
+ Log.SystemInfo($"The service '{serviceInfo.ServiceKey}' registered on protocol {serviceInfo.Protocol}, endpoint: {serviceInfo.Endpoint}");
+ }
+ else
+ {
+ var exists = _table[key];
+ var endpoint = new ServiceEndpointInfo
+ {
+ Endpoint = serviceInfo.Endpoint,
+ Protocol = serviceInfo.Protocol
+ };
+ if (exists.Endpoints.Contains(endpoint) == false)
+ {
+ Log.Info($"The service '{serviceInfo.ServiceKey}' register endpoint: {serviceInfo.Endpoint} on protocol {serviceInfo.Protocol}");
+ exists.Endpoints.Add(endpoint);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Fault append service ({0} {1}) endpoint '{2}'", serviceInfo.ServiceKey, serviceInfo.Version, serviceInfo.Endpoint);
+ result = InvokeResult.Fault(ex.Message);
+ }
+ finally
+ {
+ _lock.ExitWriteLock();
+ }
+ Save();
+ result = InvokeResult.Succeeding();
+ }
+ else
+ {
+ result = InvokeResult.Fault($"Appending endpoint '{serviceInfo.Endpoint}' canceled for service {serviceInfo.ServiceKey} ({serviceInfo.Version}) because endpoind no avaliable");
+ }
+ return result;
+ }
+
+ public IEnumerable Get()
+ {
+ _lock.EnterReadLock();
+ try
+ {
+ return _table.Values.ToList();
+ }
+ finally
+ {
+ _lock.ExitReadLock();
+ }
+ }
+
+ public void Dispose()
+ {
+ _lock.Dispose();
+ }
+ }
+}
diff --git a/ZeroLevel.Discovery/Startup.cs b/ZeroLevel.Discovery/Startup.cs
new file mode 100644
index 0000000..71915fc
--- /dev/null
+++ b/ZeroLevel.Discovery/Startup.cs
@@ -0,0 +1,79 @@
+using Microsoft.Owin.Hosting;
+using Owin;
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http;
+using System.Web.Http.Controllers;
+using System.Web.Http.Routing;
+
+namespace ZeroLevel.Discovery
+{
+ public class LogRequestAndResponseHandler : DelegatingHandler
+ {
+ protected override async Task SendAsync(
+ HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ // log request body
+ string requestBody = await request.Content.ReadAsStringAsync();
+ Log.Debug(requestBody);
+
+ // let other handlers process the request
+ var result = await base.SendAsync(request, cancellationToken);
+
+ if (result.Content != null)
+ {
+ //(result.Content as ObjectContent).Formatter.MediaTypeMappings.Clear();
+ // once response body is ready, log it
+ var responseBody = await result.Content.ReadAsStringAsync();
+ Log.Debug(responseBody);
+ }
+ return result;
+ }
+ }
+
+ public class EnableInheritRoutingDirectRouteProvider : DefaultDirectRouteProvider
+ {
+ protected override IReadOnlyList GetActionRouteFactories(HttpActionDescriptor actionDescriptor)
+ {
+ // inherit route attributes decorated on base class controller's actions
+ return actionDescriptor.GetCustomAttributes(inherit: true);
+ }
+ }
+
+ public class Startup
+ {
+ // This code configures Web API. The Startup class is specified as a type
+ // parameter in the WebApp.Start method.
+ public void Configuration(IAppBuilder appBuilder)
+ {
+ // Configure Web API for self-host.
+ HttpConfiguration config = new HttpConfiguration();
+ config.MapHttpAttributeRoutes(new EnableInheritRoutingDirectRouteProvider());
+ config.Routes.MapHttpRoute(
+ name: "DefaultApi",
+ routeTemplate: "api/{controller}/{action}/{id}",
+ defaults: new { id = RouteParameter.Optional }
+ );
+ config.EnsureInitialized();
+ config.Formatters.Remove(config.Formatters.XmlFormatter);
+ config.Formatters.Add(config.Formatters.JsonFormatter);
+ //if (_log_request_response)
+ {
+ config.MessageHandlers.Add(new LogRequestAndResponseHandler());
+ }
+ appBuilder.UseWebApi(config);
+ }
+
+ private static bool _log_request_response;
+ public static void StartWebPanel(int port,
+ bool log_request_response)
+ {
+ _log_request_response = log_request_response;
+ string baseAddress = string.Format("http://*:{0}/", port);
+ WebApp.Start(url: baseAddress);
+ Log.Info(string.Format("Web panel url: {0}", baseAddress));
+ }
+ }
+}
diff --git a/ZeroLevel.Discovery/ZeroLevel.Discovery.csproj b/ZeroLevel.Discovery/ZeroLevel.Discovery.csproj
new file mode 100644
index 0000000..299b78e
--- /dev/null
+++ b/ZeroLevel.Discovery/ZeroLevel.Discovery.csproj
@@ -0,0 +1,97 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {4F55B23F-938C-4DA2-B6DC-B6BC66D36073}
+ Exe
+ ZeroLevel.Discovery
+ ZeroLevel.Discovery
+ v4.7.2
+ 512
+ true
+ true
+
+
+ AnyCPU
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ AnyCPU
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+ ..\packages\Microsoft.Owin.4.0.1\lib\net45\Microsoft.Owin.dll
+
+
+ ..\packages\Microsoft.Owin.Host.HttpListener.4.0.1\lib\net45\Microsoft.Owin.Host.HttpListener.dll
+
+
+ ..\packages\Microsoft.Owin.Hosting.4.0.1\lib\net45\Microsoft.Owin.Hosting.dll
+
+
+ ..\packages\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll
+
+
+ ..\packages\Owin.1.0\lib\net40\Owin.dll
+
+
+
+
+ ..\packages\Microsoft.AspNet.WebApi.Client.5.2.7\lib\net45\System.Net.Http.Formatting.dll
+
+
+
+ ..\packages\Microsoft.AspNet.WebApi.Core.5.2.7\lib\net45\System.Web.Http.dll
+
+
+ ..\packages\Microsoft.AspNet.WebApi.Owin.5.2.7\lib\net45\System.Web.Http.Owin.dll
+
+
+ ..\packages\Microsoft.AspNet.WebApi.SelfHost.5.2.7\lib\net45\System.Web.Http.SelfHost.dll
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Component
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {37020d8d-34e8-4ec3-a447-8396d5080457}
+ ZeroLevel
+
+
+
+
\ No newline at end of file
diff --git a/ZeroLevel.Discovery/obj/Debug/TemporaryGeneratedFile_036C0B5B-1481-4323-8D20-8F5ADCB23D92.cs b/ZeroLevel.Discovery/obj/Debug/TemporaryGeneratedFile_036C0B5B-1481-4323-8D20-8F5ADCB23D92.cs
new file mode 100644
index 0000000..e69de29
diff --git a/ZeroLevel.Discovery/obj/Debug/TemporaryGeneratedFile_5937a670-0e60-4077-877b-f7221da3dda1.cs b/ZeroLevel.Discovery/obj/Debug/TemporaryGeneratedFile_5937a670-0e60-4077-877b-f7221da3dda1.cs
new file mode 100644
index 0000000..e69de29
diff --git a/ZeroLevel.Discovery/obj/Debug/TemporaryGeneratedFile_E7A71F73-0F8D-4B9B-B56E-8E70B10BC5D3.cs b/ZeroLevel.Discovery/obj/Debug/TemporaryGeneratedFile_E7A71F73-0F8D-4B9B-B56E-8E70B10BC5D3.cs
new file mode 100644
index 0000000..e69de29
diff --git a/ZeroLevel.Discovery/obj/Debug/ZeroLevel.Discovery.csproj.CoreCompileInputs.cache b/ZeroLevel.Discovery/obj/Debug/ZeroLevel.Discovery.csproj.CoreCompileInputs.cache
new file mode 100644
index 0000000..1d781a8
--- /dev/null
+++ b/ZeroLevel.Discovery/obj/Debug/ZeroLevel.Discovery.csproj.CoreCompileInputs.cache
@@ -0,0 +1 @@
+564f93dfbd5c0e962177d594e0bc0c5a954a167e
diff --git a/ZeroLevel.Discovery/obj/Debug/ZeroLevel.Discovery.csprojAssemblyReference.cache b/ZeroLevel.Discovery/obj/Debug/ZeroLevel.Discovery.csprojAssemblyReference.cache
new file mode 100644
index 0000000..01f2ae0
Binary files /dev/null and b/ZeroLevel.Discovery/obj/Debug/ZeroLevel.Discovery.csprojAssemblyReference.cache differ
diff --git a/ZeroLevel.Discovery/packages.config b/ZeroLevel.Discovery/packages.config
new file mode 100644
index 0000000..b1d1905
--- /dev/null
+++ b/ZeroLevel.Discovery/packages.config
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ZeroLevel.Microservices/BaseProxy.cs b/ZeroLevel.Microservices/BaseProxy.cs
new file mode 100644
index 0000000..9315394
--- /dev/null
+++ b/ZeroLevel.Microservices/BaseProxy.cs
@@ -0,0 +1,158 @@
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Text;
+
+namespace ZeroLevel.ProxyREST
+{
+ public abstract class BaseProxy
+ {
+ private readonly string _baseUrl;
+
+ private Uri BuildRequestUrl(string baseUri, string resource, IDictionary parameters)
+ {
+ if (null == resource) throw new ArgumentNullException("resource");
+ var stringBuilder = new StringBuilder(baseUri);
+ if (baseUri[baseUri.Length - 1] != '/')
+ stringBuilder.Append('/');
+ if (resource[0] != '/')
+ {
+ stringBuilder.Append(resource);
+ }
+ else
+ {
+ stringBuilder.Append(resource.Substring(1));
+ }
+ parameters.
+ Do(list =>
+ {
+ if (list.Count > 0)
+ {
+ stringBuilder.Append("?");
+ foreach (string key in list.Keys)
+ {
+ var val = list[key];
+ if (val == null)
+ {
+ stringBuilder.Append(key);
+ }
+ else
+ {
+ var vtype = val.GetType();
+ if (vtype.IsArray)
+ {
+ if (vtype.GetElementType() == typeof(string))
+ {
+ var arr = (string[])val;
+ stringBuilder.Append(string.Join("&", arr.Select(i => string.Format("{0}[]={1}", key, i))));
+ }
+ else
+ {
+ var arr = (object[])val;
+ stringBuilder.Append(string.Join("&", arr.Select(i => string.Format("{0}[]={1}", key, JsonConvert.SerializeObject(i)))));
+ }
+ }
+ else
+ {
+ if (vtype == typeof(string))
+ {
+ stringBuilder.AppendFormat("{0}={1}", key, val);
+ }
+ else
+ {
+ stringBuilder.AppendFormat("{0}={1}", key, JsonConvert.SerializeObject(val));
+ }
+ }
+ }
+ stringBuilder.Append("&");
+ }
+ }
+ });
+ return new Uri(stringBuilder.ToString().TrimEnd('&'));
+ }
+
+ protected T SendRequest(string resource, string method, object body, IDictionary parameters = null)
+ {
+ string statusCode = null;
+ string reason = null;
+ try
+ {
+ var request = (HttpWebRequest)WebRequest.Create(BuildRequestUrl(_baseUrl, resource, parameters));
+ request.UseDefaultCredentials = true;
+ request.Method = method;
+ request.Proxy = null;
+ request.AutomaticDecompression = DecompressionMethods.GZip;
+ if (body != null)
+ {
+ request.Accept = "application/json";
+ request.ContentType = "application/json";
+ using (var streamWriter = new StreamWriter(request.GetRequestStream()))
+ {
+ streamWriter.Write(JsonConvert.SerializeObject(body));
+ streamWriter.Flush();
+ }
+ }
+ using (var response = (HttpWebResponse)request.GetResponse())
+ {
+ statusCode = response.StatusCode.ToString();
+ reason = response.StatusDescription;
+ if (response.StatusCode == HttpStatusCode.OK)
+ {
+ using (var stream = new StreamReader(response.GetResponseStream()))
+ {
+ string json = stream.ReadToEnd();
+ return JsonConvert.DeserializeObject(json);
+ }
+ }
+ else
+ {
+ throw new Exception("Bad status code");
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ var line = string.Format("Сбой запроса ресурса {0} методом {1}. Код ошибки: {2}. Комментарий: {3}",
+ resource,
+ method,
+ statusCode ?? "Uncknown",
+ reason ?? ex.Message);
+ Log.Error(ex, line);
+ throw new InvalidOperationException(line, ex);
+ }
+ }
+
+ protected T GET(string resource, IDictionary parameters = null)
+ {
+ return SendRequest(resource, "GET", null, parameters);
+ }
+
+ protected T POST(string resource, object body, IDictionary parameters = null)
+ {
+ return SendRequest(resource, "POST", body, parameters);
+ }
+
+ protected T DELETE(string resource, object body, IDictionary parameters = null)
+ {
+ return SendRequest(resource, "DELETE", body, parameters);
+ }
+
+ static BaseProxy()
+ {
+ ServicePointManager.ServerCertificateValidationCallback += (sender, certificate, chain, sslPolicyErrors) => true;
+ ServicePointManager.Expect100Continue = false;
+ ServicePointManager.DefaultConnectionLimit = 8;
+ }
+
+ public BaseProxy(string baseUri)
+ {
+ if (false == baseUri.EndsWith("/"))
+ _baseUrl = baseUri + "/";
+ else
+ _baseUrl = baseUri;
+ }
+ }
+}
diff --git a/ZeroLevel.Microservices/Contracts/IDiscoveryClient.cs b/ZeroLevel.Microservices/Contracts/IDiscoveryClient.cs
new file mode 100644
index 0000000..5470b3b
--- /dev/null
+++ b/ZeroLevel.Microservices/Contracts/IDiscoveryClient.cs
@@ -0,0 +1,15 @@
+using System.Collections.Generic;
+using ZeroLevel.Network.Microservices;
+
+namespace ZeroLevel.Microservices.Contracts
+{
+ public interface IDiscoveryClient
+ {
+ void Register(MicroserviceInfo info);
+ IEnumerable GetServiceEndpoints(string serviceKey);
+ IEnumerable GetServiceEndpointsByGroup(string serviceGroup);
+ IEnumerable GetServiceEndpointsByType(string serviceType);
+
+ ServiceEndpointInfo GetService(string serviceKey, string endpoint);
+ }
+}
diff --git a/ZeroLevel.Microservices/Contracts/IExchangeService.cs b/ZeroLevel.Microservices/Contracts/IExchangeService.cs
new file mode 100644
index 0000000..0f60134
--- /dev/null
+++ b/ZeroLevel.Microservices/Contracts/IExchangeService.cs
@@ -0,0 +1,13 @@
+namespace ZeroLevel.Microservices.Contracts
+{
+ public interface IExchangeService
+ {
+ string Name { get; }
+ string Key { get; }
+ string Endpoint { get; }
+ string Version { get; }
+ string Protocol { get; }
+ string Group { get; }
+ string Type { get; }
+ }
+}
diff --git a/ZeroLevel.Microservices/ExServiceHost.cs b/ZeroLevel.Microservices/ExServiceHost.cs
new file mode 100644
index 0000000..635b517
--- /dev/null
+++ b/ZeroLevel.Microservices/ExServiceHost.cs
@@ -0,0 +1,519 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using ZeroLevel.Microservices.Contracts;
+using ZeroLevel.Network.Microservices;
+using ZeroLevel.Services.Network;
+
+namespace ZeroLevel.Microservices
+{
+ internal sealed class ExServiceHost
+ : IDisposable
+ {
+ private class MetaService
+ {
+ public MicroserviceInfo ServiceInfo { get; set; }
+ public ExService Server { get; set; }
+ }
+ private bool _disposed = false;
+ private readonly long _registerTaskKey = -1;
+ private readonly IDiscoveryClient _discoveryClient;
+ private readonly ConcurrentDictionary _services
+ = new ConcurrentDictionary();
+
+ internal ExServiceHost(IDiscoveryClient client)
+ {
+ _discoveryClient = client;
+ _registerTaskKey = Sheduller.RemindEvery(TimeSpan.FromMilliseconds(50), TimeSpan.FromSeconds(15), RegisterServicesInDiscovery);
+ }
+
+ internal IExService RegisterService(IExchangeService service)
+ {
+ try
+ {
+ if (_disposed) throw new ObjectDisposedException("ExServiceHost");
+ if (service == null) throw new ArgumentNullException(nameof(service));
+ ValidateService(service);
+
+ var key = $"{service.Key}.{service.Protocol}";
+ if (_services.ContainsKey(key))
+ {
+ throw new Exception($"[ExServiceHost] Service {key} already registered");
+ }
+
+ var server = ExchangeTransportFactory.GetServer(service.Protocol);
+ if (false == _services.TryAdd(key, new MetaService
+ {
+ Server = server,
+ ServiceInfo = new MicroserviceInfo
+ {
+ Endpoint = $"{server.Endpoint.Address}:{server.Endpoint.Port}",
+ Protocol = service.Protocol,
+ ServiceKey = service.Key,
+ Version = service.Version,
+ ServiceGroup = service.Group,
+ ServiceType = service.Type
+ }
+ }))
+ {
+ server.Dispose();
+ return null;
+ }
+
+ RegisterServiceInboxes(service);
+
+ return server;
+ }
+ catch (Exception ex)
+ {
+ Log.SystemError(ex, "[ExServiceHost] Fault register service");
+ return null;
+ }
+ }
+
+ internal IExService RegisterService(MicroserviceInfo serviceInfo)
+ {
+ try
+ {
+ if (_disposed) throw new ObjectDisposedException("ExServiceHost");
+ if (serviceInfo == null) throw new ArgumentNullException(nameof(serviceInfo));
+ ValidateService(serviceInfo);
+
+ var key = $"{serviceInfo.ServiceKey}.{serviceInfo.Protocol}";
+ if (_services.ContainsKey(key))
+ {
+ throw new Exception($"[ExServiceHost] Service {key} already registered");
+ }
+
+ var server = ExchangeTransportFactory.GetServer(serviceInfo.Protocol);
+ if (false == _services.TryAdd(key, new MetaService
+ {
+ Server = server,
+ ServiceInfo = new MicroserviceInfo
+ {
+ Endpoint = $"{server.Endpoint.Address}:{server.Endpoint.Port}",
+ Protocol = serviceInfo.Protocol,
+ ServiceKey = serviceInfo.ServiceKey,
+ Version = serviceInfo.Version,
+ ServiceGroup = serviceInfo.ServiceGroup,
+ ServiceType = serviceInfo.ServiceType
+ }
+ }))
+ {
+ server.Dispose();
+ return null;
+ }
+ return server;
+ }
+ catch (Exception ex)
+ {
+ Log.SystemError(ex, "[ExServiceHost] Fault register service");
+ return null;
+ }
+ }
+
+ #region Private methods
+ private void ValidateService(IExchangeService service)
+ {
+ if (string.IsNullOrWhiteSpace(service.Protocol))
+ {
+ throw new ArgumentNullException("Service.Protocol");
+ }
+ if (string.IsNullOrWhiteSpace(service.Key))
+ {
+ throw new ArgumentNullException("Service.Key");
+ }
+ }
+ private void ValidateService(MicroserviceInfo service)
+ {
+ if (string.IsNullOrWhiteSpace(service.Protocol))
+ {
+ throw new ArgumentNullException("Service.Protocol");
+ }
+ if (string.IsNullOrWhiteSpace(service.ServiceKey))
+ {
+ throw new ArgumentNullException("ServiceKey");
+ }
+ }
+
+ private void RegisterServiceInboxes(IExchangeService service)
+ {
+ MethodInfo[] methods = service.
+ GetType().
+ GetMethods(BindingFlags.NonPublic | BindingFlags.Public |
+ BindingFlags.Instance |
+ BindingFlags.FlattenHierarchy |
+ BindingFlags.Instance);
+
+ var registerHandler = this.GetType().GetMethod("RegisterHandler");
+ var registerReplier = this.GetType().GetMethod("RegisterReplier");
+ var registerReplierWithNoRequestBody = this.GetType().GetMethod("RegisterReplierWithNoRequestBody");
+
+ foreach (MethodInfo mi in methods)
+ {
+ try
+ {
+ foreach (Attribute attr in Attribute.GetCustomAttributes(mi, typeof(ExchangeAttribute)))
+ {
+ if (attr.GetType() == typeof(ExchangeMainHandlerAttribute))
+ {
+ var firstArgType = mi.GetParameters().First().ParameterType;
+ MethodInfo genericMethod = registerHandler.MakeGenericMethod(firstArgType);
+ genericMethod.Invoke(this, new object[] { service.Protocol, ZBaseNetwork.DEFAULT_MESSAGE_INBOX, CreateDelegate(mi, service) });
+ }
+ else if (attr.GetType() == typeof(ExchangeHandlerAttribute))
+ {
+ var firstArgType = mi.GetParameters().First().ParameterType;
+ MethodInfo genericMethod = registerHandler.MakeGenericMethod(firstArgType);
+ genericMethod.Invoke(this, new object[] { service.Protocol, (attr as ExchangeHandlerAttribute).Inbox, CreateDelegate(mi, service) });
+ }
+ else if (attr.GetType() == typeof(ExchangeMainReplierAttribute))
+ {
+ var returnType = mi.ReturnType;
+ var firstArgType = mi.GetParameters().First().ParameterType;
+ MethodInfo genericMethod = registerReplier.MakeGenericMethod(firstArgType, returnType);
+ genericMethod.Invoke(this, new object[] { service.Protocol, ZBaseNetwork.DEFAULT_REQUEST_INBOX, CreateDelegate(mi, service) });
+ }
+ else if (attr.GetType() == typeof(ExchangeReplierAttribute))
+ {
+ var returnType = mi.ReturnType;
+ var firstArgType = mi.GetParameters().First().ParameterType;
+ MethodInfo genericMethod = registerReplier.MakeGenericMethod(firstArgType, returnType);
+ genericMethod.Invoke(this, new object[] { service.Protocol, (attr as ExchangeReplierAttribute).Inbox, CreateDelegate(mi, service) });
+ }
+ else if (attr.GetType() == typeof(ExchangeMainReplierWithoutArgAttribute))
+ {
+ var returnType = mi.ReturnType;
+ var firstArgType = mi.GetParameters().First().ParameterType;
+ MethodInfo genericMethod = registerReplierWithNoRequestBody.MakeGenericMethod(returnType);
+ genericMethod.Invoke(this, new object[] { service.Protocol, ZBaseNetwork.DEFAULT_REQUEST_INBOX, CreateDelegate(mi, service) });
+ }
+ else if (attr.GetType() == typeof(ExchangeReplierWithoutArgAttribute))
+ {
+ var returnType = mi.ReturnType;
+ var firstArgType = mi.GetParameters().First().ParameterType;
+ MethodInfo genericMethod = registerReplierWithNoRequestBody.MakeGenericMethod(returnType);
+ genericMethod.Invoke(this, new object[] { service.Protocol, (attr as ExchangeReplierAttribute).Inbox, CreateDelegate(mi, service) });
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Debug($"[ZExchange] Can't register method {mi.Name} as inbox handler. {ex.ToString()}");
+ }
+ }
+ }
+
+ private void RegisterServicesInDiscovery()
+ {
+ var services = _services.
+ Values.
+ Select(s => s.ServiceInfo).
+ ToList();
+ foreach (var service in services)
+ {
+ _discoveryClient.Register(service);
+ }
+ }
+ #endregion
+
+ #region Utils
+ private static Delegate CreateDelegate(MethodInfo methodInfo, object target)
+ {
+ Func getType;
+ var isAction = methodInfo.ReturnType.Equals((typeof(void)));
+ var types = methodInfo.GetParameters().Select(p => p.ParameterType);
+ if (isAction)
+ {
+ getType = Expression.GetActionType;
+ }
+ else
+ {
+ getType = Expression.GetFuncType;
+ types = types.Concat(new[] { methodInfo.ReturnType });
+ }
+ if (methodInfo.IsStatic)
+ {
+ return Delegate.CreateDelegate(getType(types.ToArray()), methodInfo);
+ }
+ return Delegate.CreateDelegate(getType(types.ToArray()), target, methodInfo.Name);
+ }
+ #endregion
+
+ #region Inboxes
+ ///
+ /// Регистрация обработчика входящих сообщений
+ ///
+ /// Тип сообщения
+ /// Транспортный протокол
+ /// Имя точки приема
+ /// Обработчик
+ private void RegisterHandler(MetaService meta, string inbox, Action handler)
+ {
+ if (_disposed) return;
+ try
+ {
+ meta.Server.RegisterInbox(inbox, handler);
+ }
+ catch (Exception ex)
+ {
+ Log.SystemError(ex, $"[Exchange] Register inbox handler error. Protocol '{meta.ServiceInfo.Protocol}'. Inbox '{inbox}'. Service '{meta.ServiceInfo.ServiceKey}'");
+ }
+ }
+ ///
+ /// Регистрация метода отдающего ответ на входящий запрос
+ ///
+ /// Тип входного сообщения
+ /// Тип ответа
+ /// Транспортный протокол
+ /// Имя точки приема
+ /// Обработчик
+ private void RegisterReplier(MetaService meta, string inbox, Func handler)
+ {
+ if (_disposed) return;
+ try
+ {
+ meta.Server.RegisterInbox(inbox, handler);
+ }
+ catch (Exception ex)
+ {
+ Log.SystemError(ex, $"[Exchange] Register inbox replier error. Protocol '{meta.ServiceInfo.Protocol}'. Inbox '{inbox}'. Service '{meta.ServiceInfo.ServiceKey}'");
+ }
+ }
+ ///
+ /// Регистрация метода отдающего ответ на входящий запрос, не принимающего входящих данных
+ ///
+ /// Тип ответа
+ /// Транспортный протокол
+ /// Имя точки приема
+ /// Обработчик
+ private void RegisterReplierWithNoRequestBody(MetaService meta, string inbox, Func handler)
+ {
+ if (_disposed) return;
+ try
+ {
+ meta.Server.RegisterInbox(inbox, handler);
+ }
+ catch (Exception ex)
+ {
+ Log.SystemError(ex, $"[Exchange] Register inbox replier error. Protocol '{meta.ServiceInfo.Protocol}'. Inbox '{inbox}'. Service '{meta.ServiceInfo.ServiceKey}'");
+ }
+ }
+ #endregion
+
+ #region Transport helpers
+ ///
+ /// Call service with round-robin balancing
+ ///
+ /// Service key
+ /// Service call code
+ /// true - service called succesfully
+ internal bool CallService(string serviceKey, Func callHandler)
+ {
+ if (_disposed) return false;
+ List candidates;
+ try
+ {
+ candidates = _discoveryClient.GetServiceEndpoints(serviceKey)?.ToList();
+ }
+ catch (Exception ex)
+ {
+ Log.SystemError(ex, $"[ExServiceHost] Error when trying get endpoints for service key '{serviceKey}'");
+ return false;
+ }
+ if (candidates == null || candidates.Any() == false)
+ {
+ Log.Debug($"[ExServiceHost] Not found endpoints for service key '{serviceKey}'");
+ return false;
+ }
+ var success = false;
+ foreach (var service in candidates)
+ {
+ IExClient transport;
+ try
+ {
+ transport = ExchangeTransportFactory.GetClient(service.Protocol, service.Endpoint);
+ }
+ catch (Exception ex)
+ {
+ Log.SystemError(ex, "[ExServiceHost] Can't get transport for protocol '{0}', service '{1}'", service.Protocol, serviceKey);
+ continue;
+ }
+ try
+ {
+ success = callHandler(service.Endpoint, transport);
+ }
+ catch (Exception ex)
+ {
+ Log.SystemError(ex, $"[ExServiceHost] Error send/request data in service '{serviceKey}'. Endpoint '{service.Endpoint}'");
+ success = false;
+ }
+ if (success)
+ {
+ break;
+ }
+ }
+ return success;
+ }
+
+ internal bool CallServiceDirect(string endpoint, string serviceKey, Func callHandler)
+ {
+ if (_disposed) return false;
+ ServiceEndpointInfo candidate = null;
+ try
+ {
+ candidate = _discoveryClient.GetService(serviceKey, endpoint);
+ }
+ catch (Exception ex)
+ {
+ Log.SystemError(ex, $"[ExServiceHost] Error when trying get service info by key '{serviceKey}' and endpoint '{endpoint}'");
+ return false;
+ }
+ if (candidate == null)
+ {
+ Log.Debug($"[ExServiceHost] Not found service info for key '{serviceKey}' and endpoint '{endpoint}'");
+ return false;
+ }
+ IExClient transport;
+ try
+ {
+ transport = ExchangeTransportFactory.GetClient(candidate.Protocol, candidate.Endpoint);
+ }
+ catch (Exception ex)
+ {
+ Log.SystemError(ex, $"[ExServiceHost] Can't get transport for protocol '{candidate.Protocol}', service '{serviceKey}'");
+ return false;
+ }
+ return callHandler(transport);
+ }
+
+ internal IEnumerable GetClientEnumerator(string serviceKey)
+ {
+ if (!_disposed)
+ {
+ List candidates;
+ try
+ {
+ candidates = _discoveryClient.GetServiceEndpoints(serviceKey)?.ToList();
+ }
+ catch (Exception ex)
+ {
+ Log.SystemError(ex, $"[Exchange] Error when trying get endpoints for service key '{serviceKey}'");
+ candidates = null;
+ }
+ if (candidates != null && candidates.Any())
+ {
+ foreach (var service in candidates)
+ {
+ IExClient transport;
+ try
+ {
+ transport = ExchangeTransportFactory.GetClient(service.Protocol, service.Endpoint);
+ }
+ catch (Exception ex)
+ {
+ Log.SystemError(ex, "[Exchange] Can't get transport for protocol '{0}', endpoint '{1}'", service.Protocol, service.Endpoint);
+ continue;
+ }
+ yield return transport;
+ }
+ }
+ else
+ {
+ Log.Debug($"[Exchange] Not found endpoints for service key '{serviceKey}'");
+ }
+ }
+ }
+
+ internal IEnumerable GetClientEnumeratorByType(string serviceType)
+ {
+ if (!_disposed)
+ {
+ List candidates;
+ try
+ {
+ candidates = _discoveryClient.GetServiceEndpointsByType(serviceType)?.ToList();
+ }
+ catch (Exception ex)
+ {
+ Log.SystemError(ex, $"[Exchange] Error when trying get endpoints for service type '{serviceType}'");
+ candidates = null;
+ }
+ if (candidates != null && candidates.Any())
+ {
+ foreach (var service in candidates)
+ {
+ IExClient transport;
+ try
+ {
+ transport = ExchangeTransportFactory.GetClient(service.Protocol, service.Endpoint);
+ }
+ catch (Exception ex)
+ {
+ Log.SystemError(ex, "[Exchange] Can't get transport for protocol '{0}', endpoint '{1}'", service.Protocol, service.Endpoint);
+ continue;
+ }
+ yield return transport;
+ }
+ }
+ else
+ {
+ Log.Debug($"[Exchange] Not found endpoints for service type '{serviceType}'");
+ }
+ }
+ }
+
+ internal IEnumerable GetClientEnumeratorByGroup(string serviceGroup)
+ {
+ if (!_disposed)
+ {
+ List candidates;
+ try
+ {
+ candidates = _discoveryClient.GetServiceEndpointsByGroup(serviceGroup)?.ToList();
+ }
+ catch (Exception ex)
+ {
+ Log.SystemError(ex, $"[Exchange] Error when trying get endpoints for service group '{serviceGroup}'");
+ candidates = null;
+ }
+ if (candidates != null && candidates.Any())
+ {
+ foreach (var service in candidates)
+ {
+ IExClient transport;
+ try
+ {
+ transport = ExchangeTransportFactory.GetClient(service.Protocol, service.Endpoint);
+ }
+ catch (Exception ex)
+ {
+ Log.SystemError(ex, "[Exchange] Can't get transport for protocol '{0}', endpoint '{1}'", service.Protocol, service.Endpoint);
+ continue;
+ }
+ yield return transport;
+ }
+ }
+ else
+ {
+ Log.Debug($"[Exchange] Not found endpoints for service group '{serviceGroup}'");
+ }
+ }
+ }
+ #endregion
+
+ public void Dispose()
+ {
+ if (_disposed) return;
+ _disposed = true;
+ Sheduller.Remove(_registerTaskKey);
+ foreach (var s in _services)
+ {
+ s.Value.Server.Dispose();
+ }
+ }
+ }
+}
diff --git a/ZeroLevel.Microservices/Exchange.cs b/ZeroLevel.Microservices/Exchange.cs
new file mode 100644
index 0000000..4a90eca
--- /dev/null
+++ b/ZeroLevel.Microservices/Exchange.cs
@@ -0,0 +1,640 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using ZeroLevel.Microservices.Contracts;
+using ZeroLevel.Microservices.Model;
+using ZeroLevel.Network.Microservices;
+using ZeroLevel.Services.Network;
+using ZeroLevel.Services.Serialization;
+
+namespace ZeroLevel.Microservices
+{
+ ///
+ /// Обеспечивает обмен данными между сервисами
+ ///
+ public sealed class Exchange :
+ IDisposable
+ {
+ private readonly IDiscoveryClient _discoveryClient;
+ private readonly ExServiceHost _host;
+
+ #region Ctor
+ public Exchange(IDiscoveryClient discoveryClient)
+ {
+ this._discoveryClient = discoveryClient ?? throw new ArgumentNullException(nameof(discoveryClient));
+ this._host = new ExServiceHost(this._discoveryClient);
+ }
+ #endregion
+
+ ///
+ /// Регистрация сервиса
+ ///
+ public IExService RegisterService(IExchangeService service)
+ {
+ return _host.RegisterService(service);
+ }
+
+ public IExService RegisterService(MicroserviceInfo service)
+ {
+ return _host.RegisterService(service);
+ }
+
+ #region Balanced send
+ ///
+ /// Отправка сообщения сервису
+ ///
+ /// Ключ сервиса
+ /// Имя точки приема сообщений
+ /// Сообщение
+ ///
+ public bool Send(string serviceKey, string inbox, T data)
+ {
+ try
+ {
+ return _host.CallService(serviceKey, (endpoint, transport) => transport.Send(inbox, data).Success);
+ }
+ catch (Exception ex)
+ {
+ Log.SystemError(ex, $"[Exchange] Error send data in service '{serviceKey}'. Inbox '{inbox}'");
+ }
+ return false;
+ }
+
+ public bool Send(string serviceKey, T data) => Send(serviceKey, ZBaseNetwork.DEFAULT_MESSAGE_INBOX, data);
+ #endregion
+
+ #region Balanced request
+ public Tresp Request(string serviceKey, string inbox, Treq data)
+ {
+ Tresp response = default(Tresp);
+ try
+ {
+ if (false == _host.CallService(serviceKey, (endpoint, transport) =>
+ {
+ try
+ {
+ using (var waiter = new ManualResetEventSlim(false))
+ {
+ if (false == transport.Request(inbox, data, resp =>
+ {
+ response = resp;
+ waiter.Set();
+ }).Success)
+ {
+ return false;
+ }
+ if (false == waiter.Wait(ZBaseNetwork.MAX_REQUEST_TIME_MS))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Log.SystemError(ex, $"[Exchange] Error request to service '{serviceKey}'. Inbox '{inbox}'");
+ }
+ return false;
+ }))
+ {
+ Log.SystemWarning($"[Exchange] No responce on request. Service key '{serviceKey}'. Inbox '{inbox}'");
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.SystemError(ex, $"[Exchange] Error request to service '{serviceKey}'. Inbox '{inbox}'");
+ }
+ return response;
+ }
+
+ public Tresp Request(string serviceKey, string inbox)
+ {
+ Tresp response = default(Tresp);
+ try
+ {
+ if (false == _host.CallService(serviceKey, (endpoint, transport) =>
+ {
+ try
+ {
+ using (var waiter = new ManualResetEventSlim(false))
+ {
+ if (false == transport.Request(inbox, resp =>
+ {
+ response = resp;
+ waiter.Set();
+ }).Success)
+ {
+ return false;
+ }
+ if (false == waiter.Wait(ZBaseNetwork.MAX_REQUEST_TIME_MS))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Log.SystemError(ex, $"[Exchange] Error request to service '{serviceKey}'. Inbox '{inbox}'");
+ }
+ return false;
+ }))
+ {
+ Log.SystemWarning($"[Exchange] No responce on request. Service key '{serviceKey}'. Inbox '{inbox}'");
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.SystemError(ex, $"[Exchange] Error request to service '{serviceKey}'. Inbox '{inbox}'");
+ }
+ return response;
+ }
+
+ public Tresp Request(string serviceKey, Treq data) =>
+ Request(serviceKey, ZBaseNetwork.DEFAULT_REQUEST_INBOX, data);
+
+ public Tresp Request(string serviceKey) =>
+ Request(serviceKey, ZBaseNetwork.DEFAULT_REQUEST_INBOX);
+ #endregion
+
+ #region Direct request
+ public Tresp RequestDirect(string endpoint, string serviceKey, string inbox, Treq data)
+ {
+ Tresp response = default(Tresp);
+ try
+ {
+ if (false == _host.CallServiceDirect(endpoint, serviceKey, (transport) =>
+ {
+ try
+ {
+ using (var waiter = new ManualResetEventSlim(false))
+ {
+ if (false == transport.Request(inbox, data, resp =>
+ {
+ response = resp;
+ waiter.Set();
+ }).Success)
+ {
+ return false;
+ }
+ if (false == waiter.Wait(ZBaseNetwork.MAX_REQUEST_TIME_MS))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Log.SystemError(ex, $"[Exchange] Error direct request to '{endpoint}'. Service key '{serviceKey}'. Inbox '{inbox}'");
+ }
+ return false;
+ }))
+ {
+ Log.SystemWarning($"[Exchange] No responce on direct request to '{endpoint}'. Service key '{serviceKey}'. Inbox '{inbox}'");
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.SystemError(ex, $"[Exchange] Error direct request to '{endpoint}'. Service key '{serviceKey}'. Inbox '{inbox}'");
+ }
+ return response;
+ }
+
+ public Tresp RequestDirect(string endpoint, string serviceKey, string inbox)
+ {
+ Tresp response = default(Tresp);
+ try
+ {
+ if (false == _host.CallServiceDirect(endpoint, serviceKey, (transport) =>
+ {
+ try
+ {
+ using (var waiter = new ManualResetEventSlim(false))
+ {
+ if (false == transport.Request(inbox, resp =>
+ {
+ response = resp;
+ waiter.Set();
+ }).Success)
+ {
+ return false;
+ }
+ if (false == waiter.Wait(ZBaseNetwork.MAX_REQUEST_TIME_MS))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Log.SystemError(ex, $"[Exchange] Error direct request to '{endpoint}'. Service key '{serviceKey}'. Inbox '{inbox}'");
+ }
+ return false;
+ }))
+ {
+ Log.SystemWarning($"[Exchange] No responce on direct request to '{endpoint}'. Service key '{serviceKey}'. Inbox '{inbox}'");
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.SystemError(ex, $"[Exchange] Error direct request to service '{serviceKey}'. Inbox '{inbox}'");
+ }
+ return response;
+ }
+
+ public Tresp RequestDirect(string endpoint, string serviceKey, Treq data) =>
+ RequestDirect(endpoint, serviceKey, ZBaseNetwork.DEFAULT_REQUEST_INBOX, data);
+
+ public Tresp RequestDirect(string endpoint, string serviceKey) =>
+ RequestDirect(endpoint, serviceKey, ZBaseNetwork.DEFAULT_REQUEST_INBOX);
+ #endregion
+
+ #region Broadcast
+ ///
+ /// Отправка сообщения всем сервисам с указанным ключом в указанный обработчик
+ ///
+ /// Тип сообщения
+ /// Ключ сервиса
+ /// Имя обработчика
+ /// Сообщение
+ /// true - при успешной отправке
+ public bool SendBroadcast(string serviceKey, string inbox, T data)
+ {
+ try
+ {
+ foreach (var client in _host.GetClientEnumerator(serviceKey))
+ {
+ Task.Run(() =>
+ {
+ try
+ {
+ client.Send(inbox, data);
+ }
+ catch (Exception ex)
+ {
+ Log.SystemError(ex, $"[Exchange] Error broadcast send data to services '{serviceKey}'. Inbox '{inbox}'");
+ }
+ });
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.SystemError(ex, $"[Exchange] Error broadcast send data in service '{serviceKey}'. Inbox '{inbox}'");
+ }
+ return false;
+ }
+ ///
+ /// Отправка сообщения всем сервисам с указанным ключом, в обработчик по умолчанию
+ ///
+ /// Тип сообщения
+ /// Ключ сервиса
+ /// Сообщение
+ /// true - при успешной отправке
+ public bool SendBroadcast(string serviceKey, T data) => SendBroadcast(serviceKey, ZBaseNetwork.DEFAULT_MESSAGE_INBOX, data);
+ ///
+ /// Отправка сообщения всем сервисам конкретного типа в указанный обработчик
+ ///
+ /// Тип сообщения
+ /// Тип сервиса
+ /// Имя обработчика
+ /// Сообщение
+ /// true - при успешной отправке
+ public bool SendBroadcastByType(string serviceType, string inbox, T data)
+ {
+ try
+ {
+ foreach (var client in _host.GetClientEnumeratorByType(serviceType))
+ {
+ Task.Run(() =>
+ {
+ try
+ {
+ client.Send(inbox, data);
+ }
+ catch (Exception ex)
+ {
+ Log.SystemError(ex, $"[Exchange] Error broadcast send data to services with type '{serviceType}'. Inbox '{inbox}'");
+ }
+ });
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.SystemError(ex, $"[Exchange] Error broadcast send data to services with type '{serviceType}'. Inbox '{inbox}'");
+ }
+ return false;
+ }
+ ///
+ /// Отправка сообщения всем сервисам конкретного типа, в обработчик по умолчанию
+ ///
+ /// Тип сообщения
+ /// Тип сервиса
+ /// Сообщение
+ /// true - при успешной отправке
+ public bool SendBroadcastByType(string serviceType, T data) =>
+ SendBroadcastByType(serviceType, ZBaseNetwork.DEFAULT_MESSAGE_INBOX, data);
+ ///
+ /// Отправка сообщения всем сервисам конкретной группы в указанный обработчик
+ ///
+ /// Тип сообщения
+ /// Группа сервиса
+ /// Имя обработчика
+ /// Сообщение
+ /// true - при успешной отправке
+ public bool SendBroadcastByGroup(string serviceGroup, string inbox, T data)
+ {
+ try
+ {
+ foreach (var client in _host.GetClientEnumeratorByGroup(serviceGroup))
+ {
+ Task.Run(() =>
+ {
+ try
+ {
+ client.Send(inbox, data);
+ }
+ catch (Exception ex)
+ {
+ Log.SystemError(ex, $"[Exchange] Error broadcast send data to services with type '{serviceGroup}'. Inbox '{inbox}'");
+ }
+ });
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.SystemError(ex, $"[Exchange] Error broadcast send data to services with type '{serviceGroup}'. Inbox '{inbox}'");
+ }
+ return false;
+ }
+ ///
+ /// Отправка сообщения всем сервисам конкретной группы, в обработчик по умолчанию
+ ///
+ /// Тип сообщения
+ /// Группа сервиса
+ /// Сообщение
+ /// true - при успешной отправке
+ public bool SendBroadcastByGroup(string serviceGroup, T data) =>
+ SendBroadcastByGroup(serviceGroup, ZBaseNetwork.DEFAULT_MESSAGE_INBOX, data);
+ ///
+ /// Широковещательный опрос сервисов по ключу
+ ///
+ /// Тип запроса
+ /// Тип ответа
+ /// Ключ сервиса
+ /// Имя обработчика
+ /// Запрос
+ /// Обработчик ответа
+ /// true - в случае успешной рассылки
+ public IEnumerable RequestBroadcast(string serviceKey, string inbox, Treq data)
+ {
+ try
+ {
+ var clients = _host.GetClientEnumerator(serviceKey).ToList();
+ return _RequestBroadcast(clients, inbox, data);
+ }
+ catch (Exception ex)
+ {
+ Log.SystemError(ex, $"[Exchange] Error broadcast request to service '{serviceKey}'. Inbox '{inbox}'");
+ }
+ return Enumerable.Empty();
+ }
+ ///
+ /// Широковещательный опрос сервисов по ключу, без сообщеня запроса
+ ///
+ /// Тип ответа
+ /// Ключ сервиса
+ /// Имя обработчика
+ /// Обработчик ответа
+ /// true - в случае успешной рассылки
+ public IEnumerable RequestBroadcast(string serviceKey, string inbox)
+ {
+ try
+ {
+ var clients = _host.GetClientEnumerator(serviceKey).ToList();
+ return _RequestBroadcast(clients, inbox);
+ }
+ catch (Exception ex)
+ {
+ Log.SystemError(ex, $"[Exchange] Error broadcast request to service '{serviceKey}'. Inbox '{inbox}'");
+ }
+ return Enumerable.Empty();
+ }
+ ///
+ /// Широковещательный опрос сервисов по ключу, в обработчик по умолчанию
+ ///
+ /// Тип запроса
+ /// Тип ответа
+ /// Ключ сервиса
+ /// Запрос
+ /// Обработчик ответа
+ /// true - в случае успешной рассылки
+ public IEnumerable RequestBroadcast(string serviceKey, Treq data) =>
+ RequestBroadcast(serviceKey, ZBaseNetwork.DEFAULT_REQUEST_INBOX, data);
+ ///
+ /// Широковещательный опрос сервисов по ключу, без сообщеня запроса, в обработчик по умолчанию
+ ///
+ /// Тип ответа
+ /// Ключ сервиса
+ /// Обработчик ответа
+ /// true - в случае успешной рассылки
+ public IEnumerable RequestBroadcast(string serviceKey) =>
+ RequestBroadcast(serviceKey, ZBaseNetwork.DEFAULT_REQUEST_INBOX);
+ ///
+ /// Широковещательный опрос сервисов по типу сервису
+ ///
+ /// Тип запроса
+ /// Тип ответа
+ /// Тип сервиса
+ /// Имя обработчика
+ /// Запрос
+ /// Обработчик ответа
+ /// true - в случае успешной рассылки
+ public IEnumerable RequestBroadcastByType(string serviceType, string inbox, Treq data)
+ {
+ try
+ {
+ var clients = _host.GetClientEnumeratorByType(serviceType).ToList();
+ return _RequestBroadcast(clients, inbox, data);
+ }
+ catch (Exception ex)
+ {
+ Log.SystemError(ex, $"[Exchange] Error broadcast request to service by type '{serviceType}'. Inbox '{inbox}'");
+ }
+ return Enumerable.Empty();
+ }
+ ///
+ /// Широковещательный опрос сервисов по типу сервису, без сообщеня запроса
+ ///
+ /// Тип ответа
+ /// Тип сервиса
+ /// Имя обработчика
+ /// Обработчик ответа
+ /// true - в случае успешной рассылки
+ public IEnumerable RequestBroadcastByType(string serviceType, string inbox)
+ {
+ try
+ {
+ var clients = _host.GetClientEnumeratorByType(serviceType).ToList();
+ return _RequestBroadcast(clients, inbox);
+ }
+ catch (Exception ex)
+ {
+ Log.SystemError(ex, $"[Exchange] Error broadcast request to service by type '{serviceType}'. Inbox '{inbox}'");
+ }
+ return Enumerable.Empty();
+ }
+ ///
+ /// Широковещательный опрос сервисов по типу сервису, в обработчик по умолчанию
+ ///
+ /// Тип запроса
+ /// Тип ответа
+ /// Тип сервиса
+ /// Запрос
+ /// Обработчик ответа
+ /// true - в случае успешной рассылки
+ public IEnumerable RequestBroadcastByType(string serviceType, Treq data) =>
+ RequestBroadcastByType(serviceType, ZBaseNetwork.DEFAULT_REQUEST_INBOX, data);
+ ///
+ /// Широковещательный опрос сервисов по типу, без сообщеня запроса, в обработчик по умолчанию
+ ///
+ /// Тип ответа
+ /// Тип сервиса
+ /// Обработчик ответа
+ /// true - в случае успешной рассылки
+ public IEnumerable RequestBroadcastByType(string serviceType) =>
+ RequestBroadcastByType(serviceType, ZBaseNetwork.DEFAULT_REQUEST_INBOX);
+ ///
+ /// Широковещательный опрос сервисов по группе сервисов
+ ///
+ /// Тип запроса
+ /// Тип ответа
+ /// Группа сервиса
+ /// Имя обработчика
+ /// Запрос
+ /// Обработчик ответа
+ /// true - в случае успешной рассылки
+ public IEnumerable RequestBroadcastByGroup(string serviceGroup, string inbox, Treq data)
+ {
+ try
+ {
+ var clients = _host.GetClientEnumeratorByGroup(serviceGroup).ToList();
+ return _RequestBroadcast(clients, inbox, data);
+ }
+ catch (Exception ex)
+ {
+ Log.SystemError(ex, $"[Exchange] Error broadcast request to service by type '{serviceGroup}'. Inbox '{inbox}'");
+ }
+ return Enumerable.Empty();
+ }
+ ///
+ /// Широковещательный опрос сервисов по группе сервисов, без сообщения запроса
+ ///
+ /// Тип ответа
+ /// Группа сервиса
+ /// Имя обработчика
+ /// Обработчик ответа
+ /// true - в случае успешной рассылки
+ public IEnumerable RequestBroadcastByGroup(string serviceGroup, string inbox)
+ {
+ try
+ {
+ var clients = _host.GetClientEnumeratorByGroup(serviceGroup).ToList();
+ return _RequestBroadcast(clients, inbox);
+ }
+ catch (Exception ex)
+ {
+ Log.SystemError(ex, $"[Exchange] Error broadcast request to service by type '{serviceGroup}'. Inbox '{inbox}'");
+ }
+ return Enumerable.Empty();
+ }
+ ///
+ /// Широковещательный опрос сервисов по группе сервисов в обработчик по умолчанию
+ ///
+ /// Тип запроса
+ /// Тип ответа
+ /// Группа сервиса
+ /// Запрос
+ /// Обработчик ответа
+ /// true - в случае успешной рассылки
+ public IEnumerable RequestBroadcastByGroup(string serviceGroup, Treq data) =>
+ RequestBroadcastByGroup(serviceGroup, ZBaseNetwork.DEFAULT_REQUEST_INBOX, data);
+ ///
+ /// Широковещательный опрос сервисов по группе сервисов, без сообщения запроса, в обработчик по умолчанию
+ ///
+ /// Тип ответа
+ /// Группа сервиса
+ /// Обработчик ответа
+ /// true - в случае успешной рассылки
+ public IEnumerable RequestBroadcastByGroup(string serviceGroup) =>
+ RequestBroadcastByGroup(serviceGroup, ZBaseNetwork.DEFAULT_REQUEST_INBOX);
+
+ #region Private
+ private IEnumerable _RequestBroadcast(List clients, string inbox, Treq data)
+ {
+ var response = new List();
+ using (var waiter = new CountdownEvent(clients.Count))
+ {
+ foreach (var client in clients)
+ {
+ Task.Run(() =>
+ {
+ try
+ {
+ if (false == client.Request(inbox, data, resp => { waiter.Signal(); response.Add(resp); }).Success)
+ {
+ waiter.Signal();
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.SystemError(ex, $"[Exchange] Error direct request to service '{client.Endpoint}' in broadcast request. Inbox '{inbox}'");
+ waiter.Signal();
+ }
+ });
+ }
+ waiter.Wait(ZBaseNetwork.MAX_REQUEST_TIME_MS);
+ }
+ return response;
+ }
+
+ private IEnumerable _RequestBroadcast(List clients, string inbox)
+ {
+ var response = new List();
+ using (var waiter = new CountdownEvent(clients.Count))
+ {
+ foreach (var client in clients)
+ {
+ Task.Run(() =>
+ {
+ try
+ {
+ if (false == client.Request(inbox, resp => { waiter.Signal(); response.Add(resp); }).Success)
+ {
+ waiter.Signal();
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.SystemError(ex, $"[Exchange] Error direct request to service '{client.Endpoint}' in broadcast request. Inbox '{inbox}'");
+ waiter.Signal();
+ }
+ });
+ }
+ waiter.Wait(ZBaseNetwork.MAX_REQUEST_TIME_MS);
+ }
+ return response;
+ }
+ #endregion
+
+ #endregion
+
+ public void Dispose()
+ {
+ this._host.Dispose();
+ }
+ }
+}
diff --git a/ZeroLevel.Microservices/ExchangeTransportFactory.cs b/ZeroLevel.Microservices/ExchangeTransportFactory.cs
new file mode 100644
index 0000000..4015c70
--- /dev/null
+++ b/ZeroLevel.Microservices/ExchangeTransportFactory.cs
@@ -0,0 +1,110 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Reflection;
+using ZeroLevel.Network.Microservices;
+using ZeroLevel.Services.Network;
+using ZeroLevel.Services.Network.Contract;
+
+namespace ZeroLevel.Microservices
+{
+ internal static class ExchangeTransportFactory
+ {
+ private static readonly Dictionary _customServers = new Dictionary();
+ private static readonly Dictionary _customClients = new Dictionary();
+ private static readonly ConcurrentDictionary _clientInstances = new ConcurrentDictionary();
+
+ ///
+ /// Сканирование указанной сборки для поиска типов реализующих интерфейсы
+ /// IExchangeServer или IExchangeClient
+ ///
+ internal static void ScanAndRegisterCustomTransport(Assembly asm)
+ {
+ foreach (var type in asm.GetExportedTypes())
+ {
+ var serverAttr = type.GetCustomAttribute();
+ if (serverAttr != null &&
+ string.IsNullOrWhiteSpace(serverAttr.Protocol) == false &&
+ typeof(IZObservableServer).IsAssignableFrom(type))
+ {
+ _customServers[serverAttr.Protocol] = type;
+ }
+ var clientAttr = type.GetCustomAttribute();
+ if (clientAttr != null &&
+ string.IsNullOrWhiteSpace(clientAttr.Protocol) == false &&
+ typeof(IZTransport).IsAssignableFrom(type))
+ {
+ _customClients[clientAttr.Protocol] = type;
+ }
+ }
+ }
+
+ ///
+ /// Создает сервер для приема сообщений по указанному протоколу
+ ///
+ /// Протокол
+ /// Сервер
+ internal static ExService GetServer(string protocol)
+ {
+ ExService instance = null;
+ if (protocol.Equals("socket", StringComparison.OrdinalIgnoreCase))
+ {
+ instance = new ExService(new ZExSocketObservableServer(new System.Net.IPEndPoint(IPFinder.GetNonLoopbackAddress(), IPFinder.GetFreeTcpPort())));
+ }
+ else
+ {
+ var key = protocol.Trim().ToLowerInvariant();
+ if (_customServers.ContainsKey(key))
+ {
+ instance = new ExService((IZObservableServer)Activator.CreateInstance(_customServers[key]));
+ }
+ }
+ if (instance != null)
+ {
+ return instance;
+ }
+ throw new NotSupportedException($"Protocol {protocol} not supported");
+ }
+
+ ///
+ /// Создает клиента для обращений к серверу по указанному протоколу
+ ///
+ /// Протокол
+ /// Адрес сервера
+ /// Клиент
+ internal static ExClient GetClient(string protocol, string endpoint)
+ {
+ ExClient instance = null;
+ var cachee_key = $"{protocol}:{endpoint}";
+ if (_clientInstances.ContainsKey(cachee_key))
+ {
+ instance = _clientInstances[cachee_key];
+ if (instance.Status == ZTransportStatus.Working)
+ {
+ return instance;
+ }
+ _clientInstances.TryRemove(cachee_key, out instance);
+ instance.Dispose();
+ instance = null;
+ }
+ if (protocol.Equals("socket", StringComparison.OrdinalIgnoreCase))
+ {
+ instance = new ExClient(new ZSocketClient(SocketExtensions.CreateIPEndPoint(endpoint)));
+ }
+ else
+ {
+ var key = protocol.Trim().ToLowerInvariant();
+ if (_customClients.ContainsKey(key))
+ {
+ instance = new ExClient((IZTransport)Activator.CreateInstance(_customClients[key], new object[] { endpoint }));
+ }
+ }
+ if (instance != null)
+ {
+ _clientInstances[cachee_key] = instance;
+ return instance;
+ }
+ throw new NotSupportedException($"Protocol {protocol} not supported");
+ }
+ }
+}
diff --git a/ZeroLevel.Microservices/Model/Checkpoint.cs b/ZeroLevel.Microservices/Model/Checkpoint.cs
new file mode 100644
index 0000000..57f40f8
--- /dev/null
+++ b/ZeroLevel.Microservices/Model/Checkpoint.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Runtime.Serialization;
+using ZeroLevel.Services.Serialization;
+
+namespace ZeroLevel.Microservices.Model
+{
+ [DataContract]
+ public class Checkpoint :
+ ICloneable,
+ IEquatable,
+ IBinarySerializable
+ {
+ public Guid Id { get; set; }
+ public string SourceAppKey { get; set; }
+ public string DestinationAppKey { get; set; }
+ public string ReasonPhrase { get; set; }
+ public long Timestamp { get; set; }
+ public CheckpointType CheckpointType { get; set; }
+ public byte[] Payload { get; set; }
+
+ #region IBinarySerializable
+ public void Deserialize(IBinaryReader reader)
+ {
+ this.Id = reader.ReadGuid();
+ this.Timestamp = reader.ReadLong();
+ this.SourceAppKey = reader.ReadString();
+ this.DestinationAppKey = reader.ReadString();
+ this.ReasonPhrase = reader.ReadString();
+ this.CheckpointType = (CheckpointType)reader.ReadInt32();
+ this.Payload = reader.ReadBytes();
+ }
+
+ public void Serialize(IBinaryWriter writer)
+ {
+ writer.WriteGuid(this.Id);
+ writer.WriteLong(this.Timestamp);
+ writer.WriteString(this.SourceAppKey);
+ writer.WriteString(this.DestinationAppKey);
+ writer.WriteString(this.ReasonPhrase);
+ writer.WriteInt32((int)this.CheckpointType);
+ writer.WriteBytes(this.Payload);
+ }
+ #endregion
+
+ #region Ctors
+ public Checkpoint()
+ {
+ this.Id = Guid.NewGuid();
+ this.Timestamp = DateTime.Now.Ticks;
+ }
+ public Checkpoint(Guid id)
+ {
+ this.Timestamp = DateTime.Now.Ticks;
+ this.Id = id;
+ }
+ public Checkpoint(Checkpoint other)
+ {
+ this.Id = other.Id;
+ this.Timestamp = other.Timestamp;
+ this.SourceAppKey = other.SourceAppKey;
+ this.DestinationAppKey = other.DestinationAppKey;
+ this.CheckpointType = other.CheckpointType;
+ this.Payload = other.Payload;
+ this.ReasonPhrase = other.ReasonPhrase;
+ }
+ #endregion
+
+ #region Equals & Hash
+ public override int GetHashCode()
+ {
+ return base.GetHashCode();
+ }
+
+ public override bool Equals(object obj)
+ {
+ return this.Equals(obj as Checkpoint);
+ }
+ #endregion
+
+ #region ICloneable
+ public object Clone()
+ {
+ return new Checkpoint(this);
+ }
+ #endregion
+
+ #region IEquatable
+ public bool Equals(Checkpoint other)
+ {
+ if (this.Id != other.Id) return false;
+ if (this.Timestamp != other.Timestamp) return false;
+ if (this.CheckpointType != other.CheckpointType) return false;
+ if (string.Compare(this.SourceAppKey, other.SourceAppKey, StringComparison.OrdinalIgnoreCase) != 0) return false;
+ if (string.Compare(this.DestinationAppKey, other.DestinationAppKey, StringComparison.OrdinalIgnoreCase) != 0) return false;
+ if (string.Compare(this.ReasonPhrase, other.ReasonPhrase, StringComparison.OrdinalIgnoreCase) != 0) return false;
+ if (false == ArrayExtensions.Equals(this.Payload, other.Payload)) return false;
+ return true;
+ }
+ #endregion
+ }
+}
diff --git a/ZeroLevel.Microservices/Model/CheckpointArc.cs b/ZeroLevel.Microservices/Model/CheckpointArc.cs
new file mode 100644
index 0000000..21f9d43
--- /dev/null
+++ b/ZeroLevel.Microservices/Model/CheckpointArc.cs
@@ -0,0 +1,21 @@
+using System.Runtime.Serialization;
+
+namespace ZeroLevel.Microservices.Model
+{
+ [DataContract]
+ public sealed class CheckpointArc
+ : Checkpoint
+ {
+ public CheckpointArc(Checkpoint other)
+ {
+ this.Id = other.Id;
+ this.Id = other.Id;
+ this.Timestamp = other.Timestamp;
+ this.SourceAppKey = other.SourceAppKey;
+ this.DestinationAppKey = other.DestinationAppKey;
+ this.CheckpointType = other.CheckpointType;
+ this.Payload = other.Payload;
+ this.ReasonPhrase = other.ReasonPhrase;
+ }
+ }
+}
diff --git a/ZeroLevel.Microservices/Model/CheckpointType.cs b/ZeroLevel.Microservices/Model/CheckpointType.cs
new file mode 100644
index 0000000..7dac225
--- /dev/null
+++ b/ZeroLevel.Microservices/Model/CheckpointType.cs
@@ -0,0 +1,10 @@
+namespace ZeroLevel.Microservices.Model
+{
+ public enum CheckpointType
+ {
+ Interrupt = 0,
+ Fatal = 1,
+ Finish = 2,
+ Transfer = 3
+ }
+}
diff --git a/ZeroLevel.Microservices/Properties/AssemblyInfo.cs b/ZeroLevel.Microservices/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..da29277
--- /dev/null
+++ b/ZeroLevel.Microservices/Properties/AssemblyInfo.cs
@@ -0,0 +1,36 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("ZeroLevel.Microservices")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("ZeroLevel.Microservices")]
+[assembly: AssemblyCopyright("Copyright © 2019")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("6452d91a-2dac-4982-83af-77472051e81b")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/ZeroLevel.Microservices/WebApiDiscoveryClient.cs b/ZeroLevel.Microservices/WebApiDiscoveryClient.cs
new file mode 100644
index 0000000..cbbdb8b
--- /dev/null
+++ b/ZeroLevel.Microservices/WebApiDiscoveryClient.cs
@@ -0,0 +1,179 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using ZeroLevel.Microservices.Contracts;
+using ZeroLevel.Models;
+using ZeroLevel.Network.Microservices;
+using ZeroLevel.ProxyREST;
+using ZeroLevel.Services.Collections;
+
+namespace ZeroLevel.Microservices
+{
+ public sealed class WebApiDiscoveryClient :
+ BaseProxy, IDiscoveryClient
+ {
+ #region WebAPI
+ private IEnumerable GetRecords()
+ {
+ return GET>("api/v0/routes");
+ }
+
+ public InvokeResult Post(MicroserviceInfo info)
+ {
+ return POST("api/v0/routes", info);
+ }
+ #endregion
+
+ // Таблица по ключам
+ private readonly ConcurrentDictionary> _tableByKey =
+ new ConcurrentDictionary>();
+ private readonly ConcurrentDictionary> _tableByGroups =
+ new ConcurrentDictionary>();
+ private readonly ConcurrentDictionary> _tableByTypes =
+ new ConcurrentDictionary>();
+
+ private ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
+
+ public WebApiDiscoveryClient(string url)
+ : base(url)
+ {
+ UpdateServiceListInfo();
+ Sheduller.RemindEvery(TimeSpan.FromSeconds(30), UpdateServiceListInfo);
+ }
+
+ private void UpdateOrAddRecord(string key, ServiceEndpointsInfo info)
+ {
+ var groupName = info.ServiceGroup.ToLowerInvariant();
+ var typeName = info.ServiceType.ToLowerInvariant();
+ if (_tableByKey.ContainsKey(key) == false)
+ {
+ _tableByKey.TryAdd(key, new RoundRobinCollection());
+ }
+ else
+ {
+ _tableByKey[key].Clear();
+ }
+ if (_tableByGroups.ContainsKey(groupName) == false)
+ {
+ _tableByGroups.TryAdd(groupName, new RoundRobinCollection());
+ }
+ if (_tableByTypes.ContainsKey(typeName) == false)
+ {
+ _tableByTypes.TryAdd(typeName, new RoundRobinCollection());
+ }
+ foreach (var e in info.Endpoints)
+ {
+ if (false == _tableByKey[key].Contains(e))
+ {
+ _tableByKey[key].Add(e);
+ _tableByGroups[groupName].Add(e);
+ _tableByTypes[typeName].Add(e);
+ }
+ }
+ }
+
+ private void UpdateServiceListInfo()
+ {
+ IEnumerable records;
+ try
+ {
+ records = GetRecords();
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "[WebApiDiscoveryClient] Update service list error, discrovery service response is absent");
+ return;
+ }
+ if (records == null)
+ {
+ Log.Warning("[WebApiDiscoveryClient] Update service list canceled, discrovery response is empty");
+ return;
+ }
+ _lock.EnterWriteLock();
+ try
+ {
+ _tableByGroups.Clear();
+ _tableByTypes.Clear();
+ var keysToRemove = new List(_tableByKey.Keys);
+ foreach (var info in records)
+ {
+ var key = info.ServiceKey.Trim().ToLowerInvariant();
+ UpdateOrAddRecord(key, info);
+ keysToRemove.Remove(key);
+ }
+ RoundRobinCollection removed;
+ foreach (var key in keysToRemove)
+ {
+ _tableByKey.TryRemove(key, out removed);
+ removed.Dispose();
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "[WebApiDiscoveryClient] Update service list error");
+ }
+ finally
+ {
+ _lock.ExitWriteLock();
+ }
+ }
+
+ public void Register(MicroserviceInfo info)
+ {
+ try
+ {
+ var result = Post(info);
+ if (result.Success == false)
+ {
+ Log.Warning($"[WebApiDiscoveryClient] Service can't register. Discovery reason: {result.Comment}. Comment: {result.Comment}");
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "[WebApiDiscoveryClient] Fault register");
+ }
+ }
+
+ public ServiceEndpointInfo GetService(string serviceKey, string endpoint)
+ {
+ var key = serviceKey.Trim().ToLowerInvariant();
+ if (_tableByKey.ContainsKey(key) && _tableByKey[key].MoveNext())
+ {
+ return _tableByKey[key].Find(s => s.Endpoint.Equals(endpoint, StringComparison.OrdinalIgnoreCase)).FirstOrDefault();
+ }
+ return null;
+ }
+
+ public IEnumerable GetServiceEndpoints(string serviceKey)
+ {
+ var key = serviceKey.Trim().ToLowerInvariant();
+ if (_tableByKey.ContainsKey(key) && _tableByKey[key].MoveNext())
+ {
+ return _tableByKey[key].GetCurrentSeq();
+ }
+ return Enumerable.Empty();
+ }
+
+ public IEnumerable GetServiceEndpointsByGroup(string serviceGroup)
+ {
+ var group = serviceGroup.Trim().ToLowerInvariant();
+ if (_tableByGroups.ContainsKey(group) && _tableByGroups[group].MoveNext())
+ {
+ return _tableByGroups[group].GetCurrentSeq();
+ }
+ return Enumerable.Empty();
+ }
+
+ public IEnumerable GetServiceEndpointsByType(string serviceType)
+ {
+ var type = serviceType.Trim().ToLowerInvariant();
+ if (_tableByTypes.ContainsKey(type) && _tableByTypes[type].MoveNext())
+ {
+ return _tableByTypes[type].GetCurrentSeq();
+ }
+ return Enumerable.Empty();
+ }
+ }
+}
diff --git a/ZeroLevel.Microservices/ZeroLevel.Microservices.csproj b/ZeroLevel.Microservices/ZeroLevel.Microservices.csproj
new file mode 100644
index 0000000..a8f737e
--- /dev/null
+++ b/ZeroLevel.Microservices/ZeroLevel.Microservices.csproj
@@ -0,0 +1,71 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {6452D91A-2DAC-4982-83AF-77472051E81B}
+ Library
+ Properties
+ ZeroLevel.Microservices
+ ZeroLevel.Microservices
+ v4.7.2
+ 512
+ true
+
+
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+ ..\..\..\..\source\repos\ES\packages\Newtonsoft.Json.12.0.1\lib\net45\Newtonsoft.Json.dll
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {37020d8d-34e8-4ec3-a447-8396d5080457}
+ ZeroLevel
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ZeroLevel.Microservices/obj/Debug/TemporaryGeneratedFile_036C0B5B-1481-4323-8D20-8F5ADCB23D92.cs b/ZeroLevel.Microservices/obj/Debug/TemporaryGeneratedFile_036C0B5B-1481-4323-8D20-8F5ADCB23D92.cs
new file mode 100644
index 0000000..e69de29
diff --git a/ZeroLevel.Microservices/obj/Debug/TemporaryGeneratedFile_5937a670-0e60-4077-877b-f7221da3dda1.cs b/ZeroLevel.Microservices/obj/Debug/TemporaryGeneratedFile_5937a670-0e60-4077-877b-f7221da3dda1.cs
new file mode 100644
index 0000000..e69de29
diff --git a/ZeroLevel.Microservices/obj/Debug/TemporaryGeneratedFile_E7A71F73-0F8D-4B9B-B56E-8E70B10BC5D3.cs b/ZeroLevel.Microservices/obj/Debug/TemporaryGeneratedFile_E7A71F73-0F8D-4B9B-B56E-8E70B10BC5D3.cs
new file mode 100644
index 0000000..e69de29
diff --git a/ZeroLevel.Microservices/obj/Debug/ZeroLevel.Microservices.csproj.CoreCompileInputs.cache b/ZeroLevel.Microservices/obj/Debug/ZeroLevel.Microservices.csproj.CoreCompileInputs.cache
new file mode 100644
index 0000000..2a7843d
--- /dev/null
+++ b/ZeroLevel.Microservices/obj/Debug/ZeroLevel.Microservices.csproj.CoreCompileInputs.cache
@@ -0,0 +1 @@
+f044fc552d067d297acec51362b39633b46e17c2
diff --git a/ZeroLevel.Microservices/obj/Debug/ZeroLevel.Microservices.csprojAssemblyReference.cache b/ZeroLevel.Microservices/obj/Debug/ZeroLevel.Microservices.csprojAssemblyReference.cache
new file mode 100644
index 0000000..6a06b16
Binary files /dev/null and b/ZeroLevel.Microservices/obj/Debug/ZeroLevel.Microservices.csprojAssemblyReference.cache differ
diff --git a/ZeroLevel.Microservices/packages.config b/ZeroLevel.Microservices/packages.config
new file mode 100644
index 0000000..97c22dc
--- /dev/null
+++ b/ZeroLevel.Microservices/packages.config
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/ZeroLevel.sln b/ZeroLevel.sln
new file mode 100644
index 0000000..c2f94eb
--- /dev/null
+++ b/ZeroLevel.sln
@@ -0,0 +1,65 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.28307.421
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZeroLevel", "ZeroLevel\ZeroLevel.csproj", "{37020D8D-34E8-4EC3-A447-8396D5080457}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZeroLevel.Microservices", "ZeroLevel.Microservices\ZeroLevel.Microservices.csproj", "{6452D91A-2DAC-4982-83AF-77472051E81B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZeroLevel.Discovery", "ZeroLevel.Discovery\ZeroLevel.Discovery.csproj", "{4F55B23F-938C-4DA2-B6DC-B6BC66D36073}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {37020D8D-34E8-4EC3-A447-8396D5080457}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {37020D8D-34E8-4EC3-A447-8396D5080457}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {37020D8D-34E8-4EC3-A447-8396D5080457}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {37020D8D-34E8-4EC3-A447-8396D5080457}.Debug|x64.Build.0 = Debug|Any CPU
+ {37020D8D-34E8-4EC3-A447-8396D5080457}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {37020D8D-34E8-4EC3-A447-8396D5080457}.Debug|x86.Build.0 = Debug|Any CPU
+ {37020D8D-34E8-4EC3-A447-8396D5080457}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {37020D8D-34E8-4EC3-A447-8396D5080457}.Release|Any CPU.Build.0 = Release|Any CPU
+ {37020D8D-34E8-4EC3-A447-8396D5080457}.Release|x64.ActiveCfg = Release|Any CPU
+ {37020D8D-34E8-4EC3-A447-8396D5080457}.Release|x64.Build.0 = Release|Any CPU
+ {37020D8D-34E8-4EC3-A447-8396D5080457}.Release|x86.ActiveCfg = Release|Any CPU
+ {37020D8D-34E8-4EC3-A447-8396D5080457}.Release|x86.Build.0 = Release|Any CPU
+ {6452D91A-2DAC-4982-83AF-77472051E81B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6452D91A-2DAC-4982-83AF-77472051E81B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6452D91A-2DAC-4982-83AF-77472051E81B}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {6452D91A-2DAC-4982-83AF-77472051E81B}.Debug|x64.Build.0 = Debug|Any CPU
+ {6452D91A-2DAC-4982-83AF-77472051E81B}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {6452D91A-2DAC-4982-83AF-77472051E81B}.Debug|x86.Build.0 = Debug|Any CPU
+ {6452D91A-2DAC-4982-83AF-77472051E81B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6452D91A-2DAC-4982-83AF-77472051E81B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6452D91A-2DAC-4982-83AF-77472051E81B}.Release|x64.ActiveCfg = Release|Any CPU
+ {6452D91A-2DAC-4982-83AF-77472051E81B}.Release|x64.Build.0 = Release|Any CPU
+ {6452D91A-2DAC-4982-83AF-77472051E81B}.Release|x86.ActiveCfg = Release|Any CPU
+ {6452D91A-2DAC-4982-83AF-77472051E81B}.Release|x86.Build.0 = Release|Any CPU
+ {4F55B23F-938C-4DA2-B6DC-B6BC66D36073}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4F55B23F-938C-4DA2-B6DC-B6BC66D36073}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4F55B23F-938C-4DA2-B6DC-B6BC66D36073}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {4F55B23F-938C-4DA2-B6DC-B6BC66D36073}.Debug|x64.Build.0 = Debug|Any CPU
+ {4F55B23F-938C-4DA2-B6DC-B6BC66D36073}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {4F55B23F-938C-4DA2-B6DC-B6BC66D36073}.Debug|x86.Build.0 = Debug|Any CPU
+ {4F55B23F-938C-4DA2-B6DC-B6BC66D36073}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4F55B23F-938C-4DA2-B6DC-B6BC66D36073}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4F55B23F-938C-4DA2-B6DC-B6BC66D36073}.Release|x64.ActiveCfg = Release|Any CPU
+ {4F55B23F-938C-4DA2-B6DC-B6BC66D36073}.Release|x64.Build.0 = Release|Any CPU
+ {4F55B23F-938C-4DA2-B6DC-B6BC66D36073}.Release|x86.ActiveCfg = Release|Any CPU
+ {4F55B23F-938C-4DA2-B6DC-B6BC66D36073}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {0081B29F-FCF8-45FA-A5DF-732CD3ED2E11}
+ EndGlobalSection
+EndGlobal
diff --git a/ZeroLevel/Models/BaseModel.cs b/ZeroLevel/Models/BaseModel.cs
new file mode 100644
index 0000000..f2c8c2e
--- /dev/null
+++ b/ZeroLevel/Models/BaseModel.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace ZeroLevel.Models
+{
+ [DataContract]
+ [Serializable]
+ public abstract class BaseModel
+ {
+ #region Equal
+ public bool Equals(BaseModel other)
+ {
+ if (this == null) // и так бывает
+ throw new NullReferenceException();
+ if (other == null)
+ return false;
+ if (ReferenceEquals(this, other))
+ return true;
+ if (this.GetType() != other.GetType())
+ return false;
+ return true;
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (this == null)
+ throw new NullReferenceException();
+
+ return Equals(obj as BaseModel);
+ }
+
+ public static bool operator ==(BaseModel first, BaseModel second) => Equals(first, second);
+ public static bool operator !=(BaseModel first, BaseModel second) => !Equals(first, second);
+ #endregion
+
+ public abstract override int GetHashCode();
+ public abstract object Clone();
+ }
+}
diff --git a/ZeroLevel/Models/BinaryDocument.cs b/ZeroLevel/Models/BinaryDocument.cs
new file mode 100644
index 0000000..ae76f40
--- /dev/null
+++ b/ZeroLevel/Models/BinaryDocument.cs
@@ -0,0 +1,121 @@
+using System;
+using System.Collections.Generic;
+using ZeroLevel.DocumentObjectModel;
+using ZeroLevel.Services.Serialization;
+
+namespace ZeroLevel.Models
+{
+ ///
+ /// Документ в бинарном представлении
+ ///
+ public class BinaryDocument :
+ IBinarySerializable,
+ IEquatable,
+ ICloneable
+ {
+ ///
+ /// Идентификатор
+ ///
+ public Guid Id { get; set; }
+ ///
+ /// Имя файла
+ ///
+ public string FileName { get; set; }
+ ///
+ /// Тип содержимого (pdf, doc и т.п.)
+ ///
+ public string ContentType { get; set; }
+ ///
+ /// Содержимое
+ ///
+ public byte[] Document { get; set; }
+ ///
+ /// Дата создания
+ ///
+ public DateTime Created { get; set; }
+ ///
+ /// Опциональные заголовки
+ ///
+ public List Headers { get; set; }
+ ///
+ /// Категории
+ ///
+ public List Categories { get; set; }
+
+ #region Ctors
+ public BinaryDocument()
+ {
+ Created = DateTime.Now;
+ Headers = new List();
+ Categories = new List();
+ }
+
+ public BinaryDocument(BinaryDocument other)
+ {
+ var data = MessageSerializer.Serialize(other);
+ using (var reader = new MemoryStreamReader(data))
+ {
+ Deserialize(reader);
+ }
+ }
+ #endregion
+
+ #region IBinarySerializable
+ public void Serialize(IBinaryWriter writer)
+ {
+ writer.WriteGuid(this.Id);
+ writer.WriteString(this.FileName);
+ writer.WriteString(this.ContentType);
+ writer.WriteBytes(this.Document);
+ writer.WriteDateTime(this.Created);
+ writer.WriteCollection(this.Headers);
+ writer.WriteCollection(this.Categories);
+ }
+
+ public void Deserialize(IBinaryReader reader)
+ {
+ this.Id = reader.ReadGuid();
+ this.FileName = reader.ReadString();
+ this.ContentType = reader.ReadString();
+ this.Document = reader.ReadBytes();
+ this.Created = reader.ReadDateTime() ?? DateTime.Now;
+ this.Headers = reader.ReadCollection();
+ this.Categories = reader.ReadCollection();
+ }
+ #endregion
+
+ #region Equals & Hash
+ public override bool Equals(object obj)
+ {
+ return this.Equals(obj as BinaryDocument);
+ }
+
+ public bool Equals(BinaryDocument other)
+ {
+ if (this == null)
+ throw new NullReferenceException();
+ if (other == null) return false;
+ if (ReferenceEquals(this, other)) return true;
+ if (this.GetType() != other.GetType()) return false;
+ if (this.Id != other.Id) return false;
+ if (DateTime.Compare(this.Created, other.Created) != 0) return false;
+ if (ArrayExtensions.UnsafeEquals(this.Document, other.Document) == false) return false;
+ if (string.Compare(this.ContentType, other.ContentType) != 0) return false;
+ if (string.Compare(this.FileName, other.FileName) != 0) return false;
+ if (this.Headers.NoOrderingEquals(other.Headers) == false) return false;
+ if (this.Categories.NoOrderingEquals(other.Categories) == false) return false;
+ return true;
+ }
+
+ public override int GetHashCode()
+ {
+ return Id.GetHashCode();
+ }
+ #endregion
+
+ public object Clone()
+ {
+ return new BinaryDocument(this);
+ }
+ }
+}
diff --git a/ZeroLevel/Models/IEntity.cs b/ZeroLevel/Models/IEntity.cs
new file mode 100644
index 0000000..2c00619
--- /dev/null
+++ b/ZeroLevel/Models/IEntity.cs
@@ -0,0 +1,16 @@
+using System;
+
+namespace ZeroLevel.Models
+{
+ public interface IEntity
+ : ICloneable
+ {
+ Guid Id { get; }
+ }
+
+ public interface IEntity
+ : ICloneable
+ {
+ TKey Id { get; }
+ }
+}
diff --git a/ZeroLevel/Models/InvokeResult.cs b/ZeroLevel/Models/InvokeResult.cs
new file mode 100644
index 0000000..2ec5753
--- /dev/null
+++ b/ZeroLevel/Models/InvokeResult.cs
@@ -0,0 +1,111 @@
+using System;
+using System.Runtime.Serialization;
+using ZeroLevel.Services.Serialization;
+
+namespace ZeroLevel.Models
+{
+ ///
+ /// Результат выполнения действий
+ ///
+ [DataContract]
+ public class InvokeResult :
+ IBinarySerializable
+ {
+ #region Static
+ private static readonly InvokeResult _successResultWitoutComment = new InvokeResult(true, String.Empty);
+ #endregion
+
+ #region Ctor
+ public InvokeResult()
+ {
+ }
+
+ public InvokeResult(bool success, string comment)
+ {
+ Success = success;
+ Comment = comment;
+ }
+ #endregion
+
+ #region Properties
+ ///
+ /// Успех выполнения операции
+ ///
+ [DataMember]
+ public bool Success;
+ ///
+ /// Комментарий (сообщение об ошибке при сбое, или доп. информация)
+ ///
+ [DataMember]
+ public string Comment;
+ #endregion
+
+ #region Fabric methods
+ ///
+ /// Сбой при выполнении плана действий
+ ///
+ public static InvokeResult Fault(string comment) { return new InvokeResult(false, comment); }
+ ///
+ /// Успешное выполнение
+ ///
+ public static InvokeResult Succeeding(string comment = "") { return new InvokeResult(true, comment); }
+ ///
+ /// Успешное выполнение
+ ///
+ public static InvokeResult Succeeding() { return _successResultWitoutComment; }
+ #endregion
+
+ public virtual void Serialize(IBinaryWriter writer)
+ {
+ writer.WriteBoolean(this.Success);
+ writer.WriteString(this.Comment);
+ }
+
+ public virtual void Deserialize(IBinaryReader reader)
+ {
+ this.Success = reader.ReadBoolean();
+ this.Comment = reader.ReadString();
+ }
+ }
+
+ public sealed class InvokeResult :
+ InvokeResult
+ {
+ private T _value;
+ public T Value { get { return _value; } }
+
+ #region Ctor
+ public InvokeResult(bool success, string comment)
+ {
+ Success = success;
+ Comment = comment;
+ }
+
+ public InvokeResult(T value, bool success, string comment)
+ {
+ _value = value;
+ Success = success;
+ Comment = comment;
+ }
+ #endregion
+
+ #region Fabric methods
+ public static InvokeResult Succeeding(T value, string comment = "") { return new InvokeResult(value, true, comment); }
+ public static InvokeResult Fault(string comment) { return new InvokeResult(false, comment); }
+ #endregion
+
+ public override void Serialize(IBinaryWriter writer)
+ {
+ writer.WriteBoolean(this.Success);
+ writer.WriteString(this.Comment);
+ writer.WriteCompatible(this.Value);
+ }
+
+ public override void Deserialize(IBinaryReader reader)
+ {
+ this.Success = reader.ReadBoolean();
+ this.Comment = reader.ReadString();
+ this._value = reader.ReadCompatible();
+ }
+ }
+}
diff --git a/ZeroLevel/Properties/AssemblyInfo.cs b/ZeroLevel/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..9d2c8ed
--- /dev/null
+++ b/ZeroLevel/Properties/AssemblyInfo.cs
@@ -0,0 +1,36 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("ZeroLevel")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("ZeroLevel")]
+[assembly: AssemblyCopyright("Copyright © 2019")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("658210ea-6b29-4c1b-a13c-3bc7edc2770e")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/ZeroLevel/Services/Application/BaseWindowsService.cs b/ZeroLevel/Services/Application/BaseWindowsService.cs
new file mode 100644
index 0000000..ffbd7dd
--- /dev/null
+++ b/ZeroLevel/Services/Application/BaseWindowsService.cs
@@ -0,0 +1,121 @@
+using System;
+using System.ServiceProcess;
+using System.Threading;
+
+namespace ZeroLevel.Services.Applications
+{
+ public abstract class BaseWindowsService
+ : ServiceBase, IZeroService
+ {
+ public string Name { get; }
+
+ public BaseWindowsService()
+ {
+ Name = GetType().Name;
+ }
+
+ public BaseWindowsService(string name)
+ {
+ Name = name;
+ }
+
+ public ZeroServiceState State => _state;
+ private ZeroServiceState _state;
+
+
+ private ManualResetEvent InteraciveModeWorkingFlag = new ManualResetEvent(false);
+
+ public void InteractiveStart(string[] args)
+ {
+ InteraciveModeWorkingFlag.Reset();
+ OnStart(args);
+ while (false == InteraciveModeWorkingFlag.WaitOne(2000))
+ {
+ }
+ }
+
+ #region IZeroService
+ public abstract void StartAction();
+ public abstract void StopAction();
+ public abstract void PauseAction();
+ public abstract void ResumeAction();
+ #endregion
+
+ #region Windows service
+ protected override void OnStart(string[] args)
+ {
+ if (_state == ZeroServiceState.Initialized)
+ {
+ try
+ {
+ StartAction();
+ _state = ZeroServiceState.Started;
+ Log.Debug($"[{Name}] Service started");
+ }
+ catch (Exception ex)
+ {
+ Log.SystemFatal(ex, $"[{Name}] Failed to start service");
+ Stop();
+ }
+ }
+ }
+
+ protected override void OnPause()
+ {
+ if (_state == ZeroServiceState.Started)
+ {
+ try
+ {
+ PauseAction();
+ _state = ZeroServiceState.Paused;
+ Log.Debug($"[{Name}] Service paused");
+ }
+ catch (Exception ex)
+ {
+ Log.SystemFatal(ex, $"[{Name}] Failed to pause service");
+ Stop();
+ }
+ }
+ }
+
+ protected override void OnContinue()
+ {
+ if (_state == ZeroServiceState.Paused)
+ {
+ try
+ {
+ ResumeAction();
+ _state = ZeroServiceState.Started;
+ Log.Debug($"[{Name}] Service continue work after pause");
+ }
+ catch (Exception ex)
+ {
+ Log.SystemFatal(ex, $"[{Name}] Failed to continue work service after pause");
+ Stop();
+ }
+ }
+ }
+
+ protected override void OnStop()
+ {
+ if (_state != ZeroServiceState.Stopped)
+ {
+ _state = ZeroServiceState.Stopped;
+ try
+ {
+ StopAction();
+ Log.Debug($"[{Name}] Service stopped");
+ }
+ catch (Exception ex)
+ {
+ Log.SystemFatal(ex, $"[{Name}] Failed to stop service");
+ }
+ finally
+ {
+ InteraciveModeWorkingFlag?.Set();
+ }
+ }
+ }
+ #endregion
+ }
+}
diff --git a/ZeroLevel/Services/Application/BasicServiceInstaller.cs b/ZeroLevel/Services/Application/BasicServiceInstaller.cs
new file mode 100644
index 0000000..c98a18b
--- /dev/null
+++ b/ZeroLevel/Services/Application/BasicServiceInstaller.cs
@@ -0,0 +1,114 @@
+using System;
+using System.Collections;
+using System.Configuration.Install;
+using System.ServiceProcess;
+
+namespace ZeroLevel.Services.Applications
+{
+ internal static class BasicServiceInstaller
+ {
+ private class InstallOptions
+ {
+ public string ServiceName;
+ public string ServiceDisplayName;
+ public string ServiceDescription;
+ public ServiceStartMode ServiceStartType = ServiceStartMode.Automatic;
+ public ServiceAccount ServiceAccountType = ServiceAccount.LocalSystem;
+ public string ServiceUserName;
+ public string ServiceUserPassword;
+ }
+
+ private static InstallOptions ReadOptions(IConfiguration configuration)
+ {
+ if (configuration == null)
+ {
+ configuration = Configuration.Default;
+ }
+ var options = new InstallOptions();
+ if (configuration.Contains("ServiceDescription"))
+ {
+ options.ServiceDescription = configuration.First("ServiceDescription");
+ }
+ if (configuration.Contains("ServiceName"))
+ {
+ options.ServiceName = configuration.First("ServiceName");
+ }
+ if (configuration.Contains("ServiceDisplayName"))
+ {
+ options.ServiceDisplayName = configuration.First("ServiceDisplayName");
+ }
+ else
+ {
+ options.ServiceDisplayName = options.ServiceName;
+ }
+ if (configuration.Contains("ServiceUserName"))
+ {
+ options.ServiceUserName = configuration.First("ServiceUserName");
+ }
+ if (configuration.Contains("ServiceUserPassword"))
+ {
+ options.ServiceUserPassword = configuration.First("ServiceUserPassword");
+ }
+
+ if (configuration.Contains("ServiceStartType"))
+ {
+ var startType = configuration.First("ServiceStartType");
+ ServiceStartMode mode;
+ if (Enum.TryParse(startType, out mode))
+ {
+ options.ServiceStartType = mode;
+ }
+ else
+ {
+ options.ServiceStartType = ServiceStartMode.Automatic;
+ }
+ }
+ if (configuration.Contains("ServiceAccountType"))
+ {
+ var accountType = configuration.First("ServiceAccountType");
+ ServiceAccount type;
+ if (Enum.TryParse(accountType, out type))
+ {
+ options.ServiceAccountType = type;
+ }
+ else
+ {
+ options.ServiceAccountType = ServiceAccount.LocalService;
+ }
+ }
+ return options;
+ }
+
+ public static void Install(IConfiguration configuration)
+ {
+ CreateInstaller(ReadOptions(configuration)).Install(new Hashtable());
+ }
+
+ public static void Uninstall(IConfiguration configuration)
+ {
+ CreateInstaller(ReadOptions(configuration)).Uninstall(null);
+ }
+
+ private static Installer CreateInstaller(InstallOptions options)
+ {
+ var installer = new TransactedInstaller();
+ installer.Installers.Add(new ServiceInstaller()
+ {
+ ServiceName = options.ServiceName,
+ DisplayName = options.ServiceDisplayName,
+ StartType = options.ServiceStartType,
+ Description = options.ServiceDescription
+ });
+ installer.Installers.Add(new ServiceProcessInstaller
+ {
+ Account = options.ServiceAccountType,
+ Username = (options.ServiceAccountType == ServiceAccount.User) ? options.ServiceUserName : null,
+ Password = (options.ServiceAccountType == ServiceAccount.User) ? options.ServiceUserPassword : null
+ });
+ var installContext = new InstallContext(options.ServiceName + ".install.log", null);
+ installContext.Parameters["assemblypath"] = Configuration.AppLocation;
+ installer.Context = installContext;
+ return installer;
+ }
+ }
+}
diff --git a/ZeroLevel/Services/Application/BusinessApplication.cs b/ZeroLevel/Services/Application/BusinessApplication.cs
new file mode 100644
index 0000000..899fff6
--- /dev/null
+++ b/ZeroLevel/Services/Application/BusinessApplication.cs
@@ -0,0 +1,161 @@
+using System;
+using System.IO;
+using System.Reflection;
+using System.ServiceProcess;
+using ZeroLevel.Services.Applications;
+
+namespace ZeroLevel
+{
+ public class Bootstrap
+ {
+ static Bootstrap()
+ {
+ // Хак, чтобы не переписывать runtime секцию конфига при каждом обновлении Newtonsoft пакета
+ AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
+ }
+
+ private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
+ {
+ try
+ {
+ Log.Debug($"[Bootstrap] Resolve assembly '{args.Name}'");
+ if (args.Name.StartsWith("Newtonsoft.Json", StringComparison.Ordinal))
+ {
+ return Assembly.LoadFile(Path.Combine(Configuration.BaseDirectory, "Newtonsoft.Json.dll"));
+ }
+ else if (args.Name.Equals("Microsoft.Owin", StringComparison.Ordinal))
+ {
+ return Assembly.LoadFile(Path.Combine(Configuration.BaseDirectory, "Microsoft.Owin.dll"));
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"[Bootstrap] Fault load assembly '{args.Name}'");
+ }
+ return null;
+ }
+
+ ///
+ /// Установка приложения в качестве службы
+ ///
+ private static void InstallApplication()
+ {
+ try
+ {
+ Configuration.Save(Configuration.ReadFromApplicationConfig());
+ Log.AddTextFileLogger("install.log");
+ BasicServiceInstaller.Install(Configuration.Default);
+ }
+ catch (Exception ex)
+ {
+ Log.SystemFatal(ex, "[Bootstrap] Fault service install");
+ }
+ }
+ ///
+ /// Удаление приложения из служб
+ ///
+ private static void UninstallApplication()
+ {
+ try
+ {
+ Configuration.Save(Configuration.ReadFromApplicationConfig());
+ Log.AddTextFileLogger("uninstall.log");
+ BasicServiceInstaller.Uninstall(Configuration.Default);
+ }
+ catch (Exception ex)
+ {
+ Log.SystemFatal(ex, "[Bootstrap] Fault service uninstall");
+ }
+ }
+
+ public static void Startup(string[] args, Func preStartConfiguration = null, Func postStartConfiguration = null)
+ where T : IZeroService, new()
+ {
+ var cmd = Configuration.ReadFromCommandLine(args);
+ if (cmd.Contains("install", "setup"))
+ {
+ InstallApplication();
+ }
+ else if (cmd.Contains("uninstall", "remove"))
+ {
+ UninstallApplication();
+ }
+ else
+ {
+ Configuration.Save(Configuration.ReadFromApplicationConfig());
+ Log.CreateLoggingFromConfiguration(Configuration.Default);
+ IZeroService service = null;
+ if (preStartConfiguration != null)
+ {
+ try
+ {
+ if (preStartConfiguration() == false)
+ {
+ Log.SystemInfo("[Bootstrap] Service start canceled, because custom preconfig return false");
+ return;
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.SystemError(ex, "[Bootstrap] Service start canceled, preconfig faulted");
+ return;
+ }
+ }
+ try
+ {
+ service = new T();
+ }
+ catch (Exception ex)
+ {
+ Log.SystemError(ex, "[Bootstrap] Service start canceled, service constructor call fault");
+ }
+ if (postStartConfiguration != null)
+ {
+ try
+ {
+ if (postStartConfiguration() == false)
+ {
+ Log.SystemInfo("[Bootstrap] Service start canceled, because custom postconfig return false");
+ return;
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.SystemError(ex, "[Bootstrap] Service start canceled, postconfig faulted");
+ return;
+ }
+ }
+ // Исключения в процессе работы приложения перехыватываются уровнем ниже
+ if (Environment.UserInteractive)
+ {
+ try
+ {
+ Log.Debug("[Bootstrap] The service starting (interactive mode)");
+ service?.InteractiveStart(args);
+ Log.Debug("[Bootstrap] The service stopped (interactive mode)");
+ }
+ catch (Exception ex)
+ {
+ Log.SystemFatal(ex, "[Bootstrap] The service start in interactive mode was faulted with error");
+ }
+ }
+ else
+ {
+ try
+ {
+ Log.Debug("[Bootstrap] The service starting (windows service)");
+ ServiceBase.Run(new ServiceBase[] { service as ServiceBase });
+ Log.Debug("[Bootstrap] The service stopped (windows service)");
+ }
+ catch (Exception ex)
+ {
+ Log.SystemFatal(ex, "[Bootstrap] The service start was faulted with error");
+ }
+ }
+ }
+ try { Sheduller.Dispose(); } catch { }
+ try { Log.Dispose(); } catch { }
+ try { Injector.Default.Dispose(); Injector.Dispose(); } catch { }
+ }
+ }
+}
diff --git a/ZeroLevel/Services/Application/IZeroService.cs b/ZeroLevel/Services/Application/IZeroService.cs
new file mode 100644
index 0000000..9440799
--- /dev/null
+++ b/ZeroLevel/Services/Application/IZeroService.cs
@@ -0,0 +1,14 @@
+namespace ZeroLevel.Services.Applications
+{
+ public interface IZeroService
+ {
+ ZeroServiceState State { get; }
+
+ void StartAction();
+ void StopAction();
+ void PauseAction();
+ void ResumeAction();
+
+ void InteractiveStart(string[] args);
+ }
+}
diff --git a/ZeroLevel/Services/Application/ZeroServiceState.cs b/ZeroLevel/Services/Application/ZeroServiceState.cs
new file mode 100644
index 0000000..317757c
--- /dev/null
+++ b/ZeroLevel/Services/Application/ZeroServiceState.cs
@@ -0,0 +1,22 @@
+using System;
+
+namespace ZeroLevel.Services.Applications
+{
+ [Flags]
+ public enum ZeroServiceState : int
+ {
+ Initialized = 0,
+ ///
+ /// Сервис работает
+ ///
+ Started = 1,
+ ///
+ /// Работа сервиса приостановлена
+ ///
+ Paused = 2,
+ ///
+ /// Сервис остановлен (ресурсы освобождены)
+ ///
+ Stopped = 3
+ }
+}
diff --git a/ZeroLevel/Services/Async/AsyncConditionVariable.cs b/ZeroLevel/Services/Async/AsyncConditionVariable.cs
new file mode 100644
index 0000000..074e595
--- /dev/null
+++ b/ZeroLevel/Services/Async/AsyncConditionVariable.cs
@@ -0,0 +1,184 @@
+using System;
+using System.Diagnostics;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace ZeroLevel.Services.Async
+{
+ ///
+ /// An async-compatible condition variable. This type uses Mesa-style semantics (the notifying tasks do not yield).
+ ///
+ [DebuggerDisplay("Id = {Id}, AsyncLockId = {_asyncLock.Id}")]
+ [DebuggerTypeProxy(typeof(DebugView))]
+ public sealed class AsyncConditionVariable
+ {
+ ///
+ /// The lock associated with this condition variable.
+ ///
+ private readonly AsyncLock _asyncLock;
+
+ ///
+ /// The queue of waiting tasks.
+ ///
+ private readonly IAsyncWaitQueue