diff --git a/Tests/ZeroLevel.UnitTests/ConfigurationTest.cs b/Tests/ZeroLevel.UnitTests/ConfigurationTest.cs index ea6fab3..06a51aa 100644 --- a/Tests/ZeroLevel.UnitTests/ConfigurationTest.cs +++ b/Tests/ZeroLevel.UnitTests/ConfigurationTest.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.IO; using System.Linq; using Xunit; using ZeroLevel.Services.Config; @@ -125,6 +126,103 @@ namespace ZeroLevel.UnitTests Assert.Equal(i, set.Default.First("i")); } + /* +{ + "url": "http://tes.io", + "port": 9091, + "limits": { + "min": { + "cpu": 20, + "gpu": 0, + "mem": 200 + }, + "max": { + "cpu": 40, + "gpu": 50, + "mem": 600 + }, + "optional": null, + "persistent": true + } +} + */ + [Fact] + public void JsonConfigTest() + { + // Arrange + var json = ZeroLevel.UnitTests.Properties.Resources.test_json_config; + var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); + File.WriteAllBytes(tempFilePath, json); + + // Act + IConfigurationSet set; + try + { + set = Configuration.ReadSetFromJsonFile(tempFilePath); + } + finally + { + File.Delete(tempFilePath); + } + + // Assert + Assert.Equal("http://tes.io", set.Default.First("url")); + Assert.Equal(9091, set.Default.First("port")); + Assert.Equal(20, set["limits.min"].First("cpu")); + Assert.Equal(0, set["limits.min"].First("gpu")); + Assert.Equal(200, set["limits.min"].First("mem")); + Assert.Equal(40, set["limits.max"].First("cpu")); + Assert.Equal(50, set["limits.max"].First("gpu")); + Assert.Equal(600, set["limits.max"].First("mem")); + Assert.Equal(string.Empty, set["limits"].First("optional")); + Assert.Equal(true, set["limits"].First("persistent")); + } + + + /* +--- +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: my-ingress +spec: + rules: + - host: my-app.s<номер своего логина>.edu.slurm.io + http: + paths: + - backend: + serviceName: my-service + servicePort: 80 +... + */ + [Fact] + public void YamlConfigTest() + { + // Arrange + var json = ZeroLevel.UnitTests.Properties.Resources.test_yaml_config; + var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); + File.WriteAllBytes(tempFilePath, json); + + // Act + IConfigurationSet set; + try + { + set = Configuration.ReadSetFromYamlFile(tempFilePath); + } + finally + { + File.Delete(tempFilePath); + } + + // Assert + Assert.Equal("extensions/v1beta1", set.Default.First("apiVersion")); + Assert.Equal("Ingress", set.Default.First("kind")); + Assert.Equal("my-ingress", set["metadata"].First("name")); + Assert.Equal("my-app.s<номер своего логина>.edu.slurm.io", set["spec.rules.0"].First("host")); + Assert.Equal("my-service", set["spec.rules.0.http.paths.0.backend"].First("serviceName")); + Assert.Equal(80, set["spec.rules.0.http.paths.0.backend"].First("servicePort")); + } + [Fact] public void MergeTest() diff --git a/Tests/ZeroLevel.UnitTests/Properties/Resources.Designer.cs b/Tests/ZeroLevel.UnitTests/Properties/Resources.Designer.cs new file mode 100644 index 0000000..da1d807 --- /dev/null +++ b/Tests/ZeroLevel.UnitTests/Properties/Resources.Designer.cs @@ -0,0 +1,83 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace ZeroLevel.UnitTests.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ZeroLevel.UnitTests.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] test_json_config { + get { + object obj = ResourceManager.GetObject("test_json_config", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] test_yaml_config { + get { + object obj = ResourceManager.GetObject("test_yaml_config", resourceCulture); + return ((byte[])(obj)); + } + } + } +} diff --git a/Tests/ZeroLevel.UnitTests/Properties/Resources.resx b/Tests/ZeroLevel.UnitTests/Properties/Resources.resx new file mode 100644 index 0000000..3ac1556 --- /dev/null +++ b/Tests/ZeroLevel.UnitTests/Properties/Resources.resx @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + ..\Resources\test_json_config.json;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\Resources\test_yaml_config.yaml;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Tests/ZeroLevel.UnitTests/Resources/test_config.json b/Tests/ZeroLevel.UnitTests/Resources/test_config.json new file mode 100644 index 0000000..05aae72 --- /dev/null +++ b/Tests/ZeroLevel.UnitTests/Resources/test_config.json @@ -0,0 +1,18 @@ +{ + "url": "http://tes.io", + "port": 9091, + "limits": { + "min": { + "cpu": 20, + "gpu": 0, + "mem": 200 + }, + "max": { + "cpu": 40, + "gpu": 50, + "mem": 600 + }, + "optional": null, + "persistent": true + } +} \ No newline at end of file diff --git a/Tests/ZeroLevel.UnitTests/Resources/test_json_config.json b/Tests/ZeroLevel.UnitTests/Resources/test_json_config.json new file mode 100644 index 0000000..05aae72 --- /dev/null +++ b/Tests/ZeroLevel.UnitTests/Resources/test_json_config.json @@ -0,0 +1,18 @@ +{ + "url": "http://tes.io", + "port": 9091, + "limits": { + "min": { + "cpu": 20, + "gpu": 0, + "mem": 200 + }, + "max": { + "cpu": 40, + "gpu": 50, + "mem": 600 + }, + "optional": null, + "persistent": true + } +} \ No newline at end of file diff --git a/Tests/ZeroLevel.UnitTests/Resources/test_yaml_config.yaml b/Tests/ZeroLevel.UnitTests/Resources/test_yaml_config.yaml new file mode 100644 index 0000000..f35c3c9 --- /dev/null +++ b/Tests/ZeroLevel.UnitTests/Resources/test_yaml_config.yaml @@ -0,0 +1,15 @@ +--- +# file: practice/1.kube-basics-lecture/1.9.ingress/ingress.yaml +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: my-ingress +spec: + rules: + - host: my-app.s<номер своего логина>.edu.slurm.io + http: + paths: + - backend: + serviceName: my-service + servicePort: 80 +... diff --git a/Tests/ZeroLevel.UnitTests/ZeroLevel.UnitTests.csproj b/Tests/ZeroLevel.UnitTests/ZeroLevel.UnitTests.csproj index 68f40c2..a79bb12 100644 --- a/Tests/ZeroLevel.UnitTests/ZeroLevel.UnitTests.csproj +++ b/Tests/ZeroLevel.UnitTests/ZeroLevel.UnitTests.csproj @@ -21,4 +21,19 @@ + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + diff --git a/Tests/test_json_config.json b/Tests/test_json_config.json new file mode 100644 index 0000000..05aae72 --- /dev/null +++ b/Tests/test_json_config.json @@ -0,0 +1,18 @@ +{ + "url": "http://tes.io", + "port": 9091, + "limits": { + "min": { + "cpu": 20, + "gpu": 0, + "mem": 200 + }, + "max": { + "cpu": 40, + "gpu": 50, + "mem": 600 + }, + "optional": null, + "persistent": true + } +} \ No newline at end of file diff --git a/Tests/test_yaml_config.yaml b/Tests/test_yaml_config.yaml new file mode 100644 index 0000000..ac4ef85 --- /dev/null +++ b/Tests/test_yaml_config.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: my-ingress +spec: + rules: + - host: my-app.s<номер своего логина>.edu.slurm.io + http: + paths: + - backend: + serviceName: my-service + servicePort: 80 +... \ No newline at end of file diff --git a/ZeroLevel.Sleopok.Engine/Models/IndexInfo.cs b/ZeroLevel.Sleopok.Engine/Models/IndexInfo.cs index 32cc95c..c69abe5 100644 --- a/ZeroLevel.Sleopok.Engine/Models/IndexInfo.cs +++ b/ZeroLevel.Sleopok.Engine/Models/IndexInfo.cs @@ -44,22 +44,15 @@ namespace ZeroLevel.Sleopok.Engine.Models switch (member.MemberType) { case MemberTypes.Field: - getter = TypeGetterSetterBuilder.BuildGetter(member as FieldInfo); + getter = TypeGetterSetterBuilder.BuildGetter((member as FieldInfo)!); break; case MemberTypes.Property: - getter = TypeGetterSetterBuilder.BuildGetter(member as PropertyInfo); + getter = TypeGetterSetterBuilder.BuildGetter((member as PropertyInfo)!); break; default: return; } var name = FSUtils.FileNameCorrection(string.IsNullOrWhiteSpace(sleoAttribute.Name) ? member.Name : sleoAttribute.Name); - _fields.Add(new SleoField - { - FieldType = type, - Boost = sleoAttribute.Boost, - Name = name, - Getter = getter, - ExactMatch = sleoAttribute.AvaliableForExactMatch - }); + _fields.Add(new SleoField(type, name, sleoAttribute.Boost, sleoAttribute.AvaliableForExactMatch, getter)); } }); } diff --git a/ZeroLevel.Sleopok.Engine/Models/SleoField.cs b/ZeroLevel.Sleopok.Engine/Models/SleoField.cs index dc86aa0..b23c1e6 100644 --- a/ZeroLevel.Sleopok.Engine/Models/SleoField.cs +++ b/ZeroLevel.Sleopok.Engine/Models/SleoField.cs @@ -15,6 +15,9 @@ namespace ZeroLevel.Sleopok.Engine.Models } internal sealed class SleoField { + internal SleoField(SleoFieldType fieldType, string name, float boost, bool exactMatch, Func getter) => + (FieldType, Name, Boost, ExactMatch, Getter) = (fieldType, name, boost, exactMatch, getter); + public SleoFieldType FieldType; public string Name; public float Boost; diff --git a/ZeroLevel.Sleopok.Engine/Models/SleoIndexAttribute.cs b/ZeroLevel.Sleopok.Engine/Models/SleoIndexAttribute.cs index c67aa6b..ed018e0 100644 --- a/ZeroLevel.Sleopok.Engine/Models/SleoIndexAttribute.cs +++ b/ZeroLevel.Sleopok.Engine/Models/SleoIndexAttribute.cs @@ -5,9 +5,9 @@ namespace ZeroLevel.Sleopok.Engine.Models public sealed class SleoIndexAttribute : Attribute { - public string Name { get; private set; } - public float Boost { get; private set; } = 1.0f; - public bool AvaliableForExactMatch { get; private set; } = false; + public readonly string Name; + public readonly float Boost; + public readonly bool AvaliableForExactMatch; public SleoIndexAttribute(string name, float boost = 1.0f, bool avaliableForExactMatch = false) { diff --git a/ZeroLevel.Sleopok.Engine/Services/Indexes/IndexReader.cs b/ZeroLevel.Sleopok.Engine/Services/Indexes/IndexReader.cs index b7614de..db433f5 100644 --- a/ZeroLevel.Sleopok.Engine/Services/Indexes/IndexReader.cs +++ b/ZeroLevel.Sleopok.Engine/Services/Indexes/IndexReader.cs @@ -8,8 +8,9 @@ namespace ZeroLevel.Sleopok.Engine.Services.Indexes { public class FieldRecords { + public FieldRecords(string field, IDictionary> records) => (Field, Records) = (field, records); public string Field { get; set; } - public Dictionary> Records { get; set; } + public IDictionary> Records { get; set; } } internal sealed class IndexReader @@ -54,11 +55,7 @@ namespace ZeroLevel.Sleopok.Engine.Services.Indexes foreach (var field in _indexInfo.Fields) { var docs = await _storage.GetAllDocuments(field.Name); - yield return new FieldRecords - { - Field = field.Name, - Records = docs - }; + yield return new FieldRecords(field.Name, docs); } } } diff --git a/ZeroLevel.Sleopok.Engine/Services/Storage/DataStorage.cs b/ZeroLevel.Sleopok.Engine/Services/Storage/DataStorage.cs index d288758..84a1d20 100644 --- a/ZeroLevel.Sleopok.Engine/Services/Storage/DataStorage.cs +++ b/ZeroLevel.Sleopok.Engine/Services/Storage/DataStorage.cs @@ -112,7 +112,7 @@ namespace ZeroLevel.Sleopok.Engine.Services.Storage public IPartitionDataWriter GetWriter(string field) { - return new DateSourceWriter(_store.CreateBuilder(new StoreMetadata { Field = field })); + return new DateSourceWriter(_store.CreateBuilder(new StoreMetadata(field))); } /// @@ -126,7 +126,7 @@ namespace ZeroLevel.Sleopok.Engine.Services.Storage public async Task> GetDocuments(string field, string[] tokens, float boost, bool exactMatch) { var documents = new Dictionary(); - var accessor = _store.CreateAccessor(new StoreMetadata { Field = field }); + var accessor = _store.CreateAccessor(new StoreMetadata(field)); if (accessor != null) { using (accessor) @@ -156,7 +156,7 @@ namespace ZeroLevel.Sleopok.Engine.Services.Storage public async Task>> GetAllDocuments(string field) { var documents = new Dictionary>(); - var accessor = _store.CreateAccessor(new StoreMetadata { Field = field }); + var accessor = _store.CreateAccessor(new StoreMetadata(field)); if (accessor != null) { using (accessor) @@ -183,7 +183,7 @@ namespace ZeroLevel.Sleopok.Engine.Services.Storage { using (TextWriter writer = new StreamWriter(stream)) { - await foreach (var i in _store.Bypass(new StoreMetadata { Field = key })) + await foreach (var i in _store.Bypass(new StoreMetadata(key))) { writer.WriteLine(i.Key); writer.WriteLine(string.Join(' ', Compressor.DecompressToDocuments(i.Value))); @@ -193,7 +193,7 @@ namespace ZeroLevel.Sleopok.Engine.Services.Storage public int HasData(string field) { - var partition = _store.CreateAccessor(new StoreMetadata { Field = field }); + var partition = _store.CreateAccessor(new StoreMetadata(field)); if (partition != null) { using (partition) diff --git a/ZeroLevel.Sleopok.Engine/Services/Storage/StoreMetadata.cs b/ZeroLevel.Sleopok.Engine/Services/Storage/StoreMetadata.cs index db37074..784955c 100644 --- a/ZeroLevel.Sleopok.Engine/Services/Storage/StoreMetadata.cs +++ b/ZeroLevel.Sleopok.Engine/Services/Storage/StoreMetadata.cs @@ -5,6 +5,8 @@ /// public sealed class StoreMetadata { + public StoreMetadata(string field) => Field = field; + /// /// Поле документа /// diff --git a/ZeroLevel.Sleopok.Engine/Services/Storage/StoreRecord.cs b/ZeroLevel.Sleopok.Engine/Services/Storage/StoreRecord.cs deleted file mode 100644 index 15bf68b..0000000 --- a/ZeroLevel.Sleopok.Engine/Services/Storage/StoreRecord.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace ZeroLevel.Sleopok.Engine.Services.Storage -{ - public sealed class StoreRecord - { - /// - /// Токен / ключ - /// - public string Token { get; set; } - /// - /// Идентификаторы документов / значение - /// - public string[] Documents { get; set; } - } -} diff --git a/ZeroLevel/Services/Collections/CyclicBuffer.cs b/ZeroLevel/Services/Collections/CyclicBuffer.cs new file mode 100644 index 0000000..a0a151e --- /dev/null +++ b/ZeroLevel/Services/Collections/CyclicBuffer.cs @@ -0,0 +1,53 @@ +using System; + +namespace ZeroLevel.Services.Collections +{ + /// + /// Циклический буфер + /// + public class CyclicBuffer + { + private T[] _buffer; + private int _head; + private int _tail; + + public CyclicBuffer(int size) + { + if (size <= 0) + throw new ArgumentException($"{nameof(size)} must be positive."); + _buffer = new T[size]; + _head = 0; + _tail = 0; + } + + public bool IsFull { get { return (_tail + 1) % _buffer.Length == _head; } } + public bool IsEmpty { get { return _head == _tail; } } + + public void Enqueue(T item) + { + _buffer[_tail] = item; + _tail = (_tail + 1) % _buffer.Length; + + // Если буфер полон, сдвигаем head + if (IsFull) + _head = (_head + 1) % _buffer.Length; + } + + public T this[int index] + { + get + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException(nameof(index)); + + int effectiveIndex = (_head + index) % _buffer.Length; + return _buffer[effectiveIndex]; + } + } + + public int Count + { + get { return (_tail >= _head) ? _tail - _head : _buffer.Length + _tail - _head; } + } + } +} diff --git a/ZeroLevel/Services/Config/Configuration.cs b/ZeroLevel/Services/Config/Configuration.cs index 7ff8c4f..8b3d8a5 100644 --- a/ZeroLevel/Services/Config/Configuration.cs +++ b/ZeroLevel/Services/Config/Configuration.cs @@ -139,7 +139,7 @@ namespace ZeroLevel } catch (Exception ex) { - Log.Error(ex, $"[Configuration.ReadFromEnvironmentVariables] Can't read environment variables"); + Log.Error(ex, $"[{nameof(Configuration)}.ReadFromEnvironmentVariables] Can't read environment variables"); throw; } } @@ -156,7 +156,7 @@ namespace ZeroLevel } catch (Exception ex) { - Log.Error(ex, $"[Configuration.ReadFromApplicationConfig] Can't read app.config file"); + Log.Error(ex, $"[{nameof(Configuration)}.ReadFromApplicationConfig] Can't read app.config file"); throw; } } @@ -168,7 +168,7 @@ namespace ZeroLevel } catch (Exception ex) { - Log.Error(ex, $"[Configuration.ReadOrEmptyFromApplicationConfig] Can't read app.config file"); + Log.Error(ex, $"[{nameof(Configuration)}.ReadOrEmptyFromApplicationConfig] Can't read app.config file"); } return _empty; } @@ -185,7 +185,7 @@ namespace ZeroLevel } catch (Exception ex) { - Log.Error(ex, $"[Configuration.ReadSetFromApplicationConfig] Can't read app.config file"); + Log.Error(ex, $"[{nameof(Configuration)}.ReadSetFromApplicationConfig] Can't read app.config file"); throw; } } @@ -197,7 +197,7 @@ namespace ZeroLevel } catch (Exception ex) { - Log.Error(ex, $"[Configuration.ReadOrEmptySetFromApplicationConfig] Can't read app.config file"); + Log.Error(ex, $"[{nameof(Configuration)}.ReadOrEmptySetFromApplicationConfig] Can't read app.config file"); } return _emptySet; } @@ -214,7 +214,7 @@ namespace ZeroLevel } catch (Exception ex) { - Log.Error(ex, $"[Configuration.ReadFromApplicationConfig] Can't read config file '{configFilePath}'"); + Log.Error(ex, $"[{nameof(Configuration)}.ReadFromApplicationConfig] Can't read config file '{configFilePath}'"); throw; } } @@ -226,7 +226,7 @@ namespace ZeroLevel } catch (Exception ex) { - Log.Error(ex, $"[Configuration.ReadOrEmptyFromApplicationConfig] Can't read config file '{configFilePath}'"); + Log.Error(ex, $"[{nameof(Configuration)}.ReadOrEmptyFromApplicationConfig] Can't read config file '{configFilePath}'"); } return _empty; } @@ -243,7 +243,7 @@ namespace ZeroLevel } catch (Exception ex) { - Log.Error(ex, $"[Configuration.ReadSetFromApplicationConfig] Can't read config file '{configFilePath}'"); + Log.Error(ex, $"[{nameof(Configuration)}.ReadSetFromApplicationConfig] Can't read config file '{configFilePath}'"); throw; } } @@ -255,7 +255,7 @@ namespace ZeroLevel } catch (Exception ex) { - Log.Error(ex, $"[Configuration.ReadOrEmptySetFromApplicationConfig] Can't read config file '{configFilePath}'"); + Log.Error(ex, $"[{nameof(Configuration)}.ReadOrEmptySetFromApplicationConfig] Can't read config file '{configFilePath}'"); } return _emptySet; } @@ -273,7 +273,7 @@ namespace ZeroLevel } catch (Exception ex) { - Log.Error(ex, $"[Configuration.ReadFromIniFile] Can't read config file '{path}'"); + Log.Error(ex, $"[{nameof(Configuration)}.ReadFromIniFile] Can't read config file '{path}'"); throw; } } @@ -285,7 +285,7 @@ namespace ZeroLevel } catch (Exception ex) { - Log.Error(ex, $"[Configuration.ReadOrEmptyFromIniFile] Can't read config file '{path}'"); + Log.Error(ex, $"[{nameof(Configuration)}.ReadOrEmptyFromIniFile] Can't read config file '{path}'"); } return _empty; } @@ -303,7 +303,7 @@ namespace ZeroLevel } catch (Exception ex) { - Log.Error(ex, $"[Configuration.ReadSetFromIniFile] Can't read config file '{path}'"); + Log.Error(ex, $"[{nameof(Configuration)}.ReadSetFromIniFile] Can't read config file '{path}'"); throw; } } @@ -315,7 +315,7 @@ namespace ZeroLevel } catch (Exception ex) { - Log.Error(ex, $"[Configuration.ReadOrEmptySetFromIniFile] Can't read config file '{path}'"); + Log.Error(ex, $"[{nameof(Configuration)}.ReadOrEmptySetFromIniFile] Can't read config file '{path}'"); } return _emptySet; } @@ -333,7 +333,7 @@ namespace ZeroLevel } catch (Exception ex) { - Log.Error(ex, $"[Configuration.ReadFromCommandLine] Can't read command line args"); + Log.Error(ex, $"[{nameof(Configuration)}.ReadFromCommandLine] Can't read command line args"); throw; } } @@ -345,7 +345,7 @@ namespace ZeroLevel } catch (Exception ex) { - Log.Error(ex, $"[Configuration.ReadOrEmptyFromCommandLine] Can't read command line args"); + Log.Error(ex, $"[{nameof(Configuration)}.ReadOrEmptyFromCommandLine] Can't read command line args"); } return _empty; } @@ -358,7 +358,7 @@ namespace ZeroLevel } catch (Exception ex) { - Log.Error(ex, $"[Configuration.ReadFromBinaryReader] Can't read config from binaryReader"); + Log.Error(ex, $"[{nameof(Configuration)}.ReadFromBinaryReader] Can't read config from binaryReader"); throw; } } @@ -370,7 +370,7 @@ namespace ZeroLevel } catch (Exception ex) { - Log.Error(ex, $"[Configuration.ReadOrEmptyFromBinaryReader] Can't read config from binaryReader"); + Log.Error(ex, $"[{nameof(Configuration)}.ReadOrEmptyFromBinaryReader] Can't read config from binaryReader"); } return _empty; } @@ -383,7 +383,7 @@ namespace ZeroLevel } catch (Exception ex) { - Log.Error(ex, $"[Configuration.ReadSetFromBinaryReader] Can't read config from binaryReader"); + Log.Error(ex, $"[{nameof(Configuration)}.ReadSetFromBinaryReader] Can't read config from binaryReader"); throw; } } @@ -395,10 +395,131 @@ namespace ZeroLevel } catch (Exception ex) { - Log.Error(ex, $"[Configuration.ReadSetOrEmptyFromBinaryReader] Can't read config from binaryReader"); + Log.Error(ex, $"[{nameof(Configuration)}.ReadSetOrEmptyFromBinaryReader] Can't read config from binaryReader"); } return _emptySet; } + + /// + /// Create configuration from Json file + /// + /// Path to the Json file + /// Configuration + public static IConfiguration ReadFromJsonFile(string path) + { + try + { + return new JsonFileReader(path).ReadConfiguration(); + } + catch (Exception ex) + { + Log.Error(ex, $"[{nameof(Configuration)}.ReadFromJsonFile] Can't read config file '{path}'"); + throw; + } + } + public static IConfiguration ReadOrEmptyFromJsonFile(string path) + { + try + { + return new JsonFileReader(path).ReadConfiguration(); + } + catch (Exception ex) + { + Log.Error(ex, $"[{nameof(Configuration)}.ReadOrEmptyFromJsonFile] Can't read config file '{path}'"); + } + return _empty; + } + + /// + /// Creating a configuration from an Json file, including sections + /// + /// Path to the Json file + /// Configuration + public static IConfigurationSet ReadSetFromJsonFile(string path) + { + try + { + return new JsonFileReader(path).ReadConfigurationSet(); + } + catch (Exception ex) + { + Log.Error(ex, $"[{nameof(Configuration)}.ReadSetFromJsonFile] Can't read config file '{path}'"); + throw; + } + } + public static IConfigurationSet ReadSetOrEmptyFromJsonFile(string path) + { + try + { + return new JsonFileReader(path).ReadConfigurationSet(); + } + catch (Exception ex) + { + Log.Error(ex, $"[{nameof(Configuration)}.ReadSetOrEmptyFromJsonFile] Can't read config file '{path}'"); + } + return _emptySet; + } + + /// + /// Create configuration from Yaml file + /// + /// Path to the Yaml file + /// Configuration + public static IConfiguration ReadFromYamlFile(string path) + { + try + { + return new YamlFileReader(path).ReadConfiguration(); + } + catch (Exception ex) + { + Log.Error(ex, $"[{nameof(Configuration)}.ReadFromYamlFile] Can't read config file '{path}'"); + throw; + } + } + public static IConfiguration ReadOrEmptyFromYamlFile(string path) + { + try + { + return new YamlFileReader(path).ReadConfiguration(); + } + catch (Exception ex) + { + Log.Error(ex, $"[{nameof(Configuration)}.ReadOrEmptyFromYamlFile] Can't read config file '{path}'"); + } + return _empty; + } + + /// + /// Creating a configuration from an Yaml file, including sections + /// + /// Path to the Yaml file + /// Configuration + public static IConfigurationSet ReadSetFromYamlFile(string path) + { + try + { + return new YamlFileReader(path).ReadConfigurationSet(); + } + catch (Exception ex) + { + Log.Error(ex, $"[{nameof(Configuration)}.ReadSetFromYamlFile] Can't read config file '{path}'"); + throw; + } + } + public static IConfigurationSet ReadSetOrEmptyFromYamlFile(string path) + { + try + { + return new YamlFileReader(path).ReadConfigurationSet(); + } + catch (Exception ex) + { + Log.Error(ex, $"[{nameof(Configuration)}.ReadSetOrEmptyFromYamlFile] Can't read config file '{path}'"); + } + return _emptySet; + } + #endregion Read configuration public static IConfiguration Merge(ConfigurationRecordExistBehavior existRecordBehavior, params IConfiguration[] configurations) diff --git a/ZeroLevel/Services/Config/Implementation/IniFileReader.cs b/ZeroLevel/Services/Config/Implementation/IniFileReader.cs index cdbcc1f..fface02 100644 --- a/ZeroLevel/Services/Config/Implementation/IniFileReader.cs +++ b/ZeroLevel/Services/Config/Implementation/IniFileReader.cs @@ -15,13 +15,17 @@ namespace ZeroLevel.Services.Config.Implementation internal IniFileReader(string configPath) { if (String.IsNullOrWhiteSpace(configPath)) - throw new ArgumentNullException("configPath", "File path not found"); + { + Log.Fatal($"[{nameof(IniFileReader)}] File path is null or empty"); + throw new ArgumentNullException("configPath", "File path is null or empty"); + } if (!File.Exists(configPath)) { configPath = Path.Combine(Configuration.BaseDirectory, configPath); if (!File.Exists(configPath)) { - throw new FileNotFoundException("File path not exists: " + configPath); + Log.Fatal($"[{nameof(IniFileReader)}] File path '{configPath}' not exists"); + throw new FileNotFoundException($"File path '{configPath}' not exists"); } } _iniPath = configPath; diff --git a/ZeroLevel/Services/Config/Implementation/JsonFileReader.cs b/ZeroLevel/Services/Config/Implementation/JsonFileReader.cs new file mode 100644 index 0000000..1646e34 --- /dev/null +++ b/ZeroLevel/Services/Config/Implementation/JsonFileReader.cs @@ -0,0 +1,89 @@ +using System; +using System.IO; + +namespace ZeroLevel.Services.Config.Implementation +{ + /// + /// Read from JSON file, aka 'ConfigurationBuilder().AddJsonFile(configPath)' from Microsoft.Extensions.Configuration + /// + internal sealed class JsonFileReader + : IConfigurationReader + { + private readonly string _jsonPath; + + internal JsonFileReader(string configPath) + { + if (String.IsNullOrWhiteSpace(configPath)) + { + Log.Fatal($"[{nameof(JsonFileReader)}] File path is null or empty"); + throw new ArgumentNullException("configPath", "File path is null or empty"); + } + if (!File.Exists(configPath)) + { + configPath = Path.Combine(Configuration.BaseDirectory, configPath); + if (!File.Exists(configPath)) + { + Log.Fatal($"[{nameof(JsonFileReader)}] File path '{configPath}' not exists"); + throw new FileNotFoundException($"File path '{configPath}' not exists"); + } + } + _jsonPath = configPath; + } + + public IConfiguration ReadConfiguration() + { + var set = ReadConfigurationSet(); + return set.Default; + } + + public IConfigurationSet ReadConfigurationSet() + { + try + { + using (Stream stream = new FileStream(_jsonPath, + FileMode.Open, + FileAccess.Read, + FileShare.ReadWrite, + bufferSize: 1, + FileOptions.SequentialScan)) + { + IConfigurationSet set = Configuration.CreateSet(); + var dict = JsonConfigurationFileParser.Parse(stream); + foreach (var kv in dict) + { + if (string.CompareOrdinal(Configuration.DEFAULT_SECTION_NAME, kv.Key) == 0) + { + foreach (var set_kv in kv.Value) + { + set.Default.Append(set_kv.Key, set_kv.Value); + } + } + else + { + var sectionName = kv.Key; + IConfiguration section; + if (false == set.ContainsSection(sectionName)) + { + section = set.CreateSection(sectionName); + } + else + { + section = set.GetSection(sectionName); + } + foreach (var set_kv in kv.Value) + { + section.Append(set_kv.Key, set_kv.Value); + } + } + } + return set; + } + } + catch (Exception ex) + { + Log.Error(ex, $"[JsonFileReader] Failed to load configuration from file '{_jsonPath}'."); + throw new InvalidDataException($"Failed to load configuration from file '{_jsonPath}'."); + } + } + } +} diff --git a/ZeroLevel/Services/Config/Implementation/YamlFileReader.cs b/ZeroLevel/Services/Config/Implementation/YamlFileReader.cs new file mode 100644 index 0000000..3c61e9e --- /dev/null +++ b/ZeroLevel/Services/Config/Implementation/YamlFileReader.cs @@ -0,0 +1,113 @@ +using System; +using YamlDotNet.Serialization.NamingConventions; +using YamlDotNet.Serialization; +using System.IO; +using ZeroLevel.Services.Formats.YAML; + +namespace ZeroLevel.Services.Config.Implementation +{ + internal sealed class YamlFileReader + : IConfigurationReader + { + private readonly string _yamlPath; + + internal YamlFileReader(string configPath) + { + if (String.IsNullOrWhiteSpace(configPath)) + { + Log.Fatal($"[{nameof(JsonFileReader)}] File path is null or empty"); + throw new ArgumentNullException("configPath", "File path is null or empty"); + } + if (!File.Exists(configPath)) + { + configPath = Path.Combine(Configuration.BaseDirectory, configPath); + if (!File.Exists(configPath)) + { + Log.Fatal($"[{nameof(JsonFileReader)}] File path '{configPath}' not exists"); + throw new FileNotFoundException($"File path '{configPath}' not exists"); + } + } + _yamlPath = configPath; + } + + public IConfiguration ReadConfiguration() + { + var set = ReadConfigurationSet(); + return set.Default; + } + + private static Stream GenerateStreamFromString(string s) + { + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + writer.Write(s); + writer.Flush(); + stream.Position = 0; + return stream; + } + + public IConfigurationSet ReadConfigurationSet() + { + try + { + var yaml = File.ReadAllText(_yamlPath); + var json = FullYamlToJsonConverter.Convert(yaml); + + using (Stream stream = GenerateStreamFromString(json)) + { + IConfigurationSet set = Configuration.CreateSet(); + var dict = JsonConfigurationFileParser.Parse(stream); + foreach (var kv in dict) + { + if (string.CompareOrdinal(Configuration.DEFAULT_SECTION_NAME, kv.Key) == 0) + { + foreach (var set_kv in kv.Value) + { + set.Default.Append(set_kv.Key, set_kv.Value); + } + } + else + { + var sectionName = kv.Key; + IConfiguration section; + if (false == set.ContainsSection(sectionName)) + { + section = set.CreateSection(sectionName); + } + else + { + section = set.GetSection(sectionName); + } + foreach (var set_kv in kv.Value) + { + section.Append(set_kv.Key, set_kv.Value); + } + } + } + return set; + } + } + catch (Exception ex) + { + Log.Error(ex, $"[YamlFileReader] Failed to load configuration from file '{_yamlPath}'."); + throw new InvalidDataException($"Failed to load configuration from file '{_yamlPath}'."); + } + } + + + + private static class FullYamlToJsonConverter + { + public static string Convert(string yaml) + { + var deserializer = new DeserializerBuilder().Build(); + var yamlObject = deserializer.Deserialize(yaml); + var serializer = new SerializerBuilder() + .JsonCompatible() + .Build(); + var json = serializer.Serialize(yamlObject); + return json; + } + } + } +} diff --git a/ZeroLevel/Services/Config/JsonConfigurationFileParser.cs b/ZeroLevel/Services/Config/JsonConfigurationFileParser.cs new file mode 100644 index 0000000..a9ae62f --- /dev/null +++ b/ZeroLevel/Services/Config/JsonConfigurationFileParser.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text.Json; + +namespace ZeroLevel.Services.Config +{ + /// + /// Edited version from Microsoft.Extensions.Configuration + /// + internal sealed class JsonConfigurationFileParser + { + private JsonConfigurationFileParser() { } + + private readonly Dictionary>> _data = new Dictionary>>(StringComparer.OrdinalIgnoreCase); + private readonly Stack _paths = new Stack(); + + public static IDictionary>> Parse(Stream input) + => new JsonConfigurationFileParser().ParseStream(input); + + private Dictionary>> ParseStream(Stream input) + { + var jsonDocumentOptions = new JsonDocumentOptions + { + CommentHandling = JsonCommentHandling.Skip, + AllowTrailingCommas = true, + }; + + using (var reader = new StreamReader(input)) + using (JsonDocument doc = JsonDocument.Parse(reader.ReadToEnd(), jsonDocumentOptions)) + { + if (doc.RootElement.ValueKind != JsonValueKind.Object) + { + throw new FormatException($"Top-level JSON element must be an object. Instead, '{doc.RootElement.ValueKind}' was found."); + } + VisitObjectElement(doc.RootElement); + } + + return _data; + } + + private void VisitObjectElement(JsonElement element) + { + foreach (JsonProperty property in element.EnumerateObject()) + { + EnterContext(property.Name); + VisitValue(property.Value); + ExitContext(); + } + } + + private void VisitArrayElement(JsonElement element) + { + int index = 0; + foreach (JsonElement arrayElement in element.EnumerateArray()) + { + EnterContext(index.ToString()); + VisitValue(arrayElement); + ExitContext(); + index++; + } + } + + private void VisitValue(JsonElement value) + { + Debug.Assert(_paths.Count > 0); + + switch (value.ValueKind) + { + case JsonValueKind.Object: + VisitObjectElement(value); + break; + + case JsonValueKind.Array: + VisitArrayElement(value); + break; + + case JsonValueKind.Number: + case JsonValueKind.String: + case JsonValueKind.True: + case JsonValueKind.False: + case JsonValueKind.Null: + if (_data.ContainsKey(CurrentSection) == false) + { + _data[CurrentSection] = new Dictionary>(StringComparer.OrdinalIgnoreCase); + } + var data = _data[CurrentSection]; + + string key = _paths.Peek(); + if (data.ContainsKey(key)) + { + data[key].Add(value.ToString()); + } + else + { + data[key] = new List { value.ToString() }; + } + break; + + default: + throw new FormatException($"Unsupported JSON token '{value.ValueKind}' was found."); + } + } + + public static readonly string KeyDelimiter = "."; + + private string CurrentSection = Configuration.DEFAULT_SECTION_NAME; + private void EnterContext(string context) + { + if (_paths.Count > 0) + { + CurrentSection = string.Join(KeyDelimiter, _paths.Reverse()); + } + else + { + CurrentSection = Configuration.DEFAULT_SECTION_NAME; + } + _paths.Push(context); + } + + private void ExitContext() => _paths.Pop(); + } +} diff --git a/ZeroLevel/Services/Formats/YAML/YamlToJsonConverter.cs b/ZeroLevel/Services/Formats/YAML/YamlToJsonConverter.cs new file mode 100644 index 0000000..f783405 --- /dev/null +++ b/ZeroLevel/Services/Formats/YAML/YamlToJsonConverter.cs @@ -0,0 +1,284 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.RegularExpressions; + +namespace ZeroLevel.Services.Formats.YAML +{ + public static class YamlToJsonConverter + { + public static string Convert(string yaml) + { + if (string.IsNullOrEmpty(yaml)) + { + return "{}"; + } + + yaml = RemoveComments(yaml); + Dictionary yamlData = ParseYaml(yaml, 0); + return ConvertToJson(yamlData); + } + + private static string RemoveComments(string yaml) + { + return Regex.Replace(yaml, @"#.*", string.Empty); + } + + private static Dictionary ParseYaml(string yaml, int indentLevel) + { + Dictionary data = new Dictionary(); + string[] lines = yaml.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); + string currentKey = null; + + for (int i = 0; i < lines.Length; i++) + { + string line = lines[i].Trim(); + + if (string.IsNullOrEmpty(line)) + { + continue; + } + + int currentLineIndent = CountLeadingSpaces(lines[i]); + + if (currentLineIndent < indentLevel) + { + break; + } + if (currentLineIndent > indentLevel) + { + continue; + } + + string trimmedLine = line.TrimStart(); + + if (trimmedLine.StartsWith("-")) + { + if (!data.ContainsKey(currentKey)) + { + data[currentKey] = new List(); + } + List list = (List)data[currentKey]; + string listValue = trimmedLine.Substring(1).Trim(); + if (listValue.StartsWith("{")) + { + listValue = listValue.Substring(1, listValue.Length - 2).Trim(); + string[] entries = listValue.Split(','); + Dictionary inlineObj = new Dictionary(); + foreach (string entry in entries) + { + string[] kvp = entry.Trim().Split(':'); + inlineObj.Add(kvp[0].Trim(), kvp[1].Trim()); + } + list.Add(inlineObj); + + } + else if (listValue.StartsWith("[")) + { + listValue = listValue.Substring(1, listValue.Length - 2).Trim(); + string[] entries = listValue.Split(','); + List inlineList = new List(); + foreach (string entry in entries) + { + inlineList.Add(entry.Trim()); + } + list.Add(inlineList); + } + + else + { + list.Add(GetValue(listValue)); + } + + + } + else + { + string[] parts = trimmedLine.Split(new[] { ':' }, 2); + + if (parts.Length == 2) + { + currentKey = parts[0].Trim(); + string value = parts[1].Trim(); + + if (string.IsNullOrEmpty(value)) + { + int nextIndentLevel = int.MaxValue; + for (int j = i + 1; j < lines.Length; j++) + { + int tempIndent = CountLeadingSpaces(lines[j]); + if (tempIndent > currentLineIndent) + { + nextIndentLevel = tempIndent; + break; + } + } + + StringBuilder subYaml = new StringBuilder(); + for (int j = i + 1; j < lines.Length; j++) + { + if (CountLeadingSpaces(lines[j]) >= nextIndentLevel) + { + subYaml.AppendLine(lines[j]); + + } + else + { + break; + } + } + + data[currentKey] = ParseYaml(subYaml.ToString(), nextIndentLevel); + i += subYaml.ToString().Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None).Length; + + } + + else if (value.StartsWith("{")) + { + value = value.Substring(1, value.Length - 2).Trim(); + string[] entries = value.Split(','); + Dictionary inlineObj = new Dictionary(); + foreach (string entry in entries) + { + string[] kvp = entry.Trim().Split(':'); + inlineObj.Add(kvp[0].Trim(), kvp[1].Trim()); + } + data.Add(currentKey, inlineObj); + } + else if (value.StartsWith("[")) + { + value = value.Substring(1, value.Length - 2).Trim(); + string[] entries = value.Split(','); + List inlineList = new List(); + foreach (string entry in entries) + { + inlineList.Add(entry.Trim()); + } + data.Add(currentKey, inlineList); + + } + + else + { + data[currentKey] = GetValue(value); + } + + } + } + } + return data; + } + + private static object GetValue(string value) + { + if (value.ToLower() == "true") + { + return true; + } + if (value.ToLower() == "false") + { + return false; + } + if (value.ToLower() == "null") + { + return null; + } + if (int.TryParse(value, out int intValue)) + { + return intValue; + } + if (double.TryParse(value, out double doubleValue)) + { + return doubleValue; + } + return value; + } + + private static string ConvertToJson(Dictionary data) + { + StringBuilder json = new StringBuilder("{"); + bool first = true; + + foreach (KeyValuePair pair in data) + { + if (!first) + { + json.Append(","); + } + + json.Append($"\"{pair.Key}\":"); + + if (pair.Value is Dictionary subData) + { + json.Append(ConvertToJson(subData)); + } + else if (pair.Value is List listData) + { + json.Append(ConvertListToJson(listData)); + } + else if (pair.Value is string) + { + json.Append($"\"{pair.Value}\""); + } + else if (pair.Value is bool || pair.Value is int || pair.Value is double || pair.Value == null) + { + json.Append($"{pair.Value.ToString().ToLower()}"); + } + + first = false; + } + + json.Append("}"); + return json.ToString(); + } + private static string ConvertListToJson(List list) + { + StringBuilder json = new StringBuilder("["); + bool first = true; + foreach (object item in list) + { + if (!first) + { + json.Append(","); + } + if (item is Dictionary) + { + json.Append(ConvertToJson((Dictionary)item)); + } + else if (item is List) + { + json.Append(ConvertListToJson((List)item)); + } + else if (item is string) + { + json.Append($"\"{item}\""); + } + else if (item is bool || item is int || item is double || item == null) + { + json.Append($"{item.ToString().ToLower()}"); + } + + first = false; + } + json.Append("]"); + return json.ToString(); + } + + private static int CountLeadingSpaces(string line) + { + int count = 0; + foreach (char c in line) + { + if (c == ' ') + { + count++; + } + else + { + break; + } + } + return count; + } + } +} diff --git a/ZeroLevel/Services/Network/Proxies/Proxy.cs b/ZeroLevel/Services/Network/Proxies/Proxy.cs index f5641a9..de307d8 100644 --- a/ZeroLevel/Services/Network/Proxies/Proxy.cs +++ b/ZeroLevel/Services/Network/Proxies/Proxy.cs @@ -8,17 +8,17 @@ namespace ZeroLevel.Services.Network.Proxies public class Proxy : IDisposable { - private readonly ProxyBalancer _balancer = new ProxyBalancer(); + private readonly ProxyBalancer _balancer = new(); public void AppendServer(IPEndPoint ep) => _balancer.AddEndpoint(ep); - private Socket _incomingSocket; + private readonly Socket _incomingSocket; public Proxy(IPEndPoint listenEndpoint) { _incomingSocket = new Socket(listenEndpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); _incomingSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.NoDelay, true); - _incomingSocket.Bind(listenEndpoint); + _incomingSocket.Bind(listenEndpoint); } public void Run() @@ -32,10 +32,10 @@ namespace ZeroLevel.Services.Network.Proxies { var socket = await _incomingSocket.AcceptAsync(); // no await! - Task.Run(async () => + await Task.Run(async () => { - await CreateProxyConnection(socket); - }); + await CreateProxyConnection(socket); + }).ConfigureAwait(false); } } catch (Exception ex) @@ -53,10 +53,8 @@ namespace ZeroLevel.Services.Network.Proxies var server = new Socket(endpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); server.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.NoDelay, true); server.Connect(endpoint); - using (var bind = new ProxyBinding(connection, server)) - { - await bind.Bind(); - } + using var bind = new ProxyBinding(connection, server); + await bind.Bind(); } catch (Exception ex) { diff --git a/ZeroLevel/Services/PartitionStorage/Partition/StorePartitionAccessor.cs b/ZeroLevel/Services/PartitionStorage/Partition/StorePartitionAccessor.cs index 3f378e9..c3f5cb6 100644 --- a/ZeroLevel/Services/PartitionStorage/Partition/StorePartitionAccessor.cs +++ b/ZeroLevel/Services/PartitionStorage/Partition/StorePartitionAccessor.cs @@ -526,8 +526,8 @@ namespace ZeroLevel.Services.PartitionStorage source.Seek(range.Start, SeekOrigin.Begin); var size = range.End - range.Start; byte[] buffer = new byte[size]; - source.Read(buffer, 0, buffer.Length); - target.Write(buffer, 0, buffer.Length); + var count = source.Read(buffer, 0, buffer.Length); + target.Write(buffer, 0, count); } #endregion diff --git a/ZeroLevel/Services/Utils/Timestamp.cs b/ZeroLevel/Services/Utils/Timestamp.cs new file mode 100644 index 0000000..74ed319 --- /dev/null +++ b/ZeroLevel/Services/Utils/Timestamp.cs @@ -0,0 +1,23 @@ +using System; + +namespace ZeroLevel.Services.Utils +{ + public static class Timestamp + { + /// + /// Current unix timestamp in ms + /// + public static long UtcNow => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + /// + /// Unix timestamp in ms + /// + /// + /// + public static long FromDateTimeOffset(DateTimeOffset offset) => offset.ToUnixTimeMilliseconds(); + public static long UtcNowAddDays(int days) => DateTimeOffset.UtcNow.AddDays(days).ToUnixTimeMilliseconds(); + public static long UtcNowAddSeconds(int seconds) => DateTimeOffset.UtcNow.AddSeconds(seconds).ToUnixTimeMilliseconds(); + public static long Max => DateTimeOffset.MaxValue.ToUnixTimeMilliseconds(); + public static long Min => DateTimeOffset.MinValue.ToUnixTimeMilliseconds(); + public static DateTimeOffset ToDateTimeOffsest(long timeStamp) => DateTimeOffset.FromUnixTimeMilliseconds(timeStamp); + } +} diff --git a/ZeroLevel/ZeroLevel.csproj b/ZeroLevel/ZeroLevel.csproj index 2a8f663..64a48ae 100644 --- a/ZeroLevel/ZeroLevel.csproj +++ b/ZeroLevel/ZeroLevel.csproj @@ -8,7 +8,7 @@ True ZeroLevel $(AssemblyVersion) - 4.0.0.1 + 4.0.0.2 $(AssemblyVersion) latest Ogoun @@ -38,7 +38,9 @@ - + + +