From f1c7a29b38dd873c8089a28feba2966934a3a7a3 Mon Sep 17 00:00:00 2001 From: BlubbFish Date: Mon, 9 Dec 2019 17:25:54 +0100 Subject: [PATCH] Strip Swan library Part 1 --- .../Collections/CollectionCacheRepository.cs | 49 ++ .../Configuration/PropertyDisplayAttribute.cs | 62 ++ Swan.Tiny/Definitions.Types.cs | 125 ++++ Swan.Tiny/Definitions.cs | 34 ++ .../DependencyContainer.cs | 575 ++++++++++++++++++ ...ependencyContainerRegistrationException.cs | 34 ++ .../DependencyContainerResolutionException.cs | 25 + .../DependencyContainerResolveOptions.cs | 106 ++++ ...pendencyContainerWeakReferenceException.cs | 18 + .../DependencyInjection/ObjectFactoryBase.cs | 352 +++++++++++ .../DependencyInjection/RegisterOptions.cs | 119 ++++ .../DependencyInjection/TypeRegistration.cs | 61 ++ .../TypesConcurrentDictionary.cs | 265 ++++++++ Swan.Tiny/Diagnostics/HighResolutionTimer.cs | 30 + Swan.Tiny/Enums.cs | 61 ++ Swan.Tiny/Extensions.ByteArrays.cs | 498 +++++++++++++++ Swan.Tiny/Extensions.Dictionaries.cs | 86 +++ Swan.Tiny/Extensions.Functional.cs | 173 ++++++ Swan.Tiny/Extensions.Reflection.cs | 411 +++++++++++++ Swan.Tiny/Extensions.Strings.cs | 364 +++++++++++ Swan.Tiny/Extensions.ValueTypes.cs | 135 ++++ Swan.Tiny/Extensions.cs | 230 +++++++ Swan.Tiny/Formatters/HumanizeJson.cs | 126 ++++ Swan.Tiny/Formatters/Json.Converter.cs | 259 ++++++++ Swan.Tiny/Formatters/Json.Deserializer.cs | 332 ++++++++++ Swan.Tiny/Formatters/Json.Serializer.cs | 331 ++++++++++ .../Formatters/Json.SerializerOptions.cs | 133 ++++ Swan.Tiny/Formatters/Json.cs | 340 +++++++++++ Swan.Tiny/Formatters/JsonPropertyAttribute.cs | 40 ++ Swan.Tiny/Logging/ConsoleLogger.cs | 139 +++++ Swan.Tiny/Logging/DebugLogger.cs | 49 ++ Swan.Tiny/Logging/ILogger.cs | 24 + Swan.Tiny/Logging/LogLevel.cs | 41 ++ .../Logging/LogMessageReceivedEventArgs.cs | 147 +++++ Swan.Tiny/Logging/Logger.cs | 423 +++++++++++++ Swan.Tiny/Logging/TextLogger.cs | 63 ++ Swan.Tiny/Mappers/CopyableAttribute.cs | 11 + Swan.Tiny/Mappers/IObjectMap.cs | 31 + Swan.Tiny/Mappers/ObjectMap.cs | 109 ++++ .../ObjectMapper.PropertyInfoComparer.cs | 20 + Swan.Tiny/Mappers/ObjectMapper.cs | 309 ++++++++++ Swan.Tiny/Net/Dns/DnsClient.Interfaces.cs | 96 +++ Swan.Tiny/Net/Dns/DnsClient.Request.cs | 558 +++++++++++++++++ .../Net/Dns/DnsClient.ResourceRecords.cs | 344 +++++++++++ Swan.Tiny/Net/Dns/DnsClient.Response.cs | 174 ++++++ Swan.Tiny/Net/Dns/DnsClient.cs | 65 ++ Swan.Tiny/Net/Dns/DnsQueryException.cs | 28 + Swan.Tiny/Net/Dns/DnsQueryResult.cs | 130 ++++ Swan.Tiny/Net/Dns/DnsRecord.cs | 239 ++++++++ Swan.Tiny/Net/Dns/Enums.Dns.cs | 167 +++++ Swan.Tiny/Net/Network.cs | 289 +++++++++ Swan.Tiny/ProcessResult.cs | 51 ++ Swan.Tiny/ProcessRunner.cs | 353 +++++++++++ Swan.Tiny/Reflection/AttributeCache.cs | 161 +++++ Swan.Tiny/Reflection/ConstructorTypeCache.cs | 42 ++ Swan.Tiny/Reflection/ExtendedTypeInfo.cs | 247 ++++++++ Swan.Tiny/Reflection/IPropertyProxy.cs | 22 + Swan.Tiny/Reflection/PropertyProxy.cs | 44 ++ Swan.Tiny/Reflection/PropertyTypeCache.cs | 56 ++ Swan.Tiny/Reflection/TypeCache.cs | 71 +++ Swan.Tiny/SingletonBase.cs | 54 ++ Swan.Tiny/StructEndiannessAttribute.cs | 27 + Swan.Tiny/Swan.Tiny.csproj | 7 + Swan.Tiny/SwanRuntime.cs | 200 ++++++ Swan.Tiny/Terminal.Output.cs | 88 +++ Swan.Tiny/Terminal.Settings.cs | 46 ++ Swan.Tiny/Terminal.cs | 330 ++++++++++ Swan.Tiny/TerminalWriters.Enums.cs | 29 + Swan.Tiny/Threading/AtomicBoolean.cs | 22 + Swan.Tiny/Threading/AtomicTypeBase.cs | 219 +++++++ Swan.Tiny/Threading/ExclusiveTimer.cs | 206 +++++++ Swan.Tiny/Threading/IWaitEvent.cs | 63 ++ Swan.Tiny/Threading/WaitEventFactory.cs | 188 ++++++ 73 files changed, 11356 insertions(+) create mode 100644 Swan.Tiny/Collections/CollectionCacheRepository.cs create mode 100644 Swan.Tiny/Configuration/PropertyDisplayAttribute.cs create mode 100644 Swan.Tiny/Definitions.Types.cs create mode 100644 Swan.Tiny/Definitions.cs create mode 100644 Swan.Tiny/DependencyInjection/DependencyContainer.cs create mode 100644 Swan.Tiny/DependencyInjection/DependencyContainerRegistrationException.cs create mode 100644 Swan.Tiny/DependencyInjection/DependencyContainerResolutionException.cs create mode 100644 Swan.Tiny/DependencyInjection/DependencyContainerResolveOptions.cs create mode 100644 Swan.Tiny/DependencyInjection/DependencyContainerWeakReferenceException.cs create mode 100644 Swan.Tiny/DependencyInjection/ObjectFactoryBase.cs create mode 100644 Swan.Tiny/DependencyInjection/RegisterOptions.cs create mode 100644 Swan.Tiny/DependencyInjection/TypeRegistration.cs create mode 100644 Swan.Tiny/DependencyInjection/TypesConcurrentDictionary.cs create mode 100644 Swan.Tiny/Diagnostics/HighResolutionTimer.cs create mode 100644 Swan.Tiny/Enums.cs create mode 100644 Swan.Tiny/Extensions.ByteArrays.cs create mode 100644 Swan.Tiny/Extensions.Dictionaries.cs create mode 100644 Swan.Tiny/Extensions.Functional.cs create mode 100644 Swan.Tiny/Extensions.Reflection.cs create mode 100644 Swan.Tiny/Extensions.Strings.cs create mode 100644 Swan.Tiny/Extensions.ValueTypes.cs create mode 100644 Swan.Tiny/Extensions.cs create mode 100644 Swan.Tiny/Formatters/HumanizeJson.cs create mode 100644 Swan.Tiny/Formatters/Json.Converter.cs create mode 100644 Swan.Tiny/Formatters/Json.Deserializer.cs create mode 100644 Swan.Tiny/Formatters/Json.Serializer.cs create mode 100644 Swan.Tiny/Formatters/Json.SerializerOptions.cs create mode 100644 Swan.Tiny/Formatters/Json.cs create mode 100644 Swan.Tiny/Formatters/JsonPropertyAttribute.cs create mode 100644 Swan.Tiny/Logging/ConsoleLogger.cs create mode 100644 Swan.Tiny/Logging/DebugLogger.cs create mode 100644 Swan.Tiny/Logging/ILogger.cs create mode 100644 Swan.Tiny/Logging/LogLevel.cs create mode 100644 Swan.Tiny/Logging/LogMessageReceivedEventArgs.cs create mode 100644 Swan.Tiny/Logging/Logger.cs create mode 100644 Swan.Tiny/Logging/TextLogger.cs create mode 100644 Swan.Tiny/Mappers/CopyableAttribute.cs create mode 100644 Swan.Tiny/Mappers/IObjectMap.cs create mode 100644 Swan.Tiny/Mappers/ObjectMap.cs create mode 100644 Swan.Tiny/Mappers/ObjectMapper.PropertyInfoComparer.cs create mode 100644 Swan.Tiny/Mappers/ObjectMapper.cs create mode 100644 Swan.Tiny/Net/Dns/DnsClient.Interfaces.cs create mode 100644 Swan.Tiny/Net/Dns/DnsClient.Request.cs create mode 100644 Swan.Tiny/Net/Dns/DnsClient.ResourceRecords.cs create mode 100644 Swan.Tiny/Net/Dns/DnsClient.Response.cs create mode 100644 Swan.Tiny/Net/Dns/DnsClient.cs create mode 100644 Swan.Tiny/Net/Dns/DnsQueryException.cs create mode 100644 Swan.Tiny/Net/Dns/DnsQueryResult.cs create mode 100644 Swan.Tiny/Net/Dns/DnsRecord.cs create mode 100644 Swan.Tiny/Net/Dns/Enums.Dns.cs create mode 100644 Swan.Tiny/Net/Network.cs create mode 100644 Swan.Tiny/ProcessResult.cs create mode 100644 Swan.Tiny/ProcessRunner.cs create mode 100644 Swan.Tiny/Reflection/AttributeCache.cs create mode 100644 Swan.Tiny/Reflection/ConstructorTypeCache.cs create mode 100644 Swan.Tiny/Reflection/ExtendedTypeInfo.cs create mode 100644 Swan.Tiny/Reflection/IPropertyProxy.cs create mode 100644 Swan.Tiny/Reflection/PropertyProxy.cs create mode 100644 Swan.Tiny/Reflection/PropertyTypeCache.cs create mode 100644 Swan.Tiny/Reflection/TypeCache.cs create mode 100644 Swan.Tiny/SingletonBase.cs create mode 100644 Swan.Tiny/StructEndiannessAttribute.cs create mode 100644 Swan.Tiny/Swan.Tiny.csproj create mode 100644 Swan.Tiny/SwanRuntime.cs create mode 100644 Swan.Tiny/Terminal.Output.cs create mode 100644 Swan.Tiny/Terminal.Settings.cs create mode 100644 Swan.Tiny/Terminal.cs create mode 100644 Swan.Tiny/TerminalWriters.Enums.cs create mode 100644 Swan.Tiny/Threading/AtomicBoolean.cs create mode 100644 Swan.Tiny/Threading/AtomicTypeBase.cs create mode 100644 Swan.Tiny/Threading/ExclusiveTimer.cs create mode 100644 Swan.Tiny/Threading/IWaitEvent.cs create mode 100644 Swan.Tiny/Threading/WaitEventFactory.cs diff --git a/Swan.Tiny/Collections/CollectionCacheRepository.cs b/Swan.Tiny/Collections/CollectionCacheRepository.cs new file mode 100644 index 0000000..008b9a8 --- /dev/null +++ b/Swan.Tiny/Collections/CollectionCacheRepository.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; + +namespace Swan.Collections { + /// + /// A thread-safe collection cache repository for types. + /// + /// The type of member to cache. + public class CollectionCacheRepository { + private readonly Lazy>> _data = new Lazy>>(() => new ConcurrentDictionary>(), true); + + /// + /// Determines whether the cache contains the specified key. + /// + /// The key. + /// true if the cache contains the key, otherwise false. + public Boolean ContainsKey(Type key) => this._data.Value.ContainsKey(key); + + /// + /// Retrieves the properties stored for the specified type. + /// If the properties are not available, it calls the factory method to retrieve them + /// and returns them as an array of PropertyInfo. + /// + /// The key. + /// The factory. + /// + /// An array of the properties stored for the specified type. + /// + /// + /// key + /// or + /// factory. + /// + /// type. + public IEnumerable Retrieve(Type key, Func> factory) { + if(key == null) { + throw new ArgumentNullException(nameof(key)); + } + + if(factory == null) { + throw new ArgumentNullException(nameof(factory)); + } + + return this._data.Value.GetOrAdd(key, k => factory.Invoke(k).Where(item => item != null)); + } + } +} diff --git a/Swan.Tiny/Configuration/PropertyDisplayAttribute.cs b/Swan.Tiny/Configuration/PropertyDisplayAttribute.cs new file mode 100644 index 0000000..c5e9d97 --- /dev/null +++ b/Swan.Tiny/Configuration/PropertyDisplayAttribute.cs @@ -0,0 +1,62 @@ +using System; + +namespace Swan.Configuration { + /// + /// An attribute used to include additional information to a Property for serialization. + /// + /// Previously we used DisplayAttribute from DataAnnotation. + /// + /// + [AttributeUsage(AttributeTargets.Property)] + public sealed class PropertyDisplayAttribute : Attribute { + /// + /// Gets or sets the name. + /// + /// + /// The name. + /// + public String Name { + get; set; + } + + /// + /// Gets or sets the description. + /// + /// + /// The description. + /// + public String Description { + get; set; + } + + /// + /// Gets or sets the name of the group. + /// + /// + /// The name of the group. + /// + public String GroupName { + get; set; + } + + /// + /// Gets or sets the default value. + /// + /// + /// The default value. + /// + public Object DefaultValue { + get; set; + } + + /// + /// Gets or sets the format string to call with method ToString. + /// + /// + /// The format. + /// + public String Format { + get; set; + } + } +} diff --git a/Swan.Tiny/Definitions.Types.cs b/Swan.Tiny/Definitions.Types.cs new file mode 100644 index 0000000..6fc4742 --- /dev/null +++ b/Swan.Tiny/Definitions.Types.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Net; + +using Swan.Reflection; + +namespace Swan { + /// + /// Contains useful constants and definitions. + /// + public static partial class Definitions { + #region Main Dictionary Definition + + /// + /// The basic types information. + /// + public static readonly Lazy> BasicTypesInfo = new Lazy>(() => new Dictionary { + // Non-Nullables + {typeof(DateTime), new ExtendedTypeInfo()}, + {typeof(Byte), new ExtendedTypeInfo()}, + {typeof(SByte), new ExtendedTypeInfo()}, + {typeof(Int32), new ExtendedTypeInfo()}, + {typeof(UInt32), new ExtendedTypeInfo()}, + {typeof(Int16), new ExtendedTypeInfo()}, + {typeof(UInt16), new ExtendedTypeInfo()}, + {typeof(Int64), new ExtendedTypeInfo()}, + {typeof(UInt64), new ExtendedTypeInfo()}, + {typeof(Single), new ExtendedTypeInfo()}, + {typeof(Double), new ExtendedTypeInfo()}, + {typeof(Char), new ExtendedTypeInfo()}, + {typeof(Boolean), new ExtendedTypeInfo()}, + {typeof(Decimal), new ExtendedTypeInfo()}, + {typeof(Guid), new ExtendedTypeInfo()}, + + // Strings is also considered a basic type (it's the only basic reference type) + {typeof(String), new ExtendedTypeInfo()}, + + // Nullables + {typeof(DateTime?), new ExtendedTypeInfo()}, + {typeof(Byte?), new ExtendedTypeInfo()}, + {typeof(SByte?), new ExtendedTypeInfo()}, + {typeof(Int32?), new ExtendedTypeInfo()}, + {typeof(UInt32?), new ExtendedTypeInfo()}, + {typeof(Int16?), new ExtendedTypeInfo()}, + {typeof(UInt16?), new ExtendedTypeInfo()}, + {typeof(Int64?), new ExtendedTypeInfo()}, + {typeof(UInt64?), new ExtendedTypeInfo()}, + {typeof(Single?), new ExtendedTypeInfo()}, + {typeof(Double?), new ExtendedTypeInfo()}, + {typeof(Char?), new ExtendedTypeInfo()}, + {typeof(Boolean?), new ExtendedTypeInfo()}, + {typeof(Decimal?), new ExtendedTypeInfo()}, + {typeof(Guid?), new ExtendedTypeInfo()}, + + // Additional Types + {typeof(TimeSpan), new ExtendedTypeInfo()}, + {typeof(TimeSpan?), new ExtendedTypeInfo()}, + {typeof(IPAddress), new ExtendedTypeInfo()}, + }); + + #endregion + + /// + /// Contains all basic types, including string, date time, and all of their nullable counterparts. + /// + /// + /// All basic types. + /// + public static IReadOnlyCollection AllBasicTypes { get; } = new ReadOnlyCollection(BasicTypesInfo.Value.Keys.ToArray()); + + /// + /// Gets all numeric types including their nullable counterparts. + /// Note that Booleans and Guids are not considered numeric types. + /// + /// + /// All numeric types. + /// + public static IReadOnlyCollection AllNumericTypes { + get; + } = new ReadOnlyCollection(BasicTypesInfo.Value.Where(kvp => kvp.Value.IsNumeric).Select(kvp => kvp.Key).ToArray()); + + /// + /// Gets all numeric types without their nullable counterparts. + /// Note that Booleans and Guids are not considered numeric types. + /// + /// + /// All numeric value types. + /// + public static IReadOnlyCollection AllNumericValueTypes { + get; + } = new ReadOnlyCollection(BasicTypesInfo.Value.Where(kvp => kvp.Value.IsNumeric && !kvp.Value.IsNullableValueType).Select(kvp => kvp.Key).ToArray()); + + /// + /// Contains all basic value types. i.e. excludes string and nullables. + /// + /// + /// All basic value types. + /// + public static IReadOnlyCollection AllBasicValueTypes { + get; + } = new ReadOnlyCollection(BasicTypesInfo.Value.Where(kvp => kvp.Value.IsValueType).Select(kvp => kvp.Key).ToArray()); + + /// + /// Contains all basic value types including the string type. i.e. excludes nullables. + /// + /// + /// All basic value and string types. + /// + public static IReadOnlyCollection AllBasicValueAndStringTypes { + get; + } = new ReadOnlyCollection(BasicTypesInfo.Value.Where(kvp => kvp.Value.IsValueType || kvp.Key == typeof(String)).Select(kvp => kvp.Key).ToArray()); + + /// + /// Gets all nullable value types. i.e. excludes string and all basic value types. + /// + /// + /// All basic nullable value types. + /// + public static IReadOnlyCollection AllBasicNullableValueTypes { + get; + } = new ReadOnlyCollection(BasicTypesInfo.Value.Where(kvp => kvp.Value.IsNullableValueType).Select(kvp => kvp.Key).ToArray()); + } +} diff --git a/Swan.Tiny/Definitions.cs b/Swan.Tiny/Definitions.cs new file mode 100644 index 0000000..d91bed1 --- /dev/null +++ b/Swan.Tiny/Definitions.cs @@ -0,0 +1,34 @@ +using System; +using System.Text; + +namespace Swan { + /// + /// Contains useful constants and definitions. + /// + public static partial class Definitions { + /// + /// The MS Windows codepage 1252 encoding used in some legacy scenarios + /// such as default CSV text encoding from Excel. + /// + public static readonly Encoding Windows1252Encoding; + + /// + /// The encoding associated with the default ANSI code page in the operating + /// system's regional and language settings. + /// + public static readonly Encoding CurrentAnsiEncoding; + + /// + /// Initializes the class. + /// + static Definitions() { + CurrentAnsiEncoding = Encoding.GetEncoding(default(Int32)); + try { + Windows1252Encoding = Encoding.GetEncoding(1252); + } catch { + // ignore, the codepage is not available use default + Windows1252Encoding = CurrentAnsiEncoding; + } + } + } +} diff --git a/Swan.Tiny/DependencyInjection/DependencyContainer.cs b/Swan.Tiny/DependencyInjection/DependencyContainer.cs new file mode 100644 index 0000000..efbe114 --- /dev/null +++ b/Swan.Tiny/DependencyInjection/DependencyContainer.cs @@ -0,0 +1,575 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Swan.DependencyInjection { + /// + /// The concrete implementation of a simple IoC container + /// based largely on TinyIoC (https://github.com/grumpydev/TinyIoC). + /// + /// + public partial class DependencyContainer : IDisposable { + private readonly Object _autoRegisterLock = new Object(); + + private Boolean _disposed; + + static DependencyContainer() { + } + + /// + /// Initializes a new instance of the class. + /// + public DependencyContainer() { + this.RegisteredTypes = new TypesConcurrentDictionary(this); + _ = this.Register(this); + } + + private DependencyContainer(DependencyContainer parent) : this() => this.Parent = parent; + + /// + /// Lazy created Singleton instance of the container for simple scenarios. + /// + public static DependencyContainer Current { get; } = new DependencyContainer(); + + internal DependencyContainer Parent { + get; + } + + internal TypesConcurrentDictionary RegisteredTypes { + get; + } + + /// + public void Dispose() { + if(this._disposed) { + return; + } + + this._disposed = true; + + foreach(IDisposable disposable in this.RegisteredTypes.Values.Select(item => item as IDisposable)) { + disposable?.Dispose(); + } + + GC.SuppressFinalize(this); + } + + /// + /// Gets the child container. + /// + /// A new instance of the class. + public DependencyContainer GetChildContainer() => new DependencyContainer(this); + + #region Registration + + /// + /// Attempt to automatically register all non-generic classes and interfaces in the current app domain. + /// Types will only be registered if they pass the supplied registration predicate. + /// + /// What action to take when encountering duplicate implementations of an interface/base class. + /// Predicate to determine if a particular type should be registered. + public void AutoRegister(DependencyContainerDuplicateImplementationAction duplicateAction = DependencyContainerDuplicateImplementationAction.RegisterSingle, Func registrationPredicate = null) => this.AutoRegister(AppDomain.CurrentDomain.GetAssemblies().Where(a => !IsIgnoredAssembly(a)), duplicateAction, registrationPredicate); + + /// + /// Attempt to automatically register all non-generic classes and interfaces in the specified assemblies + /// Types will only be registered if they pass the supplied registration predicate. + /// + /// Assemblies to process. + /// What action to take when encountering duplicate implementations of an interface/base class. + /// Predicate to determine if a particular type should be registered. + public void AutoRegister(IEnumerable assemblies, DependencyContainerDuplicateImplementationAction duplicateAction = DependencyContainerDuplicateImplementationAction.RegisterSingle, Func registrationPredicate = null) { + lock(this._autoRegisterLock) { + List types = assemblies.SelectMany(a => a.GetAllTypes()).Where(t => !IsIgnoredType(t, registrationPredicate)).ToList(); + + List concreteTypes = types.Where(type => type.IsClass && !type.IsAbstract && type != this.GetType() && type.DeclaringType != this.GetType() && !type.IsGenericTypeDefinition).ToList(); + + foreach(Type type in concreteTypes) { + try { + _ = this.RegisteredTypes.Register(type, String.Empty, GetDefaultObjectFactory(type, type)); + } catch(MethodAccessException) { + // Ignore methods we can't access - added for Silverlight + } + } + + IEnumerable abstractInterfaceTypes = types.Where(type => (type.IsInterface || type.IsAbstract) && type.DeclaringType != this.GetType() && !type.IsGenericTypeDefinition); + + foreach(Type type in abstractInterfaceTypes) { + Type localType = type; + List implementations = concreteTypes.Where(implementationType => localType.IsAssignableFrom(implementationType)).ToList(); + + if(implementations.Skip(1).Any()) { + if(duplicateAction == DependencyContainerDuplicateImplementationAction.Fail) { + throw new DependencyContainerRegistrationException(type, implementations); + } + + if(duplicateAction == DependencyContainerDuplicateImplementationAction.RegisterMultiple) { + _ = this.RegisterMultiple(type, implementations); + } + } + + Type firstImplementation = implementations.FirstOrDefault(); + + if(firstImplementation == null) { + continue; + } + + try { + _ = this.RegisteredTypes.Register(type, String.Empty, GetDefaultObjectFactory(type, firstImplementation)); + } catch(MethodAccessException) { + // Ignore methods we can't access - added for Silverlight + } + } + } + } + + /// + /// Creates/replaces a named container class registration with default options. + /// + /// Type to register. + /// Name of registration. + /// RegisterOptions for fluent API. + public RegisterOptions Register(Type registerType, String name = "") => this.RegisteredTypes.Register(registerType, name, GetDefaultObjectFactory(registerType, registerType)); + + /// + /// Creates/replaces a named container class registration with a given implementation and default options. + /// + /// Type to register. + /// Type to instantiate that implements RegisterType. + /// Name of registration. + /// RegisterOptions for fluent API. + public RegisterOptions Register(Type registerType, Type registerImplementation, String name = "") => this.RegisteredTypes.Register(registerType, name, GetDefaultObjectFactory(registerType, registerImplementation)); + + /// + /// Creates/replaces a named container class registration with a specific, strong referenced, instance. + /// + /// Type to register. + /// Instance of RegisterType to register. + /// Name of registration. + /// RegisterOptions for fluent API. + public RegisterOptions Register(Type registerType, Object instance, String name = "") => this.RegisteredTypes.Register(registerType, name, new InstanceFactory(registerType, registerType, instance)); + + /// + /// Creates/replaces a named container class registration with a specific, strong referenced, instance. + /// + /// Type to register. + /// Type of instance to register that implements RegisterType. + /// Instance of RegisterImplementation to register. + /// Name of registration. + /// RegisterOptions for fluent API. + public RegisterOptions Register(Type registerType, Type registerImplementation, Object instance, String name = "") => this.RegisteredTypes.Register(registerType, name, new InstanceFactory(registerType, registerImplementation, instance)); + + /// + /// Creates/replaces a container class registration with a user specified factory. + /// + /// Type to register. + /// Factory/lambda that returns an instance of RegisterType. + /// Name of registration. + /// RegisterOptions for fluent API. + public RegisterOptions Register(Type registerType, Func, Object> factory, String name = "") => this.RegisteredTypes.Register(registerType, name, new DelegateFactory(registerType, factory)); + + /// + /// Creates/replaces a named container class registration with default options. + /// + /// Type to register. + /// Name of registration. + /// RegisterOptions for fluent API. + public RegisterOptions Register(String name = "") where TRegister : class => this.Register(typeof(TRegister), name); + + /// + /// Creates/replaces a named container class registration with a given implementation and default options. + /// + /// Type to register. + /// Type to instantiate that implements RegisterType. + /// Name of registration. + /// RegisterOptions for fluent API. + public RegisterOptions Register(String name = "") where TRegister : class where TRegisterImplementation : class, TRegister => this.Register(typeof(TRegister), typeof(TRegisterImplementation), name); + + /// + /// Creates/replaces a named container class registration with a specific, strong referenced, instance. + /// + /// Type to register. + /// Instance of RegisterType to register. + /// Name of registration. + /// RegisterOptions for fluent API. + public RegisterOptions Register(TRegister instance, String name = "") where TRegister : class => this.Register(typeof(TRegister), instance, name); + + /// + /// Creates/replaces a named container class registration with a specific, strong referenced, instance. + /// + /// Type to register. + /// Type of instance to register that implements RegisterType. + /// Instance of RegisterImplementation to register. + /// Name of registration. + /// RegisterOptions for fluent API. + public RegisterOptions Register(TRegisterImplementation instance, String name = "") where TRegister : class where TRegisterImplementation : class, TRegister => this.Register(typeof(TRegister), typeof(TRegisterImplementation), instance, name); + + /// + /// Creates/replaces a named container class registration with a user specified factory. + /// + /// Type to register. + /// Factory/lambda that returns an instance of RegisterType. + /// Name of registration. + /// RegisterOptions for fluent API. + public RegisterOptions Register(Func, TRegister> factory, String name = "") where TRegister : class { + if(factory == null) { + throw new ArgumentNullException(nameof(factory)); + } + + return this.Register(typeof(TRegister), factory, name); + } + + /// + /// Register multiple implementations of a type. + /// + /// Internally this registers each implementation using the full name of the class as its registration name. + /// + /// Type that each implementation implements. + /// Types that implement RegisterType. + /// MultiRegisterOptions for the fluent API. + public MultiRegisterOptions RegisterMultiple(IEnumerable implementationTypes) => this.RegisterMultiple(typeof(TRegister), implementationTypes); + + /// + /// Register multiple implementations of a type. + /// + /// Internally this registers each implementation using the full name of the class as its registration name. + /// + /// Type that each implementation implements. + /// Types that implement RegisterType. + /// MultiRegisterOptions for the fluent API. + public MultiRegisterOptions RegisterMultiple(Type registrationType, IEnumerable implementationTypes) { + if(implementationTypes == null) { + throw new ArgumentNullException(nameof(implementationTypes), "types is null."); + } + + foreach(Type type in implementationTypes.Where(type => !registrationType.IsAssignableFrom(type))) { + throw new ArgumentException($"types: The type {registrationType.FullName} is not assignable from {type.FullName}"); + } + + if(implementationTypes.Count() != implementationTypes.Distinct().Count()) { + IEnumerable queryForDuplicatedTypes = implementationTypes.GroupBy(i => i).Where(j => j.Count() > 1).Select(j => j.Key.FullName); + + String fullNamesOfDuplicatedTypes = String.Join(",\n", queryForDuplicatedTypes.ToArray()); + + throw new ArgumentException($"types: The same implementation type cannot be specified multiple times for {registrationType.FullName}\n\n{fullNamesOfDuplicatedTypes}"); + } + + List registerOptions = implementationTypes.Select(type => this.Register(registrationType, type, type.FullName)).ToList(); + + return new MultiRegisterOptions(registerOptions); + } + + #endregion + + #region Unregistration + + /// + /// Remove a named container class registration. + /// + /// Type to unregister. + /// Name of registration. + /// true if the registration is successfully found and removed; otherwise, false. + public Boolean Unregister(String name = "") => this.Unregister(typeof(TRegister), name); + + /// + /// Remove a named container class registration. + /// + /// Type to unregister. + /// Name of registration. + /// true if the registration is successfully found and removed; otherwise, false. + public Boolean Unregister(Type registerType, String name = "") => this.RegisteredTypes.RemoveRegistration(new TypeRegistration(registerType, name)); + + #endregion + + #region Resolution + + /// + /// Attempts to resolve a named type using specified options and the supplied constructor parameters. + /// + /// Parameters are used in conjunction with normal container resolution to find the most suitable constructor (if one exists). + /// All user supplied parameters must exist in at least one resolvable constructor of RegisterType or resolution will fail. + /// + /// Type to resolve. + /// Name of registration. + /// Resolution options. + /// Instance of type. + /// Unable to resolve the type. + public Object Resolve(Type resolveType, String name = null, DependencyContainerResolveOptions options = null) => this.RegisteredTypes.ResolveInternal(new TypeRegistration(resolveType, name), options ?? DependencyContainerResolveOptions.Default); + + /// + /// Attempts to resolve a named type using specified options and the supplied constructor parameters. + /// + /// Parameters are used in conjunction with normal container resolution to find the most suitable constructor (if one exists). + /// All user supplied parameters must exist in at least one resolvable constructor of RegisterType or resolution will fail. + /// + /// Type to resolve. + /// Name of registration. + /// Resolution options. + /// Instance of type. + /// Unable to resolve the type. + public TResolveType Resolve(String name = null, DependencyContainerResolveOptions options = null) where TResolveType : class => (TResolveType)this.Resolve(typeof(TResolveType), name, options); + + /// + /// Attempts to predict whether a given type can be resolved with the supplied constructor parameters options. + /// Parameters are used in conjunction with normal container resolution to find the most suitable constructor (if one exists). + /// All user supplied parameters must exist in at least one resolvable constructor of RegisterType or resolution will fail. + /// Note: Resolution may still fail if user defined factory registrations fail to construct objects when called. + /// + /// Type to resolve. + /// The name. + /// Resolution options. + /// + /// Bool indicating whether the type can be resolved. + /// + public Boolean CanResolve(Type resolveType, String name = null, DependencyContainerResolveOptions options = null) => this.RegisteredTypes.CanResolve(new TypeRegistration(resolveType, name), options); + + /// + /// Attempts to predict whether a given named type can be resolved with the supplied constructor parameters options. + /// + /// Parameters are used in conjunction with normal container resolution to find the most suitable constructor (if one exists). + /// All user supplied parameters must exist in at least one resolvable constructor of RegisterType or resolution will fail. + /// + /// Note: Resolution may still fail if user defined factory registrations fail to construct objects when called. + /// + /// Type to resolve. + /// Name of registration. + /// Resolution options. + /// Bool indicating whether the type can be resolved. + public Boolean CanResolve(String name = null, DependencyContainerResolveOptions options = null) where TResolveType : class => this.CanResolve(typeof(TResolveType), name, options); + + /// + /// Attempts to resolve a type using the default options. + /// + /// Type to resolve. + /// Resolved type or default if resolve fails. + /// true if resolved successfully, false otherwise. + public Boolean TryResolve(Type resolveType, out Object resolvedType) { + try { + resolvedType = this.Resolve(resolveType); + return true; + } catch(DependencyContainerResolutionException) { + resolvedType = null; + return false; + } + } + + /// + /// Attempts to resolve a type using the given options. + /// + /// Type to resolve. + /// Resolution options. + /// Resolved type or default if resolve fails. + /// true if resolved successfully, false otherwise. + public Boolean TryResolve(Type resolveType, DependencyContainerResolveOptions options, out Object resolvedType) { + try { + resolvedType = this.Resolve(resolveType, options: options); + return true; + } catch(DependencyContainerResolutionException) { + resolvedType = null; + return false; + } + } + + /// + /// Attempts to resolve a type using the default options and given name. + /// + /// Type to resolve. + /// Name of registration. + /// Resolved type or default if resolve fails. + /// true if resolved successfully, false otherwise. + public Boolean TryResolve(Type resolveType, String name, out Object resolvedType) { + try { + resolvedType = this.Resolve(resolveType, name); + return true; + } catch(DependencyContainerResolutionException) { + resolvedType = null; + return false; + } + } + + /// + /// Attempts to resolve a type using the given options and name. + /// + /// Type to resolve. + /// Name of registration. + /// Resolution options. + /// Resolved type or default if resolve fails. + /// true if resolved successfully, false otherwise. + public Boolean TryResolve(Type resolveType, String name, DependencyContainerResolveOptions options, out Object resolvedType) { + try { + resolvedType = this.Resolve(resolveType, name, options); + return true; + } catch(DependencyContainerResolutionException) { + resolvedType = null; + return false; + } + } + + /// + /// Attempts to resolve a type using the default options. + /// + /// Type to resolve. + /// Resolved type or default if resolve fails. + /// true if resolved successfully, false otherwise. + public Boolean TryResolve(out TResolveType resolvedType) where TResolveType : class { + try { + resolvedType = this.Resolve(); + return true; + } catch(DependencyContainerResolutionException) { + resolvedType = default; + return false; + } + } + + /// + /// Attempts to resolve a type using the given options. + /// + /// Type to resolve. + /// Resolution options. + /// Resolved type or default if resolve fails. + /// true if resolved successfully, false otherwise. + public Boolean TryResolve(DependencyContainerResolveOptions options, out TResolveType resolvedType) where TResolveType : class { + try { + resolvedType = this.Resolve(options: options); + return true; + } catch(DependencyContainerResolutionException) { + resolvedType = default; + return false; + } + } + + /// + /// Attempts to resolve a type using the default options and given name. + /// + /// Type to resolve. + /// Name of registration. + /// Resolved type or default if resolve fails. + /// true if resolved successfully, false otherwise. + public Boolean TryResolve(String name, out TResolveType resolvedType) where TResolveType : class { + try { + resolvedType = this.Resolve(name); + return true; + } catch(DependencyContainerResolutionException) { + resolvedType = default; + return false; + } + } + + /// + /// Attempts to resolve a type using the given options and name. + /// + /// Type to resolve. + /// Name of registration. + /// Resolution options. + /// Resolved type or default if resolve fails. + /// true if resolved successfully, false otherwise. + public Boolean TryResolve(String name, DependencyContainerResolveOptions options, out TResolveType resolvedType) where TResolveType : class { + try { + resolvedType = this.Resolve(name, options); + return true; + } catch(DependencyContainerResolutionException) { + resolvedType = default; + return false; + } + } + + /// + /// Returns all registrations of a type. + /// + /// Type to resolveAll. + /// Whether to include un-named (default) registrations. + /// IEnumerable. + public IEnumerable ResolveAll(Type resolveType, Boolean includeUnnamed = false) => this.RegisteredTypes.Resolve(resolveType, includeUnnamed); + + /// + /// Returns all registrations of a type. + /// + /// Type to resolveAll. + /// Whether to include un-named (default) registrations. + /// IEnumerable. + public IEnumerable ResolveAll(Boolean includeUnnamed = true) where TResolveType : class => this.ResolveAll(typeof(TResolveType), includeUnnamed).Cast(); + + /// + /// Attempts to resolve all public property dependencies on the given object using the given resolve options. + /// + /// Object to "build up". + /// Resolve options to use. + public void BuildUp(Object input, DependencyContainerResolveOptions resolveOptions = null) { + if(resolveOptions == null) { + resolveOptions = DependencyContainerResolveOptions.Default; + } + + IEnumerable properties = input.GetType().GetProperties().Where(property => property.GetCacheGetMethod() != null && property.GetCacheSetMethod() != null && !property.PropertyType.IsValueType); + + foreach(PropertyInfo property in properties.Where(property => property.GetValue(input, null) == null)) { + try { + property.SetValue(input, this.RegisteredTypes.ResolveInternal(new TypeRegistration(property.PropertyType), resolveOptions), null); + } catch(DependencyContainerResolutionException) { + // Catch any resolution errors and ignore them + } + } + } + + #endregion + + #region Internal Methods + + internal static Boolean IsValidAssignment(Type registerType, Type registerImplementation) { + if(!registerType.IsGenericTypeDefinition) { + if(!registerType.IsAssignableFrom(registerImplementation)) { + return false; + } + } else { + if(registerType.IsInterface && registerImplementation.GetInterfaces().All(t => t.Name != registerType.Name)) { + return false; + } + + if(registerType.IsAbstract && registerImplementation.BaseType != registerType) { + return false; + } + } + + return true; + } + + private static Boolean IsIgnoredAssembly(Assembly assembly) { + // TODO - find a better way to remove "system" assemblies from the auto registration + List> ignoreChecks = new List> + { + asm => asm.FullName.StartsWith("Microsoft.", StringComparison.Ordinal), + asm => asm.FullName.StartsWith("System.", StringComparison.Ordinal), + asm => asm.FullName.StartsWith("System,", StringComparison.Ordinal), + asm => asm.FullName.StartsWith("CR_ExtUnitTest", StringComparison.Ordinal), + asm => asm.FullName.StartsWith("mscorlib,", StringComparison.Ordinal), + asm => asm.FullName.StartsWith("CR_VSTest", StringComparison.Ordinal), + asm => asm.FullName.StartsWith("DevExpress.CodeRush", StringComparison.Ordinal), + asm => asm.FullName.StartsWith("xunit.", StringComparison.Ordinal), + }; + + return ignoreChecks.Any(check => check(assembly)); + } + + private static Boolean IsIgnoredType(Type type, Func registrationPredicate) { + // TODO - find a better way to remove "system" types from the auto registration + List> ignoreChecks = new List>() + { + t => t.FullName?.StartsWith("System.", StringComparison.Ordinal) ?? false, + t => t.FullName?.StartsWith("Microsoft.", StringComparison.Ordinal) ?? false, + t => t.IsPrimitive, + t => t.IsGenericTypeDefinition, + t => t.GetConstructors(BindingFlags.Instance | BindingFlags.Public).Length == 0 && + !(t.IsInterface || t.IsAbstract), + }; + + if(registrationPredicate != null) { + ignoreChecks.Add(t => !registrationPredicate(t)); + } + + return ignoreChecks.Any(check => check(type)); + } + + private static ObjectFactoryBase GetDefaultObjectFactory(Type registerType, Type registerImplementation) => registerType.IsInterface || registerType.IsAbstract ? (ObjectFactoryBase)new SingletonFactory(registerType, registerImplementation) : new MultiInstanceFactory(registerType, registerImplementation); + + #endregion + } +} diff --git a/Swan.Tiny/DependencyInjection/DependencyContainerRegistrationException.cs b/Swan.Tiny/DependencyInjection/DependencyContainerRegistrationException.cs new file mode 100644 index 0000000..38dfae6 --- /dev/null +++ b/Swan.Tiny/DependencyInjection/DependencyContainerRegistrationException.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Swan.DependencyInjection { + /// + /// Generic Constraint Registration Exception. + /// + /// + public class DependencyContainerRegistrationException : Exception { + private const String ConvertErrorText = "Cannot convert current registration of {0} to {1}"; + private const String RegisterErrorText = "Cannot register type {0} - abstract classes or interfaces are not valid implementation types for {1}."; + private const String ErrorText = "Duplicate implementation of type {0} found ({1})."; + + /// + /// Initializes a new instance of the class. + /// + /// Type of the register. + /// The types. + public DependencyContainerRegistrationException(Type registerType, IEnumerable types) : base(String.Format(ErrorText, registerType, GetTypesString(types))) { + } + + /// + /// Initializes a new instance of the class. + /// + /// The type. + /// The method. + /// if set to true [is type factory]. + public DependencyContainerRegistrationException(Type type, String method, Boolean isTypeFactory = false) : base(isTypeFactory ? String.Format(RegisterErrorText, type.FullName, method) : String.Format(ConvertErrorText, type.FullName, method)) { + } + + private static String GetTypesString(IEnumerable types) => String.Join(",", types.Select(type => type.FullName)); + } +} \ No newline at end of file diff --git a/Swan.Tiny/DependencyInjection/DependencyContainerResolutionException.cs b/Swan.Tiny/DependencyInjection/DependencyContainerResolutionException.cs new file mode 100644 index 0000000..63d6795 --- /dev/null +++ b/Swan.Tiny/DependencyInjection/DependencyContainerResolutionException.cs @@ -0,0 +1,25 @@ +using System; + +namespace Swan.DependencyInjection { + /// + /// An exception for dependency resolutions. + /// + /// + [Serializable] + public class DependencyContainerResolutionException : Exception { + /// + /// Initializes a new instance of the class. + /// + /// The type. + public DependencyContainerResolutionException(Type type) : base($"Unable to resolve type: {type.FullName}") { + } + + /// + /// Initializes a new instance of the class. + /// + /// The type. + /// The inner exception. + public DependencyContainerResolutionException(Type type, Exception innerException) : base($"Unable to resolve type: {type.FullName}", innerException) { + } + } +} diff --git a/Swan.Tiny/DependencyInjection/DependencyContainerResolveOptions.cs b/Swan.Tiny/DependencyInjection/DependencyContainerResolveOptions.cs new file mode 100644 index 0000000..4bf2546 --- /dev/null +++ b/Swan.Tiny/DependencyInjection/DependencyContainerResolveOptions.cs @@ -0,0 +1,106 @@ +using System.Collections.Generic; + +namespace Swan.DependencyInjection { + /// + /// Resolution settings. + /// + public class DependencyContainerResolveOptions { + /// + /// Gets the default options (attempt resolution of unregistered types, fail on named resolution if name not found). + /// + public static DependencyContainerResolveOptions Default { get; } = new DependencyContainerResolveOptions(); + + /// + /// Gets or sets the unregistered resolution action. + /// + /// + /// The unregistered resolution action. + /// + public DependencyContainerUnregisteredResolutionAction UnregisteredResolutionAction { get; set; } = DependencyContainerUnregisteredResolutionAction.AttemptResolve; + + /// + /// Gets or sets the named resolution failure action. + /// + /// + /// The named resolution failure action. + /// + public DependencyContainerNamedResolutionFailureAction NamedResolutionFailureAction { get; set; } = DependencyContainerNamedResolutionFailureAction.Fail; + + /// + /// Gets the constructor parameters. + /// + /// + /// The constructor parameters. + /// + public Dictionary ConstructorParameters { get; } = new Dictionary(); + + /// + /// Clones this instance. + /// + /// + public DependencyContainerResolveOptions Clone() => new DependencyContainerResolveOptions { + NamedResolutionFailureAction = NamedResolutionFailureAction, + UnregisteredResolutionAction = UnregisteredResolutionAction, + }; + } + + /// + /// Defines Resolution actions. + /// + public enum DependencyContainerUnregisteredResolutionAction { + /// + /// Attempt to resolve type, even if the type isn't registered. + /// + /// Registered types/options will always take precedence. + /// + AttemptResolve, + + /// + /// Fail resolution if type not explicitly registered + /// + Fail, + + /// + /// Attempt to resolve unregistered type if requested type is generic + /// and no registration exists for the specific generic parameters used. + /// + /// Registered types/options will always take precedence. + /// + GenericsOnly, + } + + /// + /// Enumerates failure actions. + /// + public enum DependencyContainerNamedResolutionFailureAction { + /// + /// The attempt unnamed resolution + /// + AttemptUnnamedResolution, + + /// + /// The fail + /// + Fail, + } + + /// + /// Enumerates duplicate definition actions. + /// + public enum DependencyContainerDuplicateImplementationAction { + /// + /// The register single + /// + RegisterSingle, + + /// + /// The register multiple + /// + RegisterMultiple, + + /// + /// The fail + /// + Fail, + } +} \ No newline at end of file diff --git a/Swan.Tiny/DependencyInjection/DependencyContainerWeakReferenceException.cs b/Swan.Tiny/DependencyInjection/DependencyContainerWeakReferenceException.cs new file mode 100644 index 0000000..22953f4 --- /dev/null +++ b/Swan.Tiny/DependencyInjection/DependencyContainerWeakReferenceException.cs @@ -0,0 +1,18 @@ +using System; + +namespace Swan.DependencyInjection { + /// + /// Weak Reference Exception. + /// + /// + public class DependencyContainerWeakReferenceException : Exception { + private const String ErrorText = "Unable to instantiate {0} - referenced object has been reclaimed"; + + /// + /// Initializes a new instance of the class. + /// + /// The type. + public DependencyContainerWeakReferenceException(Type type) : base(String.Format(ErrorText, type.FullName)) { + } + } +} diff --git a/Swan.Tiny/DependencyInjection/ObjectFactoryBase.cs b/Swan.Tiny/DependencyInjection/ObjectFactoryBase.cs new file mode 100644 index 0000000..d2189ee --- /dev/null +++ b/Swan.Tiny/DependencyInjection/ObjectFactoryBase.cs @@ -0,0 +1,352 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Swan.DependencyInjection { + /// + /// Represents an abstract class for Object Factory. + /// + public abstract class ObjectFactoryBase { + /// + /// Whether to assume this factory successfully constructs its objects + /// + /// Generally set to true for delegate style factories as CanResolve cannot delve + /// into the delegates they contain. + /// + public virtual Boolean AssumeConstruction => false; + + /// + /// The type the factory instantiates. + /// + public abstract Type CreatesType { + get; + } + + /// + /// Constructor to use, if specified. + /// + public ConstructorInfo Constructor { + get; private set; + } + + /// + /// Gets the singleton variant. + /// + /// + /// The singleton variant. + /// + /// singleton. + public virtual ObjectFactoryBase SingletonVariant => throw new DependencyContainerRegistrationException(this.GetType(), "singleton"); + + /// + /// Gets the multi instance variant. + /// + /// + /// The multi instance variant. + /// + /// multi-instance. + public virtual ObjectFactoryBase MultiInstanceVariant => throw new DependencyContainerRegistrationException(this.GetType(), "multi-instance"); + + /// + /// Gets the strong reference variant. + /// + /// + /// The strong reference variant. + /// + /// strong reference. + public virtual ObjectFactoryBase StrongReferenceVariant => throw new DependencyContainerRegistrationException(this.GetType(), "strong reference"); + + /// + /// Gets the weak reference variant. + /// + /// + /// The weak reference variant. + /// + /// weak reference. + public virtual ObjectFactoryBase WeakReferenceVariant => throw new DependencyContainerRegistrationException(this.GetType(), "weak reference"); + + /// + /// Create the type. + /// + /// Type user requested to be resolved. + /// Container that requested the creation. + /// The options. + /// Instance of type. + public abstract Object GetObject(Type requestedType, DependencyContainer container, DependencyContainerResolveOptions options); + + /// + /// Gets the factory for child container. + /// + /// The type. + /// The parent. + /// The child. + /// + public virtual ObjectFactoryBase GetFactoryForChildContainer(Type type, DependencyContainer parent, DependencyContainer child) => this; + } + + /// + /// + /// IObjectFactory that creates new instances of types for each resolution. + /// + internal class MultiInstanceFactory : ObjectFactoryBase { + private readonly Type _registerType; + private readonly Type _registerImplementation; + + public MultiInstanceFactory(Type registerType, Type registerImplementation) { + if(registerImplementation.IsAbstract || registerImplementation.IsInterface) { + throw new DependencyContainerRegistrationException(registerImplementation, "MultiInstanceFactory", true); + } + + if(!DependencyContainer.IsValidAssignment(registerType, registerImplementation)) { + throw new DependencyContainerRegistrationException(registerImplementation, "MultiInstanceFactory", true); + } + + this._registerType = registerType; + this._registerImplementation = registerImplementation; + } + + public override Type CreatesType => this._registerImplementation; + + public override ObjectFactoryBase SingletonVariant => + new SingletonFactory(this._registerType, this._registerImplementation); + + public override ObjectFactoryBase MultiInstanceVariant => this; + + public override Object GetObject(Type requestedType, DependencyContainer container, DependencyContainerResolveOptions options) { + try { + return container.RegisteredTypes.ConstructType(this._registerImplementation, this.Constructor, options); + } catch(DependencyContainerResolutionException ex) { + throw new DependencyContainerResolutionException(this._registerType, ex); + } + } + } + + /// + /// + /// IObjectFactory that invokes a specified delegate to construct the object. + /// + internal class DelegateFactory : ObjectFactoryBase { + private readonly Type _registerType; + + private readonly Func, Object> _factory; + + public DelegateFactory( + Type registerType, + Func, Object> factory) { + this._factory = factory ?? throw new ArgumentNullException(nameof(factory)); + + this._registerType = registerType; + } + + public override Boolean AssumeConstruction => true; + + public override Type CreatesType => this._registerType; + + public override ObjectFactoryBase WeakReferenceVariant => new WeakDelegateFactory(this._registerType, this._factory); + + public override ObjectFactoryBase StrongReferenceVariant => this; + + public override Object GetObject(Type requestedType, DependencyContainer container, DependencyContainerResolveOptions options) { + try { + return this._factory.Invoke(container, options.ConstructorParameters); + } catch(Exception ex) { + throw new DependencyContainerResolutionException(this._registerType, ex); + } + } + } + + /// + /// + /// IObjectFactory that invokes a specified delegate to construct the object + /// Holds the delegate using a weak reference. + /// + internal class WeakDelegateFactory : ObjectFactoryBase { + private readonly Type _registerType; + + private readonly WeakReference _factory; + + public WeakDelegateFactory(Type registerType, Func, Object> factory) { + if(factory == null) { + throw new ArgumentNullException(nameof(factory)); + } + + this._factory = new WeakReference(factory); + + this._registerType = registerType; + } + + public override Boolean AssumeConstruction => true; + + public override Type CreatesType => this._registerType; + + public override ObjectFactoryBase StrongReferenceVariant { + get { + if(!(this._factory.Target is Func, global::System.Object> factory)) { + throw new DependencyContainerWeakReferenceException(this._registerType); + } + + return new DelegateFactory(this._registerType, factory); + } + } + + public override ObjectFactoryBase WeakReferenceVariant => this; + + public override Object GetObject(Type requestedType, DependencyContainer container, DependencyContainerResolveOptions options) { + if(!(this._factory.Target is Func, global::System.Object> factory)) { + throw new DependencyContainerWeakReferenceException(this._registerType); + } + + try { + return factory.Invoke(container, options.ConstructorParameters); + } catch(Exception ex) { + throw new DependencyContainerResolutionException(this._registerType, ex); + } + } + } + + /// + /// Stores an particular instance to return for a type. + /// + internal class InstanceFactory : ObjectFactoryBase, IDisposable { + private readonly Type _registerType; + private readonly Type _registerImplementation; + private readonly Object _instance; + + public InstanceFactory(Type registerType, Type registerImplementation, Object instance) { + if(!DependencyContainer.IsValidAssignment(registerType, registerImplementation)) { + throw new DependencyContainerRegistrationException(registerImplementation, "InstanceFactory", true); + } + + this._registerType = registerType; + this._registerImplementation = registerImplementation; + this._instance = instance; + } + + public override Boolean AssumeConstruction => true; + + public override Type CreatesType => this._registerImplementation; + + public override ObjectFactoryBase MultiInstanceVariant => new MultiInstanceFactory(this._registerType, this._registerImplementation); + + public override ObjectFactoryBase WeakReferenceVariant => new WeakInstanceFactory(this._registerType, this._registerImplementation, this._instance); + + public override ObjectFactoryBase StrongReferenceVariant => this; + + public override Object GetObject(Type requestedType, DependencyContainer container, DependencyContainerResolveOptions options) => this._instance; + + public void Dispose() { + IDisposable disposable = this._instance as IDisposable; + + disposable?.Dispose(); + } + } + + /// + /// Stores the instance with a weak reference. + /// + internal class WeakInstanceFactory : ObjectFactoryBase, IDisposable { + private readonly Type _registerType; + private readonly Type _registerImplementation; + private readonly WeakReference _instance; + + public WeakInstanceFactory(Type registerType, Type registerImplementation, Object instance) { + if(!DependencyContainer.IsValidAssignment(registerType, registerImplementation)) { + throw new DependencyContainerRegistrationException(registerImplementation, "WeakInstanceFactory", true); + } + + this._registerType = registerType; + this._registerImplementation = registerImplementation; + this._instance = new WeakReference(instance); + } + + public override Type CreatesType => this._registerImplementation; + + public override ObjectFactoryBase MultiInstanceVariant => new MultiInstanceFactory(this._registerType, this._registerImplementation); + + public override ObjectFactoryBase WeakReferenceVariant => this; + + public override ObjectFactoryBase StrongReferenceVariant { + get { + Object instance = this._instance.Target; + + if(instance == null) { + throw new DependencyContainerWeakReferenceException(this._registerType); + } + + return new InstanceFactory(this._registerType, this._registerImplementation, instance); + } + } + + public override Object GetObject(Type requestedType, DependencyContainer container, DependencyContainerResolveOptions options) { + Object instance = this._instance.Target; + + if(instance == null) { + throw new DependencyContainerWeakReferenceException(this._registerType); + } + + return instance; + } + + public void Dispose() => (this._instance.Target as IDisposable)?.Dispose(); + } + + /// + /// A factory that lazy instantiates a type and always returns the same instance. + /// + internal class SingletonFactory : ObjectFactoryBase, IDisposable { + private readonly Type _registerType; + private readonly Type _registerImplementation; + private readonly Object _singletonLock = new Object(); + private Object _current; + + public SingletonFactory(Type registerType, Type registerImplementation) { + if(registerImplementation.IsAbstract || registerImplementation.IsInterface) { + throw new DependencyContainerRegistrationException(registerImplementation, nameof(SingletonFactory), true); + } + + if(!DependencyContainer.IsValidAssignment(registerType, registerImplementation)) { + throw new DependencyContainerRegistrationException(registerImplementation, nameof(SingletonFactory), true); + } + + this._registerType = registerType; + this._registerImplementation = registerImplementation; + } + + public override Type CreatesType => this._registerImplementation; + + public override ObjectFactoryBase SingletonVariant => this; + + public override ObjectFactoryBase MultiInstanceVariant => + new MultiInstanceFactory(this._registerType, this._registerImplementation); + + public override Object GetObject( + Type requestedType, + DependencyContainer container, + DependencyContainerResolveOptions options) { + if(options.ConstructorParameters.Count != 0) { + throw new ArgumentException("Cannot specify parameters for singleton types"); + } + + lock(this._singletonLock) { + if(this._current == null) { + this._current = container.RegisteredTypes.ConstructType(this._registerImplementation, this.Constructor, options); + } + } + + return this._current; + } + + public override ObjectFactoryBase GetFactoryForChildContainer( + Type type, + DependencyContainer parent, + DependencyContainer child) { + // We make sure that the singleton is constructed before the child container takes the factory. + // Otherwise the results would vary depending on whether or not the parent container had resolved + // the type before the child container does. + _ = this.GetObject(type, parent, DependencyContainerResolveOptions.Default); + return this; + } + + public void Dispose() => (this._current as IDisposable)?.Dispose(); + } +} diff --git a/Swan.Tiny/DependencyInjection/RegisterOptions.cs b/Swan.Tiny/DependencyInjection/RegisterOptions.cs new file mode 100644 index 0000000..4c83ed2 --- /dev/null +++ b/Swan.Tiny/DependencyInjection/RegisterOptions.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Swan.DependencyInjection { + /// + /// Registration options for "fluent" API. + /// + public sealed class RegisterOptions { + private readonly TypesConcurrentDictionary _registeredTypes; + private readonly DependencyContainer.TypeRegistration _registration; + + /// + /// Initializes a new instance of the class. + /// + /// The registered types. + /// The registration. + public RegisterOptions(TypesConcurrentDictionary registeredTypes, DependencyContainer.TypeRegistration registration) { + this._registeredTypes = registeredTypes; + this._registration = registration; + } + + /// + /// Make registration a singleton (single instance) if possible. + /// + /// A registration options for fluent API. + /// Generic constraint registration exception. + public RegisterOptions AsSingleton() { + ObjectFactoryBase currentFactory = this._registeredTypes.GetCurrentFactory(this._registration); + + if(currentFactory == null) { + throw new DependencyContainerRegistrationException(this._registration.Type, "singleton"); + } + + return this._registeredTypes.AddUpdateRegistration(this._registration, currentFactory.SingletonVariant); + } + + /// + /// Make registration multi-instance if possible. + /// + /// A registration options for fluent API. + /// Generic constraint registration exception. + public RegisterOptions AsMultiInstance() { + ObjectFactoryBase currentFactory = this._registeredTypes.GetCurrentFactory(this._registration); + + if(currentFactory == null) { + throw new DependencyContainerRegistrationException(this._registration.Type, "multi-instance"); + } + + return this._registeredTypes.AddUpdateRegistration(this._registration, currentFactory.MultiInstanceVariant); + } + + /// + /// Make registration hold a weak reference if possible. + /// + /// A registration options for fluent API. + /// Generic constraint registration exception. + public RegisterOptions WithWeakReference() { + ObjectFactoryBase currentFactory = this._registeredTypes.GetCurrentFactory(this._registration); + + if(currentFactory == null) { + throw new DependencyContainerRegistrationException(this._registration.Type, "weak reference"); + } + + return this._registeredTypes.AddUpdateRegistration(this._registration, currentFactory.WeakReferenceVariant); + } + + /// + /// Make registration hold a strong reference if possible. + /// + /// A registration options for fluent API. + /// Generic constraint registration exception. + public RegisterOptions WithStrongReference() { + ObjectFactoryBase currentFactory = this._registeredTypes.GetCurrentFactory(this._registration); + + if(currentFactory == null) { + throw new DependencyContainerRegistrationException(this._registration.Type, "strong reference"); + } + + return this._registeredTypes.AddUpdateRegistration(this._registration, currentFactory.StrongReferenceVariant); + } + } + + /// + /// Registration options for "fluent" API when registering multiple implementations. + /// + public sealed class MultiRegisterOptions { + private IEnumerable _registerOptions; + + /// + /// Initializes a new instance of the class. + /// + /// The register options. + public MultiRegisterOptions(IEnumerable registerOptions) => this._registerOptions = registerOptions; + + /// + /// Make registration a singleton (single instance) if possible. + /// + /// A registration multi-instance for fluent API. + /// Generic Constraint Registration Exception. + public MultiRegisterOptions AsSingleton() { + this._registerOptions = this.ExecuteOnAllRegisterOptions(ro => ro.AsSingleton()); + return this; + } + + /// + /// Make registration multi-instance if possible. + /// + /// A registration multi-instance for fluent API. + /// Generic Constraint Registration Exception. + public MultiRegisterOptions AsMultiInstance() { + this._registerOptions = this.ExecuteOnAllRegisterOptions(ro => ro.AsMultiInstance()); + return this; + } + + private IEnumerable ExecuteOnAllRegisterOptions( + Func action) => this._registerOptions.Select(action).ToList(); + } +} \ No newline at end of file diff --git a/Swan.Tiny/DependencyInjection/TypeRegistration.cs b/Swan.Tiny/DependencyInjection/TypeRegistration.cs new file mode 100644 index 0000000..2af00c0 --- /dev/null +++ b/Swan.Tiny/DependencyInjection/TypeRegistration.cs @@ -0,0 +1,61 @@ +using System; + +namespace Swan.DependencyInjection { + public partial class DependencyContainer { + /// + /// Represents a Type Registration within the IoC Container. + /// + public sealed class TypeRegistration { + private readonly Int32 _hashCode; + + /// + /// Initializes a new instance of the class. + /// + /// The type. + /// The name. + public TypeRegistration(Type type, String name = null) { + this.Type = type; + this.Name = name ?? String.Empty; + + this._hashCode = String.Concat(this.Type.FullName, "|", this.Name).GetHashCode(); + } + + /// + /// Gets the type. + /// + /// + /// The type. + /// + public Type Type { + get; + } + + /// + /// Gets the name. + /// + /// + /// The name. + /// + public String Name { + get; + } + + /// + /// Determines whether the specified , is equal to this instance. + /// + /// The to compare with this instance. + /// + /// true if the specified is equal to this instance; otherwise, false. + /// + public override Boolean Equals(Object obj) => !(obj is TypeRegistration typeRegistration) || typeRegistration.Type != this.Type ? false : String.Compare(this.Name, typeRegistration.Name, StringComparison.Ordinal) == 0; + + /// + /// Returns a hash code for this instance. + /// + /// + /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. + /// + public override Int32 GetHashCode() => this._hashCode; + } + } +} \ No newline at end of file diff --git a/Swan.Tiny/DependencyInjection/TypesConcurrentDictionary.cs b/Swan.Tiny/DependencyInjection/TypesConcurrentDictionary.cs new file mode 100644 index 0000000..143ffb1 --- /dev/null +++ b/Swan.Tiny/DependencyInjection/TypesConcurrentDictionary.cs @@ -0,0 +1,265 @@ +#nullable enable +using System; +using System.Linq.Expressions; +using System.Reflection; +using System.Collections.Generic; +using System.Linq; +using System.Collections.Concurrent; + +namespace Swan.DependencyInjection { + /// + /// Represents a Concurrent Dictionary for TypeRegistration. + /// + public class TypesConcurrentDictionary : ConcurrentDictionary { + private static readonly ConcurrentDictionary ObjectConstructorCache = new ConcurrentDictionary(); + + private readonly DependencyContainer _dependencyContainer; + + internal TypesConcurrentDictionary(DependencyContainer dependencyContainer) => this._dependencyContainer = dependencyContainer; + + /// + /// Represents a delegate to build an object with the parameters. + /// + /// The parameters. + /// The built object. + public delegate Object ObjectConstructor(params Object?[] parameters); + + internal IEnumerable Resolve(Type resolveType, Boolean includeUnnamed) { + IEnumerable registrations = this.Keys.Where(tr => tr.Type == resolveType).Concat(this.GetParentRegistrationsForType(resolveType)).Distinct(); + + if(!includeUnnamed) { + registrations = registrations.Where(tr => !String.IsNullOrEmpty(tr.Name)); + } + + return registrations.Select(registration => this.ResolveInternal(registration, DependencyContainerResolveOptions.Default)); + } + + internal ObjectFactoryBase GetCurrentFactory(DependencyContainer.TypeRegistration registration) { + _ = this.TryGetValue(registration, out ObjectFactoryBase? current); + + return current!; + } + + internal RegisterOptions Register(Type registerType, String name, ObjectFactoryBase factory) => this.AddUpdateRegistration(new DependencyContainer.TypeRegistration(registerType, name), factory); + + internal RegisterOptions AddUpdateRegistration(DependencyContainer.TypeRegistration typeRegistration, ObjectFactoryBase factory) { + this[typeRegistration] = factory; + + return new RegisterOptions(this, typeRegistration); + } + + internal Boolean RemoveRegistration(DependencyContainer.TypeRegistration typeRegistration) => this.TryRemove(typeRegistration, out _); + + internal Object ResolveInternal(DependencyContainer.TypeRegistration registration, DependencyContainerResolveOptions? options = null) { + if(options == null) { + options = DependencyContainerResolveOptions.Default; + } + + // Attempt container resolution + if(this.TryGetValue(registration, out ObjectFactoryBase? factory)) { + try { + return factory.GetObject(registration.Type, this._dependencyContainer, options); + } catch(DependencyContainerResolutionException) { + throw; + } catch(Exception ex) { + throw new DependencyContainerResolutionException(registration.Type, ex); + } + } + + // Attempt to get a factory from parent if we can + ObjectFactoryBase? bubbledObjectFactory = this.GetParentObjectFactory(registration); + if(bubbledObjectFactory != null) { + try { + return bubbledObjectFactory.GetObject(registration.Type, this._dependencyContainer, options); + } catch(DependencyContainerResolutionException) { + throw; + } catch(Exception ex) { + throw new DependencyContainerResolutionException(registration.Type, ex); + } + } + + // Fail if requesting named resolution and settings set to fail if unresolved + if(!String.IsNullOrEmpty(registration.Name) && options.NamedResolutionFailureAction == DependencyContainerNamedResolutionFailureAction.Fail) { + throw new DependencyContainerResolutionException(registration.Type); + } + + // Attempted unnamed fallback container resolution if relevant and requested + if(!String.IsNullOrEmpty(registration.Name) && options.NamedResolutionFailureAction == DependencyContainerNamedResolutionFailureAction.AttemptUnnamedResolution) { + if(this.TryGetValue(new DependencyContainer.TypeRegistration(registration.Type, String.Empty), out factory)) { + try { + return factory.GetObject(registration.Type, this._dependencyContainer, options); + } catch(DependencyContainerResolutionException) { + throw; + } catch(Exception ex) { + throw new DependencyContainerResolutionException(registration.Type, ex); + } + } + } + + // Attempt unregistered construction if possible and requested + Boolean isValid = options.UnregisteredResolutionAction == DependencyContainerUnregisteredResolutionAction.AttemptResolve || registration.Type.IsGenericType && options.UnregisteredResolutionAction == DependencyContainerUnregisteredResolutionAction.GenericsOnly; + + return isValid && !registration.Type.IsAbstract && !registration.Type.IsInterface ? this.ConstructType(registration.Type, null, options) : throw new DependencyContainerResolutionException(registration.Type); + } + + internal Boolean CanResolve(DependencyContainer.TypeRegistration registration, DependencyContainerResolveOptions? options = null) { + if(options == null) { + options = DependencyContainerResolveOptions.Default; + } + + Type checkType = registration.Type; + String name = registration.Name; + + if(this.TryGetValue(new DependencyContainer.TypeRegistration(checkType, name), out ObjectFactoryBase? factory)) { + return factory.AssumeConstruction ? true : factory.Constructor == null ? this.GetBestConstructor(factory.CreatesType, options) != null : this.CanConstruct(factory.Constructor, options); + } + + // Fail if requesting named resolution and settings set to fail if unresolved + // Or bubble up if we have a parent + if(!String.IsNullOrEmpty(name) && options.NamedResolutionFailureAction == DependencyContainerNamedResolutionFailureAction.Fail) { + return this._dependencyContainer.Parent?.RegisteredTypes.CanResolve(registration, options.Clone()) ?? false; + } + + // Attempted unnamed fallback container resolution if relevant and requested + if(!String.IsNullOrEmpty(name) && options.NamedResolutionFailureAction == DependencyContainerNamedResolutionFailureAction.AttemptUnnamedResolution) { + if(this.TryGetValue(new DependencyContainer.TypeRegistration(checkType), out factory)) { + return factory.AssumeConstruction ? true : this.GetBestConstructor(factory.CreatesType, options) != null; + } + } + + // Check if type is an automatic lazy factory request or an IEnumerable + if(IsAutomaticLazyFactoryRequest(checkType) || registration.Type.IsIEnumerable()) { + return true; + } + + // Attempt unregistered construction if possible and requested + // If we cant', bubble if we have a parent + if(options.UnregisteredResolutionAction == DependencyContainerUnregisteredResolutionAction.AttemptResolve || checkType.IsGenericType && options.UnregisteredResolutionAction == DependencyContainerUnregisteredResolutionAction.GenericsOnly) { + return this.GetBestConstructor(checkType, options) != null || (this._dependencyContainer.Parent?.RegisteredTypes.CanResolve(registration, options.Clone()) ?? false); + } + + // Bubble resolution up the container tree if we have a parent + return this._dependencyContainer.Parent != null && this._dependencyContainer.Parent.RegisteredTypes.CanResolve(registration, options.Clone()); + } + + internal Object ConstructType(Type implementationType, ConstructorInfo? constructor, DependencyContainerResolveOptions? options = null) { + Type typeToConstruct = implementationType; + + if(constructor == null) { + // Try and get the best constructor that we can construct + // if we can't construct any then get the constructor + // with the least number of parameters so we can throw a meaningful + // resolve exception + constructor = this.GetBestConstructor(typeToConstruct, options) ?? GetTypeConstructors(typeToConstruct).LastOrDefault(); + } + + if(constructor == null) { + throw new DependencyContainerResolutionException(typeToConstruct); + } + + ParameterInfo[] ctorParams = constructor.GetParameters(); + Object?[] args = new Object?[ctorParams.Length]; + + for(Int32 parameterIndex = 0; parameterIndex < ctorParams.Length; parameterIndex++) { + ParameterInfo currentParam = ctorParams[parameterIndex]; + + try { + args[parameterIndex] = options?.ConstructorParameters.GetValueOrDefault(currentParam.Name, this.ResolveInternal(new DependencyContainer.TypeRegistration(currentParam.ParameterType), options.Clone())); + } catch(DependencyContainerResolutionException ex) { + // If a constructor parameter can't be resolved + // it will throw, so wrap it and throw that this can't + // be resolved. + throw new DependencyContainerResolutionException(typeToConstruct, ex); + } catch(Exception ex) { + throw new DependencyContainerResolutionException(typeToConstruct, ex); + } + } + + try { + return CreateObjectConstructionDelegateWithCache(constructor).Invoke(args); + } catch(Exception ex) { + throw new DependencyContainerResolutionException(typeToConstruct, ex); + } + } + + private static ObjectConstructor CreateObjectConstructionDelegateWithCache(ConstructorInfo constructor) { + if(ObjectConstructorCache.TryGetValue(constructor, out ObjectConstructor? objectConstructor)) { + return objectConstructor; + } + + // We could lock the cache here, but there's no real side + // effect to two threads creating the same ObjectConstructor + // at the same time, compared to the cost of a lock for + // every creation. + ParameterInfo[] constructorParams = constructor.GetParameters(); + ParameterExpression lambdaParams = Expression.Parameter(typeof(Object[]), "parameters"); + Expression[] newParams = new Expression[constructorParams.Length]; + + for(Int32 i = 0; i < constructorParams.Length; i++) { + BinaryExpression paramsParameter = Expression.ArrayIndex(lambdaParams, Expression.Constant(i)); + + newParams[i] = Expression.Convert(paramsParameter, constructorParams[i].ParameterType); + } + + NewExpression newExpression = Expression.New(constructor, newParams); + + LambdaExpression constructionLambda = Expression.Lambda(typeof(ObjectConstructor), newExpression, lambdaParams); + + objectConstructor = (ObjectConstructor)constructionLambda.Compile(); + + ObjectConstructorCache[constructor] = objectConstructor; + return objectConstructor; + } + + private static IEnumerable GetTypeConstructors(Type type) => type.GetConstructors().OrderByDescending(ctor => ctor.GetParameters().Length); + + private static Boolean IsAutomaticLazyFactoryRequest(Type type) { + if(!type.IsGenericType) { + return false; + } + + Type genericType = type.GetGenericTypeDefinition(); + + // Just a func + if(genericType == typeof(Func<>)) { + return true; + } + + // 2 parameter func with string as first parameter (name) + if(genericType == typeof(Func<,>) && type.GetGenericArguments()[0] == typeof(String)) { + return true; + } + + // 3 parameter func with string as first parameter (name) and IDictionary as second (parameters) + return genericType == typeof(Func<,,>) && type.GetGenericArguments()[0] == typeof(String) && type.GetGenericArguments()[1] == typeof(IDictionary); + } + + private ObjectFactoryBase? GetParentObjectFactory(DependencyContainer.TypeRegistration registration) => this._dependencyContainer.Parent == null + ? null + : this._dependencyContainer.Parent.RegisteredTypes.TryGetValue(registration, out ObjectFactoryBase? factory) ? factory.GetFactoryForChildContainer(registration.Type, this._dependencyContainer.Parent, this._dependencyContainer) : this._dependencyContainer.Parent.RegisteredTypes.GetParentObjectFactory(registration); + + private ConstructorInfo? GetBestConstructor(Type type, DependencyContainerResolveOptions? options) => type.IsValueType ? null : GetTypeConstructors(type).FirstOrDefault(ctor => this.CanConstruct(ctor, options)); + + private Boolean CanConstruct(MethodBase ctor, DependencyContainerResolveOptions? options) { + foreach(ParameterInfo parameter in ctor.GetParameters()) { + if(String.IsNullOrEmpty(parameter.Name)) { + return false; + } + + Boolean isParameterOverload = options!.ConstructorParameters.ContainsKey(parameter.Name); + + if(parameter.ParameterType.IsPrimitive && !isParameterOverload) { + return false; + } + + if(!isParameterOverload && !this.CanResolve(new DependencyContainer.TypeRegistration(parameter.ParameterType), options.Clone())) { + return false; + } + } + + return true; + } + + private IEnumerable GetParentRegistrationsForType(Type resolveType) => this._dependencyContainer.Parent == null ? Array.Empty() : this._dependencyContainer.Parent.RegisteredTypes.Keys.Where(tr => tr.Type == resolveType).Concat(this._dependencyContainer.Parent.RegisteredTypes.GetParentRegistrationsForType(resolveType)); + } +} diff --git a/Swan.Tiny/Diagnostics/HighResolutionTimer.cs b/Swan.Tiny/Diagnostics/HighResolutionTimer.cs new file mode 100644 index 0000000..ad96c40 --- /dev/null +++ b/Swan.Tiny/Diagnostics/HighResolutionTimer.cs @@ -0,0 +1,30 @@ +using System; +using System.Diagnostics; + +namespace Swan.Diagnostics { + /// + /// Provides access to a high-resolution, time measuring device. + /// + /// + public class HighResolutionTimer : Stopwatch { + /// + /// Initializes a new instance of the class. + /// + /// High-resolution timer not available. + public HighResolutionTimer() { + if(!IsHighResolution) { + throw new NotSupportedException("High-resolution timer not available"); + } + } + + /// + /// Gets the number of microseconds per timer tick. + /// + public static Double MicrosecondsPerTick { get; } = 1000000d / Frequency; + + /// + /// Gets the elapsed microseconds. + /// + public Int64 ElapsedMicroseconds => (Int64)(this.ElapsedTicks * MicrosecondsPerTick); + } +} diff --git a/Swan.Tiny/Enums.cs b/Swan.Tiny/Enums.cs new file mode 100644 index 0000000..a586509 --- /dev/null +++ b/Swan.Tiny/Enums.cs @@ -0,0 +1,61 @@ +namespace Swan { + /// + /// Enumeration of Operating Systems. + /// + public enum OperatingSystem { + /// + /// Unknown OS + /// + Unknown, + + /// + /// Windows + /// + Windows, + + /// + /// UNIX/Linux + /// + Unix, + + /// + /// macOS (OSX) + /// + Osx, + } + + /// + /// Defines Endianness, big or little. + /// + public enum Endianness { + /// + /// In big endian, you store the most significant byte in the smallest address. + /// + Big, + + /// + /// In little endian, you store the least significant byte in the smallest address. + /// + Little, + } + + /// + /// Enumerates the JSON serializer cases to use: None (keeps the same case), PascalCase, or camelCase. + /// + public enum JsonSerializerCase { + /// + /// The none + /// + None, + + /// + /// The pascal case (eg. PascalCase) + /// + PascalCase, + + /// + /// The camel case (eg. camelCase) + /// + CamelCase, + } +} diff --git a/Swan.Tiny/Extensions.ByteArrays.cs b/Swan.Tiny/Extensions.ByteArrays.cs new file mode 100644 index 0000000..5abc946 --- /dev/null +++ b/Swan.Tiny/Extensions.ByteArrays.cs @@ -0,0 +1,498 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Swan { + /// + /// Provides various extension methods for byte arrays and streams. + /// + public static class ByteArrayExtensions { + /// + /// Converts an array of bytes to its lower-case, hexadecimal representation. + /// + /// The bytes. + /// if set to true add the 0x prefix tot he output. + /// + /// The specified string instance; no actual conversion is performed. + /// + /// bytes. + public static String ToLowerHex(this Byte[] bytes, Boolean addPrefix = false) => ToHex(bytes, addPrefix, "x2"); + + /// + /// Converts an array of bytes to its upper-case, hexadecimal representation. + /// + /// The bytes. + /// if set to true [add prefix]. + /// + /// The specified string instance; no actual conversion is performed. + /// + /// bytes. + public static String ToUpperHex(this Byte[] bytes, Boolean addPrefix = false) => ToHex(bytes, addPrefix, "X2"); + + /// + /// Converts an array of bytes to a sequence of dash-separated, hexadecimal, + /// uppercase characters. + /// + /// The bytes. + /// + /// A string of hexadecimal pairs separated by hyphens, where each pair represents + /// the corresponding element in value; for example, "7F-2C-4A-00". + /// + public static String ToDashedHex(this Byte[] bytes) => BitConverter.ToString(bytes); + + /// + /// Converts an array of bytes to a base-64 encoded string. + /// + /// The bytes. + /// A converted from an array of bytes. + public static String ToBase64(this Byte[] bytes) => Convert.ToBase64String(bytes); + + /// + /// Converts a set of hexadecimal characters (uppercase or lowercase) + /// to a byte array. String length must be a multiple of 2 and + /// any prefix (such as 0x) has to be avoided for this to work properly. + /// + /// The hexadecimal. + /// + /// A byte array containing the results of encoding the specified set of characters. + /// + /// hex. + public static Byte[] ConvertHexadecimalToBytes(this String @this) { + if(String.IsNullOrWhiteSpace(@this)) { + throw new ArgumentNullException(nameof(@this)); + } + + return Enumerable.Range(0, @this.Length / 2).Select(x => Convert.ToByte(@this.Substring(x * 2, 2), 16)).ToArray(); + } + + /// + /// Gets the bit value at the given offset. + /// + /// The b. + /// The offset. + /// The length. + /// + /// Bit value at the given offset. + /// + public static Byte GetBitValueAt(this Byte @this, Byte offset, Byte length = 1) => (Byte)((@this >> offset) & ~(0xff << length)); + + /// + /// Sets the bit value at the given offset. + /// + /// The b. + /// The offset. + /// The length. + /// The value. + /// Bit value at the given offset. + public static Byte SetBitValueAt(this Byte @this, Byte offset, Byte length, Byte value) { + Int32 mask = ~(0xff << length); + Byte valueAt = (Byte)(value & mask); + + return (Byte)((valueAt << offset) | (@this & ~(mask << offset))); + } + + /// + /// Sets the bit value at the given offset. + /// + /// The b. + /// The offset. + /// The value. + /// Bit value at the given offset. + public static Byte SetBitValueAt(this Byte @this, Byte offset, Byte value) => @this.SetBitValueAt(offset, 1, value); + + /// + /// Splits a byte array delimited by the specified sequence of bytes. + /// Each individual element in the result will contain the split sequence terminator if it is found to be delimited by it. + /// For example if you split [1,2,3,4] by a sequence of [2,3] this method will return a list with 2 byte arrays, one containing [1,2,3] and the + /// second one containing 4. Use the Trim extension methods to remove terminator sequences. + /// + /// The buffer. + /// The offset at which to start splitting bytes. Any bytes before this will be discarded. + /// The sequence. + /// + /// A byte array containing the results the specified sequence of bytes. + /// + /// + /// buffer + /// or + /// sequence. + /// + public static List Split(this Byte[] @this, Int32 offset, params Byte[] sequence) { + if(@this == null) { + throw new ArgumentNullException(nameof(@this)); + } + + if(sequence == null) { + throw new ArgumentNullException(nameof(sequence)); + } + + Int32 seqOffset = offset.Clamp(0, @this.Length - 1); + + List result = new List(); + + while(seqOffset < @this.Length) { + Int32 separatorStartIndex = @this.GetIndexOf(sequence, seqOffset); + + if(separatorStartIndex >= 0) { + Byte[] item = new Byte[separatorStartIndex - seqOffset + sequence.Length]; + Array.Copy(@this, seqOffset, item, 0, item.Length); + result.Add(item); + seqOffset += item.Length; + } else { + Byte[] item = new Byte[@this.Length - seqOffset]; + Array.Copy(@this, seqOffset, item, 0, item.Length); + result.Add(item); + break; + } + } + + return result; + } + + /// + /// Clones the specified buffer, byte by byte. + /// + /// The buffer. + /// + /// A byte array containing the results of encoding the specified set of characters. + /// + /// this + public static Byte[] DeepClone(this Byte[] @this) { + if(@this == null) { + throw new ArgumentNullException(nameof(@this)); + } + + Byte[] result = new Byte[@this.Length]; + Array.Copy(@this, result, @this.Length); + return result; + } + + /// + /// Removes the specified sequence from the start of the buffer if the buffer begins with such sequence. + /// + /// The buffer. + /// The sequence. + /// + /// A new trimmed byte array. + /// + /// buffer. + public static Byte[] TrimStart(this Byte[] buffer, params Byte[] sequence) { + if(buffer == null) { + throw new ArgumentNullException(nameof(buffer)); + } + + if(buffer.StartsWith(sequence) == false) { + return buffer.DeepClone(); + } + + Byte[] result = new Byte[buffer.Length - sequence.Length]; + Array.Copy(buffer, sequence.Length, result, 0, result.Length); + return result; + } + + /// + /// Removes the specified sequence from the end of the buffer if the buffer ends with such sequence. + /// + /// The buffer. + /// The sequence. + /// + /// A byte array containing the results of encoding the specified set of characters. + /// + /// buffer. + public static Byte[] TrimEnd(this Byte[] buffer, params Byte[] sequence) { + if(buffer == null) { + throw new ArgumentNullException(nameof(buffer)); + } + + if(buffer.EndsWith(sequence) == false) { + return buffer.DeepClone(); + } + + Byte[] result = new Byte[buffer.Length - sequence.Length]; + Array.Copy(buffer, 0, result, 0, result.Length); + return result; + } + + /// + /// Removes the specified sequence from the end and the start of the buffer + /// if the buffer ends and/or starts with such sequence. + /// + /// The buffer. + /// The sequence. + /// A byte array containing the results of encoding the specified set of characters. + public static Byte[] Trim(this Byte[] buffer, params Byte[] sequence) { + Byte[] trimStart = buffer.TrimStart(sequence); + return trimStart.TrimEnd(sequence); + } + + /// + /// Determines if the specified buffer ends with the given sequence of bytes. + /// + /// The buffer. + /// The sequence. + /// + /// True if the specified buffer is ends; otherwise, false. + /// + /// buffer. + public static Boolean EndsWith(this Byte[] buffer, params Byte[] sequence) { + if(buffer == null) { + throw new ArgumentNullException(nameof(buffer)); + } + + Int32 startIndex = buffer.Length - sequence.Length; + return buffer.GetIndexOf(sequence, startIndex) == startIndex; + } + + /// + /// Determines if the specified buffer starts with the given sequence of bytes. + /// + /// The buffer. + /// The sequence. + /// true if the specified buffer starts; otherwise, false. + public static Boolean StartsWith(this Byte[] buffer, params Byte[] sequence) => buffer.GetIndexOf(sequence) == 0; + + /// + /// Determines whether the buffer contains the specified sequence. + /// + /// The buffer. + /// The sequence. + /// + /// true if [contains] [the specified sequence]; otherwise, false. + /// + public static Boolean Contains(this Byte[] buffer, params Byte[] sequence) => buffer.GetIndexOf(sequence) >= 0; + + /// + /// Determines whether the buffer exactly matches, byte by byte the specified sequence. + /// + /// The buffer. + /// The sequence. + /// + /// true if [is equal to] [the specified sequence]; otherwise, false. + /// + /// buffer. + public static Boolean IsEqualTo(this Byte[] buffer, params Byte[] sequence) { + if(ReferenceEquals(buffer, sequence)) { + return true; + } + + if(buffer == null) { + throw new ArgumentNullException(nameof(buffer)); + } + + return buffer.Length == sequence.Length && buffer.GetIndexOf(sequence) == 0; + } + + /// + /// Returns the first instance of the matched sequence based on the given offset. + /// If no matches are found then this method returns -1. + /// + /// The buffer. + /// The sequence. + /// The offset. + /// The index of the sequence. + /// + /// buffer + /// or + /// sequence. + /// + public static Int32 GetIndexOf(this Byte[] buffer, Byte[] sequence, Int32 offset = 0) { + if(buffer == null) { + throw new ArgumentNullException(nameof(buffer)); + } + + if(sequence == null) { + throw new ArgumentNullException(nameof(sequence)); + } + + if(sequence.Length == 0) { + return -1; + } + + if(sequence.Length > buffer.Length) { + return -1; + } + + Int32 seqOffset = offset < 0 ? 0 : offset; + + Int32 matchedCount = 0; + for(Int32 i = seqOffset; i < buffer.Length; i++) { + if(buffer[i] == sequence[matchedCount]) { + matchedCount++; + } else { + matchedCount = 0; + } + + if(matchedCount == sequence.Length) { + return i - (matchedCount - 1); + } + } + + return -1; + } + + /// + /// Appends the Memory Stream with the specified buffer. + /// + /// The stream. + /// The buffer. + /// + /// The same MemoryStream instance. + /// + /// + /// stream + /// or + /// buffer. + /// + public static MemoryStream Append(this MemoryStream stream, Byte[] buffer) { + if(stream == null) { + throw new ArgumentNullException(nameof(stream)); + } + + if(buffer == null) { + throw new ArgumentNullException(nameof(buffer)); + } + + stream.Write(buffer, 0, buffer.Length); + return stream; + } + + /// + /// Appends the Memory Stream with the specified buffer. + /// + /// The stream. + /// The buffer. + /// + /// Block of bytes to the current stream using data read from a buffer. + /// + /// buffer. + public static MemoryStream Append(this MemoryStream stream, IEnumerable buffer) => Append(stream, buffer?.ToArray()); + + /// + /// Appends the Memory Stream with the specified set of buffers. + /// + /// The stream. + /// The buffers. + /// + /// Block of bytes to the current stream using data read from a buffer. + /// + /// buffers. + public static MemoryStream Append(this MemoryStream stream, IEnumerable buffers) { + if(buffers == null) { + throw new ArgumentNullException(nameof(buffers)); + } + + foreach(Byte[] buffer in buffers) { + _ = Append(stream, buffer); + } + + return stream; + } + + /// + /// Converts an array of bytes into text with the specified encoding. + /// + /// The buffer. + /// The encoding. + /// A that contains the results of decoding the specified sequence of bytes. + public static String ToText(this IEnumerable buffer, Encoding encoding) => encoding.GetString(buffer.ToArray()); + + /// + /// Converts an array of bytes into text with UTF8 encoding. + /// + /// The buffer. + /// A that contains the results of decoding the specified sequence of bytes. + public static String ToText(this IEnumerable buffer) => buffer.ToText(Encoding.UTF8); + + /// + /// Reads the bytes asynchronous. + /// + /// The stream. + /// The length. + /// Length of the buffer. + /// The cancellation token. + /// + /// A byte array containing the results of encoding the specified set of characters. + /// + /// stream. + public static async Task ReadBytesAsync(this Stream stream, Int64 length, Int32 bufferLength, CancellationToken cancellationToken = default) { + if(stream == null) { + throw new ArgumentNullException(nameof(stream)); + } + + using MemoryStream dest = new MemoryStream(); + try { + Byte[] buff = new Byte[bufferLength]; + while(length > 0) { + if(length < bufferLength) { + bufferLength = (Int32)length; + } + + Int32 nread = await stream.ReadAsync(buff, 0, bufferLength, cancellationToken).ConfigureAwait(false); + if(nread == 0) { + break; + } + + dest.Write(buff, 0, nread); + length -= nread; + } + } catch { + // ignored + } + + return dest.ToArray(); + } + + /// + /// Reads the bytes asynchronous. + /// + /// The stream. + /// The length. + /// The cancellation token. + /// + /// A byte array containing the results of encoding the specified set of characters. + /// + /// stream. + public static async Task ReadBytesAsync(this Stream stream, Int32 length, CancellationToken cancellationToken = default) { + if(stream == null) { + throw new ArgumentNullException(nameof(stream)); + } + + Byte[] buff = new Byte[length]; + Int32 offset = 0; + try { + while(length > 0) { + Int32 nread = await stream.ReadAsync(buff, offset, length, cancellationToken).ConfigureAwait(false); + if(nread == 0) { + break; + } + + offset += nread; + length -= nread; + } + } catch { + // ignored + } + + return new ArraySegment(buff, 0, offset).ToArray(); + } + + private static String ToHex(Byte[] bytes, Boolean addPrefix, String format) { + if(bytes == null) { + throw new ArgumentNullException(nameof(bytes)); + } + + StringBuilder sb = new StringBuilder(bytes.Length * 2); + + foreach(Byte item in bytes) { + _ = sb.Append(item.ToString(format, CultureInfo.InvariantCulture)); + } + + return $"{(addPrefix ? "0x" : String.Empty)}{sb}"; + } + } +} diff --git a/Swan.Tiny/Extensions.Dictionaries.cs b/Swan.Tiny/Extensions.Dictionaries.cs new file mode 100644 index 0000000..273e9e8 --- /dev/null +++ b/Swan.Tiny/Extensions.Dictionaries.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; + +namespace Swan { + /// + /// Extension methods. + /// + public static partial class Extensions { + /// + /// Gets the value if exists or default. + /// + /// The type of the key. + /// The type of the value. + /// The dictionary. + /// The key. + /// The default value. + /// + /// The value of the provided key or default. + /// + /// dict. + public static TValue GetValueOrDefault(this IDictionary dict, TKey key, TValue defaultValue = default) { + if(dict == null) { + throw new ArgumentNullException(nameof(dict)); + } + + return dict.ContainsKey(key) ? dict[key] : defaultValue; + } + + /// + /// Adds a key/value pair to the Dictionary if the key does not already exist. + /// If the value is null, the key will not be updated. + /// Based on ConcurrentDictionary.GetOrAdd method. + /// + /// The type of the key. + /// The type of the value. + /// The dictionary. + /// The key. + /// The value factory. + /// + /// The value for the key. + /// + /// + /// dict + /// or + /// valueFactory. + /// + public static TValue GetOrAdd(this IDictionary dict, TKey key, Func valueFactory) { + if(dict == null) { + throw new ArgumentNullException(nameof(dict)); + } + + if(valueFactory == null) { + throw new ArgumentNullException(nameof(valueFactory)); + } + + if(!dict.ContainsKey(key)) { + TValue value = valueFactory(key); + if(Equals(value, default)) { + return default; + } + + dict[key] = value; + } + + return dict[key]; + } + + /// + /// Executes the item action for each element in the Dictionary. + /// + /// The type of the key. + /// The type of the value. + /// The dictionary. + /// The item action. + /// dict. + public static void ForEach(this IDictionary dict, Action itemAction) { + if(dict == null) { + throw new ArgumentNullException(nameof(dict)); + } + + foreach(KeyValuePair kvp in dict) { + itemAction(kvp.Key, kvp.Value); + } + } + } +} \ No newline at end of file diff --git a/Swan.Tiny/Extensions.Functional.cs b/Swan.Tiny/Extensions.Functional.cs new file mode 100644 index 0000000..e6d8491 --- /dev/null +++ b/Swan.Tiny/Extensions.Functional.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Swan { + /// + /// Functional programming extension methods. + /// + public static class FunctionalExtensions { + /// + /// Whens the specified condition. + /// + /// The type of IQueryable. + /// The list. + /// The condition. + /// The function. + /// + /// The IQueryable. + /// + /// + /// this + /// or + /// condition + /// or + /// fn. + /// + public static IQueryable When(this IQueryable list, Func condition, Func, IQueryable> fn) { + if(list == null) { + throw new ArgumentNullException(nameof(list)); + } + + if(condition == null) { + throw new ArgumentNullException(nameof(condition)); + } + + if(fn == null) { + throw new ArgumentNullException(nameof(fn)); + } + + return condition() ? fn(list) : list; + } + + /// + /// Whens the specified condition. + /// + /// The type of IEnumerable. + /// The list. + /// The condition. + /// The function. + /// + /// The IEnumerable. + /// + /// + /// this + /// or + /// condition + /// or + /// fn. + /// + public static IEnumerable When(this IEnumerable list, Func condition, Func, IEnumerable> fn) { + if(list == null) { + throw new ArgumentNullException(nameof(list)); + } + + if(condition == null) { + throw new ArgumentNullException(nameof(condition)); + } + + if(fn == null) { + throw new ArgumentNullException(nameof(fn)); + } + + return condition() ? fn(list) : list; + } + + /// + /// Adds the value when the condition is true. + /// + /// The type of IList element. + /// The list. + /// The condition. + /// The value. + /// + /// The IList. + /// + /// + /// this + /// or + /// condition + /// or + /// value. + /// + public static IList AddWhen(this IList list, Func condition, Func value) { + if(list == null) { + throw new ArgumentNullException(nameof(list)); + } + + if(condition == null) { + throw new ArgumentNullException(nameof(condition)); + } + + if(value == null) { + throw new ArgumentNullException(nameof(value)); + } + + if(condition()) { + list.Add(value()); + } + + return list; + } + + /// + /// Adds the value when the condition is true. + /// + /// The type of IList element. + /// The list. + /// if set to true [condition]. + /// The value. + /// + /// The IList. + /// + /// list. + public static IList AddWhen(this IList list, Boolean condition, T value) { + if(list == null) { + throw new ArgumentNullException(nameof(list)); + } + + if(condition) { + list.Add(value); + } + + return list; + } + + /// + /// Adds the range when the condition is true. + /// + /// The type of List element. + /// The list. + /// The condition. + /// The value. + /// + /// The List. + /// + /// + /// this + /// or + /// condition + /// or + /// value. + /// + public static List AddRangeWhen(this List list, Func condition, Func> value) { + if(list == null) { + throw new ArgumentNullException(nameof(list)); + } + + if(condition == null) { + throw new ArgumentNullException(nameof(condition)); + } + + if(value == null) { + throw new ArgumentNullException(nameof(value)); + } + + if(condition()) { + list.AddRange(value()); + } + + return list; + } + } +} \ No newline at end of file diff --git a/Swan.Tiny/Extensions.Reflection.cs b/Swan.Tiny/Extensions.Reflection.cs new file mode 100644 index 0000000..8953aae --- /dev/null +++ b/Swan.Tiny/Extensions.Reflection.cs @@ -0,0 +1,411 @@ +#nullable enable +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; + +using Swan.Configuration; +using Swan.Reflection; + +namespace Swan { + /// + /// Provides various extension methods for Reflection and Types. + /// + public static class ReflectionExtensions { + private static readonly Lazy, Func>> CacheGetMethods = new Lazy, Func>>(() => new ConcurrentDictionary, Func>(), true); + + private static readonly Lazy, Action>> CacheSetMethods = new Lazy, Action>>(() => new ConcurrentDictionary, Action>(), true); + + #region Assembly Extensions + + /// + /// Gets all types within an assembly in a safe manner. + /// + /// The assembly. + /// + /// Array of Type objects representing the types specified by an assembly. + /// + /// assembly. + public static IEnumerable GetAllTypes(this Assembly assembly) { + if(assembly == null) { + throw new ArgumentNullException(nameof(assembly)); + } + + try { + return assembly.GetTypes(); + } catch(ReflectionTypeLoadException e) { + return e.Types.Where(t => t != null); + } + } + + #endregion + + #region Type Extensions + + /// + /// The closest programmatic equivalent of default(T). + /// + /// The type. + /// + /// Default value of this type. + /// + /// type. + public static Object? GetDefault(this Type type) { + if(type == null) { + throw new ArgumentNullException(nameof(type)); + } + + return type.IsValueType ? Activator.CreateInstance(type) : default; + } + + /// + /// Determines whether this type is compatible with ICollection. + /// + /// The type. + /// + /// true if the specified source type is collection; otherwise, false. + /// + /// sourceType. + public static Boolean IsCollection(this Type sourceType) { + if(sourceType == null) { + throw new ArgumentNullException(nameof(sourceType)); + } + + return sourceType != typeof(String) && typeof(IEnumerable).IsAssignableFrom(sourceType); + } + + /// + /// Gets a method from a type given the method name, binding flags, generic types and parameter types. + /// + /// Type of the source. + /// The binding flags. + /// Name of the method. + /// The generic types. + /// The parameter types. + /// + /// An object that represents the method with the specified name. + /// + /// + /// The exception that is thrown when binding to a member results in more than one member matching the + /// binding criteria. This class cannot be inherited. + /// + public static MethodInfo GetMethod(this Type type, BindingFlags bindingFlags, String methodName, Type[] genericTypes, Type[] parameterTypes) { + if(type == null) { + throw new ArgumentNullException(nameof(type)); + } + + if(methodName == null) { + throw new ArgumentNullException(nameof(methodName)); + } + + if(genericTypes == null) { + throw new ArgumentNullException(nameof(genericTypes)); + } + + if(parameterTypes == null) { + throw new ArgumentNullException(nameof(parameterTypes)); + } + + List methods = type.GetMethods(bindingFlags) + .Where(mi => String.Equals(methodName, mi.Name, StringComparison.Ordinal)) + .Where(mi => mi.ContainsGenericParameters) + .Where(mi => mi.GetGenericArguments().Length == genericTypes.Length) + .Where(mi => mi.GetParameters().Length == parameterTypes.Length).Select(mi => mi.MakeGenericMethod(genericTypes)) + .Where(mi => mi.GetParameters().Select(pi => pi.ParameterType).SequenceEqual(parameterTypes)).ToList(); + + return methods.Count > 1 ? throw new AmbiguousMatchException() : methods.FirstOrDefault(); + } + + /// + /// Determines whether [is i enumerable request]. + /// + /// The type. + /// + /// true if [is i enumerable request] [the specified type]; otherwise, false. + /// + /// type. + public static Boolean IsIEnumerable(this Type type) => type == null ? throw new ArgumentNullException(nameof(type)) : type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>); + + #endregion + + /// + /// Tries to parse using the basic types. + /// + /// The type. + /// The value. + /// The result. + /// + /// true if parsing was successful; otherwise, false. + /// + /// type + public static Boolean TryParseBasicType(this Type type, Object value, out Object? result) { + if(type == null) { + throw new ArgumentNullException(nameof(type)); + } + + if(type != typeof(Boolean)) { + return TryParseBasicType(type, value.ToStringInvariant(), out result); + } + + result = value.ToBoolean(); + return true; + } + + /// + /// Tries to parse using the basic types. + /// + /// The type. + /// The value. + /// The result. + /// + /// true if parsing was successful; otherwise, false. + /// + /// type + public static Boolean TryParseBasicType(this Type type, String value, out Object? result) { + if(type == null) { + throw new ArgumentNullException(nameof(type)); + } + + result = null; + + return Definitions.BasicTypesInfo.Value.ContainsKey(type) && Definitions.BasicTypesInfo.Value[type].TryParse(value, out result); + } + + /// + /// Tries the type of the set basic value to a property. + /// + /// The property information. + /// The value. + /// The object. + /// + /// true if parsing was successful; otherwise, false. + /// + /// propertyInfo. + public static Boolean TrySetBasicType(this PropertyInfo propertyInfo, Object value, Object target) { + if(propertyInfo == null) { + throw new ArgumentNullException(nameof(propertyInfo)); + } + + try { + if(propertyInfo.PropertyType.TryParseBasicType(value, out Object? propertyValue)) { + propertyInfo.SetValue(target, propertyValue); + return true; + } + } catch { + // swallow + } + + return false; + } + + /// + /// Tries the type of the set to an array a basic type. + /// + /// The type. + /// The value. + /// The array. + /// The index. + /// + /// true if parsing was successful; otherwise, false. + /// + /// type + public static Boolean TrySetArrayBasicType(this Type type, Object value, Array target, Int32 index) { + if(type == null) { + throw new ArgumentNullException(nameof(type)); + } + + if(target == null) { + return false; + } + + try { + if(value == null) { + target.SetValue(null, index); + return true; + } + + if(type.TryParseBasicType(value, out Object? propertyValue)) { + target.SetValue(propertyValue, index); + return true; + } + + if(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) { + target.SetValue(null, index); + return true; + } + } catch { + // swallow + } + + return false; + } + + /// + /// Tries to set a property array with another array. + /// + /// The property. + /// The value. + /// The object. + /// + /// true if parsing was successful; otherwise, false. + /// + /// propertyInfo. + public static Boolean TrySetArray(this PropertyInfo propertyInfo, IEnumerable? value, Object obj) { + if(propertyInfo == null) { + throw new ArgumentNullException(nameof(propertyInfo)); + } + + Type? elementType = propertyInfo.PropertyType.GetElementType(); + + if(elementType == null || value == null) { + return false; + } + + Array targetArray = Array.CreateInstance(elementType, value.Count()); + + Int32 i = 0; + + foreach(Object sourceElement in value) { + Boolean result = elementType.TrySetArrayBasicType(sourceElement, targetArray, i++); + + if(!result) { + return false; + } + } + + propertyInfo.SetValue(obj, targetArray); + + return true; + } + + /// + /// Gets property actual value or PropertyDisplayAttribute.DefaultValue if presented. + /// + /// If the PropertyDisplayAttribute.Format value is presented, the property value + /// will be formatted accordingly. + /// + /// If the object contains a null value, a empty string will be returned. + /// + /// The property information. + /// The object. + /// The property value or null. + /// propertyInfo. + public static String? ToFormattedString(this PropertyInfo propertyInfo, Object target) { + if(propertyInfo == null) { + throw new ArgumentNullException(nameof(propertyInfo)); + } + + try { + Object? value = propertyInfo.GetValue(target); + PropertyDisplayAttribute attr = AttributeCache.DefaultCache.Value.RetrieveOne(propertyInfo); + + if(attr == null) { + return value?.ToString() ?? String.Empty; + } + + Object valueToFormat = value ?? attr.DefaultValue; + + return String.IsNullOrEmpty(attr.Format) ? (valueToFormat?.ToString() ?? String.Empty) : ConvertObjectAndFormat(propertyInfo.PropertyType, valueToFormat, attr.Format); + } catch { + return null; + } + } + + /// + /// Gets a MethodInfo from a Property Get method. + /// + /// The property information. + /// if set to true [non public]. + /// + /// The cached MethodInfo. + /// + public static Func? GetCacheGetMethod(this PropertyInfo propertyInfo, Boolean nonPublic = false) { + Tuple key = Tuple.Create(!nonPublic, propertyInfo); + + // TODO: Fix public logic + return !nonPublic && !CacheGetMethods.Value.ContainsKey(key) && !propertyInfo.GetGetMethod(true)!.IsPublic ? null : CacheGetMethods.Value.GetOrAdd(key, x => y => x.Item2.GetGetMethod(nonPublic)?.Invoke(y, null)!); + //y => x => y.Item2.CreatePropertyProxy().GetValue(x)); + } + + /// + /// Gets a MethodInfo from a Property Set method. + /// + /// The property information. + /// if set to true [non public]. + /// + /// The cached MethodInfo. + /// + public static Action? GetCacheSetMethod(this PropertyInfo propertyInfo, Boolean nonPublic = false) { + Tuple key = Tuple.Create(!nonPublic, propertyInfo); + + return !nonPublic && !CacheSetMethods.Value.ContainsKey(key) && !propertyInfo.GetSetMethod(true)!.IsPublic ? null : CacheSetMethods.Value.GetOrAdd(key, x => (obj, args) => x.Item2.GetSetMethod(nonPublic)!.Invoke(obj, args)); + //y => (obj, args) => y.Item2.CreatePropertyProxy().SetValue(obj, args)); + } + + /// + /// Convert a string to a boolean. + /// + /// The string. + /// + /// true if the string represents a valid truly value, otherwise false. + /// + public static Boolean ToBoolean(this String str) { + try { + return Convert.ToBoolean(str); + } catch(FormatException) { + // ignored + } + + try { + return Convert.ToBoolean(Convert.ToInt32(str)); + } catch { + // ignored + } + + return false; + } + + /// + /// Creates a property proxy that stores getter and setter delegates. + /// + /// The property information. + /// + /// The property proxy. + /// + /// this. + public static IPropertyProxy? CreatePropertyProxy(this PropertyInfo @this) { + if(@this == null) { + throw new ArgumentNullException(nameof(@this)); + } + + Type genericType = typeof(PropertyProxy<,>).MakeGenericType(@this.DeclaringType!, @this.PropertyType); + + return Activator.CreateInstance(genericType, @this) as IPropertyProxy; + } + + /// + /// Convert a object to a boolean. + /// + /// The value. + /// + /// true if the string represents a valid truly value, otherwise false. + /// + public static Boolean ToBoolean(this Object value) => value.ToStringInvariant().ToBoolean(); + + private static String ConvertObjectAndFormat(Type propertyType, Object value, String format) => + propertyType == typeof(DateTime) || propertyType == typeof(DateTime?) + ? Convert.ToDateTime(value, CultureInfo.InvariantCulture).ToString(format) + : propertyType == typeof(Int32) || propertyType == typeof(Int32?) + ? Convert.ToInt32(value, CultureInfo.InvariantCulture).ToString(format) + : propertyType == typeof(Decimal) || propertyType == typeof(Decimal?) + ? Convert.ToDecimal(value, CultureInfo.InvariantCulture).ToString(format) + : propertyType == typeof(Double) || propertyType == typeof(Double?) + ? Convert.ToDouble(value, CultureInfo.InvariantCulture).ToString(format) + : propertyType == typeof(Byte) || propertyType == typeof(Byte?) + ? Convert.ToByte(value, CultureInfo.InvariantCulture).ToString(format) + : value?.ToString() ?? String.Empty; + } +} diff --git a/Swan.Tiny/Extensions.Strings.cs b/Swan.Tiny/Extensions.Strings.cs new file mode 100644 index 0000000..e8bf398 --- /dev/null +++ b/Swan.Tiny/Extensions.Strings.cs @@ -0,0 +1,364 @@ +#nullable enable +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +using Swan.Formatters; + +namespace Swan { + /// + /// String related extension methods. + /// + public static class StringExtensions { + #region Private Declarations + + private const RegexOptions StandardRegexOptions = RegexOptions.Multiline | RegexOptions.Compiled | RegexOptions.CultureInvariant; + + private static readonly String[] ByteSuffixes = { "B", "KB", "MB", "GB", "TB" }; + + private static readonly Lazy SplitLinesRegex = new Lazy(() => new Regex("\r\n|\r|\n", StandardRegexOptions)); + + private static readonly Lazy UnderscoreRegex = new Lazy(() => new Regex(@"_", StandardRegexOptions)); + + private static readonly Lazy CamelCaseRegEx = new Lazy(() => new Regex(@"[a-z][A-Z]", StandardRegexOptions)); + + private static readonly Lazy SplitCamelCaseString = new Lazy(() => m => { + String x = m.ToString(); + return x[0] + " " + x[1..]; + }); + + private static readonly Lazy InvalidFilenameChars = new Lazy(() => Path.GetInvalidFileNameChars().Select(c => c.ToString()).ToArray()); + + #endregion + + /// + /// Returns a string that represents the given item + /// It tries to use InvariantCulture if the ToString(IFormatProvider) + /// overload exists. + /// + /// The item. + /// A that represents the current object. + public static String ToStringInvariant(this Object? @this) { + if(@this == null) { + return String.Empty; + } + + Type itemType = @this.GetType(); + + return itemType == typeof(String) ? @this as String ?? String.Empty : Definitions.BasicTypesInfo.Value.ContainsKey(itemType) ? Definitions.BasicTypesInfo.Value[itemType].ToStringInvariant(@this) : @this.ToString()!; + } + + /// + /// Returns a string that represents the given item + /// It tries to use InvariantCulture if the ToString(IFormatProvider) + /// overload exists. + /// + /// The type to get the string. + /// The item. + /// A that represents the current object. + public static String ToStringInvariant(this T item) => typeof(String) == typeof(T) ? item as String ?? String.Empty : ToStringInvariant(item as Object); + + /// + /// Removes the control characters from a string except for those specified. + /// + /// The input. + /// When specified, these characters will not be removed. + /// + /// A string that represents the current object. + /// + /// input. + public static String RemoveControlCharsExcept(this String value, params Char[]? excludeChars) { + if(value == null) { + throw new ArgumentNullException(nameof(value)); + } + + if(excludeChars == null) { + excludeChars = Array.Empty(); + } + + return new String(value.Where(c => Char.IsControl(c) == false || excludeChars.Contains(c)).ToArray()); + } + + /// + /// Removes all control characters from a string, including new line sequences. + /// + /// The input. + /// A that represents the current object. + /// input. + public static String RemoveControlChars(this String value) => value.RemoveControlCharsExcept(null); + + /// + /// Outputs JSON string representing this object. + /// + /// The object. + /// if set to true format the output. + /// A that represents the current object. + public static String ToJson(this Object @this, Boolean format = true) => @this == null ? String.Empty : Json.Serialize(@this, format); + + /// + /// Returns text representing the properties of the specified object in a human-readable format. + /// While this method is fairly expensive computationally speaking, it provides an easy way to + /// examine objects. + /// + /// The object. + /// A that represents the current object. + public static String Stringify(this Object @this) { + if(@this == null) { + return "(null)"; + } + + try { + String jsonText = Json.Serialize(@this, false, "$type"); + Object? jsonData = Json.Deserialize(jsonText); + + return new HumanizeJson(jsonData, 0).GetResult(); + } catch { + return @this.ToStringInvariant(); + } + } + + /// + /// Retrieves a section of the string, inclusive of both, the start and end indexes. + /// This behavior is unlike JavaScript's Slice behavior where the end index is non-inclusive + /// If the string is null it returns an empty string. + /// + /// The string. + /// The start index. + /// The end index. + /// Retrieves a substring from this instance. + public static String Slice(this String @this, Int32 startIndex, Int32 endIndex) { + if(@this == null) { + return String.Empty; + } + + Int32 end = endIndex.Clamp(startIndex, @this.Length - 1); + + return startIndex >= end ? String.Empty : @this.Substring(startIndex, end - startIndex + 1); + } + + /// + /// Gets a part of the string clamping the length and startIndex parameters to safe values. + /// If the string is null it returns an empty string. This is basically just a safe version + /// of string.Substring. + /// + /// The string. + /// The start index. + /// The length. + /// Retrieves a substring from this instance. + public static String SliceLength(this String @this, Int32 startIndex, Int32 length) { + if(@this == null) { + return String.Empty; + } + + Int32 start = startIndex.Clamp(0, @this.Length - 1); + Int32 len = length.Clamp(0, @this.Length - start); + + return len == 0 ? String.Empty : @this.Substring(start, len); + } + + /// + /// Splits the specified text into r, n or rn separated lines. + /// + /// The text. + /// + /// An array whose elements contain the substrings from this instance + /// that are delimited by one or more characters in separator. + /// + public static String[] ToLines(this String @this) => @this == null ? Array.Empty() : SplitLinesRegex.Value.Split(@this); + + /// + /// Humanizes (make more human-readable) an identifier-style string + /// in either camel case or snake case. For example, CamelCase will be converted to + /// Camel Case and Snake_Case will be converted to Snake Case. + /// + /// The identifier-style string. + /// A humanized. + public static String Humanize(this String value) { + if(value == null) { + return String.Empty; + } + + String returnValue = UnderscoreRegex.Value.Replace(value, " "); + returnValue = CamelCaseRegEx.Value.Replace(returnValue, SplitCamelCaseString.Value); + return returnValue; + } + + /// + /// Humanizes (make more human-readable) an boolean. + /// + /// if set to true [value]. + /// A that represents the current boolean. + public static String Humanize(this Boolean value) => value ? "Yes" : "No"; + + /// + /// Humanizes (make more human-readable) the specified value. + /// + /// The value. + /// A that represents the current object. + public static String Humanize(this Object value) => + value switch + { + String stringValue => stringValue.Humanize(), + Boolean boolValue => boolValue.Humanize(), + _ => value.Stringify() + }; + + /// + /// Indents the specified multi-line text with the given amount of leading spaces + /// per line. + /// + /// The text. + /// The spaces. + /// A that represents the current object. + public static String Indent(this String value, Int32 spaces = 4) { + if(value == null) { + value = String.Empty; + } + + if(spaces <= 0) { + return value; + } + + String[] lines = value.ToLines(); + StringBuilder builder = new StringBuilder(); + String indentStr = new String(' ', spaces); + + foreach(String line in lines) { + _ = builder.AppendLine($"{indentStr}{line}"); + } + + return builder.ToString().TrimEnd(); + } + + /// + /// Gets the line and column number (i.e. not index) of the + /// specified character index. Useful to locate text in a multi-line + /// string the same way a text editor does. + /// Please not that the tuple contains first the line number and then the + /// column number. + /// + /// The string. + /// Index of the character. + /// A 2-tuple whose value is (item1, item2). + public static Tuple TextPositionAt(this String value, Int32 charIndex) { + if(value == null) { + return Tuple.Create(0, 0); + } + + Int32 index = charIndex.Clamp(0, value.Length - 1); + + Int32 lineIndex = 0; + Int32 colNumber = 0; + + for(Int32 i = 0; i <= index; i++) { + if(value[i] == '\n') { + lineIndex++; + colNumber = 0; + continue; + } + + if(value[i] != '\r') { + colNumber++; + } + } + + return Tuple.Create(lineIndex + 1, colNumber); + } + + /// + /// Makes the file name system safe. + /// + /// The s. + /// + /// A string with a safe file name. + /// + /// s. + public static String ToSafeFilename(this String value) => value == null ? throw new ArgumentNullException(nameof(value)) : InvalidFilenameChars.Value.Aggregate(value, (current, c) => current.Replace(c, String.Empty)).Slice(0, 220); + + /// + /// Formats a long into the closest bytes string. + /// + /// The bytes length. + /// + /// The string representation of the current Byte object, formatted as specified by the format parameter. + /// + public static String FormatBytes(this Int64 bytes) => ((UInt64)bytes).FormatBytes(); + + /// + /// Formats a long into the closest bytes string. + /// + /// The bytes length. + /// + /// A copy of format in which the format items have been replaced by the string + /// representations of the corresponding arguments. + /// + public static String FormatBytes(this UInt64 bytes) { + Int32 i; + Double dblSByte = bytes; + + for(i = 0; i < ByteSuffixes.Length && bytes >= 1024; i++, bytes /= 1024) { + dblSByte = bytes / 1024.0; + } + + return $"{dblSByte:0.##} {ByteSuffixes[i]}"; + } + + /// + /// Truncates the specified value. + /// + /// The value. + /// The maximum length. + /// + /// Retrieves a substring from this instance. + /// The substring starts at a specified character position and has a specified length. + /// + public static String? Truncate(this String value, Int32 maximumLength) => Truncate(value, maximumLength, String.Empty); + + /// + /// Truncates the specified value and append the omission last. + /// + /// The value. + /// The maximum length. + /// The omission. + /// + /// Retrieves a substring from this instance. + /// The substring starts at a specified character position and has a specified length. + /// + public static String? Truncate(this String value, Int32 maximumLength, String omission) => value == null ? null : value.Length > maximumLength ? value.Substring(0, maximumLength) + (omission ?? String.Empty) : value; + + /// + /// Determines whether the specified contains any of characters in + /// the specified array of . + /// + /// + /// true if contains any of ; + /// otherwise, false. + /// + /// + /// A to test. + /// + /// + /// An array of that contains characters to find. + /// + public static Boolean Contains(this String value, params Char[] chars) => chars?.Length == 0 || !String.IsNullOrEmpty(value) && chars != null && value.IndexOfAny(chars) > -1; + + /// + /// Replaces all chars in a string. + /// + /// The value. + /// The replace value. + /// The chars. + /// The string with the characters replaced. + public static String ReplaceAll(this String value, String replaceValue, params Char[] chars) => chars.Aggregate(value, (current, c) => current.Replace(new String(new[] { c }), replaceValue)); + + /// + /// Convert hex character to an integer. Return -1 if char is something + /// other than a hex char. + /// + /// The c. + /// Converted integer. + public static Int32 Hex2Int(this Char value) => value >= '0' && value <= '9' ? value - '0' : value >= 'A' && value <= 'F' ? value - 'A' + 10 : value >= 'a' && value <= 'f' ? value - 'a' + 10 : -1; + } +} diff --git a/Swan.Tiny/Extensions.ValueTypes.cs b/Swan.Tiny/Extensions.ValueTypes.cs new file mode 100644 index 0000000..c509011 --- /dev/null +++ b/Swan.Tiny/Extensions.ValueTypes.cs @@ -0,0 +1,135 @@ +using System; +using System.Reflection; +using System.Runtime.InteropServices; + +using Swan.Reflection; + +namespace Swan { + /// + /// Provides various extension methods for value types and structs. + /// + public static class ValueTypeExtensions { + /// + /// Clamps the specified value between the minimum and the maximum. + /// + /// The type of value to clamp. + /// The value. + /// The minimum. + /// The maximum. + /// A value that indicates the relative order of the objects being compared. + public static T Clamp(this T @this, T min, T max) where T : struct, IComparable => @this.CompareTo(min) < 0 ? min : @this.CompareTo(max) > 0 ? max : @this; + + /// + /// Clamps the specified value between the minimum and the maximum. + /// + /// The value. + /// The minimum. + /// The maximum. + /// A value that indicates the relative order of the objects being compared. + public static Int32 Clamp(this Int32 @this, Int32 min, Int32 max) => @this < min ? min : (@this > max ? max : @this); + + /// + /// Determines whether the specified value is between a minimum and a maximum value. + /// + /// The type of value to check. + /// The value. + /// The minimum. + /// The maximum. + /// + /// true if the specified minimum is between; otherwise, false. + /// + public static Boolean IsBetween(this T @this, T min, T max) where T : struct, IComparable => @this.CompareTo(min) >= 0 && @this.CompareTo(max) <= 0; + + /// + /// Converts an array of bytes into the given struct type. + /// + /// The type of structure to convert. + /// The data. + /// a struct type derived from convert an array of bytes ref=ToStruct". + public static T ToStruct(this Byte[] @this) where T : struct => @this == null ? throw new ArgumentNullException(nameof(@this)) : ToStruct(@this, 0, @this.Length); + + /// + /// Converts an array of bytes into the given struct type. + /// + /// The type of structure to convert. + /// The data. + /// The offset. + /// The length. + /// + /// A managed object containing the data pointed to by the ptr parameter. + /// + /// data. + public static T ToStruct(this Byte[] @this, Int32 offset, Int32 length) where T : struct { + if(@this == null) { + throw new ArgumentNullException(nameof(@this)); + } + + Byte[] buffer = new Byte[length]; + Array.Copy(@this, offset, buffer, 0, buffer.Length); + GCHandle handle = GCHandle.Alloc(GetStructBytes(buffer), GCHandleType.Pinned); + + try { + return Marshal.PtrToStructure(handle.AddrOfPinnedObject()); + } finally { + handle.Free(); + } + } + + /// + /// Converts a struct to an array of bytes. + /// + /// The type of structure to convert. + /// The object. + /// A byte array containing the results of encoding the specified set of characters. + public static Byte[] ToBytes(this T @this) where T : struct { + Byte[] data = new Byte[Marshal.SizeOf(@this)]; + GCHandle handle = GCHandle.Alloc(data, GCHandleType.Pinned); + + try { + Marshal.StructureToPtr(@this, handle.AddrOfPinnedObject(), false); + return GetStructBytes(data); + } finally { + handle.Free(); + } + } + + /// + /// Swaps the endianness of an unsigned long to an unsigned integer. + /// + /// The bytes contained in a long. + /// + /// A 32-bit unsigned integer equivalent to the ulong + /// contained in longBytes. + /// + public static UInt32 SwapEndianness(this UInt64 @this) => (UInt32)(((@this & 0x000000ff) << 24) + ((@this & 0x0000ff00) << 8) + ((@this & 0x00ff0000) >> 8) + ((@this & 0xff000000) >> 24)); + + private static Byte[] GetStructBytes(Byte[] data) { + if(data == null) { + throw new ArgumentNullException(nameof(data)); + } + + FieldInfo[] fields = typeof(T).GetTypeInfo() + .GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + + StructEndiannessAttribute endian = AttributeCache.DefaultCache.Value.RetrieveOne(); + + foreach(FieldInfo field in fields) { + if(endian == null && !field.IsDefined(typeof(StructEndiannessAttribute), false)) { + continue; + } + + Int32 offset = Marshal.OffsetOf(field.Name).ToInt32(); + Int32 length = Marshal.SizeOf(field.FieldType); + + endian ??= AttributeCache.DefaultCache.Value.RetrieveOne(field); + + if(endian != null && (endian.Endianness == Endianness.Big && BitConverter.IsLittleEndian || + endian.Endianness == Endianness.Little && !BitConverter.IsLittleEndian)) { + Array.Reverse(data, offset, length); + } + } + + return data; + } + } +} \ No newline at end of file diff --git a/Swan.Tiny/Extensions.cs b/Swan.Tiny/Extensions.cs new file mode 100644 index 0000000..0f67daa --- /dev/null +++ b/Swan.Tiny/Extensions.cs @@ -0,0 +1,230 @@ +#nullable enable +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using Swan.Lite.Reflection; +using Swan.Mappers; +using Swan.Reflection; + +namespace Swan { + /// + /// Extension methods. + /// + public static partial class Extensions { + /// + /// Iterates over the public, instance, readable properties of the source and + /// tries to write a compatible value to a public, instance, writable property in the destination. + /// + /// The type of the source. + /// The source. + /// The target. + /// The ignore properties. + /// + /// Number of properties that was copied successful. + /// + public static Int32 CopyPropertiesTo(this T source, Object? target, params String[]? ignoreProperties) where T : class => ObjectMapper.Copy(source, target, GetCopyableProperties(target), ignoreProperties); + + /// + /// Iterates over the public, instance, readable properties of the source and + /// tries to write a compatible value to a public, instance, writable property in the destination. + /// + /// The source. + /// The destination. + /// Properties to copy. + /// + /// Number of properties that were successfully copied. + /// + public static Int32 CopyOnlyPropertiesTo(this Object source, Object target, params String[]? propertiesToCopy) => ObjectMapper.Copy(source, target, propertiesToCopy); + + /// + /// Copies the properties to new instance of T. + /// + /// The new object type. + /// The source. + /// The ignore properties. + /// + /// The specified type with properties copied. + /// + /// source. + public static T CopyPropertiesToNew(this Object source, String[]? ignoreProperties = null) where T : class { + if(source == null) { + throw new ArgumentNullException(nameof(source)); + } + + T target = Activator.CreateInstance(); + _ = ObjectMapper.Copy(source, target, GetCopyableProperties(target), ignoreProperties); + + return target; + } + + /// + /// Copies the only properties to new instance of T. + /// + /// Object Type. + /// The source. + /// The properties to copy. + /// + /// The specified type with properties copied. + /// + /// source. + public static T CopyOnlyPropertiesToNew(this Object source, params String[] propertiesToCopy) where T : class { + if(source == null) { + throw new ArgumentNullException(nameof(source)); + } + + T target = Activator.CreateInstance(); + _ = ObjectMapper.Copy(source, target, propertiesToCopy); + + return target; + } + + /// + /// Iterates over the keys of the source and tries to write a compatible value to a public, + /// instance, writable property in the destination. + /// + /// The source. + /// The target. + /// The ignore keys. + /// Number of properties that was copied successful. + public static Int32 CopyKeyValuePairTo(this IDictionary source, Object? target, params String[] ignoreKeys) => source == null ? throw new ArgumentNullException(nameof(source)) : ObjectMapper.Copy(source, target, null, ignoreKeys); + + /// + /// Iterates over the keys of the source and tries to write a compatible value to a public, + /// instance, writable property in the destination. + /// + /// Object Type. + /// The source. + /// The ignore keys. + /// + /// The specified type with properties copied. + /// + public static T CopyKeyValuePairToNew(this IDictionary source, params String[] ignoreKeys) { + if(source == null) { + throw new ArgumentNullException(nameof(source)); + } + + T target = Activator.CreateInstance(); + _ = source.CopyKeyValuePairTo(target, ignoreKeys); + return target; + } + + /// + /// Does the specified action. + /// + /// The action. + /// The retry interval. + /// The retry count. + public static void Retry(this Action action, TimeSpan retryInterval = default, Int32 retryCount = 3) { + if(action == null) { + throw new ArgumentNullException(nameof(action)); + } + + _ = Retry(() => { action(); return null; }, retryInterval, retryCount); + } + + /// + /// Does the specified action. + /// + /// The type of the source. + /// The action. + /// The retry interval. + /// The retry count. + /// + /// The return value of the method that this delegate encapsulates. + /// + /// action. + /// Represents one or many errors that occur during application execution. + public static T Retry(this Func action, TimeSpan retryInterval = default, Int32 retryCount = 3) { + if(action == null) { + throw new ArgumentNullException(nameof(action)); + } + + if(retryInterval == default) { + retryInterval = TimeSpan.FromSeconds(1); + } + + global::System.Collections.Generic.List exceptions = new List(); + + for(Int32 retry = 0; retry < retryCount; retry++) { + try { + if(retry > 0) { + Task.Delay(retryInterval).Wait(); + } + + return action(); + } catch(Exception ex) { + exceptions.Add(ex); + } + } + + throw new AggregateException(exceptions); + } + + /// + /// Gets the copyable properties. + /// + /// If there is no properties with the attribute AttributeCache returns all the properties. + /// + /// The object. + /// + /// Array of properties. + /// + /// model. + /// + public static IEnumerable GetCopyableProperties(this Object? @this) { + if(@this == null) { + throw new ArgumentNullException(nameof(@this)); + } + + global::System.Collections.Generic.IEnumerable collection = PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties(@this.GetType(), true); + + global::System.Collections.Generic.IEnumerable properties = collection.Select(x => new { + x.Name, + HasAttribute = AttributeCache.DefaultCache.Value.RetrieveOne(x) != null, + }).Where(x => x.HasAttribute).Select(x => x.Name); + + return properties.Any() ? properties : collection.Select(x => x.Name); + } + + internal static void CreateTarget(this Object source, Type targetType, Boolean includeNonPublic, ref Object? target) { + switch(source) { + // do nothing. Simply skip creation + case String _: + break; + // When using arrays, there is no default constructor, attempt to build a compatible array + case IList sourceObjectList when targetType.IsArray: + Type? elementType = targetType.GetElementType(); + + if(elementType != null) { + target = Array.CreateInstance(elementType, sourceObjectList.Count); + } + + break; + default: + IEnumerable> constructors = ConstructorTypeCache.DefaultCache.Value.RetrieveAllConstructors(targetType, includeNonPublic); + + // Try to check if empty constructor is available + if(constructors.Any(x => x.Item2.Length == 0)) { + target = Activator.CreateInstance(targetType, includeNonPublic); + } else { + Tuple firstCtor = constructors.OrderBy(x => x.Item2.Length).FirstOrDefault(); + + target = Activator.CreateInstance(targetType, firstCtor?.Item2.Select(arg => arg.GetType().GetDefault()).ToArray()); + } + + break; + } + } + + internal static String GetNameWithCase(this String name, JsonSerializerCase jsonSerializerCase) => jsonSerializerCase switch + { + JsonSerializerCase.PascalCase => Char.ToUpperInvariant(name[0]) + name.Substring(1), + JsonSerializerCase.CamelCase => Char.ToLowerInvariant(name[0]) + name.Substring(1), + JsonSerializerCase.None => name, + _ => throw new ArgumentOutOfRangeException(nameof(jsonSerializerCase), jsonSerializerCase, null) + }; + } +} diff --git a/Swan.Tiny/Formatters/HumanizeJson.cs b/Swan.Tiny/Formatters/HumanizeJson.cs new file mode 100644 index 0000000..8099413 --- /dev/null +++ b/Swan.Tiny/Formatters/HumanizeJson.cs @@ -0,0 +1,126 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System; + +namespace Swan.Formatters { + internal class HumanizeJson { + private readonly StringBuilder _builder = new StringBuilder(); + private readonly Int32 _indent; + private readonly String _indentStr; + private readonly Object _obj; + + public HumanizeJson(Object obj, Int32 indent) { + if(obj == null) { + return; + } + + this._indent = indent; + this._indentStr = new String(' ', indent * 4); + this._obj = obj; + + this.ParseObject(); + } + + public String GetResult() => this._builder == null ? String.Empty : this._builder.ToString().TrimEnd(); + + private void ParseObject() { + switch(this._obj) { + case Dictionary dictionary: + this.AppendDictionary(dictionary); + break; + case List list: + this.AppendList(list); + break; + default: + this.AppendString(); + break; + } + } + + private void AppendDictionary(Dictionary objects) { + foreach(KeyValuePair kvp in objects) { + if(kvp.Value == null) { + continue; + } + + Boolean writeOutput = false; + + switch(kvp.Value) { + case Dictionary valueDictionary: + if(valueDictionary.Count > 0) { + writeOutput = true; + _ = this._builder.Append($"{this._indentStr}{kvp.Key,-16}: object").AppendLine(); + } + + break; + case List valueList: + if(valueList.Count > 0) { + writeOutput = true; + _ = this._builder.Append($"{this._indentStr}{kvp.Key,-16}: array[{valueList.Count}]").AppendLine(); + } + + break; + default: + writeOutput = true; + _ = this._builder.Append($"{this._indentStr}{kvp.Key,-16}: "); + break; + } + + if(writeOutput) { + _ = this._builder.AppendLine(new HumanizeJson(kvp.Value, this._indent + 1).GetResult()); + } + } + } + + private void AppendList(List objects) { + Int32 index = 0; + foreach(Object value in objects) { + Boolean writeOutput = false; + + switch(value) { + case Dictionary valueDictionary: + if(valueDictionary.Count > 0) { + writeOutput = true; + _ = this._builder.Append($"{this._indentStr}[{index}]: object").AppendLine(); + } + + break; + case List valueList: + if(valueList.Count > 0) { + writeOutput = true; + _ = this._builder.Append($"{this._indentStr}[{index}]: array[{valueList.Count}]").AppendLine(); + } + + break; + default: + writeOutput = true; + _ = this._builder.Append($"{this._indentStr}[{index}]: "); + break; + } + + index++; + + if(writeOutput) { + _ = this._builder.AppendLine(new HumanizeJson(value, this._indent + 1).GetResult()); + } + } + } + + private void AppendString() { + String stringValue = this._obj.ToString(); + + if(stringValue.Length + this._indentStr.Length > 96 || stringValue.IndexOf('\r') >= 0 || + stringValue.IndexOf('\n') >= 0) { + _ = this._builder.AppendLine(); + IEnumerable stringLines = stringValue.ToLines().Select(l => l.Trim()); + + foreach(String line in stringLines) { + _ = this._builder.AppendLine($"{this._indentStr}{line}"); + } + } else { + _ = this._builder.Append($"{stringValue}"); + } + } + } +} \ No newline at end of file diff --git a/Swan.Tiny/Formatters/Json.Converter.cs b/Swan.Tiny/Formatters/Json.Converter.cs new file mode 100644 index 0000000..6878bcb --- /dev/null +++ b/Swan.Tiny/Formatters/Json.Converter.cs @@ -0,0 +1,259 @@ +#nullable enable +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using Swan.Reflection; + +namespace Swan.Formatters { + /// + /// A very simple, light-weight JSON library written by Mario + /// to teach Geo how things are done + /// + /// This is an useful helper for small tasks but it doesn't represent a full-featured + /// serializer such as the beloved Json.NET. + /// + public static partial class Json { + private class Converter { + private static readonly ConcurrentDictionary MemberInfoNameCache = new ConcurrentDictionary(); + + private static readonly ConcurrentDictionary ListAddMethodCache = new ConcurrentDictionary(); + + private readonly Object? _target; + private readonly Type _targetType; + private readonly Boolean _includeNonPublic; + private readonly JsonSerializerCase _jsonSerializerCase; + + private Converter(Object? source, Type targetType, ref Object? targetInstance, Boolean includeNonPublic, JsonSerializerCase jsonSerializerCase) { + this._targetType = targetInstance != null ? targetInstance.GetType() : targetType; + this._includeNonPublic = includeNonPublic; + this._jsonSerializerCase = jsonSerializerCase; + + if(source == null) { + return; + } + + Type sourceType = source.GetType(); + + if(this._targetType == null || this._targetType == typeof(Object)) { + this._targetType = sourceType; + } + + if(sourceType == this._targetType) { + this._target = source; + return; + } + + if(!this.TrySetInstance(targetInstance, source, ref this._target)) { + return; + } + + this.ResolveObject(source, ref this._target); + } + + internal static Object? FromJsonResult(Object? source, JsonSerializerCase jsonSerializerCase, Type? targetType = null, Boolean includeNonPublic = false) { + Object? nullRef = null; + return new Converter(source, targetType ?? typeof(Object), ref nullRef, includeNonPublic, jsonSerializerCase).GetResult(); + } + + private static Object? FromJsonResult(Object source, Type targetType, ref Object? targetInstance, Boolean includeNonPublic) => new Converter(source, targetType, ref targetInstance, includeNonPublic, JsonSerializerCase.None).GetResult(); + + private static Type? GetAddMethodParameterType(Type targetType) => ListAddMethodCache.GetOrAdd(targetType, x => x.GetMethods().FirstOrDefault(m => m.Name == AddMethodName && m.IsPublic && m.GetParameters().Length == 1)?.GetParameters()[0].ParameterType!); + + private static void GetByteArray(String sourceString, ref Object? target) { + try { + target = Convert.FromBase64String(sourceString); + } // Try conversion from Base 64 + catch(FormatException) { + target = Encoding.UTF8.GetBytes(sourceString); + } // Get the string bytes in UTF8 + } + + private Object GetSourcePropertyValue(IDictionary sourceProperties, MemberInfo targetProperty) { + String targetPropertyName = MemberInfoNameCache.GetOrAdd(targetProperty, x => AttributeCache.DefaultCache.Value.RetrieveOne(x)?.PropertyName ?? x.Name.GetNameWithCase(this._jsonSerializerCase)); + + return sourceProperties.GetValueOrDefault(targetPropertyName); + } + + private Boolean TrySetInstance(Object? targetInstance, Object source, ref Object? target) { + if(targetInstance == null) { + // Try to create a default instance + try { + source.CreateTarget(this._targetType, this._includeNonPublic, ref target); + } catch { + return false; + } + } else { + target = targetInstance; + } + + return true; + } + + private Object? GetResult() => this._target ?? this._targetType.GetDefault(); + + private void ResolveObject(Object source, ref Object? target) { + switch(source) { + // Case 0: Special Cases Handling (Source and Target are of specific convertible types) + // Case 0.1: Source is string, Target is byte[] + case String sourceString when this._targetType == typeof(Byte[]): + GetByteArray(sourceString, ref target); + break; + + // Case 1.1: Source is Dictionary, Target is IDictionary + case Dictionary sourceProperties when target is IDictionary targetDictionary: + this.PopulateDictionary(sourceProperties, targetDictionary); + break; + + // Case 1.2: Source is Dictionary, Target is not IDictionary (i.e. it is a complex type) + case Dictionary sourceProperties: + this.PopulateObject(sourceProperties); + break; + + // Case 2.1: Source is List, Target is Array + case List sourceList when target is Array targetArray: + this.PopulateArray(sourceList, targetArray); + break; + + // Case 2.2: Source is List, Target is IList + case List sourceList when target is IList targetList: + this.PopulateIList(sourceList, targetList); + break; + + // Case 3: Source is a simple type; Attempt conversion + default: + String sourceStringValue = source.ToStringInvariant(); + + // Handle basic types or enumerations if not + if(!this._targetType.TryParseBasicType(sourceStringValue, out target)) { + this.GetEnumValue(sourceStringValue, ref target); + } + + break; + } + } + + private void PopulateIList(IEnumerable objects, IList list) { + Type? parameterType = GetAddMethodParameterType(this._targetType); + if(parameterType == null) { + return; + } + + foreach(Object item in objects) { + try { + _ = list.Add(FromJsonResult(item, this._jsonSerializerCase, parameterType, this._includeNonPublic)); + } catch { + // ignored + } + } + } + + private void PopulateArray(IList objects, Array array) { + Type? elementType = this._targetType.GetElementType(); + + for(Int32 i = 0; i < objects.Count; i++) { + try { + Object? targetItem = FromJsonResult(objects[i], this._jsonSerializerCase, elementType, this._includeNonPublic); + array.SetValue(targetItem, i); + } catch { + // ignored + } + } + } + + private void GetEnumValue(String sourceStringValue, ref Object? target) { + Type? enumType = Nullable.GetUnderlyingType(this._targetType); + if(enumType == null && this._targetType.IsEnum) { + enumType = this._targetType; + } + + if(enumType == null) { + return; + } + + try { + target = Enum.Parse(enumType, sourceStringValue); + } catch { + // ignored + } + } + + private void PopulateDictionary(IDictionary sourceProperties, IDictionary targetDictionary) { + // find the add method of the target dictionary + MethodInfo addMethod = this._targetType.GetMethods().FirstOrDefault(m => m.Name == AddMethodName && m.IsPublic && m.GetParameters().Length == 2); + + // skip if we don't have a compatible add method + if(addMethod == null) { + return; + } + + global::System.Reflection.ParameterInfo[] addMethodParameters = addMethod.GetParameters(); + if(addMethodParameters[0].ParameterType != typeof(String)) { + return; + } + + // Retrieve the target entry type + Type targetEntryType = addMethodParameters[1].ParameterType; + + // Add the items to the target dictionary + foreach(KeyValuePair sourceProperty in sourceProperties) { + try { + Object? targetEntryValue = FromJsonResult(sourceProperty.Value, this._jsonSerializerCase, targetEntryType, this._includeNonPublic); + targetDictionary.Add(sourceProperty.Key, targetEntryValue); + } catch { + // ignored + } + } + } + + private void PopulateObject(IDictionary sourceProperties) { + if(this._targetType.IsValueType) { + this.PopulateFields(sourceProperties); + } + + this.PopulateProperties(sourceProperties); + } + + private void PopulateProperties(IDictionary sourceProperties) { + global::System.Collections.Generic.IEnumerable properties = PropertyTypeCache.DefaultCache.Value.RetrieveFilteredProperties(this._targetType, false, p => p.CanWrite); + + foreach(PropertyInfo property in properties) { + Object sourcePropertyValue = this.GetSourcePropertyValue(sourceProperties, property); + if(sourcePropertyValue == null) { + continue; + } + + try { + Object? currentPropertyValue = !property.PropertyType.IsArray ? property?.GetCacheGetMethod(this._includeNonPublic)!(this._target!) : null; + + Object? targetPropertyValue = FromJsonResult(sourcePropertyValue, property.PropertyType, ref currentPropertyValue, this._includeNonPublic); + + property?.GetCacheSetMethod(this._includeNonPublic)!(this._target!, new[] { targetPropertyValue }!); + } catch { + // ignored + } + } + } + + private void PopulateFields(IDictionary sourceProperties) { + foreach(FieldInfo field in FieldTypeCache.DefaultCache.Value.RetrieveAllFields(this._targetType)) { + Object sourcePropertyValue = this.GetSourcePropertyValue(sourceProperties, field); + if(sourcePropertyValue == null) { + continue; + } + + Object? targetPropertyValue = FromJsonResult(sourcePropertyValue, this._jsonSerializerCase, field.FieldType, this._includeNonPublic); + + try { + field.SetValue(this._target, targetPropertyValue); + } catch { + // ignored + } + } + } + } + } +} diff --git a/Swan.Tiny/Formatters/Json.Deserializer.cs b/Swan.Tiny/Formatters/Json.Deserializer.cs new file mode 100644 index 0000000..36ab944 --- /dev/null +++ b/Swan.Tiny/Formatters/Json.Deserializer.cs @@ -0,0 +1,332 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Text; + +namespace Swan.Formatters { + /// + /// A very simple, light-weight JSON library written by Mario + /// to teach Geo how things are done + /// + /// This is an useful helper for small tasks but it doesn't represent a full-featured + /// serializer such as the beloved Json.NET. + /// + public partial class Json { + /// + /// A simple JSON Deserializer. + /// + private class Deserializer { + #region State Variables + + private readonly Object? _result; + private readonly String _json; + + private Dictionary? _resultObject; + private List? _resultArray; + private ReadState _state = ReadState.WaitingForRootOpen; + private String? _currentFieldName; + + private Int32 _index; + + #endregion + + private Deserializer(String? json, Int32 startIndex) { + if(json == null) { + this._json = ""; + return; + } + this._json = json; + + for(this._index = startIndex; this._index < this._json.Length; this._index++) { + switch(this._state) { + case ReadState.WaitingForRootOpen: + this.WaitForRootOpen(); + continue; + case ReadState.WaitingForField when Char.IsWhiteSpace(this._json, this._index): + continue; + case ReadState.WaitingForField when this._resultObject != null && this._json[this._index] == CloseObjectChar || this._resultArray != null && this._json[this._index] == CloseArrayChar: + // Handle empty arrays and empty objects + this._result = this._resultObject ?? this._resultArray as Object; + return; + case ReadState.WaitingForField when this._json[this._index] != StringQuotedChar: + throw this.CreateParserException($"'{StringQuotedChar}'"); + case ReadState.WaitingForField: { + Int32 charCount = this.GetFieldNameCount(); + + this._currentFieldName = Unescape(this._json.SliceLength(this._index + 1, charCount)); + this._index += charCount + 1; + this._state = ReadState.WaitingForColon; + continue; + } + + case ReadState.WaitingForColon when Char.IsWhiteSpace(this._json, this._index): + continue; + case ReadState.WaitingForColon when this._json[this._index] != ValueSeparatorChar: + throw this.CreateParserException($"'{ValueSeparatorChar}'"); + case ReadState.WaitingForColon: + this._state = ReadState.WaitingForValue; + continue; + case ReadState.WaitingForValue when Char.IsWhiteSpace(this._json, this._index): + continue; + case ReadState.WaitingForValue when this._resultObject != null && this._json[this._index] == CloseObjectChar || this._resultArray != null && this._json[this._index] == CloseArrayChar: + // Handle empty arrays and empty objects + this._result = this._resultObject ?? this._resultArray as Object; + return; + case ReadState.WaitingForValue: + this.ExtractValue(); + continue; + } + + if(this._state != ReadState.WaitingForNextOrRootClose || Char.IsWhiteSpace(this._json, this._index)) { + continue; + } + + if(this._json[this._index] == FieldSeparatorChar) { + if(this._resultObject != null) { + this._state = ReadState.WaitingForField; + this._currentFieldName = null; + continue; + } + + this._state = ReadState.WaitingForValue; + continue; + } + + if((this._resultObject == null || this._json[this._index] != CloseObjectChar) && (this._resultArray == null || this._json[this._index] != CloseArrayChar)) { + throw this.CreateParserException($"'{FieldSeparatorChar}' '{CloseObjectChar}' or '{CloseArrayChar}'"); + } + + this._result = this._resultObject ?? this._resultArray as Object; + return; + } + } + + internal static Object? DeserializeInternal(String? json) => new Deserializer(json, 0)._result; + + private void WaitForRootOpen() { + if(Char.IsWhiteSpace(this._json, this._index)) { + return; + } + + switch(this._json[this._index]) { + case OpenObjectChar: + this._resultObject = new Dictionary(); + this._state = ReadState.WaitingForField; + return; + case OpenArrayChar: + this._resultArray = new List(); + this._state = ReadState.WaitingForValue; + return; + default: + throw this.CreateParserException($"'{OpenObjectChar}' or '{OpenArrayChar}'"); + } + } + + private void ExtractValue() { + // determine the value based on what it starts with + switch(this._json[this._index]) { + case StringQuotedChar: // expect a string + this.ExtractStringQuoted(); + break; + + case OpenObjectChar: // expect object + case OpenArrayChar: // expect array + this.ExtractObject(); + break; + + case 't': // expect true + this.ExtractConstant(TrueLiteral, true); + break; + + case 'f': // expect false + this.ExtractConstant(FalseLiteral, false); + break; + + case 'n': // expect null + this.ExtractConstant(NullLiteral, null); + break; + + default: // expect number + this.ExtractNumber(); + break; + } + + this._currentFieldName = null; + this._state = ReadState.WaitingForNextOrRootClose; + } + + private static String Unescape(String str) { + // check if we need to unescape at all + if(str.IndexOf(StringEscapeChar) < 0) { + return str; + } + + StringBuilder builder = new StringBuilder(str.Length); + for(Int32 i = 0; i < str.Length; i++) { + if(str[i] != StringEscapeChar) { + _ = builder.Append(str[i]); + continue; + } + + if(i + 1 > str.Length - 1) { + break; + } + + // escape sequence begins here + switch(str[i + 1]) { + case 'u': + i = ExtractEscapeSequence(str, i, builder); + break; + case 'b': + _ = builder.Append('\b'); + i += 1; + break; + case 't': + _ = builder.Append('\t'); + i += 1; + break; + case 'n': + _ = builder.Append('\n'); + i += 1; + break; + case 'f': + _ = builder.Append('\f'); + i += 1; + break; + case 'r': + _ = builder.Append('\r'); + i += 1; + break; + default: + _ = builder.Append(str[i + 1]); + i += 1; + break; + } + } + + return builder.ToString(); + } + + private static Int32 ExtractEscapeSequence(String str, Int32 i, StringBuilder builder) { + Int32 startIndex = i + 2; + Int32 endIndex = i + 5; + if(endIndex > str.Length - 1) { + _ = builder.Append(str[i + 1]); + i += 1; + return i; + } + + Byte[] hexCode = str.Slice(startIndex, endIndex).ConvertHexadecimalToBytes(); + _ = builder.Append(Encoding.BigEndianUnicode.GetChars(hexCode)); + i += 5; + return i; + } + + private Int32 GetFieldNameCount() { + Int32 charCount = 0; + for(Int32 j = this._index + 1; j < this._json.Length; j++) { + if(this._json[j] == StringQuotedChar && this._json[j - 1] != StringEscapeChar) { + break; + } + + charCount++; + } + + return charCount; + } + + private void ExtractObject() { + // Extract and set the value + Deserializer deserializer = new Deserializer(this._json, this._index); + + if(this._currentFieldName != null) { + this._resultObject![this._currentFieldName] = deserializer._result!; + } else { + this._resultArray!.Add(deserializer._result!); + } + + this._index = deserializer._index; + } + + private void ExtractNumber() { + Int32 charCount = 0; + for(Int32 j = this._index; j < this._json.Length; j++) { + if(Char.IsWhiteSpace(this._json[j]) || this._json[j] == FieldSeparatorChar || this._resultObject != null && this._json[j] == CloseObjectChar || this._resultArray != null && this._json[j] == CloseArrayChar) { + break; + } + + charCount++; + } + + // Extract and set the value + String stringValue = this._json.SliceLength(this._index, charCount); + + if(Decimal.TryParse(stringValue, System.Globalization.NumberStyles.Number, System.Globalization.CultureInfo.InvariantCulture, out Decimal value) == false) { + throw this.CreateParserException("[number]"); + } + + if(this._currentFieldName != null) { + this._resultObject![this._currentFieldName] = value; + } else { + this._resultArray!.Add(value); + } + + this._index += charCount - 1; + } + + private void ExtractConstant(String boolValue, Boolean? value) { + if(this._json.SliceLength(this._index, boolValue.Length) != boolValue) { + throw this.CreateParserException($"'{ValueSeparatorChar}'"); + } + + // Extract and set the value + if(this._currentFieldName != null) { + this._resultObject![this._currentFieldName] = value; + } else { + this._resultArray!.Add(value); + } + + this._index += boolValue.Length - 1; + } + + private void ExtractStringQuoted() { + Int32 charCount = 0; + Boolean escapeCharFound = false; + for(Int32 j = this._index + 1; j < this._json.Length; j++) { + if(this._json[j] == StringQuotedChar && !escapeCharFound) { + break; + } + + escapeCharFound = this._json[j] == StringEscapeChar && !escapeCharFound; + charCount++; + } + + // Extract and set the value + String value = Unescape(this._json.SliceLength(this._index + 1, charCount)); + if(this._currentFieldName != null) { + this._resultObject![this._currentFieldName] = value; + } else { + this._resultArray!.Add(value); + } + + this._index += charCount + 1; + } + + private FormatException CreateParserException(String expected) { + Tuple textPosition = this._json.TextPositionAt(this._index); + return new FormatException($"Parser error (Line {textPosition.Item1}, Col {textPosition.Item2}, State {this._state}): Expected {expected} but got '{this._json[this._index]}'."); + } + + /// + /// Defines the different JSON read states. + /// + private enum ReadState { + WaitingForRootOpen, + WaitingForField, + WaitingForColon, + WaitingForValue, + WaitingForNextOrRootClose, + } + } + } +} diff --git a/Swan.Tiny/Formatters/Json.Serializer.cs b/Swan.Tiny/Formatters/Json.Serializer.cs new file mode 100644 index 0000000..f028578 --- /dev/null +++ b/Swan.Tiny/Formatters/Json.Serializer.cs @@ -0,0 +1,331 @@ +#nullable enable +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace Swan.Formatters { + /// + /// A very simple, light-weight JSON library written by Mario + /// to teach Geo how things are done + /// + /// This is an useful helper for small tasks but it doesn't represent a full-featured + /// serializer such as the beloved Json.NET. + /// + public partial class Json { + /// + /// A simple JSON serializer. + /// + private class Serializer { + #region Private Declarations + + private static readonly Dictionary IndentStrings = new Dictionary(); + + private readonly SerializerOptions? _options; + private readonly String _result; + private readonly StringBuilder? _builder; + private readonly String? _lastCommaSearch; + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The object. + /// The depth. + /// The options. + private Serializer(Object? obj, Int32 depth, SerializerOptions options) { + if(depth > 20) { + throw new InvalidOperationException("The max depth (20) has been reached. Serializer can not continue."); + } + + // Basic Type Handling (nulls, strings, number, date and bool) + this._result = ResolveBasicType(obj); + + if(!String.IsNullOrWhiteSpace(this._result)) { + return; + } + + this._options = options; + + // Handle circular references correctly and avoid them + if(options.IsObjectPresent(obj!)) { + this._result = $"{{ \"$circref\": \"{Escape(obj!.GetHashCode().ToStringInvariant(), false)}\" }}"; + return; + } + + // At this point, we will need to construct the object with a StringBuilder. + this._lastCommaSearch = FieldSeparatorChar + (this._options.Format ? Environment.NewLine : String.Empty); + this._builder = new StringBuilder(); + + this._result = obj switch + { + IDictionary itemsZero when itemsZero.Count == 0 => EmptyObjectLiteral, + IDictionary items => this.ResolveDictionary(items, depth), + IEnumerable enumerableZero when !enumerableZero.Cast().Any() => EmptyArrayLiteral, + IEnumerable enumerableBytes when enumerableBytes is Byte[] bytes => Serialize(bytes.ToBase64(), depth, this._options), + IEnumerable enumerable => this.ResolveEnumerable(enumerable, depth), + _ => this.ResolveObject(obj!, depth) + }; + } + + internal static String Serialize(Object? obj, Int32 depth, SerializerOptions options) => new Serializer(obj, depth, options)._result; + + #endregion + + #region Helper Methods + + private static String ResolveBasicType(Object? obj) { + switch(obj) { + case null: + return NullLiteral; + case String s: + return Escape(s, true); + case Boolean b: + return b ? TrueLiteral : FalseLiteral; + case Type _: + case Assembly _: + case MethodInfo _: + case PropertyInfo _: + case EventInfo _: + return Escape(obj.ToString()!, true); + case DateTime d: + return $"{StringQuotedChar}{d:s}{StringQuotedChar}"; + default: + Type targetType = obj.GetType(); + + if(!Definitions.BasicTypesInfo.Value.ContainsKey(targetType)) { + return String.Empty; + } + + String escapedValue = Escape(Definitions.BasicTypesInfo.Value[targetType].ToStringInvariant(obj), false); + + return Decimal.TryParse(escapedValue, out _) ? $"{escapedValue}" : $"{StringQuotedChar}{escapedValue}{StringQuotedChar}"; + } + } + + private static Boolean IsNonEmptyJsonArrayOrObject(String serialized) { + if(serialized == EmptyObjectLiteral || serialized == EmptyArrayLiteral) { + return false; + } + + // find the first position the character is not a space + return serialized.Where(c => c != ' ').Select(c => c == OpenObjectChar || c == OpenArrayChar).FirstOrDefault(); + } + + private static String Escape(String str, Boolean quoted) { + if(str == null) { + return String.Empty; + } + + StringBuilder builder = new StringBuilder(str.Length * 2); + if(quoted) { + _ = builder.Append(StringQuotedChar); + } + + Escape(str, builder); + if(quoted) { + _ = builder.Append(StringQuotedChar); + } + + return builder.ToString(); + } + + private static void Escape(String str, StringBuilder builder) { + foreach(Char currentChar in str) { + switch(currentChar) { + case '\\': + case '"': + case '/': + _ = builder + .Append('\\') + .Append(currentChar); + break; + case '\b': + _ = builder.Append("\\b"); + break; + case '\t': + _ = builder.Append("\\t"); + break; + case '\n': + _ = builder.Append("\\n"); + break; + case '\f': + _ = builder.Append("\\f"); + break; + case '\r': + _ = builder.Append("\\r"); + break; + default: + if(currentChar < ' ') { + Byte[] escapeBytes = BitConverter.GetBytes((UInt16)currentChar); + if(BitConverter.IsLittleEndian == false) { + Array.Reverse(escapeBytes); + } + + _ = builder.Append("\\u") + .Append(escapeBytes[1].ToString("X").PadLeft(2, '0')) + .Append(escapeBytes[0].ToString("X").PadLeft(2, '0')); + } else { + _ = builder.Append(currentChar); + } + + break; + } + } + } + + private Dictionary CreateDictionary(Dictionary fields, String targetType, Object target) { + // Create the dictionary and extract the properties + global::System.Collections.Generic.Dictionary objectDictionary = new Dictionary(); + + if(String.IsNullOrWhiteSpace(this._options?.TypeSpecifier) == false) { + objectDictionary[this._options?.TypeSpecifier!] = targetType; + } + + foreach(global::System.Collections.Generic.KeyValuePair field in fields) { + // Build the dictionary using property names and values + // Note: used to be: property.GetValue(target); but we would be reading private properties + try { + objectDictionary[field.Key] = field.Value is PropertyInfo property ? property.GetCacheGetMethod((Boolean)(this._options?.IncludeNonPublic)!)?.Invoke(target) : (field.Value as FieldInfo)?.GetValue(target); + } catch { + /* ignored */ + } + } + + return objectDictionary; + } + + private String ResolveDictionary(IDictionary items, Int32 depth) { + this.Append(OpenObjectChar, depth); + this.AppendLine(); + + // Iterate through the elements and output recursively + Int32 writeCount = 0; + foreach(Object? key in items.Keys) { + // Serialize and append the key (first char indented) + this.Append(StringQuotedChar, depth + 1); + Escape(key?.ToString()!, this._builder!); + _ = this._builder?.Append(StringQuotedChar).Append(ValueSeparatorChar).Append(" "); + + // Serialize and append the value + String serializedValue = Serialize(items[key!], depth + 1, this._options!); + + if(IsNonEmptyJsonArrayOrObject(serializedValue)) { + this.AppendLine(); + } + + this.Append(serializedValue, 0); + + // Add a comma and start a new line -- We will remove the last one when we are done writing the elements + this.Append(FieldSeparatorChar, 0); + this.AppendLine(); + writeCount++; + } + + // Output the end of the object and set the result + this.RemoveLastComma(); + this.Append(CloseObjectChar, writeCount > 0 ? depth : 0); + return this._builder!.ToString(); + } + + private String ResolveObject(Object target, Int32 depth) { + Type targetType = target.GetType(); + + if(targetType.IsEnum) { + return Convert.ToInt64(target, System.Globalization.CultureInfo.InvariantCulture).ToString(); + } + + global::System.Collections.Generic.Dictionary fields = this._options!.GetProperties(targetType); + + if(fields.Count == 0 && String.IsNullOrWhiteSpace(this._options.TypeSpecifier)) { + return EmptyObjectLiteral; + } + + // If we arrive here, then we convert the object into a + // dictionary of property names and values and call the serialization + // function again + global::System.Collections.Generic.Dictionary objectDictionary = this.CreateDictionary(fields, targetType.ToString(), target); + + return Serialize(objectDictionary, depth, this._options); + } + + private String ResolveEnumerable(IEnumerable target, Int32 depth) { + // Cast the items as a generic object array + global::System.Collections.Generic.IEnumerable items = target.Cast(); + + this.Append(OpenArrayChar, depth); + this.AppendLine(); + + // Iterate through the elements and output recursively + Int32 writeCount = 0; + foreach(Object entry in items) { + String serializedValue = Serialize(entry, depth + 1, this._options!); + + if(IsNonEmptyJsonArrayOrObject(serializedValue)) { + this.Append(serializedValue, 0); + } else { + this.Append(serializedValue, depth + 1); + } + + this.Append(FieldSeparatorChar, 0); + this.AppendLine(); + writeCount++; + } + + // Output the end of the array and set the result + this.RemoveLastComma(); + this.Append(CloseArrayChar, writeCount > 0 ? depth : 0); + return this._builder!.ToString(); + } + + private void SetIndent(Int32 depth) { + if(this._options!.Format == false || depth <= 0) { + return; + } + + _ = this._builder!.Append(IndentStrings.GetOrAdd(depth, x => new String(' ', x * 4))); + } + + /// + /// Removes the last comma in the current string builder. + /// + private void RemoveLastComma() { + if(this._builder!.Length < this._lastCommaSearch!.Length) { + return; + } + + if(this._lastCommaSearch.Where((t, i) => this._builder[this._builder.Length - this._lastCommaSearch.Length + i] != t).Any()) { + return; + } + + // If we got this far, we simply remove the comma character + _ = this._builder.Remove(this._builder.Length - this._lastCommaSearch.Length, 1); + } + + private void Append(String text, Int32 depth) { + this.SetIndent(depth); + _ = this._builder!.Append(text); + } + + private void Append(Char text, Int32 depth) { + this.SetIndent(depth); + _ = this._builder!.Append(text); + } + + private void AppendLine() { + if(this._options!.Format == false) { + return; + } + + _ = this._builder!.Append(Environment.NewLine); + } + + #endregion + } + } +} diff --git a/Swan.Tiny/Formatters/Json.SerializerOptions.cs b/Swan.Tiny/Formatters/Json.SerializerOptions.cs new file mode 100644 index 0000000..ff4c9bf --- /dev/null +++ b/Swan.Tiny/Formatters/Json.SerializerOptions.cs @@ -0,0 +1,133 @@ +#nullable enable +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Swan.Reflection; + +namespace Swan.Formatters { + /// + /// A very simple, light-weight JSON library written by Mario + /// to teach Geo how things are done + /// + /// This is an useful helper for small tasks but it doesn't represent a full-featured + /// serializer such as the beloved Json.NET. + /// + public class SerializerOptions { + private static readonly ConcurrentDictionary, MemberInfo>> + TypeCache = new ConcurrentDictionary, MemberInfo>>(); + + private readonly String[]? _includeProperties; + private readonly String[]? _excludeProperties; + private readonly Dictionary> _parentReferences = new Dictionary>(); + + /// + /// Initializes a new instance of the class. + /// + /// if set to true [format]. + /// The type specifier. + /// The include properties. + /// The exclude properties. + /// if set to true [include non public]. + /// The parent references. + /// The json serializer case. + public SerializerOptions(Boolean format, String? typeSpecifier, String[]? includeProperties, String[]? excludeProperties = null, Boolean includeNonPublic = true, IReadOnlyCollection? parentReferences = null, JsonSerializerCase jsonSerializerCase = JsonSerializerCase.None) { + this._includeProperties = includeProperties; + this._excludeProperties = excludeProperties; + + this.IncludeNonPublic = includeNonPublic; + this.Format = format; + this.TypeSpecifier = typeSpecifier; + this.JsonSerializerCase = jsonSerializerCase; + + if(parentReferences == null) { + return; + } + + foreach(WeakReference parentReference in parentReferences.Where(x => x.IsAlive)) { + _ = this.IsObjectPresent(parentReference.Target); + } + } + + /// + /// Gets a value indicating whether this is format. + /// + /// + /// true if format; otherwise, false. + /// + public Boolean Format { + get; + } + + /// + /// Gets the type specifier. + /// + /// + /// The type specifier. + /// + public String? TypeSpecifier { + get; + } + + /// + /// Gets a value indicating whether [include non public]. + /// + /// + /// true if [include non public]; otherwise, false. + /// + public Boolean IncludeNonPublic { + get; + } + + /// + /// Gets the json serializer case. + /// + /// + /// The json serializer case. + /// + public JsonSerializerCase JsonSerializerCase { + get; + } + + internal Boolean IsObjectPresent(Object? target) { + if(target == null) { + return false; + } + Int32 hashCode = target.GetHashCode(); + + if(this._parentReferences.ContainsKey(hashCode)) { + if(this._parentReferences[hashCode].Any(p => ReferenceEquals(p.Target, target))) { + return true; + } + + this._parentReferences[hashCode].Add(new WeakReference(target)); + return false; + } + + this._parentReferences.Add(hashCode, new List { new WeakReference(target) }); + return false; + } + + internal Dictionary GetProperties(Type targetType) => this.GetPropertiesCache(targetType).When(() => this._includeProperties?.Length > 0, query => query.Where(p => this._includeProperties.Contains(p.Key.Item1))).When(() => this._excludeProperties?.Length > 0, query => query.Where(p => !this._excludeProperties.Contains(p.Key.Item1))).ToDictionary(x => x.Key.Item2, x => x.Value); + + private Dictionary, MemberInfo> GetPropertiesCache(Type targetType) { + if(TypeCache.TryGetValue(targetType, out Dictionary, MemberInfo>? current)) { + return current; + } + + List fields = new List(PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties(targetType).Where(p => p.CanRead)); + + // If the target is a struct (value type) navigate the fields. + if(targetType.IsValueType) { + fields.AddRange(FieldTypeCache.DefaultCache.Value.RetrieveAllFields(targetType)); + } + + Dictionary, MemberInfo> value = fields.ToDictionary(x => Tuple.Create(x.Name, x.GetCustomAttribute()?.PropertyName ?? x.Name.GetNameWithCase(this.JsonSerializerCase)), x => x); + + TypeCache.TryAdd(targetType, value); + + return value; + } + } +} diff --git a/Swan.Tiny/Formatters/Json.cs b/Swan.Tiny/Formatters/Json.cs new file mode 100644 index 0000000..c28d76f --- /dev/null +++ b/Swan.Tiny/Formatters/Json.cs @@ -0,0 +1,340 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using Swan.Collections; +using Swan.Reflection; + +namespace Swan.Formatters { + /// + /// A very simple, light-weight JSON library written by Mario + /// to teach Geo how things are done + /// + /// This is an useful helper for small tasks but it doesn't represent a full-featured + /// serializer such as the beloved Json.NET. + /// + public static partial class Json { + #region Constants + + internal const String AddMethodName = "Add"; + + private const Char OpenObjectChar = '{'; + private const Char CloseObjectChar = '}'; + + private const Char OpenArrayChar = '['; + private const Char CloseArrayChar = ']'; + + private const Char FieldSeparatorChar = ','; + private const Char ValueSeparatorChar = ':'; + + private const Char StringEscapeChar = '\\'; + private const Char StringQuotedChar = '"'; + + private const String EmptyObjectLiteral = "{ }"; + private const String EmptyArrayLiteral = "[ ]"; + private const String TrueLiteral = "true"; + private const String FalseLiteral = "false"; + private const String NullLiteral = "null"; + + #endregion + + private static readonly CollectionCacheRepository IgnoredPropertiesCache = new CollectionCacheRepository(); + + #region Public API + + /// + /// Serializes the specified object into a JSON string. + /// + /// The object. + /// if set to true it formats and indents the output. + /// The type specifier. Leave null or empty to avoid setting. + /// if set to true non-public getters will be also read. + /// The included property names. + /// The excluded property names. + /// + /// A that represents the current object. + /// + /// + /// The following example describes how to serialize a simple object. + /// + /// using Swan.Formatters; + /// + /// class Example + /// { + /// static void Main() + /// { + /// var obj = new { One = "One", Two = "Two" }; + /// + /// var serial = Json.Serialize(obj); // {"One": "One","Two": "Two"} + /// } + /// } + /// + /// + /// The following example details how to serialize an object using the . + /// + /// + /// using Swan.Attributes; + /// using Swan.Formatters; + /// + /// class Example + /// { + /// class JsonPropertyExample + /// { + /// [JsonProperty("data")] + /// public string Data { get; set; } + /// + /// [JsonProperty("ignoredData", true)] + /// public string IgnoredData { get; set; } + /// } + /// + /// static void Main() + /// { + /// var obj = new JsonPropertyExample() { Data = "OK", IgnoredData = "OK" }; + /// + /// // {"data": "OK"} + /// var serializedObj = Json.Serialize(obj); + /// } + /// } + /// + /// + public static String Serialize(Object? obj, Boolean format = false, String? typeSpecifier = null, Boolean includeNonPublic = false, String[]? includedNames = null, params String[] excludedNames) => Serialize(obj, format, typeSpecifier, includeNonPublic, includedNames, excludedNames, null, JsonSerializerCase.None); + + /// + /// Serializes the specified object into a JSON string. + /// + /// The object. + /// The json serializer case. + /// if set to true [format]. + /// The type specifier. + /// + /// A that represents the current object. + /// + public static String Serialize(Object? obj, JsonSerializerCase jsonSerializerCase, Boolean format = false, String? typeSpecifier = null) => Serialize(obj, format, typeSpecifier, false, null, null, null, jsonSerializerCase); + + /// + /// Serializes the specified object into a JSON string. + /// + /// The object. + /// if set to true it formats and indents the output. + /// The type specifier. Leave null or empty to avoid setting. + /// if set to true non-public getters will be also read. + /// The included property names. + /// The excluded property names. + /// The parent references. + /// The json serializer case. + /// + /// A that represents the current object. + /// + public static String Serialize(Object? obj, Boolean format, String? typeSpecifier, Boolean includeNonPublic, String[]? includedNames, String[]? excludedNames, List? parentReferences, JsonSerializerCase jsonSerializerCase) { + if(obj != null && (obj is String || Definitions.AllBasicValueTypes.Contains(obj.GetType()))) { + return SerializePrimitiveValue(obj); + } + + SerializerOptions options = new SerializerOptions(format, typeSpecifier, includedNames, GetExcludedNames(obj?.GetType(), excludedNames), includeNonPublic, parentReferences, jsonSerializerCase); + + return Serialize(obj, options); + } + + /// + /// Serializes the specified object using the SerializerOptions provided. + /// + /// The object. + /// The options. + /// + /// A that represents the current object. + /// + public static String Serialize(Object? obj, SerializerOptions options) => Serializer.Serialize(obj, 0, options); + + /// + /// Serializes the specified object only including the specified property names. + /// + /// The object. + /// if set to true it formats and indents the output. + /// The include names. + /// A that represents the current object. + /// + /// The following example shows how to serialize a simple object including the specified properties. + /// + /// using Swan.Formatters; + /// + /// class Example + /// { + /// static void Main() + /// { + /// // object to serialize + /// var obj = new { One = "One", Two = "Two", Three = "Three" }; + /// + /// // the included names + /// var includedNames = new[] { "Two", "Three" }; + /// + /// // serialize only the included names + /// var data = Json.SerializeOnly(basicObject, true, includedNames); + /// // {"Two": "Two","Three": "Three" } + /// } + /// } + /// + /// + public static String SerializeOnly(Object? obj, Boolean format, params String[] includeNames) => Serialize(obj, new SerializerOptions(format, null, includeNames)); + + /// + /// Serializes the specified object excluding the specified property names. + /// + /// The object. + /// if set to true it formats and indents the output. + /// The exclude names. + /// A that represents the current object. + /// + /// The following code shows how to serialize a simple object excluding the specified properties. + /// + /// using Swan.Formatters; + /// + /// class Example + /// { + /// static void Main() + /// { + /// // object to serialize + /// var obj = new { One = "One", Two = "Two", Three = "Three" }; + /// + /// // the excluded names + /// var excludeNames = new[] { "Two", "Three" }; + /// + /// // serialize excluding + /// var data = Json.SerializeExcluding(basicObject, false, includedNames); + /// // {"One": "One"} + /// } + /// } + /// + /// + public static String SerializeExcluding(Object? obj, Boolean format, params String[] excludeNames) => Serialize(obj, new SerializerOptions(format, null, null, excludeNames)); + + /// + /// Deserializes the specified json string as either a Dictionary[string, object] or as a List[object] + /// depending on the syntax of the JSON string. + /// + /// The JSON string. + /// The json serializer case. + /// + /// Type of the current deserializes. + /// + /// + /// The following code shows how to deserialize a JSON string into a Dictionary. + /// + /// using Swan.Formatters; + /// class Example + /// { + /// static void Main() + /// { + /// // json to deserialize + /// var basicJson = "{\"One\":\"One\",\"Two\":\"Two\",\"Three\":\"Three\"}"; + /// // deserializes the specified json into a Dictionary<string, object>. + /// var data = Json.Deserialize(basicJson, JsonSerializerCase.None); + /// } + /// } + /// + public static Object? Deserialize(String? json, JsonSerializerCase jsonSerializerCase) => Converter.FromJsonResult(Deserializer.DeserializeInternal(json), jsonSerializerCase); + + /// + /// Deserializes the specified json string as either a Dictionary[string, object] or as a List[object] + /// depending on the syntax of the JSON string. + /// + /// The JSON string. + /// + /// Type of the current deserializes. + /// + /// + /// The following code shows how to deserialize a JSON string into a Dictionary. + /// + /// using Swan.Formatters; + /// class Example + /// { + /// static void Main() + /// { + /// // json to deserialize + /// var basicJson = "{\"One\":\"One\",\"Two\":\"Two\",\"Three\":\"Three\"}"; + /// // deserializes the specified json into a Dictionary<string, object>. + /// var data = Json.Deserialize(basicJson); + /// } + /// } + /// + public static Object? Deserialize(String? json) => Deserialize(json, JsonSerializerCase.None); + + /// + /// Deserializes the specified JSON string and converts it to the specified object type. + /// Non-public constructors and property setters are ignored. + /// + /// The type of object to deserialize. + /// The JSON string. + /// The JSON serializer case. + /// + /// The deserialized specified type object. + /// + /// + /// The following code describes how to deserialize a JSON string into an object of type T. + /// + /// using Swan.Formatters; + /// class Example + /// { + /// static void Main() + /// { + /// // json type BasicJson to serialize + /// var basicJson = "{\"One\":\"One\",\"Two\":\"Two\",\"Three\":\"Three\"}"; + /// // deserializes the specified string in a new instance of the type BasicJson. + /// var data = Json.Deserialize<BasicJson>(basicJson); + /// } + /// } + /// + public static T Deserialize(String json, JsonSerializerCase jsonSerializerCase = JsonSerializerCase.None) where T : notnull => (T)Deserialize(json, typeof(T), jsonSerializerCase: jsonSerializerCase)!; + + /// + /// Deserializes the specified JSON string and converts it to the specified object type. + /// + /// The type of object to deserialize. + /// The JSON string. + /// if set to true, it also uses the non-public constructors and property setters. + /// The deserialized specified type object. + public static T Deserialize(String json, Boolean includeNonPublic) where T : notnull => (T)Deserialize(json, typeof(T), includeNonPublic)!; + + /// + /// Deserializes the specified JSON string and converts it to the specified object type. + /// + /// The JSON string. + /// Type of the result. + /// if set to true, it also uses the non-public constructors and property setters. + /// The json serializer case. + /// + /// Type of the current conversion from json result. + /// + public static Object? Deserialize(String json, Type resultType, Boolean includeNonPublic = false, JsonSerializerCase jsonSerializerCase = JsonSerializerCase.None) => Converter.FromJsonResult(Deserializer.DeserializeInternal(json), jsonSerializerCase, resultType, includeNonPublic); + + #endregion + + #region Private API + + private static String[]? GetExcludedNames(Type? type, String[]? excludedNames) { + if(type == null) { + return excludedNames; + } + + global::System.Collections.Generic.IEnumerable excludedByAttr = IgnoredPropertiesCache.Retrieve(type, t => t.GetProperties() + .Where(x => AttributeCache.DefaultCache.Value.RetrieveOne(x)?.Ignored == true) + .Select(x => x.Name)); + + if(excludedByAttr?.Any() != true) { + return excludedNames; + } + + return excludedNames?.Any(String.IsNullOrWhiteSpace) == true + ? excludedByAttr.Intersect(excludedNames.Where(y => !String.IsNullOrWhiteSpace(y))).ToArray() + : excludedByAttr.ToArray(); + } + + private static String SerializePrimitiveValue(Object obj) => obj switch + { + String stringValue => stringValue, + Boolean boolValue => boolValue ? TrueLiteral : FalseLiteral, + _ => obj.ToString()! + }; + + #endregion + } +} diff --git a/Swan.Tiny/Formatters/JsonPropertyAttribute.cs b/Swan.Tiny/Formatters/JsonPropertyAttribute.cs new file mode 100644 index 0000000..0a569af --- /dev/null +++ b/Swan.Tiny/Formatters/JsonPropertyAttribute.cs @@ -0,0 +1,40 @@ +using System; + +namespace Swan.Formatters { + /// + /// An attribute used to help setup a property behavior when serialize/deserialize JSON. + /// + /// + [AttributeUsage(AttributeTargets.Property)] + public sealed class JsonPropertyAttribute : Attribute { + /// + /// Initializes a new instance of the class. + /// + /// Name of the property. + /// if set to true [ignored]. + public JsonPropertyAttribute(String propertyName, Boolean ignored = false) { + this.PropertyName = propertyName ?? throw new ArgumentNullException(nameof(propertyName)); + this.Ignored = ignored; + } + + /// + /// Gets or sets the name of the property. + /// + /// + /// The name of the property. + /// + public String PropertyName { + get; + } + + /// + /// Gets or sets a value indicating whether this is ignored. + /// + /// + /// true if ignored; otherwise, false. + /// + public Boolean Ignored { + get; + } + } +} \ No newline at end of file diff --git a/Swan.Tiny/Logging/ConsoleLogger.cs b/Swan.Tiny/Logging/ConsoleLogger.cs new file mode 100644 index 0000000..6665fe6 --- /dev/null +++ b/Swan.Tiny/Logging/ConsoleLogger.cs @@ -0,0 +1,139 @@ +using System; +using Swan.Lite.Logging; + +namespace Swan.Logging { + /// + /// Represents a Console implementation of ILogger. + /// + /// + public class ConsoleLogger : TextLogger, ILogger { + /// + /// Initializes a new instance of the class. + /// + protected ConsoleLogger() { + // Empty + } + + /// + /// Gets the current instance of ConsoleLogger. + /// + /// + /// The instance. + /// + public static ConsoleLogger Instance { get; } = new ConsoleLogger(); + + /// + /// Gets or sets the debug logging prefix. + /// + /// + /// The debug prefix. + /// + public static String DebugPrefix { get; set; } = "DBG"; + + /// + /// Gets or sets the trace logging prefix. + /// + /// + /// The trace prefix. + /// + public static String TracePrefix { get; set; } = "TRC"; + + /// + /// Gets or sets the warning logging prefix. + /// + /// + /// The warn prefix. + /// + public static String WarnPrefix { get; set; } = "WRN"; + + /// + /// Gets or sets the fatal logging prefix. + /// + /// + /// The fatal prefix. + /// + public static String FatalPrefix { get; set; } = "FAT"; + + /// + /// Gets or sets the error logging prefix. + /// + /// + /// The error prefix. + /// + public static String ErrorPrefix { get; set; } = "ERR"; + + /// + /// Gets or sets the information logging prefix. + /// + /// + /// The information prefix. + /// + public static String InfoPrefix { get; set; } = "INF"; + + /// + /// Gets or sets the color of the information output logging. + /// + /// + /// The color of the information. + /// + public static ConsoleColor InfoColor { get; set; } = ConsoleColor.Cyan; + + /// + /// Gets or sets the color of the debug output logging. + /// + /// + /// The color of the debug. + /// + public static ConsoleColor DebugColor { get; set; } = ConsoleColor.Gray; + + /// + /// Gets or sets the color of the trace output logging. + /// + /// + /// The color of the trace. + /// + public static ConsoleColor TraceColor { get; set; } = ConsoleColor.DarkGray; + + /// + /// Gets or sets the color of the warning logging. + /// + /// + /// The color of the warn. + /// + public static ConsoleColor WarnColor { get; set; } = ConsoleColor.Yellow; + + /// + /// Gets or sets the color of the error logging. + /// + /// + /// The color of the error. + /// + public static ConsoleColor ErrorColor { get; set; } = ConsoleColor.DarkRed; + + /// + /// Gets or sets the color of the error logging. + /// + /// + /// The color of the error. + /// + public static ConsoleColor FatalColor { get; set; } = ConsoleColor.Red; + + /// + public LogLevel LogLevel { get; set; } = DebugLogger.IsDebuggerAttached ? LogLevel.Trace : LogLevel.Info; + + /// + public void Log(LogMessageReceivedEventArgs logEvent) { + // Select the writer based on the message type + TerminalWriters writer = logEvent.MessageType == LogLevel.Error ? TerminalWriters.StandardError : TerminalWriters.StandardOutput; + + (String outputMessage, ConsoleColor color) = this.GetOutputAndColor(logEvent); + + Terminal.Write(outputMessage, color, writer); + } + + /// + public void Dispose() { + // Do nothing + } + } +} diff --git a/Swan.Tiny/Logging/DebugLogger.cs b/Swan.Tiny/Logging/DebugLogger.cs new file mode 100644 index 0000000..9aeb43f --- /dev/null +++ b/Swan.Tiny/Logging/DebugLogger.cs @@ -0,0 +1,49 @@ +using System; +using Swan.Lite.Logging; + +namespace Swan.Logging { + /// + /// Represents a logger target. This target will write to the + /// Debug console using System.Diagnostics.Debug. + /// + /// + public class DebugLogger : TextLogger, ILogger { + /// + /// Initializes a new instance of the class. + /// + protected DebugLogger() { + // Empty + } + + /// + /// Gets the current instance of DebugLogger. + /// + /// + /// The instance. + /// + public static DebugLogger Instance { get; } = new DebugLogger(); + + /// + /// Gets a value indicating whether a debugger is attached. + /// + /// + /// true if this instance is debugger attached; otherwise, false. + /// + public static Boolean IsDebuggerAttached => System.Diagnostics.Debugger.IsAttached; + + /// + public LogLevel LogLevel { get; set; } = IsDebuggerAttached ? LogLevel.Trace : LogLevel.None; + + /// + public void Log(LogMessageReceivedEventArgs logEvent) { + (String outputMessage, ConsoleColor _) = this.GetOutputAndColor(logEvent); + + System.Diagnostics.Debug.Write(outputMessage); + } + + /// + public void Dispose() { + // do nothing + } + } +} diff --git a/Swan.Tiny/Logging/ILogger.cs b/Swan.Tiny/Logging/ILogger.cs new file mode 100644 index 0000000..26107a7 --- /dev/null +++ b/Swan.Tiny/Logging/ILogger.cs @@ -0,0 +1,24 @@ +using System; + +namespace Swan.Logging { + /// + /// Interface for a logger implementation. + /// + public interface ILogger : IDisposable { + /// + /// Gets the log level. + /// + /// + /// The log level. + /// + LogLevel LogLevel { + get; + } + + /// + /// Logs the specified log event. + /// + /// The instance containing the event data. + void Log(LogMessageReceivedEventArgs logEvent); + } +} diff --git a/Swan.Tiny/Logging/LogLevel.cs b/Swan.Tiny/Logging/LogLevel.cs new file mode 100644 index 0000000..92a9307 --- /dev/null +++ b/Swan.Tiny/Logging/LogLevel.cs @@ -0,0 +1,41 @@ +namespace Swan { + /// + /// Defines the log levels. + /// + public enum LogLevel { + /// + /// The none message type + /// + None, + + /// + /// The trace message type + /// + Trace, + + /// + /// The debug message type + /// + Debug, + + /// + /// The information message type + /// + Info, + + /// + /// The warning message type + /// + Warning, + + /// + /// The error message type + /// + Error, + + /// + /// The fatal message type + /// + Fatal, + } +} \ No newline at end of file diff --git a/Swan.Tiny/Logging/LogMessageReceivedEventArgs.cs b/Swan.Tiny/Logging/LogMessageReceivedEventArgs.cs new file mode 100644 index 0000000..97e5fa4 --- /dev/null +++ b/Swan.Tiny/Logging/LogMessageReceivedEventArgs.cs @@ -0,0 +1,147 @@ +#nullable enable +using System; + +namespace Swan { + /// + /// Event arguments representing the message that is logged + /// on to the terminal. Use the properties to forward the data to + /// your logger of choice. + /// + /// + public class LogMessageReceivedEventArgs : EventArgs { + /// + /// Initializes a new instance of the class. + /// + /// The sequence. + /// Type of the message. + /// The UTC date. + /// The source. + /// The message. + /// The extended data. + /// Name of the caller member. + /// The caller file path. + /// The caller line number. + public LogMessageReceivedEventArgs( + UInt64 sequence, + LogLevel messageType, + DateTime utcDate, + String source, + String message, + Object? extendedData, + String callerMemberName, + String callerFilePath, + Int32 callerLineNumber) { + this.Sequence = sequence; + this.MessageType = messageType; + this.UtcDate = utcDate; + this.Source = source; + this.Message = message; + this.CallerMemberName = callerMemberName; + this.CallerFilePath = callerFilePath; + this.CallerLineNumber = callerLineNumber; + this.ExtendedData = extendedData; + } + + /// + /// Gets logging message sequence. + /// + /// + /// The sequence. + /// + public UInt64 Sequence { + get; + } + + /// + /// Gets the type of the message. + /// It can be a combination as the enumeration is a set of bitwise flags. + /// + /// + /// The type of the message. + /// + public LogLevel MessageType { + get; + } + + /// + /// Gets the UTC date at which the event at which the message was logged. + /// + /// + /// The UTC date. + /// + public DateTime UtcDate { + get; + } + + /// + /// Gets the name of the source where the logging message + /// came from. This can come empty if the logger did not set it. + /// + /// + /// The source. + /// + public String Source { + get; + } + + /// + /// Gets the body of the message. + /// + /// + /// The message. + /// + public String Message { + get; + } + + /// + /// Gets the name of the caller member. + /// + /// + /// The name of the caller member. + /// + public String CallerMemberName { + get; + } + + /// + /// Gets the caller file path. + /// + /// + /// The caller file path. + /// + public String CallerFilePath { + get; + } + + /// + /// Gets the caller line number. + /// + /// + /// The caller line number. + /// + public Int32 CallerLineNumber { + get; + } + + /// + /// Gets an object representing extended data. + /// It could be an exception or anything else. + /// + /// + /// The extended data. + /// + public Object? ExtendedData { + get; + } + + /// + /// Gets the Extended Data properties cast as an Exception (if possible) + /// Otherwise, it return null. + /// + /// + /// The exception. + /// + public Exception? Exception => this.ExtendedData as Exception; + } +} diff --git a/Swan.Tiny/Logging/Logger.cs b/Swan.Tiny/Logging/Logger.cs new file mode 100644 index 0000000..2bedf85 --- /dev/null +++ b/Swan.Tiny/Logging/Logger.cs @@ -0,0 +1,423 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace Swan.Logging { + /// + /// Entry-point for logging. Use this static class to register/unregister + /// loggers instances. By default, the ConsoleLogger is registered. + /// + public static class Logger { + private static readonly Object SyncLock = new Object(); + private static readonly List Loggers = new List(); + + private static UInt64 _loggingSequence; + + static Logger() { + if(Terminal.IsConsolePresent) { + Loggers.Add(ConsoleLogger.Instance); + } + + if(DebugLogger.IsDebuggerAttached) { + Loggers.Add(DebugLogger.Instance); + } + } + + #region Standard Public API + + /// + /// Registers the logger. + /// + /// The type of logger to register. + /// There is already a logger with that class registered. + public static void RegisterLogger() where T : ILogger { + lock(SyncLock) { + ILogger loggerInstance = Loggers.FirstOrDefault(x => x.GetType() == typeof(T)); + + if(loggerInstance != null) { + throw new InvalidOperationException("There is already a logger with that class registered."); + } + + Loggers.Add(Activator.CreateInstance()); + } + } + + /// + /// Registers the logger. + /// + /// The logger. + public static void RegisterLogger(ILogger logger) { + lock(SyncLock) { + Loggers.Add(logger); + } + } + + /// + /// Unregisters the logger. + /// + /// The logger. + /// logger. + public static void UnregisterLogger(ILogger logger) => RemoveLogger(x => x == logger); + + /// + /// Unregisters the logger. + /// + /// The type of logger to unregister. + public static void UnregisterLogger() => RemoveLogger(x => x.GetType() == typeof(T)); + + /// + /// Remove all the loggers. + /// + public static void NoLogging() { + lock(SyncLock) { + Loggers.Clear(); + } + } + + #region Debug + + /// + /// Logs a debug message to the console. + /// + /// The message. + /// The source. + /// The extended data. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Debug(this String message, String source = null, Object extendedData = null, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(LogLevel.Debug, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); + + /// + /// Logs a debug message to the console. + /// + /// The message. + /// The source. + /// The extended data. + /// Name of the caller member. + /// The caller file path. + /// The caller line number. + public static void Debug(this String message, Type source, Object extendedData = null, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(LogLevel.Debug, message, source?.FullName, extendedData, callerMemberName, callerFilePath, callerLineNumber); + + /// + /// Logs a debug message to the console. + /// + /// The exception. + /// The source. + /// The message. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Debug(this Exception extendedData, String source, String message, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(LogLevel.Debug, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); + + #endregion + + #region Trace + + /// + /// Logs a trace message to the console. + /// + /// The text. + /// The source. + /// The extended data. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Trace(this String message, String source = null, Object extendedData = null, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(LogLevel.Trace, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); + + /// + /// Logs a trace message to the console. + /// + /// The message. + /// The source. + /// The extended data. + /// Name of the caller member. + /// The caller file path. + /// The caller line number. + public static void Trace(this String message, Type source, Object extendedData = null, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(LogLevel.Trace, message, source?.FullName, extendedData, callerMemberName, callerFilePath, callerLineNumber); + + /// + /// Logs a trace message to the console. + /// + /// The extended data. + /// The source. + /// The message. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Trace(this Exception extendedData, String source, String message, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(LogLevel.Trace, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); + + #endregion + + #region Warn + + /// + /// Logs a warning message to the console. + /// + /// The text. + /// The source. + /// The extended data. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Warn(this String message, String source = null, Object extendedData = null, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(LogLevel.Warning, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); + + /// + /// Logs a warning message to the console. + /// + /// The message. + /// The source. + /// The extended data. + /// Name of the caller member. + /// The caller file path. + /// The caller line number. + public static void Warn(this String message, Type source, Object extendedData = null, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(LogLevel.Warning, message, source?.FullName, extendedData, callerMemberName, callerFilePath, callerLineNumber); + + /// + /// Logs a warning message to the console. + /// + /// The extended data. + /// The source. + /// The message. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Warn(this Exception extendedData, String source, String message, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(LogLevel.Warning, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); + + #endregion + + #region Fatal + + /// + /// Logs a warning message to the console. + /// + /// The text. + /// The source. + /// The extended data. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Fatal(this String message, String source = null, Object extendedData = null, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(LogLevel.Fatal, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); + + /// + /// Logs a warning message to the console. + /// + /// The message. + /// The source. + /// The extended data. + /// Name of the caller member. + /// The caller file path. + /// The caller line number. + public static void Fatal(this String message, Type source, Object extendedData = null, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(LogLevel.Fatal, message, source?.FullName, extendedData, callerMemberName, callerFilePath, callerLineNumber); + + /// + /// Logs a warning message to the console. + /// + /// The extended data. + /// The source. + /// The message. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Fatal(this Exception extendedData, String source, String message, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(LogLevel.Fatal, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); + + #endregion + + #region Info + + /// + /// Logs an info message to the console. + /// + /// The text. + /// The source. + /// The extended data. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Info(this String message, String source = null, Object extendedData = null, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(LogLevel.Info, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); + + /// + /// Logs an info message to the console. + /// + /// The message. + /// The source. + /// The extended data. + /// Name of the caller member. + /// The caller file path. + /// The caller line number. + public static void Info(this String message, Type source, Object extendedData = null, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(LogLevel.Info, message, source?.FullName, extendedData, callerMemberName, callerFilePath, callerLineNumber); + + /// + /// Logs an info message to the console. + /// + /// The extended data. + /// The source. + /// The message. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Info(this Exception extendedData, String source, String message, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(LogLevel.Info, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); + + #endregion + + #region Error + + /// + /// Logs an error message to the console's standard error. + /// + /// The text. + /// The source. + /// The extended data. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Error(this String message, String source = null, Object extendedData = null, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(LogLevel.Error, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); + + /// + /// Logs an error message to the console's standard error. + /// + /// The message. + /// The source. + /// The extended data. + /// Name of the caller member. + /// The caller file path. + /// The caller line number. + public static void Error(this String message, Type source, Object extendedData = null, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(LogLevel.Error, message, source?.FullName, extendedData, callerMemberName, callerFilePath, callerLineNumber); + + /// + /// Logs an error message to the console's standard error. + /// + /// The exception. + /// The source. + /// The message. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Error(this Exception ex, String source, String message, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(LogLevel.Error, message, source, ex, callerMemberName, callerFilePath, callerLineNumber); + + #endregion + + #endregion + + #region Extended Public API + + /// + /// Logs the specified message. + /// + /// The message. + /// The source. + /// Type of the message. + /// The extended data. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Log(this String message, String source, LogLevel messageType, Object extendedData = null, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(messageType, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); + + /// + /// Logs the specified message. + /// + /// The message. + /// The source. + /// Type of the message. + /// The extended data. + /// Name of the caller member. + /// The caller file path. + /// The caller line number. + public static void Log(this String message, Type source, LogLevel messageType, Object extendedData = null, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(messageType, message, source?.FullName, extendedData, callerMemberName, callerFilePath, callerLineNumber); + + /// + /// Logs an error message to the console's standard error. + /// + /// The ex. + /// The source. + /// The message. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Log(this Exception ex, String source = null, String message = null, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(LogLevel.Error, message ?? ex.Message, source ?? ex.Source, ex, callerMemberName, callerFilePath, callerLineNumber); + + /// + /// Logs an error message to the console's standard error. + /// + /// The ex. + /// The source. + /// The message. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Log(this Exception ex, Type source = null, String message = null, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(LogLevel.Error, message ?? ex.Message, source?.FullName ?? ex.Source, ex, callerMemberName, callerFilePath, callerLineNumber); + + /// + /// Logs a trace message showing all possible non-null properties of the given object + /// This method is expensive as it uses Stringify internally. + /// + /// The object. + /// The source. + /// The title. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Dump(this Object obj, String source, String text = "Object Dump", [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) { + if(obj == null) { + return; + } + + String message = $"{text} ({obj.GetType()}): {Environment.NewLine}{obj.Stringify().Indent(5)}"; + LogMessage(LogLevel.Trace, message, source, obj, callerMemberName, callerFilePath, callerLineNumber); + } + + /// + /// Logs a trace message showing all possible non-null properties of the given object + /// This method is expensive as it uses Stringify internally. + /// + /// The object. + /// The source. + /// The text. + /// Name of the caller member. + /// The caller file path. + /// The caller line number. + public static void Dump(this Object obj, Type source, String text = "Object Dump", [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) { + if(obj == null) { + return; + } + + String message = $"{text} ({obj.GetType()}): {Environment.NewLine}{obj.Stringify().Indent(5)}"; + LogMessage(LogLevel.Trace, message, source?.FullName, obj, callerMemberName, callerFilePath, callerLineNumber); + } + + #endregion + + private static void RemoveLogger(Func criteria) { + lock(SyncLock) { + ILogger loggerInstance = Loggers.FirstOrDefault(criteria); + + if(loggerInstance == null) { + throw new InvalidOperationException("The logger is not registered."); + } + + loggerInstance.Dispose(); + + _ = Loggers.Remove(loggerInstance); + } + } + + private static void LogMessage(LogLevel logLevel, String message, String sourceName, Object extendedData, String callerMemberName, String callerFilePath, Int32 callerLineNumber) { + UInt64 sequence = _loggingSequence; + DateTime date = DateTime.UtcNow; + _loggingSequence++; + + String loggerMessage = String.IsNullOrWhiteSpace(message) ? String.Empty : message.RemoveControlCharsExcept('\n'); + + LogMessageReceivedEventArgs eventArgs = new LogMessageReceivedEventArgs(sequence, logLevel, date, sourceName, loggerMessage, extendedData, callerMemberName, callerFilePath, callerLineNumber); + + foreach(ILogger logger in Loggers) { + _ = Task.Run(() => { + if(logger.LogLevel <= logLevel) { + logger.Log(eventArgs); + } + }); + } + } + } +} diff --git a/Swan.Tiny/Logging/TextLogger.cs b/Swan.Tiny/Logging/TextLogger.cs new file mode 100644 index 0000000..dcf97e8 --- /dev/null +++ b/Swan.Tiny/Logging/TextLogger.cs @@ -0,0 +1,63 @@ +using Swan.Logging; +using System; + +namespace Swan.Lite.Logging { + /// + /// Use this class for text-based logger. + /// + public abstract class TextLogger { + /// + /// Gets or sets the logging time format. + /// set to null or empty to prevent output. + /// + /// + /// The logging time format. + /// + public static String LoggingTimeFormat { get; set; } = "HH:mm:ss.fff"; + + /// + /// Gets the color of the output of the message (the output message has a new line char in the end). + /// + /// The instance containing the event data. + /// + /// The output message formatted and the color of the console to be used. + /// + protected (String outputMessage, ConsoleColor color) GetOutputAndColor(LogMessageReceivedEventArgs logEvent) { + (String prefix, ConsoleColor color) = GetConsoleColorAndPrefix(logEvent.MessageType); + + String loggerMessage = String.IsNullOrWhiteSpace(logEvent.Message) ? String.Empty : logEvent.Message.RemoveControlCharsExcept('\n'); + + String outputMessage = CreateOutputMessage(logEvent.Source, loggerMessage, prefix, logEvent.UtcDate); + + // Further format the output in the case there is an exception being logged + if(logEvent.MessageType == LogLevel.Error && logEvent.Exception != null) { + try { + outputMessage += $"{logEvent.Exception.Stringify().Indent()}{Environment.NewLine}"; + } catch { + // Ignore + } + } + + return (outputMessage, color); + } + + private static (String Prefix, ConsoleColor color) GetConsoleColorAndPrefix(LogLevel messageType) => messageType switch + { + LogLevel.Debug => (ConsoleLogger.DebugPrefix, ConsoleLogger.DebugColor), + LogLevel.Error => (ConsoleLogger.ErrorPrefix, ConsoleLogger.ErrorColor), + LogLevel.Info => (ConsoleLogger.InfoPrefix, ConsoleLogger.InfoColor), + LogLevel.Trace => (ConsoleLogger.TracePrefix, ConsoleLogger.TraceColor), + LogLevel.Warning => (ConsoleLogger.WarnPrefix, ConsoleLogger.WarnColor), + LogLevel.Fatal => (ConsoleLogger.FatalPrefix, ConsoleLogger.FatalColor), + _ => (new String(' ', ConsoleLogger.InfoPrefix.Length), Terminal.Settings.DefaultColor), + }; + + private static String CreateOutputMessage(String sourceName, String loggerMessage, String prefix, DateTime date) { + String friendlySourceName = String.IsNullOrWhiteSpace(sourceName) ? String.Empty : sourceName.SliceLength(sourceName.LastIndexOf('.') + 1, sourceName.Length); + + String outputMessage = String.IsNullOrWhiteSpace(sourceName) ? loggerMessage : $"[{friendlySourceName}] {loggerMessage}"; + + return String.IsNullOrWhiteSpace(LoggingTimeFormat) ? $" {prefix} >> {outputMessage}{Environment.NewLine}" : $" {date.ToLocalTime().ToString(LoggingTimeFormat)} {prefix} >> {outputMessage}{Environment.NewLine}"; + } + } +} diff --git a/Swan.Tiny/Mappers/CopyableAttribute.cs b/Swan.Tiny/Mappers/CopyableAttribute.cs new file mode 100644 index 0000000..c4af960 --- /dev/null +++ b/Swan.Tiny/Mappers/CopyableAttribute.cs @@ -0,0 +1,11 @@ +using System; + +namespace Swan.Mappers { + /// + /// Represents an attribute to select which properties are copyable between objects. + /// + /// + [AttributeUsage(AttributeTargets.Property)] + public class CopyableAttribute : Attribute { + } +} diff --git a/Swan.Tiny/Mappers/IObjectMap.cs b/Swan.Tiny/Mappers/IObjectMap.cs new file mode 100644 index 0000000..040ba54 --- /dev/null +++ b/Swan.Tiny/Mappers/IObjectMap.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Swan.Mappers { + /// + /// Interface object map. + /// + public interface IObjectMap { + /// + /// Gets or sets the map. + /// + Dictionary> Map { + get; + } + + /// + /// Gets or sets the type of the source. + /// + Type SourceType { + get; + } + + /// + /// Gets or sets the type of the destination. + /// + Type DestinationType { + get; + } + } +} diff --git a/Swan.Tiny/Mappers/ObjectMap.cs b/Swan.Tiny/Mappers/ObjectMap.cs new file mode 100644 index 0000000..e30549a --- /dev/null +++ b/Swan.Tiny/Mappers/ObjectMap.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +namespace Swan.Mappers { + /// + /// Represents an object map. + /// + /// The type of the source. + /// The type of the destination. + /// + public class ObjectMap : IObjectMap { + internal ObjectMap(IEnumerable intersect) { + this.SourceType = typeof(TSource); + this.DestinationType = typeof(TDestination); + this.Map = intersect.ToDictionary(property => this.DestinationType.GetProperty(property.Name), property => new List { this.SourceType.GetProperty(property.Name) }); + } + + /// + public Dictionary> Map { + get; + } + + /// + public Type SourceType { + get; + } + + /// + public Type DestinationType { + get; + } + + /// + /// Maps the property. + /// + /// The type of the destination property. + /// The type of the source property. + /// The destination property. + /// The source property. + /// + /// An object map representation of type of the destination property + /// and type of the source property. + /// + public ObjectMap MapProperty(Expression> destinationProperty, Expression> sourceProperty) { + PropertyInfo propertyDestinationInfo = (destinationProperty.Body as MemberExpression)?.Member as PropertyInfo; + + if(propertyDestinationInfo == null) { + throw new ArgumentException("Invalid destination expression", nameof(destinationProperty)); + } + + List sourceMembers = GetSourceMembers(sourceProperty); + + if(sourceMembers.Any() == false) { + throw new ArgumentException("Invalid source expression", nameof(sourceProperty)); + } + + // reverse order + sourceMembers.Reverse(); + this.Map[propertyDestinationInfo] = sourceMembers; + + return this; + } + + /// + /// Removes the map property. + /// + /// The type of the destination property. + /// The destination property. + /// + /// An object map representation of type of the destination property + /// and type of the source property. + /// + /// Invalid destination expression. + public ObjectMap RemoveMapProperty(Expression> destinationProperty) { + PropertyInfo propertyDestinationInfo = (destinationProperty.Body as MemberExpression)?.Member as PropertyInfo; + + if(propertyDestinationInfo == null) { + throw new ArgumentException("Invalid destination expression", nameof(destinationProperty)); + } + + if(this.Map.ContainsKey(propertyDestinationInfo)) { + _ = this.Map.Remove(propertyDestinationInfo); + } + + return this; + } + + private static List GetSourceMembers(Expression> sourceProperty) { + List sourceMembers = new List(); + MemberExpression initialExpression = sourceProperty.Body as MemberExpression; + + while(true) { + PropertyInfo propertySourceInfo = initialExpression?.Member as PropertyInfo; + + if(propertySourceInfo == null) { + break; + } + + sourceMembers.Add(propertySourceInfo); + initialExpression = initialExpression.Expression as MemberExpression; + } + + return sourceMembers; + } + } +} diff --git a/Swan.Tiny/Mappers/ObjectMapper.PropertyInfoComparer.cs b/Swan.Tiny/Mappers/ObjectMapper.PropertyInfoComparer.cs new file mode 100644 index 0000000..02c1dda --- /dev/null +++ b/Swan.Tiny/Mappers/ObjectMapper.PropertyInfoComparer.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Swan.Mappers { + /// + /// Represents an AutoMapper-like object to map from one object type + /// to another using defined properties map or using the default behaviour + /// to copy same named properties from one object to another. + /// + /// The extension methods like CopyPropertiesTo use the default behaviour. + /// + public partial class ObjectMapper { + internal class PropertyInfoComparer : IEqualityComparer { + public Boolean Equals(PropertyInfo x, PropertyInfo y) => x != null && y != null && x.Name == y.Name && x.PropertyType == y.PropertyType; + + public Int32 GetHashCode(PropertyInfo obj) => obj.Name.GetHashCode() + obj.PropertyType.Name.GetHashCode(); + } + } +} \ No newline at end of file diff --git a/Swan.Tiny/Mappers/ObjectMapper.cs b/Swan.Tiny/Mappers/ObjectMapper.cs new file mode 100644 index 0000000..c7360b4 --- /dev/null +++ b/Swan.Tiny/Mappers/ObjectMapper.cs @@ -0,0 +1,309 @@ +#nullable enable +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Swan.Reflection; + +namespace Swan.Mappers { + /// + /// Represents an AutoMapper-like object to map from one object type + /// to another using defined properties map or using the default behaviour + /// to copy same named properties from one object to another. + /// + /// The extension methods like CopyPropertiesTo use the default behaviour. + /// + /// + /// The following code explains how to map an object's properties into an instance of type T. + /// + /// using Swan.Mappers; + /// + /// class Example + /// { + /// class Person + /// { + /// public string Name { get; set; } + /// public int Age { get; set; } + /// } + /// + /// static void Main() + /// { + /// var obj = new { Name = "John", Age = 42 }; + /// + /// var person = Runtime.ObjectMapper.Map<Person>(obj); + /// } + /// } + /// + /// + /// The following code explains how to explicitly map certain properties. + /// + /// using Swan.Mappers; + /// + /// class Example + /// { + /// class User + /// { + /// public string Name { get; set; } + /// public Role Role { get; set; } + /// } + /// + /// public class Role + /// { + /// public string Name { get; set; } + /// } + /// + /// class UserDto + /// { + /// public string Name { get; set; } + /// public string Role { get; set; } + /// } + /// + /// static void Main() + /// { + /// // create a User object + /// var person = + /// new User { Name = "Phillip", Role = new Role { Name = "Admin" } }; + /// + /// // create an Object Mapper + /// var mapper = new ObjectMapper(); + /// + /// // map the User's Role.Name to UserDto's Role + /// mapper.CreateMap<User, UserDto>() + /// .MapProperty(d => d.Role, x => x.Role.Name); + /// + /// // apply the previous map and retrieve a UserDto object + /// var destination = mapper.Map<UserDto>(person); + /// } + /// } + /// + /// + public partial class ObjectMapper { + private static readonly Lazy LazyInstance = new Lazy(() => new ObjectMapper()); + + private readonly List _maps = new List(); + + /// + /// Gets the current. + /// + /// + /// The current. + /// + public static ObjectMapper Current => LazyInstance.Value; + + /// + /// Copies the specified source. + /// + /// The source. + /// The target. + /// The properties to copy. + /// The ignore properties. + /// + /// Copied properties count. + /// + /// + /// source + /// or + /// target. + /// + public static Int32 Copy(Object source, Object? target, IEnumerable? propertiesToCopy = null, params String[]? ignoreProperties) { + if(source == null) { + throw new ArgumentNullException(nameof(source)); + } + + if(target == null) { + throw new ArgumentNullException(nameof(target)); + } + + return CopyInternal(target, GetSourceMap(source), propertiesToCopy, ignoreProperties); + } + + /// + /// Copies the specified source. + /// + /// The source. + /// The target. + /// The properties to copy. + /// The ignore properties. + /// + /// Copied properties count. + /// + /// + /// source + /// or + /// target. + /// + public static Int32 Copy(IDictionary source, Object? target, IEnumerable? propertiesToCopy = null, params String[] ignoreProperties) { + if(source == null) { + throw new ArgumentNullException(nameof(source)); + } + + if(target == null) { + throw new ArgumentNullException(nameof(target)); + } + + return CopyInternal(target, source.ToDictionary(x => x.Key.ToLowerInvariant(), x => Tuple.Create(typeof(Object), x.Value)), propertiesToCopy, ignoreProperties); + } + + /// + /// Creates the map. + /// + /// The type of the source. + /// The type of the destination. + /// + /// An object map representation of type of the destination property + /// and type of the source property. + /// + /// + /// You can't create an existing map + /// or + /// Types doesn't match. + /// + public ObjectMap CreateMap() { + if(this._maps.Any(x => x.SourceType == typeof(TSource) && x.DestinationType == typeof(TDestination))) { + throw new InvalidOperationException("You can't create an existing map"); + } + + IEnumerable sourceType = PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties(true); + IEnumerable destinationType = PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties(true); + + PropertyInfo[] intersect = sourceType.Intersect(destinationType, new PropertyInfoComparer()).ToArray(); + + if(!intersect.Any()) { + throw new InvalidOperationException("Types doesn't match"); + } + + ObjectMap map = new ObjectMap(intersect); + + this._maps.Add(map); + + return map; + } + + /// + /// Maps the specified source. + /// + /// The type of the destination. + /// The source. + /// if set to true [automatic resolve]. + /// + /// A new instance of the map. + /// + /// source. + /// You can't map from type {source.GetType().Name} to {typeof(TDestination).Name}. + public TDestination Map(Object source, Boolean autoResolve = true) { + if(source == null) { + throw new ArgumentNullException(nameof(source)); + } + + TDestination destination = Activator.CreateInstance(); + IObjectMap map = this._maps.FirstOrDefault(x => x.SourceType == source.GetType() && x.DestinationType == typeof(TDestination)); + + if(map != null) { + foreach(KeyValuePair> property in map.Map) { + Object finalSource = property.Value.Aggregate(source, (current, sourceProperty) => sourceProperty.GetValue(current)!); + + property.Key.SetValue(destination, finalSource); + } + } else { + if(!autoResolve) { + throw new InvalidOperationException($"You can't map from type {source.GetType().Name} to {typeof(TDestination).Name}"); + } + + // Missing mapping, try to use default behavior + _ = Copy(source, destination); + } + + return destination; + } + + private static Int32 CopyInternal(Object target, Dictionary> sourceProperties, IEnumerable? propertiesToCopy, IEnumerable? ignoreProperties) { + // Filter properties + IEnumerable? requiredProperties = propertiesToCopy?.Where(p => !String.IsNullOrWhiteSpace(p)).Select(p => p.ToLowerInvariant()); + + IEnumerable? ignoredProperties = ignoreProperties?.Where(p => !String.IsNullOrWhiteSpace(p)).Select(p => p.ToLowerInvariant()); + + IEnumerable properties = PropertyTypeCache.DefaultCache.Value.RetrieveFilteredProperties(target.GetType(), true, x => x.CanWrite); + + return properties.Select(x => x.Name).Distinct().ToDictionary(x => x.ToLowerInvariant(), x => properties.First(y => y.Name == x)).Where(x => sourceProperties.Keys.Contains(x.Key)).When(() => requiredProperties != null, q => q.Where(y => requiredProperties.Contains(y.Key))).When(() => ignoredProperties != null, q => q.Where(y => !ignoredProperties.Contains(y.Key))).ToDictionary(x => x.Value, x => sourceProperties[x.Key]).Sum(x => TrySetValue(x.Key, x.Value, target) ? 1 : 0); + } + + private static Boolean TrySetValue(PropertyInfo propertyInfo, Tuple property, Object target) { + try { + (Type type, Object value) = property; + + if(type.IsEnum) { + propertyInfo.SetValue(target, + Enum.ToObject(propertyInfo.PropertyType, value)); + + return true; + } + + if(type.IsValueType || propertyInfo.PropertyType != type) { + return propertyInfo.TrySetBasicType(value, target); + } + + if(propertyInfo.PropertyType.IsArray) { + _ = propertyInfo.TrySetArray(value as IEnumerable, target); + return true; + } + + propertyInfo.SetValue(target, GetValue(value, propertyInfo.PropertyType)); + + return true; + } catch { + // swallow + } + + return false; + } + + private static Object? GetValue(Object source, Type targetType) { + if(source == null) { + return null; + } + + Object? target = null; + + source.CreateTarget(targetType, false, ref target); + + switch(source) { + case String _: + target = source; + break; + case IList sourceList when target is IList targetList: + MethodInfo addMethod = targetType.GetMethods().FirstOrDefault(m => m.Name == Formatters.Json.AddMethodName && m.IsPublic && m.GetParameters().Length == 1); + + if(addMethod == null) { + return target; + } + + Boolean? isItemValueType = targetList.GetType().GetElementType()?.IsValueType; + + foreach(Object? item in sourceList) { + try { + if(isItemValueType != null) { + _ = targetList.Add((Boolean)isItemValueType ? item : item?.CopyPropertiesToNew()); + } + } catch { + // ignored + } + } + + break; + default: + _ = source.CopyPropertiesTo(target); + break; + } + + return target; + } + + private static Dictionary> GetSourceMap(Object source) { + // select distinct properties because they can be duplicated by inheritance + PropertyInfo[] sourceProperties = PropertyTypeCache.DefaultCache.Value.RetrieveFilteredProperties(source.GetType(), true, x => x.CanRead).ToArray(); + + return sourceProperties.Select(x => x.Name).Distinct().ToDictionary(x => x.ToLowerInvariant(), x => Tuple.Create(sourceProperties.First(y => y.Name == x).PropertyType, sourceProperties.First(y => y.Name == x).GetValue(source)))!; + } + } +} diff --git a/Swan.Tiny/Net/Dns/DnsClient.Interfaces.cs b/Swan.Tiny/Net/Dns/DnsClient.Interfaces.cs new file mode 100644 index 0000000..d869e38 --- /dev/null +++ b/Swan.Tiny/Net/Dns/DnsClient.Interfaces.cs @@ -0,0 +1,96 @@ +using System; +using System.Threading.Tasks; +using System.Collections.Generic; + +namespace Swan.Net.Dns { + /// + /// DnsClient public interfaces. + /// + internal partial class DnsClient { + public interface IDnsMessage { + IList Questions { + get; + } + + Int32 Size { + get; + } + Byte[] ToArray(); + } + + public interface IDnsMessageEntry { + DnsDomain Name { + get; + } + DnsRecordType Type { + get; + } + DnsRecordClass Class { + get; + } + + Int32 Size { + get; + } + Byte[] ToArray(); + } + + public interface IDnsResourceRecord : IDnsMessageEntry { + TimeSpan TimeToLive { + get; + } + Int32 DataLength { + get; + } + Byte[] Data { + get; + } + } + + public interface IDnsRequest : IDnsMessage { + Int32 Id { + get; set; + } + DnsOperationCode OperationCode { + get; set; + } + Boolean RecursionDesired { + get; set; + } + } + + public interface IDnsResponse : IDnsMessage { + Int32 Id { + get; set; + } + IList AnswerRecords { + get; + } + IList AuthorityRecords { + get; + } + IList AdditionalRecords { + get; + } + Boolean IsRecursionAvailable { + get; set; + } + Boolean IsAuthorativeServer { + get; set; + } + Boolean IsTruncated { + get; set; + } + DnsOperationCode OperationCode { + get; set; + } + DnsResponseCode ResponseCode { + get; set; + } + } + + public interface IDnsRequestResolver { + Task Request(DnsClientRequest request); + } + } +} diff --git a/Swan.Tiny/Net/Dns/DnsClient.Request.cs b/Swan.Tiny/Net/Dns/DnsClient.Request.cs new file mode 100644 index 0000000..8b9f396 --- /dev/null +++ b/Swan.Tiny/Net/Dns/DnsClient.Request.cs @@ -0,0 +1,558 @@ +#nullable enable +using Swan.Formatters; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Runtime.InteropServices; +using System.Text; + +namespace Swan.Net.Dns { + /// + /// DnsClient Request inner class. + /// + internal partial class DnsClient { + public class DnsClientRequest : IDnsRequest { + private readonly IDnsRequestResolver _resolver; + private readonly IDnsRequest _request; + + public DnsClientRequest(IPEndPoint dns, IDnsRequest? request = null, IDnsRequestResolver? resolver = null) { + this.Dns = dns; + this._request = request == null ? new DnsRequest() : new DnsRequest(request); + this._resolver = resolver ?? new DnsUdpRequestResolver(); + } + + public Int32 Id { + get => this._request.Id; + set => this._request.Id = value; + } + + public DnsOperationCode OperationCode { + get => this._request.OperationCode; + set => this._request.OperationCode = value; + } + + public Boolean RecursionDesired { + get => this._request.RecursionDesired; + set => this._request.RecursionDesired = value; + } + + public IList Questions => this._request.Questions; + + public Int32 Size => this._request.Size; + + public IPEndPoint Dns { + get; set; + } + + public Byte[] ToArray() => this._request.ToArray(); + + public override String ToString() => this._request.ToString()!; + + /// + /// Resolves this request into a response using the provided DNS information. The given + /// request strategy is used to retrieve the response. + /// + /// Throw if a malformed response is received from the server. + /// Thrown if a IO error occurs. + /// Thrown if a the reading or writing to the socket fails. + /// The response received from server. + public async Task Resolve() { + try { + DnsClientResponse response = await this._resolver.Request(this).ConfigureAwait(false); + + if(response.Id != this.Id) { + throw new DnsQueryException(response, "Mismatching request/response IDs"); + } + + if(response.ResponseCode != DnsResponseCode.NoError) { + throw new DnsQueryException(response); + } + + return response; + } catch(Exception e) { + if(e is ArgumentException || e is SocketException) { + throw new DnsQueryException("Invalid response", e); + } + + throw; + } + } + } + + public class DnsRequest : IDnsRequest { + private static readonly Random Random = new Random(); + + private DnsHeader header; + + public DnsRequest() { + this.Questions = new List(); + this.header = new DnsHeader { + OperationCode = DnsOperationCode.Query, + Response = false, + Id = Random.Next(UInt16.MaxValue), + }; + } + + public DnsRequest(IDnsRequest request) { + this.header = new DnsHeader(); + this.Questions = new List(request.Questions); + + this.header.Response = false; + + this.Id = request.Id; + this.OperationCode = request.OperationCode; + this.RecursionDesired = request.RecursionDesired; + } + + public IList Questions { + get; + } + + public Int32 Size => this.header.Size + this.Questions.Sum(q => q.Size); + + public Int32 Id { + get => this.header.Id; + set => this.header.Id = value; + } + + public DnsOperationCode OperationCode { + get => this.header.OperationCode; + set => this.header.OperationCode = value; + } + + public Boolean RecursionDesired { + get => this.header.RecursionDesired; + set => this.header.RecursionDesired = value; + } + + public Byte[] ToArray() { + this.UpdateHeader(); + using MemoryStream result = new MemoryStream(this.Size); + + return result.Append(this.header.ToArray()).Append(this.Questions.Select(q => q.ToArray())).ToArray(); + } + + public override String ToString() { + this.UpdateHeader(); + + return Json.Serialize(this, true); + } + + private void UpdateHeader() => this.header.QuestionCount = this.Questions.Count; + } + + public class DnsTcpRequestResolver : IDnsRequestResolver { + public async Task Request(DnsClientRequest request) { + TcpClient tcp = new TcpClient(); + + try { + await tcp.Client.ConnectAsync(request.Dns).ConfigureAwait(false); + + NetworkStream stream = tcp.GetStream(); + Byte[] buffer = request.ToArray(); + Byte[] length = BitConverter.GetBytes((UInt16)buffer.Length); + + if(BitConverter.IsLittleEndian) { + Array.Reverse(length); + } + + await stream.WriteAsync(length, 0, length.Length).ConfigureAwait(false); + await stream.WriteAsync(buffer, 0, buffer.Length).ConfigureAwait(false); + + buffer = new Byte[2]; + await Read(stream, buffer).ConfigureAwait(false); + + if(BitConverter.IsLittleEndian) { + Array.Reverse(buffer); + } + + buffer = new Byte[BitConverter.ToUInt16(buffer, 0)]; + await Read(stream, buffer).ConfigureAwait(false); + + DnsResponse response = DnsResponse.FromArray(buffer); + + return new DnsClientResponse(request, response, buffer); + } finally { + tcp.Dispose(); + } + } + + private static async Task Read(Stream stream, Byte[] buffer) { + Int32 length = buffer.Length; + Int32 offset = 0; + Int32 size; + + while(length > 0 && (size = await stream.ReadAsync(buffer, offset, length).ConfigureAwait(false)) > 0) { + offset += size; + length -= size; + } + + if(length > 0) { + throw new IOException("Unexpected end of stream"); + } + } + } + + public class DnsUdpRequestResolver : IDnsRequestResolver { + private readonly IDnsRequestResolver _fallback; + + public DnsUdpRequestResolver(IDnsRequestResolver fallback) => this._fallback = fallback; + + public DnsUdpRequestResolver() => this._fallback = new DnsNullRequestResolver(); + + public async Task Request(DnsClientRequest request) { + UdpClient udp = new UdpClient(); + IPEndPoint dns = request.Dns; + + try { + udp.Client.SendTimeout = 7000; + udp.Client.ReceiveTimeout = 7000; + + await udp.Client.ConnectAsync(dns).ConfigureAwait(false); + + + _ = await udp.SendAsync(request.ToArray(), request.Size).ConfigureAwait(false); + + List bufferList = new List(); + + do { + Byte[] tempBuffer = new Byte[1024]; + Int32 receiveCount = udp.Client.Receive(tempBuffer); + bufferList.AddRange(tempBuffer.Skip(0).Take(receiveCount)); + } + while(udp.Client.Available > 0 || bufferList.Count == 0); + + Byte[] buffer = bufferList.ToArray(); + DnsResponse response = DnsResponse.FromArray(buffer); + + return response.IsTruncated + ? await this._fallback.Request(request).ConfigureAwait(false) + : new DnsClientResponse(request, response, buffer); + } finally { + udp.Dispose(); + } + } + } + + public class DnsNullRequestResolver : IDnsRequestResolver { + public Task Request(DnsClientRequest request) => throw new DnsQueryException("Request failed"); + } + + // 12 bytes message header + [StructEndianness(Endianness.Big)] + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct DnsHeader { + public const Int32 SIZE = 12; + + private UInt16 id; + + // Question count: number of questions in the Question section + private UInt16 questionCount; + + // Answer record count: number of records in the Answer section + private UInt16 answerCount; + + // Authority record count: number of records in the Authority section + private UInt16 authorityCount; + + // Additional record count: number of records in the Additional section + private UInt16 addtionalCount; + + public Int32 Id { + get => this.id; + set => this.id = (UInt16)value; + } + + public Int32 QuestionCount { + get => this.questionCount; + set => this.questionCount = (UInt16)value; + } + + public Int32 AnswerRecordCount { + get => this.answerCount; + set => this.answerCount = (UInt16)value; + } + + public Int32 AuthorityRecordCount { + get => this.authorityCount; + set => this.authorityCount = (UInt16)value; + } + + public Int32 AdditionalRecordCount { + get => this.addtionalCount; + set => this.addtionalCount = (UInt16)value; + } + + public Boolean Response { + get => this.Qr == 1; + set => this.Qr = Convert.ToByte(value); + } + + public DnsOperationCode OperationCode { + get => (DnsOperationCode)this.Opcode; + set => this.Opcode = (Byte)value; + } + + public Boolean AuthorativeServer { + get => this.Aa == 1; + set => this.Aa = Convert.ToByte(value); + } + + public Boolean Truncated { + get => this.Tc == 1; + set => this.Tc = Convert.ToByte(value); + } + + public Boolean RecursionDesired { + get => this.Rd == 1; + set => this.Rd = Convert.ToByte(value); + } + + public Boolean RecursionAvailable { + get => this.Ra == 1; + set => this.Ra = Convert.ToByte(value); + } + + public DnsResponseCode ResponseCode { + get => (DnsResponseCode)this.RCode; + set => this.RCode = (Byte)value; + } + + public Int32 Size => SIZE; + + // Query/Response Flag + private Byte Qr { + get => this.Flag0.GetBitValueAt(7); + set => this.Flag0 = this.Flag0.SetBitValueAt(7, 1, value); + } + + // Operation Code + private Byte Opcode { + get => this.Flag0.GetBitValueAt(3, 4); + set => this.Flag0 = this.Flag0.SetBitValueAt(3, 4, value); + } + + // Authorative Answer Flag + private Byte Aa { + get => this.Flag0.GetBitValueAt(2); + set => this.Flag0 = this.Flag0.SetBitValueAt(2, 1, value); + } + + // Truncation Flag + private Byte Tc { + get => this.Flag0.GetBitValueAt(1); + set => this.Flag0 = this.Flag0.SetBitValueAt(1, 1, value); + } + + // Recursion Desired + private Byte Rd { + get => this.Flag0.GetBitValueAt(0); + set => this.Flag0 = this.Flag0.SetBitValueAt(0, 1, value); + } + + // Recursion Available + private Byte Ra { + get => this.Flag1.GetBitValueAt(7); + set => this.Flag1 = this.Flag1.SetBitValueAt(7, 1, value); + } + + // Zero (Reserved) + private Byte Z { + get => this.Flag1.GetBitValueAt(4, 3); + set { + } + } + + // Response Code + private Byte RCode { + get => this.Flag1.GetBitValueAt(0, 4); + set => this.Flag1 = this.Flag1.SetBitValueAt(0, 4, value); + } + + private Byte Flag0 { + get; + set; + } + + private Byte Flag1 { + get; + set; + } + + public static DnsHeader FromArray(Byte[] header) => header.Length < SIZE ? throw new ArgumentException("Header length too small") : header.ToStruct(0, SIZE); + + public Byte[] ToArray() => this.ToBytes(); + + public override String ToString() => Json.SerializeExcluding(this, true, nameof(this.Size)); + } + + public class DnsDomain : IComparable { + private readonly String[] _labels; + + public DnsDomain(String domain) : this(domain.Split('.')) { + } + + public DnsDomain(String[] labels) => this._labels = labels; + + public Int32 Size => this._labels.Sum(l => l.Length) + this._labels.Length + 1; + + public static DnsDomain FromArray(Byte[] message, Int32 offset) => FromArray(message, offset, out _); + + public static DnsDomain FromArray(Byte[] message, Int32 offset, out Int32 endOffset) { + List labels = new List(); + Boolean endOffsetAssigned = false; + endOffset = 0; + Byte lengthOrPointer; + + while((lengthOrPointer = message[offset++]) > 0) { + // Two heighest bits are set (pointer) + if(lengthOrPointer.GetBitValueAt(6, 2) == 3) { + if(!endOffsetAssigned) { + endOffsetAssigned = true; + endOffset = offset + 1; + } + + UInt16 pointer = lengthOrPointer.GetBitValueAt(0, 6); + offset = (pointer << 8) | message[offset]; + + continue; + } + + if(lengthOrPointer.GetBitValueAt(6, 2) != 0) { + throw new ArgumentException("Unexpected bit pattern in label length"); + } + + Byte length = lengthOrPointer; + Byte[] label = new Byte[length]; + Array.Copy(message, offset, label, 0, length); + + labels.Add(label); + + offset += length; + } + + if(!endOffsetAssigned) { + endOffset = offset; + } + + return new DnsDomain(labels.Select(l => l.ToText(Encoding.ASCII)).ToArray()); + } + + public static DnsDomain PointerName(IPAddress ip) => new DnsDomain(FormatReverseIP(ip)); + + public Byte[] ToArray() { + Byte[] result = new Byte[this.Size]; + Int32 offset = 0; + + foreach(Byte[] l in this._labels.Select(label => Encoding.ASCII.GetBytes(label))) { + result[offset++] = (Byte)l.Length; + l.CopyTo(result, offset); + + offset += l.Length; + } + + result[offset] = 0; + + return result; + } + + public override String ToString() => String.Join(".", this._labels); + + public Int32 CompareTo(DnsDomain other) => String.Compare(this.ToString(), other.ToString(), StringComparison.Ordinal); + + public override Boolean Equals(Object? obj) => obj is DnsDomain domain && this.CompareTo(domain) == 0; + + public override Int32 GetHashCode() => this.ToString().GetHashCode(); + + private static String FormatReverseIP(IPAddress ip) { + Byte[] address = ip.GetAddressBytes(); + + if(address.Length == 4) { + return String.Join(".", address.Reverse().Select(b => b.ToString())) + ".in-addr.arpa"; + } + + Byte[] nibbles = new Byte[address.Length * 2]; + + for(Int32 i = 0, j = 0; i < address.Length; i++, j = 2 * i) { + Byte b = address[i]; + + nibbles[j] = b.GetBitValueAt(4, 4); + nibbles[j + 1] = b.GetBitValueAt(0, 4); + } + + return String.Join(".", nibbles.Reverse().Select(b => b.ToString("x"))) + ".ip6.arpa"; + } + } + + public class DnsQuestion : IDnsMessageEntry { + public static IList GetAllFromArray(Byte[] message, Int32 offset, Int32 questionCount) => GetAllFromArray(message, offset, questionCount, out _); + + public static IList GetAllFromArray(Byte[] message, Int32 offset, Int32 questionCount, out Int32 endOffset) { + IList questions = new List(questionCount); + + for(Int32 i = 0; i < questionCount; i++) { + questions.Add(FromArray(message, offset, out offset)); + } + + endOffset = offset; + return questions; + } + + public static DnsQuestion FromArray(Byte[] message, Int32 offset, out Int32 endOffset) { + DnsDomain domain = DnsDomain.FromArray(message, offset, out offset); + Tail tail = message.ToStruct(offset, Tail.SIZE); + + endOffset = offset + Tail.SIZE; + + return new DnsQuestion(domain, tail.Type, tail.Class); + } + + public DnsQuestion(DnsDomain domain, DnsRecordType type = DnsRecordType.A, DnsRecordClass klass = DnsRecordClass.IN) { + this.Name = domain; + this.Type = type; + this.Class = klass; + } + + public DnsDomain Name { + get; + } + + public DnsRecordType Type { + get; + } + + public DnsRecordClass Class { + get; + } + + public Int32 Size => this.Name.Size + Tail.SIZE; + + public Byte[] ToArray() => new MemoryStream(this.Size).Append(this.Name.ToArray()).Append(new Tail { Type = Type, Class = Class }.ToBytes()).ToArray(); + + public override String ToString() => Json.SerializeOnly(this, true, nameof(this.Name), nameof(this.Type), nameof(this.Class)); + + [StructEndianness(Endianness.Big)] + [StructLayout(LayoutKind.Sequential, Pack = 2)] + private struct Tail { + public const Int32 SIZE = 4; + + private UInt16 type; + private UInt16 klass; + + public DnsRecordType Type { + get => (DnsRecordType)this.type; + set => this.type = (UInt16)value; + } + + public DnsRecordClass Class { + get => (DnsRecordClass)this.klass; + set => this.klass = (UInt16)value; + } + } + } + } +} diff --git a/Swan.Tiny/Net/Dns/DnsClient.ResourceRecords.cs b/Swan.Tiny/Net/Dns/DnsClient.ResourceRecords.cs new file mode 100644 index 0000000..10cf023 --- /dev/null +++ b/Swan.Tiny/Net/Dns/DnsClient.ResourceRecords.cs @@ -0,0 +1,344 @@ +using Swan.Formatters; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Runtime.InteropServices; + +namespace Swan.Net.Dns { + /// + /// DnsClient public methods. + /// + internal partial class DnsClient { + public abstract class DnsResourceRecordBase : IDnsResourceRecord { + private readonly IDnsResourceRecord _record; + + protected DnsResourceRecordBase(IDnsResourceRecord record) => this._record = record; + + public DnsDomain Name => this._record.Name; + + public DnsRecordType Type => this._record.Type; + + public DnsRecordClass Class => this._record.Class; + + public TimeSpan TimeToLive => this._record.TimeToLive; + + public Int32 DataLength => this._record.DataLength; + + public Byte[] Data => this._record.Data; + + public Int32 Size => this._record.Size; + + protected virtual String[] IncludedProperties => new[] { nameof(this.Name), nameof(this.Type), nameof(this.Class), nameof(this.TimeToLive), nameof(this.DataLength) }; + + public Byte[] ToArray() => this._record.ToArray(); + + public override String ToString() => Json.SerializeOnly(this, true, this.IncludedProperties); + } + + public class DnsResourceRecord : IDnsResourceRecord { + public DnsResourceRecord(DnsDomain domain, Byte[] data, DnsRecordType type, DnsRecordClass klass = DnsRecordClass.IN, TimeSpan ttl = default) { + this.Name = domain; + this.Type = type; + this.Class = klass; + this.TimeToLive = ttl; + this.Data = data; + } + + public DnsDomain Name { + get; + } + + public DnsRecordType Type { + get; + } + + public DnsRecordClass Class { + get; + } + + public TimeSpan TimeToLive { + get; + } + + public Int32 DataLength => this.Data.Length; + + public Byte[] Data { + get; + } + + public Int32 Size => this.Name.Size + Tail.SIZE + this.Data.Length; + + public static DnsResourceRecord FromArray(Byte[] message, Int32 offset, out Int32 endOffset) { + DnsDomain domain = DnsDomain.FromArray(message, offset, out offset); + Tail tail = message.ToStruct(offset, Tail.SIZE); + + Byte[] data = new Byte[tail.DataLength]; + + offset += Tail.SIZE; + Array.Copy(message, offset, data, 0, data.Length); + + endOffset = offset + data.Length; + + return new DnsResourceRecord(domain, data, tail.Type, tail.Class, tail.TimeToLive); + } + + public Byte[] ToArray() => new MemoryStream(this.Size).Append(this.Name.ToArray()).Append(new Tail() { Type = Type, Class = Class, TimeToLive = TimeToLive, DataLength = this.Data.Length, }.ToBytes()).Append(this.Data).ToArray(); + + public override String ToString() => Json.SerializeOnly(this, true, nameof(this.Name), nameof(this.Type), nameof(this.Class), nameof(this.TimeToLive), nameof(this.DataLength)); + + [StructEndianness(Endianness.Big)] + [StructLayout(LayoutKind.Sequential, Pack = 2)] + private struct Tail { + public const Int32 SIZE = 10; + + private UInt16 type; + private UInt16 klass; + private UInt32 ttl; + private UInt16 dataLength; + + public DnsRecordType Type { + get => (DnsRecordType)this.type; + set => this.type = (UInt16)value; + } + + public DnsRecordClass Class { + get => (DnsRecordClass)this.klass; + set => this.klass = (UInt16)value; + } + + public TimeSpan TimeToLive { + get => TimeSpan.FromSeconds(this.ttl); + set => this.ttl = (UInt32)value.TotalSeconds; + } + + public Int32 DataLength { + get => this.dataLength; + set => this.dataLength = (UInt16)value; + } + } + } + + public class DnsPointerResourceRecord : DnsResourceRecordBase { + public DnsPointerResourceRecord(IDnsResourceRecord record, Byte[] message, Int32 dataOffset) : base(record) => this.PointerDomainName = DnsDomain.FromArray(message, dataOffset); + + public DnsDomain PointerDomainName { + get; + } + + protected override String[] IncludedProperties { + get { + List temp = new List(base.IncludedProperties) { nameof(this.PointerDomainName) }; + return temp.ToArray(); + } + } + } + + public class DnsIPAddressResourceRecord : DnsResourceRecordBase { + public DnsIPAddressResourceRecord(IDnsResourceRecord record) : base(record) => this.IPAddress = new IPAddress(this.Data); + + public IPAddress IPAddress { + get; + } + + protected override String[] IncludedProperties => new List(base.IncludedProperties) { nameof(this.IPAddress) }.ToArray(); + } + + public class DnsNameServerResourceRecord : DnsResourceRecordBase { + public DnsNameServerResourceRecord(IDnsResourceRecord record, Byte[] message, Int32 dataOffset) : base(record) => this.NSDomainName = DnsDomain.FromArray(message, dataOffset); + + public DnsDomain NSDomainName { + get; + } + + protected override String[] IncludedProperties => new List(base.IncludedProperties) { nameof(this.NSDomainName) }.ToArray(); + } + + public class DnsCanonicalNameResourceRecord : DnsResourceRecordBase { + public DnsCanonicalNameResourceRecord(IDnsResourceRecord record, Byte[] message, Int32 dataOffset) : base(record) => this.CanonicalDomainName = DnsDomain.FromArray(message, dataOffset); + + public DnsDomain CanonicalDomainName { + get; + } + + protected override String[] IncludedProperties => new List(base.IncludedProperties) { nameof(this.CanonicalDomainName) }.ToArray(); + } + + public class DnsMailExchangeResourceRecord : DnsResourceRecordBase { + private const Int32 PreferenceSize = 2; + + public DnsMailExchangeResourceRecord(IDnsResourceRecord record, Byte[] message, Int32 dataOffset) + : base(record) { + Byte[] preference = new Byte[PreferenceSize]; + Array.Copy(message, dataOffset, preference, 0, preference.Length); + + if(BitConverter.IsLittleEndian) { + Array.Reverse(preference); + } + + dataOffset += PreferenceSize; + + this.Preference = BitConverter.ToUInt16(preference, 0); + this.ExchangeDomainName = DnsDomain.FromArray(message, dataOffset); + } + + public Int32 Preference { + get; + } + + public DnsDomain ExchangeDomainName { + get; + } + + protected override String[] IncludedProperties => new List(base.IncludedProperties) + { + nameof(this.Preference), + nameof(this.ExchangeDomainName), + }.ToArray(); + } + + public class DnsStartOfAuthorityResourceRecord : DnsResourceRecordBase { + public DnsStartOfAuthorityResourceRecord(IDnsResourceRecord record, Byte[] message, Int32 dataOffset) : base(record) { + this.MasterDomainName = DnsDomain.FromArray(message, dataOffset, out dataOffset); + this.ResponsibleDomainName = DnsDomain.FromArray(message, dataOffset, out dataOffset); + + Options tail = message.ToStruct(dataOffset, Options.SIZE); + + this.SerialNumber = tail.SerialNumber; + this.RefreshInterval = tail.RefreshInterval; + this.RetryInterval = tail.RetryInterval; + this.ExpireInterval = tail.ExpireInterval; + this.MinimumTimeToLive = tail.MinimumTimeToLive; + } + + public DnsStartOfAuthorityResourceRecord(DnsDomain domain, DnsDomain master, DnsDomain responsible, Int64 serial, TimeSpan refresh, TimeSpan retry, TimeSpan expire, TimeSpan minTtl, TimeSpan ttl = default) + : base(Create(domain, master, responsible, serial, refresh, retry, expire, minTtl, ttl)) { + this.MasterDomainName = master; + this.ResponsibleDomainName = responsible; + + this.SerialNumber = serial; + this.RefreshInterval = refresh; + this.RetryInterval = retry; + this.ExpireInterval = expire; + this.MinimumTimeToLive = minTtl; + } + + public DnsDomain MasterDomainName { + get; + } + + public DnsDomain ResponsibleDomainName { + get; + } + + public Int64 SerialNumber { + get; + } + + public TimeSpan RefreshInterval { + get; + } + + public TimeSpan RetryInterval { + get; + } + + public TimeSpan ExpireInterval { + get; + } + + public TimeSpan MinimumTimeToLive { + get; + } + + protected override String[] IncludedProperties => new List(base.IncludedProperties) + { + nameof(this.MasterDomainName), + nameof(this.ResponsibleDomainName), + nameof(this.SerialNumber), + }.ToArray(); + + private static IDnsResourceRecord Create(DnsDomain domain, DnsDomain master, DnsDomain responsible, Int64 serial, TimeSpan refresh, TimeSpan retry, TimeSpan expire, TimeSpan minTtl, TimeSpan ttl) { + MemoryStream data = new MemoryStream(Options.SIZE + master.Size + responsible.Size); + Options tail = new Options { + SerialNumber = serial, + RefreshInterval = refresh, + RetryInterval = retry, + ExpireInterval = expire, + MinimumTimeToLive = minTtl, + }; + + _ = data.Append(master.ToArray()).Append(responsible.ToArray()).Append(tail.ToBytes()); + + return new DnsResourceRecord(domain, data.ToArray(), DnsRecordType.SOA, DnsRecordClass.IN, ttl); + } + + [StructEndianness(Endianness.Big)] + [StructLayout(LayoutKind.Sequential, Pack = 4)] + public struct Options { + public const Int32 SIZE = 20; + + private UInt32 serialNumber; + private UInt32 refreshInterval; + private UInt32 retryInterval; + private UInt32 expireInterval; + private UInt32 ttl; + + public Int64 SerialNumber { + get => this.serialNumber; + set => this.serialNumber = (UInt32)value; + } + + public TimeSpan RefreshInterval { + get => TimeSpan.FromSeconds(this.refreshInterval); + set => this.refreshInterval = (UInt32)value.TotalSeconds; + } + + public TimeSpan RetryInterval { + get => TimeSpan.FromSeconds(this.retryInterval); + set => this.retryInterval = (UInt32)value.TotalSeconds; + } + + public TimeSpan ExpireInterval { + get => TimeSpan.FromSeconds(this.expireInterval); + set => this.expireInterval = (UInt32)value.TotalSeconds; + } + + public TimeSpan MinimumTimeToLive { + get => TimeSpan.FromSeconds(this.ttl); + set => this.ttl = (UInt32)value.TotalSeconds; + } + } + } + + private static class DnsResourceRecordFactory { + public static IList GetAllFromArray(Byte[] message, Int32 offset, Int32 count, out Int32 endOffset) { + List result = new List(count); + + for(Int32 i = 0; i < count; i++) { + result.Add(GetFromArray(message, offset, out offset)); + } + + endOffset = offset; + return result; + } + + private static IDnsResourceRecord GetFromArray(Byte[] message, Int32 offset, out Int32 endOffset) { + DnsResourceRecord record = DnsResourceRecord.FromArray(message, offset, out endOffset); + Int32 dataOffset = endOffset - record.DataLength; + + return record.Type switch + { + DnsRecordType.A => (new DnsIPAddressResourceRecord(record)), + DnsRecordType.AAAA => new DnsIPAddressResourceRecord(record), + DnsRecordType.NS => new DnsNameServerResourceRecord(record, message, dataOffset), + DnsRecordType.CNAME => new DnsCanonicalNameResourceRecord(record, message, dataOffset), + DnsRecordType.SOA => new DnsStartOfAuthorityResourceRecord(record, message, dataOffset), + DnsRecordType.PTR => new DnsPointerResourceRecord(record, message, dataOffset), + DnsRecordType.MX => new DnsMailExchangeResourceRecord(record, message, dataOffset), + _ => record + }; + } + } + } +} diff --git a/Swan.Tiny/Net/Dns/DnsClient.Response.cs b/Swan.Tiny/Net/Dns/DnsClient.Response.cs new file mode 100644 index 0000000..5799ef8 --- /dev/null +++ b/Swan.Tiny/Net/Dns/DnsClient.Response.cs @@ -0,0 +1,174 @@ +using Swan.Formatters; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; + +namespace Swan.Net.Dns { + /// + /// DnsClient Response inner class. + /// + internal partial class DnsClient { + public class DnsClientResponse : IDnsResponse { + private readonly DnsResponse _response; + private readonly Byte[] _message; + + internal DnsClientResponse(DnsClientRequest request, DnsResponse response, Byte[] message) { + this.Request = request; + + this._message = message; + this._response = response; + } + + public DnsClientRequest Request { + get; + } + + public Int32 Id { + get => this._response.Id; + set { + } + } + + public IList AnswerRecords => this._response.AnswerRecords; + + public IList AuthorityRecords => new ReadOnlyCollection(this._response.AuthorityRecords); + + public IList AdditionalRecords => new ReadOnlyCollection(this._response.AdditionalRecords); + + public Boolean IsRecursionAvailable { + get => this._response.IsRecursionAvailable; + set { + } + } + + public Boolean IsAuthorativeServer { + get => this._response.IsAuthorativeServer; + set { + } + } + + public Boolean IsTruncated { + get => this._response.IsTruncated; + set { + } + } + + public DnsOperationCode OperationCode { + get => this._response.OperationCode; + set { + } + } + + public DnsResponseCode ResponseCode { + get => this._response.ResponseCode; + set { + } + } + + public IList Questions => new ReadOnlyCollection(this._response.Questions); + + public Int32 Size => this._message.Length; + + public Byte[] ToArray() => this._message; + + public override String ToString() => this._response.ToString(); + } + + public class DnsResponse : IDnsResponse { + private DnsHeader _header; + + public DnsResponse(DnsHeader header, IList questions, IList answers, IList authority, IList additional) { + this._header = header; + this.Questions = questions; + this.AnswerRecords = answers; + this.AuthorityRecords = authority; + this.AdditionalRecords = additional; + } + + public IList Questions { + get; + } + + public IList AnswerRecords { + get; + } + + public IList AuthorityRecords { + get; + } + + public IList AdditionalRecords { + get; + } + + public Int32 Id { + get => this._header.Id; + set => this._header.Id = value; + } + + public Boolean IsRecursionAvailable { + get => this._header.RecursionAvailable; + set => this._header.RecursionAvailable = value; + } + + public Boolean IsAuthorativeServer { + get => this._header.AuthorativeServer; + set => this._header.AuthorativeServer = value; + } + + public Boolean IsTruncated { + get => this._header.Truncated; + set => this._header.Truncated = value; + } + + public DnsOperationCode OperationCode { + get => this._header.OperationCode; + set => this._header.OperationCode = value; + } + + public DnsResponseCode ResponseCode { + get => this._header.ResponseCode; + set => this._header.ResponseCode = value; + } + + public Int32 Size => this._header.Size + this.Questions.Sum(q => q.Size) + this.AnswerRecords.Sum(a => a.Size) + this.AuthorityRecords.Sum(a => a.Size) + this.AdditionalRecords.Sum(a => a.Size); + + public static DnsResponse FromArray(Byte[] message) { + DnsHeader header = DnsHeader.FromArray(message); + Int32 offset = header.Size; + + if(!header.Response || header.QuestionCount == 0) { + throw new ArgumentException("Invalid response message"); + } + + return header.Truncated + ? new DnsResponse(header, DnsQuestion.GetAllFromArray(message, offset, header.QuestionCount), new List(), new List(), new List()) + : new DnsResponse(header, DnsQuestion.GetAllFromArray(message, offset, header.QuestionCount, out offset), DnsResourceRecordFactory.GetAllFromArray(message, offset, header.AnswerRecordCount, out offset), DnsResourceRecordFactory.GetAllFromArray(message, offset, header.AuthorityRecordCount, out offset), DnsResourceRecordFactory.GetAllFromArray(message, offset, header.AdditionalRecordCount, out _)); + } + + public Byte[] ToArray() { + this.UpdateHeader(); + MemoryStream result = new MemoryStream(this.Size); + + _ = result.Append(this._header.ToArray()).Append(this.Questions.Select(q => q.ToArray())).Append(this.AnswerRecords.Select(a => a.ToArray())).Append(this.AuthorityRecords.Select(a => a.ToArray())).Append(this.AdditionalRecords.Select(a => a.ToArray())); + + return result.ToArray(); + } + + public override String ToString() { + this.UpdateHeader(); + + return Json.SerializeOnly(this, true, nameof(this.Questions), nameof(this.AnswerRecords), nameof(this.AuthorityRecords), nameof(this.AdditionalRecords)); + } + + private void UpdateHeader() { + this._header.QuestionCount = this.Questions.Count; + this._header.AnswerRecordCount = this.AnswerRecords.Count; + this._header.AuthorityRecordCount = this.AuthorityRecords.Count; + this._header.AdditionalRecordCount = this.AdditionalRecords.Count; + } + } + } +} \ No newline at end of file diff --git a/Swan.Tiny/Net/Dns/DnsClient.cs b/Swan.Tiny/Net/Dns/DnsClient.cs new file mode 100644 index 0000000..d030418 --- /dev/null +++ b/Swan.Tiny/Net/Dns/DnsClient.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +#nullable enable +using System.Net; +using System.Threading.Tasks; + +namespace Swan.Net.Dns { + /// + /// DnsClient public methods. + /// + internal partial class DnsClient { + private readonly IPEndPoint _dns; + private readonly IDnsRequestResolver _resolver; + + public DnsClient(IPEndPoint dns, IDnsRequestResolver? resolver = null) { + this._dns = dns; + this._resolver = resolver ?? new DnsUdpRequestResolver(new DnsTcpRequestResolver()); + } + + public DnsClient(IPAddress ip, Int32 port = Network.DnsDefaultPort, IDnsRequestResolver? resolver = null) : this(new IPEndPoint(ip, port), resolver) { + } + + public DnsClientRequest Create(IDnsRequest? request = null) => new DnsClientRequest(this._dns, request, this._resolver); + + public async Task> Lookup(String domain, DnsRecordType type = DnsRecordType.A) { + if(String.IsNullOrWhiteSpace(domain)) { + throw new ArgumentNullException(nameof(domain)); + } + + if(type != DnsRecordType.A && type != DnsRecordType.AAAA) { + throw new ArgumentException("Invalid record type " + type); + } + + DnsClientResponse response = await this.Resolve(domain, type).ConfigureAwait(false); + List ips = response.AnswerRecords.Where(r => r.Type == type).Cast().Select(r => r.IPAddress).ToList(); + + return ips.Count == 0 ? throw new DnsQueryException(response, "No matching records") : ips; + } + + public async Task Reverse(IPAddress ip) { + if(ip == null) { + throw new ArgumentNullException(nameof(ip)); + } + + DnsClientResponse response = await this.Resolve(DnsDomain.PointerName(ip), DnsRecordType.PTR); + IDnsResourceRecord ptr = response.AnswerRecords.FirstOrDefault(r => r.Type == DnsRecordType.PTR); + + return ptr == null ? throw new DnsQueryException(response, "No matching records") : ((DnsPointerResourceRecord)ptr).PointerDomainName.ToString(); + } + + public Task Resolve(String domain, DnsRecordType type) => this.Resolve(new DnsDomain(domain), type); + + public Task Resolve(DnsDomain domain, DnsRecordType type) { + DnsClientRequest request = this.Create(); + DnsQuestion question = new DnsQuestion(domain, type); + + request.Questions.Add(question); + request.OperationCode = DnsOperationCode.Query; + request.RecursionDesired = true; + + return request.Resolve(); + } + } +} diff --git a/Swan.Tiny/Net/Dns/DnsQueryException.cs b/Swan.Tiny/Net/Dns/DnsQueryException.cs new file mode 100644 index 0000000..c0f9e7f --- /dev/null +++ b/Swan.Tiny/Net/Dns/DnsQueryException.cs @@ -0,0 +1,28 @@ +#nullable enable +using System; + +namespace Swan.Net.Dns { + /// + /// An exception thrown when the DNS query fails. + /// + /// + [Serializable] + public class DnsQueryException : Exception { + internal DnsQueryException(String message) : base(message) { + } + + internal DnsQueryException(String message, Exception e) : base(message, e) { + } + + internal DnsQueryException(DnsClient.IDnsResponse response) : this(response, Format(response)) { + } + + internal DnsQueryException(DnsClient.IDnsResponse response, String message) : base(message) => this.Response = response; + + internal DnsClient.IDnsResponse? Response { + get; + } + + private static String Format(DnsClient.IDnsResponse response) => $"Invalid response received with code {response.ResponseCode}"; + } +} diff --git a/Swan.Tiny/Net/Dns/DnsQueryResult.cs b/Swan.Tiny/Net/Dns/DnsQueryResult.cs new file mode 100644 index 0000000..76b8fa7 --- /dev/null +++ b/Swan.Tiny/Net/Dns/DnsQueryResult.cs @@ -0,0 +1,130 @@ +namespace Swan.Net.Dns { + using System.Collections.Generic; + + /// + /// Represents a response from a DNS server. + /// + public class DnsQueryResult { + private readonly List _mAnswerRecords = new List(); + private readonly List _mAdditionalRecords = new List(); + private readonly List _mAuthorityRecords = new List(); + + /// + /// Initializes a new instance of the class. + /// + /// The response. + internal DnsQueryResult(DnsClient.IDnsResponse response) : this() { + this.Id = response.Id; + this.IsAuthoritativeServer = response.IsAuthorativeServer; + this.IsRecursionAvailable = response.IsRecursionAvailable; + this.IsTruncated = response.IsTruncated; + this.OperationCode = response.OperationCode; + this.ResponseCode = response.ResponseCode; + + if(response.AnswerRecords != null) { + foreach(DnsClient.IDnsResourceRecord record in response.AnswerRecords) { + this.AnswerRecords.Add(new DnsRecord(record)); + } + } + + if(response.AuthorityRecords != null) { + foreach(DnsClient.IDnsResourceRecord record in response.AuthorityRecords) { + this.AuthorityRecords.Add(new DnsRecord(record)); + } + } + + if(response.AdditionalRecords != null) { + foreach(DnsClient.IDnsResourceRecord record in response.AdditionalRecords) { + this.AdditionalRecords.Add(new DnsRecord(record)); + } + } + } + + private DnsQueryResult() { + } + + /// + /// Gets the identifier. + /// + /// + /// The identifier. + /// + public System.Int32 Id { + get; + } + + /// + /// Gets a value indicating whether this instance is authoritative server. + /// + /// + /// true if this instance is authoritative server; otherwise, false. + /// + public System.Boolean IsAuthoritativeServer { + get; + } + + /// + /// Gets a value indicating whether this instance is truncated. + /// + /// + /// true if this instance is truncated; otherwise, false. + /// + public System.Boolean IsTruncated { + get; + } + + /// + /// Gets a value indicating whether this instance is recursion available. + /// + /// + /// true if this instance is recursion available; otherwise, false. + /// + public System.Boolean IsRecursionAvailable { + get; + } + + /// + /// Gets the operation code. + /// + /// + /// The operation code. + /// + public DnsOperationCode OperationCode { + get; + } + + /// + /// Gets the response code. + /// + /// + /// The response code. + /// + public DnsResponseCode ResponseCode { + get; + } + + /// + /// Gets the answer records. + /// + /// + /// The answer records. + /// + public IList AnswerRecords => this._mAnswerRecords; + + /// + /// Gets the additional records. + /// + /// + /// The additional records. + /// + public IList AdditionalRecords => this._mAdditionalRecords; + + /// + /// Gets the authority records. + /// + /// + /// The authority records. + /// + public IList AuthorityRecords => this._mAuthorityRecords; + } +} diff --git a/Swan.Tiny/Net/Dns/DnsRecord.cs b/Swan.Tiny/Net/Dns/DnsRecord.cs new file mode 100644 index 0000000..699e75a --- /dev/null +++ b/Swan.Tiny/Net/Dns/DnsRecord.cs @@ -0,0 +1,239 @@ +using System; +using System.Net; +using System.Text; + +namespace Swan.Net.Dns { + /// + /// Represents a DNS record entry. + /// + public class DnsRecord { + /// + /// Initializes a new instance of the class. + /// + /// The record. + internal DnsRecord(DnsClient.IDnsResourceRecord record) : this() { + this.Name = record.Name.ToString(); + this.Type = record.Type; + this.Class = record.Class; + this.TimeToLive = record.TimeToLive; + this.Data = record.Data; + + // PTR + this.PointerDomainName = (record as DnsClient.DnsPointerResourceRecord)?.PointerDomainName?.ToString(); + + // A + this.IPAddress = (record as DnsClient.DnsIPAddressResourceRecord)?.IPAddress; + + // NS + this.NameServerDomainName = (record as DnsClient.DnsNameServerResourceRecord)?.NSDomainName?.ToString(); + + // CNAME + this.CanonicalDomainName = (record as DnsClient.DnsCanonicalNameResourceRecord)?.CanonicalDomainName.ToString(); + + // MX + this.MailExchangerDomainName = (record as DnsClient.DnsMailExchangeResourceRecord)?.ExchangeDomainName.ToString(); + this.MailExchangerPreference = (record as DnsClient.DnsMailExchangeResourceRecord)?.Preference; + + // SOA + this.SoaMasterDomainName = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.MasterDomainName.ToString(); + this.SoaResponsibleDomainName = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.ResponsibleDomainName.ToString(); + this.SoaSerialNumber = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.SerialNumber; + this.SoaRefreshInterval = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.RefreshInterval; + this.SoaRetryInterval = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.RetryInterval; + this.SoaExpireInterval = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.ExpireInterval; + this.SoaMinimumTimeToLive = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.MinimumTimeToLive; + } + + private DnsRecord() { + // placeholder + } + + /// + /// Gets the name. + /// + /// + /// The name. + /// + public String Name { + get; + } + + /// + /// Gets the type. + /// + /// + /// The type. + /// + public DnsRecordType Type { + get; + } + + /// + /// Gets the class. + /// + /// + /// The class. + /// + public DnsRecordClass Class { + get; + } + + /// + /// Gets the time to live. + /// + /// + /// The time to live. + /// + public TimeSpan TimeToLive { + get; + } + + /// + /// Gets the raw data of the record. + /// + /// + /// The data. + /// + public Byte[] Data { + get; + } + + /// + /// Gets the data text bytes in ASCII encoding. + /// + /// + /// The data text. + /// + public String DataText => this.Data == null ? String.Empty : Encoding.ASCII.GetString(this.Data); + + /// + /// Gets the name of the pointer domain. + /// + /// + /// The name of the pointer domain. + /// + public String PointerDomainName { + get; + } + + /// + /// Gets the ip address. + /// + /// + /// The ip address. + /// + public IPAddress IPAddress { + get; + } + + /// + /// Gets the name of the name server domain. + /// + /// + /// The name of the name server domain. + /// + public String NameServerDomainName { + get; + } + + /// + /// Gets the name of the canonical domain. + /// + /// + /// The name of the canonical domain. + /// + public String CanonicalDomainName { + get; + } + + /// + /// Gets the mail exchanger preference. + /// + /// + /// The mail exchanger preference. + /// + public Int32? MailExchangerPreference { + get; + } + + /// + /// Gets the name of the mail exchanger domain. + /// + /// + /// The name of the mail exchanger domain. + /// + public String MailExchangerDomainName { + get; + } + + /// + /// Gets the name of the soa master domain. + /// + /// + /// The name of the soa master domain. + /// + public String SoaMasterDomainName { + get; + } + + /// + /// Gets the name of the soa responsible domain. + /// + /// + /// The name of the soa responsible domain. + /// + public String SoaResponsibleDomainName { + get; + } + + /// + /// Gets the soa serial number. + /// + /// + /// The soa serial number. + /// + public Int64? SoaSerialNumber { + get; + } + + /// + /// Gets the soa refresh interval. + /// + /// + /// The soa refresh interval. + /// + public TimeSpan? SoaRefreshInterval { + get; + } + + /// + /// Gets the soa retry interval. + /// + /// + /// The soa retry interval. + /// + public TimeSpan? SoaRetryInterval { + get; + } + + /// + /// Gets the soa expire interval. + /// + /// + /// The soa expire interval. + /// + public TimeSpan? SoaExpireInterval { + get; + } + + /// + /// Gets the soa minimum time to live. + /// + /// + /// The soa minimum time to live. + /// + public TimeSpan? SoaMinimumTimeToLive { + get; + } + } +} diff --git a/Swan.Tiny/Net/Dns/Enums.Dns.cs b/Swan.Tiny/Net/Dns/Enums.Dns.cs new file mode 100644 index 0000000..8b1f21c --- /dev/null +++ b/Swan.Tiny/Net/Dns/Enums.Dns.cs @@ -0,0 +1,167 @@ +// ReSharper disable InconsistentNaming +namespace Swan.Net.Dns { + /// + /// Enumerates the different DNS record types. + /// + public enum DnsRecordType { + /// + /// A records + /// + A = 1, + + /// + /// Nameserver records + /// + NS = 2, + + /// + /// CNAME records + /// + CNAME = 5, + + /// + /// SOA records + /// + SOA = 6, + + /// + /// WKS records + /// + WKS = 11, + + /// + /// PTR records + /// + PTR = 12, + + /// + /// MX records + /// + MX = 15, + + /// + /// TXT records + /// + TXT = 16, + + /// + /// A records fot IPv6 + /// + AAAA = 28, + + /// + /// SRV records + /// + SRV = 33, + + /// + /// ANY records + /// + ANY = 255, + } + + /// + /// Enumerates the different DNS record classes. + /// + public enum DnsRecordClass { + /// + /// IN records + /// + IN = 1, + + /// + /// ANY records + /// + ANY = 255, + } + + /// + /// Enumerates the different DNS operation codes. + /// + public enum DnsOperationCode { + /// + /// Query operation + /// + Query = 0, + + /// + /// IQuery operation + /// + IQuery, + + /// + /// Status operation + /// + Status, + + /// + /// Notify operation + /// + Notify = 4, + + /// + /// Update operation + /// + Update, + } + + /// + /// Enumerates the different DNS query response codes. + /// + public enum DnsResponseCode { + /// + /// No error + /// + NoError = 0, + + /// + /// No error + /// + FormatError, + + /// + /// Format error + /// + ServerFailure, + + /// + /// Server failure error + /// + NameError, + + /// + /// Name error + /// + NotImplemented, + + /// + /// Not implemented error + /// + Refused, + + /// + /// Refused error + /// + YXDomain, + + /// + /// YXRR error + /// + YXRRSet, + + /// + /// NXRR Set error + /// + NXRRSet, + + /// + /// Not authorized error + /// + NotAuth, + + /// + /// Not zone error + /// + NotZone, + } +} diff --git a/Swan.Tiny/Net/Network.cs b/Swan.Tiny/Net/Network.cs new file mode 100644 index 0000000..e92ac24 --- /dev/null +++ b/Swan.Tiny/Net/Network.cs @@ -0,0 +1,289 @@ +using Swan.Net.Dns; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace Swan.Net { + /// + /// Provides miscellaneous network utilities such as a Public IP finder, + /// a DNS client to query DNS records of any kind, and an NTP client. + /// + public static class Network { + /// + /// The DNS default port. + /// + public const Int32 DnsDefaultPort = 53; + + /// + /// The NTP default port. + /// + public const Int32 NtpDefaultPort = 123; + + /// + /// Gets the name of the host. + /// + /// + /// The name of the host. + /// + public static String HostName => IPGlobalProperties.GetIPGlobalProperties().HostName; + + /// + /// Gets the name of the network domain. + /// + /// + /// The name of the network domain. + /// + public static String DomainName => IPGlobalProperties.GetIPGlobalProperties().DomainName; + + #region IP Addresses and Adapters Information Methods + + /// + /// Gets the active IPv4 interfaces. + /// Only those interfaces with a valid unicast address and a valid gateway will be returned in the collection. + /// + /// + /// A collection of NetworkInterface/IPInterfaceProperties pairs + /// that represents the active IPv4 interfaces. + /// + public static Dictionary GetIPv4Interfaces() { + // zero conf ip address + IPAddress zeroConf = new IPAddress(0); + + NetworkInterface[] adapters = NetworkInterface.GetAllNetworkInterfaces().Where(network => network.OperationalStatus == OperationalStatus.Up && network.NetworkInterfaceType != NetworkInterfaceType.Unknown && network.NetworkInterfaceType != NetworkInterfaceType.Loopback).ToArray(); + + Dictionary result = new Dictionary(); + + foreach(NetworkInterface adapter in adapters) { + IPInterfaceProperties properties = adapter.GetIPProperties(); + if(properties == null || properties.GatewayAddresses.Count == 0 || properties.GatewayAddresses.All(gateway => Equals(gateway.Address, zeroConf)) || properties.UnicastAddresses.Count == 0 || properties.GatewayAddresses.All(address => Equals(address.Address, zeroConf)) || properties.UnicastAddresses.Any(a => a.Address.AddressFamily == AddressFamily.InterNetwork) == false) { + continue; + } + + result[adapter] = properties; + } + + return result; + } + + /// + /// Retrieves the local ip addresses. + /// + /// if set to true [include loopback]. + /// An array of local ip addresses. + public static IPAddress[] GetIPv4Addresses(Boolean includeLoopback = true) => GetIPv4Addresses(NetworkInterfaceType.Unknown, true, includeLoopback); + + /// + /// Retrieves the local ip addresses. + /// + /// Type of the interface. + /// if set to true [skip type filter]. + /// if set to true [include loopback]. + /// An array of local ip addresses. + public static IPAddress[] GetIPv4Addresses(NetworkInterfaceType interfaceType, Boolean skipTypeFilter = false, Boolean includeLoopback = false) { + List addressList = new List(); + NetworkInterface[] interfaces = NetworkInterface.GetAllNetworkInterfaces() + .Where(ni => (skipTypeFilter || ni.NetworkInterfaceType == interfaceType) && ni.OperationalStatus == OperationalStatus.Up).ToArray(); + + foreach(NetworkInterface networkInterface in interfaces) { + IPInterfaceProperties properties = networkInterface.GetIPProperties(); + + if(properties.GatewayAddresses.All(g => g.Address.AddressFamily != AddressFamily.InterNetwork)) { + continue; + } + + addressList.AddRange(properties.UnicastAddresses.Where(i => i.Address.AddressFamily == AddressFamily.InterNetwork).Select(i => i.Address)); + } + + if(includeLoopback || interfaceType == NetworkInterfaceType.Loopback) { + addressList.Add(IPAddress.Loopback); + } + + return addressList.ToArray(); + } + + /// + /// Gets the public IP address using ipify.org. + /// + /// The cancellation token. + /// A public IP address of the result produced by this Task. + public static async Task GetPublicIPAddressAsync(CancellationToken cancellationToken = default) { + using HttpClient client = new HttpClient(); + HttpResponseMessage response = await client.GetAsync("https://api.ipify.org", cancellationToken).ConfigureAwait(false); + return IPAddress.Parse(await response.Content.ReadAsStringAsync().ConfigureAwait(false)); + } + + /// + /// Gets the configured IPv4 DNS servers for the active network interfaces. + /// + /// + /// A collection of NetworkInterface/IPInterfaceProperties pairs + /// that represents the active IPv4 interfaces. + /// + public static IPAddress[] GetIPv4DnsServers() => GetIPv4Interfaces().Select(a => a.Value.DnsAddresses.Where(d => d.AddressFamily == AddressFamily.InterNetwork)).SelectMany(d => d).ToArray(); + + #endregion + + #region DNS and NTP Clients + + /// + /// Gets the DNS host entry (a list of IP addresses) for the domain name. + /// + /// The FQDN. + /// An array of local ip addresses of the result produced by this task. + public static Task GetDnsHostEntryAsync(String fqdn) { + IPAddress dnsServer = GetIPv4DnsServers().FirstOrDefault() ?? IPAddress.Parse("8.8.8.8"); + return GetDnsHostEntryAsync(fqdn, dnsServer, DnsDefaultPort); + } + + /// + /// Gets the DNS host entry (a list of IP addresses) for the domain name. + /// + /// The FQDN. + /// The DNS server. + /// The port. + /// + /// An array of local ip addresses of the result produced by this task. + /// + /// fqdn. + public static async Task GetDnsHostEntryAsync(String fqdn, IPAddress dnsServer, Int32 port) { + if(fqdn == null) { + throw new ArgumentNullException(nameof(fqdn)); + } + + if(fqdn.IndexOf(".", StringComparison.Ordinal) == -1) { + fqdn += "." + IPGlobalProperties.GetIPGlobalProperties().DomainName; + } + + while(true) { + if(!fqdn.EndsWith(".", StringComparison.OrdinalIgnoreCase)) { + break; + } + + fqdn = fqdn[0..^1]; + } + + DnsClient client = new DnsClient(dnsServer, port); + IList result = await client.Lookup(fqdn).ConfigureAwait(false); + return result.ToArray(); + } + + /// + /// Gets the reverse lookup FQDN of the given IP Address. + /// + /// The query. + /// The DNS server. + /// The port. + /// A that represents the current object. + public static Task GetDnsPointerEntryAsync(IPAddress query, IPAddress dnsServer, Int32 port) { + DnsClient client = new DnsClient(dnsServer, port); + return client.Reverse(query); + } + + /// + /// Gets the reverse lookup FQDN of the given IP Address. + /// + /// The query. + /// A that represents the current object. + public static Task GetDnsPointerEntryAsync(IPAddress query) { + DnsClient client = new DnsClient(GetIPv4DnsServers().FirstOrDefault()); + return client.Reverse(query); + } + + /// + /// Queries the DNS server for the specified record type. + /// + /// The query. + /// Type of the record. + /// The DNS server. + /// The port. + /// Queries the DNS server for the specified record type of the result produced by this Task. + public static async Task QueryDnsAsync(String query, DnsRecordType recordType, IPAddress dnsServer, Int32 port) { + if(query == null) { + throw new ArgumentNullException(nameof(query)); + } + + DnsClient client = new DnsClient(dnsServer, port); + DnsClient.DnsClientResponse response = await client.Resolve(query, recordType).ConfigureAwait(false); + return new DnsQueryResult(response); + } + + /// + /// Queries the DNS server for the specified record type. + /// + /// The query. + /// Type of the record. + /// Queries the DNS server for the specified record type of the result produced by this Task. + public static Task QueryDnsAsync(String query, DnsRecordType recordType) => QueryDnsAsync(query, recordType, GetIPv4DnsServers().FirstOrDefault(), DnsDefaultPort); + + /// + /// Gets the UTC time by querying from an NTP server. + /// + /// The NTP server address. + /// The port. + /// The UTC time by querying from an NTP server of the result produced by this Task. + public static async Task GetNetworkTimeUtcAsync(IPAddress ntpServerAddress, Int32 port = NtpDefaultPort) { + if(ntpServerAddress == null) { + throw new ArgumentNullException(nameof(ntpServerAddress)); + } + + // NTP message size - 16 bytes of the digest (RFC 2030) + Byte[] ntpData = new Byte[48]; + + // Setting the Leap Indicator, Version Number and Mode values + ntpData[0] = 0x1B; // LI = 0 (no warning), VN = 3 (IPv4 only), Mode = 3 (Client Mode) + + // The UDP port number assigned to NTP is 123 + IPEndPoint endPoint = new IPEndPoint(ntpServerAddress, port); + Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + + + await socket.ConnectAsync(endPoint).ConfigureAwait(false); + + + socket.ReceiveTimeout = 3000; // Stops code hang if NTP is blocked + _ = socket.Send(ntpData); + _ = socket.Receive(ntpData); + socket.Dispose(); + + // Offset to get to the "Transmit Timestamp" field (time at which the reply + // departed the server for the client, in 64-bit timestamp format." + const Byte serverReplyTime = 40; + + // Get the seconds part + UInt64 intPart = BitConverter.ToUInt32(ntpData, serverReplyTime); + + // Get the seconds fraction + UInt64 fractPart = BitConverter.ToUInt32(ntpData, serverReplyTime + 4); + + // Convert From big-endian to little-endian to match the platform + if(BitConverter.IsLittleEndian) { + intPart = intPart.SwapEndianness(); + fractPart = intPart.SwapEndianness(); + } + + UInt64 milliseconds = intPart * 1000 + fractPart * 1000 / 0x100000000L; + + // The time is given in UTC + return new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds((Int64)milliseconds); + } + + /// + /// Gets the UTC time by querying from an NTP server. + /// + /// The NTP server, by default pool.ntp.org. + /// The port, by default NTP 123. + /// The UTC time by querying from an NTP server of the result produced by this Task. + public static async Task GetNetworkTimeUtcAsync(String ntpServerName = "pool.ntp.org", Int32 port = NtpDefaultPort) { + IPAddress[] addresses = await GetDnsHostEntryAsync(ntpServerName).ConfigureAwait(false); + return await GetNetworkTimeUtcAsync(addresses.First(), port).ConfigureAwait(false); + } + + #endregion + } +} diff --git a/Swan.Tiny/ProcessResult.cs b/Swan.Tiny/ProcessResult.cs new file mode 100644 index 0000000..9703050 --- /dev/null +++ b/Swan.Tiny/ProcessResult.cs @@ -0,0 +1,51 @@ +using System; + +namespace Swan { + /// + /// Represents the text of the standard output and standard error + /// of a process, including its exit code. + /// + public class ProcessResult { + /// + /// Initializes a new instance of the class. + /// + /// The exit code. + /// The standard output. + /// The standard error. + public ProcessResult(Int32 exitCode, String standardOutput, String standardError) { + this.ExitCode = exitCode; + this.StandardOutput = standardOutput; + this.StandardError = standardError; + } + + /// + /// Gets the exit code. + /// + /// + /// The exit code. + /// + public Int32 ExitCode { + get; + } + + /// + /// Gets the text of the standard output. + /// + /// + /// The standard output. + /// + public String StandardOutput { + get; + } + + /// + /// Gets the text of the standard error. + /// + /// + /// The standard error. + /// + public String StandardError { + get; + } + } +} \ No newline at end of file diff --git a/Swan.Tiny/ProcessRunner.cs b/Swan.Tiny/ProcessRunner.cs new file mode 100644 index 0000000..a429c34 --- /dev/null +++ b/Swan.Tiny/ProcessRunner.cs @@ -0,0 +1,353 @@ +#nullable enable +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Swan { + /// + /// Provides methods to help create external processes, and efficiently capture the + /// standard error and standard output streams. + /// + public static class ProcessRunner { + /// + /// Defines a delegate to handle binary data reception from the standard + /// output or standard error streams from a process. + /// + /// The process data. + /// The process. + public delegate void ProcessDataReceivedCallback(Byte[] processData, Process process); + + /// + /// Runs the process asynchronously and if the exit code is 0, + /// returns all of the standard output text. If the exit code is something other than 0 + /// it returns the contents of standard error. + /// This method is meant to be used for programs that output a relatively small amount of text. + /// + /// The filename. + /// The arguments. + /// The working directory. + /// The cancellation token. + /// The type of the result produced by this Task. + /// + /// The following code explains how to run an external process using the + /// method. + /// + /// class Example + /// { + /// using System.Threading.Tasks; + /// using Swan; + /// + /// static async Task Main() + /// { + /// // execute a process and save its output + /// var data = await ProcessRunner. + /// GetProcessOutputAsync("dotnet", "--help"); + /// + /// // print the output + /// data.WriteLine(); + /// } + /// } + /// + /// + public static async Task GetProcessOutputAsync(String filename, String arguments = "", String? workingDirectory = null, CancellationToken cancellationToken = default) { + ProcessResult result = await GetProcessResultAsync(filename, arguments, workingDirectory, cancellationToken: cancellationToken).ConfigureAwait(false); + return result.ExitCode == 0 ? result.StandardOutput : result.StandardError; + } + + /// + /// Runs the process asynchronously and if the exit code is 0, + /// returns all of the standard output text. If the exit code is something other than 0 + /// it returns the contents of standard error. + /// This method is meant to be used for programs that output a relatively small amount + /// of text using a different encoder. + /// + /// The filename. + /// The arguments. + /// The encoding. + /// The cancellation token. + /// + /// The type of the result produced by this Task. + /// + public static async Task GetProcessEncodedOutputAsync(String filename, String arguments = "", Encoding? encoding = null, CancellationToken cancellationToken = default) { + ProcessResult result = await GetProcessResultAsync(filename, arguments, null, encoding, cancellationToken).ConfigureAwait(false); + return result.ExitCode == 0 ? result.StandardOutput : result.StandardError; + } + + /// + /// Executes a process asynchronously and returns the text of the standard output and standard error streams + /// along with the exit code. This method is meant to be used for programs that output a relatively small + /// amount of text. + /// + /// The filename. + /// The arguments. + /// The cancellation token. + /// + /// Text of the standard output and standard error streams along with the exit code as a instance. + /// + /// filename. + public static Task GetProcessResultAsync(String filename, String arguments = "", CancellationToken cancellationToken = default) => GetProcessResultAsync(filename, arguments, null, Definitions.CurrentAnsiEncoding, cancellationToken); + + /// + /// Executes a process asynchronously and returns the text of the standard output and standard error streams + /// along with the exit code. This method is meant to be used for programs that output a relatively small + /// amount of text. + /// + /// The filename. + /// The arguments. + /// The working directory. + /// The encoding. + /// The cancellation token. + /// + /// Text of the standard output and standard error streams along with the exit code as a instance. + /// + /// filename. + /// + /// The following code describes how to run an external process using the method. + /// + /// class Example + /// { + /// using System.Threading.Tasks; + /// using Swan; + /// + /// static async Task Main() + /// { + /// // Execute a process asynchronously + /// var data = await ProcessRunner.GetProcessResultAsync("dotnet", "--help"); + /// + /// // print out the exit code + /// $"{data.ExitCode}".WriteLine(); + /// + /// // print out the output + /// data.StandardOutput.WriteLine(); + /// // and the error if exists + /// data.StandardError.Error(); + /// } + /// } + /// + public static async Task GetProcessResultAsync(String filename, String arguments, String? workingDirectory, Encoding? encoding = null, CancellationToken cancellationToken = default) { + if(filename == null) { + throw new ArgumentNullException(nameof(filename)); + } + + if(encoding == null) { + encoding = Definitions.CurrentAnsiEncoding; + } + + StringBuilder standardOutputBuilder = new StringBuilder(); + StringBuilder standardErrorBuilder = new StringBuilder(); + + Int32 processReturn = await RunProcessAsync(filename, arguments, workingDirectory, (data, proc) => standardOutputBuilder.Append(encoding.GetString(data)), (data, proc) => standardErrorBuilder.Append(encoding.GetString(data)), encoding, true, cancellationToken).ConfigureAwait(false); + + return new ProcessResult(processReturn, standardOutputBuilder.ToString(), standardErrorBuilder.ToString()); + } + + /// + /// Runs an external process asynchronously, providing callbacks to + /// capture binary data from the standard error and standard output streams. + /// The callbacks contain a reference to the process so you can respond to output or + /// error streams by writing to the process' input stream. + /// The exit code (return value) will be -1 for forceful termination of the process. + /// + /// The filename. + /// The arguments. + /// The working directory. + /// The on output data. + /// The on error data. + /// The encoding. + /// if set to true the next data callback will wait until the current one completes. + /// The cancellation token. + /// + /// Value type will be -1 for forceful termination of the process. + /// + public static Task RunProcessAsync(String filename, String arguments, String? workingDirectory, ProcessDataReceivedCallback onOutputData, ProcessDataReceivedCallback? onErrorData, Encoding encoding, Boolean syncEvents = true, CancellationToken cancellationToken = default) { + if(filename == null) { + throw new ArgumentNullException(nameof(filename)); + } + + return Task.Run(() => { + // Setup the process and its corresponding start info + Process process = new Process { + EnableRaisingEvents = false, + StartInfo = new ProcessStartInfo { + Arguments = arguments, + CreateNoWindow = true, + FileName = filename, + RedirectStandardError = true, + StandardErrorEncoding = encoding, + RedirectStandardOutput = true, + StandardOutputEncoding = encoding, + UseShellExecute = false, + }, + }; + + if(!String.IsNullOrWhiteSpace(workingDirectory)) { + process.StartInfo.WorkingDirectory = workingDirectory; + } + + // Launch the process and discard any buffered data for standard error and standard output + _ = process.Start(); + process.StandardError.DiscardBufferedData(); + process.StandardOutput.DiscardBufferedData(); + + // Launch the asynchronous stream reading tasks + Task[] readTasks = new Task[2]; + readTasks[0] = CopyStreamAsync(process, process.StandardOutput.BaseStream, onOutputData, syncEvents, cancellationToken); + readTasks[1] = CopyStreamAsync(process, process.StandardError.BaseStream, onErrorData, syncEvents, cancellationToken); + + try { + // Wait for all tasks to complete + Task.WaitAll(readTasks, cancellationToken); + } catch(TaskCanceledException) { + // ignore + } finally { + // Wait for the process to exit + while(cancellationToken.IsCancellationRequested == false) { + if(process.HasExited || process.WaitForExit(5)) { + break; + } + } + + // Forcefully kill the process if it do not exit + try { + if(process.HasExited == false) { + process.Kill(); + } + } catch { + // swallow + } + } + + try { + // Retrieve and return the exit code. + // -1 signals error + return process.HasExited ? process.ExitCode : -1; + } catch { + return -1; + } + }, cancellationToken); + } + + /// + /// Runs an external process asynchronously, providing callbacks to + /// capture binary data from the standard error and standard output streams. + /// The callbacks contain a reference to the process so you can respond to output or + /// error streams by writing to the process' input stream. + /// The exit code (return value) will be -1 for forceful termination of the process. + /// + /// The filename. + /// The arguments. + /// The on output data. + /// The on error data. + /// if set to true the next data callback will wait until the current one completes. + /// The cancellation token. + /// Value type will be -1 for forceful termination of the process. + /// + /// The following example illustrates how to run an external process using the + /// + /// method. + /// + /// class Example + /// { + /// using System.Diagnostics; + /// using System.Text; + /// using System.Threading.Tasks; + /// using Swan; + /// + /// static async Task Main() + /// { + /// // Execute a process asynchronously + /// var data = await ProcessRunner + /// .RunProcessAsync("dotnet", "--help", Print, Print); + /// + /// // flush all messages + /// Terminal.Flush(); + /// } + /// + /// // a callback to print both output or errors + /// static void Print(byte[] data, Process proc) => + /// Encoding.GetEncoding(0).GetString(data).WriteLine(); + /// } + /// + /// + public static Task RunProcessAsync(String filename, String arguments, ProcessDataReceivedCallback onOutputData, ProcessDataReceivedCallback? onErrorData, Boolean syncEvents = true, CancellationToken cancellationToken = default) => RunProcessAsync(filename, arguments, null, onOutputData, onErrorData, Definitions.CurrentAnsiEncoding, syncEvents, cancellationToken); + + /// + /// Copies the stream asynchronously. + /// + /// The process. + /// The source stream. + /// The on data callback. + /// if set to true [synchronize events]. + /// The cancellation token. + /// Total copies stream. + private static Task CopyStreamAsync(Process process, Stream baseStream, ProcessDataReceivedCallback? onDataCallback, Boolean syncEvents, CancellationToken ct) => Task.Run(async () => { + // define some state variables + Byte[] swapBuffer = new Byte[2048]; // the buffer to copy data from one stream to the next + UInt64 totalCount = 0; // the total amount of bytes read + Boolean hasExited = false; + + while(ct.IsCancellationRequested == false) { + try { + // Check if process is no longer valid + // if this condition holds, simply read the last bits of data available. + Int32 readCount; // the bytes read in any given event + if(process.HasExited || process.WaitForExit(1)) { + while(true) { + try { + readCount = await baseStream.ReadAsync(swapBuffer, 0, swapBuffer.Length, ct); + + if(readCount > 0) { + totalCount += (UInt64)readCount; + onDataCallback?.Invoke(swapBuffer.Skip(0).Take(readCount).ToArray(), process); + } else { + hasExited = true; + break; + } + } catch { + hasExited = true; + break; + } + } + } + + if(hasExited) { + break; + } + + // Try reading from the stream. < 0 means no read occurred. + readCount = await baseStream.ReadAsync(swapBuffer, 0, swapBuffer.Length, ct).ConfigureAwait(false); + + // When no read is done, we need to let is rest for a bit + if(readCount <= 0) { + await Task.Delay(1, ct).ConfigureAwait(false); // do not hog CPU cycles doing nothing. + continue; + } + + totalCount += (UInt64)readCount; + if(onDataCallback == null) { + continue; + } + + // Create the buffer to pass to the callback + Byte[] eventBuffer = swapBuffer.Skip(0).Take(readCount).ToArray(); + + // Create the data processing callback invocation + Task eventTask = Task.Run(() => onDataCallback.Invoke(eventBuffer, process), ct); + + // wait for the event to process before the next read occurs + if(syncEvents) { + eventTask.Wait(ct); + } + } catch { + break; + } + } + + return totalCount; + }, ct); + } +} diff --git a/Swan.Tiny/Reflection/AttributeCache.cs b/Swan.Tiny/Reflection/AttributeCache.cs new file mode 100644 index 0000000..46423fa --- /dev/null +++ b/Swan.Tiny/Reflection/AttributeCache.cs @@ -0,0 +1,161 @@ +#nullable enable +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Swan.Reflection { + /// + /// A thread-safe cache of attributes belonging to a given key (MemberInfo or Type). + /// + /// The Retrieve method is the most useful one in this class as it + /// calls the retrieval process if the type is not contained + /// in the cache. + /// + public class AttributeCache { + private readonly Lazy, IEnumerable>> _data = new Lazy, IEnumerable>>(() => new ConcurrentDictionary, IEnumerable>(), true); + + /// + /// Initializes a new instance of the class. + /// + /// The property cache object. + public AttributeCache(PropertyTypeCache? propertyCache = null) => this.PropertyTypeCache = propertyCache ?? PropertyTypeCache.DefaultCache.Value; + + /// + /// Gets the default cache. + /// + /// + /// The default cache. + /// + public static Lazy DefaultCache { get; } = new Lazy(() => new AttributeCache()); + + /// + /// A PropertyTypeCache object for caching properties and their attributes. + /// + public PropertyTypeCache PropertyTypeCache { + get; + } + + /// + /// Determines whether [contains] [the specified member]. + /// + /// The type of the attribute to be retrieved. + /// The member. + /// + /// true if [contains] [the specified member]; otherwise, false. + /// + public Boolean Contains(MemberInfo member) => this._data.Value.ContainsKey(new Tuple(member, typeof(T))); + + /// + /// Gets specific attributes from a member constrained to an attribute. + /// + /// The type of the attribute to be retrieved. + /// The member. + /// true to inspect the ancestors of element; otherwise, false. + /// An array of the attributes stored for the specified type. + public IEnumerable Retrieve(MemberInfo member, Boolean inherit = false) where T : Attribute { + if(member == null) { + throw new ArgumentNullException(nameof(member)); + } + + return this.Retrieve(new Tuple(member, typeof(T)), t => member.GetCustomAttributes(inherit)); + } + + /// + /// Gets all attributes of a specific type from a member. + /// + /// The member. + /// The attribute type. + /// true to inspect the ancestors of element; otherwise, false. + /// An array of the attributes stored for the specified type. + public IEnumerable Retrieve(MemberInfo member, Type type, Boolean inherit = false) { + if(member == null) { + throw new ArgumentNullException(nameof(member)); + } + + if(type == null) { + throw new ArgumentNullException(nameof(type)); + } + + return this.Retrieve(new Tuple(member, type), t => member.GetCustomAttributes(type, inherit)); + } + + /// + /// Gets one attribute of a specific type from a member. + /// + /// The attribute type. + /// The member. + /// true to inspect the ancestors of element; otherwise, false. + /// An attribute stored for the specified type. + public T RetrieveOne(MemberInfo member, Boolean inherit = false) where T : Attribute { + if(member == null) { + return default!; + } + + IEnumerable attr = this.Retrieve(new Tuple(member, typeof(T)), t => member.GetCustomAttributes(typeof(T), inherit)); + + return ConvertToAttribute(attr); + } + + /// + /// Gets one attribute of a specific type from a generic type. + /// + /// The type of the attribute. + /// The type to retrieve the attribute. + /// if set to true [inherit]. + /// An attribute stored for the specified type. + public TAttribute RetrieveOne(Boolean inherit = false) where TAttribute : Attribute { + IEnumerable attr = this.Retrieve(new Tuple(typeof(T), typeof(TAttribute)), t => typeof(T).GetCustomAttributes(typeof(TAttribute), inherit)); + + return ConvertToAttribute(attr); + } + + /// + /// Gets all properties an their attributes of a given type constrained to only attributes. + /// + /// The type of the attribute to retrieve. + /// The type of the object. + /// true to inspect the ancestors of element; otherwise, false. + /// A dictionary of the properties and their attributes stored for the specified type. + public Dictionary> Retrieve(Type type, Boolean inherit = false) where T : Attribute => this.PropertyTypeCache.RetrieveAllProperties(type, true).ToDictionary(x => x, x => this.Retrieve(x, inherit)); + + /// + /// Gets all properties and their attributes of a given type. + /// + /// The object type used to extract the properties from. + /// The type of the attribute. + /// true to inspect the ancestors of element; otherwise, false. + /// + /// A dictionary of the properties and their attributes stored for the specified type. + /// + public Dictionary> RetrieveFromType(Boolean inherit = false) => this.RetrieveFromType(typeof(TAttribute), inherit); + + /// + /// Gets all properties and their attributes of a given type. + /// + /// The object type used to extract the properties from. + /// Type of the attribute. + /// true to inspect the ancestors of element; otherwise, false. + /// + /// A dictionary of the properties and their attributes stored for the specified type. + /// + public Dictionary> RetrieveFromType(Type attributeType, Boolean inherit = false) { + if(attributeType == null) { + throw new ArgumentNullException(nameof(attributeType)); + } + + return this.PropertyTypeCache.RetrieveAllProperties(true).ToDictionary(x => x, x => this.Retrieve(x, attributeType, inherit)); + } + + private static T ConvertToAttribute(IEnumerable attr) where T : Attribute => attr?.Any() != true ? (default!) : attr.Count() == 1 ? (T)Convert.ChangeType(attr.First(), typeof(T)) : throw new AmbiguousMatchException("Multiple custom attributes of the same type found."); + + private IEnumerable Retrieve(Tuple key, Func, IEnumerable> factory) { + if(factory == null) { + throw new ArgumentNullException(nameof(factory)); + } + + return this._data.Value.GetOrAdd(key, k => factory.Invoke(k).Where(item => item != null)); + } + } +} diff --git a/Swan.Tiny/Reflection/ConstructorTypeCache.cs b/Swan.Tiny/Reflection/ConstructorTypeCache.cs new file mode 100644 index 0000000..e1a42af --- /dev/null +++ b/Swan.Tiny/Reflection/ConstructorTypeCache.cs @@ -0,0 +1,42 @@ +using Swan.Reflection; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Swan.Lite.Reflection { + /// + /// A thread-safe cache of constructors belonging to a given type. + /// + public class ConstructorTypeCache : TypeCache> { + /// + /// Gets the default cache. + /// + /// + /// The default cache. + /// + public static Lazy DefaultCache { get; } = new Lazy(() => new ConstructorTypeCache()); + + /// + /// Retrieves all constructors order by the number of parameters ascending. + /// + /// The type to inspect. + /// if set to true [include non public]. + /// + /// A collection with all the constructors in the given type. + /// + public IEnumerable> RetrieveAllConstructors(Boolean includeNonPublic = false) => this.Retrieve(GetConstructors(includeNonPublic)); + + /// + /// Retrieves all constructors order by the number of parameters ascending. + /// + /// The type. + /// if set to true [include non public]. + /// + /// A collection with all the constructors in the given type. + /// + public IEnumerable> RetrieveAllConstructors(Type type, Boolean includeNonPublic = false) => this.Retrieve(type, GetConstructors(includeNonPublic)); + + private static Func>> GetConstructors(Boolean includeNonPublic) => t => t.GetConstructors(includeNonPublic ? BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance : BindingFlags.Public | BindingFlags.Instance).Select(x => Tuple.Create(x, x.GetParameters())).OrderBy(x => x.Item2.Length).ToList(); + } +} \ No newline at end of file diff --git a/Swan.Tiny/Reflection/ExtendedTypeInfo.cs b/Swan.Tiny/Reflection/ExtendedTypeInfo.cs new file mode 100644 index 0000000..020bcf9 --- /dev/null +++ b/Swan.Tiny/Reflection/ExtendedTypeInfo.cs @@ -0,0 +1,247 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using System.Reflection; + +namespace Swan.Reflection { + /// + /// Provides extended information about a type. + /// + /// This class is mainly used to define sets of types within the Definition class + /// and it is not meant for other than querying the BasicTypesInfo dictionary. + /// + public class ExtendedTypeInfo { + private const String TryParseMethodName = nameof(Byte.TryParse); + private const String ToStringMethodName = nameof(ToString); + + private static readonly Type[] NumericTypes = + { + typeof(Byte), + typeof(SByte), + typeof(Decimal), + typeof(Double), + typeof(Single), + typeof(Int32), + typeof(UInt32), + typeof(Int64), + typeof(UInt64), + typeof(Int16), + typeof(UInt16), + }; + + private readonly ParameterInfo[]? _tryParseParameters; + private readonly Int32 _toStringArgumentLength; + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The t. + public ExtendedTypeInfo(Type t) { + this.Type = t ?? throw new ArgumentNullException(nameof(t)); + this.IsNullableValueType = this.Type.IsGenericType && this.Type.GetGenericTypeDefinition() == typeof(Nullable<>); + + this.IsValueType = t.IsValueType; + + this.UnderlyingType = this.IsNullableValueType ? new NullableConverter(this.Type).UnderlyingType : this.Type; + + this.IsNumeric = NumericTypes.Contains(this.UnderlyingType); + + // Extract the TryParse method info + try { + this.TryParseMethodInfo = this.UnderlyingType.GetMethod(TryParseMethodName, new[] { typeof(String), typeof(NumberStyles), typeof(IFormatProvider), this.UnderlyingType.MakeByRefType() }) ?? this.UnderlyingType.GetMethod(TryParseMethodName, new[] { typeof(String), this.UnderlyingType.MakeByRefType() }); + + this._tryParseParameters = this.TryParseMethodInfo?.GetParameters(); + } catch { + // ignored + } + + // Extract the ToString method Info + try { + this.ToStringMethodInfo = this.UnderlyingType.GetMethod(ToStringMethodName, new[] { typeof(IFormatProvider) }) ?? this.UnderlyingType.GetMethod(ToStringMethodName, Array.Empty()); + + this._toStringArgumentLength = this.ToStringMethodInfo?.GetParameters().Length ?? 0; + } catch { + // ignored + } + } + + #endregion + + #region Properties + + /// + /// Gets the type this extended info class provides for. + /// + /// + /// The type. + /// + public Type Type { + get; + } + + /// + /// Gets a value indicating whether the type is a nullable value type. + /// + /// + /// true if this instance is nullable value type; otherwise, false. + /// + public Boolean IsNullableValueType { + get; + } + + /// + /// Gets a value indicating whether the type or underlying type is numeric. + /// + /// + /// true if this instance is numeric; otherwise, false. + /// + public Boolean IsNumeric { + get; + } + + /// + /// Gets a value indicating whether the type is value type. + /// Nullable value types have this property set to False. + /// + public Boolean IsValueType { + get; + } + + /// + /// When dealing with nullable value types, this property will + /// return the underlying value type of the nullable, + /// Otherwise it will return the same type as the Type property. + /// + /// + /// The type of the underlying. + /// + public Type UnderlyingType { + get; + } + + /// + /// Gets the try parse method information. If the type does not contain + /// a suitable TryParse static method, it will return null. + /// + /// + /// The try parse method information. + /// + public MethodInfo? TryParseMethodInfo { + get; + } + + /// + /// Gets the ToString method info + /// It will prefer the overload containing the IFormatProvider argument. + /// + /// + /// To string method information. + /// + public MethodInfo? ToStringMethodInfo { + get; + } + + /// + /// Gets a value indicating whether the type contains a suitable TryParse method. + /// + /// + /// true if this instance can parse natively; otherwise, false. + /// + public Boolean CanParseNatively => this.TryParseMethodInfo != null; + + #endregion + + #region Methods + + /// + /// Tries to parse the string into an object of the type this instance represents. + /// Returns false when no suitable TryParse methods exists for the type or when parsing fails + /// for any reason. When possible, this method uses CultureInfo.InvariantCulture and NumberStyles.Any. + /// + /// The s. + /// The result. + /// true if parse was converted successfully; otherwise, false. + public Boolean TryParse(String s, out Object? result) { + result = this.Type.GetDefault(); + + try { + if(this.Type == typeof(String)) { + result = Convert.ChangeType(s, this.Type, CultureInfo.InvariantCulture); + return true; + } + + if(this.IsNullableValueType && String.IsNullOrEmpty(s) || !this.CanParseNatively) { + return true; + } + + // Build the arguments of the TryParse method + List dynamicArguments = new List { s }; + if(this._tryParseParameters != null) { + for(Int32 pi = 1; pi < this._tryParseParameters.Length - 1; pi++) { + ParameterInfo argInfo = this._tryParseParameters[pi]; + if(argInfo.ParameterType == typeof(IFormatProvider)) { + dynamicArguments.Add(CultureInfo.InvariantCulture); + } else if(argInfo.ParameterType == typeof(NumberStyles)) { + dynamicArguments.Add(NumberStyles.Any); + } else { + dynamicArguments.Add(null); + } + } + } + + dynamicArguments.Add(null); + Object?[] parseArguments = dynamicArguments.ToArray(); + + if((Boolean)this.TryParseMethodInfo?.Invoke(null, parseArguments)!) { + result = parseArguments[^1]; + return true; + } + } catch { + // Ignore + } + + return false; + } + + /// + /// Converts this instance to its string representation, + /// trying to use the CultureInfo.InvariantCulture + /// IFormat provider if the overload is available. + /// + /// The instance. + /// A that represents the current object. + public String ToStringInvariant(Object? instance) => instance == null ? String.Empty : this._toStringArgumentLength != 1 ? instance.ToString()! : this.ToStringMethodInfo?.Invoke(instance, new Object[] { CultureInfo.InvariantCulture }) as String ?? String.Empty; + + #endregion + } + + /// + /// Provides extended information about a type. + /// + /// This class is mainly used to define sets of types within the Constants class + /// and it is not meant for other than querying the BasicTypesInfo dictionary. + /// + /// The type of extended type information. + public class ExtendedTypeInfo : ExtendedTypeInfo { + /// + /// Initializes a new instance of the class. + /// + public ExtendedTypeInfo() : base(typeof(T)) { + // placeholder + } + + /// + /// Converts this instance to its string representation, + /// trying to use the CultureInfo.InvariantCulture + /// IFormat provider if the overload is available. + /// + /// The instance. + /// A that represents the current object. + public String ToStringInvariant(T instance) => base.ToStringInvariant(instance); + } +} diff --git a/Swan.Tiny/Reflection/IPropertyProxy.cs b/Swan.Tiny/Reflection/IPropertyProxy.cs new file mode 100644 index 0000000..169786d --- /dev/null +++ b/Swan.Tiny/Reflection/IPropertyProxy.cs @@ -0,0 +1,22 @@ +using System; + +namespace Swan.Reflection { + /// + /// Represents a generic interface to store getters and setters. + /// + public interface IPropertyProxy { + /// + /// Gets the property value via a stored delegate. + /// + /// The instance. + /// The property value. + Object GetValue(Object instance); + + /// + /// Sets the property value via a stored delegate. + /// + /// The instance. + /// The value. + void SetValue(Object instance, Object value); + } +} \ No newline at end of file diff --git a/Swan.Tiny/Reflection/PropertyProxy.cs b/Swan.Tiny/Reflection/PropertyProxy.cs new file mode 100644 index 0000000..f06b58e --- /dev/null +++ b/Swan.Tiny/Reflection/PropertyProxy.cs @@ -0,0 +1,44 @@ +using System; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace Swan.Reflection { + /// + /// Represents a generic class to store getters and setters. + /// + /// The type of the class. + /// The type of the property. + /// + public sealed class PropertyProxy : IPropertyProxy where TClass : class { + private readonly Func _getter; + private readonly Action _setter; + + /// + /// Initializes a new instance of the class. + /// + /// The property. + public PropertyProxy(PropertyInfo property) { + if(property == null) { + throw new ArgumentNullException(nameof(property)); + } + + MethodInfo getterInfo = property.GetGetMethod(false); + if(getterInfo != null) { + this._getter = (Func)Delegate.CreateDelegate(typeof(Func), getterInfo); + } + + MethodInfo setterInfo = property.GetSetMethod(false); + if(setterInfo != null) { + this._setter = (Action)Delegate.CreateDelegate(typeof(Action), setterInfo); + } + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + Object IPropertyProxy.GetValue(Object instance) => this._getter(instance as TClass); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + void IPropertyProxy.SetValue(Object instance, Object value) => this._setter(instance as TClass, (TProperty)value); + } +} \ No newline at end of file diff --git a/Swan.Tiny/Reflection/PropertyTypeCache.cs b/Swan.Tiny/Reflection/PropertyTypeCache.cs new file mode 100644 index 0000000..c5ec4c6 --- /dev/null +++ b/Swan.Tiny/Reflection/PropertyTypeCache.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Swan.Reflection { + /// + /// A thread-safe cache of properties belonging to a given type. + /// + public class PropertyTypeCache : TypeCache { + /// + /// Gets the default cache. + /// + /// + /// The default cache. + /// + public static Lazy DefaultCache { get; } = new Lazy(() => new PropertyTypeCache()); + + /// + /// Retrieves all properties. + /// + /// The type to inspect. + /// if set to true [only public]. + /// + /// A collection with all the properties in the given type. + /// + public IEnumerable RetrieveAllProperties(Boolean onlyPublic = false) => this.Retrieve(onlyPublic ? GetAllPublicPropertiesFunc() : GetAllPropertiesFunc()); + + /// + /// Retrieves all properties. + /// + /// The type. + /// if set to true [only public]. + /// + /// A collection with all the properties in the given type. + /// + public IEnumerable RetrieveAllProperties(Type type, Boolean onlyPublic = false) => this.Retrieve(type, onlyPublic ? GetAllPublicPropertiesFunc() : GetAllPropertiesFunc()); + + /// + /// Retrieves the filtered properties. + /// + /// The type. + /// if set to true [only public]. + /// The filter. + /// + /// A collection with all the properties in the given type. + /// + public IEnumerable RetrieveFilteredProperties(Type type, Boolean onlyPublic, Func filter) => this.Retrieve(type, onlyPublic ? GetAllPublicPropertiesFunc(filter) : GetAllPropertiesFunc(filter)); + + private static Func> GetAllPropertiesFunc(Func filter = null) => GetPropertiesFunc(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, filter); + + private static Func> GetAllPublicPropertiesFunc(Func filter = null) => GetPropertiesFunc(BindingFlags.Public | BindingFlags.Instance, filter); + + private static Func> GetPropertiesFunc(BindingFlags flags, Func filter = null) => t => t.GetProperties(flags).Where(filter ?? (p => p.CanRead || p.CanWrite)); + } +} \ No newline at end of file diff --git a/Swan.Tiny/Reflection/TypeCache.cs b/Swan.Tiny/Reflection/TypeCache.cs new file mode 100644 index 0000000..d6bd3b3 --- /dev/null +++ b/Swan.Tiny/Reflection/TypeCache.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using Swan.Collections; + +namespace Swan.Reflection { + /// + /// A thread-safe cache of members belonging to a given type. + /// + /// The Retrieve method is the most useful one in this class as it + /// calls the retrieval process if the type is not contained + /// in the cache. + /// + /// The type of Member to be cached. + public abstract class TypeCache : CollectionCacheRepository { + /// + /// Determines whether the cache contains the specified type. + /// + /// The type of the out. + /// + /// true if [contains]; otherwise, false. + /// + public Boolean Contains() => this.ContainsKey(typeof(TOut)); + + /// + /// Retrieves the properties stored for the specified type. + /// If the properties are not available, it calls the factory method to retrieve them + /// and returns them as an array of PropertyInfo. + /// + /// The type of the out. + /// The factory. + /// An array of the properties stored for the specified type. + public IEnumerable Retrieve(Func> factory) => this.Retrieve(typeof(TOut), factory); + } + + /// + /// A thread-safe cache of fields belonging to a given type + /// The Retrieve method is the most useful one in this class as it + /// calls the retrieval process if the type is not contained + /// in the cache. + /// + public class FieldTypeCache : TypeCache { + /// + /// Gets the default cache. + /// + /// + /// The default cache. + /// + public static Lazy DefaultCache { get; } = new Lazy(() => new FieldTypeCache()); + + /// + /// Retrieves all fields. + /// + /// The type to inspect. + /// + /// A collection with all the fields in the given type. + /// + public IEnumerable RetrieveAllFields() => this.Retrieve(GetAllFieldsFunc()); + + /// + /// Retrieves all fields. + /// + /// The type. + /// + /// A collection with all the fields in the given type. + /// + public IEnumerable RetrieveAllFields(Type type) => this.Retrieve(type, GetAllFieldsFunc()); + + private static Func> GetAllFieldsFunc() => t => t.GetFields(BindingFlags.Public | BindingFlags.Instance); + } +} \ No newline at end of file diff --git a/Swan.Tiny/SingletonBase.cs b/Swan.Tiny/SingletonBase.cs new file mode 100644 index 0000000..cc03cdb --- /dev/null +++ b/Swan.Tiny/SingletonBase.cs @@ -0,0 +1,54 @@ +using System; + +namespace Swan { + /// + /// Represents a singleton pattern abstract class. + /// + /// The type of class. + public abstract class SingletonBase : IDisposable where T : class { + /// + /// The static, singleton instance reference. + /// + protected static readonly Lazy LazyInstance = new Lazy(valueFactory: () => Activator.CreateInstance(typeof(T), true) as T, isThreadSafe: true); + + private Boolean _isDisposing; // To detect redundant calls + + /// + /// Gets the instance that this singleton represents. + /// If the instance is null, it is constructed and assigned when this member is accessed. + /// + /// + /// The instance. + /// + public static T Instance => LazyInstance.Value; + + /// + public void Dispose() => this.Dispose(true); + + /// + /// Releases unmanaged and - optionally - managed resources. + /// Call the GC.SuppressFinalize if you override this method and use + /// a non-default class finalizer (destructor). + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(Boolean disposeManaged) { + if(this._isDisposing) { + return; + } + + this._isDisposing = true; + + // free managed resources + if(LazyInstance == null) { + return; + } + + try { + IDisposable disposableInstance = LazyInstance.Value as IDisposable; + disposableInstance?.Dispose(); + } catch { + // swallow + } + } + } +} diff --git a/Swan.Tiny/StructEndiannessAttribute.cs b/Swan.Tiny/StructEndiannessAttribute.cs new file mode 100644 index 0000000..5648093 --- /dev/null +++ b/Swan.Tiny/StructEndiannessAttribute.cs @@ -0,0 +1,27 @@ +using System; + +namespace Swan { + /// + /// An attribute used to help conversion structs back and forth into arrays of bytes via + /// extension methods included in this library ToStruct and ToBytes. + /// + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Struct)] + public class StructEndiannessAttribute : Attribute { + /// + /// Initializes a new instance of the class. + /// + /// The endianness. + public StructEndiannessAttribute(Endianness endianness) => this.Endianness = endianness; + + /// + /// Gets the endianness. + /// + /// + /// The endianness. + /// + public Endianness Endianness { + get; + } + } +} \ No newline at end of file diff --git a/Swan.Tiny/Swan.Tiny.csproj b/Swan.Tiny/Swan.Tiny.csproj new file mode 100644 index 0000000..feeeacf --- /dev/null +++ b/Swan.Tiny/Swan.Tiny.csproj @@ -0,0 +1,7 @@ + + + + netcoreapp3.0 + + + diff --git a/Swan.Tiny/SwanRuntime.cs b/Swan.Tiny/SwanRuntime.cs new file mode 100644 index 0000000..d93109d --- /dev/null +++ b/Swan.Tiny/SwanRuntime.cs @@ -0,0 +1,200 @@ +using Swan.Logging; +using System; +using System.IO; +using System.Reflection; +using System.Threading; + +namespace Swan { + /// + /// Provides utility methods to retrieve information about the current application. + /// + public static class SwanRuntime { + private static readonly Lazy EntryAssemblyLazy = new Lazy(Assembly.GetEntryAssembly); + + private static readonly Lazy CompanyNameLazy = new Lazy(() => { + AssemblyCompanyAttribute attribute = EntryAssembly.GetCustomAttribute(typeof(AssemblyCompanyAttribute)) as AssemblyCompanyAttribute; + return attribute?.Company ?? String.Empty; + }); + + private static readonly Lazy ProductNameLazy = new Lazy(() => { + AssemblyProductAttribute attribute = EntryAssembly.GetCustomAttribute(typeof(AssemblyProductAttribute)) as AssemblyProductAttribute; + return attribute?.Product ?? String.Empty; + }); + + private static readonly Lazy ProductTrademarkLazy = new Lazy(() => { + AssemblyTrademarkAttribute attribute = EntryAssembly.GetCustomAttribute(typeof(AssemblyTrademarkAttribute)) as AssemblyTrademarkAttribute; + return attribute?.Trademark ?? String.Empty; + }); + + private static readonly String ApplicationMutexName = "Global\\{{" + EntryAssembly.FullName + "}}"; + + private static readonly Object SyncLock = new Object(); + + private static OperatingSystem? _oS; + + #region Properties + + /// + /// Gets the current Operating System. + /// + /// + /// The os. + /// + public static OperatingSystem OS { + get { + if(_oS.HasValue == false) { + String windowsDirectory = Environment.GetEnvironmentVariable("windir"); + _oS = String.IsNullOrEmpty(windowsDirectory) == false && windowsDirectory.Contains(@"\") && Directory.Exists(windowsDirectory) + ? (OperatingSystem?)OperatingSystem.Windows + : (OperatingSystem?)(File.Exists(@"/proc/sys/kernel/ostype") ? OperatingSystem.Unix : OperatingSystem.Osx); + } + + return _oS ?? OperatingSystem.Unknown; + } + } + + + /// + /// Checks if this application (including version number) is the only instance currently running. + /// + /// + /// true if this instance is the only instance; otherwise, false. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Codequalität", "IDE0067:Objekte verwerfen, bevor Bereich verloren geht", Justification = "")] + public static Boolean IsTheOnlyInstance { + get { + lock(SyncLock) { + try { + // Try to open existing mutex. + _ = Mutex.OpenExisting(ApplicationMutexName); + } catch { + try { + // If exception occurred, there is no such mutex. + Mutex appMutex = new Mutex(true, ApplicationMutexName); + $"Application Mutex created {appMutex} named '{ApplicationMutexName}'".Debug(typeof(SwanRuntime)); + + // Only one instance. + return true; + } catch { + // Sometimes the user can't create the Global Mutex + } + } + + // More than one instance. + return false; + } + } + } + + /// + /// Gets a value indicating whether this application instance is using the MONO runtime. + /// + /// + /// true if this instance is using MONO runtime; otherwise, false. + /// + public static Boolean IsUsingMonoRuntime => Type.GetType("Mono.Runtime") != null; + + /// + /// Gets the assembly that started the application. + /// + /// + /// The entry assembly. + /// + public static Assembly EntryAssembly => EntryAssemblyLazy.Value; + + /// + /// Gets the name of the entry assembly. + /// + /// + /// The name of the entry assembly. + /// + public static AssemblyName EntryAssemblyName => EntryAssemblyLazy.Value.GetName(); + + /// + /// Gets the entry assembly version. + /// + public static Version EntryAssemblyVersion => EntryAssemblyName.Version; + + /// + /// Gets the full path to the folder containing the assembly that started the application. + /// + /// + /// The entry assembly directory. + /// + public static String EntryAssemblyDirectory { + get { + UriBuilder uri = new UriBuilder(EntryAssembly.CodeBase); + String path = Uri.UnescapeDataString(uri.Path); + return Path.GetDirectoryName(path); + } + } + + /// + /// Gets the name of the company. + /// + /// + /// The name of the company. + /// + public static String CompanyName => CompanyNameLazy.Value; + + /// + /// Gets the name of the product. + /// + /// + /// The name of the product. + /// + public static String ProductName => ProductNameLazy.Value; + + /// + /// Gets the trademark. + /// + /// + /// The product trademark. + /// + public static String ProductTrademark => ProductTrademarkLazy.Value; + + /// + /// Gets a local storage path with a version. + /// + /// + /// The local storage path. + /// + public static String LocalStoragePath { + get { + String localAppDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), EntryAssemblyName.Name); + + String returnPath = Path.Combine(localAppDataPath, EntryAssemblyVersion.ToString()); + + if(!Directory.Exists(returnPath)) { + _ = Directory.CreateDirectory(returnPath); + } + + return returnPath; + } + } + + #endregion + + #region Methods + + /// + /// Build a full path pointing to the current user's desktop with the given filename. + /// + /// The filename. + /// + /// The fully qualified location of path, such as "C:\MyFile.txt". + /// + /// filename. + public static String GetDesktopFilePath(String filename) { + if(String.IsNullOrWhiteSpace(filename)) { + throw new ArgumentNullException(nameof(filename)); + } + + String pathWithFilename = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), filename); + + return Path.GetFullPath(pathWithFilename); + } + + #endregion + } +} diff --git a/Swan.Tiny/Terminal.Output.cs b/Swan.Tiny/Terminal.Output.cs new file mode 100644 index 0000000..ec08772 --- /dev/null +++ b/Swan.Tiny/Terminal.Output.cs @@ -0,0 +1,88 @@ +#nullable enable +using System; + +namespace Swan { + /// + /// A console terminal helper to create nicer output and receive input from the user + /// This class is thread-safe :). + /// + public static partial class Terminal { + /// + /// Writes a character a number of times, optionally adding a new line at the end. + /// + /// The character code. + /// The color. + /// The count. + /// if set to true [new line]. + /// The writer flags. + public static void Write(Char charCode, ConsoleColor? color = null, Int32 count = 1, Boolean newLine = false, TerminalWriters writerFlags = TerminalWriters.StandardOutput) { + lock(SyncLock) { + String text = new String(charCode, count); + + if(newLine) { + text += Environment.NewLine; + } + + Byte[] buffer = OutputEncoding.GetBytes(text); + OutputContext context = new OutputContext { + OutputColor = color ?? Settings.DefaultColor, + OutputText = OutputEncoding.GetChars(buffer), + OutputWriters = writerFlags, + }; + + EnqueueOutput(context); + } + } + + /// + /// Writes the specified text in the given color. + /// + /// The text. + /// The color. + /// The writer flags. + public static void Write(String? text, ConsoleColor? color = null, TerminalWriters writerFlags = TerminalWriters.StandardOutput) { + if(text == null) { + return; + } + + lock(SyncLock) { + Byte[] buffer = OutputEncoding.GetBytes(text); + OutputContext context = new OutputContext { + OutputColor = color ?? Settings.DefaultColor, + OutputText = OutputEncoding.GetChars(buffer), + OutputWriters = writerFlags, + }; + + EnqueueOutput(context); + } + } + + /// + /// Writes a New Line Sequence to the standard output. + /// + /// The writer flags. + public static void WriteLine(TerminalWriters writerFlags = TerminalWriters.StandardOutput) => Write(Environment.NewLine, Settings.DefaultColor, writerFlags); + + /// + /// Writes a line of text in the current console foreground color + /// to the standard output. + /// + /// The text. + /// The color. + /// The writer flags. + public static void WriteLine(String text, ConsoleColor? color = null, TerminalWriters writerFlags = TerminalWriters.StandardOutput) => Write($"{text ?? String.Empty}{Environment.NewLine}", color, writerFlags); + + /// + /// As opposed to WriteLine methods, it prepends a Carriage Return character to the text + /// so that the console moves the cursor one position up after the text has been written out. + /// + /// The text. + /// The color. + /// The writer flags. + public static void OverwriteLine(String text, ConsoleColor? color = null, TerminalWriters writerFlags = TerminalWriters.StandardOutput) { + Write($"\r{text ?? String.Empty}", color, writerFlags); + Flush(); + CursorLeft = 0; + } + } +} diff --git a/Swan.Tiny/Terminal.Settings.cs b/Swan.Tiny/Terminal.Settings.cs new file mode 100644 index 0000000..abb700c --- /dev/null +++ b/Swan.Tiny/Terminal.Settings.cs @@ -0,0 +1,46 @@ +using System; + +namespace Swan { + /// + /// A console terminal helper to create nicer output and receive input from the user + /// This class is thread-safe :). + /// + public static partial class Terminal { + /// + /// Terminal global settings. + /// + public static class Settings { + /// + /// Gets or sets the default output color. + /// + /// + /// The default color. + /// + public static ConsoleColor DefaultColor { get; set; } = Console.ForegroundColor; + + /// + /// Gets the color of the border. + /// + /// + /// The color of the border. + /// + public static ConsoleColor BorderColor { get; } = ConsoleColor.DarkGreen; + + /// + /// Gets or sets the user input prefix. + /// + /// + /// The user input prefix. + /// + public static String UserInputPrefix { get; set; } = "USR"; + + /// + /// Gets or sets the user option text. + /// + /// + /// The user option text. + /// + public static String UserOptionText { get; set; } = " Option: "; + } + } +} diff --git a/Swan.Tiny/Terminal.cs b/Swan.Tiny/Terminal.cs new file mode 100644 index 0000000..596c8c8 --- /dev/null +++ b/Swan.Tiny/Terminal.cs @@ -0,0 +1,330 @@ +using System; +using System.Collections.Concurrent; +using System.Text; +using System.Threading; + +using Swan.Threading; + +namespace Swan { + /// + /// A console terminal helper to create nicer output and receive input from the user. + /// This class is thread-safe :). + /// + public static partial class Terminal { + #region Private Declarations + + private const Int32 OutputFlushInterval = 15; + private static readonly ExclusiveTimer DequeueOutputTimer; + private static readonly Object SyncLock = new Object(); + private static readonly ConcurrentQueue OutputQueue = new ConcurrentQueue(); + + private static readonly ManualResetEventSlim OutputDone = new ManualResetEventSlim(false); + private static readonly ManualResetEventSlim InputDone = new ManualResetEventSlim(true); + + private static Boolean? _isConsolePresent; + + #endregion + + #region Constructors + + /// + /// Initializes static members of the class. + /// + static Terminal() { + lock(SyncLock) { + if(DequeueOutputTimer != null) { + return; + } + + if(IsConsolePresent) { + Console.CursorVisible = false; + } + + // Here we start the output task, fire-and-forget + DequeueOutputTimer = new ExclusiveTimer(DequeueOutputCycle); + DequeueOutputTimer.Resume(OutputFlushInterval); + } + } + + #endregion + + #region Synchronized Cursor Movement + + /// + /// Gets or sets the cursor left position. + /// + /// + /// The cursor left. + /// + public static Int32 CursorLeft { + get { + if(IsConsolePresent == false) { + return -1; + } + + lock(SyncLock) { + Flush(); + return Console.CursorLeft; + } + } + set { + if(IsConsolePresent == false) { + return; + } + + lock(SyncLock) { + Flush(); + Console.CursorLeft = value; + } + } + } + + /// + /// Gets or sets the cursor top position. + /// + /// + /// The cursor top. + /// + public static Int32 CursorTop { + get { + if(IsConsolePresent == false) { + return -1; + } + + lock(SyncLock) { + Flush(); + return Console.CursorTop; + } + } + set { + if(IsConsolePresent == false) { + return; + } + + lock(SyncLock) { + Flush(); + Console.CursorTop = value; + } + } + } + + #endregion + + #region Properties + + /// + /// Gets a value indicating whether the Console is present. + /// + /// + /// true if this instance is console present; otherwise, false. + /// + public static Boolean IsConsolePresent { + get { + if(_isConsolePresent == null) { + _isConsolePresent = true; + try { + Int32 windowHeight = Console.WindowHeight; + _isConsolePresent = windowHeight >= 0; + } catch { + _isConsolePresent = false; + } + } + + return _isConsolePresent.Value; + } + } + + /// + /// Gets the available output writers in a bitwise mask. + /// + /// + /// The available writers. + /// + public static TerminalWriters AvailableWriters => IsConsolePresent ? TerminalWriters.StandardError | TerminalWriters.StandardOutput : TerminalWriters.None; + + /// + /// Gets or sets the output encoding for the current console. + /// + /// + /// The output encoding. + /// + public static Encoding OutputEncoding { + get => Console.OutputEncoding; + set => Console.OutputEncoding = value; + } + + #endregion + + #region Methods + + /// + /// Waits for all of the queued output messages to be written out to the console. + /// Call this method if it is important to display console text before + /// quitting the application such as showing usage or help. + /// Set the timeout to null or TimeSpan.Zero to wait indefinitely. + /// + /// The timeout. Set the amount of time to black before this method exits. + public static void Flush(TimeSpan? timeout = null) { + if(timeout == null) { + timeout = TimeSpan.Zero; + } + + DateTime startTime = DateTime.UtcNow; + + while(OutputQueue.Count > 0) { + // Manually trigger a timer cycle to run immediately + DequeueOutputTimer.Change(0, OutputFlushInterval); + + // Wait for the output to finish + if(OutputDone.Wait(OutputFlushInterval)) { + break; + } + + // infinite timeout + if(timeout.Value == TimeSpan.Zero) { + continue; + } + + // break if we have reached a timeout condition + if(DateTime.UtcNow.Subtract(startTime) >= timeout.Value) { + break; + } + } + } + + /// + /// Sets the cursor position. + /// + /// The left. + /// The top. + public static void SetCursorPosition(Int32 left, Int32 top) { + if(!IsConsolePresent) { + return; + } + + lock(SyncLock) { + Flush(); + Console.SetCursorPosition(left.Clamp(0, left), top.Clamp(0, top)); + } + } + + /// + /// Moves the output cursor one line up starting at left position 0 + /// Please note that backlining the cursor does not clear the contents of the + /// previous line so you might need to clear it by writing an empty string the + /// length of the console width. + /// + public static void BacklineCursor() => SetCursorPosition(0, CursorTop - 1); + + /// + /// Writes a standard banner to the standard output + /// containing the company name, product name, assembly version and trademark. + /// + /// The color. + public static void WriteWelcomeBanner(ConsoleColor color = ConsoleColor.Gray) { + WriteLine($"{SwanRuntime.CompanyName} {SwanRuntime.ProductName} [Version {SwanRuntime.EntryAssemblyVersion}]", color); + WriteLine($"{SwanRuntime.ProductTrademark}", color); + } + + /// + /// Enqueues the output to be written to the console + /// This is the only method that should enqueue to the output + /// Please note that if AvailableWriters is None, then no output will be enqueued. + /// + /// The context. + private static void EnqueueOutput(OutputContext context) { + lock(SyncLock) { + TerminalWriters availableWriters = AvailableWriters; + + if(availableWriters == TerminalWriters.None || context.OutputWriters == TerminalWriters.None) { + OutputDone.Set(); + return; + } + + if((context.OutputWriters & availableWriters) == TerminalWriters.None) { + return; + } + + OutputDone.Reset(); + OutputQueue.Enqueue(context); + } + } + + /// + /// Runs a Terminal I/O cycle in the thread. + /// + private static void DequeueOutputCycle() { + if(AvailableWriters == TerminalWriters.None) { + OutputDone.Set(); + return; + } + + InputDone.Wait(); + + if(OutputQueue.Count <= 0) { + OutputDone.Set(); + return; + } + + OutputDone.Reset(); + + while(OutputQueue.Count > 0) { + if(!OutputQueue.TryDequeue(out OutputContext context)) { + continue; + } + + // Process Console output and Skip over stuff we can't display so we don't stress the output too much. + if(!IsConsolePresent) { + continue; + } + + Console.ForegroundColor = context.OutputColor; + + // Output to the standard output + if(context.OutputWriters.HasFlag(TerminalWriters.StandardOutput)) { + Console.Out.Write(context.OutputText); + } + + // output to the standard error + if(context.OutputWriters.HasFlag(TerminalWriters.StandardError)) { + Console.Error.Write(context.OutputText); + } + + Console.ResetColor(); + Console.ForegroundColor = context.OriginalColor; + } + } + + #endregion + + #region Output Context + + /// + /// Represents an asynchronous output context. + /// + private sealed class OutputContext { + /// + /// Initializes a new instance of the class. + /// + public OutputContext() { + this.OriginalColor = Settings.DefaultColor; + this.OutputWriters = IsConsolePresent ? TerminalWriters.StandardOutput : TerminalWriters.None; + } + + public ConsoleColor OriginalColor { + get; + } + public ConsoleColor OutputColor { + get; set; + } + public Char[] OutputText { + get; set; + } + public TerminalWriters OutputWriters { + get; set; + } + } + + #endregion + } +} diff --git a/Swan.Tiny/TerminalWriters.Enums.cs b/Swan.Tiny/TerminalWriters.Enums.cs new file mode 100644 index 0000000..202f3b1 --- /dev/null +++ b/Swan.Tiny/TerminalWriters.Enums.cs @@ -0,0 +1,29 @@ +using System; + +namespace Swan { + /// + /// Defines a set of bitwise standard terminal writers. + /// + [Flags] + public enum TerminalWriters { + /// + /// Prevents output + /// + None = 0, + + /// + /// Writes to the Console.Out + /// + StandardOutput = 1, + + /// + /// Writes to the Console.Error + /// + StandardError = 2, + + /// + /// Writes to all possible terminal writers + /// + All = StandardOutput | StandardError, + } +} diff --git a/Swan.Tiny/Threading/AtomicBoolean.cs b/Swan.Tiny/Threading/AtomicBoolean.cs new file mode 100644 index 0000000..c5510ef --- /dev/null +++ b/Swan.Tiny/Threading/AtomicBoolean.cs @@ -0,0 +1,22 @@ +using System; + +namespace Swan.Threading { + /// + /// Fast, atomic boolean combining interlocked to write value and volatile to read values. + /// + public sealed class AtomicBoolean : AtomicTypeBase { + /// + /// Initializes a new instance of the class. + /// + /// if set to true [initial value]. + public AtomicBoolean(Boolean initialValue = default) : base(initialValue ? 1 : 0) { + // placeholder + } + + /// + protected override Boolean FromLong(Int64 backingValue) => backingValue != 0; + + /// + protected override Int64 ToLong(Boolean value) => value ? 1 : 0; + } +} \ No newline at end of file diff --git a/Swan.Tiny/Threading/AtomicTypeBase.cs b/Swan.Tiny/Threading/AtomicTypeBase.cs new file mode 100644 index 0000000..4ea9c50 --- /dev/null +++ b/Swan.Tiny/Threading/AtomicTypeBase.cs @@ -0,0 +1,219 @@ +using System; +using System.Threading; + +namespace Swan.Threading { + /// + /// Provides a generic implementation of an Atomic (interlocked) type + /// + /// Idea taken from Memory model and .NET operations in article: + /// http://igoro.com/archive/volatile-keyword-in-c-memory-model-explained/. + /// + /// The structure type backed by a 64-bit value. + public abstract class AtomicTypeBase : IComparable, IComparable, IComparable>, IEquatable, IEquatable> where T : struct, IComparable, IComparable, IEquatable { + private Int64 _backingValue; + + /// + /// Initializes a new instance of the class. + /// + /// The initial value. + protected AtomicTypeBase(Int64 initialValue) => this.BackingValue = initialValue; + + /// + /// Gets or sets the value. + /// + public T Value { + get => this.FromLong(this.BackingValue); + set => this.BackingValue = this.ToLong(value); + } + + /// + /// Gets or sets the backing value. + /// + protected Int64 BackingValue { + get => Interlocked.Read(ref this._backingValue); + set => Interlocked.Exchange(ref this._backingValue, value); + } + + /// + /// Implements the operator ==. + /// + /// a. + /// The b. + /// + /// The result of the operator. + /// + public static Boolean operator ==(AtomicTypeBase a, T b) => a?.Equals(b) == true; + + /// + /// Implements the operator !=. + /// + /// a. + /// The b. + /// + /// The result of the operator. + /// + public static Boolean operator !=(AtomicTypeBase a, T b) => a?.Equals(b) == false; + + /// + /// Implements the operator >. + /// + /// a. + /// The b. + /// + /// The result of the operator. + /// + public static Boolean operator >(AtomicTypeBase a, T b) => a.CompareTo(b) > 0; + + /// + /// Implements the operator <. + /// + /// a. + /// The b. + /// + /// The result of the operator. + /// + public static Boolean operator <(AtomicTypeBase a, T b) => a.CompareTo(b) < 0; + + /// + /// Implements the operator >=. + /// + /// a. + /// The b. + /// + /// The result of the operator. + /// + public static Boolean operator >=(AtomicTypeBase a, T b) => a.CompareTo(b) >= 0; + + /// + /// Implements the operator <=. + /// + /// a. + /// The b. + /// + /// The result of the operator. + /// + public static Boolean operator <=(AtomicTypeBase a, T b) => a.CompareTo(b) <= 0; + + /// + /// Implements the operator ++. + /// + /// The instance. + /// + /// The result of the operator. + /// + public static AtomicTypeBase operator ++(AtomicTypeBase instance) { + _ = Interlocked.Increment(ref instance._backingValue); + return instance; + } + + /// + /// Implements the operator --. + /// + /// The instance. + /// + /// The result of the operator. + /// + public static AtomicTypeBase operator --(AtomicTypeBase instance) { + _ = Interlocked.Decrement(ref instance._backingValue); + return instance; + } + + /// + /// Implements the operator -<. + /// + /// The instance. + /// The operand. + /// + /// The result of the operator. + /// + public static AtomicTypeBase operator +(AtomicTypeBase instance, Int64 operand) { + instance.BackingValue += operand; + return instance; + } + + /// + /// Implements the operator -. + /// + /// The instance. + /// The operand. + /// + /// The result of the operator. + /// + public static AtomicTypeBase operator -(AtomicTypeBase instance, Int64 operand) { + instance.BackingValue -= operand; + return instance; + } + + /// + /// Compares the value to the other instance. + /// + /// The other instance. + /// 0 if equal, 1 if this instance is greater, -1 if this instance is less than. + /// When types are incompatible. + public Int32 CompareTo(Object other) => other switch + { + null => 1, + AtomicTypeBase atomic => this.BackingValue.CompareTo(atomic.BackingValue), + T variable => this.Value.CompareTo(variable), + + _ => throw new ArgumentException("Incompatible comparison types"), + }; + + /// + /// Compares the value to the other instance. + /// + /// The other instance. + /// 0 if equal, 1 if this instance is greater, -1 if this instance is less than. + public Int32 CompareTo(T other) => this.Value.CompareTo(other); + + /// + /// Compares the value to the other instance. + /// + /// The other instance. + /// 0 if equal, 1 if this instance is greater, -1 if this instance is less than. + public Int32 CompareTo(AtomicTypeBase other) => this.BackingValue.CompareTo(other?.BackingValue ?? default); + + /// + /// Determines whether the specified , is equal to this instance. + /// + /// The to compare with this instance. + /// + /// true if the specified is equal to this instance; otherwise, false. + /// + public override Boolean Equals(Object other) => other switch + { + AtomicTypeBase atomic => this.Equals(atomic), + T variable => this.Equals(variable), + + _ => false, + }; + + /// + /// Returns a hash code for this instance. + /// + /// + /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. + /// + public override Int32 GetHashCode() => this.BackingValue.GetHashCode(); + + /// + public Boolean Equals(AtomicTypeBase other) => this.BackingValue == (other?.BackingValue ?? default); + + /// + public Boolean Equals(T other) => Equals(this.Value, other); + + /// + /// Converts from a long value to the target type. + /// + /// The backing value. + /// The value converted form a long value. + protected abstract T FromLong(Int64 backingValue); + + /// + /// Converts from the target type to a long value. + /// + /// The value. + /// The value converted to a long value. + protected abstract Int64 ToLong(T value); + } +} diff --git a/Swan.Tiny/Threading/ExclusiveTimer.cs b/Swan.Tiny/Threading/ExclusiveTimer.cs new file mode 100644 index 0000000..d3c9c3e --- /dev/null +++ b/Swan.Tiny/Threading/ExclusiveTimer.cs @@ -0,0 +1,206 @@ +using System; +using System.Threading; + +namespace Swan.Threading { + /// + /// A threading implementation that executes at most one cycle at a time + /// in a thread. Callback execution is NOT guaranteed to be carried out + /// on the same thread every time the timer fires. + /// + public sealed class ExclusiveTimer : IDisposable { + private readonly Object _syncLock = new Object(); + private readonly ManualResetEventSlim _cycleDoneEvent = new ManualResetEventSlim(true); + private readonly Timer _backingTimer; + private readonly TimerCallback _userCallback; + private readonly AtomicBoolean _isDisposing = new AtomicBoolean(); + private readonly AtomicBoolean _isDisposed = new AtomicBoolean(); + private Int32 _period; + + /// + /// Initializes a new instance of the class. + /// + /// The timer callback. + /// The state. + /// The due time. + /// The period. + public ExclusiveTimer(TimerCallback timerCallback, Object state, Int32 dueTime, Int32 period) { + this._period = period; + this._userCallback = timerCallback; + this._backingTimer = new Timer(this.InternalCallback, state ?? this, dueTime, Timeout.Infinite); + } + + /// + /// Initializes a new instance of the class. + /// + /// The timer callback. + /// The state. + /// The due time. + /// The period. + public ExclusiveTimer(TimerCallback timerCallback, Object state, TimeSpan dueTime, TimeSpan period) : this(timerCallback, state, Convert.ToInt32(dueTime.TotalMilliseconds), Convert.ToInt32(period.TotalMilliseconds)) { + // placeholder + } + + /// + /// Initializes a new instance of the class. + /// + /// The timer callback. + public ExclusiveTimer(TimerCallback timerCallback) : this(timerCallback, null, Timeout.Infinite, Timeout.Infinite) { + // placeholder + } + + /// + /// Initializes a new instance of the class. + /// + /// The timer callback. + /// The due time. + /// The period. + public ExclusiveTimer(Action timerCallback, Int32 dueTime, Int32 period) : this(s => timerCallback?.Invoke(), null, dueTime, period) { + // placeholder + } + + /// + /// Initializes a new instance of the class. + /// + /// The timer callback. + /// The due time. + /// The period. + public ExclusiveTimer(Action timerCallback, TimeSpan dueTime, TimeSpan period) : this(s => timerCallback?.Invoke(), null, dueTime, period) { + // placeholder + } + + /// + /// Initializes a new instance of the class. + /// + /// The timer callback. + public ExclusiveTimer(Action timerCallback) : this(timerCallback, Timeout.Infinite, Timeout.Infinite) { + // placeholder + } + + /// + /// Gets a value indicating whether this instance is disposing. + /// + /// + /// true if this instance is disposing; otherwise, false. + /// + public Boolean IsDisposing => this._isDisposing.Value; + + /// + /// Gets a value indicating whether this instance is disposed. + /// + /// + /// true if this instance is disposed; otherwise, false. + /// + public Boolean IsDisposed => this._isDisposed.Value; + + /// + /// Waits until the time is elapsed. + /// + /// The until date. + /// The cancellation token. + public static void WaitUntil(DateTime untilDate, CancellationToken cancellationToken = default) { + static void Callback(IWaitEvent waitEvent) { + try { + waitEvent.Complete(); + waitEvent.Begin(); + } catch { + // ignore + } + } + + using IWaitEvent delayLock = WaitEventFactory.Create(true); + using ExclusiveTimer _ = new ExclusiveTimer(() => Callback(delayLock), 0, 15); + while(!cancellationToken.IsCancellationRequested && DateTime.UtcNow < untilDate) { + delayLock.Wait(); + } + } + + /// + /// Waits the specified wait time. + /// + /// The wait time. + /// The cancellation token. + public static void Wait(TimeSpan waitTime, CancellationToken cancellationToken = default) => + WaitUntil(DateTime.UtcNow.Add(waitTime), cancellationToken); + + /// + /// Changes the start time and the interval between method invocations for the internal timer. + /// + /// The due time. + /// The period. + public void Change(Int32 dueTime, Int32 period) { + this._period = period; + + _ = this._backingTimer.Change(dueTime, Timeout.Infinite); + } + + /// + /// Changes the start time and the interval between method invocations for the internal timer. + /// + /// The due time. + /// The period. + public void Change(TimeSpan dueTime, TimeSpan period) => this.Change(Convert.ToInt32(dueTime.TotalMilliseconds), Convert.ToInt32(period.TotalMilliseconds)); + + /// + /// Changes the interval between method invocations for the internal timer. + /// + /// The period. + public void Resume(Int32 period) => this.Change(0, period); + + /// + /// Changes the interval between method invocations for the internal timer. + /// + /// The period. + public void Resume(TimeSpan period) => this.Change(TimeSpan.Zero, period); + + /// + /// Pauses this instance. + /// + public void Pause() => this.Change(Timeout.Infinite, Timeout.Infinite); + + /// + public void Dispose() { + lock(this._syncLock) { + if(this._isDisposed == true || this._isDisposing == true) { + return; + } + + this._isDisposing.Value = true; + } + + try { + this._cycleDoneEvent.Wait(); + this._cycleDoneEvent.Dispose(); + this.Pause(); + this._backingTimer.Dispose(); + } finally { + this._isDisposed.Value = true; + this._isDisposing.Value = false; + } + } + + /// + /// Logic that runs every time the timer hits the due time. + /// + /// The state. + private void InternalCallback(Object state) { + lock(this._syncLock) { + if(this.IsDisposed || this.IsDisposing) { + return; + } + } + + if(this._cycleDoneEvent.IsSet == false) { + return; + } + + this._cycleDoneEvent.Reset(); + + try { + this._userCallback(state); + } finally { + this._cycleDoneEvent?.Set(); + _ = this._backingTimer?.Change(this._period, Timeout.Infinite); + } + } + } +} diff --git a/Swan.Tiny/Threading/IWaitEvent.cs b/Swan.Tiny/Threading/IWaitEvent.cs new file mode 100644 index 0000000..19331c0 --- /dev/null +++ b/Swan.Tiny/Threading/IWaitEvent.cs @@ -0,0 +1,63 @@ +using System; + +namespace Swan.Threading { + /// + /// Provides a generalized API for ManualResetEvent and ManualResetEventSlim. + /// + /// + public interface IWaitEvent : IDisposable { + /// + /// Gets a value indicating whether the event is in the completed state. + /// + Boolean IsCompleted { + get; + } + + /// + /// Gets a value indicating whether the Begin method has been called. + /// It returns false after the Complete method is called. + /// + Boolean IsInProgress { + get; + } + + /// + /// Returns true if the underlying handle is not closed and it is still valid. + /// + Boolean IsValid { + get; + } + + /// + /// Gets a value indicating whether this instance is disposed. + /// + Boolean IsDisposed { + get; + } + + /// + /// Enters the state in which waiters need to wait. + /// All future waiters will block when they call the Wait method. + /// + void Begin(); + + /// + /// Leaves the state in which waiters need to wait. + /// All current waiters will continue. + /// + void Complete(); + + /// + /// Waits for the event to be completed. + /// + void Wait(); + + /// + /// Waits for the event to be completed. + /// Returns true when there was no timeout. False if the timeout was reached. + /// + /// The maximum amount of time to wait for. + /// true when there was no timeout. false if the timeout was reached. + Boolean Wait(TimeSpan timeout); + } +} diff --git a/Swan.Tiny/Threading/WaitEventFactory.cs b/Swan.Tiny/Threading/WaitEventFactory.cs new file mode 100644 index 0000000..8def40c --- /dev/null +++ b/Swan.Tiny/Threading/WaitEventFactory.cs @@ -0,0 +1,188 @@ +using System; +using System.Threading; + +namespace Swan.Threading { + /// + /// Provides a Manual Reset Event factory with a unified API. + /// + /// + /// The following example shows how to use the WaitEventFactory class. + /// + /// using Swan.Threading; + /// + /// public class Example + /// { + /// // create a WaitEvent using the slim version + /// private static readonly IWaitEvent waitEvent = WaitEventFactory.CreateSlim(false); + /// + /// public static void Main() + /// { + /// Task.Factory.StartNew(() => + /// { + /// DoWork(1); + /// }); + /// + /// Task.Factory.StartNew(() => + /// { + /// DoWork(2); + /// }); + /// + /// // send first signal + /// waitEvent.Complete(); + /// waitEvent.Begin(); + /// + /// Thread.Sleep(TimeSpan.FromSeconds(2)); + /// + /// // send second signal + /// waitEvent.Complete(); + /// + /// Terminal.Readline(); + /// } + /// + /// public static void DoWork(int taskNumber) + /// { + /// $"Data retrieved:{taskNumber}".WriteLine(); + /// waitEvent.Wait(); + /// + /// Thread.Sleep(TimeSpan.FromSeconds(2)); + /// $"All finished up {taskNumber}".WriteLine(); + /// } + /// } + /// + /// + public static class WaitEventFactory { + #region Factory Methods + + /// + /// Creates a Wait Event backed by a standard ManualResetEvent. + /// + /// if initially set to completed. Generally true. + /// The Wait Event. + public static IWaitEvent Create(Boolean isCompleted) => new WaitEvent(isCompleted); + + /// + /// Creates a Wait Event backed by a ManualResetEventSlim. + /// + /// if initially set to completed. Generally true. + /// The Wait Event. + public static IWaitEvent CreateSlim(Boolean isCompleted) => new WaitEventSlim(isCompleted); + + /// + /// Creates a Wait Event backed by a ManualResetEventSlim. + /// + /// if initially set to completed. Generally true. + /// if set to true creates a slim version of the wait event. + /// The Wait Event. + public static IWaitEvent Create(Boolean isCompleted, Boolean useSlim) => useSlim ? CreateSlim(isCompleted) : Create(isCompleted); + + #endregion + + #region Backing Classes + + /// + /// Defines a WaitEvent backed by a ManualResetEvent. + /// + private class WaitEvent : IWaitEvent { + private ManualResetEvent _event; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true [is completed]. + public WaitEvent(Boolean isCompleted) => this._event = new ManualResetEvent(isCompleted); + + /// + public Boolean IsDisposed { + get; private set; + } + + /// + public Boolean IsValid => this.IsDisposed || this._event == null ? false : this._event?.SafeWaitHandle?.IsClosed ?? true ? false : !(this._event?.SafeWaitHandle?.IsInvalid ?? true); + + /// + public Boolean IsCompleted => this.IsValid == false ? true : this._event?.WaitOne(0) ?? true; + + /// + public Boolean IsInProgress => !this.IsCompleted; + + /// + public void Begin() => this._event?.Reset(); + + /// + public void Complete() => this._event?.Set(); + + /// + void IDisposable.Dispose() { + if(this.IsDisposed) { + return; + } + + this.IsDisposed = true; + + _ = this._event?.Set(); + this._event?.Dispose(); + this._event = null; + } + + /// + public void Wait() => this._event?.WaitOne(); + + /// + public Boolean Wait(TimeSpan timeout) => this._event?.WaitOne(timeout) ?? true; + } + + /// + /// Defines a WaitEvent backed by a ManualResetEventSlim. + /// + private class WaitEventSlim : IWaitEvent { + private ManualResetEventSlim _event; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true [is completed]. + public WaitEventSlim(Boolean isCompleted) => this._event = new ManualResetEventSlim(isCompleted); + + /// + public Boolean IsDisposed { + get; private set; + } + + /// + public Boolean IsValid => this.IsDisposed || this._event?.WaitHandle?.SafeWaitHandle == null ? false : !this._event.WaitHandle.SafeWaitHandle.IsClosed && !this._event.WaitHandle.SafeWaitHandle.IsInvalid; + + /// + public Boolean IsCompleted => this.IsValid == false || this._event.IsSet; + + /// + public Boolean IsInProgress => !this.IsCompleted; + + /// + public void Begin() => this._event?.Reset(); + + /// + public void Complete() => this._event?.Set(); + + /// + void IDisposable.Dispose() { + if(this.IsDisposed) { + return; + } + + this.IsDisposed = true; + + this._event?.Set(); + this._event?.Dispose(); + this._event = null; + } + + /// + public void Wait() => this._event?.Wait(); + + /// + public Boolean Wait(TimeSpan timeout) => this._event?.Wait(timeout) ?? true; + } + + #endregion + } +} \ No newline at end of file