diff --git a/Swan.Lite/Collections/CollectionCacheRepository.cs b/Swan.Lite/Collections/CollectionCacheRepository.cs index 496f410..008b9a8 100644 --- a/Swan.Lite/Collections/CollectionCacheRepository.cs +++ b/Swan.Lite/Collections/CollectionCacheRepository.cs @@ -3,50 +3,47 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -namespace Swan.Collections -{ +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); + /// - /// A thread-safe collection cache repository for types. + /// Determines whether the cache contains the specified key. /// - /// 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 bool ContainsKey(Type key) => _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 _data.Value.GetOrAdd(key, k => factory.Invoke(k).Where(item => item != null)); - } - } + /// 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.Lite/Collections/ComponentCollection`1.cs b/Swan.Lite/Collections/ComponentCollection`1.cs index 7421c09..118712f 100644 --- a/Swan.Lite/Collections/ComponentCollection`1.cs +++ b/Swan.Lite/Collections/ComponentCollection`1.cs @@ -3,74 +3,75 @@ using System.Collections; using System.Collections.Generic; using Swan.Configuration; -namespace Swan.Collections -{ +namespace Swan.Collections { + /// + /// Implements a collection of components. + /// Each component in the collection may be given a unique name for later retrieval. + /// + /// The type of components in the collection. + /// + public class ComponentCollection : ConfiguredObject, IComponentCollection { + private readonly List _components = new List(); + + private readonly List<(String, T)> _componentsWithSafeNames = new List<(String, T)>(); + + private readonly Dictionary _namedComponents = new Dictionary(); + + /// + public Int32 Count => this._components.Count; + + /// + public IReadOnlyDictionary Named => this._namedComponents; + + /// + public IReadOnlyList<(String SafeName, T Component)> WithSafeNames => this._componentsWithSafeNames; + + /// + public T this[Int32 index] => this._components[index]; + + /// + public T this[String key] => this._namedComponents[key]; + + /// + public IEnumerator GetEnumerator() => this._components.GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this._components).GetEnumerator(); + + /// + /// The collection is locked. + public void Add(String name, T component) { + this.EnsureConfigurationNotLocked(); + + if(name != null) { + if(name.Length == 0) { + throw new ArgumentException("Component name is empty.", nameof(name)); + } + + if(this._namedComponents.ContainsKey(name)) { + throw new ArgumentException("Duplicate component name.", nameof(name)); + } + } + + if(component == null) { + throw new ArgumentNullException(nameof(component)); + } + + if(this._components.Contains(component)) { + throw new ArgumentException("Component has already been added.", nameof(component)); + } + + this._components.Add(component); + this._componentsWithSafeNames.Add((name ?? $"<{component.GetType().Name}>", component)); + + if(name != null) { + this._namedComponents.Add(name, component); + } + } + /// - /// Implements a collection of components. - /// Each component in the collection may be given a unique name for later retrieval. + /// Locks the collection, preventing further additions. /// - /// The type of components in the collection. - /// - public class ComponentCollection : ConfiguredObject, IComponentCollection - { - private readonly List _components = new List(); - - private readonly List<(string, T)> _componentsWithSafeNames = new List<(string, T)>(); - - private readonly Dictionary _namedComponents = new Dictionary(); - - /// - public int Count => _components.Count; - - /// - public IReadOnlyDictionary Named => _namedComponents; - - /// - public IReadOnlyList<(string SafeName, T Component)> WithSafeNames => _componentsWithSafeNames; - - /// - public T this[int index] => _components[index]; - - /// - public T this[string key] => _namedComponents[key]; - - /// - public IEnumerator GetEnumerator() => _components.GetEnumerator(); - - /// - IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_components).GetEnumerator(); - - /// - /// The collection is locked. - public void Add(string name, T component) - { - EnsureConfigurationNotLocked(); - - if (name != null) - { - if (name.Length == 0) - throw new ArgumentException("Component name is empty.", nameof(name)); - - if (_namedComponents.ContainsKey(name)) - throw new ArgumentException("Duplicate component name.", nameof(name)); - } - - if (component == null) - throw new ArgumentNullException(nameof(component)); - - if (_components.Contains(component)) - throw new ArgumentException("Component has already been added.", nameof(component)); - - _components.Add(component); - _componentsWithSafeNames.Add((name ?? $"<{component.GetType().Name}>", component)); - - if (name != null) - _namedComponents.Add(name, component); - } - - /// - /// Locks the collection, preventing further additions. - /// - public void Lock() => LockConfiguration(); - } + public void Lock() => this.LockConfiguration(); + } } diff --git a/Swan.Lite/Collections/ConcurrentDataDictionary`2.cs b/Swan.Lite/Collections/ConcurrentDataDictionary`2.cs index 58fa75a..38a5f15 100644 --- a/Swan.Lite/Collections/ConcurrentDataDictionary`2.cs +++ b/Swan.Lite/Collections/ConcurrentDataDictionary`2.cs @@ -4,301 +4,266 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -namespace Swan.Collections -{ +namespace Swan.Collections { + /// + /// Represents a thread-safe collection of key/value pairs that does not store null values + /// and can be accessed by multiple threads concurrently. + /// + /// The type of keys in the dictionary. This must be a reference type. + /// The type of values in the dictionary. This must be a reference type. + /// + public sealed class ConcurrentDataDictionary : IDataDictionary where TKey : class where TValue : class { + #region Private data + + private readonly ConcurrentDictionary _dictionary; + + #endregion + + #region Instance management + /// - /// Represents a thread-safe collection of key/value pairs that does not store null values - /// and can be accessed by multiple threads concurrently. + /// Initializes a new instance of the class + /// that is empty, has the default concurrency level, has the default initial capacity, + /// and uses the default comparer for . /// - /// The type of keys in the dictionary. This must be a reference type. - /// The type of values in the dictionary. This must be a reference type. - /// - public sealed class ConcurrentDataDictionary : IDataDictionary - where TKey : class - where TValue : class - { - #region Private data - - private readonly ConcurrentDictionary _dictionary; - - #endregion - - #region Instance management - - /// - /// Initializes a new instance of the class - /// that is empty, has the default concurrency level, has the default initial capacity, - /// and uses the default comparer for . - /// - /// - public ConcurrentDataDictionary() - { - _dictionary = new ConcurrentDictionary(); - } - - /// - /// Initializes a new instance of the class - /// that contains elements copied from the specified , has the default concurrency level, - /// has the default initial capacity, and uses the default comparer for . - /// - /// The whose elements are copied - /// to the new . - /// is . - /// - /// Since does not store null values, - /// key/value pairs whose value is will not be copied from . - /// - /// - public ConcurrentDataDictionary(IEnumerable> collection) - { - if (collection == null) - throw new ArgumentNullException(nameof(collection)); - - _dictionary = new ConcurrentDictionary(collection.Where(pair => pair.Value != null)); - } - - /// - /// Initializes a new instance of the class - /// that is empty, has the default concurrency level and capacity, and uses the specified . - /// - /// The equality comparison implementation to use when comparing keys. - /// is . - /// - public ConcurrentDataDictionary(IEqualityComparer comparer) - { - _dictionary = new ConcurrentDictionary(comparer); - } - - /// - /// Initializes a new instance of the class - /// that contains elements copied from the specified , has the default concurrency level, - /// has the default initial capacity, and uses the specified . - /// - /// The whose elements are copied - /// to the new . - /// The equality comparison implementation to use when comparing keys. - /// - /// Since does not store null values, - /// key/value pairs whose value is will not be copied from . - /// - /// - /// is . - /// - or -. - /// is . - /// - /// - public ConcurrentDataDictionary(IEnumerable> collection, IEqualityComparer comparer) - { - if (collection == null) - throw new ArgumentNullException(nameof(collection)); - - _dictionary = new ConcurrentDictionary(collection.Where(pair => pair.Value != null), comparer); - } - - /// - /// Initializes a new instance of the class - /// that is empty, has the specified concurrency level and capacity, and uses the default comparer for the key type. - /// - /// The estimated number of threads that will update - /// the concurrently. - /// The initial number of elements that the can contain. - /// - /// is less than 1. - /// - or -. - /// is less than 0. - /// - /// - public ConcurrentDataDictionary(int concurrencyLevel, int capacity) - { - _dictionary = new ConcurrentDictionary(concurrencyLevel, capacity); - } - - /// - /// Initializes a new instance of the class - /// that contains elements copied from the specified , has the specified concurrency level, - /// has the default initial capacity, and uses the specified . - /// - /// The estimated number of threads that will update - /// the concurrently. - /// The whose elements are copied - /// to the new . - /// The equality comparison implementation to use when comparing keys. - /// - /// Since does not store null values, - /// key/value pairs whose value is will not be copied from . - /// - /// - /// is . - /// - or -. - /// is . - /// - /// is less than 1. - /// - public ConcurrentDataDictionary(int concurrencyLevel, IEnumerable> collection, IEqualityComparer comparer) - { - if (collection == null) - throw new ArgumentNullException(nameof(collection)); - - _dictionary = new ConcurrentDictionary( - concurrencyLevel, - collection.Where(pair => pair.Value != null), - comparer); - } - - #endregion - - #region Public APIs - - /// - public int Count => _dictionary.Count; - - /// - public bool IsEmpty => _dictionary.IsEmpty; - - /// - public ICollection Keys => _dictionary.Keys; - - /// - public ICollection Values => _dictionary.Values; - - /// - public TValue? this[TKey key] - { - get => _dictionary.TryGetValue(key ?? throw new ArgumentNullException(nameof(key)), out var value) ? value : null; - set - { - if (value != null) - { - _dictionary[key] = value; - } - else - { - _dictionary.TryRemove(key, out _); - } - } - } - - /// - public void Clear() => _dictionary.Clear(); - - /// - public bool ContainsKey(TKey key) => _dictionary.ContainsKey(key); - - /// - public TValue? GetOrAdd(TKey key, TValue value) - { - if (key == null) - throw new ArgumentNullException(nameof(key)); - - if (value != null) - return _dictionary.GetOrAdd(key, value); - - return _dictionary.TryGetValue(key, out var retrievedValue) ? retrievedValue : null; - } - - /// - public bool Remove(TKey key) => _dictionary.TryRemove(key, out _); - - /// - public bool TryAdd(TKey key, TValue value) - { - if (key == null) - throw new ArgumentNullException(nameof(key)); - - return value == null || _dictionary.TryAdd(key, value); - } - - /// - public bool TryGetValue(TKey key, out TValue value) => _dictionary.TryGetValue(key, out value); - - /// - public bool TryRemove(TKey key, out TValue value) => _dictionary.TryRemove(key, out value); - - /// - public bool TryUpdate(TKey key, TValue newValue, TValue comparisonValue) - { - if (key == null) - throw new ArgumentNullException(nameof(key)); - - return newValue != null && comparisonValue != null && _dictionary.TryUpdate(key, newValue, comparisonValue); - } - - #endregion - - #region Implementation of IDictionary - - /// - void IDictionary.Add(TKey key, TValue value) - { - if (value != null) - { - ((IDictionary)_dictionary).Add(key, value); - } - else - { - _dictionary.TryRemove(key, out _); - } - } - - #endregion - - #region Implementation of IReadOnlyDictionary - - /// - IEnumerable IReadOnlyDictionary.Keys => _dictionary.Keys; - - /// - IEnumerable IReadOnlyDictionary.Values => _dictionary.Values; - - #endregion - - #region Implementation of ICollection> - - /// - /// - /// This property is always for a . - /// - bool ICollection>.IsReadOnly => false; - - /// - void ICollection>.Add(KeyValuePair item) - { - if (item.Value != null) - { - ((ICollection>)_dictionary).Add(item); - } - else - { - _dictionary.TryRemove(item.Key, out _); - } - } - - /// - bool ICollection>.Contains(KeyValuePair item) - => ((ICollection>)_dictionary).Contains(item); - - /// - void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) - => ((ICollection>)_dictionary).CopyTo(array, arrayIndex); - - /// - bool ICollection>.Remove(KeyValuePair item) - => ((ICollection>)_dictionary).Remove(item); - - #endregion - - #region Implementation of IEnumerable> - - /// - IEnumerator> IEnumerable>.GetEnumerator() => _dictionary.GetEnumerator(); - - #endregion - - #region Implementation of IEnumerable - - /// - IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_dictionary).GetEnumerator(); - - #endregion - } + /// + public ConcurrentDataDictionary() => this._dictionary = new ConcurrentDictionary(); + + /// + /// Initializes a new instance of the class + /// that contains elements copied from the specified , has the default concurrency level, + /// has the default initial capacity, and uses the default comparer for . + /// + /// The whose elements are copied + /// to the new . + /// is . + /// + /// Since does not store null values, + /// key/value pairs whose value is will not be copied from . + /// + /// + public ConcurrentDataDictionary(IEnumerable> collection) { + if(collection == null) { + throw new ArgumentNullException(nameof(collection)); + } + + this._dictionary = new ConcurrentDictionary(collection.Where(pair => pair.Value != null)); + } + + /// + /// Initializes a new instance of the class + /// that is empty, has the default concurrency level and capacity, and uses the specified . + /// + /// The equality comparison implementation to use when comparing keys. + /// is . + /// + public ConcurrentDataDictionary(IEqualityComparer comparer) => this._dictionary = new ConcurrentDictionary(comparer); + + /// + /// Initializes a new instance of the class + /// that contains elements copied from the specified , has the default concurrency level, + /// has the default initial capacity, and uses the specified . + /// + /// The whose elements are copied + /// to the new . + /// The equality comparison implementation to use when comparing keys. + /// + /// Since does not store null values, + /// key/value pairs whose value is will not be copied from . + /// + /// + /// is . + /// - or -. + /// is . + /// + /// + public ConcurrentDataDictionary(IEnumerable> collection, IEqualityComparer comparer) { + if(collection == null) { + throw new ArgumentNullException(nameof(collection)); + } + + this._dictionary = new ConcurrentDictionary(collection.Where(pair => pair.Value != null), comparer); + } + + /// + /// Initializes a new instance of the class + /// that is empty, has the specified concurrency level and capacity, and uses the default comparer for the key type. + /// + /// The estimated number of threads that will update + /// the concurrently. + /// The initial number of elements that the can contain. + /// + /// is less than 1. + /// - or -. + /// is less than 0. + /// + /// + public ConcurrentDataDictionary(Int32 concurrencyLevel, Int32 capacity) => this._dictionary = new ConcurrentDictionary(concurrencyLevel, capacity); + + /// + /// Initializes a new instance of the class + /// that contains elements copied from the specified , has the specified concurrency level, + /// has the default initial capacity, and uses the specified . + /// + /// The estimated number of threads that will update + /// the concurrently. + /// The whose elements are copied + /// to the new . + /// The equality comparison implementation to use when comparing keys. + /// + /// Since does not store null values, + /// key/value pairs whose value is will not be copied from . + /// + /// + /// is . + /// - or -. + /// is . + /// + /// is less than 1. + /// + public ConcurrentDataDictionary(Int32 concurrencyLevel, IEnumerable> collection, IEqualityComparer comparer) { + if(collection == null) { + throw new ArgumentNullException(nameof(collection)); + } + + this._dictionary = new ConcurrentDictionary(concurrencyLevel, collection.Where(pair => pair.Value != null), comparer); + } + + #endregion + + #region Public APIs + + /// + public Int32 Count => this._dictionary.Count; + + /// + public Boolean IsEmpty => this._dictionary.IsEmpty; + + /// + public ICollection Keys => this._dictionary.Keys; + + /// + public ICollection Values => this._dictionary.Values; + + /// + public TValue this[TKey key] { + get => this._dictionary.TryGetValue(key ?? throw new ArgumentNullException(nameof(key)), out TValue value) ? value : null; + set { + if(value != null) { + this._dictionary[key] = value; + } else { + _ = this._dictionary.TryRemove(key, out _); + } + } + } + + /// + public void Clear() => this._dictionary.Clear(); + + /// + public Boolean ContainsKey(TKey key) => this._dictionary.ContainsKey(key); + + /// + public TValue GetOrAdd(TKey key, TValue value) { + if(key == null) { + throw new ArgumentNullException(nameof(key)); + } + + return value != null ? this._dictionary.GetOrAdd(key, value) : this._dictionary.TryGetValue(key, out TValue retrievedValue) ? retrievedValue : null; + } + + /// + public Boolean Remove(TKey key) => this._dictionary.TryRemove(key, out _); + + /// + public Boolean TryAdd(TKey key, TValue value) { + if(key == null) { + throw new ArgumentNullException(nameof(key)); + } + + return value == null || this._dictionary.TryAdd(key, value); + } + + /// + public Boolean TryGetValue(TKey key, out TValue value) => this._dictionary.TryGetValue(key, out value); + + /// + public Boolean TryRemove(TKey key, out TValue value) => this._dictionary.TryRemove(key, out value); + + /// + public Boolean TryUpdate(TKey key, TValue newValue, TValue comparisonValue) { + if(key == null) { + throw new ArgumentNullException(nameof(key)); + } + + return newValue != null && comparisonValue != null && this._dictionary.TryUpdate(key, newValue, comparisonValue); + } + + #endregion + + #region Implementation of IDictionary + + /// + void IDictionary.Add(TKey key, TValue value) { + if(value != null) { + ((IDictionary)this._dictionary).Add(key, value); + } else { + _ = this._dictionary.TryRemove(key, out _); + } + } + + #endregion + + #region Implementation of IReadOnlyDictionary + + /// + IEnumerable IReadOnlyDictionary.Keys => this._dictionary.Keys; + + /// + IEnumerable IReadOnlyDictionary.Values => this._dictionary.Values; + + #endregion + + #region Implementation of ICollection> + + /// + /// + /// This property is always for a . + /// + Boolean ICollection>.IsReadOnly => false; + + /// + void ICollection>.Add(KeyValuePair item) { + if(item.Value != null) { + ((ICollection>)this._dictionary).Add(item); + } else { + _ = this._dictionary.TryRemove(item.Key, out _); + } + } + + /// + Boolean ICollection>.Contains(KeyValuePair item) => ((ICollection>)this._dictionary).Contains(item); + + /// + void ICollection>.CopyTo(KeyValuePair[] array, Int32 arrayIndex) => ((ICollection>)this._dictionary).CopyTo(array, arrayIndex); + + /// + Boolean ICollection>.Remove(KeyValuePair item) => ((ICollection>)this._dictionary).Remove(item); + + #endregion + + #region Implementation of IEnumerable> + + /// + IEnumerator> IEnumerable>.GetEnumerator() => this._dictionary.GetEnumerator(); + + #endregion + + #region Implementation of IEnumerable + + /// + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this._dictionary).GetEnumerator(); + + #endregion + } } diff --git a/Swan.Lite/Collections/DataDictionary`2.cs b/Swan.Lite/Collections/DataDictionary`2.cs index 24f5143..e5ed5c9 100644 --- a/Swan.Lite/Collections/DataDictionary`2.cs +++ b/Swan.Lite/Collections/DataDictionary`2.cs @@ -1,341 +1,312 @@ -using System; + +using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -namespace Swan.Collections -{ +namespace Swan.Collections { + /// + /// Represents a non-thread-safe collection of key/value pairs that does not store null values. + /// + /// The type of keys in the dictionary. This must be a reference type. + /// The type of values in the dictionary. This must be a reference type. + /// + public sealed class DataDictionary : IDataDictionary + where TKey : class + where TValue : class { + #region Private data + + private readonly Dictionary _dictionary; + + #endregion + + #region Instance management + /// - /// Represents a non-thread-safe collection of key/value pairs that does not store null values. + /// Initializes a new instance of the class + /// that is empty, has the default initial capacity, + /// and uses the default comparer for . /// - /// The type of keys in the dictionary. This must be a reference type. - /// The type of values in the dictionary. This must be a reference type. - /// - public sealed class DataDictionary : IDataDictionary - where TKey : class - where TValue : class - { - #region Private data - - private readonly Dictionary _dictionary; - - #endregion - - #region Instance management - - /// - /// Initializes a new instance of the class - /// that is empty, has the default initial capacity, - /// and uses the default comparer for . - /// - /// - public DataDictionary() - { - _dictionary = new Dictionary(); - } - - /// - /// Initializes a new instance of the class - /// that contains elements copied from the specified , - /// has the default initial capacity, and uses the default comparer for . - /// - /// The whose elements are copied - /// to the new . - /// is . - /// - /// Since does not store null values, - /// key/value pairs whose value is will not be copied from . - /// - /// - public DataDictionary(IEnumerable> collection) - { - if (collection == null) - throw new ArgumentNullException(nameof(collection)); - - _dictionary = new Dictionary(); - foreach (var pair in collection.Where(pair => pair.Value != null)) - { - _dictionary.Add(pair.Key, pair.Value); - } - } - - /// - /// Initializes a new instance of the class - /// that is empty, has the default capacity, and uses the specified . - /// - /// The equality comparison implementation to use when comparing keys. - /// is . - /// - public DataDictionary(IEqualityComparer comparer) - { - _dictionary = new Dictionary(comparer); - } - - /// - /// Initializes a new instance of the class - /// that contains elements copied from the specified , - /// has the default initial capacity, and uses the specified . - /// - /// The whose elements are copied - /// to the new . - /// The equality comparison implementation to use when comparing keys. - /// - /// Since does not store null values, - /// key/value pairs whose value is will not be copied from . - /// - /// - /// is . - /// - or -. - /// is . - /// - /// - public DataDictionary(IEnumerable> collection, IEqualityComparer comparer) - { - if (collection == null) - throw new ArgumentNullException(nameof(collection)); - - _dictionary = new Dictionary(comparer); - foreach (var pair in collection.Where(pair => pair.Value != null)) - { - _dictionary.Add(pair.Key, pair.Value); - } - } - - /// - /// Initializes a new instance of the class - /// that is empty, has the specified capacity, and uses the default comparer for the key type. - /// - /// The initial number of elements that the can contain. - /// is less than 0. - /// - public DataDictionary(int capacity) - { - _dictionary = new Dictionary(capacity); - } - - /// - /// Initializes a new instance of the class - /// that contains elements copied from the specified , - /// has the specified capacity, and uses the specified . - /// - /// The initial number of elements that the can contain. - /// The whose elements are copied - /// to the new . - /// The equality comparison implementation to use when comparing keys. - /// - /// Since does not store null values, - /// key/value pairs whose value is will not be copied from . - /// - /// - /// is . - /// - or -. - /// is . - /// - /// is less than 0. - /// - public DataDictionary(int capacity, IEnumerable> collection, IEqualityComparer comparer) - { - if (collection == null) - throw new ArgumentNullException(nameof(collection)); - - _dictionary = new Dictionary(capacity, comparer); - foreach (var pair in collection.Where(pair => pair.Value != null)) - { - _dictionary.Add(pair.Key, pair.Value); - } - } - - #endregion - - #region Public APIs - - /// - public int Count => _dictionary.Count; - - /// - public bool IsEmpty => _dictionary.Count == 0; - - /// - public ICollection Keys => _dictionary.Keys; - - /// - public ICollection Values => _dictionary.Values; - - /// - public TValue? this[TKey key] - { - get => _dictionary.TryGetValue(key ?? throw new ArgumentNullException(nameof(key)), out var value) ? value : null; - set - { - if (value != null) - { - _dictionary[key] = value; - } - else - { - _dictionary.Remove(key); - } - } - } - - /// - public void Clear() => _dictionary.Clear(); - - /// - public bool ContainsKey(TKey key) - { - // _dictionary.ContainsKey will take care of throwing on a null key. - return _dictionary.ContainsKey(key); - } - - /// - public TValue? GetOrAdd(TKey key, TValue value) - { - // _dictionary.TryGetValue will take care of throwing on a null key. - if (_dictionary.TryGetValue(key, out var result)) - return result; - - if (value == null) - return null; - - _dictionary.Add(key, value); - return value; - } - - /// - public bool Remove(TKey key) - { - // _dictionary.Remove will take care of throwing on a null key. - return _dictionary.Remove(key); - } - - /// - public bool TryAdd(TKey key, TValue value) - { - // _dictionary.ContainsKey will take care of throwing on a null key. - if (_dictionary.ContainsKey(key)) - return false; - - if (value != null) - _dictionary.Add(key, value); - - return true; - } - - /// - public bool TryGetValue(TKey key, out TValue value) => _dictionary.TryGetValue(key, out value); - - /// - public bool TryRemove(TKey key, out TValue value) - { - // TryGetValue will take care of throwing on a null key. - if (!_dictionary.TryGetValue(key, out value)) - return false; - - _dictionary.Remove(key); - return true; - } - - /// - public bool TryUpdate(TKey key, TValue newValue, TValue comparisonValue) - { - // TryGetValue will take care of throwing on a null key. - if (!_dictionary.TryGetValue(key, out var value)) - return false; - - if (value != comparisonValue) - return false; - - _dictionary[key] = newValue; - return true; - } - - #endregion - - #region Implementation of IDictionary - - /// - void IDictionary.Add(TKey key, TValue value) - { - // Validating the key seems redundant, because both Add and Remove - // will throw on a null key. - // This way, though, the code path on null key does not depend on value. - // Without this validation, there should be two unit tests for null key, - // one with a null value and one with a non-null value, - // which makes no sense. - if (key == null) - throw new ArgumentNullException(nameof(key)); - - if (value != null) - { - _dictionary.Add(key, value); - } - else - { - _dictionary.Remove(key); - } - } - - #endregion - - #region Implementation of IReadOnlyDictionary - - /// - IEnumerable IReadOnlyDictionary.Keys => _dictionary.Keys; - - /// - IEnumerable IReadOnlyDictionary.Values => _dictionary.Values; - - #endregion - - #region Implementation of ICollection> - - /// - /// - /// This property is always for a . - /// - bool ICollection>.IsReadOnly => false; - - /// - void ICollection>.Add(KeyValuePair item) - { - if (item.Value != null) - { - ((ICollection>)_dictionary).Add(item); - } - else - { - _dictionary.Remove(item.Key); - } - } - - /// - bool ICollection>.Contains(KeyValuePair item) - => ((ICollection>)_dictionary).Contains(item); - - /// - void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) - => ((ICollection>)_dictionary).CopyTo(array, arrayIndex); - - /// - bool ICollection>.Remove(KeyValuePair item) - => ((ICollection>) _dictionary).Remove(item); - - #endregion - - #region Implementation of IEnumerable> - - /// - IEnumerator> IEnumerable>.GetEnumerator() => _dictionary.GetEnumerator(); - - #endregion - - #region Implementation of IEnumerable - - /// - IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_dictionary).GetEnumerator(); - - #endregion - } + /// + public DataDictionary() => this._dictionary = new Dictionary(); + + /// + /// Initializes a new instance of the class + /// that contains elements copied from the specified , + /// has the default initial capacity, and uses the default comparer for . + /// + /// The whose elements are copied + /// to the new . + /// is . + /// + /// Since does not store null values, + /// key/value pairs whose value is will not be copied from . + /// + /// + public DataDictionary(IEnumerable> collection) { + if(collection == null) { + throw new ArgumentNullException(nameof(collection)); + } + + this._dictionary = new Dictionary(); + foreach(KeyValuePair pair in collection.Where(pair => pair.Value != null)) { + this._dictionary.Add(pair.Key, pair.Value); + } + } + + /// + /// Initializes a new instance of the class + /// that is empty, has the default capacity, and uses the specified . + /// + /// The equality comparison implementation to use when comparing keys. + /// is . + /// + public DataDictionary(IEqualityComparer comparer) => this._dictionary = new Dictionary(comparer); + + /// + /// Initializes a new instance of the class + /// that contains elements copied from the specified , + /// has the default initial capacity, and uses the specified . + /// + /// The whose elements are copied + /// to the new . + /// The equality comparison implementation to use when comparing keys. + /// + /// Since does not store null values, + /// key/value pairs whose value is will not be copied from . + /// + /// + /// is . + /// - or -. + /// is . + /// + /// + public DataDictionary(IEnumerable> collection, IEqualityComparer comparer) { + if(collection == null) { + throw new ArgumentNullException(nameof(collection)); + } + + this._dictionary = new Dictionary(comparer); + foreach(KeyValuePair pair in collection.Where(pair => pair.Value != null)) { + this._dictionary.Add(pair.Key, pair.Value); + } + } + + /// + /// Initializes a new instance of the class + /// that is empty, has the specified capacity, and uses the default comparer for the key type. + /// + /// The initial number of elements that the can contain. + /// is less than 0. + /// + public DataDictionary(Int32 capacity) => this._dictionary = new Dictionary(capacity); + + /// + /// Initializes a new instance of the class + /// that contains elements copied from the specified , + /// has the specified capacity, and uses the specified . + /// + /// The initial number of elements that the can contain. + /// The whose elements are copied + /// to the new . + /// The equality comparison implementation to use when comparing keys. + /// + /// Since does not store null values, + /// key/value pairs whose value is will not be copied from . + /// + /// + /// is . + /// - or -. + /// is . + /// + /// is less than 0. + /// + public DataDictionary(Int32 capacity, IEnumerable> collection, IEqualityComparer comparer) { + if(collection == null) { + throw new ArgumentNullException(nameof(collection)); + } + + this._dictionary = new Dictionary(capacity, comparer); + foreach(KeyValuePair pair in collection.Where(pair => pair.Value != null)) { + this._dictionary.Add(pair.Key, pair.Value); + } + } + + #endregion + + #region Public APIs + + /// + public Int32 Count => this._dictionary.Count; + + /// + public Boolean IsEmpty => this._dictionary.Count == 0; + + /// + public ICollection Keys => this._dictionary.Keys; + + /// + public ICollection Values => this._dictionary.Values; + + /// + public TValue this[TKey key] { + get => this._dictionary.TryGetValue(key ?? throw new ArgumentNullException(nameof(key)), out TValue value) ? value : null; + set { + if(value != null) { + this._dictionary[key] = value; + } else { + _ = this._dictionary.Remove(key); + } + } + } + + /// + public void Clear() => this._dictionary.Clear(); + + /// + public Boolean ContainsKey(TKey key) => + // _dictionary.ContainsKey will take care of throwing on a null key. + this._dictionary.ContainsKey(key); + + /// + public TValue GetOrAdd(TKey key, TValue value) { + // _dictionary.TryGetValue will take care of throwing on a null key. + if(this._dictionary.TryGetValue(key, out TValue result)) { + return result; + } + + if(value == null) { + return null; + } + + this._dictionary.Add(key, value); + return value; + } + + /// + public Boolean Remove(TKey key) => + // _dictionary.Remove will take care of throwing on a null key. + this._dictionary.Remove(key); + + /// + public Boolean TryAdd(TKey key, TValue value) { + // _dictionary.ContainsKey will take care of throwing on a null key. + if(this._dictionary.ContainsKey(key)) { + return false; + } + + if(value != null) { + this._dictionary.Add(key, value); + } + + return true; + } + + /// + public Boolean TryGetValue(TKey key, out TValue value) => this._dictionary.TryGetValue(key, out value); + + /// + public Boolean TryRemove(TKey key, out TValue value) { + // TryGetValue will take care of throwing on a null key. + if(!this._dictionary.TryGetValue(key, out value)) { + return false; + } + + _ = this._dictionary.Remove(key); + return true; + } + + /// + public Boolean TryUpdate(TKey key, TValue newValue, TValue comparisonValue) { + // TryGetValue will take care of throwing on a null key. + if(!this._dictionary.TryGetValue(key, out TValue value)) { + return false; + } + + if(value != comparisonValue) { + return false; + } + + this._dictionary[key] = newValue; + return true; + } + + #endregion + + #region Implementation of IDictionary + + /// + void IDictionary.Add(TKey key, TValue value) { + // Validating the key seems redundant, because both Add and Remove + // will throw on a null key. + // This way, though, the code path on null key does not depend on value. + // Without this validation, there should be two unit tests for null key, + // one with a null value and one with a non-null value, + // which makes no sense. + if(key == null) { + throw new ArgumentNullException(nameof(key)); + } + + if(value != null) { + this._dictionary.Add(key, value); + } else { + _ = this._dictionary.Remove(key); + } + } + + #endregion + + #region Implementation of IReadOnlyDictionary + + /// + IEnumerable IReadOnlyDictionary.Keys => this._dictionary.Keys; + + /// + IEnumerable IReadOnlyDictionary.Values => this._dictionary.Values; + + #endregion + + #region Implementation of ICollection> + + /// + /// + /// This property is always for a . + /// + Boolean ICollection>.IsReadOnly => false; + + /// + void ICollection>.Add(KeyValuePair item) { + if(item.Value != null) { + ((ICollection>)this._dictionary).Add(item); + } else { + _ = this._dictionary.Remove(item.Key); + } + } + + /// + Boolean ICollection>.Contains(KeyValuePair item) => ((ICollection>)this._dictionary).Contains(item); + + /// + void ICollection>.CopyTo(KeyValuePair[] array, Int32 arrayIndex) => ((ICollection>)this._dictionary).CopyTo(array, arrayIndex); + + /// + Boolean ICollection>.Remove(KeyValuePair item) => ((ICollection>)this._dictionary).Remove(item); + + #endregion + + #region Implementation of IEnumerable> + + /// + IEnumerator> IEnumerable>.GetEnumerator() => this._dictionary.GetEnumerator(); + + #endregion + + #region Implementation of IEnumerable + + /// + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this._dictionary).GetEnumerator(); + + #endregion + } } diff --git a/Swan.Lite/Collections/DisposableComponentCollection`1.cs b/Swan.Lite/Collections/DisposableComponentCollection`1.cs index 4f42fa6..60c2e48 100644 --- a/Swan.Lite/Collections/DisposableComponentCollection`1.cs +++ b/Swan.Lite/Collections/DisposableComponentCollection`1.cs @@ -1,49 +1,46 @@ using System; -namespace Swan.Collections -{ +namespace Swan.Collections { + /// + /// Implements a collection of components that automatically disposes each component + /// implementing . + /// Each component in the collection may be given a unique name for later retrieval. + /// + /// The type of components in the collection. + /// + /// + public class DisposableComponentCollection : ComponentCollection, IDisposable { /// - /// Implements a collection of components that automatically disposes each component - /// implementing . - /// Each component in the collection may be given a unique name for later retrieval. + /// Finalizes an instance of the class. /// - /// The type of components in the collection. - /// - /// - public class DisposableComponentCollection : ComponentCollection, IDisposable - { - /// - /// Finalizes an instance of the class. - /// - ~DisposableComponentCollection() - { - Dispose(false); - } - - /// - /// Releases unmanaged and - optionally - managed resources. - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Releases unmanaged and - optionally - managed resources. - /// - /// - /// to release both managed and unmanaged resources; to release only unmanaged resources. - /// - protected virtual void Dispose(bool disposing) - { - if (!disposing) return; - - foreach (var component in this) - { - if (component is IDisposable disposable) - disposable.Dispose(); - } - } - } + ~DisposableComponentCollection() { + this.Dispose(false); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + public void Dispose() { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// + /// to release both managed and unmanaged resources; to release only unmanaged resources. + /// + protected virtual void Dispose(Boolean disposing) { + if(!disposing) { + return; + } + + foreach(T component in this) { + if(component is IDisposable disposable) { + disposable.Dispose(); + } + } + } + } } \ No newline at end of file diff --git a/Swan.Lite/Collections/IComponentCollection`1.cs b/Swan.Lite/Collections/IComponentCollection`1.cs index cf16760..5d44964 100644 --- a/Swan.Lite/Collections/IComponentCollection`1.cs +++ b/Swan.Lite/Collections/IComponentCollection`1.cs @@ -1,54 +1,56 @@ using System; using System.Collections.Generic; -namespace Swan.Collections -{ +namespace Swan.Collections { + /// + /// Represents a collection of components. + /// Each component in the collection may be given a unique name for later retrieval. + /// + /// The type of components in the collection. + public interface IComponentCollection : IReadOnlyList { /// - /// Represents a collection of components. - /// Each component in the collection may be given a unique name for later retrieval. + /// Gets an interface representing the named components. /// - /// The type of components in the collection. - public interface IComponentCollection : IReadOnlyList - { - /// - /// Gets an interface representing the named components. - /// - /// - /// The named components. - /// - IReadOnlyDictionary Named { get; } - - /// - /// Gets an interface representing all components - /// associated with safe names. - /// The safe name of a component is never . - /// If a component's unique name if , its safe name - /// will be some non- string somehow identifying it. - /// Note that safe names are not necessarily unique. - /// - /// - /// A list of s, each containing a safe name and a component. - /// - IReadOnlyList<(string SafeName, T Component)> WithSafeNames { get; } - - /// - /// Gets the component with the specified name. - /// - /// - /// The component. - /// - /// The name. - /// The component with the specified . - /// is null. - /// The property is retrieved and is not found. - T this[string name] { get; } - - /// - /// Adds a component to the collection, - /// giving it the specified if it is not . - /// - /// The name given to the module, or . - /// The component. - void Add(string name, T component); - } + /// + /// The named components. + /// + IReadOnlyDictionary Named { + get; + } + + /// + /// Gets an interface representing all components + /// associated with safe names. + /// The safe name of a component is never . + /// If a component's unique name if , its safe name + /// will be some non- string somehow identifying it. + /// Note that safe names are not necessarily unique. + /// + /// + /// A list of s, each containing a safe name and a component. + /// + IReadOnlyList<(String SafeName, T Component)> WithSafeNames { + get; + } + + /// + /// Gets the component with the specified name. + /// + /// + /// The component. + /// + /// The name. + /// The component with the specified . + /// is null. + /// The property is retrieved and is not found. + T this[String name] { get; } + + /// + /// Adds a component to the collection, + /// giving it the specified if it is not . + /// + /// The name given to the module, or . + /// The component. + void Add(String name, T component); + } } \ No newline at end of file diff --git a/Swan.Lite/Collections/IDataDictionary`2.cs b/Swan.Lite/Collections/IDataDictionary`2.cs index 800a7b0..e5f4885 100644 --- a/Swan.Lite/Collections/IDataDictionary`2.cs +++ b/Swan.Lite/Collections/IDataDictionary`2.cs @@ -1,34 +1,32 @@ using System; using System.Collections.Generic; -namespace Swan.Collections -{ +namespace Swan.Collections { + /// + /// Represents a generic collection of key/value pairs that does not store + /// null values. + /// + /// The type of keys in the dictionary. This must be a reference type. + /// The type of values in the dictionary. This must be a reference type. + public interface IDataDictionary : IDictionary, IReadOnlyDictionary where TKey : class where TValue : class { /// - /// Represents a generic collection of key/value pairs that does not store - /// null values. + /// Gets a value that indicates whether the is empty. /// - /// The type of keys in the dictionary. This must be a reference type. - /// The type of values in the dictionary. This must be a reference type. - public interface IDataDictionary : IDictionary, IReadOnlyDictionary - where TKey : class - where TValue : class - { - /// - /// Gets a value that indicates whether the is empty. - /// - /// - /// if the is empty; otherwise, . - /// - bool IsEmpty { get; } - - /// - /// Attempts to remove and return the value that has the specified key from the . - /// - /// The key of the element to remove and return. - /// When this method returns, the value removed from the , - /// if the key is found; otherwise, . This parameter is passed uninitialized. - /// if the value was removed successfully; otherwise, . - /// is . - bool TryRemove(TKey key, out TValue value); - } + /// + /// if the is empty; otherwise, . + /// + Boolean IsEmpty { + get; + } + + /// + /// Attempts to remove and return the value that has the specified key from the . + /// + /// The key of the element to remove and return. + /// When this method returns, the value removed from the , + /// if the key is found; otherwise, . This parameter is passed uninitialized. + /// if the value was removed successfully; otherwise, . + /// is . + Boolean TryRemove(TKey key, out TValue value); + } } diff --git a/Swan.Lite/Configuration/ConfiguredObject.cs b/Swan.Lite/Configuration/ConfiguredObject.cs index 1855c81..57702a8 100644 --- a/Swan.Lite/Configuration/ConfiguredObject.cs +++ b/Swan.Lite/Configuration/ConfiguredObject.cs @@ -1,77 +1,70 @@ using System; -namespace Swan.Configuration -{ +namespace Swan.Configuration { + /// + /// Base class for objects whose configuration may be locked, + /// thus becoming read-only, at a certain moment in their lifetime. + /// + public abstract class ConfiguredObject { + private readonly Object _syncRoot = new Object(); + private Boolean _configurationLocked; + /// - /// Base class for objects whose configuration may be locked, - /// thus becoming read-only, at a certain moment in their lifetime. + /// Gets a value indicating whether s configuration has already been locked + /// and has therefore become read-only. /// - public abstract class ConfiguredObject - { - private readonly object _syncRoot = new object(); - private bool _configurationLocked; - - /// - /// Gets a value indicating whether s configuration has already been locked - /// and has therefore become read-only. - /// - /// - /// if the configuration is locked; otherwise, . - /// - /// - protected bool ConfigurationLocked - { - get - { - lock (_syncRoot) - { - return _configurationLocked; - } - } - } - - /// - /// Locks this instance's configuration, preventing further modifications. - /// - /// - /// Configuration locking must be enforced by derived classes - /// by calling at the start - /// of methods and property setters that could change the object's - /// configuration. - /// Immediately before locking the configuration, this method calls - /// as a last chance to validate configuration data, and to lock the configuration of contained objects. - /// - /// - protected void LockConfiguration() - { - lock (_syncRoot) - { - if (_configurationLocked) - return; - - OnBeforeLockConfiguration(); - _configurationLocked = true; - } - } - - /// - /// Called immediately before locking the configuration. - /// - /// - protected virtual void OnBeforeLockConfiguration() - { - } - - /// - /// Checks whether a module's configuration has become read-only - /// and, if so, throws an . - /// - /// The configuration is locked. - /// - protected void EnsureConfigurationNotLocked() - { - if (ConfigurationLocked) - throw new InvalidOperationException($"Configuration of this {GetType().Name} instance is locked."); - } - } + /// + /// if the configuration is locked; otherwise, . + /// + /// + protected Boolean ConfigurationLocked { + get { + lock(this._syncRoot) { + return this._configurationLocked; + } + } + } + + /// + /// Locks this instance's configuration, preventing further modifications. + /// + /// + /// Configuration locking must be enforced by derived classes + /// by calling at the start + /// of methods and property setters that could change the object's + /// configuration. + /// Immediately before locking the configuration, this method calls + /// as a last chance to validate configuration data, and to lock the configuration of contained objects. + /// + /// + protected void LockConfiguration() { + lock(this._syncRoot) { + if(this._configurationLocked) { + return; + } + + this.OnBeforeLockConfiguration(); + this._configurationLocked = true; + } + } + + /// + /// Called immediately before locking the configuration. + /// + /// + protected virtual void OnBeforeLockConfiguration() { + } + + /// + /// Checks whether a module's configuration has become read-only + /// and, if so, throws an . + /// + /// The configuration is locked. + /// + protected void EnsureConfigurationNotLocked() { + if(this.ConfigurationLocked) { + throw new InvalidOperationException($"Configuration of this {this.GetType().Name} instance is locked."); + } + } + } } diff --git a/Swan.Lite/Configuration/PropertyDisplayAttribute.cs b/Swan.Lite/Configuration/PropertyDisplayAttribute.cs index 2f45af5..c5e9d97 100644 --- a/Swan.Lite/Configuration/PropertyDisplayAttribute.cs +++ b/Swan.Lite/Configuration/PropertyDisplayAttribute.cs @@ -1,54 +1,62 @@ using System; -namespace Swan.Configuration -{ +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 { /// - /// An attribute used to include additional information to a Property for serialization. - /// - /// Previously we used DisplayAttribute from DataAnnotation. + /// Gets or sets the name. /// - /// - [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; } - } + /// + /// 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.Lite/Configuration/SettingsProvider.cs b/Swan.Lite/Configuration/SettingsProvider.cs index 6e1020b..2ba5e5a 100644 --- a/Swan.Lite/Configuration/SettingsProvider.cs +++ b/Swan.Lite/Configuration/SettingsProvider.cs @@ -41,10 +41,9 @@ namespace Swan.Configuration /// /// /// The type of settings model. - public sealed class SettingsProvider - : SingletonBase> + public sealed class SettingsProvider : SingletonBase> { - private readonly object _syncRoot = new object(); + private readonly Object _syncRoot = new Object(); private T _global; @@ -55,8 +54,7 @@ namespace Swan.Configuration /// /// The configuration file path. /// - public string ConfigurationFilePath { get; set; } = - Path.Combine(SwanRuntime.EntryAssemblyDirectory, "appsettings.json"); + public String ConfigurationFilePath { get; set; } = Path.Combine(SwanRuntime.EntryAssemblyDirectory, "appsettings.json"); /// /// Gets the global settings object. @@ -68,12 +66,13 @@ namespace Swan.Configuration { get { - lock (_syncRoot) + lock (this._syncRoot) { - if (Equals(_global, default(T))) - ReloadGlobalSettings(); - - return _global; + if (Equals(this._global, default(T)!)) { + this.ReloadGlobalSettings(); + } + + return this._global; } } } @@ -83,20 +82,21 @@ namespace Swan.Configuration /// public void ReloadGlobalSettings() { - if (File.Exists(ConfigurationFilePath) == false || File.ReadAllText(ConfigurationFilePath).Length == 0) - { - ResetGlobalSettings(); + if (File.Exists(this.ConfigurationFilePath) == false || File.ReadAllText(this.ConfigurationFilePath).Length == 0) + { + this.ResetGlobalSettings(); return; } - lock (_syncRoot) - _global = Json.Deserialize(File.ReadAllText(ConfigurationFilePath)); - } + lock (this._syncRoot) { + this._global = Json.Deserialize(File.ReadAllText(this.ConfigurationFilePath)); + } + } /// /// Persists the global settings. /// - public void PersistGlobalSettings() => File.WriteAllText(ConfigurationFilePath, Json.Serialize(Global, true)); + public void PersistGlobalSettings() => File.WriteAllText(this.ConfigurationFilePath, Json.Serialize(this.Global, true)); /// /// Updates settings from list. @@ -106,29 +106,34 @@ namespace Swan.Configuration /// A list of settings of type ref="ExtendedPropertyInfo". /// /// propertyList. - public List RefreshFromList(List> propertyList) + public List RefreshFromList(List> propertyList) { - if (propertyList == null) - throw new ArgumentNullException(nameof(propertyList)); + if (propertyList == null) { + throw new ArgumentNullException(nameof(propertyList)); + } + + List changedSettings = new List(); + IEnumerable globalProps = PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties(); - var changedSettings = new List(); - var globalProps = PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties(); + foreach (ExtendedPropertyInfo property in propertyList) + { + PropertyInfo propertyInfo = globalProps.FirstOrDefault(x => x.Name == property.Property); - foreach (var property in propertyList) - { - var propertyInfo = globalProps.FirstOrDefault(x => x.Name == property.Property); + if (propertyInfo == null) { + continue; + } + + Object originalValue = propertyInfo.GetValue(this.Global); + Boolean isChanged = propertyInfo.PropertyType.IsArray + ? property.Value is IEnumerable enumerable && propertyInfo.TrySetArray(enumerable.Cast(), this.Global) + : this.SetValue(property.Value, originalValue, propertyInfo); - if (propertyInfo == null) continue; - - var originalValue = propertyInfo.GetValue(Global); - var isChanged = propertyInfo.PropertyType.IsArray - ? property.Value is IEnumerable enumerable && propertyInfo.TrySetArray(enumerable.Cast(), Global) - : SetValue(property.Value, originalValue, propertyInfo); - - if (!isChanged) continue; - - changedSettings.Add(property.Property); - PersistGlobalSettings(); + if (!isChanged) { + continue; + } + + changedSettings.Add(property.Property); + this.PersistGlobalSettings(); } return changedSettings; @@ -138,9 +143,9 @@ namespace Swan.Configuration /// Gets the list. /// /// A List of ExtendedPropertyInfo of the type T. - public List>? GetList() - { - var jsonData = Json.Deserialize(Json.Serialize(Global)) as Dictionary; + public List> GetList() + { + Dictionary jsonData = Json.Deserialize(Json.Serialize(this.Global)) as Dictionary; return jsonData?.Keys .Select(p => new ExtendedPropertyInfo(p) { Value = jsonData[p] }) @@ -152,26 +157,27 @@ namespace Swan.Configuration /// public void ResetGlobalSettings() { - lock (_syncRoot) - _global = Activator.CreateInstance(); - - PersistGlobalSettings(); + lock (this._syncRoot) { + this._global = Activator.CreateInstance(); + } + + this.PersistGlobalSettings(); } - private bool SetValue(object property, object originalValue, PropertyInfo propertyInfo) + private Boolean SetValue(Object property, Object originalValue, PropertyInfo propertyInfo) { switch (property) { case null when originalValue == null: break; case null: - propertyInfo.SetValue(Global, null); + propertyInfo.SetValue(this.Global, null); return true; default: - if (propertyInfo.PropertyType.TryParseBasicType(property, out var propertyValue) && + if (propertyInfo.PropertyType.TryParseBasicType(property, out Object propertyValue) && !propertyValue.Equals(originalValue)) { - propertyInfo.SetValue(Global, propertyValue); + propertyInfo.SetValue(this.Global, propertyValue); return true; } diff --git a/Swan.Lite/Cryptography/Hasher.cs b/Swan.Lite/Cryptography/Hasher.cs index a78adac..5dfc677 100644 --- a/Swan.Lite/Cryptography/Hasher.cs +++ b/Swan.Lite/Cryptography/Hasher.cs @@ -3,122 +3,123 @@ using System.IO; using System.Security.Cryptography; using System.Text; -namespace Swan.Cryptography -{ +namespace Swan.Cryptography { + /// + /// Use this class to compute a hash in MD4, SHA1, SHA256 or SHA512. + /// + public static class Hasher { + private static readonly Lazy Md5Hasher = new Lazy(MD5.Create, true); + private static readonly Lazy SHA1Hasher = new Lazy(SHA1.Create, true); + private static readonly Lazy SHA256Hasher = new Lazy(SHA256.Create, true); + private static readonly Lazy SHA512Hasher = new Lazy(SHA512.Create, true); + /// - /// Use this class to compute a hash in MD4, SHA1, SHA256 or SHA512. + /// Computes the MD5 hash of the given stream. + /// Do not use for large streams as this reads ALL bytes at once. /// - public static class Hasher - { - private static readonly Lazy Md5Hasher = new Lazy(MD5.Create, true); - private static readonly Lazy SHA1Hasher = new Lazy(SHA1.Create, true); - private static readonly Lazy SHA256Hasher = new Lazy(SHA256.Create, true); - private static readonly Lazy SHA512Hasher = new Lazy(SHA512.Create, true); - - /// - /// Computes the MD5 hash of the given stream. - /// Do not use for large streams as this reads ALL bytes at once. - /// - /// The stream. - /// if set to true [create hasher]. - /// - /// The computed hash code. - /// - /// stream. - [Obsolete("Use a better hasher.")] - public static byte[] ComputeMD5(Stream @this, bool createHasher = false) - { - if (@this == null) - throw new ArgumentNullException(nameof(@this)); - - var md5 = MD5.Create(); - const int bufferSize = 4096; - - var readAheadBuffer = new byte[bufferSize]; - var readAheadBytesRead = @this.Read(readAheadBuffer, 0, readAheadBuffer.Length); - - do - { - var bytesRead = readAheadBytesRead; - var buffer = readAheadBuffer; - - readAheadBuffer = new byte[bufferSize]; - readAheadBytesRead = @this.Read(readAheadBuffer, 0, readAheadBuffer.Length); - - if (readAheadBytesRead == 0) - md5.TransformFinalBlock(buffer, 0, bytesRead); - else - md5.TransformBlock(buffer, 0, bytesRead, buffer, 0); - } - while (readAheadBytesRead != 0); - - return md5.Hash; - } - - /// - /// Computes the MD5 hash of the given string using UTF8 byte encoding. - /// - /// The input string. - /// if set to true [create hasher]. - /// The computed hash code. - [Obsolete("Use a better hasher.")] - public static byte[] ComputeMD5(string value, bool createHasher = false) => - ComputeMD5(Encoding.UTF8.GetBytes(value), createHasher); - - /// - /// Computes the MD5 hash of the given byte array. - /// - /// The data. - /// if set to true [create hasher]. - /// The computed hash code. - [Obsolete("Use a better hasher.")] - public static byte[] ComputeMD5(byte[] data, bool createHasher = false) => - (createHasher ? MD5.Create() : Md5Hasher.Value).ComputeHash(data); - - /// - /// Computes the SHA-1 hash of the given string using UTF8 byte encoding. - /// - /// The input string. - /// if set to true [create hasher]. - /// - /// The computes a Hash-based Message Authentication Code (HMAC) - /// using the SHA1 hash function. - /// - [Obsolete("Use a better hasher.")] - public static byte[] ComputeSha1(string @this, bool createHasher = false) - { - var inputBytes = Encoding.UTF8.GetBytes(@this); - return (createHasher ? SHA1.Create() : SHA1Hasher.Value).ComputeHash(inputBytes); - } - - /// - /// Computes the SHA-256 hash of the given string using UTF8 byte encoding. - /// - /// The input string. - /// if set to true [create hasher]. - /// - /// The computes a Hash-based Message Authentication Code (HMAC) - /// by using the SHA256 hash function. - /// - public static byte[] ComputeSha256(string value, bool createHasher = false) - { - var inputBytes = Encoding.UTF8.GetBytes(value); - return (createHasher ? SHA256.Create() : SHA256Hasher.Value).ComputeHash(inputBytes); - } - - /// - /// Computes the SHA-512 hash of the given string using UTF8 byte encoding. - /// - /// The input string. - /// if set to true [create hasher]. - /// - /// The computes a Hash-based Message Authentication Code (HMAC) - /// using the SHA512 hash function. - /// - public static byte[] ComputeSha512(string value, bool createHasher = false) - { - var inputBytes = Encoding.UTF8.GetBytes(value); - return (createHasher ? SHA512.Create() : SHA512Hasher.Value).ComputeHash(inputBytes); - } - } + /// The stream. + /// if set to true [create hasher]. + /// + /// The computed hash code. + /// + /// stream. + [Obsolete("Use a better hasher.")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Codequalität", "IDE0067:Objekte verwerfen, bevor Bereich verloren geht", Justification = "")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Stil", "IDE0045:In bedingten Ausdruck konvertieren", Justification = "")] + public static Byte[] ComputeMD5(Stream @this, Boolean createHasher = false) { + if(@this == null) { + throw new ArgumentNullException(nameof(@this)); + } + + MD5 md5 = MD5.Create(); + const Int32 bufferSize = 4096; + + Byte[] readAheadBuffer = new Byte[bufferSize]; + Int32 readAheadBytesRead = @this.Read(readAheadBuffer, 0, readAheadBuffer.Length); + + do { + Int32 bytesRead = readAheadBytesRead; + Byte[] buffer = readAheadBuffer; + + readAheadBuffer = new Byte[bufferSize]; + readAheadBytesRead = @this.Read(readAheadBuffer, 0, readAheadBuffer.Length); + + if(readAheadBytesRead == 0) { + _ = md5.TransformFinalBlock(buffer, 0, bytesRead); + } else { + _ = md5.TransformBlock(buffer, 0, bytesRead, buffer, 0); + } + } + while(readAheadBytesRead != 0); + + return md5.Hash; + } + + /// + /// Computes the MD5 hash of the given string using UTF8 byte encoding. + /// + /// The input string. + /// if set to true [create hasher]. + /// The computed hash code. + [Obsolete("Use a better hasher.")] + public static Byte[] ComputeMD5(String value, Boolean createHasher = false) => ComputeMD5(Encoding.UTF8.GetBytes(value), createHasher); + + /// + /// Computes the MD5 hash of the given byte array. + /// + /// The data. + /// if set to true [create hasher]. + /// The computed hash code. + [Obsolete("Use a better hasher.")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Codequalität", "IDE0067:Objekte verwerfen, bevor Bereich verloren geht", Justification = "")] + public static Byte[] ComputeMD5(Byte[] data, Boolean createHasher = false) => (createHasher ? MD5.Create() : Md5Hasher.Value).ComputeHash(data); + + /// + /// Computes the SHA-1 hash of the given string using UTF8 byte encoding. + /// + /// The input string. + /// if set to true [create hasher]. + /// + /// The computes a Hash-based Message Authentication Code (HMAC) + /// using the SHA1 hash function. + /// + [Obsolete("Use a better hasher.")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Codequalität", "IDE0067:Objekte verwerfen, bevor Bereich verloren geht", Justification = "")] + public static Byte[] ComputeSha1(String @this, Boolean createHasher = false) { + Byte[] inputBytes = Encoding.UTF8.GetBytes(@this); + return (createHasher ? SHA1.Create() : SHA1Hasher.Value).ComputeHash(inputBytes); + } + + + /// + /// Computes the SHA-256 hash of the given string using UTF8 byte encoding. + /// + /// The input string. + /// if set to true [create hasher]. + /// + /// The computes a Hash-based Message Authentication Code (HMAC) + /// by using the SHA256 hash function. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Codequalität", "IDE0067:Objekte verwerfen, bevor Bereich verloren geht", Justification = "")] + public static Byte[] ComputeSha256(String value, Boolean createHasher = false) { + Byte[] inputBytes = Encoding.UTF8.GetBytes(value); + return (createHasher ? SHA256.Create() : SHA256Hasher.Value).ComputeHash(inputBytes); + } + + + /// + /// Computes the SHA-512 hash of the given string using UTF8 byte encoding. + /// + /// The input string. + /// if set to true [create hasher]. + /// + /// The computes a Hash-based Message Authentication Code (HMAC) + /// using the SHA512 hash function. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Codequalität", "IDE0067:Objekte verwerfen, bevor Bereich verloren geht", Justification = "")] + public static Byte[] ComputeSha512(String value, Boolean createHasher = false) { + Byte[] inputBytes = Encoding.UTF8.GetBytes(value); + return (createHasher ? SHA512.Create() : SHA512Hasher.Value).ComputeHash(inputBytes); + } + } } diff --git a/Swan.Lite/DateTimeSpan.cs b/Swan.Lite/DateTimeSpan.cs index f512d9c..1c45ff4 100644 --- a/Swan.Lite/DateTimeSpan.cs +++ b/Swan.Lite/DateTimeSpan.cs @@ -1,174 +1,165 @@ using System; -namespace Swan -{ +namespace Swan { + /// + /// Represents a struct of DateTimeSpan to compare dates and get in + /// separate fields the amount of time between those dates. + /// + /// Based on https://stackoverflow.com/a/9216404/1096693. + /// + public struct DateTimeSpan { /// - /// Represents a struct of DateTimeSpan to compare dates and get in - /// separate fields the amount of time between those dates. - /// - /// Based on https://stackoverflow.com/a/9216404/1096693. + /// Initializes a new instance of the struct. /// - public struct DateTimeSpan - { - /// - /// Initializes a new instance of the struct. - /// - /// The years. - /// The months. - /// The days. - /// The hours. - /// The minutes. - /// The seconds. - /// The milliseconds. - public DateTimeSpan(int years, int months, int days, int hours, int minutes, int seconds, int milliseconds) - { - Years = years; - Months = months; - Days = days; - Hours = hours; - Minutes = minutes; - Seconds = seconds; - Milliseconds = milliseconds; - } - - /// - /// Gets the years. - /// - /// - /// The years. - /// - public int Years { get; } - - /// - /// Gets the months. - /// - /// - /// The months. - /// - public int Months { get; } - - /// - /// Gets the days. - /// - /// - /// The days. - /// - public int Days { get; } - - /// - /// Gets the hours. - /// - /// - /// The hours. - /// - public int Hours { get; } - - /// - /// Gets the minutes. - /// - /// - /// The minutes. - /// - public int Minutes { get; } - - /// - /// Gets the seconds. - /// - /// - /// The seconds. - /// - public int Seconds { get; } - - /// - /// Gets the milliseconds. - /// - /// - /// The milliseconds. - /// - public int Milliseconds { get; } - - internal static DateTimeSpan CompareDates(DateTime date1, DateTime date2) - { - if (date2 < date1) - { - var sub = date1; - date1 = date2; - date2 = sub; - } - - var current = date1; - var years = 0; - var months = 0; - var days = 0; - - var phase = Phase.Years; - var span = new DateTimeSpan(); - var officialDay = current.Day; - - while (phase != Phase.Done) - { - switch (phase) - { - case Phase.Years: - if (current.AddYears(years + 1) > date2) - { - phase = Phase.Months; - current = current.AddYears(years); - } - else - { - years++; - } - - break; - case Phase.Months: - if (current.AddMonths(months + 1) > date2) - { - phase = Phase.Days; - current = current.AddMonths(months); - if (current.Day < officialDay && - officialDay <= DateTime.DaysInMonth(current.Year, current.Month)) - current = current.AddDays(officialDay - current.Day); - } - else - { - months++; - } - - break; - case Phase.Days: - if (current.AddDays(days + 1) > date2) - { - current = current.AddDays(days); - var timespan = date2 - current; - span = new DateTimeSpan( - years, - months, - days, - timespan.Hours, - timespan.Minutes, - timespan.Seconds, - timespan.Milliseconds); - phase = Phase.Done; - } - else - { - days++; - } - - break; - } - } - - return span; - } - - private enum Phase - { - Years, - Months, - Days, - Done, - } - } + /// The years. + /// The months. + /// The days. + /// The hours. + /// The minutes. + /// The seconds. + /// The milliseconds. + public DateTimeSpan(Int32 years, Int32 months, Int32 days, Int32 hours, Int32 minutes, Int32 seconds, Int32 milliseconds) { + this.Years = years; + this.Months = months; + this.Days = days; + this.Hours = hours; + this.Minutes = minutes; + this.Seconds = seconds; + this.Milliseconds = milliseconds; + } + + /// + /// Gets the years. + /// + /// + /// The years. + /// + public Int32 Years { + get; + } + + /// + /// Gets the months. + /// + /// + /// The months. + /// + public Int32 Months { + get; + } + + /// + /// Gets the days. + /// + /// + /// The days. + /// + public Int32 Days { + get; + } + + /// + /// Gets the hours. + /// + /// + /// The hours. + /// + public Int32 Hours { + get; + } + + /// + /// Gets the minutes. + /// + /// + /// The minutes. + /// + public Int32 Minutes { + get; + } + + /// + /// Gets the seconds. + /// + /// + /// The seconds. + /// + public Int32 Seconds { + get; + } + + /// + /// Gets the milliseconds. + /// + /// + /// The milliseconds. + /// + public Int32 Milliseconds { + get; + } + + internal static DateTimeSpan CompareDates(DateTime date1, DateTime date2) { + if(date2 < date1) { + DateTime sub = date1; + date1 = date2; + date2 = sub; + } + + DateTime current = date1; + Int32 years = 0; + Int32 months = 0; + Int32 days = 0; + + Phase phase = Phase.Years; + DateTimeSpan span = new DateTimeSpan(); + Int32 officialDay = current.Day; + + while(phase != Phase.Done) { + switch(phase) { + case Phase.Years: + if(current.AddYears(years + 1) > date2) { + phase = Phase.Months; + current = current.AddYears(years); + } else { + years++; + } + + break; + case Phase.Months: + if(current.AddMonths(months + 1) > date2) { + phase = Phase.Days; + current = current.AddMonths(months); + if(current.Day < officialDay && + officialDay <= DateTime.DaysInMonth(current.Year, current.Month)) { + current = current.AddDays(officialDay - current.Day); + } + } else { + months++; + } + + break; + case Phase.Days: + if(current.AddDays(days + 1) > date2) { + current = current.AddDays(days); + TimeSpan timespan = date2 - current; + span = new DateTimeSpan(years, months, days, timespan.Hours, timespan.Minutes, timespan.Seconds, timespan.Milliseconds); + phase = Phase.Done; + } else { + days++; + } + + break; + } + } + + return span; + } + + private enum Phase { + Years, + Months, + Days, + Done, + } + } } \ No newline at end of file diff --git a/Swan.Lite/Definitions.Types.cs b/Swan.Lite/Definitions.Types.cs index 68895e9..6fc4742 100644 --- a/Swan.Lite/Definitions.Types.cs +++ b/Swan.Lite/Definitions.Types.cs @@ -3,136 +3,123 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Net; + using Swan.Reflection; -namespace Swan -{ +namespace Swan { + /// + /// Contains useful constants and definitions. + /// + public static partial class Definitions { + #region Main Dictionary Definition + /// - /// Contains useful constants and definitions. + /// The basic types information. /// - 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(int), new ExtendedTypeInfo()}, - {typeof(uint), new ExtendedTypeInfo()}, - {typeof(short), new ExtendedTypeInfo()}, - {typeof(ushort), new ExtendedTypeInfo()}, - {typeof(long), new ExtendedTypeInfo()}, - {typeof(ulong), new ExtendedTypeInfo()}, - {typeof(float), new ExtendedTypeInfo()}, - {typeof(double), new ExtendedTypeInfo()}, - {typeof(char), new ExtendedTypeInfo()}, - {typeof(bool), new ExtendedTypeInfo()}, - {typeof(decimal), new ExtendedTypeInfo()}, - {typeof(Guid), new ExtendedTypeInfo()}, + 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()}, + // 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(int?), new ExtendedTypeInfo()}, - {typeof(uint?), new ExtendedTypeInfo()}, - {typeof(short?), new ExtendedTypeInfo()}, - {typeof(ushort?), new ExtendedTypeInfo()}, - {typeof(long?), new ExtendedTypeInfo()}, - {typeof(ulong?), new ExtendedTypeInfo()}, - {typeof(float?), new ExtendedTypeInfo()}, - {typeof(double?), new ExtendedTypeInfo()}, - {typeof(char?), new ExtendedTypeInfo()}, - {typeof(bool?), new ExtendedTypeInfo()}, - {typeof(decimal?), new ExtendedTypeInfo()}, - {typeof(Guid?), 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()); - } + // 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.Lite/Definitions.cs b/Swan.Lite/Definitions.cs index 3c4e97a..d91bed1 100644 --- a/Swan.Lite/Definitions.cs +++ b/Swan.Lite/Definitions.cs @@ -1,39 +1,34 @@ -using System.Text; +using System; +using System.Text; -namespace Swan -{ +namespace Swan { + /// + /// Contains useful constants and definitions. + /// + public static partial class Definitions { /// - /// Contains useful constants and definitions. + /// The MS Windows codepage 1252 encoding used in some legacy scenarios + /// such as default CSV text encoding from Excel. /// - 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(int)); - try - { - Windows1252Encoding = Encoding.GetEncoding(1252); - } - catch - { - // ignore, the codepage is not available use default - Windows1252Encoding = CurrentAnsiEncoding; - } - } - } + 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.Lite/Diagnostics/Benchmark.cs b/Swan.Lite/Diagnostics/Benchmark.cs index d79f98b..ed7e5d7 100644 --- a/Swan.Lite/Diagnostics/Benchmark.cs +++ b/Swan.Lite/Diagnostics/Benchmark.cs @@ -4,118 +4,102 @@ using System.Diagnostics; using System.Linq; using System.Text; -namespace Swan.Diagnostics -{ +namespace Swan.Diagnostics { + /// + /// A simple benchmarking class. + /// + /// + /// The following code demonstrates how to create a simple benchmark. + /// + /// namespace Examples.Benchmark.Simple + /// { + /// using Swan.Diagnostics; + /// + /// public class SimpleBenchmark + /// { + /// public static void Main() + /// { + /// using (Benchmark.Start("Test")) + /// { + /// // do some logic in here + /// } + /// + /// // dump results into a string + /// var results = Benchmark.Dump(); + /// } + /// } + /// + /// } + /// + /// + public static partial class Benchmark { + private static readonly Object SyncLock = new Object(); + private static readonly Dictionary> Measures = new Dictionary>(); + /// - /// A simple benchmarking class. + /// Starts measuring with the given identifier. /// - /// - /// The following code demonstrates how to create a simple benchmark. - /// - /// namespace Examples.Benchmark.Simple - /// { - /// using Swan.Diagnostics; - /// - /// public class SimpleBenchmark - /// { - /// public static void Main() - /// { - /// using (Benchmark.Start("Test")) - /// { - /// // do some logic in here - /// } - /// - /// // dump results into a string - /// var results = Benchmark.Dump(); - /// } - /// } - /// - /// } - /// - /// - public static partial class Benchmark - { - private static readonly object SyncLock = new object(); - private static readonly Dictionary> Measures = new Dictionary>(); - - /// - /// Starts measuring with the given identifier. - /// - /// The identifier. - /// A disposable object that when disposed, adds a benchmark result. - public static IDisposable Start(string identifier) => new BenchmarkUnit(identifier); - - /// - /// Outputs the benchmark statistics. - /// - /// A string containing human-readable statistics. - public static string Dump() - { - var builder = new StringBuilder(); - - lock (SyncLock) - { - foreach (var kvp in Measures) - { - builder.Append($"BID: {kvp.Key,-30} | ") - .Append($"CNT: {kvp.Value.Count,6} | ") - .Append($"AVG: {kvp.Value.Average(t => t.TotalMilliseconds),8:0.000} ms. | ") - .Append($"MAX: {kvp.Value.Max(t => t.TotalMilliseconds),8:0.000} ms. | ") - .Append($"MIN: {kvp.Value.Min(t => t.TotalMilliseconds),8:0.000} ms. | ") - .Append(Environment.NewLine); - } - } - - return builder.ToString().TrimEnd(); - } - - /// - /// Measures the elapsed time of the given action as a TimeSpan - /// This method uses a high precision Stopwatch if it is available. - /// - /// The target. - /// - /// A time interval that represents a specified time, where the specification is in units of ticks. - /// - /// target. - public static TimeSpan BenchmarkAction(Action target) - { - if (target == null) - throw new ArgumentNullException(nameof(target)); - - var sw = Stopwatch.IsHighResolution ? new HighResolutionTimer() : new Stopwatch(); - - try - { - sw.Start(); - target.Invoke(); - } - catch - { - // swallow - } - finally - { - sw.Stop(); - } - - return TimeSpan.FromTicks(sw.ElapsedTicks); - } - - /// - /// Adds the specified result to the given identifier. - /// - /// The identifier. - /// The elapsed. - private static void Add(string identifier, TimeSpan elapsed) - { - lock (SyncLock) - { - if (Measures.ContainsKey(identifier) == false) - Measures[identifier] = new List(1024 * 1024); - - Measures[identifier].Add(elapsed); - } - } - } + /// The identifier. + /// A disposable object that when disposed, adds a benchmark result. + public static IDisposable Start(String identifier) => new BenchmarkUnit(identifier); + + /// + /// Outputs the benchmark statistics. + /// + /// A string containing human-readable statistics. + public static String Dump() { + StringBuilder builder = new StringBuilder(); + + lock(SyncLock) { + foreach(KeyValuePair> kvp in Measures) { + _ = builder.Append($"BID: {kvp.Key,-30} | ").Append($"CNT: {kvp.Value.Count,6} | ").Append($"AVG: {kvp.Value.Average(t => t.TotalMilliseconds),8:0.000} ms. | ").Append($"MAX: {kvp.Value.Max(t => t.TotalMilliseconds),8:0.000} ms. | ").Append($"MIN: {kvp.Value.Min(t => t.TotalMilliseconds),8:0.000} ms. | ").Append(Environment.NewLine); + } + } + + return builder.ToString().TrimEnd(); + } + + /// + /// Measures the elapsed time of the given action as a TimeSpan + /// This method uses a high precision Stopwatch if it is available. + /// + /// The target. + /// + /// A time interval that represents a specified time, where the specification is in units of ticks. + /// + /// target. + public static TimeSpan BenchmarkAction(Action target) { + if(target == null) { + throw new ArgumentNullException(nameof(target)); + } + + Stopwatch sw = Stopwatch.IsHighResolution ? new HighResolutionTimer() : new Stopwatch(); + + try { + sw.Start(); + target.Invoke(); + } catch { + // swallow + } finally { + sw.Stop(); + } + + return TimeSpan.FromTicks(sw.ElapsedTicks); + } + + /// + /// Adds the specified result to the given identifier. + /// + /// The identifier. + /// The elapsed. + private static void Add(String identifier, TimeSpan elapsed) { + lock(SyncLock) { + if(Measures.ContainsKey(identifier) == false) { + Measures[identifier] = new List(1024 * 1024); + } + + Measures[identifier].Add(elapsed); + } + } + } } diff --git a/Swan.Lite/Diagnostics/BenchmarkUnit.cs b/Swan.Lite/Diagnostics/BenchmarkUnit.cs index e896995..cbc4cde 100644 --- a/Swan.Lite/Diagnostics/BenchmarkUnit.cs +++ b/Swan.Lite/Diagnostics/BenchmarkUnit.cs @@ -1,50 +1,47 @@ -using System; +#nullable enable +using System; using System.Diagnostics; -namespace Swan.Diagnostics -{ - public static partial class Benchmark - { - /// - /// Represents a disposable benchmark unit. - /// - /// - private sealed class BenchmarkUnit : IDisposable - { - private readonly string _identifier; - private bool _isDisposed; // To detect redundant calls - private Stopwatch? _stopwatch = new Stopwatch(); - - /// - /// Initializes a new instance of the class. - /// - /// The identifier. - public BenchmarkUnit(string identifier) - { - _identifier = identifier; - _stopwatch?.Start(); - } - - /// - public void Dispose() => Dispose(true); - - /// - /// Releases unmanaged and - optionally - managed resources. - /// - /// true to release both managed and unmanaged resources; false to release only unmanaged resources. - private void Dispose(bool alsoManaged) - { - if (_isDisposed) return; - - if (alsoManaged) - { - Add(_identifier, _stopwatch?.Elapsed ?? default); - _stopwatch?.Stop(); - } - - _stopwatch = null; - _isDisposed = true; - } - } - } +namespace Swan.Diagnostics { + public static partial class Benchmark { + /// + /// Represents a disposable benchmark unit. + /// + /// + private sealed class BenchmarkUnit : IDisposable { + private readonly String _identifier; + private Boolean _isDisposed; // To detect redundant calls + private Stopwatch? _stopwatch = new Stopwatch(); + + /// + /// Initializes a new instance of the class. + /// + /// The identifier. + public BenchmarkUnit(String identifier) { + this._identifier = identifier; + this._stopwatch?.Start(); + } + + /// + public void Dispose() => this.Dispose(true); + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + private void Dispose(Boolean alsoManaged) { + if(this._isDisposed) { + return; + } + + if(alsoManaged) { + Add(this._identifier, this._stopwatch?.Elapsed ?? default); + this._stopwatch?.Stop(); + } + + this._stopwatch = null; + this._isDisposed = true; + } + } + } } diff --git a/Swan.Lite/Diagnostics/HighResolutionTimer.cs b/Swan.Lite/Diagnostics/HighResolutionTimer.cs index b419d09..ad96c40 100644 --- a/Swan.Lite/Diagnostics/HighResolutionTimer.cs +++ b/Swan.Lite/Diagnostics/HighResolutionTimer.cs @@ -1,32 +1,30 @@ -namespace Swan.Diagnostics -{ - using System; - using System.Diagnostics; - +using System; +using System.Diagnostics; + +namespace Swan.Diagnostics { + /// + /// Provides access to a high-resolution, time measuring device. + /// + /// + public class HighResolutionTimer : Stopwatch { /// - /// Provides access to a high-resolution, time measuring device. + /// Initializes a new instance of the class. /// - /// - 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 long ElapsedMicroseconds => (long)(ElapsedTicks * MicrosecondsPerTick); - } + /// 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.Lite/EnumHelper.cs b/Swan.Lite/EnumHelper.cs index cf49ec0..d867c14 100644 --- a/Swan.Lite/EnumHelper.cs +++ b/Swan.Lite/EnumHelper.cs @@ -1,171 +1,114 @@ using System; using System.Collections.Generic; using System.Linq; + using Swan.Collections; -namespace Swan -{ +namespace Swan { + /// + /// Provide Enumerations helpers with internal cache. + /// + public class EnumHelper + : SingletonBase>> { /// - /// Provide Enumerations helpers with internal cache. + /// Gets all the names and enumerators from a specific Enum type. /// - public class EnumHelper - : SingletonBase>> - { - /// - /// Gets all the names and enumerators from a specific Enum type. - /// - /// The type of the attribute to be retrieved. - /// A tuple of enumerator names and their value stored for the specified type. - public static IEnumerable> Retrieve() - where T : struct, IConvertible - { - return Instance.Retrieve(typeof(T), t => Enum.GetValues(t) - .Cast() - .Select(item => Tuple.Create(Enum.GetName(t, item), item))); - } - - /// - /// Gets the cached items with the enum item value. - /// - /// The type of enumeration. - /// if set to true [humanize]. - /// - /// A collection of Type/Tuple pairs - /// that represents items with the enum item value. - /// - public static IEnumerable> GetItemsWithValue(bool humanize = true) - where T : struct, IConvertible - { - return Retrieve() - .Select(x => Tuple.Create((int) x.Item2, humanize ? x.Item1.Humanize() : x.Item1)); - } - - /// - /// Gets the flag values. - /// - /// The type of the enum. - /// The value. - /// if set to true [ignore zero]. - /// - /// A list of values in the flag. - /// - public static IEnumerable GetFlagValues(int value, bool ignoreZero = false) - where TEnum : struct, IConvertible - { - return Retrieve() - .Select(x => (int) x.Item2) - .When(() => ignoreZero, q => q.Where(f => f != 0)) - .Where(x => (x & value) == x); - } - - /// - /// Gets the flag values. - /// - /// The type of the enum. - /// The value. - /// if set to true [ignore zero]. - /// - /// A list of values in the flag. - /// - public static IEnumerable GetFlagValues(long value, bool ignoreZero = false) - where TEnum : struct, IConvertible - { - return Retrieve() - .Select(x => (long) x.Item2) - .When(() => ignoreZero, q => q.Where(f => f != 0)) - .Where(x => (x & value) == x); - } - - /// - /// Gets the flag values. - /// - /// The type of the enum. - /// The value. - /// if set to true [ignore zero]. - /// - /// A list of values in the flag. - /// - public static IEnumerable GetFlagValues(byte value, bool ignoreZero = false) - where TEnum : struct, IConvertible - { - return Retrieve() - .Select(x => (byte) x.Item2) - .When(() => ignoreZero, q => q.Where(f => f != 0)) - .Where(x => (x & value) == x); - } - - /// - /// Gets the flag names. - /// - /// The type of the enum. - /// the value. - /// if set to true [ignore zero]. - /// if set to true [humanize]. - /// - /// A list of flag names. - /// - public static IEnumerable GetFlagNames(int value, bool ignoreZero = false, bool humanize = true) - where TEnum : struct, IConvertible - { - return Retrieve() - .When(() => ignoreZero, q => q.Where(f => (int) f.Item2 != 0)) - .Where(x => ((int) x.Item2 & value) == (int) x.Item2) - .Select(x => humanize ? x.Item1.Humanize() : x.Item1); - } - - /// - /// Gets the flag names. - /// - /// The type of the enum. - /// The value. - /// if set to true [ignore zero]. - /// if set to true [humanize]. - /// - /// A list of flag names. - /// - public static IEnumerable GetFlagNames(long value, bool ignoreZero = false, bool humanize = true) - where TEnum : struct, IConvertible - { - return Retrieve() - .When(() => ignoreZero, q => q.Where(f => (long) f.Item2 != 0)) - .Where(x => ((long) x.Item2 & value) == (long) x.Item2) - .Select(x => humanize ? x.Item1.Humanize() : x.Item1); - } - - /// - /// Gets the flag names. - /// - /// The type of the enum. - /// The value. - /// if set to true [ignore zero]. - /// if set to true [humanize]. - /// - /// A list of flag names. - /// - public static IEnumerable GetFlagNames(byte value, bool ignoreZero = false, bool humanize = true) - where TEnum : struct, IConvertible - { - return Retrieve() - .When(() => ignoreZero, q => q.Where(f => (byte) f.Item2 != 0)) - .Where(x => ((byte) x.Item2 & value) == (byte) x.Item2) - .Select(x => humanize ? x.Item1.Humanize() : x.Item1); - } - - /// - /// Gets the cached items with the enum item index. - /// - /// The type of enumeration. - /// if set to true [humanize]. - /// - /// A collection of Type/Tuple pairs that represents items with the enum item value. - /// - public static IEnumerable> GetItemsWithIndex(bool humanize = true) - where T : struct, IConvertible - { - var i = 0; - - return Retrieve() - .Select(x => Tuple.Create(i++, humanize ? x.Item1.Humanize() : x.Item1)); - } - } + /// The type of the attribute to be retrieved. + /// A tuple of enumerator names and their value stored for the specified type. + public static IEnumerable> Retrieve() where T : struct, IConvertible => Instance.Retrieve(typeof(T), t => Enum.GetValues(t).Cast().Select(item => Tuple.Create(Enum.GetName(t, item), item))); + + /// + /// Gets the cached items with the enum item value. + /// + /// The type of enumeration. + /// if set to true [humanize]. + /// + /// A collection of Type/Tuple pairs + /// that represents items with the enum item value. + /// + public static IEnumerable> GetItemsWithValue(Boolean humanize = true) where T : struct, IConvertible => Retrieve().Select(x => Tuple.Create((Int32)x.Item2, humanize ? x.Item1.Humanize() : x.Item1)); + + /// + /// Gets the flag values. + /// + /// The type of the enum. + /// The value. + /// if set to true [ignore zero]. + /// + /// A list of values in the flag. + /// + public static IEnumerable GetFlagValues(Int32 value, Boolean ignoreZero = false) where TEnum : struct, IConvertible => Retrieve().Select(x => (Int32)x.Item2).When(() => ignoreZero, q => q.Where(f => f != 0)).Where(x => (x & value) == x); + + /// + /// Gets the flag values. + /// + /// The type of the enum. + /// The value. + /// if set to true [ignore zero]. + /// + /// A list of values in the flag. + /// + public static IEnumerable GetFlagValues(Int64 value, Boolean ignoreZero = false) where TEnum : struct, IConvertible => Retrieve().Select(x => (Int64)x.Item2).When(() => ignoreZero, q => q.Where(f => f != 0)).Where(x => (x & value) == x); + + /// + /// Gets the flag values. + /// + /// The type of the enum. + /// The value. + /// if set to true [ignore zero]. + /// + /// A list of values in the flag. + /// + public static IEnumerable GetFlagValues(Byte value, Boolean ignoreZero = false) where TEnum : struct, IConvertible => Retrieve().Select(x => (Byte)x.Item2).When(() => ignoreZero, q => q.Where(f => f != 0)).Where(x => (x & value) == x); + + /// + /// Gets the flag names. + /// + /// The type of the enum. + /// the value. + /// if set to true [ignore zero]. + /// if set to true [humanize]. + /// + /// A list of flag names. + /// + public static IEnumerable GetFlagNames(Int32 value, Boolean ignoreZero = false, Boolean humanize = true) where TEnum : struct, IConvertible => Retrieve().When(() => ignoreZero, q => q.Where(f => (Int32)f.Item2 != 0)).Where(x => ((Int32)x.Item2 & value) == (Int32)x.Item2).Select(x => humanize ? x.Item1.Humanize() : x.Item1); + + /// + /// Gets the flag names. + /// + /// The type of the enum. + /// The value. + /// if set to true [ignore zero]. + /// if set to true [humanize]. + /// + /// A list of flag names. + /// + public static IEnumerable GetFlagNames(Int64 value, Boolean ignoreZero = false, Boolean humanize = true) where TEnum : struct, IConvertible => Retrieve().When(() => ignoreZero, q => q.Where(f => (Int64)f.Item2 != 0)).Where(x => ((Int64)x.Item2 & value) == (Int64)x.Item2).Select(x => humanize ? x.Item1.Humanize() : x.Item1); + + /// + /// Gets the flag names. + /// + /// The type of the enum. + /// The value. + /// if set to true [ignore zero]. + /// if set to true [humanize]. + /// + /// A list of flag names. + /// + public static IEnumerable GetFlagNames(Byte value, Boolean ignoreZero = false, Boolean humanize = true) where TEnum : struct, IConvertible => Retrieve().When(() => ignoreZero, q => q.Where(f => (Byte)f.Item2 != 0)).Where(x => ((Byte)x.Item2 & value) == (Byte)x.Item2).Select(x => humanize ? x.Item1.Humanize() : x.Item1); + + /// + /// Gets the cached items with the enum item index. + /// + /// The type of enumeration. + /// if set to true [humanize]. + /// + /// A collection of Type/Tuple pairs that represents items with the enum item value. + /// + public static IEnumerable> GetItemsWithIndex(Boolean humanize = true) where T : struct, IConvertible { + Int32 i = 0; + + return Retrieve().Select(x => Tuple.Create(i++, humanize ? x.Item1.Humanize() : x.Item1)); + } + } } \ No newline at end of file diff --git a/Swan.Lite/Enums.cs b/Swan.Lite/Enums.cs index 38d2b42..a586509 100644 --- a/Swan.Lite/Enums.cs +++ b/Swan.Lite/Enums.cs @@ -1,65 +1,61 @@ -namespace Swan -{ +namespace Swan { + /// + /// Enumeration of Operating Systems. + /// + public enum OperatingSystem { /// - /// Enumeration of Operating Systems. + /// Unknown OS /// - public enum OperatingSystem - { - /// - /// Unknown OS - /// - Unknown, - - /// - /// Windows - /// - Windows, - - /// - /// UNIX/Linux - /// - Unix, - - /// - /// macOS (OSX) - /// - Osx, - } - + Unknown, + /// - /// Defines Endianness, big or little. + /// Windows /// - 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, - } - + Windows, + /// - /// Enumerates the JSON serializer cases to use: None (keeps the same case), PascalCase, or camelCase. + /// UNIX/Linux /// - public enum JsonSerializerCase - { - /// - /// The none - /// - None, - - /// - /// The pascal case (eg. PascalCase) - /// - PascalCase, - - /// - /// The camel case (eg. camelCase) - /// - CamelCase, - } + 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.Lite/Extensions.ByteArrays.cs b/Swan.Lite/Extensions.ByteArrays.cs index 3888912..5abc946 100644 --- a/Swan.Lite/Extensions.ByteArrays.cs +++ b/Swan.Lite/Extensions.ByteArrays.cs @@ -7,498 +7,492 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -namespace Swan -{ +namespace Swan { + /// + /// Provides various extension methods for byte arrays and streams. + /// + public static class ByteArrayExtensions { /// - /// Provides various extension methods for byte arrays and streams. + /// Converts an array of bytes to its lower-case, hexadecimal representation. /// - 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, bool 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, bool 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) - { - var mask = ~(0xff << length); - var 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, int offset, params byte[] sequence) - { - if (@this == null) - throw new ArgumentNullException(nameof(@this)); - - if (sequence == null) - throw new ArgumentNullException(nameof(sequence)); - - var seqOffset = offset.Clamp(0, @this.Length - 1); - - var result = new List(); - - while (seqOffset < @this.Length) - { - var separatorStartIndex = @this.GetIndexOf(sequence, seqOffset); - - if (separatorStartIndex >= 0) - { - var item = new byte[separatorStartIndex - seqOffset + sequence.Length]; - Array.Copy(@this, seqOffset, item, 0, item.Length); - result.Add(item); - seqOffset += item.Length; - } - else - { - var 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)); - - var 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(); - - var 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(); - - var 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) - { - var 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 bool EndsWith(this byte[] buffer, params byte[] sequence) - { - if (buffer == null) - throw new ArgumentNullException(nameof(buffer)); - - var 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 bool 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 bool 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 bool 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 int GetIndexOf(this byte[] buffer, byte[] sequence, int 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; - - var seqOffset = offset < 0 ? 0 : offset; - - var matchedCount = 0; - for (var 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 (var 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, long length, int bufferLength, CancellationToken cancellationToken = default) - { - if (stream == null) - throw new ArgumentNullException(nameof(stream)); - - using (var dest = new MemoryStream()) - { - try - { - var buff = new byte[bufferLength]; - while (length > 0) - { - if (length < bufferLength) - bufferLength = (int)length; - - var 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, int length, CancellationToken cancellationToken = default) - { - if (stream == null) - throw new ArgumentNullException(nameof(stream)); - - var buff = new byte[length]; - var offset = 0; - try - { - while (length > 0) - { - var 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, bool addPrefix, string format) - { - if (bytes == null) - throw new ArgumentNullException(nameof(bytes)); - - var sb = new StringBuilder(bytes.Length * 2); - - foreach (var item in bytes) - sb.Append(item.ToString(format, CultureInfo.InvariantCulture)); - - return $"{(addPrefix ? "0x" : string.Empty)}{sb}"; - } - } + /// 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.Lite/Extensions.ComponentCollections.cs b/Swan.Lite/Extensions.ComponentCollections.cs index f14b782..8dfdd62 100644 --- a/Swan.Lite/Extensions.ComponentCollections.cs +++ b/Swan.Lite/Extensions.ComponentCollections.cs @@ -1,21 +1,20 @@ using System; + using Swan.Collections; -namespace Swan -{ +namespace Swan { + /// + /// Provides extension methods for types implementing . + /// + public static class ComponentCollectionExtensions { /// - /// Provides extension methods for types implementing . + /// Adds the specified component to a collection, without giving it a name. /// - public static class ComponentCollectionExtensions - { - /// - /// Adds the specified component to a collection, without giving it a name. - /// - /// The type of components in the collection. - /// The on which this method is called. - /// The component to add. - /// is . - /// - public static void Add(this IComponentCollection @this, T component) => @this.Add(null, component); - } + /// The type of components in the collection. + /// The on which this method is called. + /// The component to add. + /// is . + /// + public static void Add(this IComponentCollection @this, T component) => @this.Add(null, component); + } } \ No newline at end of file diff --git a/Swan.Lite/Extensions.Dates.cs b/Swan.Lite/Extensions.Dates.cs index 811df20..7a3b508 100644 --- a/Swan.Lite/Extensions.Dates.cs +++ b/Swan.Lite/Extensions.Dates.cs @@ -3,232 +3,224 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -namespace Swan -{ +namespace Swan { + /// + /// Provides extension methods for . + /// + public static class DateExtensions { + private static readonly Dictionary DateRanges = new Dictionary() { + { "minute", 59}, + { "hour", 23}, + { "dayOfMonth", 31}, + { "month", 12}, + { "dayOfWeek", 6}, + }; + /// - /// Provides extension methods for . + /// Converts the date to a YYYY-MM-DD string. /// - public static class DateExtensions - { - private static readonly Dictionary DateRanges = new Dictionary() - { - { "minute", 59}, - { "hour", 23}, - { "dayOfMonth", 31}, - { "month", 12}, - { "dayOfWeek", 6}, - }; - - /// - /// Converts the date to a YYYY-MM-DD string. - /// - /// The on which this method is called. - /// The concatenation of date.Year, date.Month and date.Day. - public static string ToSortableDate(this DateTime @this) - => $"{@this.Year:0000}-{@this.Month:00}-{@this.Day:00}"; - - /// - /// Converts the date to a YYYY-MM-DD HH:II:SS string. - /// - /// The on which this method is called. - /// The concatenation of date.Year, date.Month, date.Day, date.Hour, date.Minute and date.Second. - public static string ToSortableDateTime(this DateTime @this) - => $"{@this.Year:0000}-{@this.Month:00}-{@this.Day:00} {@this.Hour:00}:{@this.Minute:00}:{@this.Second:00}"; - - /// - /// Parses a YYYY-MM-DD and optionally it time part, HH:II:SS into a DateTime. - /// - /// The sortable date. - /// - /// A new instance of the DateTime structure to - /// the specified year, month, day, hour, minute and second. - /// - /// sortableDate. - /// - /// Represents errors that occur during application execution. - /// - /// - /// Unable to parse sortable date and time. - sortableDate. - /// - public static DateTime ToDateTime(this string @this) - { - if (string.IsNullOrWhiteSpace(@this)) - throw new ArgumentNullException(nameof(@this)); - - var hour = 0; - var minute = 0; - var second = 0; - - var dateTimeParts = @this.Split(' '); - - try - { - if (dateTimeParts.Length != 1 && dateTimeParts.Length != 2) - throw new Exception(); - - var dateParts = dateTimeParts[0].Split('-'); - if (dateParts.Length != 3) throw new Exception(); - - var year = int.Parse(dateParts[0]); - var month = int.Parse(dateParts[1]); - var day = int.Parse(dateParts[2]); - - if (dateTimeParts.Length > 1) - { - var timeParts = dateTimeParts[1].Split(':'); - if (timeParts.Length != 3) throw new Exception(); - - hour = int.Parse(timeParts[0]); - minute = int.Parse(timeParts[1]); - second = int.Parse(timeParts[2]); - } - - return new DateTime(year, month, day, hour, minute, second); - } - catch (Exception) - { - throw new ArgumentException("Unable to parse sortable date and time.", nameof(@this)); - } - } - - /// - /// Creates a date range. - /// - /// The start date. - /// The end date. - /// - /// A sequence of integral numbers within a specified date's range. - /// - public static IEnumerable DateRange(this DateTime startDate, DateTime endDate) - => Enumerable.Range(0, (endDate - startDate).Days + 1).Select(d => startDate.AddDays(d)); - - /// - /// Rounds up a date to match a timespan. - /// - /// The datetime. - /// The timespan to match. - /// - /// A new instance of the DateTime structure to the specified datetime and timespan ticks. - /// - public static DateTime RoundUp(this DateTime date, TimeSpan timeSpan) - => new DateTime(((date.Ticks + timeSpan.Ticks - 1) / timeSpan.Ticks) * timeSpan.Ticks); - - /// - /// Get this datetime as a Unix epoch timestamp (seconds since Jan 1, 1970, midnight UTC). - /// - /// The on which this method is called. - /// Seconds since Unix epoch. - public static long ToUnixEpochDate(this DateTime @this) => new DateTimeOffset(@this).ToUniversalTime().ToUnixTimeSeconds(); - - /// - /// Compares a Date to another and returns a DateTimeSpan. - /// - /// The date start. - /// The date end. - /// A DateTimeSpan with the Years, Months, Days, Hours, Minutes, Seconds and Milliseconds between the dates. - public static DateTimeSpan GetDateTimeSpan(this DateTime dateStart, DateTime dateEnd) - => DateTimeSpan.CompareDates(dateStart, dateEnd); - - /// - /// Compare the Date elements(Months, Days, Hours, Minutes). - /// - /// The on which this method is called. - /// The minute (0-59). - /// The hour. (0-23). - /// The day of month. (1-31). - /// The month. (1-12). - /// The day of week. (0-6)(Sunday = 0). - /// Returns true if Months, Days, Hours and Minutes match, otherwise false. - public static bool AsCronCanRun(this DateTime @this, int? minute = null, int? hour = null, int? dayOfMonth = null, int? month = null, int? dayOfWeek = null) - { - var results = new List - { - GetElementParts(minute, @this.Minute), - GetElementParts(hour, @this.Hour), - GetElementParts(dayOfMonth, @this.Day), - GetElementParts(month, @this.Month), - GetElementParts(dayOfWeek, (int) @this.DayOfWeek), - }; - - return results.Any(x => x != false); - } - - /// - /// Compare the Date elements(Months, Days, Hours, Minutes). - /// - /// The on which this method is called. - /// The minute (0-59). - /// The hour. (0-23). - /// The day of month. (1-31). - /// The month. (1-12). - /// The day of week. (0-6)(Sunday = 0). - /// Returns true if Months, Days, Hours and Minutes match, otherwise false. - public static bool AsCronCanRun(this DateTime @this, string minute = "*", string hour = "*", string dayOfMonth = "*", string month = "*", string dayOfWeek = "*") - { - var results = new List - { - GetElementParts(minute, nameof(minute), @this.Minute), - GetElementParts(hour, nameof(hour), @this.Hour), - GetElementParts(dayOfMonth, nameof(dayOfMonth), @this.Day), - GetElementParts(month, nameof(month), @this.Month), - GetElementParts(dayOfWeek, nameof(dayOfWeek), (int) @this.DayOfWeek), - }; - - return results.Any(x => x != false); - } - - /// - /// Converts a to the RFC1123 format. - /// - /// The on which this method is called. - /// The string representation of according to RFC1123. - /// - /// If is not a UTC date / time, its UTC equivalent is converted, leaving unchanged. - /// - public static string ToRfc1123String(this DateTime @this) - => @this.ToUniversalTime().ToString("R", CultureInfo.InvariantCulture); - - private static bool? GetElementParts(int? status, int value) => status.HasValue ? status.Value == value : (bool?) null; - - private static bool? GetElementParts(string parts, string type, int value) - { - if (string.IsNullOrWhiteSpace(parts) || parts == "*") - return null; - - if (parts.Contains(",")) - { - return parts.Split(',').Select(int.Parse).Contains(value); - } - - var stop = DateRanges[type]; - - if (parts.Contains("/")) - { - var multiple = int.Parse(parts.Split('/').Last()); - var start = type == "dayOfMonth" || type == "month" ? 1 : 0; - - for (var i = start; i <= stop; i += multiple) - if (i == value) return true; - - return false; - } - - if (parts.Contains("-")) - { - var range = parts.Split('-'); - var start = int.Parse(range.First()); - stop = Math.Max(stop, int.Parse(range.Last())); - - if ((type == "dayOfMonth" || type == "month") && start == 0) - start = 1; - - for (var i = start; i <= stop; i++) - if (i == value) return true; - - return false; - } - - return int.Parse(parts) == value; - } - } + /// The on which this method is called. + /// The concatenation of date.Year, date.Month and date.Day. + public static String ToSortableDate(this DateTime @this) => $"{@this.Year:0000}-{@this.Month:00}-{@this.Day:00}"; + + /// + /// Converts the date to a YYYY-MM-DD HH:II:SS string. + /// + /// The on which this method is called. + /// The concatenation of date.Year, date.Month, date.Day, date.Hour, date.Minute and date.Second. + public static String ToSortableDateTime(this DateTime @this) => $"{@this.Year:0000}-{@this.Month:00}-{@this.Day:00} {@this.Hour:00}:{@this.Minute:00}:{@this.Second:00}"; + + /// + /// Parses a YYYY-MM-DD and optionally it time part, HH:II:SS into a DateTime. + /// + /// The sortable date. + /// + /// A new instance of the DateTime structure to + /// the specified year, month, day, hour, minute and second. + /// + /// sortableDate. + /// + /// Represents errors that occur during application execution. + /// + /// + /// Unable to parse sortable date and time. - sortableDate. + /// + public static DateTime ToDateTime(this String @this) { + if(String.IsNullOrWhiteSpace(@this)) { + throw new ArgumentNullException(nameof(@this)); + } + + Int32 hour = 0; + Int32 minute = 0; + Int32 second = 0; + + String[] dateTimeParts = @this.Split(' '); + + try { + if(dateTimeParts.Length != 1 && dateTimeParts.Length != 2) { + throw new Exception(); + } + + String[] dateParts = dateTimeParts[0].Split('-'); + if(dateParts.Length != 3) { + throw new Exception(); + } + + Int32 year = Int32.Parse(dateParts[0]); + Int32 month = Int32.Parse(dateParts[1]); + Int32 day = Int32.Parse(dateParts[2]); + + if(dateTimeParts.Length > 1) { + String[] timeParts = dateTimeParts[1].Split(':'); + if(timeParts.Length != 3) { + throw new Exception(); + } + + hour = Int32.Parse(timeParts[0]); + minute = Int32.Parse(timeParts[1]); + second = Int32.Parse(timeParts[2]); + } + + return new DateTime(year, month, day, hour, minute, second); + } catch(Exception) { + throw new ArgumentException("Unable to parse sortable date and time.", nameof(@this)); + } + } + + /// + /// Creates a date range. + /// + /// The start date. + /// The end date. + /// + /// A sequence of integral numbers within a specified date's range. + /// + public static IEnumerable DateRange(this DateTime startDate, DateTime endDate) => Enumerable.Range(0, (endDate - startDate).Days + 1).Select(d => startDate.AddDays(d)); + + /// + /// Rounds up a date to match a timespan. + /// + /// The datetime. + /// The timespan to match. + /// + /// A new instance of the DateTime structure to the specified datetime and timespan ticks. + /// + public static DateTime RoundUp(this DateTime date, TimeSpan timeSpan) => new DateTime(((date.Ticks + timeSpan.Ticks - 1) / timeSpan.Ticks) * timeSpan.Ticks); + + /// + /// Get this datetime as a Unix epoch timestamp (seconds since Jan 1, 1970, midnight UTC). + /// + /// The on which this method is called. + /// Seconds since Unix epoch. + public static Int64 ToUnixEpochDate(this DateTime @this) => new DateTimeOffset(@this).ToUniversalTime().ToUnixTimeSeconds(); + + /// + /// Compares a Date to another and returns a DateTimeSpan. + /// + /// The date start. + /// The date end. + /// A DateTimeSpan with the Years, Months, Days, Hours, Minutes, Seconds and Milliseconds between the dates. + public static DateTimeSpan GetDateTimeSpan(this DateTime dateStart, DateTime dateEnd) => DateTimeSpan.CompareDates(dateStart, dateEnd); + + /// + /// Compare the Date elements(Months, Days, Hours, Minutes). + /// + /// The on which this method is called. + /// The minute (0-59). + /// The hour. (0-23). + /// The day of month. (1-31). + /// The month. (1-12). + /// The day of week. (0-6)(Sunday = 0). + /// Returns true if Months, Days, Hours and Minutes match, otherwise false. + public static Boolean AsCronCanRun(this DateTime @this, Int32? minute = null, Int32? hour = null, Int32? dayOfMonth = null, Int32? month = null, Int32? dayOfWeek = null) { + List results = new List { + GetElementParts(minute, @this.Minute), + GetElementParts(hour, @this.Hour), + GetElementParts(dayOfMonth, @this.Day), + GetElementParts(month, @this.Month), + GetElementParts(dayOfWeek, (Int32) @this.DayOfWeek), + }; + + return results.Any(x => x != false); + } + + /// + /// Compare the Date elements(Months, Days, Hours, Minutes). + /// + /// The on which this method is called. + /// The minute (0-59). + /// The hour. (0-23). + /// The day of month. (1-31). + /// The month. (1-12). + /// The day of week. (0-6)(Sunday = 0). + /// Returns true if Months, Days, Hours and Minutes match, otherwise false. + public static Boolean AsCronCanRun(this DateTime @this, String minute = "*", String hour = "*", String dayOfMonth = "*", String month = "*", String dayOfWeek = "*") { + List results = new List { + GetElementParts(minute, nameof(minute), @this.Minute), + GetElementParts(hour, nameof(hour), @this.Hour), + GetElementParts(dayOfMonth, nameof(dayOfMonth), @this.Day), + GetElementParts(month, nameof(month), @this.Month), + GetElementParts(dayOfWeek, nameof(dayOfWeek), (Int32) @this.DayOfWeek), + }; + + return results.Any(x => x != false); + } + + /// + /// Converts a to the RFC1123 format. + /// + /// The on which this method is called. + /// The string representation of according to RFC1123. + /// + /// If is not a UTC date / time, its UTC equivalent is converted, leaving unchanged. + /// + public static String ToRfc1123String(this DateTime @this) => @this.ToUniversalTime().ToString("R", CultureInfo.InvariantCulture); + + private static Boolean? GetElementParts(Int32? status, Int32 value) => status.HasValue ? status.Value == value : (Boolean?)null; + + private static Boolean? GetElementParts(String parts, String type, Int32 value) { + if(String.IsNullOrWhiteSpace(parts) || parts == "*") { + return null; + } + + if(parts.Contains(",")) { + return parts.Split(',').Select(Int32.Parse).Contains(value); + } + + Int32 stop = DateRanges[type]; + + if(parts.Contains("/")) { + Int32 multiple = Int32.Parse(parts.Split('/').Last()); + Int32 start = type == "dayOfMonth" || type == "month" ? 1 : 0; + + for(Int32 i = start; i <= stop; i += multiple) { + if(i == value) { + return true; + } + } + + return false; + } + + if(parts.Contains("-")) { + String[] range = parts.Split('-'); + Int32 start = Int32.Parse(range.First()); + stop = Math.Max(stop, Int32.Parse(range.Last())); + + if((type == "dayOfMonth" || type == "month") && start == 0) { + start = 1; + } + + for(Int32 i = start; i <= stop; i++) { + if(i == value) { + return true; + } + } + + return false; + } + + return Int32.Parse(parts) == value; + } + } } diff --git a/Swan.Lite/Extensions.Dictionaries.cs b/Swan.Lite/Extensions.Dictionaries.cs index bb830cf..273e9e8 100644 --- a/Swan.Lite/Extensions.Dictionaries.cs +++ b/Swan.Lite/Extensions.Dictionaries.cs @@ -1,86 +1,86 @@ using System; using System.Collections.Generic; -namespace Swan -{ +namespace Swan { + /// + /// Extension methods. + /// + public static partial class Extensions { /// - /// Extension methods. + /// Gets the value if exists or default. /// - 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)) - { - var 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 (var kvp in dict) - { - itemAction(kvp.Key, kvp.Value); - } - } - } + /// 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.Lite/Extensions.Exceptions.cs b/Swan.Lite/Extensions.Exceptions.cs index dc75679..d324afc 100644 --- a/Swan.Lite/Extensions.Exceptions.cs +++ b/Swan.Lite/Extensions.Exceptions.cs @@ -2,49 +2,31 @@ using System.Linq; using System.Threading; -namespace Swan -{ +namespace Swan { + /// + /// Provides extension methods for . + /// + public static class ExceptionExtensions { /// - /// Provides extension methods for . + /// Returns a value that tells whether an is of a type that + /// we better not catch and ignore. /// - public static class ExceptionExtensions - { - /// - /// Returns a value that tells whether an is of a type that - /// we better not catch and ignore. - /// - /// The exception being thrown. - /// if is a critical exception; - /// otherwise, . - public static bool IsCriticalException(this Exception @this) - => @this.IsCriticalExceptionCore() - || (@this.InnerException?.IsCriticalException() ?? false) - || (@this is AggregateException aggregateException && aggregateException.InnerExceptions.Any(e => e.IsCriticalException())); - - /// - /// Returns a value that tells whether an is of a type that - /// will likely cause application failure. - /// - /// The exception being thrown. - /// if is a fatal exception; - /// otherwise, . - public static bool IsFatalException(this Exception @this) - => @this.IsFatalExceptionCore() - || (@this.InnerException?.IsFatalException() ?? false) - || (@this is AggregateException aggregateException && aggregateException.InnerExceptions.Any(e => e.IsFatalException())); - - private static bool IsCriticalExceptionCore(this Exception @this) - => IsFatalExceptionCore(@this) - || @this is AppDomainUnloadedException - || @this is BadImageFormatException - || @this is CannotUnloadAppDomainException - || @this is InvalidProgramException - || @this is NullReferenceException; - - private static bool IsFatalExceptionCore(this Exception @this) - => @this is StackOverflowException - || @this is OutOfMemoryException - || @this is ThreadAbortException - || @this is AccessViolationException; - } + /// The exception being thrown. + /// if is a critical exception; + /// otherwise, . + public static Boolean IsCriticalException(this Exception @this) => @this.IsCriticalExceptionCore() || (@this.InnerException?.IsCriticalException() ?? false) || @this is AggregateException aggregateException && aggregateException.InnerExceptions.Any(e => e.IsCriticalException()); + + /// + /// Returns a value that tells whether an is of a type that + /// will likely cause application failure. + /// + /// The exception being thrown. + /// if is a fatal exception; + /// otherwise, . + public static Boolean IsFatalException(this Exception @this) => @this.IsFatalExceptionCore() || (@this.InnerException?.IsFatalException() ?? false) || @this is AggregateException aggregateException && aggregateException.InnerExceptions.Any(e => e.IsFatalException()); + + private static Boolean IsCriticalExceptionCore(this Exception @this) => IsFatalExceptionCore(@this) || @this is AppDomainUnloadedException || @this is BadImageFormatException || @this is CannotUnloadAppDomainException || @this is InvalidProgramException || @this is NullReferenceException; + + private static Boolean IsFatalExceptionCore(this Exception @this) => @this is StackOverflowException || @this is OutOfMemoryException || @this is ThreadAbortException || @this is AccessViolationException; + } } \ No newline at end of file diff --git a/Swan.Lite/Extensions.Functional.cs b/Swan.Lite/Extensions.Functional.cs index dcbcbf9..e6d8491 100644 --- a/Swan.Lite/Extensions.Functional.cs +++ b/Swan.Lite/Extensions.Functional.cs @@ -2,178 +2,172 @@ using System.Collections.Generic; using System.Linq; -namespace Swan -{ +namespace Swan { + /// + /// Functional programming extension methods. + /// + public static class FunctionalExtensions { /// - /// Functional programming extension methods. + /// Whens the specified condition. /// - 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, - bool 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; - } - } + /// 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.Lite/Extensions.Reflection.cs b/Swan.Lite/Extensions.Reflection.cs index 93ea372..8953aae 100644 --- a/Swan.Lite/Extensions.Reflection.cs +++ b/Swan.Lite/Extensions.Reflection.cs @@ -1,455 +1,411 @@ -using System; +#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 -{ +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 + /// - /// Provides various extension methods for Reflection and Types. + /// Gets all types within an assembly in a safe manner. /// - 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 bool 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)); - - var 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 bool 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 bool TryParseBasicType(this Type type, object value, out object? result) - { - if (type == null) - throw new ArgumentNullException(nameof(type)); - - if (type != typeof(bool)) - 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 bool 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 bool TrySetBasicType(this PropertyInfo propertyInfo, object value, object target) - { - if (propertyInfo == null) - throw new ArgumentNullException(nameof(propertyInfo)); - - try - { - if (propertyInfo.PropertyType.TryParseBasicType(value, out var 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 bool TrySetArrayBasicType(this Type type, object value, Array target, int 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 var 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 bool TrySetArray(this PropertyInfo propertyInfo, IEnumerable? value, object obj) - { - if (propertyInfo == null) - throw new ArgumentNullException(nameof(propertyInfo)); - - var elementType = propertyInfo.PropertyType.GetElementType(); - - if (elementType == null || value == null) - return false; - - var targetArray = Array.CreateInstance(elementType, value.Count()); - - var i = 0; - - foreach (var sourceElement in value) - { - var 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 - { - var value = propertyInfo.GetValue(target); - var attr = AttributeCache.DefaultCache.Value.RetrieveOne(propertyInfo); - - if (attr == null) return value?.ToString() ?? string.Empty; - - var 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, bool nonPublic = false) - { - var 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, bool nonPublic = false) - { - var 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 bool 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)); - - var 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 bool ToBoolean(this object value) => value.ToStringInvariant().ToBoolean(); - - private static string ConvertObjectAndFormat(Type propertyType, object value, string format) - { - if (propertyType == typeof(DateTime) || propertyType == typeof(DateTime?)) - return Convert.ToDateTime(value, CultureInfo.InvariantCulture).ToString(format); - if (propertyType == typeof(int) || propertyType == typeof(int?)) - return Convert.ToInt32(value, CultureInfo.InvariantCulture).ToString(format); - if (propertyType == typeof(decimal) || propertyType == typeof(decimal?)) - return Convert.ToDecimal(value, CultureInfo.InvariantCulture).ToString(format); - if (propertyType == typeof(double) || propertyType == typeof(double?)) - return Convert.ToDouble(value, CultureInfo.InvariantCulture).ToString(format); - if (propertyType == typeof(byte) || propertyType == typeof(byte?)) - return Convert.ToByte(value, CultureInfo.InvariantCulture).ToString(format); - - return value?.ToString() ?? string.Empty; - } - } + /// 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.Lite/Extensions.Strings.cs b/Swan.Lite/Extensions.Strings.cs index a927034..e8bf398 100644 --- a/Swan.Lite/Extensions.Strings.cs +++ b/Swan.Lite/Extensions.Strings.cs @@ -1,405 +1,364 @@ -using System; +#nullable enable +using System; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; + using Swan.Formatters; -namespace Swan -{ +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 + /// - /// String related extension methods. + /// Returns a string that represents the given item + /// It tries to use InvariantCulture if the ToString(IFormatProvider) + /// overload exists. /// - 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 => - { - var x = m.ToString(); - return x[0] + " " + x.Substring(1, x.Length - 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; - - var itemType = @this.GetType(); - - if (itemType == typeof(string)) - return @this as string ?? string.Empty; - - return 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, bool 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 - { - var jsonText = Json.Serialize(@this, false, "$type"); - var 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, int startIndex, int endIndex) - { - if (@this == null) - return string.Empty; - - var 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, int startIndex, int length) - { - if (@this == null) - return string.Empty; - - var start = startIndex.Clamp(0, @this.Length - 1); - var 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; - - var 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 bool 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) => + /// 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(), - bool 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, int spaces = 4) - { - if (value == null) value = string.Empty; - if (spaces <= 0) return value; - - var lines = value.ToLines(); - var builder = new StringBuilder(); - var indentStr = new string(' ', spaces); - - foreach (var 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, int charIndex) - { - if (value == null) - return Tuple.Create(0, 0); - - var index = charIndex.Clamp(0, value.Length - 1); - - var lineIndex = 0; - var colNumber = 0; - - for (var 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 long bytes) => ((ulong)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 ulong bytes) - { - int 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, int 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, int maximumLength, string omission) - { - if (value == null) - return null; - - return 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 bool Contains(this string value, params char[] chars) => - chars?.Length == 0 || (!string.IsNullOrEmpty(value) && 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 int 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; - } + { + 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.Lite/Extensions.Tasks.cs b/Swan.Lite/Extensions.Tasks.cs index f43d34b..5329d8a 100644 --- a/Swan.Lite/Extensions.Tasks.cs +++ b/Swan.Lite/Extensions.Tasks.cs @@ -1,63 +1,59 @@ using System; using System.Threading.Tasks; -namespace Swan -{ +namespace Swan { + /// + /// Provides extension methods for and . + /// + public static class TaskExtensions { /// - /// Provides extension methods for and . + /// Suspends execution until the specified is completed. + /// This method operates similarly to the C# operator, + /// but is meant to be called from a non- method. /// - public static class TaskExtensions - { - /// - /// Suspends execution until the specified is completed. - /// This method operates similarly to the C# operator, - /// but is meant to be called from a non- method. - /// - /// The on which this method is called. - /// is . - public static void Await(this Task @this) => @this.GetAwaiter().GetResult(); - - /// - /// Suspends execution until the specified is completed - /// and returns its result. - /// This method operates similarly to the C# operator, - /// but is meant to be called from a non- method. - /// - /// The type of the task's result. - /// The on which this method is called. - /// The result of . - /// is . - public static TResult Await(this Task @this) => @this.GetAwaiter().GetResult(); - - /// - /// Suspends execution until the specified is completed. - /// This method operates similarly to the C# operator, - /// but is meant to be called from a non- method. - /// - /// The on which this method is called. - /// If set to , - /// attempts to marshal the continuation back to the original context captured. - /// This parameter has the same effect as calling the - /// method. - /// is . - public static void Await(this Task @this, bool continueOnCapturedContext) - => @this.ConfigureAwait(continueOnCapturedContext).GetAwaiter().GetResult(); - - /// - /// Suspends execution until the specified is completed - /// and returns its result. - /// This method operates similarly to the C# operator, - /// but is meant to be called from a non- method. - /// - /// The type of the task's result. - /// The on which this method is called. - /// If set to , - /// attempts to marshal the continuation back to the original context captured. - /// This parameter has the same effect as calling the - /// method. - /// The result of . - /// is . - public static TResult Await(this Task @this, bool continueOnCapturedContext) - => @this.ConfigureAwait(continueOnCapturedContext).GetAwaiter().GetResult(); - } + /// The on which this method is called. + /// is . + public static void Await(this Task @this) => @this.GetAwaiter().GetResult(); + + /// + /// Suspends execution until the specified is completed + /// and returns its result. + /// This method operates similarly to the C# operator, + /// but is meant to be called from a non- method. + /// + /// The type of the task's result. + /// The on which this method is called. + /// The result of . + /// is . + public static TResult Await(this Task @this) => @this.GetAwaiter().GetResult(); + + /// + /// Suspends execution until the specified is completed. + /// This method operates similarly to the C# operator, + /// but is meant to be called from a non- method. + /// + /// The on which this method is called. + /// If set to , + /// attempts to marshal the continuation back to the original context captured. + /// This parameter has the same effect as calling the + /// method. + /// is . + public static void Await(this Task @this, Boolean continueOnCapturedContext) => @this.ConfigureAwait(continueOnCapturedContext).GetAwaiter().GetResult(); + + /// + /// Suspends execution until the specified is completed + /// and returns its result. + /// This method operates similarly to the C# operator, + /// but is meant to be called from a non- method. + /// + /// The type of the task's result. + /// The on which this method is called. + /// If set to , + /// attempts to marshal the continuation back to the original context captured. + /// This parameter has the same effect as calling the + /// method. + /// The result of . + /// is . + public static TResult Await(this Task @this, Boolean continueOnCapturedContext) => @this.ConfigureAwait(continueOnCapturedContext).GetAwaiter().GetResult(); + } } diff --git a/Swan.Lite/Extensions.ValueTypes.cs b/Swan.Lite/Extensions.ValueTypes.cs index 6bd4066..c509011 100644 --- a/Swan.Lite/Extensions.ValueTypes.cs +++ b/Swan.Lite/Extensions.ValueTypes.cs @@ -1,165 +1,135 @@ using System; using System.Reflection; using System.Runtime.InteropServices; + using Swan.Reflection; -namespace Swan -{ +namespace Swan { + /// + /// Provides various extension methods for value types and structs. + /// + public static class ValueTypeExtensions { /// - /// Provides various extension methods for value types and structs. + /// Clamps the specified value between the minimum and the maximum. /// - 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 - { - if (@this.CompareTo(min) < 0) return min; - - return @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 int Clamp(this int @this, int min, int 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 bool IsBetween(this T @this, T min, T max) - where T : struct, IComparable - { - return @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 - { - return @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, int offset, int length) - where T : struct - { - if (@this == null) - throw new ArgumentNullException(nameof(@this)); - - var buffer = new byte[length]; - Array.Copy(@this, offset, buffer, 0, buffer.Length); - var 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 - { - var data = new byte[Marshal.SizeOf(@this)]; - var 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 uint SwapEndianness(this ulong @this) - => (uint)(((@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)); - - var fields = typeof(T).GetTypeInfo() - .GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - - var endian = AttributeCache.DefaultCache.Value.RetrieveOne(); - - foreach (var field in fields) - { - if (endian == null && !field.IsDefined(typeof(StructEndiannessAttribute), false)) - continue; - - var offset = Marshal.OffsetOf(field.Name).ToInt32(); - var length = Marshal.SizeOf(field.FieldType); - - endian = 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; - } - } + /// 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.Lite/Extensions.cs b/Swan.Lite/Extensions.cs index 9d6837f..0f67daa 100644 --- a/Swan.Lite/Extensions.cs +++ b/Swan.Lite/Extensions.cs @@ -1,276 +1,230 @@ -using System; +#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 -{ +namespace Swan { + /// + /// Extension methods. + /// + public static partial class Extensions { /// - /// Extension methods. + /// 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. /// - 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 int 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 int 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)); - - var 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)); - - var 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 int 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)); - - var 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, - int 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, - int retryCount = 3) - { - if (action == null) - throw new ArgumentNullException(nameof(action)); - - if (retryInterval == default) - retryInterval = TimeSpan.FromSeconds(1); - - var exceptions = new List(); - - for (var 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)); - - var collection = PropertyTypeCache.DefaultCache.Value - .RetrieveAllProperties(@this.GetType(), true); - - var 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, - bool 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: - var elementType = targetType.GetElementType(); - - if (elementType != null) - target = Array.CreateInstance(elementType, sourceObjectList.Count); - break; - default: - var 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 - { - var 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) - }; - } + /// 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.Lite/Formatters/CsvReader.cs b/Swan.Lite/Formatters/CsvReader.cs index fa8fa39..3b207e4 100644 --- a/Swan.Lite/Formatters/CsvReader.cs +++ b/Swan.Lite/Formatters/CsvReader.cs @@ -5,644 +5,584 @@ using System.Linq; using System.Text; using Swan.Reflection; -namespace Swan.Formatters -{ +namespace Swan.Formatters { + /// + /// Represents a reader designed for CSV text. + /// It is capable of deserializing objects from individual lines of CSV text, + /// transforming CSV lines of text into objects, + /// or simply reading the lines of CSV as an array of strings. + /// + /// + /// + /// The following example describes how to load a list of objects from a CSV file. + /// + /// using Swan.Formatters; + /// + /// class Example + /// { + /// class Person + /// { + /// public string Name { get; set; } + /// public int Age { get; set; } + /// } + /// + /// static void Main() + /// { + /// // load records from a CSV file + /// var loadedRecords = + /// CsvReader.LoadRecords<Person>("C:\\Users\\user\\Documents\\file.csv"); + /// + /// // loadedRecords = + /// // [ + /// // { Age = 20, Name = "George" } + /// // { Age = 18, Name = "Juan" } + /// // ] + /// } + /// } + /// + /// The following code explains how to read a CSV formatted string. + /// + /// using Swan.Formatters; + /// using System.Text; + /// using Swan.Formatters; + /// + /// class Example + /// { + /// static void Main() + /// { + /// // data to be read + /// var data = @"Company,OpenPositions,MainTechnology,Revenue + /// Co,2,""C#, MySQL, JavaScript, HTML5 and CSS3"",500 + /// Ca,2,""C#, MySQL, JavaScript, HTML5 and CSS3"",600"; + /// + /// using(var stream = new MemoryStream(Encoding.UTF8.GetBytes(data))) + /// { + /// // create a CSV reader + /// var reader = new CsvReader(stream, false, Encoding.UTF8); + /// } + /// } + /// } + /// + /// + public class CsvReader : IDisposable { + private static readonly PropertyTypeCache TypeCache = new PropertyTypeCache(); + + private readonly Object _syncLock = new Object(); + + private UInt64 _count; + private Char _escapeCharacter = '"'; + private Char _separatorCharacter = ','; + + private Boolean _hasDisposed; // To detect redundant calls + private String[] _headings; + private Dictionary _defaultMap; + private StreamReader _reader; + + #region Constructors + /// - /// Represents a reader designed for CSV text. - /// It is capable of deserializing objects from individual lines of CSV text, - /// transforming CSV lines of text into objects, - /// or simply reading the lines of CSV as an array of strings. + /// Initializes a new instance of the class. /// - /// - /// - /// The following example describes how to load a list of objects from a CSV file. - /// - /// using Swan.Formatters; - /// - /// class Example - /// { - /// class Person - /// { - /// public string Name { get; set; } - /// public int Age { get; set; } - /// } - /// - /// static void Main() - /// { - /// // load records from a CSV file - /// var loadedRecords = - /// CsvReader.LoadRecords<Person>("C:\\Users\\user\\Documents\\file.csv"); - /// - /// // loadedRecords = - /// // [ - /// // { Age = 20, Name = "George" } - /// // { Age = 18, Name = "Juan" } - /// // ] - /// } - /// } - /// - /// The following code explains how to read a CSV formatted string. - /// - /// using Swan.Formatters; - /// using System.Text; - /// using Swan.Formatters; - /// - /// class Example - /// { - /// static void Main() - /// { - /// // data to be read - /// var data = @"Company,OpenPositions,MainTechnology,Revenue - /// Co,2,""C#, MySQL, JavaScript, HTML5 and CSS3"",500 - /// Ca,2,""C#, MySQL, JavaScript, HTML5 and CSS3"",600"; - /// - /// using(var stream = new MemoryStream(Encoding.UTF8.GetBytes(data))) - /// { - /// // create a CSV reader - /// var reader = new CsvReader(stream, false, Encoding.UTF8); - /// } - /// } - /// } - /// - /// - public class CsvReader : IDisposable - { - private static readonly PropertyTypeCache TypeCache = new PropertyTypeCache(); - - private readonly object _syncLock = new object(); - - private ulong _count; - private char _escapeCharacter = '"'; - private char _separatorCharacter = ','; - - private bool _hasDisposed; // To detect redundant calls - private string[] _headings; - private Dictionary _defaultMap; - private StreamReader _reader; - - #region Constructors - - /// - /// Initializes a new instance of the class. - /// - /// The stream. - /// if set to true leaves the input stream open. - /// The text encoding. - public CsvReader(Stream inputStream, bool leaveOpen, Encoding textEncoding) - { - if (inputStream == null) - throw new ArgumentNullException(nameof(inputStream)); - - if (textEncoding == null) - throw new ArgumentNullException(nameof(textEncoding)); - - _reader = new StreamReader(inputStream, textEncoding, true, 2048, leaveOpen); - } - - /// - /// Initializes a new instance of the class. - /// It will automatically close the stream upon disposing. - /// - /// The stream. - /// The text encoding. - public CsvReader(Stream stream, Encoding textEncoding) - : this(stream, false, textEncoding) - { - // placeholder - } - - /// - /// Initializes a new instance of the class. - /// It automatically closes the stream when disposing this reader - /// and uses the Windows 1253 encoding. - /// - /// The stream. - public CsvReader(Stream stream) - : this(stream, false, Definitions.Windows1252Encoding) - { - } - - /// - /// Initializes a new instance of the class. - /// It uses the Windows 1252 Encoding by default and it automatically closes the file - /// when this reader is disposed of. - /// - /// The filename. - public CsvReader(string filename) - : this(File.OpenRead(filename), false, Definitions.Windows1252Encoding) - { - // placeholder - } - - /// - /// Initializes a new instance of the class. - /// It automatically closes the file when disposing this reader. - /// - /// The filename. - /// The encoding. - public CsvReader(string filename, Encoding encoding) - : this(File.OpenRead(filename), false, encoding) - { - // placeholder - } - - #endregion - - #region Properties - - /// - /// Gets number of lines that have been read, including the headings. - /// - /// - /// The count. - /// - public ulong Count - { - get - { - lock (_syncLock) - { - return _count; - } - } - } - - /// - /// Gets or sets the escape character. - /// By default it is the double quote '"'. - /// - /// - /// The escape character. - /// - public char EscapeCharacter - { - get => _escapeCharacter; - set - { - lock (_syncLock) - { - _escapeCharacter = value; - } - } - } - - /// - /// Gets or sets the separator character. - /// By default it is the comma character ','. - /// - /// - /// The separator character. - /// - public char SeparatorCharacter - { - get => _separatorCharacter; - set - { - lock (_syncLock) - { - _separatorCharacter = value; - } - } - } - - /// - /// Gets a value indicating whether the stream reader is at the end of the stream - /// In other words, if no more data can be read, this will be set to true. - /// - /// - /// true if [end of stream]; otherwise, false. - /// - public bool EndOfStream - { - get - { - lock (_syncLock) - { - return _reader.EndOfStream; - } - } - } - - #endregion - - #region Generic, Main ReadLine method - - /// - /// Reads a line of CSV text into an array of strings. - /// - /// An array of the specified element type containing copies of the elements of the ArrayList. - /// Cannot read past the end of the stream. - public string[] ReadLine() - { - lock (_syncLock) - { - if (_reader.EndOfStream) - throw new EndOfStreamException("Cannot read past the end of the stream"); - - var values = ParseRecord(_reader, _escapeCharacter, _separatorCharacter); - _count++; - return values; - } - } - - #endregion - - #region Read Methods - - /// - /// Skips a line of CSV text. - /// This operation does not increment the Count property and it is useful when you need to read the headings - /// skipping over a few lines as Reading headings is only supported - /// as the first read operation (i.e. while count is still 0). - /// - /// Cannot read past the end of the stream. - public void SkipRecord() - { - lock (_syncLock) - { - if (_reader.EndOfStream) - throw new EndOfStreamException("Cannot read past the end of the stream"); - - ParseRecord(_reader, _escapeCharacter, _separatorCharacter); - } - } - - /// - /// Reads a line of CSV text and stores the values read as a representation of the column names - /// to be used for parsing objects. You have to call this method before calling ReadObject methods. - /// - /// An array of the specified element type containing copies of the elements of the ArrayList. - /// - /// Reading headings is only supported as the first read operation. - /// or - /// ReadHeadings. - /// - /// Cannot read past the end of the stream. - public string[] ReadHeadings() - { - lock (_syncLock) - { - if (_headings != null) - throw new InvalidOperationException($"The {nameof(ReadHeadings)} method had already been called."); - - if (_count != 0) - throw new InvalidOperationException("Reading headings is only supported as the first read operation."); - - _headings = ReadLine(); - _defaultMap = _headings.ToDictionary(x => x, x => x); - - return _headings.ToArray(); - } - } - - /// - /// Reads a line of CSV text, converting it into a dynamic object in which properties correspond to the names of the headings. - /// - /// The mappings between CSV headings (keys) and object properties (values). - /// Object of the type of the elements in the collection of key/value pairs. - /// ReadHeadings. - /// Cannot read past the end of the stream. - /// map. - public IDictionary ReadObject(IDictionary map) - { - lock (_syncLock) - { - if (_headings == null) - throw new InvalidOperationException($"Call the {nameof(ReadHeadings)} method before reading as an object."); - - if (map == null) - throw new ArgumentNullException(nameof(map)); - - var result = new Dictionary(); - var values = ReadLine(); - - for (var i = 0; i < _headings.Length; i++) - { - if (i > values.Length - 1) - break; - - result[_headings[i]] = values[i]; - } - - return result; - } - } - - /// - /// Reads a line of CSV text, converting it into a dynamic object - /// The property names correspond to the names of the CSV headings. - /// - /// Object of the type of the elements in the collection of key/value pairs. - public IDictionary ReadObject() => ReadObject(_defaultMap); - - /// - /// Reads a line of CSV text converting it into an object of the given type, using a map (or Dictionary) - /// where the keys are the names of the headings and the values are the names of the instance properties - /// in the given Type. The result object must be already instantiated. - /// - /// The type of object to map. - /// The map. - /// The result. - /// map - /// or - /// result. - /// ReadHeadings. - /// Cannot read past the end of the stream. - public void ReadObject(IDictionary map, ref T result) - { - lock (_syncLock) - { - // Check arguments - { - if (map == null) - throw new ArgumentNullException(nameof(map)); - - if (_reader.EndOfStream) - throw new EndOfStreamException("Cannot read past the end of the stream"); - - if (_headings == null) - throw new InvalidOperationException($"Call the {nameof(ReadHeadings)} method before reading as an object."); - - if (Equals(result, default(T))) - throw new ArgumentNullException(nameof(result)); - } - - // Read line and extract values - var values = ReadLine(); - - // Extract properties from cache - var properties = TypeCache - .RetrieveFilteredProperties(typeof(T), true, x => x.CanWrite && Definitions.BasicTypesInfo.Value.ContainsKey(x.PropertyType)); - - // Assign property values for each heading - for (var i = 0; i < _headings.Length; i++) - { - // break if no more headings are matched - if (i > values.Length - 1) - break; - - // skip if no heading is available or the heading is empty - if (map.ContainsKey(_headings[i]) == false && - string.IsNullOrWhiteSpace(map[_headings[i]]) == false) - continue; - - // Prepare the target property - var propertyName = map[_headings[i]]; - - // Parse and assign the basic type value to the property if exists - properties - .FirstOrDefault(p => p.Name == propertyName)? - .TrySetBasicType(values[i], result); - } - } - } - - /// - /// Reads a line of CSV text converting it into an object of the given type, using a map (or Dictionary) - /// where the keys are the names of the headings and the values are the names of the instance properties - /// in the given Type. - /// - /// The type of object to map. - /// The map of CSV headings (keys) and Type property names (values). - /// The conversion of specific type of object. - /// map. - /// ReadHeadings. - /// Cannot read past the end of the stream. - public T ReadObject(IDictionary map) - where T : new() - { - var result = Activator.CreateInstance(); - ReadObject(map, ref result); - return result; - } - - /// - /// Reads a line of CSV text converting it into an object of the given type, and assuming - /// the property names of the target type match the heading names of the file. - /// - /// The type of object. - /// The conversion of specific type of object. - public T ReadObject() - where T : new() - { - return ReadObject(_defaultMap); - } - - #endregion - - #region Support Methods - - /// - /// Parses a line of standard CSV text into an array of strings. - /// Note that quoted values might have new line sequences in them. Field values will contain such sequences. - /// - /// The reader. - /// The escape character. - /// The separator character. - /// An array of the specified element type containing copies of the elements of the ArrayList. - private static string[] ParseRecord(StreamReader reader, char escapeCharacter = '"', char separatorCharacter = ',') - { - var values = new List(); - var currentValue = new StringBuilder(1024); - var currentState = ReadState.WaitingForNewField; - string line; - - while ((line = reader.ReadLine()) != null) - { - for (var charIndex = 0; charIndex < line.Length; charIndex++) - { - // Get the current and next character - var currentChar = line[charIndex]; - var nextChar = charIndex < line.Length - 1 ? line[charIndex + 1] : new char?(); - - // Perform logic based on state and decide on next state - switch (currentState) - { - case ReadState.WaitingForNewField: - { - currentValue.Clear(); - - if (currentChar == escapeCharacter) - { - currentState = ReadState.PushingQuoted; - continue; - } - - if (currentChar == separatorCharacter) - { - values.Add(currentValue.ToString()); - currentState = ReadState.WaitingForNewField; - continue; - } - - currentValue.Append(currentChar); - currentState = ReadState.PushingNormal; - continue; - } - - case ReadState.PushingNormal: - { - // Handle field content delimiter by comma - if (currentChar == separatorCharacter) - { - currentState = ReadState.WaitingForNewField; - values.Add(currentValue.ToString()); - currentValue.Clear(); - continue; - } - - // Handle double quote escaping - if (currentChar == escapeCharacter && nextChar.HasValue && nextChar == escapeCharacter) - { - // advance 1 character now. The loop will advance one more. - currentValue.Append(currentChar); - charIndex++; - continue; - } - - currentValue.Append(currentChar); - break; - } - - case ReadState.PushingQuoted: - { - // Handle field content delimiter by ending double quotes - if (currentChar == escapeCharacter && (nextChar.HasValue == false || nextChar != escapeCharacter)) - { - currentState = ReadState.PushingNormal; - continue; - } - - // Handle double quote escaping - if (currentChar == escapeCharacter && nextChar.HasValue && nextChar == escapeCharacter) - { - // advance 1 character now. The loop will advance one more. - currentValue.Append(currentChar); - charIndex++; - continue; - } - - currentValue.Append(currentChar); - break; - } - } - } - - // determine if we need to continue reading a new line if it is part of the quoted - // field value - if (currentState == ReadState.PushingQuoted) - { - // we need to add the new line sequence to the output of the field - // because we were pushing a quoted value - currentValue.Append(Environment.NewLine); - } - else - { - // push anything that has not been pushed (flush) into a last value - values.Add(currentValue.ToString()); - currentValue.Clear(); - - // stop reading more lines we have reached the end of the CSV record - break; - } - } - - // If we ended up pushing quoted and no closing quotes we might - // have additional text in yt - if (currentValue.Length > 0) - { - values.Add(currentValue.ToString()); - } - - return values.ToArray(); - } - - #endregion - - #region Helpers - - /// - /// Loads the records from the stream - /// This method uses Windows 1252 encoding. - /// - /// The type of IList items to load. - /// The stream. - /// A generic collection of objects that can be individually accessed by index. - public static IList LoadRecords(Stream stream) - where T : new() - { - var result = new List(); - - using (var reader = new CsvReader(stream)) - { - reader.ReadHeadings(); - while (!reader.EndOfStream) - { - result.Add(reader.ReadObject()); - } - } - - return result; - } - - /// - /// Loads the records from the give file path. - /// This method uses Windows 1252 encoding. - /// - /// The type of IList items to load. - /// The file path. - /// A generic collection of objects that can be individually accessed by index. - public static IList LoadRecords(string filePath) - where T : new() - { - return LoadRecords(File.OpenRead(filePath)); - } - - #endregion - - #region IDisposable Support - - /// - /// Releases unmanaged and - optionally - managed resources. - /// - /// true to release both managed and unmanaged resources; false to release only unmanaged resources. - protected virtual void Dispose(bool disposing) - { - if (_hasDisposed) return; - - if (disposing) - { - try - { - _reader.Dispose(); - } - finally - { - _reader = null; - } - } - - _hasDisposed = true; - } - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - #endregion - - /// - /// Defines the 3 different read states - /// for the parsing state machine. - /// - private enum ReadState - { - WaitingForNewField, - PushingNormal, - PushingQuoted, - } - } + /// The stream. + /// if set to true leaves the input stream open. + /// The text encoding. + public CsvReader(Stream inputStream, Boolean leaveOpen, Encoding textEncoding) { + if(inputStream == null) { + throw new ArgumentNullException(nameof(inputStream)); + } + + if(textEncoding == null) { + throw new ArgumentNullException(nameof(textEncoding)); + } + + this._reader = new StreamReader(inputStream, textEncoding, true, 2048, leaveOpen); + } + + /// + /// Initializes a new instance of the class. + /// It will automatically close the stream upon disposing. + /// + /// The stream. + /// The text encoding. + public CsvReader(Stream stream, Encoding textEncoding) : this(stream, false, textEncoding) { + // placeholder + } + + /// + /// Initializes a new instance of the class. + /// It automatically closes the stream when disposing this reader + /// and uses the Windows 1253 encoding. + /// + /// The stream. + public CsvReader(Stream stream) : this(stream, false, Definitions.Windows1252Encoding) { + } + + /// + /// Initializes a new instance of the class. + /// It uses the Windows 1252 Encoding by default and it automatically closes the file + /// when this reader is disposed of. + /// + /// The filename. + public CsvReader(String filename) : this(File.OpenRead(filename), false, Definitions.Windows1252Encoding) { + // placeholder + } + + /// + /// Initializes a new instance of the class. + /// It automatically closes the file when disposing this reader. + /// + /// The filename. + /// The encoding. + public CsvReader(String filename, Encoding encoding) : this(File.OpenRead(filename), false, encoding) { + // placeholder + } + + #endregion + + #region Properties + + /// + /// Gets number of lines that have been read, including the headings. + /// + /// + /// The count. + /// + public UInt64 Count { + get { + lock(this._syncLock) { + return this._count; + } + } + } + + /// + /// Gets or sets the escape character. + /// By default it is the double quote '"'. + /// + /// + /// The escape character. + /// + public Char EscapeCharacter { + get => this._escapeCharacter; + set { + lock(this._syncLock) { + this._escapeCharacter = value; + } + } + } + + /// + /// Gets or sets the separator character. + /// By default it is the comma character ','. + /// + /// + /// The separator character. + /// + public Char SeparatorCharacter { + get => this._separatorCharacter; + set { + lock(this._syncLock) { + this._separatorCharacter = value; + } + } + } + + /// + /// Gets a value indicating whether the stream reader is at the end of the stream + /// In other words, if no more data can be read, this will be set to true. + /// + /// + /// true if [end of stream]; otherwise, false. + /// + public Boolean EndOfStream { + get { + lock(this._syncLock) { + return this._reader.EndOfStream; + } + } + } + + #endregion + + #region Generic, Main ReadLine method + + /// + /// Reads a line of CSV text into an array of strings. + /// + /// An array of the specified element type containing copies of the elements of the ArrayList. + /// Cannot read past the end of the stream. + public String[] ReadLine() { + lock(this._syncLock) { + if(this._reader.EndOfStream) { + throw new EndOfStreamException("Cannot read past the end of the stream"); + } + + String[] values = ParseRecord(this._reader, this._escapeCharacter, this._separatorCharacter); + this._count++; + return values; + } + } + + #endregion + + #region Read Methods + + /// + /// Skips a line of CSV text. + /// This operation does not increment the Count property and it is useful when you need to read the headings + /// skipping over a few lines as Reading headings is only supported + /// as the first read operation (i.e. while count is still 0). + /// + /// Cannot read past the end of the stream. + public void SkipRecord() { + lock(this._syncLock) { + if(this._reader.EndOfStream) { + throw new EndOfStreamException("Cannot read past the end of the stream"); + } + + _ = ParseRecord(this._reader, this._escapeCharacter, this._separatorCharacter); + } + } + + /// + /// Reads a line of CSV text and stores the values read as a representation of the column names + /// to be used for parsing objects. You have to call this method before calling ReadObject methods. + /// + /// An array of the specified element type containing copies of the elements of the ArrayList. + /// + /// Reading headings is only supported as the first read operation. + /// or + /// ReadHeadings. + /// + /// Cannot read past the end of the stream. + public String[] ReadHeadings() { + lock(this._syncLock) { + if(this._headings != null) { + throw new InvalidOperationException($"The {nameof(ReadHeadings)} method had already been called."); + } + + if(this._count != 0) { + throw new InvalidOperationException("Reading headings is only supported as the first read operation."); + } + + this._headings = this.ReadLine(); + this._defaultMap = this._headings.ToDictionary(x => x, x => x); + + return this._headings.ToArray(); + } + } + + /// + /// Reads a line of CSV text, converting it into a dynamic object in which properties correspond to the names of the headings. + /// + /// The mappings between CSV headings (keys) and object properties (values). + /// Object of the type of the elements in the collection of key/value pairs. + /// ReadHeadings. + /// Cannot read past the end of the stream. + /// map. + public IDictionary ReadObject(IDictionary map) { + lock(this._syncLock) { + if(this._headings == null) { + throw new InvalidOperationException($"Call the {nameof(ReadHeadings)} method before reading as an object."); + } + + if(map == null) { + throw new ArgumentNullException(nameof(map)); + } + + Dictionary result = new Dictionary(); + String[] values = this.ReadLine(); + + for(Int32 i = 0; i < this._headings.Length; i++) { + if(i > values.Length - 1) { + break; + } + + result[this._headings[i]] = values[i]; + } + + return result; + } + } + + /// + /// Reads a line of CSV text, converting it into a dynamic object + /// The property names correspond to the names of the CSV headings. + /// + /// Object of the type of the elements in the collection of key/value pairs. + public IDictionary ReadObject() => this.ReadObject(this._defaultMap); + + /// + /// Reads a line of CSV text converting it into an object of the given type, using a map (or Dictionary) + /// where the keys are the names of the headings and the values are the names of the instance properties + /// in the given Type. The result object must be already instantiated. + /// + /// The type of object to map. + /// The map. + /// The result. + /// map + /// or + /// result. + /// ReadHeadings. + /// Cannot read past the end of the stream. + public void ReadObject(IDictionary map, ref T result) { + lock(this._syncLock) { + // Check arguments + { + if(map == null) { + throw new ArgumentNullException(nameof(map)); + } + + if(this._reader.EndOfStream) { + throw new EndOfStreamException("Cannot read past the end of the stream"); + } + + if(this._headings == null) { + throw new InvalidOperationException($"Call the {nameof(ReadHeadings)} method before reading as an object."); + } + + if(Equals(result, default(T))) { + throw new ArgumentNullException(nameof(result)); + } + } + + // Read line and extract values + String[] values = this.ReadLine(); + + // Extract properties from cache + IEnumerable properties = TypeCache .RetrieveFilteredProperties(typeof(T), true, x => x.CanWrite && Definitions.BasicTypesInfo.Value.ContainsKey(x.PropertyType)); + + // Assign property values for each heading + for(Int32 i = 0; i < this._headings.Length; i++) { + // break if no more headings are matched + if(i > values.Length - 1) { + break; + } + + // skip if no heading is available or the heading is empty + if(map.ContainsKey(this._headings[i]) == false && String.IsNullOrWhiteSpace(map[this._headings[i]]) == false) { + continue; + } + + // Prepare the target property + String propertyName = map[this._headings[i]]; + + // Parse and assign the basic type value to the property if exists + _ = properties .FirstOrDefault(p => p.Name == propertyName)? .TrySetBasicType(values[i], result); + } + } + } + + /// + /// Reads a line of CSV text converting it into an object of the given type, using a map (or Dictionary) + /// where the keys are the names of the headings and the values are the names of the instance properties + /// in the given Type. + /// + /// The type of object to map. + /// The map of CSV headings (keys) and Type property names (values). + /// The conversion of specific type of object. + /// map. + /// ReadHeadings. + /// Cannot read past the end of the stream. + public T ReadObject(IDictionary map) where T : new() { + T result = Activator.CreateInstance(); + this.ReadObject(map, ref result); + return result; + } + + /// + /// Reads a line of CSV text converting it into an object of the given type, and assuming + /// the property names of the target type match the heading names of the file. + /// + /// The type of object. + /// The conversion of specific type of object. + public T ReadObject() where T : new() => this.ReadObject(this._defaultMap); + + #endregion + + #region Support Methods + + /// + /// Parses a line of standard CSV text into an array of strings. + /// Note that quoted values might have new line sequences in them. Field values will contain such sequences. + /// + /// The reader. + /// The escape character. + /// The separator character. + /// An array of the specified element type containing copies of the elements of the ArrayList. + private static String[] ParseRecord(StreamReader reader, Char escapeCharacter = '"', Char separatorCharacter = ',') { + List values = new List(); + StringBuilder currentValue = new StringBuilder(1024); + ReadState currentState = ReadState.WaitingForNewField; + String line; + + while((line = reader.ReadLine()) != null) { + for(Int32 charIndex = 0; charIndex < line.Length; charIndex++) { + // Get the current and next character + Char currentChar = line[charIndex]; + Char? nextChar = charIndex < line.Length - 1 ? line[charIndex + 1] : new global::System.Char?(); + + // Perform logic based on state and decide on next state + switch(currentState) { + case ReadState.WaitingForNewField: { + _ = currentValue.Clear(); + + if(currentChar == escapeCharacter) { + currentState = ReadState.PushingQuoted; + continue; + } + + if(currentChar == separatorCharacter) { + values.Add(currentValue.ToString()); + currentState = ReadState.WaitingForNewField; + continue; + } + + _ = currentValue.Append(currentChar); + currentState = ReadState.PushingNormal; + continue; + } + + case ReadState.PushingNormal: { + // Handle field content delimiter by comma + if(currentChar == separatorCharacter) { + currentState = ReadState.WaitingForNewField; + values.Add(currentValue.ToString()); + _ = currentValue.Clear(); + continue; + } + + // Handle double quote escaping + if(currentChar == escapeCharacter && nextChar.HasValue && nextChar == escapeCharacter) { + // advance 1 character now. The loop will advance one more. + _ = currentValue.Append(currentChar); + charIndex++; + continue; + } + + _ = currentValue.Append(currentChar); + break; + } + + case ReadState.PushingQuoted: { + // Handle field content delimiter by ending double quotes + if(currentChar == escapeCharacter && (nextChar.HasValue == false || nextChar != escapeCharacter)) { + currentState = ReadState.PushingNormal; + continue; + } + + // Handle double quote escaping + if(currentChar == escapeCharacter && nextChar.HasValue && nextChar == escapeCharacter) { + // advance 1 character now. The loop will advance one more. + _ = currentValue.Append(currentChar); + charIndex++; + continue; + } + + _ = currentValue.Append(currentChar); + break; + } + } + } + + // determine if we need to continue reading a new line if it is part of the quoted + // field value + if(currentState == ReadState.PushingQuoted) { + // we need to add the new line sequence to the output of the field + // because we were pushing a quoted value + _ = currentValue.Append(Environment.NewLine); + } else { + // push anything that has not been pushed (flush) into a last value + values.Add(currentValue.ToString()); + _ = currentValue.Clear(); + + // stop reading more lines we have reached the end of the CSV record + break; + } + } + + // If we ended up pushing quoted and no closing quotes we might + // have additional text in yt + if(currentValue.Length > 0) { + values.Add(currentValue.ToString()); + } + + return values.ToArray(); + } + + #endregion + + #region Helpers + + /// + /// Loads the records from the stream + /// This method uses Windows 1252 encoding. + /// + /// The type of IList items to load. + /// The stream. + /// A generic collection of objects that can be individually accessed by index. + public static IList LoadRecords(Stream stream) where T : new() { + List result = new List(); + + using(CsvReader reader = new CsvReader(stream)) { + _ = reader.ReadHeadings(); + while(!reader.EndOfStream) { + result.Add(reader.ReadObject()); + } + } + + return result; + } + + /// + /// Loads the records from the give file path. + /// This method uses Windows 1252 encoding. + /// + /// The type of IList items to load. + /// The file path. + /// A generic collection of objects that can be individually accessed by index. + public static IList LoadRecords(String filePath) where T : new() => LoadRecords(File.OpenRead(filePath)); + + #endregion + + #region IDisposable Support + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(Boolean disposing) { + if(this._hasDisposed) { + return; + } + + if(disposing) { + try { + this._reader.Dispose(); + } finally { + this._reader = null; + } + } + + this._hasDisposed = true; + } + + /// + public void Dispose() { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + #endregion + + /// + /// Defines the 3 different read states + /// for the parsing state machine. + /// + private enum ReadState { + WaitingForNewField, + PushingNormal, + PushingQuoted, + } + } } diff --git a/Swan.Lite/Formatters/CsvWriter.cs b/Swan.Lite/Formatters/CsvWriter.cs index 8cfa6c5..6a1cadf 100644 --- a/Swan.Lite/Formatters/CsvWriter.cs +++ b/Swan.Lite/Formatters/CsvWriter.cs @@ -7,453 +7,415 @@ using System.Reflection; using System.Text; using Swan.Reflection; -namespace Swan.Formatters -{ +namespace Swan.Formatters { + /// + /// A CSV writer useful for exporting a set of objects. + /// + /// + /// The following code describes how to save a list of objects into a CSV file. + /// + /// using System.Collections.Generic; + /// using Swan.Formatters; + /// + /// class Example + /// { + /// class Person + /// { + /// public string Name { get; set; } + /// public int Age { get; set; } + /// } + /// + /// static void Main() + /// { + /// // create a list of people + /// var people = new List<Person> + /// { + /// new Person { Name = "Artyom", Age = 20 }, + /// new Person { Name = "Aloy", Age = 18 } + /// } + /// + /// // write items inside file.csv + /// CsvWriter.SaveRecords(people, "C:\\Users\\user\\Documents\\file.csv"); + /// + /// // output + /// // | Name | Age | + /// // | Artyom | 20 | + /// // | Aloy | 18 | + /// } + /// } + /// + /// + public class CsvWriter : IDisposable { + private static readonly PropertyTypeCache TypeCache = new PropertyTypeCache(); + + private readonly Object _syncLock = new Object(); + private readonly Stream _outputStream; + private readonly Encoding _encoding; + private readonly Boolean _leaveStreamOpen; + private Boolean _isDisposing; + private UInt64 _mCount; + + #region Constructors + /// - /// A CSV writer useful for exporting a set of objects. + /// Initializes a new instance of the class. /// - /// - /// The following code describes how to save a list of objects into a CSV file. - /// - /// using System.Collections.Generic; - /// using Swan.Formatters; - /// - /// class Example - /// { - /// class Person - /// { - /// public string Name { get; set; } - /// public int Age { get; set; } - /// } - /// - /// static void Main() - /// { - /// // create a list of people - /// var people = new List<Person> - /// { - /// new Person { Name = "Artyom", Age = 20 }, - /// new Person { Name = "Aloy", Age = 18 } - /// } - /// - /// // write items inside file.csv - /// CsvWriter.SaveRecords(people, "C:\\Users\\user\\Documents\\file.csv"); - /// - /// // output - /// // | Name | Age | - /// // | Artyom | 20 | - /// // | Aloy | 18 | - /// } - /// } - /// - /// - public class CsvWriter : IDisposable - { - private static readonly PropertyTypeCache TypeCache = new PropertyTypeCache(); - - private readonly object _syncLock = new object(); - private readonly Stream _outputStream; - private readonly Encoding _encoding; - private readonly bool _leaveStreamOpen; - private bool _isDisposing; - private ulong _mCount; - - #region Constructors - - /// - /// Initializes a new instance of the class. - /// - /// The output stream. - /// if set to true [leave open]. - /// The encoding. - public CsvWriter(Stream outputStream, bool leaveOpen, Encoding encoding) - { - _outputStream = outputStream; - _encoding = encoding; - _leaveStreamOpen = leaveOpen; - } - - /// - /// Initializes a new instance of the class. - /// It automatically closes the stream when disposing this writer. - /// - /// The output stream. - /// The encoding. - public CsvWriter(Stream outputStream, Encoding encoding) - : this(outputStream, false, encoding) - { - // placeholder - } - - /// - /// Initializes a new instance of the class. - /// It uses the Windows 1252 encoding and automatically closes - /// the stream upon disposing this writer. - /// - /// The output stream. - public CsvWriter(Stream outputStream) - : this(outputStream, false, Definitions.Windows1252Encoding) - { - // placeholder - } - - /// - /// Initializes a new instance of the class. - /// It opens the file given file, automatically closes the stream upon - /// disposing of this writer, and uses the Windows 1252 encoding. - /// - /// The filename. - public CsvWriter(string filename) - : this(File.OpenWrite(filename), false, Definitions.Windows1252Encoding) - { - // placeholder - } - - /// - /// Initializes a new instance of the class. - /// It opens the file given file, automatically closes the stream upon - /// disposing of this writer, and uses the given text encoding for output. - /// - /// The filename. - /// The encoding. - public CsvWriter(string filename, Encoding encoding) - : this(File.OpenWrite(filename), false, encoding) - { - // placeholder - } - - #endregion - - #region Properties - - /// - /// Gets or sets the field separator character. - /// - /// - /// The separator character. - /// - public char SeparatorCharacter { get; set; } = ','; - - /// - /// Gets or sets the escape character to use to escape field values. - /// - /// - /// The escape character. - /// - public char EscapeCharacter { get; set; } = '"'; - - /// - /// Gets or sets the new line character sequence to use when writing a line. - /// - /// - /// The new line sequence. - /// - public string NewLineSequence { get; set; } = Environment.NewLine; - - /// - /// Defines a list of properties to ignore when outputting CSV lines. - /// - /// - /// The ignore property names. - /// - public List IgnorePropertyNames { get; } = new List(); - - /// - /// Gets number of lines that have been written, including the headings line. - /// - /// - /// The count. - /// - public ulong Count - { - get - { - lock (_syncLock) - { - return _mCount; - } - } - } - - #endregion - - #region Helpers - - /// - /// Saves the items to a stream. - /// It uses the Windows 1252 text encoding for output. - /// - /// The type of enumeration. - /// The items. - /// The stream. - /// true if stream is truncated, default false. - /// Number of item saved. - public static int SaveRecords(IEnumerable items, Stream stream, bool truncateData = false) - { - // truncate the file if it had data - if (truncateData && stream.Length > 0) - stream.SetLength(0); - - using (var writer = new CsvWriter(stream)) - { - writer.WriteHeadings(); - writer.WriteObjects(items); - return (int)writer.Count; - } - } - - /// - /// Saves the items to a CSV file. - /// If the file exits, it overwrites it. If it does not, it creates it. - /// It uses the Windows 1252 text encoding for output. - /// - /// The type of enumeration. - /// The items. - /// The file path. - /// Number of item saved. - public static int SaveRecords(IEnumerable items, string filePath) => SaveRecords(items, File.OpenWrite(filePath), true); - - #endregion - - #region Generic, main Write Line Method - - /// - /// Writes a line of CSV text. Items are converted to strings. - /// If items are found to be null, empty strings are written out. - /// If items are not string, the ToStringInvariant() method is called on them. - /// - /// The items. - public void WriteLine(params object[] items) - => WriteLine(items.Select(x => x == null ? string.Empty : x.ToStringInvariant())); - - /// - /// Writes a line of CSV text. Items are converted to strings. - /// If items are found to be null, empty strings are written out. - /// If items are not string, the ToStringInvariant() method is called on them. - /// - /// The items. - public void WriteLine(IEnumerable items) - => WriteLine(items.Select(x => x == null ? string.Empty : x.ToStringInvariant())); - - /// - /// Writes a line of CSV text. - /// If items are found to be null, empty strings are written out. - /// - /// The items. - public void WriteLine(params string[] items) => WriteLine((IEnumerable) items); - - /// - /// Writes a line of CSV text. - /// If items are found to be null, empty strings are written out. - /// - /// The items. - public void WriteLine(IEnumerable items) - { - lock (_syncLock) - { - var length = items.Count(); - var separatorBytes = _encoding.GetBytes(new[] { SeparatorCharacter }); - var endOfLineBytes = _encoding.GetBytes(NewLineSequence); - - // Declare state variables here to avoid recreation, allocation and - // reassignment in every loop - bool needsEnclosing; - string textValue; - byte[] output; - - for (var i = 0; i < length; i++) - { - textValue = items.ElementAt(i); - - // Determine if we need the string to be enclosed - // (it either contains an escape, new line, or separator char) - needsEnclosing = textValue.IndexOf(SeparatorCharacter) >= 0 - || textValue.IndexOf(EscapeCharacter) >= 0 - || textValue.IndexOf('\r') >= 0 - || textValue.IndexOf('\n') >= 0; - - // Escape the escape characters by repeating them twice for every instance - textValue = textValue.Replace($"{EscapeCharacter}", - $"{EscapeCharacter}{EscapeCharacter}"); - - // Enclose the text value if we need to - if (needsEnclosing) - textValue = string.Format($"{EscapeCharacter}{textValue}{EscapeCharacter}", textValue); - - // Get the bytes to write to the stream and write them - output = _encoding.GetBytes(textValue); - _outputStream.Write(output, 0, output.Length); - - // only write a separator if we are moving in between values. - // the last value should not be written. - if (i < length - 1) - _outputStream.Write(separatorBytes, 0, separatorBytes.Length); - } - - // output the newline sequence - _outputStream.Write(endOfLineBytes, 0, endOfLineBytes.Length); - _mCount += 1; - } - } - - #endregion - - #region Write Object Method - - /// - /// Writes a row of CSV text. It handles the special cases where the object is - /// a dynamic object or and array. It also handles non-collection objects fine. - /// If you do not like the way the output is handled, you can simply write an extension - /// method of this class and use the WriteLine method instead. - /// - /// The item. - /// item. - public void WriteObject(object item) - { - if (item == null) - throw new ArgumentNullException(nameof(item)); - - lock (_syncLock) - { - switch (item) - { - case IDictionary typedItem: - WriteLine(GetFilteredDictionary(typedItem)); - return; - case ICollection typedItem: - WriteLine(typedItem.Cast()); - return; - default: - WriteLine(GetFilteredTypeProperties(item.GetType()) - .Select(x => x.ToFormattedString(item))); - break; - } - } - } - - /// - /// Writes a row of CSV text. It handles the special cases where the object is - /// a dynamic object or and array. It also handles non-collection objects fine. - /// If you do not like the way the output is handled, you can simply write an extension - /// method of this class and use the WriteLine method instead. - /// - /// The type of object to write. - /// The item. - public void WriteObject(T item) => WriteObject(item as object); - - /// - /// Writes a set of items, one per line and atomically by repeatedly calling the - /// WriteObject method. For more info check out the description of the WriteObject - /// method. - /// - /// The type of object to write. - /// The items. - public void WriteObjects(IEnumerable items) - { - lock (_syncLock) - { - foreach (var item in items) - WriteObject(item); - } - } - - #endregion - - #region Write Headings Methods - - /// - /// Writes the headings. - /// - /// The type of object to extract headings. - /// type. - public void WriteHeadings(Type type) - { - if (type == null) - throw new ArgumentNullException(nameof(type)); - - var properties = GetFilteredTypeProperties(type).Select(p => p.Name).Cast(); - WriteLine(properties); - } - - /// - /// Writes the headings. - /// - /// The type of object to extract headings. - public void WriteHeadings() => WriteHeadings(typeof(T)); - - /// - /// Writes the headings. - /// - /// The dictionary to extract headings. - /// dictionary. - public void WriteHeadings(IDictionary dictionary) - { - if (dictionary == null) - throw new ArgumentNullException(nameof(dictionary)); - - WriteLine(GetFilteredDictionary(dictionary, true)); - } - - /// - /// Writes the headings. - /// - /// The object to extract headings. - /// obj. - public void WriteHeadings(object obj) - { - if (obj == null) - throw new ArgumentNullException(nameof(obj)); - - WriteHeadings(obj.GetType()); - } - - #endregion - - #region IDisposable Support - - /// - public void Dispose() => Dispose(true); - - /// - /// Releases unmanaged and - optionally - managed resources. - /// - /// true to release both managed and unmanaged resources; false to release only unmanaged resources. - protected virtual void Dispose(bool disposeAlsoManaged) - { - if (_isDisposing) return; - - if (disposeAlsoManaged) - { - if (_leaveStreamOpen == false) - { - _outputStream.Dispose(); - } - } - - _isDisposing = true; - } - - #endregion - - #region Support Methods - - private IEnumerable GetFilteredDictionary(IDictionary dictionary, bool filterKeys = false) - => dictionary - .Keys - .Cast() - .Select(key => key == null ? string.Empty : key.ToStringInvariant()) - .Where(stringKey => !IgnorePropertyNames.Contains(stringKey)) - .Select(stringKey => - filterKeys - ? stringKey - : dictionary[stringKey] == null ? string.Empty : dictionary[stringKey].ToStringInvariant()); - - private IEnumerable GetFilteredTypeProperties(Type type) - => TypeCache.Retrieve(type, t => - t.GetProperties(BindingFlags.Public | BindingFlags.Instance) - .Where(p => p.CanRead)) - .Where(p => !IgnorePropertyNames.Contains(p.Name)); - - #endregion - - } + /// The output stream. + /// if set to true [leave open]. + /// The encoding. + public CsvWriter(Stream outputStream, Boolean leaveOpen, Encoding encoding) { + this._outputStream = outputStream; + this._encoding = encoding; + this._leaveStreamOpen = leaveOpen; + } + + /// + /// Initializes a new instance of the class. + /// It automatically closes the stream when disposing this writer. + /// + /// The output stream. + /// The encoding. + public CsvWriter(Stream outputStream, Encoding encoding) : this(outputStream, false, encoding) { + // placeholder + } + + /// + /// Initializes a new instance of the class. + /// It uses the Windows 1252 encoding and automatically closes + /// the stream upon disposing this writer. + /// + /// The output stream. + public CsvWriter(Stream outputStream) : this(outputStream, false, Definitions.Windows1252Encoding) { + // placeholder + } + + /// + /// Initializes a new instance of the class. + /// It opens the file given file, automatically closes the stream upon + /// disposing of this writer, and uses the Windows 1252 encoding. + /// + /// The filename. + public CsvWriter(String filename) : this(File.OpenWrite(filename), false, Definitions.Windows1252Encoding) { + // placeholder + } + + /// + /// Initializes a new instance of the class. + /// It opens the file given file, automatically closes the stream upon + /// disposing of this writer, and uses the given text encoding for output. + /// + /// The filename. + /// The encoding. + public CsvWriter(String filename, Encoding encoding) : this(File.OpenWrite(filename), false, encoding) { + // placeholder + } + + #endregion + + #region Properties + + /// + /// Gets or sets the field separator character. + /// + /// + /// The separator character. + /// + public Char SeparatorCharacter { get; set; } = ','; + + /// + /// Gets or sets the escape character to use to escape field values. + /// + /// + /// The escape character. + /// + public Char EscapeCharacter { get; set; } = '"'; + + /// + /// Gets or sets the new line character sequence to use when writing a line. + /// + /// + /// The new line sequence. + /// + public String NewLineSequence { get; set; } = Environment.NewLine; + + /// + /// Defines a list of properties to ignore when outputting CSV lines. + /// + /// + /// The ignore property names. + /// + public List IgnorePropertyNames { get; } = new List(); + + /// + /// Gets number of lines that have been written, including the headings line. + /// + /// + /// The count. + /// + public UInt64 Count { + get { + lock(this._syncLock) { + return this._mCount; + } + } + } + + #endregion + + #region Helpers + + /// + /// Saves the items to a stream. + /// It uses the Windows 1252 text encoding for output. + /// + /// The type of enumeration. + /// The items. + /// The stream. + /// true if stream is truncated, default false. + /// Number of item saved. + public static Int32 SaveRecords(IEnumerable items, Stream stream, Boolean truncateData = false) { + // truncate the file if it had data + if(truncateData && stream.Length > 0) { + stream.SetLength(0); + } + + using CsvWriter writer = new CsvWriter(stream); + writer.WriteHeadings(); + writer.WriteObjects(items); + return (Int32)writer.Count; + } + + /// + /// Saves the items to a CSV file. + /// If the file exits, it overwrites it. If it does not, it creates it. + /// It uses the Windows 1252 text encoding for output. + /// + /// The type of enumeration. + /// The items. + /// The file path. + /// Number of item saved. + public static Int32 SaveRecords(IEnumerable items, String filePath) => SaveRecords(items, File.OpenWrite(filePath), true); + + #endregion + + #region Generic, main Write Line Method + + /// + /// Writes a line of CSV text. Items are converted to strings. + /// If items are found to be null, empty strings are written out. + /// If items are not string, the ToStringInvariant() method is called on them. + /// + /// The items. + public void WriteLine(params Object[] items) => this.WriteLine(items.Select(x => x == null ? String.Empty : x.ToStringInvariant())); + + /// + /// Writes a line of CSV text. Items are converted to strings. + /// If items are found to be null, empty strings are written out. + /// If items are not string, the ToStringInvariant() method is called on them. + /// + /// The items. + public void WriteLine(IEnumerable items) => this.WriteLine(items.Select(x => x == null ? String.Empty : x.ToStringInvariant())); + + /// + /// Writes a line of CSV text. + /// If items are found to be null, empty strings are written out. + /// + /// The items. + public void WriteLine(params String[] items) => this.WriteLine((IEnumerable)items); + + /// + /// Writes a line of CSV text. + /// If items are found to be null, empty strings are written out. + /// + /// The items. + public void WriteLine(IEnumerable items) { + lock(this._syncLock) { + Int32 length = items.Count(); + Byte[] separatorBytes = this._encoding.GetBytes(new[] { this.SeparatorCharacter }); + Byte[] endOfLineBytes = this._encoding.GetBytes(this.NewLineSequence); + + // Declare state variables here to avoid recreation, allocation and + // reassignment in every loop + Boolean needsEnclosing; + String textValue; + Byte[] output; + + for(Int32 i = 0; i < length; i++) { + textValue = items.ElementAt(i); + + // Determine if we need the string to be enclosed + // (it either contains an escape, new line, or separator char) + needsEnclosing = textValue.IndexOf(this.SeparatorCharacter) >= 0 || textValue.IndexOf(this.EscapeCharacter) >= 0 || textValue.IndexOf('\r') >= 0 || textValue.IndexOf('\n') >= 0; + + // Escape the escape characters by repeating them twice for every instance + textValue = textValue.Replace($"{this.EscapeCharacter}", $"{this.EscapeCharacter}{this.EscapeCharacter}"); + + // Enclose the text value if we need to + if(needsEnclosing) { + textValue = String.Format($"{this.EscapeCharacter}{textValue}{this.EscapeCharacter}", textValue); + } + + // Get the bytes to write to the stream and write them + output = this._encoding.GetBytes(textValue); + this._outputStream.Write(output, 0, output.Length); + + // only write a separator if we are moving in between values. + // the last value should not be written. + if(i < length - 1) { + this._outputStream.Write(separatorBytes, 0, separatorBytes.Length); + } + } + + // output the newline sequence + this._outputStream.Write(endOfLineBytes, 0, endOfLineBytes.Length); + this._mCount += 1; + } + } + + #endregion + + #region Write Object Method + + /// + /// Writes a row of CSV text. It handles the special cases where the object is + /// a dynamic object or and array. It also handles non-collection objects fine. + /// If you do not like the way the output is handled, you can simply write an extension + /// method of this class and use the WriteLine method instead. + /// + /// The item. + /// item. + public void WriteObject(Object item) { + if(item == null) { + throw new ArgumentNullException(nameof(item)); + } + + lock(this._syncLock) { + switch(item) { + case IDictionary typedItem: + this.WriteLine(this.GetFilteredDictionary(typedItem)); + return; + case ICollection typedItem: + this.WriteLine(typedItem.Cast()); + return; + default: + this.WriteLine(this.GetFilteredTypeProperties(item.GetType()).Select(x => x.ToFormattedString(item))); + break; + } + } + } + + /// + /// Writes a row of CSV text. It handles the special cases where the object is + /// a dynamic object or and array. It also handles non-collection objects fine. + /// If you do not like the way the output is handled, you can simply write an extension + /// method of this class and use the WriteLine method instead. + /// + /// The type of object to write. + /// The item. + public void WriteObject(T item) => this.WriteObject(item as Object); + + /// + /// Writes a set of items, one per line and atomically by repeatedly calling the + /// WriteObject method. For more info check out the description of the WriteObject + /// method. + /// + /// The type of object to write. + /// The items. + public void WriteObjects(IEnumerable items) { + lock(this._syncLock) { + foreach(T item in items) { + this.WriteObject(item); + } + } + } + + #endregion + + #region Write Headings Methods + + /// + /// Writes the headings. + /// + /// The type of object to extract headings. + /// type. + public void WriteHeadings(Type type) { + if(type == null) { + throw new ArgumentNullException(nameof(type)); + } + + IEnumerable properties = this.GetFilteredTypeProperties(type).Select(p => p.Name).Cast(); + this.WriteLine(properties); + } + + /// + /// Writes the headings. + /// + /// The type of object to extract headings. + public void WriteHeadings() => this.WriteHeadings(typeof(T)); + + /// + /// Writes the headings. + /// + /// The dictionary to extract headings. + /// dictionary. + public void WriteHeadings(IDictionary dictionary) { + if(dictionary == null) { + throw new ArgumentNullException(nameof(dictionary)); + } + + this.WriteLine(this.GetFilteredDictionary(dictionary, true)); + } + + /// + /// Writes the headings. + /// + /// The object to extract headings. + /// obj. + public void WriteHeadings(Object obj) { + if(obj == null) { + throw new ArgumentNullException(nameof(obj)); + } + + this.WriteHeadings(obj.GetType()); + } + + #endregion + + #region IDisposable Support + + /// + public void Dispose() => this.Dispose(true); + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(Boolean disposeAlsoManaged) { + if(this._isDisposing) { + return; + } + + if(disposeAlsoManaged) { + if(this._leaveStreamOpen == false) { + this._outputStream.Dispose(); + } + } + + this._isDisposing = true; + } + + #endregion + + #region Support Methods + + private IEnumerable GetFilteredDictionary(IDictionary dictionary, Boolean filterKeys = false) => dictionary.Keys.Cast() + .Select(key => key == null ? String.Empty : key.ToStringInvariant()) + .Where(stringKey => !this.IgnorePropertyNames.Contains(stringKey)) + .Select(stringKey => filterKeys ? stringKey : dictionary[stringKey] == null ? String.Empty : dictionary[stringKey].ToStringInvariant()); + + private IEnumerable GetFilteredTypeProperties(Type type) => TypeCache.Retrieve(type, t => t.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.CanRead)).Where(p => !this.IgnorePropertyNames.Contains(p.Name)); + + #endregion + + } } \ No newline at end of file diff --git a/Swan.Lite/Formatters/HumanizeJson.cs b/Swan.Lite/Formatters/HumanizeJson.cs index 102aebd..8099413 100644 --- a/Swan.Lite/Formatters/HumanizeJson.cs +++ b/Swan.Lite/Formatters/HumanizeJson.cs @@ -1,150 +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 int _indent; - private readonly string _indentStr; - private readonly object _obj; - - public HumanizeJson(object obj, int indent) - { - if (obj == null) - { - return; - } - - _indent = indent; - _indentStr = new string(' ', indent * 4); - _obj = obj; - - ParseObject(); - } - - public string GetResult() => _builder == null ? string.Empty : _builder.ToString().TrimEnd(); - - private void ParseObject() - { - switch (_obj) - { - case Dictionary dictionary: - AppendDictionary(dictionary); - break; - case List list: - AppendList(list); - break; - default: - AppendString(); - break; - } - } - - private void AppendDictionary(Dictionary objects) - { - foreach (var kvp in objects) - { - if (kvp.Value == null) continue; - - var writeOutput = false; - - switch (kvp.Value) - { - case Dictionary valueDictionary: - if (valueDictionary.Count > 0) - { - writeOutput = true; - _builder - .Append($"{_indentStr}{kvp.Key,-16}: object") - .AppendLine(); - } - - break; - case List valueList: - if (valueList.Count > 0) - { - writeOutput = true; - _builder - .Append($"{_indentStr}{kvp.Key,-16}: array[{valueList.Count}]") - .AppendLine(); - } - - break; - default: - writeOutput = true; - _builder.Append($"{_indentStr}{kvp.Key,-16}: "); - break; - } - - if (writeOutput) - _builder.AppendLine(new HumanizeJson(kvp.Value, _indent + 1).GetResult()); - } - } - - private void AppendList(List objects) - { - var index = 0; - foreach (var value in objects) - { - var writeOutput = false; - - switch (value) - { - case Dictionary valueDictionary: - if (valueDictionary.Count > 0) - { - writeOutput = true; - _builder - .Append($"{_indentStr}[{index}]: object") - .AppendLine(); - } - - break; - case List valueList: - if (valueList.Count > 0) - { - writeOutput = true; - _builder - .Append($"{_indentStr}[{index}]: array[{valueList.Count}]") - .AppendLine(); - } - - break; - default: - writeOutput = true; - _builder.Append($"{_indentStr}[{index}]: "); - break; - } - - index++; - - if (writeOutput) - _builder.AppendLine(new HumanizeJson(value, _indent + 1).GetResult()); - } - } - - private void AppendString() - { - var stringValue = _obj.ToString(); - - if (stringValue.Length + _indentStr.Length > 96 || stringValue.IndexOf('\r') >= 0 || - stringValue.IndexOf('\n') >= 0) - { - _builder.AppendLine(); - var stringLines = stringValue.ToLines().Select(l => l.Trim()); - - foreach (var line in stringLines) - { - _builder.AppendLine($"{_indentStr}{line}"); - } - } - else - { - _builder.Append($"{stringValue}"); - } - } - } +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.Lite/Formatters/Json.Converter.cs b/Swan.Lite/Formatters/Json.Converter.cs index 38b5401..6878bcb 100644 --- a/Swan.Lite/Formatters/Json.Converter.cs +++ b/Swan.Lite/Formatters/Json.Converter.cs @@ -1,4 +1,5 @@ -using System; +#nullable enable +using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; @@ -7,332 +8,252 @@ 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 bool _includeNonPublic; - private readonly JsonSerializerCase _jsonSerializerCase; - - private Converter( - object? source, - Type targetType, - ref object? targetInstance, - bool includeNonPublic, - JsonSerializerCase jsonSerializerCase) - { - _targetType = targetInstance != null ? targetInstance.GetType() : targetType; - _includeNonPublic = includeNonPublic; - _jsonSerializerCase = jsonSerializerCase; - - if (source == null) - { - return; - } - - var sourceType = source.GetType(); - - if (_targetType == null || _targetType == typeof(object)) _targetType = sourceType; - if (sourceType == _targetType) - { - _target = source; - return; - } - - if (!TrySetInstance(targetInstance, source, ref _target)) - return; - - ResolveObject(source, ref _target); - } - - internal static object? FromJsonResult( - object? source, - JsonSerializerCase jsonSerializerCase, - Type? targetType = null, - bool 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, - bool includeNonPublic) - { - return 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) - { - var targetPropertyName = MemberInfoNameCache.GetOrAdd( - targetProperty, - x => AttributeCache.DefaultCache.Value.RetrieveOne(x)?.PropertyName ?? x.Name.GetNameWithCase(_jsonSerializerCase)); - - return sourceProperties.GetValueOrDefault(targetPropertyName); - } - - private bool TrySetInstance(object? targetInstance, object source, ref object? target) - { - if (targetInstance == null) - { - // Try to create a default instance - try - { - source.CreateTarget(_targetType, _includeNonPublic, ref target); - } - catch - { - return false; - } - } - else - { - target = targetInstance; - } - - return true; - } - - private object? GetResult() => _target ?? _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 _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: - PopulateDictionary(sourceProperties, targetDictionary); - break; - - // Case 1.2: Source is Dictionary, Target is not IDictionary (i.e. it is a complex type) - case Dictionary sourceProperties: - PopulateObject(sourceProperties); - break; - - // Case 2.1: Source is List, Target is Array - case List sourceList when target is Array targetArray: - PopulateArray(sourceList, targetArray); - break; - - // Case 2.2: Source is List, Target is IList - case List sourceList when target is IList targetList: - PopulateIList(sourceList, targetList); - break; - - // Case 3: Source is a simple type; Attempt conversion - default: - var sourceStringValue = source.ToStringInvariant(); - - // Handle basic types or enumerations if not - if (!_targetType.TryParseBasicType(sourceStringValue, out target)) - GetEnumValue(sourceStringValue, ref target); - - break; - } - } - - private void PopulateIList(IEnumerable objects, IList list) - { - var parameterType = GetAddMethodParameterType(_targetType); - if (parameterType == null) return; - - foreach (var item in objects) - { - try - { - list.Add(FromJsonResult( - item, - _jsonSerializerCase, - parameterType, - _includeNonPublic)); - } - catch - { - // ignored - } - } - } - - private void PopulateArray(IList objects, Array array) - { - var elementType = _targetType.GetElementType(); - - for (var i = 0; i < objects.Count; i++) - { - try - { - var targetItem = FromJsonResult( - objects[i], - _jsonSerializerCase, - elementType, - _includeNonPublic); - array.SetValue(targetItem, i); - } - catch - { - // ignored - } - } - } - - private void GetEnumValue(string sourceStringValue, ref object? target) - { - var enumType = Nullable.GetUnderlyingType(_targetType); - if (enumType == null && _targetType.IsEnum) enumType = _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 - var addMethod = _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; - var addMethodParameters = addMethod.GetParameters(); - if (addMethodParameters[0].ParameterType != typeof(string)) return; - - // Retrieve the target entry type - var targetEntryType = addMethodParameters[1].ParameterType; - - // Add the items to the target dictionary - foreach (var sourceProperty in sourceProperties) - { - try - { - var targetEntryValue = FromJsonResult( - sourceProperty.Value, - _jsonSerializerCase, - targetEntryType, - _includeNonPublic); - targetDictionary.Add(sourceProperty.Key, targetEntryValue); - } - catch - { - // ignored - } - } - } - - private void PopulateObject(IDictionary sourceProperties) - { - if (_targetType.IsValueType) - { - PopulateFields(sourceProperties); - } - - PopulateProperties(sourceProperties); - } - - private void PopulateProperties(IDictionary sourceProperties) - { - var properties = PropertyTypeCache.DefaultCache.Value.RetrieveFilteredProperties(_targetType, false, p => p.CanWrite); - - foreach (var property in properties) - { - var sourcePropertyValue = GetSourcePropertyValue(sourceProperties, property); - if (sourcePropertyValue == null) continue; - - try - { - var currentPropertyValue = !property.PropertyType.IsArray - ? property.GetCacheGetMethod(_includeNonPublic)(_target) - : null; - - var targetPropertyValue = FromJsonResult( - sourcePropertyValue, - property.PropertyType, - ref currentPropertyValue, - _includeNonPublic); - - property.GetCacheSetMethod(_includeNonPublic)(_target, new[] { targetPropertyValue }); - } - catch - { - // ignored - } - } - } - - private void PopulateFields(IDictionary sourceProperties) - { - foreach (var field in FieldTypeCache.DefaultCache.Value.RetrieveAllFields(_targetType)) - { - var sourcePropertyValue = GetSourcePropertyValue(sourceProperties, field); - if (sourcePropertyValue == null) continue; - - var targetPropertyValue = FromJsonResult( - sourcePropertyValue, - _jsonSerializerCase, - field.FieldType, - _includeNonPublic); - - try - { - field.SetValue(_target, targetPropertyValue); - } - catch - { - // ignored - } - } - } - } - } +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.Lite/Formatters/Json.Deserializer.cs b/Swan.Lite/Formatters/Json.Deserializer.cs index 4caa463..36ab944 100644 --- a/Swan.Lite/Formatters/Json.Deserializer.cs +++ b/Swan.Lite/Formatters/Json.Deserializer.cs @@ -1,348 +1,332 @@ -using System; +#nullable enable +using System; using System.Collections.Generic; using System.Text; -namespace Swan.Formatters -{ +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 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. + /// A simple JSON Deserializer. /// - 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 int _index; - - #endregion - - private Deserializer(string json, int startIndex) - { - _json = json; - - for (_index = startIndex; _index < _json.Length; _index++) - { - switch (_state) - { - case ReadState.WaitingForRootOpen: - WaitForRootOpen(); - continue; - case ReadState.WaitingForField when char.IsWhiteSpace(_json, _index): - continue; - case ReadState.WaitingForField when (_resultObject != null && _json[_index] == CloseObjectChar) - || (_resultArray != null && _json[_index] == CloseArrayChar): - // Handle empty arrays and empty objects - _result = _resultObject ?? _resultArray as object; - return; - case ReadState.WaitingForField when _json[_index] != StringQuotedChar: - throw CreateParserException($"'{StringQuotedChar}'"); - case ReadState.WaitingForField: - { - var charCount = GetFieldNameCount(); - - _currentFieldName = Unescape(_json.SliceLength(_index + 1, charCount)); - _index += charCount + 1; - _state = ReadState.WaitingForColon; - continue; - } - - case ReadState.WaitingForColon when char.IsWhiteSpace(_json, _index): - continue; - case ReadState.WaitingForColon when _json[_index] != ValueSeparatorChar: - throw CreateParserException($"'{ValueSeparatorChar}'"); - case ReadState.WaitingForColon: - _state = ReadState.WaitingForValue; - continue; - case ReadState.WaitingForValue when char.IsWhiteSpace(_json, _index): - continue; - case ReadState.WaitingForValue when (_resultObject != null && _json[_index] == CloseObjectChar) - || (_resultArray != null && _json[_index] == CloseArrayChar): - // Handle empty arrays and empty objects - _result = _resultObject ?? _resultArray as object; - return; - case ReadState.WaitingForValue: - ExtractValue(); - continue; - } - - if (_state != ReadState.WaitingForNextOrRootClose || char.IsWhiteSpace(_json, _index)) continue; - - if (_json[_index] == FieldSeparatorChar) - { - if (_resultObject != null) - { - _state = ReadState.WaitingForField; - _currentFieldName = null; - continue; - } - - _state = ReadState.WaitingForValue; - continue; - } - - if ((_resultObject == null || _json[_index] != CloseObjectChar) && - (_resultArray == null || _json[_index] != CloseArrayChar)) - { - throw CreateParserException($"'{FieldSeparatorChar}' '{CloseObjectChar}' or '{CloseArrayChar}'"); - } - - _result = _resultObject ?? _resultArray as object; - return; - } - } - - internal static object? DeserializeInternal(string json) => new Deserializer(json, 0)._result; - - private void WaitForRootOpen() - { - if (char.IsWhiteSpace(_json, _index)) return; - - switch (_json[_index]) - { - case OpenObjectChar: - _resultObject = new Dictionary(); - _state = ReadState.WaitingForField; - return; - case OpenArrayChar: - _resultArray = new List(); - _state = ReadState.WaitingForValue; - return; - default: - throw CreateParserException($"'{OpenObjectChar}' or '{OpenArrayChar}'"); - } - } - - private void ExtractValue() - { - // determine the value based on what it starts with - switch (_json[_index]) - { - case StringQuotedChar: // expect a string - ExtractStringQuoted(); - break; - - case OpenObjectChar: // expect object - case OpenArrayChar: // expect array - ExtractObject(); - break; - - case 't': // expect true - ExtractConstant(TrueLiteral, true); - break; - - case 'f': // expect false - ExtractConstant(FalseLiteral, false); - break; - - case 'n': // expect null - ExtractConstant(NullLiteral, null); - break; - - default: // expect number - ExtractNumber(); - break; - } - - _currentFieldName = null; - _state = ReadState.WaitingForNextOrRootClose; - } - - private static string Unescape(string str) - { - // check if we need to unescape at all - if (str.IndexOf(StringEscapeChar) < 0) - return str; - - var builder = new StringBuilder(str.Length); - for (var 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 int ExtractEscapeSequence(string str, int i, StringBuilder builder) - { - var startIndex = i + 2; - var endIndex = i + 5; - if (endIndex > str.Length - 1) - { - builder.Append(str[i + 1]); - i += 1; - return i; - } - - var hexCode = str.Slice(startIndex, endIndex).ConvertHexadecimalToBytes(); - builder.Append(Encoding.BigEndianUnicode.GetChars(hexCode)); - i += 5; - return i; - } - - private int GetFieldNameCount() - { - var charCount = 0; - for (var j = _index + 1; j < _json.Length; j++) - { - if (_json[j] == StringQuotedChar && _json[j - 1] != StringEscapeChar) - break; - - charCount++; - } - - return charCount; - } - - private void ExtractObject() - { - // Extract and set the value - var deserializer = new Deserializer(_json, _index); - - if (_currentFieldName != null) - _resultObject[_currentFieldName] = deserializer._result; - else - _resultArray.Add(deserializer._result); - - _index = deserializer._index; - } - - private void ExtractNumber() - { - var charCount = 0; - for (var j = _index; j < _json.Length; j++) - { - if (char.IsWhiteSpace(_json[j]) || _json[j] == FieldSeparatorChar - || (_resultObject != null && _json[j] == CloseObjectChar) - || (_resultArray != null && _json[j] == CloseArrayChar)) - break; - - charCount++; - } - - // Extract and set the value - var stringValue = _json.SliceLength(_index, charCount); - - if (decimal.TryParse(stringValue, System.Globalization.NumberStyles.Number, System.Globalization.CultureInfo.InvariantCulture, out var value) == false) - throw CreateParserException("[number]"); - - if (_currentFieldName != null) - _resultObject[_currentFieldName] = value; - else - _resultArray.Add(value); - - _index += charCount - 1; - } - - private void ExtractConstant(string boolValue, bool? value) - { - if (_json.SliceLength(_index, boolValue.Length) != boolValue) - throw CreateParserException($"'{ValueSeparatorChar}'"); - - // Extract and set the value - if (_currentFieldName != null) - _resultObject[_currentFieldName] = value; - else - _resultArray.Add(value); - - _index += boolValue.Length - 1; - } - - private void ExtractStringQuoted() - { - var charCount = 0; - var escapeCharFound = false; - for (var j = _index + 1; j < _json.Length; j++) - { - if (_json[j] == StringQuotedChar && !escapeCharFound) - break; - - escapeCharFound = _json[j] == StringEscapeChar && !escapeCharFound; - charCount++; - } - - // Extract and set the value - var value = Unescape(_json.SliceLength(_index + 1, charCount)); - if (_currentFieldName != null) - _resultObject[_currentFieldName] = value; - else - _resultArray.Add(value); - - _index += charCount + 1; - } - - private FormatException CreateParserException(string expected) - { - var textPosition = _json.TextPositionAt(_index); - return new FormatException( - $"Parser error (Line {textPosition.Item1}, Col {textPosition.Item2}, State {_state}): Expected {expected} but got '{_json[_index]}'."); - } - - /// - /// Defines the different JSON read states. - /// - private enum ReadState - { - WaitingForRootOpen, - WaitingForField, - WaitingForColon, - WaitingForValue, - WaitingForNextOrRootClose, - } - } - } + 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.Lite/Formatters/Json.Serializer.cs b/Swan.Lite/Formatters/Json.Serializer.cs index 95a3181..f028578 100644 --- a/Swan.Lite/Formatters/Json.Serializer.cs +++ b/Swan.Lite/Formatters/Json.Serializer.cs @@ -1,348 +1,331 @@ -using System; +#nullable enable +using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; -namespace Swan.Formatters -{ +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 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. + /// A simple JSON serializer. /// - 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, int 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) - _result = ResolveBasicType(obj); - - if (!string.IsNullOrWhiteSpace(_result)) - return; - - _options = options; - - // Handle circular references correctly and avoid them - if (options.IsObjectPresent(obj!)) - { - _result = $"{{ \"$circref\": \"{Escape(obj!.GetHashCode().ToStringInvariant(), false)}\" }}"; - return; - } - - // At this point, we will need to construct the object with a StringBuilder. - _lastCommaSearch = FieldSeparatorChar + (_options.Format ? Environment.NewLine : string.Empty); - _builder = new StringBuilder(); - - _result = obj switch - { - IDictionary itemsZero when itemsZero.Count == 0 => EmptyObjectLiteral, - IDictionary items => ResolveDictionary(items, depth), - IEnumerable enumerableZero when !enumerableZero.Cast().Any() => EmptyArrayLiteral, - IEnumerable enumerableBytes when enumerableBytes is byte[] bytes => Serialize(bytes.ToBase64(), depth, _options), - IEnumerable enumerable => ResolveEnumerable(enumerable, depth), - _ => ResolveObject(obj!, depth) - }; - } - - internal static string Serialize(object? obj, int 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 bool 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: - var targetType = obj.GetType(); - - if (!Definitions.BasicTypesInfo.Value.ContainsKey(targetType)) - return string.Empty; - - var escapedValue = Escape(Definitions.BasicTypesInfo.Value[targetType].ToStringInvariant(obj), false); - - return decimal.TryParse(escapedValue, out _) - ? $"{escapedValue}" - : $"{StringQuotedChar}{escapedValue}{StringQuotedChar}"; - } - } - - private static bool 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, bool quoted) - { - if (str == null) - return string.Empty; - - var 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 (var 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 < ' ') - { - var escapeBytes = BitConverter.GetBytes((ushort)currentChar); - if (BitConverter.IsLittleEndian == false) - Array.Reverse(escapeBytes); - - builder.Append("\\u") + 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 - var objectDictionary = new Dictionary(); - - if (string.IsNullOrWhiteSpace(_options.TypeSpecifier) == false) - objectDictionary[_options.TypeSpecifier] = targetType; - - foreach (var 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(_options.IncludeNonPublic)?.Invoke(target) - : (field.Value as FieldInfo)?.GetValue(target); - } - catch - { - /* ignored */ - } - } - - return objectDictionary; - } - - private string ResolveDictionary(IDictionary items, int depth) - { - Append(OpenObjectChar, depth); - AppendLine(); - - // Iterate through the elements and output recursively - var writeCount = 0; - foreach (var key in items.Keys) - { - // Serialize and append the key (first char indented) - Append(StringQuotedChar, depth + 1); - Escape(key.ToString(), _builder); - _builder - .Append(StringQuotedChar) - .Append(ValueSeparatorChar) - .Append(" "); - - // Serialize and append the value - var serializedValue = Serialize(items[key], depth + 1, _options); - - if (IsNonEmptyJsonArrayOrObject(serializedValue)) AppendLine(); - Append(serializedValue, 0); - - // Add a comma and start a new line -- We will remove the last one when we are done writing the elements - Append(FieldSeparatorChar, 0); - AppendLine(); - writeCount++; - } - - // Output the end of the object and set the result - RemoveLastComma(); - Append(CloseObjectChar, writeCount > 0 ? depth : 0); - return _builder.ToString(); - } - - private string ResolveObject(object target, int depth) - { - var targetType = target.GetType(); - - if (targetType.IsEnum) - return Convert.ToInt64(target, System.Globalization.CultureInfo.InvariantCulture).ToString(); - - var fields = _options.GetProperties(targetType); - - if (fields.Count == 0 && string.IsNullOrWhiteSpace(_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 - var objectDictionary = CreateDictionary(fields, targetType.ToString(), target); - - return Serialize(objectDictionary, depth, _options); - } - - private string ResolveEnumerable(IEnumerable target, int depth) - { - // Cast the items as a generic object array - var items = target.Cast(); - - Append(OpenArrayChar, depth); - AppendLine(); - - // Iterate through the elements and output recursively - var writeCount = 0; - foreach (var entry in items) - { - var serializedValue = Serialize(entry, depth + 1, _options); - - if (IsNonEmptyJsonArrayOrObject(serializedValue)) - Append(serializedValue, 0); - else - Append(serializedValue, depth + 1); - - Append(FieldSeparatorChar, 0); - AppendLine(); - writeCount++; - } - - // Output the end of the array and set the result - RemoveLastComma(); - Append(CloseArrayChar, writeCount > 0 ? depth : 0); - return _builder.ToString(); - } - - private void SetIndent(int depth) - { - if (_options.Format == false || depth <= 0) return; - - _builder.Append(IndentStrings.GetOrAdd(depth, x => new string(' ', x * 4))); - } - - /// - /// Removes the last comma in the current string builder. - /// - private void RemoveLastComma() - { - if (_builder.Length < _lastCommaSearch.Length) - return; - - if (_lastCommaSearch.Where((t, i) => _builder[_builder.Length - _lastCommaSearch.Length + i] != t).Any()) - { - return; - } - - // If we got this far, we simply remove the comma character - _builder.Remove(_builder.Length - _lastCommaSearch.Length, 1); - } - - private void Append(string text, int depth) - { - SetIndent(depth); - _builder.Append(text); - } - - private void Append(char text, int depth) - { - SetIndent(depth); - _builder.Append(text); - } - - private void AppendLine() - { - if (_options.Format == false) return; - _builder.Append(Environment.NewLine); - } - - #endregion - } - } + .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.Lite/Formatters/Json.SerializerOptions.cs b/Swan.Lite/Formatters/Json.SerializerOptions.cs index ee80f12..ff4c9bf 100644 --- a/Swan.Lite/Formatters/Json.SerializerOptions.cs +++ b/Swan.Lite/Formatters/Json.SerializerOptions.cs @@ -1,144 +1,133 @@ -using System; +#nullable enable +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Reflection; using Swan.Reflection; -namespace Swan.Formatters -{ +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>(); + /// - /// 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. + /// Initializes a new instance of the class. /// - 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( - bool format, - string? typeSpecifier, - string[]? includeProperties, - string[]? excludeProperties = null, - bool includeNonPublic = true, - IReadOnlyCollection? parentReferences = null, - JsonSerializerCase jsonSerializerCase = JsonSerializerCase.None) - { - _includeProperties = includeProperties; - _excludeProperties = excludeProperties; - - IncludeNonPublic = includeNonPublic; - Format = format; - TypeSpecifier = typeSpecifier; - JsonSerializerCase = jsonSerializerCase; - - if (parentReferences == null) - return; - - foreach (var parentReference in parentReferences.Where(x => x.IsAlive)) - { - IsObjectPresent(parentReference.Target); - } - } - - /// - /// Gets a value indicating whether this is format. - /// - /// - /// true if format; otherwise, false. - /// - public bool 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 bool IncludeNonPublic { get; } - - /// - /// Gets the json serializer case. - /// - /// - /// The json serializer case. - /// - public JsonSerializerCase JsonSerializerCase { get; } - - internal bool IsObjectPresent(object target) - { - var hashCode = target.GetHashCode(); - - if (_parentReferences.ContainsKey(hashCode)) - { - if (_parentReferences[hashCode].Any(p => ReferenceEquals(p.Target, target))) - return true; - - _parentReferences[hashCode].Add(new WeakReference(target)); - return false; - } - - _parentReferences.Add(hashCode, new List { new WeakReference(target) }); - return false; - } - - internal Dictionary GetProperties(Type targetType) - => GetPropertiesCache(targetType) - .When(() => _includeProperties?.Length > 0, - query => query.Where(p => _includeProperties.Contains(p.Key.Item1))) - .When(() => _excludeProperties?.Length > 0, - query => query.Where(p => !_excludeProperties.Contains(p.Key.Item1))) - .ToDictionary(x => x.Key.Item2, x => x.Value); - - private Dictionary, MemberInfo> GetPropertiesCache(Type targetType) - { - if (TypeCache.TryGetValue(targetType, out var current)) - return current; - - var 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)); - } - - var value = fields - .ToDictionary( - x => Tuple.Create(x.Name, - x.GetCustomAttribute()?.PropertyName ?? x.Name.GetNameWithCase(JsonSerializerCase)), - x => x); - - TypeCache.TryAdd(targetType, value); - - return value; - } - } + /// 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.Lite/Formatters/Json.cs b/Swan.Lite/Formatters/Json.cs index d214965..c28d76f 100644 --- a/Swan.Lite/Formatters/Json.cs +++ b/Swan.Lite/Formatters/Json.cs @@ -1,379 +1,340 @@ -using System; +#nullable enable +using System; using System.Collections.Generic; using System.Linq; using Swan.Collections; using Swan.Reflection; -namespace Swan.Formatters -{ +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 + /// - /// 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. + /// Serializes the specified object into a JSON string. /// - 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, - bool format = false, - string? typeSpecifier = null, - bool includeNonPublic = false, - string[]? includedNames = null, - params string[] excludedNames) - { - return 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, - bool 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, - bool format, - string? typeSpecifier, - bool includeNonPublic, - string[]? includedNames, - string[]? excludedNames, - List? parentReferences, - JsonSerializerCase jsonSerializerCase) - { - if (obj != null && (obj is string || Definitions.AllBasicValueTypes.Contains(obj.GetType()))) - { - return SerializePrimitiveValue(obj); - } - - var 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, bool 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, bool 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) - => (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, bool includeNonPublic) => (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, bool 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; - - var excludedByAttr = IgnoredPropertiesCache.Retrieve(type, t => t.GetProperties() + /// 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, - bool boolValue => boolValue ? TrueLiteral : FalseLiteral, - _ => obj.ToString() - }; - - #endregion - } + .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.Lite/Formatters/JsonPropertyAttribute.cs b/Swan.Lite/Formatters/JsonPropertyAttribute.cs index d05ac4d..0a569af 100644 --- a/Swan.Lite/Formatters/JsonPropertyAttribute.cs +++ b/Swan.Lite/Formatters/JsonPropertyAttribute.cs @@ -1,39 +1,40 @@ using System; -namespace Swan.Formatters -{ +namespace Swan.Formatters { + /// + /// An attribute used to help setup a property behavior when serialize/deserialize JSON. + /// + /// + [AttributeUsage(AttributeTargets.Property)] + public sealed class JsonPropertyAttribute : Attribute { /// - /// An attribute used to help setup a property behavior when serialize/deserialize JSON. + /// Initializes a new instance of the class. /// - /// - [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, bool ignored = false) - { - PropertyName = propertyName ?? throw new ArgumentNullException(nameof(propertyName)); - 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 bool Ignored { get; } - } + /// 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.Lite/FromString.cs b/Swan.Lite/FromString.cs index 6d920dc..3c10587 100644 --- a/Swan.Lite/FromString.cs +++ b/Swan.Lite/FromString.cs @@ -1,344 +1,302 @@ -using System; +#nullable enable +using System; using System.Collections.Concurrent; using System.ComponentModel; using System.Linq.Expressions; using System.Reflection; -namespace Swan -{ +namespace Swan { + /// + /// Provides a standard way to convert strings to different types. + /// + public static class FromString { + // It doesn't matter which converter we get here: ConvertFromInvariantString is not virtual. + private static readonly MethodInfo ConvertFromInvariantStringMethod = new Func(TypeDescriptor.GetConverter(typeof(Int32)).ConvertFromInvariantString).Method; + + private static readonly MethodInfo? TryConvertToInternalMethod = typeof(FromString).GetMethod(nameof(TryConvertToInternal), BindingFlags.Static | BindingFlags.NonPublic); + + private static readonly MethodInfo? ConvertToInternalMethod = typeof(FromString).GetMethod(nameof(ConvertToInternal), BindingFlags.Static | BindingFlags.NonPublic); + + private static readonly ConcurrentDictionary> GenericTryConvertToMethods = new ConcurrentDictionary>(); + + private static readonly ConcurrentDictionary> GenericConvertToMethods = new ConcurrentDictionary>(); + /// - /// Provides a standard way to convert strings to different types. + /// Determines whether a string can be converted to the specified type. /// - public static class FromString - { - // It doesn't matter which converter we get here: ConvertFromInvariantString is not virtual. - private static readonly MethodInfo ConvertFromInvariantStringMethod - = new Func(TypeDescriptor.GetConverter(typeof(int)).ConvertFromInvariantString).Method; - - private static readonly MethodInfo TryConvertToInternalMethod - = typeof(FromString).GetMethod(nameof(TryConvertToInternal), BindingFlags.Static | BindingFlags.NonPublic); - - private static readonly MethodInfo ConvertToInternalMethod - = typeof(FromString).GetMethod(nameof(ConvertToInternal), BindingFlags.Static | BindingFlags.NonPublic); - - private static readonly ConcurrentDictionary> GenericTryConvertToMethods - = new ConcurrentDictionary>(); - - private static readonly ConcurrentDictionary> GenericConvertToMethods - = new ConcurrentDictionary>(); - - /// - /// Determines whether a string can be converted to the specified type. - /// - /// The type resulting from the conversion. - /// if the conversion is possible; - /// otherwise, . - /// is . - public static bool CanConvertTo(Type type) - => TypeDescriptor.GetConverter(type).CanConvertFrom(typeof(string)); - - /// - /// Determines whether a string can be converted to the specified type. - /// - /// The type resulting from the conversion. - /// if the conversion is possible; - /// otherwise, . - public static bool CanConvertTo() - => TypeDescriptor.GetConverter(typeof(TResult)).CanConvertFrom(typeof(string)); - - /// - /// Attempts to convert a string to the specified type. - /// - /// The type resulting from the conversion. - /// The string to convert. - /// When this method returns , - /// the result of the conversion. This parameter is passed uninitialized. - /// if the conversion is successful; - /// otherwise, . - /// is . - public static bool TryConvertTo(Type type, string str, out object? result) - { - var converter = TypeDescriptor.GetConverter(type); - if (!converter.CanConvertFrom(typeof(string))) - { - result = null; - return false; - } - - try - { - result = converter.ConvertFromInvariantString(str); - return true; - } - catch (Exception e) when (!e.IsCriticalException()) - { - result = null; - return false; - } - } - - /// - /// Attempts to convert a string to the specified type. - /// - /// The type resulting from the conversion. - /// The string to convert. - /// When this method returns , - /// the result of the conversion. This parameter is passed uninitialized. - /// if the conversion is successful; - /// otherwise, . - public static bool TryConvertTo(string str, out TResult result) - { - var converter = TypeDescriptor.GetConverter(typeof(TResult)); - if (!converter.CanConvertFrom(typeof(string))) - { - result = default; - return false; - } - - try - { - result = (TResult)converter.ConvertFromInvariantString(str); - return true; - } - catch (Exception e) when (!e.IsCriticalException()) - { - result = default; - return false; - } - } - - /// - /// Converts a string to the specified type. - /// - /// The type resulting from the conversion. - /// The string to convert. - /// An instance of . - /// is . - /// The conversion was not successful. - public static object ConvertTo(Type type, string str) - { - if (type == null) - throw new ArgumentNullException(nameof(type)); - - try - { - return TypeDescriptor.GetConverter(type).ConvertFromInvariantString(str); - } - catch (Exception e) when (!e.IsCriticalException()) - { - throw new StringConversionException(type, e); - } - } - - /// - /// Converts a string to the specified type. - /// - /// The type resulting from the conversion. - /// The string to convert. - /// An instance of . - /// - /// The conversion was not successful. - /// - public static TResult ConvertTo(string str) - { - try - { - return (TResult)TypeDescriptor.GetConverter(typeof(TResult)).ConvertFromInvariantString(str); - } - catch (Exception e) when (!e.IsCriticalException()) - { - throw new StringConversionException(typeof(TResult), e); - } - } - - /// - /// Attempts to convert an array of strings to an array of the specified type. - /// - /// The type resulting from the conversion of each - /// element of . - /// The array to convert. - /// When this method returns , - /// the result of the conversion. This parameter is passed uninitialized. - /// if the conversion is successful; - /// otherwise, . - /// is . - public static bool TryConvertTo(Type type, string[] strings, out object? result) - { - if (strings == null) - { - result = null; - return false; - } - - var method = GenericTryConvertToMethods.GetOrAdd(type, BuildNonGenericTryConvertLambda); - var (success, methodResult) = method(strings); - result = methodResult; - return success; - } - - /// - /// Attempts to convert an array of strings to an array of the specified type. - /// - /// The type resulting from the conversion of each - /// element of . - /// The array to convert. - /// When this method returns , - /// the result of the conversion. This parameter is passed uninitialized. - /// if the conversion is successful; - /// otherwise, . - public static bool TryConvertTo(string[] strings, out TResult[]? result) - { - if (strings == null) - { - result = null; - return false; - } - - var converter = TypeDescriptor.GetConverter(typeof(TResult)); - if (!converter.CanConvertFrom(typeof(string))) - { - result = null; - return false; - } - - try - { - result = new TResult[strings.Length]; - var i = 0; - foreach (var str in strings) - result[i++] = (TResult)converter.ConvertFromInvariantString(str); - - return true; - } - catch (Exception e) when (!e.IsCriticalException()) - { - result = null; - return false; - } - } - - /// - /// Converts an array of strings to an array of the specified type. - /// - /// The type resulting from the conversion of each - /// element of . - /// The array to convert. - /// An array of . - /// is . - /// The conversion of at least one - /// of the elements of was not successful. - public static object? ConvertTo(Type type, string[] strings) - { - if (strings == null) - return null; - - var method = GenericConvertToMethods.GetOrAdd(type, BuildNonGenericConvertLambda); - return method(strings); - } - - /// - /// Converts an array of strings to an array of the specified type. - /// - /// The type resulting from the conversion of each - /// element of . - /// The array to convert. - /// An array of . - /// The conversion of at least one - /// of the elements of was not successful. - public static TResult[]? ConvertTo(string[] strings) - { - if (strings == null) - return null; - - var converter = TypeDescriptor.GetConverter(typeof(TResult)); - var result = new TResult[strings.Length]; - var i = 0; - try - { - foreach (var str in strings) - result[i++] = (TResult)converter.ConvertFromInvariantString(str); - } - catch (Exception e) when (!e.IsCriticalException()) - { - throw new StringConversionException(typeof(TResult), e); - } - - return result; - } - - /// - /// Converts a expression, if the type can be converted to string, to a new expression including - /// the conversion to string. - /// - /// The type. - /// The string. - /// A new expression where the previous expression is converted to string. - public static Expression? ConvertExpressionTo(Type type, Expression str) - { - var converter = TypeDescriptor.GetConverter(type); - - return converter.CanConvertFrom(typeof(string)) - ? Expression.Convert( - Expression.Call(Expression.Constant(converter), ConvertFromInvariantStringMethod, str), - type) - : null; - } - - private static Func BuildNonGenericTryConvertLambda(Type type) - { - var methodInfo = TryConvertToInternalMethod.MakeGenericMethod(type); - var parameter = Expression.Parameter(typeof(string[])); - var body = Expression.Call(methodInfo, parameter); - var lambda = Expression.Lambda>(body, parameter); - return lambda.Compile(); - } - - private static (bool Success, object? Result) TryConvertToInternal(string[] strings) - { - var converter = TypeDescriptor.GetConverter(typeof(TResult)); - if (!converter.CanConvertFrom(typeof(string))) - return (false, null); - - var result = new TResult[strings.Length]; - var i = 0; - try - { - foreach (var str in strings) - result[i++] = (TResult)converter.ConvertFromInvariantString(str); - - return (true, result); - } - catch (Exception e) when (!e.IsCriticalException()) - { - return (false, null); - } - } - - private static Func BuildNonGenericConvertLambda(Type type) - { - var methodInfo = ConvertToInternalMethod.MakeGenericMethod(type); - var parameter = Expression.Parameter(typeof(string[])); - var body = Expression.Call(methodInfo, parameter); - var lambda = Expression.Lambda>(body, parameter); - return lambda.Compile(); - } - - private static object ConvertToInternal(string[] strings) - { - var converter = TypeDescriptor.GetConverter(typeof(TResult)); - var result = new TResult[strings.Length]; - var i = 0; - try - { - foreach (var str in strings) - result[i++] = (TResult)converter.ConvertFromInvariantString(str); - - return result; - } - catch (Exception e) when (!e.IsCriticalException()) - { - throw new StringConversionException(typeof(TResult), e); - } - } - } + /// The type resulting from the conversion. + /// if the conversion is possible; + /// otherwise, . + /// is . + public static Boolean CanConvertTo(Type type) => TypeDescriptor.GetConverter(type).CanConvertFrom(typeof(String)); + + /// + /// Determines whether a string can be converted to the specified type. + /// + /// The type resulting from the conversion. + /// if the conversion is possible; + /// otherwise, . + public static Boolean CanConvertTo() => TypeDescriptor.GetConverter(typeof(TResult)).CanConvertFrom(typeof(String)); + + /// + /// Attempts to convert a string to the specified type. + /// + /// The type resulting from the conversion. + /// The string to convert. + /// When this method returns , + /// the result of the conversion. This parameter is passed uninitialized. + /// if the conversion is successful; + /// otherwise, . + /// is . + public static Boolean TryConvertTo(Type type, String str, out Object? result) { + TypeConverter converter = TypeDescriptor.GetConverter(type); + if(!converter.CanConvertFrom(typeof(String))) { + result = null; + return false; + } + + try { + result = converter.ConvertFromInvariantString(str); + return true; + } catch(Exception e) when(!e.IsCriticalException()) { + result = null; + return false; + } + } + + /// + /// Attempts to convert a string to the specified type. + /// + /// The type resulting from the conversion. + /// The string to convert. + /// When this method returns , + /// the result of the conversion. This parameter is passed uninitialized. + /// if the conversion is successful; + /// otherwise, . + public static Boolean TryConvertTo(String str, out TResult result) where TResult : notnull { + TypeConverter converter = TypeDescriptor.GetConverter(typeof(TResult)); + if(!converter.CanConvertFrom(typeof(String))) { + result = default!; + return false; + } + + try { + result = (TResult)converter.ConvertFromInvariantString(str); + return true; + } catch(Exception e) when(!e.IsCriticalException()) { + result = default!; + return false; + } + } + + /// + /// Converts a string to the specified type. + /// + /// The type resulting from the conversion. + /// The string to convert. + /// An instance of . + /// is . + /// The conversion was not successful. + public static Object ConvertTo(Type type, String str) { + if(type == null) { + throw new ArgumentNullException(nameof(type)); + } + + try { + return TypeDescriptor.GetConverter(type).ConvertFromInvariantString(str); + } catch(Exception e) when(!e.IsCriticalException()) { + throw new StringConversionException(type, e); + } + } + + /// + /// Converts a string to the specified type. + /// + /// The type resulting from the conversion. + /// The string to convert. + /// An instance of . + /// + /// The conversion was not successful. + /// + public static TResult ConvertTo(String str) { + try { + return (TResult)TypeDescriptor.GetConverter(typeof(TResult)).ConvertFromInvariantString(str); + } catch(Exception e) when(!e.IsCriticalException()) { + throw new StringConversionException(typeof(TResult), e); + } + } + + /// + /// Attempts to convert an array of strings to an array of the specified type. + /// + /// The type resulting from the conversion of each + /// element of . + /// The array to convert. + /// When this method returns , + /// the result of the conversion. This parameter is passed uninitialized. + /// if the conversion is successful; + /// otherwise, . + /// is . + public static Boolean TryConvertTo(Type type, String[] strings, out Object? result) { + if(strings == null) { + result = null; + return false; + } + + Func method = GenericTryConvertToMethods.GetOrAdd(type, BuildNonGenericTryConvertLambda); + (Boolean success, Object methodResult) = method(strings); + result = methodResult; + return success; + } + + /// + /// Attempts to convert an array of strings to an array of the specified type. + /// + /// The type resulting from the conversion of each + /// element of . + /// The array to convert. + /// When this method returns , + /// the result of the conversion. This parameter is passed uninitialized. + /// if the conversion is successful; + /// otherwise, . + public static Boolean TryConvertTo(String[] strings, out TResult[]? result) { + if(strings == null) { + result = null; + return false; + } + + TypeConverter converter = TypeDescriptor.GetConverter(typeof(TResult)); + if(!converter.CanConvertFrom(typeof(String))) { + result = null; + return false; + } + + try { + result = new TResult[strings.Length]; + Int32 i = 0; + foreach(String str in strings) { + result[i++] = (TResult)converter.ConvertFromInvariantString(str); + } + + return true; + } catch(Exception e) when(!e.IsCriticalException()) { + result = null; + return false; + } + } + + /// + /// Converts an array of strings to an array of the specified type. + /// + /// The type resulting from the conversion of each + /// element of . + /// The array to convert. + /// An array of . + /// is . + /// The conversion of at least one + /// of the elements of was not successful. + public static Object? ConvertTo(Type type, String[] strings) { + if(strings == null) { + return null; + } + + Func method = GenericConvertToMethods.GetOrAdd(type, BuildNonGenericConvertLambda); + return method(strings); + } + + /// + /// Converts an array of strings to an array of the specified type. + /// + /// The type resulting from the conversion of each + /// element of . + /// The array to convert. + /// An array of . + /// The conversion of at least one + /// of the elements of was not successful. + public static TResult[]? ConvertTo(String[] strings) { + if(strings == null) { + return null; + } + + TypeConverter converter = TypeDescriptor.GetConverter(typeof(TResult)); + TResult[] result = new TResult[strings.Length]; + Int32 i = 0; + try { + foreach(String str in strings) { + result[i++] = (TResult)converter.ConvertFromInvariantString(str); + } + } catch(Exception e) when(!e.IsCriticalException()) { + throw new StringConversionException(typeof(TResult), e); + } + + return result; + } + + /// + /// Converts a expression, if the type can be converted to string, to a new expression including + /// the conversion to string. + /// + /// The type. + /// The string. + /// A new expression where the previous expression is converted to string. + public static Expression? ConvertExpressionTo(Type type, Expression str) { + TypeConverter converter = TypeDescriptor.GetConverter(type); + + return converter.CanConvertFrom(typeof(String)) + ? Expression.Convert( + Expression.Call(Expression.Constant(converter), ConvertFromInvariantStringMethod, str), + type) + : null; + } + + private static Func BuildNonGenericTryConvertLambda(Type type) { + MethodInfo? methodInfo = TryConvertToInternalMethod?.MakeGenericMethod(type); + ParameterExpression parameter = Expression.Parameter(typeof(String[])); + MethodCallExpression body = Expression.Call(methodInfo, parameter); + Expression> lambda = Expression.Lambda>(body, parameter); + return lambda.Compile(); + } + + private static (Boolean Success, Object? Result) TryConvertToInternal(String[] strings) { + TypeConverter converter = TypeDescriptor.GetConverter(typeof(TResult)); + if(!converter.CanConvertFrom(typeof(String))) { + return (false, null); + } + + TResult[] result = new TResult[strings.Length]; + Int32 i = 0; + try { + foreach(String str in strings) { + result[i++] = (TResult)converter.ConvertFromInvariantString(str); + } + + return (true, result); + } catch(Exception e) when(!e.IsCriticalException()) { + return (false, null); + } + } + + private static Func BuildNonGenericConvertLambda(Type type) { + MethodInfo? methodInfo = ConvertToInternalMethod?.MakeGenericMethod(type); + ParameterExpression parameter = Expression.Parameter(typeof(String[])); + MethodCallExpression body = Expression.Call(methodInfo, parameter); + Expression> lambda = Expression.Lambda>(body, parameter); + return lambda.Compile(); + } + + private static Object ConvertToInternal(String[] strings) { + TypeConverter converter = TypeDescriptor.GetConverter(typeof(TResult)); + TResult[] result = new TResult[strings.Length]; + Int32 i = 0; + try { + foreach(String str in strings) { + result[i++] = (TResult)converter.ConvertFromInvariantString(str); + } + + return result; + } catch(Exception e) when(!e.IsCriticalException()) { + throw new StringConversionException(typeof(TResult), e); + } + } + } } diff --git a/Swan.Lite/Logging/ConsoleLogger.cs b/Swan.Lite/Logging/ConsoleLogger.cs index 38df7c5..6665fe6 100644 --- a/Swan.Lite/Logging/ConsoleLogger.cs +++ b/Swan.Lite/Logging/ConsoleLogger.cs @@ -1,146 +1,139 @@ using System; using Swan.Lite.Logging; -namespace Swan.Logging -{ +namespace Swan.Logging { + /// + /// Represents a Console implementation of ILogger. + /// + /// + public class ConsoleLogger : TextLogger, ILogger { /// - /// Represents a Console implementation of ILogger. + /// Initializes a new instance of the class. /// - /// - 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 - var writer = logEvent.MessageType == LogLevel.Error - ? TerminalWriters.StandardError - : TerminalWriters.StandardOutput; - - var (outputMessage, color) = GetOutputAndColor(logEvent); - - Terminal.Write(outputMessage, color, writer); - } - - /// - public void Dispose() - { - // Do nothing - } - } + 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.Lite/Logging/DebugLogger.cs b/Swan.Lite/Logging/DebugLogger.cs index 4ff26a5..9aeb43f 100644 --- a/Swan.Lite/Logging/DebugLogger.cs +++ b/Swan.Lite/Logging/DebugLogger.cs @@ -1,53 +1,49 @@ -using Swan.Lite.Logging; +using System; +using Swan.Lite.Logging; -namespace Swan.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 { /// - /// Represents a logger target. This target will write to the - /// Debug console using System.Diagnostics.Debug. + /// Initializes a new instance of the class. /// - /// - 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 bool IsDebuggerAttached => System.Diagnostics.Debugger.IsAttached; - - /// - public LogLevel LogLevel { get; set; } = IsDebuggerAttached ? LogLevel.Trace : LogLevel.None; - - /// - public void Log(LogMessageReceivedEventArgs logEvent) - { - var (outputMessage, _) = GetOutputAndColor(logEvent); - - System.Diagnostics.Debug.Write(outputMessage); - } - - /// - public void Dispose() - { - // do nothing - } - } + 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.Lite/Logging/FileLogger.cs b/Swan.Lite/Logging/FileLogger.cs index b131360..53cf982 100644 --- a/Swan.Lite/Logging/FileLogger.cs +++ b/Swan.Lite/Logging/FileLogger.cs @@ -6,129 +6,118 @@ using System.Threading.Tasks; using Swan.Lite.Logging; using Swan.Threading; -namespace Swan.Logging -{ +namespace Swan.Logging { + /// + /// A helper class to write into files the messages sent by the . + /// + /// + public class FileLogger : TextLogger, ILogger { + private readonly ManualResetEventSlim _doneEvent = new ManualResetEventSlim(true); + private readonly ConcurrentQueue _logQueue = new ConcurrentQueue(); + private readonly ExclusiveTimer _timer; + private readonly String _filePath; + + private Boolean _disposedValue; // To detect redundant calls + /// - /// A helper class to write into files the messages sent by the . + /// Initializes a new instance of the class. /// - /// - public class FileLogger : TextLogger, ILogger - { - private readonly ManualResetEventSlim _doneEvent = new ManualResetEventSlim(true); - private readonly ConcurrentQueue _logQueue = new ConcurrentQueue(); - private readonly ExclusiveTimer _timer; - private readonly string _filePath; - - private bool _disposedValue; // To detect redundant calls - - /// - /// Initializes a new instance of the class. - /// - public FileLogger() - : this(SwanRuntime.EntryAssemblyDirectory, true) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The filePath. - /// if set to true [daily file]. - public FileLogger(string filePath, bool dailyFile) - { - _filePath = filePath; - DailyFile = dailyFile; - - _timer = new ExclusiveTimer( - async () => await WriteLogEntries().ConfigureAwait(false), - TimeSpan.Zero, - TimeSpan.FromSeconds(5)); - } - - /// - public LogLevel LogLevel { get; set; } - - /// - /// Gets the file path. - /// - /// - /// The file path. - /// - public string FilePath => DailyFile - ? Path.Combine(Path.GetDirectoryName(_filePath), Path.GetFileNameWithoutExtension(_filePath) + $"_{DateTime.UtcNow:yyyyMMdd}" + Path.GetExtension(_filePath)) - : _filePath; - - /// - /// Gets a value indicating whether [daily file]. - /// - /// - /// true if [daily file]; otherwise, false. - /// - public bool DailyFile { get; } - - /// - public void Log(LogMessageReceivedEventArgs logEvent) - { - var (outputMessage, _) = GetOutputAndColor(logEvent); - - _logQueue.Enqueue(outputMessage); - } - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Releases unmanaged and - optionally - managed resources. - /// - /// true to release both managed and unmanaged resources; false to release only unmanaged resources. - protected virtual void Dispose(bool disposing) - { - if (_disposedValue) return; - - if (disposing) - { - _timer.Pause(); - _timer.Dispose(); - - _doneEvent.Wait(); - _doneEvent.Reset(); - WriteLogEntries(true).Await(); - _doneEvent.Dispose(); - } - - _disposedValue = true; - } - - private async Task WriteLogEntries(bool finalCall = false) - { - if (_logQueue.IsEmpty) - return; - - if (!finalCall && !_doneEvent.IsSet) - return; - - _doneEvent.Reset(); - - try - { - using (var file = File.AppendText(FilePath)) - { - while (!_logQueue.IsEmpty) - { - if (_logQueue.TryDequeue(out var entry)) - await file.WriteAsync(entry).ConfigureAwait(false); - } - } - } - finally - { - if (!finalCall) - _doneEvent.Set(); - } - } - } + public FileLogger() : this(SwanRuntime.EntryAssemblyDirectory, true) { + } + + /// + /// Initializes a new instance of the class. + /// + /// The filePath. + /// if set to true [daily file]. + public FileLogger(String filePath, Boolean dailyFile) { + this._filePath = filePath; + this.DailyFile = dailyFile; + + this._timer = new ExclusiveTimer(async () => await this.WriteLogEntries().ConfigureAwait(false), TimeSpan.Zero, TimeSpan.FromSeconds(5)); + } + + /// + public LogLevel LogLevel { + get; set; + } + + /// + /// Gets the file path. + /// + /// + /// The file path. + /// + public String FilePath => this.DailyFile ? Path.Combine(Path.GetDirectoryName(this._filePath), Path.GetFileNameWithoutExtension(this._filePath) + $"_{DateTime.UtcNow:yyyyMMdd}" + Path.GetExtension(this._filePath)) : this._filePath; + + /// + /// Gets a value indicating whether [daily file]. + /// + /// + /// true if [daily file]; otherwise, false. + /// + public Boolean DailyFile { + get; + } + + /// + public void Log(LogMessageReceivedEventArgs logEvent) { + (String outputMessage, ConsoleColor _) = this.GetOutputAndColor(logEvent); + + this._logQueue.Enqueue(outputMessage); + } + + /// + public void Dispose() { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(Boolean disposing) { + if(this._disposedValue) { + return; + } + + if(disposing) { + this._timer.Pause(); + this._timer.Dispose(); + + this._doneEvent.Wait(); + this._doneEvent.Reset(); + this.WriteLogEntries(true).Await(); + this._doneEvent.Dispose(); + } + + this._disposedValue = true; + } + + private async Task WriteLogEntries(Boolean finalCall = false) { + if(this._logQueue.IsEmpty) { + return; + } + + if(!finalCall && !this._doneEvent.IsSet) { + return; + } + + this._doneEvent.Reset(); + + try { + using StreamWriter file = File.AppendText(this.FilePath); + while(!this._logQueue.IsEmpty) { + if(this._logQueue.TryDequeue(out String entry)) { + await file.WriteAsync(entry).ConfigureAwait(false); + } + } + } finally { + if(!finalCall) { + this._doneEvent.Set(); + } + } + } + } } diff --git a/Swan.Lite/Logging/ILogger.cs b/Swan.Lite/Logging/ILogger.cs index 4dbf6ca..26107a7 100644 --- a/Swan.Lite/Logging/ILogger.cs +++ b/Swan.Lite/Logging/ILogger.cs @@ -1,24 +1,24 @@ -namespace Swan.Logging -{ - using System; - +using System; + +namespace Swan.Logging { + /// + /// Interface for a logger implementation. + /// + public interface ILogger : IDisposable { /// - /// Interface for a logger implementation. + /// Gets the log level. /// - 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); - } + /// + /// 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.Lite/Logging/LogLevel.cs b/Swan.Lite/Logging/LogLevel.cs index 24da6f5..92a9307 100644 --- a/Swan.Lite/Logging/LogLevel.cs +++ b/Swan.Lite/Logging/LogLevel.cs @@ -1,43 +1,41 @@ -namespace Swan -{ +namespace Swan { + /// + /// Defines the log levels. + /// + public enum LogLevel { /// - /// Defines the log levels. + /// The none message type /// - 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, - } + 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.Lite/Logging/LogMessageReceivedEventArgs.cs b/Swan.Lite/Logging/LogMessageReceivedEventArgs.cs index 6614ae4..97e5fa4 100644 --- a/Swan.Lite/Logging/LogMessageReceivedEventArgs.cs +++ b/Swan.Lite/Logging/LogMessageReceivedEventArgs.cs @@ -1,131 +1,147 @@ -using System; +#nullable enable +using System; -namespace Swan -{ +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 { /// - /// Event arguments representing the message that is logged - /// on to the terminal. Use the properties to forward the data to - /// your logger of choice. + /// Initializes a new instance of the class. /// - /// - 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( - ulong sequence, - LogLevel messageType, - DateTime utcDate, - string source, - string message, - object? extendedData, - string callerMemberName, - string callerFilePath, - int callerLineNumber) - { - Sequence = sequence; - MessageType = messageType; - UtcDate = utcDate; - Source = source; - Message = message; - CallerMemberName = callerMemberName; - CallerFilePath = callerFilePath; - CallerLineNumber = callerLineNumber; - ExtendedData = extendedData; - } - - /// - /// Gets logging message sequence. - /// - /// - /// The sequence. - /// - public ulong 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 int 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 => ExtendedData as Exception; - } + /// 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.Lite/Logging/Logger.cs b/Swan.Lite/Logging/Logger.cs index 9a06269..2bedf85 100644 --- a/Swan.Lite/Logging/Logger.cs +++ b/Swan.Lite/Logging/Logger.cs @@ -4,651 +4,420 @@ using System.Linq; using System.Runtime.CompilerServices; using System.Threading.Tasks; -namespace Swan.Logging -{ +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 + /// - /// Entry-point for logging. Use this static class to register/unregister - /// loggers instances. By default, the ConsoleLogger is registered. + /// Registers the logger. /// - public static class Logger - { - private static readonly object SyncLock = new object(); - private static readonly List Loggers = new List(); - - private static ulong _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) - { - var 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] int 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] int 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] int 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] int 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] int 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] int 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] int 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] int 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] int 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] int 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] int 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] int 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] int 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] int 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] int 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] int 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] int 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] int 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] int 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] int 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] int 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] int 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] int callerLineNumber = 0) - { - if (obj == null) return; - var 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] int callerLineNumber = 0) - { - if (obj == null) return; - var 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) - { - var 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, - int callerLineNumber) - { - var sequence = _loggingSequence; - var date = DateTime.UtcNow; - _loggingSequence++; - - var loggerMessage = string.IsNullOrWhiteSpace(message) ? - string.Empty : message.RemoveControlCharsExcept('\n'); - - var eventArgs = new LogMessageReceivedEventArgs( - sequence, - logLevel, - date, - sourceName, - loggerMessage, - extendedData, - callerMemberName, - callerFilePath, - callerLineNumber); - - foreach (var logger in Loggers) - { - Task.Run(() => - { - if (logger.LogLevel <= logLevel) - logger.Log(eventArgs); - }); - } - } - } + /// 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.Lite/Logging/TextLogger.cs b/Swan.Lite/Logging/TextLogger.cs index b087592..dcf97e8 100644 --- a/Swan.Lite/Logging/TextLogger.cs +++ b/Swan.Lite/Logging/TextLogger.cs @@ -1,89 +1,63 @@ using Swan.Logging; using System; -namespace Swan.Lite.Logging -{ +namespace Swan.Lite.Logging { + /// + /// Use this class for text-based logger. + /// + public abstract class TextLogger { /// - /// Use this class for text-based logger. + /// Gets or sets the logging time format. + /// set to null or empty to prevent output. /// - 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) - { - var (prefix , color) = GetConsoleColorAndPrefix(logEvent.MessageType); - - var loggerMessage = string.IsNullOrWhiteSpace(logEvent.Message) - ? string.Empty - : logEvent.Message.RemoveControlCharsExcept('\n'); - - var 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) - { - switch (messageType) - { - case LogLevel.Debug: - return (ConsoleLogger.DebugPrefix, ConsoleLogger.DebugColor); - case LogLevel.Error: - return (ConsoleLogger.ErrorPrefix, ConsoleLogger.ErrorColor); - case LogLevel.Info: - return (ConsoleLogger.InfoPrefix, ConsoleLogger.InfoColor); - case LogLevel.Trace: - return (ConsoleLogger.TracePrefix, ConsoleLogger.TraceColor); - case LogLevel.Warning: - return (ConsoleLogger.WarnPrefix, ConsoleLogger.WarnColor); - case LogLevel.Fatal: - return (ConsoleLogger.FatalPrefix, ConsoleLogger.FatalColor); - default: - return (new string(' ', ConsoleLogger.InfoPrefix.Length), Terminal.Settings.DefaultColor); - } - } - - private static string CreateOutputMessage(string sourceName, string loggerMessage, string prefix, DateTime date) - { - var friendlySourceName = string.IsNullOrWhiteSpace(sourceName) - ? string.Empty - : sourceName.SliceLength(sourceName.LastIndexOf('.') + 1, sourceName.Length); - - var outputMessage = string.IsNullOrWhiteSpace(sourceName) - ? loggerMessage - : $"[{friendlySourceName}] {loggerMessage}"; - - return string.IsNullOrWhiteSpace(LoggingTimeFormat) - ? $" {prefix} >> {outputMessage}{Environment.NewLine}" - : $" {date.ToLocalTime().ToString(LoggingTimeFormat)} {prefix} >> {outputMessage}{Environment.NewLine}"; - } - } + /// + /// 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.Lite/Mappers/CopyableAttribute.cs b/Swan.Lite/Mappers/CopyableAttribute.cs index 4f04954..c4af960 100644 --- a/Swan.Lite/Mappers/CopyableAttribute.cs +++ b/Swan.Lite/Mappers/CopyableAttribute.cs @@ -1,13 +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 - { - } +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.Lite/Mappers/IObjectMap.cs b/Swan.Lite/Mappers/IObjectMap.cs index 77a3c1b..040ba54 100644 --- a/Swan.Lite/Mappers/IObjectMap.cs +++ b/Swan.Lite/Mappers/IObjectMap.cs @@ -2,26 +2,30 @@ using System.Collections.Generic; using System.Reflection; -namespace Swan.Mappers -{ +namespace Swan.Mappers { + /// + /// Interface object map. + /// + public interface IObjectMap { /// - /// Interface object map. + /// Gets or sets the 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; } - } + 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.Lite/Mappers/ObjectMap.cs b/Swan.Lite/Mappers/ObjectMap.cs index edc59b0..e30549a 100644 --- a/Swan.Lite/Mappers/ObjectMap.cs +++ b/Swan.Lite/Mappers/ObjectMap.cs @@ -4,112 +4,106 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; -namespace Swan.Mappers -{ +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; + } + /// - /// Represents an object map. + /// Maps the property. /// - /// The type of the source. - /// The type of the destination. - /// - public class ObjectMap : IObjectMap - { - internal ObjectMap(IEnumerable intersect) - { - SourceType = typeof(TSource); - DestinationType = typeof(TDestination); - Map = intersect.ToDictionary( - property => DestinationType.GetProperty(property.Name), - property => new List {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) - { - var propertyDestinationInfo = (destinationProperty.Body as MemberExpression)?.Member as PropertyInfo; - - if (propertyDestinationInfo == null) - { - throw new ArgumentException("Invalid destination expression", nameof(destinationProperty)); - } - - var sourceMembers = GetSourceMembers(sourceProperty); - - if (sourceMembers.Any() == false) - { - throw new ArgumentException("Invalid source expression", nameof(sourceProperty)); - } - - // reverse order - sourceMembers.Reverse(); - 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) - { - var propertyDestinationInfo = (destinationProperty.Body as MemberExpression)?.Member as PropertyInfo; - - if (propertyDestinationInfo == null) - throw new ArgumentException("Invalid destination expression", nameof(destinationProperty)); - - if (Map.ContainsKey(propertyDestinationInfo)) - { - Map.Remove(propertyDestinationInfo); - } - - return this; - } - - private static List GetSourceMembers(Expression> sourceProperty) - { - var sourceMembers = new List(); - var initialExpression = sourceProperty.Body as MemberExpression; - - while (true) - { - var propertySourceInfo = initialExpression?.Member as PropertyInfo; - - if (propertySourceInfo == null) break; - sourceMembers.Add(propertySourceInfo); - initialExpression = initialExpression.Expression as MemberExpression; - } - - return sourceMembers; - } - } + /// 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.Lite/Mappers/ObjectMapper.PropertyInfoComparer.cs b/Swan.Lite/Mappers/ObjectMapper.PropertyInfoComparer.cs index bd5f1a7..02c1dda 100644 --- a/Swan.Lite/Mappers/ObjectMapper.PropertyInfoComparer.cs +++ b/Swan.Lite/Mappers/ObjectMapper.PropertyInfoComparer.cs @@ -1,24 +1,20 @@ -using System.Collections.Generic; +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 bool Equals(PropertyInfo x, PropertyInfo y) - => x != null && y != null && x.Name == y.Name && x.PropertyType == y.PropertyType; - - public int GetHashCode(PropertyInfo obj) - => obj.Name.GetHashCode() + obj.PropertyType.Name.GetHashCode(); - } - } +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.Lite/Mappers/ObjectMapper.cs b/Swan.Lite/Mappers/ObjectMapper.cs index a2c3ce7..c7360b4 100644 --- a/Swan.Lite/Mappers/ObjectMapper.cs +++ b/Swan.Lite/Mappers/ObjectMapper.cs @@ -1,372 +1,309 @@ -using System; +#nullable enable +using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; using Swan.Reflection; -namespace Swan.Mappers -{ +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(); + /// - /// 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. + /// Gets the current. /// - /// - /// 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 int 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 int 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 (_maps.Any(x => x.SourceType == typeof(TSource) && x.DestinationType == typeof(TDestination))) - throw new InvalidOperationException("You can't create an existing map"); - - var sourceType = PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties(true); - var destinationType = PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties(true); - - var intersect = sourceType.Intersect(destinationType, new PropertyInfoComparer()).ToArray(); - - if (!intersect.Any()) - throw new InvalidOperationException("Types doesn't match"); - - var map = new ObjectMap(intersect); - - _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, bool autoResolve = true) - { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - var destination = Activator.CreateInstance(); - var map = _maps - .FirstOrDefault(x => x.SourceType == source.GetType() && x.DestinationType == typeof(TDestination)); - - if (map != null) - { - foreach (var property in map.Map) - { - var 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 int CopyInternal( - object target, - Dictionary> sourceProperties, - IEnumerable? propertiesToCopy, - IEnumerable? ignoreProperties) - { - // Filter properties - var requiredProperties = propertiesToCopy? - .Where(p => !string.IsNullOrWhiteSpace(p)) - .Select(p => p.ToLowerInvariant()); - - var ignoredProperties = ignoreProperties? - .Where(p => !string.IsNullOrWhiteSpace(p)) - .Select(p => p.ToLowerInvariant()); - - var 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 bool TrySetValue(PropertyInfo propertyInfo, Tuple property, object target) - { - try - { - var (type, 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: - var addMethod = targetType.GetMethods() - .FirstOrDefault( - m => m.Name == Formatters.Json.AddMethodName && m.IsPublic && m.GetParameters().Length == 1); - - if (addMethod == null) return target; - - var isItemValueType = targetList.GetType().GetElementType().IsValueType; - - foreach (var item in sourceList) - { - try - { - targetList.Add(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 - var 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))); - } - } + /// + /// 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.Lite/ObjectComparer.cs b/Swan.Lite/ObjectComparer.cs index a00a3f3..8b17a54 100644 --- a/Swan.Lite/ObjectComparer.cs +++ b/Swan.Lite/ObjectComparer.cs @@ -5,189 +5,176 @@ using System.Linq; using System.Reflection; using Swan.Reflection; -namespace Swan -{ +namespace Swan { + /// + /// Represents a quick object comparer using the public properties of an object + /// or the public members in a structure. + /// + public static class ObjectComparer { /// - /// Represents a quick object comparer using the public properties of an object - /// or the public members in a structure. + /// Compare if two variables of the same type are equal. /// - public static class ObjectComparer - { - /// - /// Compare if two variables of the same type are equal. - /// - /// The type of objects to compare. - /// The left. - /// The right. - /// true if the variables are equal; otherwise, false. - public static bool AreEqual(T left, T right) => AreEqual(left, right, typeof(T)); - - /// - /// Compare if two variables of the same type are equal. - /// - /// The left. - /// The right. - /// Type of the target. - /// - /// true if the variables are equal; otherwise, false. - /// - /// targetType. - public static bool AreEqual(object left, object right, Type targetType) - { - if (targetType == null) - throw new ArgumentNullException(nameof(targetType)); - - if (Definitions.BasicTypesInfo.Value.ContainsKey(targetType)) - return Equals(left, right); - - return targetType.IsValueType || targetType.IsArray - ? AreStructsEqual(left, right, targetType) - : AreObjectsEqual(left, right, targetType); - } - - /// - /// Compare if two objects of the same type are equal. - /// - /// The type of objects to compare. - /// The left. - /// The right. - /// true if the objects are equal; otherwise, false. - public static bool AreObjectsEqual(T left, T right) - where T : class - { - return AreObjectsEqual(left, right, typeof(T)); - } - - /// - /// Compare if two objects of the same type are equal. - /// - /// The left. - /// The right. - /// Type of the target. - /// true if the objects are equal; otherwise, false. - /// targetType. - public static bool AreObjectsEqual(object left, object right, Type targetType) - { - if (targetType == null) - throw new ArgumentNullException(nameof(targetType)); - - var properties = PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties(targetType).ToArray(); - - foreach (var propertyTarget in properties) - { - var targetPropertyGetMethod = propertyTarget.GetCacheGetMethod(); - - if (propertyTarget.PropertyType.IsArray) - { - var leftObj = targetPropertyGetMethod(left) as IEnumerable; - var rightObj = targetPropertyGetMethod(right) as IEnumerable; - - if (!AreEnumerationsEquals(leftObj, rightObj)) - return false; - } - else - { - if (!Equals(targetPropertyGetMethod(left), targetPropertyGetMethod(right))) - return false; - } - } - - return true; - } - - /// - /// Compare if two structures of the same type are equal. - /// - /// The type of structs to compare. - /// The left. - /// The right. - /// true if the structs are equal; otherwise, false. - public static bool AreStructsEqual(T left, T right) - where T : struct - { - return AreStructsEqual(left, right, typeof(T)); - } - - /// - /// Compare if two structures of the same type are equal. - /// - /// The left. - /// The right. - /// Type of the target. - /// - /// true if the structs are equal; otherwise, false. - /// - /// targetType. - public static bool AreStructsEqual(object left, object right, Type targetType) - { - if (targetType == null) - throw new ArgumentNullException(nameof(targetType)); - - var fields = new List(FieldTypeCache.DefaultCache.Value.RetrieveAllFields(targetType)) - .Union(PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties(targetType)); - - foreach (var targetMember in fields) - { - switch (targetMember) - { - case FieldInfo field: - if (Equals(field.GetValue(left), field.GetValue(right)) == false) - return false; - break; - case PropertyInfo property: - var targetPropertyGetMethod = property.GetCacheGetMethod(); - - if (targetPropertyGetMethod != null && - !Equals(targetPropertyGetMethod(left), targetPropertyGetMethod(right))) - return false; - break; - } - } - - return true; - } - - /// - /// Compare if two enumerables are equal. - /// - /// The type of enums to compare. - /// The left. - /// The right. - /// - /// true if two specified types are equal; otherwise, false. - /// - /// - /// left - /// or - /// right. - /// - public static bool AreEnumerationsEquals(T left, T right) - where T : IEnumerable - { - if (Equals(left, default(T))) - throw new ArgumentNullException(nameof(left)); - - if (Equals(right, default(T))) - throw new ArgumentNullException(nameof(right)); - - var leftEnumerable = left.Cast().ToArray(); - var rightEnumerable = right.Cast().ToArray(); - - if (leftEnumerable.Length != rightEnumerable.Length) - return false; - - for (var i = 0; i < leftEnumerable.Length; i++) - { - var leftEl = leftEnumerable[i]; - var rightEl = rightEnumerable[i]; - - if (!AreEqual(leftEl, rightEl, leftEl.GetType())) - { - return false; - } - } - - return true; - } - } + /// The type of objects to compare. + /// The left. + /// The right. + /// true if the variables are equal; otherwise, false. + public static Boolean AreEqual(T left, T right) => AreEqual(left, right, typeof(T)); + + /// + /// Compare if two variables of the same type are equal. + /// + /// The left. + /// The right. + /// Type of the target. + /// + /// true if the variables are equal; otherwise, false. + /// + /// targetType. + public static Boolean AreEqual(Object left, Object right, Type targetType) { + if(targetType == null) { + throw new ArgumentNullException(nameof(targetType)); + } + + if(Definitions.BasicTypesInfo.Value.ContainsKey(targetType)) { + return Equals(left, right); + } + + return targetType.IsValueType || targetType.IsArray ? AreStructsEqual(left, right, targetType) : AreObjectsEqual(left, right, targetType); + } + + /// + /// Compare if two objects of the same type are equal. + /// + /// The type of objects to compare. + /// The left. + /// The right. + /// true if the objects are equal; otherwise, false. + public static Boolean AreObjectsEqual(T left, T right) where T : class => AreObjectsEqual(left, right, typeof(T)); + + /// + /// Compare if two objects of the same type are equal. + /// + /// The left. + /// The right. + /// Type of the target. + /// true if the objects are equal; otherwise, false. + /// targetType. + public static Boolean AreObjectsEqual(Object left, Object right, Type targetType) { + if(targetType == null) { + throw new ArgumentNullException(nameof(targetType)); + } + + PropertyInfo[] properties = PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties(targetType).ToArray(); + + foreach(PropertyInfo propertyTarget in properties) { + Func targetPropertyGetMethod = propertyTarget.GetCacheGetMethod(); + + if(propertyTarget.PropertyType.IsArray) { + IEnumerable leftObj = targetPropertyGetMethod(left) as IEnumerable; + IEnumerable rightObj = targetPropertyGetMethod(right) as IEnumerable; + + if(!AreEnumerationsEquals(leftObj, rightObj)) { + return false; + } + } else { + if(!Equals(targetPropertyGetMethod(left), targetPropertyGetMethod(right))) { + return false; + } + } + } + + return true; + } + + /// + /// Compare if two structures of the same type are equal. + /// + /// The type of structs to compare. + /// The left. + /// The right. + /// true if the structs are equal; otherwise, false. + public static Boolean AreStructsEqual(T left, T right) where T : struct => AreStructsEqual(left, right, typeof(T)); + + /// + /// Compare if two structures of the same type are equal. + /// + /// The left. + /// The right. + /// Type of the target. + /// + /// true if the structs are equal; otherwise, false. + /// + /// targetType. + public static Boolean AreStructsEqual(Object left, Object right, Type targetType) { + if(targetType == null) { + throw new ArgumentNullException(nameof(targetType)); + } + + IEnumerable fields = new List(FieldTypeCache.DefaultCache.Value.RetrieveAllFields(targetType)).Union(PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties(targetType)); + + foreach(MemberInfo targetMember in fields) { + switch(targetMember) { + case FieldInfo field: + if(Equals(field.GetValue(left), field.GetValue(right)) == false) { + return false; + } + + break; + case PropertyInfo property: + Func targetPropertyGetMethod = property.GetCacheGetMethod(); + + if(targetPropertyGetMethod != null && + !Equals(targetPropertyGetMethod(left), targetPropertyGetMethod(right))) { + return false; + } + + break; + } + } + + return true; + } + + /// + /// Compare if two enumerables are equal. + /// + /// The type of enums to compare. + /// The left. + /// The right. + /// + /// true if two specified types are equal; otherwise, false. + /// + /// + /// left + /// or + /// right. + /// + public static Boolean AreEnumerationsEquals(T left, T right) where T : IEnumerable { + if(Equals(left, default(T))) { + throw new ArgumentNullException(nameof(left)); + } + + if(Equals(right, default(T))) { + throw new ArgumentNullException(nameof(right)); + } + + Object[] leftEnumerable = left.Cast().ToArray(); + Object[] rightEnumerable = right.Cast().ToArray(); + + if(leftEnumerable.Length != rightEnumerable.Length) { + return false; + } + + for(Int32 i = 0; i < leftEnumerable.Length; i++) { + Object leftEl = leftEnumerable[i]; + Object rightEl = rightEnumerable[i]; + + if(!AreEqual(leftEl, rightEl, leftEl.GetType())) { + return false; + } + } + + return true; + } + } } \ No newline at end of file diff --git a/Swan.Lite/Paginator.cs b/Swan.Lite/Paginator.cs index 7be3e43..9f5c34b 100644 --- a/Swan.Lite/Paginator.cs +++ b/Swan.Lite/Paginator.cs @@ -1,99 +1,85 @@ using System; -namespace Swan -{ +namespace Swan { + /// + /// A utility class to compute paging or batching offsets. + /// + public class Paginator { /// - /// A utility class to compute paging or batching offsets. + /// Initializes a new instance of the class. /// - public class Paginator - { - /// - /// Initializes a new instance of the class. - /// - /// The total count of items to page over. - /// The desired size of individual pages. - public Paginator(int totalCount, int pageSize) - { - TotalCount = totalCount; - PageSize = pageSize; - PageCount = ComputePageCount(); - } - - /// - /// Gets the desired number of items per page. - /// - public int PageSize { get; } - - /// - /// Gets the total number of items to page over. - /// - public int TotalCount { get; } - - /// - /// Gets the computed number of pages. - /// - public int PageCount { get; } - - /// - /// Gets the start item index of the given page. - /// - /// Zero-based index of the page. - /// The start item index. - public int GetFirstItemIndex(int pageIndex) - { - pageIndex = FixPageIndex(pageIndex); - return pageIndex * PageSize; - } - - /// - /// Gets the end item index of the given page. - /// - /// Zero-based index of the page. - /// The end item index. - public int GetLastItemIndex(int pageIndex) - { - var startIndex = GetFirstItemIndex(pageIndex); - return Math.Min(startIndex + PageSize - 1, TotalCount - 1); - } - - /// - /// Gets the item count of the given page index. - /// - /// Zero-based index of the page. - /// The number of items that the page contains. - public int GetItemCount(int pageIndex) - { - pageIndex = FixPageIndex(pageIndex); - return (pageIndex >= PageCount - 1) - ? GetLastItemIndex(pageIndex) - GetFirstItemIndex(pageIndex) + 1 - : PageSize; - } - - /// - /// Fixes the index of the page by applying bound logic. - /// - /// Index of the page. - /// A limit-bound index. - private int FixPageIndex(int pageIndex) - { - if (pageIndex < 0) return 0; - - return pageIndex >= PageCount ? PageCount - 1 : pageIndex; - } - - /// - /// Computes the number of pages for the paginator. - /// - /// The page count. - private int ComputePageCount() - { - // include this if when you always want at least 1 page - if (TotalCount == 0) - return 0; - - return TotalCount % PageSize != 0 - ? (TotalCount / PageSize) + 1 - : TotalCount / PageSize; - } - } + /// The total count of items to page over. + /// The desired size of individual pages. + public Paginator(Int32 totalCount, Int32 pageSize) { + this.TotalCount = totalCount; + this.PageSize = pageSize; + this.PageCount = this.ComputePageCount(); + } + + /// + /// Gets the desired number of items per page. + /// + public Int32 PageSize { + get; + } + + /// + /// Gets the total number of items to page over. + /// + public Int32 TotalCount { + get; + } + + /// + /// Gets the computed number of pages. + /// + public Int32 PageCount { + get; + } + + /// + /// Gets the start item index of the given page. + /// + /// Zero-based index of the page. + /// The start item index. + public Int32 GetFirstItemIndex(Int32 pageIndex) { + pageIndex = this.FixPageIndex(pageIndex); + return pageIndex * this.PageSize; + } + + /// + /// Gets the end item index of the given page. + /// + /// Zero-based index of the page. + /// The end item index. + public Int32 GetLastItemIndex(Int32 pageIndex) { + Int32 startIndex = this.GetFirstItemIndex(pageIndex); + return Math.Min(startIndex + this.PageSize - 1, this.TotalCount - 1); + } + + /// + /// Gets the item count of the given page index. + /// + /// Zero-based index of the page. + /// The number of items that the page contains. + public Int32 GetItemCount(Int32 pageIndex) { + pageIndex = this.FixPageIndex(pageIndex); + return (pageIndex >= this.PageCount - 1) ? this.GetLastItemIndex(pageIndex) - this.GetFirstItemIndex(pageIndex) + 1 : this.PageSize; + } + + /// + /// Fixes the index of the page by applying bound logic. + /// + /// Index of the page. + /// A limit-bound index. + private Int32 FixPageIndex(Int32 pageIndex) => pageIndex < 0 ? 0 : pageIndex >= this.PageCount ? this.PageCount - 1 : pageIndex; + + /// + /// Computes the number of pages for the paginator. + /// + /// The page count. + private Int32 ComputePageCount() => + // include this if when you always want at least 1 page + this.TotalCount == 0 ? 0 : this.TotalCount % this.PageSize != 0 ? (this.TotalCount / this.PageSize) + 1 : this.TotalCount / this.PageSize; + } } diff --git a/Swan.Lite/Parsers/ArgumentOptionAttribute.cs b/Swan.Lite/Parsers/ArgumentOptionAttribute.cs index 1353243..7ecda76 100644 --- a/Swan.Lite/Parsers/ArgumentOptionAttribute.cs +++ b/Swan.Lite/Parsers/ArgumentOptionAttribute.cs @@ -1,102 +1,105 @@ using System; -namespace Swan.Parsers -{ +namespace Swan.Parsers { + /// + /// Models an option specification. + /// Based on CommandLine (Copyright 2005-2015 Giacomo Stelluti Scala and Contributors.). + /// + [AttributeUsage(AttributeTargets.Property)] + public sealed class ArgumentOptionAttribute + : Attribute { /// - /// Models an option specification. - /// Based on CommandLine (Copyright 2005-2015 Giacomo Stelluti Scala and Contributors.). + /// Initializes a new instance of the class. + /// The default long name will be inferred from target property. /// - [AttributeUsage(AttributeTargets.Property)] - public sealed class ArgumentOptionAttribute - : Attribute - { - /// - /// Initializes a new instance of the class. - /// The default long name will be inferred from target property. - /// - public ArgumentOptionAttribute() - : this(string.Empty, string.Empty) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The long name of the option. - public ArgumentOptionAttribute(string longName) - : this(string.Empty, longName) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The short name of the option. - /// The long name of the option or null if not used. - public ArgumentOptionAttribute(char shortName, string longName) - : this(new string(shortName, 1), longName) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The short name of the option.. - public ArgumentOptionAttribute(char shortName) - : this(new string(shortName, 1), string.Empty) - { - } - - private ArgumentOptionAttribute(string shortName, string longName) - { - ShortName = shortName ?? throw new ArgumentNullException(nameof(shortName)); - LongName = longName ?? throw new ArgumentNullException(nameof(longName)); - } - - /// - /// Gets long name of this command line option. This name is usually a single English word. - /// - /// - /// The long name. - /// - public string LongName { get; } - - /// - /// Gets a short name of this command line option, made of one character. - /// - /// - /// The short name. - /// - public string ShortName { get; } - - /// - /// When applying attribute to target properties, - /// it allows you to split an argument and consume its content as a sequence. - /// - public char Separator { get; set; } = '\0'; - - /// - /// Gets or sets mapped property default value. - /// - /// - /// The default value. - /// - public object DefaultValue { get; set; } - - /// - /// Gets or sets a value indicating whether a command line option is required. - /// - /// - /// true if required; otherwise, false. - /// - public bool Required { get; set; } - - /// - /// Gets or sets a short description of this command line option. Usually a sentence summary. - /// - /// - /// The help text. - /// - public string HelpText { get; set; } - } + public ArgumentOptionAttribute() + : this(String.Empty, String.Empty) { + } + + /// + /// Initializes a new instance of the class. + /// + /// The long name of the option. + public ArgumentOptionAttribute(String longName) + : this(String.Empty, longName) { + } + + /// + /// Initializes a new instance of the class. + /// + /// The short name of the option. + /// The long name of the option or null if not used. + public ArgumentOptionAttribute(Char shortName, String longName) + : this(new String(shortName, 1), longName) { + } + + /// + /// Initializes a new instance of the class. + /// + /// The short name of the option.. + public ArgumentOptionAttribute(Char shortName) + : this(new String(shortName, 1), String.Empty) { + } + + private ArgumentOptionAttribute(String shortName, String longName) { + this.ShortName = shortName ?? throw new ArgumentNullException(nameof(shortName)); + this.LongName = longName ?? throw new ArgumentNullException(nameof(longName)); + } + + /// + /// Gets long name of this command line option. This name is usually a single English word. + /// + /// + /// The long name. + /// + public String LongName { + get; + } + + /// + /// Gets a short name of this command line option, made of one character. + /// + /// + /// The short name. + /// + public String ShortName { + get; + } + + /// + /// When applying attribute to target properties, + /// it allows you to split an argument and consume its content as a sequence. + /// + public Char Separator { get; set; } = '\0'; + + /// + /// Gets or sets mapped property default value. + /// + /// + /// The default value. + /// + public Object DefaultValue { + get; set; + } + + /// + /// Gets or sets a value indicating whether a command line option is required. + /// + /// + /// true if required; otherwise, false. + /// + public Boolean Required { + get; set; + } + + /// + /// Gets or sets a short description of this command line option. Usually a sentence summary. + /// + /// + /// The help text. + /// + public String HelpText { + get; set; + } + } } \ No newline at end of file diff --git a/Swan.Lite/Parsers/ArgumentParse.Validator.cs b/Swan.Lite/Parsers/ArgumentParse.Validator.cs index 655ddba..71622ca 100644 --- a/Swan.Lite/Parsers/ArgumentParse.Validator.cs +++ b/Swan.Lite/Parsers/ArgumentParse.Validator.cs @@ -4,157 +4,126 @@ using System.Linq; using System.Reflection; using Swan.Reflection; -namespace Swan.Parsers -{ - /// - /// Provides methods to parse command line arguments. - /// - /// Based on CommandLine (Copyright 2005-2015 Giacomo Stelluti Scala and Contributors). - /// - public partial class ArgumentParser - { - private sealed class Validator - { - private readonly object _instance; - private readonly IEnumerable _args; - private readonly List _updatedList = new List(); - private readonly ArgumentParserSettings _settings; - - private readonly PropertyInfo[] _properties; - - public Validator( - PropertyInfo[] properties, - IEnumerable args, - object instance, - ArgumentParserSettings settings) - { - _args = args; - _instance = instance; - _settings = settings; - _properties = properties; - - PopulateInstance(); - SetDefaultValues(); - GetRequiredList(); - } - - public List UnknownList { get; } = new List(); - public List RequiredList { get; } = new List(); - - public bool IsValid() => (_settings.IgnoreUnknownArguments || !UnknownList.Any()) && !RequiredList.Any(); - - public IEnumerable GetPropertiesOptions() - => _properties.Select(p => AttributeCache.DefaultCache.Value.RetrieveOne(p)) - .Where(x => x != null); - - private void GetRequiredList() - { - foreach (var targetProperty in _properties) - { - var optionAttr = AttributeCache.DefaultCache.Value.RetrieveOne(targetProperty); - - if (optionAttr == null || optionAttr.Required == false) - continue; - - if (targetProperty.GetValue(_instance) == null) - { - RequiredList.Add(optionAttr.LongName ?? optionAttr.ShortName); - } - } - } - - private void SetDefaultValues() - { - foreach (var targetProperty in _properties.Except(_updatedList)) - { - var optionAttr = AttributeCache.DefaultCache.Value.RetrieveOne(targetProperty); - - var defaultValue = optionAttr?.DefaultValue; - - if (defaultValue == null) - continue; - - if (SetPropertyValue(targetProperty, defaultValue.ToString(), _instance, optionAttr)) - _updatedList.Add(targetProperty); - } - } - - private void PopulateInstance() - { - const char dash = '-'; - var propertyName = string.Empty; - - foreach (var arg in _args) - { - var ignoreSetValue = string.IsNullOrWhiteSpace(propertyName); - - if (ignoreSetValue) - { - if (string.IsNullOrWhiteSpace(arg) || arg[0] != dash) continue; - - propertyName = arg.Substring(1); - - if (!string.IsNullOrWhiteSpace(propertyName) && propertyName[0] == dash) - propertyName = propertyName.Substring(1); - } - - var targetProperty = TryGetProperty(propertyName); - - if (targetProperty == null) - { - // Skip if the property is not found - UnknownList.Add(propertyName); - continue; - } - - if (!ignoreSetValue && SetPropertyValue(targetProperty, arg, _instance)) - { - _updatedList.Add(targetProperty); - propertyName = string.Empty; - } - else if (targetProperty.PropertyType == typeof(bool)) - { - // If the arg is a boolean property set it to true. - targetProperty.SetValue(_instance, true); - - _updatedList.Add(targetProperty); - propertyName = string.Empty; - } - } - - if (!string.IsNullOrEmpty(propertyName)) - { - UnknownList.Add(propertyName); - } - } - - private bool SetPropertyValue( - PropertyInfo targetProperty, - string propertyValueString, - object result, - ArgumentOptionAttribute optionAttr = null) - { - if (!targetProperty.PropertyType.IsEnum) - { - return targetProperty.PropertyType.IsArray - ? targetProperty.TrySetArray(propertyValueString.Split(optionAttr?.Separator ?? ','), result) - : targetProperty.TrySetBasicType(propertyValueString, result); - } - - var parsedValue = Enum.Parse( - targetProperty.PropertyType, - propertyValueString, - _settings.CaseInsensitiveEnumValues); - - targetProperty.SetValue(result, Enum.ToObject(targetProperty.PropertyType, parsedValue)); - - return true; - } - - private PropertyInfo TryGetProperty(string propertyName) - => _properties.FirstOrDefault(p => - string.Equals(AttributeCache.DefaultCache.Value.RetrieveOne(p)?.LongName, propertyName, _settings.NameComparer) || - string.Equals(AttributeCache.DefaultCache.Value.RetrieveOne(p)?.ShortName, propertyName, _settings.NameComparer)); - } - } +namespace Swan.Parsers { + /// + /// Provides methods to parse command line arguments. + /// + /// Based on CommandLine (Copyright 2005-2015 Giacomo Stelluti Scala and Contributors). + /// + public partial class ArgumentParser { + private sealed class Validator { + private readonly Object _instance; + private readonly IEnumerable _args; + private readonly List _updatedList = new List(); + private readonly ArgumentParserSettings _settings; + + private readonly PropertyInfo[] _properties; + + public Validator(PropertyInfo[] properties, IEnumerable args, Object instance, ArgumentParserSettings settings) { + this._args = args; + this._instance = instance; + this._settings = settings; + this._properties = properties; + + this.PopulateInstance(); + this.SetDefaultValues(); + this.GetRequiredList(); + } + + public List UnknownList { get; } = new List(); + public List RequiredList { get; } = new List(); + + public Boolean IsValid() => (this._settings.IgnoreUnknownArguments || !this.UnknownList.Any()) && !this.RequiredList.Any(); + + public IEnumerable GetPropertiesOptions() => this._properties.Select(p => AttributeCache.DefaultCache.Value.RetrieveOne(p)).Where(x => x != null); + + private void GetRequiredList() { + foreach(PropertyInfo targetProperty in this._properties) { + ArgumentOptionAttribute optionAttr = AttributeCache.DefaultCache.Value.RetrieveOne(targetProperty); + + if(optionAttr == null || optionAttr.Required == false) { + continue; + } + + if(targetProperty.GetValue(this._instance) == null) { + this.RequiredList.Add(optionAttr.LongName ?? optionAttr.ShortName); + } + } + } + + private void SetDefaultValues() { + foreach(PropertyInfo targetProperty in this._properties.Except(this._updatedList)) { + ArgumentOptionAttribute optionAttr = AttributeCache.DefaultCache.Value.RetrieveOne(targetProperty); + + Object defaultValue = optionAttr?.DefaultValue; + + if(defaultValue == null) { + continue; + } + + if(this.SetPropertyValue(targetProperty, defaultValue.ToString(), this._instance, optionAttr)) { + this._updatedList.Add(targetProperty); + } + } + } + + private void PopulateInstance() { + const Char dash = '-'; + String propertyName = String.Empty; + + foreach(String arg in this._args) { + Boolean ignoreSetValue = String.IsNullOrWhiteSpace(propertyName); + + if(ignoreSetValue) { + if(String.IsNullOrWhiteSpace(arg) || arg[0] != dash) { + continue; + } + + propertyName = arg.Substring(1); + + if(!String.IsNullOrWhiteSpace(propertyName) && propertyName[0] == dash) { + propertyName = propertyName.Substring(1); + } + } + + PropertyInfo targetProperty = this.TryGetProperty(propertyName); + + if(targetProperty == null) { + // Skip if the property is not found + this.UnknownList.Add(propertyName); + continue; + } + + if(!ignoreSetValue && this.SetPropertyValue(targetProperty, arg, this._instance)) { + this._updatedList.Add(targetProperty); + propertyName = String.Empty; + } else if(targetProperty.PropertyType == typeof(Boolean)) { + // If the arg is a boolean property set it to true. + targetProperty.SetValue(this._instance, true); + + this._updatedList.Add(targetProperty); + propertyName = String.Empty; + } + } + + if(!String.IsNullOrEmpty(propertyName)) { + this.UnknownList.Add(propertyName); + } + } + + private Boolean SetPropertyValue(PropertyInfo targetProperty, String propertyValueString, Object result, ArgumentOptionAttribute optionAttr = null) { + if(!targetProperty.PropertyType.IsEnum) { + return targetProperty.PropertyType.IsArray ? targetProperty.TrySetArray(propertyValueString.Split(optionAttr?.Separator ?? ','), result) : targetProperty.TrySetBasicType(propertyValueString, result); + } + + Object parsedValue = Enum.Parse(targetProperty.PropertyType, propertyValueString, this._settings.CaseInsensitiveEnumValues); + + targetProperty.SetValue(result, Enum.ToObject(targetProperty.PropertyType, parsedValue)); + + return true; + } + + private PropertyInfo TryGetProperty(String propertyName) => this._properties.FirstOrDefault(p => String.Equals(AttributeCache.DefaultCache.Value.RetrieveOne(p)?.LongName, propertyName, this._settings.NameComparer) || String.Equals(AttributeCache.DefaultCache.Value.RetrieveOne(p)?.ShortName, propertyName, this._settings.NameComparer)); + } + } } \ No newline at end of file diff --git a/Swan.Lite/Parsers/ArgumentParser.TypeResolver.cs b/Swan.Lite/Parsers/ArgumentParser.TypeResolver.cs index 3241669..08163eb 100644 --- a/Swan.Lite/Parsers/ArgumentParser.TypeResolver.cs +++ b/Swan.Lite/Parsers/ArgumentParser.TypeResolver.cs @@ -1,57 +1,49 @@ -using System; +#nullable enable +using System; using System.Linq; using System.Reflection; using Swan.Reflection; -namespace Swan.Parsers -{ - /// - /// Provides methods to parse command line arguments. - /// - public partial class ArgumentParser - { - private sealed class TypeResolver - { - private readonly string _selectedVerb; - - private PropertyInfo[]? _properties; - - public TypeResolver(string selectedVerb) - { - _selectedVerb = selectedVerb; - } - - public PropertyInfo[]? Properties => _properties?.Any() == true ? _properties : null; - - public object? GetOptionsObject(T instance) - { - _properties = PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties(true).ToArray(); - - if (!_properties.Any(x => x.GetCustomAttributes(typeof(VerbOptionAttribute), false).Any())) - return instance; - - var selectedVerb = string.IsNullOrWhiteSpace(_selectedVerb) - ? null - : _properties.FirstOrDefault(x => - AttributeCache.DefaultCache.Value.RetrieveOne(x).Name == _selectedVerb); - - if (selectedVerb == null) return null; - - var type = instance.GetType(); - - var verbProperty = type.GetProperty(selectedVerb.Name); - - if (verbProperty?.GetValue(instance) == null) - { - var propertyInstance = Activator.CreateInstance(selectedVerb.PropertyType); - verbProperty?.SetValue(instance, propertyInstance); - } - - _properties = PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties(selectedVerb.PropertyType, true) - .ToArray(); - - return verbProperty?.GetValue(instance); - } - } - } +namespace Swan.Parsers { + /// + /// Provides methods to parse command line arguments. + /// + public partial class ArgumentParser { + private sealed class TypeResolver { + private readonly String _selectedVerb; + + private PropertyInfo[]? _properties; + + public TypeResolver(String selectedVerb) => this._selectedVerb = selectedVerb; + + public PropertyInfo[]? Properties => this._properties?.Any() == true ? this._properties : null; + + public Object? GetOptionsObject(T instance) { + this._properties = PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties(true).ToArray(); + + if(!this._properties.Any(x => x.GetCustomAttributes(typeof(VerbOptionAttribute), false).Any())) { + return instance; + } + + PropertyInfo? selectedVerb = String.IsNullOrWhiteSpace(this._selectedVerb) ? null : this._properties.FirstOrDefault(x => AttributeCache.DefaultCache.Value.RetrieveOne(x).Name == this._selectedVerb); + + if(selectedVerb == null) { + return null; + } + + Type? type = instance?.GetType(); + + PropertyInfo? verbProperty = type?.GetProperty(selectedVerb.Name); + + if(verbProperty?.GetValue(instance) == null) { + Object? propertyInstance = Activator.CreateInstance(selectedVerb.PropertyType); + verbProperty?.SetValue(instance, propertyInstance); + } + + this._properties = PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties(selectedVerb.PropertyType, true).ToArray(); + + return verbProperty?.GetValue(instance); + } + } + } } diff --git a/Swan.Lite/Parsers/ArgumentParser.cs b/Swan.Lite/Parsers/ArgumentParser.cs index 9ee6c86..914b275 100644 --- a/Swan.Lite/Parsers/ArgumentParser.cs +++ b/Swan.Lite/Parsers/ArgumentParser.cs @@ -3,251 +3,240 @@ using System; using System.Collections.Generic; using System.Linq; -namespace Swan.Parsers -{ +namespace Swan.Parsers { + /// + /// Provides methods to parse command line arguments. + /// Based on CommandLine (Copyright 2005-2015 Giacomo Stelluti Scala and Contributors.). + /// + /// + /// The following example shows how to parse CLI arguments into objects. + /// + /// class Example + /// { + /// using System; + /// using Swan.Parsers; + /// + /// static void Main(string[] args) + /// { + /// // parse the supplied command-line arguments into the options object + /// var res = Runtime.ArgumentParser.ParseArguments(args, out var options); + /// } + /// + /// class Options + /// { + /// [ArgumentOption('v', "verbose", HelpText = "Set verbose mode.")] + /// public bool Verbose { get; set; } + /// + /// [ArgumentOption('u', Required = true, HelpText = "Set user name.")] + /// public string Username { get; set; } + /// + /// [ArgumentOption('n', "names", Separator = ',', + /// Required = true, HelpText = "A list of files separated by a comma")] + /// public string[] Files { get; set; } + /// + /// [ArgumentOption('p', "port", DefaultValue = 22, HelpText = "Set port.")] + /// public int Port { get; set; } + /// + /// [ArgumentOption("color", DefaultValue = ConsoleColor.Red, + /// HelpText = "Set a color.")] + /// public ConsoleColor Color { get; set; } + /// } + /// } + /// + /// The following code describes how to parse CLI verbs. + /// + /// class Example2 + /// { + /// using Swan; + /// using Swan.Parsers; + /// + /// static void Main(string[] args) + /// { + /// // create an instance of the VerbOptions class + /// var options = new VerbOptions(); + /// + /// // parse the supplied command-line arguments into the options object + /// var res = Runtime.ArgumentParser.ParseArguments(args, options); + /// + /// // if there were no errors parsing + /// if (res) + /// { + /// if(options.Run != null) + /// { + /// // run verb was selected + /// } + /// + /// if(options.Print != null) + /// { + /// // print verb was selected + /// } + /// } + /// + /// // flush all error messages + /// Terminal.Flush(); + /// } + /// + /// class VerbOptions + /// { + /// [VerbOption("run", HelpText = "Run verb.")] + /// public RunVerbOption Run { get; set; } + /// + /// [VerbOption("print", HelpText = "Print verb.")] + /// public PrintVerbOption Print { get; set; } + /// } + /// + /// class RunVerbOption + /// { + /// [ArgumentOption('o', "outdir", HelpText = "Output directory", + /// DefaultValue = "", Required = false)] + /// public string OutDir { get; set; } + /// } + /// + /// class PrintVerbOption + /// { + /// [ArgumentOption('t', "text", HelpText = "Text to print", + /// DefaultValue = "", Required = false)] + /// public string Text { get; set; } + /// } + /// } + /// + /// + public partial class ArgumentParser { /// - /// Provides methods to parse command line arguments. - /// Based on CommandLine (Copyright 2005-2015 Giacomo Stelluti Scala and Contributors.). + /// Initializes a new instance of the class. /// - /// - /// The following example shows how to parse CLI arguments into objects. - /// - /// class Example - /// { - /// using System; - /// using Swan.Parsers; - /// - /// static void Main(string[] args) - /// { - /// // parse the supplied command-line arguments into the options object - /// var res = Runtime.ArgumentParser.ParseArguments(args, out var options); - /// } - /// - /// class Options - /// { - /// [ArgumentOption('v', "verbose", HelpText = "Set verbose mode.")] - /// public bool Verbose { get; set; } - /// - /// [ArgumentOption('u', Required = true, HelpText = "Set user name.")] - /// public string Username { get; set; } - /// - /// [ArgumentOption('n', "names", Separator = ',', - /// Required = true, HelpText = "A list of files separated by a comma")] - /// public string[] Files { get; set; } - /// - /// [ArgumentOption('p', "port", DefaultValue = 22, HelpText = "Set port.")] - /// public int Port { get; set; } - /// - /// [ArgumentOption("color", DefaultValue = ConsoleColor.Red, - /// HelpText = "Set a color.")] - /// public ConsoleColor Color { get; set; } - /// } - /// } - /// - /// The following code describes how to parse CLI verbs. - /// - /// class Example2 - /// { - /// using Swan; - /// using Swan.Parsers; - /// - /// static void Main(string[] args) - /// { - /// // create an instance of the VerbOptions class - /// var options = new VerbOptions(); - /// - /// // parse the supplied command-line arguments into the options object - /// var res = Runtime.ArgumentParser.ParseArguments(args, options); - /// - /// // if there were no errors parsing - /// if (res) - /// { - /// if(options.Run != null) - /// { - /// // run verb was selected - /// } - /// - /// if(options.Print != null) - /// { - /// // print verb was selected - /// } - /// } - /// - /// // flush all error messages - /// Terminal.Flush(); - /// } - /// - /// class VerbOptions - /// { - /// [VerbOption("run", HelpText = "Run verb.")] - /// public RunVerbOption Run { get; set; } - /// - /// [VerbOption("print", HelpText = "Print verb.")] - /// public PrintVerbOption Print { get; set; } - /// } - /// - /// class RunVerbOption - /// { - /// [ArgumentOption('o', "outdir", HelpText = "Output directory", - /// DefaultValue = "", Required = false)] - /// public string OutDir { get; set; } - /// } - /// - /// class PrintVerbOption - /// { - /// [ArgumentOption('t', "text", HelpText = "Text to print", - /// DefaultValue = "", Required = false)] - /// public string Text { get; set; } - /// } - /// } - /// - /// - public partial class ArgumentParser - { - /// - /// Initializes a new instance of the class. - /// - public ArgumentParser() - : this(new ArgumentParserSettings()) - { - } - - /// - /// Initializes a new instance of the class, - /// configurable with using a delegate. - /// - /// The parse settings. - public ArgumentParser(ArgumentParserSettings parseSettings) - { - Settings = parseSettings ?? throw new ArgumentNullException(nameof(parseSettings)); - } - - /// - /// Gets the current. - /// - /// - /// The current. - /// - public static ArgumentParser Current { get; } = new ArgumentParser(); - - /// - /// Gets the instance that implements in use. - /// - /// - /// The settings. - /// - public ArgumentParserSettings Settings { get; } - - /// - /// Parses a string array of command line arguments constructing values in an instance of type . - /// - /// The type of the options. - /// The arguments. - /// The instance. - /// - /// true if was converted successfully; otherwise, false. - /// - /// - /// The exception that is thrown when a null reference (Nothing in Visual Basic) - /// is passed to a method that does not accept it as a valid argument. - /// - /// - /// The exception that is thrown when a method call is invalid for the object's current state. - /// - public bool ParseArguments(IEnumerable args, out T instance) - { - instance = Activator.CreateInstance(); - return ParseArguments(args, instance); - } - - /// - /// Parses a string array of command line arguments constructing values in an instance of type . - /// - /// The type of the options. - /// The arguments. - /// The instance. - /// - /// true if was converted successfully; otherwise, false. - /// - /// - /// The exception that is thrown when a null reference (Nothing in Visual Basic) - /// is passed to a method that does not accept it as a valid argument. - /// - /// - /// The exception that is thrown when a method call is invalid for the object's current state. - /// - public bool ParseArguments(IEnumerable args, T instance) - { - if (args == null) - throw new ArgumentNullException(nameof(args)); - - if (Equals(instance, default(T))) - throw new ArgumentNullException(nameof(instance)); - - var typeResolver = new TypeResolver(args.FirstOrDefault()); - var options = typeResolver.GetOptionsObject(instance); - - if (options == null) - { - ReportUnknownVerb(); - return false; - } - - if (typeResolver.Properties == null) - throw new InvalidOperationException($"Type {typeof(T).Name} is not valid"); - - var validator = new Validator(typeResolver.Properties, args, options, Settings); - - if (validator.IsValid()) - return true; - - ReportIssues(validator); - return false; - } - - private static void ReportUnknownVerb() - { - Terminal.WriteLine("No verb was specified", ConsoleColor.Red); - Terminal.WriteLine("Valid verbs:", ConsoleColor.Cyan); - - PropertyTypeCache.DefaultCache.Value - .RetrieveAllProperties(true) - .Select(x => AttributeCache.DefaultCache.Value.RetrieveOne(x)) - .Where(x => x != null) - .ToList() - .ForEach(x => Terminal.WriteLine(x.ToString(), ConsoleColor.Cyan)); - } - - private void ReportIssues(Validator validator) - { - if (Settings.WriteBanner) - Terminal.WriteWelcomeBanner(); - - var options = validator.GetPropertiesOptions(); - - foreach (var option in options) - { - Terminal.WriteLine(string.Empty); - - // TODO: If Enum list values - var shortName = string.IsNullOrWhiteSpace(option.ShortName) ? string.Empty : $"-{option.ShortName}"; - var longName = string.IsNullOrWhiteSpace(option.LongName) ? string.Empty : $"--{option.LongName}"; - var comma = string.IsNullOrWhiteSpace(shortName) || string.IsNullOrWhiteSpace(longName) - ? string.Empty - : ", "; - var defaultValue = option.DefaultValue == null ? string.Empty : $"(Default: {option.DefaultValue}) "; - - Terminal.WriteLine($" {shortName}{comma}{longName}\t\t{defaultValue}{option.HelpText}", ConsoleColor.Cyan); - } - - Terminal.WriteLine(string.Empty); - Terminal.WriteLine(" --help\t\tDisplay this help screen.", ConsoleColor.Cyan); - - if (validator.UnknownList.Any()) - Terminal.WriteLine($"Unknown arguments: {string.Join(", ", validator.UnknownList)}", ConsoleColor.Red); - - if (validator.RequiredList.Any()) - Terminal.WriteLine($"Required arguments: {string.Join(", ", validator.RequiredList)}", ConsoleColor.Red); - } - } + public ArgumentParser() : this(new ArgumentParserSettings()) { + } + + /// + /// Initializes a new instance of the class, + /// configurable with using a delegate. + /// + /// The parse settings. + public ArgumentParser(ArgumentParserSettings parseSettings) => this.Settings = parseSettings ?? throw new ArgumentNullException(nameof(parseSettings)); + + /// + /// Gets the current. + /// + /// + /// The current. + /// + public static ArgumentParser Current { get; } = new ArgumentParser(); + + /// + /// Gets the instance that implements in use. + /// + /// + /// The settings. + /// + public ArgumentParserSettings Settings { + get; + } + + /// + /// Parses a string array of command line arguments constructing values in an instance of type . + /// + /// The type of the options. + /// The arguments. + /// The instance. + /// + /// true if was converted successfully; otherwise, false. + /// + /// + /// The exception that is thrown when a null reference (Nothing in Visual Basic) + /// is passed to a method that does not accept it as a valid argument. + /// + /// + /// The exception that is thrown when a method call is invalid for the object's current state. + /// + public Boolean ParseArguments(IEnumerable args, out T instance) { + instance = Activator.CreateInstance(); + return this.ParseArguments(args, instance); + } + + /// + /// Parses a string array of command line arguments constructing values in an instance of type . + /// + /// The type of the options. + /// The arguments. + /// The instance. + /// + /// true if was converted successfully; otherwise, false. + /// + /// + /// The exception that is thrown when a null reference (Nothing in Visual Basic) + /// is passed to a method that does not accept it as a valid argument. + /// + /// + /// The exception that is thrown when a method call is invalid for the object's current state. + /// + public Boolean ParseArguments(IEnumerable args, T instance) { + if(args == null) { + throw new ArgumentNullException(nameof(args)); + } + + if(Equals(instance, default(T))) { + throw new ArgumentNullException(nameof(instance)); + } + + TypeResolver typeResolver = new TypeResolver(args.FirstOrDefault()); + Object options = typeResolver.GetOptionsObject(instance); + + if(options == null) { + ReportUnknownVerb(); + return false; + } + + if(typeResolver.Properties == null) { + throw new InvalidOperationException($"Type {typeof(T).Name} is not valid"); + } + + Validator validator = new Validator(typeResolver.Properties, args, options, this.Settings); + + if(validator.IsValid()) { + return true; + } + + this.ReportIssues(validator); + return false; + } + + private static void ReportUnknownVerb() { + Terminal.WriteLine("No verb was specified", ConsoleColor.Red); + Terminal.WriteLine("Valid verbs:", ConsoleColor.Cyan); + + PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties(true).Select(x => AttributeCache.DefaultCache.Value.RetrieveOne(x)).Where(x => x != null).ToList().ForEach(x => Terminal.WriteLine(x.ToString(), ConsoleColor.Cyan)); + } + + private void ReportIssues(Validator validator) { + if(this.Settings.WriteBanner) { + Terminal.WriteWelcomeBanner(); + } + + IEnumerable options = validator.GetPropertiesOptions(); + + foreach(ArgumentOptionAttribute option in options) { + Terminal.WriteLine(String.Empty); + + // TODO: If Enum list values + String shortName = String.IsNullOrWhiteSpace(option.ShortName) ? String.Empty : $"-{option.ShortName}"; + String longName = String.IsNullOrWhiteSpace(option.LongName) ? String.Empty : $"--{option.LongName}"; + String comma = String.IsNullOrWhiteSpace(shortName) || String.IsNullOrWhiteSpace(longName) ? String.Empty : ", "; + String defaultValue = option.DefaultValue == null ? String.Empty : $"(Default: {option.DefaultValue}) "; + + Terminal.WriteLine($" {shortName}{comma}{longName}\t\t{defaultValue}{option.HelpText}", ConsoleColor.Cyan); + } + + Terminal.WriteLine(String.Empty); + Terminal.WriteLine(" --help\t\tDisplay this help screen.", ConsoleColor.Cyan); + + if(validator.UnknownList.Any()) { + Terminal.WriteLine($"Unknown arguments: {String.Join(", ", validator.UnknownList)}", ConsoleColor.Red); + } + + if(validator.RequiredList.Any()) { + Terminal.WriteLine($"Required arguments: {String.Join(", ", validator.RequiredList)}", ConsoleColor.Red); + } + } + } } diff --git a/Swan.Lite/Parsers/ArgumentParserSettings.cs b/Swan.Lite/Parsers/ArgumentParserSettings.cs index dbe2f5b..687828d 100644 --- a/Swan.Lite/Parsers/ArgumentParserSettings.cs +++ b/Swan.Lite/Parsers/ArgumentParserSettings.cs @@ -1,53 +1,51 @@ using System; -namespace Swan.Parsers -{ +namespace Swan.Parsers { + /// + /// Provides settings for . + /// Based on CommandLine (Copyright 2005-2015 Giacomo Stelluti Scala and Contributors.). + /// + public class ArgumentParserSettings { /// - /// Provides settings for . - /// Based on CommandLine (Copyright 2005-2015 Giacomo Stelluti Scala and Contributors.). + /// Gets or sets a value indicating whether [write banner]. /// - public class ArgumentParserSettings - { - /// - /// Gets or sets a value indicating whether [write banner]. - /// - /// - /// true if [write banner]; otherwise, false. - /// - public bool WriteBanner { get; set; } = true; - - /// - /// Gets or sets a value indicating whether perform case sensitive comparisons. - /// Note that case insensitivity only applies to parameters, not the values - /// assigned to them (for example, enum parsing). - /// - /// - /// true if [case sensitive]; otherwise, false. - /// - public bool CaseSensitive { get; set; } = false; - - /// - /// Gets or sets a value indicating whether perform case sensitive comparisons of values. - /// Note that case insensitivity only applies to values, not the parameters. - /// - /// - /// true if [case insensitive enum values]; otherwise, false. - /// - public bool CaseInsensitiveEnumValues { get; set; } = true; - - /// - /// Gets or sets a value indicating whether the parser shall move on to the next argument and ignore the given argument if it - /// encounter an unknown arguments. - /// - /// - /// true to allow parsing the arguments with different class options that do not have all the arguments. - /// - /// - /// This allows fragmented version class parsing, useful for project with add-on where add-ons also requires command line arguments but - /// when these are unknown by the main program at build time. - /// - public bool IgnoreUnknownArguments { get; set; } = true; - - internal StringComparison NameComparer => CaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; - } + /// + /// true if [write banner]; otherwise, false. + /// + public Boolean WriteBanner { get; set; } = true; + + /// + /// Gets or sets a value indicating whether perform case sensitive comparisons. + /// Note that case insensitivity only applies to parameters, not the values + /// assigned to them (for example, enum parsing). + /// + /// + /// true if [case sensitive]; otherwise, false. + /// + public Boolean CaseSensitive { get; set; } = false; + + /// + /// Gets or sets a value indicating whether perform case sensitive comparisons of values. + /// Note that case insensitivity only applies to values, not the parameters. + /// + /// + /// true if [case insensitive enum values]; otherwise, false. + /// + public Boolean CaseInsensitiveEnumValues { get; set; } = true; + + /// + /// Gets or sets a value indicating whether the parser shall move on to the next argument and ignore the given argument if it + /// encounter an unknown arguments. + /// + /// + /// true to allow parsing the arguments with different class options that do not have all the arguments. + /// + /// + /// This allows fragmented version class parsing, useful for project with add-on where add-ons also requires command line arguments but + /// when these are unknown by the main program at build time. + /// + public Boolean IgnoreUnknownArguments { get; set; } = true; + + internal StringComparison NameComparer => this.CaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; + } } \ No newline at end of file diff --git a/Swan.Lite/Parsers/ExpressionParser.cs b/Swan.Lite/Parsers/ExpressionParser.cs index c189e70..301710f 100644 --- a/Swan.Lite/Parsers/ExpressionParser.cs +++ b/Swan.Lite/Parsers/ExpressionParser.cs @@ -3,115 +3,107 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -namespace Swan.Parsers -{ +namespace Swan.Parsers { + /// + /// Represents a generic expression parser. + /// + public abstract class ExpressionParser { /// - /// Represents a generic expression parser. + /// Resolves the expression. /// - public abstract class ExpressionParser - { - /// - /// Resolves the expression. - /// - /// The type of expression result. - /// The tokens. - /// The representation of the expression parsed. - public virtual T ResolveExpression(IEnumerable tokens) => - ResolveExpression(tokens, System.Globalization.CultureInfo.InvariantCulture); - - /// - /// Resolves the expression. - /// - /// The type of expression result. - /// The tokens. - /// The format provider. - /// The representation of the expression parsed. - public virtual T ResolveExpression(IEnumerable tokens, IFormatProvider formatProvider) - { - var conversion = Expression.Convert(Parse(tokens,formatProvider), typeof(T)); - return Expression.Lambda>(conversion).Compile()(); - } - - /// - /// Parses the specified tokens. - /// - /// The tokens. - /// - /// The final expression. - /// - public virtual Expression Parse(IEnumerable tokens) => - Parse(tokens, System.Globalization.CultureInfo.InvariantCulture); - - /// - /// Parses the specified tokens. - /// - /// The tokens. - /// The format provider. - /// - /// The final expression. - /// - public virtual Expression Parse(IEnumerable tokens, IFormatProvider formatProvider) - { - var expressionStack = new List>(); - - foreach (var token in tokens) - { - if (expressionStack.Any() == false) - expressionStack.Add(new Stack()); - - switch (token.Type) - { - case TokenType.Wall: - expressionStack.Add(new Stack()); - break; - case TokenType.Number: - expressionStack.Last().Push(Expression.Constant(Convert.ToDecimal(token.Value, formatProvider))); - break; - case TokenType.Variable: - ResolveVariable(token.Value, expressionStack.Last()); - break; - case TokenType.String: - expressionStack.Last().Push(Expression.Constant(token.Value)); - break; - case TokenType.Operator: - ResolveOperator(token.Value, expressionStack.Last()); - break; - case TokenType.Function: - ResolveFunction(token.Value, expressionStack.Last()); - - if (expressionStack.Count > 1 && expressionStack.Last().Count == 1) - { - var lastValue = expressionStack.Last().Pop(); - expressionStack.Remove(expressionStack.Last()); - expressionStack.Last().Push(lastValue); - } - - break; - } - } - - return expressionStack.Last().Pop(); - } - - /// - /// Resolves the variable. - /// - /// The value. - /// The expression stack. - public abstract void ResolveVariable(string value, Stack expressionStack); - - /// - /// Resolves the operator. - /// - /// The value. - /// The expression stack. - public abstract void ResolveOperator(string value, Stack expressionStack); - - /// - /// Resolves the function. - /// - /// The value. - /// The expression stack. - public abstract void ResolveFunction(string value, Stack expressionStack); - } + /// The type of expression result. + /// The tokens. + /// The representation of the expression parsed. + public virtual T ResolveExpression(IEnumerable tokens) => this.ResolveExpression(tokens, System.Globalization.CultureInfo.InvariantCulture); + + /// + /// Resolves the expression. + /// + /// The type of expression result. + /// The tokens. + /// The format provider. + /// The representation of the expression parsed. + public virtual T ResolveExpression(IEnumerable tokens, IFormatProvider formatProvider) { + UnaryExpression conversion = Expression.Convert(this.Parse(tokens, formatProvider), typeof(T)); + return Expression.Lambda>(conversion).Compile()(); + } + + /// + /// Parses the specified tokens. + /// + /// The tokens. + /// + /// The final expression. + /// + public virtual Expression Parse(IEnumerable tokens) => this.Parse(tokens, System.Globalization.CultureInfo.InvariantCulture); + + /// + /// Parses the specified tokens. + /// + /// The tokens. + /// The format provider. + /// + /// The final expression. + /// + public virtual Expression Parse(IEnumerable tokens, IFormatProvider formatProvider) { + List> expressionStack = new List>(); + + foreach(Token token in tokens) { + if(expressionStack.Any() == false) { + expressionStack.Add(new Stack()); + } + + switch(token.Type) { + case TokenType.Wall: + expressionStack.Add(new Stack()); + break; + case TokenType.Number: + expressionStack.Last().Push(Expression.Constant(Convert.ToDecimal(token.Value, formatProvider))); + break; + case TokenType.Variable: + this.ResolveVariable(token.Value, expressionStack.Last()); + break; + case TokenType.String: + expressionStack.Last().Push(Expression.Constant(token.Value)); + break; + case TokenType.Operator: + this.ResolveOperator(token.Value, expressionStack.Last()); + break; + case TokenType.Function: + this.ResolveFunction(token.Value, expressionStack.Last()); + + if(expressionStack.Count > 1 && expressionStack.Last().Count == 1) { + Expression lastValue = expressionStack.Last().Pop(); + _ = expressionStack.Remove(expressionStack.Last()); + expressionStack.Last().Push(lastValue); + } + + break; + } + } + + return expressionStack.Last().Pop(); + } + + /// + /// Resolves the variable. + /// + /// The value. + /// The expression stack. + public abstract void ResolveVariable(String value, Stack expressionStack); + + /// + /// Resolves the operator. + /// + /// The value. + /// The expression stack. + public abstract void ResolveOperator(String value, Stack expressionStack); + + /// + /// Resolves the function. + /// + /// The value. + /// The expression stack. + public abstract void ResolveFunction(String value, Stack expressionStack); + } } diff --git a/Swan.Lite/Parsers/Operator.cs b/Swan.Lite/Parsers/Operator.cs index 598645a..dff79ff 100644 --- a/Swan.Lite/Parsers/Operator.cs +++ b/Swan.Lite/Parsers/Operator.cs @@ -1,32 +1,38 @@ -namespace Swan.Parsers -{ +using System; + +namespace Swan.Parsers { + /// + /// Represents an operator with precedence. + /// + public class Operator { /// - /// Represents an operator with precedence. + /// Gets or sets the name. /// - public class Operator - { - /// - /// Gets or sets the name. - /// - /// - /// The name. - /// - public string Name { get; set; } - - /// - /// Gets or sets the precedence. - /// - /// - /// The precedence. - /// - public int Precedence { get; set; } - - /// - /// Gets or sets a value indicating whether [right associative]. - /// - /// - /// true if [right associative]; otherwise, false. - /// - public bool RightAssociative { get; set; } - } + /// + /// The name. + /// + public String Name { + get; set; + } + + /// + /// Gets or sets the precedence. + /// + /// + /// The precedence. + /// + public Int32 Precedence { + get; set; + } + + /// + /// Gets or sets a value indicating whether [right associative]. + /// + /// + /// true if [right associative]; otherwise, false. + /// + public Boolean RightAssociative { + get; set; + } + } } diff --git a/Swan.Lite/Parsers/Token.cs b/Swan.Lite/Parsers/Token.cs index 20d30b8..581a8f2 100644 --- a/Swan.Lite/Parsers/Token.cs +++ b/Swan.Lite/Parsers/Token.cs @@ -1,35 +1,38 @@ -namespace Swan.Parsers -{ +using System; + +namespace Swan.Parsers { + /// + /// Represents a Token structure. + /// + public struct Token { /// - /// Represents a Token structure. + /// Initializes a new instance of the struct. /// - public struct Token - { - /// - /// Initializes a new instance of the struct. - /// - /// The type. - /// The value. - public Token(TokenType type, string value) - { - Type = type; - Value = type == TokenType.Function || type == TokenType.Operator ? value.ToLowerInvariant() : value; - } - - /// - /// Gets or sets the type. - /// - /// - /// The type. - /// - public TokenType Type { get; set; } - - /// - /// Gets the value. - /// - /// - /// The value. - /// - public string Value { get; } - } + /// The type. + /// The value. + public Token(TokenType type, String value) { + this.Type = type; + this.Value = type == TokenType.Function || type == TokenType.Operator ? value.ToLowerInvariant() : value; + } + + /// + /// Gets or sets the type. + /// + /// + /// The type. + /// + public TokenType Type { + get; set; + } + + /// + /// Gets the value. + /// + /// + /// The value. + /// + public String Value { + get; + } + } } diff --git a/Swan.Lite/Parsers/TokenType.cs b/Swan.Lite/Parsers/TokenType.cs index 5777f0d..c92b57d 100644 --- a/Swan.Lite/Parsers/TokenType.cs +++ b/Swan.Lite/Parsers/TokenType.cs @@ -1,48 +1,46 @@ -namespace Swan.Parsers -{ +namespace Swan.Parsers { + /// + /// Enums the token types. + /// + public enum TokenType { /// - /// Enums the token types. + /// The number /// - public enum TokenType - { - /// - /// The number - /// - Number, - - /// - /// The string - /// - String, - - /// - /// The variable - /// - Variable, - - /// - /// The function - /// - Function, - - /// - /// The parenthesis - /// - Parenthesis, - - /// - /// The operator - /// - Operator, - - /// - /// The comma - /// - Comma, - - /// - /// The wall, used to specified the end of argument list of the following function - /// - Wall, - } + Number, + + /// + /// The string + /// + String, + + /// + /// The variable + /// + Variable, + + /// + /// The function + /// + Function, + + /// + /// The parenthesis + /// + Parenthesis, + + /// + /// The operator + /// + Operator, + + /// + /// The comma + /// + Comma, + + /// + /// The wall, used to specified the end of argument list of the following function + /// + Wall, + } } diff --git a/Swan.Lite/Parsers/Tokenizer.cs b/Swan.Lite/Parsers/Tokenizer.cs index 4d56714..7f0adf6 100644 --- a/Swan.Lite/Parsers/Tokenizer.cs +++ b/Swan.Lite/Parsers/Tokenizer.cs @@ -2,142 +2,138 @@ using System.Collections.Generic; using System.Linq; -namespace Swan.Parsers -{ +namespace Swan.Parsers { + /// + /// Represents a generic tokenizer. + /// + public abstract class Tokenizer { + private const Char PeriodChar = '.'; + private const Char CommaChar = ','; + private const Char StringQuotedChar = '"'; + private const Char OpenFuncChar = '('; + private const Char CloseFuncChar = ')'; + private const Char NegativeChar = '-'; + + private const String OpenFuncStr = "("; + + private readonly List _operators = new List(); + /// - /// Represents a generic tokenizer. + /// Initializes a new instance of the class. + /// This constructor will use the following default operators: + /// + /// + /// + /// Operator + /// Precedence + /// + /// + /// = + /// 1 + /// + /// + /// != + /// 1 + /// + /// + /// > + /// 2 + /// + /// + /// < + /// 2 + /// + /// + /// >= + /// 2 + /// + /// + /// <= + /// 2 + /// + /// + /// + + /// 3 + /// + /// + /// & + /// 3 + /// + /// + /// - + /// 3 + /// + /// + /// * + /// 4 + /// + /// + /// (backslash) + /// 4 + /// + /// + /// / + /// 4 + /// + /// + /// ^ + /// 4 + /// + /// /// - public abstract class Tokenizer + /// The input. + protected Tokenizer(String input) { + this._operators.AddRange(this.GetDefaultOperators()); + this.Tokenize(input); + } + + /// + /// Initializes a new instance of the class. + /// + /// The input. + /// The operators to use. + protected Tokenizer(String input, IEnumerable operators) { + this._operators.AddRange(operators); + this.Tokenize(input); + } + + /// + /// Gets the tokens. + /// + /// + /// The tokens. + /// + public List Tokens { get; } = new List(); + + /// + /// Validates the input and return the start index for tokenizer. + /// + /// The input. + /// The start index. + /// true if the input is valid, otherwise false. + public abstract Boolean ValidateInput(String input, out Int32 startIndex); + + /// + /// Resolves the type of the function or member. + /// + /// The input. + /// The token type. + public abstract TokenType ResolveFunctionOrMemberType(String input); + + /// + /// Evaluates the function or member. + /// + /// The input. + /// The position. + /// true if the input is a valid function or variable, otherwise false. + public virtual Boolean EvaluateFunctionOrMember(String input, Int32 position) => false; + + /// + /// Gets the default operators. + /// + /// An array with the operators to use for the tokenizer. + public virtual Operator[] GetDefaultOperators() => new[] { - private const char PeriodChar = '.'; - private const char CommaChar = ','; - private const char StringQuotedChar = '"'; - private const char OpenFuncChar = '('; - private const char CloseFuncChar = ')'; - private const char NegativeChar = '-'; - - private const string OpenFuncStr = "("; - - private readonly List _operators = new List(); - - /// - /// Initializes a new instance of the class. - /// This constructor will use the following default operators: - /// - /// - /// - /// Operator - /// Precedence - /// - /// - /// = - /// 1 - /// - /// - /// != - /// 1 - /// - /// - /// > - /// 2 - /// - /// - /// < - /// 2 - /// - /// - /// >= - /// 2 - /// - /// - /// <= - /// 2 - /// - /// - /// + - /// 3 - /// - /// - /// & - /// 3 - /// - /// - /// - - /// 3 - /// - /// - /// * - /// 4 - /// - /// - /// (backslash) - /// 4 - /// - /// - /// / - /// 4 - /// - /// - /// ^ - /// 4 - /// - /// - /// - /// The input. - protected Tokenizer(string input) - { - _operators.AddRange(GetDefaultOperators()); - Tokenize(input); - } - - /// - /// Initializes a new instance of the class. - /// - /// The input. - /// The operators to use. - protected Tokenizer(string input, IEnumerable operators) - { - _operators.AddRange(operators); - Tokenize(input); - } - - /// - /// Gets the tokens. - /// - /// - /// The tokens. - /// - public List Tokens { get; } = new List(); - - /// - /// Validates the input and return the start index for tokenizer. - /// - /// The input. - /// The start index. - /// true if the input is valid, otherwise false. - public abstract bool ValidateInput(string input, out int startIndex); - - /// - /// Resolves the type of the function or member. - /// - /// The input. - /// The token type. - public abstract TokenType ResolveFunctionOrMemberType(string input); - - /// - /// Evaluates the function or member. - /// - /// The input. - /// The position. - /// true if the input is a valid function or variable, otherwise false. - public virtual bool EvaluateFunctionOrMember(string input, int position) => false; - - /// - /// Gets the default operators. - /// - /// An array with the operators to use for the tokenizer. - public virtual Operator[] GetDefaultOperators() => new[] - { new Operator {Name = "=", Precedence = 1}, new Operator {Name = "!=", Precedence = 1}, new Operator {Name = ">", Precedence = 2}, @@ -151,211 +147,162 @@ namespace Swan.Parsers new Operator {Name = "/", Precedence = 4}, new Operator {Name = "\\", Precedence = 4}, new Operator {Name = "^", Precedence = 4}, - }; - - /// - /// Shunting the yard. - /// - /// if set to true [include function stopper] (Token type Wall). - /// - /// Enumerable of the token in in. - /// - /// - /// Wrong token - /// or - /// Mismatched parenthesis. - /// - public virtual IEnumerable ShuntingYard(bool includeFunctionStopper = true) - { - var stack = new Stack(); - - foreach (var tok in Tokens) - { - switch (tok.Type) - { - case TokenType.Number: - case TokenType.Variable: - case TokenType.String: - yield return tok; - break; - case TokenType.Function: - stack.Push(tok); - break; - case TokenType.Operator: - while (stack.Any() && stack.Peek().Type == TokenType.Operator && - CompareOperators(tok.Value, stack.Peek().Value)) - yield return stack.Pop(); - - stack.Push(tok); - break; - case TokenType.Comma: - while (stack.Any() && (stack.Peek().Type != TokenType.Comma && - stack.Peek().Type != TokenType.Parenthesis)) - yield return stack.Pop(); - - break; - case TokenType.Parenthesis: - if (tok.Value == OpenFuncStr) - { - if (stack.Any() && stack.Peek().Type == TokenType.Function) - { - if (includeFunctionStopper) - yield return new Token(TokenType.Wall, tok.Value); - } - - stack.Push(tok); - } - else - { - while (stack.Peek().Value != OpenFuncStr) - yield return stack.Pop(); - - stack.Pop(); - - if (stack.Any() && stack.Peek().Type == TokenType.Function) - { - yield return stack.Pop(); - } - } - - break; - default: - throw new InvalidOperationException("Wrong token"); - } - } - - while (stack.Any()) - { - var tok = stack.Pop(); - if (tok.Type == TokenType.Parenthesis) - throw new InvalidOperationException("Mismatched parenthesis"); - - yield return tok; - } - } - - private static bool CompareOperators(Operator op1, Operator op2) => op1.RightAssociative - ? op1.Precedence < op2.Precedence - : op1.Precedence <= op2.Precedence; - - private void Tokenize(string input) - { - if (!ValidateInput(input, out var startIndex)) - { - return; - } - - for (var i = startIndex; i < input.Length; i++) - { - if (char.IsWhiteSpace(input, i)) continue; - - if (input[i] == CommaChar) - { - Tokens.Add(new Token(TokenType.Comma, new string(new[] { input[i] }))); - continue; - } - - if (input[i] == StringQuotedChar) - { - i = ExtractString(input, i); - continue; - } - - if (char.IsLetter(input, i) || EvaluateFunctionOrMember(input, i)) - { - i = ExtractFunctionOrMember(input, i); - - continue; - } - - if (char.IsNumber(input, i) || ( - input[i] == NegativeChar && - ((Tokens.Any() && Tokens.Last().Type != TokenType.Number) || !Tokens.Any()))) - { - i = ExtractNumber(input, i); - continue; - } - - if (input[i] == OpenFuncChar || - input[i] == CloseFuncChar) - { - Tokens.Add(new Token(TokenType.Parenthesis, new string(new[] { input[i] }))); - continue; - } - - i = ExtractOperator(input, i); - } - } - - private int ExtractData( - string input, - int i, - Func tokenTypeEvaluation, - Func evaluation, - int right = 0, - int left = -1) - { - var charCount = 0; - for (var j = i + right; j < input.Length; j++) - { - if (evaluation(input[j])) - break; - - charCount++; - } - - // Extract and set the value - var value = input.SliceLength(i + right, charCount); - Tokens.Add(new Token(tokenTypeEvaluation(value), value)); - - i += charCount + left; - return i; - } - - private int ExtractOperator(string input, int i) => - ExtractData( - input, - i, - x => TokenType.Operator, - x => x == OpenFuncChar || - x == CommaChar || - x == PeriodChar || - x == StringQuotedChar || - char.IsWhiteSpace(x) || - char.IsNumber(x)); - - private int ExtractFunctionOrMember(string input, int i) => - ExtractData( - input, - i, - ResolveFunctionOrMemberType, - x => x == OpenFuncChar || - x == CloseFuncChar || - x == CommaChar || - char.IsWhiteSpace(x)); - - private int ExtractNumber(string input, int i) => - ExtractData( - input, - i, - x => TokenType.Number, - x => !char.IsNumber(x) && x != PeriodChar && x != NegativeChar); - - private int ExtractString(string input, int i) - { - var length = ExtractData(input, i, x => TokenType.String, x => x == StringQuotedChar, 1, 1); - - // open string, report issue - if (length == input.Length && input[length - 1] != StringQuotedChar) - throw new FormatException($"Parser error (Position {i}): Expected '\"' but got '{input[length - 1]}'."); - - return length; - } - - private bool CompareOperators(string op1, string op2) - => CompareOperators(GetOperatorOrDefault(op1), GetOperatorOrDefault(op2)); - - private Operator GetOperatorOrDefault(string op) - => _operators.FirstOrDefault(x => x.Name == op) ?? new Operator { Name = op, Precedence = 0 }; - } + }; + + /// + /// Shunting the yard. + /// + /// if set to true [include function stopper] (Token type Wall). + /// + /// Enumerable of the token in in. + /// + /// + /// Wrong token + /// or + /// Mismatched parenthesis. + /// + public virtual IEnumerable ShuntingYard(Boolean includeFunctionStopper = true) { + Stack stack = new Stack(); + + foreach(Token tok in this.Tokens) { + switch(tok.Type) { + case TokenType.Number: + case TokenType.Variable: + case TokenType.String: + yield return tok; + break; + case TokenType.Function: + stack.Push(tok); + break; + case TokenType.Operator: + while(stack.Any() && stack.Peek().Type == TokenType.Operator && this.CompareOperators(tok.Value, stack.Peek().Value)) { + yield return stack.Pop(); + } + + stack.Push(tok); + break; + case TokenType.Comma: + while(stack.Any() && stack.Peek().Type != TokenType.Comma && stack.Peek().Type != TokenType.Parenthesis) { + yield return stack.Pop(); + } + + break; + case TokenType.Parenthesis: + if(tok.Value == OpenFuncStr) { + if(stack.Any() && stack.Peek().Type == TokenType.Function) { + if(includeFunctionStopper) { + yield return new Token(TokenType.Wall, tok.Value); + } + } + + stack.Push(tok); + } else { + while(stack.Peek().Value != OpenFuncStr) { + yield return stack.Pop(); + } + + _ = stack.Pop(); + + if(stack.Any() && stack.Peek().Type == TokenType.Function) { + yield return stack.Pop(); + } + } + + break; + default: + throw new InvalidOperationException("Wrong token"); + } + } + + while(stack.Any()) { + Token tok = stack.Pop(); + if(tok.Type == TokenType.Parenthesis) { + throw new InvalidOperationException("Mismatched parenthesis"); + } + + yield return tok; + } + } + + private static Boolean CompareOperators(Operator op1, Operator op2) => op1.RightAssociative ? op1.Precedence < op2.Precedence : op1.Precedence <= op2.Precedence; + + private void Tokenize(String input) { + if(!this.ValidateInput(input, out Int32 startIndex)) { + return; + } + + for(Int32 i = startIndex; i < input.Length; i++) { + if(Char.IsWhiteSpace(input, i)) { + continue; + } + + if(input[i] == CommaChar) { + this.Tokens.Add(new Token(TokenType.Comma, new String(new[] { input[i] }))); + continue; + } + + if(input[i] == StringQuotedChar) { + i = this.ExtractString(input, i); + continue; + } + + if(Char.IsLetter(input, i) || this.EvaluateFunctionOrMember(input, i)) { + i = this.ExtractFunctionOrMember(input, i); + + continue; + } + + if(Char.IsNumber(input, i) || input[i] == NegativeChar && (this.Tokens.Any() && this.Tokens.Last().Type != TokenType.Number || !this.Tokens.Any())) { + i = this.ExtractNumber(input, i); + continue; + } + + if(input[i] == OpenFuncChar || input[i] == CloseFuncChar) { + this.Tokens.Add(new Token(TokenType.Parenthesis, new String(new[] { input[i] }))); + continue; + } + + i = this.ExtractOperator(input, i); + } + } + + private Int32 ExtractData(String input, Int32 i, Func tokenTypeEvaluation, Func evaluation, Int32 right = 0, Int32 left = -1) { + Int32 charCount = 0; + for(Int32 j = i + right; j < input.Length; j++) { + if(evaluation(input[j])) { + break; + } + + charCount++; + } + + // Extract and set the value + String value = input.SliceLength(i + right, charCount); + this.Tokens.Add(new Token(tokenTypeEvaluation(value), value)); + + i += charCount + left; + return i; + } + + private Int32 ExtractOperator(String input, Int32 i) => this.ExtractData(input, i, x => TokenType.Operator, x => x == OpenFuncChar || x == CommaChar || x == PeriodChar || x == StringQuotedChar || Char.IsWhiteSpace(x) || Char.IsNumber(x)); + + private Int32 ExtractFunctionOrMember(String input, Int32 i) => this.ExtractData(input, i, this.ResolveFunctionOrMemberType, x => x == OpenFuncChar || x == CloseFuncChar || x == CommaChar || Char.IsWhiteSpace(x)); + + private Int32 ExtractNumber(String input, Int32 i) => this.ExtractData(input, i, x => TokenType.Number, x => !Char.IsNumber(x) && x != PeriodChar && x != NegativeChar); + + private Int32 ExtractString(String input, Int32 i) { + Int32 length = this.ExtractData(input, i, x => TokenType.String, x => x == StringQuotedChar, 1, 1); + + // open string, report issue + if(length == input.Length && input[length - 1] != StringQuotedChar) { + throw new FormatException($"Parser error (Position {i}): Expected '\"' but got '{input[length - 1]}'."); + } + + return length; + } + + private Boolean CompareOperators(String op1, String op2) => CompareOperators(this.GetOperatorOrDefault(op1), this.GetOperatorOrDefault(op2)); + + private Operator GetOperatorOrDefault(String op) => this._operators.FirstOrDefault(x => x.Name == op) ?? new Operator { Name = op, Precedence = 0 }; + } } diff --git a/Swan.Lite/Parsers/VerbOptionAttribute.cs b/Swan.Lite/Parsers/VerbOptionAttribute.cs index 79496b2..a31342d 100644 --- a/Swan.Lite/Parsers/VerbOptionAttribute.cs +++ b/Swan.Lite/Parsers/VerbOptionAttribute.cs @@ -1,40 +1,39 @@ using System; -namespace Swan.Parsers -{ +namespace Swan.Parsers { + /// + /// Models a verb option. + /// + [AttributeUsage(AttributeTargets.Property)] + public sealed class VerbOptionAttribute : Attribute { /// - /// Models a verb option. + /// Initializes a new instance of the class. /// - [AttributeUsage(AttributeTargets.Property)] - public sealed class VerbOptionAttribute : Attribute - { - /// - /// Initializes a new instance of the class. - /// - /// The name. - /// name. - public VerbOptionAttribute(string name) - { - Name = name ?? throw new ArgumentNullException(nameof(name)); - } - - /// - /// Gets the name of the verb option. - /// - /// - /// Name. - /// - public string Name { get; } - - /// - /// Gets or sets a short description of this command line verb. Usually a sentence summary. - /// - /// - /// The help text. - /// - public string HelpText { get; set; } - - /// - public override string ToString() => $" {Name}\t\t{HelpText}"; - } + /// The name. + /// name. + public VerbOptionAttribute(String name) => this.Name = name ?? throw new ArgumentNullException(nameof(name)); + + /// + /// Gets the name of the verb option. + /// + /// + /// Name. + /// + public String Name { + get; + } + + /// + /// Gets or sets a short description of this command line verb. Usually a sentence summary. + /// + /// + /// The help text. + /// + public String HelpText { + get; set; + } + + /// + public override String ToString() => $" {this.Name}\t\t{this.HelpText}"; + } } \ No newline at end of file diff --git a/Swan.Lite/Reflection/AttributeCache.cs b/Swan.Lite/Reflection/AttributeCache.cs index c4440af..46423fa 100644 --- a/Swan.Lite/Reflection/AttributeCache.cs +++ b/Swan.Lite/Reflection/AttributeCache.cs @@ -1,188 +1,161 @@ -using System; +#nullable enable +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Reflection; -namespace Swan.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); + /// - /// 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. + /// Initializes a new instance of the class. /// - 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) - { - 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 bool Contains(MemberInfo member) => _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, bool inherit = false) - where T : Attribute - { - if (member == null) - throw new ArgumentNullException(nameof(member)); - - return 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, bool inherit = false) - { - if (member == null) - throw new ArgumentNullException(nameof(member)); - - if (type == null) - throw new ArgumentNullException(nameof(type)); - - return 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, bool inherit = false) - where T : Attribute - { - if (member == null) - return default; - - var attr = 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(bool inherit = false) - where TAttribute : Attribute - { - var attr = 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, bool inherit = false) - where T : Attribute => - PropertyTypeCache.RetrieveAllProperties(type, true) - .ToDictionary(x => x, x => 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(bool inherit = false) - => 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, bool inherit = false) - { - if (attributeType == null) - throw new ArgumentNullException(nameof(attributeType)); - - return PropertyTypeCache.RetrieveAllProperties(true) - .ToDictionary(x => x, x => Retrieve(x, attributeType, inherit)); - } - - private static T ConvertToAttribute(IEnumerable attr) - where T : Attribute - { - if (attr?.Any() != true) - return default; - - return 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 _data.Value.GetOrAdd(key, k => factory.Invoke(k).Where(item => item != null)); - } - } + /// 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.Lite/Reflection/ConstructorTypeCache.cs b/Swan.Lite/Reflection/ConstructorTypeCache.cs index 5498213..e1a42af 100644 --- a/Swan.Lite/Reflection/ConstructorTypeCache.cs +++ b/Swan.Lite/Reflection/ConstructorTypeCache.cs @@ -4,48 +4,39 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -namespace Swan.Lite.Reflection -{ +namespace Swan.Lite.Reflection { + /// + /// A thread-safe cache of constructors belonging to a given type. + /// + public class ConstructorTypeCache : TypeCache> { /// - /// A thread-safe cache of constructors belonging to a given type. + /// Gets the default cache. /// - 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(bool includeNonPublic = false) - => 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, bool includeNonPublic = false) - => Retrieve(type, GetConstructors(includeNonPublic)); - - private static Func>> GetConstructors(bool 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(); - } + /// + /// 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.Lite/Reflection/ExtendedPropertyInfo.cs b/Swan.Lite/Reflection/ExtendedPropertyInfo.cs index aa869f1..a3e3ee4 100644 --- a/Swan.Lite/Reflection/ExtendedPropertyInfo.cs +++ b/Swan.Lite/Reflection/ExtendedPropertyInfo.cs @@ -2,106 +2,112 @@ using System.Reflection; using Swan.Configuration; -namespace Swan.Reflection -{ +namespace Swan.Reflection { + /// + /// Represents a Property object from a Object Reflection Property with extended values. + /// + public class ExtendedPropertyInfo { /// - /// Represents a Property object from a Object Reflection Property with extended values. + /// Initializes a new instance of the class. /// - public class ExtendedPropertyInfo - { - /// - /// Initializes a new instance of the class. - /// - /// The property information. - public ExtendedPropertyInfo(PropertyInfo propertyInfo) - { - if (propertyInfo == null) - { - throw new ArgumentNullException(nameof(propertyInfo)); - } - - Property = propertyInfo.Name; - DataType = propertyInfo.PropertyType.Name; - - foreach (PropertyDisplayAttribute display in AttributeCache.DefaultCache.Value.Retrieve(propertyInfo, true)) - { - Name = display.Name; - Description = display.Description; - GroupName = display.GroupName; - DefaultValue = display.DefaultValue; - } - } - - /// - /// Gets or sets the property. - /// - /// - /// The property. - /// - public string Property { get; } - - /// - /// Gets or sets the type of the data. - /// - /// - /// The type of the data. - /// - public string DataType { get; } - - /// - /// Gets or sets the value. - /// - /// - /// The value. - /// - public object Value { get; set; } - - /// - /// Gets or sets the default value. - /// - /// - /// The default value. - /// - public object DefaultValue { get; } - - /// - /// Gets or sets the name. - /// - /// - /// The name. - /// - public string Name { get; } - - /// - /// Gets or sets the description. - /// - /// - /// The description. - /// - public string Description { get; } - - /// - /// Gets or sets the name of the group. - /// - /// - /// The name of the group. - /// - public string GroupName { get; } - } - + /// The property information. + public ExtendedPropertyInfo(PropertyInfo propertyInfo) { + if(propertyInfo == null) { + throw new ArgumentNullException(nameof(propertyInfo)); + } + + this.Property = propertyInfo.Name; + this.DataType = propertyInfo.PropertyType.Name; + + foreach(PropertyDisplayAttribute display in AttributeCache.DefaultCache.Value.Retrieve(propertyInfo, true)) { + this.Name = display.Name; + this.Description = display.Description; + this.GroupName = display.GroupName; + this.DefaultValue = display.DefaultValue; + } + } + /// - /// Represents a Property object from a Object Reflection Property with extended values. + /// Gets or sets the property. /// - /// The type of the object. - public class ExtendedPropertyInfo : ExtendedPropertyInfo - { - /// - /// Initializes a new instance of the class. - /// - /// The property. - public ExtendedPropertyInfo(string property) - : base(typeof(T).GetProperty(property)) - { - } - } + /// + /// The property. + /// + public String Property { + get; + } + + /// + /// Gets or sets the type of the data. + /// + /// + /// The type of the data. + /// + public String DataType { + get; + } + + /// + /// Gets or sets the value. + /// + /// + /// The value. + /// + public Object Value { + get; set; + } + + /// + /// Gets or sets the default value. + /// + /// + /// The default value. + /// + public Object DefaultValue { + get; + } + + /// + /// Gets or sets the name. + /// + /// + /// The name. + /// + public String Name { + get; + } + + /// + /// Gets or sets the description. + /// + /// + /// The description. + /// + public String Description { + get; + } + + /// + /// Gets or sets the name of the group. + /// + /// + /// The name of the group. + /// + public String GroupName { + get; + } + } + + /// + /// Represents a Property object from a Object Reflection Property with extended values. + /// + /// The type of the object. + public class ExtendedPropertyInfo : ExtendedPropertyInfo { + /// + /// Initializes a new instance of the class. + /// + /// The property. + public ExtendedPropertyInfo(String property) : base(typeof(T).GetProperty(property)) { + } + } } \ No newline at end of file diff --git a/Swan.Lite/Reflection/ExtendedTypeInfo.cs b/Swan.Lite/Reflection/ExtendedTypeInfo.cs index fbaed95..020bcf9 100644 --- a/Swan.Lite/Reflection/ExtendedTypeInfo.cs +++ b/Swan.Lite/Reflection/ExtendedTypeInfo.cs @@ -1,267 +1,247 @@ -using System; +#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 +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 = { - 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(float), - typeof(int), - typeof(uint), - typeof(long), - typeof(ulong), - typeof(short), - typeof(ushort), - }; - - private readonly ParameterInfo[]? _tryParseParameters; - private readonly int _toStringArgumentLength; - - #region Constructors - - /// - /// Initializes a new instance of the class. - /// - /// The t. - public ExtendedTypeInfo(Type t) - { - Type = t ?? throw new ArgumentNullException(nameof(t)); - IsNullableValueType = Type.IsGenericType - && Type.GetGenericTypeDefinition() == typeof(Nullable<>); - - IsValueType = t.IsValueType; - - UnderlyingType = IsNullableValueType ? - new NullableConverter(Type).UnderlyingType : - Type; - - IsNumeric = NumericTypes.Contains(UnderlyingType); - - // Extract the TryParse method info - try - { - TryParseMethodInfo = UnderlyingType.GetMethod(TryParseMethodName, - new[] { typeof(string), typeof(NumberStyles), typeof(IFormatProvider), UnderlyingType.MakeByRefType() }) ?? - UnderlyingType.GetMethod(TryParseMethodName, - new[] { typeof(string), UnderlyingType.MakeByRefType() }); - - _tryParseParameters = TryParseMethodInfo?.GetParameters(); - } - catch - { - // ignored - } - - // Extract the ToString method Info - try - { - ToStringMethodInfo = UnderlyingType.GetMethod(ToStringMethodName, - new[] { typeof(IFormatProvider) }) ?? - UnderlyingType.GetMethod(ToStringMethodName, - Array.Empty()); - - _toStringArgumentLength = 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 bool IsNullableValueType { get; } - - /// - /// Gets a value indicating whether the type or underlying type is numeric. - /// - /// - /// true if this instance is numeric; otherwise, false. - /// - public bool IsNumeric { get; } - - /// - /// Gets a value indicating whether the type is value type. - /// Nullable value types have this property set to False. - /// - public bool 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 bool CanParseNatively => 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 bool TryParse(string s, out object? result) - { - result = Type.GetDefault(); - - try - { - if (Type == typeof(string)) - { - result = Convert.ChangeType(s, Type, CultureInfo.InvariantCulture); - return true; - } - - if ((IsNullableValueType && string.IsNullOrEmpty(s)) || !CanParseNatively) - { - return true; - } - - // Build the arguments of the TryParse method - var dynamicArguments = new List { s }; - - for (var pi = 1; pi < _tryParseParameters.Length - 1; pi++) - { - var argInfo = _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); - var parseArguments = dynamicArguments.ToArray(); - - if ((bool) TryParseMethodInfo.Invoke(null, parseArguments)) - { - result = parseArguments[parseArguments.Length - 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) - { - if (instance == null) - return string.Empty; - - return _toStringArgumentLength != 1 - ? instance.ToString() - : ToStringMethodInfo.Invoke(instance, new object[] {CultureInfo.InvariantCulture}) as string ?? string.Empty; - } - - #endregion - } - + 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 + /// - /// 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. + /// Initializes a new instance of the class. /// - /// 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); - } + /// 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.Lite/Reflection/IPropertyProxy.cs b/Swan.Lite/Reflection/IPropertyProxy.cs index 8b88856..169786d 100644 --- a/Swan.Lite/Reflection/IPropertyProxy.cs +++ b/Swan.Lite/Reflection/IPropertyProxy.cs @@ -1,22 +1,22 @@ -namespace Swan.Reflection -{ +using System; + +namespace Swan.Reflection { + /// + /// Represents a generic interface to store getters and setters. + /// + public interface IPropertyProxy { /// - /// Represents a generic interface to store getters and setters. + /// Gets the property value via a stored delegate. /// - 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); - } + /// 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.Lite/Reflection/MethodInfoCache.cs b/Swan.Lite/Reflection/MethodInfoCache.cs index ca363d0..e0c8c80 100644 --- a/Swan.Lite/Reflection/MethodInfoCache.cs +++ b/Swan.Lite/Reflection/MethodInfoCache.cs @@ -2,116 +2,109 @@ using System.Collections.Concurrent; using System.Reflection; -namespace Swan.Reflection -{ +namespace Swan.Reflection { + /// + /// Represents a Method Info Cache. + /// + public class MethodInfoCache : ConcurrentDictionary { /// - /// Represents a Method Info Cache. + /// 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. /// - public class MethodInfoCache : ConcurrentDictionary - { - /// - /// 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 type. - /// The name. - /// The alias. - /// The types. - /// - /// The cached MethodInfo. - /// - /// name - /// or - /// factory. - public MethodInfo Retrieve(string name, string alias, params Type[] types) - => Retrieve(typeof(T), name, alias, types); - - /// - /// Retrieves the specified name. - /// - /// The type of type. - /// The name. - /// The types. - /// - /// The cached MethodInfo. - /// - public MethodInfo Retrieve(string name, params Type[] types) - => Retrieve(typeof(T), name, name, types); - - /// - /// Retrieves the specified type. - /// - /// The type. - /// The name. - /// The types. - /// - /// An array of the properties stored for the specified type. - /// - public MethodInfo Retrieve(Type type, string name, params Type[] types) - => Retrieve(type, name, name, types); - - /// - /// Retrieves the specified type. - /// - /// The type. - /// The name. - /// The alias. - /// The types. - /// - /// The cached MethodInfo. - /// - public MethodInfo Retrieve(Type type, string name, string alias, params Type[] types) - { - if (type == null) - throw new ArgumentNullException(nameof(type)); - - if (alias == null) - throw new ArgumentNullException(nameof(alias)); - - if (name == null) - throw new ArgumentNullException(nameof(name)); - - return GetOrAdd( - alias, - x => type.GetMethod(name, types ?? Array.Empty())); - } - - /// - /// Retrieves the specified name. - /// - /// The type of type. - /// The name. - /// - /// The cached MethodInfo. - /// - public MethodInfo Retrieve(string name) - => Retrieve(typeof(T), name); - - /// - /// Retrieves the specified type. - /// - /// The type. - /// The name. - /// - /// The cached MethodInfo. - /// - /// - /// type - /// or - /// name. - /// - public MethodInfo Retrieve(Type type, string name) - { - if (type == null) - throw new ArgumentNullException(nameof(type)); - - if (name == null) - throw new ArgumentNullException(nameof(name)); - - return GetOrAdd( - name, - type.GetMethod); - } - } + /// The type of type. + /// The name. + /// The alias. + /// The types. + /// + /// The cached MethodInfo. + /// + /// name + /// or + /// factory. + public MethodInfo Retrieve(String name, String alias, params Type[] types) => this.Retrieve(typeof(T), name, alias, types); + + /// + /// Retrieves the specified name. + /// + /// The type of type. + /// The name. + /// The types. + /// + /// The cached MethodInfo. + /// + public MethodInfo Retrieve(String name, params Type[] types) => this.Retrieve(typeof(T), name, name, types); + + /// + /// Retrieves the specified type. + /// + /// The type. + /// The name. + /// The types. + /// + /// An array of the properties stored for the specified type. + /// + public MethodInfo Retrieve(Type type, String name, params Type[] types) => this.Retrieve(type, name, name, types); + + /// + /// Retrieves the specified type. + /// + /// The type. + /// The name. + /// The alias. + /// The types. + /// + /// The cached MethodInfo. + /// + public MethodInfo Retrieve(Type type, String name, String alias, params Type[] types) { + if(type == null) { + throw new ArgumentNullException(nameof(type)); + } + + if(alias == null) { + throw new ArgumentNullException(nameof(alias)); + } + + if(name == null) { + throw new ArgumentNullException(nameof(name)); + } + + return this.GetOrAdd(alias, x => type.GetMethod(name, types ?? Array.Empty())); + } + + /// + /// Retrieves the specified name. + /// + /// The type of type. + /// The name. + /// + /// The cached MethodInfo. + /// + public MethodInfo Retrieve(String name) => this.Retrieve(typeof(T), name); + + /// + /// Retrieves the specified type. + /// + /// The type. + /// The name. + /// + /// The cached MethodInfo. + /// + /// + /// type + /// or + /// name. + /// + public MethodInfo Retrieve(Type type, String name) { + if(type == null) { + throw new ArgumentNullException(nameof(type)); + } + + if(name == null) { + throw new ArgumentNullException(nameof(name)); + } + + return this.GetOrAdd(name, type.GetMethod); + } + } } diff --git a/Swan.Lite/Reflection/PropertyProxy.cs b/Swan.Lite/Reflection/PropertyProxy.cs index 0373d72..f06b58e 100644 --- a/Swan.Lite/Reflection/PropertyProxy.cs +++ b/Swan.Lite/Reflection/PropertyProxy.cs @@ -2,46 +2,43 @@ using System.Reflection; using System.Runtime.CompilerServices; -namespace Swan.Reflection -{ +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; + /// - /// Represents a generic class to store getters and setters. + /// Initializes a new instance of the class. /// - /// 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)); - - var getterInfo = property.GetGetMethod(false); - if (getterInfo != null) - _getter = (Func)Delegate.CreateDelegate(typeof(Func), getterInfo); - - var setterInfo = property.GetSetMethod(false); - if (setterInfo != null) - _setter = (Action)Delegate.CreateDelegate(typeof(Action), setterInfo); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - object IPropertyProxy.GetValue(object instance) => - _getter(instance as TClass); - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - void IPropertyProxy.SetValue(object instance, object value) => - _setter(instance as TClass, (TProperty)value); - } + /// 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.Lite/Reflection/PropertyTypeCache.cs b/Swan.Lite/Reflection/PropertyTypeCache.cs index 444e8fa..c5ec4c6 100644 --- a/Swan.Lite/Reflection/PropertyTypeCache.cs +++ b/Swan.Lite/Reflection/PropertyTypeCache.cs @@ -3,72 +3,54 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -namespace Swan.Reflection -{ +namespace Swan.Reflection { + /// + /// A thread-safe cache of properties belonging to a given type. + /// + public class PropertyTypeCache : TypeCache { /// - /// A thread-safe cache of properties belonging to a given type. + /// Gets the default cache. /// - 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(bool onlyPublic = false) - => 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, bool onlyPublic = false) - => 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, - bool onlyPublic, - Func filter) - => 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)); - } + /// + /// 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.Lite/Reflection/TypeCache.cs b/Swan.Lite/Reflection/TypeCache.cs index 48ea8cd..d6bd3b3 100644 --- a/Swan.Lite/Reflection/TypeCache.cs +++ b/Swan.Lite/Reflection/TypeCache.cs @@ -3,76 +3,69 @@ using System.Collections.Generic; using System.Reflection; using Swan.Collections; -namespace Swan.Reflection -{ +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 { /// - /// 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. + /// Determines whether the cache contains the specified type. /// - /// 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 bool Contains() => 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) - => Retrieve(typeof(TOut), factory); - } - + /// The type of the out. + /// + /// true if [contains]; otherwise, false. + /// + public Boolean Contains() => this.ContainsKey(typeof(TOut)); + /// - /// 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. + /// 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. /// - 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() - => Retrieve(GetAllFieldsFunc()); - - /// - /// Retrieves all fields. - /// - /// The type. - /// - /// A collection with all the fields in the given type. - /// - public IEnumerable RetrieveAllFields(Type type) - => Retrieve(type, GetAllFieldsFunc()); - - private static Func> GetAllFieldsFunc() - => t => t.GetFields(BindingFlags.Public | BindingFlags.Instance); - } + /// 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.Lite/SingletonBase.cs b/Swan.Lite/SingletonBase.cs index c932abb..cc03cdb 100644 --- a/Swan.Lite/SingletonBase.cs +++ b/Swan.Lite/SingletonBase.cs @@ -1,59 +1,54 @@ using System; -namespace Swan -{ +namespace Swan { + /// + /// Represents a singleton pattern abstract class. + /// + /// The type of class. + public abstract class SingletonBase : IDisposable where T : class { /// - /// Represents a singleton pattern abstract class. + /// The static, singleton instance reference. /// - /// 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 bool _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() => 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(bool disposeManaged) - { - if (_isDisposing) return; - - _isDisposing = true; - - // free managed resources - if (LazyInstance == null) return; - - try - { - var disposableInstance = LazyInstance.Value as IDisposable; - disposableInstance?.Dispose(); - } - catch - { - // swallow - } - } - } + 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.Lite/StringConversionException.cs b/Swan.Lite/StringConversionException.cs index 30f7517..d34f326 100644 --- a/Swan.Lite/StringConversionException.cs +++ b/Swan.Lite/StringConversionException.cs @@ -1,76 +1,62 @@ using System; using System.Runtime.Serialization; -namespace Swan -{ +namespace Swan { + /// + /// The exception that is thrown when a conversion from a string to a + /// specified type fails. + /// + /// + [Serializable] + public class StringConversionException : Exception { /// - /// The exception that is thrown when a conversion from a string to a - /// specified type fails. + /// Initializes a new instance of the class. /// - /// - [Serializable] - public class StringConversionException : Exception - { - /// - /// Initializes a new instance of the class. - /// - public StringConversionException() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The error message that explains the reason for the exception. - public StringConversionException(string message) - : base(message) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, - /// or if no inner exception is specified. - public StringConversionException(string message, Exception innerException) - : base(message, innerException) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The desired resulting type of the attempted conversion. - public StringConversionException(Type type) - : base(BuildStandardMessageForType(type)) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The desired resulting type of the attempted conversion. - /// The exception that is the cause of the current exception, - /// or if no inner exception is specified. - public StringConversionException(Type type, Exception innerException) - : base(BuildStandardMessageForType(type), innerException) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data - /// about the exception being thrown. - /// The that contains contextual information - /// about the source or destination. - protected StringConversionException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } - - private static string BuildStandardMessageForType(Type type) - => $"Cannot convert a string to an instance of {type.FullName}"; - } + public StringConversionException() { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + public StringConversionException(String message) : base(message) { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, + /// or if no inner exception is specified. + public StringConversionException(String message, Exception innerException) : base(message, innerException) { + } + + /// + /// Initializes a new instance of the class. + /// + /// The desired resulting type of the attempted conversion. + public StringConversionException(Type type) : base(BuildStandardMessageForType(type)) { + } + + /// + /// Initializes a new instance of the class. + /// + /// The desired resulting type of the attempted conversion. + /// The exception that is the cause of the current exception, + /// or if no inner exception is specified. + public StringConversionException(Type type, Exception innerException) : base(BuildStandardMessageForType(type), innerException) { + } + + /// + /// Initializes a new instance of the class. + /// + /// The that holds the serialized object data + /// about the exception being thrown. + /// The that contains contextual information + /// about the source or destination. + protected StringConversionException(SerializationInfo info, StreamingContext context) : base(info, context) { + } + + private static String BuildStandardMessageForType(Type type) => $"Cannot convert a string to an instance of {type.FullName}"; + } } diff --git a/Swan.Lite/StructEndiannessAttribute.cs b/Swan.Lite/StructEndiannessAttribute.cs index 16b7932..5648093 100644 --- a/Swan.Lite/StructEndiannessAttribute.cs +++ b/Swan.Lite/StructEndiannessAttribute.cs @@ -1,30 +1,27 @@ using System; -namespace Swan -{ +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 { /// - /// An attribute used to help conversion structs back and forth into arrays of bytes via - /// extension methods included in this library ToStruct and ToBytes. + /// Initializes a new instance of the class. /// - /// - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Struct)] - public class StructEndiannessAttribute : Attribute - { - /// - /// Initializes a new instance of the class. - /// - /// The endianness. - public StructEndiannessAttribute(Endianness endianness) - { - Endianness = endianness; - } - - /// - /// Gets the endianness. - /// - /// - /// The endianness. - /// - public Endianness Endianness { get; } - } + /// 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.Lite/Swan.Lite.csproj b/Swan.Lite/Swan.Lite.csproj index 8d1f58e..198baea 100644 --- a/Swan.Lite/Swan.Lite.csproj +++ b/Swan.Lite/Swan.Lite.csproj @@ -13,6 +13,5 @@ https://raw.githubusercontent.com/unosquare/swan/master/LICENSE best-practices netcore network objectmapper json-serialization 8.0 - enable diff --git a/Swan.Lite/SwanRuntime.cs b/Swan.Lite/SwanRuntime.cs index de24abf..d93109d 100644 --- a/Swan.Lite/SwanRuntime.cs +++ b/Swan.Lite/SwanRuntime.cs @@ -4,230 +4,197 @@ using System.IO; using System.Reflection; using System.Threading; -namespace Swan -{ +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 + /// - /// Provides utility methods to retrieve information about the current application. + /// Gets the current Operating System. /// - public static class SwanRuntime - { - private static readonly Lazy EntryAssemblyLazy = new Lazy(Assembly.GetEntryAssembly); - - private static readonly Lazy CompanyNameLazy = new Lazy(() => - { - var attribute = - EntryAssembly.GetCustomAttribute(typeof(AssemblyCompanyAttribute)) as AssemblyCompanyAttribute; - return attribute?.Company ?? string.Empty; - }); - - private static readonly Lazy ProductNameLazy = new Lazy(() => - { - var attribute = - EntryAssembly.GetCustomAttribute(typeof(AssemblyProductAttribute)) as AssemblyProductAttribute; - return attribute?.Product ?? string.Empty; - }); - - private static readonly Lazy ProductTrademarkLazy = new Lazy(() => - { - var 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) - { - var windowsDirectory = Environment.GetEnvironmentVariable("windir"); - if (string.IsNullOrEmpty(windowsDirectory) == false - && windowsDirectory.Contains(@"\") - && Directory.Exists(windowsDirectory)) - { - _oS = OperatingSystem.Windows; - } - else - { - _oS = 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. - /// - public static bool IsTheOnlyInstance - { - get - { - lock (SyncLock) - { - try - { - // Try to open existing mutex. - Mutex.OpenExisting(ApplicationMutexName); - } - catch - { - try - { - // If exception occurred, there is no such mutex. - var 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 bool 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 - { - var uri = new UriBuilder(EntryAssembly.CodeBase); - var 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 - { - var localAppDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - EntryAssemblyName.Name); - - var 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)); - - var pathWithFilename = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), - filename); - - return Path.GetFullPath(pathWithFilename); - } - - #endregion - } + /// + /// 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.Lite/Terminal.Graphics.cs b/Swan.Lite/Terminal.Graphics.cs index 1a1b5be..45cb03d 100644 --- a/Swan.Lite/Terminal.Graphics.cs +++ b/Swan.Lite/Terminal.Graphics.cs @@ -1,37 +1,36 @@ -namespace Swan -{ +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 { /// - /// A console terminal helper to create nicer output and receive input from the user - /// This class is thread-safe :). + /// Represents a Table to print in console. /// - public static partial class Terminal - { - /// - /// Represents a Table to print in console. - /// - private static class Table - { - public static void Vertical() => Write('\u2502', Settings.BorderColor); - - public static void RightTee() => Write('\u2524', Settings.BorderColor); - - public static void TopRight() => Write('\u2510', Settings.BorderColor); - - public static void BottomLeft() => Write('\u2514', Settings.BorderColor); - - public static void BottomTee() => Write('\u2534', Settings.BorderColor); - - public static void TopTee() => Write('\u252c', Settings.BorderColor); - - public static void LeftTee() => Write('\u251c', Settings.BorderColor); - - public static void Horizontal(int length) => Write(new string('\u2500', length), Settings.BorderColor); - - public static void Tee() => Write('\u253c', Settings.BorderColor); - - public static void BottomRight() => Write('\u2518', Settings.BorderColor); - - public static void TopLeft() => Write('\u250C', Settings.BorderColor); - } - } + private static class Table { + public static void Vertical() => Write('\u2502', Settings.BorderColor); + + public static void RightTee() => Write('\u2524', Settings.BorderColor); + + public static void TopRight() => Write('\u2510', Settings.BorderColor); + + public static void BottomLeft() => Write('\u2514', Settings.BorderColor); + + public static void BottomTee() => Write('\u2534', Settings.BorderColor); + + public static void TopTee() => Write('\u252c', Settings.BorderColor); + + public static void LeftTee() => Write('\u251c', Settings.BorderColor); + + public static void Horizontal(Int32 length) => Write(new String('\u2500', length), Settings.BorderColor); + + public static void Tee() => Write('\u253c', Settings.BorderColor); + + public static void BottomRight() => Write('\u2518', Settings.BorderColor); + + public static void TopLeft() => Write('\u250C', Settings.BorderColor); + } + } } diff --git a/Swan.Lite/Terminal.Interaction.cs b/Swan.Lite/Terminal.Interaction.cs index 20d7e23..c41836d 100644 --- a/Swan.Lite/Terminal.Interaction.cs +++ b/Swan.Lite/Terminal.Interaction.cs @@ -1,261 +1,238 @@ -using System; +#nullable enable +using System; using System.Collections.Generic; -using Swan.Lite.Logging; using System.Globalization; -using Swan.Logging; + +using Swan.Lite.Logging; -namespace Swan -{ +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 ReadKey + /// - /// A console terminal helper to create nicer output and receive input from the user - /// This class is thread-safe :). + /// Reads a key from the Terminal. This is the closest equivalent to Console.ReadKey. /// - public static partial class Terminal - { - #region ReadKey - - /// - /// Reads a key from the Terminal. This is the closest equivalent to Console.ReadKey. - /// - /// if set to true the pressed key will not be rendered to the output. - /// if set to true the output will continue to be shown. - /// This is useful for services and daemons that are running as console applications and wait for a key to exit the program. - /// The console key information. - public static ConsoleKeyInfo ReadKey(bool intercept, bool disableLocking = false) - { - if (!IsConsolePresent) return default; - if (disableLocking) return Console.ReadKey(intercept); - - lock (SyncLock) - { - Flush(); - InputDone.Reset(); - - try - { - Console.CursorVisible = true; - return Console.ReadKey(intercept); - } - finally - { - Console.CursorVisible = false; - InputDone.Set(); - } - } - } - - /// - /// Reads a key from the Terminal. - /// - /// The prompt. - /// if set to true [prevent echo]. - /// The console key information. - public static ConsoleKeyInfo ReadKey(string prompt, bool preventEcho = true) - { - if (!IsConsolePresent) return default; - - lock (SyncLock) - { - if (prompt != null) - { - Write($"{GetNowFormatted()}{Settings.UserInputPrefix} << {prompt} ", ConsoleColor.White); - } - - var input = ReadKey(true); - var echo = preventEcho ? string.Empty : input.Key.ToString(); - WriteLine(echo); - return input; - } - } - - #endregion - - #region Other Terminal Read Methods - - /// - /// Clears the screen. - /// - public static void Clear() - { - Flush(); - Console.Clear(); - } - - /// - /// Reads a line of text from the console. - /// - /// The read line. - public static string? ReadLine() - { - if (IsConsolePresent == false) return default; - - lock (SyncLock) - { - Flush(); - InputDone.Reset(); - - try - { - Console.CursorVisible = true; - return Console.ReadLine(); - } - finally - { - Console.CursorVisible = false; - InputDone.Set(); - } - } - } - - /// - /// Reads a line from the input. - /// - /// The prompt. - /// The read line. - public static string? ReadLine(string prompt) - { - if (!IsConsolePresent) return null; - - lock (SyncLock) - { - Write($"{GetNowFormatted()}{Settings.UserInputPrefix} << {prompt}: ", ConsoleColor.White); - - return ReadLine(); - } - } - - /// - /// Reads a number from the input. If unable to parse, it returns the default number. - /// - /// The prompt. - /// The default number. - /// - /// Conversions of string representation of a number to its 32-bit signed integer equivalent. - /// - public static int ReadNumber(string prompt, int defaultNumber) - { - if (!IsConsolePresent) return defaultNumber; - - lock (SyncLock) - { - Write($"{GetNowFormatted()}{Settings.UserInputPrefix} << {prompt} (default is {defaultNumber}): ", - ConsoleColor.White); - - var input = ReadLine(); - return int.TryParse(input, out var parsedInt) ? parsedInt : defaultNumber; - } - } - - /// - /// Creates a table prompt where the user can enter an option based on the options dictionary provided. - /// - /// The title. - /// The options. - /// Any key option. - /// - /// A value that identifies the console key that was pressed. - /// - /// options. - public static ConsoleKeyInfo ReadPrompt( - string title, - IDictionary options, - string anyKeyOption) - { - if (!IsConsolePresent) return default; - - if (options == null) - throw new ArgumentNullException(nameof(options)); - - const ConsoleColor textColor = ConsoleColor.White; - var lineLength = Console.WindowWidth; - var lineAlign = -(lineLength - 2); - var textFormat = "{0," + lineAlign + "}"; - - // lock the output as an atomic operation - lock (SyncLock) - { - { - // Top border - Table.TopLeft(); - Table.Horizontal(-lineAlign); - Table.TopRight(); - } - - { - // Title - Table.Vertical(); - var titleText = string.Format(CultureInfo.CurrentCulture, - textFormat, - string.IsNullOrWhiteSpace(title) ? " Select an option from the list below." : $" {title}"); - Write(titleText, textColor); - Table.Vertical(); - } - - { - // Title Bottom - Table.LeftTee(); - Table.Horizontal(lineLength - 2); - Table.RightTee(); - } - - // Options - foreach (var kvp in options) - { - Table.Vertical(); - Write(string.Format( - CultureInfo.CurrentCulture, - textFormat, - $" {"[ " + kvp.Key + " ]",-10} {kvp.Value}"), - textColor); - Table.Vertical(); - } - - // Any Key Options - if (string.IsNullOrWhiteSpace(anyKeyOption) == false) - { - Table.Vertical(); - Write(string.Format(CultureInfo.CurrentCulture, textFormat, " "), ConsoleColor.Gray); - Table.Vertical(); - - Table.Vertical(); - Write(string.Format( - CultureInfo.CurrentCulture, - textFormat, - $" {" ",-10} {anyKeyOption}"), - ConsoleColor.Gray); - Table.Vertical(); - } - - { - // Input section - Table.LeftTee(); - Table.Horizontal(lineLength - 2); - Table.RightTee(); - - Table.Vertical(); - Write(string.Format(CultureInfo.CurrentCulture, textFormat, Settings.UserOptionText), - ConsoleColor.Green); - Table.Vertical(); - - Table.BottomLeft(); - Table.Horizontal(lineLength - 2); - Table.BottomRight(); - } - } - - var inputLeft = Settings.UserOptionText.Length + 3; - - SetCursorPosition(inputLeft, CursorTop - 1); - var userInput = ReadKey(true); - Write(userInput.Key.ToString(), ConsoleColor.Gray); - - SetCursorPosition(0, CursorTop + 2); - return userInput; - } - - #endregion - - private static string GetNowFormatted() => - $" {(string.IsNullOrWhiteSpace(TextLogger.LoggingTimeFormat) ? string.Empty : DateTime.Now.ToString(TextLogger.LoggingTimeFormat) + " ")}"; - } + /// if set to true the pressed key will not be rendered to the output. + /// if set to true the output will continue to be shown. + /// This is useful for services and daemons that are running as console applications and wait for a key to exit the program. + /// The console key information. + public static ConsoleKeyInfo ReadKey(Boolean intercept, Boolean disableLocking = false) { + if(!IsConsolePresent) { + return default; + } + + if(disableLocking) { + return Console.ReadKey(intercept); + } + + lock(SyncLock) { + Flush(); + InputDone.Reset(); + + try { + Console.CursorVisible = true; + return Console.ReadKey(intercept); + } finally { + Console.CursorVisible = false; + InputDone.Set(); + } + } + } + + /// + /// Reads a key from the Terminal. + /// + /// The prompt. + /// if set to true [prevent echo]. + /// The console key information. + public static ConsoleKeyInfo ReadKey(String prompt, Boolean preventEcho = true) { + if(!IsConsolePresent) { + return default; + } + + lock(SyncLock) { + if(prompt != null) { + Write($"{GetNowFormatted()}{Settings.UserInputPrefix} << {prompt} ", ConsoleColor.White); + } + + ConsoleKeyInfo input = ReadKey(true); + String echo = preventEcho ? String.Empty : input.Key.ToString(); + WriteLine(echo); + return input; + } + } + + #endregion + + #region Other Terminal Read Methods + + /// + /// Clears the screen. + /// + public static void Clear() { + Flush(); + Console.Clear(); + } + + /// + /// Reads a line of text from the console. + /// + /// The read line. + public static String? ReadLine() { + if(IsConsolePresent == false) { + return default; + } + + lock(SyncLock) { + Flush(); + InputDone.Reset(); + + try { + Console.CursorVisible = true; + return Console.ReadLine(); + } finally { + Console.CursorVisible = false; + InputDone.Set(); + } + } + } + + /// + /// Reads a line from the input. + /// + /// The prompt. + /// The read line. + public static String? ReadLine(String prompt) { + if(!IsConsolePresent) { + return null; + } + + lock(SyncLock) { + Write($"{GetNowFormatted()}{Settings.UserInputPrefix} << {prompt}: ", ConsoleColor.White); + + return ReadLine(); + } + } + + /// + /// Reads a number from the input. If unable to parse, it returns the default number. + /// + /// The prompt. + /// The default number. + /// + /// Conversions of string representation of a number to its 32-bit signed integer equivalent. + /// + public static Int32 ReadNumber(String prompt, Int32 defaultNumber) { + if(!IsConsolePresent) { + return defaultNumber; + } + + lock(SyncLock) { + Write($"{GetNowFormatted()}{Settings.UserInputPrefix} << {prompt} (default is {defaultNumber}): ", ConsoleColor.White); + + String? input = ReadLine(); + return Int32.TryParse(input, out Int32 parsedInt) ? parsedInt : defaultNumber; + } + } + + /// + /// Creates a table prompt where the user can enter an option based on the options dictionary provided. + /// + /// The title. + /// The options. + /// Any key option. + /// + /// A value that identifies the console key that was pressed. + /// + /// options. + public static ConsoleKeyInfo ReadPrompt(String title, IDictionary options, String anyKeyOption) { + if(!IsConsolePresent) { + return default; + } + + if(options == null) { + throw new ArgumentNullException(nameof(options)); + } + + const ConsoleColor textColor = ConsoleColor.White; + Int32 lineLength = Console.WindowWidth; + Int32 lineAlign = -(lineLength - 2); + String textFormat = "{0," + lineAlign + "}"; + + // lock the output as an atomic operation + lock(SyncLock) { + { + // Top border + Table.TopLeft(); + Table.Horizontal(-lineAlign); + Table.TopRight(); + } + + { + // Title + Table.Vertical(); + String titleText = String.Format(CultureInfo.CurrentCulture, textFormat, String.IsNullOrWhiteSpace(title) ? " Select an option from the list below." : $" {title}"); + Write(titleText, textColor); + Table.Vertical(); + } + + { + // Title Bottom + Table.LeftTee(); + Table.Horizontal(lineLength - 2); + Table.RightTee(); + } + + // Options + foreach(KeyValuePair kvp in options) { + Table.Vertical(); + Write(String.Format(CultureInfo.CurrentCulture, textFormat, $" {"[ " + kvp.Key + " ]",-10} {kvp.Value}"), textColor); + Table.Vertical(); + } + + // Any Key Options + if(String.IsNullOrWhiteSpace(anyKeyOption) == false) { + Table.Vertical(); + Write(String.Format(CultureInfo.CurrentCulture, textFormat, " "), ConsoleColor.Gray); + Table.Vertical(); + + Table.Vertical(); + Write(String.Format(CultureInfo.CurrentCulture, textFormat, $" {" ",-10} {anyKeyOption}"), ConsoleColor.Gray); + Table.Vertical(); + } + + { + // Input section + Table.LeftTee(); + Table.Horizontal(lineLength - 2); + Table.RightTee(); + + Table.Vertical(); + Write(String.Format(CultureInfo.CurrentCulture, textFormat, Settings.UserOptionText), ConsoleColor.Green); + Table.Vertical(); + + Table.BottomLeft(); + Table.Horizontal(lineLength - 2); + Table.BottomRight(); + } + } + + Int32 inputLeft = Settings.UserOptionText.Length + 3; + + SetCursorPosition(inputLeft, CursorTop - 1); + ConsoleKeyInfo userInput = ReadKey(true); + Write(userInput.Key.ToString(), ConsoleColor.Gray); + + SetCursorPosition(0, CursorTop + 2); + return userInput; + } + + #endregion + + private static String GetNowFormatted() => $" {(String.IsNullOrWhiteSpace(TextLogger.LoggingTimeFormat) ? String.Empty : DateTime.Now.ToString(TextLogger.LoggingTimeFormat) + " ")}"; + } } diff --git a/Swan.Lite/Terminal.Output.cs b/Swan.Lite/Terminal.Output.cs index d9b8f28..ec08772 100644 --- a/Swan.Lite/Terminal.Output.cs +++ b/Swan.Lite/Terminal.Output.cs @@ -1,97 +1,88 @@ -using System; +#nullable enable +using System; -namespace Swan -{ +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 { /// - /// A console terminal helper to create nicer output and receive input from the user - /// This class is thread-safe :). + /// Writes a character a number of times, optionally adding a new line at the end. /// - 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, int count = 1, bool newLine = false, TerminalWriters writerFlags = TerminalWriters.StandardOutput) - { - lock (SyncLock) - { - var text = new string(charCode, count); - - if (newLine) - { - text += Environment.NewLine; - } - - var buffer = OutputEncoding.GetBytes(text); - var 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) - { - var buffer = OutputEncoding.GetBytes(text); - var 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; - } - } + /// 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.Lite/Terminal.Settings.cs b/Swan.Lite/Terminal.Settings.cs index 20d8118..abb700c 100644 --- a/Swan.Lite/Terminal.Settings.cs +++ b/Swan.Lite/Terminal.Settings.cs @@ -1,49 +1,46 @@ using System; -namespace Swan -{ +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 { /// - /// A console terminal helper to create nicer output and receive input from the user - /// This class is thread-safe :). + /// Terminal global settings. /// - 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: "; - } - } + 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.Lite/Terminal.cs b/Swan.Lite/Terminal.cs index c407e4c..596c8c8 100644 --- a/Swan.Lite/Terminal.cs +++ b/Swan.Lite/Terminal.cs @@ -2,338 +2,329 @@ using System.Collections.Concurrent; using System.Text; using System.Threading; + using Swan.Threading; -namespace Swan -{ +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 + /// - /// A console terminal helper to create nicer output and receive input from the user. - /// This class is thread-safe :). + /// Initializes static members of the class. /// - public static partial class Terminal - { - #region Private Declarations - - private const int 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 bool? _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 int 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 int 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 bool IsConsolePresent - { - get - { - if (_isConsolePresent == null) - { - _isConsolePresent = true; - try - { - var 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; - var 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(int left, int 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) - { - var 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 var 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() - { - OriginalColor = Settings.DefaultColor; - 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 - } + 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.Lite/TerminalWriters.Enums.cs b/Swan.Lite/TerminalWriters.Enums.cs index b1d4b9d..202f3b1 100644 --- a/Swan.Lite/TerminalWriters.Enums.cs +++ b/Swan.Lite/TerminalWriters.Enums.cs @@ -1,31 +1,29 @@ using System; -namespace Swan -{ +namespace Swan { + /// + /// Defines a set of bitwise standard terminal writers. + /// + [Flags] + public enum TerminalWriters { /// - /// Defines a set of bitwise standard terminal writers. + /// Prevents output /// - [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, - } + 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.Lite/Threading/AtomicBoolean.cs b/Swan.Lite/Threading/AtomicBoolean.cs index 7db2f17..c5510ef 100644 --- a/Swan.Lite/Threading/AtomicBoolean.cs +++ b/Swan.Lite/Threading/AtomicBoolean.cs @@ -1,24 +1,22 @@ -namespace Swan.Threading -{ +using System; + +namespace Swan.Threading { + /// + /// Fast, atomic boolean combining interlocked to write value and volatile to read values. + /// + public sealed class AtomicBoolean : AtomicTypeBase { /// - /// Fast, atomic boolean combining interlocked to write value and volatile to read values. + /// Initializes a new instance of the class. /// - public sealed class AtomicBoolean : AtomicTypeBase - { - /// - /// Initializes a new instance of the class. - /// - /// if set to true [initial value]. - public AtomicBoolean(bool initialValue = default) - : base(initialValue ? 1 : 0) - { - // placeholder - } - - /// - protected override bool FromLong(long backingValue) => backingValue != 0; - - /// - protected override long ToLong(bool value) => value ? 1 : 0; - } + /// 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.Lite/Threading/AtomicDateTime.cs b/Swan.Lite/Threading/AtomicDateTime.cs index 8bec976..dfbbbdd 100644 --- a/Swan.Lite/Threading/AtomicDateTime.cs +++ b/Swan.Lite/Threading/AtomicDateTime.cs @@ -1,26 +1,22 @@ using System; -namespace Swan.Threading -{ +namespace Swan.Threading { + /// + /// Defines an atomic DateTime. + /// + public sealed class AtomicDateTime : AtomicTypeBase { /// - /// Defines an atomic DateTime. + /// Initializes a new instance of the class. /// - public sealed class AtomicDateTime : AtomicTypeBase - { - /// - /// Initializes a new instance of the class. - /// - /// The initial value. - public AtomicDateTime(DateTime initialValue) - : base(initialValue.Ticks) - { - // placeholder - } - - /// - protected override DateTime FromLong(long backingValue) => new DateTime(backingValue); - - /// - protected override long ToLong(DateTime value) => value.Ticks; - } + /// The initial value. + public AtomicDateTime(DateTime initialValue) : base(initialValue.Ticks) { + // placeholder + } + + /// + protected override DateTime FromLong(Int64 backingValue) => new DateTime(backingValue); + + /// + protected override Int64 ToLong(DateTime value) => value.Ticks; + } } diff --git a/Swan.Lite/Threading/AtomicDouble.cs b/Swan.Lite/Threading/AtomicDouble.cs index b611a08..9372c44 100644 --- a/Swan.Lite/Threading/AtomicDouble.cs +++ b/Swan.Lite/Threading/AtomicDouble.cs @@ -1,28 +1,22 @@ using System; -namespace Swan.Threading -{ +namespace Swan.Threading { + /// + /// Fast, atomic double combining interlocked to write value and volatile to read values. + /// + public sealed class AtomicDouble : AtomicTypeBase { /// - /// Fast, atomic double combining interlocked to write value and volatile to read values. + /// Initializes a new instance of the class. /// - public sealed class AtomicDouble : AtomicTypeBase - { - /// - /// Initializes a new instance of the class. - /// - /// if set to true [initial value]. - public AtomicDouble(double initialValue = default) - : base(BitConverter.DoubleToInt64Bits(initialValue)) - { - // placeholder - } - - /// - protected override double FromLong(long backingValue) => - BitConverter.Int64BitsToDouble(backingValue); - - /// - protected override long ToLong(double value) => - BitConverter.DoubleToInt64Bits(value); - } + /// if set to true [initial value]. + public AtomicDouble(Double initialValue = default) : base(BitConverter.DoubleToInt64Bits(initialValue)) { + // placeholder + } + + /// + protected override Double FromLong(Int64 backingValue) => BitConverter.Int64BitsToDouble(backingValue); + + /// + protected override Int64 ToLong(Double value) => BitConverter.DoubleToInt64Bits(value); + } } \ No newline at end of file diff --git a/Swan.Lite/Threading/AtomicEnum.cs b/Swan.Lite/Threading/AtomicEnum.cs index b06af12..0551ba3 100644 --- a/Swan.Lite/Threading/AtomicEnum.cs +++ b/Swan.Lite/Threading/AtomicEnum.cs @@ -1,43 +1,38 @@ using System; using System.Threading; -namespace Swan.Threading -{ +namespace Swan.Threading { + /// + /// Defines an atomic generic Enum. + /// + /// The type of enum. + public sealed class AtomicEnum where T : struct, IConvertible { + private Int64 _backingValue; + /// - /// Defines an atomic generic Enum. + /// Initializes a new instance of the class. /// - /// The type of enum. - public sealed class AtomicEnum - where T : struct, IConvertible - { - private long _backingValue; - - /// - /// Initializes a new instance of the class. - /// - /// The initial value. - /// T must be an enumerated type. - public AtomicEnum(T initialValue) - { - if (!Enum.IsDefined(typeof(T), initialValue)) - throw new ArgumentException("T must be an enumerated type"); - - Value = initialValue; - } - - /// - /// Gets or sets the value. - /// - public T Value - { - get => (T)Enum.ToObject(typeof(T), BackingValue); - set => BackingValue = Convert.ToInt64(value); - } - - private long BackingValue - { - get => Interlocked.Read(ref _backingValue); - set => Interlocked.Exchange(ref _backingValue, value); - } - } + /// The initial value. + /// T must be an enumerated type. + public AtomicEnum(T initialValue) { + if(!Enum.IsDefined(typeof(T), initialValue)) { + throw new ArgumentException("T must be an enumerated type"); + } + + this.Value = initialValue; + } + + /// + /// Gets or sets the value. + /// + public T Value { + get => (T)Enum.ToObject(typeof(T), this.BackingValue); + set => this.BackingValue = Convert.ToInt64(value); + } + + private Int64 BackingValue { + get => Interlocked.Read(ref this._backingValue); + set => Interlocked.Exchange(ref this._backingValue, value); + } + } } \ No newline at end of file diff --git a/Swan.Lite/Threading/AtomicInteger.cs b/Swan.Lite/Threading/AtomicInteger.cs index 5cad25d..0193813 100644 --- a/Swan.Lite/Threading/AtomicInteger.cs +++ b/Swan.Lite/Threading/AtomicInteger.cs @@ -1,28 +1,22 @@ using System; -namespace Swan.Threading -{ +namespace Swan.Threading { + /// + /// Represents an atomically readable or writable integer. + /// + public class AtomicInteger : AtomicTypeBase { /// - /// Represents an atomically readable or writable integer. + /// Initializes a new instance of the class. /// - public class AtomicInteger : AtomicTypeBase - { - /// - /// Initializes a new instance of the class. - /// - /// if set to true [initial value]. - public AtomicInteger(int initialValue = default) - : base(Convert.ToInt64(initialValue)) - { - // placeholder - } - - /// - protected override int FromLong(long backingValue) => - Convert.ToInt32(backingValue); - - /// - protected override long ToLong(int value) => - Convert.ToInt64(value); - } + /// if set to true [initial value]. + public AtomicInteger(Int32 initialValue = default) : base(Convert.ToInt64(initialValue)) { + // placeholder + } + + /// + protected override Int32 FromLong(Int64 backingValue) => Convert.ToInt32(backingValue); + + /// + protected override Int64 ToLong(Int32 value) => Convert.ToInt64(value); + } } \ No newline at end of file diff --git a/Swan.Lite/Threading/AtomicLong.cs b/Swan.Lite/Threading/AtomicLong.cs index b675187..56023fb 100644 --- a/Swan.Lite/Threading/AtomicLong.cs +++ b/Swan.Lite/Threading/AtomicLong.cs @@ -1,24 +1,22 @@ -namespace Swan.Threading -{ +using System; + +namespace Swan.Threading { + /// + /// Fast, atomic long combining interlocked to write value and volatile to read values. + /// + public sealed class AtomicLong : AtomicTypeBase { /// - /// Fast, atomic long combining interlocked to write value and volatile to read values. + /// Initializes a new instance of the class. /// - public sealed class AtomicLong : AtomicTypeBase - { - /// - /// Initializes a new instance of the class. - /// - /// if set to true [initial value]. - public AtomicLong(long initialValue = default) - : base(initialValue) - { - // placeholder - } - - /// - protected override long FromLong(long backingValue) => backingValue; - - /// - protected override long ToLong(long value) => value; - } + /// if set to true [initial value]. + public AtomicLong(Int64 initialValue = default) : base(initialValue) { + // placeholder + } + + /// + protected override Int64 FromLong(Int64 backingValue) => backingValue; + + /// + protected override Int64 ToLong(Int64 value) => value; + } } diff --git a/Swan.Lite/Threading/AtomicTimeSpan.cs b/Swan.Lite/Threading/AtomicTimeSpan.cs index ec27877..3e1dc79 100644 --- a/Swan.Lite/Threading/AtomicTimeSpan.cs +++ b/Swan.Lite/Threading/AtomicTimeSpan.cs @@ -1,26 +1,22 @@ using System; -namespace Swan.Threading -{ +namespace Swan.Threading { + /// + /// Represents an atomic TimeSpan type. + /// + public sealed class AtomicTimeSpan : AtomicTypeBase { /// - /// Represents an atomic TimeSpan type. + /// Initializes a new instance of the class. /// - public sealed class AtomicTimeSpan : AtomicTypeBase - { - /// - /// Initializes a new instance of the class. - /// - /// The initial value. - public AtomicTimeSpan(TimeSpan initialValue) - : base(initialValue.Ticks) - { - // placeholder - } - - /// - protected override TimeSpan FromLong(long backingValue) => TimeSpan.FromTicks(backingValue); - - /// - protected override long ToLong(TimeSpan value) => value.Ticks < 0 ? 0 : value.Ticks; - } + /// The initial value. + public AtomicTimeSpan(TimeSpan initialValue) : base(initialValue.Ticks) { + // placeholder + } + + /// + protected override TimeSpan FromLong(Int64 backingValue) => TimeSpan.FromTicks(backingValue); + + /// + protected override Int64 ToLong(TimeSpan value) => value.Ticks < 0 ? 0 : value.Ticks; + } } diff --git a/Swan.Lite/Threading/AtomicTypeBase.cs b/Swan.Lite/Threading/AtomicTypeBase.cs index fae6cd3..4ea9c50 100644 --- a/Swan.Lite/Threading/AtomicTypeBase.cs +++ b/Swan.Lite/Threading/AtomicTypeBase.cs @@ -1,243 +1,219 @@ using System; using System.Threading; -namespace Swan.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; + /// - /// 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/. + /// Initializes a new instance of the class. /// - /// 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 long _backingValue; - - /// - /// Initializes a new instance of the class. - /// - /// The initial value. - protected AtomicTypeBase(long initialValue) - { - BackingValue = initialValue; - } - - /// - /// Gets or sets the value. - /// - public T Value - { - get => FromLong(BackingValue); - set => BackingValue = ToLong(value); - } - - /// - /// Gets or sets the backing value. - /// - protected long BackingValue - { - get => Interlocked.Read(ref _backingValue); - set => Interlocked.Exchange(ref _backingValue, value); - } - - /// - /// Implements the operator ==. - /// - /// a. - /// The b. - /// - /// The result of the operator. - /// - public static bool operator ==(AtomicTypeBase a, T b) => a?.Equals(b) == true; - - /// - /// Implements the operator !=. - /// - /// a. - /// The b. - /// - /// The result of the operator. - /// - public static bool operator !=(AtomicTypeBase a, T b) => a?.Equals(b) == false; - - /// - /// Implements the operator >. - /// - /// a. - /// The b. - /// - /// The result of the operator. - /// - public static bool operator >(AtomicTypeBase a, T b) => a.CompareTo(b) > 0; - - /// - /// Implements the operator <. - /// - /// a. - /// The b. - /// - /// The result of the operator. - /// - public static bool operator <(AtomicTypeBase a, T b) => a.CompareTo(b) < 0; - - /// - /// Implements the operator >=. - /// - /// a. - /// The b. - /// - /// The result of the operator. - /// - public static bool operator >=(AtomicTypeBase a, T b) => a.CompareTo(b) >= 0; - - /// - /// Implements the operator <=. - /// - /// a. - /// The b. - /// - /// The result of the operator. - /// - public static bool 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, long operand) - { - instance.BackingValue = instance.BackingValue + operand; - return instance; - } - - /// - /// Implements the operator -. - /// - /// The instance. - /// The operand. - /// - /// The result of the operator. - /// - public static AtomicTypeBase operator -(AtomicTypeBase instance, long operand) - { - instance.BackingValue = 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 int CompareTo(object other) - { - switch (other) - { - case null: - return 1; - case AtomicTypeBase atomic: - return BackingValue.CompareTo(atomic.BackingValue); - case T variable: - return 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 int CompareTo(T other) => 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 int CompareTo(AtomicTypeBase other) => 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 bool Equals(object other) - { - switch (other) - { - case AtomicTypeBase atomic: - return Equals(atomic); - case T variable: - return Equals(variable); - } - - return 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 int GetHashCode() => BackingValue.GetHashCode(); - - /// - public bool Equals(AtomicTypeBase other) => - BackingValue == (other?.BackingValue ?? default); - - /// - public bool Equals(T other) => Equals(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(long backingValue); - - /// - /// Converts from the target type to a long value. - /// - /// The value. - /// The value converted to a long value. - protected abstract long ToLong(T value); - } + /// 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.Lite/Threading/CancellationTokenOwner.cs b/Swan.Lite/Threading/CancellationTokenOwner.cs index d09ec0c..a12b0e5 100644 --- a/Swan.Lite/Threading/CancellationTokenOwner.cs +++ b/Swan.Lite/Threading/CancellationTokenOwner.cs @@ -1,68 +1,61 @@ using System; using System.Threading; -namespace Swan.Threading -{ +namespace Swan.Threading { + /// + /// Acts as a but with reusable tokens. + /// + public sealed class CancellationTokenOwner : IDisposable { + private readonly Object _syncLock = new Object(); + private Boolean _isDisposed; + private CancellationTokenSource _tokenSource = new CancellationTokenSource(); + /// - /// Acts as a but with reusable tokens. + /// Gets the token of the current. /// - public sealed class CancellationTokenOwner : IDisposable - { - private readonly object _syncLock = new object(); - private bool _isDisposed; - private CancellationTokenSource _tokenSource = new CancellationTokenSource(); - - /// - /// Gets the token of the current. - /// - public CancellationToken Token - { - get - { - lock (_syncLock) - { - return _isDisposed - ? CancellationToken.None - : _tokenSource.Token; - } - } - } - - /// - /// Cancels the last referenced token and creates a new token source. - /// - public void Cancel() - { - lock (_syncLock) - { - if (_isDisposed) return; - _tokenSource.Cancel(); - _tokenSource.Dispose(); - _tokenSource = new CancellationTokenSource(); - } - } - - /// - public void Dispose() => Dispose(true); - - /// - /// Releases unmanaged and - optionally - managed resources. - /// - /// true to release both managed and unmanaged resources; false to release only unmanaged resources. - private void Dispose(bool disposing) - { - lock (_syncLock) - { - if (_isDisposed) return; - - if (disposing) - { - _tokenSource.Cancel(); - _tokenSource.Dispose(); - } - - _isDisposed = true; - } - } - } + public CancellationToken Token { + get { + lock(this._syncLock) { + return this._isDisposed ? CancellationToken.None : this._tokenSource.Token; + } + } + } + + /// + /// Cancels the last referenced token and creates a new token source. + /// + public void Cancel() { + lock(this._syncLock) { + if(this._isDisposed) { + return; + } + + this._tokenSource.Cancel(); + this._tokenSource.Dispose(); + this._tokenSource = new CancellationTokenSource(); + } + } + + /// + public void Dispose() => this.Dispose(true); + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + private void Dispose(Boolean disposing) { + lock(this._syncLock) { + if(this._isDisposed) { + return; + } + + if(disposing) { + this._tokenSource.Cancel(); + this._tokenSource.Dispose(); + } + + this._isDisposed = true; + } + } + } } diff --git a/Swan.Lite/Threading/ExclusiveTimer.cs b/Swan.Lite/Threading/ExclusiveTimer.cs index 5a214ff..d3c9c3e 100644 --- a/Swan.Lite/Threading/ExclusiveTimer.cs +++ b/Swan.Lite/Threading/ExclusiveTimer.cs @@ -1,236 +1,206 @@ using System; using System.Threading; -namespace Swan.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; + /// - /// 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. + /// Initializes a new instance of the class. /// - 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 int _period; - - /// - /// Initializes a new instance of the class. - /// - /// The timer callback. - /// The state. - /// The due time. - /// The period. - public ExclusiveTimer(TimerCallback timerCallback, object state, int dueTime, int period) - { - _period = period; - _userCallback = timerCallback; - _backingTimer = new Timer(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, int dueTime, int 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 bool IsDisposing => _isDisposing.Value; - - /// - /// Gets a value indicating whether this instance is disposed. - /// - /// - /// true if this instance is disposed; otherwise, false. - /// - public bool IsDisposed => _isDisposed.Value; - - /// - /// Waits until the time is elapsed. - /// - /// The until date. - /// The cancellation token. - public static void WaitUntil(DateTime untilDate, CancellationToken cancellationToken = default) - { - void Callback(IWaitEvent waitEvent) - { - try - { - waitEvent.Complete(); - waitEvent.Begin(); - } - catch - { - // ignore - } - } - - using (var delayLock = WaitEventFactory.Create(true)) - { - using (var _ = 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(int dueTime, int period) - { - _period = period; - - _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) - => Change(Convert.ToInt32(dueTime.TotalMilliseconds), Convert.ToInt32(period.TotalMilliseconds)); - - /// - /// Changes the interval between method invocations for the internal timer. - /// - /// The period. - public void Resume(int period) => Change(0, period); - - /// - /// Changes the interval between method invocations for the internal timer. - /// - /// The period. - public void Resume(TimeSpan period) => Change(TimeSpan.Zero, period); - - /// - /// Pauses this instance. - /// - public void Pause() => Change(Timeout.Infinite, Timeout.Infinite); - - /// - public void Dispose() - { - lock (_syncLock) - { - if (_isDisposed == true || _isDisposing == true) - return; - - _isDisposing.Value = true; - } - - try - { - _cycleDoneEvent.Wait(); - _cycleDoneEvent.Dispose(); - Pause(); - _backingTimer.Dispose(); - } - finally - { - _isDisposed.Value = true; - _isDisposing.Value = false; - } - } - - /// - /// Logic that runs every time the timer hits the due time. - /// - /// The state. - private void InternalCallback(object state) - { - lock (_syncLock) - { - if (IsDisposed || IsDisposing) - return; - } - - if (_cycleDoneEvent.IsSet == false) - return; - - _cycleDoneEvent.Reset(); - - try - { - _userCallback(state); - } - finally - { - _cycleDoneEvent?.Set(); - _backingTimer?.Change(_period, Timeout.Infinite); - } - } - } + /// 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.Lite/Threading/ISyncLocker.cs b/Swan.Lite/Threading/ISyncLocker.cs index be75c8d..fad9af3 100644 --- a/Swan.Lite/Threading/ISyncLocker.cs +++ b/Swan.Lite/Threading/ISyncLocker.cs @@ -1,24 +1,22 @@ using System; -namespace Swan.Threading -{ +namespace Swan.Threading { + /// + /// Defines a generic interface for synchronized locking mechanisms. + /// + public interface ISyncLocker : IDisposable { /// - /// Defines a generic interface for synchronized locking mechanisms. + /// Acquires a writer lock. + /// The lock is released when the returned locking object is disposed. /// - public interface ISyncLocker : IDisposable - { - /// - /// Acquires a writer lock. - /// The lock is released when the returned locking object is disposed. - /// - /// A disposable locking object. - IDisposable AcquireWriterLock(); - - /// - /// Acquires a reader lock. - /// The lock is released when the returned locking object is disposed. - /// - /// A disposable locking object. - IDisposable AcquireReaderLock(); - } + /// A disposable locking object. + IDisposable AcquireWriterLock(); + + /// + /// Acquires a reader lock. + /// The lock is released when the returned locking object is disposed. + /// + /// A disposable locking object. + IDisposable AcquireReaderLock(); + } } diff --git a/Swan.Lite/Threading/IWaitEvent.cs b/Swan.Lite/Threading/IWaitEvent.cs index 6b949e8..19331c0 100644 --- a/Swan.Lite/Threading/IWaitEvent.cs +++ b/Swan.Lite/Threading/IWaitEvent.cs @@ -1,57 +1,63 @@ using System; -namespace Swan.Threading -{ +namespace Swan.Threading { + /// + /// Provides a generalized API for ManualResetEvent and ManualResetEventSlim. + /// + /// + public interface IWaitEvent : IDisposable { /// - /// Provides a generalized API for ManualResetEvent and ManualResetEventSlim. + /// Gets a value indicating whether the event is in the completed state. /// - /// - public interface IWaitEvent : IDisposable - { - /// - /// Gets a value indicating whether the event is in the completed state. - /// - bool IsCompleted { get; } - - /// - /// Gets a value indicating whether the Begin method has been called. - /// It returns false after the Complete method is called. - /// - bool IsInProgress { get; } - - /// - /// Returns true if the underlying handle is not closed and it is still valid. - /// - bool IsValid { get; } - - /// - /// Gets a value indicating whether this instance is disposed. - /// - bool 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. - bool Wait(TimeSpan timeout); - } + 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.Lite/Threading/IWorker.cs b/Swan.Lite/Threading/IWorker.cs index f70c60c..6551e9d 100644 --- a/Swan.Lite/Threading/IWorker.cs +++ b/Swan.Lite/Threading/IWorker.cs @@ -1,69 +1,77 @@ using System; using System.Threading.Tasks; -namespace Swan.Threading -{ +namespace Swan.Threading { + /// + /// Defines a standard API to control background application workers. + /// + public interface IWorker { /// - /// Defines a standard API to control background application workers. + /// Gets the current state of the worker. /// - public interface IWorker - { - /// - /// Gets the current state of the worker. - /// - WorkerState WorkerState { get; } - - /// - /// Gets a value indicating whether this instance is disposed. - /// - /// - /// true if this instance is disposed; otherwise, false. - /// - bool IsDisposed { get; } - - /// - /// Gets a value indicating whether this instance is currently being disposed. - /// - /// - /// true if this instance is disposing; otherwise, false. - /// - bool IsDisposing { get; } - - /// - /// Gets or sets the time interval used to execute cycles. - /// - TimeSpan Period { get; set; } - - /// - /// Gets the name identifier of this worker. - /// - string Name { get; } - - /// - /// Starts execution of worker cycles. - /// - /// The awaitable task. - Task StartAsync(); - - /// - /// Pauses execution of worker cycles. - /// - /// The awaitable task. - Task PauseAsync(); - - /// - /// Resumes execution of worker cycles. - /// - /// The awaitable task. - Task ResumeAsync(); - - /// - /// Permanently stops execution of worker cycles. - /// An interrupt is always sent to the worker. If you wish to stop - /// the worker without interrupting then call the - /// method, await it, and finally call the method. - /// - /// The awaitable task. - Task StopAsync(); - } + WorkerState WorkerState { + get; + } + + /// + /// Gets a value indicating whether this instance is disposed. + /// + /// + /// true if this instance is disposed; otherwise, false. + /// + Boolean IsDisposed { + get; + } + + /// + /// Gets a value indicating whether this instance is currently being disposed. + /// + /// + /// true if this instance is disposing; otherwise, false. + /// + Boolean IsDisposing { + get; + } + + /// + /// Gets or sets the time interval used to execute cycles. + /// + TimeSpan Period { + get; set; + } + + /// + /// Gets the name identifier of this worker. + /// + String Name { + get; + } + + /// + /// Starts execution of worker cycles. + /// + /// The awaitable task. + Task StartAsync(); + + /// + /// Pauses execution of worker cycles. + /// + /// The awaitable task. + Task PauseAsync(); + + /// + /// Resumes execution of worker cycles. + /// + /// The awaitable task. + Task ResumeAsync(); + + /// + /// Permanently stops execution of worker cycles. + /// An interrupt is always sent to the worker. If you wish to stop + /// the worker without interrupting then call the + /// method, await it, and finally call the method. + /// + /// The awaitable task. + Task StopAsync(); + } } diff --git a/Swan.Lite/Threading/IWorkerDelayProvider.cs b/Swan.Lite/Threading/IWorkerDelayProvider.cs index 8868c14..b2f1f19 100644 --- a/Swan.Lite/Threading/IWorkerDelayProvider.cs +++ b/Swan.Lite/Threading/IWorkerDelayProvider.cs @@ -1,21 +1,20 @@ -using System.Threading; +using System; +using System.Threading; using System.Threading.Tasks; -namespace Swan.Threading -{ +namespace Swan.Threading { + /// + /// An interface for a worker cycle delay provider. + /// + public interface IWorkerDelayProvider { /// - /// An interface for a worker cycle delay provider. + /// Suspends execution queues a new cycle for execution. The delay is given in + /// milliseconds. When overridden in a derived class the wait handle will be set + /// whenever an interrupt is received. /// - public interface IWorkerDelayProvider - { - /// - /// Suspends execution queues a new cycle for execution. The delay is given in - /// milliseconds. When overridden in a derived class the wait handle will be set - /// whenever an interrupt is received. - /// - /// The remaining delay to wait for in the cycle. - /// Contains a reference to a task with the scheduled period delay. - /// The cancellation token to cancel waiting. - void ExecuteCycleDelay(int wantedDelay, Task delayTask, CancellationToken token); - } + /// The remaining delay to wait for in the cycle. + /// Contains a reference to a task with the scheduled period delay. + /// The cancellation token to cancel waiting. + void ExecuteCycleDelay(Int32 wantedDelay, Task delayTask, CancellationToken token); + } } diff --git a/Swan.Lite/Threading/PeriodicTask.cs b/Swan.Lite/Threading/PeriodicTask.cs index 11aaf37..54901cc 100644 --- a/Swan.Lite/Threading/PeriodicTask.cs +++ b/Swan.Lite/Threading/PeriodicTask.cs @@ -3,99 +3,83 @@ using System.Threading; using System.Threading.Tasks; using Swan.Logging; -namespace Swan.Threading -{ +namespace Swan.Threading { + /// + /// Schedule an action to be periodically executed on the thread pool. + /// + public sealed class PeriodicTask : IDisposable { /// - /// Schedule an action to be periodically executed on the thread pool. + /// The minimum interval between action invocations. + /// The value of this field is equal to 100 milliseconds. /// - public sealed class PeriodicTask : IDisposable - { - /// - /// The minimum interval between action invocations. - /// The value of this field is equal to 100 milliseconds. - /// - public static readonly TimeSpan MinInterval = TimeSpan.FromMilliseconds(100); - - private readonly Func _action; - private readonly CancellationTokenSource _cancellationTokenSource; - - private TimeSpan _interval; - - /// - /// Initializes a new instance of the class. - /// - /// The interval between invocations of . - /// The callback to invoke periodically. - /// A that can be used to cancel operations. - public PeriodicTask(TimeSpan interval, Func action, CancellationToken cancellationToken = default) - { - _action = action ?? throw new ArgumentNullException(nameof(action)); - _cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - _interval = ValidateInterval(interval); - - Task.Run(ActionLoop); - } - - /// - /// Finalizes an instance of the class. - /// - ~PeriodicTask() - { - Dispose(false); - } - - /// - /// Gets or sets the interval between periodic action invocations. - /// Changes to this property take effect after next action invocation. - /// - /// - public TimeSpan Interval - { - get => _interval; - set => _interval = ValidateInterval(value); - } - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - private void Dispose(bool disposing) - { - if (disposing) - { - _cancellationTokenSource.Cancel(); - _cancellationTokenSource.Dispose(); - } - } - - private static TimeSpan ValidateInterval(TimeSpan value) - => value < MinInterval ? MinInterval : value; - - private async Task ActionLoop() - { - for (; ; ) - { - try - { - await Task.Delay(Interval, _cancellationTokenSource.Token).ConfigureAwait(false); - await _action(_cancellationTokenSource.Token).ConfigureAwait(false); - } - catch (OperationCanceledException) when (_cancellationTokenSource.IsCancellationRequested) - { - break; - } - catch (TaskCanceledException) when (_cancellationTokenSource.IsCancellationRequested) - { - break; - } - catch (Exception ex) - { - ex.Log(nameof(PeriodicTask)); - } - } - } - } + public static readonly TimeSpan MinInterval = TimeSpan.FromMilliseconds(100); + + private readonly Func _action; + private readonly CancellationTokenSource _cancellationTokenSource; + + private TimeSpan _interval; + + /// + /// Initializes a new instance of the class. + /// + /// The interval between invocations of . + /// The callback to invoke periodically. + /// A that can be used to cancel operations. + public PeriodicTask(TimeSpan interval, Func action, CancellationToken cancellationToken = default) { + this._action = action ?? throw new ArgumentNullException(nameof(action)); + this._cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + this._interval = ValidateInterval(interval); + + _ = Task.Run(this.ActionLoop); + } + + /// + /// Finalizes an instance of the class. + /// + ~PeriodicTask() { + this.Dispose(false); + } + + /// + /// Gets or sets the interval between periodic action invocations. + /// Changes to this property take effect after next action invocation. + /// + /// + public TimeSpan Interval { + get => this._interval; + set => this._interval = ValidateInterval(value); + } + + /// + public void Dispose() { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(Boolean disposing) { + if(disposing) { + this._cancellationTokenSource.Cancel(); + this._cancellationTokenSource.Dispose(); + } + } + + private static TimeSpan ValidateInterval(TimeSpan value) + => value < MinInterval ? MinInterval : value; + + private async Task ActionLoop() { + for(; ; ) + { + try { + await Task.Delay(this.Interval, this._cancellationTokenSource.Token).ConfigureAwait(false); + await this._action(this._cancellationTokenSource.Token).ConfigureAwait(false); + } catch(OperationCanceledException) when(this._cancellationTokenSource.IsCancellationRequested) { + break; + } catch(TaskCanceledException) when(this._cancellationTokenSource.IsCancellationRequested) { + break; + } catch(Exception ex) { + ex.Log(nameof(PeriodicTask)); + } + } + } + } } diff --git a/Swan.Lite/Threading/RunnerBase.cs b/Swan.Lite/Threading/RunnerBase.cs index a3aec78..491978a 100644 --- a/Swan.Lite/Threading/RunnerBase.cs +++ b/Swan.Lite/Threading/RunnerBase.cs @@ -4,175 +4,162 @@ using System.Threading; using Swan.Configuration; using Swan.Logging; -namespace Swan.Threading -{ +namespace Swan.Threading { + /// + /// Represents an background worker abstraction with a life cycle and running at a independent thread. + /// + public abstract class RunnerBase : ConfiguredObject { + private Thread _worker; + private CancellationTokenSource _cancelTokenSource; + private ManualResetEvent _workFinished; + /// - /// Represents an background worker abstraction with a life cycle and running at a independent thread. + /// Initializes a new instance of the class. /// - public abstract class RunnerBase : ConfiguredObject - { - private Thread _worker; - private CancellationTokenSource _cancelTokenSource; - private ManualResetEvent _workFinished; - - /// - /// Initializes a new instance of the class. - /// - /// if set to true [is enabled]. - protected RunnerBase(bool isEnabled) - { - Name = GetType().Name; - IsEnabled = isEnabled; - } - - /// - /// Gets the error messages. - /// - /// - /// The error messages. - /// - public List ErrorMessages { get; } = new List(); - - /// - /// Gets the name. - /// - /// - /// The name. - /// - public string Name { get; } - - /// - /// Gets a value indicating whether this instance is running. - /// - /// - /// true if this instance is running; otherwise, false. - /// - public bool IsRunning { get; private set; } - - /// - /// Gets a value indicating whether this instance is enabled. - /// - /// - /// true if this instance is enabled; otherwise, false. - /// - public bool IsEnabled { get; } - - /// - /// Starts this instance. - /// - public virtual void Start() - { - if (IsEnabled == false) - return; - - "Start Requested".Debug(Name); - _cancelTokenSource = new CancellationTokenSource(); - _workFinished = new ManualResetEvent(false); - - _worker = new Thread(() => - { - _workFinished.Reset(); - IsRunning = true; - try - { - Setup(); - DoBackgroundWork(_cancelTokenSource.Token); - } - catch (ThreadAbortException) - { - $"{nameof(ThreadAbortException)} caught.".Warn(Name); - } - catch (Exception ex) - { - $"{ex.GetType()}: {ex.Message}\r\n{ex.StackTrace}".Error(Name); - } - finally - { - Cleanup(); - _workFinished?.Set(); - IsRunning = false; - "Stopped Completely".Debug(Name); - } - }) - { - IsBackground = true, - Name = $"{Name}Thread", - }; - - _worker.Start(); - } - - /// - /// Stops this instance. - /// - public virtual void Stop() - { - if (IsEnabled == false || IsRunning == false) - return; - - "Stop Requested".Debug(Name); - _cancelTokenSource.Cancel(); - var waitRetries = 5; - while (waitRetries >= 1) - { - if (_workFinished.WaitOne(250)) - { - waitRetries = -1; - break; - } - - waitRetries--; - } - - if (waitRetries < 0) - { - "Workbench stopped gracefully".Debug(Name); - } - else - { - "Did not respond to stop request. Aborting thread and waiting . . .".Warn(Name); - _worker.Abort(); - - if (_workFinished.WaitOne(5000) == false) - "Waited and no response. Worker might have been left in an inconsistent state.".Error(Name); - else - "Waited for worker and it finally responded (OK).".Debug(Name); - } - - _workFinished.Dispose(); - _workFinished = null; - } - - /// - /// Setups this instance. - /// - protected void Setup() - { - EnsureConfigurationNotLocked(); - OnSetup(); - LockConfiguration(); - } - - /// - /// Cleanups this instance. - /// - protected virtual void Cleanup() - { - // empty - } - - /// - /// Called when [setup]. - /// - protected virtual void OnSetup() - { - // empty - } - - /// - /// Does the background work. - /// - /// The ct. - protected abstract void DoBackgroundWork(CancellationToken cancellationToken); - } + /// if set to true [is enabled]. + protected RunnerBase(Boolean isEnabled) { + this.Name = this.GetType().Name; + this.IsEnabled = isEnabled; + } + + /// + /// Gets the error messages. + /// + /// + /// The error messages. + /// + public List ErrorMessages { get; } = new List(); + + /// + /// Gets the name. + /// + /// + /// The name. + /// + public String Name { + get; + } + + /// + /// Gets a value indicating whether this instance is running. + /// + /// + /// true if this instance is running; otherwise, false. + /// + public Boolean IsRunning { + get; private set; + } + + /// + /// Gets a value indicating whether this instance is enabled. + /// + /// + /// true if this instance is enabled; otherwise, false. + /// + public Boolean IsEnabled { + get; + } + + /// + /// Starts this instance. + /// + public virtual void Start() { + if(this.IsEnabled == false) { + return; + } + + "Start Requested".Debug(this.Name); + this._cancelTokenSource = new CancellationTokenSource(); + this._workFinished = new ManualResetEvent(false); + + this._worker = new Thread(() => { + _ = this._workFinished.Reset(); + this.IsRunning = true; + try { + this.Setup(); + this.DoBackgroundWork(this._cancelTokenSource.Token); + } catch(ThreadAbortException) { + $"{nameof(ThreadAbortException)} caught.".Warn(this.Name); + } catch(Exception ex) { + $"{ex.GetType()}: {ex.Message}\r\n{ex.StackTrace}".Error(this.Name); + } finally { + this.Cleanup(); + _ = this._workFinished?.Set(); + this.IsRunning = false; + "Stopped Completely".Debug(this.Name); + } + }) { + IsBackground = true, + Name = $"{this.Name}Thread", + }; + + this._worker.Start(); + } + + /// + /// Stops this instance. + /// + public virtual void Stop() { + if(this.IsEnabled == false || this.IsRunning == false) { + return; + } + + "Stop Requested".Debug(this.Name); + this._cancelTokenSource.Cancel(); + Int32 waitRetries = 5; + while(waitRetries >= 1) { + if(this._workFinished.WaitOne(250)) { + waitRetries = -1; + break; + } + + waitRetries--; + } + + if(waitRetries < 0) { + "Workbench stopped gracefully".Debug(this.Name); + } else { + "Did not respond to stop request. Aborting thread and waiting . . .".Warn(this.Name); + this._worker.Abort(); + + if(this._workFinished.WaitOne(5000) == false) { + "Waited and no response. Worker might have been left in an inconsistent state.".Error(this.Name); + } else { + "Waited for worker and it finally responded (OK).".Debug(this.Name); + } + } + + this._workFinished.Dispose(); + this._workFinished = null; + } + + /// + /// Setups this instance. + /// + protected void Setup() { + this.EnsureConfigurationNotLocked(); + this.OnSetup(); + this.LockConfiguration(); + } + + /// + /// Cleanups this instance. + /// + protected virtual void Cleanup() { + // empty + } + + /// + /// Called when [setup]. + /// + protected virtual void OnSetup() { + // empty + } + + /// + /// Does the background work. + /// + /// The ct. + protected abstract void DoBackgroundWork(CancellationToken cancellationToken); + } } \ No newline at end of file diff --git a/Swan.Lite/Threading/SyncLockerFactory.cs b/Swan.Lite/Threading/SyncLockerFactory.cs index 1d3ac94..a552a26 100644 --- a/Swan.Lite/Threading/SyncLockerFactory.cs +++ b/Swan.Lite/Threading/SyncLockerFactory.cs @@ -1,185 +1,179 @@ using System; using System.Threading; -namespace Swan.Threading -{ +namespace Swan.Threading { + /// + /// Provides factory methods to create synchronized reader-writer locks + /// that support a generalized locking and releasing api and syntax. + /// + public static class SyncLockerFactory { + #region Enums and Interfaces + /// - /// Provides factory methods to create synchronized reader-writer locks - /// that support a generalized locking and releasing api and syntax. + /// Enumerates the locking operations. /// - public static class SyncLockerFactory - { - #region Enums and Interfaces - - /// - /// Enumerates the locking operations. - /// - private enum LockHolderType - { - Read, - Write, - } - - /// - /// Defines methods for releasing locks. - /// - private interface ISyncReleasable - { - /// - /// Releases the writer lock. - /// - void ReleaseWriterLock(); - - /// - /// Releases the reader lock. - /// - void ReleaseReaderLock(); - } - - #endregion - - #region Factory Methods - - /// - /// Creates a reader-writer lock backed by a standard ReaderWriterLock. - /// - /// The synchronized locker. - public static ISyncLocker Create() => new SyncLocker(); - - /// - /// Creates a reader-writer lock backed by a ReaderWriterLockSlim. - /// - /// The synchronized locker. - public static ISyncLocker CreateSlim() => new SyncLockerSlim(); - - /// - /// Creates a reader-writer lock. - /// - /// if set to true it uses the Slim version of a reader-writer lock. - /// The Sync Locker. - public static ISyncLocker Create(bool useSlim) => useSlim ? CreateSlim() : Create(); - - #endregion - - #region Private Classes - - /// - /// The lock releaser. Calling the dispose method releases the lock entered by the parent SyncLocker. - /// - /// - private sealed class SyncLockReleaser : IDisposable - { - private readonly ISyncReleasable _parent; - private readonly LockHolderType _operation; - - private bool _isDisposed; - - /// - /// Initializes a new instance of the class. - /// - /// The parent. - /// The operation. - public SyncLockReleaser(ISyncReleasable parent, LockHolderType operation) - { - _parent = parent; - _operation = operation; - } - - /// - public void Dispose() - { - if (_isDisposed) return; - _isDisposed = true; - - if (_operation == LockHolderType.Read) - _parent.ReleaseReaderLock(); - else - _parent.ReleaseWriterLock(); - } - } - - /// - /// The Sync Locker backed by a ReaderWriterLock. - /// - /// - /// - private sealed class SyncLocker : ISyncLocker, ISyncReleasable - { - private bool _isDisposed; - private ReaderWriterLock _locker = new ReaderWriterLock(); - - /// - public IDisposable AcquireReaderLock() - { - _locker?.AcquireReaderLock(Timeout.Infinite); - return new SyncLockReleaser(this, LockHolderType.Read); - } - - /// - public IDisposable AcquireWriterLock() - { - _locker?.AcquireWriterLock(Timeout.Infinite); - return new SyncLockReleaser(this, LockHolderType.Write); - } - - /// - public void ReleaseWriterLock() => _locker?.ReleaseWriterLock(); - - /// - public void ReleaseReaderLock() => _locker?.ReleaseReaderLock(); - - /// - public void Dispose() - { - if (_isDisposed) return; - _isDisposed = true; - _locker?.ReleaseLock(); - _locker = null; - } - } - - /// - /// The Sync Locker backed by ReaderWriterLockSlim. - /// - /// - /// - private sealed class SyncLockerSlim : ISyncLocker, ISyncReleasable - { - private bool _isDisposed; - - private ReaderWriterLockSlim _locker - = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); - - /// - public IDisposable AcquireReaderLock() - { - _locker?.EnterReadLock(); - return new SyncLockReleaser(this, LockHolderType.Read); - } - - /// - public IDisposable AcquireWriterLock() - { - _locker?.EnterWriteLock(); - return new SyncLockReleaser(this, LockHolderType.Write); - } - - /// - public void ReleaseWriterLock() => _locker?.ExitWriteLock(); - - /// - public void ReleaseReaderLock() => _locker?.ExitReadLock(); - - /// - public void Dispose() - { - if (_isDisposed) return; - _isDisposed = true; - _locker?.Dispose(); - _locker = null; - } - } - - #endregion - } + private enum LockHolderType { + Read, + Write, + } + + /// + /// Defines methods for releasing locks. + /// + private interface ISyncReleasable { + /// + /// Releases the writer lock. + /// + void ReleaseWriterLock(); + + /// + /// Releases the reader lock. + /// + void ReleaseReaderLock(); + } + + #endregion + + #region Factory Methods + + /// + /// Creates a reader-writer lock backed by a standard ReaderWriterLock. + /// + /// The synchronized locker. + public static ISyncLocker Create() => new SyncLocker(); + + /// + /// Creates a reader-writer lock backed by a ReaderWriterLockSlim. + /// + /// The synchronized locker. + public static ISyncLocker CreateSlim() => new SyncLockerSlim(); + + /// + /// Creates a reader-writer lock. + /// + /// if set to true it uses the Slim version of a reader-writer lock. + /// The Sync Locker. + public static ISyncLocker Create(Boolean useSlim) => useSlim ? CreateSlim() : Create(); + + #endregion + + #region Private Classes + + /// + /// The lock releaser. Calling the dispose method releases the lock entered by the parent SyncLocker. + /// + /// + private sealed class SyncLockReleaser : IDisposable { + private readonly ISyncReleasable _parent; + private readonly LockHolderType _operation; + + private Boolean _isDisposed; + + /// + /// Initializes a new instance of the class. + /// + /// The parent. + /// The operation. + public SyncLockReleaser(ISyncReleasable parent, LockHolderType operation) { + this._parent = parent; + this._operation = operation; + } + + /// + public void Dispose() { + if(this._isDisposed) { + return; + } + + this._isDisposed = true; + + if(this._operation == LockHolderType.Read) { + this._parent.ReleaseReaderLock(); + } else { + this._parent.ReleaseWriterLock(); + } + } + } + + /// + /// The Sync Locker backed by a ReaderWriterLock. + /// + /// + /// + private sealed class SyncLocker : ISyncLocker, ISyncReleasable { + private Boolean _isDisposed; + private ReaderWriterLock _locker = new ReaderWriterLock(); + + /// + public IDisposable AcquireReaderLock() { + this._locker?.AcquireReaderLock(Timeout.Infinite); + return new SyncLockReleaser(this, LockHolderType.Read); + } + + /// + public IDisposable AcquireWriterLock() { + this._locker?.AcquireWriterLock(Timeout.Infinite); + return new SyncLockReleaser(this, LockHolderType.Write); + } + + /// + public void ReleaseWriterLock() => this._locker?.ReleaseWriterLock(); + + /// + public void ReleaseReaderLock() => this._locker?.ReleaseReaderLock(); + + /// + public void Dispose() { + if(this._isDisposed) { + return; + } + + this._isDisposed = true; + _ = this._locker?.ReleaseLock(); + this._locker = null; + } + } + + /// + /// The Sync Locker backed by ReaderWriterLockSlim. + /// + /// + /// + private sealed class SyncLockerSlim : ISyncLocker, ISyncReleasable { + private Boolean _isDisposed; + + private ReaderWriterLockSlim _locker = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); + + /// + public IDisposable AcquireReaderLock() { + this._locker?.EnterReadLock(); + return new SyncLockReleaser(this, LockHolderType.Read); + } + + /// + public IDisposable AcquireWriterLock() { + this._locker?.EnterWriteLock(); + return new SyncLockReleaser(this, LockHolderType.Write); + } + + /// + public void ReleaseWriterLock() => this._locker?.ExitWriteLock(); + + /// + public void ReleaseReaderLock() => this._locker?.ExitReadLock(); + + /// + public void Dispose() { + if(this._isDisposed) { + return; + } + + this._isDisposed = true; + this._locker?.Dispose(); + this._locker = null; + } + } + + #endregion + } } \ No newline at end of file diff --git a/Swan.Lite/Threading/WaitEventFactory.cs b/Swan.Lite/Threading/WaitEventFactory.cs index 1c098c0..8def40c 100644 --- a/Swan.Lite/Threading/WaitEventFactory.cs +++ b/Swan.Lite/Threading/WaitEventFactory.cs @@ -1,219 +1,188 @@ using System; using System.Threading; -namespace Swan.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 + /// - /// Provides a Manual Reset Event factory with a unified API. + /// Creates a Wait Event backed by a standard ManualResetEvent. /// - /// - /// 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(bool 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(bool 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(bool isCompleted, bool 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(bool isCompleted) - { - _event = new ManualResetEvent(isCompleted); - } - - /// - public bool IsDisposed { get; private set; } - - /// - public bool IsValid - { - get - { - if (IsDisposed || _event == null) - return false; - - if (_event?.SafeWaitHandle?.IsClosed ?? true) - return false; - - return !(_event?.SafeWaitHandle?.IsInvalid ?? true); - } - } - - /// - public bool IsCompleted - { - get - { - if (IsValid == false) - return true; - - return _event?.WaitOne(0) ?? true; - } - } - - /// - public bool IsInProgress => !IsCompleted; - - /// - public void Begin() => _event?.Reset(); - - /// - public void Complete() => _event?.Set(); - - /// - void IDisposable.Dispose() - { - if (IsDisposed) return; - IsDisposed = true; - - _event?.Set(); - _event?.Dispose(); - _event = null; - } - - /// - public void Wait() => _event?.WaitOne(); - - /// - public bool Wait(TimeSpan timeout) => _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(bool isCompleted) - { - _event = new ManualResetEventSlim(isCompleted); - } - - /// - public bool IsDisposed { get; private set; } - - /// - public bool IsValid - { - get - { - if (IsDisposed || _event?.WaitHandle?.SafeWaitHandle == null) return false; - - return !_event.WaitHandle.SafeWaitHandle.IsClosed && !_event.WaitHandle.SafeWaitHandle.IsInvalid; - } - } - - /// - public bool IsCompleted => IsValid == false || _event.IsSet; - - /// - public bool IsInProgress => !IsCompleted; - - /// - public void Begin() => _event?.Reset(); - - /// - public void Complete() => _event?.Set(); - - /// - void IDisposable.Dispose() - { - if (IsDisposed) return; - IsDisposed = true; - - _event?.Set(); - _event?.Dispose(); - _event = null; - } - - /// - public void Wait() => _event?.Wait(); - - /// - public bool Wait(TimeSpan timeout) => _event?.Wait(timeout) ?? true; - } - - #endregion - } + /// 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 diff --git a/Swan.Lite/Threading/WorkerState.cs b/Swan.Lite/Threading/WorkerState.cs index 6e5feee..1ef3c0e 100644 --- a/Swan.Lite/Threading/WorkerState.cs +++ b/Swan.Lite/Threading/WorkerState.cs @@ -1,33 +1,31 @@ -namespace Swan.Threading -{ +namespace Swan.Threading { + /// + /// Enumerates the different states in which a worker can be. + /// + public enum WorkerState { /// - /// Enumerates the different states in which a worker can be. + /// The worker has been created and it is ready to start. /// - public enum WorkerState - { - /// - /// The worker has been created and it is ready to start. - /// - Created, - - /// - /// The worker is running it cycle logic. - /// - Running, - - /// - /// The worker is running its delay logic. - /// - Waiting, - - /// - /// The worker is in the paused or suspended state. - /// - Paused, - - /// - /// The worker is stopped and ready for disposal. - /// - Stopped, - } + Created, + + /// + /// The worker is running it cycle logic. + /// + Running, + + /// + /// The worker is running its delay logic. + /// + Waiting, + + /// + /// The worker is in the paused or suspended state. + /// + Paused, + + /// + /// The worker is stopped and ready for disposal. + /// + Stopped, + } } diff --git a/Swan.Lite/Validators/IValidator.cs b/Swan.Lite/Validators/IValidator.cs index eb30992..87f54fd 100644 --- a/Swan.Lite/Validators/IValidator.cs +++ b/Swan.Lite/Validators/IValidator.cs @@ -1,21 +1,23 @@ -namespace Swan.Validators -{ +using System; + +namespace Swan.Validators { + /// + /// A simple Validator interface. + /// + public interface IValidator { /// - /// A simple Validator interface. + /// The error message. /// - public interface IValidator - { - /// - /// The error message. - /// - string ErrorMessage { get; } - - /// - /// Checks if a value is valid. - /// - /// The type. - /// The value. - /// True if it is valid.False if it is not. - bool IsValid(T value); - } + String ErrorMessage { + get; + } + + /// + /// Checks if a value is valid. + /// + /// The type. + /// The value. + /// True if it is valid.False if it is not. + Boolean IsValid(T value); + } } \ No newline at end of file diff --git a/Swan.Lite/Validators/ObjectValidationResult.cs b/Swan.Lite/Validators/ObjectValidationResult.cs index 69519ed..16a86ea 100644 --- a/Swan.Lite/Validators/ObjectValidationResult.cs +++ b/Swan.Lite/Validators/ObjectValidationResult.cs @@ -1,47 +1,48 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; -namespace Swan.Validators -{ +namespace Swan.Validators { + /// + /// Defines a validation result containing all validation errors and their properties. + /// + public class ObjectValidationResult { + private readonly List _errors = new List(); + /// - /// Defines a validation result containing all validation errors and their properties. + /// A list of errors. /// - public class ObjectValidationResult - { - private readonly List _errors = new List(); - - /// - /// A list of errors. - /// - public IReadOnlyList Errors => _errors; - - /// - /// true if there are no errors; otherwise, false. - /// - public bool IsValid => !Errors.Any(); - - /// - /// Adds an error with a specified property name. - /// - /// The property name. - /// The error message. - public void Add(string propertyName, string errorMessage) => - _errors.Add(new ValidationError { ErrorMessage = errorMessage, PropertyName = propertyName }); - - /// - /// Defines a validation error. - /// - public class ValidationError - { - /// - /// The property name. - /// - public string PropertyName { get; internal set; } - - /// - /// The message error. - /// - public string ErrorMessage { get; internal set; } - } - } + public IReadOnlyList Errors => this._errors; + + /// + /// true if there are no errors; otherwise, false. + /// + public Boolean IsValid => !this.Errors.Any(); + + /// + /// Adds an error with a specified property name. + /// + /// The property name. + /// The error message. + public void Add(String propertyName, String errorMessage) => this._errors.Add(new ValidationError { ErrorMessage = errorMessage, PropertyName = propertyName }); + + /// + /// Defines a validation error. + /// + public class ValidationError { + /// + /// The property name. + /// + public String PropertyName { + get; internal set; + } + + /// + /// The message error. + /// + public String ErrorMessage { + get; internal set; + } + } + } } \ No newline at end of file diff --git a/Swan.Lite/Validators/ObjectValidator.cs b/Swan.Lite/Validators/ObjectValidator.cs index 933d97b..6a7e5c5 100644 --- a/Swan.Lite/Validators/ObjectValidator.cs +++ b/Swan.Lite/Validators/ObjectValidator.cs @@ -3,171 +3,170 @@ using System.Collections.Concurrent; using System.Collections.Generic; using Swan.Reflection; -namespace Swan.Validators -{ +namespace Swan.Validators { + /// + /// Represents an object validator. + /// + /// + /// The following code describes how to perform a simple object validation. + /// + /// using Swan.Validators; + /// + /// class Example + /// { + /// public static void Main() + /// { + /// // create an instance of ObjectValidator + /// var obj = new ObjectValidator(); + /// + /// // Add a validation to the 'Simple' class with a custom error message + /// obj.AddValidator<Simple>(x => + /// !string.IsNullOrEmpty(x.Name), "Name must not be empty"); + /// + /// // check if object is valid + /// var res = obj.IsValid(new Simple { Name = "Name" }); + /// } + /// + /// class Simple + /// { + /// public string Name { get; set; } + /// } + /// } + /// + /// + /// The following code shows of to validate an object with a custom validator and some attributes using the Runtime ObjectValidator singleton. + /// + /// using Swan.Validators; + /// + /// class Example + /// { + /// public static void Main() + /// { + /// // create an instance of ObjectValidator + /// Runtime.ObjectValidator + /// .AddValidator<Simple>(x => + /// !x.Name.Equals("Name"), "Name must not be 'Name'"); + /// + /// // validate object + /// var res = Runtime.ObjectValidator + /// .Validate(new Simple{ Name = "name", Number = 5, Email ="email@mail.com"}) + /// } + /// + /// class Simple + /// { + /// [NotNull] + /// public string Name { get; set; } + /// + /// [Range(1, 10)] + /// public int Number { get; set; } + /// + /// [Email] + /// public string Email { get; set; } + /// } + /// } + /// + /// + public class ObjectValidator { + private static readonly Lazy LazyInstance = new Lazy(() => new ObjectValidator()); + + private readonly ConcurrentDictionary>> _predicates = new ConcurrentDictionary>>(); + /// - /// Represents an object validator. + /// Gets the current. /// - /// - /// The following code describes how to perform a simple object validation. - /// - /// using Swan.Validators; - /// - /// class Example - /// { - /// public static void Main() - /// { - /// // create an instance of ObjectValidator - /// var obj = new ObjectValidator(); - /// - /// // Add a validation to the 'Simple' class with a custom error message - /// obj.AddValidator<Simple>(x => - /// !string.IsNullOrEmpty(x.Name), "Name must not be empty"); - /// - /// // check if object is valid - /// var res = obj.IsValid(new Simple { Name = "Name" }); - /// } - /// - /// class Simple - /// { - /// public string Name { get; set; } - /// } - /// } - /// - /// - /// The following code shows of to validate an object with a custom validator and some attributes using the Runtime ObjectValidator singleton. - /// - /// using Swan.Validators; - /// - /// class Example - /// { - /// public static void Main() - /// { - /// // create an instance of ObjectValidator - /// Runtime.ObjectValidator - /// .AddValidator<Simple>(x => - /// !x.Name.Equals("Name"), "Name must not be 'Name'"); - /// - /// // validate object - /// var res = Runtime.ObjectValidator - /// .Validate(new Simple{ Name = "name", Number = 5, Email ="email@mail.com"}) - /// } - /// - /// class Simple - /// { - /// [NotNull] - /// public string Name { get; set; } - /// - /// [Range(1, 10)] - /// public int Number { get; set; } - /// - /// [Email] - /// public string Email { get; set; } - /// } - /// } - /// - /// - public class ObjectValidator - { - private static readonly Lazy LazyInstance = new Lazy(() => new ObjectValidator()); - - private readonly ConcurrentDictionary>> _predicates = - new ConcurrentDictionary>>(); - - /// - /// Gets the current. - /// - /// - /// The current. - /// - public static ObjectValidator Current => LazyInstance.Value; - - /// - /// Validates an object given the specified validators and attributes. - /// - /// The type of the object. - /// The object. - /// A validation result. - public ObjectValidationResult Validate(T target) - { - var errorList = new ObjectValidationResult(); - ValidateObject(target, false, errorList.Add); - - return errorList; - } - - /// - /// Validates an object given the specified validators and attributes. - /// - /// The type. - /// The object. - /// - /// true if the specified object is valid; otherwise, false. - /// - /// obj. - public bool IsValid(T target) => ValidateObject(target); - - /// - /// Adds a validator to a specific class. - /// - /// The type of the object. - /// The predicate that will be evaluated. - /// The message. - /// - /// predicate - /// or - /// message. - /// - public void AddValidator(Predicate predicate, string message) - where T : class - { - if (predicate == null) - throw new ArgumentNullException(nameof(predicate)); - - if (string.IsNullOrEmpty(message)) - throw new ArgumentNullException(message); - - if (!_predicates.TryGetValue(typeof(T), out var existing)) - { - existing = new List>(); - _predicates[typeof(T)] = existing; - } - - existing.Add(Tuple.Create((Delegate)predicate, message)); - } - - private bool ValidateObject(T obj, bool returnOnError = true, Action action = null) - { - if (Equals(obj, null)) - throw new ArgumentNullException(nameof(obj)); - - if (_predicates.ContainsKey(typeof(T))) - { - foreach (var (@delegate, value) in _predicates[typeof(T)]) - { - if ((bool)@delegate.DynamicInvoke(obj)) continue; - - action?.Invoke(string.Empty, value); - if (returnOnError) return false; - } - } - - var properties = AttributeCache.DefaultCache.Value.RetrieveFromType(); - - foreach (var prop in properties) - { - foreach (var attribute in prop.Value) - { - var val = (IValidator)attribute; - - if (val.IsValid(prop.Key.GetValue(obj, null))) continue; - - action?.Invoke(prop.Key.Name, val.ErrorMessage); - if (returnOnError) return false; - } - } - - return true; - } - } + /// + /// The current. + /// + public static ObjectValidator Current => LazyInstance.Value; + + /// + /// Validates an object given the specified validators and attributes. + /// + /// The type of the object. + /// The object. + /// A validation result. + public ObjectValidationResult Validate(T target) { + ObjectValidationResult errorList = new ObjectValidationResult(); + _ = this.ValidateObject(target, false, errorList.Add); + + return errorList; + } + + /// + /// Validates an object given the specified validators and attributes. + /// + /// The type. + /// The object. + /// + /// true if the specified object is valid; otherwise, false. + /// + /// obj. + public Boolean IsValid(T target) => this.ValidateObject(target); + + /// + /// Adds a validator to a specific class. + /// + /// The type of the object. + /// The predicate that will be evaluated. + /// The message. + /// + /// predicate + /// or + /// message. + /// + public void AddValidator(Predicate predicate, String message) where T : class { + if(predicate == null) { + throw new ArgumentNullException(nameof(predicate)); + } + + if(String.IsNullOrEmpty(message)) { + throw new ArgumentNullException(message); + } + + if(!this._predicates.TryGetValue(typeof(T), out List> existing)) { + existing = new List>(); + this._predicates[typeof(T)] = existing; + } + + existing.Add(Tuple.Create((Delegate)predicate, message)); + } + + private Boolean ValidateObject(T obj, Boolean returnOnError = true, Action action = null) { + if(Equals(obj, null)) { + throw new ArgumentNullException(nameof(obj)); + } + + if(this._predicates.ContainsKey(typeof(T))) { + foreach((Delegate @delegate, String value) in this._predicates[typeof(T)]) { + if((Boolean)@delegate.DynamicInvoke(obj)) { + continue; + } + + action?.Invoke(String.Empty, value); + if(returnOnError) { + return false; + } + } + } + + Dictionary> properties = AttributeCache.DefaultCache.Value.RetrieveFromType(); + + foreach(KeyValuePair> prop in properties) { + foreach(Object attribute in prop.Value) { + IValidator val = (IValidator)attribute; + + if(val.IsValid(prop.Key.GetValue(obj, null))) { + continue; + } + + action?.Invoke(prop.Key.Name, val.ErrorMessage); + if(returnOnError) { + return false; + } + } + } + + return true; + } + } } \ No newline at end of file diff --git a/Swan.Lite/Validators/Validators.cs b/Swan.Lite/Validators/Validators.cs index 5cf0692..35aeb01 100644 --- a/Swan.Lite/Validators/Validators.cs +++ b/Swan.Lite/Validators/Validators.cs @@ -1,132 +1,121 @@ using System; using System.Text.RegularExpressions; -namespace Swan.Validators -{ +namespace Swan.Validators { + /// + /// Regex validator. + /// + [AttributeUsage(AttributeTargets.Property)] + public class MatchAttribute : Attribute, IValidator { /// - /// Regex validator. + /// Initializes a new instance of the class. /// - [AttributeUsage(AttributeTargets.Property)] - public class MatchAttribute : Attribute, IValidator - { - /// - /// Initializes a new instance of the class. - /// - /// A regex string. - /// The error message. - /// Expression. - public MatchAttribute(string regex, string errorMessage = null) - { - Expression = regex ?? throw new ArgumentNullException(nameof(regex)); - ErrorMessage = errorMessage ?? "String does not match the specified regular expression"; - } - - /// - /// The string regex used to find a match. - /// - public string Expression { get; } - - /// - public string ErrorMessage { get; internal set; } - - /// - public bool IsValid(T value) - { - if (Equals(value, default(T))) - return false; - - return !(value is string) - ? throw new ArgumentException("Property is not a string") - : Regex.IsMatch(value.ToString(), Expression); - } - } - + /// A regex string. + /// The error message. + /// Expression. + public MatchAttribute(String regex, String errorMessage = null) { + this.Expression = regex ?? throw new ArgumentNullException(nameof(regex)); + this.ErrorMessage = errorMessage ?? "String does not match the specified regular expression"; + } + /// - /// Email validator. + /// The string regex used to find a match. /// - [AttributeUsage(AttributeTargets.Property)] - public class EmailAttribute : MatchAttribute - { - private const string EmailRegExp = - @"^(?("")("".+?(? - /// Initializes a new instance of the class. - /// - /// The error message. - public EmailAttribute(string errorMessage = null) - : base(EmailRegExp, errorMessage ?? "String is not an email") - { - } - } - + public String Expression { + get; + } + + /// + public String ErrorMessage { + get; internal set; + } + + /// + public Boolean IsValid(T value) => Equals(value, default(T)) ? false : !(value is String) ? throw new ArgumentException("Property is not a string") : Regex.IsMatch(value.ToString(), this.Expression); + } + + /// + /// Email validator. + /// + [AttributeUsage(AttributeTargets.Property)] + public class EmailAttribute : MatchAttribute { + private const String EmailRegExp = + @"^(?("")("".+?(? - /// A not null validator. + /// Initializes a new instance of the class. /// - [AttributeUsage(AttributeTargets.Property)] - public class NotNullAttribute : Attribute, IValidator - { - /// - public string ErrorMessage => "Value is null"; - - /// - public bool IsValid(T value) => !Equals(default(T), value); - } - + /// The error message. + public EmailAttribute(String errorMessage = null) : base(EmailRegExp, errorMessage ?? "String is not an email") { + } + } + + /// + /// A not null validator. + /// + [AttributeUsage(AttributeTargets.Property)] + public class NotNullAttribute : Attribute, IValidator { + /// + public String ErrorMessage => "Value is null"; + + /// + public Boolean IsValid(T value) => !Equals(default(T), value); + } + + /// + /// A range constraint validator. + /// + [AttributeUsage(AttributeTargets.Property)] + public class RangeAttribute : Attribute, IValidator { /// - /// A range constraint validator. + /// Initializes a new instance of the class. + /// Constructor that takes integer minimum and maximum values. /// - [AttributeUsage(AttributeTargets.Property)] - public class RangeAttribute : Attribute, IValidator - { - /// - /// Initializes a new instance of the class. - /// Constructor that takes integer minimum and maximum values. - /// - /// The minimum value. - /// The maximum value. - public RangeAttribute(int min, int max) - { - if (min >= max) - throw new InvalidOperationException("Maximum value must be greater than minimum"); - - Maximum = max; - Minimum = min; - } - - /// - /// Initializes a new instance of the class. - /// Constructor that takes double minimum and maximum values. - /// - /// The minimum value. - /// The maximum value. - public RangeAttribute(double min, double max) - { - if (min >= max) - throw new InvalidOperationException("Maximum value must be greater than minimum"); - - Maximum = max; - Minimum = min; - } - - /// - public string ErrorMessage => "Value is not within the specified range"; - - /// - /// Maximum value for the range. - /// - public IComparable Maximum { get; } - - /// - /// Minimum value for the range. - /// - public IComparable Minimum { get; } - - /// - public bool IsValid(T value) - => value is IComparable comparable - ? comparable.CompareTo(Minimum) >= 0 && comparable.CompareTo(Maximum) <= 0 - : throw new ArgumentException(nameof(value)); - } + /// The minimum value. + /// The maximum value. + public RangeAttribute(Int32 min, Int32 max) { + if(min >= max) { + throw new InvalidOperationException("Maximum value must be greater than minimum"); + } + + this.Maximum = max; + this.Minimum = min; + } + + /// + /// Initializes a new instance of the class. + /// Constructor that takes double minimum and maximum values. + /// + /// The minimum value. + /// The maximum value. + public RangeAttribute(Double min, Double max) { + if(min >= max) { + throw new InvalidOperationException("Maximum value must be greater than minimum"); + } + + this.Maximum = max; + this.Minimum = min; + } + + /// + public String ErrorMessage => "Value is not within the specified range"; + + /// + /// Maximum value for the range. + /// + public IComparable Maximum { + get; + } + + /// + /// Minimum value for the range. + /// + public IComparable Minimum { + get; + } + + /// + public Boolean IsValid(T value) => value is IComparable comparable ? comparable.CompareTo(this.Minimum) >= 0 && comparable.CompareTo(this.Maximum) <= 0 : throw new ArgumentException(nameof(value)); + } } \ No newline at end of file