pull/1/head
Ogoun 6 years ago
parent a40feceda2
commit 9dd2d3e020

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
<appSettings>
<add key="console" value="true"/>
<add key="port" value="8885"/>
</appSettings>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Microsoft.Owin" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.1.0" newVersion="4.0.1.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

@ -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<T>(HttpRequestMessage request, Func<T> 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
}
}

@ -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<T>(this HttpRequestMessage request, T val, HttpStatusCode code = HttpStatusCode.OK)
{
var response = request.CreateResponse<T>(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;
}
}
}

@ -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<ServiceEndpointsInfo>))]
public HttpResponseMessage GetRoutes(HttpRequestMessage request)
{
try
{
return request.CreateSelfDestroyingResponse(Injector.Default.Resolve<RouteTable>().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<RouteTable>().Append(service);
return request.CreateSelfDestroyingResponse(ir);
}
catch (Exception ex)
{
Log.Error(ex, "Error with append endpoint");
return BadRequestAnswer(request, ex);
}
}
}
}

@ -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<RouteTable>(new RouteTable());
var port = Configuration.Default.First<int>("port");
Startup.StartWebPanel(port, false);
}
public override void StopAction()
{
}
}
}

@ -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<string, Type> _customServers = new Dictionary<string, Type>();
private static readonly Dictionary<string, Type> _customClients = new Dictionary<string, Type>();
private static readonly ConcurrentDictionary<string, ExClient> _clientInstances = new ConcurrentDictionary<string, ExClient>();
/// <summary>
/// Сканирование указанной сборки для поиска типов реализующих интерфейсы
/// IExchangeServer или IExchangeClient
/// </summary>
internal static void ScanAndRegisterCustomTransport(Assembly asm)
{
foreach (var type in asm.GetExportedTypes())
{
var serverAttr = type.GetCustomAttribute<ExchangeServerAttribute>();
if (serverAttr != null &&
string.IsNullOrWhiteSpace(serverAttr.Protocol) == false &&
typeof(IZObservableServer).IsAssignableFrom(type))
{
_customServers[serverAttr.Protocol] = type;
}
var clientAttr = type.GetCustomAttribute<ExchangeClientAttribute>();
if (clientAttr != null &&
string.IsNullOrWhiteSpace(clientAttr.Protocol) == false &&
typeof(IZTransport).IsAssignableFrom(type))
{
_customClients[clientAttr.Protocol] = type;
}
}
}
/// <summary>
/// Создает сервер для приема сообщений по указанному протоколу
/// </summary>
/// <param name="protocol">Протокол</param>
/// <returns>Сервер</returns>
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");
}
/// <summary>
/// Создает клиента для обращений к серверу по указанному протоколу
/// </summary>
/// <param name="protocol">Протокол</param>
/// <param name="endpoint">Адрес сервера</param>
/// <returns>Клиент</returns>
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");
}
}
}

@ -0,0 +1,10 @@
namespace ZeroLevel.Discovery
{
class Program
{
static void Main(string[] args)
{
Bootstrap.Startup<DiscoveryService>(args);
}
}
}

@ -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")]

@ -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<string, ServiceEndpointsInfo> _table = new Dictionary<string, ServiceEndpointsInfo>();
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<Dictionary<string, ServiceEndpointsInfo>>(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<string, List<ServiceEndpointInfo>>();
_lock.EnterReadLock();
try
{
foreach (var pair in _table)
{
var endpointsToRemove = new List<ServiceEndpointInfo>();
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<ServiceEndpointInfo>());
}
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<ServiceEndpointInfo>()
});
_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<ServiceEndpointsInfo> Get()
{
_lock.EnterReadLock();
try
{
return _table.Values.ToList();
}
finally
{
_lock.ExitReadLock();
}
}
public void Dispose()
{
_lock.Dispose();
}
}
}

@ -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<HttpResponseMessage> 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<IDirectRouteFactory> GetActionRouteFactories(HttpActionDescriptor actionDescriptor)
{
// inherit route attributes decorated on base class controller's actions
return actionDescriptor.GetCustomAttributes<IDirectRouteFactory>(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<Startup>(url: baseAddress);
Log.Info(string.Format("Web panel url: {0}", baseAddress));
}
}
}

@ -0,0 +1,97 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{4F55B23F-938C-4DA2-B6DC-B6BC66D36073}</ProjectGuid>
<OutputType>Exe</OutputType>
<RootNamespace>ZeroLevel.Discovery</RootNamespace>
<AssemblyName>ZeroLevel.Discovery</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.Owin, Version=4.0.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.Owin.4.0.1\lib\net45\Microsoft.Owin.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Owin.Host.HttpListener, Version=4.0.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.Owin.Host.HttpListener.4.0.1\lib\net45\Microsoft.Owin.Host.HttpListener.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Owin.Hosting, Version=4.0.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.Owin.Hosting.4.0.1\lib\net45\Microsoft.Owin.Hosting.dll</HintPath>
</Reference>
<Reference Include="Newtonsoft.Json, Version=6.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\packages\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="Owin, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f0ebd12fd5e55cc5, processorArchitecture=MSIL">
<HintPath>..\packages\Owin.1.0\lib\net40\Owin.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Net.Http.Formatting, Version=5.2.7.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.AspNet.WebApi.Client.5.2.7\lib\net45\System.Net.Http.Formatting.dll</HintPath>
</Reference>
<Reference Include="System.ServiceProcess" />
<Reference Include="System.Web.Http, Version=5.2.7.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.AspNet.WebApi.Core.5.2.7\lib\net45\System.Web.Http.dll</HintPath>
</Reference>
<Reference Include="System.Web.Http.Owin, Version=5.2.7.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.AspNet.WebApi.Owin.5.2.7\lib\net45\System.Web.Http.Owin.dll</HintPath>
</Reference>
<Reference Include="System.Web.Http.SelfHost, Version=5.2.7.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.AspNet.WebApi.SelfHost.5.2.7\lib\net45\System.Web.Http.SelfHost.dll</HintPath>
</Reference>
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Controllers\BaseController.cs" />
<Compile Include="Controllers\HttpRequestMessagesExtensions.cs" />
<Compile Include="Controllers\RoutesController.cs" />
<Compile Include="DiscoveryService.cs">
<SubType>Component</SubType>
</Compile>
<Compile Include="ExchangeTransportFactory.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="RouteTable.cs" />
<Compile Include="Startup.cs" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ZeroLevel\ZeroLevel.csproj">
<Project>{37020d8d-34e8-4ec3-a447-8396d5080457}</Project>
<Name>ZeroLevel</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.AspNet.WebApi.Client" version="5.2.7" targetFramework="net472" />
<package id="Microsoft.AspNet.WebApi.Core" version="5.2.7" targetFramework="net472" />
<package id="Microsoft.AspNet.WebApi.Owin" version="5.2.7" targetFramework="net472" />
<package id="Microsoft.AspNet.WebApi.SelfHost" version="5.2.7" targetFramework="net472" />
<package id="Microsoft.Owin" version="4.0.1" targetFramework="net472" />
<package id="Microsoft.Owin.Host.HttpListener" version="4.0.1" targetFramework="net472" />
<package id="Microsoft.Owin.Hosting" version="4.0.1" targetFramework="net472" />
<package id="Newtonsoft.Json" version="6.0.4" targetFramework="net472" />
<package id="Owin" version="1.0" targetFramework="net472" />
</packages>

@ -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<string, object> 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<T>(string resource, string method, object body, IDictionary<string, object> 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<T>(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<T>(string resource, IDictionary<string, object> parameters = null)
{
return SendRequest<T>(resource, "GET", null, parameters);
}
protected T POST<T>(string resource, object body, IDictionary<string, object> parameters = null)
{
return SendRequest<T>(resource, "POST", body, parameters);
}
protected T DELETE<T>(string resource, object body, IDictionary<string, object> parameters = null)
{
return SendRequest<T>(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;
}
}
}

@ -0,0 +1,15 @@
using System.Collections.Generic;
using ZeroLevel.Network.Microservices;
namespace ZeroLevel.Microservices.Contracts
{
public interface IDiscoveryClient
{
void Register(MicroserviceInfo info);
IEnumerable<ServiceEndpointInfo> GetServiceEndpoints(string serviceKey);
IEnumerable<ServiceEndpointInfo> GetServiceEndpointsByGroup(string serviceGroup);
IEnumerable<ServiceEndpointInfo> GetServiceEndpointsByType(string serviceType);
ServiceEndpointInfo GetService(string serviceKey, string endpoint);
}
}

@ -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; }
}
}

@ -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<string, MetaService> _services
= new ConcurrentDictionary<string, MetaService>();
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<Type[], Type> 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
/// <summary>
/// Регистрация обработчика входящих сообщений
/// </summary>
/// <typeparam name="T">Тип сообщения</typeparam>
/// <param name="protocol">Транспортный протокол</param>
/// <param name="inbox">Имя точки приема</param>
/// <param name="handler">Обработчик</param>
private void RegisterHandler<T>(MetaService meta, string inbox, Action<T, long, IZBackward> 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}'");
}
}
/// <summary>
/// Регистрация метода отдающего ответ на входящий запрос
/// </summary>
/// <typeparam name="Treq">Тип входного сообщения</typeparam>
/// <typeparam name="Tresp">Тип ответа</typeparam>
/// <param name="protocol">Транспортный протокол</param>
/// <param name="inbox">Имя точки приема</param>
/// <param name="replier">Обработчик</param>
private void RegisterReplier<Treq, Tresp>(MetaService meta, string inbox, Func<Treq, long, IZBackward, Tresp> 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}'");
}
}
/// <summary>
/// Регистрация метода отдающего ответ на входящий запрос, не принимающего входящих данных
/// </summary>
/// <typeparam name="Tresp">Тип ответа</typeparam>
/// <param name="protocol">Транспортный протокол</param>
/// <param name="inbox">Имя точки приема</param>
/// <param name="replier">Обработчик</param>
private void RegisterReplierWithNoRequestBody<Tresp>(MetaService meta, string inbox, Func<long, IZBackward, Tresp> 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
/// <summary>
/// Call service with round-robin balancing
/// </summary>
/// <param name="serviceKey">Service key</param>
/// <param name="callHandler">Service call code</param>
/// <returns>true - service called succesfully</returns>
internal bool CallService(string serviceKey, Func<string, IExClient, bool> callHandler)
{
if (_disposed) return false;
List<ServiceEndpointInfo> 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<IExClient, bool> 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<IExClient> GetClientEnumerator(string serviceKey)
{
if (!_disposed)
{
List<ServiceEndpointInfo> 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<IExClient> GetClientEnumeratorByType(string serviceType)
{
if (!_disposed)
{
List<ServiceEndpointInfo> 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<IExClient> GetClientEnumeratorByGroup(string serviceGroup)
{
if (!_disposed)
{
List<ServiceEndpointInfo> 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();
}
}
}
}

@ -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
{
/// <summary>
/// Обеспечивает обмен данными между сервисами
/// </summary>
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
/// <summary>
/// Регистрация сервиса
/// </summary>
public IExService RegisterService(IExchangeService service)
{
return _host.RegisterService(service);
}
public IExService RegisterService(MicroserviceInfo service)
{
return _host.RegisterService(service);
}
#region Balanced send
/// <summary>
/// Отправка сообщения сервису
/// </summary>
/// <param name="serviceKey">Ключ сервиса</param>
/// <param name="inbox">Имя точки приема сообщений</param>
/// <param name="data">Сообщение</param>
/// <returns></returns>
public bool Send<T>(string serviceKey, string inbox, T data)
{
try
{
return _host.CallService(serviceKey, (endpoint, transport) => transport.Send<T>(inbox, data).Success);
}
catch (Exception ex)
{
Log.SystemError(ex, $"[Exchange] Error send data in service '{serviceKey}'. Inbox '{inbox}'");
}
return false;
}
public bool Send<T>(string serviceKey, T data) => Send(serviceKey, ZBaseNetwork.DEFAULT_MESSAGE_INBOX, data);
#endregion
#region Balanced request
public Tresp Request<Treq, Tresp>(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<Treq, Tresp>(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<Tresp>(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<Tresp>(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<Treq, Tresp>(string serviceKey, Treq data) =>
Request<Treq, Tresp>(serviceKey, ZBaseNetwork.DEFAULT_REQUEST_INBOX, data);
public Tresp Request<Tresp>(string serviceKey) =>
Request<Tresp>(serviceKey, ZBaseNetwork.DEFAULT_REQUEST_INBOX);
#endregion
#region Direct request
public Tresp RequestDirect<Treq, Tresp>(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<Treq, Tresp>(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<Tresp>(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<Tresp>(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<Treq, Tresp>(string endpoint, string serviceKey, Treq data) =>
RequestDirect<Treq, Tresp>(endpoint, serviceKey, ZBaseNetwork.DEFAULT_REQUEST_INBOX, data);
public Tresp RequestDirect<Tresp>(string endpoint, string serviceKey) =>
RequestDirect<Tresp>(endpoint, serviceKey, ZBaseNetwork.DEFAULT_REQUEST_INBOX);
#endregion
#region Broadcast
/// <summary>
/// Отправка сообщения всем сервисам с указанным ключом в указанный обработчик
/// </summary>
/// <typeparam name="T">Тип сообщения</typeparam>
/// <param name="serviceKey">Ключ сервиса</param>
/// <param name="inbox">Имя обработчика</param>
/// <param name="data">Сообщение</param>
/// <returns>true - при успешной отправке</returns>
public bool SendBroadcast<T>(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;
}
/// <summary>
/// Отправка сообщения всем сервисам с указанным ключом, в обработчик по умолчанию
/// </summary>
/// <typeparam name="T">Тип сообщения</typeparam>
/// <param name="serviceKey">Ключ сервиса</param>
/// <param name="data">Сообщение</param>
/// <returns>true - при успешной отправке</returns>
public bool SendBroadcast<T>(string serviceKey, T data) => SendBroadcast(serviceKey, ZBaseNetwork.DEFAULT_MESSAGE_INBOX, data);
/// <summary>
/// Отправка сообщения всем сервисам конкретного типа в указанный обработчик
/// </summary>
/// <typeparam name="T">Тип сообщения</typeparam>
/// <param name="serviceType">Тип сервиса</param>
/// <param name="inbox">Имя обработчика</param>
/// <param name="data">Сообщение</param>
/// <returns>true - при успешной отправке</returns>
public bool SendBroadcastByType<T>(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;
}
/// <summary>
/// Отправка сообщения всем сервисам конкретного типа, в обработчик по умолчанию
/// </summary>
/// <typeparam name="T">Тип сообщения</typeparam>
/// <param name="serviceType">Тип сервиса</param>
/// <param name="data">Сообщение</param>
/// <returns>true - при успешной отправке</returns>
public bool SendBroadcastByType<T>(string serviceType, T data) =>
SendBroadcastByType(serviceType, ZBaseNetwork.DEFAULT_MESSAGE_INBOX, data);
/// <summary>
/// Отправка сообщения всем сервисам конкретной группы в указанный обработчик
/// </summary>
/// <typeparam name="T">Тип сообщения</typeparam>
/// <param name="serviceGroup">Группа сервиса</param>
/// <param name="inbox">Имя обработчика</param>
/// <param name="data">Сообщение</param>
/// <returns>true - при успешной отправке</returns>
public bool SendBroadcastByGroup<T>(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;
}
/// <summary>
/// Отправка сообщения всем сервисам конкретной группы, в обработчик по умолчанию
/// </summary>
/// <typeparam name="T">Тип сообщения</typeparam>
/// <param name="serviceGroup">Группа сервиса</param>
/// <param name="data">Сообщение</param>
/// <returns>true - при успешной отправке</returns>
public bool SendBroadcastByGroup<T>(string serviceGroup, T data) =>
SendBroadcastByGroup(serviceGroup, ZBaseNetwork.DEFAULT_MESSAGE_INBOX, data);
/// <summary>
/// Широковещательный опрос сервисов по ключу
/// </summary>
/// <typeparam name="Treq">Тип запроса</typeparam>
/// <typeparam name="Tresp">Тип ответа</typeparam>
/// <param name="serviceKey">Ключ сервиса</param>
/// <param name="inbox">Имя обработчика</param>
/// <param name="data">Запрос</param>
/// <param name="responseHandler">Обработчик ответа</param>
/// <returns>true - в случае успешной рассылки</returns>
public IEnumerable<Tresp> RequestBroadcast<Treq, Tresp>(string serviceKey, string inbox, Treq data)
{
try
{
var clients = _host.GetClientEnumerator(serviceKey).ToList();
return _RequestBroadcast<Treq, Tresp>(clients, inbox, data);
}
catch (Exception ex)
{
Log.SystemError(ex, $"[Exchange] Error broadcast request to service '{serviceKey}'. Inbox '{inbox}'");
}
return Enumerable.Empty<Tresp>();
}
/// <summary>
/// Широковещательный опрос сервисов по ключу, без сообщеня запроса
/// </summary>
/// <typeparam name="Tresp">Тип ответа</typeparam>
/// <param name="serviceKey">Ключ сервиса</param>
/// <param name="inbox">Имя обработчика</param>
/// <param name="responseHandler">Обработчик ответа</param>
/// <returns>true - в случае успешной рассылки</returns>
public IEnumerable<Tresp> RequestBroadcast<Tresp>(string serviceKey, string inbox)
{
try
{
var clients = _host.GetClientEnumerator(serviceKey).ToList();
return _RequestBroadcast<Tresp>(clients, inbox);
}
catch (Exception ex)
{
Log.SystemError(ex, $"[Exchange] Error broadcast request to service '{serviceKey}'. Inbox '{inbox}'");
}
return Enumerable.Empty<Tresp>();
}
/// <summary>
/// Широковещательный опрос сервисов по ключу, в обработчик по умолчанию
/// </summary>
/// <typeparam name="Treq">Тип запроса</typeparam>
/// <typeparam name="Tresp">Тип ответа</typeparam>
/// <param name="serviceKey">Ключ сервиса</param>
/// <param name="data">Запрос</param>
/// <param name="responseHandler">Обработчик ответа</param>
/// <returns>true - в случае успешной рассылки</returns>
public IEnumerable<Tresp> RequestBroadcast<Treq, Tresp>(string serviceKey, Treq data) =>
RequestBroadcast<Treq, Tresp>(serviceKey, ZBaseNetwork.DEFAULT_REQUEST_INBOX, data);
/// <summary>
/// Широковещательный опрос сервисов по ключу, без сообщеня запроса, в обработчик по умолчанию
/// </summary>
/// <typeparam name="Tresp">Тип ответа</typeparam>
/// <param name="serviceKey">Ключ сервиса</param>
/// <param name="responseHandler">Обработчик ответа</param>
/// <returns>true - в случае успешной рассылки</returns>
public IEnumerable<Tresp> RequestBroadcast<Tresp>(string serviceKey) =>
RequestBroadcast<Tresp>(serviceKey, ZBaseNetwork.DEFAULT_REQUEST_INBOX);
/// <summary>
/// Широковещательный опрос сервисов по типу сервису
/// </summary>
/// <typeparam name="Treq">Тип запроса</typeparam>
/// <typeparam name="Tresp">Тип ответа</typeparam>
/// <param name="serviceType">Тип сервиса</param>
/// <param name="inbox">Имя обработчика</param>
/// <param name="data">Запрос</param>
/// <param name="responseHandler">Обработчик ответа</param>
/// <returns>true - в случае успешной рассылки</returns>
public IEnumerable<Tresp> RequestBroadcastByType<Treq, Tresp>(string serviceType, string inbox, Treq data)
{
try
{
var clients = _host.GetClientEnumeratorByType(serviceType).ToList();
return _RequestBroadcast<Treq, Tresp>(clients, inbox, data);
}
catch (Exception ex)
{
Log.SystemError(ex, $"[Exchange] Error broadcast request to service by type '{serviceType}'. Inbox '{inbox}'");
}
return Enumerable.Empty<Tresp>();
}
/// <summary>
/// Широковещательный опрос сервисов по типу сервису, без сообщеня запроса
/// </summary>
/// <typeparam name="Tresp">Тип ответа</typeparam>
/// <param name="serviceType">Тип сервиса</param>
/// <param name="inbox">Имя обработчика</param>
/// <param name="responseHandler">Обработчик ответа</param>
/// <returns>true - в случае успешной рассылки</returns>
public IEnumerable<Tresp> RequestBroadcastByType<Tresp>(string serviceType, string inbox)
{
try
{
var clients = _host.GetClientEnumeratorByType(serviceType).ToList();
return _RequestBroadcast<Tresp>(clients, inbox);
}
catch (Exception ex)
{
Log.SystemError(ex, $"[Exchange] Error broadcast request to service by type '{serviceType}'. Inbox '{inbox}'");
}
return Enumerable.Empty<Tresp>();
}
/// <summary>
/// Широковещательный опрос сервисов по типу сервису, в обработчик по умолчанию
/// </summary>
/// <typeparam name="Treq">Тип запроса</typeparam>
/// <typeparam name="Tresp">Тип ответа</typeparam>
/// <param name="serviceType">Тип сервиса</param>
/// <param name="data">Запрос</param>
/// <param name="responseHandler">Обработчик ответа</param>
/// <returns>true - в случае успешной рассылки</returns>
public IEnumerable<Tresp> RequestBroadcastByType<Treq, Tresp>(string serviceType, Treq data) =>
RequestBroadcastByType<Treq, Tresp>(serviceType, ZBaseNetwork.DEFAULT_REQUEST_INBOX, data);
/// <summary>
/// Широковещательный опрос сервисов по типу, без сообщеня запроса, в обработчик по умолчанию
/// </summary>
/// <typeparam name="Tresp">Тип ответа</typeparam>
/// <param name="serviceType">Тип сервиса</param>
/// <param name="responseHandler">Обработчик ответа</param>
/// <returns>true - в случае успешной рассылки</returns>
public IEnumerable<Tresp> RequestBroadcastByType<Tresp>(string serviceType) =>
RequestBroadcastByType<Tresp>(serviceType, ZBaseNetwork.DEFAULT_REQUEST_INBOX);
/// <summary>
/// Широковещательный опрос сервисов по группе сервисов
/// </summary>
/// <typeparam name="Treq">Тип запроса</typeparam>
/// <typeparam name="Tresp">Тип ответа</typeparam>
/// <param name="serviceGroup">Группа сервиса</param>
/// <param name="inbox">Имя обработчика</param>
/// <param name="data">Запрос</param>
/// <param name="responseHandler">Обработчик ответа</param>
/// <returns>true - в случае успешной рассылки</returns>
public IEnumerable<Tresp> RequestBroadcastByGroup<Treq, Tresp>(string serviceGroup, string inbox, Treq data)
{
try
{
var clients = _host.GetClientEnumeratorByGroup(serviceGroup).ToList();
return _RequestBroadcast<Treq, Tresp>(clients, inbox, data);
}
catch (Exception ex)
{
Log.SystemError(ex, $"[Exchange] Error broadcast request to service by type '{serviceGroup}'. Inbox '{inbox}'");
}
return Enumerable.Empty<Tresp>();
}
/// <summary>
/// Широковещательный опрос сервисов по группе сервисов, без сообщения запроса
/// </summary>
/// <typeparam name="Tresp">Тип ответа</typeparam>
/// <param name="serviceGroup">Группа сервиса</param>
/// <param name="inbox">Имя обработчика</param>
/// <param name="responseHandler">Обработчик ответа</param>
/// <returns>true - в случае успешной рассылки</returns>
public IEnumerable<Tresp> RequestBroadcastByGroup<Tresp>(string serviceGroup, string inbox)
{
try
{
var clients = _host.GetClientEnumeratorByGroup(serviceGroup).ToList();
return _RequestBroadcast<Tresp>(clients, inbox);
}
catch (Exception ex)
{
Log.SystemError(ex, $"[Exchange] Error broadcast request to service by type '{serviceGroup}'. Inbox '{inbox}'");
}
return Enumerable.Empty<Tresp>();
}
/// <summary>
/// Широковещательный опрос сервисов по группе сервисов в обработчик по умолчанию
/// </summary>
/// <typeparam name="Treq">Тип запроса</typeparam>
/// <typeparam name="Tresp">Тип ответа</typeparam>
/// <param name="serviceGroup">Группа сервиса</param>
/// <param name="data">Запрос</param>
/// <param name="responseHandler">Обработчик ответа</param>
/// <returns>true - в случае успешной рассылки</returns>
public IEnumerable<Tresp> RequestBroadcastByGroup<Treq, Tresp>(string serviceGroup, Treq data) =>
RequestBroadcastByGroup<Treq, Tresp>(serviceGroup, ZBaseNetwork.DEFAULT_REQUEST_INBOX, data);
/// <summary>
/// Широковещательный опрос сервисов по группе сервисов, без сообщения запроса, в обработчик по умолчанию
/// </summary>
/// <typeparam name="Tresp">Тип ответа</typeparam>
/// <param name="serviceGroup">Группа сервиса</param>
/// <param name="responseHandler">Обработчик ответа</param>
/// <returns>true - в случае успешной рассылки</returns>
public IEnumerable<Tresp> RequestBroadcastByGroup<Tresp>(string serviceGroup) =>
RequestBroadcastByGroup<Tresp>(serviceGroup, ZBaseNetwork.DEFAULT_REQUEST_INBOX);
#region Private
private IEnumerable<Tresp> _RequestBroadcast<Treq, Tresp>(List<IExClient> clients, string inbox, Treq data)
{
var response = new List<Tresp>();
using (var waiter = new CountdownEvent(clients.Count))
{
foreach (var client in clients)
{
Task.Run(() =>
{
try
{
if (false == client.Request<Treq, Tresp>(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<Tresp> _RequestBroadcast<Tresp>(List<IExClient> clients, string inbox)
{
var response = new List<Tresp>();
using (var waiter = new CountdownEvent(clients.Count))
{
foreach (var client in clients)
{
Task.Run(() =>
{
try
{
if (false == client.Request<Tresp>(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();
}
}
}

@ -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<string, Type> _customServers = new Dictionary<string, Type>();
private static readonly Dictionary<string, Type> _customClients = new Dictionary<string, Type>();
private static readonly ConcurrentDictionary<string, ExClient> _clientInstances = new ConcurrentDictionary<string, ExClient>();
/// <summary>
/// Сканирование указанной сборки для поиска типов реализующих интерфейсы
/// IExchangeServer или IExchangeClient
/// </summary>
internal static void ScanAndRegisterCustomTransport(Assembly asm)
{
foreach (var type in asm.GetExportedTypes())
{
var serverAttr = type.GetCustomAttribute<ExchangeServerAttribute>();
if (serverAttr != null &&
string.IsNullOrWhiteSpace(serverAttr.Protocol) == false &&
typeof(IZObservableServer).IsAssignableFrom(type))
{
_customServers[serverAttr.Protocol] = type;
}
var clientAttr = type.GetCustomAttribute<ExchangeClientAttribute>();
if (clientAttr != null &&
string.IsNullOrWhiteSpace(clientAttr.Protocol) == false &&
typeof(IZTransport).IsAssignableFrom(type))
{
_customClients[clientAttr.Protocol] = type;
}
}
}
/// <summary>
/// Создает сервер для приема сообщений по указанному протоколу
/// </summary>
/// <param name="protocol">Протокол</param>
/// <returns>Сервер</returns>
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");
}
/// <summary>
/// Создает клиента для обращений к серверу по указанному протоколу
/// </summary>
/// <param name="protocol">Протокол</param>
/// <param name="endpoint">Адрес сервера</param>
/// <returns>Клиент</returns>
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");
}
}
}

@ -0,0 +1,101 @@
using System;
using System.Runtime.Serialization;
using ZeroLevel.Services.Serialization;
namespace ZeroLevel.Microservices.Model
{
[DataContract]
public class Checkpoint :
ICloneable,
IEquatable<Checkpoint>,
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
}
}

@ -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;
}
}
}

@ -0,0 +1,10 @@
namespace ZeroLevel.Microservices.Model
{
public enum CheckpointType
{
Interrupt = 0,
Fatal = 1,
Finish = 2,
Transfer = 3
}
}

@ -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")]

@ -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<ServiceEndpointsInfo> GetRecords()
{
return GET<IEnumerable<ServiceEndpointsInfo>>("api/v0/routes");
}
public InvokeResult Post(MicroserviceInfo info)
{
return POST<InvokeResult>("api/v0/routes", info);
}
#endregion
// Таблица по ключам
private readonly ConcurrentDictionary<string, RoundRobinCollection<ServiceEndpointInfo>> _tableByKey =
new ConcurrentDictionary<string, RoundRobinCollection<ServiceEndpointInfo>>();
private readonly ConcurrentDictionary<string, RoundRobinCollection<ServiceEndpointInfo>> _tableByGroups =
new ConcurrentDictionary<string, RoundRobinCollection<ServiceEndpointInfo>>();
private readonly ConcurrentDictionary<string, RoundRobinCollection<ServiceEndpointInfo>> _tableByTypes =
new ConcurrentDictionary<string, RoundRobinCollection<ServiceEndpointInfo>>();
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<ServiceEndpointInfo>());
}
else
{
_tableByKey[key].Clear();
}
if (_tableByGroups.ContainsKey(groupName) == false)
{
_tableByGroups.TryAdd(groupName, new RoundRobinCollection<ServiceEndpointInfo>());
}
if (_tableByTypes.ContainsKey(typeName) == false)
{
_tableByTypes.TryAdd(typeName, new RoundRobinCollection<ServiceEndpointInfo>());
}
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<ServiceEndpointsInfo> 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<string>(_tableByKey.Keys);
foreach (var info in records)
{
var key = info.ServiceKey.Trim().ToLowerInvariant();
UpdateOrAddRecord(key, info);
keysToRemove.Remove(key);
}
RoundRobinCollection<ServiceEndpointInfo> 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<ServiceEndpointInfo> GetServiceEndpoints(string serviceKey)
{
var key = serviceKey.Trim().ToLowerInvariant();
if (_tableByKey.ContainsKey(key) && _tableByKey[key].MoveNext())
{
return _tableByKey[key].GetCurrentSeq();
}
return Enumerable.Empty<ServiceEndpointInfo>();
}
public IEnumerable<ServiceEndpointInfo> GetServiceEndpointsByGroup(string serviceGroup)
{
var group = serviceGroup.Trim().ToLowerInvariant();
if (_tableByGroups.ContainsKey(group) && _tableByGroups[group].MoveNext())
{
return _tableByGroups[group].GetCurrentSeq();
}
return Enumerable.Empty<ServiceEndpointInfo>();
}
public IEnumerable<ServiceEndpointInfo> GetServiceEndpointsByType(string serviceType)
{
var type = serviceType.Trim().ToLowerInvariant();
if (_tableByTypes.ContainsKey(type) && _tableByTypes[type].MoveNext())
{
return _tableByTypes[type].GetCurrentSeq();
}
return Enumerable.Empty<ServiceEndpointInfo>();
}
}
}

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{6452D91A-2DAC-4982-83AF-77472051E81B}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>ZeroLevel.Microservices</RootNamespace>
<AssemblyName>ZeroLevel.Microservices</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\..\..\..\source\repos\ES\packages\Newtonsoft.Json.12.0.1\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Runtime.Serialization" />
<Reference Include="System.ServiceProcess" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="BaseProxy.cs" />
<Compile Include="Contracts\IDiscoveryClient.cs" />
<Compile Include="Contracts\IExchangeService.cs" />
<Compile Include="Exchange.cs" />
<Compile Include="ExchangeTransportFactory.cs" />
<Compile Include="ExServiceHost.cs" />
<Compile Include="Model\Checkpoint.cs" />
<Compile Include="Model\CheckpointArc.cs" />
<Compile Include="Model\CheckpointType.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="WebApiDiscoveryClient.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ZeroLevel\ZeroLevel.csproj">
<Project>{37020d8d-34e8-4ec3-a447-8396d5080457}</Project>
<Name>ZeroLevel</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Newtonsoft.Json" version="12.0.1" targetFramework="net472" />
</packages>

@ -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

@ -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();
}
}

@ -0,0 +1,121 @@
using System;
using System.Collections.Generic;
using ZeroLevel.DocumentObjectModel;
using ZeroLevel.Services.Serialization;
namespace ZeroLevel.Models
{
/// <summary>
/// Документ в бинарном представлении
/// </summary>
public class BinaryDocument :
IBinarySerializable,
IEquatable<BinaryDocument>,
ICloneable
{
/// <summary>
/// Идентификатор
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// Имя файла
/// </summary>
public string FileName { get; set; }
/// <summary>
/// Тип содержимого (pdf, doc и т.п.)
/// </summary>
public string ContentType { get; set; }
/// <summary>
/// Содержимое
/// </summary>
public byte[] Document { get; set; }
/// <summary>
/// Дата создания
/// </summary>
public DateTime Created { get; set; }
/// <summary>
/// Опциональные заголовки
/// </summary>
public List<Header> Headers { get; set; }
/// <summary>
/// Категории
/// </summary>
public List<Category> Categories { get; set; }
#region Ctors
public BinaryDocument()
{
Created = DateTime.Now;
Headers = new List<Header>();
Categories = new List<Category>();
}
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<Header>();
this.Categories = reader.ReadCollection<Category>();
}
#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);
}
}
}

@ -0,0 +1,16 @@
using System;
namespace ZeroLevel.Models
{
public interface IEntity
: ICloneable
{
Guid Id { get; }
}
public interface IEntity<TKey>
: ICloneable
{
TKey Id { get; }
}
}

@ -0,0 +1,111 @@
using System;
using System.Runtime.Serialization;
using ZeroLevel.Services.Serialization;
namespace ZeroLevel.Models
{
/// <summary>
/// Результат выполнения действий
/// </summary>
[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
/// <summary>
/// Успех выполнения операции
/// </summary>
[DataMember]
public bool Success;
/// <summary>
/// Комментарий (сообщение об ошибке при сбое, или доп. информация)
/// </summary>
[DataMember]
public string Comment;
#endregion
#region Fabric methods
/// <summary>
/// Сбой при выполнении плана действий
/// </summary>
public static InvokeResult Fault(string comment) { return new InvokeResult(false, comment); }
/// <summary>
/// Успешное выполнение
/// </summary>
public static InvokeResult Succeeding(string comment = "") { return new InvokeResult(true, comment); }
/// <summary>
/// Успешное выполнение
/// </summary>
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<T> :
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<T> Succeeding(T value, string comment = "") { return new InvokeResult<T>(value, true, comment); }
public static InvokeResult<T> Fault<T>(string comment) { return new InvokeResult<T>(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<T>();
}
}
}

@ -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")]

@ -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
}
}

@ -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;
}
}
}

@ -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;
}
/// <summary>
/// Установка приложения в качестве службы
/// </summary>
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");
}
}
/// <summary>
/// Удаление приложения из служб
/// </summary>
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<T>(string[] args, Func<bool> preStartConfiguration = null, Func<bool> 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 { }
}
}
}

@ -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);
}
}

@ -0,0 +1,22 @@
using System;
namespace ZeroLevel.Services.Applications
{
[Flags]
public enum ZeroServiceState : int
{
Initialized = 0,
/// <summary>
/// Сервис работает
/// </summary>
Started = 1,
/// <summary>
/// Работа сервиса приостановлена
/// </summary>
Paused = 2,
/// <summary>
/// Сервис остановлен (ресурсы освобождены)
/// </summary>
Stopped = 3
}
}

@ -0,0 +1,184 @@
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace ZeroLevel.Services.Async
{
/// <summary>
/// An async-compatible condition variable. This type uses Mesa-style semantics (the notifying tasks do not yield).
/// </summary>
[DebuggerDisplay("Id = {Id}, AsyncLockId = {_asyncLock.Id}")]
[DebuggerTypeProxy(typeof(DebugView))]
public sealed class AsyncConditionVariable
{
/// <summary>
/// The lock associated with this condition variable.
/// </summary>
private readonly AsyncLock _asyncLock;
/// <summary>
/// The queue of waiting tasks.
/// </summary>
private readonly IAsyncWaitQueue<object> _queue;
/// <summary>
/// The semi-unique identifier for this instance. This is 0 if the id has not yet been created.
/// </summary>
private int _id;
/// <summary>
/// The object used for mutual exclusion.
/// </summary>
private readonly object _mutex;
/// <summary>
/// Creates an async-compatible condition variable associated with an async-compatible lock.
/// </summary>
/// <param name="asyncLock">The lock associated with this condition variable.</param>
/// <param name="queue">The wait queue used to manage waiters.</param>
public AsyncConditionVariable(AsyncLock asyncLock, IAsyncWaitQueue<object> queue)
{
_asyncLock = asyncLock;
_queue = queue;
_mutex = new object();
}
/// <summary>
/// Creates an async-compatible condition variable associated with an async-compatible lock.
/// </summary>
/// <param name="asyncLock">The lock associated with this condition variable.</param>
public AsyncConditionVariable(AsyncLock asyncLock)
: this(asyncLock, new DefaultAsyncWaitQueue<object>())
{
}
/// <summary>
/// Gets a semi-unique identifier for this asynchronous condition variable.
/// </summary>
public int Id
{
get { return IdManager<AsyncConditionVariable>.GetId(ref _id); }
}
/// <summary>
/// Sends a signal to a single task waiting on this condition variable. The associated lock MUST be held when calling this method, and it will still be held when this method returns.
/// </summary>
public void Notify()
{
IDisposable finish = null;
lock (_mutex)
{
if (!_queue.IsEmpty)
finish = _queue.Dequeue();
}
if (finish != null)
finish.Dispose();
}
/// <summary>
/// Sends a signal to all tasks waiting on this condition variable. The associated lock MUST be held when calling this method, and it will still be held when this method returns.
/// </summary>
public void NotifyAll()
{
IDisposable finish;
lock (_mutex)
{
finish = _queue.DequeueAll();
}
finish.Dispose();
}
/// <summary>
/// Asynchronously waits for a signal on this condition variable. The associated lock MUST be held when calling this method, and it will still be held when this method returns, even if the method is cancelled.
/// </summary>
/// <param name="cancellationToken">The cancellation signal used to cancel this wait.</param>
public Task WaitAsync(CancellationToken cancellationToken)
{
lock (_mutex)
{
// Begin waiting for either a signal or cancellation.
var task = _queue.Enqueue(cancellationToken);
// Attach to the signal or cancellation.
var retTcs = new TaskCompletionSource();
task.ContinueWith(async t =>
{
// Re-take the lock.
await _asyncLock.LockAsync().ConfigureAwait(false);
// Propagate the cancellation exception if necessary.
retTcs.TryCompleteFromCompletedTask(t);
}, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
var ret = retTcs.Task;
// Release the lock while we are waiting.
_asyncLock.ReleaseLock();
return ret;
}
}
/// <summary>
/// Synchronously waits for a signal on this condition variable. This method may block the calling thread. The associated lock MUST be held when calling this method, and it will still be held when this method returns, even if the method is cancelled.
/// </summary>
/// <param name="cancellationToken">The cancellation signal used to cancel this wait.</param>
public void Wait(CancellationToken cancellationToken)
{
Task enqueuedTask;
lock (_mutex)
{
// Begin waiting for either a signal or cancellation.
enqueuedTask = _queue.Enqueue(cancellationToken);
}
// Release the lock while we are waiting.
_asyncLock.ReleaseLock();
// Wait for the signal or cancellation.
enqueuedTask.WaitWithoutException();
// Re-take the lock.
_asyncLock.Lock();
// Propagate the cancellation exception if necessary.
enqueuedTask.WaitAndUnwrapException();
}
/// <summary>
/// Asynchronously waits for a signal on this condition variable. The associated lock MUST be held when calling this method, and it will still be held when this method returns.
/// </summary>
public Task WaitAsync()
{
return WaitAsync(CancellationToken.None);
}
/// <summary>
/// Synchronously waits for a signal on this condition variable. This method may block the calling thread. The associated lock MUST be held when calling this method, and it will still be held when this method returns.
/// </summary>
public void Wait()
{
Wait(CancellationToken.None);
}
// ReSharper disable UnusedMember.Local
[DebuggerNonUserCode]
private sealed class DebugView
{
private readonly AsyncConditionVariable _cv;
public DebugView(AsyncConditionVariable cv)
{
_cv = cv;
}
public int Id { get { return _cv.Id; } }
public AsyncLock AsyncLock { get { return _cv._asyncLock; } }
public IAsyncWaitQueue<object> WaitQueue { get { return _cv._queue; } }
}
// ReSharper restore UnusedMember.Local
}
}

@ -0,0 +1,38 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ZeroLevel.Services.Async
{
public static class AsyncHelper
{
private static readonly TaskFactory _taskFactory = new
TaskFactory(CancellationToken.None,
TaskCreationOptions.None,
TaskContinuationOptions.None,
TaskScheduler.Default);
public static TResult RunSync<TResult>(Func<Task<TResult>> func)
=> _taskFactory
.StartNew(func)
.Unwrap()
.GetAwaiter()
.GetResult();
public static void RunSync(Func<Task> func)
=> _taskFactory
.StartNew(func)
.Unwrap()
.GetAwaiter()
.GetResult();
public async static Task<T> WithTimeout<T>(this Task<T> task, int duration)
{
var retTask = await Task.WhenAny(task, Task.Delay(duration))
.ConfigureAwait(false);
if (retTask is Task<T>) return task.Result;
return default(T);
}
}
}

@ -0,0 +1,191 @@
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace ZeroLevel.Services.Async
{
/// <summary>
/// A mutual exclusion lock that is compatible with async. Note that this lock is <b>not</b> recursive!
/// </summary>
[DebuggerDisplay("Id = {Id}, Taken = {_taken}")]
[DebuggerTypeProxy(typeof(DebugView))]
public sealed class AsyncLock
{
/// <summary>
/// Whether the lock is taken by a task.
/// </summary>
private bool _taken;
/// <summary>
/// The queue of TCSs that other tasks are awaiting to acquire the lock.
/// </summary>
private readonly IAsyncWaitQueue<IDisposable> _queue;
/// <summary>
/// The semi-unique identifier for this instance. This is 0 if the id has not yet been created.
/// </summary>
private int _id;
/// <summary>
/// The object used for mutual exclusion.
/// </summary>
private readonly object _mutex;
/// <summary>
/// Creates a new async-compatible mutual exclusion lock.
/// </summary>
public AsyncLock()
: this(new DefaultAsyncWaitQueue<IDisposable>())
{
}
/// <summary>
/// Creates a new async-compatible mutual exclusion lock using the specified wait queue.
/// </summary>
/// <param name="queue">The wait queue used to manage waiters.</param>
public AsyncLock(IAsyncWaitQueue<IDisposable> queue)
{
_queue = queue;
_mutex = new object();
}
/// <summary>
/// Gets a semi-unique identifier for this asynchronous lock.
/// </summary>
public int Id
{
get { return IdManager<AsyncLock>.GetId(ref _id); }
}
/// <summary>
/// Asynchronously acquires the lock. Returns a disposable that releases the lock when disposed.
/// </summary>
/// <param name="cancellationToken">The cancellation token used to cancel the lock. If this is already set, then this method will attempt to take the lock immediately (succeeding if the lock is currently available).</param>
/// <returns>A disposable that releases the lock when disposed.</returns>
public Task<IDisposable> LockAsync(CancellationToken cancellationToken)
{
Task<IDisposable> ret;
lock (_mutex)
{
if (!_taken)
{
// If the lock is available, take it immediately.
_taken = true;
ret = TaskShim.FromResult<IDisposable>(new Key(this));
}
else
{
// Wait for the lock to become available or cancellation.
ret = _queue.Enqueue(cancellationToken);
}
}
return ret;
}
/// <summary>
/// Synchronously acquires the lock. Returns a disposable that releases the lock when disposed. This method may block the calling thread.
/// </summary>
/// <param name="cancellationToken">The cancellation token used to cancel the lock. If this is already set, then this method will attempt to take the lock immediately (succeeding if the lock is currently available).</param>
public IDisposable Lock(CancellationToken cancellationToken)
{
Task<IDisposable> enqueuedTask;
lock (_mutex)
{
if (!_taken)
{
_taken = true;
return new Key(this);
}
enqueuedTask = _queue.Enqueue(cancellationToken);
}
return enqueuedTask.WaitAndUnwrapException();
}
/// <summary>
/// Asynchronously acquires the lock. Returns a disposable that releases the lock when disposed.
/// </summary>
/// <returns>A disposable that releases the lock when disposed.</returns>
public Task<IDisposable> LockAsync()
{
return LockAsync(CancellationToken.None);
}
/// <summary>
/// Synchronously acquires the lock. Returns a disposable that releases the lock when disposed. This method may block the calling thread.
/// </summary>
public IDisposable Lock()
{
return Lock(CancellationToken.None);
}
/// <summary>
/// Releases the lock.
/// </summary>
internal void ReleaseLock()
{
IDisposable finish = null;
lock (_mutex)
{
if (_queue.IsEmpty)
_taken = false;
else
finish = _queue.Dequeue(new Key(this));
}
if (finish != null)
finish.Dispose();
}
/// <summary>
/// The disposable which releases the lock.
/// </summary>
private sealed class Key : IDisposable
{
/// <summary>
/// The lock to release.
/// </summary>
private AsyncLock _asyncLock;
/// <summary>
/// Creates the key for a lock.
/// </summary>
/// <param name="asyncLock">The lock to release. May not be <c>null</c>.</param>
public Key(AsyncLock asyncLock)
{
_asyncLock = asyncLock;
}
/// <summary>
/// Release the lock.
/// </summary>
public void Dispose()
{
if (_asyncLock == null)
return;
_asyncLock.ReleaseLock();
_asyncLock = null;
}
}
// ReSharper disable UnusedMember.Local
[DebuggerNonUserCode]
private sealed class DebugView
{
private readonly AsyncLock _mutex;
public DebugView(AsyncLock mutex)
{
_mutex = mutex;
}
public int Id { get { return _mutex.Id; } }
public bool Taken { get { return _mutex._taken; } }
public IAsyncWaitQueue<IDisposable> WaitQueue { get { return _mutex._queue; } }
}
// ReSharper restore UnusedMember.Local
}
}

@ -0,0 +1,159 @@
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace ZeroLevel.Services.Async
{
/// <summary>
/// An async-compatible manual-reset event.
/// </summary>
[DebuggerDisplay("Id = {Id}, IsSet = {GetStateForDebugger}")]
[DebuggerTypeProxy(typeof(DebugView))]
public sealed class AsyncManualResetEvent
{
/// <summary>
/// The object used for synchronization.
/// </summary>
private readonly object _mutex;
/// <summary>
/// The current state of the event.
/// </summary>
private TaskCompletionSource<object> _tcs;
/// <summary>
/// The semi-unique identifier for this instance. This is 0 if the id has not yet been created.
/// </summary>
private int _id;
[DebuggerNonUserCode]
private bool GetStateForDebugger
{
get
{
return _tcs.Task.IsCompleted;
}
}
/// <summary>
/// Creates an async-compatible manual-reset event.
/// </summary>
/// <param name="set">Whether the manual-reset event is initially set or unset.</param>
public AsyncManualResetEvent(bool set)
{
_mutex = new object();
_tcs = TaskCompletionSourceExtensions.CreateAsyncTaskSource<object>();
if (set)
_tcs.TrySetResult(null);
}
/// <summary>
/// Creates an async-compatible manual-reset event that is initially unset.
/// </summary>
public AsyncManualResetEvent()
: this(false)
{
}
/// <summary>
/// Gets a semi-unique identifier for this asynchronous manual-reset event.
/// </summary>
public int Id
{
get { return IdManager<AsyncManualResetEvent>.GetId(ref _id); }
}
/// <summary>
/// Whether this event is currently set. This member is seldom used; code using this member has a high possibility of race conditions.
/// </summary>
public bool IsSet
{
get { lock (_mutex) return _tcs.Task.IsCompleted; }
}
/// <summary>
/// Asynchronously waits for this event to be set.
/// </summary>
public Task WaitAsync()
{
lock (_mutex)
{
return _tcs.Task;
}
}
/// <summary>
/// Asynchronously waits for this event to be set or for the wait to be canceled.
/// </summary>
/// <param name="cancellationToken">The cancellation token used to cancel the wait. If this token is already canceled, this method will first check whether the event is set.</param>
public Task WaitAsync(CancellationToken cancellationToken)
{
var waitTask = WaitAsync();
if (waitTask.IsCompleted)
return waitTask;
return waitTask.WaitAsync(cancellationToken);
}
/// <summary>
/// Synchronously waits for this event to be set. This method may block the calling thread.
/// </summary>
public void Wait()
{
WaitAsync().WaitAndUnwrapException();
}
/// <summary>
/// Synchronously waits for this event to be set. This method may block the calling thread.
/// </summary>
/// <param name="cancellationToken">The cancellation token used to cancel the wait. If this token is already canceled, this method will first check whether the event is set.</param>
public void Wait(CancellationToken cancellationToken)
{
var ret = WaitAsync();
if (ret.IsCompleted)
return;
ret.WaitAndUnwrapException(cancellationToken);
}
/// <summary>
/// Sets the event, atomically completing every task returned by <see cref="O:Nito.AsyncEx.AsyncManualResetEvent.WaitAsync"/>. If the event is already set, this method does nothing.
/// </summary>
public void Set()
{
lock (_mutex)
{
_tcs.TrySetResult(null);
}
}
/// <summary>
/// Resets the event. If the event is already reset, this method does nothing.
/// </summary>
public void Reset()
{
lock (_mutex)
{
if (_tcs.Task.IsCompleted)
_tcs = TaskCompletionSourceExtensions.CreateAsyncTaskSource<object>();
}
}
// ReSharper disable UnusedMember.Local
[DebuggerNonUserCode]
private sealed class DebugView
{
private readonly AsyncManualResetEvent _mre;
public DebugView(AsyncManualResetEvent mre)
{
_mre = mre;
}
public int Id { get { return _mre.Id; } }
public bool IsSet { get { return _mre.GetStateForDebugger; } }
public Task CurrentTask { get { return _mre._tcs.Task; } }
}
// ReSharper restore UnusedMember.Local
}
}

@ -0,0 +1,135 @@
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace ZeroLevel.Services.Async
{
/// <summary>
/// An async-compatible monitor.
/// </summary>
[DebuggerDisplay("Id = {Id}, ConditionVariableId = {_conditionVariable.Id}")]
public sealed class AsyncMonitor
{
/// <summary>
/// The lock.
/// </summary>
private readonly AsyncLock _asyncLock;
/// <summary>
/// The condition variable.
/// </summary>
private readonly AsyncConditionVariable _conditionVariable;
/// <summary>
/// Constructs a new monitor.
/// </summary>
public AsyncMonitor(IAsyncWaitQueue<IDisposable> lockQueue, IAsyncWaitQueue<object> conditionVariableQueue)
{
_asyncLock = new AsyncLock(lockQueue);
_conditionVariable = new AsyncConditionVariable(_asyncLock, conditionVariableQueue);
}
/// <summary>
/// Constructs a new monitor.
/// </summary>
public AsyncMonitor()
: this(new DefaultAsyncWaitQueue<IDisposable>(), new DefaultAsyncWaitQueue<object>())
{
}
/// <summary>
/// Gets a semi-unique identifier for this monitor.
/// </summary>
public int Id
{
get { return _asyncLock.Id; }
}
/// <summary>
/// Asynchronously enters the monitor. Returns a disposable that leaves the monitor when disposed.
/// </summary>
/// <param name="cancellationToken">The cancellation token used to cancel the enter. If this is already set, then this method will attempt to enter the monitor immediately (succeeding if the monitor is currently available).</param>
/// <returns>A disposable that leaves the monitor when disposed.</returns>
public Task<IDisposable> EnterAsync(CancellationToken cancellationToken)
{
return _asyncLock.LockAsync(cancellationToken);
}
/// <summary>
/// Synchronously enters the monitor. Returns a disposable that leaves the monitor when disposed. This method may block the calling thread.
/// </summary>
/// <param name="cancellationToken">The cancellation token used to cancel the enter. If this is already set, then this method will attempt to enter the monitor immediately (succeeding if the monitor is currently available).</param>
public IDisposable Enter(CancellationToken cancellationToken)
{
return _asyncLock.Lock(cancellationToken);
}
/// <summary>
/// Asynchronously enters the monitor. Returns a disposable that leaves the monitor when disposed.
/// </summary>
/// <returns>A disposable that leaves the monitor when disposed.</returns>
public Task<IDisposable> EnterAsync()
{
return EnterAsync(CancellationToken.None);
}
/// <summary>
/// Asynchronously enters the monitor. Returns a disposable that leaves the monitor when disposed. This method may block the calling thread.
/// </summary>
public IDisposable Enter()
{
return Enter(CancellationToken.None);
}
/// <summary>
/// Asynchronously waits for a pulse signal on this monitor. The monitor MUST already be entered when calling this method, and it will still be entered when this method returns, even if the method is cancelled. This method internally will leave the monitor while waiting for a notification.
/// </summary>
/// <param name="cancellationToken">The cancellation signal used to cancel this wait.</param>
public Task WaitAsync(CancellationToken cancellationToken)
{
return _conditionVariable.WaitAsync(cancellationToken);
}
/// <summary>
/// Asynchronously waits for a pulse signal on this monitor. This method may block the calling thread. The monitor MUST already be entered when calling this method, and it will still be entered when this method returns, even if the method is cancelled. This method internally will leave the monitor while waiting for a notification.
/// </summary>
/// <param name="cancellationToken">The cancellation signal used to cancel this wait.</param>
public void Wait(CancellationToken cancellationToken)
{
_conditionVariable.Wait(cancellationToken);
}
/// <summary>
/// Asynchronously waits for a pulse signal on this monitor. The monitor MUST already be entered when calling this method, and it will still be entered when this method returns. This method internally will leave the monitor while waiting for a notification.
/// </summary>
public Task WaitAsync()
{
return WaitAsync(CancellationToken.None);
}
/// <summary>
/// Asynchronously waits for a pulse signal on this monitor. This method may block the calling thread. The monitor MUST already be entered when calling this method, and it will still be entered when this method returns. This method internally will leave the monitor while waiting for a notification.
/// </summary>
public void Wait()
{
Wait(CancellationToken.None);
}
/// <summary>
/// Sends a signal to a single task waiting on this monitor. The monitor MUST already be entered when calling this method, and it will still be entered when this method returns.
/// </summary>
public void Pulse()
{
_conditionVariable.Notify();
}
/// <summary>
/// Sends a signal to all tasks waiting on this monitor. The monitor MUST already be entered when calling this method, and it will still be entered when this method returns.
/// </summary>
public void PulseAll()
{
_conditionVariable.NotifyAll();
}
}
}

@ -0,0 +1,737 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace ZeroLevel.Services.Async
{
/// <summary>
/// An async-compatible producer/consumer queue.
/// </summary>
/// <typeparam name="T">The type of elements contained in the queue.</typeparam>
[DebuggerDisplay("Count = {_queue.Count}, MaxCount = {_maxCount}")]
[DebuggerTypeProxy(typeof(AsyncProducerConsumerQueue<>.DebugView))]
public sealed class AsyncProducerConsumerQueue<T> : IDisposable
{
/// <summary>
/// The underlying queue.
/// </summary>
private readonly Queue<T> _queue;
/// <summary>
/// The maximum number of elements allowed in the queue.
/// </summary>
private readonly int _maxCount;
/// <summary>
/// The mutual-exclusion lock protecting the queue.
/// </summary>
private readonly AsyncLock _mutex;
/// <summary>
/// A condition variable that is signalled when the queue is not full.
/// </summary>
private readonly AsyncConditionVariable _notFull;
/// <summary>
/// A condition variable that is signalled when the queue is completed or not empty.
/// </summary>
private readonly AsyncConditionVariable _completedOrNotEmpty;
/// <summary>
/// A cancellation token source that is canceled when the queue is marked completed for adding.
/// </summary>
private readonly CancellationTokenSource _completed;
/// <summary>
/// A cached result that is common when calling <see cref="o:AsyncProducerConsumerQueueExtensions.TryDequeueFromAnyAsync"/>.
/// </summary>
internal static readonly DequeueResult FalseResult = new DequeueResult(null, default(T));
/// <summary>
/// Creates a new async-compatible producer/consumer queue with the specified initial elements and a maximum element count.
/// </summary>
/// <param name="collection">The initial elements to place in the queue.</param>
/// <param name="maxCount">The maximum element count. This must be greater than zero.</param>
public AsyncProducerConsumerQueue(IEnumerable<T> collection, int maxCount)
{
if (maxCount <= 0)
throw new ArgumentOutOfRangeException("maxCount", "The maximum count must be greater than zero.");
_queue = collection == null ? new Queue<T>() : new Queue<T>(collection);
if (maxCount < _queue.Count)
throw new ArgumentException("The maximum count cannot be less than the number of elements in the collection.", "maxCount");
_maxCount = maxCount;
_mutex = new AsyncLock();
_notFull = new AsyncConditionVariable(_mutex);
_completedOrNotEmpty = new AsyncConditionVariable(_mutex);
_completed = new CancellationTokenSource();
}
/// <summary>
/// Creates a new async-compatible producer/consumer queue with the specified initial elements.
/// </summary>
/// <param name="collection">The initial elements to place in the queue.</param>
public AsyncProducerConsumerQueue(IEnumerable<T> collection)
: this(collection, int.MaxValue)
{
}
/// <summary>
/// Creates a new async-compatible producer/consumer queue with a maximum element count.
/// </summary>
/// <param name="maxCount">The maximum element count. This must be greater than zero.</param>
public AsyncProducerConsumerQueue(int maxCount)
: this(null, maxCount)
{
}
/// <summary>
/// Creates a new async-compatible producer/consumer queue.
/// </summary>
public AsyncProducerConsumerQueue()
: this(null, int.MaxValue)
{
}
/// <summary>
/// Whether the queue is empty.
/// </summary>
private bool Empty { get { return _queue.Count == 0; } }
/// <summary>
/// Whether the queue is full.
/// </summary>
private bool Full { get { return _queue.Count == _maxCount; } }
/// <summary>
/// Releases resources held by this instance. After disposal, any use of this instance is undefined.
/// </summary>
public void Dispose()
{
_completed.Dispose();
}
/// <summary>
/// Asynchronously marks the producer/consumer queue as complete for adding.
/// </summary>
[Obsolete("Use CompleteAdding() instead.")]
public async Task CompleteAddingAsync()
{
using (await _mutex.LockAsync().ConfigureAwait(false))
{
if (_completed.IsCancellationRequested)
return;
_completed.Cancel();
_completedOrNotEmpty.NotifyAll();
}
}
/// <summary>
/// Synchronously marks the producer/consumer queue as complete for adding.
/// </summary>
public void CompleteAdding()
{
using (_mutex.Lock())
{
if (_completed.IsCancellationRequested)
return;
_completed.Cancel();
_completedOrNotEmpty.NotifyAll();
}
}
/// <summary>
/// Attempts to enqueue an item.
/// </summary>
/// <param name="item">The item to enqueue.</param>
/// <param name="cancellationToken">A cancellation token that can be used to abort the enqueue operation. If <paramref name="abort"/> is not <c>null</c>, then this token must include signals from the <paramref name="abort"/> object.</param>
/// <param name="abort">A synchronization object used to cancel related enqueue operations. May be <c>null</c> if this is the only enqueue operation.</param>
internal async Task<AsyncProducerConsumerQueue<T>> TryEnqueueAsync(T item, CancellationToken cancellationToken, TaskCompletionSource abort)
{
try
{
using (var combinedToken = CancellationTokenHelpers.Normalize(_completed.Token, cancellationToken))
using (await _mutex.LockAsync().ConfigureAwait(false))
{
// Wait for the queue to be not full.
while (Full)
await _notFull.WaitAsync(combinedToken.Token).ConfigureAwait(false);
// Explicitly check whether the queue has been marked complete to prevent a race condition where notFull is signalled at the same time the queue is marked complete.
if (_completed.IsCancellationRequested)
return null;
// Set the abort signal. If another queue has already set the abort signal, then abort.
if (abort != null && !abort.TrySetCanceled())
return null;
_queue.Enqueue(item);
_completedOrNotEmpty.Notify();
return this;
}
}
catch (OperationCanceledException)
{
return null;
}
}
/// <summary>
/// Attempts to enqueue an item. This method may block the calling thread.
/// </summary>
/// <param name="item">The item to enqueue.</param>
/// <param name="cancellationToken">A cancellation token that can be used to abort the enqueue operation.</param>
internal AsyncProducerConsumerQueue<T> DoTryEnqueue(T item, CancellationToken cancellationToken)
{
try
{
using (var combinedToken = CancellationTokenHelpers.Normalize(_completed.Token, cancellationToken))
using (_mutex.Lock())
{
// Wait for the queue to be not full.
while (Full)
_notFull.Wait(combinedToken.Token);
// Explicitly check whether the queue has been marked complete to prevent a race condition where notFull is signalled at the same time the queue is marked complete.
if (_completed.IsCancellationRequested)
return null;
_queue.Enqueue(item);
_completedOrNotEmpty.Notify();
return this;
}
}
catch (OperationCanceledException)
{
return null;
}
}
/// <summary>
/// Attempts to enqueue an item to the producer/consumer queue. Returns <c>false</c> if the producer/consumer queue has completed adding.
/// </summary>
/// <param name="item">The item to enqueue.</param>
/// <param name="cancellationToken">A cancellation token that can be used to abort the enqueue operation.</param>
public async Task<bool> TryEnqueueAsync(T item, CancellationToken cancellationToken)
{
var ret = await TryEnqueueAsync(item, cancellationToken, null).ConfigureAwait(false);
if (ret != null)
return true;
cancellationToken.ThrowIfCancellationRequested();
return false;
}
/// <summary>
/// Attempts to enqueue an item to the producer/consumer queue. Returns <c>false</c> if the producer/consumer queue has completed adding. This method may block the calling thread.
/// </summary>
/// <param name="item">The item to enqueue.</param>
/// <param name="cancellationToken">A cancellation token that can be used to abort the enqueue operation.</param>
public bool TryEnqueue(T item, CancellationToken cancellationToken)
{
var ret = DoTryEnqueue(item, cancellationToken);
if (ret != null)
return true;
cancellationToken.ThrowIfCancellationRequested();
return false;
}
/// <summary>
/// Attempts to enqueue an item to the producer/consumer queue. Returns <c>false</c> if the producer/consumer queue has completed adding.
/// </summary>
/// <param name="item">The item to enqueue.</param>
public Task<bool> TryEnqueueAsync(T item)
{
return TryEnqueueAsync(item, CancellationToken.None);
}
/// <summary>
/// Attempts to enqueue an item to the producer/consumer queue. Returns <c>false</c> if the producer/consumer queue has completed adding. This method may block the calling thread.
/// </summary>
/// <param name="item">The item to enqueue.</param>
public bool TryEnqueue(T item)
{
return TryEnqueue(item, CancellationToken.None);
}
/// <summary>
/// Enqueues an item to the producer/consumer queue. Throws <see cref="InvalidOperationException"/> if the producer/consumer queue has completed adding.
/// </summary>
/// <param name="item">The item to enqueue.</param>
/// <param name="cancellationToken">A cancellation token that can be used to abort the enqueue operation.</param>
public async Task EnqueueAsync(T item, CancellationToken cancellationToken)
{
var result = await TryEnqueueAsync(item, cancellationToken).ConfigureAwait(false);
if (!result)
throw new InvalidOperationException("Enqueue failed; the producer/consumer queue has completed adding.");
}
/// <summary>
/// Enqueues an item to the producer/consumer queue. Throws <see cref="InvalidOperationException"/> if the producer/consumer queue has completed adding. This method may block the calling thread.
/// </summary>
/// <param name="item">The item to enqueue.</param>
/// <param name="cancellationToken">A cancellation token that can be used to abort the enqueue operation.</param>
public void Enqueue(T item, CancellationToken cancellationToken)
{
var result = TryEnqueue(item, cancellationToken);
if (!result)
throw new InvalidOperationException("Enqueue failed; the producer/consumer queue has completed adding.");
}
/// <summary>
/// Enqueues an item to the producer/consumer queue. Throws <see cref="InvalidOperationException"/> if the producer/consumer queue has completed adding.
/// </summary>
/// <param name="item">The item to enqueue.</param>
public Task EnqueueAsync(T item)
{
return EnqueueAsync(item, CancellationToken.None);
}
/// <summary>
/// Enqueues an item to the producer/consumer queue. This method may block the calling thread. Throws <see cref="InvalidOperationException"/> if the producer/consumer queue has completed adding.
/// </summary>
/// <param name="item">The item to enqueue.</param>
public void Enqueue(T item)
{
Enqueue(item, CancellationToken.None);
}
/// <summary>
/// Asynchronously waits until an item is available to dequeue. Returns <c>false</c> if the producer/consumer queue has completed adding and there are no more items.
/// </summary>
/// <param name="cancellationToken">A cancellation token that can be used to abort the asynchronous wait.</param>
public async Task<bool> OutputAvailableAsync(CancellationToken cancellationToken)
{
using (await _mutex.LockAsync().ConfigureAwait(false))
{
while (!_completed.IsCancellationRequested && Empty)
await _completedOrNotEmpty.WaitAsync(cancellationToken).ConfigureAwait(false);
return !Empty;
}
}
/// <summary>
/// Asynchronously waits until an item is available to dequeue. Returns <c>false</c> if the producer/consumer queue has completed adding and there are no more items.
/// </summary>
public Task<bool> OutputAvailableAsync()
{
return OutputAvailableAsync(CancellationToken.None);
}
/// <summary>
/// Provides a (synchronous) consuming enumerable for items in the producer/consumer queue.
/// </summary>
/// <param name="cancellationToken">A cancellation token that can be used to abort the synchronous enumeration.</param>
public IEnumerable<T> GetConsumingEnumerable(CancellationToken cancellationToken)
{
while (true)
{
var result = DoTryDequeue(cancellationToken);
if (!result.Success)
yield break;
yield return result.Item;
}
}
/// <summary>
/// Provides a (synchronous) consuming enumerable for items in the producer/consumer queue.
/// </summary>
public IEnumerable<T> GetConsumingEnumerable()
{
return GetConsumingEnumerable(CancellationToken.None);
}
/// <summary>
/// Attempts to dequeue an item.
/// </summary>
/// <param name="cancellationToken">A cancellation token that can be used to abort the dequeue operation. If <paramref name="abort"/> is not <c>null</c>, then this token must include signals from the <paramref name="abort"/> object.</param>
/// <param name="abort">A synchronization object used to cancel related dequeue operations. May be <c>null</c> if this is the only dequeue operation.</param>
internal async Task<DequeueResult> TryDequeueAsync(CancellationToken cancellationToken, TaskCompletionSource abort)
{
try
{
using (await _mutex.LockAsync().ConfigureAwait(false))
{
while (!_completed.IsCancellationRequested && Empty)
await _completedOrNotEmpty.WaitAsync(cancellationToken).ConfigureAwait(false);
if (_completed.IsCancellationRequested && Empty)
return FalseResult;
if (abort != null && !abort.TrySetCanceled())
return FalseResult;
var item = _queue.Dequeue();
_notFull.Notify();
return new DequeueResult(this, item);
}
}
catch (OperationCanceledException)
{
return FalseResult;
}
}
/// <summary>
/// Attempts to dequeue an item. This method may block the calling thread.
/// </summary>
/// <param name="cancellationToken">A cancellation token that can be used to abort the dequeue operation.</param>
internal DequeueResult DoTryDequeue(CancellationToken cancellationToken)
{
try
{
using (_mutex.Lock())
{
while (!_completed.IsCancellationRequested && Empty)
_completedOrNotEmpty.Wait(cancellationToken);
if (_completed.IsCancellationRequested && Empty)
return FalseResult;
var item = _queue.Dequeue();
_notFull.Notify();
return new DequeueResult(this, item);
}
}
catch (OperationCanceledException)
{
return FalseResult;
}
}
/// <summary>
/// Attempts to dequeue an item from the producer/consumer queue.
/// </summary>
/// <param name="cancellationToken">A cancellation token that can be used to abort the dequeue operation.</param>
public async Task<DequeueResult> TryDequeueAsync(CancellationToken cancellationToken)
{
var ret = await TryDequeueAsync(cancellationToken, null).ConfigureAwait(false);
if (ret.Success)
return ret;
cancellationToken.ThrowIfCancellationRequested();
return ret;
}
/// <summary>
/// Attempts to dequeue an item from the producer/consumer queue. This method may block the calling thread.
/// </summary>
/// <param name="cancellationToken">A cancellation token that can be used to abort the dequeue operation.</param>
public DequeueResult TryDequeue(CancellationToken cancellationToken)
{
var ret = DoTryDequeue(cancellationToken);
if (ret.Success)
return ret;
cancellationToken.ThrowIfCancellationRequested();
return ret;
}
/// <summary>
/// Attempts to dequeue an item from the producer/consumer queue.
/// </summary>
public Task<DequeueResult> TryDequeueAsync()
{
return TryDequeueAsync(CancellationToken.None);
}
/// <summary>
/// Attempts to dequeue an item from the producer/consumer queue. This method may block the calling thread.
/// </summary>
public DequeueResult TryDequeue()
{
return TryDequeue(CancellationToken.None);
}
/// <summary>
/// Dequeues an item from the producer/consumer queue. Returns the dequeued item. Throws <see cref="InvalidOperationException"/> if the producer/consumer queue has completed adding and is empty.
/// </summary>
/// <param name="cancellationToken">A cancellation token that can be used to abort the dequeue operation.</param>
/// <returns>The dequeued item.</returns>
public async Task<T> DequeueAsync(CancellationToken cancellationToken)
{
var ret = await TryDequeueAsync(cancellationToken).ConfigureAwait(false);
if (!ret.Success)
throw new InvalidOperationException("Dequeue failed; the producer/consumer queue has completed adding and is empty.");
return ret.Item;
}
/// <summary>
/// Dequeues an item from the producer/consumer queue. Returns the dequeued item. This method may block the calling thread. Throws <see cref="InvalidOperationException"/> if the producer/consumer queue has completed adding and is empty.
/// </summary>
/// <param name="cancellationToken">A cancellation token that can be used to abort the dequeue operation.</param>
public T Dequeue(CancellationToken cancellationToken)
{
var ret = TryDequeue(cancellationToken);
if (!ret.Success)
throw new InvalidOperationException("Dequeue failed; the producer/consumer queue has completed adding and is empty.");
return ret.Item;
}
/// <summary>
/// Dequeues an item from the producer/consumer queue. Returns the dequeued item. Throws <see cref="InvalidOperationException"/> if the producer/consumer queue has completed adding and is empty.
/// </summary>
/// <returns>The dequeued item.</returns>
public Task<T> DequeueAsync()
{
return DequeueAsync(CancellationToken.None);
}
/// <summary>
/// Dequeues an item from the producer/consumer queue. Returns the dequeued item. This method may block the calling thread. Throws <see cref="InvalidOperationException"/> if the producer/consumer queue has completed adding and is empty.
/// </summary>
/// <returns>The dequeued item.</returns>
public T Dequeue()
{
return Dequeue(CancellationToken.None);
}
/// <summary>
/// The result of a <c>TryDequeue</c>, <c>DequeueFromAny</c>, or <c>TryDequeueFromAny</c> operation.
/// </summary>
public sealed class DequeueResult
{
internal DequeueResult(AsyncProducerConsumerQueue<T> queue, T item)
{
Queue = queue;
Item = item;
}
/// <summary>
/// The queue from which the item was dequeued, or <c>null</c> if the operation failed.
/// </summary>
public AsyncProducerConsumerQueue<T> Queue { get; private set; }
/// <summary>
/// Whether the operation was successful. This is <c>true</c> if and only if <see cref="Queue"/> is not <c>null</c>.
/// </summary>
public bool Success { get { return Queue != null; } }
/// <summary>
/// The dequeued item. This is only valid if <see cref="Queue"/> is not <c>null</c>.
/// </summary>
public T Item { get; private set; }
}
[DebuggerNonUserCode]
internal sealed class DebugView
{
private readonly AsyncProducerConsumerQueue<T> _queue;
public DebugView(AsyncProducerConsumerQueue<T> queue)
{
_queue = queue;
}
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
public T[] Items
{
get { return _queue._queue.ToArray(); }
}
}
}
/// <summary>
/// Provides methods for working on multiple <see cref="AsyncProducerConsumerQueue{T}"/> instances.
/// </summary>
public static class AsyncProducerConsumerQueueExtensions
{
/// <summary>
/// Attempts to enqueue an item to any of a number of producer/consumer queues. Returns the producer/consumer queue that received the item. Returns <c>null</c> if all producer/consumer queues have completed adding.
/// </summary>
/// <param name="queues">The producer/consumer queues.</param>
/// <param name="item">The item to enqueue.</param>
/// <param name="cancellationToken">A cancellation token that can be used to abort the enqueue operation.</param>
/// <returns>The producer/consumer queue that received the item.</returns>
public static async Task<AsyncProducerConsumerQueue<T>> TryEnqueueToAnyAsync<T>(this IEnumerable<AsyncProducerConsumerQueue<T>> queues, T item, CancellationToken cancellationToken)
{
var abort = new TaskCompletionSource();
using (var abortCancellationToken = CancellationTokenHelpers.FromTask(abort.Task))
using (var combinedToken = CancellationTokenHelpers.Normalize(abortCancellationToken.Token, cancellationToken))
{
var token = combinedToken.Token;
var tasks = queues.Select(q => q.TryEnqueueAsync(item, token, abort));
var results = await TaskShim.WhenAll(tasks).ConfigureAwait(false);
var ret = results.FirstOrDefault(x => x != null);
if (ret == null)
cancellationToken.ThrowIfCancellationRequested();
return ret;
}
}
/// <summary>
/// Attempts to enqueue an item to any of a number of producer/consumer queues. Returns the producer/consumer queue that received the item. Returns <c>null</c> if all producer/consumer queues have completed adding. This method may block the calling thread.
/// </summary>
/// <param name="queues">The producer/consumer queues.</param>
/// <param name="item">The item to enqueue.</param>
/// <param name="cancellationToken">A cancellation token that can be used to abort the enqueue operation.</param>
/// <returns>The producer/consumer queue that received the item.</returns>
public static AsyncProducerConsumerQueue<T> TryEnqueueToAny<T>(this IEnumerable<AsyncProducerConsumerQueue<T>> queues, T item, CancellationToken cancellationToken)
{
return TryEnqueueToAnyAsync(queues, item, cancellationToken).WaitAndUnwrapException();
}
/// <summary>
/// Attempts to enqueue an item to any of a number of producer/consumer queues. Returns the producer/consumer queue that received the item. Returns <c>null</c> if all producer/consumer queues have completed adding.
/// </summary>
/// <param name="queues">The producer/consumer queues.</param>
/// <param name="item">The item to enqueue.</param>
/// <returns>The producer/consumer queue that received the item.</returns>
public static Task<AsyncProducerConsumerQueue<T>> TryEnqueueToAnyAsync<T>(this IEnumerable<AsyncProducerConsumerQueue<T>> queues, T item)
{
return TryEnqueueToAnyAsync(queues, item, CancellationToken.None);
}
/// <summary>
/// Attempts to enqueue an item to any of a number of producer/consumer queues. Returns the producer/consumer queue that received the item. Returns <c>null</c> if all producer/consumer queues have completed adding. This method may block the calling thread.
/// </summary>
/// <param name="queues">The producer/consumer queues.</param>
/// <param name="item">The item to enqueue.</param>
/// <returns>The producer/consumer queue that received the item.</returns>
public static AsyncProducerConsumerQueue<T> TryEnqueueToAny<T>(this IEnumerable<AsyncProducerConsumerQueue<T>> queues, T item)
{
return TryEnqueueToAny(queues, item, CancellationToken.None);
}
/// <summary>
/// Enqueues an item to any of a number of producer/consumer queues. Returns the producer/consumer queue that received the item. Throws <see cref="InvalidOperationException"/> if all producer/consumer queues have completed adding.
/// </summary>
/// <param name="queues">The producer/consumer queues.</param>
/// <param name="item">The item to enqueue.</param>
/// <param name="cancellationToken">A cancellation token that can be used to abort the enqueue operation.</param>
/// <returns>The producer/consumer queue that received the item.</returns>
public static async Task<AsyncProducerConsumerQueue<T>> EnqueueToAnyAsync<T>(this IEnumerable<AsyncProducerConsumerQueue<T>> queues, T item, CancellationToken cancellationToken)
{
var ret = await TryEnqueueToAnyAsync(queues, item, cancellationToken).ConfigureAwait(false);
if (ret == null)
throw new InvalidOperationException("Enqueue failed; all producer/consumer queues have completed adding.");
return ret;
}
/// <summary>
/// Enqueues an item to any of a number of producer/consumer queues. Returns the producer/consumer queue that received the item. Throws <see cref="InvalidOperationException"/> if all producer/consumer queues have completed adding. This method may block the calling thread.
/// </summary>
/// <param name="queues">The producer/consumer queues.</param>
/// <param name="item">The item to enqueue.</param>
/// <param name="cancellationToken">A cancellation token that can be used to abort the enqueue operation.</param>
/// <returns>The producer/consumer queue that received the item.</returns>
public static AsyncProducerConsumerQueue<T> EnqueueToAny<T>(this IEnumerable<AsyncProducerConsumerQueue<T>> queues, T item, CancellationToken cancellationToken)
{
var ret = TryEnqueueToAny(queues, item, cancellationToken);
if (ret == null)
throw new InvalidOperationException("Enqueue failed; all producer/consumer queues have completed adding.");
return ret;
}
/// <summary>
/// Enqueues an item to any of a number of producer/consumer queues. Returns the producer/consumer queue that received the item. Throws <see cref="InvalidOperationException"/> if all producer/consumer queues have completed adding.
/// </summary>
/// <param name="queues">The producer/consumer queues.</param>
/// <param name="item">The item to enqueue.</param>
/// <returns>The producer/consumer queue that received the item.</returns>
public static Task<AsyncProducerConsumerQueue<T>> EnqueueToAnyAsync<T>(this IEnumerable<AsyncProducerConsumerQueue<T>> queues, T item)
{
return EnqueueToAnyAsync(queues, item, CancellationToken.None);
}
/// <summary>
/// Enqueues an item to any of a number of producer/consumer queues. Returns the producer/consumer queue that received the item. Throws <see cref="InvalidOperationException"/> if all producer/consumer queues have completed adding. This method may block the calling thread.
/// </summary>
/// <param name="queues">The producer/consumer queues.</param>
/// <param name="item">The item to enqueue.</param>
/// <returns>The producer/consumer queue that received the item.</returns>
public static AsyncProducerConsumerQueue<T> EnqueueToAny<T>(this IEnumerable<AsyncProducerConsumerQueue<T>> queues, T item)
{
return EnqueueToAny(queues, item, CancellationToken.None);
}
/// <summary>
/// Attempts to dequeue an item from any of a number of producer/consumer queues. The operation "fails" if all the producer/consumer queues have completed adding and are empty.
/// </summary>
/// <param name="queues">The producer/consumer queues.</param>
/// <param name="cancellationToken">A cancellation token that can be used to abort the dequeue operation.</param>
public static async Task<AsyncProducerConsumerQueue<T>.DequeueResult> TryDequeueFromAnyAsync<T>(this IEnumerable<AsyncProducerConsumerQueue<T>> queues, CancellationToken cancellationToken)
{
var abort = new TaskCompletionSource();
using (var abortCancellationToken = CancellationTokenHelpers.FromTask(abort.Task))
using (var combinedToken = CancellationTokenHelpers.Normalize(abortCancellationToken.Token, cancellationToken))
{
var token = combinedToken.Token;
var tasks = queues.Select(q => q.TryDequeueAsync(token, abort));
var results = await TaskShim.WhenAll(tasks).ConfigureAwait(false);
var result = results.FirstOrDefault(x => x.Success);
if (result != null)
return result;
cancellationToken.ThrowIfCancellationRequested();
return AsyncProducerConsumerQueue<T>.FalseResult;
}
}
/// <summary>
/// Attempts to dequeue an item from any of a number of producer/consumer queues. The operation "fails" if all the producer/consumer queues have completed adding and are empty. This method may block the calling thread.
/// </summary>
/// <param name="queues">The producer/consumer queues.</param>
/// <param name="cancellationToken">A cancellation token that can be used to abort the dequeue operation.</param>
public static AsyncProducerConsumerQueue<T>.DequeueResult TryDequeueFromAny<T>(this IEnumerable<AsyncProducerConsumerQueue<T>> queues, CancellationToken cancellationToken)
{
return TryDequeueFromAnyAsync(queues, cancellationToken).WaitAndUnwrapException();
}
/// <summary>
/// Attempts to dequeue an item from any of a number of producer/consumer queues. The operation "fails" if all the producer/consumer queues have completed adding and are empty.
/// </summary>
/// <param name="queues">The producer/consumer queues.</param>
public static Task<AsyncProducerConsumerQueue<T>.DequeueResult> TryDequeueFromAnyAsync<T>(this IEnumerable<AsyncProducerConsumerQueue<T>> queues)
{
return TryDequeueFromAnyAsync(queues, CancellationToken.None);
}
/// <summary>
/// Attempts to dequeue an item from any of a number of producer/consumer queues. The operation "fails" if all the producer/consumer queues have completed adding and are empty. This method may block the calling thread.
/// </summary>
/// <param name="queues">The producer/consumer queues.</param>
public static AsyncProducerConsumerQueue<T>.DequeueResult TryDequeueFromAny<T>(this IEnumerable<AsyncProducerConsumerQueue<T>> queues)
{
return TryDequeueFromAny(queues, CancellationToken.None);
}
/// <summary>
/// Dequeues an item from any of a number of producer/consumer queues. Throws <see cref="InvalidOperationException"/> if all the producer/consumer queues have completed adding and are empty.
/// </summary>
/// <param name="queues">The producer/consumer queues.</param>
/// <param name="cancellationToken">A cancellation token that can be used to abort the dequeue operation.</param>
public static async Task<AsyncProducerConsumerQueue<T>.DequeueResult> DequeueFromAnyAsync<T>(this IEnumerable<AsyncProducerConsumerQueue<T>> queues, CancellationToken cancellationToken)
{
var ret = await TryDequeueFromAnyAsync(queues, cancellationToken).ConfigureAwait(false);
if (!ret.Success)
throw new InvalidOperationException("Dequeue failed; all producer/consumer queues have completed adding and are empty.");
return ret;
}
/// <summary>
/// Dequeues an item from any of a number of producer/consumer queues. Throws <see cref="InvalidOperationException"/> if all the producer/consumer queues have completed adding and are empty. This method may block the calling thread.
/// </summary>
/// <param name="queues">The producer/consumer queues.</param>
/// <param name="cancellationToken">A cancellation token that can be used to abort the dequeue operation.</param>
public static AsyncProducerConsumerQueue<T>.DequeueResult DequeueFromAny<T>(this IEnumerable<AsyncProducerConsumerQueue<T>> queues, CancellationToken cancellationToken)
{
var ret = TryDequeueFromAny(queues, cancellationToken);
if (!ret.Success)
throw new InvalidOperationException("Dequeue failed; all producer/consumer queues have completed adding and are empty.");
return ret;
}
/// <summary>
/// Dequeues an item from any of a number of producer/consumer queues. Throws <see cref="InvalidOperationException"/> if all the producer/consumer queues have completed adding and are empty.
/// </summary>
/// <param name="queues">The producer/consumer queues.</param>
public static Task<AsyncProducerConsumerQueue<T>.DequeueResult> DequeueFromAnyAsync<T>(this IEnumerable<AsyncProducerConsumerQueue<T>> queues)
{
return DequeueFromAnyAsync(queues, CancellationToken.None);
}
/// <summary>
/// Dequeues an item from any of a number of producer/consumer queues. Throws <see cref="InvalidOperationException"/> if all the producer/consumer queues have completed adding and are empty. This method may block the calling thread.
/// </summary>
/// <param name="queues">The producer/consumer queues.</param>
public static AsyncProducerConsumerQueue<T>.DequeueResult DequeueFromAny<T>(this IEnumerable<AsyncProducerConsumerQueue<T>> queues)
{
return DequeueFromAny(queues, CancellationToken.None);
}
}
}

@ -0,0 +1,769 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace ZeroLevel.Services.Async
{
/// <summary>
/// A reader/writer lock that is compatible with async. Note that this lock is <b>not</b> recursive!
/// </summary>
[DebuggerDisplay("Id = {Id}, State = {GetStateForDebugger}, ReaderCount = {GetReaderCountForDebugger}, UpgradeInProgress = {GetUpgradeInProgressForDebugger}")]
[DebuggerTypeProxy(typeof(DebugView))]
public sealed class AsyncReaderWriterLock
{
/// <summary>
/// The queue of TCSs that other tasks are awaiting to acquire the lock as writers.
/// </summary>
private readonly IAsyncWaitQueue<IDisposable> _writerQueue;
/// <summary>
/// The queue of TCSs that other tasks are awaiting to acquire the lock as readers.
/// </summary>
private readonly IAsyncWaitQueue<IDisposable> _readerQueue;
/// <summary>
/// The queue of TCSs that other tasks are awaiting to acquire the lock as upgradeable readers.
/// </summary>
private readonly IAsyncWaitQueue<UpgradeableReaderKey> _upgradeableReaderQueue;
/// <summary>
/// The queue of TCSs that other tasks are awaiting to upgrade a reader lock to a writer lock.
/// </summary>
private readonly IAsyncWaitQueue<IDisposable> _upgradeReaderQueue;
/// <summary>
/// The semi-unique identifier for this instance. This is 0 if the id has not yet been created.
/// </summary>
private int _id;
/// <summary>
/// The current upgradeable reader lock key, if any. If this is not <c>null</c>, then there is an upgradeable reader lock held.
/// </summary>
private UpgradeableReaderKey _upgradeableReaderKey;
/// <summary>
/// Number of reader locks held (including an upgradeable reader lock, if applicable); -1 if a writer lock is held; 0 if no locks are held.
/// </summary>
private int _locksHeld;
/// <summary>
/// The object used for mutual exclusion.
/// </summary>
private readonly object _mutex;
[DebuggerNonUserCode]
internal State GetStateForDebugger
{
get
{
if (_locksHeld == 0)
return State.Unlocked;
if (_locksHeld == -1)
if (_upgradeableReaderKey != null)
return State.WriteLockedWithUpgradeableReader;
else
return State.WriteLocked;
if (_upgradeableReaderKey != null)
return State.ReadLockedWithUpgradeableReader;
return State.ReadLocked;
}
}
internal enum State
{
Unlocked,
ReadLocked,
ReadLockedWithUpgradeableReader,
WriteLocked,
WriteLockedWithUpgradeableReader,
}
[DebuggerNonUserCode]
internal int GetReaderCountForDebugger { get { return (_locksHeld > 0 ? _locksHeld : 0); } }
[DebuggerNonUserCode]
internal bool GetUpgradeInProgressForDebugger { get { return !_upgradeReaderQueue.IsEmpty; } }
/// <summary>
/// Creates a new async-compatible reader/writer lock.
/// </summary>
public AsyncReaderWriterLock(IAsyncWaitQueue<IDisposable> writerQueue, IAsyncWaitQueue<IDisposable> readerQueue,
IAsyncWaitQueue<UpgradeableReaderKey> upgradeableReaderQueue, IAsyncWaitQueue<IDisposable> upgradeReaderQueue)
{
_writerQueue = writerQueue;
_readerQueue = readerQueue;
_upgradeableReaderQueue = upgradeableReaderQueue;
_upgradeReaderQueue = upgradeReaderQueue;
_mutex = new object();
}
/// <summary>
/// Creates a new async-compatible reader/writer lock.
/// </summary>
public AsyncReaderWriterLock()
: this(new DefaultAsyncWaitQueue<IDisposable>(), new DefaultAsyncWaitQueue<IDisposable>(),
new DefaultAsyncWaitQueue<UpgradeableReaderKey>(), new DefaultAsyncWaitQueue<IDisposable>())
{
}
/// <summary>
/// Gets a semi-unique identifier for this asynchronous lock.
/// </summary>
public int Id
{
get { return IdManager<AsyncReaderWriterLock>.GetId(ref _id); }
}
internal object SyncObject
{
get { return _mutex; }
}
/// <summary>
/// Applies a continuation to the task that will call <see cref="ReleaseWaiters"/> if the task is canceled. This method may not be called while holding the sync lock.
/// </summary>
/// <param name="task">The task to observe for cancellation.</param>
private void ReleaseWaitersWhenCanceled(Task task)
{
task.ContinueWith(t =>
{
List<IDisposable> finishes;
lock (SyncObject) { finishes = ReleaseWaiters(); }
foreach (var finish in finishes)
finish.Dispose();
}, CancellationToken.None, TaskContinuationOptions.OnlyOnCanceled | TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
}
/// <summary>
/// Asynchronously acquires the lock as a reader. Returns a disposable that releases the lock when disposed.
/// </summary>
/// <param name="cancellationToken">The cancellation token used to cancel the lock. If this is already set, then this method will attempt to take the lock immediately (succeeding if the lock is currently available).</param>
/// <returns>A disposable that releases the lock when disposed.</returns>
public Task<IDisposable> ReaderLockAsync(CancellationToken cancellationToken)
{
Task<IDisposable> ret;
lock (SyncObject)
{
// If the lock is available or in read mode and there are no waiting writers, upgradeable readers, or upgrading readers, take it immediately.
if (_locksHeld >= 0 && _writerQueue.IsEmpty && _upgradeableReaderQueue.IsEmpty && _upgradeReaderQueue.IsEmpty)
{
++_locksHeld;
ret = TaskShim.FromResult<IDisposable>(new ReaderKey(this));
}
else
{
// Wait for the lock to become available or cancellation.
ret = _readerQueue.Enqueue(cancellationToken);
}
}
return ret;
}
/// <summary>
/// Synchronously acquires the lock as a reader. Returns a disposable that releases the lock when disposed. This method may block the calling thread.
/// </summary>
/// <param name="cancellationToken">The cancellation token used to cancel the lock. If this is already set, then this method will attempt to take the lock immediately (succeeding if the lock is currently available).</param>
/// <returns>A disposable that releases the lock when disposed.</returns>
public IDisposable ReaderLock(CancellationToken cancellationToken)
{
Task<IDisposable> ret;
lock (SyncObject)
{
// If the lock is available or in read mode and there are no waiting writers, upgradeable readers, or upgrading readers, take it immediately.
if (_locksHeld >= 0 && _writerQueue.IsEmpty && _upgradeableReaderQueue.IsEmpty && _upgradeReaderQueue.IsEmpty)
{
++_locksHeld;
return new ReaderKey(this);
}
// Wait for the lock to become available or cancellation.
ret = _readerQueue.Enqueue(cancellationToken);
}
return ret.WaitAndUnwrapException();
}
/// <summary>
/// Asynchronously acquires the lock as a reader. Returns a disposable that releases the lock when disposed.
/// </summary>
/// <returns>A disposable that releases the lock when disposed.</returns>
public Task<IDisposable> ReaderLockAsync()
{
return ReaderLockAsync(CancellationToken.None);
}
/// <summary>
/// Synchronously acquires the lock as a reader. Returns a disposable that releases the lock when disposed. This method may block the calling thread.
/// </summary>
/// <returns>A disposable that releases the lock when disposed.</returns>
public IDisposable ReaderLock()
{
return ReaderLock(CancellationToken.None);
}
/// <summary>
/// Asynchronously acquires the lock as a writer. Returns a disposable that releases the lock when disposed.
/// </summary>
/// <param name="cancellationToken">The cancellation token used to cancel the lock. If this is already set, then this method will attempt to take the lock immediately (succeeding if the lock is currently available).</param>
/// <returns>A disposable that releases the lock when disposed.</returns>
public Task<IDisposable> WriterLockAsync(CancellationToken cancellationToken)
{
Task<IDisposable> ret;
lock (SyncObject)
{
// If the lock is available, take it immediately.
if (_locksHeld == 0)
{
_locksHeld = -1;
ret = TaskShim.FromResult<IDisposable>(new WriterKey(this));
}
else
{
// Wait for the lock to become available or cancellation.
ret = _writerQueue.Enqueue(cancellationToken);
}
}
ReleaseWaitersWhenCanceled(ret);
return ret;
}
/// <summary>
/// Synchronously acquires the lock as a writer. Returns a disposable that releases the lock when disposed. This method may block the calling thread.
/// </summary>
/// <param name="cancellationToken">The cancellation token used to cancel the lock. If this is already set, then this method will attempt to take the lock immediately (succeeding if the lock is currently available).</param>
/// <returns>A disposable that releases the lock when disposed.</returns>
public IDisposable WriterLock(CancellationToken cancellationToken)
{
Task<IDisposable> ret;
lock (SyncObject)
{
// If the lock is available, take it immediately.
if (_locksHeld == 0)
{
_locksHeld = -1;
return new WriterKey(this);
}
// Wait for the lock to become available or cancellation.
ret = _writerQueue.Enqueue(cancellationToken);
}
ReleaseWaitersWhenCanceled(ret);
return ret.WaitAndUnwrapException();
}
/// <summary>
/// Asynchronously acquires the lock as a writer. Returns a disposable that releases the lock when disposed.
/// </summary>
/// <returns>A disposable that releases the lock when disposed.</returns>
public Task<IDisposable> WriterLockAsync()
{
return WriterLockAsync(CancellationToken.None);
}
/// <summary>
/// Asynchronously acquires the lock as a writer. Returns a disposable that releases the lock when disposed. This method may block the calling thread.
/// </summary>
/// <returns>A disposable that releases the lock when disposed.</returns>
public IDisposable WriterLock()
{
return WriterLock(CancellationToken.None);
}
/// <summary>
/// Asynchronously acquires the lock as a reader with the option to upgrade. Returns a key that can be used to upgrade and downgrade the lock, and releases the lock when disposed.
/// </summary>
/// <param name="cancellationToken">The cancellation token used to cancel the lock. If this is already set, then this method will attempt to take the lock immediately (succeeding if the lock is currently available).</param>
/// <returns>A key that can be used to upgrade and downgrade this lock, and releases the lock when disposed.</returns>
public Task<UpgradeableReaderKey> UpgradeableReaderLockAsync(CancellationToken cancellationToken)
{
Task<UpgradeableReaderKey> ret;
lock (SyncObject)
{
// If the lock is available, take it immediately.
if (_locksHeld == 0 || (_locksHeld > 0 && _upgradeableReaderKey == null))
{
++_locksHeld;
_upgradeableReaderKey = new UpgradeableReaderKey(this);
ret = TaskShim.FromResult(_upgradeableReaderKey);
}
else
{
// Wait for the lock to become available or cancellation.
ret = _upgradeableReaderQueue.Enqueue(cancellationToken);
}
}
ReleaseWaitersWhenCanceled(ret);
return ret;
}
/// <summary>
/// Synchronously acquires the lock as a reader with the option to upgrade. Returns a key that can be used to upgrade and downgrade the lock, and releases the lock when disposed. This method may block the calling thread.
/// </summary>
/// <param name="cancellationToken">The cancellation token used to cancel the lock. If this is already set, then this method will attempt to take the lock immediately (succeeding if the lock is currently available).</param>
/// <returns>A key that can be used to upgrade and downgrade this lock, and releases the lock when disposed.</returns>
public UpgradeableReaderKey UpgradeableReaderLock(CancellationToken cancellationToken)
{
Task<UpgradeableReaderKey> ret;
lock (SyncObject)
{
// If the lock is available, take it immediately.
if (_locksHeld == 0 || (_locksHeld > 0 && _upgradeableReaderKey == null))
{
++_locksHeld;
_upgradeableReaderKey = new UpgradeableReaderKey(this);
return _upgradeableReaderKey;
}
// Wait for the lock to become available or cancellation.
ret = _upgradeableReaderQueue.Enqueue(cancellationToken);
}
ReleaseWaitersWhenCanceled(ret);
return ret.WaitAndUnwrapException();
}
/// <summary>
/// Asynchronously acquires the lock as a reader with the option to upgrade. Returns a key that can be used to upgrade and downgrade the lock, and releases the lock when disposed.
/// </summary>
/// <returns>A key that can be used to upgrade and downgrade this lock, and releases the lock when disposed.</returns>
public Task<UpgradeableReaderKey> UpgradeableReaderLockAsync()
{
return UpgradeableReaderLockAsync(CancellationToken.None);
}
/// <summary>
/// Synchronously acquires the lock as a reader with the option to upgrade. Returns a key that can be used to upgrade and downgrade the lock, and releases the lock when disposed. This method may block the calling thread.
/// </summary>
/// <returns>A key that can be used to upgrade and downgrade this lock, and releases the lock when disposed.</returns>
public UpgradeableReaderKey UpgradeableReaderLock()
{
return UpgradeableReaderLock(CancellationToken.None);
}
/// <summary>
/// Asynchronously upgrades a reader lock to a writer lock. This method assumes the sync lock is already held.
/// </summary>
internal Task<IDisposable> UpgradeAsync(CancellationToken cancellationToken)
{
Task<IDisposable> ret;
// If the lock is available, take it immediately.
if (_locksHeld == 1)
{
_locksHeld = -1;
ret = TaskShim.FromResult<IDisposable>(new UpgradeableReaderKey.UpgradeKey(_upgradeableReaderKey));
}
else
{
// Wait for the lock to become available or cancellation.
ret = _upgradeReaderQueue.Enqueue(cancellationToken);
}
return ret;
}
/// <summary>
/// Downgrades a writer lock to a reader lock. This method assumes the sync lock is already held.
/// </summary>
internal List<IDisposable> Downgrade()
{
_locksHeld = 1;
return ReleaseWaiters();
}
/// <summary>
/// Grants lock(s) to waiting tasks. This method assumes the sync lock is already held.
/// </summary>
private List<IDisposable> ReleaseWaiters()
{
var ret = new List<IDisposable>();
if (_locksHeld == 0)
{
// Give priority to writers.
if (!_writerQueue.IsEmpty)
{
ret.Add(_writerQueue.Dequeue(new WriterKey(this)));
_locksHeld = -1;
return ret;
}
// Then to upgradeable readers.
if (!_upgradeableReaderQueue.IsEmpty)
{
_upgradeableReaderKey = new UpgradeableReaderKey(this);
ret.Add(_upgradeableReaderQueue.Dequeue(_upgradeableReaderKey));
++_locksHeld;
}
// Finally to readers.
while (!_readerQueue.IsEmpty)
{
ret.Add(_readerQueue.Dequeue(new ReaderKey(this)));
++_locksHeld;
}
return ret;
}
// Give priority to upgrading readers.
if (_locksHeld == 1)
{
if (!_upgradeReaderQueue.IsEmpty)
{
ret.Add(_upgradeReaderQueue.Dequeue(new UpgradeableReaderKey.UpgradeKey(_upgradeableReaderKey)));
_locksHeld = -1;
}
}
if (_locksHeld > 0)
{
// If there are current reader locks and waiting writers, then do nothing.
if (!_writerQueue.IsEmpty || !_upgradeableReaderQueue.IsEmpty || !_upgradeReaderQueue.IsEmpty)
return ret;
// If there are current reader locks but no upgradeable reader lock, try to release an upgradeable reader.
if (_upgradeableReaderKey == null && !_upgradeableReaderQueue.IsEmpty)
{
_upgradeableReaderKey = new UpgradeableReaderKey(this);
ret.Add(_upgradeableReaderQueue.Dequeue(_upgradeableReaderKey));
}
}
return ret;
}
/// <summary>
/// Releases the lock as a reader.
/// </summary>
internal void ReleaseReaderLock()
{
List<IDisposable> finishes;
lock (SyncObject)
{
--_locksHeld;
finishes = ReleaseWaiters();
}
foreach (var finish in finishes)
finish.Dispose();
}
/// <summary>
/// Releases the lock as a writer.
/// </summary>
internal void ReleaseWriterLock()
{
List<IDisposable> finishes;
lock (SyncObject)
{
_locksHeld = 0;
finishes = ReleaseWaiters();
}
foreach (var finish in finishes)
finish.Dispose();
}
/// <summary>
/// Releases the lock as an upgradeable reader.
/// </summary>
internal void ReleaseUpgradeableReaderLock(Task upgrade)
{
IDisposable cancelFinish = null;
List<IDisposable> finishes;
lock (SyncObject)
{
if (upgrade != null)
cancelFinish = _upgradeReaderQueue.TryCancel(upgrade);
_upgradeableReaderKey = null;
--_locksHeld;
finishes = ReleaseWaiters();
}
if (cancelFinish != null)
cancelFinish.Dispose();
foreach (var finish in finishes)
finish.Dispose();
}
/// <summary>
/// The disposable which releases the reader lock.
/// </summary>
private sealed class ReaderKey : IDisposable
{
/// <summary>
/// The lock to release.
/// </summary>
private AsyncReaderWriterLock _asyncReaderWriterLock;
/// <summary>
/// Creates the key for a lock.
/// </summary>
/// <param name="asyncReaderWriterLock">The lock to release. May not be <c>null</c>.</param>
public ReaderKey(AsyncReaderWriterLock asyncReaderWriterLock)
{
_asyncReaderWriterLock = asyncReaderWriterLock;
}
/// <summary>
/// Release the lock.
/// </summary>
public void Dispose()
{
if (_asyncReaderWriterLock == null)
return;
_asyncReaderWriterLock.ReleaseReaderLock();
_asyncReaderWriterLock = null;
}
}
/// <summary>
/// The disposable which releases the writer lock.
/// </summary>
private sealed class WriterKey : IDisposable
{
/// <summary>
/// The lock to release.
/// </summary>
private AsyncReaderWriterLock _asyncReaderWriterLock;
/// <summary>
/// Creates the key for a lock.
/// </summary>
/// <param name="asyncReaderWriterLock">The lock to release. May not be <c>null</c>.</param>
public WriterKey(AsyncReaderWriterLock asyncReaderWriterLock)
{
_asyncReaderWriterLock = asyncReaderWriterLock;
}
/// <summary>
/// Release the lock.
/// </summary>
public void Dispose()
{
if (_asyncReaderWriterLock == null)
return;
_asyncReaderWriterLock.ReleaseWriterLock();
_asyncReaderWriterLock = null;
}
}
/// <summary>
/// The disposable which manages the upgradeable reader lock.
/// </summary>
[DebuggerDisplay("State = {GetStateForDebugger}, ReaderWriterLockId = {_asyncReaderWriterLock.Id}")]
public sealed class UpgradeableReaderKey : IDisposable
{
/// <summary>
/// The lock to release.
/// </summary>
private readonly AsyncReaderWriterLock _asyncReaderWriterLock;
/// <summary>
/// The task doing the upgrade.
/// </summary>
private Task<IDisposable> _upgrade;
/// <summary>
/// Whether or not this instance has been disposed.
/// </summary>
private bool _disposed;
[DebuggerNonUserCode]
internal State GetStateForDebugger
{
get
{
if (_upgrade == null)
return State.Reader;
if (_upgrade.Status == TaskStatus.RanToCompletion)
return State.Writer;
return State.UpgradingToWriter;
}
}
internal enum State
{
Reader,
UpgradingToWriter,
Writer,
}
/// <summary>
/// Creates the key for a lock.
/// </summary>
/// <param name="asyncReaderWriterLock">The lock to release. May not be <c>null</c>.</param>
internal UpgradeableReaderKey(AsyncReaderWriterLock asyncReaderWriterLock)
{
_asyncReaderWriterLock = asyncReaderWriterLock;
}
/// <summary>
/// Gets a value indicating whether this lock has been upgraded to a write lock.
/// </summary>
public bool Upgraded
{
get
{
Task task;
lock (_asyncReaderWriterLock.SyncObject) { task = _upgrade; }
return (task != null && task.Status == TaskStatus.RanToCompletion);
}
}
/// <summary>
/// Upgrades the reader lock to a writer lock. Returns a disposable that downgrades the writer lock to a reader lock when disposed.
/// </summary>
/// <param name="cancellationToken">The cancellation token used to cancel the upgrade. If this is already set, then this method will attempt to upgrade immediately (succeeding if the lock is currently available).</param>
public Task<IDisposable> UpgradeAsync(CancellationToken cancellationToken)
{
lock (_asyncReaderWriterLock.SyncObject)
{
if (_upgrade != null)
throw new InvalidOperationException("Cannot upgrade.");
_upgrade = _asyncReaderWriterLock.UpgradeAsync(cancellationToken);
}
_asyncReaderWriterLock.ReleaseWaitersWhenCanceled(_upgrade);
var ret = new TaskCompletionSource<IDisposable>();
_upgrade.ContinueWith(t =>
{
if (t.IsCanceled)
lock (_asyncReaderWriterLock.SyncObject) { _upgrade = null; }
ret.TryCompleteFromCompletedTask(t);
}, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
return ret.Task;
}
/// <summary>
/// Synchronously upgrades the reader lock to a writer lock. Returns a disposable that downgrades the writer lock to a reader lock when disposed. This method may block the calling thread.
/// </summary>
/// <param name="cancellationToken">The cancellation token used to cancel the upgrade. If this is already set, then this method will attempt to upgrade immediately (succeeding if the lock is currently available).</param>
public IDisposable Upgrade(CancellationToken cancellationToken)
{
lock (_asyncReaderWriterLock.SyncObject)
{
if (_upgrade != null)
throw new InvalidOperationException("Cannot upgrade.");
_upgrade = _asyncReaderWriterLock.UpgradeAsync(cancellationToken);
}
_asyncReaderWriterLock.ReleaseWaitersWhenCanceled(_upgrade);
var ret = new TaskCompletionSource<IDisposable>();
_upgrade.ContinueWith(t =>
{
if (t.IsCanceled)
lock (_asyncReaderWriterLock.SyncObject) { _upgrade = null; }
ret.TryCompleteFromCompletedTask(t);
}, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
return ret.Task.WaitAndUnwrapException();
}
/// <summary>
/// Upgrades the reader lock to a writer lock. Returns a disposable that downgrades the writer lock to a reader lock when disposed.
/// </summary>
public Task<IDisposable> UpgradeAsync()
{
return UpgradeAsync(CancellationToken.None);
}
/// <summary>
/// Synchronously upgrades the reader lock to a writer lock. Returns a disposable that downgrades the writer lock to a reader lock when disposed. This method may block the calling thread.
/// </summary>
public IDisposable Upgrade()
{
return Upgrade(CancellationToken.None);
}
/// <summary>
/// Downgrades the writer lock to a reader lock.
/// </summary>
private void Downgrade()
{
List<IDisposable> finishes;
lock (_asyncReaderWriterLock.SyncObject)
{
finishes = _asyncReaderWriterLock.Downgrade();
_upgrade = null;
}
foreach (var finish in finishes)
finish.Dispose();
}
/// <summary>
/// Release the lock.
/// </summary>
public void Dispose()
{
if (_disposed)
return;
_asyncReaderWriterLock.ReleaseUpgradeableReaderLock(_upgrade);
_disposed = true;
}
/// <summary>
/// The disposable which downgrades an upgradeable reader key.
/// </summary>
internal sealed class UpgradeKey : IDisposable
{
/// <summary>
/// The upgradeable reader key to downgrade.
/// </summary>
private UpgradeableReaderKey _key;
/// <summary>
/// Creates the upgrade key for an upgradeable reader key.
/// </summary>
/// <param name="key">The upgradeable reader key to downgrade. May not be <c>null</c>.</param>
public UpgradeKey(UpgradeableReaderKey key)
{
_key = key;
}
/// <summary>
/// Downgrade the upgradeable reader key.
/// </summary>
public void Dispose()
{
if (_key == null)
return;
_key.Downgrade();
_key = null;
}
}
}
// ReSharper disable UnusedMember.Local
[DebuggerNonUserCode]
private sealed class DebugView
{
private readonly AsyncReaderWriterLock _rwl;
public DebugView(AsyncReaderWriterLock rwl)
{
_rwl = rwl;
}
public int Id { get { return _rwl.Id; } }
public State State { get { return _rwl.GetStateForDebugger; } }
public int ReaderCount { get { return _rwl.GetReaderCountForDebugger; } }
public bool UpgradeInProgress { get { return _rwl.GetUpgradeInProgressForDebugger; } }
public IAsyncWaitQueue<IDisposable> ReaderWaitQueue { get { return _rwl._readerQueue; } }
public IAsyncWaitQueue<IDisposable> WriterWaitQueue { get { return _rwl._writerQueue; } }
public IAsyncWaitQueue<UpgradeableReaderKey> UpgradeableReaderWaitQueue { get { return _rwl._upgradeableReaderQueue; } }
public IAsyncWaitQueue<IDisposable> UpgradeReaderWaitQueue { get { return _rwl._upgradeReaderQueue; } }
}
// ReSharper restore UnusedMember.Local
}
}

@ -0,0 +1,177 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace ZeroLevel.Services.Async
{
/// <summary>
/// An async-compatible semaphore. Alternatively, you could use <c>SemaphoreSlim</c> on .NET 4.5 / Windows Store.
/// </summary>
[DebuggerDisplay("Id = {Id}, CurrentCount = {_count}")]
[DebuggerTypeProxy(typeof(DebugView))]
public sealed class AsyncSemaphore
{
/// <summary>
/// The queue of TCSs that other tasks are awaiting to acquire the semaphore.
/// </summary>
private readonly IAsyncWaitQueue<object> _queue;
/// <summary>
/// The number of waits that will be immediately granted.
/// </summary>
private int _count;
/// <summary>
/// The semi-unique identifier for this instance. This is 0 if the id has not yet been created.
/// </summary>
private int _id;
/// <summary>
/// The object used for mutual exclusion.
/// </summary>
private readonly object _mutex;
/// <summary>
/// Creates a new async-compatible semaphore with the specified initial count.
/// </summary>
/// <param name="initialCount">The initial count for this semaphore. This must be greater than or equal to zero.</param>
/// <param name="queue">The wait queue used to manage waiters.</param>
public AsyncSemaphore(int initialCount, IAsyncWaitQueue<object> queue)
{
_queue = queue;
_count = initialCount;
_mutex = new object();
}
/// <summary>
/// Creates a new async-compatible semaphore with the specified initial count.
/// </summary>
/// <param name="initialCount">The initial count for this semaphore. This must be greater than or equal to zero.</param>
public AsyncSemaphore(int initialCount)
: this(initialCount, new DefaultAsyncWaitQueue<object>())
{
}
/// <summary>
/// Gets a semi-unique identifier for this asynchronous semaphore.
/// </summary>
public int Id
{
get { return IdManager<AsyncSemaphore>.GetId(ref _id); }
}
/// <summary>
/// Gets the number of slots currently available on this semaphore.
/// </summary>
public int CurrentCount
{
get { lock (_mutex) { return _count; } }
}
/// <summary>
/// Asynchronously waits for a slot in the semaphore to be available.
/// </summary>
/// <param name="cancellationToken">The cancellation token used to cancel the wait. If this is already set, then this method will attempt to take the slot immediately (succeeding if a slot is currently available).</param>
public Task WaitAsync(CancellationToken cancellationToken)
{
Task ret;
lock (_mutex)
{
// If the semaphore is available, take it immediately and return.
if (_count != 0)
{
--_count;
ret = TaskConstants.Completed;
}
else
{
// Wait for the semaphore to become available or cancellation.
ret = _queue.Enqueue(cancellationToken);
}
}
return ret;
}
/// <summary>
/// Synchronously waits for a slot in the semaphore to be available. This method may block the calling thread.
/// </summary>
/// <param name="cancellationToken">The cancellation token used to cancel the wait. If this is already set, then this method will attempt to take the slot immediately (succeeding if a slot is currently available).</param>
public void Wait(CancellationToken cancellationToken)
{
WaitAsync(cancellationToken).WaitAndUnwrapException();
}
/// <summary>
/// Asynchronously waits for a slot in the semaphore to be available.
/// </summary>
public Task WaitAsync()
{
return WaitAsync(CancellationToken.None);
}
/// <summary>
/// Synchronously waits for a slot in the semaphore to be available. This method may block the calling thread.
/// </summary>
public void Wait()
{
Wait(CancellationToken.None);
}
/// <summary>
/// Releases the semaphore.
/// </summary>
public void Release(int releaseCount)
{
if (releaseCount == 0)
return;
var finishes = new List<IDisposable>();
lock (_mutex)
{
if (_count > int.MaxValue - releaseCount)
throw new InvalidOperationException("Could not release semaphore.");
var oldCount = _count;
while (releaseCount != 0)
{
if (_queue.IsEmpty)
++_count;
else
finishes.Add(_queue.Dequeue());
--releaseCount;
}
}
foreach (var finish in finishes)
finish.Dispose();
}
/// <summary>
/// Releases the semaphore.
/// </summary>
public void Release()
{
Release(1);
}
// ReSharper disable UnusedMember.Local
[DebuggerNonUserCode]
private sealed class DebugView
{
private readonly AsyncSemaphore _semaphore;
public DebugView(AsyncSemaphore semaphore)
{
_semaphore = semaphore;
}
public int Id { get { return _semaphore.Id; } }
public int CurrentCount { get { return _semaphore._count; } }
public IAsyncWaitQueue<object> WaitQueue { get { return _semaphore._queue; } }
}
// ReSharper restore UnusedMember.Local
}
}

@ -0,0 +1,208 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace ZeroLevel.Services.Async
{
/// <summary>
/// A collection of cancelable <see cref="TaskCompletionSource{T}"/> instances. Implementations must be threadsafe <b>and</b> must work correctly if the caller is holding a lock.
/// </summary>
/// <typeparam name="T">The type of the results. If this isn't needed, use <see cref="Object"/>.</typeparam>
public interface IAsyncWaitQueue<T>
{
/// <summary>
/// Gets whether the queue is empty.
/// </summary>
bool IsEmpty { get; }
/// <summary>
/// Creates a new entry and queues it to this wait queue. The returned task must support both synchronous and asynchronous waits.
/// </summary>
/// <returns>The queued task.</returns>
Task<T> Enqueue();
/// <summary>
/// Removes a single entry in the wait queue. Returns a disposable that completes that entry.
/// </summary>
/// <param name="result">The result used to complete the wait queue entry. If this isn't needed, use <c>default(T)</c>.</param>
IDisposable Dequeue(T result = default(T));
/// <summary>
/// Removes all entries in the wait queue. Returns a disposable that completes all entries.
/// </summary>
/// <param name="result">The result used to complete the wait queue entries. If this isn't needed, use <c>default(T)</c>.</param>
IDisposable DequeueAll(T result = default(T));
/// <summary>
/// Attempts to remove an entry from the wait queue. Returns a disposable that cancels the entry.
/// </summary>
/// <param name="task">The task to cancel.</param>
/// <returns>A value indicating whether the entry was found and canceled.</returns>
IDisposable TryCancel(Task task);
/// <summary>
/// Removes all entries from the wait queue. Returns a disposable that cancels all entries.
/// </summary>
IDisposable CancelAll();
}
/// <summary>
/// Provides extension methods for wait queues.
/// </summary>
public static class AsyncWaitQueueExtensions
{
/// <summary>
/// Creates a new entry and queues it to this wait queue. If the cancellation token is already canceled, this method immediately returns a canceled task without modifying the wait queue.
/// </summary>
/// <param name="this">The wait queue.</param>
/// <param name="token">The token used to cancel the wait.</param>
/// <returns>The queued task.</returns>
public static Task<T> Enqueue<T>(this IAsyncWaitQueue<T> @this, CancellationToken token)
{
if (token.IsCancellationRequested)
return TaskConstants<T>.Canceled;
var ret = @this.Enqueue();
if (token.CanBeCanceled)
{
var registration = token.Register(() => @this.TryCancel(ret).Dispose(), useSynchronizationContext: false);
ret.ContinueWith(_ => registration.Dispose(), CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
}
return ret;
}
}
/// <summary>
/// The default wait queue implementation, which uses a double-ended queue.
/// </summary>
/// <typeparam name="T">The type of the results. If this isn't needed, use <see cref="Object"/>.</typeparam>
[DebuggerDisplay("Count = {Count}")]
[DebuggerTypeProxy(typeof(DefaultAsyncWaitQueue<>.DebugView))]
public sealed class DefaultAsyncWaitQueue<T> : IAsyncWaitQueue<T>
{
private readonly Deque<TaskCompletionSource<T>> _queue = new Deque<TaskCompletionSource<T>>();
private int Count
{
get { lock (_queue) { return _queue.Count; } }
}
bool IAsyncWaitQueue<T>.IsEmpty
{
get { return Count == 0; }
}
Task<T> IAsyncWaitQueue<T>.Enqueue()
{
var tcs = new TaskCompletionSource<T>();
lock (_queue)
_queue.AddToBack(tcs);
return tcs.Task;
}
IDisposable IAsyncWaitQueue<T>.Dequeue(T result)
{
TaskCompletionSource<T> tcs;
lock (_queue)
tcs = _queue.RemoveFromFront();
return new CompleteDisposable(result, tcs);
}
IDisposable IAsyncWaitQueue<T>.DequeueAll(T result)
{
TaskCompletionSource<T>[] taskCompletionSources;
lock (_queue)
{
taskCompletionSources = _queue.ToArray();
_queue.Clear();
}
return new CompleteDisposable(result, taskCompletionSources);
}
IDisposable IAsyncWaitQueue<T>.TryCancel(Task task)
{
TaskCompletionSource<T> tcs = null;
lock (_queue)
{
for (int i = 0; i != _queue.Count; ++i)
{
if (_queue[i].Task == task)
{
tcs = _queue[i];
_queue.RemoveAt(i);
break;
}
}
}
if (tcs == null)
return new CancelDisposable();
return new CancelDisposable(tcs);
}
IDisposable IAsyncWaitQueue<T>.CancelAll()
{
TaskCompletionSource<T>[] taskCompletionSources;
lock (_queue)
{
taskCompletionSources = _queue.ToArray();
_queue.Clear();
}
return new CancelDisposable(taskCompletionSources);
}
private sealed class CancelDisposable : IDisposable
{
private readonly TaskCompletionSource<T>[] _taskCompletionSources;
public CancelDisposable(params TaskCompletionSource<T>[] taskCompletionSources)
{
_taskCompletionSources = taskCompletionSources;
}
public void Dispose()
{
foreach (var cts in _taskCompletionSources)
cts.TrySetCanceled();
}
}
private sealed class CompleteDisposable : IDisposable
{
private readonly TaskCompletionSource<T>[] _taskCompletionSources;
private readonly T _result;
public CompleteDisposable(T result, params TaskCompletionSource<T>[] taskCompletionSources)
{
_result = result;
_taskCompletionSources = taskCompletionSources;
}
public void Dispose()
{
foreach (var cts in _taskCompletionSources)
cts.TrySetResult(_result);
}
}
[DebuggerNonUserCode]
internal sealed class DebugView
{
private readonly DefaultAsyncWaitQueue<T> _queue;
public DebugView(DefaultAsyncWaitQueue<T> queue)
{
_queue = queue;
}
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
public Task<T>[] Tasks
{
get { return _queue._queue.Select(x => x.Task).ToArray(); }
}
}
}
}

@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace ZeroLevel.Services.Async
{
/// <summary>
/// Helper methods for cancellation tokens.
/// </summary>
public static class CancellationTokenHelpers
{
/// <summary>
/// Initializes the static members.
/// </summary>
static CancellationTokenHelpers()
{
Canceled = new CancellationToken(true);
}
/// <summary>
/// Gets <see cref="CancellationToken.None"/>, a cancellation token that is never canceled.
/// </summary>
public static CancellationToken None { get { return CancellationToken.None; } }
/// <summary>
/// Gets a cancellation token that is already canceled.
/// </summary>
public static CancellationToken Canceled { get; private set; }
/// <summary>
/// Creates a cancellation token that is canceled after the due time.
/// </summary>
/// <param name="dueTime">The due time after which to cancel the token.</param>
/// <returns>A cancellation token that is canceled after the due time.</returns>
public static NormalizedCancellationToken Timeout(TimeSpan dueTime)
{
var cts = new CancellationTokenSource();
cts.CancelAfter(dueTime);
return new NormalizedCancellationToken(cts);
}
/// <summary>
/// Creates a cancellation token that is canceled after the due time.
/// </summary>
/// <param name="dueTime">The due time after which to cancel the token.</param>
/// <returns>A cancellation token that is canceled after the due time.</returns>
public static NormalizedCancellationToken Timeout(int dueTime)
{
var cts = new CancellationTokenSource();
cts.CancelAfter(dueTime);
return new NormalizedCancellationToken(cts);
}
/// <summary>
/// Reduces a set of cancellation tokens by removing any cancellation tokens that cannot be canceled. If any tokens are already canceled, the returned token will be canceled.
/// </summary>
/// <param name="cancellationTokens">The cancellation tokens to reduce.</param>
public static NormalizedCancellationToken Normalize(params CancellationToken[] cancellationTokens)
{
return Normalize((IEnumerable<CancellationToken>)cancellationTokens);
}
/// <summary>
/// Reduces a set of cancellation tokens by removing any cancellation tokens that cannot be canceled. If any tokens are already canceled, the returned token will be canceled.
/// </summary>
/// <param name="cancellationTokens">The cancellation tokens to reduce.</param>
public static NormalizedCancellationToken Normalize(IEnumerable<CancellationToken> cancellationTokens)
{
var tokens = cancellationTokens.Where(t => t.CanBeCanceled).ToArray();
if (tokens.Length == 0)
return new NormalizedCancellationToken();
if (tokens.Length == 1)
return new NormalizedCancellationToken(tokens[0]);
var alreadyCanceled = tokens.FirstOrDefault(t => t.IsCancellationRequested);
if (alreadyCanceled.IsCancellationRequested)
return new NormalizedCancellationToken(alreadyCanceled);
return new NormalizedCancellationToken(CancellationTokenSource.CreateLinkedTokenSource(tokens));
}
/// <summary>
/// Creates a cancellation token that is canceled when the provided <see cref="Task"/> completes.
/// </summary>
/// <param name="source">The task to observe.</param>
/// <param name="continuationOptions">The options to use for the task continuation.</param>
public static NormalizedCancellationToken FromTask(Task source, TaskContinuationOptions continuationOptions)
{
var cts = new CancellationTokenSource();
source.ContinueWith(_ => cts.Cancel(), CancellationToken.None, continuationOptions, TaskScheduler.Default);
return new NormalizedCancellationToken(cts);
}
/// <summary>
/// Creates a cancellation token that is canceled when the provided <see cref="Task"/> completes.
/// </summary>
/// <param name="source">The task to observe.</param>
public static NormalizedCancellationToken FromTask(Task source)
{
return FromTask(source, TaskContinuationOptions.None);
}
}
}

@ -0,0 +1,47 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ZeroLevel.Services.Async
{
/// <summary>
/// Holds the task for a cancellation token, as well as the token registration. The registration is disposed when this instance is disposed.
/// </summary>
public sealed class CancellationTokenTaskSource<T>
: IDisposable
{
/// <summary>
/// The cancellation token registration, if any. This is <c>null</c> if the registration was not necessary.
/// </summary>
private readonly IDisposable _registration;
/// <summary>
/// Creates a task for the specified cancellation token, registering with the token if necessary.
/// </summary>
/// <param name="cancellationToken">The cancellation token to observe.</param>
public CancellationTokenTaskSource(CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
Task = System.Threading.Tasks.Task.FromCanceled<T>(cancellationToken);
return;
}
var tcs = new TaskCompletionSource<T>();
_registration = cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken), useSynchronizationContext: false);
Task = tcs.Task;
}
/// <summary>
/// Gets the task for the source cancellation token.
/// </summary>
public Task<T> Task { get; private set; }
/// <summary>
/// Disposes the cancellation token registration, if any. Note that this may cause <see cref="Task"/> to never complete.
/// </summary>
public void Dispose()
{
_registration?.Dispose();
}
}
}

@ -0,0 +1,836 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace ZeroLevel.Services.Async
{
/// <summary>
/// A double-ended queue (deque), which provides O(1) indexed access, O(1) removals from the front and back, amortized O(1) insertions to the front and back, and O(N) insertions and removals anywhere else (with the operations getting slower as the index approaches the middle).
/// </summary>
/// <typeparam name="T">The type of elements contained in the deque.</typeparam>
[DebuggerDisplay("Count = {Count}, Capacity = {Capacity}")]
[DebuggerTypeProxy(typeof(Deque<>.DebugView))]
internal sealed class Deque<T> : IList<T>, System.Collections.IList
{
/// <summary>
/// The default capacity.
/// </summary>
private const int DefaultCapacity = 8;
/// <summary>
/// The circular buffer that holds the view.
/// </summary>
private T[] buffer;
/// <summary>
/// The offset into <see cref="buffer"/> where the view begins.
/// </summary>
private int offset;
/// <summary>
/// Initializes a new instance of the <see cref="Deque&lt;T&gt;"/> class with the specified capacity.
/// </summary>
/// <param name="capacity">The initial capacity. Must be greater than <c>0</c>.</param>
public Deque(int capacity)
{
if (capacity < 1)
throw new ArgumentOutOfRangeException("capacity", "Capacity must be greater than 0.");
buffer = new T[capacity];
}
/// <summary>
/// Initializes a new instance of the <see cref="Deque&lt;T&gt;"/> class with the elements from the specified collection.
/// </summary>
/// <param name="collection">The collection.</param>
public Deque(IEnumerable<T> collection)
{
int count = collection.Count();
if (count > 0)
{
buffer = new T[count];
DoInsertRange(0, collection, count);
}
else
{
buffer = new T[DefaultCapacity];
}
}
/// <summary>
/// Initializes a new instance of the <see cref="Deque&lt;T&gt;"/> class.
/// </summary>
public Deque()
: this(DefaultCapacity)
{
}
#region GenericListImplementations
/// <summary>
/// Gets a value indicating whether this list is read-only. This implementation always returns <c>false</c>.
/// </summary>
/// <returns>true if this list is read-only; otherwise, false.</returns>
bool ICollection<T>.IsReadOnly
{
get { return false; }
}
/// <summary>
/// Gets or sets the item at the specified index.
/// </summary>
/// <param name="index">The index of the item to get or set.</param>
/// <exception cref="T:System.ArgumentOutOfRangeException"><paramref name="index"/> is not a valid index in this list.</exception>
/// <exception cref="T:System.NotSupportedException">This property is set and the list is read-only.</exception>
public T this[int index]
{
get
{
CheckExistingIndexArgument(this.Count, index);
return DoGetItem(index);
}
set
{
CheckExistingIndexArgument(this.Count, index);
DoSetItem(index, value);
}
}
/// <summary>
/// Inserts an item to this list at the specified index.
/// </summary>
/// <param name="index">The zero-based index at which <paramref name="item"/> should be inserted.</param>
/// <param name="item">The object to insert into this list.</param>
/// <exception cref="T:System.ArgumentOutOfRangeException">
/// <paramref name="index"/> is not a valid index in this list.
/// </exception>
/// <exception cref="T:System.NotSupportedException">
/// This list is read-only.
/// </exception>
public void Insert(int index, T item)
{
CheckNewIndexArgument(Count, index);
DoInsert(index, item);
}
/// <summary>
/// Removes the item at the specified index.
/// </summary>
/// <param name="index">The zero-based index of the item to remove.</param>
/// <exception cref="T:System.ArgumentOutOfRangeException">
/// <paramref name="index"/> is not a valid index in this list.
/// </exception>
/// <exception cref="T:System.NotSupportedException">
/// This list is read-only.
/// </exception>
public void RemoveAt(int index)
{
CheckExistingIndexArgument(Count, index);
DoRemoveAt(index);
}
/// <summary>
/// Determines the index of a specific item in this list.
/// </summary>
/// <param name="item">The object to locate in this list.</param>
/// <returns>The index of <paramref name="item"/> if found in this list; otherwise, -1.</returns>
public int IndexOf(T item)
{
var comparer = EqualityComparer<T>.Default;
int ret = 0;
foreach (var sourceItem in this)
{
if (comparer.Equals(item, sourceItem))
return ret;
++ret;
}
return -1;
}
/// <summary>
/// Adds an item to the end of this list.
/// </summary>
/// <param name="item">The object to add to this list.</param>
/// <exception cref="T:System.NotSupportedException">
/// This list is read-only.
/// </exception>
void ICollection<T>.Add(T item)
{
DoInsert(Count, item);
}
/// <summary>
/// Determines whether this list contains a specific value.
/// </summary>
/// <param name="item">The object to locate in this list.</param>
/// <returns>
/// true if <paramref name="item"/> is found in this list; otherwise, false.
/// </returns>
bool ICollection<T>.Contains(T item)
{
return this.Contains(item, null);
}
/// <summary>
/// Copies the elements of this list to an <see cref="T:System.Array"/>, starting at a particular <see cref="T:System.Array"/> index.
/// </summary>
/// <param name="array">The one-dimensional <see cref="T:System.Array"/> that is the destination of the elements copied from this slice. The <see cref="T:System.Array"/> must have zero-based indexing.</param>
/// <param name="arrayIndex">The zero-based index in <paramref name="array"/> at which copying begins.</param>
/// <exception cref="T:System.ArgumentNullException">
/// <paramref name="array"/> is null.
/// </exception>
/// <exception cref="T:System.ArgumentOutOfRangeException">
/// <paramref name="arrayIndex"/> is less than 0.
/// </exception>
/// <exception cref="T:System.ArgumentException">
/// <paramref name="arrayIndex"/> is equal to or greater than the length of <paramref name="array"/>.
/// -or-
/// The number of elements in the source <see cref="T:System.Collections.Generic.ICollection`1"/> is greater than the available space from <paramref name="arrayIndex"/> to the end of the destination <paramref name="array"/>.
/// </exception>
void ICollection<T>.CopyTo(T[] array, int arrayIndex)
{
if (array == null)
throw new ArgumentNullException("array", "Array is null");
int count = this.Count;
CheckRangeArguments(array.Length, arrayIndex, count);
for (int i = 0; i != count; ++i)
{
array[arrayIndex + i] = this[i];
}
}
/// <summary>
/// Removes the first occurrence of a specific object from this list.
/// </summary>
/// <param name="item">The object to remove from this list.</param>
/// <returns>
/// true if <paramref name="item"/> was successfully removed from this list; otherwise, false. This method also returns false if <paramref name="item"/> is not found in this list.
/// </returns>
/// <exception cref="T:System.NotSupportedException">
/// This list is read-only.
/// </exception>
public bool Remove(T item)
{
int index = IndexOf(item);
if (index == -1)
return false;
DoRemoveAt(index);
return true;
}
/// <summary>
/// Returns an enumerator that iterates through the collection.
/// </summary>
/// <returns>
/// A <see cref="T:System.Collections.Generic.IEnumerator`1"/> that can be used to iterate through the collection.
/// </returns>
public IEnumerator<T> GetEnumerator()
{
int count = this.Count;
for (int i = 0; i != count; ++i)
{
yield return DoGetItem(i);
}
}
/// <summary>
/// Returns an enumerator that iterates through a collection.
/// </summary>
/// <returns>
/// An <see cref="T:System.Collections.IEnumerator"/> object that can be used to iterate through the collection.
/// </returns>
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
#endregion
#region ObjectListImplementations
int System.Collections.IList.Add(object value)
{
AddToBack((T)value);
return Count - 1;
}
bool System.Collections.IList.Contains(object value)
{
return this.Contains((T)value);
}
int System.Collections.IList.IndexOf(object value)
{
return IndexOf((T)value);
}
void System.Collections.IList.Insert(int index, object value)
{
Insert(index, (T)value);
}
bool System.Collections.IList.IsFixedSize
{
get { return false; }
}
bool System.Collections.IList.IsReadOnly
{
get { return false; }
}
void System.Collections.IList.Remove(object value)
{
Remove((T)value);
}
object System.Collections.IList.this[int index]
{
get
{
return this[index];
}
set
{
this[index] = (T)value;
}
}
void System.Collections.ICollection.CopyTo(Array array, int index)
{
if (array == null)
throw new ArgumentNullException("array", "Destination array cannot be null.");
CheckRangeArguments(array.Length, index, Count);
for (int i = 0; i != Count; ++i)
{
try
{
array.SetValue(this[i], index + i);
}
catch (InvalidCastException ex)
{
throw new ArgumentException("Destination array is of incorrect type.", ex);
}
}
}
bool System.Collections.ICollection.IsSynchronized
{
get { return false; }
}
object System.Collections.ICollection.SyncRoot
{
get { return this; }
}
#endregion
#region GenericListHelpers
/// <summary>
/// Checks the <paramref name="index"/> argument to see if it refers to a valid insertion point in a source of a given length.
/// </summary>
/// <param name="sourceLength">The length of the source. This parameter is not checked for validity.</param>
/// <param name="index">The index into the source.</param>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is not a valid index to an insertion point for the source.</exception>
private static void CheckNewIndexArgument(int sourceLength, int index)
{
if (index < 0 || index > sourceLength)
{
throw new ArgumentOutOfRangeException("index", "Invalid new index " + index + " for source length " + sourceLength);
}
}
/// <summary>
/// Checks the <paramref name="index"/> argument to see if it refers to an existing element in a source of a given length.
/// </summary>
/// <param name="sourceLength">The length of the source. This parameter is not checked for validity.</param>
/// <param name="index">The index into the source.</param>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is not a valid index to an existing element for the source.</exception>
private static void CheckExistingIndexArgument(int sourceLength, int index)
{
if (index < 0 || index >= sourceLength)
{
throw new ArgumentOutOfRangeException("index", "Invalid existing index " + index + " for source length " + sourceLength);
}
}
/// <summary>
/// Checks the <paramref name="offset"/> and <paramref name="count"/> arguments for validity when applied to a source of a given length. Allows 0-element ranges, including a 0-element range at the end of the source.
/// </summary>
/// <param name="sourceLength">The length of the source. This parameter is not checked for validity.</param>
/// <param name="offset">The index into source at which the range begins.</param>
/// <param name="count">The number of elements in the range.</param>
/// <exception cref="ArgumentOutOfRangeException">Either <paramref name="offset"/> or <paramref name="count"/> is less than 0.</exception>
/// <exception cref="ArgumentException">The range [offset, offset + count) is not within the range [0, sourceLength).</exception>
private static void CheckRangeArguments(int sourceLength, int offset, int count)
{
if (offset < 0)
{
throw new ArgumentOutOfRangeException("offset", "Invalid offset " + offset);
}
if (count < 0)
{
throw new ArgumentOutOfRangeException("count", "Invalid count " + count);
}
if (sourceLength - offset < count)
{
throw new ArgumentException("Invalid offset (" + offset + ") or count + (" + count + ") for source length " + sourceLength);
}
}
#endregion
/// <summary>
/// Gets a value indicating whether this instance is empty.
/// </summary>
private bool IsEmpty
{
get { return Count == 0; }
}
/// <summary>
/// Gets a value indicating whether this instance is at full capacity.
/// </summary>
private bool IsFull
{
get { return Count == Capacity; }
}
/// <summary>
/// Gets a value indicating whether the buffer is "split" (meaning the beginning of the view is at a later index in <see cref="buffer"/> than the end).
/// </summary>
private bool IsSplit
{
get
{
// Overflow-safe version of "(offset + Count) > Capacity"
return offset > (Capacity - Count);
}
}
/// <summary>
/// Gets or sets the capacity for this deque. This value must always be greater than zero, and this property cannot be set to a value less than <see cref="Count"/>.
/// </summary>
/// <exception cref="InvalidOperationException"><c>Capacity</c> cannot be set to a value less than <see cref="Count"/>.</exception>
public int Capacity
{
get
{
return buffer.Length;
}
set
{
if (value < 1)
throw new ArgumentOutOfRangeException("value", "Capacity must be greater than 0.");
if (value < Count)
throw new InvalidOperationException("Capacity cannot be set to a value less than Count");
if (value == buffer.Length)
return;
// Create the new buffer and copy our existing range.
T[] newBuffer = new T[value];
if (IsSplit)
{
// The existing buffer is split, so we have to copy it in parts
int length = Capacity - offset;
Array.Copy(buffer, offset, newBuffer, 0, length);
Array.Copy(buffer, 0, newBuffer, length, Count - length);
}
else
{
// The existing buffer is whole
Array.Copy(buffer, offset, newBuffer, 0, Count);
}
// Set up to use the new buffer.
buffer = newBuffer;
offset = 0;
}
}
/// <summary>
/// Gets the number of elements contained in this deque.
/// </summary>
/// <returns>The number of elements contained in this deque.</returns>
public int Count { get; private set; }
/// <summary>
/// Applies the offset to <paramref name="index"/>, resulting in a buffer index.
/// </summary>
/// <param name="index">The deque index.</param>
/// <returns>The buffer index.</returns>
private int DequeIndexToBufferIndex(int index)
{
return (index + offset) % Capacity;
}
/// <summary>
/// Gets an element at the specified view index.
/// </summary>
/// <param name="index">The zero-based view index of the element to get. This index is guaranteed to be valid.</param>
/// <returns>The element at the specified index.</returns>
private T DoGetItem(int index)
{
return buffer[DequeIndexToBufferIndex(index)];
}
/// <summary>
/// Sets an element at the specified view index.
/// </summary>
/// <param name="index">The zero-based view index of the element to get. This index is guaranteed to be valid.</param>
/// <param name="item">The element to store in the list.</param>
private void DoSetItem(int index, T item)
{
buffer[DequeIndexToBufferIndex(index)] = item;
}
/// <summary>
/// Inserts an element at the specified view index.
/// </summary>
/// <param name="index">The zero-based view index at which the element should be inserted. This index is guaranteed to be valid.</param>
/// <param name="item">The element to store in the list.</param>
private void DoInsert(int index, T item)
{
EnsureCapacityForOneElement();
if (index == 0)
{
DoAddToFront(item);
return;
}
else if (index == Count)
{
DoAddToBack(item);
return;
}
DoInsertRange(index, new[] { item }, 1);
}
/// <summary>
/// Removes an element at the specified view index.
/// </summary>
/// <param name="index">The zero-based view index of the element to remove. This index is guaranteed to be valid.</param>
private void DoRemoveAt(int index)
{
if (index == 0)
{
DoRemoveFromFront();
return;
}
else if (index == Count - 1)
{
DoRemoveFromBack();
return;
}
DoRemoveRange(index, 1);
}
/// <summary>
/// Increments <see cref="offset"/> by <paramref name="value"/> using modulo-<see cref="Capacity"/> arithmetic.
/// </summary>
/// <param name="value">The value by which to increase <see cref="offset"/>. May not be negative.</param>
/// <returns>The value of <see cref="offset"/> after it was incremented.</returns>
private int PostIncrement(int value)
{
int ret = offset;
offset += value;
offset %= Capacity;
return ret;
}
/// <summary>
/// Decrements <see cref="offset"/> by <paramref name="value"/> using modulo-<see cref="Capacity"/> arithmetic.
/// </summary>
/// <param name="value">The value by which to reduce <see cref="offset"/>. May not be negative or greater than <see cref="Capacity"/>.</param>
/// <returns>The value of <see cref="offset"/> before it was decremented.</returns>
private int PreDecrement(int value)
{
offset -= value;
if (offset < 0)
offset += Capacity;
return offset;
}
/// <summary>
/// Inserts a single element to the back of the view. <see cref="IsFull"/> must be false when this method is called.
/// </summary>
/// <param name="value">The element to insert.</param>
private void DoAddToBack(T value)
{
buffer[DequeIndexToBufferIndex(Count)] = value;
++Count;
}
/// <summary>
/// Inserts a single element to the front of the view. <see cref="IsFull"/> must be false when this method is called.
/// </summary>
/// <param name="value">The element to insert.</param>
private void DoAddToFront(T value)
{
buffer[PreDecrement(1)] = value;
++Count;
}
/// <summary>
/// Removes and returns the last element in the view. <see cref="IsEmpty"/> must be false when this method is called.
/// </summary>
/// <returns>The former last element.</returns>
private T DoRemoveFromBack()
{
T ret = buffer[DequeIndexToBufferIndex(Count - 1)];
--Count;
return ret;
}
/// <summary>
/// Removes and returns the first element in the view. <see cref="IsEmpty"/> must be false when this method is called.
/// </summary>
/// <returns>The former first element.</returns>
private T DoRemoveFromFront()
{
--Count;
return buffer[PostIncrement(1)];
}
/// <summary>
/// Inserts a range of elements into the view.
/// </summary>
/// <param name="index">The index into the view at which the elements are to be inserted.</param>
/// <param name="collection">The elements to insert.</param>
/// <param name="collectionCount">The number of elements in <paramref name="collection"/>. Must be greater than zero, and the sum of <paramref name="collectionCount"/> and <see cref="Count"/> must be less than or equal to <see cref="Capacity"/>.</param>
private void DoInsertRange(int index, IEnumerable<T> collection, int collectionCount)
{
// Make room in the existing list
if (index < Count / 2)
{
// Inserting into the first half of the list
// Move lower items down: [0, index) -> [Capacity - collectionCount, Capacity - collectionCount + index)
// This clears out the low "index" number of items, moving them "collectionCount" places down;
// after rotation, there will be a "collectionCount"-sized hole at "index".
int copyCount = index;
int writeIndex = Capacity - collectionCount;
for (int j = 0; j != copyCount; ++j)
buffer[DequeIndexToBufferIndex(writeIndex + j)] = buffer[DequeIndexToBufferIndex(j)];
// Rotate to the new view
this.PreDecrement(collectionCount);
}
else
{
// Inserting into the second half of the list
// Move higher items up: [index, count) -> [index + collectionCount, collectionCount + count)
int copyCount = Count - index;
int writeIndex = index + collectionCount;
for (int j = copyCount - 1; j != -1; --j)
buffer[DequeIndexToBufferIndex(writeIndex + j)] = buffer[DequeIndexToBufferIndex(index + j)];
}
// Copy new items into place
int i = index;
foreach (T item in collection)
{
buffer[DequeIndexToBufferIndex(i)] = item;
++i;
}
// Adjust valid count
Count += collectionCount;
}
/// <summary>
/// Removes a range of elements from the view.
/// </summary>
/// <param name="index">The index into the view at which the range begins.</param>
/// <param name="collectionCount">The number of elements in the range. This must be greater than 0 and less than or equal to <see cref="Count"/>.</param>
private void DoRemoveRange(int index, int collectionCount)
{
if (index == 0)
{
// Removing from the beginning: rotate to the new view
this.PostIncrement(collectionCount);
Count -= collectionCount;
return;
}
else if (index == Count - collectionCount)
{
// Removing from the ending: trim the existing view
Count -= collectionCount;
return;
}
if ((index + (collectionCount / 2)) < Count / 2)
{
// Removing from first half of list
// Move lower items up: [0, index) -> [collectionCount, collectionCount + index)
int copyCount = index;
int writeIndex = collectionCount;
for (int j = copyCount - 1; j != -1; --j)
buffer[DequeIndexToBufferIndex(writeIndex + j)] = buffer[DequeIndexToBufferIndex(j)];
// Rotate to new view
this.PostIncrement(collectionCount);
}
else
{
// Removing from second half of list
// Move higher items down: [index + collectionCount, count) -> [index, count - collectionCount)
int copyCount = Count - collectionCount - index;
int readIndex = index + collectionCount;
for (int j = 0; j != copyCount; ++j)
buffer[DequeIndexToBufferIndex(index + j)] = buffer[DequeIndexToBufferIndex(readIndex + j)];
}
// Adjust valid count
Count -= collectionCount;
}
/// <summary>
/// Doubles the capacity if necessary to make room for one more element. When this method returns, <see cref="IsFull"/> is false.
/// </summary>
private void EnsureCapacityForOneElement()
{
if (this.IsFull)
{
this.Capacity = this.Capacity * 2;
}
}
/// <summary>
/// Inserts a single element at the back of this deque.
/// </summary>
/// <param name="value">The element to insert.</param>
public void AddToBack(T value)
{
EnsureCapacityForOneElement();
DoAddToBack(value);
}
/// <summary>
/// Inserts a single element at the front of this deque.
/// </summary>
/// <param name="value">The element to insert.</param>
public void AddToFront(T value)
{
EnsureCapacityForOneElement();
DoAddToFront(value);
}
/// <summary>
/// Inserts a collection of elements into this deque.
/// </summary>
/// <param name="index">The index at which the collection is inserted.</param>
/// <param name="collection">The collection of elements to insert.</param>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is not a valid index to an insertion point for the source.</exception>
public void InsertRange(int index, IEnumerable<T> collection)
{
int collectionCount = collection.Count();
CheckNewIndexArgument(Count, index);
// Overflow-safe check for "this.Count + collectionCount > this.Capacity"
if (collectionCount > Capacity - Count)
{
this.Capacity = checked(Count + collectionCount);
}
if (collectionCount == 0)
{
return;
}
this.DoInsertRange(index, collection, collectionCount);
}
/// <summary>
/// Removes a range of elements from this deque.
/// </summary>
/// <param name="offset">The index into the deque at which the range begins.</param>
/// <param name="count">The number of elements to remove.</param>
/// <exception cref="ArgumentOutOfRangeException">Either <paramref name="offset"/> or <paramref name="count"/> is less than 0.</exception>
/// <exception cref="ArgumentException">The range [<paramref name="offset"/>, <paramref name="offset"/> + <paramref name="count"/>) is not within the range [0, <see cref="Count"/>).</exception>
public void RemoveRange(int offset, int count)
{
CheckRangeArguments(Count, offset, count);
if (count == 0)
{
return;
}
this.DoRemoveRange(offset, count);
}
/// <summary>
/// Removes and returns the last element of this deque.
/// </summary>
/// <returns>The former last element.</returns>
/// <exception cref="InvalidOperationException">The deque is empty.</exception>
public T RemoveFromBack()
{
if (this.IsEmpty)
throw new InvalidOperationException("The deque is empty.");
return this.DoRemoveFromBack();
}
/// <summary>
/// Removes and returns the first element of this deque.
/// </summary>
/// <returns>The former first element.</returns>
/// <exception cref="InvalidOperationException">The deque is empty.</exception>
public T RemoveFromFront()
{
if (this.IsEmpty)
throw new InvalidOperationException("The deque is empty.");
return this.DoRemoveFromFront();
}
/// <summary>
/// Removes all items from this deque.
/// </summary>
public void Clear()
{
this.offset = 0;
this.Count = 0;
}
[DebuggerNonUserCode]
private sealed class DebugView
{
private readonly Deque<T> deque;
public DebugView(Deque<T> deque)
{
this.deque = deque;
}
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
public T[] Items
{
get
{
var array = new T[deque.Count];
((ICollection<T>)deque).CopyTo(array, 0);
return array;
}
}
}
}
}

@ -0,0 +1,23 @@
using System;
using System.Runtime.ExceptionServices;
namespace ZeroLevel.Services.Async
{
/// <summary>
/// Provides helper (non-extension) methods dealing with exceptions.
/// </summary>
public static class ExceptionHelpers
{
/// <summary>
/// Attempts to prepare the exception for re-throwing by preserving the stack trace.
/// The returned exception should be immediately thrown.
/// </summary>
/// <param name="exception">The exception. May not be <c>null</c>.</param>
/// <returns>The <see cref="Exception"/> that was passed into this method.</returns>
public static Exception PrepareForRethrow(Exception exception)
{
ExceptionDispatchInfo.Capture(exception).Throw();
return exception;
}
}
}

@ -0,0 +1,48 @@
using System.Threading;
namespace ZeroLevel.Services.Async
{
/// <summary>
/// Allocates Ids for instances on demand. 0 is an invalid/unassigned Id. Ids may be non-unique in very long-running systems. This is similar to the Id system used by <see cref="System.Threading.Tasks.Task"/> and <see cref="System.Threading.Tasks.TaskScheduler"/>.
/// </summary>
/// <typeparam name="TTag">The type for which ids are generated.</typeparam>
// ReSharper disable UnusedTypeParameter
internal static class IdManager<TTag>
// ReSharper restore UnusedTypeParameter
{
/// <summary>
/// The last id generated for this type. This is 0 if no ids have been generated.
/// </summary>
// ReSharper disable StaticFieldInGenericType
private static int _lastId;
// ReSharper restore StaticFieldInGenericType
/// <summary>
/// Returns the id, allocating it if necessary.
/// </summary>
/// <param name="id">A reference to the field containing the id.</param>
public static int GetId(ref int id)
{
// If the Id has already been assigned, just use it.
if (id != 0)
return id;
// Determine the new Id without modifying "id", since other threads may also be determining the new Id at the same time.
int newId;
// The Increment is in a while loop to ensure we get a non-zero Id:
// If we are incrementing -1, then we want to skip over 0.
// If there are tons of Id allocations going on, we want to skip over 0 no matter how many times we get it.
do
{
newId = Interlocked.Increment(ref _lastId);
} while (newId == 0);
// Update the Id unless another thread already updated it.
Interlocked.CompareExchange(ref id, newId, 0);
// Return the current Id, regardless of whether it's our new Id or a new Id from another thread.
return id;
}
}
}

@ -0,0 +1,61 @@
using System;
using System.Threading;
namespace ZeroLevel.Services.Async
{
/// <summary>
/// A <see cref="Token"/> that may or may not also reference its own <see cref="CancellationTokenSource"/>. Instances of this type should always be disposed.
/// </summary>
public sealed class NormalizedCancellationToken : IDisposable
{
/// <summary>
/// The <see cref="CancellationTokenSource"/>, if any. If this is not <c>null</c>, then <see cref="_token"/> is <c>_cts.Token</c>.
/// </summary>
private readonly CancellationTokenSource _cts;
/// <summary>
/// The <see cref="Token"/>. If <see cref="_cts"/> is not <c>null</c>, then this is <c>_cts.Token</c>.
/// </summary>
private readonly CancellationToken _token;
/// <summary>
/// Creates a normalized cancellation token that can never be canceled.
/// </summary>
public NormalizedCancellationToken()
{
}
/// <summary>
/// Creates a normalized cancellation token from a <see cref="CancellationTokenSource"/>. <see cref="Token"/> is set to the <see cref="CancellationTokenSource.Token"/> property of <paramref name="cts"/>.
/// </summary>
/// <param name="cts">The source for this token.</param>
public NormalizedCancellationToken(CancellationTokenSource cts)
{
_cts = cts;
_token = cts.Token;
}
/// <summary>
/// Creates a normalized cancellation token from a <see cref="CancellationToken"/>. <see cref="Token"/> is set to <paramref name="token"/>.
/// </summary>
/// <param name="token">The source for this token.</param>
public NormalizedCancellationToken(CancellationToken token)
{
_token = token;
}
/// <summary>
/// Releases any resources used by this normalized cancellation token.
/// </summary>
public void Dispose()
{
if (_cts != null)
_cts.Dispose();
}
/// <summary>
/// Gets the <see cref="CancellationToken"/> for this normalized cancellation token.
/// </summary>
public CancellationToken Token { get { return _token; } }
}
}

@ -0,0 +1,137 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace ZeroLevel.Services.Async
{
/// <summary>
/// Represents the producer side of a <see cref="System.Threading.Tasks.Task"/> unbound to a delegate, providing access to the consumer side through the <see cref="Task"/> property.
/// </summary>
public sealed class TaskCompletionSource
{
/// <summary>
/// The underlying TCS.
/// </summary>
private readonly TaskCompletionSource<object> _tcs;
/// <summary>
/// Initializes a new instance of the <see cref="TaskCompletionSource"/> class.
/// </summary>
public TaskCompletionSource()
{
_tcs = new TaskCompletionSource<object>();
}
/// <summary>
/// Initializes a new instance of the <see cref="TaskCompletionSource"/> class with the specified state.
/// </summary>
/// <param name="state">The state to use as the underlying <see cref="Task"/>'s <see cref="IAsyncResult.AsyncState"/>.</param>
public TaskCompletionSource(object state)
{
_tcs = new TaskCompletionSource<object>(state);
}
/// <summary>
/// Initializes a new instance of the <see cref="TaskCompletionSource"/> class with the specified options.
/// </summary>
/// <param name="creationOptions">The options to use when creating the underlying <see cref="Task"/>.</param>
public TaskCompletionSource(TaskCreationOptions creationOptions)
{
_tcs = new TaskCompletionSource<object>(creationOptions);
}
/// <summary>
/// Initializes a new instance of the <see cref="TaskCompletionSource"/> class with the specified state and options.
/// </summary>
/// <param name="state">The state to use as the underlying <see cref="Task"/>'s <see cref="IAsyncResult.AsyncState"/>.</param>
/// <param name="creationOptions">The options to use when creating the underlying <see cref="Task"/>.</param>
public TaskCompletionSource(object state, TaskCreationOptions creationOptions)
{
_tcs = new TaskCompletionSource<object>(state, creationOptions);
}
/// <summary>
/// Gets the <see cref="Task"/> created by this <see cref="TaskCompletionSource"/>.
/// </summary>
public Task Task
{
get { return _tcs.Task; }
}
/// <summary>
/// Transitions the underlying <see cref="Task"/> into the <see cref="TaskStatus.Canceled"/> state.
/// </summary>
/// <exception cref="InvalidOperationException">The underlying <see cref="Task"/> has already been completed.</exception>
public void SetCanceled()
{
_tcs.SetCanceled();
}
/// <summary>
/// Attempts to transition the underlying <see cref="Task"/> into the <see cref="TaskStatus.Canceled"/> state.
/// </summary>
/// <returns><c>true</c> if the operation was successful; otherwise, <c>false</c>.</returns>
public bool TrySetCanceled()
{
return _tcs.TrySetCanceled();
}
/// <summary>
/// Transitions the underlying <see cref="Task"/> into the <see cref="TaskStatus.Faulted"/> state.
/// </summary>
/// <param name="exception">The exception to bind to this <see cref="Task"/>. May not be <c>null</c>.</param>
/// <exception cref="InvalidOperationException">The underlying <see cref="Task"/> has already been completed.</exception>
public void SetException(Exception exception)
{
_tcs.SetException(exception);
}
/// <summary>
/// Transitions the underlying <see cref="Task"/> into the <see cref="TaskStatus.Faulted"/> state.
/// </summary>
/// <param name="exceptions">The collection of exceptions to bind to this <see cref="Task"/>. May not be <c>null</c> or contain <c>null</c> elements.</param>
/// <exception cref="InvalidOperationException">The underlying <see cref="Task"/> has already been completed.</exception>
public void SetException(IEnumerable<Exception> exceptions)
{
_tcs.SetException(exceptions);
}
/// <summary>
/// Attempts to transition the underlying <see cref="Task"/> into the <see cref="TaskStatus.Faulted"/> state.
/// </summary>
/// <param name="exception">The exception to bind to this <see cref="Task"/>. May not be <c>null</c>.</param>
/// <returns><c>true</c> if the operation was successful; otherwise, <c>false</c>.</returns>
public bool TrySetException(Exception exception)
{
return _tcs.TrySetException(exception);
}
/// <summary>
/// Attempts to transition the underlying <see cref="Task"/> into the <see cref="TaskStatus.Faulted"/> state.
/// </summary>
/// <param name="exceptions">The collection of exceptions to bind to this <see cref="Task"/>. May not be <c>null</c> or contain <c>null</c> elements.</param>
/// <returns><c>true</c> if the operation was successful; otherwise, <c>false</c>.</returns>
public bool TrySetException(IEnumerable<Exception> exceptions)
{
return _tcs.TrySetException(exceptions);
}
/// <summary>
/// Transitions the underlying <see cref="Task"/> into the <see cref="TaskStatus.RanToCompletion"/> state.
/// </summary>
/// <exception cref="InvalidOperationException">The underlying <see cref="Task"/> has already been completed.</exception>
public void SetResult()
{
_tcs.SetResult(null);
}
/// <summary>
/// Attempts to transition the underlying <see cref="Task"/> into the <see cref="TaskStatus.RanToCompletion"/> state.
/// </summary>
/// <returns><c>true</c> if the operation was successful; otherwise, <c>false</c>.</returns>
public bool TrySetResult()
{
return _tcs.TrySetResult(null);
}
}
}

@ -0,0 +1,278 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading.Tasks;
namespace ZeroLevel.Services.Async
{
/// <summary>
/// Provides extension methods for <see cref="TaskCompletionSource{TResult}"/>.
/// </summary>
public static class TaskCompletionSourceExtensions
{
/// <summary>
/// Attempts to complete a <see cref="TaskCompletionSource{TResult}"/>, propagating the completion of <paramref name="task"/>.
/// </summary>
/// <typeparam name="TResult">The type of the result of the target asynchronous operation.</typeparam>
/// <typeparam name="TSourceResult">The type of the result of the source asynchronous operation.</typeparam>
/// <param name="this">The task completion source. May not be <c>null</c>.</param>
/// <param name="task">The task. May not be <c>null</c>.</param>
/// <returns><c>true</c> if this method completed the task completion source; <c>false</c> if it was already completed.</returns>
public static bool TryCompleteFromCompletedTask<TResult, TSourceResult>(this TaskCompletionSource<TResult> @this, Task<TSourceResult> task) where TSourceResult : TResult
{
if (@this == null)
throw new ArgumentNullException(nameof(@this));
if (task == null)
throw new ArgumentNullException(nameof(task));
if (task.IsFaulted)
return @this.TrySetException(task.Exception.InnerExceptions);
if (task.IsCanceled)
{
try
{
task.WaitAndUnwrapException();
}
catch (OperationCanceledException exception)
{
var token = exception.CancellationToken;
return token.IsCancellationRequested ? @this.TrySetCanceled(token) : @this.TrySetCanceled();
}
}
return @this.TrySetResult(task.Result);
}
/// <summary>
/// Attempts to complete a <see cref="TaskCompletionSource{TResult}"/>, propagating the completion of <paramref name="task"/> but using the result value from <paramref name="resultFunc"/> if the task completed successfully.
/// </summary>
/// <typeparam name="TResult">The type of the result of the target asynchronous operation.</typeparam>
/// <param name="this">The task completion source. May not be <c>null</c>.</param>
/// <param name="task">The task. May not be <c>null</c>.</param>
/// <param name="resultFunc">A delegate that returns the result with which to complete the task completion source, if the task completed successfully. May not be <c>null</c>.</param>
/// <returns><c>true</c> if this method completed the task completion source; <c>false</c> if it was already completed.</returns>
public static bool TryCompleteFromCompletedTask<TResult>(this TaskCompletionSource<TResult> @this, Task task, Func<TResult> resultFunc)
{
if (@this == null)
throw new ArgumentNullException(nameof(@this));
if (task == null)
throw new ArgumentNullException(nameof(task));
if (resultFunc == null)
throw new ArgumentNullException(nameof(resultFunc));
if (task.IsFaulted)
return @this.TrySetException(task.Exception.InnerExceptions);
if (task.IsCanceled)
{
try
{
task.WaitAndUnwrapException();
}
catch (OperationCanceledException exception)
{
var token = exception.CancellationToken;
return token.IsCancellationRequested ? @this.TrySetCanceled(token) : @this.TrySetCanceled();
}
}
return @this.TrySetResult(resultFunc());
}
/// <summary>
/// Attempts to complete a <see cref="TaskCompletionSource{TResult}"/>, propagating the completion of <paramref name="eventArgs"/>.
/// </summary>
/// <typeparam name="TResult">The type of the result of the asynchronous operation.</typeparam>
/// <param name="this">The task completion source. May not be <c>null</c>.</param>
/// <param name="eventArgs">The event arguments passed to the completion event. May not be <c>null</c>.</param>
/// <param name="getResult">The delegate used to retrieve the result. This is only invoked if <paramref name="eventArgs"/> indicates successful completion. May not be <c>null</c>.</param>
/// <returns><c>true</c> if this method completed the task completion source; <c>false</c> if it was already completed.</returns>
public static bool TryCompleteFromEventArgs<TResult>(this TaskCompletionSource<TResult> @this, AsyncCompletedEventArgs eventArgs, Func<TResult> getResult)
{
if (eventArgs.Cancelled)
return @this.TrySetCanceled();
if (eventArgs.Error != null)
return @this.TrySetException(eventArgs.Error);
return @this.TrySetResult(getResult());
}
/// <summary>
/// Attempts to complete a <see cref="TaskCompletionSource"/>, propagating the completion of <paramref name="task"/>.
/// </summary>
/// <param name="this">The task completion source. May not be <c>null</c>.</param>
/// <param name="task">The task. May not be <c>null</c>.</param>
/// <returns><c>true</c> if this method completed the task completion source; <c>false</c> if it was already completed.</returns>
public static bool TryCompleteFromCompletedTask(this TaskCompletionSource @this, Task task)
{
if (task.IsFaulted)
return @this.TrySetException(task.Exception.InnerExceptions);
if (task.IsCanceled)
return @this.TrySetCanceled();
return @this.TrySetResult();
}
/// <summary>
/// Attempts to complete a <see cref="TaskCompletionSource"/>, propagating the completion of <paramref name="eventArgs"/>.
/// </summary>
/// <param name="this">The task completion source. May not be <c>null</c>.</param>
/// <param name="eventArgs">The event arguments passed to the completion event. May not be <c>null</c>.</param>
/// <returns><c>true</c> if this method completed the task completion source; <c>false</c> if it was already completed.</returns>
public static bool TryCompleteFromEventArgs(this TaskCompletionSource @this, AsyncCompletedEventArgs eventArgs)
{
if (eventArgs.Cancelled)
return @this.TrySetCanceled();
if (eventArgs.Error != null)
return @this.TrySetException(eventArgs.Error);
return @this.TrySetResult();
}
/// <summary>
/// Attempts to complete a <see cref="TaskCompletionSource{TResult}"/> with the specified value, forcing all continuations onto a threadpool thread even if they specified <c>ExecuteSynchronously</c>.
/// </summary>
/// <typeparam name="TResult">The type of the result of the asynchronous operation.</typeparam>
/// <param name="this">The task completion source. May not be <c>null</c>.</param>
/// <param name="result">The result of the asynchronous operation.</param>
public static void TrySetResultWithBackgroundContinuations<TResult>(this TaskCompletionSource<TResult> @this, TResult result)
{
// Set the result on a threadpool thread, so any synchronous continuations will execute in the background.
TaskShim.Run(() => @this.TrySetResult(result));
// Wait for the TCS task to complete; note that the continuations may not be complete.
@this.Task.Wait();
}
/// <summary>
/// Attempts to complete a <see cref="TaskCompletionSource"/>, forcing all continuations onto a threadpool thread even if they specified <c>ExecuteSynchronously</c>.
/// </summary>
/// <param name="this">The task completion source. May not be <c>null</c>.</param>
public static void TrySetResultWithBackgroundContinuations(this TaskCompletionSource @this)
{
// Set the result on a threadpool thread, so any synchronous continuations will execute in the background.
TaskShim.Run(() => @this.TrySetResult());
// Wait for the TCS task to complete; note that the continuations may not be complete.
@this.Task.Wait();
}
/// <summary>
/// Attempts to complete a <see cref="TaskCompletionSource{TResult}"/> as canceled, forcing all continuations onto a threadpool thread even if they specified <c>ExecuteSynchronously</c>.
/// </summary>
/// <typeparam name="TResult">The type of the result of the asynchronous operation.</typeparam>
/// <param name="this">The task completion source. May not be <c>null</c>.</param>
public static void TrySetCanceledWithBackgroundContinuations<TResult>(this TaskCompletionSource<TResult> @this)
{
// Complete on a threadpool thread, so any synchronous continuations will execute in the background.
TaskShim.Run(() => @this.TrySetCanceled());
// Wait for the TCS task to complete; note that the continuations may not be complete.
try
{
@this.Task.Wait();
}
catch (AggregateException)
{
}
}
/// <summary>
/// Creates a new TCS for use with async code, and which forces its continuations to execute asynchronously.
/// </summary>
/// <typeparam name="TResult">The type of the result of the TCS.</typeparam>
public static TaskCompletionSource<TResult> CreateAsyncTaskSource<TResult>()
{
return new TaskCompletionSource<TResult>(TaskCreationOptions.RunContinuationsAsynchronously);
}
/// <summary>
/// Attempts to complete a <see cref="TaskCompletionSource"/> as canceled, forcing all continuations onto a threadpool thread even if they specified <c>ExecuteSynchronously</c>.
/// </summary>
/// <param name="this">The task completion source. May not be <c>null</c>.</param>
public static void TrySetCanceledWithBackgroundContinuations(this TaskCompletionSource @this)
{
// Set the result on a threadpool thread, so any synchronous continuations will execute in the background.
TaskShim.Run(() => @this.TrySetCanceled());
// Wait for the TCS task to complete; note that the continuations may not be complete.
try
{
@this.Task.Wait();
}
catch (AggregateException)
{
}
}
/// <summary>
/// Attempts to complete a <see cref="TaskCompletionSource{TResult}"/> as faulted, forcing all continuations onto a threadpool thread even if they specified <c>ExecuteSynchronously</c>.
/// </summary>
/// <typeparam name="TResult">The type of the result of the asynchronous operation.</typeparam>
/// <param name="this">The task completion source. May not be <c>null</c>.</param>
/// <param name="exception">The exception to bind to the task.</param>
public static void TrySetExceptionWithBackgroundContinuations<TResult>(this TaskCompletionSource<TResult> @this, Exception exception)
{
// Complete on a threadpool thread, so any synchronous continuations will execute in the background.
TaskShim.Run(() => @this.TrySetException(exception));
// Wait for the TCS task to complete; note that the continuations may not be complete.
try
{
@this.Task.Wait();
}
catch (AggregateException)
{
}
}
/// <summary>
/// Attempts to complete a <see cref="TaskCompletionSource"/> as faulted, forcing all continuations onto a threadpool thread even if they specified <c>ExecuteSynchronously</c>.
/// </summary>
/// <param name="this">The task completion source. May not be <c>null</c>.</param>
/// <param name="exception">The exception to bind to the task.</param>
public static void TrySetExceptionWithBackgroundContinuations(this TaskCompletionSource @this, Exception exception)
{
// Set the result on a threadpool thread, so any synchronous continuations will execute in the background.
TaskShim.Run(() => @this.TrySetException(exception));
// Wait for the TCS task to complete; note that the continuations may not be complete.
try
{
@this.Task.Wait();
}
catch (AggregateException)
{
}
}
/// <summary>
/// Attempts to complete a <see cref="TaskCompletionSource{TResult}"/> as faulted, forcing all continuations onto a threadpool thread even if they specified <c>ExecuteSynchronously</c>.
/// </summary>
/// <typeparam name="TResult">The type of the result of the asynchronous operation.</typeparam>
/// <param name="this">The task completion source. May not be <c>null</c>.</param>
/// <param name="exceptions">The exceptions to bind to the task.</param>
public static void TrySetExceptionWithBackgroundContinuations<TResult>(this TaskCompletionSource<TResult> @this, IEnumerable<Exception> exceptions)
{
// Complete on a threadpool thread, so any synchronous continuations will execute in the background.
TaskShim.Run(() => @this.TrySetException(exceptions));
// Wait for the TCS task to complete; note that the continuations may not be complete.
try
{
@this.Task.Wait();
}
catch (AggregateException)
{
}
}
/// <summary>
/// Attempts to complete a <see cref="TaskCompletionSource"/> as faulted, forcing all continuations onto a threadpool thread even if they specified <c>ExecuteSynchronously</c>.
/// </summary>
/// <param name="this">The task completion source. May not be <c>null</c>.</param>
/// <param name="exceptions">The exceptions to bind to the task.</param>
public static void TrySetExceptionWithBackgroundContinuations(this TaskCompletionSource @this, IEnumerable<Exception> exceptions)
{
// Set the result on a threadpool thread, so any synchronous continuations will execute in the background.
TaskShim.Run(() => @this.TrySetException(exceptions));
// Wait for the TCS task to complete; note that the continuations may not be complete.
try
{
@this.Task.Wait();
}
catch (AggregateException)
{
}
}
}
}

@ -0,0 +1,143 @@
using System.Threading.Tasks;
namespace ZeroLevel.Services.Async
{
/// <summary>
/// Provides completed task constants.
/// </summary>
public static class TaskConstants
{
private static readonly Task<bool> booleanTrue = TaskShim.FromResult(true);
private static readonly Task<int> intNegativeOne = TaskShim.FromResult(-1);
/// <summary>
/// A task that has been completed with the value <c>true</c>.
/// </summary>
public static Task<bool> BooleanTrue
{
get
{
return booleanTrue;
}
}
/// <summary>
/// A task that has been completed with the value <c>false</c>.
/// </summary>
public static Task<bool> BooleanFalse
{
get
{
return TaskConstants<bool>.Default;
}
}
/// <summary>
/// A task that has been completed with the value <c>0</c>.
/// </summary>
public static Task<int> Int32Zero
{
get
{
return TaskConstants<int>.Default;
}
}
/// <summary>
/// A task that has been completed with the value <c>-1</c>.
/// </summary>
public static Task<int> Int32NegativeOne
{
get
{
return intNegativeOne;
}
}
/// <summary>
/// A <see cref="Task"/> that has been completed.
/// </summary>
public static Task Completed
{
get
{
return booleanTrue;
}
}
/// <summary>
/// A <see cref="Task"/> that will never complete.
/// </summary>
public static Task Never
{
get
{
return TaskConstants<bool>.Never;
}
}
/// <summary>
/// A task that has been canceled.
/// </summary>
public static Task Canceled
{
get
{
return TaskConstants<bool>.Canceled;
}
}
}
/// <summary>
/// Provides completed task constants.
/// </summary>
/// <typeparam name="T">The type of the task result.</typeparam>
public static class TaskConstants<T>
{
private static readonly Task<T> defaultValue = TaskShim.FromResult(default(T));
private static readonly Task<T> never = new TaskCompletionSource<T>().Task;
private static readonly Task<T> canceled = CanceledTask();
private static Task<T> CanceledTask()
{
var tcs = new TaskCompletionSource<T>();
tcs.SetCanceled();
return tcs.Task;
}
/// <summary>
/// A task that has been completed with the default value of <typeparamref name="T"/>.
/// </summary>
public static Task<T> Default
{
get
{
return defaultValue;
}
}
/// <summary>
/// A <see cref="Task"/> that will never complete.
/// </summary>
public static Task<T> Never
{
get
{
return never;
}
}
/// <summary>
/// A task that has been canceled.
/// </summary>
public static Task<T> Canceled
{
get
{
return canceled;
}
}
}
}

@ -0,0 +1,155 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ZeroLevel.Services.Async
{
/// <summary>
/// Provides synchronous extension methods for tasks.
/// </summary>
public static class TaskExtensions
{
/// <summary>
/// Asynchronously waits for the task to complete, or for the cancellation token to be canceled.
/// </summary>
/// <param name="this">The task to wait for. May not be <c>null</c>.</param>
/// <param name="cancellationToken">The cancellation token that cancels the wait.</param>
public static Task WaitAsync(this Task @this, CancellationToken cancellationToken)
{
if (@this == null)
throw new ArgumentNullException(nameof(@this));
if (!cancellationToken.CanBeCanceled)
return @this;
if (cancellationToken.IsCancellationRequested)
return Task.FromCanceled(cancellationToken);
return DoWaitAsync(@this, cancellationToken);
}
private static async Task DoWaitAsync(Task task, CancellationToken cancellationToken)
{
using (var cancelTaskSource = new CancellationTokenTaskSource<object>(cancellationToken))
await await Task.WhenAny(task, cancelTaskSource.Task).ConfigureAwait(false);
}
/// <summary>
/// Asynchronously waits for the task to complete, or for the cancellation token to be canceled.
/// </summary>
/// <typeparam name="TResult">The type of the task result.</typeparam>
/// <param name="this">The task to wait for. May not be <c>null</c>.</param>
/// <param name="cancellationToken">The cancellation token that cancels the wait.</param>
public static Task<TResult> WaitAsync<TResult>(this Task<TResult> @this, CancellationToken cancellationToken)
{
if (@this == null)
throw new ArgumentNullException(nameof(@this));
if (!cancellationToken.CanBeCanceled)
return @this;
if (cancellationToken.IsCancellationRequested)
return Task.FromCanceled<TResult>(cancellationToken);
return DoWaitAsync(@this, cancellationToken);
}
private static async Task<TResult> DoWaitAsync<TResult>(Task<TResult> task, CancellationToken cancellationToken)
{
using (var cancelTaskSource = new CancellationTokenTaskSource<TResult>(cancellationToken))
return await await Task.WhenAny(task, cancelTaskSource.Task).ConfigureAwait(false);
}
/// <summary>
/// Waits for the task to complete, unwrapping any exceptions.
/// </summary>
/// <param name="task">The task. May not be <c>null</c>.</param>
public static void WaitAndUnwrapException(this Task task)
{
task.GetAwaiter().GetResult();
}
/// <summary>
/// Waits for the task to complete, unwrapping any exceptions.
/// </summary>
/// <param name="task">The task. May not be <c>null</c>.</param>
/// <param name="cancellationToken">A cancellation token to observe while waiting for the task to complete.</param>
/// <exception cref="OperationCanceledException">The <paramref name="cancellationToken"/> was cancelled before the <paramref name="task"/> completed, or the <paramref name="task"/> raised an <see cref="OperationCanceledException"/>.</exception>
public static void WaitAndUnwrapException(this Task task, CancellationToken cancellationToken)
{
try
{
task.Wait(cancellationToken);
}
catch (AggregateException ex)
{
throw ExceptionHelpers.PrepareForRethrow(ex.InnerException);
}
}
/// <summary>
/// Waits for the task to complete, unwrapping any exceptions.
/// </summary>
/// <typeparam name="TResult">The type of the result of the task.</typeparam>
/// <param name="task">The task. May not be <c>null</c>.</param>
/// <returns>The result of the task.</returns>
public static TResult WaitAndUnwrapException<TResult>(this Task<TResult> task)
{
return task.GetAwaiter().GetResult();
}
/// <summary>
/// Waits for the task to complete, unwrapping any exceptions.
/// </summary>
/// <typeparam name="TResult">The type of the result of the task.</typeparam>
/// <param name="task">The task. May not be <c>null</c>.</param>
/// <param name="cancellationToken">A cancellation token to observe while waiting for the task to complete.</param>
/// <returns>The result of the task.</returns>
/// <exception cref="OperationCanceledException">The <paramref name="cancellationToken"/> was cancelled before the <paramref name="task"/> completed, or the <paramref name="task"/> raised an <see cref="OperationCanceledException"/>.</exception>
public static TResult WaitAndUnwrapException<TResult>(this Task<TResult> task, CancellationToken cancellationToken)
{
try
{
task.Wait(cancellationToken);
return task.Result;
}
catch (AggregateException ex)
{
throw ExceptionHelpers.PrepareForRethrow(ex.InnerException);
}
}
/// <summary>
/// Waits for the task to complete, but does not raise task exceptions. The task exception (if any) is unobserved.
/// </summary>
/// <param name="task">The task. May not be <c>null</c>.</param>
public static void WaitWithoutException(this Task task)
{
// Check to see if it's completed first, so we don't cause unnecessary allocation of a WaitHandle.
if (task.IsCompleted)
{
return;
}
var asyncResult = (IAsyncResult)task;
asyncResult.AsyncWaitHandle.WaitOne();
}
/// <summary>
/// Waits for the task to complete, but does not raise task exceptions. The task exception (if any) is unobserved.
/// </summary>
/// <param name="task">The task. May not be <c>null</c>.</param>
/// <param name="cancellationToken">A cancellation token to observe while waiting for the task to complete.</param>
/// <exception cref="OperationCanceledException">The <paramref name="cancellationToken"/> was cancelled before the <paramref name="task"/> completed.</exception>
public static void WaitWithoutException(this Task task, CancellationToken cancellationToken)
{
// Check to see if it's completed first, so we don't cause unnecessary allocation of a WaitHandle.
if (task.IsCompleted)
{
return;
}
cancellationToken.ThrowIfCancellationRequested();
var index = WaitHandle.WaitAny(new[] { ((IAsyncResult)task).AsyncWaitHandle, cancellationToken.WaitHandle });
if (index != 0)
{
cancellationToken.ThrowIfCancellationRequested();
}
}
}
}

@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace ZeroLevel.Services.Async
{
internal static class TaskShim
{
public static Task Run(Action func)
{
return Task.Run(func);
}
public static Task Run(Func<Task> func)
{
return Task.Run(func);
}
public static Task<T> Run<T>(Func<T> func)
{
return Task.Run(func);
}
public static Task<T> Run<T>(Func<Task<T>> func)
{
return Task.Run(func);
}
public static Task<T> FromResult<T>(T value)
{
return Task.FromResult(value);
}
public static Task<T[]> WhenAll<T>(IEnumerable<Task<T>> tasks)
{
return Task.WhenAll(tasks);
}
public static Task<T[]> WhenAll<T>(params Task<T>[] tasks)
{
return Task.WhenAll(tasks);
}
public static Task WhenAll(params Task[] tasks)
{
return Task.WhenAll(tasks);
}
public static Task WhenAll(IEnumerable<Task> tasks)
{
return Task.WhenAll(tasks);
}
}
}

@ -0,0 +1,141 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using ZeroLevel.Services.Invokation;
namespace ZeroLevel.Services.Collections
{
public class EverythingStorage :
IEverythingStorage
{
private class ConcreteTypeRepository
{
private readonly IInvokeWrapper _wrapper;
private readonly Invoker _insert;
private readonly Invoker _containsKey;
private readonly Invoker _remove;
private readonly Invoker _getter;
private readonly object _instance;
public ConcreteTypeRepository(Type entityType)
{
_wrapper = InvokeWrapper.Create();
var genericType = typeof(Dictionary<,>);
var instanceType = genericType.MakeGenericType(new Type[] { typeof(string), entityType });
_instance = Activator.CreateInstance(instanceType);
var insert_key = _wrapper.Configure(instanceType, "Insert").Single();
_insert = _wrapper.GetInvoker(insert_key);
var contains_key = _wrapper.Configure(instanceType, "ContainsKey").Single();
_containsKey = _wrapper.GetInvoker(contains_key);
var remove_key = _wrapper.Configure(instanceType, "Remove").Single();
_remove = _wrapper.GetInvoker(remove_key);
var p = instanceType.GetProperty("Item", entityType);
var getter = p.GetGetMethod();
var get_key = _wrapper.Configure(getter);
_getter = _wrapper.GetInvoker(get_key);
}
public void Insert<T>(string key, T entity)
{
_insert.Invoke(_instance, new object[] { key, entity, true });
}
public void InsertOrUpdate<T>(string key, T entity)
{
if ((bool)_containsKey.Invoke(_instance, key))
_remove.Invoke(_instance, key);
_insert.Invoke(_instance, new object[] { key, entity, true });
}
public bool ContainsKey(string key)
{
return (bool)_containsKey.Invoke(_instance, key);
}
public void Remove<T>(string key)
{
_remove.Invoke(_instance, key);
}
public T Get<T>(string key)
{
return (T)_getter.Invoke(_instance, key);
}
}
private readonly ConcurrentDictionary<Type, ConcreteTypeRepository> _shardedRepositories =
new ConcurrentDictionary<Type, ConcreteTypeRepository>();
private ConcreteTypeRepository this[Type type]
{
get
{
if (_shardedRepositories.ContainsKey(type) == false)
{
var r = new ConcreteTypeRepository(type);
_shardedRepositories.AddOrUpdate(type, r, (t, old) => old);
}
return _shardedRepositories[type];
}
}
public bool TryAdd<T>(string key, T value)
{
try
{
this[typeof(T)].Insert<T>(key, value);
return true;
}
catch
{ }
return false;
}
public bool ContainsKey<T>(string key)
{
return this[typeof(T)].ContainsKey(key);
}
public bool TryRemove<T>(string key)
{
try
{
this[typeof(T)].Remove<T>(key);
return true;
}
catch
{ }
return false;
}
public void Add<T>(string key, T value)
{
this[typeof(T)].Insert<T>(key, value);
}
public void Remove<T>(string key)
{
this[typeof(T)].Remove<T>(key);
}
public T Get<T>(string key)
{
return this[typeof(T)].Get<T>(key);
}
public static IEverythingStorage Create()
{
return new EverythingStorage();
}
public void AddOrUpdate<T>(string key, T value)
{
this[typeof(T)].InsertOrUpdate<T>(key, value);
}
}
}

@ -0,0 +1,145 @@
using System;
using System.Collections.Generic;
namespace ZeroLevel.Services.Collections
{
/// <summary>
/// Очередь фиксированной длины
/// </summary>
public sealed class FixSizeQueue<T> :
IFixSizeQueue<T>
{
private readonly T[] _array;
private long _nextIndex;
private long _startIndex;
private long _count;
private readonly object _accessLocker = new object();
public FixSizeQueue(long capacity)
{
if (capacity <= 0)
{
capacity = 1024;
}
_array = new T[capacity];
_nextIndex = 0;
_count = 0;
}
/// <summary>
/// Добавление элемента в очередь, при достижении предела по размеру,
/// перезаписывается самый старый элемент
/// </summary>
public void Push(T item)
{
lock (_accessLocker)
{
_array[_nextIndex] = item;
_nextIndex = (_nextIndex + 1) % _array.Length;
if (_count < _array.Length)
{
_count++;
}
else
{
_startIndex = (_startIndex + 1) % _array.Length;
}
}
}
public bool Equals(T x, T y)
{
if (x == null && y == null) return true;
if ((object)x == (object)y) return true;
if (x == null || y == null) return false;
if (ReferenceEquals(x, y)) return true;
return x.Equals(y);
}
public bool Contains(T item, IComparer<T> comparer = null)
{
lock (_accessLocker)
{
Func<T, T, bool> eq_func;
if (comparer == null)
{
eq_func = Equals;
}
else
{
eq_func = (x, y) => comparer.Compare(x, y) == 0;
}
var cursor = _startIndex;
if (_count > 0)
{
do
{
if (eq_func(_array[cursor], item))
return true;
cursor = (cursor + 1) % _array.Length;
} while (cursor != _nextIndex);
}
}
return false;
}
public long Count
{
get
{
return _count;
}
}
public bool TryTake(out T t)
{
lock (_accessLocker)
{
if (_count > 0)
{
t = _array[_startIndex];
_array[_startIndex] = default(T);
_startIndex = (_startIndex + 1) % _array.Length;
_count--;
return true;
}
}
t = default(T);
return false;
}
public T Take()
{
T ret;
lock (_accessLocker)
{
if (_count > 0)
{
ret = _array[_startIndex];
_array[_startIndex] = default(T);
_startIndex = (_startIndex + 1) % _array.Length;
_count--;
return ret;
}
}
throw new System.Exception("Collection is empty");
}
public IEnumerable<T> Dump()
{
var dump = new List<T>();
lock (_accessLocker)
{
var cursor = _startIndex;
if (_count > 0)
{
do
{
dump.Add(_array[cursor]);
cursor = (cursor + 1) % _array.Length;
} while (cursor != _nextIndex);
}
}
return dump;
}
}
}

@ -0,0 +1,13 @@
namespace ZeroLevel.Services.Collections
{
public interface IEverythingStorage
{
bool TryAdd<T>(string key, T value);
bool ContainsKey<T>(string key);
bool TryRemove<T>(string key);
void Add<T>(string key, T value);
void AddOrUpdate<T>(string key, T value);
void Remove<T>(string key);
T Get<T>(string key);
}
}

@ -0,0 +1,14 @@
using System.Collections.Generic;
namespace ZeroLevel.Services.Collections
{
public interface IFixSizeQueue<T>
{
void Push(T item);
long Count { get; }
bool TryTake(out T t);
T Take();
IEnumerable<T> Dump();
bool Contains(T item, IComparer<T> comparer = null);
}
}

@ -0,0 +1,132 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
namespace ZeroLevel.Services.Collections
{
public sealed class RoundRobinCollection<T> :
IDisposable
{
private readonly List<T> _collection =
new List<T>();
private int _index = -1;
private readonly ReaderWriterLockSlim _lock =
new ReaderWriterLockSlim();
public int Count { get { return _collection.Count; } }
public void Add(T item)
{
_lock.EnterWriteLock();
try
{
_collection.Add(item);
if (_index == -1) _index = 0;
}
finally
{
_lock.ExitWriteLock();
}
}
public void Remove(T item)
{
_lock.EnterWriteLock();
try
{
_collection.Remove(item);
if (_index >= _collection.Count)
{
if (_collection.Count == 0) _index = -1;
else _index = 0;
}
}
finally
{
_lock.ExitWriteLock();
}
}
public bool Contains(T item)
{
_lock.EnterReadLock();
try
{
return _collection.Contains(item);
}
finally
{
_lock.ExitReadLock();
}
}
public bool MoveNext()
{
_lock.EnterReadLock();
try
{
if (_collection.Count > 0)
{
_index = Interlocked.Increment(ref _index) % _collection.Count;
return true;
}
}
finally
{
_lock.ExitReadLock();
}
return false;
}
public T Current
{
get
{
return _index == -1 ? default(T) : _collection[_index];
}
}
public IEnumerable<T> Source { get { return _collection; } }
public IEnumerable<T> GetCurrentSeq()
{
_lock.EnterReadLock();
try
{
var arr = new T[_collection.Count];
int p = 0;
for (int i = _index; i < _collection.Count; i++, p++)
{
arr[p] = _collection[i];
}
for (int i = 0; i < _index; i++, p++)
{
arr[p] = _collection[i];
}
return arr;
}
finally
{
_lock.ExitReadLock();
}
}
public IEnumerable<T> Find(Func<T, bool> selector)
{
return _collection.Where(selector);
}
public void Clear()
{
_collection.Clear();
_index = -1;
}
public void Dispose()
{
_collection.Clear();
_lock.Dispose();
}
}
}

@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace ZeroLevel.Services.Collections
{
/// <summary>
/// Циклический разреженный итератор
/// позволяет выполнять циклический обход массива, с возможностью отмечать элементы
/// которые требуется прпускать при следующих обходах.
/// </summary>
public class SparseIterator<T>
{
private readonly T[] _array;
private readonly HashSet<int> _removed = new HashSet<int>();
private int index = -1;
public SparseIterator(IEnumerable<T> items)
{
_array = items.ToArray();
}
/// <summary>
/// Текущий элемент последовательности
/// </summary>
public T Current
{
get
{
if (index >= 0 && index < _array.Length)
{
return _array[index];
}
throw new IndexOutOfRangeException();
}
}
/// <summary>
/// Указывает на отсутствие элементов в последовательности или на
/// то что все элементы были отмечены для пропуска
/// </summary>
public bool IsEmpty
{
get
{
return _array.Length == 0 || _array.Length == _removed.Count;
}
}
/// <summary>
/// Сдвиг на следующий элемент, если достигнут конец последовательности,
/// переключается на первый неотмеченный для пропуска элемент
/// </summary>
/// <returns>вернет -1 если последовательность пуста, или если не осталось элементов не отмеченных для пропуска</returns>
public int MoveNext()
{
do
{
index++;
} while (_removed.Contains(index));
if (index >= _array.Length)
{
if (IsEmpty) return -1;
index = -1;
do
{
index++;
} while (_removed.Contains(index));
}
return index;
}
/// <summary>
/// Отмечает текущий элемент для пропуска при следующем обходе
/// </summary>
/// <returns></returns>
public bool Exclude()
{
if (index >= 0)
{
return _removed.Add(index);
}
return false;
}
}
}

@ -0,0 +1,411 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using ZeroLevel.Services.Reflection;
using ZeroLevel.Services.Serialization;
namespace ZeroLevel.Services.Config
{
/// <summary>
/// Упрощенная конфигурация, без разделения параметров по секциям
/// </summary>
internal sealed class BaseConfiguration :
IConfiguration
{
#region Private members
/// <summary>
/// Указывает на заморозку конфигурации, все изменения запрещены
/// </summary>
private bool _freezed = false;
/// <summary>
/// Указывает на перманентную заморозку конфигурации, разморозка запрещена
/// </summary>
private bool _permanentFreezed = false;
private readonly object _freezeLock = new object();
/// <summary>
/// Список вида ключ-значение
/// </summary>
private readonly ConcurrentDictionary<string, IList<string>> _keyValues = new ConcurrentDictionary<string, IList<string>>();
/// <summary>
/// Пустой список
/// </summary>
private static readonly IEnumerable<string> EmptyValuesList = new List<string>(0);
private static string GetKey(string key)
{
if (string.IsNullOrWhiteSpace(key))
{
throw new ArgumentNullException("key");
}
return key.Trim().ToLower(CultureInfo.InvariantCulture);
}
#endregion
#region Properties
/// <summary>
/// Список значений по ключу
/// </summary>
/// <param name="key">Ключ</param>
/// <returns>Список значений</returns>
public IEnumerable<string> this[string key]
{
get
{
key = key.ToLower(CultureInfo.CurrentCulture);
IList<string> result;
if (_keyValues.TryGetValue(key, out result))
{
return result;
}
return EmptyValuesList;
}
}
/// <summary>
/// Список ключей
/// </summary>
public IEnumerable<string> Keys
{
get { return _keyValues.Keys; }
}
public bool Freezed
{
get
{
return _freezed;
}
}
#endregion
#region Public methods
#region Get
/// <summary>
/// Получение списка значение соотвествующих указанному ключу
/// </summary>
/// <param name="key">Ключ</param>
/// <returns>Список значений</returns>
public IEnumerable<string> Items(string key)
{
return this[key];
}
/// <summary>
/// Получение первого значения для указанного ключа
/// </summary>
/// <param name="key">Ключ</param>
/// <returns>Первое значение, или null если ключ есть, но нет значений, или KeyNotFoundException если нет ключа</returns>
public string First(string key)
{
IList<string> result;
if (_keyValues.TryGetValue(GetKey(key), out result))
{
if (result.Count > 0)
return result[0];
return null;
}
throw new KeyNotFoundException("Key not found: " + key);
}
public void DoWithFirst(string key, Action<string> action)
{
if (Contains(key))
{
action(First(key));
}
}
public void DoWithFirst<T>(string key, Action<T> action)
{
if (Contains(key))
{
action(First<T>(key));
}
}
/// <summary>
/// Получение первого значения для указанного ключа, с попыткой преобразования в указанный тип
/// </summary>
/// <typeparam name="T">Ожидаемый тип</typeparam>
/// <param name="key">Ключ</param>
/// <returns>Первое значение, или default(T) если ключ есть, но нет значений, или KeyNotFoundException если нет ключа</returns>
public T First<T>(string key)
{
IList<string> result;
if (_keyValues.TryGetValue(GetKey(key), out result))
{
if (result.Count > 0)
return (T)StringToTypeConverter.TryConvert(result[0], typeof(T));
return default(T);
}
throw new KeyNotFoundException("Parameter not found: " + key);
}
/// <summary>
/// Получение первого значения для указанного ключа, или значения по умолчанию
/// </summary>
/// <param name="key">Ключ</param>
/// <param name="defaultValue">Значение по умолчанию</param>
/// <returns>Первое значение, или значение по умолчанию если нет значений или ключа</returns>
public string FirstOrDefault(string key, string defaultValue)
{
IList<string> result;
if (_keyValues.TryGetValue(GetKey(key), out result))
{
if (result.Count > 0)
return result[0];
}
return defaultValue;
}
/// <summary>
/// Получение первого значения для указанного ключа, или значения по умолчанию, с попыткой преобразования в указанный тип
/// </summary>
/// <typeparam name="T">Ожидаемый тип</typeparam>
/// <param name="key">Ключ</param>
/// <returns>Первое значение, или default(T) если нет значений или ключа</returns>
public T FirstOrDefault<T>(string key)
{
IList<string> result;
if (_keyValues.TryGetValue(GetKey(key), out result))
{
if (result.Count > 0)
return (T)StringToTypeConverter.TryConvert(result[0], typeof(T));
}
return default(T);
}
/// <summary>
/// Получение первого значения для указанного ключа, или значения по умолчанию, с попыткой преобразования в указанный тип
/// </summary>
/// <typeparam name="T">Ожидаемый тип</typeparam>
/// <param name="key">Ключ</param>
/// <param name="defaultValue">Значение по умолчанию</param>
/// <returns>Первое значение, или значение по умолчанию если нет значений или ключа</returns>
public T FirstOrDefault<T>(string key, T defaultValue)
{
IList<string> result;
if (_keyValues.TryGetValue(GetKey(key), out result))
{
if (result.Count > 0)
return (T)StringToTypeConverter.TryConvert(result[0], typeof(T));
}
return defaultValue;
}
/// <summary>
/// Проверка наличия ключа и непустого списка связанных с ним значений
/// </summary>
/// <param name="key">Ключ</param>
/// <returns>true - если существует ключ и есть хотя бы одно значение</returns>
public bool Contains(string key)
{
key = GetKey(key);
return _keyValues.ContainsKey(key) && _keyValues[key].Count > 0;
}
/// <summary>
/// Проверка наличия одного из ключей
/// </summary>
public bool Contains(params string[] keys)
{
foreach (var key in keys)
if (Contains(key)) return true;
return false;
}
/// <summary>
/// Проверка наличия ключа и связанного с ним значения
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
/// <returns></returns>
public bool ContainsValue(string key, string value)
{
IList<string> result;
if (_keyValues.TryGetValue(GetKey(key), out result))
{
return result.Contains(value);
}
return false;
}
/// <summary>
/// Количество значений связанных с указанным ключом
/// </summary>
/// <param name="key">Ключ</param>
/// <returns>Количество значений</returns>
public int Count(string key)
{
key = GetKey(key);
if (_keyValues.ContainsKey(key))
{
return _keyValues[key].Count;
}
return 0;
}
#endregion
/// <summary>
/// Добавление ключа и связанного с ним значения
/// </summary>
/// <param name="key">Ключ</param>
/// <param name="value">Значение</param>
public IConfiguration Append(string key, string value)
{
if (false == _freezed)
{
key = GetKey(key);
if (false == _keyValues.ContainsKey(key))
{
_keyValues.TryAdd(key, new List<string>());
}
_keyValues[key].Add(value?.Trim() ?? null);
}
return this;
}
/// <summary>
/// Задает значение в единственном числе,
/// существующее значение будет перезаписано
/// </summary>
public IConfiguration SetUnique(string key, string value)
{
if (false == _freezed)
{
key = GetKey(key);
if (false == _keyValues.ContainsKey(key))
{
_keyValues.TryAdd(key, new List<string>());
}
_keyValues[key].Clear();
_keyValues[key].Add(value?.Trim() ?? null);
}
return this;
}
/// <summary>
/// Очистка связанного с ключом списка значений
/// </summary>
/// <param name="key">Ключ</param>
public IConfiguration Clear(string key)
{
if (false == _freezed)
{
key = GetKey(key);
if (_keyValues.ContainsKey(key))
{
_keyValues[key].Clear();
}
}
return this;
}
/// <summary>
/// Очистка конфигурации
/// </summary>
public IConfiguration Clear()
{
if (false == _freezed)
{
_keyValues.Clear();
}
return this;
}
/// <summary>
/// Удаление ключа и связанных с ним значений
/// </summary>
/// <param name="key">Ключ</param>
public IConfiguration Remove(string key)
{
if (false == _freezed)
{
IList<string> removed;
_keyValues.TryRemove(GetKey(key), out removed);
}
return this;
}
public bool Freeze(bool permanent = false)
{
lock (_freezeLock)
{
if (false == _freezed)
{
_freezed = true;
_permanentFreezed = permanent;
return true;
}
else if (_permanentFreezed == false && permanent)
{
_permanentFreezed = true;
return true;
}
return false;
}
}
public bool Unfreeze()
{
lock (_freezeLock)
{
if (_freezed && _permanentFreezed == false)
{
_freezed = false;
return true;
}
return false;
}
}
#endregion
#region IEquatable
public bool Equals(IConfiguration other)
{
if (other == null)
{
return false;
}
if (this.Keys.NoOrderingEquals(other.Keys) == false)
{
return false;
}
foreach (var key in Keys)
{
if (this[key].NoOrderingEquals(other[key]) == false)
{
return false;
}
}
return true;
}
#endregion
#region Binary Serializable
public void Serialize(IBinaryWriter writer)
{
writer.WriteBoolean(this._freezed);
writer.WriteBoolean(this._permanentFreezed);
writer.WriteInt32(_keyValues.Count);
foreach (var pair in _keyValues)
{
writer.WriteString(pair.Key);
writer.WriteInt32(pair.Value.Count);
foreach (var value in pair.Value)
{
writer.WriteString(value);
}
}
}
public void Deserialize(IBinaryReader reader)
{
this._freezed = reader.ReadBoolean();
this._permanentFreezed = reader.ReadBoolean();
var count = reader.ReadInt32();
_keyValues.Clear();
for (int i = 0; i < count; i++)
{
var key = reader.ReadString();
var count_values = reader.ReadInt32();
var list_values = new List<string>();
for (var k = 0; k < count_values; k++)
{
list_values.Add(reader.ReadString());
}
_keyValues.TryAdd(key, list_values);
}
}
#endregion
}
}

@ -0,0 +1,225 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using ZeroLevel.Services.Serialization;
namespace ZeroLevel.Services.Config
{
/// <summary>
/// Наборы именованых конфигураций (секций)
/// </summary>
internal sealed class BaseConfigurationSet :
IConfigurationSet
{
#region Private members
/// <summary>
/// Список секций
/// </summary>
private readonly ConcurrentDictionary<string, IConfiguration> _sections = new ConcurrentDictionary<string, IConfiguration>();
private static string GetKey(string key)
{
if (string.IsNullOrWhiteSpace(key))
{
throw new ArgumentNullException(nameof(key));
}
return key.Trim().ToLower(CultureInfo.InvariantCulture);
}
#endregion
#region Properties
public IConfiguration Default
{
get { return _sections[Configuration.DEFAULT_SECTION_NAME]; }
}
public IConfiguration this[string sectionName]
{
get
{
return CreateSection(sectionName);
}
}
public IEnumerable<string> SectionNames
{
get { return _sections.Keys; }
}
public IEnumerable<IConfiguration> Sections
{
get { return _sections.Values; }
}
public bool SectionsFreezed
{
get
{
return _sectionsFreezed;
}
}
#endregion
#region Methods
public BaseConfigurationSet()
{
CreateSection(Configuration.DEFAULT_SECTION_NAME);
}
public BaseConfigurationSet(IConfiguration defaultConfiguration)
{
_sections.TryAdd(Configuration.DEFAULT_SECTION_NAME, defaultConfiguration);
}
public IConfiguration CreateSection(string sectionName)
{
var key = GetKey(sectionName);
IConfiguration exists;
if (_sections.TryGetValue(key, out exists))
{
return exists;
}
else if (false == _sectionsFreezed)
{
_sections.TryAdd(key, new BaseConfiguration());
return _sections[key];
}
throw new Exception("Sections change freezed");
}
public IConfiguration GetSection(string sectionName)
{
var key = GetKey(sectionName);
IConfiguration exists;
if (_sections.TryGetValue(key, out exists))
{
return exists;
}
throw new KeyNotFoundException("Section not found: " + sectionName);
}
public bool ContainsSection(string sectionName)
{
var key = GetKey(sectionName);
return _sections.ContainsKey(key);
}
public bool RemoveSection(string sectionName)
{
if (false == _sectionsFreezed)
{
var key = GetKey(sectionName);
if (_sections.ContainsKey(key))
{
IConfiguration removed;
return _sections.TryRemove(key, out removed);
}
}
return false;
}
#endregion
#region IEquatable
public bool Equals(IConfigurationSet other)
{
if (other == null) return false;
return this.SectionNames.NoOrderingEquals(other.SectionNames) &&
this.Sections.NoOrderingEquals(other.Sections);
}
#endregion
#region Freezing
private readonly object _freezeLock = new object();
public bool FreezeConfiguration(bool permanent = false)
{
bool result = false;
lock (_freezeLock)
{
foreach (var s in _sections)
{
result |= s.Value.Freeze(permanent);
}
}
return result || FreezeSections(permanent);
}
public bool UnfreezeConfiguration()
{
bool result = false;
lock (_freezeLock)
{
foreach (var s in _sections)
{
result |= s.Value.Unfreeze();
}
}
return result || UnfreezeSections();
}
private bool _sectionsFreezed = false;
private bool _permanentSectionsFreezed = false;
public bool FreezeSections(bool permanent = false)
{
lock (_freezeLock)
{
if (false == _sectionsFreezed)
{
_sectionsFreezed = true;
_permanentSectionsFreezed = permanent;
return true;
}
else if (_permanentSectionsFreezed == false && permanent)
{
_permanentSectionsFreezed = true;
return true;
}
return false;
}
}
public bool UnfreezeSections()
{
lock (_freezeLock)
{
if (_sectionsFreezed && _permanentSectionsFreezed == false)
{
_sectionsFreezed = false;
return true;
}
return false;
}
}
#endregion
#region Binary Serializable
public void Serialize(IBinaryWriter writer)
{
writer.WriteBoolean(this._sectionsFreezed);
writer.WriteBoolean(this._permanentSectionsFreezed);
writer.WriteInt32(_sections.Count);
foreach (var s in _sections)
{
writer.WriteString(s.Key);
writer.Write<IConfiguration>(s.Value);
}
}
public void Deserialize(IBinaryReader reader)
{
this._sectionsFreezed = reader.ReadBoolean();
this._permanentSectionsFreezed = reader.ReadBoolean();
var count = reader.ReadInt32();
_sections.Clear();
for (int i = 0; i < count; i++)
{
var key = reader.ReadString();
_sections.TryAdd(key, reader.Read<BaseConfiguration>());
}
}
#endregion
}
}

@ -0,0 +1,177 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using ZeroLevel.Services.Config;
using ZeroLevel.Services.Config.Implementation;
using ZeroLevel.Services.Serialization;
namespace ZeroLevel
{
public static class Configuration
{
/// <summary>
/// Путь к каталогу приложения
/// </summary>
public static string BaseDirectory =
Path.GetDirectoryName(Assembly.GetExecutingAssembly()?.CodeBase)?.
Replace("file:\\", string.Empty);
public static string AppLocation = Assembly.GetEntryAssembly()?.Location;
public const string DEFAULT_SECTION_NAME = "_defaultsection";
#region Ctor
static Configuration()
{
_empty = new BaseConfiguration();
_emptySet = new BaseConfigurationSet();
_empty.Freeze(true);
_emptySet.FreezeConfiguration(true);
}
#endregion
#region Cachee
private static readonly IConfiguration _empty;
private static readonly IConfigurationSet _emptySet;
private static readonly ConcurrentDictionary<string, IConfiguration> _cachee = new ConcurrentDictionary<string, IConfiguration>();
private static readonly ConcurrentDictionary<string, IConfigurationSet> _setCachee = new ConcurrentDictionary<string, IConfigurationSet>();
public static IConfiguration Empty { get { return _empty; } }
public static IConfigurationSet EmptySet { get { return _emptySet; } }
public static IConfiguration Default { get; private set; }
public static IConfigurationSet DefaultSet { get; private set; }
public static void Save(string name, IConfiguration configuration)
{
_cachee.AddOrUpdate(name, configuration, (oldKey, oldValue) => configuration);
}
public static void Save(IConfiguration configuration)
{
if (Default == null)
{
Default = configuration;
}
else
{
throw new Exception("Default configuration set already");
}
}
public static void Save(string name, IConfigurationSet configurationSet)
{
_setCachee.AddOrUpdate(name, configurationSet, (oldKey, oldValue) => configurationSet);
}
public static void Save(IConfigurationSet configuration)
{
if (DefaultSet == null)
{
DefaultSet = configuration;
}
else
{
throw new Exception("Default configurationset set already");
}
}
public static IConfiguration Get(string name)
{
IConfiguration result;
if (false == _cachee.TryGetValue(name, out result))
{
throw new KeyNotFoundException(string.Format("Not found configuration '{0}'", name));
}
return result;
}
public static IConfigurationSet GetSet(string name)
{
IConfigurationSet result;
if (false == _setCachee.TryGetValue(name, out result))
{
throw new KeyNotFoundException(string.Format("Not found configuration set '{0}'", name));
}
return result;
}
#endregion
#region Factory
public static IConfiguration Create()
{
return new BaseConfiguration();
}
public static IConfigurationSet CreateSet()
{
return new BaseConfigurationSet();
}
public static IConfigurationSet CreateSet(IConfiguration defaultConfiguration)
{
return new BaseConfigurationSet(defaultConfiguration);
}
#endregion
#region Read configuration
/// <summary>
/// Создание конфигурации из секции AppSettings файла app.config или web.config
/// </summary>
/// <returns>Конфигурация</returns>
public static IConfiguration ReadFromApplicationConfig() { return new ApplicationConfigReader().ReadConfiguration(); }
/// <summary>
/// Создание конфигурации из секции AppSettings файла app.config или web.config, дополняется секцией 'ConnectionStrings'
/// </summary>
/// <returns>Конфигурация</returns>
public static IConfigurationSet ReadSetFromApplicationConfig() { return new ApplicationConfigReader().ReadConfigurationSet(); }
/// <summary>
/// Создание конфигурации из секции AppSettings файла app.config или web.config
/// </summary>
/// <returns>Конфигурация</returns>
public static IConfiguration ReadFromApplicationConfig(string configFilePath) { return new ApplicationConfigReader(configFilePath).ReadConfiguration(); }
/// <summary>
/// Создание конфигурации из секции AppSettings файла app.config или web.config, дополняется секцией 'ConnectionStrings'
/// </summary>
/// <returns>Конфигурация</returns>
public static IConfigurationSet ReadSetFromApplicationConfig(string configFilePath) { return new ApplicationConfigReader(configFilePath).ReadConfigurationSet(); }
/// <summary>
/// Создание конфигурации из ini файла
/// </summary>
/// <param name="path">Путь к ini-файлу</param>
/// <returns>Конфигурация</returns>
public static IConfiguration ReadFromIniFile(string path) { return new IniFileReader(path).ReadConfiguration(); }
/// <summary>
/// Создание конфигурации из ini файла, с учетом секций
/// </summary>
/// <param name="path">Путь к ini-файлу</param>
/// <returns>Конфигурация</returns>
public static IConfigurationSet ReadSetFromIniFile(string path) { return new IniFileReader(path).ReadConfigurationSet(); }
/// <summary>
/// Создание конфигурации из параметров командной строки
/// </summary>
/// <param name="args">Параметры командной строки</param>
/// <returns>Конфигурация</returns>
public static IConfiguration ReadFromCommandLine(string[] args) { return new CommandLineReader(args).ReadConfiguration(); }
public static IConfigurationSet ReadBinary(IBinaryReader reader) { return reader.Read<BaseConfigurationSet>(); }
#endregion
#region Write configuration
/// <summary>
/// Запись простой конфигурации в ini-файл
/// </summary>
/// <param name="configuration">Конфигурация</param>
/// <param name="path">Путь к ini-файлу</param>
public static void WriteToIniFile(IConfiguration configuration, string path) { new IniFileWriter(path).WriteConfiguration(configuration); }
/// <summary>
/// Запись полной конфигурации в ini-файл
/// </summary>
/// <param name="configuration">Конфигурация</param>
/// <param name="path">Путь к ini-файлу</param>
public static void WriteSetToIniFile(IConfigurationSet configuration, string path) { new IniFileWriter(path).WriteConfigurationSet(configuration); }
#endregion
}
}

@ -0,0 +1,116 @@
using System;
using System.Collections.Generic;
using ZeroLevel.Services.Serialization;
namespace ZeroLevel
{
/// <summary>
/// Интерфейс конфигурационных данных
/// </summary>
public interface IConfiguration :
IEquatable<IConfiguration>,
IBinarySerializable
{
#region Properties
/// <summary>
/// Получение списка значений по ключу
/// </summary>
IEnumerable<string> this[string key] { get; }
/// <summary>
/// Перечисление ключей
/// </summary>
IEnumerable<string> Keys { get; }
/// <summary>
/// Указывает что конфигурация заблокирована на изменения
/// </summary>
bool Freezed { get; }
#endregion
#region Methods
/// <summary>
/// Получение списка значений параметра по ключу
/// </summary>
/// <param name="key">Имя параметра</param>
/// <returns>Список значений</returns>
IEnumerable<string> Items(string key);
/// <summary>
/// Получение одного(первого) значения параметра по ключу
/// </summary>
string First(string key);
/// <summary>
/// Получить первое значение в виде объекта типа T
/// </summary>
T First<T>(string key);
/// <summary>
/// Получить первое значение или значение по умолчанию
/// </summary>
string FirstOrDefault(string name, string defaultValue);
/// <summary>
/// Получить первое значение в виде объекта типа T или получить значение по умолчанию
/// </summary>
T FirstOrDefault<T>(string name);
/// <summary>
/// Получить первое значение в виде объекта типа T или получить переданное значение по умолчанию
/// </summary>
T FirstOrDefault<T>(string name, T defaultValue);
/// <summary>
/// Проверка наличия ключа
/// </summary>
bool Contains(string key);
/// <summary>
/// Проверка наличия одного из ключей
/// </summary>
bool Contains(params string[] keys);
/// <summary>
/// Проверка наличия значения по ключу
/// </summary>
bool ContainsValue(string key, string value);
/// <summary>
/// Количество значений параметра
/// </summary>
int Count(string key);
/// <summary>
/// Выполняет указанное действие только в случае если в конфигурации есть ключ
/// </summary>
void DoWithFirst(string key, Action<string> action);
/// <summary>
/// Выполняет указанное действие только в случае если в конфигурации есть ключ
/// </summary>
void DoWithFirst<T>(string key, Action<T> action);
#endregion
#region Create, Clean, Delete
/// <summary>
/// Очистка всей секции
/// </summary>
IConfiguration Clear();
/// <summary>
/// Очистка значения ключа
/// </summary>
IConfiguration Clear(string key);
/// <summary>
/// Удаление ключа и значений
/// </summary>
IConfiguration Remove(string key);
/// <summary>
/// Добавление параметра
/// </summary>
IConfiguration Append(string key, string value);
/// <summary>
/// Задает значение в единственном числе,
/// существующее значение будет перезаписано
/// </summary>
IConfiguration SetUnique(string key, string value);
/// <summary>
/// Запрещает вносить какие-либо изменения в конфигурацию
/// </summary>
/// <returns>false - если уже установлен запрет</returns>
bool Freeze(bool permanent = false);
/// <summary>
/// Убирает запрет на внесение изменений в конфигурацию
/// </summary>
/// <returns>false - если запрет снят</returns>
bool Unfreeze();
#endregion
}
}

@ -0,0 +1,8 @@
namespace ZeroLevel.Services.Config
{
public interface IConfigurationReader
{
IConfiguration ReadConfiguration();
IConfigurationSet ReadConfigurationSet();
}
}

@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using ZeroLevel.Services.Serialization;
namespace ZeroLevel
{
/// <summary>
/// Интерфейс набора конфигурационных данных
/// </summary>
public interface IConfigurationSet :
IEquatable<IConfigurationSet>,
IBinarySerializable
{
#region Properties
/// <summary>
/// Получение конфигурации по умолчанию
/// </summary>
IConfiguration Default { get; }
/// <summary>
/// Получение конфигурации по имени
/// </summary>
IConfiguration this[string sectionName] { get; }
/// <summary>
/// Получение имен конфигураций
/// </summary>
IEnumerable<string> SectionNames { get; }
/// <summary>
/// Получение всех конфигураций
/// </summary>
IEnumerable<IConfiguration> Sections { get; }
/// <summary>
/// Указывает, заблокирован или нет набор секций
/// </summary>
bool SectionsFreezed { get; }
#endregion
#region Methods
/// <summary>
/// Создание секции параметров
/// </summary>
/// <param name="sectionName">Название секции</param>
IConfiguration CreateSection(string sectionName);
/// <summary>
/// Запрос секции данных по имени секции
/// </summary>
/// <param name="sectionName">Название секции</param>
/// <returns>Секция с данными</returns>
IConfiguration GetSection(string sectionName);
/// <summary>
/// Проверка наличия секции с указанным именем
/// </summary>
/// <param name="sectionName">Название секции</param>
/// <returns>true - секция существует</returns>
bool ContainsSection(string sectionName);
/// <summary>
/// Удаление секции
/// </summary>
/// <param name="sectionName">Название секции</param>
/// <returns>false - если секция уже удалена или не существует</returns>
bool RemoveSection(string sectionName);
/// <summary>
/// Запрещает вносить какие-либо изменения в существующую конфигурацию во всех секциях
/// а также менять набор секций
/// </summary>
/// <returns>false - если уже установлен запрет</returns>
bool FreezeConfiguration(bool permanent = false);
/// <summary>
/// Запрещает вносить какие-либо изменения в существующий набор секций
/// </summary>
/// <returns>false - если уже установлен запрет</returns>
bool FreezeSections(bool permanent = false);
/// <summary>
/// Убирает запрет на внесение изменений в существующую конфигурацию во всех секциях
/// а также разрешает менять набор секций
/// </summary>
/// <returns>false - если запрет снят</returns>
bool UnfreezeConfiguration();
/// <summary>
/// Убирает запрет на внесение изменений в существующий набор секций
/// </summary>
/// <returns>false - если запрет снят</returns>
bool UnfreezeSections();
#endregion
}
}

@ -0,0 +1,8 @@
namespace ZeroLevel.Services.Config
{
public interface IConfigurationWriter
{
void WriteConfiguration(IConfiguration configuration);
void WriteConfigurationSet(IConfigurationSet configuration);
}
}

@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;
namespace ZeroLevel.Services.Config.Implementation
{
internal sealed class AppWebConfigReader
{
private readonly string _configFilePath;
internal AppWebConfigReader(string configFilePath = null)
{
if (configFilePath == null)
{
var appConfig = Path.Combine(Configuration.BaseDirectory, string.Format("{0}.config", System.AppDomain.CurrentDomain.FriendlyName));
if (File.Exists(appConfig))
{
_configFilePath = appConfig;
}
else
{
var webConfig = Path.Combine(Configuration.BaseDirectory, "web.config");
if (File.Exists(webConfig))
{
_configFilePath = webConfig;
}
else
{
_configFilePath = Directory.GetFiles(Configuration.BaseDirectory, "*.config").FirstOrDefault();
}
}
}
else
{
if (configFilePath.IndexOf(':') < 0)
{
this._configFilePath = Path.Combine(Configuration.BaseDirectory, configFilePath);
}
else
{
this._configFilePath = configFilePath;
}
}
}
internal IEnumerable<string> GetSections()
{
if (_configFilePath != null)
{
var xdoc = XDocument.Load(_configFilePath);
var cs = xdoc.Descendants("connectionStrings").
Select(x => x.Name.LocalName);
return xdoc.Descendants("section")
.Select(x => x.Attribute("name").Value).Union(cs);
}
return Enumerable.Empty<string>();
}
internal IEnumerable<Tuple<string, string>> ReadSection(string sectionName)
{
if (_configFilePath != null)
{
var xdoc = XDocument.Load(_configFilePath);
return xdoc.Descendants(sectionName).
SelectMany(x => x.Nodes().Where(n => null != (n as XElement)).Select(n =>
{
var xe = n as XElement;
return new Tuple<string, string>(FindName(xe), FindValue(xe));
}));
}
return Enumerable.Empty<Tuple<string, string>>();
}
private static string FindName(XElement n)
{
var attributes = n.Attributes().
ToDictionary(i => i.Name.LocalName.ToLowerInvariant(), j => j.Value);
foreach (var v in new[] { "key", "name", "code", "id" })
{
if (attributes.ContainsKey(v))
return attributes[v];
}
return n.Name.LocalName;
}
private static string FindValue(XElement n)
{
var attributes = n.Attributes().
ToDictionary(i => i.Name.LocalName.ToLowerInvariant(), j => j.Value);
foreach (var v in new[] { "value", "val", "file", "db", "connectionstring" })
{
if (attributes.ContainsKey(v))
return attributes[v];
}
return n.Value;
}
internal IEnumerable<Tuple<string, string>> ReadAppSettings()
{
return ReadSection("appSettings");
}
}
}

@ -0,0 +1,39 @@
namespace ZeroLevel.Services.Config.Implementation
{
internal sealed class ApplicationConfigReader
: IConfigurationReader
{
private readonly AppWebConfigReader _reader;
internal ApplicationConfigReader() { _reader = new AppWebConfigReader(); }
internal ApplicationConfigReader(string configFilePath) { _reader = new AppWebConfigReader(configFilePath); }
public IConfiguration ReadConfiguration()
{
var result = Configuration.Create();
foreach (var pair in _reader.ReadAppSettings())
{
result.Append(pair.Item1, pair.Item2);
}
return result;
}
public IConfigurationSet ReadConfigurationSet()
{
var result = Configuration.CreateSet();
foreach (var pair in _reader.ReadAppSettings())
{
result.Default.Append(pair.Item1, pair.Item2);
}
foreach (var section in _reader.GetSections())
{
var sectionConfig = result.CreateSection(section);
foreach (var pair in _reader.ReadSection(section))
{
sectionConfig.Append(pair.Item1, pair.Item2);
}
}
return result;
}
}
}

@ -0,0 +1,54 @@
using System;
using System.Globalization;
namespace ZeroLevel.Services.Config.Implementation
{
internal sealed class CommandLineReader
: IConfigurationReader
{
private readonly string[] _args;
public CommandLineReader(string[] args)
{
_args = args;
}
public IConfiguration ReadConfiguration()
{
var result = Configuration.Create();
if (_args != null)
{
try
{
foreach (string arg in _args)
{
int index = arg.IndexOf('=');
string key;
string value;
if (index >= 0)
{
key = arg.Substring(0, index).TrimStart('/').Trim().ToLower(CultureInfo.CurrentCulture);
value = arg.Substring(index + 1, arg.Length - index - 1).Trim(' ', '"');
}
else
{
key = arg.TrimStart('-', '/').Trim().ToLower(CultureInfo.CurrentCulture);
value = string.Empty;
}
result.Append(key, value);
}
}
catch (Exception ex)
{
throw new InvalidOperationException("Не удалось создать конфигурацию из аргументов командной строки", ex);
}
}
return result;
}
public IConfigurationSet ReadConfigurationSet()
{
return Configuration.CreateSet(ReadConfiguration());
}
}
}

@ -0,0 +1,125 @@
using System;
using System.Globalization;
using System.IO;
namespace ZeroLevel.Services.Config.Implementation
{
/// <summary>
/// Чтение конфигурации из ini файла
/// </summary>
internal sealed class IniFileReader
: IConfigurationReader
{
private readonly string _iniPath;
internal IniFileReader(string configPath)
{
if (String.IsNullOrWhiteSpace(configPath))
throw new ArgumentNullException("configPath", "Не указан путь к конфигурационному файлу");
if (!File.Exists(configPath))
{
configPath = Path.Combine(Configuration.BaseDirectory, configPath);
if (!File.Exists(configPath))
{
throw new FileNotFoundException("Не существует конфигурационный файл: " + configPath);
}
}
_iniPath = configPath;
}
public IConfiguration ReadConfiguration()
{
var result = Configuration.Create();
string sectionName = null;
foreach (var line in File.ReadAllLines(_iniPath))
{
if (string.IsNullOrWhiteSpace(line))
{
continue;
}
int index = line.IndexOf('=');
string key;
string originalKey;
string value;
if (index >= 0)
{
originalKey = line.Substring(0, index).Trim();
key = originalKey.ToLower(CultureInfo.InvariantCulture);
value = line.Substring(index + 1, line.Length - index - 1).Trim();
}
else
{
originalKey = line.Trim();
key = originalKey.ToLower(CultureInfo.InvariantCulture);
value = string.Empty;
}
if (key[0].Equals(';') || key[0].Equals('#'))
continue;
if (string.IsNullOrEmpty(value) && key[0].Equals('[') && key[key.Length - 1].Equals(']'))
{
sectionName = originalKey.Trim('[', ']');
}
else
{
if (!string.IsNullOrEmpty(sectionName))
{
result.Append(string.Format("{0}.{1}", sectionName, key), value);
}
else
{
result.Append(key, value);
}
}
}
return result;
}
public IConfigurationSet ReadConfigurationSet()
{
var result = Configuration.CreateSet();
string sectionName = null;
foreach (var line in File.ReadAllLines(_iniPath))
{
if (string.IsNullOrWhiteSpace(line))
{
continue;
}
int index = line.IndexOf('=');
string key;
string originalKey;
string value;
if (index >= 0)
{
originalKey = line.Substring(0, index).Trim();
key = originalKey.ToLower(CultureInfo.InvariantCulture);
value = line.Substring(index + 1, line.Length - index - 1).Trim();
}
else
{
originalKey = line.Trim();
key = originalKey.ToLower(CultureInfo.InvariantCulture);
value = string.Empty;
}
if (key[0].Equals(';') || key[0].Equals('#'))
continue;
if (string.IsNullOrEmpty(value) && key[0].Equals('[') && key[key.Length - 1].Equals(']'))
{
sectionName = originalKey.Trim('[', ']');
}
else
{
if (!string.IsNullOrEmpty(sectionName))
{
var currentSection = (false == result.ContainsSection(sectionName)) ? result.CreateSection(sectionName) : result.GetSection(sectionName);
currentSection.Append(key, value);
}
else
{
result.Default.Append(key, value);
}
}
}
return result;
}
}
}

@ -0,0 +1,79 @@
using System.IO;
namespace ZeroLevel.Services.Config.Implementation
{
/// <summary>
/// Запись конфигурации в ini-файл
/// </summary>
public class IniFileWriter
: IConfigurationWriter
{
/// <summary>
/// Путь к ini-файлу
/// </summary>
private readonly string _iniPath;
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="IniFileWriter"/>
/// </summary>
/// <param name="iniPath">Путь к ini-файлу</param>
public IniFileWriter(string iniPath)
{
_iniPath = iniPath;
}
/// <summary>
/// Запись простой конфигурации
/// </summary>
/// <param name="configuration">Конфигурация</param>
public void WriteConfiguration(IConfiguration configuration)
{
using (TextWriter writer = new StreamWriter(_iniPath, false))
{
foreach (string key in configuration.Keys)
{
if (configuration.Count(key) > 0)
{
foreach (string value in configuration[key])
{
writer.WriteLine(key.Trim() + "=" + value.Trim());
}
}
else
{
writer.WriteLine(key.Trim());
}
}
writer.Flush();
}
}
/// <summary>
/// Запись конфигурации разбитой по секциям
/// </summary>
/// <param name="configuration">Конфигурация</param>
public void WriteConfigurationSet(IConfigurationSet configuration)
{
using (TextWriter writer = new StreamWriter(_iniPath, false))
{
foreach (string section in configuration.SectionNames)
{
if (false == section.Equals(Configuration.DEFAULT_SECTION_NAME, System.StringComparison.Ordinal))
writer.WriteLine("[" + section + "]");
foreach (string key in configuration[section].Keys)
{
if (configuration[section].Count(key) > 0)
{
foreach (string value in configuration[section][key])
{
writer.WriteLine(key + "=" + value);
}
}
else
{
writer.WriteLine(key);
}
}
}
writer.Flush();
}
}
}
}

@ -0,0 +1,51 @@
using ZeroLevel.DocumentObjectModel.Flow;
namespace ZeroLevel.DocumentObjectModel
{
public interface IContentReader<T>
{
// Primitives
void ReadText(Text text);
void ReadQuote(Quote quote);
void ReadLink(Link link, int order);
void ReadImage(Image image, int order);
void ReadAudio(Audio audio, int order);
void ReadVideo(Video video, int order);
// Containers
void EnterSection(Section section);
void LeaveSection(Section section);
void EnterParagraph(Paragraph paragraph);
void LeaveParagraph(Paragraph paragraph);
void EnterList(List list);
void EnterListItem(List list, IContentElement item, int order);
void LeaveListItem(List list, IContentElement item, int order);
void LeaveList(List list);
void EnterTable(Table table);
void EnterColumns(Table table);
void ReadColumn(Table table, Column column, int order);
void LeaveColumns(Table table);
void EnterRow(Table table, Row row, int order);
void EnterRowCell(Table table, Row row, IContentElement cell, int order);
void LeaveRowCell(Table table, Row row, IContentElement cell, int order);
void LeaveRow(Table table, Row row, int order);
void LeaveTable(Table table);
void EnterGallery(Gallery gallery);
void LeaveGallery(Gallery gallery);
void EnterAudioplayer(Audioplayer player);
void LeaveAudioplayer(Audioplayer player);
void EnterVideoplayer(Videoplayer player);
void LeaveVideoplayer(Videoplayer player);
// Feature
void ReadForm(FormContent form);
T Complete();
}
}

@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
namespace ZeroLevel.DocumentObjectModel
{
public interface IMetadataReader<T>
{
void ReadId(Guid Id);
void ReadSummary(string summary);
void ReadHeader(string header);
void EnterIdentifier(Identifier identifier);
void ReadVersion(int version);
void ReadTimestamp(string timestamp);
void ReadDateLabel(string datelabel);
void LeaveIdentifier(Identifier identifier);
void EnterTagsBlock(TagMetadata tagBlock);
void EnterKeywords(IEnumerable<string> keywords);
void ReadKeyword(string keyword, int order);
void LeaveKeywords(IEnumerable<string> keywords);
void EnterPlaces(IEnumerable<Tag> places);
void ReadPlace(Tag place, int order);
void LeavePlaces(IEnumerable<Tag> places);
void EnterCompanies(IEnumerable<Tag> companies);
void ReadCompany(Tag company, int order);
void LeaveCompanies(IEnumerable<Tag> companies);
void EnterPersons(IEnumerable<Tag> persons);
void ReadPerson(Tag person, int order);
void LeavePersons(IEnumerable<Tag> persons);
void LeaveTagsBlock(TagMetadata tagBlock);
void EnterDescriptioveBlock(DescriptiveMetadata metadata);
void ReadAuthors(string byline);
void ReadCopiright(string copyright);
void ReadCreated(DateTime created);
void ReadLanguage(string language);
void ReadPriority(Priority priority);
void ReadSource(Agency source);
void ReadPublisher(Agency publisher);
void ReadOriginal(Tag original);
void EnterHeaders(IEnumerable<Header> headers);
void ReadHeader(Header header, int order);
void LeaveHeaders(IEnumerable<Header> headers);
void LeaveDescriptioveBlock(DescriptiveMetadata metadata);
void EnterAsides(IEnumerable<AsideContent> asides);
void ReadAside(AsideContent aside, int order);
void LeaveAsides(IEnumerable<AsideContent> asides);
void EnterAssotiations(IEnumerable<Assotiation> assotiations);
void ReadAssotiation(Assotiation assotiation, int order);
void LeaveAssotiations(IEnumerable<Assotiation> assotiations);
void EnterCategories(IEnumerable<Category> categories);
void ReadCategory(Category category, int order);
void LeaveCategories(IEnumerable<Category> categories);
T Complete();
}
}

@ -0,0 +1,129 @@
using DOM.DSL.Model;
using DOM.DSL.Services;
using DOM.DSL.Tokens;
using System;
using System.Collections.Generic;
namespace DOM.DSL.Contexts
{
internal class TBlockContext :
TContext
{
private readonly string _name;
private TToken _blockToken;
private List<TToken> _tokens;
public string Name { get { return _name; } }
public TBlockContext(TContext parent, string name)
{
ParentContext = parent;
_name = name;
_tokens = new List<TToken>();
}
public override void Read(TStringReader reader)
{
if (_name.Equals("if", StringComparison.OrdinalIgnoreCase) ||
_name.Equals("for", StringComparison.OrdinalIgnoreCase))
{
var spaces_count = reader.SkipSpaces();
if (reader.Current.Equals(TChar.TokenStart))
{
reader.Move();
var name = reader.ReadIdentity();
if (name.Equals(string.Format("end{0}", _name)))
{
reader.Move(name.Length);
return;
}
if (_elementNames.Contains(name))
{
reader.Move(name.Length);
var elementContext = new TElementContext(this, name);
elementContext.Read(reader);
_blockToken = elementContext.Complete();
var body = new TRootContext(this);
body.Read(reader);
_tokens.AddRange(body.Complete());
}
else
{
_tokens.Add(new TTextToken { Text = "@" + _name + " @" + name });
}
}
else
{
_tokens.Add(new TTextToken { Text = "@" + _name + new string(' ', spaces_count) });
}
}
else if (_name.Equals("block", StringComparison.OrdinalIgnoreCase))
{
/*----------------------------------------------------------------*/
if (reader.Current.Equals(TChar.PropertyOrFuncStart))
{
reader.Move();
var name = reader.ReadIdentity();
if (name.Equals(string.Format("end{0}", _name)))
{
reader.Move(name.Length);
return;
}
if (name.Equals("to", StringComparison.OrdinalIgnoreCase))
{
reader.Move(name.Length);
if (reader.Current.Equals(TChar.FuncArgsStart))
{
reader.Move();
reader.SkipSpaces();
var var_name = reader.ReadIdentity();
if (string.IsNullOrWhiteSpace(var_name))
{
return;
}
reader.Move(var_name.Length);
reader.SkipSpaces();
if (reader.Current.Equals(TChar.FuncArgsEnd))
{
reader.Move();
reader.SkipSpaces();
reader.SkipBreaks();
_blockToken = new TTextToken { Text = var_name };
var body = new TRootContext(this);
body.Read(reader);
_tokens.AddRange(body.Complete());
}
}
}
}
/*----------------------------------------------------------------*/
}
else if (_name.Equals("comm", StringComparison.OrdinalIgnoreCase))
{
do
{
var offset = reader.FindOffsetTo(TChar.TokenStart);
if (offset == -1) return;
reader.Move(offset + 1);
var name = reader.ReadIdentity();
if (name.Equals(string.Format("end{0}", _name)))
{
reader.Move(name.Length);
return;
}
} while (reader.EOF == false);
}
else
{
var body = new TRootContext(this);
body.Read(reader);
_tokens.AddRange(body.Complete());
}
}
public TToken Complete()
{
return new TBlockToken(_name, _blockToken, _tokens);
}
}
}

@ -0,0 +1,27 @@
using DOM.DSL.Services;
using System.Collections.Generic;
namespace DOM.DSL.Contexts
{
internal abstract class TContext
{
protected static HashSet<string> _blockNames = new HashSet<string> { "for", "if", "flow", "comm", "block" };
protected static HashSet<string> _endblockNames = new HashSet<string> { "endfor", "endif", "endflow", "endcomm", "endblock" };
protected static HashSet<string> _elementNames = new HashSet<string> { "text", "now", "nowutc", "guid",
"id","summary","header","categories", "directions","author","copyright","created",
"lang","priority","source","publisher","meta","timestamp","datelabel",
"version","keywords","companies","persons","places",
"self", "order", "counter", "aside", "assotiations",
"list","listitem","text","link","image","quote",
"form","video","audio","section","paragraph","table",
"columns","column","tablerow","tablecell","videoplayer","audioplayer",
"gallery", "content", "system", "buf", "build",
"env", "var", "identifier", "desc", "descriptive", "headers", "tags",
"null", "empty", "utc"
};
public TContext ParentContext { get; protected set; }
public abstract void Read(TStringReader reader);
}
}

@ -0,0 +1,78 @@
using DOM.DSL.Model;
using DOM.DSL.Services;
using DOM.DSL.Tokens;
namespace DOM.DSL.Contexts
{
internal class TElementContext :
TContext
{
private readonly string _name;
private TToken _next;
public TElementContext(TContext parent, string name)
{
ParentContext = parent;
_name = name;
}
public override void Read(TStringReader reader)
{
if (reader.EOF == false && reader.Current == TChar.PropertyOrFuncStart)
{
if (reader.Move())
{
reader.SkipBreaks();
var name = reader.ReadIdentity();
if (false == string.IsNullOrWhiteSpace(name))
{
reader.Move(name.Length);
if (this._name.Equals("system"))
{
// Like a '@system.ignorespace'
if (reader.Current == TChar.FuncArgsStart)
{
reader.Move();
reader.SkipBreaks();
var context = new TFunctionContext(this, name);
context.Read(reader);
_next = new TSystemToken { Command = name, Arg = context.Complete() };
}
else
{
_next = new TSystemToken { Command = name, Arg = null };
}
}
else
{
if (reader.Current == TChar.FuncArgsStart)
{
// Function '@now.format(dd-mm)'
reader.Move();
var context = new TFunctionContext(this, name);
context.Read(reader);
_next = context.Complete();
}
else
{
// Property '@now.year'
var context = new TPropertyContext(this, name);
context.Read(reader);
_next = context.Complete();
}
}
}
else
{
_next = new TTextToken { Text = TChar.PropertyOrFuncStart + name };
}
}
}
}
public TToken Complete()
{
return new TElementToken { ElementName = _name, NextToken = _next?.Clone() };
}
}
}

@ -0,0 +1,187 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using DOM.DSL.Tokens;
using DOM.DSL.Services;
using DOM.DSL.Model;
namespace DOM.DSL.Contexts
{
internal class TFunctionContext :
TContext
{
private string _name;
private List<TToken> _argTokens;
private TToken _nextToken;
public TFunctionContext(TContext parent, string name)
{
ParentContext = parent;
_argTokens = new List<TToken>();
_name = name;
}
public override void Read(TStringReader reader)
{
var text = new StringBuilder();
var argTokens = new List<TToken>();
var flushTextToken = new Action(() =>
{
if (text.Length > 0)
{
argTokens.Add(new TTextToken { Text = text.ToString() });
text.Clear();
}
});
var flushArgToken = new Action(() =>
{
if (argTokens.Count > 0)
{
_argTokens.Add(new TBlockToken("", null, argTokens.Select(t => t.Clone()).ToArray()));
argTokens.Clear();
}
});
while (reader.EOF == false)
{
switch (reader.Current)
{
#region Ecsaping
case TChar.Escape:
{
switch (reader.Next)
{
case 's':
text.Append(' ');
reader.Move(2);
break;
case 'r':
text.Append(TChar.CaretReturn);
reader.Move(2);
break;
case 'n':
text.Append(TChar.Newline);
reader.Move(2);
break;
case 't':
text.Append(TChar.Tab);
reader.Move(2);
break;
case '@':
case '(':
case ')':
case '.':
case ',':
case '\\':
text.Append(reader.Next);
reader.Move(2);
break;
default:
text.Append(reader.Current);
reader.Move();
break;
}
}
break;
#endregion
case TChar.FuncArgsEnd:
{
flushTextToken();
flushArgToken();
if (reader.Next == TChar.PropertyOrFuncStart)
{
if (reader.Move(2))
{
reader.SkipBreaks();
var name = reader.ReadIdentity();
if (false == string.IsNullOrWhiteSpace(name))
{
reader.Move(name.Length);
if (reader.Current == TChar.FuncArgsStart)
{
// Function '@now.format(dd-mm)'
reader.Move();
var context = new TFunctionContext(this, name);
context.Read(reader);
_nextToken = context.Complete();
}
else
{
// Property '@now.year'
var context = new TPropertyContext(this, name);
context.Read(reader);
_nextToken = context.Complete();
}
}
}
}
else
{
reader.Move();
}
}
return;
case TChar.FuncArgsSeparator:
flushTextToken();
flushArgToken();
reader.Move();
break;
case TChar.TokenStart:
{
if (reader.Move())
{
var name = reader.ReadIdentity();
if (_elementNames.Contains(name))
{
flushTextToken();
reader.Move(name.Length);
var elementContext = new TElementContext(this, name);
elementContext.Read(reader);
argTokens.Add(elementContext.Complete());
}
else if (_blockNames.Contains(name))
{
flushTextToken();
reader.Move(name.Length);
var blockContext = new TBlockContext(this, name);
blockContext.Read(reader);
argTokens.Add(blockContext.Complete());
}
else
{
text.Append(TChar.TokenStart);
text.Append(reader.Current);
}
}
else
{
text.Append(reader.Current);
}
}
break;
default:
{
text.Append(reader.Current);
reader.Move();
}
break;
}
}
flushTextToken();
flushArgToken();
}
public TToken Complete()
{
return new TFunctionToken
{
FunctionName = _name,
NextToken = _nextToken?.Clone(),
FunctionArgs = _argTokens?.Select(t => t.Clone())
};
}
}
}

@ -0,0 +1,185 @@
using DOM.DSL.Model;
using DOM.DSL.Services;
using DOM.DSL.Tokens;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace DOM.DSL.Contexts
{
internal class TPropertyContext :
TContext
{
private string _name;
private List<TToken> _indexTokens;
private TToken _nextToken;
public TPropertyContext(TContext parent, string name)
{
ParentContext = parent;
_name = name;
_indexTokens = new List<TToken>();
}
public override void Read(TStringReader reader)
{
if (reader.EOF) return;
if (reader.Current == TChar.PropertyOrFuncStart)
{
if (reader.Move())
{
reader.SkipBreaks();
var name = reader.ReadIdentity();
if (false == string.IsNullOrWhiteSpace(name))
{
reader.Move(name.Length);
if (reader.Current == TChar.FuncArgsStart)
{
// Function '@now.format(dd-mm)'
reader.Move();
var context = new TFunctionContext(this, name);
context.Read(reader);
_nextToken = context.Complete();
}
else
{
// Property '@now.year'
var context = new TPropertyContext(this, name);
context.Read(reader);
_nextToken = context.Complete();
}
}
}
}
else if (reader.Current == TChar.PropertyIndexStart)
{
var text = new StringBuilder();
var flushTextToken = new Action(() =>
{
if (text.Length > 0)
{
_indexTokens.Add(new TTextToken { Text = text.ToString() });
text.Clear();
}
});
reader.Move();
while (reader.EOF == false)
{
switch (reader.Current)
{
#region Ecsaping
case TChar.Escape:
{
switch (reader.Next)
{
case 's':
text.Append(' ');
reader.Move(2);
break;
case 'r':
text.Append(TChar.CaretReturn);
reader.Move(2);
break;
case 'n':
text.Append(TChar.Newline);
reader.Move(2);
break;
case 't':
text.Append(TChar.Tab);
reader.Move(2);
break;
case '@':
case '(':
case ')':
case '.':
case ',':
case '\\':
text.Append(reader.Next);
reader.Move(2);
break;
default:
text.Append(reader.Current);
reader.Move();
break;
}
}
break;
#endregion
case TChar.PropertyIndexEnd:
{
flushTextToken();
if (reader.Next == TChar.PropertyOrFuncStart)
{
reader.Move(2);
reader.SkipBreaks();
var name = reader.ReadIdentity();
reader.Move(name.Length);
if (reader.Current.Equals(TChar.FuncArgsStart))
{
reader.Move();
var context = new TFunctionContext(this, name);
context.Read(reader);
_nextToken = context.Complete();
}
else
{
text.Append(name);
}
}
else
{
reader.Move();
}
}
return;
case TChar.TokenStart:
{
if (reader.Move())
{
var name = reader.ReadIdentity();
if (_elementNames.Contains(name))
{
flushTextToken();
reader.Move(name.Length);
var elementContext = new TElementContext(this, name);
elementContext.Read(reader);
_indexTokens.Add(elementContext.Complete());
}
else
{
text.Append(TChar.TokenStart);
text.Append(reader.Current);
}
}
else
{
text.Append(reader.Current);
}
}
break;
default:
{
text.Append(reader.Current);
reader.Move();
}
break;
}
}
flushTextToken();
}
}
public TToken Complete()
{
return new TPropertyToken
{
PropertyName = _name,
PropertyIndex = new TBlockToken(_name, null, _indexTokens.Select(t => t.Clone()).ToArray()),
NextToken = _nextToken?.Clone()
};
}
}
}

@ -0,0 +1,146 @@
using DOM.DSL.Model;
using DOM.DSL.Services;
using DOM.DSL.Tokens;
using System;
using System.Collections.Generic;
using System.Text;
namespace DOM.DSL.Contexts
{
internal class TRootContext :
TContext
{
private readonly List<TToken> _tokens;
public TRootContext()
{
ParentContext = null;
_tokens = new List<TToken>();
}
public TRootContext(TContext parent)
{
ParentContext = parent;
_tokens = new List<TToken>();
}
public override void Read(TStringReader reader)
{
var text = new StringBuilder();
var flushTextToken = new Action(() =>
{
if (text.Length > 0)
{
_tokens.Add(new TTextToken { Text = text.ToString() });
text.Clear();
}
});
while (reader.EOF == false)
{
switch (reader.Current)
{
#region Ecsaping
case TChar.Escape:
{
switch (reader.Next)
{
case 's':
text.Append(' ');
reader.Move(2);
break;
case 'r':
text.Append(TChar.CaretReturn);
reader.Move(2);
break;
case 'n':
text.Append(TChar.Newline);
reader.Move(2);
break;
case 't':
text.Append(TChar.Tab);
reader.Move(2);
break;
case '@':
case '(':
case ')':
case '.':
case ',':
case '\\':
text.Append(reader.Next);
reader.Move(2);
break;
default:
text.Append(reader.Current);
reader.Move();
break;
}
}
break;
#endregion
case TChar.TokenStart:
{
if (reader.Move())
{
var name = reader.ReadIdentity();
if (_elementNames.Contains(name))
{
flushTextToken();
reader.Move(name.Length);
var elementContext = new TElementContext(this, name);
reader.SkipBreaks();
elementContext.Read(reader);
_tokens.Add(elementContext.Complete());
}
else if (_blockNames.Contains(name))
{
flushTextToken();
reader.Move(name.Length);
var blockContext = new TBlockContext(this, name);
blockContext.Read(reader);
_tokens.Add(blockContext.Complete());
}
else if (ParentContext != null && ParentContext is TBlockContext &&
name.Equals("end" + (ParentContext as TBlockContext).Name, StringComparison.OrdinalIgnoreCase))
{
reader.Move(name.Length);
flushTextToken();
return;
}
else
{
text.Append(TChar.TokenStart);
text.Append(name);
reader.Move(name.Length);
}
}
else
{
text.Append(TChar.TokenStart);
reader.Move();
}
}
break;
case TChar.CaretReturn:
case TChar.Newline:
case TChar.Tab:
reader.Move();
break;
default:
{
text.Append(reader.Current);
reader.Move();
}
break;
}
}
flushTextToken();
}
public IEnumerable<TToken> Complete()
{
return _tokens;
}
}
}

@ -0,0 +1,20 @@
using System.Text;
using ZeroLevel.DocumentObjectModel.Flow;
namespace DOM.DSL.Contracts
{
public interface ISpecialTableBuilder
{
/// <summary>
/// Указывает что ожидается запись тела ячейки таблицы
/// </summary>
bool WaitCellBody { get; }
void WriteToCell(string part);
void EnterTable(Column[] colunmns);
void EnterRow(int count_columns);
void EnterCell(int order);
void LeaveCell();
void LeaveRow();
void FlushTable(StringBuilder builder);
}
}

@ -0,0 +1,9 @@
using DOM.DSL.Tokens;
namespace DOM.DSL.Contracts
{
public interface TCloneable
{
TToken Clone();
}
}

@ -0,0 +1,47 @@
namespace DOM.DSL.Model
{
internal sealed class DOMRenderElementCounter
{
public int SectionId { get; private set; } = -1;
public int ParagraphId { get; private set; } = -1;
public int ListId { get; private set; } = -1;
public int ListItemId { get; private set; } = -1;
public int TableId { get; private set; } = -1;
public int ColumnId { get; private set; } = -1;
public int RowId { get; private set; } = -1;
public int CellId { get; private set; } = -1;
public int FormId { get; private set; } = -1;
public int LinkId { get; private set; } = -1;
public int QuoteId { get; private set; } = -1;
public int TextId { get; private set; } = -1;
public int AudioplayerId { get; private set; } = -1;
public int AudioId { get; private set; } = -1;
public int VideoplayerId { get; private set; } = -1;
public int VideoId { get; private set; } = -1;
public int GalleryId { get; private set; } = -1;
public int ImageId { get; private set; } = -1;
public void IncSectionId() { SectionId++; }
public void IncParagraphId() { ParagraphId++; }
public void IncListId() { ListId++; }
public void IncListItemId() { ListItemId++; }
public void IncTableId() { TableId++; }
public void IncColumnId() { ColumnId++; }
public void IncRowId() { RowId++; }
public void IncCellId() { CellId++; }
public void IncFormId() { FormId++; }
public void IncLinkId() { LinkId++; }
public void IncQuoteId() { QuoteId++; }
public void IncTextId() { TextId++; }
public void IncAudioplayerId() { AudioplayerId++; }
public void IncAudioId() { AudioId++; }
public void IncVideoplayerId() { VideoplayerId++; }
public void IncVideoId() { VideoId++; }
public void IncGalleryId() { GalleryId++; }
public void IncImageId() { ImageId++; }
}
}

@ -0,0 +1,17 @@
namespace DOM.DSL.Model
{
internal class TChar
{
public const char TokenStart = '@';
public const char PropertyOrFuncStart = '.';
public const char PropertyIndexStart = '[';
public const char PropertyIndexEnd = ']';
public const char FuncArgsStart = '(';
public const char FuncArgsEnd = ')';
public const char FuncArgsSeparator = ',';
public const char Escape = '\\';
public const char CaretReturn = '\r';
public const char Newline = '\n';
public const char Tab = '\t';
}
}

@ -0,0 +1,144 @@
using System;
using System.Collections.Generic;
using System.Linq;
using ZeroLevel.DocumentObjectModel;
using ZeroLevel.DocumentObjectModel.Flow;
namespace DOM.DSL.Model
{
internal class TContentElement
{
private readonly Document _document;
public TContentElement(Document document)
{
_document = document;
}
private static void TraversElement(IContentElement element, ContentElementType type, Action<IContentElement> handler)
{
if (element.Type == type)
{
handler(element);
}
switch (element.Type)
{
// Containers
case ContentElementType.Section:
var section = (element as Section);
foreach (var item in section.Parts)
{
TraversElement(item, type, handler);
}
break;
case ContentElementType.Paragraph:
var paragraph = (element as Paragraph);
foreach (var item in paragraph.Parts)
{
TraversElement(item, type, handler);
}
break;
case ContentElementType.List:
var list = (element as List);
foreach (var item in list.Items)
{
TraversElement(item, type, handler);
}
break;
case ContentElementType.Gallery:
var gallery = (element as Gallery);
foreach (var item in gallery.Images)
{
TraversElement(item, type, handler);
}
break;
case ContentElementType.Audioplayer:
var audioplayer = (element as Audioplayer);
foreach (var item in audioplayer.Tracks)
{
TraversElement(item, type, handler);
}
break;
case ContentElementType.Videoplayer:
var videoplayer = (element as Videoplayer);
foreach (var item in videoplayer.Playlist)
{
TraversElement(item, type, handler);
}
break;
case ContentElementType.Table:
var table = (element as Table);
foreach (var column in table.Columns)
{
TraversElement(column, type, handler);
}
foreach (var row in table.Rows)
{
TraversElement(row, type, handler);
foreach (var cell in row.Cells)
{
TraversElement(cell, type, handler);
}
}
break;
}
}
private ContentElementType ParseContentElementType(string element_name)
{
switch (element_name)
{
case "section":
return ContentElementType.Section;
case "paragraph":
return ContentElementType.Paragraph;
case "link":
return ContentElementType.Link;
case "list":
return ContentElementType.List;
case "table":
return ContentElementType.Table;
case "audio":
return ContentElementType.Audio;
case "audioplayer":
return ContentElementType.Audioplayer;
case "form":
return ContentElementType.Form;
case "gallery":
return ContentElementType.Gallery;
case "image":
return ContentElementType.Image;
case "video":
return ContentElementType.Video;
case "videoplayer":
return ContentElementType.Videoplayer;
case "quote":
return ContentElementType.Quote;
case "text":
return ContentElementType.Text;
case "column":
return ContentElementType.Column;
case "row":
return ContentElementType.Row;
}
return ContentElementType.Unknown;
}
public IEnumerable<IContentElement> Find(string elementName, string index)
{
var type = ParseContentElementType(elementName);
if (type == ContentElementType.Unknown) return Enumerable.Empty<IContentElement>();
var list = new List<IContentElement>();
foreach (var section in _document.Content.Sections)
{
TraversElement(section, type, e=>list.Add(e));
}
return list;
}
public override string ToString()
{
return "Content";
}
}
}

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace DOM.DSL.Model
{
public sealed class TEnvironment
{
public int Delay { get; set; } = 0;
public string FileName { get; set; } = null;
public Encoding Encoding { get; set; } = null;
public string ContractName { get; set; } = null;
public string SubscriptionName { get; set; } = null;
public Guid SubscriptionId { get; set; } = Guid.Empty;
public IDictionary<string, object> CustomVariables { get; }
public TEnvironment()
{
CustomVariables = new Dictionary<string, object>();
}
public void AddCustomVar(string name, object value)
{
CustomVariables.Add(name.ToLowerInvariant().Trim(), value);
}
}
}

@ -0,0 +1,426 @@
using DOM.DSL.Contracts;
using DOM.DSL.Services;
using DOM.DSL.Tokens;
namespace DOM.DSL.Model
{
internal sealed class TFlowRules
{
#region Rules
public TBlockToken ListPrefix;
public TBlockToken ListPostfix;
public TBlockToken ListItemPrefix;
public TBlockToken ListItemPostfix;
public TBlockToken TextPrefix;
public TBlockToken TextTemplate;
public TBlockToken TextPostfix;
public TBlockToken LinkPrefix;
public TBlockToken LinkTemplate;
public TBlockToken LinkPostfix;
public TBlockToken ImagePrefix;
public TBlockToken ImageTemplate;
public TBlockToken ImagePostfix;
public TBlockToken QuotePrefix;
public TBlockToken QuoteTemplate;
public TBlockToken QuotePostfix;
public TBlockToken VideoPrefix;
public TBlockToken VideoTemplate;
public TBlockToken VideoPostfix;
public TBlockToken AudioPrefix;
public TBlockToken AudioTemplate;
public TBlockToken AudioPostfix;
public TBlockToken TablePrefix;
public TBlockToken TablePostfix;
public TBlockToken SectionPrefix;
public TBlockToken SectionPostfix;
public TBlockToken ParagraphPrefix;
public TBlockToken ParagraphPostfix;
public TBlockToken ColumnsPrefix;
public TBlockToken ColumnsPostfix;
public TBlockToken ColumnPrefix;
public TBlockToken ColumnTemplate;
public TBlockToken ColumnPostfix;
public TBlockToken RowPrefix;
public TBlockToken RowPostfix;
public TBlockToken FirstRowCellPrefix;
public TBlockToken FirstRowCellPostfix;
public TBlockToken CellPrefix;
public TBlockToken CellPostfix;
public TBlockToken FormPrefix;
public TBlockToken FormTemplate;
public TBlockToken FormPostfix;
public TBlockToken AudioplayerPrefix;
public TBlockToken AudioplayerPostfix;
public TBlockToken VideoplayerPrefix;
public TBlockToken VideoplayerPostfix;
public TBlockToken GalleryPrefix;
public TBlockToken GalleryPostfix;
#endregion
#region Special table builder
public bool UseSpecialTableBuilder = false;
public ISpecialTableBuilder SpecialTableBuilder;
#endregion
public void Bootstrap()
{
if (null == SectionPrefix) SectionPrefix = null;
if (null == SectionPostfix) SectionPostfix = null;
if (null == ParagraphPrefix) ParagraphPrefix = null;
if (null == ParagraphPostfix) ParagraphPostfix = null;
if (null == ListPrefix) ListPrefix = null;
if (null == ListPostfix) ListPostfix = null;
if (null == ListItemPrefix) ListItemPrefix = null;
if (null == ListItemPostfix) ListItemPostfix = null;
if (null == TablePrefix) TablePrefix = null;
if (null == TablePostfix) TablePostfix = null;
if (null == ColumnsPrefix) ColumnsPrefix = null;
if (null == ColumnsPostfix) ColumnsPostfix = null;
if (null == ColumnPrefix) ColumnPrefix = null;
if (null == ColumnTemplate) ColumnTemplate = null;
if (null == ColumnPostfix) ColumnPostfix = null;
if (null == RowPrefix) RowPrefix = null;
if (null == RowPostfix) RowPostfix = null;
if (null == CellPrefix) CellPrefix = null;
if (null == CellPostfix) CellPostfix = null;
if (null == FirstRowCellPrefix) FirstRowCellPrefix = null;
if (null == FirstRowCellPostfix) FirstRowCellPostfix = null;
if (null == AudioplayerPrefix) AudioplayerPrefix = null;
if (null == AudioplayerPostfix) AudioplayerPostfix = null;
if (null == VideoplayerPrefix) VideoplayerPrefix = null;
if (null == VideoplayerPostfix) VideoplayerPostfix = null;
if (null == GalleryPrefix) GalleryPrefix = null;
if (null == GalleryPostfix) GalleryPostfix = null;
if (null == FormPrefix) FormPrefix = null;
if (null == FormTemplate) FormTemplate = null;
if (null == FormPostfix) FormPostfix = null;
if (null == VideoPrefix) VideoPrefix = null;
if (null == VideoTemplate) VideoTemplate = null;
if (null == VideoPostfix) VideoPostfix = null;
if (null == AudioPrefix) AudioPrefix = null;
if (null == AudioTemplate) AudioTemplate = null;
if (null == AudioPostfix) AudioPostfix = null;
if (null == ImagePrefix) ImagePrefix = null;
if (null == ImageTemplate) ImageTemplate = null;
if (null == ImagePostfix) ImagePostfix = null;
if (null == LinkPrefix) LinkPrefix = null;
if (null == LinkTemplate) LinkTemplate = null;
if (null == LinkPostfix) LinkPostfix = null;
if (null == QuotePrefix) QuotePrefix = null;
if (null == QuoteTemplate) QuoteTemplate = new TBlockToken(new[] { new TElementToken { ElementName = "self" } });
if (null == QuotePostfix) QuotePostfix = null;
if (null == TextPrefix) TextPrefix = null;
if (null == TextTemplate) TextTemplate = new TBlockToken(new[] { new TElementToken { ElementName = "self" } });
if (null == TextPostfix) TextPostfix = null;
}
public void UpdateRule(string elementName, string functionName, TBlockToken rule_token, string special)
{
switch (elementName)
{
case "list":
switch (functionName)
{
case "prefix":
ListPrefix = rule_token;
break;
case "postfix":
ListPostfix = rule_token;
break;
case "ignore":
ListPostfix = ListPrefix = null;
break;
}
break;
case "listitem":
switch (functionName)
{
case "prefix":
ListItemPrefix = rule_token;
break;
case "postfix":
ListItemPostfix = rule_token;
break;
case "ignore":
ListItemPrefix = ListItemPostfix = null;
break;
}
break;
case "text":
switch (functionName)
{
case "prefix":
TextPrefix = rule_token;
break;
case "template":
TextTemplate = rule_token;
break;
case "postfix":
TextPostfix = rule_token;
break;
case "ignore":
TextPrefix = TextTemplate = TextPostfix = null;
break;
}
break;
case "link":
switch (functionName)
{
case "prefix":
LinkPrefix = rule_token;
break;
case "template":
LinkTemplate = rule_token;
break;
case "postfix":
LinkPostfix = rule_token;
break;
case "ignore":
LinkPrefix = LinkTemplate = LinkPostfix = null;
break;
}
break;
case "image":
switch (functionName)
{
case "prefix":
ImagePrefix = rule_token;
break;
case "template":
ImageTemplate = rule_token;
break;
case "postfix":
ImagePostfix = rule_token;
break;
case "ignore":
ImagePrefix = ImageTemplate = ImagePostfix = null;
break;
}
break;
case "quote":
switch (functionName)
{
case "prefix":
QuotePrefix = rule_token;
break;
case "template":
QuoteTemplate = rule_token;
break;
case "postfix":
QuotePostfix = rule_token;
break;
case "ignore":
QuotePrefix = QuoteTemplate = QuotePostfix = null;
break;
}
break;
case "form":
switch (functionName)
{
case "prefix":
FormPrefix = rule_token;
break;
case "template":
FormTemplate = rule_token;
break;
case "postfix":
FormPostfix = rule_token;
break;
case "ignore":
FormPrefix = FormTemplate = FormPostfix = null;
break;
}
break;
case "video":
switch (functionName)
{
case "prefix":
VideoPrefix = rule_token;
break;
case "template":
VideoTemplate = rule_token;
break;
case "postfix":
VideoPostfix = rule_token;
break;
case "ignore":
VideoPrefix = VideoTemplate = VideoPostfix = null;
break;
}
break;
case "audio":
switch (functionName)
{
case "prefix":
AudioPrefix = rule_token;
break;
case "template":
AudioTemplate = rule_token;
break;
case "postfix":
AudioPostfix = rule_token;
break;
case "ignore":
AudioPrefix = AudioTemplate = AudioPostfix = null;
break;
}
break;
case "section":
switch (functionName)
{
case "prefix":
SectionPrefix = rule_token;
break;
case "postfix":
SectionPostfix = rule_token;
break;
case "ignore":
SectionPrefix = SectionPostfix = null;
break;
}
break;
case "paragraph":
switch (functionName)
{
case "prefix":
ParagraphPrefix = rule_token;
break;
case "postfix":
ParagraphPostfix = rule_token;
break;
case "ignore":
ParagraphPrefix = ParagraphPostfix = null;
break;
}
break;
case "table":
switch (functionName)
{
case "prefix":
TablePrefix = rule_token;
break;
case "postfix":
TablePostfix = rule_token;
break;
case "ignore":
TablePrefix = TablePostfix = null;
break;
case "special": // Использование захардкоженного преобразования таблицы
//TablePrefix = TablePostfix = null;
ColumnsPrefix = ColumnsPostfix = null;
ColumnPrefix = ColumnTemplate = ColumnPostfix = null;
RowPrefix = RowPostfix = null;
CellPrefix = CellPostfix = null;
// Аргументы: (style, paddings l-t-r-b, maxcellwidth, maxtablewidth)
UseSpecialTableBuilder = true;
SpecialTableBuilder = SpecialTableBuilderFactory.CreateSpecialTableBuilder(special);
if (SpecialTableBuilder == null) UseSpecialTableBuilder = false;
break;
}
break;
case "columns":
switch (functionName)
{
case "prefix":
ColumnsPrefix = rule_token;
break;
case "postfix":
ColumnsPostfix = rule_token;
break;
case "ignore":
ColumnsPrefix = ColumnsPostfix = null;
break;
}
break;
case "column":
switch (functionName)
{
case "prefix":
ColumnPrefix = rule_token;
break;
case "template":
ColumnTemplate = rule_token;
break;
case "postfix":
ColumnPostfix = rule_token;
break;
case "ignore":
ColumnPrefix = ColumnTemplate = ColumnPostfix = null;
break;
}
break;
case "tablerow":
switch (functionName)
{
case "prefix":
RowPrefix = rule_token;
break;
case "postfix":
RowPostfix = rule_token;
break;
case "ignore":
RowPrefix = RowPostfix = null;
break;
}
break;
case "tablecell":
switch (functionName)
{
case "prefix":
CellPrefix = rule_token;
break;
case "postfix":
CellPostfix = rule_token;
break;
case "ignore":
CellPrefix = CellPostfix = null;
break;
}
break;
case "videoplayer":
switch (functionName)
{
case "prefix":
VideoplayerPrefix = rule_token;
break;
case "postfix":
VideoplayerPostfix = rule_token;
break;
case "ignore":
VideoplayerPrefix = VideoplayerPostfix = null;
break;
}
break;
case "audioplayer":
switch (functionName)
{
case "prefix":
AudioplayerPrefix = rule_token;
break;
case "postfix":
AudioplayerPostfix = rule_token;
break;
case "ignore":
AudioplayerPrefix = AudioplayerPostfix = null;
break;
}
break;
case "gallery":
switch (functionName)
{
case "prefix":
GalleryPrefix = rule_token;
break;
case "postfix":
GalleryPostfix = rule_token;
break;
case "ignore":
GalleryPrefix = GalleryPostfix = null;
break;
}
break;
}
}
}
}

@ -0,0 +1,89 @@
using DOM.DSL.Services;
using System.Linq;
using System.Text;
namespace DOM.DSL.Model
{
/// <summary>
/// Feature
/// </summary>
internal class TRenderOptions
{
public int MaxStringWidth { get; set; } = -1;
public bool ValidateAsJson { get; set; } = false;
public bool ValidateAsHtml { get; set; } = false;
public bool ValidateAsXml { get; set; } = false;
}
internal static class TRenderUtils
{
public static string SplitOn(string initial, int max)
{
var text = new StringBuilder();
var reader = new TStringReader(initial);
var current_max = 0;
while (reader.EOF == false)
{
if (char.IsLetterOrDigit(reader.Current))
{
var word = reader.ReadWord();
if ((current_max + word.Length) < max)
{
text.Append(word);
current_max += word.Length;
}
else if (word.Length >= max)
{
var lines = Enumerable.Range(0, word.Length / max)
.Select(i => word.Substring(i * max, max)).
ToArray();
int k = 0;
if(current_max > 0) text.Append("\r\n");
for (; k < lines.Length - 1; k++)
{
text.Append(lines[k]);
text.Append("\r\n");
}
text.Append(lines[k]);
current_max = lines[k].Length;
}
else
{
text.Append("\r\n");
current_max = 0;
text.Append(word);
current_max = word.Length;
}
reader.Move(word.Length);
}
else if (reader.Current == '\n')
{
current_max = 0;
text.Append(reader.Current);
reader.Move();
}
else
{
text.Append(reader.Current);
current_max++;
if (current_max >= max)
{
if (reader.Next == '\r' &&
reader.FindOffsetTo('\n') == 2)
{
text.Append("\r\n");
reader.Move(2);
}
else if (reader.Next != '\n')
{
text.Append("\r\n");
}
current_max = 0;
}
reader.Move();
}
}
return text.ToString();
}
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save

Powered by TurnKey Linux.