diff --git a/Unosquare.Swan.Lite/Abstractions/AtomicTypeBase.cs b/Unosquare.Swan.Lite/Abstractions/AtomicTypeBase.cs index ab352db..3d61fdd 100644 --- a/Unosquare.Swan.Lite/Abstractions/AtomicTypeBase.cs +++ b/Unosquare.Swan.Lite/Abstractions/AtomicTypeBase.cs @@ -1,243 +1,228 @@ -namespace Unosquare.Swan.Abstractions -{ - using System; - using System.Threading; - +using System; +using System.Threading; + +namespace Unosquare.Swan.Abstractions { + /// + /// 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) { + switch(other) { + case null: + return 1; + case AtomicTypeBase atomic: + return this.BackingValue.CompareTo(atomic.BackingValue); + case T variable: + return 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) { + switch(other) { + case AtomicTypeBase atomic: + return this.Equals(atomic); + case T variable: + return this.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 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/Unosquare.Swan.Lite/Abstractions/ExclusiveTimer.cs b/Unosquare.Swan.Lite/Abstractions/ExclusiveTimer.cs index 5a01d61..89ad5f0 100644 --- a/Unosquare.Swan.Lite/Abstractions/ExclusiveTimer.cs +++ b/Unosquare.Swan.Lite/Abstractions/ExclusiveTimer.cs @@ -1,197 +1,181 @@ -namespace Unosquare.Swan.Abstractions -{ - using System; - using System.Threading; - +using System; +using System.Threading; + +namespace Unosquare.Swan.Abstractions { + /// + /// 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) - { - // placholder - } - - /// - /// 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; - - /// - /// 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 - { - _backingTimer.Dispose(); - _cycleDoneEvent.Wait(); - _cycleDoneEvent.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) { + // placholder + } + + /// + /// 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; + + /// + /// 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._backingTimer.Dispose(); + this._cycleDoneEvent.Wait(); + this._cycleDoneEvent.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/Unosquare.Swan.Lite/Abstractions/ExpressionParser.cs b/Unosquare.Swan.Lite/Abstractions/ExpressionParser.cs index 85890f8..2816e51 100644 --- a/Unosquare.Swan.Lite/Abstractions/ExpressionParser.cs +++ b/Unosquare.Swan.Lite/Abstractions/ExpressionParser.cs @@ -1,94 +1,88 @@ -namespace Unosquare.Swan.Abstractions -{ - using System; - using System.Linq; - using System.Collections.Generic; - using System.Linq.Expressions; - +using System; +using System.Linq; +using System.Collections.Generic; +using System.Linq.Expressions; + +namespace Unosquare.Swan.Abstractions { + /// + /// 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) - { - var conversion = Expression.Convert(Parse(tokens), typeof(T)); - return Expression.Lambda>(conversion).Compile()(); - } - - /// - /// Parses the specified tokens. - /// - /// The tokens. - /// The final expression. - public virtual Expression Parse(IEnumerable tokens) - { - 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))); - 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) { + UnaryExpression conversion = Expression.Convert(this.Parse(tokens), typeof(T)); + return Expression.Lambda>(conversion).Compile()(); + } + + /// + /// Parses the specified tokens. + /// + /// The tokens. + /// The final expression. + public virtual Expression Parse(IEnumerable tokens) { + 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))); + 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/Unosquare.Swan.Lite/Abstractions/IObjectMap.cs b/Unosquare.Swan.Lite/Abstractions/IObjectMap.cs index 1c0419e..49732a4 100644 --- a/Unosquare.Swan.Lite/Abstractions/IObjectMap.cs +++ b/Unosquare.Swan.Lite/Abstractions/IObjectMap.cs @@ -1,27 +1,31 @@ -namespace Unosquare.Swan.Abstractions -{ - using System; - using System.Collections.Generic; - using System.Reflection; - +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Unosquare.Swan.Abstractions { + /// + /// 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/Unosquare.Swan.Lite/Abstractions/ISyncLocker.cs b/Unosquare.Swan.Lite/Abstractions/ISyncLocker.cs index 30a8b39..bcf1cb4 100644 --- a/Unosquare.Swan.Lite/Abstractions/ISyncLocker.cs +++ b/Unosquare.Swan.Lite/Abstractions/ISyncLocker.cs @@ -1,24 +1,22 @@ -namespace Unosquare.Swan.Abstractions -{ - using System; - +using System; + +namespace Unosquare.Swan.Abstractions { + /// + /// 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/Unosquare.Swan.Lite/Abstractions/IValidator.cs b/Unosquare.Swan.Lite/Abstractions/IValidator.cs index b8aec9d..3ec35f0 100644 --- a/Unosquare.Swan.Lite/Abstractions/IValidator.cs +++ b/Unosquare.Swan.Lite/Abstractions/IValidator.cs @@ -1,21 +1,23 @@ -namespace Unosquare.Swan.Abstractions -{ +using System; + +namespace Unosquare.Swan.Abstractions { + /// + /// 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/Unosquare.Swan.Lite/Abstractions/IWaitEvent.cs b/Unosquare.Swan.Lite/Abstractions/IWaitEvent.cs index 39758ff..dd17d43 100644 --- a/Unosquare.Swan.Lite/Abstractions/IWaitEvent.cs +++ b/Unosquare.Swan.Lite/Abstractions/IWaitEvent.cs @@ -1,57 +1,63 @@ -namespace Unosquare.Swan.Abstractions -{ - using System; - +using System; + +namespace Unosquare.Swan.Abstractions { + /// + /// 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/Unosquare.Swan.Lite/Abstractions/IWorker.cs b/Unosquare.Swan.Lite/Abstractions/IWorker.cs index 2efe9f5..ad3b114 100644 --- a/Unosquare.Swan.Lite/Abstractions/IWorker.cs +++ b/Unosquare.Swan.Lite/Abstractions/IWorker.cs @@ -1,18 +1,16 @@ -namespace Unosquare.Swan.Abstractions -{ +namespace Unosquare.Swan.Abstractions { + /// + /// A simple interface for application workers. + /// + public interface IWorker { /// - /// A simple interface for application workers. + /// Should start the task immediately and asynchronously. /// - public interface IWorker - { - /// - /// Should start the task immediately and asynchronously. - /// - void Start(); - - /// - /// Should stop the task immediately and synchronously. - /// - void Stop(); - } + void Start(); + + /// + /// Should stop the task immediately and synchronously. + /// + void Stop(); + } } diff --git a/Unosquare.Swan.Lite/Abstractions/RunnerBase.cs b/Unosquare.Swan.Lite/Abstractions/RunnerBase.cs index cd5f6b8..93fb86c 100644 --- a/Unosquare.Swan.Lite/Abstractions/RunnerBase.cs +++ b/Unosquare.Swan.Lite/Abstractions/RunnerBase.cs @@ -1,169 +1,155 @@ #if !NETSTANDARD1_3 -namespace Unosquare.Swan.Abstractions -{ - using System; - using System.Collections.Generic; - using System.Threading; - using Swan; - +using System; +using System.Collections.Generic; +using System.Threading; +namespace Unosquare.Swan.Abstractions { + /// + /// Represents an background worker abstraction with a life cycle and running at a independent thread. + /// + public abstract class RunnerBase { + 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 - { - 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 virtual void Setup() - { - // empty - } - - /// - /// Cleanups this instance. - /// - protected virtual void Cleanup() - { - // empty - } - - /// - /// Does the background work. - /// - /// The ct. - protected abstract void DoBackgroundWork(CancellationToken ct); - } + /// 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 virtual void Setup() { + // empty + } + + /// + /// Cleanups this instance. + /// + protected virtual void Cleanup() { + // empty + } + + /// + /// Does the background work. + /// + /// The ct. + protected abstract void DoBackgroundWork(CancellationToken ct); + } } #endif \ No newline at end of file diff --git a/Unosquare.Swan.Lite/Abstractions/SettingsProvider.cs b/Unosquare.Swan.Lite/Abstractions/SettingsProvider.cs index a13ed28..c134829 100644 --- a/Unosquare.Swan.Lite/Abstractions/SettingsProvider.cs +++ b/Unosquare.Swan.Lite/Abstractions/SettingsProvider.cs @@ -1,188 +1,184 @@ -namespace Unosquare.Swan.Abstractions -{ - using Formatters; - using Reflection; - using System; - using System.Collections; - using System.Collections.Generic; - using System.IO; - using System.Linq; - using System.Reflection; - +using Unosquare.Swan.Formatters; +using Unosquare.Swan.Reflection; +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; + +namespace Unosquare.Swan.Abstractions { + /// + /// Represents a provider to save and load settings using a plain JSON file. + /// + /// + /// The following example shows how to save and load settings. + /// + /// using Unosquare.Swan.Abstractions; + /// + /// public class Example + /// { + /// public static void Main() + /// { + /// // get user from settings + /// var user = SettingsProvider<Settings>.Instance.Global.User; + /// + /// // modify the port + /// SettingsProvider<Settings>.Instance.Global.Port = 20; + /// + /// // if we want these settings to persist + /// SettingsProvider<Settings>.Instance.PersistGlobalSettings(); + /// } + /// + /// public class Settings + /// { + /// public int Port { get; set; } = 9696; + /// + /// public string User { get; set; } = "User"; + /// } + /// } + /// + /// + /// The type of settings model. + public sealed class SettingsProvider + : SingletonBase> { + private readonly Object _syncRoot = new Object(); + + private T _global; + /// - /// Represents a provider to save and load settings using a plain JSON file. + /// Gets or sets the configuration file path. By default the entry assembly directory is used + /// and the filename is 'appsettings.json'. /// - /// - /// The following example shows how to save and load settings. - /// - /// using Unosquare.Swan.Abstractions; - /// - /// public class Example - /// { - /// public static void Main() - /// { - /// // get user from settings - /// var user = SettingsProvider<Settings>.Instance.Global.User; - /// - /// // modify the port - /// SettingsProvider<Settings>.Instance.Global.Port = 20; - /// - /// // if we want these settings to persist - /// SettingsProvider<Settings>.Instance.PersistGlobalSettings(); - /// } - /// - /// public class Settings - /// { - /// public int Port { get; set; } = 9696; - /// - /// public string User { get; set; } = "User"; - /// } - /// } - /// - /// - /// The type of settings model. - public sealed class SettingsProvider - : SingletonBase> - { - private readonly object _syncRoot = new object(); - - private T _global; - - /// - /// Gets or sets the configuration file path. By default the entry assembly directory is used - /// and the filename is 'appsettings.json'. - /// - /// - /// The configuration file path. - /// - public string ConfigurationFilePath { get; set; } = + /// + /// The configuration file path. + /// + public String ConfigurationFilePath { + get; set; + } = #if NETSTANDARD1_3 Path.Combine(Runtime.LocalStoragePath, "appsettings.json"); #else - Path.Combine(Runtime.EntryAssemblyDirectory, "appsettings.json"); + Path.Combine(Runtime.EntryAssemblyDirectory, "appsettings.json"); #endif - - /// - /// Gets the global settings object. - /// - /// - /// The global settings object. - /// - public T Global - { - get - { - lock (_syncRoot) - { - if (Equals(_global, default(T))) - ReloadGlobalSettings(); - - return _global; - } - } - } - - /// - /// Reloads the global settings. - /// - public void ReloadGlobalSettings() - { - if (File.Exists(ConfigurationFilePath) == false || File.ReadAllText(ConfigurationFilePath).Length == 0) - { - ResetGlobalSettings(); - return; - } - - lock (_syncRoot) - _global = Json.Deserialize(File.ReadAllText(ConfigurationFilePath)); - } - - /// - /// Persists the global settings. - /// - public void PersistGlobalSettings() => File.WriteAllText(ConfigurationFilePath, Json.Serialize(Global, true)); - - /// - /// Updates settings from list. - /// - /// The list. - /// - /// A list of settings of type ref="ExtendedPropertyInfo". - /// - /// propertyList. - public List RefreshFromList(List> propertyList) - { - if (propertyList == null) - throw new ArgumentNullException(nameof(propertyList)); - - var changedSettings = new List(); - var globalProps = Runtime.PropertyTypeCache.RetrieveAllProperties(); - - foreach (var property in propertyList) - { - var propertyInfo = globalProps.FirstOrDefault(x => x.Name == property.Property); - - 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(); - } - - return changedSettings; - } - - /// - /// Gets the list. - /// - /// A List of ExtendedPropertyInfo of the type T. - public List> GetList() - { - var jsonData = Json.Deserialize(Json.Serialize(Global)) as Dictionary; - - return jsonData?.Keys - .Select(p => new ExtendedPropertyInfo(p) { Value = jsonData[p] }) - .ToList(); - } - - /// - /// Resets the global settings. - /// - public void ResetGlobalSettings() - { - lock (_syncRoot) - _global = Activator.CreateInstance(); - - PersistGlobalSettings(); - } - - private bool SetValue(object property, object originalValue, PropertyInfo propertyInfo) - { - switch (property) - { - case null when originalValue == null: - break; - case null: - propertyInfo.SetValue(Global, null); - return true; - default: - if (propertyInfo.PropertyType.TryParseBasicType(property, out var propertyValue) && - !propertyValue.Equals(originalValue)) - { - propertyInfo.SetValue(Global, propertyValue); - return true; - } - - break; - } - - return false; - } - } + + /// + /// Gets the global settings object. + /// + /// + /// The global settings object. + /// + public T Global { + get { + lock(this._syncRoot) { + if(Equals(this._global, default(T))) { + this.ReloadGlobalSettings(); + } + + return this._global; + } + } + } + + /// + /// Reloads the global settings. + /// + public void ReloadGlobalSettings() { + if(File.Exists(this.ConfigurationFilePath) == false || File.ReadAllText(this.ConfigurationFilePath).Length == 0) { + this.ResetGlobalSettings(); + return; + } + + lock(this._syncRoot) { + this._global = Json.Deserialize(File.ReadAllText(this.ConfigurationFilePath)); + } + } + + /// + /// Persists the global settings. + /// + public void PersistGlobalSettings() => File.WriteAllText(this.ConfigurationFilePath, Json.Serialize(this.Global, true)); + + /// + /// Updates settings from list. + /// + /// The list. + /// + /// A list of settings of type ref="ExtendedPropertyInfo". + /// + /// propertyList. + public List RefreshFromList(List> propertyList) { + if(propertyList == null) { + throw new ArgumentNullException(nameof(propertyList)); + } + + List changedSettings = new List(); + IEnumerable globalProps = Runtime.PropertyTypeCache.RetrieveAllProperties(); + + foreach(ExtendedPropertyInfo property in propertyList) { + PropertyInfo 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(!isChanged) { + continue; + } + + changedSettings.Add(property.Property); + this.PersistGlobalSettings(); + } + + return changedSettings; + } + + /// + /// Gets the list. + /// + /// A List of ExtendedPropertyInfo of the type T. + public List> GetList() { + Dictionary jsonData = Json.Deserialize(Json.Serialize(this.Global)) as Dictionary; + + return jsonData?.Keys + .Select(p => new ExtendedPropertyInfo(p) { Value = jsonData[p] }) + .ToList(); + } + + /// + /// Resets the global settings. + /// + public void ResetGlobalSettings() { + lock(this._syncRoot) { + this._global = Activator.CreateInstance(); + } + + this.PersistGlobalSettings(); + } + + private Boolean SetValue(Object property, Object originalValue, PropertyInfo propertyInfo) { + switch(property) { + case null when originalValue == null: + break; + case null: + propertyInfo.SetValue(this.Global, null); + return true; + default: + if(propertyInfo.PropertyType.TryParseBasicType(property, out Object propertyValue) && + !propertyValue.Equals(originalValue)) { + propertyInfo.SetValue(this.Global, propertyValue); + return true; + } + + break; + } + + return false; + } + } } \ No newline at end of file diff --git a/Unosquare.Swan.Lite/Abstractions/SingletonBase.cs b/Unosquare.Swan.Lite/Abstractions/SingletonBase.cs index 6132885..3ecc014 100644 --- a/Unosquare.Swan.Lite/Abstractions/SingletonBase.cs +++ b/Unosquare.Swan.Lite/Abstractions/SingletonBase.cs @@ -1,59 +1,57 @@ -namespace Unosquare.Swan.Abstractions -{ - using System; - +using System; + +namespace Unosquare.Swan.Abstractions { + /// + /// 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/Unosquare.Swan.Lite/Abstractions/Tokenizer.cs b/Unosquare.Swan.Lite/Abstractions/Tokenizer.cs index ad18cce..990a4c2 100644 --- a/Unosquare.Swan.Lite/Abstractions/Tokenizer.cs +++ b/Unosquare.Swan.Lite/Abstractions/Tokenizer.cs @@ -1,143 +1,139 @@ -namespace Unosquare.Swan.Abstractions -{ - using System; - using System.Collections.Generic; - using System.Linq; - +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Unosquare.Swan.Abstractions { + /// + /// 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,309 +147,304 @@ 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 }; - } - + }; + /// - /// Represents an operator with precedence. + /// Shunting the yard. /// - 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; } - } - + /// 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 }; + } + + /// + /// Represents an operator with precedence. + /// + public class Operator { /// - /// Represents a Token structure. + /// Gets or sets the name. /// - 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 name. + /// + public String Name { + get; set; + } + /// - /// Enums the token types. + /// Gets or sets the precedence. /// - 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, - } + /// + /// 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; + } + } + + /// + /// Represents a Token structure. + /// + public struct Token { + /// + /// Initializes a new instance of the struct. + /// + /// 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; + } + } + + /// + /// Enums the token types. + /// + 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, + } } \ No newline at end of file diff --git a/Unosquare.Swan.Lite/Abstractions/ViewModelBase.cs b/Unosquare.Swan.Lite/Abstractions/ViewModelBase.cs index 5328465..911db30 100644 --- a/Unosquare.Swan.Lite/Abstractions/ViewModelBase.cs +++ b/Unosquare.Swan.Lite/Abstractions/ViewModelBase.cs @@ -1,127 +1,123 @@ -namespace Unosquare.Swan.Lite.Abstractions -{ - using System.Collections.Concurrent; - using System.Collections.Generic; - using System.ComponentModel; - using System.Linq; - using System.Runtime.CompilerServices; - using System.Threading.Tasks; - +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace Unosquare.Swan.Lite.Abstractions { + /// + /// A base class for implementing models that fire notifications when their properties change. + /// This class is ideal for implementing MVVM driven UIs. + /// + /// + public abstract class ViewModelBase : INotifyPropertyChanged { + private readonly ConcurrentDictionary QueuedNotifications = new ConcurrentDictionary(); + private readonly Boolean UseDeferredNotifications; + /// - /// A base class for implementing models that fire notifications when their properties change. - /// This class is ideal for implementing MVVM driven UIs. + /// Initializes a new instance of the class. /// - /// - public abstract class ViewModelBase : INotifyPropertyChanged - { - private readonly ConcurrentDictionary QueuedNotifications = new ConcurrentDictionary(); - private readonly bool UseDeferredNotifications; - - /// - /// Initializes a new instance of the class. - /// - /// Set to true to use deferred notifications in the background. - protected ViewModelBase(bool useDeferredNotifications) - { - UseDeferredNotifications = useDeferredNotifications; - } - - /// - /// Initializes a new instance of the class. - /// - protected ViewModelBase() - : this(false) - { - // placeholder - } - - /// - /// Occurs when a property value changes. - /// - /// - public event PropertyChangedEventHandler PropertyChanged; - - /// Checks if a property already matches a desired value. Sets the property and - /// notifies listeners only when necessary. - /// Type of the property. - /// Reference to a property with both getter and setter. - /// Desired value for the property. - /// Name of the property used to notify listeners. This - /// value is optional and can be provided automatically when invoked from compilers that - /// support CallerMemberName. - /// An rray of property names to notify in addition to notifying the changes on the current property name. - /// True if the value was changed, false if the existing value matched the - /// desired value. - protected bool SetProperty(ref T storage, T value, [CallerMemberName] string propertyName = "", string[] notifyAlso = null) - { - if (EqualityComparer.Default.Equals(storage, value)) - return false; - - storage = value; - NotifyPropertyChanged(propertyName, notifyAlso); - return true; - } - - /// - /// Notifies one or more properties changed. - /// - /// The property names. - protected void NotifyPropertyChanged(params string[] propertyNames) => NotifyPropertyChanged(null, propertyNames); - - /// - /// Notifies one or more properties changed. - /// - /// The main property. - /// The auxiliary properties. - private void NotifyPropertyChanged(string mainProperty, string[] auxiliaryProperties) - { - // Queue property notification - if (string.IsNullOrWhiteSpace(mainProperty) == false) - QueuedNotifications[mainProperty] = true; - - // Set the state for notification properties - if (auxiliaryProperties != null) - { - foreach (var property in auxiliaryProperties) - { - if (string.IsNullOrWhiteSpace(property) == false) - QueuedNotifications[property] = true; - } - } - - // Depending on operation mode, either fire the notifications in the background - // or fire them immediately - if (UseDeferredNotifications) - Task.Run(() => NotifyQueuedProperties()); - else - NotifyQueuedProperties(); - } - - /// - /// Notifies the queued properties and resets the property name to a non-queued stated. - /// - private void NotifyQueuedProperties() - { - // get a snapshot of property names. - var propertyNames = QueuedNotifications.Keys.ToArray(); - - // Iterate through the properties - foreach (var property in propertyNames) - { - // don't notify if we don't have a change - if (!QueuedNotifications[property]) continue; - - // notify and reset queued state to false - try { OnPropertyChanged(property); } - finally { QueuedNotifications[property] = false; } - } - } - - /// - /// Called when a property changes its backing value. - /// - /// Name of the property. - private void OnPropertyChanged(string propertyName) => - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName ?? string.Empty)); - } + /// Set to true to use deferred notifications in the background. + protected ViewModelBase(Boolean useDeferredNotifications) => this.UseDeferredNotifications = useDeferredNotifications; + + /// + /// Initializes a new instance of the class. + /// + protected ViewModelBase() + : this(false) { + // placeholder + } + + /// + /// Occurs when a property value changes. + /// + /// + public event PropertyChangedEventHandler PropertyChanged; + + /// Checks if a property already matches a desired value. Sets the property and + /// notifies listeners only when necessary. + /// Type of the property. + /// Reference to a property with both getter and setter. + /// Desired value for the property. + /// Name of the property used to notify listeners. This + /// value is optional and can be provided automatically when invoked from compilers that + /// support CallerMemberName. + /// An rray of property names to notify in addition to notifying the changes on the current property name. + /// True if the value was changed, false if the existing value matched the + /// desired value. + protected Boolean SetProperty(ref T storage, T value, [CallerMemberName] String propertyName = "", String[] notifyAlso = null) { + if(EqualityComparer.Default.Equals(storage, value)) { + return false; + } + + storage = value; + this.NotifyPropertyChanged(propertyName, notifyAlso); + return true; + } + + /// + /// Notifies one or more properties changed. + /// + /// The property names. + protected void NotifyPropertyChanged(params String[] propertyNames) => this.NotifyPropertyChanged(null, propertyNames); + + /// + /// Notifies one or more properties changed. + /// + /// The main property. + /// The auxiliary properties. + private void NotifyPropertyChanged(String mainProperty, String[] auxiliaryProperties) { + // Queue property notification + if(String.IsNullOrWhiteSpace(mainProperty) == false) { + this.QueuedNotifications[mainProperty] = true; + } + + // Set the state for notification properties + if(auxiliaryProperties != null) { + foreach(String property in auxiliaryProperties) { + if(String.IsNullOrWhiteSpace(property) == false) { + this.QueuedNotifications[property] = true; + } + } + } + + // Depending on operation mode, either fire the notifications in the background + // or fire them immediately + if(this.UseDeferredNotifications) { + _ = Task.Run(() => this.NotifyQueuedProperties()); + } else { + this.NotifyQueuedProperties(); + } + } + + /// + /// Notifies the queued properties and resets the property name to a non-queued stated. + /// + private void NotifyQueuedProperties() { + // get a snapshot of property names. + String[] propertyNames = this.QueuedNotifications.Keys.ToArray(); + + // Iterate through the properties + foreach(String property in propertyNames) { + // don't notify if we don't have a change + if(!this.QueuedNotifications[property]) { + continue; + } + + // notify and reset queued state to false + try { + this.OnPropertyChanged(property); + } finally { this.QueuedNotifications[property] = false; } + } + } + + /// + /// Called when a property changes its backing value. + /// + /// Name of the property. + private void OnPropertyChanged(String propertyName) => + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName ?? String.Empty)); + } } diff --git a/Unosquare.Swan.Lite/AtomicBoolean.cs b/Unosquare.Swan.Lite/AtomicBoolean.cs index bb1e27b..3648e58 100644 --- a/Unosquare.Swan.Lite/AtomicBoolean.cs +++ b/Unosquare.Swan.Lite/AtomicBoolean.cs @@ -1,26 +1,24 @@ -namespace Unosquare.Swan -{ - using Abstractions; - +using System; +using Unosquare.Swan.Abstractions; + +namespace Unosquare.Swan { + /// + /// 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/Unosquare.Swan.Lite/AtomicDouble.cs b/Unosquare.Swan.Lite/AtomicDouble.cs index 9699d76..1f1e1bf 100644 --- a/Unosquare.Swan.Lite/AtomicDouble.cs +++ b/Unosquare.Swan.Lite/AtomicDouble.cs @@ -1,29 +1,26 @@ -namespace Unosquare.Swan -{ - using System; - using Abstractions; - +using System; +using Unosquare.Swan.Abstractions; + +namespace Unosquare.Swan { + /// + /// 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/Unosquare.Swan.Lite/AtomicInteger.cs b/Unosquare.Swan.Lite/AtomicInteger.cs index cb44e47..f36ce0d 100644 --- a/Unosquare.Swan.Lite/AtomicInteger.cs +++ b/Unosquare.Swan.Lite/AtomicInteger.cs @@ -1,29 +1,26 @@ -namespace Unosquare.Swan -{ - using System; - using Abstractions; - +using System; +using Unosquare.Swan.Abstractions; + +namespace Unosquare.Swan { + /// + /// 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/Unosquare.Swan.Lite/AtomicLong.cs b/Unosquare.Swan.Lite/AtomicLong.cs index 8506cc6..471dd5f 100644 --- a/Unosquare.Swan.Lite/AtomicLong.cs +++ b/Unosquare.Swan.Lite/AtomicLong.cs @@ -1,26 +1,24 @@ -namespace Unosquare.Swan -{ - using Abstractions; - +using System; +using Unosquare.Swan.Abstractions; + +namespace Unosquare.Swan { + /// + /// Fast, atomioc long combining interlocked to write value and volatile to read values. + /// + public sealed class AtomicLong : AtomicTypeBase { /// - /// Fast, atomioc 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/Unosquare.Swan.Lite/Attributes/ArgumentOptionAttribute.cs b/Unosquare.Swan.Lite/Attributes/ArgumentOptionAttribute.cs index c7267ae..4a2e78c 100644 --- a/Unosquare.Swan.Lite/Attributes/ArgumentOptionAttribute.cs +++ b/Unosquare.Swan.Lite/Attributes/ArgumentOptionAttribute.cs @@ -1,102 +1,105 @@ -namespace Unosquare.Swan.Attributes -{ - using System; - +using System; + +namespace Unosquare.Swan.Attributes { + /// + /// 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/Unosquare.Swan.Lite/Attributes/CopyableAttribute.cs b/Unosquare.Swan.Lite/Attributes/CopyableAttribute.cs index 0fb0f29..7d38779 100644 --- a/Unosquare.Swan.Lite/Attributes/CopyableAttribute.cs +++ b/Unosquare.Swan.Lite/Attributes/CopyableAttribute.cs @@ -1,13 +1,11 @@ -namespace Unosquare.Swan.Attributes -{ - using System; - - /// - /// Represents an attribute to select which properties are copyable between objects. - /// - /// - [AttributeUsage(AttributeTargets.Property)] - public class CopyableAttribute : Attribute - { - } +using System; + +namespace Unosquare.Swan.Attributes { + /// + /// Represents an attribute to select which properties are copyable between objects. + /// + /// + [AttributeUsage(AttributeTargets.Property)] + public class CopyableAttribute : Attribute { + } } diff --git a/Unosquare.Swan.Lite/Attributes/JsonPropertyAttribute.cs b/Unosquare.Swan.Lite/Attributes/JsonPropertyAttribute.cs index 88b8060..f5aee79 100644 --- a/Unosquare.Swan.Lite/Attributes/JsonPropertyAttribute.cs +++ b/Unosquare.Swan.Lite/Attributes/JsonPropertyAttribute.cs @@ -1,39 +1,40 @@ -namespace Unosquare.Swan.Attributes -{ - using System; - +using System; + +namespace Unosquare.Swan.Attributes { + /// + /// 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/Unosquare.Swan.Lite/Attributes/PropertyDisplayAttribute.cs b/Unosquare.Swan.Lite/Attributes/PropertyDisplayAttribute.cs index db0e0f2..f708286 100644 --- a/Unosquare.Swan.Lite/Attributes/PropertyDisplayAttribute.cs +++ b/Unosquare.Swan.Lite/Attributes/PropertyDisplayAttribute.cs @@ -1,54 +1,62 @@ -namespace Unosquare.Swan.Attributes -{ - using System; - +using System; + +namespace Unosquare.Swan.Attributes { + /// + /// 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/Unosquare.Swan.Lite/Attributes/StructEndiannessAttribute.cs b/Unosquare.Swan.Lite/Attributes/StructEndiannessAttribute.cs index fed256c..a526534 100644 --- a/Unosquare.Swan.Lite/Attributes/StructEndiannessAttribute.cs +++ b/Unosquare.Swan.Lite/Attributes/StructEndiannessAttribute.cs @@ -1,30 +1,27 @@ -namespace Unosquare.Swan.Attributes -{ - using System; - +using System; + +namespace Unosquare.Swan.Attributes { + /// + /// 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/Unosquare.Swan.Lite/Attributes/Validators.cs b/Unosquare.Swan.Lite/Attributes/Validators.cs index 3c86c74..67167af 100644 --- a/Unosquare.Swan.Lite/Attributes/Validators.cs +++ b/Unosquare.Swan.Lite/Attributes/Validators.cs @@ -1,133 +1,130 @@ -namespace Unosquare.Swan.Attributes -{ - using System; - using System.Text.RegularExpressions; - using Abstractions; - +using System; +using System.Text.RegularExpressions; +using Unosquare.Swan.Abstractions; + +namespace Unosquare.Swan.Attributes { + /// + /// 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(Expression)); - 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) + /// A regex string. + /// The error message. + /// Expression. + public MatchAttribute(String regex, String errorMessage = null) { + this.Expression = regex ?? throw new ArgumentNullException(nameof(this.Expression)); + this.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 Boolean IsValid(T value) => Equals(value, default(T)) + ? false + : !(value is String) ? throw new ArgumentException("Property is not a string") - : Regex.IsMatch(value.ToString(), Expression); - } - } - + : Regex.IsMatch(value.ToString(), this.Expression); + } + + /// + /// Email validator. + /// + [AttributeUsage(AttributeTargets.Property)] + public class EmailAttribute : MatchAttribute { + private const String EmailRegExp = + @"^(?("")("".+?(? - /// Email validator. + /// Initializes a new instance of the class. /// - [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") - { - } - } - + /// 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 not null validator. + /// Initializes a new instance of the class. + /// Constructor that takes integer minimum and maximum values. /// - [AttributeUsage(AttributeTargets.Property)] - public class NotNullAttribute : Attribute, IValidator - { - /// - public string ErrorMessage => "Value is null"; - - /// - public bool IsValid(T value) => !Equals(default(T), 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; + } + /// - /// A range constraint validator. + /// Initializes a new instance of the class. + /// Constructor that takes double 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(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 diff --git a/Unosquare.Swan.Lite/Attributes/VerbOptionAttribute.cs b/Unosquare.Swan.Lite/Attributes/VerbOptionAttribute.cs index 9b9554f..1b0e676 100644 --- a/Unosquare.Swan.Lite/Attributes/VerbOptionAttribute.cs +++ b/Unosquare.Swan.Lite/Attributes/VerbOptionAttribute.cs @@ -1,40 +1,39 @@ -namespace Unosquare.Swan.Attributes -{ - using System; - +using System; + +namespace Unosquare.Swan.Attributes { + /// + /// 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/Unosquare.Swan.Lite/Components/ArgumentParse.Validator.cs b/Unosquare.Swan.Lite/Components/ArgumentParse.Validator.cs index 6241c66..4e283ed 100644 --- a/Unosquare.Swan.Lite/Components/ArgumentParse.Validator.cs +++ b/Unosquare.Swan.Lite/Components/ArgumentParse.Validator.cs @@ -1,159 +1,146 @@ -namespace Unosquare.Swan.Components -{ - using System.Linq; - using System.Reflection; - using Attributes; - using System; - using System.Collections.Generic; - - /// - /// 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 => Runtime.AttributeCache.RetrieveOne(p)) - .Where(x => x != null); - - private void GetRequiredList() - { - foreach (var targetProperty in _properties) - { - var optionAttr = Runtime.AttributeCache.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 = Runtime.AttributeCache.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.GetTypeInfo().IsEnum) - { - var parsedValue = Enum.Parse( - targetProperty.PropertyType, - propertyValueString, - _settings.CaseInsensitiveEnumValues); - - targetProperty.SetValue(result, Enum.ToObject(targetProperty.PropertyType, parsedValue)); - - return true; - } - - return targetProperty.PropertyType.IsArray - ? targetProperty.TrySetArray(propertyValueString.Split(optionAttr?.Separator ?? ','), result) - : targetProperty.TrySetBasicType(propertyValueString, result); - } - - private PropertyInfo TryGetProperty(string propertyName) - => _properties.FirstOrDefault(p => - string.Equals(Runtime.AttributeCache.RetrieveOne(p)?.LongName, propertyName, _settings.NameComparer) || - string.Equals(Runtime.AttributeCache.RetrieveOne(p)?.ShortName, propertyName, _settings.NameComparer)); - } - } +using System.Linq; +using System.Reflection; +using Unosquare.Swan.Attributes; +using System; +using System.Collections.Generic; + +namespace Unosquare.Swan.Components { + /// + /// 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 => Runtime.AttributeCache.RetrieveOne(p)) + .Where(x => x != null); + + private void GetRequiredList() { + foreach(PropertyInfo targetProperty in this._properties) { + ArgumentOptionAttribute optionAttr = Runtime.AttributeCache.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 = Runtime.AttributeCache.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.GetTypeInfo().IsEnum) { + Object parsedValue = Enum.Parse( + targetProperty.PropertyType, + propertyValueString, + this._settings.CaseInsensitiveEnumValues); + + targetProperty.SetValue(result, Enum.ToObject(targetProperty.PropertyType, parsedValue)); + + return true; + } + + return targetProperty.PropertyType.IsArray + ? targetProperty.TrySetArray(propertyValueString.Split(optionAttr?.Separator ?? ','), result) + : targetProperty.TrySetBasicType(propertyValueString, result); + } + + private PropertyInfo TryGetProperty(String propertyName) + => this._properties.FirstOrDefault(p => + String.Equals(Runtime.AttributeCache.RetrieveOne(p)?.LongName, propertyName, this._settings.NameComparer) || + String.Equals(Runtime.AttributeCache.RetrieveOne(p)?.ShortName, propertyName, this._settings.NameComparer)); + } + } } \ No newline at end of file diff --git a/Unosquare.Swan.Lite/Components/ArgumentParser.TypeResolver.cs b/Unosquare.Swan.Lite/Components/ArgumentParser.TypeResolver.cs index 8834342..30d586f 100644 --- a/Unosquare.Swan.Lite/Components/ArgumentParser.TypeResolver.cs +++ b/Unosquare.Swan.Lite/Components/ArgumentParser.TypeResolver.cs @@ -1,57 +1,52 @@ -namespace Unosquare.Swan.Components -{ - using System.Linq; - using System.Reflection; - using Attributes; - using System; - - /// - /// 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[] GetProperties() => _properties?.Any() == true ? _properties : null; - - public object GetOptionsObject(T instance) - { - _properties = Runtime.PropertyTypeCache.RetrieveAllProperties(true).ToArray(); - - if (!_properties.Any(x => x.GetCustomAttributes(typeof(VerbOptionAttribute), false).Any())) - return instance; - - var selectedVerb = string.IsNullOrWhiteSpace(_selectedVerb) +using System.Linq; +using System.Reflection; +using Unosquare.Swan.Attributes; +using System; + +namespace Unosquare.Swan.Components { + /// + /// 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[] GetProperties() => this._properties?.Any() == true ? this._properties : null; + + public Object GetOptionsObject(T instance) { + this._properties = Runtime.PropertyTypeCache.RetrieveAllProperties(true).ToArray(); + + if(!this._properties.Any(x => x.GetCustomAttributes(typeof(VerbOptionAttribute), false).Any())) { + return instance; + } + + PropertyInfo selectedVerb = String.IsNullOrWhiteSpace(this._selectedVerb) ? null - : _properties.FirstOrDefault(x => - Runtime.AttributeCache.RetrieveOne(x).Name.Equals(_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 = Runtime.PropertyTypeCache.RetrieveAllProperties(selectedVerb.PropertyType, true) - .ToArray(); - - return verbProperty?.GetValue(instance); - } - } - } + : this._properties.FirstOrDefault(x => + Runtime.AttributeCache.RetrieveOne(x).Name.Equals(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 = Runtime.PropertyTypeCache.RetrieveAllProperties(selectedVerb.PropertyType, true) + .ToArray(); + + return verbProperty?.GetValue(instance); + } + } + } } \ No newline at end of file diff --git a/Unosquare.Swan.Lite/Components/ArgumentParser.cs b/Unosquare.Swan.Lite/Components/ArgumentParser.cs index c468e35..ffbaf32 100644 --- a/Unosquare.Swan.Lite/Components/ArgumentParser.cs +++ b/Unosquare.Swan.Lite/Components/ArgumentParser.cs @@ -1,230 +1,228 @@ -namespace Unosquare.Swan.Components -{ - using System; - using System.Collections.Generic; - using Attributes; - using System.Linq; - +using System; +using System.Collections.Generic; +using Unosquare.Swan.Attributes; +using System.Linq; + +namespace Unosquare.Swan.Components { + /// + /// 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 Unosquare.Swan; + /// using Unosquare.Swan.Attributes; + /// + /// static void Main(string[] args) + /// { + /// // create an instance of the Options class + /// var options = new Options(); + /// + /// // parse the supplied command-line arguments into the options object + /// var res = Runtime.ArgumentParser.ParseArguments(args, 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 Unosquare.Swan; + /// using Unosquare.Swan.Attributes; + /// + /// 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 Unosquare.Swan; - /// using Unosquare.Swan.Attributes; - /// - /// static void Main(string[] args) - /// { - /// // create an instance of the Options class - /// var options = new Options(); - /// - /// // parse the supplied command-line arguments into the options object - /// var res = Runtime.ArgumentParser.ParseArguments(args, 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 Unosquare.Swan; - /// using Unosquare.Swan.Attributes; - /// - /// 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 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, 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; - } - - var properties = typeResolver.GetProperties(); - - if (properties == null) - throw new InvalidOperationException($"Type {typeof(T).Name} is not valid"); - - var validator = new Validator(properties, args, options, Settings); - - if (validator.IsValid()) - return true; - - ReportIssues(validator); - return false; - } - - private static void ReportUnknownVerb() - { - "No verb was specified".WriteLine(ConsoleColor.Red); - "Valid verbs:".WriteLine(ConsoleColor.Cyan); - - Runtime.PropertyTypeCache.RetrieveAllProperties(true) - .Select(x => Runtime.AttributeCache.RetrieveOne(x)) - .Where(x => x != null) - .ToList() - .ForEach(x => x.ToString().WriteLine(ConsoleColor.Cyan)); - } - - private void ReportIssues(Validator validator) - { -#if !NETSTANDARD1_3 - if (Settings.WriteBanner) - Runtime.WriteWelcomeBanner(); + 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 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, 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; + } + + System.Reflection.PropertyInfo[] properties = typeResolver.GetProperties(); + + if(properties == null) { + throw new InvalidOperationException($"Type {typeof(T).Name} is not valid"); + } + + Validator validator = new Validator(properties, args, options, this.Settings); + + if(validator.IsValid()) { + return true; + } + + this.ReportIssues(validator); + return false; + } + + private static void ReportUnknownVerb() { + "No verb was specified".WriteLine(ConsoleColor.Red); + "Valid verbs:".WriteLine(ConsoleColor.Cyan); + + Runtime.PropertyTypeCache.RetrieveAllProperties(true) + .Select(x => Runtime.AttributeCache.RetrieveOne(x)) + .Where(x => x != null) + .ToList() + .ForEach(x => x.ToString().WriteLine(ConsoleColor.Cyan)); + } + + private void ReportIssues(Validator validator) { +#if !NETSTANDARD1_3 + if(this.Settings.WriteBanner) { + Runtime.WriteWelcomeBanner(); + } #endif - - var options = validator.GetPropertiesOptions(); - - foreach (var option in options) - { - string.Empty.WriteLine(); - - // 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}) "; - - $" {shortName}{comma}{longName}\t\t{defaultValue}{option.HelpText}".WriteLine(ConsoleColor.Cyan); - } - - string.Empty.WriteLine(); - " --help\t\tDisplay this help screen.".WriteLine(ConsoleColor.Cyan); - - if (validator.UnknownList.Any()) - $"Unknown arguments: {string.Join(", ", validator.UnknownList)}".WriteLine(ConsoleColor.Red); - - if (validator.RequiredList.Any()) - $"Required arguments: {string.Join(", ", validator.RequiredList)}".WriteLine(ConsoleColor.Red); - } - } + + IEnumerable options = validator.GetPropertiesOptions(); + + foreach(ArgumentOptionAttribute option in options) { + String.Empty.WriteLine(); + + // 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}) "; + + $" {shortName}{comma}{longName}\t\t{defaultValue}{option.HelpText}".WriteLine(ConsoleColor.Cyan); + } + + String.Empty.WriteLine(); + " --help\t\tDisplay this help screen.".WriteLine(ConsoleColor.Cyan); + + if(validator.UnknownList.Any()) { + $"Unknown arguments: {String.Join(", ", validator.UnknownList)}".WriteLine(ConsoleColor.Red); + } + + if(validator.RequiredList.Any()) { + $"Required arguments: {String.Join(", ", validator.RequiredList)}".WriteLine(ConsoleColor.Red); + } + } + } } diff --git a/Unosquare.Swan.Lite/Components/ArgumentParserSettings.cs b/Unosquare.Swan.Lite/Components/ArgumentParserSettings.cs index f3f4b25..aedb32b 100644 --- a/Unosquare.Swan.Lite/Components/ArgumentParserSettings.cs +++ b/Unosquare.Swan.Lite/Components/ArgumentParserSettings.cs @@ -1,53 +1,51 @@ -namespace Unosquare.Swan.Components -{ - using System; - +using System; + +namespace Unosquare.Swan.Components { + /// + /// 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/Unosquare.Swan.Lite/Components/Benchmark.cs b/Unosquare.Swan.Lite/Components/Benchmark.cs index b970bc9..abcdd9a 100644 --- a/Unosquare.Swan.Lite/Components/Benchmark.cs +++ b/Unosquare.Swan.Lite/Components/Benchmark.cs @@ -1,130 +1,122 @@ -namespace Unosquare.Swan.Components -{ - using System; - using System.Collections.Generic; - using System.Diagnostics; - using System.Linq; - using System.Text; - +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; + +namespace Unosquare.Swan.Components { + /// + /// A simple benchmarking class. + /// + /// + /// The following code demonstrates how to create a simple benchmark. + /// + /// namespace Examples.Benchmark.Simple + /// { + /// using Unosquare.Swan.Components; + /// + /// 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 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 Unosquare.Swan.Components; - /// - /// 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 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(); - } - - /// - /// 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); - } - } - - /// - /// 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); - _stopwatch?.Stop(); - } - - _stopwatch = null; - _isDisposed = true; - } - } - } + /// 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(); + } + + /// + /// 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); + } + } + + /// + /// 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); + this._stopwatch?.Stop(); + } + + this._stopwatch = null; + this._isDisposed = true; + } + } + } } diff --git a/Unosquare.Swan.Lite/Components/CollectionCacheRepository.cs b/Unosquare.Swan.Lite/Components/CollectionCacheRepository.cs index 1438acd..e3a5230 100644 --- a/Unosquare.Swan.Lite/Components/CollectionCacheRepository.cs +++ b/Unosquare.Swan.Lite/Components/CollectionCacheRepository.cs @@ -1,44 +1,42 @@ -namespace Unosquare.Swan.Components -{ - using System; - using System.Collections.Concurrent; - using System.Collections.Generic; - using System.Linq; - +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; + +namespace Unosquare.Swan.Components { + /// + /// 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. - /// - /// type. - public IEnumerable Retrieve(Type key, Func> factory) - { - 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. + /// + /// type. + public IEnumerable Retrieve(Type key, Func> 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/Unosquare.Swan.Lite/Components/EnumHelper.cs b/Unosquare.Swan.Lite/Components/EnumHelper.cs index c4c6e6d..b131c0b 100644 --- a/Unosquare.Swan.Lite/Components/EnumHelper.cs +++ b/Unosquare.Swan.Lite/Components/EnumHelper.cs @@ -1,171 +1,144 @@ -namespace Unosquare.Swan.Components -{ - using System; - using System.Collections.Generic; - using System.Linq; - using Abstractions; - +using System; +using System.Collections.Generic; +using System.Linq; +using Unosquare.Swan.Abstractions; + +namespace Unosquare.Swan.Components { + /// + /// 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/Unosquare.Swan.Lite/Components/ObjectComparer.cs b/Unosquare.Swan.Lite/Components/ObjectComparer.cs index e28b9ea..c649868 100644 --- a/Unosquare.Swan.Lite/Components/ObjectComparer.cs +++ b/Unosquare.Swan.Lite/Components/ObjectComparer.cs @@ -1,193 +1,183 @@ -namespace Unosquare.Swan.Components -{ - using System; - using System.Collections; - using System.Collections.Generic; - using System.Linq; - using System.Reflection; - +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Unosquare.Swan.Components { + /// + /// 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.ContainsKey(targetType)) - return Equals(left, right); - - if (targetType.IsValueType() || targetType.IsArray) - return AreStructsEqual(left, right, targetType); - - return 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 = Runtime.PropertyTypeCache.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(Runtime.FieldTypeCache.RetrieveAllFields(targetType)) - .Union(Runtime.PropertyTypeCache.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)); + } + + return Definitions.BasicTypesInfo.ContainsKey(targetType) + ? Equals(left, right) + : 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 = Runtime.PropertyTypeCache.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(Runtime.FieldTypeCache.RetrieveAllFields(targetType)) + .Union(Runtime.PropertyTypeCache.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/Unosquare.Swan.Lite/Components/ObjectMap.cs b/Unosquare.Swan.Lite/Components/ObjectMap.cs index 7743e26..a9c9356 100644 --- a/Unosquare.Swan.Lite/Components/ObjectMap.cs +++ b/Unosquare.Swan.Lite/Components/ObjectMap.cs @@ -1,116 +1,116 @@ -namespace Unosquare.Swan.Components -{ - using System; - using System.Collections.Generic; - using System.Linq; - using System.Linq.Expressions; - using System.Reflection; - using Abstractions; - +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using Unosquare.Swan.Abstractions; + +namespace Unosquare.Swan.Components { + /// + /// 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/Unosquare.Swan.Lite/Components/ObjectMapper.cs b/Unosquare.Swan.Lite/Components/ObjectMapper.cs index 76f38e8..c635a13 100644 --- a/Unosquare.Swan.Lite/Components/ObjectMapper.cs +++ b/Unosquare.Swan.Lite/Components/ObjectMapper.cs @@ -1,411 +1,385 @@ -namespace Unosquare.Swan.Components -{ - using System; - using System.Collections; - using System.Collections.Generic; - using System.Linq; - using System.Reflection; - using Abstractions; - +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Unosquare.Swan.Abstractions; + +namespace Unosquare.Swan.Components { + /// + /// 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 Unosquare.Swan + /// + /// class Example + /// { + /// class Person + /// { + /// public string Name { get; set; } + /// public int Age { get; set; } + /// } + /// + /// static void Main() + /// { + /// var obj = new { Name = "Søren", Age = 42 }; + /// + /// var person = Runtime.ObjectMapper.Map<Person>(obj); + /// } + /// } + /// + /// The following code explains how to explicitly map certain properties. + /// + /// using Unosquare.Swan + /// + /// 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 class 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. + /// Copies the specified source. /// - /// - /// The following code explains how to map an object's properties into an instance of type T. - /// - /// using Unosquare.Swan - /// - /// class Example - /// { - /// class Person - /// { - /// public string Name { get; set; } - /// public int Age { get; set; } - /// } - /// - /// static void Main() - /// { - /// var obj = new { Name = "Søren", Age = 42 }; - /// - /// var person = Runtime.ObjectMapper.Map<Person>(obj); - /// } - /// } - /// - /// The following code explains how to explicitly map certain properties. - /// - /// using Unosquare.Swan - /// - /// 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 class ObjectMapper - { - private readonly List _maps = new List(); - - /// - /// 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, - string[] propertiesToCopy = null, - string[] ignoreProperties = null) - { - if (source == null) - throw new ArgumentNullException(nameof(source)); - - if (target == null) - throw new ArgumentNullException(nameof(target)); - - return Copy( + /// 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, + String[] propertiesToCopy = null, + String[] ignoreProperties = null) { + if(source == null) { + throw new ArgumentNullException(nameof(source)); + } + + if(target == null) { + throw new ArgumentNullException(nameof(target)); + } + + return Copy( target, propertiesToCopy, ignoreProperties, - GetSourceMap(source)); - } - - /// - /// 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, - string[] propertiesToCopy = null, - string[] ignoreProperties = null) - { - if (source == null) - throw new ArgumentNullException(nameof(source)); - - if (target == null) - throw new ArgumentNullException(nameof(target)); - - return Copy( + GetSourceMap(source)); + } + + /// + /// 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, + String[] propertiesToCopy = null, + String[] ignoreProperties = null) { + if(source == null) { + throw new ArgumentNullException(nameof(source)); + } + + if(target == null) { + throw new ArgumentNullException(nameof(target)); + } + + return Copy( target, propertiesToCopy, ignoreProperties, source.ToDictionary( x => x.Key.ToLowerInvariant(), - x => new TypeValuePair(typeof(object), x.Value))); - } - - /// - /// 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 = Runtime.PropertyTypeCache.RetrieveAllProperties(true); - var destinationType = Runtime.PropertyTypeCache.RetrieveAllProperties(true); - - var intersect = sourceType.Intersect(destinationType, new PropertyInfoComparer()).ToArray(); - - if (intersect.Any() == false) - { - 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 Copy( - object target, - IEnumerable propertiesToCopy, - IEnumerable ignoreProperties, - Dictionary sourceProperties) - { - // 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 = Runtime.PropertyTypeCache - .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, target) ? 1 : 0); - } - - private static bool TrySetValue(KeyValuePair property, object target) - { - try - { - SetValue(property, target); - return true; - } - catch - { - // swallow - } - - return false; - } - - private static void SetValue(KeyValuePair property, object target) - { - if (property.Value.Type.GetTypeInfo().IsEnum) - { - property.Key.SetValue(target, - Enum.ToObject(property.Key.PropertyType, property.Value.Value)); - - return; - } - - if (!property.Value.Type.IsValueType() && property.Key.PropertyType == property.Value.Type) - { - property.Key.SetValue(target, GetValue(property.Value.Value, property.Key.PropertyType)); - - return; - } - - if (property.Key.PropertyType == typeof(bool)) - { - property.Key.SetValue(target, - Convert.ToBoolean(property.Value.Value)); - - return; - } - - property.Key.TrySetBasicType(property.Value.Value, target); - } - - 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 Array targetArray: - for (var i = 0; i < sourceList.Count; i++) - { - try - { - targetArray.SetValue( - sourceList[i].GetType().IsValueType() - ? sourceList[i] - : sourceList[i].CopyPropertiesToNew(), i); - } - catch - { - // ignored - } - } - - break; - case IList sourceList when target is IList targetList: - var addMethod = targetType.GetMethods() - .FirstOrDefault( - m => m.Name.Equals(Formatters.Json.AddMethodName) && m.IsPublic && - m.GetParameters().Length == 1); - - if (addMethod == null) return target; - - foreach (var item in sourceList) - { - try - { - targetList.Add(item.GetType().IsValueType() - ? 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 = Runtime.PropertyTypeCache - .RetrieveFilteredProperties(source.GetType(), true, x => x.CanRead) - .ToArray(); - - return sourceProperties - .Select(x => x.Name) - .Distinct() - .ToDictionary( - x => x.ToLowerInvariant(), - x => new TypeValuePair(sourceProperties.First(y => y.Name == x).PropertyType, - sourceProperties.First(y => y.Name == x).GetValue(source))); - } - - internal class TypeValuePair - { - public TypeValuePair(Type type, object value) - { - Type = type; - Value = value; - } - - public Type Type { get; } - - public object Value { get; } - } - - 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(); - } - } + x => new TypeValuePair(typeof(Object), x.Value))); + } + + /// + /// 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 = Runtime.PropertyTypeCache.RetrieveAllProperties(true); + IEnumerable destinationType = Runtime.PropertyTypeCache.RetrieveAllProperties(true); + + PropertyInfo[] intersect = sourceType.Intersect(destinationType, new PropertyInfoComparer()).ToArray(); + + if(intersect.Any() == false) { + 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 Copy( + Object target, + IEnumerable propertiesToCopy, + IEnumerable ignoreProperties, + Dictionary sourceProperties) { + // 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 = Runtime.PropertyTypeCache + .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, target) ? 1 : 0); + } + + private static Boolean TrySetValue(KeyValuePair property, Object target) { + try { + SetValue(property, target); + return true; + } catch { + // swallow + } + + return false; + } + + private static void SetValue(KeyValuePair property, Object target) { + if(property.Value.Type.GetTypeInfo().IsEnum) { + property.Key.SetValue(target, + Enum.ToObject(property.Key.PropertyType, property.Value.Value)); + + return; + } + + if(!property.Value.Type.IsValueType() && property.Key.PropertyType == property.Value.Type) { + property.Key.SetValue(target, GetValue(property.Value.Value, property.Key.PropertyType)); + + return; + } + + if(property.Key.PropertyType == typeof(Boolean)) { + property.Key.SetValue(target, + Convert.ToBoolean(property.Value.Value)); + + return; + } + + _ = property.Key.TrySetBasicType(property.Value.Value, target); + } + + 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 Array targetArray: + for(Int32 i = 0; i < sourceList.Count; i++) { + try { + targetArray.SetValue( + sourceList[i].GetType().IsValueType() + ? sourceList[i] + : sourceList[i].CopyPropertiesToNew(), i); + } catch { + // ignored + } + } + + break; + case IList sourceList when target is IList targetList: + MethodInfo addMethod = targetType.GetMethods() + .FirstOrDefault( + m => m.Name.Equals(Formatters.Json.AddMethodName) && m.IsPublic && + m.GetParameters().Length == 1); + + if(addMethod == null) { + return target; + } + + foreach(Object item in sourceList) { + try { + _ = targetList.Add(item.GetType().IsValueType() + ? 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 = Runtime.PropertyTypeCache + .RetrieveFilteredProperties(source.GetType(), true, x => x.CanRead) + .ToArray(); + + return sourceProperties + .Select(x => x.Name) + .Distinct() + .ToDictionary( + x => x.ToLowerInvariant(), + x => new TypeValuePair(sourceProperties.First(y => y.Name == x).PropertyType, + sourceProperties.First(y => y.Name == x).GetValue(source))); + } + + internal class TypeValuePair { + public TypeValuePair(Type type, Object value) { + this.Type = type; + this.Value = value; + } + + public Type Type { + get; + } + + public Object Value { + get; + } + } + + 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/Unosquare.Swan.Lite/Components/ObjectValidator.cs b/Unosquare.Swan.Lite/Components/ObjectValidator.cs index 1e42cf8..40c71b0 100644 --- a/Unosquare.Swan.Lite/Components/ObjectValidator.cs +++ b/Unosquare.Swan.Lite/Components/ObjectValidator.cs @@ -1,204 +1,207 @@ -namespace Unosquare.Swan.Components -{ - using System; - using System.Linq; - using System.Collections.Concurrent; - using System.Collections.Generic; - using Abstractions; - +using System; +using System.Linq; +using System.Collections.Concurrent; +using System.Collections.Generic; +using Unosquare.Swan.Abstractions; + +namespace Unosquare.Swan.Components { + /// + /// Represents an object validator. + /// + /// + /// The following code describes how to perform a simple object validation. + /// + /// using Unosquare.Swan.Components; + /// + /// 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 Unosquare.Swan.Components; + /// + /// 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 readonly ConcurrentDictionary>> _predicates = + new ConcurrentDictionary>>(); + /// - /// Represents an object validator. + /// Validates an object given the specified validators and attributes. /// - /// - /// The following code describes how to perform a simple object validation. - /// - /// using Unosquare.Swan.Components; - /// - /// 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 Unosquare.Swan.Components; - /// - /// 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 readonly ConcurrentDictionary>> _predicates = - new ConcurrentDictionary>>(); - - /// - /// Validates an object given the specified validators and attributes. - /// - /// The type of the object. - /// The object. - /// A validation result. - public ObjectValidationResult Validate(T obj) - { - var errorList = new ObjectValidationResult(); - ValidateObject(obj, 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 obj) => ValidateObject(obj); - - /// - /// 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 validation in _predicates[typeof(T)]) - { - if ((bool) validation.Item1.DynamicInvoke(obj)) continue; - - action?.Invoke(string.Empty, validation.Item2); - if (returnOnError) return false; - } - } - - var properties = Runtime.AttributeCache.RetrieveFromType(typeof(IValidator)); - - 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 type of the object. + /// The object. + /// A validation result. + public ObjectValidationResult Validate(T obj) { + ObjectValidationResult errorList = new ObjectValidationResult(); + _ = this.ValidateObject(obj, false, errorList.Add); + + return errorList; + } + /// - /// Defines a validation result containing all validation errors and their properties. + /// Validates an object given the specified validators and attributes. /// - public class ObjectValidationResult - { - /// - /// A list of errors. - /// - public List Errors { get; set; } = new List(); - - /// - /// 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; set; } - - /// - /// The message error. - /// - public string ErrorMessage { get; set; } - } - } + /// The type. + /// The object. + /// + /// true if the specified object is valid; otherwise, false. + /// + /// obj. + public Boolean IsValid(T obj) => this.ValidateObject(obj); + + /// + /// 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(Tuple validation in this._predicates[typeof(T)]) { + if((Boolean)validation.Item1.DynamicInvoke(obj)) { + continue; + } + + action?.Invoke(String.Empty, validation.Item2); + if(returnOnError) { + return false; + } + } + } + + Dictionary> properties = Runtime.AttributeCache.RetrieveFromType(typeof(IValidator)); + + 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; + } + } + + /// + /// Defines a validation result containing all validation errors and their properties. + /// + public class ObjectValidationResult { + /// + /// A list of errors. + /// + public List Errors { get; set; } = new List(); + + /// + /// 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; set; + } + + /// + /// The message error. + /// + public String ErrorMessage { + get; set; + } + } + } } \ No newline at end of file diff --git a/Unosquare.Swan.Lite/Components/SyncLockerFactory.cs b/Unosquare.Swan.Lite/Components/SyncLockerFactory.cs index 633dfcf..8428d39 100644 --- a/Unosquare.Swan.Lite/Components/SyncLockerFactory.cs +++ b/Unosquare.Swan.Lite/Components/SyncLockerFactory.cs @@ -1,52 +1,48 @@ -namespace Unosquare.Swan.Components -{ - using System; - using System.Threading; - using Abstractions; - +using System; +using System.Threading; +using Unosquare.Swan.Abstractions; + +namespace Unosquare.Swan.Components { + /// + /// 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 - -#if !NETSTANDARD1_3 - /// - /// Creates a reader-writer lock backed by a standard ReaderWriterLock. - /// - /// The synchronized locker. - public static ISyncLocker Create() => new SyncLocker(); + 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 + +#if !NETSTANDARD1_3 + /// + /// Creates a reader-writer lock backed by a standard ReaderWriterLock. + /// + /// The synchronized locker. + public static ISyncLocker Create() => new SyncLocker(); #else /// /// Creates a reader-writer lock backed by a standard ReaderWriterLockSlim when @@ -55,143 +51,142 @@ /// The synchronized locker public static ISyncLocker Create() => new SyncLockerSlim(); #endif - - /// - /// 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(); - } - } - -#if !NETSTANDARD1_3 - /// - /// 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; - } - } + + /// + /// 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(); + } + } + } + +#if !NETSTANDARD1_3 + /// + /// 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; + } + } #endif - - /// - /// 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 - } + + /// + /// 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/Unosquare.Swan.Lite/Components/TimerControl.cs b/Unosquare.Swan.Lite/Components/TimerControl.cs index 09641f3..0ba0a29 100644 --- a/Unosquare.Swan.Lite/Components/TimerControl.cs +++ b/Unosquare.Swan.Lite/Components/TimerControl.cs @@ -1,63 +1,55 @@ #if !NETSTANDARD1_3 -namespace Unosquare.Swan.Components -{ - using System; - using System.Threading; - using Abstractions; +using System; +using System.Threading; +using Unosquare.Swan.Abstractions; +namespace Unosquare.Swan.Components { + /// + /// Use this singleton to wait for a specific TimeSpan or time. + /// + /// Internally this class will use a Timer and a ManualResetEvent to block until + /// the time condition is satisfied. + /// + /// + public class TimerControl : SingletonBase { + [System.Diagnostics.CodeAnalysis.SuppressMessage("Codequalität", "IDE0052:Ungelesene private Member entfernen", Justification = "")] + private readonly Timer _innerTimer; + private readonly IWaitEvent _delayLock = WaitEventFactory.Create(true); + /// - /// Use this singleton to wait for a specific TimeSpan or time. - /// - /// Internally this class will use a Timer and a ManualResetEvent to block until - /// the time condition is satisfied. + /// Initializes a new instance of the class. /// - /// - public class TimerControl : SingletonBase - { - private readonly Timer _innerTimer; - private readonly IWaitEvent _delayLock = WaitEventFactory.Create(true); - - /// - /// Initializes a new instance of the class. - /// - protected TimerControl() - { - _innerTimer = new Timer( - x => - { - try - { - _delayLock.Complete(); - _delayLock.Begin(); - } - catch - { - // ignore - } + protected TimerControl() => this._innerTimer = new Timer( + x => { + try { + this._delayLock.Complete(); + this._delayLock.Begin(); + } catch { + // ignore + } }, null, 0, - 15); - } - - /// - /// Waits until the time is elapsed. - /// - /// The until date. - /// The cancellation token. - public void WaitUntil(DateTime untilDate, CancellationToken ct = default) - { - while (!ct.IsCancellationRequested && DateTime.UtcNow < untilDate) - _delayLock.Wait(); - } - - /// - /// Waits the specified wait time. - /// - /// The wait time. - /// The cancellation token. - public void Wait(TimeSpan waitTime, CancellationToken ct = default) => - WaitUntil(DateTime.UtcNow.Add(waitTime), ct); - } + 15); + + /// + /// Waits until the time is elapsed. + /// + /// The until date. + /// The cancellation token. + public void WaitUntil(DateTime untilDate, CancellationToken ct = default) { + while(!ct.IsCancellationRequested && DateTime.UtcNow < untilDate) { + this._delayLock.Wait(); + } + } + + /// + /// Waits the specified wait time. + /// + /// The wait time. + /// The cancellation token. + public void Wait(TimeSpan waitTime, CancellationToken ct = default) => + this.WaitUntil(DateTime.UtcNow.Add(waitTime), ct); + } } #endif \ No newline at end of file diff --git a/Unosquare.Swan.Lite/Components/WaitEventFactory.cs b/Unosquare.Swan.Lite/Components/WaitEventFactory.cs index 5116ee7..b718e18 100644 --- a/Unosquare.Swan.Lite/Components/WaitEventFactory.cs +++ b/Unosquare.Swan.Lite/Components/WaitEventFactory.cs @@ -1,222 +1,196 @@ -#if !NETSTANDARD1_3 -namespace Unosquare.Swan.Components -{ - using System; - using System.Threading; - using Abstractions; - + + +#if !NETSTANDARD1_3 +using System; +using System.Threading; +using Unosquare.Swan.Abstractions; +namespace Unosquare.Swan.Components { + /// + /// Provides a Manual Reset Event factory with a unified API. + /// + /// + /// The following example shows how to use the WaitEventFactory class. + /// + /// using Unosquare.Swan.Components; + /// + /// 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(); + /// + /// Console.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 Unosquare.Swan.Components; - /// - /// 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(); - /// - /// Console.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 + } } #endif \ No newline at end of file diff --git a/Unosquare.Swan.Lite/DateTimeSpan.cs b/Unosquare.Swan.Lite/DateTimeSpan.cs index 0f3735c..b02dcfa 100644 --- a/Unosquare.Swan.Lite/DateTimeSpan.cs +++ b/Unosquare.Swan.Lite/DateTimeSpan.cs @@ -1,174 +1,172 @@ -namespace Unosquare.Swan -{ - using System; - +using System; + +namespace Unosquare.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/Unosquare.Swan.Lite/Definitions.Types.cs b/Unosquare.Swan.Lite/Definitions.Types.cs index a96ef8a..5f1b6fe 100644 --- a/Unosquare.Swan.Lite/Definitions.Types.cs +++ b/Unosquare.Swan.Lite/Definitions.Types.cs @@ -1,132 +1,140 @@ -namespace Unosquare.Swan -{ - using Reflection; - using System; - using System.Collections.Generic; - using System.Linq; - using System.Net; - +using Unosquare.Swan.Reflection; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; + +namespace Unosquare.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 Dictionary BasicTypesInfo = - new Dictionary - { + public static readonly Dictionary BasicTypesInfo = + 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(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()}, + {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(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 List AllBasicTypes { get; } = new List(BasicTypesInfo.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 List AllNumericTypes { get; } = new List( - BasicTypesInfo - .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 List AllNumericValueTypes { get; } = new List( - BasicTypesInfo - .Where(kvp => kvp.Value.IsNumeric && kvp.Value.IsNullableValueType == false) - .Select(kvp => kvp.Key).ToArray()); - - /// - /// Contains all basic value types. i.e. excludes string and nullables. - /// - /// - /// All basic value types. - /// - public static List AllBasicValueTypes { get; } = new List( - BasicTypesInfo - .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 List AllBasicValueAndStringTypes { get; } = new List( - BasicTypesInfo - .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 List AllBasicNullableValueTypes { get; } = new List( - BasicTypesInfo - .Where(kvp => kvp.Value.IsNullableValueType) - .Select(kvp => kvp.Key).ToArray()); - } + {typeof(IPAddress), new ExtendedTypeInfo()}, + }; + + #endregion + + /// + /// Contains all basic types, including string, date time, and all of their nullable counterparts. + /// + /// + /// All basic types. + /// + public static List AllBasicTypes { get; } = new List(BasicTypesInfo.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 List AllNumericTypes { + get; + } = new List( + BasicTypesInfo + .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 List AllNumericValueTypes { + get; + } = new List( + BasicTypesInfo + .Where(kvp => kvp.Value.IsNumeric && kvp.Value.IsNullableValueType == false) + .Select(kvp => kvp.Key).ToArray()); + + /// + /// Contains all basic value types. i.e. excludes string and nullables. + /// + /// + /// All basic value types. + /// + public static List AllBasicValueTypes { + get; + } = new List( + BasicTypesInfo + .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 List AllBasicValueAndStringTypes { + get; + } = new List( + BasicTypesInfo + .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 List AllBasicNullableValueTypes { + get; + } = new List( + BasicTypesInfo + .Where(kvp => kvp.Value.IsNullableValueType) + .Select(kvp => kvp.Key).ToArray()); + } } \ No newline at end of file diff --git a/Unosquare.Swan.Lite/Definitions.cs b/Unosquare.Swan.Lite/Definitions.cs index c321369..3dd98c4 100644 --- a/Unosquare.Swan.Lite/Definitions.cs +++ b/Unosquare.Swan.Lite/Definitions.cs @@ -1,39 +1,34 @@ -namespace Unosquare.Swan -{ - using System.Text; - +using System; +using System.Text; + +namespace Unosquare.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/Unosquare.Swan.Lite/Enums.cs b/Unosquare.Swan.Lite/Enums.cs index 0e52562..37459fe 100644 --- a/Unosquare.Swan.Lite/Enums.cs +++ b/Unosquare.Swan.Lite/Enums.cs @@ -1,60 +1,56 @@ -namespace Unosquare.Swan -{ +namespace Unosquare.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, + /// - /// Enumerates the different Application Worker States. + /// Windows /// - public enum AppWorkerState - { - /// - /// The stopped - /// - Stopped, - - /// - /// The running - /// - Running, - } - + Windows, + /// - /// Defines Endianness, big or little. + /// UNIX/Linux /// - 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, - } + Unix, + + /// + /// macOS (OSX) + /// + Osx, + } + + /// + /// Enumerates the different Application Worker States. + /// + public enum AppWorkerState { + /// + /// The stopped + /// + Stopped, + + /// + /// The running + /// + Running, + } + + /// + /// 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, + } } diff --git a/Unosquare.Swan.Lite/Eventing.cs b/Unosquare.Swan.Lite/Eventing.cs index 5e0ffcf..ca2ece2 100644 --- a/Unosquare.Swan.Lite/Eventing.cs +++ b/Unosquare.Swan.Lite/Eventing.cs @@ -1,168 +1,181 @@ -namespace Unosquare.Swan -{ - using System; - +using System; + +namespace Unosquare.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, - LogMessageType 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 LogMessageType 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, + LogMessageType 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; + } + /// - /// Event arguments representing a message logged and about to be - /// displayed on the terminal (console). Set the CancelOutput property in the - /// event handler to prevent the terminal from displaying the message. + /// Gets logging message sequence. /// - /// - public class LogMessageDisplayingEventArgs : LogMessageReceivedEventArgs - { - /// - /// Initializes a new instance of the class. - /// - /// The instance containing the event data. - public LogMessageDisplayingEventArgs(LogMessageReceivedEventArgs data) - : base( - data.Sequence, - data.MessageType, - data.UtcDate, - data.Source, - data.Message, - data.ExtendedData, - data.CallerMemberName, - data.CallerFilePath, - data.CallerLineNumber) - { - CancelOutput = false; - } - - /// - /// Gets or sets a value indicating whether the displaying of the - /// logging message should be canceled. - /// - /// - /// true if [cancel output]; otherwise, false. - /// - public bool CancelOutput { get; set; } - } + /// + /// 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 LogMessageType 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; + } + + /// + /// Event arguments representing a message logged and about to be + /// displayed on the terminal (console). Set the CancelOutput property in the + /// event handler to prevent the terminal from displaying the message. + /// + /// + public class LogMessageDisplayingEventArgs : LogMessageReceivedEventArgs { + /// + /// Initializes a new instance of the class. + /// + /// The instance containing the event data. + public LogMessageDisplayingEventArgs(LogMessageReceivedEventArgs data) + : base( + data.Sequence, + data.MessageType, + data.UtcDate, + data.Source, + data.Message, + data.ExtendedData, + data.CallerMemberName, + data.CallerFilePath, + data.CallerLineNumber) => this.CancelOutput = false; + + /// + /// Gets or sets a value indicating whether the displaying of the + /// logging message should be canceled. + /// + /// + /// true if [cancel output]; otherwise, false. + /// + public Boolean CancelOutput { + get; set; + } + } } \ No newline at end of file diff --git a/Unosquare.Swan.Lite/Extensions.ByteArrays.cs b/Unosquare.Swan.Lite/Extensions.ByteArrays.cs index 6891b28..2eb9d18 100644 --- a/Unosquare.Swan.Lite/Extensions.ByteArrays.cs +++ b/Unosquare.Swan.Lite/Extensions.ByteArrays.cs @@ -1,672 +1,682 @@ -namespace Unosquare.Swan -{ - using System; - using System.Collections.Generic; - using System.Globalization; - using System.IO; - using System.Linq; - using System.Text; - using System.Threading; - using System.Threading.Tasks; - +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Unosquare.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 hex) - { - if (string.IsNullOrWhiteSpace(hex)) - throw new ArgumentNullException(nameof(hex)); - - return Enumerable - .Range(0, hex.Length / 2) - .Select(x => Convert.ToByte(hex.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 b, byte offset, byte length = 1) => (byte)((b >> 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 b, byte offset, byte length, byte value) - { - var mask = ~(0xff << length); - var valueAt = (byte)(value & mask); - - return (byte)((valueAt << offset) | (b & ~(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 b, byte offset, byte value) => b.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[] buffer, int offset, params byte[] sequence) - { - if (buffer == null) - throw new ArgumentNullException(nameof(buffer)); - - if (sequence == null) - throw new ArgumentNullException(nameof(sequence)); - - var seqOffset = offset.Clamp(0, buffer.Length - 1); - - var result = new List(); - - while (seqOffset < buffer.Length) - { - var separatorStartIndex = buffer.GetIndexOf(sequence, seqOffset); - - if (separatorStartIndex >= 0) - { - var item = new byte[separatorStartIndex - seqOffset + sequence.Length]; - Array.Copy(buffer, seqOffset, item, 0, item.Length); - result.Add(item); - seqOffset += item.Length; - } - else - { - var item = new byte[buffer.Length - seqOffset]; - Array.Copy(buffer, 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. - public static byte[] DeepClone(this byte[] buffer) - { - if (buffer == null) - return null; - - var result = new byte[buffer.Length]; - Array.Copy(buffer, result, buffer.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 nomatches 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); - - /// - /// Retrieves a sub-array from the specified . A sub-array starts at - /// the specified element position in . - /// - /// - /// An array of T that receives a sub-array, or an empty array of T if any problems with - /// the parameters. - /// - /// - /// An array of T from which to retrieve a sub-array. - /// - /// - /// An that represents the zero-based starting position of - /// a sub-array in . - /// - /// - /// An that represents the number of elements to retrieve. - /// - /// - /// The type of elements in . - /// - public static T[] SubArray(this T[] array, int startIndex, int length) - { - int len; - if (array == null || (len = array.Length) == 0) - return new T[0]; - - if (startIndex < 0 || length <= 0 || startIndex + length > len) - return new T[0]; - - if (startIndex == 0 && length == len) - return array; - - var subArray = new T[length]; - Array.Copy(array, startIndex, subArray, 0, length); - - return subArray; - } - - /// - /// Retrieves a sub-array from the specified . A sub-array starts at - /// the specified element position in . - /// - /// - /// An array of T that receives a sub-array, or an empty array of T if any problems with - /// the parameters. - /// - /// - /// An array of T from which to retrieve a sub-array. - /// - /// - /// A that represents the zero-based starting position of - /// a sub-array in . - /// - /// - /// A that represents the number of elements to retrieve. - /// - /// - /// The type of elements in . - /// - public static T[] SubArray(this T[] array, long startIndex, long length) => array.SubArray((int)startIndex, (int)length); - - /// - /// 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 ct = 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, ct).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 ct = 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, ct).ConfigureAwait(false); - if (nread == 0) - break; - - offset += nread; - length -= nread; - } - } - catch - { - // ignored - } - - return buff.SubArray(0, offset); - } - - /// - /// Converts an array of sbytes to an array of bytes. - /// - /// The sbyte array. - /// - /// The byte array from conversion. - /// - /// sbyteArray. - public static byte[] ToByteArray(this sbyte[] sbyteArray) - { - if (sbyteArray == null) - throw new ArgumentNullException(nameof(sbyteArray)); - - var byteArray = new byte[sbyteArray.Length]; - for (var index = 0; index < sbyteArray.Length; index++) - byteArray[index] = (byte)sbyteArray[index]; - - return byteArray; - } - - /// - /// Receives a byte array and returns it transformed in an sbyte array. - /// - /// The byte array. - /// - /// The sbyte array from conversion. - /// - /// byteArray. - public static sbyte[] ToSByteArray(this byte[] byteArray) - { - if (byteArray == null) - throw new ArgumentNullException(nameof(byteArray)); - - var sbyteArray = new sbyte[byteArray.Length]; - for (var index = 0; index < byteArray.Length; index++) - sbyteArray[index] = (sbyte)byteArray[index]; - return sbyteArray; - } - - /// - /// Gets the sbytes from a string. - /// - /// The encoding. - /// The s. - /// The sbyte array from string. - public static sbyte[] GetSBytes(this Encoding encoding, string s) - => encoding.GetBytes(s).ToSByteArray(); - - /// - /// Gets the string from a sbyte array. - /// - /// The encoding. - /// The data. - /// The string. - public static string GetString(this Encoding encoding, sbyte[] data) - => encoding.GetString(data.ToByteArray()); - - /// - /// Reads a number of characters from the current source Stream and writes the data to the target array at the - /// specified index. - /// - /// The source stream. - /// The target. - /// The start. - /// The count. - /// - /// The number of bytes read. - /// - /// - /// sourceStream - /// or - /// target. - /// - public static int ReadInput(this Stream sourceStream, ref sbyte[] target, int start, int count) - { - if (sourceStream == null) - throw new ArgumentNullException(nameof(sourceStream)); - - if (target == null) - throw new ArgumentNullException(nameof(target)); - - // Returns 0 bytes if not enough space in target - if (target.Length == 0) - return 0; - - var receiver = new byte[target.Length]; - var bytesRead = 0; - var startIndex = start; - var bytesToRead = count; - while (bytesToRead > 0) - { - var n = sourceStream.Read(receiver, startIndex, bytesToRead); - if (n == 0) - break; - bytesRead += n; - startIndex += n; - bytesToRead -= n; - } - - // Returns -1 if EOF - if (bytesRead == 0) - return -1; - - for (var i = start; i < start + bytesRead; i++) - target[i] = (sbyte)receiver[i]; - - return bytesRead; - } - - 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 hex) { + if(String.IsNullOrWhiteSpace(hex)) { + throw new ArgumentNullException(nameof(hex)); + } + + return Enumerable + .Range(0, hex.Length / 2) + .Select(x => Convert.ToByte(hex.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 b, Byte offset, Byte length = 1) => (Byte)((b >> 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 b, Byte offset, Byte length, Byte value) { + Int32 mask = ~(0xff << length); + Byte valueAt = (Byte)(value & mask); + + return (Byte)((valueAt << offset) | (b & ~(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 b, Byte offset, Byte value) => b.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[] buffer, Int32 offset, params Byte[] sequence) { + if(buffer == null) { + throw new ArgumentNullException(nameof(buffer)); + } + + if(sequence == null) { + throw new ArgumentNullException(nameof(sequence)); + } + + Int32 seqOffset = offset.Clamp(0, buffer.Length - 1); + + List result = new List(); + + while(seqOffset < buffer.Length) { + Int32 separatorStartIndex = buffer.GetIndexOf(sequence, seqOffset); + + if(separatorStartIndex >= 0) { + Byte[] item = new Byte[separatorStartIndex - seqOffset + sequence.Length]; + Array.Copy(buffer, seqOffset, item, 0, item.Length); + result.Add(item); + seqOffset += item.Length; + } else { + Byte[] item = new Byte[buffer.Length - seqOffset]; + Array.Copy(buffer, 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. + public static Byte[] DeepClone(this Byte[] buffer) { + if(buffer == null) { + return null; + } + + Byte[] result = new Byte[buffer.Length]; + Array.Copy(buffer, result, buffer.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 nomatches 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); + + /// + /// Retrieves a sub-array from the specified . A sub-array starts at + /// the specified element position in . + /// + /// + /// An array of T that receives a sub-array, or an empty array of T if any problems with + /// the parameters. + /// + /// + /// An array of T from which to retrieve a sub-array. + /// + /// + /// An that represents the zero-based starting position of + /// a sub-array in . + /// + /// + /// An that represents the number of elements to retrieve. + /// + /// + /// The type of elements in . + /// + public static T[] SubArray(this T[] array, Int32 startIndex, Int32 length) { + Int32 len; + if(array == null || (len = array.Length) == 0) { + return new T[0]; + } + + if(startIndex < 0 || length <= 0 || startIndex + length > len) { + return new T[0]; + } + + if(startIndex == 0 && length == len) { + return array; + } + + T[] subArray = new T[length]; + Array.Copy(array, startIndex, subArray, 0, length); + + return subArray; + } + + /// + /// Retrieves a sub-array from the specified . A sub-array starts at + /// the specified element position in . + /// + /// + /// An array of T that receives a sub-array, or an empty array of T if any problems with + /// the parameters. + /// + /// + /// An array of T from which to retrieve a sub-array. + /// + /// + /// A that represents the zero-based starting position of + /// a sub-array in . + /// + /// + /// A that represents the number of elements to retrieve. + /// + /// + /// The type of elements in . + /// + public static T[] SubArray(this T[] array, Int64 startIndex, Int64 length) => array.SubArray((Int32)startIndex, (Int32)length); + + /// + /// 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 ct = 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, ct).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 ct = 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, ct).ConfigureAwait(false); + if(nread == 0) { + break; + } + + offset += nread; + length -= nread; + } + } catch { + // ignored + } + + return buff.SubArray(0, offset); + } + + /// + /// Converts an array of sbytes to an array of bytes. + /// + /// The sbyte array. + /// + /// The byte array from conversion. + /// + /// sbyteArray. + public static Byte[] ToByteArray(this SByte[] sbyteArray) { + if(sbyteArray == null) { + throw new ArgumentNullException(nameof(sbyteArray)); + } + + Byte[] byteArray = new Byte[sbyteArray.Length]; + for(Int32 index = 0; index < sbyteArray.Length; index++) { + byteArray[index] = (Byte)sbyteArray[index]; + } + + return byteArray; + } + + /// + /// Receives a byte array and returns it transformed in an sbyte array. + /// + /// The byte array. + /// + /// The sbyte array from conversion. + /// + /// byteArray. + public static SByte[] ToSByteArray(this Byte[] byteArray) { + if(byteArray == null) { + throw new ArgumentNullException(nameof(byteArray)); + } + + SByte[] sbyteArray = new SByte[byteArray.Length]; + for(Int32 index = 0; index < byteArray.Length; index++) { + sbyteArray[index] = (SByte)byteArray[index]; + } + + return sbyteArray; + } + + /// + /// Gets the sbytes from a string. + /// + /// The encoding. + /// The s. + /// The sbyte array from string. + public static SByte[] GetSBytes(this Encoding encoding, String s) + => encoding.GetBytes(s).ToSByteArray(); + + /// + /// Gets the string from a sbyte array. + /// + /// The encoding. + /// The data. + /// The string. + public static String GetString(this Encoding encoding, SByte[] data) + => encoding.GetString(data.ToByteArray()); + + /// + /// Reads a number of characters from the current source Stream and writes the data to the target array at the + /// specified index. + /// + /// The source stream. + /// The target. + /// The start. + /// The count. + /// + /// The number of bytes read. + /// + /// + /// sourceStream + /// or + /// target. + /// + public static Int32 ReadInput(this Stream sourceStream, ref SByte[] target, Int32 start, Int32 count) { + if(sourceStream == null) { + throw new ArgumentNullException(nameof(sourceStream)); + } + + if(target == null) { + throw new ArgumentNullException(nameof(target)); + } + + // Returns 0 bytes if not enough space in target + if(target.Length == 0) { + return 0; + } + + Byte[] receiver = new Byte[target.Length]; + Int32 bytesRead = 0; + Int32 startIndex = start; + Int32 bytesToRead = count; + while(bytesToRead > 0) { + Int32 n = sourceStream.Read(receiver, startIndex, bytesToRead); + if(n == 0) { + break; + } + + bytesRead += n; + startIndex += n; + bytesToRead -= n; + } + + // Returns -1 if EOF + if(bytesRead == 0) { + return -1; + } + + for(Int32 i = start; i < start + bytesRead; i++) { + target[i] = (SByte)receiver[i]; + } + + return bytesRead; + } + + 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}"; + } + } } \ No newline at end of file diff --git a/Unosquare.Swan.Lite/Extensions.Dates.cs b/Unosquare.Swan.Lite/Extensions.Dates.cs index b527e7c..bcfca83 100644 --- a/Unosquare.Swan.Lite/Extensions.Dates.cs +++ b/Unosquare.Swan.Lite/Extensions.Dates.cs @@ -1,230 +1,231 @@ -namespace Unosquare.Swan -{ - using System; - using System.Collections.Generic; - using System.Linq; - - /// - /// Provides various extension methods for dates. - /// - public static class DateExtensions +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Unosquare.Swan { + /// + /// Provides various extension methods for dates. + /// + public static class DateExtensions { + private static readonly Dictionary DateRanges = new Dictionary() { - 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 date. - /// The concatenation of date.Year, date.Month and date.Day. - public static string ToSortableDate(this DateTime date) - => $"{date.Year:0000}-{date.Month:00}-{date.Day:00}"; - - /// - /// Converts the date to a YYYY-MM-DD HH:II:SS string. - /// - /// The date. - /// The concatenation of date.Year, date.Month, date.Day, date.Hour, date.Minute and date.Second. - public static string ToSortableDateTime(this DateTime date) - => $"{date.Year:0000}-{date.Month:00}-{date.Day:00} {date.Hour:00}:{date.Minute:00}:{date.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 sortableDate) - { - if (string.IsNullOrWhiteSpace(sortableDate)) - throw new ArgumentNullException(nameof(sortableDate)); - - var hour = 0; - var minute = 0; - var second = 0; - - var dateTimeParts = sortableDate.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(sortableDate)); - } - } - - /// - /// Creates a date's 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 date to convert. - /// Seconds since Unix epoch. - public static long ToUnixEpochDate(this DateTime date) - { + }; + + /// + /// Converts the date to a YYYY-MM-DD string. + /// + /// The date. + /// The concatenation of date.Year, date.Month and date.Day. + public static String ToSortableDate(this DateTime date) + => $"{date.Year:0000}-{date.Month:00}-{date.Day:00}"; + + /// + /// Converts the date to a YYYY-MM-DD HH:II:SS string. + /// + /// The date. + /// The concatenation of date.Year, date.Month, date.Day, date.Hour, date.Minute and date.Second. + public static String ToSortableDateTime(this DateTime date) + => $"{date.Year:0000}-{date.Month:00}-{date.Day:00} {date.Hour:00}:{date.Minute:00}:{date.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 sortableDate) { + if(String.IsNullOrWhiteSpace(sortableDate)) { + throw new ArgumentNullException(nameof(sortableDate)); + } + + Int32 hour = 0; + Int32 minute = 0; + Int32 second = 0; + + String[] dateTimeParts = sortableDate.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(sortableDate)); + } + } + + /// + /// Creates a date's 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 date to convert. + /// Seconds since Unix epoch. + public static Int64 ToUnixEpochDate(this DateTime date) { #if NETSTANDARD2_0 return new DateTimeOffset(date).ToUniversalTime().ToUnixTimeSeconds(); #else - var epochTicks = new DateTime(1970, 1, 1).Ticks; - - return (date.Ticks - epochTicks) / TimeSpan.TicksPerSecond; + Int64 epochTicks = new DateTime(1970, 1, 1).Ticks; + + return (date.Ticks - epochTicks) / TimeSpan.TicksPerSecond; #endif - } - - /// - /// 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 date. - /// 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 date, int? minute = null, int? hour = null, int? dayOfMonth = null, int? month = null, int? dayOfWeek = null) - { - var results = new List + } + + /// + /// 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 date. + /// 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 date, Int32? minute = null, Int32? hour = null, Int32? dayOfMonth = null, Int32? month = null, Int32? dayOfWeek = null) { + List results = new List { GetElementParts(minute, date.Minute), GetElementParts(hour, date.Hour), GetElementParts(dayOfMonth, date.Day), GetElementParts(month, date.Month), - GetElementParts(dayOfWeek, (int) date.DayOfWeek), - }; - - return results.Any(x => x != false); - } - - /// - /// Compare the Date elements(Months, Days, Hours, Minutes). - /// - /// The date. - /// 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 date, string minute = "*", string hour = "*", string dayOfMonth = "*", string month = "*", string dayOfWeek = "*") - { - var results = new List + GetElementParts(dayOfWeek, (Int32) date.DayOfWeek), + }; + + return results.Any(x => x != false); + } + + /// + /// Compare the Date elements(Months, Days, Hours, Minutes). + /// + /// The date. + /// 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 date, String minute = "*", String hour = "*", String dayOfMonth = "*", String month = "*", String dayOfWeek = "*") { + List results = new List { GetElementParts(minute, nameof(minute), date.Minute), GetElementParts(hour, nameof(hour), date.Hour), GetElementParts(dayOfMonth, nameof(dayOfMonth), date.Day), GetElementParts(month, nameof(month), date.Month), - GetElementParts(dayOfWeek, nameof(dayOfWeek), (int) date.DayOfWeek), - }; - - return results.Any(x => x != false); - } - - 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; - } - } + GetElementParts(dayOfWeek, nameof(dayOfWeek), (Int32) date.DayOfWeek), + }; + + return results.Any(x => x != false); + } + + 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; + } + } } \ No newline at end of file diff --git a/Unosquare.Swan.Lite/Extensions.Dictionaries.cs b/Unosquare.Swan.Lite/Extensions.Dictionaries.cs index d1c8415..3a52ff7 100644 --- a/Unosquare.Swan.Lite/Extensions.Dictionaries.cs +++ b/Unosquare.Swan.Lite/Extensions.Dictionaries.cs @@ -1,77 +1,76 @@ -namespace Unosquare.Swan -{ - using System; - using System.Collections.Generic; - +using System; +using System.Collections.Generic; + +namespace Unosquare.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. - public static TValue GetOrAdd(this IDictionary dict, TKey key, Func valueFactory) - { - if (dict == null) - throw new ArgumentNullException(nameof(dict)); - - 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. + public static TValue GetOrAdd(this IDictionary dict, TKey key, Func valueFactory) { + if(dict == null) { + throw new ArgumentNullException(nameof(dict)); + } + + 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/Unosquare.Swan.Lite/Extensions.Functional.cs b/Unosquare.Swan.Lite/Extensions.Functional.cs index ff7c80e..34c671f 100644 --- a/Unosquare.Swan.Lite/Extensions.Functional.cs +++ b/Unosquare.Swan.Lite/Extensions.Functional.cs @@ -1,179 +1,188 @@ -namespace Unosquare.Swan -{ - using System; - using System.Collections.Generic; - using System.Linq; - +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Unosquare.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/Unosquare.Swan.Lite/Extensions.Reflection.cs b/Unosquare.Swan.Lite/Extensions.Reflection.cs index 5c5b138..98f0ed7 100644 --- a/Unosquare.Swan.Lite/Extensions.Reflection.cs +++ b/Unosquare.Swan.Lite/Extensions.Reflection.cs @@ -1,469 +1,447 @@ -namespace Unosquare.Swan -{ - using System; - using System.Collections.Concurrent; - using System.Collections; - using System.Linq; - using System.Reflection; - using System.Collections.Generic; - using Attributes; - +using System; +using System.Collections.Concurrent; +using System.Collections; +using System.Linq; +using System.Reflection; +using System.Collections.Generic; +using Unosquare.Swan.Attributes; + +namespace Unosquare.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) : null; - } - - /// - /// 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 + /// 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) : null; + } + + /// + /// 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 => 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 this instance is class. - /// - /// The type. - /// - /// true if the specified type is class; otherwise, false. - /// - public static bool IsClass(this Type type) => type.GetTypeInfo().IsClass; - - /// - /// Determines whether this instance is abstract. - /// - /// The type. - /// - /// true if the specified type is abstract; otherwise, false. - /// - public static bool IsAbstract(this Type type) => type.GetTypeInfo().IsAbstract; - - /// - /// Determines whether this instance is interface. - /// - /// The type. - /// - /// true if the specified type is interface; otherwise, false. - /// - public static bool IsInterface(this Type type) => type.GetTypeInfo().IsInterface; - - /// - /// Determines whether this instance is primitive. - /// - /// The type. - /// - /// true if the specified type is primitive; otherwise, false. - /// - public static bool IsPrimitive(this Type type) => type.GetTypeInfo().IsPrimitive; - - /// - /// Determines whether [is value type]. - /// - /// The type. - /// - /// true if [is value type] [the specified type]; otherwise, false. - /// - public static bool IsValueType(this Type type) => type.GetTypeInfo().IsValueType; - - /// - /// Determines whether [is generic type]. - /// - /// The type. - /// - /// true if [is generic type] [the specified type]; otherwise, false. - /// - public static bool IsGenericType(this Type type) => type.GetTypeInfo().IsGenericType; - - /// - /// Determines whether [is generic parameter]. - /// - /// The type. - /// - /// true if [is generic parameter] [the specified type]; otherwise, false. - /// - public static bool IsGenericParameter(this Type type) => type.IsGenericParameter; - - /// - /// Determines whether the specified attribute type is defined. - /// - /// The type. - /// Type of the attribute. - /// if set to true [inherit]. - /// - /// true if the specified attribute type is defined; otherwise, false. - /// - public static bool IsDefined(this Type type, Type attributeType, bool inherit) => - type.GetTypeInfo().IsDefined(attributeType, inherit); - - /// - /// Gets the custom attributes. - /// - /// The type. - /// Type of the attribute. - /// if set to true [inherit]. - /// - /// Attributes associated with the property represented by this PropertyInfo object. - /// - public static Attribute[] GetCustomAttributes(this Type type, Type attributeType, bool inherit) => - type.GetTypeInfo().GetCustomAttributes(attributeType, inherit).Cast().ToArray(); - - /// - /// Determines whether [is generic type definition]. - /// - /// The type. - /// - /// true if [is generic type definition] [the specified type]; otherwise, false. - /// - public static bool IsGenericTypeDefinition(this Type type) => type.GetTypeInfo().IsGenericTypeDefinition; - - /// - /// Bases the type. - /// - /// The type. - /// returns a type of data. - public static Type BaseType(this Type type) => type.GetTypeInfo().BaseType; - - /// - /// Assemblies the specified type. - /// - /// The type. - /// returns an Assembly object. - public static Assembly Assembly(this Type type) => type.GetTypeInfo().Assembly; - - /// - /// 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. - /// - public static bool TryParseBasicType(this Type type, object value, out object result) - => TryParseBasicType(type, value.ToStringInvariant(), out result); - - /// - /// Tries to parse using the basic types. - /// - /// The type. - /// The value. - /// The result. - /// - /// true if parsing was successful; otherwise, false. - /// - public static bool TryParseBasicType(this Type type, string value, out object result) - { - result = null; - - return Definitions.BasicTypesInfo.ContainsKey(type) && Definitions.BasicTypesInfo[type].TryParse(value, out result); - } - - /// - /// Tries the type of the set basic value to a property. - /// - /// The property. - /// The value. - /// The object. - /// - /// true if parsing was successful; otherwise, false. - /// - public static bool TrySetBasicType(this PropertyInfo property, object value, object obj) - { - try - { - if (property.PropertyType.TryParseBasicType(value, out var propertyValue)) - { - property.SetValue(obj, 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. - /// - public static bool TrySetArrayBasicType(this Type type, object value, Array array, int index) - { - try - { - if (value == null) - { - array.SetValue(null, index); - return true; - } - - if (type.TryParseBasicType(value, out var propertyValue)) - { - array.SetValue(propertyValue, index); - return true; - } - - if (type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) - { - array.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. - /// - public static bool TrySetArray(this PropertyInfo propertyInfo, IEnumerable value, object obj) - { - var elementType = propertyInfo.PropertyType.GetElementType(); - - if (elementType == 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. - public static string ToFormattedString(this PropertyInfo propertyInfo, object obj) - { - try - { - var value = propertyInfo.GetValue(obj); - var attr = Runtime.AttributeCache.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); - - return !nonPublic && !CacheGetMethods.Value.ContainsKey(key) && !propertyInfo.GetGetMethod(true).IsPublic - ? null - : CacheGetMethods.Value - .GetOrAdd(key, - x => y => x.Item2.GetGetMethod(nonPublic).Invoke(y, null)); - } - - /// - /// 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)); - } - - private static string ConvertObjectAndFormat(Type propertyType, object value, string format) - { - if (propertyType == typeof(DateTime) || propertyType == typeof(DateTime?)) - return Convert.ToDateTime(value).ToString(format); - if (propertyType == typeof(int) || propertyType == typeof(int?)) - return Convert.ToInt32(value).ToString(format); - if (propertyType == typeof(decimal) || propertyType == typeof(decimal?)) - return Convert.ToDecimal(value).ToString(format); - if (propertyType == typeof(double) || propertyType == typeof(double?)) - return Convert.ToDouble(value).ToString(format); - if (propertyType == typeof(byte) || propertyType == typeof(byte?)) - return Convert.ToByte(value).ToString(format); - - return value?.ToString() ?? string.Empty; - } - } + .ToList(); + + return methods.Count > 1 ? throw new AmbiguousMatchException() : methods.FirstOrDefault(); + } + + /// + /// Determines whether this instance is class. + /// + /// The type. + /// + /// true if the specified type is class; otherwise, false. + /// + public static Boolean IsClass(this Type type) => type.GetTypeInfo().IsClass; + + /// + /// Determines whether this instance is abstract. + /// + /// The type. + /// + /// true if the specified type is abstract; otherwise, false. + /// + public static Boolean IsAbstract(this Type type) => type.GetTypeInfo().IsAbstract; + + /// + /// Determines whether this instance is interface. + /// + /// The type. + /// + /// true if the specified type is interface; otherwise, false. + /// + public static Boolean IsInterface(this Type type) => type.GetTypeInfo().IsInterface; + + /// + /// Determines whether this instance is primitive. + /// + /// The type. + /// + /// true if the specified type is primitive; otherwise, false. + /// + public static Boolean IsPrimitive(this Type type) => type.GetTypeInfo().IsPrimitive; + + /// + /// Determines whether [is value type]. + /// + /// The type. + /// + /// true if [is value type] [the specified type]; otherwise, false. + /// + public static Boolean IsValueType(this Type type) => type.GetTypeInfo().IsValueType; + + /// + /// Determines whether [is generic type]. + /// + /// The type. + /// + /// true if [is generic type] [the specified type]; otherwise, false. + /// + public static Boolean IsGenericType(this Type type) => type.GetTypeInfo().IsGenericType; + + /// + /// Determines whether [is generic parameter]. + /// + /// The type. + /// + /// true if [is generic parameter] [the specified type]; otherwise, false. + /// + public static Boolean IsGenericParameter(this Type type) => type.IsGenericParameter; + + /// + /// Determines whether the specified attribute type is defined. + /// + /// The type. + /// Type of the attribute. + /// if set to true [inherit]. + /// + /// true if the specified attribute type is defined; otherwise, false. + /// + public static Boolean IsDefined(this Type type, Type attributeType, Boolean inherit) => + type.GetTypeInfo().IsDefined(attributeType, inherit); + + /// + /// Gets the custom attributes. + /// + /// The type. + /// Type of the attribute. + /// if set to true [inherit]. + /// + /// Attributes associated with the property represented by this PropertyInfo object. + /// + public static Attribute[] GetCustomAttributes(this Type type, Type attributeType, Boolean inherit) => + type.GetTypeInfo().GetCustomAttributes(attributeType, inherit).Cast().ToArray(); + + /// + /// Determines whether [is generic type definition]. + /// + /// The type. + /// + /// true if [is generic type definition] [the specified type]; otherwise, false. + /// + public static Boolean IsGenericTypeDefinition(this Type type) => type.GetTypeInfo().IsGenericTypeDefinition; + + /// + /// Bases the type. + /// + /// The type. + /// returns a type of data. + public static Type BaseType(this Type type) => type.GetTypeInfo().BaseType; + + /// + /// Assemblies the specified type. + /// + /// The type. + /// returns an Assembly object. + public static Assembly Assembly(this Type type) => type.GetTypeInfo().Assembly; + + /// + /// 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. + /// + public static Boolean TryParseBasicType(this Type type, Object value, out Object result) + => TryParseBasicType(type, value.ToStringInvariant(), out result); + + /// + /// Tries to parse using the basic types. + /// + /// The type. + /// The value. + /// The result. + /// + /// true if parsing was successful; otherwise, false. + /// + public static Boolean TryParseBasicType(this Type type, String value, out Object result) { + result = null; + + return Definitions.BasicTypesInfo.ContainsKey(type) && Definitions.BasicTypesInfo[type].TryParse(value, out result); + } + + /// + /// Tries the type of the set basic value to a property. + /// + /// The property. + /// The value. + /// The object. + /// + /// true if parsing was successful; otherwise, false. + /// + public static Boolean TrySetBasicType(this PropertyInfo property, Object value, Object obj) { + try { + if(property.PropertyType.TryParseBasicType(value, out Object propertyValue)) { + property.SetValue(obj, 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. + /// + public static Boolean TrySetArrayBasicType(this Type type, Object value, Array array, Int32 index) { + try { + if(value == null) { + array.SetValue(null, index); + return true; + } + + if(type.TryParseBasicType(value, out Object propertyValue)) { + array.SetValue(propertyValue, index); + return true; + } + + if(type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) { + array.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. + /// + public static Boolean TrySetArray(this PropertyInfo propertyInfo, IEnumerable value, Object obj) { + Type elementType = propertyInfo.PropertyType.GetElementType(); + + if(elementType == 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. + public static String ToFormattedString(this PropertyInfo propertyInfo, Object obj) { + try { + Object value = propertyInfo.GetValue(obj); + PropertyDisplayAttribute attr = Runtime.AttributeCache.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); + + return !nonPublic && !CacheGetMethods.Value.ContainsKey(key) && !propertyInfo.GetGetMethod(true).IsPublic + ? null + : CacheGetMethods.Value + .GetOrAdd(key, + x => y => x.Item2.GetGetMethod(nonPublic).Invoke(y, null)); + } + + /// + /// 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)); + } + + private static String ConvertObjectAndFormat(Type propertyType, Object value, String format) => propertyType == typeof(DateTime) || propertyType == typeof(DateTime?) + ? Convert.ToDateTime(value).ToString(format) + : propertyType == typeof(Int32) || propertyType == typeof(Int32?) + ? Convert.ToInt32(value).ToString(format) + : propertyType == typeof(Decimal) || propertyType == typeof(Decimal?) + ? Convert.ToDecimal(value).ToString(format) + : propertyType == typeof(Double) || propertyType == typeof(Double?) + ? Convert.ToDouble(value).ToString(format) + : propertyType == typeof(Byte) || propertyType == typeof(Byte?) + ? Convert.ToByte(value).ToString(format) + : value?.ToString() ?? String.Empty; + } } \ No newline at end of file diff --git a/Unosquare.Swan.Lite/Extensions.Strings.cs b/Unosquare.Swan.Lite/Extensions.Strings.cs index db5b1d4..04efe37 100644 --- a/Unosquare.Swan.Lite/Extensions.Strings.cs +++ b/Unosquare.Swan.Lite/Extensions.Strings.cs @@ -1,96 +1,91 @@ -namespace Unosquare.Swan -{ - using Formatters; - using System; - using System.IO; - using System.Linq; - using System.Security.Cryptography; - using System.Text; - using System.Text.RegularExpressions; - +using Unosquare.Swan.Formatters; +using System; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; + +namespace Unosquare.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 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); + + 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.Substring(1, x.Length - 1); + }); + + private static readonly Lazy InvalidFilenameChars = + new Lazy(() => Path.GetInvalidFileNameChars().Select(c => c.ToString()).ToArray()); + + #endregion + /// - /// String related extension methods. + /// Computes the MD5 hash of the given stream. + /// Do not use for large streams as this reads ALL bytes at once. /// - 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 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); - - 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(() => - { - return 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 - - /// - /// 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. - public static byte[] ComputeMD5(this Stream stream, bool createHasher = false) - { - if (stream == null) - throw new ArgumentNullException(nameof(stream)); - + /// The stream. + /// if set to true [create hasher]. + /// + /// The computed hash code. + /// + /// stream. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Codequalität", "IDE0067:Objekte verwerfen, bevor Bereich verloren geht", Justification = "")] + public static Byte[] ComputeMD5(this Stream stream, Boolean createHasher = false) { + if(stream == null) { + throw new ArgumentNullException(nameof(stream)); + } + #if NET452 - var md5 = MD5.Create(); - const int bufferSize = 4096; - - var readAheadBuffer = new byte[bufferSize]; - var readAheadBytesRead = stream.Read(readAheadBuffer, 0, readAheadBuffer.Length); - - do - { - var bytesRead = readAheadBytesRead; - var buffer = readAheadBuffer; - - readAheadBuffer = new byte[bufferSize]; - readAheadBytesRead = stream.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; + MD5 md5 = MD5.Create(); + const Int32 bufferSize = 4096; + + Byte[] readAheadBuffer = new Byte[bufferSize]; + Int32 readAheadBytesRead = stream.Read(readAheadBuffer, 0, readAheadBuffer.Length); + + do { + Int32 bytesRead = readAheadBytesRead; + Byte[] buffer = readAheadBuffer; + + readAheadBuffer = new Byte[bufferSize]; + readAheadBytesRead = stream.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; #else using (var ms = new MemoryStream()) { @@ -100,421 +95,407 @@ return (createHasher ? MD5.Create() : Md5Hasher.Value).ComputeHash(ms.ToArray()); } #endif - } - - /// - /// 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. - public static byte[] ComputeMD5(this string value, bool createHasher = false) => - Encoding.UTF8.GetBytes(value).ComputeMD5(createHasher); - - /// - /// Computes the MD5 hash of the given byte array. - /// - /// The data. - /// if set to true [create hasher]. - /// The computed hash code. - public static byte[] ComputeMD5(this 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. - /// - public static byte[] ComputeSha1(this string value, bool createHasher = false) - { - var inputBytes = Encoding.UTF8.GetBytes(value); - 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(this 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(this string value, bool createHasher = false) - { - var inputBytes = Encoding.UTF8.GetBytes(value); - return (createHasher ? SHA512.Create() : SHA512Hasher.Value).ComputeHash(inputBytes); - } - - /// - /// 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 obj) - { - if (obj == null) - return string.Empty; - - var itemType = obj.GetType(); - - if (itemType == typeof(string)) - return obj as string; - - return Definitions.BasicTypesInfo.ContainsKey(itemType) + } + + /// + /// 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. + public static Byte[] ComputeMD5(this String value, Boolean createHasher = false) => + Encoding.UTF8.GetBytes(value).ComputeMD5(createHasher); + + + /// + /// Computes the MD5 hash of the given byte array. + /// + /// The data. + /// if set to true [create hasher]. + /// The computed hash code. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Codequalität", "IDE0067:Objekte verwerfen, bevor Bereich verloren geht", Justification = "")] + public static Byte[] ComputeMD5(this 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. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Codequalität", "IDE0067:Objekte verwerfen, bevor Bereich verloren geht", Justification = "")] + public static Byte[] ComputeSha1(this String value, Boolean createHasher = false) { + Byte[] inputBytes = Encoding.UTF8.GetBytes(value); + 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(this 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(this String value, Boolean createHasher = false) { + Byte[] inputBytes = Encoding.UTF8.GetBytes(value); + return (createHasher ? SHA512.Create() : SHA512Hasher.Value).ComputeHash(inputBytes); + } + + /// + /// 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 obj) { + if(obj == null) { + return String.Empty; + } + + Type itemType = obj.GetType(); + + return itemType == typeof(String) + ? obj as String + : Definitions.BasicTypesInfo.ContainsKey(itemType) ? Definitions.BasicTypesInfo[itemType].ToStringInvariant(obj) - : obj.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) - { - if (typeof(string) == typeof(T)) - return Equals(item, default(T)) ? string.Empty : item as string; - - return 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 = new char[] { }; - - 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 obj, bool format = true) => - obj == null ? string.Empty : Json.Serialize(obj, 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 obj) - { - if (obj == null) - return "(null)"; - - try - { - var jsonText = Json.Serialize(obj, false, "$type"); - var jsonData = Json.Deserialize(jsonText); - - return new HumanizeJson(jsonData, 0).GetResult(); - } - catch - { - return obj.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 value, int startIndex, int endIndex) - { - if (value == null) - return string.Empty; - - var end = endIndex.Clamp(startIndex, value.Length - 1); - - return startIndex >= end ? string.Empty : value.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 str, int startIndex, int length) - { - if (str == null) - return string.Empty; - - var start = startIndex.Clamp(0, str.Length - 1); - var len = length.Clamp(0, str.Length - start); - - return len == 0 ? string.Empty : str.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 value) => - value == null ? new string[] { } : SplitLinesRegex.Value.Split(value); - - /// - /// 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 that represents the current object. - 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; - } - - /// - /// 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) - { - return 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) - { - return value >= '0' && value <= '9' - ? value - '0' - : value >= 'A' && value <= 'F' - ? value - 'A' + 10 - : value >= 'a' && value <= 'f' - ? value - 'a' + 10 - : -1; - } - } + : obj.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) ? Equals(item, default(T)) ? String.Empty : item as String : 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 = new Char[] { }; + } + + 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 obj, Boolean format = true) => + obj == null ? String.Empty : Json.Serialize(obj, 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 obj) { + if(obj == null) { + return "(null)"; + } + + try { + String jsonText = Json.Serialize(obj, false, "$type"); + Object jsonData = Json.Deserialize(jsonText); + + return new HumanizeJson(jsonData, 0).GetResult(); + } catch { + return obj.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 value, Int32 startIndex, Int32 endIndex) { + if(value == null) { + return String.Empty; + } + + Int32 end = endIndex.Clamp(startIndex, value.Length - 1); + + return startIndex >= end ? String.Empty : value.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 str, Int32 startIndex, Int32 length) { + if(str == null) { + return String.Empty; + } + + Int32 start = startIndex.Clamp(0, str.Length - 1); + Int32 len = length.Clamp(0, str.Length - start); + + return len == 0 ? String.Empty : str.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 value) => + value == null ? new String[] { } : SplitLinesRegex.Value.Split(value); + + /// + /// 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 that represents the current object. + 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; + } + + /// + /// 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) && 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; + } } \ No newline at end of file diff --git a/Unosquare.Swan.Lite/Extensions.ValueTypes.cs b/Unosquare.Swan.Lite/Extensions.ValueTypes.cs index 6454eac..c85f73a 100644 --- a/Unosquare.Swan.Lite/Extensions.ValueTypes.cs +++ b/Unosquare.Swan.Lite/Extensions.ValueTypes.cs @@ -1,168 +1,147 @@ -namespace Unosquare.Swan -{ - using System; - using System.Reflection; - using System.Runtime.InteropServices; - using Attributes; - +using System; +using System.Reflection; +using System.Runtime.InteropServices; +using Unosquare.Swan.Attributes; + +namespace Unosquare.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 value, T min, T max) - where T : struct, IComparable - { - if (value.CompareTo(min) < 0) return min; - - return value.CompareTo(max) > 0 ? max : value; - } - - /// - /// 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 value, int min, int max) - => value < min ? min : (value > max ? max : value); - - /// - /// 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 value, T min, T max) - where T : struct, IComparable - { - return value.CompareTo(min) >= 0 && value.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[] data) - where T : struct - { - return ToStruct(data, 0, data.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[] data, int offset, int length) - where T : struct - { - if (data == null) - throw new ArgumentNullException(nameof(data)); - - var buffer = new byte[length]; - Array.Copy(data, 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 obj) - where T : struct - { - var data = new byte[Marshal.SizeOf(obj)]; - var handle = GCHandle.Alloc(data, GCHandleType.Pinned); - - try - { - Marshal.StructureToPtr(obj, 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 longBytes) - => (uint) (((longBytes & 0x000000ff) << 24) + - ((longBytes & 0x0000ff00) << 8) + - ((longBytes & 0x00ff0000) >> 8) + - ((longBytes & 0xff000000) >> 24)); - - private static byte[] GetStructBytes(byte[] data) - { - if (data == null) - throw new ArgumentNullException(nameof(data)); - -#if !NETSTANDARD1_3 - var fields = typeof(T).GetTypeInfo() - .GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + /// 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 value, T min, T max) + where T : struct, IComparable => value.CompareTo(min) < 0 ? min : value.CompareTo(max) > 0 ? max : value; + + /// + /// 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 value, Int32 min, Int32 max) + => value < min ? min : (value > max ? max : value); + + /// + /// 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 value, T min, T max) + where T : struct, IComparable => value.CompareTo(min) >= 0 && value.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[] data) + where T : struct => ToStruct(data, 0, data.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[] data, Int32 offset, Int32 length) + where T : struct { + if(data == null) { + throw new ArgumentNullException(nameof(data)); + } + + Byte[] buffer = new Byte[length]; + Array.Copy(data, 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 obj) + where T : struct { + Byte[] data = new Byte[Marshal.SizeOf(obj)]; + GCHandle handle = GCHandle.Alloc(data, GCHandleType.Pinned); + + try { + Marshal.StructureToPtr(obj, 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 longBytes) + => (UInt32)(((longBytes & 0x000000ff) << 24) + + ((longBytes & 0x0000ff00) << 8) + + ((longBytes & 0x00ff0000) >> 8) + + ((longBytes & 0xff000000) >> 24)); + + private static Byte[] GetStructBytes(Byte[] data) { + if(data == null) { + throw new ArgumentNullException(nameof(data)); + } + +#if !NETSTANDARD1_3 + FieldInfo[] fields = typeof(T).GetTypeInfo() + .GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); #else var fields = typeof(T).GetTypeInfo().DeclaredFields; #endif - var endian = Runtime.AttributeCache.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 ?? Runtime.AttributeCache.RetrieveOne(field); - - if (endian != null && (endian.Endianness == Endianness.Big && BitConverter.IsLittleEndian || - endian.Endianness == Endianness.Little && !BitConverter.IsLittleEndian)) - { - Array.Reverse(data, offset, length); - } - } - - return data; - } - } + StructEndiannessAttribute endian = Runtime.AttributeCache.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 = endian ?? Runtime.AttributeCache.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/Unosquare.Swan.Lite/Extensions.cs b/Unosquare.Swan.Lite/Extensions.cs index 01e8ab9..c6fd88c 100644 --- a/Unosquare.Swan.Lite/Extensions.cs +++ b/Unosquare.Swan.Lite/Extensions.cs @@ -1,331 +1,305 @@ -namespace Unosquare.Swan -{ - using Attributes; - using System; - using System.Collections; - using System.Collections.Generic; - using System.Diagnostics; - using System.Linq; - using System.Reflection; - using System.Threading.Tasks; - +using Unosquare.Swan.Attributes; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +namespace Unosquare.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. - /// Number of properties that was copied successful. - public static int CopyPropertiesTo(this T source, object target) - where T : class - { - var copyable = GetCopyableProperties(target); - return copyable.Any() - ? CopyOnlyPropertiesTo(source, target, copyable.ToArray()) - : CopyPropertiesTo(source, target, null); - } - - /// - /// 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. - /// The ignore properties. - /// - /// Number of properties that were successfully copied. - /// - public static int CopyPropertiesTo(this object source, object target, string[] ignoreProperties = null) - => Components.ObjectMapper.Copy(source, target, null, 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 type of the source. - /// The source. - /// The target. - /// Number of properties that was copied successful. - public static int CopyOnlyPropertiesTo(this T source, object target) - where T : class - { - return CopyOnlyPropertiesTo(source, target, null); - } - - /// - /// 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, string[] propertiesToCopy) - { - return Components.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 DeepClone(this T source, string[] ignoreProperties = null) - where T : class - { - return source.CopyPropertiesToNew(ignoreProperties); - } - - /// - /// 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(); - var copyable = target.GetCopyableProperties(); - - if (copyable.Any()) - source.CopyOnlyPropertiesTo(target, copyable.ToArray()); - else - source.CopyPropertiesTo(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, string[] propertiesToCopy) - where T : class - { - if (source == null) - throw new ArgumentNullException(nameof(source)); - - var target = Activator.CreateInstance(); - source.CopyOnlyPropertiesTo(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, - string[] ignoreKeys = null) - { - return Components.ObjectMapper.Copy(source, target, null, ignoreKeys); - } - - /// - /// Measures the elapsed time of the given action as a TimeSpan - /// This method uses a high precision Stopwatch. - /// - /// The target. - /// - /// A time interval that represents a specified time, where the specification is in units of ticks. - /// - /// target. - public static TimeSpan Benchmark(this Action target) - { - if (target == null) - throw new ArgumentNullException(nameof(target)); - - var sw = new Stopwatch(); - - try - { - sw.Start(); - target.Invoke(); - } - catch - { - // swallow - } - finally - { - sw.Stop(); - } - - return TimeSpan.FromTicks(sw.ElapsedTicks); - } - - /// - /// 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); - } - - /// - /// Retrieves the exception message, plus all the inner exception messages separated by new lines. - /// - /// The ex. - /// The prior message. - /// A that represents this instance. - public static string ExceptionMessage(this Exception ex, string priorMessage = "") - { - while (true) - { - if (ex == null) - throw new ArgumentNullException(nameof(ex)); - - var fullMessage = string.IsNullOrWhiteSpace(priorMessage) + /// The type of the source. + /// The source. + /// The target. + /// Number of properties that was copied successful. + public static Int32 CopyPropertiesTo(this T source, Object target) + where T : class { + IEnumerable copyable = GetCopyableProperties(target); + return copyable.Any() + ? CopyOnlyPropertiesTo(source, target, copyable.ToArray()) + : CopyPropertiesTo(source, target, null); + } + + /// + /// 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. + /// The ignore properties. + /// + /// Number of properties that were successfully copied. + /// + public static Int32 CopyPropertiesTo(this Object source, Object target, String[] ignoreProperties = null) + => Components.ObjectMapper.Copy(source, target, null, 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 type of the source. + /// The source. + /// The target. + /// Number of properties that was copied successful. + public static Int32 CopyOnlyPropertiesTo(this T source, Object target) + where T : class => CopyOnlyPropertiesTo(source, target, null); + + /// + /// 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, String[] propertiesToCopy) => Components.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 DeepClone(this T source, String[] ignoreProperties = null) + where T : class => source.CopyPropertiesToNew(ignoreProperties); + + /// + /// 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(); + IEnumerable copyable = target.GetCopyableProperties(); + + _ = copyable.Any() ? source.CopyOnlyPropertiesTo(target, copyable.ToArray()) : source.CopyPropertiesTo(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, String[] propertiesToCopy) + where T : class { + if(source == null) { + throw new ArgumentNullException(nameof(source)); + } + + T target = Activator.CreateInstance(); + _ = source.CopyOnlyPropertiesTo(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, + String[] ignoreKeys = null) => Components.ObjectMapper.Copy(source, target, null, ignoreKeys); + + /// + /// Measures the elapsed time of the given action as a TimeSpan + /// This method uses a high precision Stopwatch. + /// + /// The target. + /// + /// A time interval that represents a specified time, where the specification is in units of ticks. + /// + /// target. + public static TimeSpan Benchmark(this Action target) { + if(target == null) { + throw new ArgumentNullException(nameof(target)); + } + + Stopwatch sw = new Stopwatch(); + + try { + sw.Start(); + target.Invoke(); + } catch { + // swallow + } finally { + sw.Stop(); + } + + return TimeSpan.FromTicks(sw.ElapsedTicks); + } + + /// + /// 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); + } + + 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); + } + + /// + /// Retrieves the exception message, plus all the inner exception messages separated by new lines. + /// + /// The ex. + /// The prior message. + /// A that represents this instance. + public static String ExceptionMessage(this Exception ex, String priorMessage = "") { + while(true) { + if(ex == null) { + throw new ArgumentNullException(nameof(ex)); + } + + String fullMessage = String.IsNullOrWhiteSpace(priorMessage) ? ex.Message - : priorMessage + "\r\n" + ex.Message; - - if (string.IsNullOrWhiteSpace(ex.InnerException?.Message)) - return fullMessage; - - ex = ex.InnerException; - priorMessage = fullMessage; - } - } - - /// - /// Gets the copyable properties. - /// - /// The object. - /// - /// Array of properties. - /// - /// model. - public static IEnumerable GetCopyableProperties(this object obj) - { - if (obj == null) - throw new ArgumentNullException(nameof(obj)); - - return Runtime.PropertyTypeCache - .RetrieveAllProperties(obj.GetType(), true) - .Select(x => new { x.Name, HasAttribute = Runtime.AttributeCache.RetrieveOne(x) != null}) - .Where(x => x.HasAttribute) - .Select(x => x.Name); - } - - /// - /// Returns true if the object is valid. - /// - /// The object. - /// - /// true if the specified model is valid; otherwise, false. - /// - public static bool IsValid(this object obj) => Runtime.ObjectValidator.IsValid(obj); - - internal static void CreateTarget( - this object source, - Type targetType, - bool includeNonPublic, - ref object target) - { - switch (source) - { - case string _: - break; // do nothing. Simply skip creation - case IList sourceObjectList when targetType.IsArray: // When using arrays, there is no default constructor, attempt to build a compatible array - var elementType = targetType.GetElementType(); - - if (elementType != null) - target = Array.CreateInstance(elementType, sourceObjectList.Count); - break; - default: - target = Activator.CreateInstance(targetType, includeNonPublic); - break; - } - } - } + : priorMessage + "\r\n" + ex.Message; + + if(String.IsNullOrWhiteSpace(ex.InnerException?.Message)) { + return fullMessage; + } + + ex = ex.InnerException; + priorMessage = fullMessage; + } + } + + /// + /// Gets the copyable properties. + /// + /// The object. + /// + /// Array of properties. + /// + /// model. + public static IEnumerable GetCopyableProperties(this Object obj) { + if(obj == null) { + throw new ArgumentNullException(nameof(obj)); + } + + return Runtime.PropertyTypeCache + .RetrieveAllProperties(obj.GetType(), true) + .Select(x => new { x.Name, HasAttribute = Runtime.AttributeCache.RetrieveOne(x) != null }) + .Where(x => x.HasAttribute) + .Select(x => x.Name); + } + + /// + /// Returns true if the object is valid. + /// + /// The object. + /// + /// true if the specified model is valid; otherwise, false. + /// + public static Boolean IsValid(this Object obj) => Runtime.ObjectValidator.IsValid(obj); + + internal static void CreateTarget( + this Object source, + Type targetType, + Boolean includeNonPublic, + ref Object target) { + switch(source) { + case String _: + break; // do nothing. Simply skip creation + case IList sourceObjectList when targetType.IsArray: // When using arrays, there is no default constructor, attempt to build a compatible array + Type elementType = targetType.GetElementType(); + + if(elementType != null) { + target = Array.CreateInstance(elementType, sourceObjectList.Count); + } + + break; + default: + target = Activator.CreateInstance(targetType, includeNonPublic); + break; + } + } + } } \ No newline at end of file diff --git a/Unosquare.Swan.Lite/Formatters/CsvReader.cs b/Unosquare.Swan.Lite/Formatters/CsvReader.cs index 210d8d9..60ccbea 100644 --- a/Unosquare.Swan.Lite/Formatters/CsvReader.cs +++ b/Unosquare.Swan.Lite/Formatters/CsvReader.cs @@ -1,650 +1,600 @@ -namespace Unosquare.Swan.Formatters -{ - using Reflection; - using System; - using System.Collections.Generic; - using System.IO; - using System.Linq; - using System.Reflection; - using System.Text; - +using Unosquare.Swan.Reflection; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace Unosquare.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 Unosquare.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 Unosquare.Swan.Formatters; + /// using System.Text; + /// using Unosquare.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 Unosquare.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 Unosquare.Swan.Formatters; - /// using System.Text; - /// using Unosquare.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.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.Equals(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; - } - - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - public void Dispose() - { - Dispose(true); - } - - #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.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.Equals(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; + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() => this.Dispose(true); + + #endregion + + /// + /// Defines the 3 different read states + /// for the parsing state machine. + /// + private enum ReadState { + WaitingForNewField, + PushingNormal, + PushingQuoted, + } + } } diff --git a/Unosquare.Swan.Lite/Formatters/CsvWriter.cs b/Unosquare.Swan.Lite/Formatters/CsvWriter.cs index 51de982..105920c 100644 --- a/Unosquare.Swan.Lite/Formatters/CsvWriter.cs +++ b/Unosquare.Swan.Lite/Formatters/CsvWriter.cs @@ -1,415 +1,401 @@ -namespace Unosquare.Swan.Formatters -{ - using Reflection; - using System; - using System.Collections; - using System.Collections.Generic; - using System.IO; - using System.Linq; - using System.Reflection; - using System.Text; - +using Unosquare.Swan.Reflection; +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace Unosquare.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 Unosquare.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 Unosquare.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)); - } - + /// 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)); + } + #if NET452 - /// - /// Writes the headings. - /// - /// The object to extract headings. - /// item - /// Unable to cast dynamic object to a suitable dictionary - item - public void WriteHeadings(dynamic item) - { - if (item == null) - throw new ArgumentNullException(nameof(item)); - - if (!(item is IDictionary dictionary)) - throw new ArgumentException("Unable to cast dynamic object to a suitable dictionary", nameof(item)); - - WriteHeadings(dictionary); - } + /// + /// Writes the headings. + /// + /// The object to extract headings. + /// item + /// Unable to cast dynamic object to a suitable dictionary - item + public void WriteHeadings(dynamic item) { + if(item == null) { + throw new ArgumentNullException(nameof(item)); + } + + if(!(item is IDictionary dictionary)) { + throw new ArgumentException("Unable to cast dynamic object to a suitable dictionary", nameof(item)); + } + + this.WriteHeadings(dictionary); + } #else /// /// Writes the headings. @@ -424,55 +410,54 @@ WriteHeadings(obj.GetType()); } #endif - - #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 - - } + + #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/Unosquare.Swan.Lite/Formatters/HumanizeJson.cs b/Unosquare.Swan.Lite/Formatters/HumanizeJson.cs index 4806deb..d20c750 100644 --- a/Unosquare.Swan.Lite/Formatters/HumanizeJson.cs +++ b/Unosquare.Swan.Lite/Formatters/HumanizeJson.cs @@ -1,150 +1,134 @@ -namespace Unosquare.Swan.Formatters -{ - using System.Collections.Generic; - using System.Linq; - using System.Text; - - 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}"); - } - } - } +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Unosquare.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/Unosquare.Swan.Lite/Formatters/Json.Converter.cs b/Unosquare.Swan.Lite/Formatters/Json.Converter.cs index a1aa96e..e323082 100644 --- a/Unosquare.Swan.Lite/Formatters/Json.Converter.cs +++ b/Unosquare.Swan.Lite/Formatters/Json.Converter.cs @@ -1,335 +1,302 @@ -namespace Unosquare.Swan.Formatters -{ - using System; - using System.Collections; - using System.Collections.Concurrent; - using System.Collections.Generic; - using System.Linq; - using System.Reflection; - using System.Text; - using Attributes; - - /// - /// 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 Converter( - object source, - Type targetType, - ref object targetInstance, - bool includeNonPublic) - { - _targetType = targetInstance != null ? targetInstance.GetType() : targetType; - _includeNonPublic = includeNonPublic; - - 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); - } - - /// - /// Converts a json deserialized object (simple type, dictionary or list) to a new instance of the specified target type. - /// - /// The source. - /// Type of the target. - /// if set to true [include non public]. - /// The target object. - internal static object FromJsonResult(object source, - Type targetType, - bool includeNonPublic) - { - object nullRef = null; - return new Converter(source, targetType, ref nullRef, includeNonPublic).GetResult(); - } - - private static object FromJsonResult(object source, - Type targetType, - ref object targetInstance, - bool includeNonPublic) - { - return new Converter(source, targetType, ref targetInstance, includeNonPublic).GetResult(); - } - - private static Type GetAddMethodParameterType(Type targetType) - => ListAddMethodCache.GetOrAdd(targetType, - x => x.GetMethods() - .FirstOrDefault( - m => m.Name.Equals(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 - { - target = Encoding.UTF8.GetBytes(sourceString); - } // Get the string bytes in UTF8 - } - - private static object GetSourcePropertyValue(IDictionary sourceProperties, - MemberInfo targetProperty) - { - var targetPropertyName = MemberInfoNameCache.GetOrAdd( - targetProperty, - x => Runtime.AttributeCache.RetrieveOne(x)?.PropertyName ?? x.Name); - - 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(IList objects, IList list) - { - var parameterType = GetAddMethodParameterType(_targetType); - if (parameterType == null) return; - - foreach (var item in objects) - { - try - { - list.Add(FromJsonResult( - item, - 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], - 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.GetTypeInfo().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.Equals(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, - 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.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.RetrieveAllFields(_targetType)) - { - var sourcePropertyValue = GetSourcePropertyValue(sourceProperties, field); - if (sourcePropertyValue == null) continue; - - var targetPropertyValue = FromJsonResult( +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using Unosquare.Swan.Attributes; + +namespace Unosquare.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 Converter( + Object source, + Type targetType, + ref Object targetInstance, + Boolean includeNonPublic) { + this._targetType = targetInstance != null ? targetInstance.GetType() : targetType; + this._includeNonPublic = includeNonPublic; + + 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); + } + + /// + /// Converts a json deserialized object (simple type, dictionary or list) to a new instance of the specified target type. + /// + /// The source. + /// Type of the target. + /// if set to true [include non public]. + /// The target object. + internal static Object FromJsonResult(Object source, + Type targetType, + Boolean includeNonPublic) { + Object nullRef = null; + return new Converter(source, targetType, ref nullRef, includeNonPublic).GetResult(); + } + + private static Object FromJsonResult(Object source, + Type targetType, + ref Object targetInstance, + Boolean includeNonPublic) => new Converter(source, targetType, ref targetInstance, includeNonPublic).GetResult(); + + private static Type GetAddMethodParameterType(Type targetType) + => ListAddMethodCache.GetOrAdd(targetType, + x => x.GetMethods() + .FirstOrDefault( + m => m.Name.Equals(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 { + target = Encoding.UTF8.GetBytes(sourceString); + } // Get the string bytes in UTF8 + } + + private static Object GetSourcePropertyValue(IDictionary sourceProperties, + MemberInfo targetProperty) { + String targetPropertyName = MemberInfoNameCache.GetOrAdd( + targetProperty, + x => Runtime.AttributeCache.RetrieveOne(x)?.PropertyName ?? x.Name); + + 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(IList objects, IList list) { + Type parameterType = GetAddMethodParameterType(this._targetType); + if(parameterType == null) { + return; + } + + foreach(Object item in objects) { + try { + _ = list.Add(FromJsonResult( + item, + 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], + 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.GetTypeInfo().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.Equals(AddMethodName) && m.IsPublic && m.GetParameters().Length == 2); + + // skip if we don't have a compatible add method + if(addMethod == null) { + return; + } + + 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, + 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) { + IEnumerable properties = PropertyTypeCache.RetrieveFilteredProperties(this._targetType, false, p => p.CanWrite); + + foreach(PropertyInfo property in properties) { + Object sourcePropertyValue = 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.RetrieveAllFields(this._targetType)) { + Object sourcePropertyValue = GetSourcePropertyValue(sourceProperties, field); + if(sourcePropertyValue == null) { + continue; + } + + Object targetPropertyValue = FromJsonResult( sourcePropertyValue, field.FieldType, - _includeNonPublic); - - try - { - field.SetValue(_target, targetPropertyValue); - } - catch - { - // ignored - } - } - } - } - } + this._includeNonPublic); + + try { + field.SetValue(this._target, targetPropertyValue); + } catch { + // ignored + } + } + } + } + } } \ No newline at end of file diff --git a/Unosquare.Swan.Lite/Formatters/Json.Deserializer.cs b/Unosquare.Swan.Lite/Formatters/Json.Deserializer.cs index 0f724ca..3bcf6cc 100644 --- a/Unosquare.Swan.Lite/Formatters/Json.Deserializer.cs +++ b/Unosquare.Swan.Lite/Formatters/Json.Deserializer.cs @@ -1,374 +1,366 @@ -namespace Unosquare.Swan.Formatters -{ - using System; - using System.Collections.Generic; - using System.Text; - +using System; +using System.Collections.Generic; +using System.Text; + +namespace Unosquare.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 Dictionary _resultObject; - private readonly List _resultArray; - - private readonly ReadState _state = ReadState.WaitingForRootOpen; - private readonly string _currentFieldName; - private readonly string _json; - - private int _index; - - #endregion - - private Deserializer(string json, int startIndex) - { - _json = json; - - for (_index = startIndex; _index < _json.Length; _index++) - { - #region Wait for { or [ - - if (_state == ReadState.WaitingForRootOpen) - { - if (char.IsWhiteSpace(_json, _index)) continue; - - if (_json[_index] == OpenObjectChar) - { - _resultObject = new Dictionary(); - _state = ReadState.WaitingForField; - continue; - } - - if (_json[_index] == OpenArrayChar) - { - _resultArray = new List(); - _state = ReadState.WaitingForValue; - continue; - } - - throw CreateParserException($"'{OpenObjectChar}' or '{OpenArrayChar}'"); - } - - #endregion - - #region Wait for opening field " (only applies for object results) - - if (_state == ReadState.WaitingForField) - { - if (char.IsWhiteSpace(_json, _index)) continue; - - // Handle empty arrays and empty objects - if ((_resultObject != null && _json[_index] == CloseObjectChar) - || (_resultArray != null && _json[_index] == CloseArrayChar)) - { - _result = _resultObject ?? _resultArray as object; - return; - } - - if (_json[_index] != StringQuotedChar) - throw CreateParserException($"'{StringQuotedChar}'"); - - var charCount = GetFieldNameCount(); - - _currentFieldName = Unescape(_json.SliceLength(_index + 1, charCount)); - _index += charCount + 1; - _state = ReadState.WaitingForColon; - continue; - } - - #endregion - - #region Wait for field-value separator : (only applies for object results - - if (_state == ReadState.WaitingForColon) - { - if (char.IsWhiteSpace(_json, _index)) continue; - - if (_json[_index] != ValueSeparatorChar) - throw CreateParserException($"'{ValueSeparatorChar}'"); - - _state = ReadState.WaitingForValue; - continue; - } - - #endregion - - #region Wait for and Parse the value - - if (_state == ReadState.WaitingForValue) - { - if (char.IsWhiteSpace(_json, _index)) continue; - - // Handle empty arrays and empty objects - if ((_resultObject != null && _json[_index] == CloseObjectChar) - || (_resultArray != null && _json[_index] == CloseArrayChar)) - { - _result = _resultObject ?? _resultArray as object; - return; - } - - // 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; - continue; - } - - #endregion - - #region Wait for closing ], } or an additional field or value , - - if (_state != ReadState.WaitingForNextOrRootClose) continue; - - if (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)) - { - _result = _resultObject ?? _resultArray as object; - return; - } - - throw CreateParserException($"'{FieldSeparatorChar}' '{CloseObjectChar}' or '{CloseArrayChar}'"); - - #endregion - } - } - - internal static object DeserializeInternal(string json) => new Deserializer(json, 0)._result; - - 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, 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).Equals(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 Dictionary _resultObject; + private readonly List _resultArray; + + private readonly ReadState _state = ReadState.WaitingForRootOpen; + private readonly String _currentFieldName; + private readonly String _json; + + private Int32 _index; + + #endregion + + private Deserializer(String json, Int32 startIndex) { + this._json = json; + + for(this._index = startIndex; this._index < this._json.Length; this._index++) { + #region Wait for { or [ + + if(this._state == ReadState.WaitingForRootOpen) { + if(Char.IsWhiteSpace(this._json, this._index)) { + continue; + } + + if(this._json[this._index] == OpenObjectChar) { + this._resultObject = new Dictionary(); + this._state = ReadState.WaitingForField; + continue; + } + + if(this._json[this._index] == OpenArrayChar) { + this._resultArray = new List(); + this._state = ReadState.WaitingForValue; + continue; + } + + throw this.CreateParserException($"'{OpenObjectChar}' or '{OpenArrayChar}'"); + } + + #endregion + + #region Wait for opening field " (only applies for object results) + + if(this._state == ReadState.WaitingForField) { + if(Char.IsWhiteSpace(this._json, this._index)) { + continue; + } + + // Handle empty arrays and empty objects + if(this._resultObject != null && this._json[this._index] == CloseObjectChar + || this._resultArray != null && this._json[this._index] == CloseArrayChar) { + this._result = this._resultObject ?? this._resultArray as Object; + return; + } + + if(this._json[this._index] != StringQuotedChar) { + throw this.CreateParserException($"'{StringQuotedChar}'"); + } + + Int32 charCount = this.GetFieldNameCount(); + + this._currentFieldName = Unescape(this._json.SliceLength(this._index + 1, charCount)); + this._index += charCount + 1; + this._state = ReadState.WaitingForColon; + continue; + } + + #endregion + + #region Wait for field-value separator : (only applies for object results + + if(this._state == ReadState.WaitingForColon) { + if(Char.IsWhiteSpace(this._json, this._index)) { + continue; + } + + if(this._json[this._index] != ValueSeparatorChar) { + throw this.CreateParserException($"'{ValueSeparatorChar}'"); + } + + this._state = ReadState.WaitingForValue; + continue; + } + + #endregion + + #region Wait for and Parse the value + + if(this._state == ReadState.WaitingForValue) { + if(Char.IsWhiteSpace(this._json, this._index)) { + continue; + } + + // Handle empty arrays and empty objects + if(this._resultObject != null && this._json[this._index] == CloseObjectChar + || this._resultArray != null && this._json[this._index] == CloseArrayChar) { + this._result = this._resultObject ?? this._resultArray as Object; + return; + } + + // 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; + continue; + } + + #endregion + + #region Wait for closing ], } or an additional field or value , + + if(this._state != ReadState.WaitingForNextOrRootClose) { + continue; + } + + if(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) { + this._result = this._resultObject ?? this._resultArray as Object; + return; + } + + throw this.CreateParserException($"'{FieldSeparatorChar}' '{CloseObjectChar}' or '{CloseArrayChar}'"); + + #endregion + } + } + + internal static Object DeserializeInternal(String json) => new Deserializer(json, 0)._result; + + 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, 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).Equals(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, + } + } + } } \ No newline at end of file diff --git a/Unosquare.Swan.Lite/Formatters/Json.Serializer.cs b/Unosquare.Swan.Lite/Formatters/Json.Serializer.cs index 66c7867..14eafd5 100644 --- a/Unosquare.Swan.Lite/Formatters/Json.Serializer.cs +++ b/Unosquare.Swan.Lite/Formatters/Json.Serializer.cs @@ -1,359 +1,347 @@ -namespace Unosquare.Swan.Formatters -{ - using System; - using System.Collections; - using System.Collections.Generic; - using System.Linq; - using System.Reflection; - using System.Text; - +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace Unosquare.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) == false) - return; - - _options = options; - _lastCommaSearch = FieldSeparatorChar + (_options.Format ? Environment.NewLine : string.Empty); - - // 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. - _builder = new StringBuilder(); - - switch (obj) - { - case IDictionary itemsZero when itemsZero.Count == 0: - _result = EmptyObjectLiteral; - break; - case IDictionary items: - _result = ResolveDictionary(items, depth); - break; - case IEnumerable enumerableZero when !enumerableZero.Cast().Any(): - _result = EmptyArrayLiteral; - break; - case IEnumerable enumerableBytes when enumerableBytes is byte[] bytes: - _result = Serialize(bytes.ToBase64(), depth, _options); - break; - case IEnumerable enumerable: - _result = ResolveEnumerable(enumerable, depth); - break; - default: - _result = ResolveObject(obj, depth); - break; - } - } - - internal static string Serialize(object obj, int depth, SerializerOptions options) - { - return 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.ContainsKey(targetType)) - return string.Empty; - - var escapedValue = Escape(Definitions.BasicTypesInfo[targetType].ToStringInvariant(obj), false); - - return decimal.TryParse(escapedValue, out _) - ? $"{escapedValue}" - : $"{StringQuotedChar}{escapedValue}{StringQuotedChar}"; - } - } - - private static bool IsNonEmptyJsonArrayOrObject(string serialized) - { - if (serialized.Equals(EmptyObjectLiteral) || serialized.Equals(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) == false) { + return; + } + + this._options = options; + this._lastCommaSearch = FieldSeparatorChar + (this._options.Format ? Environment.NewLine : String.Empty); + + // 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._builder = new StringBuilder(); + + switch(obj) { + case IDictionary itemsZero when itemsZero.Count == 0: + this._result = EmptyObjectLiteral; + break; + case IDictionary items: + this._result = this.ResolveDictionary(items, depth); + break; + case IEnumerable enumerableZero when !enumerableZero.Cast().Any(): + this._result = EmptyArrayLiteral; + break; + case IEnumerable enumerableBytes when enumerableBytes is Byte[] bytes: + this._result = Serialize(bytes.ToBase64(), depth, this._options); + break; + case IEnumerable enumerable: + this._result = this.ResolveEnumerable(enumerable, depth); + break; + default: + this._result = this.ResolveObject(obj, depth); + break; + } + } + + 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.ContainsKey(targetType)) { + return String.Empty; + } + + String escapedValue = Escape(Definitions.BasicTypesInfo[targetType].ToStringInvariant(obj), false); + + return Decimal.TryParse(escapedValue, out _) + ? $"{escapedValue}" + : $"{StringQuotedChar}{escapedValue}{StringQuotedChar}"; + } + } + + private static Boolean IsNonEmptyJsonArrayOrObject(String serialized) { + if(serialized.Equals(EmptyObjectLiteral) || serialized.Equals(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)(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(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 + Dictionary objectDictionary = new Dictionary(); + + if(String.IsNullOrWhiteSpace(this._options.TypeSpecifier) == false) { + objectDictionary[this._options.TypeSpecifier] = targetType; + } + + foreach(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(this._options.IncludeNonPublic)(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 - 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(); - 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(" "); + + // 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(); + 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 + 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 + 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 + } + } } \ No newline at end of file diff --git a/Unosquare.Swan.Lite/Formatters/Json.SerializerOptions.cs b/Unosquare.Swan.Lite/Formatters/Json.SerializerOptions.cs index fc6a7f5..4ad6a40 100644 --- a/Unosquare.Swan.Lite/Formatters/Json.SerializerOptions.cs +++ b/Unosquare.Swan.Lite/Formatters/Json.SerializerOptions.cs @@ -1,107 +1,107 @@ -namespace Unosquare.Swan.Formatters -{ - using System; - using System.Collections.Generic; - using System.Collections.Concurrent; - using System.Linq; - using System.Reflection; - using Attributes; - - /// - /// 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 - { - private 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>(); - - public SerializerOptions( - bool format, - string typeSpecifier, - string[] includeProperties, - string[] excludeProperties = null, - bool includeNonPublic = true, - IReadOnlyCollection parentReferences = null) - { - _includeProperties = includeProperties; - _excludeProperties = excludeProperties; - - IncludeNonPublic = includeNonPublic; - Format = format; - TypeSpecifier = typeSpecifier; - - if (parentReferences == null) - return; - - foreach (var parentReference in parentReferences.Where(x => x.IsAlive)) - { - IsObjectPresent(parentReference.Target); - } - } - - public bool Format { get; } - public string TypeSpecifier { get; } - public bool IncludeNonPublic { 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 static Dictionary, MemberInfo> GetPropertiesCache(Type targetType) - { - if (TypeCache.TryGetValue(targetType, out var current)) - return current; - - var fields = - new List(PropertyTypeCache.RetrieveAllProperties(targetType).Where(p => p.CanRead)); - - // If the target is a struct (value type) navigate the fields. - if (targetType.IsValueType()) - { - fields.AddRange(FieldTypeCache.RetrieveAllFields(targetType)); - } - - var value = fields - .ToDictionary( - x => Tuple.Create(x.Name, - x.GetCustomAttribute()?.PropertyName ?? x.Name), - x => x); - - TypeCache.TryAdd(targetType, value); - - return value; - } - } - } +using System; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Linq; +using System.Reflection; +using Unosquare.Swan.Attributes; + +namespace Unosquare.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 { + private 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>(); + + public SerializerOptions( + Boolean format, + String typeSpecifier, + String[] includeProperties, + String[] excludeProperties = null, + Boolean includeNonPublic = true, + IReadOnlyCollection parentReferences = null) { + this._includeProperties = includeProperties; + this._excludeProperties = excludeProperties; + + this.IncludeNonPublic = includeNonPublic; + this.Format = format; + this.TypeSpecifier = typeSpecifier; + + if(parentReferences == null) { + return; + } + + foreach(WeakReference parentReference in parentReferences.Where(x => x.IsAlive)) { + _ = this.IsObjectPresent(parentReference.Target); + } + } + + public Boolean Format { + get; + } + public String TypeSpecifier { + get; + } + public Boolean IncludeNonPublic { + get; + } + + internal Boolean IsObjectPresent(Object target) { + 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) + => 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 static Dictionary, MemberInfo> GetPropertiesCache(Type targetType) { + if(TypeCache.TryGetValue(targetType, out Dictionary, MemberInfo> current)) { + return current; + } + + List fields = + new List(PropertyTypeCache.RetrieveAllProperties(targetType).Where(p => p.CanRead)); + + // If the target is a struct (value type) navigate the fields. + if(targetType.IsValueType()) { + fields.AddRange(FieldTypeCache.RetrieveAllFields(targetType)); + } + + Dictionary, MemberInfo> value = fields + .ToDictionary( + x => Tuple.Create(x.Name, + x.GetCustomAttribute()?.PropertyName ?? x.Name), + x => x); + + _ = TypeCache.TryAdd(targetType, value); + + return value; + } + } + } } \ No newline at end of file diff --git a/Unosquare.Swan.Lite/Formatters/Json.cs b/Unosquare.Swan.Lite/Formatters/Json.cs index ddb3084..21994fd 100644 --- a/Unosquare.Swan.Lite/Formatters/Json.cs +++ b/Unosquare.Swan.Lite/Formatters/Json.cs @@ -1,331 +1,319 @@ -namespace Unosquare.Swan.Formatters -{ - using Reflection; - using System; - using Components; - using System.Collections.Generic; - using System.Linq; - using System.Reflection; - using Attributes; - +using Unosquare.Swan.Reflection; +using System; +using Unosquare.Swan.Components; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Unosquare.Swan.Attributes; + +namespace Unosquare.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 PropertyTypeCache PropertyTypeCache = new PropertyTypeCache(); + private static readonly FieldTypeCache FieldTypeCache = new FieldTypeCache(); + 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 PropertyTypeCache PropertyTypeCache = new PropertyTypeCache(); - private static readonly FieldTypeCache FieldTypeCache = new FieldTypeCache(); - 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 Unosquare.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 Unosquare.Swan.Attributes; - /// using Unosquare.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, - string[] excludedNames = null) - { - return Serialize(obj, format, typeSpecifier, includeNonPublic, includedNames, excludedNames, null); - } - - /// - /// 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. - /// - /// A that represents the current object. - /// - public static string Serialize( - object obj, - bool format, - string typeSpecifier, - bool includeNonPublic, - string[] includedNames, - string[] excludedNames, - List parentReferences) - { - 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); - - return 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 Unosquare.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) - { - var options = new SerializerOptions(format, null, includeNames); - - return Serializer.Serialize(obj, 0, options); - } - - /// - /// 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 exluding the specified properties. - /// - /// using Unosquare.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) - { - var options = new SerializerOptions(format, null, null, excludeNames); - - return Serializer.Serialize(obj, 0, options); - } - - /// - /// 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. - /// Type of the current deserializes. - /// - /// The following code shows how to deserialize a JSON string into a Dictionary. - /// - /// using Unosquare.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) => Deserializer.DeserializeInternal(json); - - /// - /// 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. - /// The deserialized specified type object. - /// - /// The following code describes how to deserialize a JSON string into an object of type T. - /// - /// using Unosquare.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) => (T)Deserialize(json, typeof(T)); - - /// - /// Deserializes the specified json string and converts it to the specified object type. - /// - /// The type of object to deserialize. - /// The json. - /// 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. - /// Type of the result. - /// if set to true, it also uses the non-public constructors and property setters. - /// Type of the current conversion from json result. - public static object Deserialize(string json, Type resultType, bool includeNonPublic = false) - => Converter.FromJsonResult(Deserializer.DeserializeInternal(json), 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 Unosquare.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 Unosquare.Swan.Attributes; + /// using Unosquare.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, + String[] excludedNames = null) => Serialize(obj, format, typeSpecifier, includeNonPublic, includedNames, excludedNames, null); + + /// + /// 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. + /// + /// A that represents the current object. + /// + public static String Serialize( + Object obj, + Boolean format, + String typeSpecifier, + Boolean includeNonPublic, + String[] includedNames, + String[] excludedNames, + List parentReferences) { + 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); + + return 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 Unosquare.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) { + SerializerOptions options = new SerializerOptions(format, null, includeNames); + + return Serializer.Serialize(obj, 0, options); + } + + /// + /// 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 exluding the specified properties. + /// + /// using Unosquare.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) { + SerializerOptions options = new SerializerOptions(format, null, null, excludeNames); + + return Serializer.Serialize(obj, 0, options); + } + + /// + /// 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. + /// Type of the current deserializes. + /// + /// The following code shows how to deserialize a JSON string into a Dictionary. + /// + /// using Unosquare.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) => Deserializer.DeserializeInternal(json); + + /// + /// 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. + /// The deserialized specified type object. + /// + /// The following code describes how to deserialize a JSON string into an object of type T. + /// + /// using Unosquare.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) => (T)Deserialize(json, typeof(T)); + + /// + /// Deserializes the specified json string and converts it to the specified object type. + /// + /// The type of object to deserialize. + /// The json. + /// 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) => (T)Deserialize(json, typeof(T), includeNonPublic); + + /// + /// Deserializes the specified json string and converts it to the specified object type. + /// + /// The json. + /// Type of the result. + /// if set to true, it also uses the non-public constructors and property setters. + /// Type of the current conversion from json result. + public static Object Deserialize(String json, Type resultType, Boolean includeNonPublic = false) + => Converter.FromJsonResult(Deserializer.DeserializeInternal(json), resultType, includeNonPublic); + + #endregion + + #region Private API + + private static String[] GetExcludedNames(Type type, String[] excludedNames) { + if(type == null) { + return excludedNames; + } + + IEnumerable excludedByAttr = IgnoredPropertiesCache.Retrieve(type, t => t.GetProperties() .Where(x => Runtime.AttributeCache.RetrieveOne(x)?.Ignored == true) - .Select(x => x.Name)); - - if (excludedByAttr?.Any() != true) - return excludedNames; - - return excludedNames == null + .Select(x => x.Name)); + + return excludedByAttr?.Any() != true + ? excludedNames + : excludedNames == null ? excludedByAttr.ToArray() - : excludedByAttr.Intersect(excludedNames).ToArray(); - } - - private static string SerializePrimitiveValue(object obj) - { - switch (obj) - { - case string stringValue: - return stringValue; - case bool boolValue: - return boolValue ? TrueLiteral : FalseLiteral; - default: - return obj.ToString(); - } - } - #endregion - } + : excludedByAttr.Intersect(excludedNames).ToArray(); + } + + private static String SerializePrimitiveValue(Object obj) { + switch(obj) { + case String stringValue: + return stringValue; + case Boolean boolValue: + return boolValue ? TrueLiteral : FalseLiteral; + default: + return obj.ToString(); + } + } + #endregion + } } diff --git a/Unosquare.Swan.Lite/Reflection/AttributeCache.cs b/Unosquare.Swan.Lite/Reflection/AttributeCache.cs index 77dd83d..1b82c1a 100644 --- a/Unosquare.Swan.Lite/Reflection/AttributeCache.cs +++ b/Unosquare.Swan.Lite/Reflection/AttributeCache.cs @@ -1,174 +1,172 @@ -namespace Unosquare.Swan.Reflection -{ - using System; - using System.Collections.Generic; - using System.Reflection; - using System.Collections.Concurrent; - using System.Linq; - +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Collections.Concurrent; +using System.Linq; + +namespace Unosquare.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 ?? Runtime.PropertyTypeCache; - } - - /// - /// 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 - { - if (type == null) - throw new ArgumentNullException(nameof(type)); - - return 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. - /// 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; - - if (attr.Count() == 1) - return (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 ?? Runtime.PropertyTypeCache; + + /// + /// 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 { + if(type == null) { + throw new ArgumentNullException(nameof(type)); + } + + return 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. + /// 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 { + if(attr?.Any() != true) { + return default; + } + + if(attr.Count() == 1) { + return (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)); + } + } } \ No newline at end of file diff --git a/Unosquare.Swan.Lite/Reflection/ExtendedPropertyInfo.cs b/Unosquare.Swan.Lite/Reflection/ExtendedPropertyInfo.cs index 6b405ff..e059fef 100644 --- a/Unosquare.Swan.Lite/Reflection/ExtendedPropertyInfo.cs +++ b/Unosquare.Swan.Lite/Reflection/ExtendedPropertyInfo.cs @@ -1,107 +1,114 @@ -namespace Unosquare.Swan.Reflection -{ - using System; - using System.Reflection; - using Attributes; - +using System; +using System.Reflection; +using Unosquare.Swan.Attributes; + +namespace Unosquare.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 Runtime.AttributeCache.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 Runtime.AttributeCache.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/Unosquare.Swan.Lite/Reflection/ExtendedTypeInfo.cs b/Unosquare.Swan.Lite/Reflection/ExtendedTypeInfo.cs index 457d2d9..e77e50f 100644 --- a/Unosquare.Swan.Lite/Reflection/ExtendedTypeInfo.cs +++ b/Unosquare.Swan.Lite/Reflection/ExtendedTypeInfo.cs @@ -1,290 +1,281 @@ -namespace Unosquare.Swan.Reflection -{ - using System; - using System.Collections.Generic; - using System.ComponentModel; - using System.Globalization; - using System.Linq; - using System.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 +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using System.Reflection; + +namespace Unosquare.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 { + #region Static Declarations + + private const String TryParseMethodName = nameof(Byte.TryParse); + private const String ToStringMethodName = nameof(ToString); + + private static readonly Type[] NumericTypes = { - #region Static Declarations - - 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), - }; - - #endregion - - #region State Management - - private readonly ParameterInfo[] _tryParseParameters; - private readonly int _toStringArgumentLength; - - #endregion - - #region Constructors - - /// - /// Initializes a new instance of the class. - /// - /// The t. - public ExtendedTypeInfo(Type t) - { - Type = t ?? throw new ArgumentNullException(nameof(t)); - IsNullableValueType = Type.GetTypeInfo().IsGenericType - && Type.GetGenericTypeDefinition() == typeof(Nullable<>); - - IsValueType = t.GetTypeInfo().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, + typeof(Byte), + typeof(SByte), + typeof(Decimal), + typeof(Double), + typeof(Single), + typeof(Int32), + typeof(UInt32), + typeof(Int64), + typeof(UInt64), + typeof(Int16), + typeof(UInt16), + }; + + #endregion + + #region State Management + + private readonly ParameterInfo[] _tryParseParameters; + private readonly Int32 _toStringArgumentLength; + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The t. + public ExtendedTypeInfo(Type t) { + this.Type = t ?? throw new ArgumentNullException(nameof(t)); + this.IsNullableValueType = this.Type.GetTypeInfo().IsGenericType + && this.Type.GetGenericTypeDefinition() == typeof(Nullable<>); + + this.IsValueType = t.GetTypeInfo().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) }) ?? - UnderlyingType.GetMethod(ToStringMethodName, - new Type[] { }); - - _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 - - /// - /// Gets the default value of this type. For reference types it return null. - /// For value types it returns the default value. - /// - /// Default value of this type. - public object GetDefault() => Type.GetTypeInfo().IsValueType ? Activator.CreateInstance(Type) : null; - - /// - /// 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 = GetDefault(); - - try - { - if (Type == typeof(string)) - { - result = Convert.ChangeType(s, Type); - return true; - } - - if (IsNullableValueType && string.IsNullOrEmpty(s)) - { - result = GetDefault(); - return true; - } - - if (CanParseNatively == false) - { - result = GetDefault(); - return false; - } - - // 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) == false) - { - result = GetDefault(); - return false; - } - - result = parseArguments[parseArguments.Length - 1]; - return true; - } - catch - { - 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; - } - - #endregion - } - + this.UnderlyingType.GetMethod(ToStringMethodName, + new Type[] { }); + + this._toStringArgumentLength = this.ToStringMethodInfo?.GetParameters().Length ?? 0; + } catch { + // ignored + } + } + + #endregion + + #region Properties + /// - /// 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. + /// Gets the type this extended info class provides for. /// - /// 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 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 + + /// + /// Gets the default value of this type. For reference types it return null. + /// For value types it returns the default value. + /// + /// Default value of this type. + public Object GetDefault() => this.Type.GetTypeInfo().IsValueType ? Activator.CreateInstance(this.Type) : null; + + /// + /// 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.GetDefault(); + + try { + if(this.Type == typeof(String)) { + result = Convert.ChangeType(s, this.Type); + return true; + } + + if(this.IsNullableValueType && String.IsNullOrEmpty(s)) { + result = this.GetDefault(); + return true; + } + + if(this.CanParseNatively == false) { + result = this.GetDefault(); + return false; + } + + // Build the arguments of the TryParse method + List dynamicArguments = new List { s }; + + 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) == false) { + result = this.GetDefault(); + return false; + } + + result = parseArguments[parseArguments.Length - 1]; + return true; + } catch { + 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; + + #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); + } } \ No newline at end of file diff --git a/Unosquare.Swan.Lite/Reflection/MethodInfoCache.cs b/Unosquare.Swan.Lite/Reflection/MethodInfoCache.cs index f4c41c9..bf90cab 100644 --- a/Unosquare.Swan.Lite/Reflection/MethodInfoCache.cs +++ b/Unosquare.Swan.Lite/Reflection/MethodInfoCache.cs @@ -1,118 +1,119 @@ -namespace Unosquare.Swan.Reflection -{ - using System; - using System.Reflection; - using System.Collections.Concurrent; - +using System; +using System.Reflection; +using System.Collections.Concurrent; + +namespace Unosquare.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. - /// type. - 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( + /// The type of type. + /// The name. + /// The alias. + /// The types. + /// + /// The cached MethodInfo. + /// + /// name + /// or + /// factory. + /// type. + 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 ?? new Type[0])); - } - - /// - /// 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( + x => type.GetMethod(name, types ?? new Type[0])); + } + + /// + /// 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); - } - } + type.GetMethod); + } + } } diff --git a/Unosquare.Swan.Lite/Reflection/TypeCache.cs b/Unosquare.Swan.Lite/Reflection/TypeCache.cs index ca80ac7..110af14 100644 --- a/Unosquare.Swan.Lite/Reflection/TypeCache.cs +++ b/Unosquare.Swan.Lite/Reflection/TypeCache.cs @@ -1,135 +1,131 @@ -namespace Unosquare.Swan.Reflection -{ - using System.Linq; - using System; - using System.Collections.Generic; - using System.Reflection; - using Components; - +using System.Linq; +using System; +using System.Collections.Generic; +using System.Reflection; +using Unosquare.Swan.Components; + +namespace Unosquare.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 + where T : MemberInfo { /// - /// 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 - where T : MemberInfo - { - /// - /// 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 properties 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 PropertyTypeCache : TypeCache - { - /// - /// 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 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 properties 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 PropertyTypeCache : TypeCache { /// - /// 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 all properties. /// - public class FieldTypeCache : TypeCache - { - /// - /// 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 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)); + } + + /// + /// 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 { + /// + /// 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/Unosquare.Swan.Lite/Runtime.cs b/Unosquare.Swan.Lite/Runtime.cs index 81992b7..4124312 100644 --- a/Unosquare.Swan.Lite/Runtime.cs +++ b/Unosquare.Swan.Lite/Runtime.cs @@ -1,366 +1,342 @@ -namespace Unosquare.Swan -{ - using Components; - using System; - using System.IO; - using System.Threading; - using Reflection; -#if !NETSTANDARD1_3 - using System.Reflection; -#endif +using Unosquare.Swan.Components; +using System; +using System.IO; +using System.Threading; +using Unosquare.Swan.Reflection; +#if !NETSTANDARD1_3 +using System.Reflection; +#endif + +namespace Unosquare.Swan { - /// - /// Provides utility methods to retrieve information about the current application. - /// -#if !NETSTANDARD1_3 - public class Runtime : MarshalByRefObject + + /// + /// Provides utility methods to retrieve information about the current application. + /// +#if !NETSTANDARD1_3 + public class Runtime : MarshalByRefObject #else public static class Runtime #endif - { - private static readonly Lazy _propertyTypeCache = new Lazy(() => new PropertyTypeCache()); - - private static readonly Lazy _attributeCache = new Lazy(() => new AttributeCache()); - - private static readonly Lazy _objectValidator = new Lazy(() => new ObjectValidator()); - - private static readonly Lazy _fieldTypeCache = new Lazy(() => new FieldTypeCache()); - - private static readonly Lazy _methodInfoCache = new Lazy(() => new MethodInfoCache()); - + { + private static readonly Lazy _propertyTypeCache = new Lazy(() => new PropertyTypeCache()); + + private static readonly Lazy _attributeCache = new Lazy(() => new AttributeCache()); + + private static readonly Lazy _objectValidator = new Lazy(() => new ObjectValidator()); + + private static readonly Lazy _fieldTypeCache = new Lazy(() => new FieldTypeCache()); + + private static readonly Lazy _methodInfoCache = new Lazy(() => new MethodInfoCache()); + #if NET452 - private static readonly Lazy EntryAssemblyLazy = new Lazy(() => Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly()); + private static readonly Lazy EntryAssemblyLazy = new Lazy(() => Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly()); #endif - + #if NETSTANDARD2_0 private static readonly Lazy EntryAssemblyLazy = new Lazy(Assembly.GetEntryAssembly); #endif - + #if NET452 - private static readonly Lazy ProcessLazy = new Lazy(System.Diagnostics.Process.GetCurrentProcess); + private static readonly Lazy ProcessLazy = new Lazy(System.Diagnostics.Process.GetCurrentProcess); #endif - -#if !NETSTANDARD1_3 - 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; - }); + +#if !NETSTANDARD1_3 + 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; + }); #endif - - private static readonly Lazy _argumentParser = - new Lazy(() => new ArgumentParser()); - - private static readonly Lazy _objectMapper = new Lazy(() => new ObjectMapper()); - -#if !NETSTANDARD1_3 - private static readonly string ApplicationMutexName = "Global\\{{" + EntryAssembly.FullName + "}}"; + + private static readonly Lazy _argumentParser = + new Lazy(() => new ArgumentParser()); + + private static readonly Lazy _objectMapper = new Lazy(() => new ObjectMapper()); + +#if !NETSTANDARD1_3 + private static readonly String ApplicationMutexName = "Global\\{{" + EntryAssembly.FullName + "}}"; #else private const string ApplicationMutexName = "Global\\{{SWANINSTANCE}}"; #endif - - 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; - } - } - + + private static readonly Object SyncLock = new Object(); + + private static OperatingSystem? _oS; + + #region Properties + + /// + /// Gets the current Operating System. + /// + /// + /// The os. + /// + public static OperatingSystem OS { + get { + if(_oS.HasValue == false) { + String windowsDirectory = Environment.GetEnvironmentVariable("windir"); + _oS = String.IsNullOrEmpty(windowsDirectory) == false + && windowsDirectory.Contains(@"\") + && Directory.Exists(windowsDirectory) + ? (OperatingSystem?)OperatingSystem.Windows + : (OperatingSystem?)(File.Exists(@"/proc/sys/kernel/ostype") ? OperatingSystem.Unix : OperatingSystem.Osx); + } + + return _oS ?? OperatingSystem.Unknown; + } + } + #if NET452 - /// - /// Gets the process associated with the current application. - /// - /// - /// The process. - /// - public static System.Diagnostics.Process Process => ProcessLazy.Value; + /// + /// Gets the process associated with the current application. + /// + /// + /// The process. + /// + public static System.Diagnostics.Process Process => ProcessLazy.Value; #endif - - /// - /// 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(Runtime)); - - // 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 property type cache. - /// - /// - /// The property type cache. - /// - public static PropertyTypeCache PropertyTypeCache => _propertyTypeCache.Value; - - /// - /// Gets the attribute cache. - /// - /// - /// The attribute cache. - /// - public static AttributeCache AttributeCache => _attributeCache.Value; - - /// - /// Gets the object validator. - /// - /// - /// The object validator. - /// - public static ObjectValidator ObjectValidator => _objectValidator.Value; - - /// - /// Gets the field type cache. - /// - /// - /// The field type cache. - /// - public static FieldTypeCache FieldTypeCache => _fieldTypeCache.Value; - - /// - /// Gets the method information cache. - /// - /// - /// The method information cache. - /// - public static MethodInfoCache MethodInfoCache => _methodInfoCache.Value; - -#if !NETSTANDARD1_3 - /// - /// 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; + + /// + /// 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(Runtime)); + + // 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 property type cache. + /// + /// + /// The property type cache. + /// + public static PropertyTypeCache PropertyTypeCache => _propertyTypeCache.Value; + + /// + /// Gets the attribute cache. + /// + /// + /// The attribute cache. + /// + public static AttributeCache AttributeCache => _attributeCache.Value; + + /// + /// Gets the object validator. + /// + /// + /// The object validator. + /// + public static ObjectValidator ObjectValidator => _objectValidator.Value; + + /// + /// Gets the field type cache. + /// + /// + /// The field type cache. + /// + public static FieldTypeCache FieldTypeCache => _fieldTypeCache.Value; + + /// + /// Gets the method information cache. + /// + /// + /// The method information cache. + /// + public static MethodInfoCache MethodInfoCache => _methodInfoCache.Value; + +#if !NETSTANDARD1_3 + /// + /// 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; #endif - - /// - /// Gets a local storage path with a version. - /// - /// - /// The local storage path. - /// - public static string LocalStoragePath - { - get - { -#if !NETSTANDARD1_3 - var localAppDataPath = + + /// + /// Gets a local storage path with a version. + /// + /// + /// The local storage path. + /// + public static String LocalStoragePath { + get { +#if !NETSTANDARD1_3 + String localAppDataPath = #if NET452 - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - EntryAssemblyName.Name); + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + EntryAssemblyName.Name); #else Path.GetDirectoryName(EntryAssembly.Location); #endif - - var returnPath = Path.Combine(localAppDataPath, EntryAssemblyVersion.ToString()); + + String returnPath = Path.Combine(localAppDataPath, EntryAssemblyVersion.ToString()); #else var returnPath = Directory.GetCurrentDirectory(); // Use current path... #endif - - if (Directory.Exists(returnPath) == false) - { - Directory.CreateDirectory(returnPath); - } - - return returnPath; - } - } - - /// - /// Gets the singleton instance created with basic defaults. - /// - /// - /// The argument parser. - /// - public static ArgumentParser ArgumentParser => _argumentParser.Value; - - /// - /// Gets the object mapper instance created with basic defaults. - /// - /// - /// The object mapper. - /// - public static ObjectMapper ObjectMapper => _objectMapper.Value; - - #endregion - - #region Methods - -#if !NETSTANDARD1_3 - /// - /// 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) - { - $"{CompanyName} {ProductName} [Version {EntryAssemblyVersion}]".WriteLine(color); - $"{ProductTrademark}".WriteLine(color); - } - - /// - /// Gets all the loaded assemblies in the current application domain. - /// - /// An array of assemblies. - public static Assembly[] GetAssemblies() => AppDomain.CurrentDomain.GetAssemblies(); - - /// - /// 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); - } + + if(Directory.Exists(returnPath) == false) { + Directory.CreateDirectory(returnPath); + } + + return returnPath; + } + } + + /// + /// Gets the singleton instance created with basic defaults. + /// + /// + /// The argument parser. + /// + public static ArgumentParser ArgumentParser => _argumentParser.Value; + + /// + /// Gets the object mapper instance created with basic defaults. + /// + /// + /// The object mapper. + /// + public static ObjectMapper ObjectMapper => _objectMapper.Value; + + #endregion + + #region Methods + +#if !NETSTANDARD1_3 + /// + /// 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) { + $"{CompanyName} {ProductName} [Version {EntryAssemblyVersion}]".WriteLine(color); + $"{ProductTrademark}".WriteLine(color); + } + + /// + /// Gets all the loaded assemblies in the current application domain. + /// + /// An array of assemblies. + public static Assembly[] GetAssemblies() => AppDomain.CurrentDomain.GetAssemblies(); + + /// + /// 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); + } #endif - - #endregion - } + + #endregion + } } \ No newline at end of file diff --git a/Unosquare.Swan.Lite/Terminal.Enums.cs b/Unosquare.Swan.Lite/Terminal.Enums.cs index 77d709e..4bd8da3 100644 --- a/Unosquare.Swan.Lite/Terminal.Enums.cs +++ b/Unosquare.Swan.Lite/Terminal.Enums.cs @@ -1,89 +1,86 @@ -namespace Unosquare.Swan -{ - using System; - +using System; + +namespace Unosquare.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 the System.Diagnostics.Debug - /// - Diagnostics = 4, - - /// - /// Writes to all possible terminal writers - /// - All = StandardOutput | Diagnostics | StandardError, - - /// - /// The error and debug writers - /// - ErrorAndDebug = StandardError | Diagnostics, - - /// - /// The output and debug writers - /// - OutputAndDebug = StandardOutput | Diagnostics, - } - + None = 0, + /// - /// Defines the bitwise flags to determine - /// which types of messages get printed on the current console. + /// Writes to the Console.Out /// - [Flags] - public enum LogMessageType - { - /// - /// The none message type - /// - None = 0, - - /// - /// The information message type - /// - Info = 1, - - /// - /// The debug message type - /// - Debug = 2, - - /// - /// The trace message type - /// - Trace = 4, - - /// - /// The error message type - /// - Error = 8, - - /// - /// The warning message type - /// - Warning = 16, - - /// - /// The fatal message type - /// - Fatal = 32, - } + StandardOutput = 1, + + /// + /// Writes to the Console.Error + /// + StandardError = 2, + + /// + /// Writes to the System.Diagnostics.Debug + /// + Diagnostics = 4, + + /// + /// Writes to all possible terminal writers + /// + All = StandardOutput | Diagnostics | StandardError, + + /// + /// The error and debug writers + /// + ErrorAndDebug = StandardError | Diagnostics, + + /// + /// The output and debug writers + /// + OutputAndDebug = StandardOutput | Diagnostics, + } + + /// + /// Defines the bitwise flags to determine + /// which types of messages get printed on the current console. + /// + [Flags] + public enum LogMessageType { + /// + /// The none message type + /// + None = 0, + + /// + /// The information message type + /// + Info = 1, + + /// + /// The debug message type + /// + Debug = 2, + + /// + /// The trace message type + /// + Trace = 4, + + /// + /// The error message type + /// + Error = 8, + + /// + /// The warning message type + /// + Warning = 16, + + /// + /// The fatal message type + /// + Fatal = 32, + } } diff --git a/Unosquare.Swan.Lite/Terminal.Graphics.cs b/Unosquare.Swan.Lite/Terminal.Graphics.cs index e6afad7..b162b07 100644 --- a/Unosquare.Swan.Lite/Terminal.Graphics.cs +++ b/Unosquare.Swan.Lite/Terminal.Graphics.cs @@ -1,47 +1,44 @@ -namespace Unosquare.Swan -{ - using System; - +using System; + +namespace Unosquare.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 - { - /// - /// Gets or sets the color of the border. - /// - /// - /// The color of the border. - /// - private static ConsoleColor BorderColor { get; } = ConsoleColor.DarkGreen; - - public static void Vertical() => ((byte)179).Write(BorderColor); - - public static void RightTee() => ((byte)180).Write(BorderColor); - - public static void TopRight() => ((byte)191).Write(BorderColor); - - public static void BottomLeft() => ((byte)192).Write(BorderColor); - - public static void BottomTee() => ((byte)193).Write(BorderColor); - - public static void TopTee() => ((byte)194).Write(BorderColor); - - public static void LeftTee() => ((byte)195).Write(BorderColor); - - public static void Horizontal(int length) => ((byte)196).Write(BorderColor, length); - - public static void Tee() => ((byte)197).Write(BorderColor); - - public static void BottomRight() => ((byte)217).Write(BorderColor); - - public static void TopLeft() => ((byte)218).Write(BorderColor); - } - } + private static class Table { + /// + /// Gets or sets the color of the border. + /// + /// + /// The color of the border. + /// + private static ConsoleColor BorderColor { get; } = ConsoleColor.DarkGreen; + + public static void Vertical() => ((Byte)179).Write(BorderColor); + + public static void RightTee() => ((Byte)180).Write(BorderColor); + + public static void TopRight() => ((Byte)191).Write(BorderColor); + + public static void BottomLeft() => ((Byte)192).Write(BorderColor); + + public static void BottomTee() => ((Byte)193).Write(BorderColor); + + public static void TopTee() => ((Byte)194).Write(BorderColor); + + public static void LeftTee() => ((Byte)195).Write(BorderColor); + + public static void Horizontal(Int32 length) => ((Byte)196).Write(BorderColor, length); + + public static void Tee() => ((Byte)197).Write(BorderColor); + + public static void BottomRight() => ((Byte)217).Write(BorderColor); + + public static void TopLeft() => ((Byte)218).Write(BorderColor); + } + } } diff --git a/Unosquare.Swan.Lite/Terminal.Interaction.cs b/Unosquare.Swan.Lite/Terminal.Interaction.cs index 7c47bda..d972964 100644 --- a/Unosquare.Swan.Lite/Terminal.Interaction.cs +++ b/Unosquare.Swan.Lite/Terminal.Interaction.cs @@ -1,216 +1,208 @@ -namespace Unosquare.Swan -{ - using System; - using System.Collections.Generic; - +using System; +using System.Collections.Generic; + +namespace Unosquare.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 == false) 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(this string prompt, bool preventEcho) - { - if (IsConsolePresent == false) return default; - - lock (SyncLock) - { - if (prompt != null) - { - ($" {(string.IsNullOrWhiteSpace(Settings.LoggingTimeFormat) ? string.Empty : DateTime.Now.ToString(Settings.LoggingTimeFormat) + " ")}" + - $"{Settings.UserInputPrefix} << {prompt} ").Write(ConsoleColor.White); - } - - var input = ReadKey(true); - var echo = preventEcho ? string.Empty : input.Key.ToString(); - echo.WriteLine(); - return input; - } - } - - /// - /// Reads a key from the terminal preventing the key from being echoed. - /// - /// The prompt. - /// A value that identifies the console key. - public static ConsoleKeyInfo ReadKey(this string prompt) => prompt.ReadKey(true); - - #endregion - - #region Other Terminal Read Methods - - /// - /// 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 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(this string prompt, int defaultNumber) - { - if (IsConsolePresent == false) return defaultNumber; - - lock (SyncLock) - { - $" {DateTime.Now:HH:mm:ss} USR << {prompt} (default is {defaultNumber}): ".Write(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. - public static ConsoleKeyInfo ReadPrompt(this string title, Dictionary options, string anyKeyOption) - { - if (IsConsolePresent == false) return default; - - const ConsoleColor textColor = ConsoleColor.White; - var lineLength = Console.BufferWidth; - 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( - textFormat, - string.IsNullOrWhiteSpace(title) ? " Select an option from the list below." : $" {title}"); - titleText.Write(textColor); - Table.Vertical(); - } - - { // Title Bottom - Table.LeftTee(); - Table.Horizontal(lineLength - 2); - Table.RightTee(); - } - - // Options - foreach (var kvp in options) - { - Table.Vertical(); - string.Format(textFormat, - $" {"[ " + kvp.Key + " ]",-10} {kvp.Value}").Write(textColor); - Table.Vertical(); - } - - // Any Key Options - if (string.IsNullOrWhiteSpace(anyKeyOption) == false) - { - Table.Vertical(); - string.Format(textFormat, " ").Write(ConsoleColor.Gray); - Table.Vertical(); - - Table.Vertical(); - string.Format(textFormat, - $" {" ",-10} {anyKeyOption}").Write(ConsoleColor.Gray); - Table.Vertical(); - } - - { // Input section - Table.LeftTee(); - Table.Horizontal(lineLength - 2); - Table.RightTee(); - - Table.Vertical(); - string.Format(textFormat, - Settings.UserOptionText).Write(ConsoleColor.Green); - Table.Vertical(); - - Table.BottomLeft(); - Table.Horizontal(lineLength - 2); - Table.BottomRight(); - } - } - - var inputLeft = Settings.UserOptionText.Length + 3; - - SetCursorPosition(inputLeft, CursorTop - 2); - var userInput = ReadKey(true); - userInput.Key.ToString().Write(ConsoleColor.Gray); - - SetCursorPosition(0, CursorTop + 2); - return userInput; - } - - #endregion - } + /// 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 == false) { + 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(this String prompt, Boolean preventEcho) { + if(IsConsolePresent == false) { + return default; + } + + lock(SyncLock) { + if(prompt != null) { + ($" {(String.IsNullOrWhiteSpace(Settings.LoggingTimeFormat) ? String.Empty : DateTime.Now.ToString(Settings.LoggingTimeFormat) + " ")}" + + $"{Settings.UserInputPrefix} << {prompt} ").Write(ConsoleColor.White); + } + + ConsoleKeyInfo input = ReadKey(true); + String echo = preventEcho ? String.Empty : input.Key.ToString(); + echo.WriteLine(); + return input; + } + } + + /// + /// Reads a key from the terminal preventing the key from being echoed. + /// + /// The prompt. + /// A value that identifies the console key. + public static ConsoleKeyInfo ReadKey(this String prompt) => prompt.ReadKey(true); + + #endregion + + #region Other Terminal Read Methods + + /// + /// 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 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(this String prompt, Int32 defaultNumber) { + if(IsConsolePresent == false) { + return defaultNumber; + } + + lock(SyncLock) { + $" {DateTime.Now:HH:mm:ss} USR << {prompt} (default is {defaultNumber}): ".Write(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. + public static ConsoleKeyInfo ReadPrompt(this String title, Dictionary options, String anyKeyOption) { + if(IsConsolePresent == false) { + return default; + } + + const ConsoleColor textColor = ConsoleColor.White; + Int32 lineLength = Console.BufferWidth; + 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( + textFormat, + String.IsNullOrWhiteSpace(title) ? " Select an option from the list below." : $" {title}"); + titleText.Write(textColor); + Table.Vertical(); + } + + { // Title Bottom + Table.LeftTee(); + Table.Horizontal(lineLength - 2); + Table.RightTee(); + } + + // Options + foreach(KeyValuePair kvp in options) { + Table.Vertical(); + String.Format(textFormat, + $" {"[ " + kvp.Key + " ]",-10} {kvp.Value}").Write(textColor); + Table.Vertical(); + } + + // Any Key Options + if(String.IsNullOrWhiteSpace(anyKeyOption) == false) { + Table.Vertical(); + String.Format(textFormat, " ").Write(ConsoleColor.Gray); + Table.Vertical(); + + Table.Vertical(); + String.Format(textFormat, + $" {" ",-10} {anyKeyOption}").Write(ConsoleColor.Gray); + Table.Vertical(); + } + + { // Input section + Table.LeftTee(); + Table.Horizontal(lineLength - 2); + Table.RightTee(); + + Table.Vertical(); + String.Format(textFormat, + Settings.UserOptionText).Write(ConsoleColor.Green); + Table.Vertical(); + + Table.BottomLeft(); + Table.Horizontal(lineLength - 2); + Table.BottomRight(); + } + } + + Int32 inputLeft = Settings.UserOptionText.Length + 3; + + SetCursorPosition(inputLeft, CursorTop - 2); + ConsoleKeyInfo userInput = ReadKey(true); + userInput.Key.ToString().Write(ConsoleColor.Gray); + + SetCursorPosition(0, CursorTop + 2); + return userInput; + } + + #endregion + } } diff --git a/Unosquare.Swan.Lite/Terminal.Logging.cs b/Unosquare.Swan.Lite/Terminal.Logging.cs index dadf1c3..a71c07a 100644 --- a/Unosquare.Swan.Lite/Terminal.Logging.cs +++ b/Unosquare.Swan.Lite/Terminal.Logging.cs @@ -1,746 +1,673 @@ -namespace Unosquare.Swan -{ - using System; - using System.Runtime.CompilerServices; - using System.Threading.Tasks; - +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace Unosquare.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 static UInt64 _loggingSequence; + + #endregion + + #region Events + /// - /// A console terminal helper to create nicer output and receive input from the user - /// This class is thread-safe :). + /// Occurs asynchronously, whenever a logging message is received by the terminal. + /// Only called when Terminal writes data via Info, Error, Trace, Warn, Fatal, Debug methods, regardless of whether or not + /// the console is present. Subscribe to this event to pass data on to your own logger. /// - public static partial class Terminal - { - #region Private Declarations - - private static ulong _loggingSequence; - + public static event EventHandler OnLogMessageReceived; + + /// + /// Occurs synchronously (so handle quickly), whenever a logging message is about to be enqueued to the + /// console output. Setting the CancelOutput to true in the event arguments prevents the + /// logging message to be written out to the console. + /// Message filtering only works with logging methods such as Trace, Debug, Info, Warn, Fatal, Error and Dump + /// Standard Write methods do not get filtering capabilities. + /// + public static event EventHandler OnLogMessageDisplaying; + + #endregion + + #region Main Logging Method + + /// + /// Logs a message. + /// + /// Type of the message. + /// The text. + /// Name of the source. + /// The extended data. Could be an exception, or a dictionary of properties or anything the user specifies. + /// Name of the caller member. + /// The caller file path. + /// The caller line number. + private static void LogMessage( + LogMessageType messageType, + String message, + String sourceName, + Object extendedData, + String callerMemberName, + String callerFilePath, + Int32 callerLineNumber) { + lock(SyncLock) { + if(!Settings.GlobalLoggingMessageType.HasFlag(messageType)) { + return; + } + + String prefix = GetConsoleColorAndPrefix(messageType, out ConsoleColor color); + + #region Create and Format the Output + + UInt64 sequence = _loggingSequence; + DateTime date = DateTime.UtcNow; + _loggingSequence++; + + String loggerMessage = String.IsNullOrWhiteSpace(message) ? + String.Empty : message.RemoveControlCharsExcept('\n'); + + String outputMessage = CreateOutputMessage(sourceName, loggerMessage, prefix, date); + + // Log the message asynchronously with the appropriate event args + LogMessageReceivedEventArgs eventArgs = new LogMessageReceivedEventArgs( + sequence, + messageType, + date, + sourceName, + loggerMessage, + extendedData, + callerMemberName, + callerFilePath, + callerLineNumber); + #endregion - - #region Events - - /// - /// Occurs asynchronously, whenever a logging message is received by the terminal. - /// Only called when Terminal writes data via Info, Error, Trace, Warn, Fatal, Debug methods, regardless of whether or not - /// the console is present. Subscribe to this event to pass data on to your own logger. - /// - public static event EventHandler OnLogMessageReceived; - - /// - /// Occurs synchronously (so handle quickly), whenever a logging message is about to be enqueued to the - /// console output. Setting the CancelOutput to true in the event arguments prevents the - /// logging message to be written out to the console. - /// Message filtering only works with logging methods such as Trace, Debug, Info, Warn, Fatal, Error and Dump - /// Standard Write methods do not get filtering capabilities. - /// - public static event EventHandler OnLogMessageDisplaying; - + + #region Fire Up External Logging Logic (Asynchronously) + + if(OnLogMessageReceived != null) { + _ = Task.Run(() => { + try { + OnLogMessageReceived?.Invoke(sourceName, eventArgs); + } catch { + // Ignore + } + }); + } + #endregion - - #region Main Logging Method - - /// - /// Logs a message. - /// - /// Type of the message. - /// The text. - /// Name of the source. - /// The extended data. Could be an exception, or a dictionary of properties or anything the user specifies. - /// Name of the caller member. - /// The caller file path. - /// The caller line number. - private static void LogMessage( - LogMessageType messageType, - string message, - string sourceName, - object extendedData, - string callerMemberName, - string callerFilePath, - int callerLineNumber) - { - lock (SyncLock) - { - if (!Settings.GlobalLoggingMessageType.HasFlag(messageType)) - return; - - var prefix = GetConsoleColorAndPrefix(messageType, out var color); - - #region Create and Format the Output - - var sequence = _loggingSequence; - var date = DateTime.UtcNow; - _loggingSequence++; - - var loggerMessage = string.IsNullOrWhiteSpace(message) ? - string.Empty : message.RemoveControlCharsExcept('\n'); - - var outputMessage = CreateOutputMessage(sourceName, loggerMessage, prefix, date); - - // Log the message asynchronously with the appropriate event args - var eventArgs = new LogMessageReceivedEventArgs( - sequence, - messageType, - date, - sourceName, - loggerMessage, - extendedData, - callerMemberName, - callerFilePath, - callerLineNumber); - - #endregion - - #region Fire Up External Logging Logic (Asynchronously) - - if (OnLogMessageReceived != null) - { - Task.Run(() => - { - try - { - OnLogMessageReceived?.Invoke(sourceName, eventArgs); - } - catch - { - // Ignore - } - }); - } - - #endregion - - #region Display the Message by Writing to the Output Queue - - // Check if we are skipping these messages to be displayed based on settings - if (!Settings.DisplayLoggingMessageType.HasFlag(messageType)) - return; - - Write(messageType, sourceName, eventArgs, outputMessage, color); - - #endregion - } - } - - private static void Write( - LogMessageType messageType, - string sourceName, - LogMessageReceivedEventArgs eventArgs, - string outputMessage, - ConsoleColor color) - { - // Select the writer based on the message type - var writer = IsConsolePresent - ? messageType.HasFlag(LogMessageType.Error) ? TerminalWriters.StandardError : TerminalWriters.StandardOutput - : TerminalWriters.None; - - // Set the writer to Diagnostics if appropriate (Error and Debugging data go to the Diagnostics debugger - // if it is attached at all - if (IsDebuggerAttached - && (IsConsolePresent == false || messageType.HasFlag(LogMessageType.Debug) || - messageType.HasFlag(LogMessageType.Error))) - writer = writer | TerminalWriters.Diagnostics; - - // Check if we really need to write this out - if (writer == TerminalWriters.None) return; - - // Further format the output in the case there is an exception being logged - if (writer.HasFlag(TerminalWriters.StandardError) && eventArgs.Exception != null) - { - try - { - outputMessage = - $"{outputMessage}{Environment.NewLine}{eventArgs.Exception.Stringify().Indent()}"; - } - catch - { - // Ignore - } - } - - // Filter output messages via events - var displayingEventArgs = new LogMessageDisplayingEventArgs(eventArgs); - OnLogMessageDisplaying?.Invoke(sourceName, displayingEventArgs); - if (displayingEventArgs.CancelOutput == false) - outputMessage.WriteLine(color, writer); - } - - private static string GetConsoleColorAndPrefix(LogMessageType messageType, out ConsoleColor color) - { - string prefix; - - // Select color and prefix based on message type - // and settings - switch (messageType) - { - case LogMessageType.Debug: - color = Settings.DebugColor; - prefix = Settings.DebugPrefix; - break; - case LogMessageType.Error: - color = Settings.ErrorColor; - prefix = Settings.ErrorPrefix; - break; - case LogMessageType.Info: - color = Settings.InfoColor; - prefix = Settings.InfoPrefix; - break; - case LogMessageType.Trace: - color = Settings.TraceColor; - prefix = Settings.TracePrefix; - break; - case LogMessageType.Warning: - color = Settings.WarnColor; - prefix = Settings.WarnPrefix; - break; - case LogMessageType.Fatal: - color = Settings.FatalColor; - prefix = Settings.FatalPrefix; - break; - default: - color = Settings.DefaultColor; - prefix = new string(' ', Settings.InfoPrefix.Length); - break; - } - - return prefix; - } - - 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(Settings.LoggingTimeFormat) - ? $" {prefix} >> {outputMessage}" - : $" {date.ToLocalTime().ToString(Settings.LoggingTimeFormat)} {prefix} >> {outputMessage}"; - } - + + #region Display the Message by Writing to the Output Queue + + // Check if we are skipping these messages to be displayed based on settings + if(!Settings.DisplayLoggingMessageType.HasFlag(messageType)) { + return; + } + + Write(messageType, sourceName, eventArgs, outputMessage, color); + #endregion - - #region Standard Public API - - #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(LogMessageType.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(LogMessageType.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(LogMessageType.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(LogMessageType.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(LogMessageType.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(LogMessageType.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(LogMessageType.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(LogMessageType.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(LogMessageType.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(LogMessageType.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(LogMessageType.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(LogMessageType.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(LogMessageType.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(LogMessageType.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(LogMessageType.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(LogMessageType.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(LogMessageType.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(LogMessageType.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, - LogMessageType 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, - LogMessageType 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(LogMessageType.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(LogMessageType.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(LogMessageType.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(LogMessageType.Trace, message, source?.FullName, obj, callerMemberName, callerFilePath, callerLineNumber); - } - - #endregion - } + } + } + + private static void Write( + LogMessageType messageType, + String sourceName, + LogMessageReceivedEventArgs eventArgs, + String outputMessage, + ConsoleColor color) { + // Select the writer based on the message type + TerminalWriters writer = IsConsolePresent + ? messageType.HasFlag(LogMessageType.Error) ? TerminalWriters.StandardError : TerminalWriters.StandardOutput + : TerminalWriters.None; + + // Set the writer to Diagnostics if appropriate (Error and Debugging data go to the Diagnostics debugger + // if it is attached at all + if(IsDebuggerAttached + && (IsConsolePresent == false || messageType.HasFlag(LogMessageType.Debug) || + messageType.HasFlag(LogMessageType.Error))) { + writer |= TerminalWriters.Diagnostics; + } + + // Check if we really need to write this out + if(writer == TerminalWriters.None) { + return; + } + + // Further format the output in the case there is an exception being logged + if(writer.HasFlag(TerminalWriters.StandardError) && eventArgs.Exception != null) { + try { + outputMessage = + $"{outputMessage}{Environment.NewLine}{eventArgs.Exception.Stringify().Indent()}"; + } catch { + // Ignore + } + } + + // Filter output messages via events + LogMessageDisplayingEventArgs displayingEventArgs = new LogMessageDisplayingEventArgs(eventArgs); + OnLogMessageDisplaying?.Invoke(sourceName, displayingEventArgs); + if(displayingEventArgs.CancelOutput == false) { + outputMessage.WriteLine(color, writer); + } + } + + private static String GetConsoleColorAndPrefix(LogMessageType messageType, out ConsoleColor color) { + String prefix; + + // Select color and prefix based on message type + // and settings + switch(messageType) { + case LogMessageType.Debug: + color = Settings.DebugColor; + prefix = Settings.DebugPrefix; + break; + case LogMessageType.Error: + color = Settings.ErrorColor; + prefix = Settings.ErrorPrefix; + break; + case LogMessageType.Info: + color = Settings.InfoColor; + prefix = Settings.InfoPrefix; + break; + case LogMessageType.Trace: + color = Settings.TraceColor; + prefix = Settings.TracePrefix; + break; + case LogMessageType.Warning: + color = Settings.WarnColor; + prefix = Settings.WarnPrefix; + break; + case LogMessageType.Fatal: + color = Settings.FatalColor; + prefix = Settings.FatalPrefix; + break; + default: + color = Settings.DefaultColor; + prefix = new String(' ', Settings.InfoPrefix.Length); + break; + } + + return prefix; + } + + 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(Settings.LoggingTimeFormat) + ? $" {prefix} >> {outputMessage}" + : $" {date.ToLocalTime().ToString(Settings.LoggingTimeFormat)} {prefix} >> {outputMessage}"; + } + + #endregion + + #region Standard Public API + + #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(LogMessageType.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(LogMessageType.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(LogMessageType.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(LogMessageType.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(LogMessageType.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(LogMessageType.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(LogMessageType.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(LogMessageType.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(LogMessageType.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(LogMessageType.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(LogMessageType.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(LogMessageType.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(LogMessageType.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(LogMessageType.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(LogMessageType.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(LogMessageType.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(LogMessageType.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(LogMessageType.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, + LogMessageType 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, + LogMessageType 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(LogMessageType.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(LogMessageType.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(LogMessageType.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(LogMessageType.Trace, message, source?.FullName, obj, callerMemberName, callerFilePath, callerLineNumber); + } + + #endregion + } } diff --git a/Unosquare.Swan.Lite/Terminal.Output.cs b/Unosquare.Swan.Lite/Terminal.Output.cs index f73ba81..8bb3ab5 100644 --- a/Unosquare.Swan.Lite/Terminal.Output.cs +++ b/Unosquare.Swan.Lite/Terminal.Output.cs @@ -1,174 +1,163 @@ -namespace Unosquare.Swan -{ - using System; - using System.Linq; - +using System; +using System.Linq; + +namespace Unosquare.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 Helper Methods + /// - /// A console terminal helper to create nicer output and receive input from the user - /// This class is thread-safe :). + /// Prints all characters in the current code page. + /// This is provided for debugging purposes only. /// - public static partial class Terminal - { - #region Helper Methods - - /// - /// Prints all characters in the current code page. - /// This is provided for debugging purposes only. - /// - public static void PrintCurrentCodePage() - { - if (!IsConsolePresent) return; - - lock (SyncLock) - { - $"Output Encoding: {OutputEncoding}".WriteLine(); - for (byte byteValue = 0; byteValue < byte.MaxValue; byteValue++) - { - var charValue = OutputEncoding.GetChars(new[] { byteValue })[0]; - - switch (byteValue) - { - case 8: // Backspace - case 9: // Tab - case 10: // Line feed - case 13: // Carriage return - charValue = '.'; - break; - } - - $"{byteValue:000} {charValue} ".Write(); - - // 7 is a beep -- Console.Beep() also works - if (byteValue == 7) " ".Write(); - - if ((byteValue + 1) % 8 == 0) - WriteLine(); - } - - WriteLine(); - } - } - - #endregion - - #region Write Methods - - /// - /// 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(this byte charCode, ConsoleColor? color = null, int count = 1, bool newLine = false, TerminalWriters writerFlags = TerminalWriters.StandardOutput) - { - lock (SyncLock) - { - var bytes = new byte[count]; - for (var i = 0; i < bytes.Length; i++) - { - bytes[i] = charCode; - } - - if (newLine) - { - var newLineBytes = OutputEncoding.GetBytes(Environment.NewLine); - bytes = bytes.Union(newLineBytes).ToArray(); - } - - var buffer = OutputEncoding.GetChars(bytes); - var context = new OutputContext - { - OutputColor = color ?? Settings.DefaultColor, - OutputText = buffer, - OutputWriters = writerFlags, - }; - - EnqueueOutput(context); - } - } - - /// - /// Writes the specified character in the default color. - /// - /// The character code. - /// The color. - /// The writer flags. - public static void Write(this char charCode, ConsoleColor? color = null, TerminalWriters writerFlags = TerminalWriters.StandardOutput) - { - lock (SyncLock) - { - var context = new OutputContext - { - OutputColor = color ?? Settings.DefaultColor, - OutputText = new[] { charCode }, - OutputWriters = writerFlags, - }; - - EnqueueOutput(context); - } - } - - /// - /// Writes the specified text in the given color. - /// - /// The text. - /// The color. - /// The writer flags. - public static void Write(this 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); - } - } - - #endregion - - #region WriteLine Methods - - /// - /// Writes a New Line Sequence to the standard output. - /// - /// The writer flags. - public static void WriteLine(TerminalWriters writerFlags = TerminalWriters.StandardOutput) - => Environment.NewLine.Write(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(this 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(this string text, ConsoleColor? color = null, TerminalWriters writerFlags = TerminalWriters.StandardOutput) - { - Write($"\r{text ?? string.Empty}", color, writerFlags); - Flush(); - CursorLeft = 0; - } - - #endregion - } + public static void PrintCurrentCodePage() { + if(!IsConsolePresent) { + return; + } + + lock(SyncLock) { + $"Output Encoding: {OutputEncoding}".WriteLine(); + for(Byte byteValue = 0; byteValue < Byte.MaxValue; byteValue++) { + Char charValue = OutputEncoding.GetChars(new[] { byteValue })[0]; + + switch(byteValue) { + case 8: // Backspace + case 9: // Tab + case 10: // Line feed + case 13: // Carriage return + charValue = '.'; + break; + } + + $"{byteValue:000} {charValue} ".Write(); + + // 7 is a beep -- Console.Beep() also works + if(byteValue == 7) { + " ".Write(); + } + + if((byteValue + 1) % 8 == 0) { + WriteLine(); + } + } + + WriteLine(); + } + } + + #endregion + + #region Write Methods + + /// + /// 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(this Byte charCode, ConsoleColor? color = null, Int32 count = 1, Boolean newLine = false, TerminalWriters writerFlags = TerminalWriters.StandardOutput) { + lock(SyncLock) { + Byte[] bytes = new Byte[count]; + for(Int32 i = 0; i < bytes.Length; i++) { + bytes[i] = charCode; + } + + if(newLine) { + Byte[] newLineBytes = OutputEncoding.GetBytes(Environment.NewLine); + bytes = bytes.Union(newLineBytes).ToArray(); + } + + Char[] buffer = OutputEncoding.GetChars(bytes); + OutputContext context = new OutputContext { + OutputColor = color ?? Settings.DefaultColor, + OutputText = buffer, + OutputWriters = writerFlags, + }; + + EnqueueOutput(context); + } + } + + /// + /// Writes the specified character in the default color. + /// + /// The character code. + /// The color. + /// The writer flags. + public static void Write(this Char charCode, ConsoleColor? color = null, TerminalWriters writerFlags = TerminalWriters.StandardOutput) { + lock(SyncLock) { + OutputContext context = new OutputContext { + OutputColor = color ?? Settings.DefaultColor, + OutputText = new[] { charCode }, + OutputWriters = writerFlags, + }; + + EnqueueOutput(context); + } + } + + /// + /// Writes the specified text in the given color. + /// + /// The text. + /// The color. + /// The writer flags. + public static void Write(this 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); + } + } + + #endregion + + #region WriteLine Methods + + /// + /// Writes a New Line Sequence to the standard output. + /// + /// The writer flags. + public static void WriteLine(TerminalWriters writerFlags = TerminalWriters.StandardOutput) + => Environment.NewLine.Write(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(this 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(this String text, ConsoleColor? color = null, TerminalWriters writerFlags = TerminalWriters.StandardOutput) { + Write($"\r{text ?? String.Empty}", color, writerFlags); + Flush(); + CursorLeft = 0; + } + + #endregion + } } \ No newline at end of file diff --git a/Unosquare.Swan.Lite/Terminal.Settings.cs b/Unosquare.Swan.Lite/Terminal.Settings.cs index d904fd2..8e00500 100644 --- a/Unosquare.Swan.Lite/Terminal.Settings.cs +++ b/Unosquare.Swan.Lite/Terminal.Settings.cs @@ -1,195 +1,190 @@ -namespace Unosquare.Swan -{ - using System; - +using System; + +namespace Unosquare.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 - { - static Settings() - { - if (IsDebuggerAttached) - { - DisplayLoggingMessageType = - LogMessageType.Debug | - LogMessageType.Error | - LogMessageType.Info | - LogMessageType.Trace | - LogMessageType.Warning | - LogMessageType.Fatal; - } - else - { - DisplayLoggingMessageType = - LogMessageType.Error | - LogMessageType.Info | - LogMessageType.Warning | - LogMessageType.Fatal; - } - - GlobalLoggingMessageType = DisplayLoggingMessageType; - } - - /// - /// Gets or sets the default output color. - /// - /// - /// The default color. - /// - public static ConsoleColor DefaultColor { get; set; } = Console.ForegroundColor; - - /// - /// 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; - - /// - /// Gets or sets the information logging prefix. - /// - /// - /// The information prefix. - /// - public static string InfoPrefix { get; set; } = "INF"; - - /// - /// 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: "; - - /// - /// 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 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 or sets the logging message types (in a bitwise mask) - /// to display in the console. - /// - /// - /// The console options. - /// - public static LogMessageType DisplayLoggingMessageType { get; set; } - - /// - /// Gets or sets the logging message types (in a bitwise mask) to global logging. - /// - /// - /// The type of the global logging message. - /// - public static LogMessageType GlobalLoggingMessageType { get; set; } - - /// - /// Gets or sets a value indicating whether [override is console present]. - /// - /// - /// true if [override is console present]; otherwise, false. - /// - public static bool OverrideIsConsolePresent { get; set; } - } - } + public static class Settings { + static Settings() { + DisplayLoggingMessageType = IsDebuggerAttached + ? LogMessageType.Debug | + LogMessageType.Error | + LogMessageType.Info | + LogMessageType.Trace | + LogMessageType.Warning | + LogMessageType.Fatal + : LogMessageType.Error | + LogMessageType.Info | + LogMessageType.Warning | + LogMessageType.Fatal; + + GlobalLoggingMessageType = DisplayLoggingMessageType; + } + + /// + /// Gets or sets the default output color. + /// + /// + /// The default color. + /// + public static ConsoleColor DefaultColor { get; set; } = Console.ForegroundColor; + + /// + /// 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; + + /// + /// Gets or sets the information logging prefix. + /// + /// + /// The information prefix. + /// + public static String InfoPrefix { get; set; } = "INF"; + + /// + /// 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: "; + + /// + /// 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 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 or sets the logging message types (in a bitwise mask) + /// to display in the console. + /// + /// + /// The console options. + /// + public static LogMessageType DisplayLoggingMessageType { + get; set; + } + + /// + /// Gets or sets the logging message types (in a bitwise mask) to global logging. + /// + /// + /// The type of the global logging message. + /// + public static LogMessageType GlobalLoggingMessageType { + get; set; + } + + /// + /// Gets or sets a value indicating whether [override is console present]. + /// + /// + /// true if [override is console present]; otherwise, false. + /// + public static Boolean OverrideIsConsolePresent { + get; set; + } + } + } } diff --git a/Unosquare.Swan.Lite/Terminal.cs b/Unosquare.Swan.Lite/Terminal.cs index ee04468..160c698 100644 --- a/Unosquare.Swan.Lite/Terminal.cs +++ b/Unosquare.Swan.Lite/Terminal.cs @@ -1,360 +1,355 @@ -namespace Unosquare.Swan -{ - using Abstractions; - using System; - using System.Collections.Concurrent; - using System.Text; - using System.Threading; - +using Unosquare.Swan.Abstractions; +using System; +using System.Collections.Concurrent; +using System.Text; +using System.Threading; + +namespace Unosquare.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) - { + static Terminal() { + lock(SyncLock) { + if(DequeueOutputTimer != null) { + return; + } + + if(IsConsolePresent) { #if !NET452 Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); #endif - 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 (Settings.OverrideIsConsolePresent) return true; - - if (_isConsolePresent == null) - { - _isConsolePresent = true; - try - { - var windowHeight = Console.WindowHeight; - _isConsolePresent = windowHeight >= 0; - } - catch - { - _isConsolePresent = false; - } - } - - return _isConsolePresent.Value; - } - } - - /// - /// 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; - - /// - /// Gets the available output writers in a bitwise mask. - /// - /// - /// The available writers. - /// - public static TerminalWriters AvailableWriters - { - get - { - var writers = TerminalWriters.None; - if (IsConsolePresent) - writers = TerminalWriters.StandardError | TerminalWriters.StandardOutput; - - if (IsDebuggerAttached) - writers = writers | TerminalWriters.Diagnostics; - - return writers; - } - } - - /// - /// 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 == false) 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); - - /// - /// 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) == false) continue; - - // Process Console output and Skip over stuff we can't display so we don't stress the output too much. - if (IsConsolePresent && (Settings.OverrideIsConsolePresent || OutputQueue.Count <= Console.BufferHeight)) - { - // Output to the standard output - if (context.OutputWriters.HasFlag(TerminalWriters.StandardOutput)) - { - Console.ForegroundColor = context.OutputColor; - Console.Out.Write(context.OutputText); - Console.ResetColor(); - Console.ForegroundColor = context.OriginalColor; - } - - // output to the standard error - if (context.OutputWriters.HasFlag(TerminalWriters.StandardError)) - { - Console.ForegroundColor = context.OutputColor; - Console.Error.Write(context.OutputText); - Console.ResetColor(); - Console.ForegroundColor = context.OriginalColor; - } - } - - // Process Debugger output - if (IsDebuggerAttached && context.OutputWriters.HasFlag(TerminalWriters.Diagnostics)) - { - System.Diagnostics.Debug.Write(new string(context.OutputText)); - } - } - } - - #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 - : IsDebuggerAttached - ? TerminalWriters.Diagnostics - : TerminalWriters.None; - } - - public ConsoleColor OriginalColor { get; } - public ConsoleColor OutputColor { get; set; } - public char[] OutputText { get; set; } - public TerminalWriters OutputWriters { get; set; } - } - - #endregion - } + 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(Settings.OverrideIsConsolePresent) { + return true; + } + + if(_isConsolePresent == null) { + _isConsolePresent = true; + try { + Int32 windowHeight = Console.WindowHeight; + _isConsolePresent = windowHeight >= 0; + } catch { + _isConsolePresent = false; + } + } + + return _isConsolePresent.Value; + } + } + + /// + /// 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; + + /// + /// Gets the available output writers in a bitwise mask. + /// + /// + /// The available writers. + /// + public static TerminalWriters AvailableWriters { + get { + TerminalWriters writers = TerminalWriters.None; + if(IsConsolePresent) { + writers = TerminalWriters.StandardError | TerminalWriters.StandardOutput; + } + + if(IsDebuggerAttached) { + writers |= TerminalWriters.Diagnostics; + } + + return writers; + } + } + + /// + /// 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 == false) { + 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); + + /// + /// 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) == false) { + continue; + } + + // Process Console output and Skip over stuff we can't display so we don't stress the output too much. + if(IsConsolePresent && (Settings.OverrideIsConsolePresent || OutputQueue.Count <= Console.BufferHeight)) { + // Output to the standard output + if(context.OutputWriters.HasFlag(TerminalWriters.StandardOutput)) { + Console.ForegroundColor = context.OutputColor; + Console.Out.Write(context.OutputText); + Console.ResetColor(); + Console.ForegroundColor = context.OriginalColor; + } + + // output to the standard error + if(context.OutputWriters.HasFlag(TerminalWriters.StandardError)) { + Console.ForegroundColor = context.OutputColor; + Console.Error.Write(context.OutputText); + Console.ResetColor(); + Console.ForegroundColor = context.OriginalColor; + } + } + + // Process Debugger output + if(IsDebuggerAttached && context.OutputWriters.HasFlag(TerminalWriters.Diagnostics)) { + System.Diagnostics.Debug.Write(new String(context.OutputText)); + } + } + } + + #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 + : IsDebuggerAttached + ? TerminalWriters.Diagnostics + : TerminalWriters.None; + } + + public ConsoleColor OriginalColor { + get; + } + public ConsoleColor OutputColor { + get; set; + } + public Char[] OutputText { + get; set; + } + public TerminalWriters OutputWriters { + get; set; + } + } + + #endregion + } }