Init...
This commit is contained in:
parent
4692422c9a
commit
6263791dff
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
/.vs
|
||||
/Swan/obj
|
||||
/Swan/bin
|
||||
/Swan.Lite/bin
|
||||
/Swan.Lite/obj
|
||||
/Unosquare.RaspberryIO/bin
|
||||
/Unosquare.RaspberryIO/obj
|
||||
/Unosquare.RaspberryIO.Abstractions/bin
|
||||
/Unosquare.RaspberryIO.Abstractions/obj
|
||||
/Unosquare.WiringPi/bin
|
||||
/Unosquare.WiringPi/obj
|
31
LICENSE
Normal file
31
LICENSE
Normal file
@ -0,0 +1,31 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016 Unosquare
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
This software contains a compiled, unmodified version of the WiringPi library
|
||||
WiringPi is a GPIO access library written in C for the BCM2835 used in the
|
||||
Raspberry Pi. It’s released under the GNU LGPLv3 license and is usable from C
|
||||
and C++ and many other languages with suitable wrappers. A program that contains
|
||||
no derivative of any portion of the Library, but is designed to work with
|
||||
the Library by being compiled or linked with it, is called a "work that uses
|
||||
the Library". Such a work, in isolation, is not a derivative work of the Library,
|
||||
and therefore falls outside the scope of this License. Raspberry IO is then,
|
||||
by definition, "work that uses the Library"
|
@ -1,2 +1,5 @@
|
||||
# RaspberryIO_26
|
||||
|
||||
# UnoSquare
|
||||
## RaspberryIO
|
||||
Based on [Unosquare.RaspberryIO v0.26.0](https://github.com/unosquare/raspberryio)
|
||||
## SWAN
|
||||
Based on https://github.com/unosquare/swan
|
52
Swan.Lite/Collections/CollectionCacheRepository.cs
Normal file
52
Swan.Lite/Collections/CollectionCacheRepository.cs
Normal file
@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Swan.Collections
|
||||
{
|
||||
/// <summary>
|
||||
/// A thread-safe collection cache repository for types.
|
||||
/// </summary>
|
||||
/// <typeparam name="TValue">The type of member to cache.</typeparam>
|
||||
public class CollectionCacheRepository<TValue>
|
||||
{
|
||||
private readonly Lazy<ConcurrentDictionary<Type, IEnumerable<TValue>>> _data =
|
||||
new Lazy<ConcurrentDictionary<Type, IEnumerable<TValue>>>(() =>
|
||||
new ConcurrentDictionary<Type, IEnumerable<TValue>>(), true);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the cache contains the specified key.
|
||||
/// </summary>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <returns><c>true</c> if the cache contains the key, otherwise <c>false</c>.</returns>
|
||||
public bool ContainsKey(Type key) => _data.Value.ContainsKey(key);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <param name="factory">The factory.</param>
|
||||
/// <returns>
|
||||
/// An array of the properties stored for the specified type.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// key
|
||||
/// or
|
||||
/// factory.
|
||||
/// </exception>
|
||||
/// <exception cref="System.ArgumentNullException">type.</exception>
|
||||
public IEnumerable<TValue> Retrieve(Type key, Func<Type, IEnumerable<TValue>> factory)
|
||||
{
|
||||
if (key == null)
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
|
||||
if (factory == null)
|
||||
throw new ArgumentNullException(nameof(factory));
|
||||
|
||||
return _data.Value.GetOrAdd(key, k => factory.Invoke(k).Where(item => item != null));
|
||||
}
|
||||
}
|
||||
}
|
76
Swan.Lite/Collections/ComponentCollection`1.cs
Normal file
76
Swan.Lite/Collections/ComponentCollection`1.cs
Normal file
@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Swan.Configuration;
|
||||
|
||||
namespace Swan.Collections
|
||||
{
|
||||
/// <summary>
|
||||
/// <para>Implements a collection of components.</para>
|
||||
/// <para>Each component in the collection may be given a unique name for later retrieval.</para>
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of components in the collection.</typeparam>
|
||||
/// <seealso cref="IComponentCollection{T}" />
|
||||
public class ComponentCollection<T> : ConfiguredObject, IComponentCollection<T>
|
||||
{
|
||||
private readonly List<T> _components = new List<T>();
|
||||
|
||||
private readonly List<(string, T)> _componentsWithSafeNames = new List<(string, T)>();
|
||||
|
||||
private readonly Dictionary<string, T> _namedComponents = new Dictionary<string, T>();
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Count => _components.Count;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyDictionary<string, T> Named => _namedComponents;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<(string SafeName, T Component)> WithSafeNames => _componentsWithSafeNames;
|
||||
|
||||
/// <inheritdoc />
|
||||
public T this[int index] => _components[index];
|
||||
|
||||
/// <inheritdoc />
|
||||
public T this[string key] => _namedComponents[key];
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerator<T> GetEnumerator() => _components.GetEnumerator();
|
||||
|
||||
/// <inheritdoc />
|
||||
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_components).GetEnumerator();
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <exception cref="InvalidOperationException">The collection is locked.</exception>
|
||||
public void Add(string name, T component)
|
||||
{
|
||||
EnsureConfigurationNotLocked();
|
||||
|
||||
if (name != null)
|
||||
{
|
||||
if (name.Length == 0)
|
||||
throw new ArgumentException("Component name is empty.", nameof(name));
|
||||
|
||||
if (_namedComponents.ContainsKey(name))
|
||||
throw new ArgumentException("Duplicate component name.", nameof(name));
|
||||
}
|
||||
|
||||
if (component == null)
|
||||
throw new ArgumentNullException(nameof(component));
|
||||
|
||||
if (_components.Contains(component))
|
||||
throw new ArgumentException("Component has already been added.", nameof(component));
|
||||
|
||||
_components.Add(component);
|
||||
_componentsWithSafeNames.Add((name ?? $"<{component.GetType().Name}>", component));
|
||||
|
||||
if (name != null)
|
||||
_namedComponents.Add(name, component);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Locks the collection, preventing further additions.
|
||||
/// </summary>
|
||||
public void Lock() => LockConfiguration();
|
||||
}
|
||||
}
|
304
Swan.Lite/Collections/ConcurrentDataDictionary`2.cs
Normal file
304
Swan.Lite/Collections/ConcurrentDataDictionary`2.cs
Normal file
@ -0,0 +1,304 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Swan.Collections
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a thread-safe collection of key/value pairs that does not store null values
|
||||
/// and can be accessed by multiple threads concurrently.
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">The type of keys in the dictionary. This must be a reference type.</typeparam>
|
||||
/// <typeparam name="TValue">The type of values in the dictionary. This must be a reference type.</typeparam>
|
||||
/// <seealso cref="IDataDictionary{TKey,TValue}"/>
|
||||
public sealed class ConcurrentDataDictionary<TKey, TValue> : IDataDictionary<TKey, TValue>
|
||||
where TKey : class
|
||||
where TValue : class
|
||||
{
|
||||
#region Private data
|
||||
|
||||
private readonly ConcurrentDictionary<TKey, TValue> _dictionary;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Instance management
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConcurrentDataDictionary{TKey,TValue}"/> class
|
||||
/// that is empty, has the default concurrency level, has the default initial capacity,
|
||||
/// and uses the default comparer for <typeparamref name="TKey"/>.
|
||||
/// </summary>
|
||||
/// <see cref="ConcurrentDictionary{TKey,TValue}()"/>
|
||||
public ConcurrentDataDictionary()
|
||||
{
|
||||
_dictionary = new ConcurrentDictionary<TKey, TValue>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConcurrentDataDictionary{TKey,TValue}"/> class
|
||||
/// that contains elements copied from the specified <see cref="IEnumerable{T}"/>, has the default concurrency level,
|
||||
/// has the default initial capacity, and uses the default comparer for <typeparamref name="TKey"/>.
|
||||
/// </summary>
|
||||
/// <param name="collection">The <see cref="IEnumerable{T}"/> whose elements are copied
|
||||
/// to the new <see cref="ConcurrentDataDictionary{TKey,TValue}"/>.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="collection"/> is <see langword="null"/>.</exception>
|
||||
/// <remarks>
|
||||
/// <para>Since <see cref="ConcurrentDataDictionary{TKey,TValue}"/> does not store null values,
|
||||
/// key/value pairs whose value is <see langword="null"/> will not be copied from <paramref name="collection"/>.</para>
|
||||
/// </remarks>
|
||||
/// <see cref="ConcurrentDictionary{TKey,TValue}(IEnumerable{KeyValuePair{TKey,TValue}})"/>
|
||||
public ConcurrentDataDictionary(IEnumerable<KeyValuePair<TKey, TValue>> collection)
|
||||
{
|
||||
if (collection == null)
|
||||
throw new ArgumentNullException(nameof(collection));
|
||||
|
||||
_dictionary = new ConcurrentDictionary<TKey, TValue>(collection.Where(pair => pair.Value != null));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConcurrentDataDictionary{TKey,TValue}"/> class
|
||||
/// that is empty, has the default concurrency level and capacity, and uses the specified <see cref="IEqualityComparer{T}"/>.
|
||||
/// </summary>
|
||||
/// <param name="comparer">The equality comparison implementation to use when comparing keys.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="comparer"/> is <see langword="null"/>.</exception>
|
||||
/// <see cref="ConcurrentDictionary{TKey,TValue}(IEqualityComparer{TKey})"/>
|
||||
public ConcurrentDataDictionary(IEqualityComparer<TKey> comparer)
|
||||
{
|
||||
_dictionary = new ConcurrentDictionary<TKey, TValue>(comparer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConcurrentDataDictionary{TKey, TValue}"/> class
|
||||
/// that contains elements copied from the specified <see cref="IEnumerable{T}"/>, has the default concurrency level,
|
||||
/// has the default initial capacity, and uses the specified <see cref="IEqualityComparer{T}"/>.
|
||||
/// </summary>
|
||||
/// <param name="collection">The <see cref="IEnumerable{T}"/> whose elements are copied
|
||||
/// to the new <see cref="ConcurrentDataDictionary{TKey,TValue}"/>.</param>
|
||||
/// <param name="comparer">The equality comparison implementation to use when comparing keys.</param>
|
||||
/// <remarks>
|
||||
/// <para>Since <see cref="ConcurrentDataDictionary{TKey,TValue}"/> does not store null values,
|
||||
/// key/value pairs whose value is <see langword="null"/> will not be copied from <paramref name="collection"/>.</para>
|
||||
/// </remarks>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// <para><paramref name="collection"/> is <see langword="null"/>.</para>
|
||||
/// <para>- or -.</para>
|
||||
/// <para><paramref name="comparer"/> is <see langword="null"/>.</para>
|
||||
/// </exception>
|
||||
/// <see cref="ConcurrentDictionary{TKey,TValue}(IEnumerable{KeyValuePair{TKey,TValue}},IEqualityComparer{TKey})"/>
|
||||
public ConcurrentDataDictionary(IEnumerable<KeyValuePair<TKey, TValue>> collection, IEqualityComparer<TKey> comparer)
|
||||
{
|
||||
if (collection == null)
|
||||
throw new ArgumentNullException(nameof(collection));
|
||||
|
||||
_dictionary = new ConcurrentDictionary<TKey, TValue>(collection.Where(pair => pair.Value != null), comparer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConcurrentDataDictionary{TKey, TValue}"/> class
|
||||
/// that is empty, has the specified concurrency level and capacity, and uses the default comparer for the key type.
|
||||
/// </summary>
|
||||
/// <param name="concurrencyLevel">The estimated number of threads that will update
|
||||
/// the <see cref="ConcurrentDataDictionary{TKey, TValue}"/> concurrently.</param>
|
||||
/// <param name="capacity">The initial number of elements that the <see cref="ConcurrentDataDictionary{TKey, TValue}"/> can contain.</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException">
|
||||
/// <para><paramref name="concurrencyLevel"/> is less than 1.</para>
|
||||
/// <para>- or -.</para>
|
||||
/// <para><paramref name="capacity"/> is less than 0.</para>
|
||||
/// </exception>
|
||||
/// <see cref="ConcurrentDictionary{TKey,TValue}(int,int)"/>
|
||||
public ConcurrentDataDictionary(int concurrencyLevel, int capacity)
|
||||
{
|
||||
_dictionary = new ConcurrentDictionary<TKey, TValue>(concurrencyLevel, capacity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConcurrentDataDictionary{TKey, TValue}"/> class
|
||||
/// that contains elements copied from the specified <see cref="IEnumerable{T}"/>, has the specified concurrency level,
|
||||
/// has the default initial capacity, and uses the specified <see cref="IEqualityComparer{T}"/>.
|
||||
/// </summary>
|
||||
/// <param name="concurrencyLevel">The estimated number of threads that will update
|
||||
/// the <see cref="ConcurrentDataDictionary{TKey, TValue}"/> concurrently.</param>
|
||||
/// <param name="collection">The <see cref="IEnumerable{T}"/> whose elements are copied
|
||||
/// to the new <see cref="ConcurrentDataDictionary{TKey,TValue}"/>.</param>
|
||||
/// <param name="comparer">The equality comparison implementation to use when comparing keys.</param>
|
||||
/// <remarks>
|
||||
/// <para>Since <see cref="ConcurrentDataDictionary{TKey,TValue}"/> does not store null values,
|
||||
/// key/value pairs whose value is <see langword="null"/> will not be copied from <paramref name="collection"/>.</para>
|
||||
/// </remarks>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// <para><paramref name="collection"/> is <see langword="null"/>.</para>
|
||||
/// <para>- or -.</para>
|
||||
/// <para><paramref name="comparer"/> is <see langword="null"/>.</para>
|
||||
/// </exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException"><paramref name="concurrencyLevel"/> is less than 1.</exception>
|
||||
/// <see cref="ConcurrentDictionary{TKey,TValue}(int,IEnumerable{KeyValuePair{TKey,TValue}},IEqualityComparer{TKey})"/>
|
||||
public ConcurrentDataDictionary(int concurrencyLevel, IEnumerable<KeyValuePair<TKey, TValue>> collection, IEqualityComparer<TKey> comparer)
|
||||
{
|
||||
if (collection == null)
|
||||
throw new ArgumentNullException(nameof(collection));
|
||||
|
||||
_dictionary = new ConcurrentDictionary<TKey, TValue>(
|
||||
concurrencyLevel,
|
||||
collection.Where(pair => pair.Value != null),
|
||||
comparer);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public APIs
|
||||
|
||||
/// <inheritdoc cref="IDataDictionary{TKey,TValue}.Count"/>
|
||||
public int Count => _dictionary.Count;
|
||||
|
||||
/// <inheritdoc cref="IDataDictionary{TKey,TValue}.IsEmpty"/>
|
||||
public bool IsEmpty => _dictionary.IsEmpty;
|
||||
|
||||
/// <inheritdoc cref="IDictionary{TKey,TValue}.Keys"/>
|
||||
public ICollection<TKey> Keys => _dictionary.Keys;
|
||||
|
||||
/// <inheritdoc cref="IDictionary{TKey,TValue}.Values"/>
|
||||
public ICollection<TValue> Values => _dictionary.Values;
|
||||
|
||||
/// <inheritdoc cref="IDataDictionary{TKey,TValue}.this"/>
|
||||
public TValue? this[TKey key]
|
||||
{
|
||||
get => _dictionary.TryGetValue(key ?? throw new ArgumentNullException(nameof(key)), out var value) ? value : null;
|
||||
set
|
||||
{
|
||||
if (value != null)
|
||||
{
|
||||
_dictionary[key] = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
_dictionary.TryRemove(key, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IDataDictionary{TKey,TValue}.Clear"/>
|
||||
public void Clear() => _dictionary.Clear();
|
||||
|
||||
/// <inheritdoc cref="IDataDictionary{TKey,TValue}.ContainsKey"/>
|
||||
public bool ContainsKey(TKey key) => _dictionary.ContainsKey(key);
|
||||
|
||||
/// <inheritdoc cref="ConcurrentDictionary{TKey,TValue}.GetOrAdd(TKey,TValue)"/>
|
||||
public TValue? GetOrAdd(TKey key, TValue value)
|
||||
{
|
||||
if (key == null)
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
|
||||
if (value != null)
|
||||
return _dictionary.GetOrAdd(key, value);
|
||||
|
||||
return _dictionary.TryGetValue(key, out var retrievedValue) ? retrievedValue : null;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IDictionary{TKey,TValue}.Remove(TKey)"/>
|
||||
public bool Remove(TKey key) => _dictionary.TryRemove(key, out _);
|
||||
|
||||
/// <inheritdoc cref="ConcurrentDictionary{TKey,TValue}.TryAdd"/>
|
||||
public bool TryAdd(TKey key, TValue value)
|
||||
{
|
||||
if (key == null)
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
|
||||
return value == null || _dictionary.TryAdd(key, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IDataDictionary{TKey,TValue}.TryGetValue"/>
|
||||
public bool TryGetValue(TKey key, out TValue value) => _dictionary.TryGetValue(key, out value);
|
||||
|
||||
/// <inheritdoc cref="IDataDictionary{TKey,TValue}.TryRemove"/>
|
||||
public bool TryRemove(TKey key, out TValue value) => _dictionary.TryRemove(key, out value);
|
||||
|
||||
/// <inheritdoc cref="ConcurrentDictionary{TKey,TValue}.TryUpdate"/>
|
||||
public bool TryUpdate(TKey key, TValue newValue, TValue comparisonValue)
|
||||
{
|
||||
if (key == null)
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
|
||||
return newValue != null && comparisonValue != null && _dictionary.TryUpdate(key, newValue, comparisonValue);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implementation of IDictionary<TKey, TValue>
|
||||
|
||||
/// <inheritdoc cref="IDictionary{TKey,TValue}.Add(TKey,TValue)"/>
|
||||
void IDictionary<TKey, TValue>.Add(TKey key, TValue value)
|
||||
{
|
||||
if (value != null)
|
||||
{
|
||||
((IDictionary<TKey, TValue>)_dictionary).Add(key, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
_dictionary.TryRemove(key, out _);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implementation of IReadOnlyDictionary<TKey, TValue>
|
||||
|
||||
/// <inheritdoc cref="IReadOnlyDictionary{TKey,TValue}.Keys"/>
|
||||
IEnumerable<TKey> IReadOnlyDictionary<TKey, TValue>.Keys => _dictionary.Keys;
|
||||
|
||||
/// <inheritdoc cref="IReadOnlyDictionary{TKey,TValue}.Values"/>
|
||||
IEnumerable<TValue> IReadOnlyDictionary<TKey, TValue>.Values => _dictionary.Values;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implementation of ICollection<KeyValuePair<TKey, TValue>>
|
||||
|
||||
/// <inheritdoc cref="ICollection{T}.IsReadOnly"/>
|
||||
/// <remarks>
|
||||
/// <para>This property is always <see langword="false"/> for a <see cref="ConcurrentDataDictionary{TKey,TValue}"/>.</para>
|
||||
/// </remarks>
|
||||
bool ICollection<KeyValuePair<TKey, TValue>>.IsReadOnly => false;
|
||||
|
||||
/// <inheritdoc cref="ICollection{T}.Add"/>
|
||||
void ICollection<KeyValuePair<TKey, TValue>>.Add(KeyValuePair<TKey, TValue> item)
|
||||
{
|
||||
if (item.Value != null)
|
||||
{
|
||||
((ICollection<KeyValuePair<TKey, TValue>>)_dictionary).Add(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
_dictionary.TryRemove(item.Key, out _);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ICollection{T}.Contains"/>
|
||||
bool ICollection<KeyValuePair<TKey, TValue>>.Contains(KeyValuePair<TKey, TValue> item)
|
||||
=> ((ICollection<KeyValuePair<TKey, TValue>>)_dictionary).Contains(item);
|
||||
|
||||
/// <inheritdoc cref="ICollection{T}.CopyTo"/>
|
||||
void ICollection<KeyValuePair<TKey, TValue>>.CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
|
||||
=> ((ICollection<KeyValuePair<TKey, TValue>>)_dictionary).CopyTo(array, arrayIndex);
|
||||
|
||||
/// <inheritdoc cref="ICollection{T}.Remove"/>
|
||||
bool ICollection<KeyValuePair<TKey, TValue>>.Remove(KeyValuePair<TKey, TValue> item)
|
||||
=> ((ICollection<KeyValuePair<TKey, TValue>>)_dictionary).Remove(item);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implementation of IEnumerable<KeyValuePair<TKey, TValue>>
|
||||
|
||||
/// <inheritdoc cref="IEnumerable{T}.GetEnumerator"/>
|
||||
IEnumerator<KeyValuePair<TKey, TValue>> IEnumerable<KeyValuePair<TKey, TValue>>.GetEnumerator() => _dictionary.GetEnumerator();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implementation of IEnumerable
|
||||
|
||||
/// <inheritdoc cref="IEnumerable.GetEnumerator"/>
|
||||
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_dictionary).GetEnumerator();
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
341
Swan.Lite/Collections/DataDictionary`2.cs
Normal file
341
Swan.Lite/Collections/DataDictionary`2.cs
Normal file
@ -0,0 +1,341 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Swan.Collections
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a non-thread-safe collection of key/value pairs that does not store null values.
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">The type of keys in the dictionary. This must be a reference type.</typeparam>
|
||||
/// <typeparam name="TValue">The type of values in the dictionary. This must be a reference type.</typeparam>
|
||||
/// <seealso cref="IDataDictionary{TKey,TValue}"/>
|
||||
public sealed class DataDictionary<TKey, TValue> : IDataDictionary<TKey, TValue>
|
||||
where TKey : class
|
||||
where TValue : class
|
||||
{
|
||||
#region Private data
|
||||
|
||||
private readonly Dictionary<TKey, TValue> _dictionary;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Instance management
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DataDictionary{TKey,TValue}"/> class
|
||||
/// that is empty, has the default initial capacity,
|
||||
/// and uses the default comparer for <typeparamref name="TKey"/>.
|
||||
/// </summary>
|
||||
/// <see cref="Dictionary{TKey,TValue}()"/>
|
||||
public DataDictionary()
|
||||
{
|
||||
_dictionary = new Dictionary<TKey, TValue>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DataDictionary{TKey,TValue}"/> class
|
||||
/// that contains elements copied from the specified <see cref="IEnumerable{T}"/>,
|
||||
/// has the default initial capacity, and uses the default comparer for <typeparamref name="TKey"/>.
|
||||
/// </summary>
|
||||
/// <param name="collection">The <see cref="IEnumerable{T}"/> whose elements are copied
|
||||
/// to the new <see cref="DataDictionary{TKey,TValue}"/>.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="collection"/> is <see langword="null"/>.</exception>
|
||||
/// <remarks>
|
||||
/// <para>Since <see cref="DataDictionary{TKey,TValue}"/> does not store null values,
|
||||
/// key/value pairs whose value is <see langword="null"/> will not be copied from <paramref name="collection"/>.</para>
|
||||
/// </remarks>
|
||||
/// <see cref="Dictionary{TKey,TValue}()"/>
|
||||
public DataDictionary(IEnumerable<KeyValuePair<TKey, TValue>> collection)
|
||||
{
|
||||
if (collection == null)
|
||||
throw new ArgumentNullException(nameof(collection));
|
||||
|
||||
_dictionary = new Dictionary<TKey, TValue>();
|
||||
foreach (var pair in collection.Where(pair => pair.Value != null))
|
||||
{
|
||||
_dictionary.Add(pair.Key, pair.Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DataDictionary{TKey,TValue}"/> class
|
||||
/// that is empty, has the default capacity, and uses the specified <see cref="IEqualityComparer{T}"/>.
|
||||
/// </summary>
|
||||
/// <param name="comparer">The equality comparison implementation to use when comparing keys.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="comparer"/> is <see langword="null"/>.</exception>
|
||||
/// <see cref="Dictionary{TKey,TValue}(IEqualityComparer{TKey})"/>
|
||||
public DataDictionary(IEqualityComparer<TKey> comparer)
|
||||
{
|
||||
_dictionary = new Dictionary<TKey, TValue>(comparer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DataDictionary{TKey, TValue}"/> class
|
||||
/// that contains elements copied from the specified <see cref="IEnumerable{T}"/>,
|
||||
/// has the default initial capacity, and uses the specified <see cref="IEqualityComparer{T}"/>.
|
||||
/// </summary>
|
||||
/// <param name="collection">The <see cref="IEnumerable{T}"/> whose elements are copied
|
||||
/// to the new <see cref="DataDictionary{TKey,TValue}"/>.</param>
|
||||
/// <param name="comparer">The equality comparison implementation to use when comparing keys.</param>
|
||||
/// <remarks>
|
||||
/// <para>Since <see cref="DataDictionary{TKey,TValue}"/> does not store null values,
|
||||
/// key/value pairs whose value is <see langword="null"/> will not be copied from <paramref name="collection"/>.</para>
|
||||
/// </remarks>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// <para><paramref name="collection"/> is <see langword="null"/>.</para>
|
||||
/// <para>- or -.</para>
|
||||
/// <para><paramref name="comparer"/> is <see langword="null"/>.</para>
|
||||
/// </exception>
|
||||
/// <see cref="Dictionary{TKey,TValue}(IEqualityComparer{TKey})"/>
|
||||
public DataDictionary(IEnumerable<KeyValuePair<TKey, TValue>> collection, IEqualityComparer<TKey> comparer)
|
||||
{
|
||||
if (collection == null)
|
||||
throw new ArgumentNullException(nameof(collection));
|
||||
|
||||
_dictionary = new Dictionary<TKey, TValue>(comparer);
|
||||
foreach (var pair in collection.Where(pair => pair.Value != null))
|
||||
{
|
||||
_dictionary.Add(pair.Key, pair.Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DataDictionary{TKey, TValue}"/> class
|
||||
/// that is empty, has the specified capacity, and uses the default comparer for the key type.
|
||||
/// </summary>
|
||||
/// <param name="capacity">The initial number of elements that the <see cref="DataDictionary{TKey, TValue}"/> can contain.</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException"><paramref name="capacity"/> is less than 0.</exception>
|
||||
/// <see cref="Dictionary{TKey,TValue}(int)"/>
|
||||
public DataDictionary(int capacity)
|
||||
{
|
||||
_dictionary = new Dictionary<TKey, TValue>(capacity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DataDictionary{TKey, TValue}"/> class
|
||||
/// that contains elements copied from the specified <see cref="IEnumerable{T}"/>,
|
||||
/// has the specified capacity, and uses the specified <see cref="IEqualityComparer{T}"/>.
|
||||
/// </summary>
|
||||
/// <param name="capacity">The initial number of elements that the <see cref="DataDictionary{TKey, TValue}"/> can contain.</param>
|
||||
/// <param name="collection">The <see cref="IEnumerable{T}"/> whose elements are copied
|
||||
/// to the new <see cref="ConcurrentDataDictionary{TKey,TValue}"/>.</param>
|
||||
/// <param name="comparer">The equality comparison implementation to use when comparing keys.</param>
|
||||
/// <remarks>
|
||||
/// <para>Since <see cref="ConcurrentDataDictionary{TKey,TValue}"/> does not store null values,
|
||||
/// key/value pairs whose value is <see langword="null"/> will not be copied from <paramref name="collection"/>.</para>
|
||||
/// </remarks>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// <para><paramref name="collection"/> is <see langword="null"/>.</para>
|
||||
/// <para>- or -.</para>
|
||||
/// <para><paramref name="comparer"/> is <see langword="null"/>.</para>
|
||||
/// </exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException"><paramref name="capacity"/> is less than 0.</exception>
|
||||
/// <see cref="Dictionary{TKey,TValue}(int,IEqualityComparer{TKey})"/>
|
||||
public DataDictionary(int capacity, IEnumerable<KeyValuePair<TKey, TValue>> collection, IEqualityComparer<TKey> comparer)
|
||||
{
|
||||
if (collection == null)
|
||||
throw new ArgumentNullException(nameof(collection));
|
||||
|
||||
_dictionary = new Dictionary<TKey, TValue>(capacity, comparer);
|
||||
foreach (var pair in collection.Where(pair => pair.Value != null))
|
||||
{
|
||||
_dictionary.Add(pair.Key, pair.Value);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public APIs
|
||||
|
||||
/// <inheritdoc cref="IDataDictionary{TKey,TValue}.Count"/>
|
||||
public int Count => _dictionary.Count;
|
||||
|
||||
/// <inheritdoc cref="IDataDictionary{TKey,TValue}.IsEmpty"/>
|
||||
public bool IsEmpty => _dictionary.Count == 0;
|
||||
|
||||
/// <inheritdoc cref="IDictionary{TKey,TValue}.Keys"/>
|
||||
public ICollection<TKey> Keys => _dictionary.Keys;
|
||||
|
||||
/// <inheritdoc cref="IDictionary{TKey,TValue}.Values"/>
|
||||
public ICollection<TValue> Values => _dictionary.Values;
|
||||
|
||||
/// <inheritdoc cref="IDataDictionary{TKey,TValue}.this"/>
|
||||
public TValue? this[TKey key]
|
||||
{
|
||||
get => _dictionary.TryGetValue(key ?? throw new ArgumentNullException(nameof(key)), out var value) ? value : null;
|
||||
set
|
||||
{
|
||||
if (value != null)
|
||||
{
|
||||
_dictionary[key] = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
_dictionary.Remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IDataDictionary{TKey,TValue}.Clear"/>
|
||||
public void Clear() => _dictionary.Clear();
|
||||
|
||||
/// <inheritdoc cref="IDataDictionary{TKey,TValue}.ContainsKey"/>
|
||||
public bool ContainsKey(TKey key)
|
||||
{
|
||||
// _dictionary.ContainsKey will take care of throwing on a null key.
|
||||
return _dictionary.ContainsKey(key);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ConcurrentDictionary{TKey,TValue}.GetOrAdd(TKey,TValue)"/>
|
||||
public TValue? GetOrAdd(TKey key, TValue value)
|
||||
{
|
||||
// _dictionary.TryGetValue will take care of throwing on a null key.
|
||||
if (_dictionary.TryGetValue(key, out var result))
|
||||
return result;
|
||||
|
||||
if (value == null)
|
||||
return null;
|
||||
|
||||
_dictionary.Add(key, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IDictionary{TKey,TValue}.Remove(TKey)"/>
|
||||
public bool Remove(TKey key)
|
||||
{
|
||||
// _dictionary.Remove will take care of throwing on a null key.
|
||||
return _dictionary.Remove(key);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ConcurrentDictionary{TKey,TValue}.TryAdd"/>
|
||||
public bool TryAdd(TKey key, TValue value)
|
||||
{
|
||||
// _dictionary.ContainsKey will take care of throwing on a null key.
|
||||
if (_dictionary.ContainsKey(key))
|
||||
return false;
|
||||
|
||||
if (value != null)
|
||||
_dictionary.Add(key, value);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IDataDictionary{TKey,TValue}.TryGetValue"/>
|
||||
public bool TryGetValue(TKey key, out TValue value) => _dictionary.TryGetValue(key, out value);
|
||||
|
||||
/// <inheritdoc cref="IDataDictionary{TKey,TValue}.TryRemove"/>
|
||||
public bool TryRemove(TKey key, out TValue value)
|
||||
{
|
||||
// TryGetValue will take care of throwing on a null key.
|
||||
if (!_dictionary.TryGetValue(key, out value))
|
||||
return false;
|
||||
|
||||
_dictionary.Remove(key);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ConcurrentDictionary{TKey,TValue}.TryUpdate"/>
|
||||
public bool TryUpdate(TKey key, TValue newValue, TValue comparisonValue)
|
||||
{
|
||||
// TryGetValue will take care of throwing on a null key.
|
||||
if (!_dictionary.TryGetValue(key, out var value))
|
||||
return false;
|
||||
|
||||
if (value != comparisonValue)
|
||||
return false;
|
||||
|
||||
_dictionary[key] = newValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implementation of IDictionary<TKey, TValue>
|
||||
|
||||
/// <inheritdoc cref="IDictionary{TKey,TValue}.Add(TKey,TValue)"/>
|
||||
void IDictionary<TKey, TValue>.Add(TKey key, TValue value)
|
||||
{
|
||||
// Validating the key seems redundant, because both Add and Remove
|
||||
// will throw on a null key.
|
||||
// This way, though, the code path on null key does not depend on value.
|
||||
// Without this validation, there should be two unit tests for null key,
|
||||
// one with a null value and one with a non-null value,
|
||||
// which makes no sense.
|
||||
if (key == null)
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
|
||||
if (value != null)
|
||||
{
|
||||
_dictionary.Add(key, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
_dictionary.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implementation of IReadOnlyDictionary<TKey, TValue>
|
||||
|
||||
/// <inheritdoc cref="IReadOnlyDictionary{TKey,TValue}.Keys"/>
|
||||
IEnumerable<TKey> IReadOnlyDictionary<TKey, TValue>.Keys => _dictionary.Keys;
|
||||
|
||||
/// <inheritdoc cref="IReadOnlyDictionary{TKey,TValue}.Values"/>
|
||||
IEnumerable<TValue> IReadOnlyDictionary<TKey, TValue>.Values => _dictionary.Values;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implementation of ICollection<KeyValuePair<TKey, TValue>>
|
||||
|
||||
/// <inheritdoc cref="ICollection{T}.IsReadOnly"/>
|
||||
/// <remarks>
|
||||
/// <para>This property is always <see langword="false"/> for a <see cref="DataDictionary{TKey,TValue}"/>.</para>
|
||||
/// </remarks>
|
||||
bool ICollection<KeyValuePair<TKey, TValue>>.IsReadOnly => false;
|
||||
|
||||
/// <inheritdoc cref="ICollection{T}.Add"/>
|
||||
void ICollection<KeyValuePair<TKey, TValue>>.Add(KeyValuePair<TKey, TValue> item)
|
||||
{
|
||||
if (item.Value != null)
|
||||
{
|
||||
((ICollection<KeyValuePair<TKey, TValue>>)_dictionary).Add(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
_dictionary.Remove(item.Key);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ICollection{T}.Contains"/>
|
||||
bool ICollection<KeyValuePair<TKey, TValue>>.Contains(KeyValuePair<TKey, TValue> item)
|
||||
=> ((ICollection<KeyValuePair<TKey, TValue>>)_dictionary).Contains(item);
|
||||
|
||||
/// <inheritdoc cref="ICollection{T}.CopyTo"/>
|
||||
void ICollection<KeyValuePair<TKey, TValue>>.CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
|
||||
=> ((ICollection<KeyValuePair<TKey, TValue>>)_dictionary).CopyTo(array, arrayIndex);
|
||||
|
||||
/// <inheritdoc cref="ICollection{T}.Remove"/>
|
||||
bool ICollection<KeyValuePair<TKey, TValue>>.Remove(KeyValuePair<TKey, TValue> item)
|
||||
=> ((ICollection<KeyValuePair<TKey, TValue>>) _dictionary).Remove(item);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implementation of IEnumerable<KeyValuePair<TKey, TValue>>
|
||||
|
||||
/// <inheritdoc cref="IEnumerable{T}.GetEnumerator"/>
|
||||
IEnumerator<KeyValuePair<TKey, TValue>> IEnumerable<KeyValuePair<TKey, TValue>>.GetEnumerator() => _dictionary.GetEnumerator();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implementation of IEnumerable
|
||||
|
||||
/// <inheritdoc cref="IEnumerable.GetEnumerator"/>
|
||||
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_dictionary).GetEnumerator();
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
49
Swan.Lite/Collections/DisposableComponentCollection`1.cs
Normal file
49
Swan.Lite/Collections/DisposableComponentCollection`1.cs
Normal file
@ -0,0 +1,49 @@
|
||||
using System;
|
||||
|
||||
namespace Swan.Collections
|
||||
{
|
||||
/// <summary>
|
||||
/// <para>Implements a collection of components that automatically disposes each component
|
||||
/// implementing <see cref="IDisposable"/>.</para>
|
||||
/// <para>Each component in the collection may be given a unique name for later retrieval.</para>
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of components in the collection.</typeparam>
|
||||
/// <seealso cref="ComponentCollection{T}" />
|
||||
/// <seealso cref="IComponentCollection{T}" />
|
||||
public class DisposableComponentCollection<T> : ComponentCollection<T>, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Finalizes an instance of the <see cref="DisposableComponentCollection{T}"/> class.
|
||||
/// </summary>
|
||||
~DisposableComponentCollection()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
/// <param name="disposing">
|
||||
/// <see langword="true"/> to release both managed and unmanaged resources; <see langword="true"/> to release only unmanaged resources.
|
||||
/// </param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposing) return;
|
||||
|
||||
foreach (var component in this)
|
||||
{
|
||||
if (component is IDisposable disposable)
|
||||
disposable.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
54
Swan.Lite/Collections/IComponentCollection`1.cs
Normal file
54
Swan.Lite/Collections/IComponentCollection`1.cs
Normal file
@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Swan.Collections
|
||||
{
|
||||
/// <summary>
|
||||
/// <para>Represents a collection of components.</para>
|
||||
/// <para>Each component in the collection may be given a unique name for later retrieval.</para>
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of components in the collection.</typeparam>
|
||||
public interface IComponentCollection<T> : IReadOnlyList<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets an <see cref="IReadOnlyDictionary{TKey,TValue}"/> interface representing the named components.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The named components.
|
||||
/// </value>
|
||||
IReadOnlyDictionary<string, T> Named { get; }
|
||||
|
||||
/// <summary>
|
||||
/// <para>Gets an <see cref="IReadOnlyList{T}"/> interface representing all components
|
||||
/// associated with safe names.</para>
|
||||
/// <para>The safe name of a component is never <see langword="null"/>.
|
||||
/// If a component's unique name if <see langword="null"/>, its safe name
|
||||
/// will be some non-<see langword="null"/> string somehow identifying it.</para>
|
||||
/// <para>Note that safe names are not necessarily unique.</para>
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// A list of <see cref="ValueTuple{T1,T2}"/>s, each containing a safe name and a component.
|
||||
/// </value>
|
||||
IReadOnlyList<(string SafeName, T Component)> WithSafeNames { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the component with the specified name.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The component.
|
||||
/// </value>
|
||||
/// <param name="name">The name.</param>
|
||||
/// <returns>The component with the specified <paramref name="name"/>.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="name"/> is null.</exception>
|
||||
/// <exception cref="KeyNotFoundException">The property is retrieved and <paramref name="name"/> is not found.</exception>
|
||||
T this[string name] { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds a component to the collection,
|
||||
/// giving it the specified <paramref name="name"/> if it is not <see langword="null"/>.
|
||||
/// </summary>
|
||||
/// <param name="name">The name given to the module, or <see langword="null"/>.</param>
|
||||
/// <param name="component">The component.</param>
|
||||
void Add(string name, T component);
|
||||
}
|
||||
}
|
34
Swan.Lite/Collections/IDataDictionary`2.cs
Normal file
34
Swan.Lite/Collections/IDataDictionary`2.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Swan.Collections
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a generic collection of key/value pairs that does not store
|
||||
/// null values.
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">The type of keys in the dictionary. This must be a reference type.</typeparam>
|
||||
/// <typeparam name="TValue">The type of values in the dictionary. This must be a reference type.</typeparam>
|
||||
public interface IDataDictionary<TKey, TValue> : IDictionary<TKey, TValue>, IReadOnlyDictionary<TKey, TValue>
|
||||
where TKey : class
|
||||
where TValue : class
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a value that indicates whether the <see cref="IDataDictionary{TKey,TValue}"/> is empty.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <see langword="true"/> if the <see cref="IDataDictionary{TKey,TValue}"/> is empty; otherwise, <see langword="false"/>.
|
||||
/// </value>
|
||||
bool IsEmpty { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to remove and return the value that has the specified key from the <see cref="IDataDictionary{TKey,TValue}"/>.
|
||||
/// </summary>
|
||||
/// <param name="key">The key of the element to remove and return.</param>
|
||||
/// <param name="value">When this method returns, the value removed from the <see cref="IDataDictionary{TKey,TValue}"/>,
|
||||
/// if the key is found; otherwise, <see langword="null"/>. This parameter is passed uninitialized.</param>
|
||||
/// <returns><see langword="true"/> if the value was removed successfully; otherwise, <see langword="false"/>.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="key"/> is <see langword="null"/>.</exception>
|
||||
bool TryRemove(TKey key, out TValue value);
|
||||
}
|
||||
}
|
77
Swan.Lite/Configuration/ConfiguredObject.cs
Normal file
77
Swan.Lite/Configuration/ConfiguredObject.cs
Normal file
@ -0,0 +1,77 @@
|
||||
using System;
|
||||
|
||||
namespace Swan.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for objects whose configuration may be locked,
|
||||
/// thus becoming read-only, at a certain moment in their lifetime.
|
||||
/// </summary>
|
||||
public abstract class ConfiguredObject
|
||||
{
|
||||
private readonly object _syncRoot = new object();
|
||||
private bool _configurationLocked;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether s configuration has already been locked
|
||||
/// and has therefore become read-only.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <see langword="true"/> if the configuration is locked; otherwise, <see langword="false"/>.
|
||||
/// </value>
|
||||
/// <seealso cref="EnsureConfigurationNotLocked"/>
|
||||
protected bool ConfigurationLocked
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
return _configurationLocked;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>Locks this instance's configuration, preventing further modifications.</para>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>Configuration locking must be enforced by derived classes
|
||||
/// by calling <see cref="EnsureConfigurationNotLocked"/> at the start
|
||||
/// of methods and property setters that could change the object's
|
||||
/// configuration.</para>
|
||||
/// <para>Immediately before locking the configuration, this method calls <see cref="OnBeforeLockConfiguration"/>
|
||||
/// as a last chance to validate configuration data, and to lock the configuration of contained objects.</para>
|
||||
/// </remarks>
|
||||
/// <seealso cref="OnBeforeLockConfiguration"/>
|
||||
protected void LockConfiguration()
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
if (_configurationLocked)
|
||||
return;
|
||||
|
||||
OnBeforeLockConfiguration();
|
||||
_configurationLocked = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called immediately before locking the configuration.
|
||||
/// </summary>
|
||||
/// <seealso cref="LockConfiguration"/>
|
||||
protected virtual void OnBeforeLockConfiguration()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a module's configuration has become read-only
|
||||
/// and, if so, throws an <see cref="InvalidOperationException"/>.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">The configuration is locked.</exception>
|
||||
/// <seealso cref="ConfigurationLocked"/>
|
||||
protected void EnsureConfigurationNotLocked()
|
||||
{
|
||||
if (ConfigurationLocked)
|
||||
throw new InvalidOperationException($"Configuration of this {GetType().Name} instance is locked.");
|
||||
}
|
||||
}
|
||||
}
|
54
Swan.Lite/Configuration/PropertyDisplayAttribute.cs
Normal file
54
Swan.Lite/Configuration/PropertyDisplayAttribute.cs
Normal file
@ -0,0 +1,54 @@
|
||||
using System;
|
||||
|
||||
namespace Swan.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// An attribute used to include additional information to a Property for serialization.
|
||||
///
|
||||
/// Previously we used DisplayAttribute from DataAnnotation.
|
||||
/// </summary>
|
||||
/// <seealso cref="System.Attribute" />
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public sealed class PropertyDisplayAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The name.
|
||||
/// </value>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the description.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The description.
|
||||
/// </value>
|
||||
public string Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the group.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The name of the group.
|
||||
/// </value>
|
||||
public string GroupName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default value.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The default value.
|
||||
/// </value>
|
||||
public object DefaultValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the format string to call with method <c>ToString</c>.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The format.
|
||||
/// </value>
|
||||
public string Format { get; set; }
|
||||
}
|
||||
}
|
184
Swan.Lite/Configuration/SettingsProvider.cs
Normal file
184
Swan.Lite/Configuration/SettingsProvider.cs
Normal file
@ -0,0 +1,184 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Swan.Formatters;
|
||||
using Swan.Reflection;
|
||||
|
||||
namespace Swan.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a provider to save and load settings using a plain JSON file.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// The following example shows how to save and load settings.
|
||||
/// <code>
|
||||
/// using Swan.Configuration;
|
||||
///
|
||||
/// 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";
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
/// <typeparam name="T">The type of settings model.</typeparam>
|
||||
public sealed class SettingsProvider<T>
|
||||
: SingletonBase<SettingsProvider<T>>
|
||||
{
|
||||
private readonly object _syncRoot = new object();
|
||||
|
||||
private T _global;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the configuration file path. By default the entry assembly directory is used
|
||||
/// and the filename is 'appsettings.json'.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The configuration file path.
|
||||
/// </value>
|
||||
public string ConfigurationFilePath { get; set; } =
|
||||
Path.Combine(SwanRuntime.EntryAssemblyDirectory, "appsettings.json");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the global settings object.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The global settings object.
|
||||
/// </value>
|
||||
public T Global
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
if (Equals(_global, default(T)))
|
||||
ReloadGlobalSettings();
|
||||
|
||||
return _global;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reloads the global settings.
|
||||
/// </summary>
|
||||
public void ReloadGlobalSettings()
|
||||
{
|
||||
if (File.Exists(ConfigurationFilePath) == false || File.ReadAllText(ConfigurationFilePath).Length == 0)
|
||||
{
|
||||
ResetGlobalSettings();
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_syncRoot)
|
||||
_global = Json.Deserialize<T>(File.ReadAllText(ConfigurationFilePath));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Persists the global settings.
|
||||
/// </summary>
|
||||
public void PersistGlobalSettings() => File.WriteAllText(ConfigurationFilePath, Json.Serialize(Global, true));
|
||||
|
||||
/// <summary>
|
||||
/// Updates settings from list.
|
||||
/// </summary>
|
||||
/// <param name="propertyList">The list.</param>
|
||||
/// <returns>
|
||||
/// A list of settings of type ref="ExtendedPropertyInfo".
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">propertyList.</exception>
|
||||
public List<string> RefreshFromList(List<ExtendedPropertyInfo<T>> propertyList)
|
||||
{
|
||||
if (propertyList == null)
|
||||
throw new ArgumentNullException(nameof(propertyList));
|
||||
|
||||
var changedSettings = new List<string>();
|
||||
var globalProps = PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties<T>();
|
||||
|
||||
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<object>(), Global)
|
||||
: SetValue(property.Value, originalValue, propertyInfo);
|
||||
|
||||
if (!isChanged) continue;
|
||||
|
||||
changedSettings.Add(property.Property);
|
||||
PersistGlobalSettings();
|
||||
}
|
||||
|
||||
return changedSettings;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list.
|
||||
/// </summary>
|
||||
/// <returns>A List of ExtendedPropertyInfo of the type T.</returns>
|
||||
public List<ExtendedPropertyInfo<T>>? GetList()
|
||||
{
|
||||
var jsonData = Json.Deserialize(Json.Serialize(Global)) as Dictionary<string, object>;
|
||||
|
||||
return jsonData?.Keys
|
||||
.Select(p => new ExtendedPropertyInfo<T>(p) { Value = jsonData[p] })
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the global settings.
|
||||
/// </summary>
|
||||
public void ResetGlobalSettings()
|
||||
{
|
||||
lock (_syncRoot)
|
||||
_global = Activator.CreateInstance<T>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
124
Swan.Lite/Cryptography/Hasher.cs
Normal file
124
Swan.Lite/Cryptography/Hasher.cs
Normal file
@ -0,0 +1,124 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace Swan.Cryptography
|
||||
{
|
||||
/// <summary>
|
||||
/// Use this class to compute a hash in MD4, SHA1, SHA256 or SHA512.
|
||||
/// </summary>
|
||||
public static class Hasher
|
||||
{
|
||||
private static readonly Lazy<MD5> Md5Hasher = new Lazy<MD5>(MD5.Create, true);
|
||||
private static readonly Lazy<SHA1> SHA1Hasher = new Lazy<SHA1>(SHA1.Create, true);
|
||||
private static readonly Lazy<SHA256> SHA256Hasher = new Lazy<SHA256>(SHA256.Create, true);
|
||||
private static readonly Lazy<SHA512> SHA512Hasher = new Lazy<SHA512>(SHA512.Create, true);
|
||||
|
||||
/// <summary>
|
||||
/// Computes the MD5 hash of the given stream.
|
||||
/// Do not use for large streams as this reads ALL bytes at once.
|
||||
/// </summary>
|
||||
/// <param name="this">The stream.</param>
|
||||
/// <param name="createHasher">if set to <c>true</c> [create hasher].</param>
|
||||
/// <returns>
|
||||
/// The computed hash code.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">stream.</exception>
|
||||
[Obsolete("Use a better hasher.")]
|
||||
public static byte[] ComputeMD5(Stream @this, bool createHasher = false)
|
||||
{
|
||||
if (@this == null)
|
||||
throw new ArgumentNullException(nameof(@this));
|
||||
|
||||
var md5 = MD5.Create();
|
||||
const int bufferSize = 4096;
|
||||
|
||||
var readAheadBuffer = new byte[bufferSize];
|
||||
var readAheadBytesRead = @this.Read(readAheadBuffer, 0, readAheadBuffer.Length);
|
||||
|
||||
do
|
||||
{
|
||||
var bytesRead = readAheadBytesRead;
|
||||
var buffer = readAheadBuffer;
|
||||
|
||||
readAheadBuffer = new byte[bufferSize];
|
||||
readAheadBytesRead = @this.Read(readAheadBuffer, 0, readAheadBuffer.Length);
|
||||
|
||||
if (readAheadBytesRead == 0)
|
||||
md5.TransformFinalBlock(buffer, 0, bytesRead);
|
||||
else
|
||||
md5.TransformBlock(buffer, 0, bytesRead, buffer, 0);
|
||||
}
|
||||
while (readAheadBytesRead != 0);
|
||||
|
||||
return md5.Hash;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the MD5 hash of the given string using UTF8 byte encoding.
|
||||
/// </summary>
|
||||
/// <param name="value">The input string.</param>
|
||||
/// <param name="createHasher">if set to <c>true</c> [create hasher].</param>
|
||||
/// <returns>The computed hash code.</returns>
|
||||
[Obsolete("Use a better hasher.")]
|
||||
public static byte[] ComputeMD5(string value, bool createHasher = false) =>
|
||||
ComputeMD5(Encoding.UTF8.GetBytes(value), createHasher);
|
||||
|
||||
/// <summary>
|
||||
/// Computes the MD5 hash of the given byte array.
|
||||
/// </summary>
|
||||
/// <param name="data">The data.</param>
|
||||
/// <param name="createHasher">if set to <c>true</c> [create hasher].</param>
|
||||
/// <returns>The computed hash code.</returns>
|
||||
[Obsolete("Use a better hasher.")]
|
||||
public static byte[] ComputeMD5(byte[] data, bool createHasher = false) =>
|
||||
(createHasher ? MD5.Create() : Md5Hasher.Value).ComputeHash(data);
|
||||
|
||||
/// <summary>
|
||||
/// Computes the SHA-1 hash of the given string using UTF8 byte encoding.
|
||||
/// </summary>
|
||||
/// <param name="this">The input string.</param>
|
||||
/// <param name="createHasher">if set to <c>true</c> [create hasher].</param>
|
||||
/// <returns>
|
||||
/// The computes a Hash-based Message Authentication Code (HMAC)
|
||||
/// using the SHA1 hash function.
|
||||
/// </returns>
|
||||
[Obsolete("Use a better hasher.")]
|
||||
public static byte[] ComputeSha1(string @this, bool createHasher = false)
|
||||
{
|
||||
var inputBytes = Encoding.UTF8.GetBytes(@this);
|
||||
return (createHasher ? SHA1.Create() : SHA1Hasher.Value).ComputeHash(inputBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the SHA-256 hash of the given string using UTF8 byte encoding.
|
||||
/// </summary>
|
||||
/// <param name="value">The input string.</param>
|
||||
/// <param name="createHasher">if set to <c>true</c> [create hasher].</param>
|
||||
/// <returns>
|
||||
/// The computes a Hash-based Message Authentication Code (HMAC)
|
||||
/// by using the SHA256 hash function.
|
||||
/// </returns>
|
||||
public static byte[] ComputeSha256(string value, bool createHasher = false)
|
||||
{
|
||||
var inputBytes = Encoding.UTF8.GetBytes(value);
|
||||
return (createHasher ? SHA256.Create() : SHA256Hasher.Value).ComputeHash(inputBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the SHA-512 hash of the given string using UTF8 byte encoding.
|
||||
/// </summary>
|
||||
/// <param name="value">The input string.</param>
|
||||
/// <param name="createHasher">if set to <c>true</c> [create hasher].</param>
|
||||
/// <returns>
|
||||
/// The computes a Hash-based Message Authentication Code (HMAC)
|
||||
/// using the SHA512 hash function.
|
||||
/// </returns>
|
||||
public static byte[] ComputeSha512(string value, bool createHasher = false)
|
||||
{
|
||||
var inputBytes = Encoding.UTF8.GetBytes(value);
|
||||
return (createHasher ? SHA512.Create() : SHA512Hasher.Value).ComputeHash(inputBytes);
|
||||
}
|
||||
}
|
||||
}
|
174
Swan.Lite/DateTimeSpan.cs
Normal file
174
Swan.Lite/DateTimeSpan.cs
Normal file
@ -0,0 +1,174 @@
|
||||
using System;
|
||||
|
||||
namespace Swan
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public struct DateTimeSpan
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DateTimeSpan"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="years">The years.</param>
|
||||
/// <param name="months">The months.</param>
|
||||
/// <param name="days">The days.</param>
|
||||
/// <param name="hours">The hours.</param>
|
||||
/// <param name="minutes">The minutes.</param>
|
||||
/// <param name="seconds">The seconds.</param>
|
||||
/// <param name="milliseconds">The milliseconds.</param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the years.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The years.
|
||||
/// </value>
|
||||
public int Years { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the months.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The months.
|
||||
/// </value>
|
||||
public int Months { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the days.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The days.
|
||||
/// </value>
|
||||
public int Days { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the hours.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The hours.
|
||||
/// </value>
|
||||
public int Hours { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the minutes.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The minutes.
|
||||
/// </value>
|
||||
public int Minutes { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the seconds.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The seconds.
|
||||
/// </value>
|
||||
public int Seconds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the milliseconds.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The milliseconds.
|
||||
/// </value>
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
138
Swan.Lite/Definitions.Types.cs
Normal file
138
Swan.Lite/Definitions.Types.cs
Normal file
@ -0,0 +1,138 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using Swan.Reflection;
|
||||
|
||||
namespace Swan
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains useful constants and definitions.
|
||||
/// </summary>
|
||||
public static partial class Definitions
|
||||
{
|
||||
#region Main Dictionary Definition
|
||||
|
||||
/// <summary>
|
||||
/// The basic types information.
|
||||
/// </summary>
|
||||
public static readonly Lazy<Dictionary<Type, ExtendedTypeInfo>> BasicTypesInfo = new Lazy<Dictionary<Type, ExtendedTypeInfo>>(() =>
|
||||
new Dictionary<Type, ExtendedTypeInfo>
|
||||
{
|
||||
// Non-Nullables
|
||||
{typeof(DateTime), new ExtendedTypeInfo<DateTime>()},
|
||||
{typeof(byte), new ExtendedTypeInfo<byte>()},
|
||||
{typeof(sbyte), new ExtendedTypeInfo<sbyte>()},
|
||||
{typeof(int), new ExtendedTypeInfo<int>()},
|
||||
{typeof(uint), new ExtendedTypeInfo<uint>()},
|
||||
{typeof(short), new ExtendedTypeInfo<short>()},
|
||||
{typeof(ushort), new ExtendedTypeInfo<ushort>()},
|
||||
{typeof(long), new ExtendedTypeInfo<long>()},
|
||||
{typeof(ulong), new ExtendedTypeInfo<ulong>()},
|
||||
{typeof(float), new ExtendedTypeInfo<float>()},
|
||||
{typeof(double), new ExtendedTypeInfo<double>()},
|
||||
{typeof(char), new ExtendedTypeInfo<char>()},
|
||||
{typeof(bool), new ExtendedTypeInfo<bool>()},
|
||||
{typeof(decimal), new ExtendedTypeInfo<decimal>()},
|
||||
{typeof(Guid), new ExtendedTypeInfo<Guid>()},
|
||||
|
||||
// Strings is also considered a basic type (it's the only basic reference type)
|
||||
{typeof(string), new ExtendedTypeInfo<string>()},
|
||||
|
||||
// Nullables
|
||||
{typeof(DateTime?), new ExtendedTypeInfo<DateTime?>()},
|
||||
{typeof(byte?), new ExtendedTypeInfo<byte?>()},
|
||||
{typeof(sbyte?), new ExtendedTypeInfo<sbyte?>()},
|
||||
{typeof(int?), new ExtendedTypeInfo<int?>()},
|
||||
{typeof(uint?), new ExtendedTypeInfo<uint?>()},
|
||||
{typeof(short?), new ExtendedTypeInfo<short?>()},
|
||||
{typeof(ushort?), new ExtendedTypeInfo<ushort?>()},
|
||||
{typeof(long?), new ExtendedTypeInfo<long?>()},
|
||||
{typeof(ulong?), new ExtendedTypeInfo<ulong?>()},
|
||||
{typeof(float?), new ExtendedTypeInfo<float?>()},
|
||||
{typeof(double?), new ExtendedTypeInfo<double?>()},
|
||||
{typeof(char?), new ExtendedTypeInfo<char?>()},
|
||||
{typeof(bool?), new ExtendedTypeInfo<bool?>()},
|
||||
{typeof(decimal?), new ExtendedTypeInfo<decimal?>()},
|
||||
{typeof(Guid?), new ExtendedTypeInfo<Guid?>()},
|
||||
|
||||
// Additional Types
|
||||
{typeof(TimeSpan), new ExtendedTypeInfo<TimeSpan>()},
|
||||
{typeof(TimeSpan?), new ExtendedTypeInfo<TimeSpan?>()},
|
||||
{typeof(IPAddress), new ExtendedTypeInfo<IPAddress>()},
|
||||
});
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Contains all basic types, including string, date time, and all of their nullable counterparts.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// All basic types.
|
||||
/// </value>
|
||||
public static IReadOnlyCollection<Type> AllBasicTypes { get; } = new ReadOnlyCollection<Type>(BasicTypesInfo.Value.Keys.ToArray());
|
||||
|
||||
/// <summary>
|
||||
/// Gets all numeric types including their nullable counterparts.
|
||||
/// Note that Booleans and Guids are not considered numeric types.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// All numeric types.
|
||||
/// </value>
|
||||
public static IReadOnlyCollection<Type> AllNumericTypes { get; } = new ReadOnlyCollection<Type>(
|
||||
BasicTypesInfo
|
||||
.Value
|
||||
.Where(kvp => kvp.Value.IsNumeric)
|
||||
.Select(kvp => kvp.Key).ToArray());
|
||||
|
||||
/// <summary>
|
||||
/// Gets all numeric types without their nullable counterparts.
|
||||
/// Note that Booleans and Guids are not considered numeric types.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// All numeric value types.
|
||||
/// </value>
|
||||
public static IReadOnlyCollection<Type> AllNumericValueTypes { get; } = new ReadOnlyCollection<Type>(
|
||||
BasicTypesInfo
|
||||
.Value
|
||||
.Where(kvp => kvp.Value.IsNumeric && !kvp.Value.IsNullableValueType)
|
||||
.Select(kvp => kvp.Key).ToArray());
|
||||
|
||||
/// <summary>
|
||||
/// Contains all basic value types. i.e. excludes string and nullables.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// All basic value types.
|
||||
/// </value>
|
||||
public static IReadOnlyCollection<Type> AllBasicValueTypes { get; } = new ReadOnlyCollection<Type>(
|
||||
BasicTypesInfo
|
||||
.Value
|
||||
.Where(kvp => kvp.Value.IsValueType)
|
||||
.Select(kvp => kvp.Key).ToArray());
|
||||
|
||||
/// <summary>
|
||||
/// Contains all basic value types including the string type. i.e. excludes nullables.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// All basic value and string types.
|
||||
/// </value>
|
||||
public static IReadOnlyCollection<Type> AllBasicValueAndStringTypes { get; } = new ReadOnlyCollection<Type>(
|
||||
BasicTypesInfo
|
||||
.Value
|
||||
.Where(kvp => kvp.Value.IsValueType || kvp.Key == typeof(string))
|
||||
.Select(kvp => kvp.Key).ToArray());
|
||||
|
||||
/// <summary>
|
||||
/// Gets all nullable value types. i.e. excludes string and all basic value types.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// All basic nullable value types.
|
||||
/// </value>
|
||||
public static IReadOnlyCollection<Type> AllBasicNullableValueTypes { get; } = new ReadOnlyCollection<Type>(
|
||||
BasicTypesInfo
|
||||
.Value
|
||||
.Where(kvp => kvp.Value.IsNullableValueType)
|
||||
.Select(kvp => kvp.Key).ToArray());
|
||||
}
|
||||
}
|
39
Swan.Lite/Definitions.cs
Normal file
39
Swan.Lite/Definitions.cs
Normal file
@ -0,0 +1,39 @@
|
||||
using System.Text;
|
||||
|
||||
namespace Swan
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains useful constants and definitions.
|
||||
/// </summary>
|
||||
public static partial class Definitions
|
||||
{
|
||||
/// <summary>
|
||||
/// The MS Windows codepage 1252 encoding used in some legacy scenarios
|
||||
/// such as default CSV text encoding from Excel.
|
||||
/// </summary>
|
||||
public static readonly Encoding Windows1252Encoding;
|
||||
|
||||
/// <summary>
|
||||
/// The encoding associated with the default ANSI code page in the operating
|
||||
/// system's regional and language settings.
|
||||
/// </summary>
|
||||
public static readonly Encoding CurrentAnsiEncoding;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the <see cref="Definitions"/> class.
|
||||
/// </summary>
|
||||
static Definitions()
|
||||
{
|
||||
CurrentAnsiEncoding = Encoding.GetEncoding(default(int));
|
||||
try
|
||||
{
|
||||
Windows1252Encoding = Encoding.GetEncoding(1252);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore, the codepage is not available use default
|
||||
Windows1252Encoding = CurrentAnsiEncoding;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
121
Swan.Lite/Diagnostics/Benchmark.cs
Normal file
121
Swan.Lite/Diagnostics/Benchmark.cs
Normal file
@ -0,0 +1,121 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Swan.Diagnostics
|
||||
{
|
||||
/// <summary>
|
||||
/// A simple benchmarking class.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// The following code demonstrates how to create a simple benchmark.
|
||||
/// <code>
|
||||
/// namespace Examples.Benchmark.Simple
|
||||
/// {
|
||||
/// using Swan.Diagnostics;
|
||||
///
|
||||
/// public class SimpleBenchmark
|
||||
/// {
|
||||
/// public static void Main()
|
||||
/// {
|
||||
/// using (Benchmark.Start("Test"))
|
||||
/// {
|
||||
/// // do some logic in here
|
||||
/// }
|
||||
///
|
||||
/// // dump results into a string
|
||||
/// var results = Benchmark.Dump();
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public static partial class Benchmark
|
||||
{
|
||||
private static readonly object SyncLock = new object();
|
||||
private static readonly Dictionary<string, List<TimeSpan>> Measures = new Dictionary<string, List<TimeSpan>>();
|
||||
|
||||
/// <summary>
|
||||
/// Starts measuring with the given identifier.
|
||||
/// </summary>
|
||||
/// <param name="identifier">The identifier.</param>
|
||||
/// <returns>A disposable object that when disposed, adds a benchmark result.</returns>
|
||||
public static IDisposable Start(string identifier) => new BenchmarkUnit(identifier);
|
||||
|
||||
/// <summary>
|
||||
/// Outputs the benchmark statistics.
|
||||
/// </summary>
|
||||
/// <returns>A string containing human-readable statistics.</returns>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Measures the elapsed time of the given action as a TimeSpan
|
||||
/// This method uses a high precision Stopwatch if it is available.
|
||||
/// </summary>
|
||||
/// <param name="target">The target.</param>
|
||||
/// <returns>
|
||||
/// A time interval that represents a specified time, where the specification is in units of ticks.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">target.</exception>
|
||||
public static TimeSpan BenchmarkAction(Action target)
|
||||
{
|
||||
if (target == null)
|
||||
throw new ArgumentNullException(nameof(target));
|
||||
|
||||
var sw = Stopwatch.IsHighResolution ? new HighResolutionTimer() : new Stopwatch();
|
||||
|
||||
try
|
||||
{
|
||||
sw.Start();
|
||||
target.Invoke();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// swallow
|
||||
}
|
||||
finally
|
||||
{
|
||||
sw.Stop();
|
||||
}
|
||||
|
||||
return TimeSpan.FromTicks(sw.ElapsedTicks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the specified result to the given identifier.
|
||||
/// </summary>
|
||||
/// <param name="identifier">The identifier.</param>
|
||||
/// <param name="elapsed">The elapsed.</param>
|
||||
private static void Add(string identifier, TimeSpan elapsed)
|
||||
{
|
||||
lock (SyncLock)
|
||||
{
|
||||
if (Measures.ContainsKey(identifier) == false)
|
||||
Measures[identifier] = new List<TimeSpan>(1024 * 1024);
|
||||
|
||||
Measures[identifier].Add(elapsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
50
Swan.Lite/Diagnostics/BenchmarkUnit.cs
Normal file
50
Swan.Lite/Diagnostics/BenchmarkUnit.cs
Normal file
@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Swan.Diagnostics
|
||||
{
|
||||
public static partial class Benchmark
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a disposable benchmark unit.
|
||||
/// </summary>
|
||||
/// <seealso cref="IDisposable" />
|
||||
private sealed class BenchmarkUnit : IDisposable
|
||||
{
|
||||
private readonly string _identifier;
|
||||
private bool _isDisposed; // To detect redundant calls
|
||||
private Stopwatch? _stopwatch = new Stopwatch();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BenchmarkUnit" /> class.
|
||||
/// </summary>
|
||||
/// <param name="identifier">The identifier.</param>
|
||||
public BenchmarkUnit(string identifier)
|
||||
{
|
||||
_identifier = identifier;
|
||||
_stopwatch?.Start();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() => Dispose(true);
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
/// <param name="alsoManaged"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
private void Dispose(bool alsoManaged)
|
||||
{
|
||||
if (_isDisposed) return;
|
||||
|
||||
if (alsoManaged)
|
||||
{
|
||||
Add(_identifier, _stopwatch?.Elapsed ?? default);
|
||||
_stopwatch?.Stop();
|
||||
}
|
||||
|
||||
_stopwatch = null;
|
||||
_isDisposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
32
Swan.Lite/Diagnostics/HighResolutionTimer.cs
Normal file
32
Swan.Lite/Diagnostics/HighResolutionTimer.cs
Normal file
@ -0,0 +1,32 @@
|
||||
namespace Swan.Diagnostics
|
||||
{
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
/// <summary>
|
||||
/// Provides access to a high-resolution, time measuring device.
|
||||
/// </summary>
|
||||
/// <seealso cref="Stopwatch" />
|
||||
public class HighResolutionTimer : Stopwatch
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HighResolutionTimer"/> class.
|
||||
/// </summary>
|
||||
/// <exception cref="NotSupportedException">High-resolution timer not available.</exception>
|
||||
public HighResolutionTimer()
|
||||
{
|
||||
if (!IsHighResolution)
|
||||
throw new NotSupportedException("High-resolution timer not available");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of microseconds per timer tick.
|
||||
/// </summary>
|
||||
public static double MicrosecondsPerTick { get; } = 1000000d / Frequency;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the elapsed microseconds.
|
||||
/// </summary>
|
||||
public long ElapsedMicroseconds => (long)(ElapsedTicks * MicrosecondsPerTick);
|
||||
}
|
||||
}
|
171
Swan.Lite/EnumHelper.cs
Normal file
171
Swan.Lite/EnumHelper.cs
Normal file
@ -0,0 +1,171 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Swan.Collections;
|
||||
|
||||
namespace Swan
|
||||
{
|
||||
/// <summary>
|
||||
/// Provide Enumerations helpers with internal cache.
|
||||
/// </summary>
|
||||
public class EnumHelper
|
||||
: SingletonBase<CollectionCacheRepository<Tuple<string, object>>>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets all the names and enumerators from a specific Enum type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the attribute to be retrieved.</typeparam>
|
||||
/// <returns>A tuple of enumerator names and their value stored for the specified type.</returns>
|
||||
public static IEnumerable<Tuple<string, object>> Retrieve<T>()
|
||||
where T : struct, IConvertible
|
||||
{
|
||||
return Instance.Retrieve(typeof(T), t => Enum.GetValues(t)
|
||||
.Cast<object>()
|
||||
.Select(item => Tuple.Create(Enum.GetName(t, item), item)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the cached items with the enum item value.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of enumeration.</typeparam>
|
||||
/// <param name="humanize">if set to <c>true</c> [humanize].</param>
|
||||
/// <returns>
|
||||
/// A collection of Type/Tuple pairs
|
||||
/// that represents items with the enum item value.
|
||||
/// </returns>
|
||||
public static IEnumerable<Tuple<int, string>> GetItemsWithValue<T>(bool humanize = true)
|
||||
where T : struct, IConvertible
|
||||
{
|
||||
return Retrieve<T>()
|
||||
.Select(x => Tuple.Create((int) x.Item2, humanize ? x.Item1.Humanize() : x.Item1));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the flag values.
|
||||
/// </summary>
|
||||
/// <typeparam name="TEnum">The type of the enum.</typeparam>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <param name="ignoreZero">if set to <c>true</c> [ignore zero].</param>
|
||||
/// <returns>
|
||||
/// A list of values in the flag.
|
||||
/// </returns>
|
||||
public static IEnumerable<int> GetFlagValues<TEnum>(int value, bool ignoreZero = false)
|
||||
where TEnum : struct, IConvertible
|
||||
{
|
||||
return Retrieve<TEnum>()
|
||||
.Select(x => (int) x.Item2)
|
||||
.When(() => ignoreZero, q => q.Where(f => f != 0))
|
||||
.Where(x => (x & value) == x);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the flag values.
|
||||
/// </summary>
|
||||
/// <typeparam name="TEnum">The type of the enum.</typeparam>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <param name="ignoreZero">if set to <c>true</c> [ignore zero].</param>
|
||||
/// <returns>
|
||||
/// A list of values in the flag.
|
||||
/// </returns>
|
||||
public static IEnumerable<long> GetFlagValues<TEnum>(long value, bool ignoreZero = false)
|
||||
where TEnum : struct, IConvertible
|
||||
{
|
||||
return Retrieve<TEnum>()
|
||||
.Select(x => (long) x.Item2)
|
||||
.When(() => ignoreZero, q => q.Where(f => f != 0))
|
||||
.Where(x => (x & value) == x);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the flag values.
|
||||
/// </summary>
|
||||
/// <typeparam name="TEnum">The type of the enum.</typeparam>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <param name="ignoreZero">if set to <c>true</c> [ignore zero].</param>
|
||||
/// <returns>
|
||||
/// A list of values in the flag.
|
||||
/// </returns>
|
||||
public static IEnumerable<byte> GetFlagValues<TEnum>(byte value, bool ignoreZero = false)
|
||||
where TEnum : struct, IConvertible
|
||||
{
|
||||
return Retrieve<TEnum>()
|
||||
.Select(x => (byte) x.Item2)
|
||||
.When(() => ignoreZero, q => q.Where(f => f != 0))
|
||||
.Where(x => (x & value) == x);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the flag names.
|
||||
/// </summary>
|
||||
/// <typeparam name="TEnum">The type of the enum.</typeparam>
|
||||
/// <param name="value">the value.</param>
|
||||
/// <param name="ignoreZero">if set to <c>true</c> [ignore zero].</param>
|
||||
/// <param name="humanize">if set to <c>true</c> [humanize].</param>
|
||||
/// <returns>
|
||||
/// A list of flag names.
|
||||
/// </returns>
|
||||
public static IEnumerable<string> GetFlagNames<TEnum>(int value, bool ignoreZero = false, bool humanize = true)
|
||||
where TEnum : struct, IConvertible
|
||||
{
|
||||
return Retrieve<TEnum>()
|
||||
.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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the flag names.
|
||||
/// </summary>
|
||||
/// <typeparam name="TEnum">The type of the enum.</typeparam>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <param name="ignoreZero">if set to <c>true</c> [ignore zero].</param>
|
||||
/// <param name="humanize">if set to <c>true</c> [humanize].</param>
|
||||
/// <returns>
|
||||
/// A list of flag names.
|
||||
/// </returns>
|
||||
public static IEnumerable<string> GetFlagNames<TEnum>(long value, bool ignoreZero = false, bool humanize = true)
|
||||
where TEnum : struct, IConvertible
|
||||
{
|
||||
return Retrieve<TEnum>()
|
||||
.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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the flag names.
|
||||
/// </summary>
|
||||
/// <typeparam name="TEnum">The type of the enum.</typeparam>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <param name="ignoreZero">if set to <c>true</c> [ignore zero].</param>
|
||||
/// <param name="humanize">if set to <c>true</c> [humanize].</param>
|
||||
/// <returns>
|
||||
/// A list of flag names.
|
||||
/// </returns>
|
||||
public static IEnumerable<string> GetFlagNames<TEnum>(byte value, bool ignoreZero = false, bool humanize = true)
|
||||
where TEnum : struct, IConvertible
|
||||
{
|
||||
return Retrieve<TEnum>()
|
||||
.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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the cached items with the enum item index.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of enumeration.</typeparam>
|
||||
/// <param name="humanize">if set to <c>true</c> [humanize].</param>
|
||||
/// <returns>
|
||||
/// A collection of Type/Tuple pairs that represents items with the enum item value.
|
||||
/// </returns>
|
||||
public static IEnumerable<Tuple<int, string>> GetItemsWithIndex<T>(bool humanize = true)
|
||||
where T : struct, IConvertible
|
||||
{
|
||||
var i = 0;
|
||||
|
||||
return Retrieve<T>()
|
||||
.Select(x => Tuple.Create(i++, humanize ? x.Item1.Humanize() : x.Item1));
|
||||
}
|
||||
}
|
||||
}
|
65
Swan.Lite/Enums.cs
Normal file
65
Swan.Lite/Enums.cs
Normal file
@ -0,0 +1,65 @@
|
||||
namespace Swan
|
||||
{
|
||||
/// <summary>
|
||||
/// Enumeration of Operating Systems.
|
||||
/// </summary>
|
||||
public enum OperatingSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// Unknown OS
|
||||
/// </summary>
|
||||
Unknown,
|
||||
|
||||
/// <summary>
|
||||
/// Windows
|
||||
/// </summary>
|
||||
Windows,
|
||||
|
||||
/// <summary>
|
||||
/// UNIX/Linux
|
||||
/// </summary>
|
||||
Unix,
|
||||
|
||||
/// <summary>
|
||||
/// macOS (OSX)
|
||||
/// </summary>
|
||||
Osx,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines Endianness, big or little.
|
||||
/// </summary>
|
||||
public enum Endianness
|
||||
{
|
||||
/// <summary>
|
||||
/// In big endian, you store the most significant byte in the smallest address.
|
||||
/// </summary>
|
||||
Big,
|
||||
|
||||
/// <summary>
|
||||
/// In little endian, you store the least significant byte in the smallest address.
|
||||
/// </summary>
|
||||
Little,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates the JSON serializer cases to use: None (keeps the same case), PascalCase, or camelCase.
|
||||
/// </summary>
|
||||
public enum JsonSerializerCase
|
||||
{
|
||||
/// <summary>
|
||||
/// The none
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// The pascal case (eg. PascalCase)
|
||||
/// </summary>
|
||||
PascalCase,
|
||||
|
||||
/// <summary>
|
||||
/// The camel case (eg. camelCase)
|
||||
/// </summary>
|
||||
CamelCase,
|
||||
}
|
||||
}
|
504
Swan.Lite/Extensions.ByteArrays.cs
Normal file
504
Swan.Lite/Extensions.ByteArrays.cs
Normal file
@ -0,0 +1,504 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Swan
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides various extension methods for byte arrays and streams.
|
||||
/// </summary>
|
||||
public static class ByteArrayExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts an array of bytes to its lower-case, hexadecimal representation.
|
||||
/// </summary>
|
||||
/// <param name="bytes">The bytes.</param>
|
||||
/// <param name="addPrefix">if set to <c>true</c> add the 0x prefix tot he output.</param>
|
||||
/// <returns>
|
||||
/// The specified string instance; no actual conversion is performed.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">bytes.</exception>
|
||||
public static string ToLowerHex(this byte[] bytes, bool addPrefix = false)
|
||||
=> ToHex(bytes, addPrefix, "x2");
|
||||
|
||||
/// <summary>
|
||||
/// Converts an array of bytes to its upper-case, hexadecimal representation.
|
||||
/// </summary>
|
||||
/// <param name="bytes">The bytes.</param>
|
||||
/// <param name="addPrefix">if set to <c>true</c> [add prefix].</param>
|
||||
/// <returns>
|
||||
/// The specified string instance; no actual conversion is performed.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">bytes.</exception>
|
||||
public static string ToUpperHex(this byte[] bytes, bool addPrefix = false)
|
||||
=> ToHex(bytes, addPrefix, "X2");
|
||||
|
||||
/// <summary>
|
||||
/// Converts an array of bytes to a sequence of dash-separated, hexadecimal,
|
||||
/// uppercase characters.
|
||||
/// </summary>
|
||||
/// <param name="bytes">The bytes.</param>
|
||||
/// <returns>
|
||||
/// A string of hexadecimal pairs separated by hyphens, where each pair represents
|
||||
/// the corresponding element in value; for example, "7F-2C-4A-00".
|
||||
/// </returns>
|
||||
public static string ToDashedHex(this byte[] bytes) => BitConverter.ToString(bytes);
|
||||
|
||||
/// <summary>
|
||||
/// Converts an array of bytes to a base-64 encoded string.
|
||||
/// </summary>
|
||||
/// <param name="bytes">The bytes.</param>
|
||||
/// <returns>A <see cref="string" /> converted from an array of bytes.</returns>
|
||||
public static string ToBase64(this byte[] bytes) => Convert.ToBase64String(bytes);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="this">The hexadecimal.</param>
|
||||
/// <returns>
|
||||
/// A byte array containing the results of encoding the specified set of characters.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">hex.</exception>
|
||||
public static byte[] ConvertHexadecimalToBytes(this string @this)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(@this))
|
||||
throw new ArgumentNullException(nameof(@this));
|
||||
|
||||
return Enumerable
|
||||
.Range(0, @this.Length / 2)
|
||||
.Select(x => Convert.ToByte(@this.Substring(x * 2, 2), 16))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the bit value at the given offset.
|
||||
/// </summary>
|
||||
/// <param name="this">The b.</param>
|
||||
/// <param name="offset">The offset.</param>
|
||||
/// <param name="length">The length.</param>
|
||||
/// <returns>
|
||||
/// Bit value at the given offset.
|
||||
/// </returns>
|
||||
public static byte GetBitValueAt(this byte @this, byte offset, byte length = 1) => (byte)((@this >> offset) & ~(0xff << length));
|
||||
|
||||
/// <summary>
|
||||
/// Sets the bit value at the given offset.
|
||||
/// </summary>
|
||||
/// <param name="this">The b.</param>
|
||||
/// <param name="offset">The offset.</param>
|
||||
/// <param name="length">The length.</param>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <returns>Bit value at the given offset.</returns>
|
||||
public static byte SetBitValueAt(this byte @this, byte offset, byte length, byte value)
|
||||
{
|
||||
var mask = ~(0xff << length);
|
||||
var valueAt = (byte)(value & mask);
|
||||
|
||||
return (byte)((valueAt << offset) | (@this & ~(mask << offset)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the bit value at the given offset.
|
||||
/// </summary>
|
||||
/// <param name="this">The b.</param>
|
||||
/// <param name="offset">The offset.</param>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <returns>Bit value at the given offset.</returns>
|
||||
public static byte SetBitValueAt(this byte @this, byte offset, byte value) => @this.SetBitValueAt(offset, 1, value);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="this">The buffer.</param>
|
||||
/// <param name="offset">The offset at which to start splitting bytes. Any bytes before this will be discarded.</param>
|
||||
/// <param name="sequence">The sequence.</param>
|
||||
/// <returns>
|
||||
/// A byte array containing the results the specified sequence of bytes.
|
||||
/// </returns>
|
||||
/// <exception cref="System.ArgumentNullException">
|
||||
/// buffer
|
||||
/// or
|
||||
/// sequence.
|
||||
/// </exception>
|
||||
public static List<byte[]> Split(this byte[] @this, int offset, params byte[] sequence)
|
||||
{
|
||||
if (@this == null)
|
||||
throw new ArgumentNullException(nameof(@this));
|
||||
|
||||
if (sequence == null)
|
||||
throw new ArgumentNullException(nameof(sequence));
|
||||
|
||||
var seqOffset = offset.Clamp(0, @this.Length - 1);
|
||||
|
||||
var result = new List<byte[]>();
|
||||
|
||||
while (seqOffset < @this.Length)
|
||||
{
|
||||
var separatorStartIndex = @this.GetIndexOf(sequence, seqOffset);
|
||||
|
||||
if (separatorStartIndex >= 0)
|
||||
{
|
||||
var item = new byte[separatorStartIndex - seqOffset + sequence.Length];
|
||||
Array.Copy(@this, seqOffset, item, 0, item.Length);
|
||||
result.Add(item);
|
||||
seqOffset += item.Length;
|
||||
}
|
||||
else
|
||||
{
|
||||
var item = new byte[@this.Length - seqOffset];
|
||||
Array.Copy(@this, seqOffset, item, 0, item.Length);
|
||||
result.Add(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clones the specified buffer, byte by byte.
|
||||
/// </summary>
|
||||
/// <param name="this">The buffer.</param>
|
||||
/// <returns>
|
||||
/// A byte array containing the results of encoding the specified set of characters.
|
||||
/// </returns>
|
||||
/// <exception cref="System.ArgumentNullException">this</exception>
|
||||
public static byte[] DeepClone(this byte[] @this)
|
||||
{
|
||||
if (@this == null)
|
||||
throw new ArgumentNullException(nameof(@this));
|
||||
|
||||
var result = new byte[@this.Length];
|
||||
Array.Copy(@this, result, @this.Length);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the specified sequence from the start of the buffer if the buffer begins with such sequence.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The buffer.</param>
|
||||
/// <param name="sequence">The sequence.</param>
|
||||
/// <returns>
|
||||
/// A new trimmed byte array.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">buffer.</exception>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the specified sequence from the end of the buffer if the buffer ends with such sequence.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The buffer.</param>
|
||||
/// <param name="sequence">The sequence.</param>
|
||||
/// <returns>
|
||||
/// A byte array containing the results of encoding the specified set of characters.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">buffer.</exception>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the specified sequence from the end and the start of the buffer
|
||||
/// if the buffer ends and/or starts with such sequence.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The buffer.</param>
|
||||
/// <param name="sequence">The sequence.</param>
|
||||
/// <returns>A byte array containing the results of encoding the specified set of characters.</returns>
|
||||
public static byte[] Trim(this byte[] buffer, params byte[] sequence)
|
||||
{
|
||||
var trimStart = buffer.TrimStart(sequence);
|
||||
return trimStart.TrimEnd(sequence);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the specified buffer ends with the given sequence of bytes.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The buffer.</param>
|
||||
/// <param name="sequence">The sequence.</param>
|
||||
/// <returns>
|
||||
/// True if the specified buffer is ends; otherwise, false.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">buffer.</exception>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the specified buffer starts with the given sequence of bytes.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The buffer.</param>
|
||||
/// <param name="sequence">The sequence.</param>
|
||||
/// <returns><c>true</c> if the specified buffer starts; otherwise, <c>false</c>.</returns>
|
||||
public static bool StartsWith(this byte[] buffer, params byte[] sequence) => buffer.GetIndexOf(sequence) == 0;
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the buffer contains the specified sequence.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The buffer.</param>
|
||||
/// <param name="sequence">The sequence.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if [contains] [the specified sequence]; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public static bool Contains(this byte[] buffer, params byte[] sequence) => buffer.GetIndexOf(sequence) >= 0;
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the buffer exactly matches, byte by byte the specified sequence.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The buffer.</param>
|
||||
/// <param name="sequence">The sequence.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if [is equal to] [the specified sequence]; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">buffer.</exception>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the first instance of the matched sequence based on the given offset.
|
||||
/// If no matches are found then this method returns -1.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The buffer.</param>
|
||||
/// <param name="sequence">The sequence.</param>
|
||||
/// <param name="offset">The offset.</param>
|
||||
/// <returns>The index of the sequence.</returns>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// buffer
|
||||
/// or
|
||||
/// sequence.
|
||||
/// </exception>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends the Memory Stream with the specified buffer.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream.</param>
|
||||
/// <param name="buffer">The buffer.</param>
|
||||
/// <returns>
|
||||
/// The same MemoryStream instance.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// stream
|
||||
/// or
|
||||
/// buffer.
|
||||
/// </exception>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends the Memory Stream with the specified buffer.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream.</param>
|
||||
/// <param name="buffer">The buffer.</param>
|
||||
/// <returns>
|
||||
/// Block of bytes to the current stream using data read from a buffer.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">buffer.</exception>
|
||||
public static MemoryStream Append(this MemoryStream stream, IEnumerable<byte> buffer) => Append(stream, buffer?.ToArray());
|
||||
|
||||
/// <summary>
|
||||
/// Appends the Memory Stream with the specified set of buffers.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream.</param>
|
||||
/// <param name="buffers">The buffers.</param>
|
||||
/// <returns>
|
||||
/// Block of bytes to the current stream using data read from a buffer.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">buffers.</exception>
|
||||
public static MemoryStream Append(this MemoryStream stream, IEnumerable<byte[]> buffers)
|
||||
{
|
||||
if (buffers == null)
|
||||
throw new ArgumentNullException(nameof(buffers));
|
||||
|
||||
foreach (var buffer in buffers)
|
||||
Append(stream, buffer);
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an array of bytes into text with the specified encoding.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The buffer.</param>
|
||||
/// <param name="encoding">The encoding.</param>
|
||||
/// <returns>A <see cref="System.String" /> that contains the results of decoding the specified sequence of bytes.</returns>
|
||||
public static string ToText(this IEnumerable<byte> buffer, Encoding encoding) => encoding.GetString(buffer.ToArray());
|
||||
|
||||
/// <summary>
|
||||
/// Converts an array of bytes into text with UTF8 encoding.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The buffer.</param>
|
||||
/// <returns>A <see cref="System.String" /> that contains the results of decoding the specified sequence of bytes.</returns>
|
||||
public static string ToText(this IEnumerable<byte> buffer) => buffer.ToText(Encoding.UTF8);
|
||||
|
||||
/// <summary>
|
||||
/// Reads the bytes asynchronous.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream.</param>
|
||||
/// <param name="length">The length.</param>
|
||||
/// <param name="bufferLength">Length of the buffer.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>
|
||||
/// A byte array containing the results of encoding the specified set of characters.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">stream.</exception>
|
||||
public static async Task<byte[]> ReadBytesAsync(this Stream stream, long length, int bufferLength, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (stream == null)
|
||||
throw new ArgumentNullException(nameof(stream));
|
||||
|
||||
using (var dest = new MemoryStream())
|
||||
{
|
||||
try
|
||||
{
|
||||
var buff = new byte[bufferLength];
|
||||
while (length > 0)
|
||||
{
|
||||
if (length < bufferLength)
|
||||
bufferLength = (int)length;
|
||||
|
||||
var nread = await stream.ReadAsync(buff, 0, bufferLength, cancellationToken).ConfigureAwait(false);
|
||||
if (nread == 0)
|
||||
break;
|
||||
|
||||
dest.Write(buff, 0, nread);
|
||||
length -= nread;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
||||
return dest.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the bytes asynchronous.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream.</param>
|
||||
/// <param name="length">The length.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>
|
||||
/// A byte array containing the results of encoding the specified set of characters.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">stream.</exception>
|
||||
public static async Task<byte[]> ReadBytesAsync(this Stream stream, int length, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (stream == null)
|
||||
throw new ArgumentNullException(nameof(stream));
|
||||
|
||||
var buff = new byte[length];
|
||||
var offset = 0;
|
||||
try
|
||||
{
|
||||
while (length > 0)
|
||||
{
|
||||
var nread = await stream.ReadAsync(buff, offset, length, cancellationToken).ConfigureAwait(false);
|
||||
if (nread == 0)
|
||||
break;
|
||||
|
||||
offset += nread;
|
||||
length -= nread;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
||||
return new ArraySegment<byte>(buff, 0, offset).ToArray();
|
||||
}
|
||||
|
||||
private static string ToHex(byte[] bytes, bool addPrefix, string format)
|
||||
{
|
||||
if (bytes == null)
|
||||
throw new ArgumentNullException(nameof(bytes));
|
||||
|
||||
var sb = new StringBuilder(bytes.Length * 2);
|
||||
|
||||
foreach (var item in bytes)
|
||||
sb.Append(item.ToString(format, CultureInfo.InvariantCulture));
|
||||
|
||||
return $"{(addPrefix ? "0x" : string.Empty)}{sb}";
|
||||
}
|
||||
}
|
||||
}
|
21
Swan.Lite/Extensions.ComponentCollections.cs
Normal file
21
Swan.Lite/Extensions.ComponentCollections.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using Swan.Collections;
|
||||
|
||||
namespace Swan
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides extension methods for types implementing <see cref="IComponentCollection{T}"/>.
|
||||
/// </summary>
|
||||
public static class ComponentCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the specified component to a collection, without giving it a name.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of components in the collection.</typeparam>
|
||||
/// <param name="this">The <see cref="IComponentCollection{T}" /> on which this method is called.</param>
|
||||
/// <param name="component">The component to add.</param>
|
||||
/// <exception cref="NullReferenceException"><paramref name="this" /> is <see langword="null" />.</exception>
|
||||
/// <seealso cref="IComponentCollection{T}.Add" />
|
||||
public static void Add<T>(this IComponentCollection<T> @this, T component) => @this.Add(null, component);
|
||||
}
|
||||
}
|
234
Swan.Lite/Extensions.Dates.cs
Normal file
234
Swan.Lite/Extensions.Dates.cs
Normal file
@ -0,0 +1,234 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
namespace Swan
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides extension methods for <see cref="DateTime"/>.
|
||||
/// </summary>
|
||||
public static class DateExtensions
|
||||
{
|
||||
private static readonly Dictionary<string, int> DateRanges = new Dictionary<string, int>()
|
||||
{
|
||||
{ "minute", 59},
|
||||
{ "hour", 23},
|
||||
{ "dayOfMonth", 31},
|
||||
{ "month", 12},
|
||||
{ "dayOfWeek", 6},
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Converts the date to a YYYY-MM-DD string.
|
||||
/// </summary>
|
||||
/// <param name="this">The <see cref="DateTime"/> on which this method is called.</param>
|
||||
/// <returns>The concatenation of date.Year, date.Month and date.Day.</returns>
|
||||
public static string ToSortableDate(this DateTime @this)
|
||||
=> $"{@this.Year:0000}-{@this.Month:00}-{@this.Day:00}";
|
||||
|
||||
/// <summary>
|
||||
/// Converts the date to a YYYY-MM-DD HH:II:SS string.
|
||||
/// </summary>
|
||||
/// <param name="this">The <see cref="DateTime"/> on which this method is called.</param>
|
||||
/// <returns>The concatenation of date.Year, date.Month, date.Day, date.Hour, date.Minute and date.Second.</returns>
|
||||
public static string ToSortableDateTime(this DateTime @this)
|
||||
=> $"{@this.Year:0000}-{@this.Month:00}-{@this.Day:00} {@this.Hour:00}:{@this.Minute:00}:{@this.Second:00}";
|
||||
|
||||
/// <summary>
|
||||
/// Parses a YYYY-MM-DD and optionally it time part, HH:II:SS into a DateTime.
|
||||
/// </summary>
|
||||
/// <param name="this">The sortable date.</param>
|
||||
/// <returns>
|
||||
/// A new instance of the DateTime structure to
|
||||
/// the specified year, month, day, hour, minute and second.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">sortableDate.</exception>
|
||||
/// <exception cref="Exception">
|
||||
/// Represents errors that occur during application execution.
|
||||
/// </exception>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// Unable to parse sortable date and time. - sortableDate.
|
||||
/// </exception>
|
||||
public static DateTime ToDateTime(this string @this)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(@this))
|
||||
throw new ArgumentNullException(nameof(@this));
|
||||
|
||||
var hour = 0;
|
||||
var minute = 0;
|
||||
var second = 0;
|
||||
|
||||
var dateTimeParts = @this.Split(' ');
|
||||
|
||||
try
|
||||
{
|
||||
if (dateTimeParts.Length != 1 && dateTimeParts.Length != 2)
|
||||
throw new Exception();
|
||||
|
||||
var dateParts = dateTimeParts[0].Split('-');
|
||||
if (dateParts.Length != 3) throw new Exception();
|
||||
|
||||
var year = int.Parse(dateParts[0]);
|
||||
var month = int.Parse(dateParts[1]);
|
||||
var day = int.Parse(dateParts[2]);
|
||||
|
||||
if (dateTimeParts.Length > 1)
|
||||
{
|
||||
var timeParts = dateTimeParts[1].Split(':');
|
||||
if (timeParts.Length != 3) throw new Exception();
|
||||
|
||||
hour = int.Parse(timeParts[0]);
|
||||
minute = int.Parse(timeParts[1]);
|
||||
second = int.Parse(timeParts[2]);
|
||||
}
|
||||
|
||||
return new DateTime(year, month, day, hour, minute, second);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
throw new ArgumentException("Unable to parse sortable date and time.", nameof(@this));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a date range.
|
||||
/// </summary>
|
||||
/// <param name="startDate">The start date.</param>
|
||||
/// <param name="endDate">The end date.</param>
|
||||
/// <returns>
|
||||
/// A sequence of integral numbers within a specified date's range.
|
||||
/// </returns>
|
||||
public static IEnumerable<DateTime> DateRange(this DateTime startDate, DateTime endDate)
|
||||
=> Enumerable.Range(0, (endDate - startDate).Days + 1).Select(d => startDate.AddDays(d));
|
||||
|
||||
/// <summary>
|
||||
/// Rounds up a date to match a timespan.
|
||||
/// </summary>
|
||||
/// <param name="date">The datetime.</param>
|
||||
/// <param name="timeSpan">The timespan to match.</param>
|
||||
/// <returns>
|
||||
/// A new instance of the DateTime structure to the specified datetime and timespan ticks.
|
||||
/// </returns>
|
||||
public static DateTime RoundUp(this DateTime date, TimeSpan timeSpan)
|
||||
=> new DateTime(((date.Ticks + timeSpan.Ticks - 1) / timeSpan.Ticks) * timeSpan.Ticks);
|
||||
|
||||
/// <summary>
|
||||
/// Get this datetime as a Unix epoch timestamp (seconds since Jan 1, 1970, midnight UTC).
|
||||
/// </summary>
|
||||
/// <param name="this">The <see cref="DateTime"/> on which this method is called.</param>
|
||||
/// <returns>Seconds since Unix epoch.</returns>
|
||||
public static long ToUnixEpochDate(this DateTime @this) => new DateTimeOffset(@this).ToUniversalTime().ToUnixTimeSeconds();
|
||||
|
||||
/// <summary>
|
||||
/// Compares a Date to another and returns a <c>DateTimeSpan</c>.
|
||||
/// </summary>
|
||||
/// <param name="dateStart">The date start.</param>
|
||||
/// <param name="dateEnd">The date end.</param>
|
||||
/// <returns>A DateTimeSpan with the Years, Months, Days, Hours, Minutes, Seconds and Milliseconds between the dates.</returns>
|
||||
public static DateTimeSpan GetDateTimeSpan(this DateTime dateStart, DateTime dateEnd)
|
||||
=> DateTimeSpan.CompareDates(dateStart, dateEnd);
|
||||
|
||||
/// <summary>
|
||||
/// Compare the Date elements(Months, Days, Hours, Minutes).
|
||||
/// </summary>
|
||||
/// <param name="this">The <see cref="DateTime"/> on which this method is called.</param>
|
||||
/// <param name="minute">The minute (0-59).</param>
|
||||
/// <param name="hour">The hour. (0-23).</param>
|
||||
/// <param name="dayOfMonth">The day of month. (1-31).</param>
|
||||
/// <param name="month">The month. (1-12).</param>
|
||||
/// <param name="dayOfWeek">The day of week. (0-6)(Sunday = 0).</param>
|
||||
/// <returns>Returns <c>true</c> if Months, Days, Hours and Minutes match, otherwise <c>false</c>.</returns>
|
||||
public static bool AsCronCanRun(this DateTime @this, int? minute = null, int? hour = null, int? dayOfMonth = null, int? month = null, int? dayOfWeek = null)
|
||||
{
|
||||
var results = new List<bool?>
|
||||
{
|
||||
GetElementParts(minute, @this.Minute),
|
||||
GetElementParts(hour, @this.Hour),
|
||||
GetElementParts(dayOfMonth, @this.Day),
|
||||
GetElementParts(month, @this.Month),
|
||||
GetElementParts(dayOfWeek, (int) @this.DayOfWeek),
|
||||
};
|
||||
|
||||
return results.Any(x => x != false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compare the Date elements(Months, Days, Hours, Minutes).
|
||||
/// </summary>
|
||||
/// <param name="this">The <see cref="DateTime"/> on which this method is called.</param>
|
||||
/// <param name="minute">The minute (0-59).</param>
|
||||
/// <param name="hour">The hour. (0-23).</param>
|
||||
/// <param name="dayOfMonth">The day of month. (1-31).</param>
|
||||
/// <param name="month">The month. (1-12).</param>
|
||||
/// <param name="dayOfWeek">The day of week. (0-6)(Sunday = 0).</param>
|
||||
/// <returns>Returns <c>true</c> if Months, Days, Hours and Minutes match, otherwise <c>false</c>.</returns>
|
||||
public static bool AsCronCanRun(this DateTime @this, string minute = "*", string hour = "*", string dayOfMonth = "*", string month = "*", string dayOfWeek = "*")
|
||||
{
|
||||
var results = new List<bool?>
|
||||
{
|
||||
GetElementParts(minute, nameof(minute), @this.Minute),
|
||||
GetElementParts(hour, nameof(hour), @this.Hour),
|
||||
GetElementParts(dayOfMonth, nameof(dayOfMonth), @this.Day),
|
||||
GetElementParts(month, nameof(month), @this.Month),
|
||||
GetElementParts(dayOfWeek, nameof(dayOfWeek), (int) @this.DayOfWeek),
|
||||
};
|
||||
|
||||
return results.Any(x => x != false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="DateTime"/> to the <see href="https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings#RFC1123">RFC1123 format</see>.
|
||||
/// </summary>
|
||||
/// <param name="this">The <see cref="DateTime"/> on which this method is called.</param>
|
||||
/// <returns>The string representation of <paramref name="this"/> according to <see href="https://tools.ietf.org/html/rfc1123#page-54">RFC1123</see>.</returns>
|
||||
/// <remarks>
|
||||
/// <para>If <paramref name="this"/> is not a UTC date / time, its UTC equivalent is converted, leaving <paramref name="this"/> unchanged.</para>
|
||||
/// </remarks>
|
||||
public static string ToRfc1123String(this DateTime @this)
|
||||
=> @this.ToUniversalTime().ToString("R", CultureInfo.InvariantCulture);
|
||||
|
||||
private static bool? GetElementParts(int? status, int value) => status.HasValue ? status.Value == value : (bool?) null;
|
||||
|
||||
private static bool? GetElementParts(string parts, string type, int value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(parts) || parts == "*")
|
||||
return null;
|
||||
|
||||
if (parts.Contains(","))
|
||||
{
|
||||
return parts.Split(',').Select(int.Parse).Contains(value);
|
||||
}
|
||||
|
||||
var stop = DateRanges[type];
|
||||
|
||||
if (parts.Contains("/"))
|
||||
{
|
||||
var multiple = int.Parse(parts.Split('/').Last());
|
||||
var start = type == "dayOfMonth" || type == "month" ? 1 : 0;
|
||||
|
||||
for (var i = start; i <= stop; i += multiple)
|
||||
if (i == value) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parts.Contains("-"))
|
||||
{
|
||||
var range = parts.Split('-');
|
||||
var start = int.Parse(range.First());
|
||||
stop = Math.Max(stop, int.Parse(range.Last()));
|
||||
|
||||
if ((type == "dayOfMonth" || type == "month") && start == 0)
|
||||
start = 1;
|
||||
|
||||
for (var i = start; i <= stop; i++)
|
||||
if (i == value) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return int.Parse(parts) == value;
|
||||
}
|
||||
}
|
||||
}
|
86
Swan.Lite/Extensions.Dictionaries.cs
Normal file
86
Swan.Lite/Extensions.Dictionaries.cs
Normal file
@ -0,0 +1,86 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Swan
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods.
|
||||
/// </summary>
|
||||
public static partial class Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the value if exists or default.
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">The type of the key.</typeparam>
|
||||
/// <typeparam name="TValue">The type of the value.</typeparam>
|
||||
/// <param name="dict">The dictionary.</param>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <param name="defaultValue">The default value.</param>
|
||||
/// <returns>
|
||||
/// The value of the provided key or default.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">dict.</exception>
|
||||
public static TValue GetValueOrDefault<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key, TValue defaultValue = default)
|
||||
{
|
||||
if (dict == null)
|
||||
throw new ArgumentNullException(nameof(dict));
|
||||
|
||||
return dict.ContainsKey(key) ? dict[key] : defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 <c>ConcurrentDictionary.GetOrAdd</c> method.
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">The type of the key.</typeparam>
|
||||
/// <typeparam name="TValue">The type of the value.</typeparam>
|
||||
/// <param name="dict">The dictionary.</param>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <param name="valueFactory">The value factory.</param>
|
||||
/// <returns>
|
||||
/// The value for the key.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// dict
|
||||
/// or
|
||||
/// valueFactory.
|
||||
/// </exception>
|
||||
public static TValue GetOrAdd<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key, Func<TKey, TValue> valueFactory)
|
||||
{
|
||||
if (dict == null)
|
||||
throw new ArgumentNullException(nameof(dict));
|
||||
|
||||
if (valueFactory == null)
|
||||
throw new ArgumentNullException(nameof(valueFactory));
|
||||
|
||||
if (!dict.ContainsKey(key))
|
||||
{
|
||||
var value = valueFactory(key);
|
||||
if (Equals(value, default)) return default;
|
||||
dict[key] = value;
|
||||
}
|
||||
|
||||
return dict[key];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the item action for each element in the Dictionary.
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">The type of the key.</typeparam>
|
||||
/// <typeparam name="TValue">The type of the value.</typeparam>
|
||||
/// <param name="dict">The dictionary.</param>
|
||||
/// <param name="itemAction">The item action.</param>
|
||||
/// <exception cref="ArgumentNullException">dict.</exception>
|
||||
public static void ForEach<TKey, TValue>(this IDictionary<TKey, TValue> dict, Action<TKey, TValue> itemAction)
|
||||
{
|
||||
if (dict == null)
|
||||
throw new ArgumentNullException(nameof(dict));
|
||||
|
||||
foreach (var kvp in dict)
|
||||
{
|
||||
itemAction(kvp.Key, kvp.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
50
Swan.Lite/Extensions.Exceptions.cs
Normal file
50
Swan.Lite/Extensions.Exceptions.cs
Normal file
@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace Swan
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides extension methods for <see cref="Exception"/>.
|
||||
/// </summary>
|
||||
public static class ExceptionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a value that tells whether an <see cref="Exception"/> is of a type that
|
||||
/// we better not catch and ignore.
|
||||
/// </summary>
|
||||
/// <param name="this">The exception being thrown.</param>
|
||||
/// <returns><see langword="true"/> if <paramref name="this"/> is a critical exception;
|
||||
/// otherwise, <see langword="false"/>.</returns>
|
||||
public static bool IsCriticalException(this Exception @this)
|
||||
=> @this.IsCriticalExceptionCore()
|
||||
|| (@this.InnerException?.IsCriticalException() ?? false)
|
||||
|| (@this is AggregateException aggregateException && aggregateException.InnerExceptions.Any(e => e.IsCriticalException()));
|
||||
|
||||
/// <summary>
|
||||
/// Returns a value that tells whether an <see cref="Exception"/> is of a type that
|
||||
/// will likely cause application failure.
|
||||
/// </summary>
|
||||
/// <param name="this">The exception being thrown.</param>
|
||||
/// <returns><see langword="true"/> if <paramref name="this"/> is a fatal exception;
|
||||
/// otherwise, <see langword="false"/>.</returns>
|
||||
public static bool IsFatalException(this Exception @this)
|
||||
=> @this.IsFatalExceptionCore()
|
||||
|| (@this.InnerException?.IsFatalException() ?? false)
|
||||
|| (@this is AggregateException aggregateException && aggregateException.InnerExceptions.Any(e => e.IsFatalException()));
|
||||
|
||||
private static bool IsCriticalExceptionCore(this Exception @this)
|
||||
=> IsFatalExceptionCore(@this)
|
||||
|| @this is AppDomainUnloadedException
|
||||
|| @this is BadImageFormatException
|
||||
|| @this is CannotUnloadAppDomainException
|
||||
|| @this is InvalidProgramException
|
||||
|| @this is NullReferenceException;
|
||||
|
||||
private static bool IsFatalExceptionCore(this Exception @this)
|
||||
=> @this is StackOverflowException
|
||||
|| @this is OutOfMemoryException
|
||||
|| @this is ThreadAbortException
|
||||
|| @this is AccessViolationException;
|
||||
}
|
||||
}
|
179
Swan.Lite/Extensions.Functional.cs
Normal file
179
Swan.Lite/Extensions.Functional.cs
Normal file
@ -0,0 +1,179 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Swan
|
||||
{
|
||||
/// <summary>
|
||||
/// Functional programming extension methods.
|
||||
/// </summary>
|
||||
public static class FunctionalExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Whens the specified condition.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of IQueryable.</typeparam>
|
||||
/// <param name="list">The list.</param>
|
||||
/// <param name="condition">The condition.</param>
|
||||
/// <param name="fn">The function.</param>
|
||||
/// <returns>
|
||||
/// The IQueryable.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// this
|
||||
/// or
|
||||
/// condition
|
||||
/// or
|
||||
/// fn.
|
||||
/// </exception>
|
||||
public static IQueryable<T> When<T>(
|
||||
this IQueryable<T> list,
|
||||
Func<bool> condition,
|
||||
Func<IQueryable<T>, IQueryable<T>> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whens the specified condition.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of IEnumerable.</typeparam>
|
||||
/// <param name="list">The list.</param>
|
||||
/// <param name="condition">The condition.</param>
|
||||
/// <param name="fn">The function.</param>
|
||||
/// <returns>
|
||||
/// The IEnumerable.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// this
|
||||
/// or
|
||||
/// condition
|
||||
/// or
|
||||
/// fn.
|
||||
/// </exception>
|
||||
public static IEnumerable<T> When<T>(
|
||||
this IEnumerable<T> list,
|
||||
Func<bool> condition,
|
||||
Func<IEnumerable<T>, IEnumerable<T>> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the value when the condition is true.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of IList element.</typeparam>
|
||||
/// <param name="list">The list.</param>
|
||||
/// <param name="condition">The condition.</param>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <returns>
|
||||
/// The IList.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// this
|
||||
/// or
|
||||
/// condition
|
||||
/// or
|
||||
/// value.
|
||||
/// </exception>
|
||||
public static IList<T> AddWhen<T>(
|
||||
this IList<T> list,
|
||||
Func<bool> condition,
|
||||
Func<T> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the value when the condition is true.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of IList element.</typeparam>
|
||||
/// <param name="list">The list.</param>
|
||||
/// <param name="condition">if set to <c>true</c> [condition].</param>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <returns>
|
||||
/// The IList.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">list.</exception>
|
||||
public static IList<T> AddWhen<T>(
|
||||
this IList<T> list,
|
||||
bool condition,
|
||||
T value)
|
||||
{
|
||||
if (list == null)
|
||||
throw new ArgumentNullException(nameof(list));
|
||||
|
||||
if (condition)
|
||||
list.Add(value);
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the range when the condition is true.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of List element.</typeparam>
|
||||
/// <param name="list">The list.</param>
|
||||
/// <param name="condition">The condition.</param>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <returns>
|
||||
/// The List.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// this
|
||||
/// or
|
||||
/// condition
|
||||
/// or
|
||||
/// value.
|
||||
/// </exception>
|
||||
public static List<T> AddRangeWhen<T>(
|
||||
this List<T> list,
|
||||
Func<bool> condition,
|
||||
Func<IEnumerable<T>> 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;
|
||||
}
|
||||
}
|
||||
}
|
455
Swan.Lite/Extensions.Reflection.cs
Normal file
455
Swan.Lite/Extensions.Reflection.cs
Normal file
@ -0,0 +1,455 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Swan.Configuration;
|
||||
using Swan.Reflection;
|
||||
|
||||
namespace Swan
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides various extension methods for Reflection and Types.
|
||||
/// </summary>
|
||||
public static class ReflectionExtensions
|
||||
{
|
||||
private static readonly Lazy<ConcurrentDictionary<Tuple<bool, PropertyInfo>, Func<object, object>>> CacheGetMethods =
|
||||
new Lazy<ConcurrentDictionary<Tuple<bool, PropertyInfo>, Func<object, object>>>(() => new ConcurrentDictionary<Tuple<bool, PropertyInfo>, Func<object, object>>(), true);
|
||||
|
||||
private static readonly Lazy<ConcurrentDictionary<Tuple<bool, PropertyInfo>, Action<object, object[]>>> CacheSetMethods =
|
||||
new Lazy<ConcurrentDictionary<Tuple<bool, PropertyInfo>, Action<object, object[]>>>(() => new ConcurrentDictionary<Tuple<bool, PropertyInfo>, Action<object, object[]>>(), true);
|
||||
|
||||
#region Assembly Extensions
|
||||
|
||||
/// <summary>
|
||||
/// Gets all types within an assembly in a safe manner.
|
||||
/// </summary>
|
||||
/// <param name="assembly">The assembly.</param>
|
||||
/// <returns>
|
||||
/// Array of Type objects representing the types specified by an assembly.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">assembly.</exception>
|
||||
public static IEnumerable<Type> 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
|
||||
|
||||
/// <summary>
|
||||
/// The closest programmatic equivalent of default(T).
|
||||
/// </summary>
|
||||
/// <param name="type">The type.</param>
|
||||
/// <returns>
|
||||
/// Default value of this type.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">type.</exception>
|
||||
public static object? GetDefault(this Type type)
|
||||
{
|
||||
if (type == null)
|
||||
throw new ArgumentNullException(nameof(type));
|
||||
|
||||
return type.IsValueType ? Activator.CreateInstance(type) : default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this type is compatible with ICollection.
|
||||
/// </summary>
|
||||
/// <param name="sourceType">The type.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the specified source type is collection; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">sourceType.</exception>
|
||||
public static bool IsCollection(this Type sourceType)
|
||||
{
|
||||
if (sourceType == null)
|
||||
throw new ArgumentNullException(nameof(sourceType));
|
||||
|
||||
return sourceType != typeof(string) &&
|
||||
typeof(IEnumerable).IsAssignableFrom(sourceType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a method from a type given the method name, binding flags, generic types and parameter types.
|
||||
/// </summary>
|
||||
/// <param name="type">Type of the source.</param>
|
||||
/// <param name="bindingFlags">The binding flags.</param>
|
||||
/// <param name="methodName">Name of the method.</param>
|
||||
/// <param name="genericTypes">The generic types.</param>
|
||||
/// <param name="parameterTypes">The parameter types.</param>
|
||||
/// <returns>
|
||||
/// An object that represents the method with the specified name.
|
||||
/// </returns>
|
||||
/// <exception cref="System.Reflection.AmbiguousMatchException">
|
||||
/// 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.
|
||||
/// </exception>
|
||||
public static MethodInfo GetMethod(
|
||||
this Type type,
|
||||
BindingFlags bindingFlags,
|
||||
string methodName,
|
||||
Type[] genericTypes,
|
||||
Type[] parameterTypes)
|
||||
{
|
||||
if (type == null)
|
||||
throw new ArgumentNullException(nameof(type));
|
||||
|
||||
if (methodName == null)
|
||||
throw new ArgumentNullException(nameof(methodName));
|
||||
|
||||
if (genericTypes == null)
|
||||
throw new ArgumentNullException(nameof(genericTypes));
|
||||
|
||||
if (parameterTypes == null)
|
||||
throw new ArgumentNullException(nameof(parameterTypes));
|
||||
|
||||
var methods = type
|
||||
.GetMethods(bindingFlags)
|
||||
.Where(mi => string.Equals(methodName, mi.Name, StringComparison.Ordinal))
|
||||
.Where(mi => mi.ContainsGenericParameters)
|
||||
.Where(mi => mi.GetGenericArguments().Length == genericTypes.Length)
|
||||
.Where(mi => mi.GetParameters().Length == parameterTypes.Length)
|
||||
.Select(mi => mi.MakeGenericMethod(genericTypes))
|
||||
.Where(mi => mi.GetParameters().Select(pi => pi.ParameterType).SequenceEqual(parameterTypes))
|
||||
.ToList();
|
||||
|
||||
return methods.Count > 1 ? throw new AmbiguousMatchException() : methods.FirstOrDefault();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether [is i enumerable request].
|
||||
/// </summary>
|
||||
/// <param name="type">The type.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if [is i enumerable request] [the specified type]; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">type.</exception>
|
||||
public static bool IsIEnumerable(this Type type)
|
||||
=> type == null
|
||||
? throw new ArgumentNullException(nameof(type))
|
||||
: type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>);
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse using the basic types.
|
||||
/// </summary>
|
||||
/// <param name="type">The type.</param>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <param name="result">The result.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if parsing was successful; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">type</exception>
|
||||
public static bool TryParseBasicType(this Type type, object value, out object? result)
|
||||
{
|
||||
if (type == null)
|
||||
throw new ArgumentNullException(nameof(type));
|
||||
|
||||
if (type != typeof(bool))
|
||||
return TryParseBasicType(type, value.ToStringInvariant(), out result);
|
||||
|
||||
result = value.ToBoolean();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse using the basic types.
|
||||
/// </summary>
|
||||
/// <param name="type">The type.</param>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <param name="result">The result.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if parsing was successful; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">type</exception>
|
||||
public static bool TryParseBasicType(this Type type, string value, out object? result)
|
||||
{
|
||||
if (type == null)
|
||||
throw new ArgumentNullException(nameof(type));
|
||||
|
||||
result = null;
|
||||
|
||||
return Definitions.BasicTypesInfo.Value.ContainsKey(type) && Definitions.BasicTypesInfo.Value[type].TryParse(value, out result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries the type of the set basic value to a property.
|
||||
/// </summary>
|
||||
/// <param name="propertyInfo">The property information.</param>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <param name="target">The object.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if parsing was successful; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">propertyInfo.</exception>
|
||||
public static bool TrySetBasicType(this PropertyInfo propertyInfo, object value, object target)
|
||||
{
|
||||
if (propertyInfo == null)
|
||||
throw new ArgumentNullException(nameof(propertyInfo));
|
||||
|
||||
try
|
||||
{
|
||||
if (propertyInfo.PropertyType.TryParseBasicType(value, out var propertyValue))
|
||||
{
|
||||
propertyInfo.SetValue(target, propertyValue);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// swallow
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries the type of the set to an array a basic type.
|
||||
/// </summary>
|
||||
/// <param name="type">The type.</param>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <param name="target">The array.</param>
|
||||
/// <param name="index">The index.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if parsing was successful; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">type</exception>
|
||||
public static bool TrySetArrayBasicType(this Type type, object value, Array target, int index)
|
||||
{
|
||||
if (type == null)
|
||||
throw new ArgumentNullException(nameof(type));
|
||||
|
||||
if (target == null)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
target.SetValue(null, index);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (type.TryParseBasicType(value, out var propertyValue))
|
||||
{
|
||||
target.SetValue(propertyValue, index);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
|
||||
{
|
||||
target.SetValue(null, index);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// swallow
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to set a property array with another array.
|
||||
/// </summary>
|
||||
/// <param name="propertyInfo">The property.</param>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <param name="obj">The object.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if parsing was successful; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">propertyInfo.</exception>
|
||||
public static bool TrySetArray(this PropertyInfo propertyInfo, IEnumerable<object>? value, object obj)
|
||||
{
|
||||
if (propertyInfo == null)
|
||||
throw new ArgumentNullException(nameof(propertyInfo));
|
||||
|
||||
var elementType = propertyInfo.PropertyType.GetElementType();
|
||||
|
||||
if (elementType == null || value == null)
|
||||
return false;
|
||||
|
||||
var targetArray = Array.CreateInstance(elementType, value.Count());
|
||||
|
||||
var i = 0;
|
||||
|
||||
foreach (var sourceElement in value)
|
||||
{
|
||||
var result = elementType.TrySetArrayBasicType(sourceElement, targetArray, i++);
|
||||
|
||||
if (!result) return false;
|
||||
}
|
||||
|
||||
propertyInfo.SetValue(obj, targetArray);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets property actual value or <c>PropertyDisplayAttribute.DefaultValue</c> if presented.
|
||||
///
|
||||
/// If the <c>PropertyDisplayAttribute.Format</c> value is presented, the property value
|
||||
/// will be formatted accordingly.
|
||||
///
|
||||
/// If the object contains a null value, a empty string will be returned.
|
||||
/// </summary>
|
||||
/// <param name="propertyInfo">The property information.</param>
|
||||
/// <param name="target">The object.</param>
|
||||
/// <returns>The property value or null.</returns>
|
||||
/// <exception cref="ArgumentNullException">propertyInfo.</exception>
|
||||
public static string? ToFormattedString(this PropertyInfo propertyInfo, object target)
|
||||
{
|
||||
if (propertyInfo == null)
|
||||
throw new ArgumentNullException(nameof(propertyInfo));
|
||||
|
||||
try
|
||||
{
|
||||
var value = propertyInfo.GetValue(target);
|
||||
var attr = AttributeCache.DefaultCache.Value.RetrieveOne<PropertyDisplayAttribute>(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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a MethodInfo from a Property Get method.
|
||||
/// </summary>
|
||||
/// <param name="propertyInfo">The property information.</param>
|
||||
/// <param name="nonPublic">if set to <c>true</c> [non public].</param>
|
||||
/// <returns>
|
||||
/// The cached MethodInfo.
|
||||
/// </returns>
|
||||
public static Func<object, object>? GetCacheGetMethod(this PropertyInfo propertyInfo, bool nonPublic = false)
|
||||
{
|
||||
var key = Tuple.Create(!nonPublic, propertyInfo);
|
||||
|
||||
// TODO: Fix public logic
|
||||
return !nonPublic && !CacheGetMethods.Value.ContainsKey(key) && !propertyInfo.GetGetMethod(true).IsPublic
|
||||
? null
|
||||
: CacheGetMethods.Value
|
||||
.GetOrAdd(key,
|
||||
x => y => x.Item2.GetGetMethod(nonPublic).Invoke(y, null));
|
||||
//y => x => y.Item2.CreatePropertyProxy().GetValue(x));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a MethodInfo from a Property Set method.
|
||||
/// </summary>
|
||||
/// <param name="propertyInfo">The property information.</param>
|
||||
/// <param name="nonPublic">if set to <c>true</c> [non public].</param>
|
||||
/// <returns>
|
||||
/// The cached MethodInfo.
|
||||
/// </returns>
|
||||
public static Action<object, object[]>? GetCacheSetMethod(this PropertyInfo propertyInfo, bool nonPublic = false)
|
||||
{
|
||||
var key = Tuple.Create(!nonPublic, propertyInfo);
|
||||
|
||||
return !nonPublic && !CacheSetMethods.Value.ContainsKey(key) && !propertyInfo.GetSetMethod(true).IsPublic
|
||||
? null
|
||||
: CacheSetMethods.Value
|
||||
.GetOrAdd(key,
|
||||
x => (obj, args) => x.Item2.GetSetMethod(nonPublic).Invoke(obj, args));
|
||||
//y => (obj, args) => y.Item2.CreatePropertyProxy().SetValue(obj, args));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a string to a boolean.
|
||||
/// </summary>
|
||||
/// <param name="str">The string.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the string represents a valid truly value, otherwise <c>false</c>.
|
||||
/// </returns>
|
||||
public static bool ToBoolean(this string str)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Convert.ToBoolean(str);
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return Convert.ToBoolean(Convert.ToInt32(str));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a property proxy that stores getter and setter delegates.
|
||||
/// </summary>
|
||||
/// <param name="this">The property information.</param>
|
||||
/// <returns>
|
||||
/// The property proxy.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">this.</exception>
|
||||
public static IPropertyProxy? CreatePropertyProxy(this PropertyInfo @this)
|
||||
{
|
||||
if (@this == null)
|
||||
throw new ArgumentNullException(nameof(@this));
|
||||
|
||||
var genericType = typeof(PropertyProxy<,>)
|
||||
.MakeGenericType(@this.DeclaringType, @this.PropertyType);
|
||||
|
||||
return Activator.CreateInstance(genericType, @this) as IPropertyProxy;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a object to a boolean.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the string represents a valid truly value, otherwise <c>false</c>.
|
||||
/// </returns>
|
||||
public static bool ToBoolean(this object value) => value.ToStringInvariant().ToBoolean();
|
||||
|
||||
private static string ConvertObjectAndFormat(Type propertyType, object value, string format)
|
||||
{
|
||||
if (propertyType == typeof(DateTime) || propertyType == typeof(DateTime?))
|
||||
return Convert.ToDateTime(value, CultureInfo.InvariantCulture).ToString(format);
|
||||
if (propertyType == typeof(int) || propertyType == typeof(int?))
|
||||
return Convert.ToInt32(value, CultureInfo.InvariantCulture).ToString(format);
|
||||
if (propertyType == typeof(decimal) || propertyType == typeof(decimal?))
|
||||
return Convert.ToDecimal(value, CultureInfo.InvariantCulture).ToString(format);
|
||||
if (propertyType == typeof(double) || propertyType == typeof(double?))
|
||||
return Convert.ToDouble(value, CultureInfo.InvariantCulture).ToString(format);
|
||||
if (propertyType == typeof(byte) || propertyType == typeof(byte?))
|
||||
return Convert.ToByte(value, CultureInfo.InvariantCulture).ToString(format);
|
||||
|
||||
return value?.ToString() ?? string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
405
Swan.Lite/Extensions.Strings.cs
Normal file
405
Swan.Lite/Extensions.Strings.cs
Normal file
@ -0,0 +1,405 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Swan.Formatters;
|
||||
|
||||
namespace Swan
|
||||
{
|
||||
/// <summary>
|
||||
/// String related extension methods.
|
||||
/// </summary>
|
||||
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<Regex> SplitLinesRegex =
|
||||
new Lazy<Regex>(() => new Regex("\r\n|\r|\n", StandardRegexOptions));
|
||||
|
||||
private static readonly Lazy<Regex> UnderscoreRegex =
|
||||
new Lazy<Regex>(() => new Regex(@"_", StandardRegexOptions));
|
||||
|
||||
private static readonly Lazy<Regex> CamelCaseRegEx =
|
||||
new Lazy<Regex>(() => new Regex(@"[a-z][A-Z]", StandardRegexOptions));
|
||||
|
||||
private static readonly Lazy<MatchEvaluator> SplitCamelCaseString = new Lazy<MatchEvaluator>(() => m =>
|
||||
{
|
||||
var x = m.ToString();
|
||||
return x[0] + " " + x.Substring(1, x.Length - 1);
|
||||
});
|
||||
|
||||
private static readonly Lazy<string[]> InvalidFilenameChars =
|
||||
new Lazy<string[]>(() => Path.GetInvalidFileNameChars().Select(c => c.ToString()).ToArray());
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string that represents the given item
|
||||
/// It tries to use InvariantCulture if the ToString(IFormatProvider)
|
||||
/// overload exists.
|
||||
/// </summary>
|
||||
/// <param name="this">The item.</param>
|
||||
/// <returns>A <see cref="string" /> that represents the current object.</returns>
|
||||
public static string ToStringInvariant(this object? @this)
|
||||
{
|
||||
if (@this == null)
|
||||
return string.Empty;
|
||||
|
||||
var itemType = @this.GetType();
|
||||
|
||||
if (itemType == typeof(string))
|
||||
return @this as string ?? string.Empty;
|
||||
|
||||
return Definitions.BasicTypesInfo.Value.ContainsKey(itemType)
|
||||
? Definitions.BasicTypesInfo.Value[itemType].ToStringInvariant(@this)
|
||||
: @this.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string that represents the given item
|
||||
/// It tries to use InvariantCulture if the ToString(IFormatProvider)
|
||||
/// overload exists.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to get the string.</typeparam>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>A <see cref="string" /> that represents the current object.</returns>
|
||||
public static string ToStringInvariant<T>(this T item)
|
||||
=> typeof(string) == typeof(T) ? item as string ?? string.Empty : ToStringInvariant(item as object);
|
||||
|
||||
/// <summary>
|
||||
/// Removes the control characters from a string except for those specified.
|
||||
/// </summary>
|
||||
/// <param name="value">The input.</param>
|
||||
/// <param name="excludeChars">When specified, these characters will not be removed.</param>
|
||||
/// <returns>
|
||||
/// A string that represents the current object.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">input.</exception>
|
||||
public static string RemoveControlCharsExcept(this string value, params char[] excludeChars)
|
||||
{
|
||||
if (value == null)
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
|
||||
if (excludeChars == null)
|
||||
excludeChars = Array.Empty<char>();
|
||||
|
||||
return new string(value
|
||||
.Where(c => char.IsControl(c) == false || excludeChars.Contains(c))
|
||||
.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes all control characters from a string, including new line sequences.
|
||||
/// </summary>
|
||||
/// <param name="value">The input.</param>
|
||||
/// <returns>A <see cref="string" /> that represents the current object.</returns>
|
||||
/// <exception cref="ArgumentNullException">input.</exception>
|
||||
public static string RemoveControlChars(this string value) => value.RemoveControlCharsExcept(null);
|
||||
|
||||
/// <summary>
|
||||
/// Outputs JSON string representing this object.
|
||||
/// </summary>
|
||||
/// <param name="this">The object.</param>
|
||||
/// <param name="format">if set to <c>true</c> format the output.</param>
|
||||
/// <returns>A <see cref="string" /> that represents the current object.</returns>
|
||||
public static string ToJson(this object @this, bool format = true) =>
|
||||
@this == null ? string.Empty : Json.Serialize(@this, format);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="this">The object.</param>
|
||||
/// <returns>A <see cref="string" /> that represents the current object.</returns>
|
||||
public static string Stringify(this object @this)
|
||||
{
|
||||
if (@this == null)
|
||||
return "(null)";
|
||||
|
||||
try
|
||||
{
|
||||
var jsonText = Json.Serialize(@this, false, "$type");
|
||||
var jsonData = Json.Deserialize(jsonText);
|
||||
|
||||
return new HumanizeJson(jsonData, 0).GetResult();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return @this.ToStringInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="this">The string.</param>
|
||||
/// <param name="startIndex">The start index.</param>
|
||||
/// <param name="endIndex">The end index.</param>
|
||||
/// <returns>Retrieves a substring from this instance.</returns>
|
||||
public static string Slice(this string @this, int startIndex, int endIndex)
|
||||
{
|
||||
if (@this == null)
|
||||
return string.Empty;
|
||||
|
||||
var end = endIndex.Clamp(startIndex, @this.Length - 1);
|
||||
|
||||
return startIndex >= end ? string.Empty : @this.Substring(startIndex, (end - startIndex) + 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="this">The string.</param>
|
||||
/// <param name="startIndex">The start index.</param>
|
||||
/// <param name="length">The length.</param>
|
||||
/// <returns>Retrieves a substring from this instance.</returns>
|
||||
public static string SliceLength(this string @this, int startIndex, int length)
|
||||
{
|
||||
if (@this == null)
|
||||
return string.Empty;
|
||||
|
||||
var start = startIndex.Clamp(0, @this.Length - 1);
|
||||
var len = length.Clamp(0, @this.Length - start);
|
||||
|
||||
return len == 0 ? string.Empty : @this.Substring(start, len);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Splits the specified text into r, n or rn separated lines.
|
||||
/// </summary>
|
||||
/// <param name="this">The text.</param>
|
||||
/// <returns>
|
||||
/// An array whose elements contain the substrings from this instance
|
||||
/// that are delimited by one or more characters in separator.
|
||||
/// </returns>
|
||||
public static string[] ToLines(this string @this) =>
|
||||
@this == null ? Array.Empty<string>() : SplitLinesRegex.Value.Split(@this);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="value">The identifier-style string.</param>
|
||||
/// <returns>A <see cref="string" /> humanized.</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Humanizes (make more human-readable) an boolean.
|
||||
/// </summary>
|
||||
/// <param name="value">if set to <c>true</c> [value].</param>
|
||||
/// <returns>A <see cref="string" /> that represents the current boolean.</returns>
|
||||
public static string Humanize(this bool value) => value ? "Yes" : "No";
|
||||
|
||||
/// <summary>
|
||||
/// Humanizes (make more human-readable) the specified value.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <returns>A <see cref="string" /> that represents the current object.</returns>
|
||||
public static string Humanize(this object value) =>
|
||||
value switch
|
||||
{
|
||||
string stringValue => stringValue.Humanize(),
|
||||
bool boolValue => boolValue.Humanize(),
|
||||
_ => value.Stringify()
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Indents the specified multi-line text with the given amount of leading spaces
|
||||
/// per line.
|
||||
/// </summary>
|
||||
/// <param name="value">The text.</param>
|
||||
/// <param name="spaces">The spaces.</param>
|
||||
/// <returns>A <see cref="string" /> that represents the current object.</returns>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="value">The string.</param>
|
||||
/// <param name="charIndex">Index of the character.</param>
|
||||
/// <returns>A 2-tuple whose value is (item1, item2).</returns>
|
||||
public static Tuple<int, int> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes the file name system safe.
|
||||
/// </summary>
|
||||
/// <param name="value">The s.</param>
|
||||
/// <returns>
|
||||
/// A string with a safe file name.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">s.</exception>
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
/// Formats a long into the closest bytes string.
|
||||
/// </summary>
|
||||
/// <param name="bytes">The bytes length.</param>
|
||||
/// <returns>
|
||||
/// The string representation of the current Byte object, formatted as specified by the format parameter.
|
||||
/// </returns>
|
||||
public static string FormatBytes(this long bytes) => ((ulong)bytes).FormatBytes();
|
||||
|
||||
/// <summary>
|
||||
/// Formats a long into the closest bytes string.
|
||||
/// </summary>
|
||||
/// <param name="bytes">The bytes length.</param>
|
||||
/// <returns>
|
||||
/// A copy of format in which the format items have been replaced by the string
|
||||
/// representations of the corresponding arguments.
|
||||
/// </returns>
|
||||
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]}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Truncates the specified value.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <param name="maximumLength">The maximum length.</param>
|
||||
/// <returns>
|
||||
/// Retrieves a substring from this instance.
|
||||
/// The substring starts at a specified character position and has a specified length.
|
||||
/// </returns>
|
||||
public static string? Truncate(this string value, int maximumLength) =>
|
||||
Truncate(value, maximumLength, string.Empty);
|
||||
|
||||
/// <summary>
|
||||
/// Truncates the specified value and append the omission last.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <param name="maximumLength">The maximum length.</param>
|
||||
/// <param name="omission">The omission.</param>
|
||||
/// <returns>
|
||||
/// Retrieves a substring from this instance.
|
||||
/// The substring starts at a specified character position and has a specified length.
|
||||
/// </returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified <see cref="string"/> contains any of characters in
|
||||
/// the specified array of <see cref="char"/>.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// <c>true</c> if <paramref name="value"/> contains any of <paramref name="chars"/>;
|
||||
/// otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
/// <param name="value">
|
||||
/// A <see cref="string"/> to test.
|
||||
/// </param>
|
||||
/// <param name="chars">
|
||||
/// An array of <see cref="char"/> that contains characters to find.
|
||||
/// </param>
|
||||
public static bool Contains(this string value, params char[] chars) =>
|
||||
chars?.Length == 0 || (!string.IsNullOrEmpty(value) && value.IndexOfAny(chars) > -1);
|
||||
|
||||
/// <summary>
|
||||
/// Replaces all chars in a string.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <param name="replaceValue">The replace value.</param>
|
||||
/// <param name="chars">The chars.</param>
|
||||
/// <returns>The string with the characters replaced.</returns>
|
||||
public static string ReplaceAll(this string value, string replaceValue, params char[] chars) =>
|
||||
chars.Aggregate(value, (current, c) => current.Replace(new string(new[] { c }), replaceValue));
|
||||
|
||||
/// <summary>
|
||||
/// Convert hex character to an integer. Return -1 if char is something
|
||||
/// other than a hex char.
|
||||
/// </summary>
|
||||
/// <param name="value">The c.</param>
|
||||
/// <returns>Converted integer.</returns>
|
||||
public static int Hex2Int(this char value) =>
|
||||
value >= '0' && value <= '9'
|
||||
? value - '0'
|
||||
: value >= 'A' && value <= 'F'
|
||||
? value - 'A' + 10
|
||||
: value >= 'a' && value <= 'f'
|
||||
? value - 'a' + 10
|
||||
: -1;
|
||||
}
|
||||
}
|
63
Swan.Lite/Extensions.Tasks.cs
Normal file
63
Swan.Lite/Extensions.Tasks.cs
Normal file
@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Swan
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides extension methods for <see cref="Task"/> and <see cref="Task{TResult}"/>.
|
||||
/// </summary>
|
||||
public static class TaskExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// <para>Suspends execution until the specified <see cref="Task"/> is completed.</para>
|
||||
/// <para>This method operates similarly to the <see langword="await"/> C# operator,
|
||||
/// but is meant to be called from a non-<see langword="async"/> method.</para>
|
||||
/// </summary>
|
||||
/// <param name="this">The <see cref="Task"/> on which this method is called.</param>
|
||||
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
|
||||
public static void Await(this Task @this) => @this.GetAwaiter().GetResult();
|
||||
|
||||
/// <summary>
|
||||
/// <para>Suspends execution until the specified <see cref="Task"/> is completed
|
||||
/// and returns its result.</para>
|
||||
/// <para>This method operates similarly to the <see langword="await"/> C# operator,
|
||||
/// but is meant to be called from a non-<see langword="async"/> method.</para>
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The type of the task's result.</typeparam>
|
||||
/// <param name="this">The <see cref="Task{TResult}"/> on which this method is called.</param>
|
||||
/// <returns>The result of <paramref name="this"/>.</returns>
|
||||
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
|
||||
public static TResult Await<TResult>(this Task<TResult> @this) => @this.GetAwaiter().GetResult();
|
||||
|
||||
/// <summary>
|
||||
/// <para>Suspends execution until the specified <see cref="Task"/> is completed.</para>
|
||||
/// <para>This method operates similarly to the <see langword="await" /> C# operator,
|
||||
/// but is meant to be called from a non-<see langword="async" /> method.</para>
|
||||
/// </summary>
|
||||
/// <param name="this">The <see cref="Task" /> on which this method is called.</param>
|
||||
/// <param name="continueOnCapturedContext">If set to <see langword="true"/>,
|
||||
/// attempts to marshal the continuation back to the original context captured.
|
||||
/// This parameter has the same effect as calling the <see cref="Task.ConfigureAwait"/>
|
||||
/// method.</param>
|
||||
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
|
||||
public static void Await(this Task @this, bool continueOnCapturedContext)
|
||||
=> @this.ConfigureAwait(continueOnCapturedContext).GetAwaiter().GetResult();
|
||||
|
||||
/// <summary>
|
||||
/// <para>Suspends execution until the specified <see cref="Task"/> is completed
|
||||
/// and returns its result.</para>
|
||||
/// <para>This method operates similarly to the <see langword="await"/> C# operator,
|
||||
/// but is meant to be called from a non-<see langword="async"/> method.</para>
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The type of the task's result.</typeparam>
|
||||
/// <param name="this">The <see cref="Task{TResult}"/> on which this method is called.</param>
|
||||
/// <param name="continueOnCapturedContext">If set to <see langword="true"/>,
|
||||
/// attempts to marshal the continuation back to the original context captured.
|
||||
/// This parameter has the same effect as calling the <see cref="Task.ConfigureAwait"/>
|
||||
/// method.</param>
|
||||
/// <returns>The result of <paramref name="this"/>.</returns>
|
||||
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
|
||||
public static TResult Await<TResult>(this Task<TResult> @this, bool continueOnCapturedContext)
|
||||
=> @this.ConfigureAwait(continueOnCapturedContext).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
165
Swan.Lite/Extensions.ValueTypes.cs
Normal file
165
Swan.Lite/Extensions.ValueTypes.cs
Normal file
@ -0,0 +1,165 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using Swan.Reflection;
|
||||
|
||||
namespace Swan
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides various extension methods for value types and structs.
|
||||
/// </summary>
|
||||
public static class ValueTypeExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Clamps the specified value between the minimum and the maximum.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of value to clamp.</typeparam>
|
||||
/// <param name="this">The value.</param>
|
||||
/// <param name="min">The minimum.</param>
|
||||
/// <param name="max">The maximum.</param>
|
||||
/// <returns>A value that indicates the relative order of the objects being compared.</returns>
|
||||
public static T Clamp<T>(this T @this, T min, T max)
|
||||
where T : struct, IComparable
|
||||
{
|
||||
if (@this.CompareTo(min) < 0) return min;
|
||||
|
||||
return @this.CompareTo(max) > 0 ? max : @this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps the specified value between the minimum and the maximum.
|
||||
/// </summary>
|
||||
/// <param name="this">The value.</param>
|
||||
/// <param name="min">The minimum.</param>
|
||||
/// <param name="max">The maximum.</param>
|
||||
/// <returns>A value that indicates the relative order of the objects being compared.</returns>
|
||||
public static int Clamp(this int @this, int min, int max)
|
||||
=> @this < min ? min : (@this > max ? max : @this);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified value is between a minimum and a maximum value.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of value to check.</typeparam>
|
||||
/// <param name="this">The value.</param>
|
||||
/// <param name="min">The minimum.</param>
|
||||
/// <param name="max">The maximum.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the specified minimum is between; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public static bool IsBetween<T>(this T @this, T min, T max)
|
||||
where T : struct, IComparable
|
||||
{
|
||||
return @this.CompareTo(min) >= 0 && @this.CompareTo(max) <= 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an array of bytes into the given struct type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of structure to convert.</typeparam>
|
||||
/// <param name="this">The data.</param>
|
||||
/// <returns>a struct type derived from convert an array of bytes ref=ToStruct".</returns>
|
||||
public static T ToStruct<T>(this byte[] @this)
|
||||
where T : struct
|
||||
{
|
||||
return @this == null ? throw new ArgumentNullException(nameof(@this)) : ToStruct<T>(@this, 0, @this.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an array of bytes into the given struct type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of structure to convert.</typeparam>
|
||||
/// <param name="this">The data.</param>
|
||||
/// <param name="offset">The offset.</param>
|
||||
/// <param name="length">The length.</param>
|
||||
/// <returns>
|
||||
/// A managed object containing the data pointed to by the ptr parameter.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">data.</exception>
|
||||
public static T ToStruct<T>(this byte[] @this, int offset, int length)
|
||||
where T : struct
|
||||
{
|
||||
if (@this == null)
|
||||
throw new ArgumentNullException(nameof(@this));
|
||||
|
||||
var buffer = new byte[length];
|
||||
Array.Copy(@this, offset, buffer, 0, buffer.Length);
|
||||
var handle = GCHandle.Alloc(GetStructBytes<T>(buffer), GCHandleType.Pinned);
|
||||
|
||||
try
|
||||
{
|
||||
return Marshal.PtrToStructure<T>(handle.AddrOfPinnedObject());
|
||||
}
|
||||
finally
|
||||
{
|
||||
handle.Free();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a struct to an array of bytes.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of structure to convert.</typeparam>
|
||||
/// <param name="this">The object.</param>
|
||||
/// <returns>A byte array containing the results of encoding the specified set of characters.</returns>
|
||||
public static byte[] ToBytes<T>(this T @this)
|
||||
where T : struct
|
||||
{
|
||||
var data = new byte[Marshal.SizeOf(@this)];
|
||||
var handle = GCHandle.Alloc(data, GCHandleType.Pinned);
|
||||
|
||||
try
|
||||
{
|
||||
Marshal.StructureToPtr(@this, handle.AddrOfPinnedObject(), false);
|
||||
return GetStructBytes<T>(data);
|
||||
}
|
||||
finally
|
||||
{
|
||||
handle.Free();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Swaps the endianness of an unsigned long to an unsigned integer.
|
||||
/// </summary>
|
||||
/// <param name="this">The bytes contained in a long.</param>
|
||||
/// <returns>
|
||||
/// A 32-bit unsigned integer equivalent to the ulong
|
||||
/// contained in longBytes.
|
||||
/// </returns>
|
||||
public static uint SwapEndianness(this ulong @this)
|
||||
=> (uint)(((@this & 0x000000ff) << 24) +
|
||||
((@this & 0x0000ff00) << 8) +
|
||||
((@this & 0x00ff0000) >> 8) +
|
||||
((@this & 0xff000000) >> 24));
|
||||
|
||||
private static byte[] GetStructBytes<T>(byte[] data)
|
||||
{
|
||||
if (data == null)
|
||||
throw new ArgumentNullException(nameof(data));
|
||||
|
||||
var fields = typeof(T).GetTypeInfo()
|
||||
.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
var endian = AttributeCache.DefaultCache.Value.RetrieveOne<StructEndiannessAttribute, T>();
|
||||
|
||||
foreach (var field in fields)
|
||||
{
|
||||
if (endian == null && !field.IsDefined(typeof(StructEndiannessAttribute), false))
|
||||
continue;
|
||||
|
||||
var offset = Marshal.OffsetOf<T>(field.Name).ToInt32();
|
||||
var length = Marshal.SizeOf(field.FieldType);
|
||||
|
||||
endian = endian ?? AttributeCache.DefaultCache.Value.RetrieveOne<StructEndiannessAttribute>(field);
|
||||
|
||||
if (endian != null && (endian.Endianness == Endianness.Big && BitConverter.IsLittleEndian ||
|
||||
endian.Endianness == Endianness.Little && !BitConverter.IsLittleEndian))
|
||||
{
|
||||
Array.Reverse(data, offset, length);
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
}
|
276
Swan.Lite/Extensions.cs
Normal file
276
Swan.Lite/Extensions.cs
Normal file
@ -0,0 +1,276 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Swan.Lite.Reflection;
|
||||
using Swan.Mappers;
|
||||
using Swan.Reflection;
|
||||
|
||||
namespace Swan
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods.
|
||||
/// </summary>
|
||||
public static partial class Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the source.</typeparam>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="target">The target.</param>
|
||||
/// <param name="ignoreProperties">The ignore properties.</param>
|
||||
/// <returns>
|
||||
/// Number of properties that was copied successful.
|
||||
/// </returns>
|
||||
public static int CopyPropertiesTo<T>(this T source, object target, params string[]? ignoreProperties)
|
||||
where T : class =>
|
||||
ObjectMapper.Copy(source, target, GetCopyableProperties(target), ignoreProperties);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="target">The destination.</param>
|
||||
/// <param name="propertiesToCopy">Properties to copy.</param>
|
||||
/// <returns>
|
||||
/// Number of properties that were successfully copied.
|
||||
/// </returns>
|
||||
public static int CopyOnlyPropertiesTo(this object source, object target, params string[]? propertiesToCopy)
|
||||
=> ObjectMapper.Copy(source, target, propertiesToCopy);
|
||||
|
||||
/// <summary>
|
||||
/// Copies the properties to new instance of T.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The new object type.</typeparam>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="ignoreProperties">The ignore properties.</param>
|
||||
/// <returns>
|
||||
/// The specified type with properties copied.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">source.</exception>
|
||||
public static T CopyPropertiesToNew<T>(this object source, string[]? ignoreProperties = null)
|
||||
where T : class
|
||||
{
|
||||
if (source == null)
|
||||
throw new ArgumentNullException(nameof(source));
|
||||
|
||||
var target = Activator.CreateInstance<T>();
|
||||
ObjectMapper.Copy(source, target, GetCopyableProperties(target), ignoreProperties);
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies the only properties to new instance of T.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Object Type.</typeparam>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="propertiesToCopy">The properties to copy.</param>
|
||||
/// <returns>
|
||||
/// The specified type with properties copied.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">source.</exception>
|
||||
public static T CopyOnlyPropertiesToNew<T>(this object source, params string[] propertiesToCopy)
|
||||
where T : class
|
||||
{
|
||||
if (source == null)
|
||||
throw new ArgumentNullException(nameof(source));
|
||||
|
||||
var target = Activator.CreateInstance<T>();
|
||||
ObjectMapper.Copy(source, target, propertiesToCopy);
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Iterates over the keys of the source and tries to write a compatible value to a public,
|
||||
/// instance, writable property in the destination.
|
||||
/// </summary>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="target">The target.</param>
|
||||
/// <param name="ignoreKeys">The ignore keys.</param>
|
||||
/// <returns>Number of properties that was copied successful.</returns>
|
||||
public static int CopyKeyValuePairTo(
|
||||
this IDictionary<string, object> source,
|
||||
object target,
|
||||
params string[] ignoreKeys) =>
|
||||
source == null
|
||||
? throw new ArgumentNullException(nameof(source))
|
||||
: ObjectMapper.Copy(source, target, null, ignoreKeys);
|
||||
|
||||
/// <summary>
|
||||
/// Iterates over the keys of the source and tries to write a compatible value to a public,
|
||||
/// instance, writable property in the destination.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Object Type.</typeparam>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="ignoreKeys">The ignore keys.</param>
|
||||
/// <returns>
|
||||
/// The specified type with properties copied.
|
||||
/// </returns>
|
||||
public static T CopyKeyValuePairToNew<T>(
|
||||
this IDictionary<string, object> source,
|
||||
params string[] ignoreKeys)
|
||||
{
|
||||
if (source == null)
|
||||
throw new ArgumentNullException(nameof(source));
|
||||
|
||||
var target = Activator.CreateInstance<T>();
|
||||
source.CopyKeyValuePairTo(target, ignoreKeys);
|
||||
return target;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Does the specified action.
|
||||
/// </summary>
|
||||
/// <param name="action">The action.</param>
|
||||
/// <param name="retryInterval">The retry interval.</param>
|
||||
/// <param name="retryCount">The retry count.</param>
|
||||
public static void Retry(
|
||||
this Action action,
|
||||
TimeSpan retryInterval = default,
|
||||
int retryCount = 3)
|
||||
{
|
||||
if (action == null)
|
||||
throw new ArgumentNullException(nameof(action));
|
||||
|
||||
Retry<object?>(() =>
|
||||
{
|
||||
action();
|
||||
return null;
|
||||
},
|
||||
retryInterval,
|
||||
retryCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Does the specified action.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the source.</typeparam>
|
||||
/// <param name="action">The action.</param>
|
||||
/// <param name="retryInterval">The retry interval.</param>
|
||||
/// <param name="retryCount">The retry count.</param>
|
||||
/// <returns>
|
||||
/// The return value of the method that this delegate encapsulates.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">action.</exception>
|
||||
/// <exception cref="AggregateException">Represents one or many errors that occur during application execution.</exception>
|
||||
public static T Retry<T>(
|
||||
this Func<T> 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<Exception>();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the copyable properties.
|
||||
///
|
||||
/// If there is no properties with the attribute <c>AttributeCache</c> returns all the properties.
|
||||
/// </summary>
|
||||
/// <param name="this">The object.</param>
|
||||
/// <returns>
|
||||
/// Array of properties.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">model.</exception>
|
||||
/// <seealso cref="AttributeCache"/>
|
||||
public static IEnumerable<string> GetCopyableProperties(this object @this)
|
||||
{
|
||||
if (@this == null)
|
||||
throw new ArgumentNullException(nameof(@this));
|
||||
|
||||
var collection = PropertyTypeCache.DefaultCache.Value
|
||||
.RetrieveAllProperties(@this.GetType(), true);
|
||||
|
||||
var properties = collection
|
||||
.Select(x => new
|
||||
{
|
||||
x.Name,
|
||||
HasAttribute = AttributeCache.DefaultCache.Value.RetrieveOne<CopyableAttribute>(x) != null,
|
||||
})
|
||||
.Where(x => x.HasAttribute)
|
||||
.Select(x => x.Name);
|
||||
|
||||
return properties.Any()
|
||||
? properties
|
||||
: collection.Select(x => x.Name);
|
||||
}
|
||||
|
||||
internal static void CreateTarget(
|
||||
this object source,
|
||||
Type targetType,
|
||||
bool includeNonPublic,
|
||||
ref object? target)
|
||||
{
|
||||
switch (source)
|
||||
{
|
||||
// do nothing. Simply skip creation
|
||||
case string _:
|
||||
break;
|
||||
// When using arrays, there is no default constructor, attempt to build a compatible array
|
||||
case IList sourceObjectList when targetType.IsArray:
|
||||
var elementType = targetType.GetElementType();
|
||||
|
||||
if (elementType != null)
|
||||
target = Array.CreateInstance(elementType, sourceObjectList.Count);
|
||||
break;
|
||||
default:
|
||||
var constructors = ConstructorTypeCache.DefaultCache.Value
|
||||
.RetrieveAllConstructors(targetType, includeNonPublic);
|
||||
|
||||
// Try to check if empty constructor is available
|
||||
if (constructors.Any(x => x.Item2.Length == 0))
|
||||
{
|
||||
target = Activator.CreateInstance(targetType, includeNonPublic);
|
||||
}
|
||||
else
|
||||
{
|
||||
var firstCtor = constructors
|
||||
.OrderBy(x => x.Item2.Length)
|
||||
.FirstOrDefault();
|
||||
|
||||
target = Activator.CreateInstance(targetType,
|
||||
firstCtor?.Item2.Select(arg => arg.GetType().GetDefault()).ToArray());
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
internal static string GetNameWithCase(this string name, JsonSerializerCase jsonSerializerCase) =>
|
||||
jsonSerializerCase switch
|
||||
{
|
||||
JsonSerializerCase.PascalCase => char.ToUpperInvariant(name[0]) + name.Substring(1),
|
||||
JsonSerializerCase.CamelCase => char.ToLowerInvariant(name[0]) + name.Substring(1),
|
||||
JsonSerializerCase.None => name,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(jsonSerializerCase), jsonSerializerCase, null)
|
||||
};
|
||||
}
|
||||
}
|
648
Swan.Lite/Formatters/CsvReader.cs
Normal file
648
Swan.Lite/Formatters/CsvReader.cs
Normal file
@ -0,0 +1,648 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Swan.Reflection;
|
||||
|
||||
namespace Swan.Formatters
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <seealso cref="System.IDisposable" />
|
||||
/// <example>
|
||||
/// The following example describes how to load a list of objects from a CSV file.
|
||||
/// <code>
|
||||
/// using Swan.Formatters;
|
||||
///
|
||||
/// class Example
|
||||
/// {
|
||||
/// class Person
|
||||
/// {
|
||||
/// public string Name { get; set; }
|
||||
/// public int Age { get; set; }
|
||||
/// }
|
||||
///
|
||||
/// static void Main()
|
||||
/// {
|
||||
/// // load records from a CSV file
|
||||
/// var loadedRecords =
|
||||
/// CsvReader.LoadRecords<Person>("C:\\Users\\user\\Documents\\file.csv");
|
||||
///
|
||||
/// // loadedRecords =
|
||||
/// // [
|
||||
/// // { Age = 20, Name = "George" }
|
||||
/// // { Age = 18, Name = "Juan" }
|
||||
/// // ]
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// The following code explains how to read a CSV formatted string.
|
||||
/// <code>
|
||||
/// using Swan.Formatters;
|
||||
/// using System.Text;
|
||||
/// using Swan.Formatters;
|
||||
///
|
||||
/// class Example
|
||||
/// {
|
||||
/// static void Main()
|
||||
/// {
|
||||
/// // data to be read
|
||||
/// var data = @"Company,OpenPositions,MainTechnology,Revenue
|
||||
/// Co,2,""C#, MySQL, JavaScript, HTML5 and CSS3"",500
|
||||
/// Ca,2,""C#, MySQL, JavaScript, HTML5 and CSS3"",600";
|
||||
///
|
||||
/// using(var stream = new MemoryStream(Encoding.UTF8.GetBytes(data)))
|
||||
/// {
|
||||
/// // create a CSV reader
|
||||
/// var reader = new CsvReader(stream, false, Encoding.UTF8);
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
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<string, string> _defaultMap;
|
||||
private StreamReader _reader;
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CsvReader" /> class.
|
||||
/// </summary>
|
||||
/// <param name="inputStream">The stream.</param>
|
||||
/// <param name="leaveOpen">if set to <c>true</c> leaves the input stream open.</param>
|
||||
/// <param name="textEncoding">The text encoding.</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CsvReader"/> class.
|
||||
/// It will automatically close the stream upon disposing.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream.</param>
|
||||
/// <param name="textEncoding">The text encoding.</param>
|
||||
public CsvReader(Stream stream, Encoding textEncoding)
|
||||
: this(stream, false, textEncoding)
|
||||
{
|
||||
// placeholder
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CsvReader"/> class.
|
||||
/// It automatically closes the stream when disposing this reader
|
||||
/// and uses the Windows 1253 encoding.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream.</param>
|
||||
public CsvReader(Stream stream)
|
||||
: this(stream, false, Definitions.Windows1252Encoding)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CsvReader"/> class.
|
||||
/// It uses the Windows 1252 Encoding by default and it automatically closes the file
|
||||
/// when this reader is disposed of.
|
||||
/// </summary>
|
||||
/// <param name="filename">The filename.</param>
|
||||
public CsvReader(string filename)
|
||||
: this(File.OpenRead(filename), false, Definitions.Windows1252Encoding)
|
||||
{
|
||||
// placeholder
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CsvReader"/> class.
|
||||
/// It automatically closes the file when disposing this reader.
|
||||
/// </summary>
|
||||
/// <param name="filename">The filename.</param>
|
||||
/// <param name="encoding">The encoding.</param>
|
||||
public CsvReader(string filename, Encoding encoding)
|
||||
: this(File.OpenRead(filename), false, encoding)
|
||||
{
|
||||
// placeholder
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets number of lines that have been read, including the headings.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The count.
|
||||
/// </value>
|
||||
public ulong Count
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
return _count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the escape character.
|
||||
/// By default it is the double quote '"'.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The escape character.
|
||||
/// </value>
|
||||
public char EscapeCharacter
|
||||
{
|
||||
get => _escapeCharacter;
|
||||
set
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
_escapeCharacter = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the separator character.
|
||||
/// By default it is the comma character ','.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The separator character.
|
||||
/// </value>
|
||||
public char SeparatorCharacter
|
||||
{
|
||||
get => _separatorCharacter;
|
||||
set
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
_separatorCharacter = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if [end of stream]; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public bool EndOfStream
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
return _reader.EndOfStream;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Generic, Main ReadLine method
|
||||
|
||||
/// <summary>
|
||||
/// Reads a line of CSV text into an array of strings.
|
||||
/// </summary>
|
||||
/// <returns>An array of the specified element type containing copies of the elements of the ArrayList.</returns>
|
||||
/// <exception cref="System.IO.EndOfStreamException">Cannot read past the end of the stream.</exception>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
/// <exception cref="System.IO.EndOfStreamException">Cannot read past the end of the stream.</exception>
|
||||
public void SkipRecord()
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (_reader.EndOfStream)
|
||||
throw new EndOfStreamException("Cannot read past the end of the stream");
|
||||
|
||||
ParseRecord(_reader, _escapeCharacter, _separatorCharacter);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <returns>An array of the specified element type containing copies of the elements of the ArrayList.</returns>
|
||||
/// <exception cref="System.InvalidOperationException">
|
||||
/// Reading headings is only supported as the first read operation.
|
||||
/// or
|
||||
/// ReadHeadings.
|
||||
/// </exception>
|
||||
/// <exception cref="System.IO.EndOfStreamException">Cannot read past the end of the stream.</exception>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a line of CSV text, converting it into a dynamic object in which properties correspond to the names of the headings.
|
||||
/// </summary>
|
||||
/// <param name="map">The mappings between CSV headings (keys) and object properties (values).</param>
|
||||
/// <returns>Object of the type of the elements in the collection of key/value pairs.</returns>
|
||||
/// <exception cref="System.InvalidOperationException">ReadHeadings.</exception>
|
||||
/// <exception cref="System.IO.EndOfStreamException">Cannot read past the end of the stream.</exception>
|
||||
/// <exception cref="System.ArgumentNullException">map.</exception>
|
||||
public IDictionary<string, object> ReadObject(IDictionary<string, string> 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<string, object>();
|
||||
var values = ReadLine();
|
||||
|
||||
for (var i = 0; i < _headings.Length; i++)
|
||||
{
|
||||
if (i > values.Length - 1)
|
||||
break;
|
||||
|
||||
result[_headings[i]] = values[i];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a line of CSV text, converting it into a dynamic object
|
||||
/// The property names correspond to the names of the CSV headings.
|
||||
/// </summary>
|
||||
/// <returns>Object of the type of the elements in the collection of key/value pairs.</returns>
|
||||
public IDictionary<string, object> ReadObject() => ReadObject(_defaultMap);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of object to map.</typeparam>
|
||||
/// <param name="map">The map.</param>
|
||||
/// <param name="result">The result.</param>
|
||||
/// <exception cref="System.ArgumentNullException">map
|
||||
/// or
|
||||
/// result.</exception>
|
||||
/// <exception cref="System.InvalidOperationException">ReadHeadings.</exception>
|
||||
/// <exception cref="System.IO.EndOfStreamException">Cannot read past the end of the stream.</exception>
|
||||
public void ReadObject<T>(IDictionary<string, string> map, ref T result)
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
// Check arguments
|
||||
{
|
||||
if (map == null)
|
||||
throw new ArgumentNullException(nameof(map));
|
||||
|
||||
if (_reader.EndOfStream)
|
||||
throw new EndOfStreamException("Cannot read past the end of the stream");
|
||||
|
||||
if (_headings == null)
|
||||
throw new InvalidOperationException($"Call the {nameof(ReadHeadings)} method before reading as an object.");
|
||||
|
||||
if (Equals(result, default(T)))
|
||||
throw new ArgumentNullException(nameof(result));
|
||||
}
|
||||
|
||||
// Read line and extract values
|
||||
var values = ReadLine();
|
||||
|
||||
// Extract properties from cache
|
||||
var properties = TypeCache
|
||||
.RetrieveFilteredProperties(typeof(T), true, x => x.CanWrite && Definitions.BasicTypesInfo.Value.ContainsKey(x.PropertyType));
|
||||
|
||||
// Assign property values for each heading
|
||||
for (var i = 0; i < _headings.Length; i++)
|
||||
{
|
||||
// break if no more headings are matched
|
||||
if (i > values.Length - 1)
|
||||
break;
|
||||
|
||||
// skip if no heading is available or the heading is empty
|
||||
if (map.ContainsKey(_headings[i]) == false &&
|
||||
string.IsNullOrWhiteSpace(map[_headings[i]]) == false)
|
||||
continue;
|
||||
|
||||
// Prepare the target property
|
||||
var propertyName = map[_headings[i]];
|
||||
|
||||
// Parse and assign the basic type value to the property if exists
|
||||
properties
|
||||
.FirstOrDefault(p => p.Name == propertyName)?
|
||||
.TrySetBasicType(values[i], result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of object to map.</typeparam>
|
||||
/// <param name="map">The map of CSV headings (keys) and Type property names (values).</param>
|
||||
/// <returns>The conversion of specific type of object.</returns>
|
||||
/// <exception cref="System.ArgumentNullException">map.</exception>
|
||||
/// <exception cref="System.InvalidOperationException">ReadHeadings.</exception>
|
||||
/// <exception cref="System.IO.EndOfStreamException">Cannot read past the end of the stream.</exception>
|
||||
public T ReadObject<T>(IDictionary<string, string> map)
|
||||
where T : new()
|
||||
{
|
||||
var result = Activator.CreateInstance<T>();
|
||||
ReadObject(map, ref result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of object.</typeparam>
|
||||
/// <returns>The conversion of specific type of object.</returns>
|
||||
public T ReadObject<T>()
|
||||
where T : new()
|
||||
{
|
||||
return ReadObject<T>(_defaultMap);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Support Methods
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="reader">The reader.</param>
|
||||
/// <param name="escapeCharacter">The escape character.</param>
|
||||
/// <param name="separatorCharacter">The separator character.</param>
|
||||
/// <returns>An array of the specified element type containing copies of the elements of the ArrayList.</returns>
|
||||
private static string[] ParseRecord(StreamReader reader, char escapeCharacter = '"', char separatorCharacter = ',')
|
||||
{
|
||||
var values = new List<string>();
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// Loads the records from the stream
|
||||
/// This method uses Windows 1252 encoding.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of IList items to load.</typeparam>
|
||||
/// <param name="stream">The stream.</param>
|
||||
/// <returns>A generic collection of objects that can be individually accessed by index.</returns>
|
||||
public static IList<T> LoadRecords<T>(Stream stream)
|
||||
where T : new()
|
||||
{
|
||||
var result = new List<T>();
|
||||
|
||||
using (var reader = new CsvReader(stream))
|
||||
{
|
||||
reader.ReadHeadings();
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
result.Add(reader.ReadObject<T>());
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the records from the give file path.
|
||||
/// This method uses Windows 1252 encoding.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of IList items to load.</typeparam>
|
||||
/// <param name="filePath">The file path.</param>
|
||||
/// <returns>A generic collection of objects that can be individually accessed by index.</returns>
|
||||
public static IList<T> LoadRecords<T>(string filePath)
|
||||
where T : new()
|
||||
{
|
||||
return LoadRecords<T>(File.OpenRead(filePath));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable Support
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_hasDisposed) return;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
try
|
||||
{
|
||||
_reader.Dispose();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_reader = null;
|
||||
}
|
||||
}
|
||||
|
||||
_hasDisposed = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Defines the 3 different read states
|
||||
/// for the parsing state machine.
|
||||
/// </summary>
|
||||
private enum ReadState
|
||||
{
|
||||
WaitingForNewField,
|
||||
PushingNormal,
|
||||
PushingQuoted,
|
||||
}
|
||||
}
|
||||
}
|
459
Swan.Lite/Formatters/CsvWriter.cs
Normal file
459
Swan.Lite/Formatters/CsvWriter.cs
Normal file
@ -0,0 +1,459 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using Swan.Reflection;
|
||||
|
||||
namespace Swan.Formatters
|
||||
{
|
||||
/// <summary>
|
||||
/// A CSV writer useful for exporting a set of objects.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// The following code describes how to save a list of objects into a CSV file.
|
||||
/// <code>
|
||||
/// using System.Collections.Generic;
|
||||
/// using Swan.Formatters;
|
||||
///
|
||||
/// class Example
|
||||
/// {
|
||||
/// class Person
|
||||
/// {
|
||||
/// public string Name { get; set; }
|
||||
/// public int Age { get; set; }
|
||||
/// }
|
||||
///
|
||||
/// static void Main()
|
||||
/// {
|
||||
/// // create a list of people
|
||||
/// var people = new List<Person>
|
||||
/// {
|
||||
/// new Person { Name = "Artyom", Age = 20 },
|
||||
/// new Person { Name = "Aloy", Age = 18 }
|
||||
/// }
|
||||
///
|
||||
/// // write items inside file.csv
|
||||
/// CsvWriter.SaveRecords(people, "C:\\Users\\user\\Documents\\file.csv");
|
||||
///
|
||||
/// // output
|
||||
/// // | Name | Age |
|
||||
/// // | Artyom | 20 |
|
||||
/// // | Aloy | 18 |
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CsvWriter" /> class.
|
||||
/// </summary>
|
||||
/// <param name="outputStream">The output stream.</param>
|
||||
/// <param name="leaveOpen">if set to <c>true</c> [leave open].</param>
|
||||
/// <param name="encoding">The encoding.</param>
|
||||
public CsvWriter(Stream outputStream, bool leaveOpen, Encoding encoding)
|
||||
{
|
||||
_outputStream = outputStream;
|
||||
_encoding = encoding;
|
||||
_leaveStreamOpen = leaveOpen;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CsvWriter"/> class.
|
||||
/// It automatically closes the stream when disposing this writer.
|
||||
/// </summary>
|
||||
/// <param name="outputStream">The output stream.</param>
|
||||
/// <param name="encoding">The encoding.</param>
|
||||
public CsvWriter(Stream outputStream, Encoding encoding)
|
||||
: this(outputStream, false, encoding)
|
||||
{
|
||||
// placeholder
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CsvWriter"/> class.
|
||||
/// It uses the Windows 1252 encoding and automatically closes
|
||||
/// the stream upon disposing this writer.
|
||||
/// </summary>
|
||||
/// <param name="outputStream">The output stream.</param>
|
||||
public CsvWriter(Stream outputStream)
|
||||
: this(outputStream, false, Definitions.Windows1252Encoding)
|
||||
{
|
||||
// placeholder
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CsvWriter"/> class.
|
||||
/// It opens the file given file, automatically closes the stream upon
|
||||
/// disposing of this writer, and uses the Windows 1252 encoding.
|
||||
/// </summary>
|
||||
/// <param name="filename">The filename.</param>
|
||||
public CsvWriter(string filename)
|
||||
: this(File.OpenWrite(filename), false, Definitions.Windows1252Encoding)
|
||||
{
|
||||
// placeholder
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CsvWriter"/> class.
|
||||
/// It opens the file given file, automatically closes the stream upon
|
||||
/// disposing of this writer, and uses the given text encoding for output.
|
||||
/// </summary>
|
||||
/// <param name="filename">The filename.</param>
|
||||
/// <param name="encoding">The encoding.</param>
|
||||
public CsvWriter(string filename, Encoding encoding)
|
||||
: this(File.OpenWrite(filename), false, encoding)
|
||||
{
|
||||
// placeholder
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the field separator character.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The separator character.
|
||||
/// </value>
|
||||
public char SeparatorCharacter { get; set; } = ',';
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the escape character to use to escape field values.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The escape character.
|
||||
/// </value>
|
||||
public char EscapeCharacter { get; set; } = '"';
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the new line character sequence to use when writing a line.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The new line sequence.
|
||||
/// </value>
|
||||
public string NewLineSequence { get; set; } = Environment.NewLine;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a list of properties to ignore when outputting CSV lines.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The ignore property names.
|
||||
/// </value>
|
||||
public List<string> IgnorePropertyNames { get; } = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets number of lines that have been written, including the headings line.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The count.
|
||||
/// </value>
|
||||
public ulong Count
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
return _mCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helpers
|
||||
|
||||
/// <summary>
|
||||
/// Saves the items to a stream.
|
||||
/// It uses the Windows 1252 text encoding for output.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of enumeration.</typeparam>
|
||||
/// <param name="items">The items.</param>
|
||||
/// <param name="stream">The stream.</param>
|
||||
/// <param name="truncateData"><c>true</c> if stream is truncated, default <c>false</c>.</param>
|
||||
/// <returns>Number of item saved.</returns>
|
||||
public static int SaveRecords<T>(IEnumerable<T> 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<T>();
|
||||
writer.WriteObjects(items);
|
||||
return (int)writer.Count;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of enumeration.</typeparam>
|
||||
/// <param name="items">The items.</param>
|
||||
/// <param name="filePath">The file path.</param>
|
||||
/// <returns>Number of item saved.</returns>
|
||||
public static int SaveRecords<T>(IEnumerable<T> items, string filePath) => SaveRecords(items, File.OpenWrite(filePath), true);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Generic, main Write Line Method
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="items">The items.</param>
|
||||
public void WriteLine(params object[] items)
|
||||
=> WriteLine(items.Select(x => x == null ? string.Empty : x.ToStringInvariant()));
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="items">The items.</param>
|
||||
public void WriteLine(IEnumerable<object> items)
|
||||
=> WriteLine(items.Select(x => x == null ? string.Empty : x.ToStringInvariant()));
|
||||
|
||||
/// <summary>
|
||||
/// Writes a line of CSV text.
|
||||
/// If items are found to be null, empty strings are written out.
|
||||
/// </summary>
|
||||
/// <param name="items">The items.</param>
|
||||
public void WriteLine(params string[] items) => WriteLine((IEnumerable<string>) items);
|
||||
|
||||
/// <summary>
|
||||
/// Writes a line of CSV text.
|
||||
/// If items are found to be null, empty strings are written out.
|
||||
/// </summary>
|
||||
/// <param name="items">The items.</param>
|
||||
public void WriteLine(IEnumerable<string> 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
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <exception cref="System.ArgumentNullException">item.</exception>
|
||||
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<object>());
|
||||
return;
|
||||
default:
|
||||
WriteLine(GetFilteredTypeProperties(item.GetType())
|
||||
.Select(x => x.ToFormattedString(item)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of object to write.</typeparam>
|
||||
/// <param name="item">The item.</param>
|
||||
public void WriteObject<T>(T item) => WriteObject(item as object);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of object to write.</typeparam>
|
||||
/// <param name="items">The items.</param>
|
||||
public void WriteObjects<T>(IEnumerable<T> items)
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
foreach (var item in items)
|
||||
WriteObject(item);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Write Headings Methods
|
||||
|
||||
/// <summary>
|
||||
/// Writes the headings.
|
||||
/// </summary>
|
||||
/// <param name="type">The type of object to extract headings.</param>
|
||||
/// <exception cref="System.ArgumentNullException">type.</exception>
|
||||
public void WriteHeadings(Type type)
|
||||
{
|
||||
if (type == null)
|
||||
throw new ArgumentNullException(nameof(type));
|
||||
|
||||
var properties = GetFilteredTypeProperties(type).Select(p => p.Name).Cast<object>();
|
||||
WriteLine(properties);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the headings.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of object to extract headings.</typeparam>
|
||||
public void WriteHeadings<T>() => WriteHeadings(typeof(T));
|
||||
|
||||
/// <summary>
|
||||
/// Writes the headings.
|
||||
/// </summary>
|
||||
/// <param name="dictionary">The dictionary to extract headings.</param>
|
||||
/// <exception cref="System.ArgumentNullException">dictionary.</exception>
|
||||
public void WriteHeadings(IDictionary dictionary)
|
||||
{
|
||||
if (dictionary == null)
|
||||
throw new ArgumentNullException(nameof(dictionary));
|
||||
|
||||
WriteLine(GetFilteredDictionary(dictionary, true));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the headings.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to extract headings.</param>
|
||||
/// <exception cref="ArgumentNullException">obj.</exception>
|
||||
public void WriteHeadings(object obj)
|
||||
{
|
||||
if (obj == null)
|
||||
throw new ArgumentNullException(nameof(obj));
|
||||
|
||||
WriteHeadings(obj.GetType());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable Support
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() => Dispose(true);
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
/// <param name="disposeAlsoManaged"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected virtual void Dispose(bool disposeAlsoManaged)
|
||||
{
|
||||
if (_isDisposing) return;
|
||||
|
||||
if (disposeAlsoManaged)
|
||||
{
|
||||
if (_leaveStreamOpen == false)
|
||||
{
|
||||
_outputStream.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
_isDisposing = true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Support Methods
|
||||
|
||||
private IEnumerable<string> GetFilteredDictionary(IDictionary dictionary, bool filterKeys = false)
|
||||
=> dictionary
|
||||
.Keys
|
||||
.Cast<object>()
|
||||
.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<PropertyInfo> GetFilteredTypeProperties(Type type)
|
||||
=> TypeCache.Retrieve(type, t =>
|
||||
t.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(p => p.CanRead))
|
||||
.Where(p => !IgnorePropertyNames.Contains(p.Name));
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
}
|
150
Swan.Lite/Formatters/HumanizeJson.cs
Normal file
150
Swan.Lite/Formatters/HumanizeJson.cs
Normal file
@ -0,0 +1,150 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Swan.Formatters
|
||||
{
|
||||
internal class HumanizeJson
|
||||
{
|
||||
private readonly StringBuilder _builder = new StringBuilder();
|
||||
private readonly int _indent;
|
||||
private readonly string _indentStr;
|
||||
private readonly object _obj;
|
||||
|
||||
public HumanizeJson(object obj, int indent)
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_indent = indent;
|
||||
_indentStr = new string(' ', indent * 4);
|
||||
_obj = obj;
|
||||
|
||||
ParseObject();
|
||||
}
|
||||
|
||||
public string GetResult() => _builder == null ? string.Empty : _builder.ToString().TrimEnd();
|
||||
|
||||
private void ParseObject()
|
||||
{
|
||||
switch (_obj)
|
||||
{
|
||||
case Dictionary<string, object> dictionary:
|
||||
AppendDictionary(dictionary);
|
||||
break;
|
||||
case List<object> list:
|
||||
AppendList(list);
|
||||
break;
|
||||
default:
|
||||
AppendString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void AppendDictionary(Dictionary<string, object> objects)
|
||||
{
|
||||
foreach (var kvp in objects)
|
||||
{
|
||||
if (kvp.Value == null) continue;
|
||||
|
||||
var writeOutput = false;
|
||||
|
||||
switch (kvp.Value)
|
||||
{
|
||||
case Dictionary<string, object> valueDictionary:
|
||||
if (valueDictionary.Count > 0)
|
||||
{
|
||||
writeOutput = true;
|
||||
_builder
|
||||
.Append($"{_indentStr}{kvp.Key,-16}: object")
|
||||
.AppendLine();
|
||||
}
|
||||
|
||||
break;
|
||||
case List<object> 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<object> objects)
|
||||
{
|
||||
var index = 0;
|
||||
foreach (var value in objects)
|
||||
{
|
||||
var writeOutput = false;
|
||||
|
||||
switch (value)
|
||||
{
|
||||
case Dictionary<string, object> valueDictionary:
|
||||
if (valueDictionary.Count > 0)
|
||||
{
|
||||
writeOutput = true;
|
||||
_builder
|
||||
.Append($"{_indentStr}[{index}]: object")
|
||||
.AppendLine();
|
||||
}
|
||||
|
||||
break;
|
||||
case List<object> 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
338
Swan.Lite/Formatters/Json.Converter.cs
Normal file
338
Swan.Lite/Formatters/Json.Converter.cs
Normal file
@ -0,0 +1,338 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using Swan.Reflection;
|
||||
|
||||
namespace Swan.Formatters
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static partial class Json
|
||||
{
|
||||
private class Converter
|
||||
{
|
||||
private static readonly ConcurrentDictionary<MemberInfo, string> MemberInfoNameCache =
|
||||
new ConcurrentDictionary<MemberInfo, string>();
|
||||
|
||||
private static readonly ConcurrentDictionary<Type, Type> ListAddMethodCache = new ConcurrentDictionary<Type, Type>();
|
||||
|
||||
private readonly object? _target;
|
||||
private readonly Type _targetType;
|
||||
private readonly bool _includeNonPublic;
|
||||
private readonly JsonSerializerCase _jsonSerializerCase;
|
||||
|
||||
private Converter(
|
||||
object? source,
|
||||
Type targetType,
|
||||
ref object? targetInstance,
|
||||
bool includeNonPublic,
|
||||
JsonSerializerCase jsonSerializerCase)
|
||||
{
|
||||
_targetType = targetInstance != null ? targetInstance.GetType() : targetType;
|
||||
_includeNonPublic = includeNonPublic;
|
||||
_jsonSerializerCase = jsonSerializerCase;
|
||||
|
||||
if (source == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var sourceType = source.GetType();
|
||||
|
||||
if (_targetType == null || _targetType == typeof(object)) _targetType = sourceType;
|
||||
if (sourceType == _targetType)
|
||||
{
|
||||
_target = source;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TrySetInstance(targetInstance, source, ref _target))
|
||||
return;
|
||||
|
||||
ResolveObject(source, ref _target);
|
||||
}
|
||||
|
||||
internal static object? FromJsonResult(
|
||||
object? source,
|
||||
JsonSerializerCase jsonSerializerCase,
|
||||
Type? targetType = null,
|
||||
bool includeNonPublic = false)
|
||||
{
|
||||
object? nullRef = null;
|
||||
return new Converter(source, targetType ?? typeof(object), ref nullRef, includeNonPublic, jsonSerializerCase).GetResult();
|
||||
}
|
||||
|
||||
private static object? FromJsonResult(object source,
|
||||
Type targetType,
|
||||
ref object? targetInstance,
|
||||
bool includeNonPublic)
|
||||
{
|
||||
return new Converter(source, targetType, ref targetInstance, includeNonPublic, JsonSerializerCase.None).GetResult();
|
||||
}
|
||||
|
||||
private static Type? GetAddMethodParameterType(Type targetType)
|
||||
=> ListAddMethodCache.GetOrAdd(targetType,
|
||||
x => x.GetMethods()
|
||||
.FirstOrDefault(
|
||||
m => m.Name == AddMethodName && m.IsPublic && m.GetParameters().Length == 1)?
|
||||
.GetParameters()[0]
|
||||
.ParameterType);
|
||||
|
||||
private static void GetByteArray(string sourceString, ref object? target)
|
||||
{
|
||||
try
|
||||
{
|
||||
target = Convert.FromBase64String(sourceString);
|
||||
} // Try conversion from Base 64
|
||||
catch (FormatException)
|
||||
{
|
||||
target = Encoding.UTF8.GetBytes(sourceString);
|
||||
} // Get the string bytes in UTF8
|
||||
}
|
||||
|
||||
private object GetSourcePropertyValue(
|
||||
IDictionary<string, object> sourceProperties,
|
||||
MemberInfo targetProperty)
|
||||
{
|
||||
var targetPropertyName = MemberInfoNameCache.GetOrAdd(
|
||||
targetProperty,
|
||||
x => AttributeCache.DefaultCache.Value.RetrieveOne<JsonPropertyAttribute>(x)?.PropertyName ?? x.Name.GetNameWithCase(_jsonSerializerCase));
|
||||
|
||||
return sourceProperties.GetValueOrDefault(targetPropertyName);
|
||||
}
|
||||
|
||||
private bool TrySetInstance(object? targetInstance, object source, ref object? target)
|
||||
{
|
||||
if (targetInstance == null)
|
||||
{
|
||||
// Try to create a default instance
|
||||
try
|
||||
{
|
||||
source.CreateTarget(_targetType, _includeNonPublic, ref target);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
target = targetInstance;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private object? GetResult() => _target ?? _targetType.GetDefault();
|
||||
|
||||
private void ResolveObject(object source, ref object? target)
|
||||
{
|
||||
switch (source)
|
||||
{
|
||||
// Case 0: Special Cases Handling (Source and Target are of specific convertible types)
|
||||
// Case 0.1: Source is string, Target is byte[]
|
||||
case string sourceString when _targetType == typeof(byte[]):
|
||||
GetByteArray(sourceString, ref target);
|
||||
break;
|
||||
|
||||
// Case 1.1: Source is Dictionary, Target is IDictionary
|
||||
case Dictionary<string, object> 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<string, object> sourceProperties:
|
||||
PopulateObject(sourceProperties);
|
||||
break;
|
||||
|
||||
// Case 2.1: Source is List, Target is Array
|
||||
case List<object> sourceList when target is Array targetArray:
|
||||
PopulateArray(sourceList, targetArray);
|
||||
break;
|
||||
|
||||
// Case 2.2: Source is List, Target is IList
|
||||
case List<object> sourceList when target is IList targetList:
|
||||
PopulateIList(sourceList, targetList);
|
||||
break;
|
||||
|
||||
// Case 3: Source is a simple type; Attempt conversion
|
||||
default:
|
||||
var sourceStringValue = source.ToStringInvariant();
|
||||
|
||||
// Handle basic types or enumerations if not
|
||||
if (!_targetType.TryParseBasicType(sourceStringValue, out target))
|
||||
GetEnumValue(sourceStringValue, ref target);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void PopulateIList(IEnumerable<object> objects, IList list)
|
||||
{
|
||||
var parameterType = GetAddMethodParameterType(_targetType);
|
||||
if (parameterType == null) return;
|
||||
|
||||
foreach (var item in objects)
|
||||
{
|
||||
try
|
||||
{
|
||||
list.Add(FromJsonResult(
|
||||
item,
|
||||
_jsonSerializerCase,
|
||||
parameterType,
|
||||
_includeNonPublic));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void PopulateArray(IList<object> objects, Array array)
|
||||
{
|
||||
var elementType = _targetType.GetElementType();
|
||||
|
||||
for (var i = 0; i < objects.Count; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
var targetItem = FromJsonResult(
|
||||
objects[i],
|
||||
_jsonSerializerCase,
|
||||
elementType,
|
||||
_includeNonPublic);
|
||||
array.SetValue(targetItem, i);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void GetEnumValue(string sourceStringValue, ref object? target)
|
||||
{
|
||||
var enumType = Nullable.GetUnderlyingType(_targetType);
|
||||
if (enumType == null && _targetType.IsEnum) enumType = _targetType;
|
||||
if (enumType == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
target = Enum.Parse(enumType, sourceStringValue);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
private void PopulateDictionary(IDictionary<string, object> sourceProperties, IDictionary targetDictionary)
|
||||
{
|
||||
// find the add method of the target dictionary
|
||||
var addMethod = _targetType.GetMethods()
|
||||
.FirstOrDefault(
|
||||
m => m.Name == AddMethodName && m.IsPublic && m.GetParameters().Length == 2);
|
||||
|
||||
// skip if we don't have a compatible add method
|
||||
if (addMethod == null) return;
|
||||
var addMethodParameters = addMethod.GetParameters();
|
||||
if (addMethodParameters[0].ParameterType != typeof(string)) return;
|
||||
|
||||
// Retrieve the target entry type
|
||||
var targetEntryType = addMethodParameters[1].ParameterType;
|
||||
|
||||
// Add the items to the target dictionary
|
||||
foreach (var sourceProperty in sourceProperties)
|
||||
{
|
||||
try
|
||||
{
|
||||
var targetEntryValue = FromJsonResult(
|
||||
sourceProperty.Value,
|
||||
_jsonSerializerCase,
|
||||
targetEntryType,
|
||||
_includeNonPublic);
|
||||
targetDictionary.Add(sourceProperty.Key, targetEntryValue);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void PopulateObject(IDictionary<string, object> sourceProperties)
|
||||
{
|
||||
if (_targetType.IsValueType)
|
||||
{
|
||||
PopulateFields(sourceProperties);
|
||||
}
|
||||
|
||||
PopulateProperties(sourceProperties);
|
||||
}
|
||||
|
||||
private void PopulateProperties(IDictionary<string, object> sourceProperties)
|
||||
{
|
||||
var properties = PropertyTypeCache.DefaultCache.Value.RetrieveFilteredProperties(_targetType, false, p => p.CanWrite);
|
||||
|
||||
foreach (var property in properties)
|
||||
{
|
||||
var sourcePropertyValue = GetSourcePropertyValue(sourceProperties, property);
|
||||
if (sourcePropertyValue == null) continue;
|
||||
|
||||
try
|
||||
{
|
||||
var currentPropertyValue = !property.PropertyType.IsArray
|
||||
? property.GetCacheGetMethod(_includeNonPublic)(_target)
|
||||
: null;
|
||||
|
||||
var targetPropertyValue = FromJsonResult(
|
||||
sourcePropertyValue,
|
||||
property.PropertyType,
|
||||
ref currentPropertyValue,
|
||||
_includeNonPublic);
|
||||
|
||||
property.GetCacheSetMethod(_includeNonPublic)(_target, new[] { targetPropertyValue });
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void PopulateFields(IDictionary<string, object> sourceProperties)
|
||||
{
|
||||
foreach (var field in FieldTypeCache.DefaultCache.Value.RetrieveAllFields(_targetType))
|
||||
{
|
||||
var sourcePropertyValue = GetSourcePropertyValue(sourceProperties, field);
|
||||
if (sourcePropertyValue == null) continue;
|
||||
|
||||
var targetPropertyValue = FromJsonResult(
|
||||
sourcePropertyValue,
|
||||
_jsonSerializerCase,
|
||||
field.FieldType,
|
||||
_includeNonPublic);
|
||||
|
||||
try
|
||||
{
|
||||
field.SetValue(_target, targetPropertyValue);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
348
Swan.Lite/Formatters/Json.Deserializer.cs
Normal file
348
Swan.Lite/Formatters/Json.Deserializer.cs
Normal file
@ -0,0 +1,348 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Swan.Formatters
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public partial class Json
|
||||
{
|
||||
/// <summary>
|
||||
/// A simple JSON Deserializer.
|
||||
/// </summary>
|
||||
private class Deserializer
|
||||
{
|
||||
#region State Variables
|
||||
|
||||
private readonly object? _result;
|
||||
private readonly string _json;
|
||||
|
||||
private Dictionary<string, object> _resultObject;
|
||||
private List<object> _resultArray;
|
||||
private ReadState _state = ReadState.WaitingForRootOpen;
|
||||
private string? _currentFieldName;
|
||||
|
||||
private int _index;
|
||||
|
||||
#endregion
|
||||
|
||||
private Deserializer(string json, int startIndex)
|
||||
{
|
||||
_json = json;
|
||||
|
||||
for (_index = startIndex; _index < _json.Length; _index++)
|
||||
{
|
||||
switch (_state)
|
||||
{
|
||||
case ReadState.WaitingForRootOpen:
|
||||
WaitForRootOpen();
|
||||
continue;
|
||||
case ReadState.WaitingForField when char.IsWhiteSpace(_json, _index):
|
||||
continue;
|
||||
case ReadState.WaitingForField when (_resultObject != null && _json[_index] == CloseObjectChar)
|
||||
|| (_resultArray != null && _json[_index] == CloseArrayChar):
|
||||
// Handle empty arrays and empty objects
|
||||
_result = _resultObject ?? _resultArray as object;
|
||||
return;
|
||||
case ReadState.WaitingForField when _json[_index] != StringQuotedChar:
|
||||
throw CreateParserException($"'{StringQuotedChar}'");
|
||||
case ReadState.WaitingForField:
|
||||
{
|
||||
var charCount = GetFieldNameCount();
|
||||
|
||||
_currentFieldName = Unescape(_json.SliceLength(_index + 1, charCount));
|
||||
_index += charCount + 1;
|
||||
_state = ReadState.WaitingForColon;
|
||||
continue;
|
||||
}
|
||||
|
||||
case ReadState.WaitingForColon when char.IsWhiteSpace(_json, _index):
|
||||
continue;
|
||||
case ReadState.WaitingForColon when _json[_index] != ValueSeparatorChar:
|
||||
throw CreateParserException($"'{ValueSeparatorChar}'");
|
||||
case ReadState.WaitingForColon:
|
||||
_state = ReadState.WaitingForValue;
|
||||
continue;
|
||||
case ReadState.WaitingForValue when char.IsWhiteSpace(_json, _index):
|
||||
continue;
|
||||
case ReadState.WaitingForValue when (_resultObject != null && _json[_index] == CloseObjectChar)
|
||||
|| (_resultArray != null && _json[_index] == CloseArrayChar):
|
||||
// Handle empty arrays and empty objects
|
||||
_result = _resultObject ?? _resultArray as object;
|
||||
return;
|
||||
case ReadState.WaitingForValue:
|
||||
ExtractValue();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_state != ReadState.WaitingForNextOrRootClose || char.IsWhiteSpace(_json, _index)) continue;
|
||||
|
||||
if (_json[_index] == FieldSeparatorChar)
|
||||
{
|
||||
if (_resultObject != null)
|
||||
{
|
||||
_state = ReadState.WaitingForField;
|
||||
_currentFieldName = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
_state = ReadState.WaitingForValue;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((_resultObject == null || _json[_index] != CloseObjectChar) &&
|
||||
(_resultArray == null || _json[_index] != CloseArrayChar))
|
||||
{
|
||||
throw CreateParserException($"'{FieldSeparatorChar}' '{CloseObjectChar}' or '{CloseArrayChar}'");
|
||||
}
|
||||
|
||||
_result = _resultObject ?? _resultArray as object;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
internal static object? DeserializeInternal(string json) => new Deserializer(json, 0)._result;
|
||||
|
||||
private void WaitForRootOpen()
|
||||
{
|
||||
if (char.IsWhiteSpace(_json, _index)) return;
|
||||
|
||||
switch (_json[_index])
|
||||
{
|
||||
case OpenObjectChar:
|
||||
_resultObject = new Dictionary<string, object>();
|
||||
_state = ReadState.WaitingForField;
|
||||
return;
|
||||
case OpenArrayChar:
|
||||
_resultArray = new List<object>();
|
||||
_state = ReadState.WaitingForValue;
|
||||
return;
|
||||
default:
|
||||
throw CreateParserException($"'{OpenObjectChar}' or '{OpenArrayChar}'");
|
||||
}
|
||||
}
|
||||
|
||||
private void ExtractValue()
|
||||
{
|
||||
// determine the value based on what it starts with
|
||||
switch (_json[_index])
|
||||
{
|
||||
case StringQuotedChar: // expect a string
|
||||
ExtractStringQuoted();
|
||||
break;
|
||||
|
||||
case OpenObjectChar: // expect object
|
||||
case OpenArrayChar: // expect array
|
||||
ExtractObject();
|
||||
break;
|
||||
|
||||
case 't': // expect true
|
||||
ExtractConstant(TrueLiteral, true);
|
||||
break;
|
||||
|
||||
case 'f': // expect false
|
||||
ExtractConstant(FalseLiteral, false);
|
||||
break;
|
||||
|
||||
case 'n': // expect null
|
||||
ExtractConstant(NullLiteral, null);
|
||||
break;
|
||||
|
||||
default: // expect number
|
||||
ExtractNumber();
|
||||
break;
|
||||
}
|
||||
|
||||
_currentFieldName = null;
|
||||
_state = ReadState.WaitingForNextOrRootClose;
|
||||
}
|
||||
|
||||
private static string Unescape(string str)
|
||||
{
|
||||
// check if we need to unescape at all
|
||||
if (str.IndexOf(StringEscapeChar) < 0)
|
||||
return str;
|
||||
|
||||
var builder = new StringBuilder(str.Length);
|
||||
for (var i = 0; i < str.Length; i++)
|
||||
{
|
||||
if (str[i] != StringEscapeChar)
|
||||
{
|
||||
builder.Append(str[i]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i + 1 > str.Length - 1)
|
||||
break;
|
||||
|
||||
// escape sequence begins here
|
||||
switch (str[i + 1])
|
||||
{
|
||||
case 'u':
|
||||
i = ExtractEscapeSequence(str, i, builder);
|
||||
break;
|
||||
case 'b':
|
||||
builder.Append('\b');
|
||||
i += 1;
|
||||
break;
|
||||
case 't':
|
||||
builder.Append('\t');
|
||||
i += 1;
|
||||
break;
|
||||
case 'n':
|
||||
builder.Append('\n');
|
||||
i += 1;
|
||||
break;
|
||||
case 'f':
|
||||
builder.Append('\f');
|
||||
i += 1;
|
||||
break;
|
||||
case 'r':
|
||||
builder.Append('\r');
|
||||
i += 1;
|
||||
break;
|
||||
default:
|
||||
builder.Append(str[i + 1]);
|
||||
i += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static int ExtractEscapeSequence(string str, int i, StringBuilder builder)
|
||||
{
|
||||
var startIndex = i + 2;
|
||||
var endIndex = i + 5;
|
||||
if (endIndex > str.Length - 1)
|
||||
{
|
||||
builder.Append(str[i + 1]);
|
||||
i += 1;
|
||||
return i;
|
||||
}
|
||||
|
||||
var hexCode = str.Slice(startIndex, endIndex).ConvertHexadecimalToBytes();
|
||||
builder.Append(Encoding.BigEndianUnicode.GetChars(hexCode));
|
||||
i += 5;
|
||||
return i;
|
||||
}
|
||||
|
||||
private int GetFieldNameCount()
|
||||
{
|
||||
var charCount = 0;
|
||||
for (var j = _index + 1; j < _json.Length; j++)
|
||||
{
|
||||
if (_json[j] == StringQuotedChar && _json[j - 1] != StringEscapeChar)
|
||||
break;
|
||||
|
||||
charCount++;
|
||||
}
|
||||
|
||||
return charCount;
|
||||
}
|
||||
|
||||
private void ExtractObject()
|
||||
{
|
||||
// Extract and set the value
|
||||
var deserializer = new Deserializer(_json, _index);
|
||||
|
||||
if (_currentFieldName != null)
|
||||
_resultObject[_currentFieldName] = deserializer._result;
|
||||
else
|
||||
_resultArray.Add(deserializer._result);
|
||||
|
||||
_index = deserializer._index;
|
||||
}
|
||||
|
||||
private void ExtractNumber()
|
||||
{
|
||||
var charCount = 0;
|
||||
for (var j = _index; j < _json.Length; j++)
|
||||
{
|
||||
if (char.IsWhiteSpace(_json[j]) || _json[j] == FieldSeparatorChar
|
||||
|| (_resultObject != null && _json[j] == CloseObjectChar)
|
||||
|| (_resultArray != null && _json[j] == CloseArrayChar))
|
||||
break;
|
||||
|
||||
charCount++;
|
||||
}
|
||||
|
||||
// Extract and set the value
|
||||
var stringValue = _json.SliceLength(_index, charCount);
|
||||
|
||||
if (decimal.TryParse(stringValue, System.Globalization.NumberStyles.Number, System.Globalization.CultureInfo.InvariantCulture, out var value) == false)
|
||||
throw CreateParserException("[number]");
|
||||
|
||||
if (_currentFieldName != null)
|
||||
_resultObject[_currentFieldName] = value;
|
||||
else
|
||||
_resultArray.Add(value);
|
||||
|
||||
_index += charCount - 1;
|
||||
}
|
||||
|
||||
private void ExtractConstant(string boolValue, bool? value)
|
||||
{
|
||||
if (_json.SliceLength(_index, boolValue.Length) != boolValue)
|
||||
throw CreateParserException($"'{ValueSeparatorChar}'");
|
||||
|
||||
// Extract and set the value
|
||||
if (_currentFieldName != null)
|
||||
_resultObject[_currentFieldName] = value;
|
||||
else
|
||||
_resultArray.Add(value);
|
||||
|
||||
_index += boolValue.Length - 1;
|
||||
}
|
||||
|
||||
private void ExtractStringQuoted()
|
||||
{
|
||||
var charCount = 0;
|
||||
var escapeCharFound = false;
|
||||
for (var j = _index + 1; j < _json.Length; j++)
|
||||
{
|
||||
if (_json[j] == StringQuotedChar && !escapeCharFound)
|
||||
break;
|
||||
|
||||
escapeCharFound = _json[j] == StringEscapeChar && !escapeCharFound;
|
||||
charCount++;
|
||||
}
|
||||
|
||||
// Extract and set the value
|
||||
var value = Unescape(_json.SliceLength(_index + 1, charCount));
|
||||
if (_currentFieldName != null)
|
||||
_resultObject[_currentFieldName] = value;
|
||||
else
|
||||
_resultArray.Add(value);
|
||||
|
||||
_index += charCount + 1;
|
||||
}
|
||||
|
||||
private FormatException CreateParserException(string expected)
|
||||
{
|
||||
var textPosition = _json.TextPositionAt(_index);
|
||||
return new FormatException(
|
||||
$"Parser error (Line {textPosition.Item1}, Col {textPosition.Item2}, State {_state}): Expected {expected} but got '{_json[_index]}'.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines the different JSON read states.
|
||||
/// </summary>
|
||||
private enum ReadState
|
||||
{
|
||||
WaitingForRootOpen,
|
||||
WaitingForField,
|
||||
WaitingForColon,
|
||||
WaitingForValue,
|
||||
WaitingForNextOrRootClose,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
348
Swan.Lite/Formatters/Json.Serializer.cs
Normal file
348
Swan.Lite/Formatters/Json.Serializer.cs
Normal file
@ -0,0 +1,348 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace Swan.Formatters
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public partial class Json
|
||||
{
|
||||
/// <summary>
|
||||
/// A simple JSON serializer.
|
||||
/// </summary>
|
||||
private class Serializer
|
||||
{
|
||||
#region Private Declarations
|
||||
|
||||
private static readonly Dictionary<int, string> IndentStrings = new Dictionary<int, string>();
|
||||
|
||||
private readonly SerializerOptions _options;
|
||||
private readonly string _result;
|
||||
private readonly StringBuilder _builder;
|
||||
private readonly string _lastCommaSearch;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Serializer" /> class.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object.</param>
|
||||
/// <param name="depth">The depth.</param>
|
||||
/// <param name="options">The options.</param>
|
||||
private Serializer(object? obj, int depth, SerializerOptions options)
|
||||
{
|
||||
if (depth > 20)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The max depth (20) has been reached. Serializer can not continue.");
|
||||
}
|
||||
|
||||
// Basic Type Handling (nulls, strings, number, date and bool)
|
||||
_result = ResolveBasicType(obj);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_result))
|
||||
return;
|
||||
|
||||
_options = options;
|
||||
|
||||
// Handle circular references correctly and avoid them
|
||||
if (options.IsObjectPresent(obj!))
|
||||
{
|
||||
_result = $"{{ \"$circref\": \"{Escape(obj!.GetHashCode().ToStringInvariant(), false)}\" }}";
|
||||
return;
|
||||
}
|
||||
|
||||
// At this point, we will need to construct the object with a StringBuilder.
|
||||
_lastCommaSearch = FieldSeparatorChar + (_options.Format ? Environment.NewLine : string.Empty);
|
||||
_builder = new StringBuilder();
|
||||
|
||||
_result = obj switch
|
||||
{
|
||||
IDictionary itemsZero when itemsZero.Count == 0 => EmptyObjectLiteral,
|
||||
IDictionary items => ResolveDictionary(items, depth),
|
||||
IEnumerable enumerableZero when !enumerableZero.Cast<object>().Any() => EmptyArrayLiteral,
|
||||
IEnumerable enumerableBytes when enumerableBytes is byte[] bytes => Serialize(bytes.ToBase64(), depth, _options),
|
||||
IEnumerable enumerable => ResolveEnumerable(enumerable, depth),
|
||||
_ => ResolveObject(obj!, depth)
|
||||
};
|
||||
}
|
||||
|
||||
internal static string Serialize(object? obj, int depth, SerializerOptions options) => new Serializer(obj, depth, options)._result;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private static string ResolveBasicType(object? obj)
|
||||
{
|
||||
switch (obj)
|
||||
{
|
||||
case null:
|
||||
return NullLiteral;
|
||||
case string s:
|
||||
return Escape(s, true);
|
||||
case bool b:
|
||||
return b ? TrueLiteral : FalseLiteral;
|
||||
case Type _:
|
||||
case Assembly _:
|
||||
case MethodInfo _:
|
||||
case PropertyInfo _:
|
||||
case EventInfo _:
|
||||
return Escape(obj.ToString(), true);
|
||||
case DateTime d:
|
||||
return $"{StringQuotedChar}{d:s}{StringQuotedChar}";
|
||||
default:
|
||||
var targetType = obj.GetType();
|
||||
|
||||
if (!Definitions.BasicTypesInfo.Value.ContainsKey(targetType))
|
||||
return string.Empty;
|
||||
|
||||
var escapedValue = Escape(Definitions.BasicTypesInfo.Value[targetType].ToStringInvariant(obj), false);
|
||||
|
||||
return decimal.TryParse(escapedValue, out _)
|
||||
? $"{escapedValue}"
|
||||
: $"{StringQuotedChar}{escapedValue}{StringQuotedChar}";
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsNonEmptyJsonArrayOrObject(string serialized)
|
||||
{
|
||||
if (serialized == EmptyObjectLiteral || serialized == EmptyArrayLiteral) return false;
|
||||
|
||||
// find the first position the character is not a space
|
||||
return serialized.Where(c => c != ' ').Select(c => c == OpenObjectChar || c == OpenArrayChar).FirstOrDefault();
|
||||
}
|
||||
|
||||
private static string Escape(string str, bool quoted)
|
||||
{
|
||||
if (str == null)
|
||||
return string.Empty;
|
||||
|
||||
var builder = new StringBuilder(str.Length * 2);
|
||||
if (quoted) builder.Append(StringQuotedChar);
|
||||
Escape(str, builder);
|
||||
if (quoted) builder.Append(StringQuotedChar);
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static void Escape(string str, StringBuilder builder)
|
||||
{
|
||||
foreach (var currentChar in str)
|
||||
{
|
||||
switch (currentChar)
|
||||
{
|
||||
case '\\':
|
||||
case '"':
|
||||
case '/':
|
||||
builder
|
||||
.Append('\\')
|
||||
.Append(currentChar);
|
||||
break;
|
||||
case '\b':
|
||||
builder.Append("\\b");
|
||||
break;
|
||||
case '\t':
|
||||
builder.Append("\\t");
|
||||
break;
|
||||
case '\n':
|
||||
builder.Append("\\n");
|
||||
break;
|
||||
case '\f':
|
||||
builder.Append("\\f");
|
||||
break;
|
||||
case '\r':
|
||||
builder.Append("\\r");
|
||||
break;
|
||||
default:
|
||||
if (currentChar < ' ')
|
||||
{
|
||||
var escapeBytes = BitConverter.GetBytes((ushort)currentChar);
|
||||
if (BitConverter.IsLittleEndian == false)
|
||||
Array.Reverse(escapeBytes);
|
||||
|
||||
builder.Append("\\u")
|
||||
.Append(escapeBytes[1].ToString("X").PadLeft(2, '0'))
|
||||
.Append(escapeBytes[0].ToString("X").PadLeft(2, '0'));
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append(currentChar);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<string, object?> CreateDictionary(
|
||||
Dictionary<string, MemberInfo> fields,
|
||||
string targetType,
|
||||
object target)
|
||||
{
|
||||
// Create the dictionary and extract the properties
|
||||
var objectDictionary = new Dictionary<string, object?>();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_options.TypeSpecifier) == false)
|
||||
objectDictionary[_options.TypeSpecifier] = targetType;
|
||||
|
||||
foreach (var field in fields)
|
||||
{
|
||||
// Build the dictionary using property names and values
|
||||
// Note: used to be: property.GetValue(target); but we would be reading private properties
|
||||
try
|
||||
{
|
||||
objectDictionary[field.Key] = field.Value is PropertyInfo property
|
||||
? property.GetCacheGetMethod(_options.IncludeNonPublic)?.Invoke(target)
|
||||
: (field.Value as FieldInfo)?.GetValue(target);
|
||||
}
|
||||
catch
|
||||
{
|
||||
/* ignored */
|
||||
}
|
||||
}
|
||||
|
||||
return objectDictionary;
|
||||
}
|
||||
|
||||
private string ResolveDictionary(IDictionary items, int depth)
|
||||
{
|
||||
Append(OpenObjectChar, depth);
|
||||
AppendLine();
|
||||
|
||||
// Iterate through the elements and output recursively
|
||||
var writeCount = 0;
|
||||
foreach (var key in items.Keys)
|
||||
{
|
||||
// Serialize and append the key (first char indented)
|
||||
Append(StringQuotedChar, depth + 1);
|
||||
Escape(key.ToString(), _builder);
|
||||
_builder
|
||||
.Append(StringQuotedChar)
|
||||
.Append(ValueSeparatorChar)
|
||||
.Append(" ");
|
||||
|
||||
// Serialize and append the value
|
||||
var serializedValue = Serialize(items[key], depth + 1, _options);
|
||||
|
||||
if (IsNonEmptyJsonArrayOrObject(serializedValue)) AppendLine();
|
||||
Append(serializedValue, 0);
|
||||
|
||||
// Add a comma and start a new line -- We will remove the last one when we are done writing the elements
|
||||
Append(FieldSeparatorChar, 0);
|
||||
AppendLine();
|
||||
writeCount++;
|
||||
}
|
||||
|
||||
// Output the end of the object and set the result
|
||||
RemoveLastComma();
|
||||
Append(CloseObjectChar, writeCount > 0 ? depth : 0);
|
||||
return _builder.ToString();
|
||||
}
|
||||
|
||||
private string ResolveObject(object target, int depth)
|
||||
{
|
||||
var targetType = target.GetType();
|
||||
|
||||
if (targetType.IsEnum)
|
||||
return Convert.ToInt64(target, System.Globalization.CultureInfo.InvariantCulture).ToString();
|
||||
|
||||
var fields = _options.GetProperties(targetType);
|
||||
|
||||
if (fields.Count == 0 && string.IsNullOrWhiteSpace(_options.TypeSpecifier))
|
||||
return EmptyObjectLiteral;
|
||||
|
||||
// If we arrive here, then we convert the object into a
|
||||
// dictionary of property names and values and call the serialization
|
||||
// function again
|
||||
var objectDictionary = CreateDictionary(fields, targetType.ToString(), target);
|
||||
|
||||
return Serialize(objectDictionary, depth, _options);
|
||||
}
|
||||
|
||||
private string ResolveEnumerable(IEnumerable target, int depth)
|
||||
{
|
||||
// Cast the items as a generic object array
|
||||
var items = target.Cast<object>();
|
||||
|
||||
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)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the last comma in the current string builder.
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
144
Swan.Lite/Formatters/Json.SerializerOptions.cs
Normal file
144
Swan.Lite/Formatters/Json.SerializerOptions.cs
Normal file
@ -0,0 +1,144 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Swan.Reflection;
|
||||
|
||||
namespace Swan.Formatters
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class SerializerOptions
|
||||
{
|
||||
private static readonly ConcurrentDictionary<Type, Dictionary<Tuple<string, string>, MemberInfo>>
|
||||
TypeCache = new ConcurrentDictionary<Type, Dictionary<Tuple<string, string>, MemberInfo>>();
|
||||
|
||||
private readonly string[]? _includeProperties;
|
||||
private readonly string[]? _excludeProperties;
|
||||
private readonly Dictionary<int, List<WeakReference>> _parentReferences = new Dictionary<int, List<WeakReference>>();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SerializerOptions"/> class.
|
||||
/// </summary>
|
||||
/// <param name="format">if set to <c>true</c> [format].</param>
|
||||
/// <param name="typeSpecifier">The type specifier.</param>
|
||||
/// <param name="includeProperties">The include properties.</param>
|
||||
/// <param name="excludeProperties">The exclude properties.</param>
|
||||
/// <param name="includeNonPublic">if set to <c>true</c> [include non public].</param>
|
||||
/// <param name="parentReferences">The parent references.</param>
|
||||
/// <param name="jsonSerializerCase">The json serializer case.</param>
|
||||
public SerializerOptions(
|
||||
bool format,
|
||||
string? typeSpecifier,
|
||||
string[]? includeProperties,
|
||||
string[]? excludeProperties = null,
|
||||
bool includeNonPublic = true,
|
||||
IReadOnlyCollection<WeakReference>? parentReferences = null,
|
||||
JsonSerializerCase jsonSerializerCase = JsonSerializerCase.None)
|
||||
{
|
||||
_includeProperties = includeProperties;
|
||||
_excludeProperties = excludeProperties;
|
||||
|
||||
IncludeNonPublic = includeNonPublic;
|
||||
Format = format;
|
||||
TypeSpecifier = typeSpecifier;
|
||||
JsonSerializerCase = jsonSerializerCase;
|
||||
|
||||
if (parentReferences == null)
|
||||
return;
|
||||
|
||||
foreach (var parentReference in parentReferences.Where(x => x.IsAlive))
|
||||
{
|
||||
IsObjectPresent(parentReference.Target);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this <see cref="SerializerOptions"/> is format.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if format; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public bool Format { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type specifier.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The type specifier.
|
||||
/// </value>
|
||||
public string? TypeSpecifier { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether [include non public].
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if [include non public]; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public bool IncludeNonPublic { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the json serializer case.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The json serializer case.
|
||||
/// </value>
|
||||
public JsonSerializerCase JsonSerializerCase { get; }
|
||||
|
||||
internal bool IsObjectPresent(object target)
|
||||
{
|
||||
var hashCode = target.GetHashCode();
|
||||
|
||||
if (_parentReferences.ContainsKey(hashCode))
|
||||
{
|
||||
if (_parentReferences[hashCode].Any(p => ReferenceEquals(p.Target, target)))
|
||||
return true;
|
||||
|
||||
_parentReferences[hashCode].Add(new WeakReference(target));
|
||||
return false;
|
||||
}
|
||||
|
||||
_parentReferences.Add(hashCode, new List<WeakReference> { new WeakReference(target) });
|
||||
return false;
|
||||
}
|
||||
|
||||
internal Dictionary<string, MemberInfo> GetProperties(Type targetType)
|
||||
=> GetPropertiesCache(targetType)
|
||||
.When(() => _includeProperties?.Length > 0,
|
||||
query => query.Where(p => _includeProperties.Contains(p.Key.Item1)))
|
||||
.When(() => _excludeProperties?.Length > 0,
|
||||
query => query.Where(p => !_excludeProperties.Contains(p.Key.Item1)))
|
||||
.ToDictionary(x => x.Key.Item2, x => x.Value);
|
||||
|
||||
private Dictionary<Tuple<string, string>, MemberInfo> GetPropertiesCache(Type targetType)
|
||||
{
|
||||
if (TypeCache.TryGetValue(targetType, out var current))
|
||||
return current;
|
||||
|
||||
var fields =
|
||||
new List<MemberInfo>(PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties(targetType).Where(p => p.CanRead));
|
||||
|
||||
// If the target is a struct (value type) navigate the fields.
|
||||
if (targetType.IsValueType)
|
||||
{
|
||||
fields.AddRange(FieldTypeCache.DefaultCache.Value.RetrieveAllFields(targetType));
|
||||
}
|
||||
|
||||
var value = fields
|
||||
.ToDictionary(
|
||||
x => Tuple.Create(x.Name,
|
||||
x.GetCustomAttribute<JsonPropertyAttribute>()?.PropertyName ?? x.Name.GetNameWithCase(JsonSerializerCase)),
|
||||
x => x);
|
||||
|
||||
TypeCache.TryAdd(targetType, value);
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
379
Swan.Lite/Formatters/Json.cs
Normal file
379
Swan.Lite/Formatters/Json.cs
Normal file
@ -0,0 +1,379 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Swan.Collections;
|
||||
using Swan.Reflection;
|
||||
|
||||
namespace Swan.Formatters
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static partial class Json
|
||||
{
|
||||
#region Constants
|
||||
|
||||
internal const string AddMethodName = "Add";
|
||||
|
||||
private const char OpenObjectChar = '{';
|
||||
private const char CloseObjectChar = '}';
|
||||
|
||||
private const char OpenArrayChar = '[';
|
||||
private const char CloseArrayChar = ']';
|
||||
|
||||
private const char FieldSeparatorChar = ',';
|
||||
private const char ValueSeparatorChar = ':';
|
||||
|
||||
private const char StringEscapeChar = '\\';
|
||||
private const char StringQuotedChar = '"';
|
||||
|
||||
private const string EmptyObjectLiteral = "{ }";
|
||||
private const string EmptyArrayLiteral = "[ ]";
|
||||
private const string TrueLiteral = "true";
|
||||
private const string FalseLiteral = "false";
|
||||
private const string NullLiteral = "null";
|
||||
|
||||
#endregion
|
||||
|
||||
private static readonly CollectionCacheRepository<string> IgnoredPropertiesCache = new CollectionCacheRepository<string>();
|
||||
|
||||
#region Public API
|
||||
|
||||
/// <summary>
|
||||
/// Serializes the specified object into a JSON string.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object.</param>
|
||||
/// <param name="format">if set to <c>true</c> it formats and indents the output.</param>
|
||||
/// <param name="typeSpecifier">The type specifier. Leave null or empty to avoid setting.</param>
|
||||
/// <param name="includeNonPublic">if set to <c>true</c> non-public getters will be also read.</param>
|
||||
/// <param name="includedNames">The included property names.</param>
|
||||
/// <param name="excludedNames">The excluded property names.</param>
|
||||
/// <returns>
|
||||
/// A <see cref="System.String" /> that represents the current object.
|
||||
/// </returns>
|
||||
/// <example>
|
||||
/// The following example describes how to serialize a simple object.
|
||||
/// <code>
|
||||
/// using Swan.Formatters;
|
||||
///
|
||||
/// class Example
|
||||
/// {
|
||||
/// static void Main()
|
||||
/// {
|
||||
/// var obj = new { One = "One", Two = "Two" };
|
||||
///
|
||||
/// var serial = Json.Serialize(obj); // {"One": "One","Two": "Two"}
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
///
|
||||
/// The following example details how to serialize an object using the <see cref="JsonPropertyAttribute"/>.
|
||||
///
|
||||
/// <code>
|
||||
/// using Swan.Attributes;
|
||||
/// using Swan.Formatters;
|
||||
///
|
||||
/// class Example
|
||||
/// {
|
||||
/// class JsonPropertyExample
|
||||
/// {
|
||||
/// [JsonProperty("data")]
|
||||
/// public string Data { get; set; }
|
||||
///
|
||||
/// [JsonProperty("ignoredData", true)]
|
||||
/// public string IgnoredData { get; set; }
|
||||
/// }
|
||||
///
|
||||
/// static void Main()
|
||||
/// {
|
||||
/// var obj = new JsonPropertyExample() { Data = "OK", IgnoredData = "OK" };
|
||||
///
|
||||
/// // {"data": "OK"}
|
||||
/// var serializedObj = Json.Serialize(obj);
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public static string Serialize(
|
||||
object? obj,
|
||||
bool format = false,
|
||||
string? typeSpecifier = null,
|
||||
bool includeNonPublic = false,
|
||||
string[]? includedNames = null,
|
||||
params string[] excludedNames)
|
||||
{
|
||||
return Serialize(obj, format, typeSpecifier, includeNonPublic, includedNames, excludedNames, null, JsonSerializerCase.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializes the specified object into a JSON string.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object.</param>
|
||||
/// <param name="jsonSerializerCase">The json serializer case.</param>
|
||||
/// <param name="format">if set to <c>true</c> [format].</param>
|
||||
/// <param name="typeSpecifier">The type specifier.</param>
|
||||
/// <returns>
|
||||
/// A <see cref="System.String" /> that represents the current object.
|
||||
/// </returns>
|
||||
public static string Serialize(
|
||||
object? obj,
|
||||
JsonSerializerCase jsonSerializerCase,
|
||||
bool format = false,
|
||||
string? typeSpecifier = null) => Serialize(obj, format, typeSpecifier, false, null, null, null, jsonSerializerCase);
|
||||
|
||||
/// <summary>
|
||||
/// Serializes the specified object into a JSON string.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object.</param>
|
||||
/// <param name="format">if set to <c>true</c> it formats and indents the output.</param>
|
||||
/// <param name="typeSpecifier">The type specifier. Leave null or empty to avoid setting.</param>
|
||||
/// <param name="includeNonPublic">if set to <c>true</c> non-public getters will be also read.</param>
|
||||
/// <param name="includedNames">The included property names.</param>
|
||||
/// <param name="excludedNames">The excluded property names.</param>
|
||||
/// <param name="parentReferences">The parent references.</param>
|
||||
/// <param name="jsonSerializerCase">The json serializer case.</param>
|
||||
/// <returns>
|
||||
/// A <see cref="System.String" /> that represents the current object.
|
||||
/// </returns>
|
||||
public static string Serialize(
|
||||
object? obj,
|
||||
bool format,
|
||||
string? typeSpecifier,
|
||||
bool includeNonPublic,
|
||||
string[]? includedNames,
|
||||
string[]? excludedNames,
|
||||
List<WeakReference>? parentReferences,
|
||||
JsonSerializerCase jsonSerializerCase)
|
||||
{
|
||||
if (obj != null && (obj is string || Definitions.AllBasicValueTypes.Contains(obj.GetType())))
|
||||
{
|
||||
return SerializePrimitiveValue(obj);
|
||||
}
|
||||
|
||||
var options = new SerializerOptions(
|
||||
format,
|
||||
typeSpecifier,
|
||||
includedNames,
|
||||
GetExcludedNames(obj?.GetType(), excludedNames),
|
||||
includeNonPublic,
|
||||
parentReferences,
|
||||
jsonSerializerCase);
|
||||
|
||||
return Serialize(obj, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializes the specified object using the SerializerOptions provided.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object.</param>
|
||||
/// <param name="options">The options.</param>
|
||||
/// <returns>
|
||||
/// A <see cref="string" /> that represents the current object.
|
||||
/// </returns>
|
||||
public static string Serialize(object? obj, SerializerOptions options) => Serializer.Serialize(obj, 0, options);
|
||||
|
||||
/// <summary>
|
||||
/// Serializes the specified object only including the specified property names.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object.</param>
|
||||
/// <param name="format">if set to <c>true</c> it formats and indents the output.</param>
|
||||
/// <param name="includeNames">The include names.</param>
|
||||
/// <returns>A <see cref="string" /> that represents the current object.</returns>
|
||||
/// <example>
|
||||
/// The following example shows how to serialize a simple object including the specified properties.
|
||||
/// <code>
|
||||
/// using Swan.Formatters;
|
||||
///
|
||||
/// class Example
|
||||
/// {
|
||||
/// static void Main()
|
||||
/// {
|
||||
/// // object to serialize
|
||||
/// var obj = new { One = "One", Two = "Two", Three = "Three" };
|
||||
///
|
||||
/// // the included names
|
||||
/// var includedNames = new[] { "Two", "Three" };
|
||||
///
|
||||
/// // serialize only the included names
|
||||
/// var data = Json.SerializeOnly(basicObject, true, includedNames);
|
||||
/// // {"Two": "Two","Three": "Three" }
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public static string SerializeOnly(object? obj, bool format, params string[] includeNames)
|
||||
=> Serialize(obj, new SerializerOptions(format, null, includeNames));
|
||||
|
||||
/// <summary>
|
||||
/// Serializes the specified object excluding the specified property names.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object.</param>
|
||||
/// <param name="format">if set to <c>true</c> it formats and indents the output.</param>
|
||||
/// <param name="excludeNames">The exclude names.</param>
|
||||
/// <returns>A <see cref="string" /> that represents the current object.</returns>
|
||||
/// <example>
|
||||
/// The following code shows how to serialize a simple object excluding the specified properties.
|
||||
/// <code>
|
||||
/// using Swan.Formatters;
|
||||
///
|
||||
/// class Example
|
||||
/// {
|
||||
/// static void Main()
|
||||
/// {
|
||||
/// // object to serialize
|
||||
/// var obj = new { One = "One", Two = "Two", Three = "Three" };
|
||||
///
|
||||
/// // the excluded names
|
||||
/// var excludeNames = new[] { "Two", "Three" };
|
||||
///
|
||||
/// // serialize excluding
|
||||
/// var data = Json.SerializeExcluding(basicObject, false, includedNames);
|
||||
/// // {"One": "One"}
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public static string SerializeExcluding(object? obj, bool format, params string[] excludeNames)
|
||||
=> Serialize(obj, new SerializerOptions(format, null, null, excludeNames));
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes the specified json string as either a Dictionary[string, object] or as a List[object]
|
||||
/// depending on the syntax of the JSON string.
|
||||
/// </summary>
|
||||
/// <param name="json">The JSON string.</param>
|
||||
/// <param name="jsonSerializerCase">The json serializer case.</param>
|
||||
/// <returns>
|
||||
/// Type of the current deserializes.
|
||||
/// </returns>
|
||||
/// <example>
|
||||
/// The following code shows how to deserialize a JSON string into a Dictionary.
|
||||
/// <code>
|
||||
/// using Swan.Formatters;
|
||||
/// class Example
|
||||
/// {
|
||||
/// static void Main()
|
||||
/// {
|
||||
/// // json to deserialize
|
||||
/// var basicJson = "{\"One\":\"One\",\"Two\":\"Two\",\"Three\":\"Three\"}";
|
||||
/// // deserializes the specified json into a Dictionary<string, object>.
|
||||
/// var data = Json.Deserialize(basicJson, JsonSerializerCase.None);
|
||||
/// }
|
||||
/// }
|
||||
/// </code></example>
|
||||
public static object? Deserialize(
|
||||
string json,
|
||||
JsonSerializerCase jsonSerializerCase)
|
||||
=> Converter.FromJsonResult(Deserializer.DeserializeInternal(json), jsonSerializerCase);
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes the specified json string as either a Dictionary[string, object] or as a List[object]
|
||||
/// depending on the syntax of the JSON string.
|
||||
/// </summary>
|
||||
/// <param name="json">The JSON string.</param>
|
||||
/// <returns>
|
||||
/// Type of the current deserializes.
|
||||
/// </returns>
|
||||
/// <example>
|
||||
/// The following code shows how to deserialize a JSON string into a Dictionary.
|
||||
/// <code>
|
||||
/// using Swan.Formatters;
|
||||
/// class Example
|
||||
/// {
|
||||
/// static void Main()
|
||||
/// {
|
||||
/// // json to deserialize
|
||||
/// var basicJson = "{\"One\":\"One\",\"Two\":\"Two\",\"Three\":\"Three\"}";
|
||||
/// // deserializes the specified json into a Dictionary<string, object>.
|
||||
/// var data = Json.Deserialize(basicJson);
|
||||
/// }
|
||||
/// }
|
||||
/// </code></example>
|
||||
public static object? Deserialize(string json)
|
||||
=> Deserialize(json, JsonSerializerCase.None);
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes the specified JSON string and converts it to the specified object type.
|
||||
/// Non-public constructors and property setters are ignored.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of object to deserialize.</typeparam>
|
||||
/// <param name="json">The JSON string.</param>
|
||||
/// <param name="jsonSerializerCase">The JSON serializer case.</param>
|
||||
/// <returns>
|
||||
/// The deserialized specified type object.
|
||||
/// </returns>
|
||||
/// <example>
|
||||
/// The following code describes how to deserialize a JSON string into an object of type T.
|
||||
/// <code>
|
||||
/// using Swan.Formatters;
|
||||
/// class Example
|
||||
/// {
|
||||
/// static void Main()
|
||||
/// {
|
||||
/// // json type BasicJson to serialize
|
||||
/// var basicJson = "{\"One\":\"One\",\"Two\":\"Two\",\"Three\":\"Three\"}";
|
||||
/// // deserializes the specified string in a new instance of the type BasicJson.
|
||||
/// var data = Json.Deserialize<BasicJson>(basicJson);
|
||||
/// }
|
||||
/// }
|
||||
/// </code></example>
|
||||
public static T Deserialize<T>(string json, JsonSerializerCase jsonSerializerCase = JsonSerializerCase.None)
|
||||
=> (T)Deserialize(json, typeof(T), jsonSerializerCase: jsonSerializerCase);
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes the specified JSON string and converts it to the specified object type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of object to deserialize.</typeparam>
|
||||
/// <param name="json">The JSON string.</param>
|
||||
/// <param name="includeNonPublic">if set to true, it also uses the non-public constructors and property setters.</param>
|
||||
/// <returns>The deserialized specified type object.</returns>
|
||||
public static T Deserialize<T>(string json, bool includeNonPublic) => (T)Deserialize(json, typeof(T), includeNonPublic);
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes the specified JSON string and converts it to the specified object type.
|
||||
/// </summary>
|
||||
/// <param name="json">The JSON string.</param>
|
||||
/// <param name="resultType">Type of the result.</param>
|
||||
/// <param name="includeNonPublic">if set to true, it also uses the non-public constructors and property setters.</param>
|
||||
/// <param name="jsonSerializerCase">The json serializer case.</param>
|
||||
/// <returns>
|
||||
/// Type of the current conversion from json result.
|
||||
/// </returns>
|
||||
public static object? Deserialize(string json, Type resultType, bool includeNonPublic = false, JsonSerializerCase jsonSerializerCase = JsonSerializerCase.None)
|
||||
=> Converter.FromJsonResult(Deserializer.DeserializeInternal(json), jsonSerializerCase, resultType, includeNonPublic);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private API
|
||||
|
||||
private static string[]? GetExcludedNames(Type? type, string[]? excludedNames)
|
||||
{
|
||||
if (type == null)
|
||||
return excludedNames;
|
||||
|
||||
var excludedByAttr = IgnoredPropertiesCache.Retrieve(type, t => t.GetProperties()
|
||||
.Where(x => AttributeCache.DefaultCache.Value.RetrieveOne<JsonPropertyAttribute>(x)?.Ignored == true)
|
||||
.Select(x => x.Name));
|
||||
|
||||
if (excludedByAttr?.Any() != true)
|
||||
return excludedNames;
|
||||
|
||||
return excludedNames?.Any(string.IsNullOrWhiteSpace) == true
|
||||
? excludedByAttr.Intersect(excludedNames.Where(y => !string.IsNullOrWhiteSpace(y))).ToArray()
|
||||
: excludedByAttr.ToArray();
|
||||
}
|
||||
|
||||
private static string SerializePrimitiveValue(object obj) =>
|
||||
obj switch
|
||||
{
|
||||
string stringValue => stringValue,
|
||||
bool boolValue => boolValue ? TrueLiteral : FalseLiteral,
|
||||
_ => obj.ToString()
|
||||
};
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
39
Swan.Lite/Formatters/JsonPropertyAttribute.cs
Normal file
39
Swan.Lite/Formatters/JsonPropertyAttribute.cs
Normal file
@ -0,0 +1,39 @@
|
||||
using System;
|
||||
|
||||
namespace Swan.Formatters
|
||||
{
|
||||
/// <summary>
|
||||
/// An attribute used to help setup a property behavior when serialize/deserialize JSON.
|
||||
/// </summary>
|
||||
/// <seealso cref="Attribute" />
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public sealed class JsonPropertyAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JsonPropertyAttribute" /> class.
|
||||
/// </summary>
|
||||
/// <param name="propertyName">Name of the property.</param>
|
||||
/// <param name="ignored">if set to <c>true</c> [ignored].</param>
|
||||
public JsonPropertyAttribute(string propertyName, bool ignored = false)
|
||||
{
|
||||
PropertyName = propertyName ?? throw new ArgumentNullException(nameof(propertyName));
|
||||
Ignored = ignored;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the property.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The name of the property.
|
||||
/// </value>
|
||||
public string PropertyName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this <see cref="JsonPropertyAttribute" /> is ignored.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if ignored; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public bool Ignored { get; }
|
||||
}
|
||||
}
|
344
Swan.Lite/FromString.cs
Normal file
344
Swan.Lite/FromString.cs
Normal file
@ -0,0 +1,344 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.ComponentModel;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Swan
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a standard way to convert strings to different types.
|
||||
/// </summary>
|
||||
public static class FromString
|
||||
{
|
||||
// It doesn't matter which converter we get here: ConvertFromInvariantString is not virtual.
|
||||
private static readonly MethodInfo ConvertFromInvariantStringMethod
|
||||
= new Func<string, object>(TypeDescriptor.GetConverter(typeof(int)).ConvertFromInvariantString).Method;
|
||||
|
||||
private static readonly MethodInfo TryConvertToInternalMethod
|
||||
= typeof(FromString).GetMethod(nameof(TryConvertToInternal), BindingFlags.Static | BindingFlags.NonPublic);
|
||||
|
||||
private static readonly MethodInfo ConvertToInternalMethod
|
||||
= typeof(FromString).GetMethod(nameof(ConvertToInternal), BindingFlags.Static | BindingFlags.NonPublic);
|
||||
|
||||
private static readonly ConcurrentDictionary<Type, Func<string[], (bool Success, object Result)>> GenericTryConvertToMethods
|
||||
= new ConcurrentDictionary<Type, Func<string[], (bool Success, object Result)>>();
|
||||
|
||||
private static readonly ConcurrentDictionary<Type, Func<string[], object>> GenericConvertToMethods
|
||||
= new ConcurrentDictionary<Type, Func<string[], object>>();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a string can be converted to the specified type.
|
||||
/// </summary>
|
||||
/// <param name="type">The type resulting from the conversion.</param>
|
||||
/// <returns><see langword="true" /> if the conversion is possible;
|
||||
/// otherwise, <see langword="false" />.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="type" /> is <see langword="null" />.</exception>
|
||||
public static bool CanConvertTo(Type type)
|
||||
=> TypeDescriptor.GetConverter(type).CanConvertFrom(typeof(string));
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a string can be converted to the specified type.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The type resulting from the conversion.</typeparam>
|
||||
/// <returns><see langword="true" /> if the conversion is possible;
|
||||
/// otherwise, <see langword="false" />.</returns>
|
||||
public static bool CanConvertTo<TResult>()
|
||||
=> TypeDescriptor.GetConverter(typeof(TResult)).CanConvertFrom(typeof(string));
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to convert a string to the specified type.
|
||||
/// </summary>
|
||||
/// <param name="type">The type resulting from the conversion.</param>
|
||||
/// <param name="str">The string to convert.</param>
|
||||
/// <param name="result">When this method returns <see langword="true" />,
|
||||
/// the result of the conversion. This parameter is passed uninitialized.</param>
|
||||
/// <returns><see langword="true" /> if the conversion is successful;
|
||||
/// otherwise, <see langword="false" />.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="type" /> is <see langword="null" />.</exception>
|
||||
public static bool TryConvertTo(Type type, string str, out object? result)
|
||||
{
|
||||
var converter = TypeDescriptor.GetConverter(type);
|
||||
if (!converter.CanConvertFrom(typeof(string)))
|
||||
{
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
result = converter.ConvertFromInvariantString(str);
|
||||
return true;
|
||||
}
|
||||
catch (Exception e) when (!e.IsCriticalException())
|
||||
{
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to convert a string to the specified type.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The type resulting from the conversion.</typeparam>
|
||||
/// <param name="str">The string to convert.</param>
|
||||
/// <param name="result">When this method returns <see langword="true" />,
|
||||
/// the result of the conversion. This parameter is passed uninitialized.</param>
|
||||
/// <returns><see langword="true" /> if the conversion is successful;
|
||||
/// otherwise, <see langword="false" />.</returns>
|
||||
public static bool TryConvertTo<TResult>(string str, out TResult result)
|
||||
{
|
||||
var converter = TypeDescriptor.GetConverter(typeof(TResult));
|
||||
if (!converter.CanConvertFrom(typeof(string)))
|
||||
{
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
result = (TResult)converter.ConvertFromInvariantString(str);
|
||||
return true;
|
||||
}
|
||||
catch (Exception e) when (!e.IsCriticalException())
|
||||
{
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a string to the specified type.
|
||||
/// </summary>
|
||||
/// <param name="type">The type resulting from the conversion.</param>
|
||||
/// <param name="str">The string to convert.</param>
|
||||
/// <returns>An instance of <paramref name="type" />.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="type" /> is <see langword="null" />.</exception>
|
||||
/// <exception cref="StringConversionException">The conversion was not successful.</exception>
|
||||
public static object ConvertTo(Type type, string str)
|
||||
{
|
||||
if (type == null)
|
||||
throw new ArgumentNullException(nameof(type));
|
||||
|
||||
try
|
||||
{
|
||||
return TypeDescriptor.GetConverter(type).ConvertFromInvariantString(str);
|
||||
}
|
||||
catch (Exception e) when (!e.IsCriticalException())
|
||||
{
|
||||
throw new StringConversionException(type, e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a string to the specified type.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The type resulting from the conversion.</typeparam>
|
||||
/// <param name="str">The string to convert.</param>
|
||||
/// <returns>An instance of <typeparamref name="TResult" />.</returns>
|
||||
/// <exception cref="StringConversionException">
|
||||
/// The conversion was not successful.
|
||||
/// </exception>
|
||||
public static TResult ConvertTo<TResult>(string str)
|
||||
{
|
||||
try
|
||||
{
|
||||
return (TResult)TypeDescriptor.GetConverter(typeof(TResult)).ConvertFromInvariantString(str);
|
||||
}
|
||||
catch (Exception e) when (!e.IsCriticalException())
|
||||
{
|
||||
throw new StringConversionException(typeof(TResult), e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to convert an array of strings to an array of the specified type.
|
||||
/// </summary>
|
||||
/// <param name="type">The type resulting from the conversion of each
|
||||
/// element of <paramref name="strings"/>.</param>
|
||||
/// <param name="strings">The array to convert.</param>
|
||||
/// <param name="result">When this method returns <see langword="true" />,
|
||||
/// the result of the conversion. This parameter is passed uninitialized.</param>
|
||||
/// <returns><see langword="true" /> if the conversion is successful;
|
||||
/// otherwise, <see langword="false" />.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="type" /> is <see langword="null" />.</exception>
|
||||
public static bool TryConvertTo(Type type, string[] strings, out object? result)
|
||||
{
|
||||
if (strings == null)
|
||||
{
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
var method = GenericTryConvertToMethods.GetOrAdd(type, BuildNonGenericTryConvertLambda);
|
||||
var (success, methodResult) = method(strings);
|
||||
result = methodResult;
|
||||
return success;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to convert an array of strings to an array of the specified type.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The type resulting from the conversion of each
|
||||
/// element of <paramref name="strings"/>.</typeparam>
|
||||
/// <param name="strings">The array to convert.</param>
|
||||
/// <param name="result">When this method returns <see langword="true" />,
|
||||
/// the result of the conversion. This parameter is passed uninitialized.</param>
|
||||
/// <returns><see langword="true" /> if the conversion is successful;
|
||||
/// otherwise, <see langword="false" />.</returns>
|
||||
public static bool TryConvertTo<TResult>(string[] strings, out TResult[]? result)
|
||||
{
|
||||
if (strings == null)
|
||||
{
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
var converter = TypeDescriptor.GetConverter(typeof(TResult));
|
||||
if (!converter.CanConvertFrom(typeof(string)))
|
||||
{
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
result = new TResult[strings.Length];
|
||||
var i = 0;
|
||||
foreach (var str in strings)
|
||||
result[i++] = (TResult)converter.ConvertFromInvariantString(str);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception e) when (!e.IsCriticalException())
|
||||
{
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an array of strings to an array of the specified type.
|
||||
/// </summary>
|
||||
/// <param name="type">The type resulting from the conversion of each
|
||||
/// element of <paramref name="strings"/>.</param>
|
||||
/// <param name="strings">The array to convert.</param>
|
||||
/// <returns>An array of <paramref name="type" />.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="type" /> is <see langword="null" />.</exception>
|
||||
/// <exception cref="StringConversionException">The conversion of at least one
|
||||
/// of the elements of <paramref name="strings"/>was not successful.</exception>
|
||||
public static object? ConvertTo(Type type, string[] strings)
|
||||
{
|
||||
if (strings == null)
|
||||
return null;
|
||||
|
||||
var method = GenericConvertToMethods.GetOrAdd(type, BuildNonGenericConvertLambda);
|
||||
return method(strings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an array of strings to an array of the specified type.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The type resulting from the conversion of each
|
||||
/// element of <paramref name="strings"/>.</typeparam>
|
||||
/// <param name="strings">The array to convert.</param>
|
||||
/// <returns>An array of <typeparamref name="TResult" />.</returns>
|
||||
/// <exception cref="StringConversionException">The conversion of at least one
|
||||
/// of the elements of <paramref name="strings"/>was not successful.</exception>
|
||||
public static TResult[]? ConvertTo<TResult>(string[] strings)
|
||||
{
|
||||
if (strings == null)
|
||||
return null;
|
||||
|
||||
var converter = TypeDescriptor.GetConverter(typeof(TResult));
|
||||
var result = new TResult[strings.Length];
|
||||
var i = 0;
|
||||
try
|
||||
{
|
||||
foreach (var str in strings)
|
||||
result[i++] = (TResult)converter.ConvertFromInvariantString(str);
|
||||
}
|
||||
catch (Exception e) when (!e.IsCriticalException())
|
||||
{
|
||||
throw new StringConversionException(typeof(TResult), e);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a expression, if the type can be converted to string, to a new expression including
|
||||
/// the conversion to string.
|
||||
/// </summary>
|
||||
/// <param name="type">The type.</param>
|
||||
/// <param name="str">The string.</param>
|
||||
/// <returns>A new expression where the previous expression is converted to string.</returns>
|
||||
public static Expression? ConvertExpressionTo(Type type, Expression str)
|
||||
{
|
||||
var converter = TypeDescriptor.GetConverter(type);
|
||||
|
||||
return converter.CanConvertFrom(typeof(string))
|
||||
? Expression.Convert(
|
||||
Expression.Call(Expression.Constant(converter), ConvertFromInvariantStringMethod, str),
|
||||
type)
|
||||
: null;
|
||||
}
|
||||
|
||||
private static Func<string[], (bool Success, object Result)> BuildNonGenericTryConvertLambda(Type type)
|
||||
{
|
||||
var methodInfo = TryConvertToInternalMethod.MakeGenericMethod(type);
|
||||
var parameter = Expression.Parameter(typeof(string[]));
|
||||
var body = Expression.Call(methodInfo, parameter);
|
||||
var lambda = Expression.Lambda<Func<string[], (bool Success, object Result)>>(body, parameter);
|
||||
return lambda.Compile();
|
||||
}
|
||||
|
||||
private static (bool Success, object? Result) TryConvertToInternal<TResult>(string[] strings)
|
||||
{
|
||||
var converter = TypeDescriptor.GetConverter(typeof(TResult));
|
||||
if (!converter.CanConvertFrom(typeof(string)))
|
||||
return (false, null);
|
||||
|
||||
var result = new TResult[strings.Length];
|
||||
var i = 0;
|
||||
try
|
||||
{
|
||||
foreach (var str in strings)
|
||||
result[i++] = (TResult)converter.ConvertFromInvariantString(str);
|
||||
|
||||
return (true, result);
|
||||
}
|
||||
catch (Exception e) when (!e.IsCriticalException())
|
||||
{
|
||||
return (false, null);
|
||||
}
|
||||
}
|
||||
|
||||
private static Func<string[], object> BuildNonGenericConvertLambda(Type type)
|
||||
{
|
||||
var methodInfo = ConvertToInternalMethod.MakeGenericMethod(type);
|
||||
var parameter = Expression.Parameter(typeof(string[]));
|
||||
var body = Expression.Call(methodInfo, parameter);
|
||||
var lambda = Expression.Lambda<Func<string[], object>>(body, parameter);
|
||||
return lambda.Compile();
|
||||
}
|
||||
|
||||
private static object ConvertToInternal<TResult>(string[] strings)
|
||||
{
|
||||
var converter = TypeDescriptor.GetConverter(typeof(TResult));
|
||||
var result = new TResult[strings.Length];
|
||||
var i = 0;
|
||||
try
|
||||
{
|
||||
foreach (var str in strings)
|
||||
result[i++] = (TResult)converter.ConvertFromInvariantString(str);
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception e) when (!e.IsCriticalException())
|
||||
{
|
||||
throw new StringConversionException(typeof(TResult), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
146
Swan.Lite/Logging/ConsoleLogger.cs
Normal file
146
Swan.Lite/Logging/ConsoleLogger.cs
Normal file
@ -0,0 +1,146 @@
|
||||
using System;
|
||||
using Swan.Lite.Logging;
|
||||
|
||||
namespace Swan.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a Console implementation of <c>ILogger</c>.
|
||||
/// </summary>
|
||||
/// <seealso cref="ILogger" />
|
||||
public class ConsoleLogger : TextLogger, ILogger
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConsoleLogger"/> class.
|
||||
/// </summary>
|
||||
protected ConsoleLogger()
|
||||
{
|
||||
// Empty
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current instance of ConsoleLogger.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The instance.
|
||||
/// </value>
|
||||
public static ConsoleLogger Instance { get; } = new ConsoleLogger();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the debug logging prefix.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The debug prefix.
|
||||
/// </value>
|
||||
public static string DebugPrefix { get; set; } = "DBG";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the trace logging prefix.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The trace prefix.
|
||||
/// </value>
|
||||
public static string TracePrefix { get; set; } = "TRC";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the warning logging prefix.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The warn prefix.
|
||||
/// </value>
|
||||
public static string WarnPrefix { get; set; } = "WRN";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the fatal logging prefix.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The fatal prefix.
|
||||
/// </value>
|
||||
public static string FatalPrefix { get; set; } = "FAT";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the error logging prefix.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The error prefix.
|
||||
/// </value>
|
||||
public static string ErrorPrefix { get; set; } = "ERR";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the information logging prefix.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The information prefix.
|
||||
/// </value>
|
||||
public static string InfoPrefix { get; set; } = "INF";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the color of the information output logging.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The color of the information.
|
||||
/// </value>
|
||||
public static ConsoleColor InfoColor { get; set; } = ConsoleColor.Cyan;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the color of the debug output logging.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The color of the debug.
|
||||
/// </value>
|
||||
public static ConsoleColor DebugColor { get; set; } = ConsoleColor.Gray;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the color of the trace output logging.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The color of the trace.
|
||||
/// </value>
|
||||
public static ConsoleColor TraceColor { get; set; } = ConsoleColor.DarkGray;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the color of the warning logging.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The color of the warn.
|
||||
/// </value>
|
||||
public static ConsoleColor WarnColor { get; set; } = ConsoleColor.Yellow;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the color of the error logging.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The color of the error.
|
||||
/// </value>
|
||||
public static ConsoleColor ErrorColor { get; set; } = ConsoleColor.DarkRed;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the color of the error logging.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The color of the error.
|
||||
/// </value>
|
||||
public static ConsoleColor FatalColor { get; set; } = ConsoleColor.Red;
|
||||
|
||||
/// <inheritdoc />
|
||||
public LogLevel LogLevel { get; set; } = DebugLogger.IsDebuggerAttached ? LogLevel.Trace : LogLevel.Info;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Log(LogMessageReceivedEventArgs logEvent)
|
||||
{
|
||||
// Select the writer based on the message type
|
||||
var writer = logEvent.MessageType == LogLevel.Error
|
||||
? TerminalWriters.StandardError
|
||||
: TerminalWriters.StandardOutput;
|
||||
|
||||
var (outputMessage, color) = GetOutputAndColor(logEvent);
|
||||
|
||||
Terminal.Write(outputMessage, color, writer);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
}
|
53
Swan.Lite/Logging/DebugLogger.cs
Normal file
53
Swan.Lite/Logging/DebugLogger.cs
Normal file
@ -0,0 +1,53 @@
|
||||
using Swan.Lite.Logging;
|
||||
|
||||
namespace Swan.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a logger target. This target will write to the
|
||||
/// Debug console using System.Diagnostics.Debug.
|
||||
/// </summary>
|
||||
/// <seealso cref="ILogger" />
|
||||
public class DebugLogger : TextLogger, ILogger
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DebugLogger"/> class.
|
||||
/// </summary>
|
||||
protected DebugLogger()
|
||||
{
|
||||
// Empty
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current instance of DebugLogger.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The instance.
|
||||
/// </value>
|
||||
public static DebugLogger Instance { get; } = new DebugLogger();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether a debugger is attached.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if this instance is debugger attached; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public static bool IsDebuggerAttached => System.Diagnostics.Debugger.IsAttached;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public LogLevel LogLevel { get; set; } = IsDebuggerAttached ? LogLevel.Trace : LogLevel.None;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Log(LogMessageReceivedEventArgs logEvent)
|
||||
{
|
||||
var (outputMessage, _) = GetOutputAndColor(logEvent);
|
||||
|
||||
System.Diagnostics.Debug.Write(outputMessage);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
134
Swan.Lite/Logging/FileLogger.cs
Normal file
134
Swan.Lite/Logging/FileLogger.cs
Normal file
@ -0,0 +1,134 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Swan.Lite.Logging;
|
||||
using Swan.Threading;
|
||||
|
||||
namespace Swan.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// A helper class to write into files the messages sent by the <see cref="Terminal" />.
|
||||
/// </summary>
|
||||
/// <seealso cref="ILogger" />
|
||||
public class FileLogger : TextLogger, ILogger
|
||||
{
|
||||
private readonly ManualResetEventSlim _doneEvent = new ManualResetEventSlim(true);
|
||||
private readonly ConcurrentQueue<string> _logQueue = new ConcurrentQueue<string>();
|
||||
private readonly ExclusiveTimer _timer;
|
||||
private readonly string _filePath;
|
||||
|
||||
private bool _disposedValue; // To detect redundant calls
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FileLogger"/> class.
|
||||
/// </summary>
|
||||
public FileLogger()
|
||||
: this(SwanRuntime.EntryAssemblyDirectory, true)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FileLogger"/> class.
|
||||
/// </summary>
|
||||
/// <param name="filePath">The filePath.</param>
|
||||
/// <param name="dailyFile">if set to <c>true</c> [daily file].</param>
|
||||
public FileLogger(string filePath, bool dailyFile)
|
||||
{
|
||||
_filePath = filePath;
|
||||
DailyFile = dailyFile;
|
||||
|
||||
_timer = new ExclusiveTimer(
|
||||
async () => await WriteLogEntries().ConfigureAwait(false),
|
||||
TimeSpan.Zero,
|
||||
TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public LogLevel LogLevel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file path.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The file path.
|
||||
/// </value>
|
||||
public string FilePath => DailyFile
|
||||
? Path.Combine(Path.GetDirectoryName(_filePath), Path.GetFileNameWithoutExtension(_filePath) + $"_{DateTime.UtcNow:yyyyMMdd}" + Path.GetExtension(_filePath))
|
||||
: _filePath;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether [daily file].
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if [daily file]; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public bool DailyFile { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Log(LogMessageReceivedEventArgs logEvent)
|
||||
{
|
||||
var (outputMessage, _) = GetOutputAndColor(logEvent);
|
||||
|
||||
_logQueue.Enqueue(outputMessage);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposedValue) return;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
_timer.Pause();
|
||||
_timer.Dispose();
|
||||
|
||||
_doneEvent.Wait();
|
||||
_doneEvent.Reset();
|
||||
WriteLogEntries(true).Await();
|
||||
_doneEvent.Dispose();
|
||||
}
|
||||
|
||||
_disposedValue = true;
|
||||
}
|
||||
|
||||
private async Task WriteLogEntries(bool finalCall = false)
|
||||
{
|
||||
if (_logQueue.IsEmpty)
|
||||
return;
|
||||
|
||||
if (!finalCall && !_doneEvent.IsSet)
|
||||
return;
|
||||
|
||||
_doneEvent.Reset();
|
||||
|
||||
try
|
||||
{
|
||||
using (var file = File.AppendText(FilePath))
|
||||
{
|
||||
while (!_logQueue.IsEmpty)
|
||||
{
|
||||
if (_logQueue.TryDequeue(out var entry))
|
||||
await file.WriteAsync(entry).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!finalCall)
|
||||
_doneEvent.Set();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
24
Swan.Lite/Logging/ILogger.cs
Normal file
24
Swan.Lite/Logging/ILogger.cs
Normal file
@ -0,0 +1,24 @@
|
||||
namespace Swan.Logging
|
||||
{
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for a logger implementation.
|
||||
/// </summary>
|
||||
public interface ILogger : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the log level.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The log level.
|
||||
/// </value>
|
||||
LogLevel LogLevel { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Logs the specified log event.
|
||||
/// </summary>
|
||||
/// <param name="logEvent">The <see cref="LogMessageReceivedEventArgs"/> instance containing the event data.</param>
|
||||
void Log(LogMessageReceivedEventArgs logEvent);
|
||||
}
|
||||
}
|
43
Swan.Lite/Logging/LogLevel.cs
Normal file
43
Swan.Lite/Logging/LogLevel.cs
Normal file
@ -0,0 +1,43 @@
|
||||
namespace Swan
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the log levels.
|
||||
/// </summary>
|
||||
public enum LogLevel
|
||||
{
|
||||
/// <summary>
|
||||
/// The none message type
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// The trace message type
|
||||
/// </summary>
|
||||
Trace,
|
||||
|
||||
/// <summary>
|
||||
/// The debug message type
|
||||
/// </summary>
|
||||
Debug,
|
||||
|
||||
/// <summary>
|
||||
/// The information message type
|
||||
/// </summary>
|
||||
Info,
|
||||
|
||||
/// <summary>
|
||||
/// The warning message type
|
||||
/// </summary>
|
||||
Warning,
|
||||
|
||||
/// <summary>
|
||||
/// The error message type
|
||||
/// </summary>
|
||||
Error,
|
||||
|
||||
/// <summary>
|
||||
/// The fatal message type
|
||||
/// </summary>
|
||||
Fatal,
|
||||
}
|
||||
}
|
131
Swan.Lite/Logging/LogMessageReceivedEventArgs.cs
Normal file
131
Swan.Lite/Logging/LogMessageReceivedEventArgs.cs
Normal file
@ -0,0 +1,131 @@
|
||||
using System;
|
||||
|
||||
namespace Swan
|
||||
{
|
||||
/// <summary>
|
||||
/// Event arguments representing the message that is logged
|
||||
/// on to the terminal. Use the properties to forward the data to
|
||||
/// your logger of choice.
|
||||
/// </summary>
|
||||
/// <seealso cref="System.EventArgs" />
|
||||
public class LogMessageReceivedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LogMessageReceivedEventArgs" /> class.
|
||||
/// </summary>
|
||||
/// <param name="sequence">The sequence.</param>
|
||||
/// <param name="messageType">Type of the message.</param>
|
||||
/// <param name="utcDate">The UTC date.</param>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="message">The message.</param>
|
||||
/// <param name="extendedData">The extended data.</param>
|
||||
/// <param name="callerMemberName">Name of the caller member.</param>
|
||||
/// <param name="callerFilePath">The caller file path.</param>
|
||||
/// <param name="callerLineNumber">The caller line number.</param>
|
||||
public LogMessageReceivedEventArgs(
|
||||
ulong sequence,
|
||||
LogLevel messageType,
|
||||
DateTime utcDate,
|
||||
string source,
|
||||
string message,
|
||||
object? extendedData,
|
||||
string callerMemberName,
|
||||
string callerFilePath,
|
||||
int callerLineNumber)
|
||||
{
|
||||
Sequence = sequence;
|
||||
MessageType = messageType;
|
||||
UtcDate = utcDate;
|
||||
Source = source;
|
||||
Message = message;
|
||||
CallerMemberName = callerMemberName;
|
||||
CallerFilePath = callerFilePath;
|
||||
CallerLineNumber = callerLineNumber;
|
||||
ExtendedData = extendedData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets logging message sequence.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The sequence.
|
||||
/// </value>
|
||||
public ulong Sequence { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the message.
|
||||
/// It can be a combination as the enumeration is a set of bitwise flags.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The type of the message.
|
||||
/// </value>
|
||||
public LogLevel MessageType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the UTC date at which the event at which the message was logged.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The UTC date.
|
||||
/// </value>
|
||||
public DateTime UtcDate { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the source where the logging message
|
||||
/// came from. This can come empty if the logger did not set it.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The source.
|
||||
/// </value>
|
||||
public string Source { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the body of the message.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The message.
|
||||
/// </value>
|
||||
public string Message { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the caller member.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The name of the caller member.
|
||||
/// </value>
|
||||
public string CallerMemberName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the caller file path.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The caller file path.
|
||||
/// </value>
|
||||
public string CallerFilePath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the caller line number.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The caller line number.
|
||||
/// </value>
|
||||
public int CallerLineNumber { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets an object representing extended data.
|
||||
/// It could be an exception or anything else.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The extended data.
|
||||
/// </value>
|
||||
public object? ExtendedData { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Extended Data properties cast as an Exception (if possible)
|
||||
/// Otherwise, it return null.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The exception.
|
||||
/// </value>
|
||||
public Exception? Exception => ExtendedData as Exception;
|
||||
}
|
||||
}
|
654
Swan.Lite/Logging/Logger.cs
Normal file
654
Swan.Lite/Logging/Logger.cs
Normal file
@ -0,0 +1,654 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Swan.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// Entry-point for logging. Use this static class to register/unregister
|
||||
/// loggers instances. By default, the <c>ConsoleLogger</c> is registered.
|
||||
/// </summary>
|
||||
public static class Logger
|
||||
{
|
||||
private static readonly object SyncLock = new object();
|
||||
private static readonly List<ILogger> Loggers = new List<ILogger>();
|
||||
|
||||
private static ulong _loggingSequence;
|
||||
|
||||
static Logger()
|
||||
{
|
||||
if (Terminal.IsConsolePresent)
|
||||
Loggers.Add(ConsoleLogger.Instance);
|
||||
|
||||
if (DebugLogger.IsDebuggerAttached)
|
||||
Loggers.Add(DebugLogger.Instance);
|
||||
}
|
||||
|
||||
#region Standard Public API
|
||||
|
||||
/// <summary>
|
||||
/// Registers the logger.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of logger to register.</typeparam>
|
||||
/// <exception cref="InvalidOperationException">There is already a logger with that class registered.</exception>
|
||||
public static void RegisterLogger<T>()
|
||||
where T : ILogger
|
||||
{
|
||||
lock (SyncLock)
|
||||
{
|
||||
var loggerInstance = Loggers.FirstOrDefault(x => x.GetType() == typeof(T));
|
||||
|
||||
if (loggerInstance != null)
|
||||
throw new InvalidOperationException("There is already a logger with that class registered.");
|
||||
|
||||
Loggers.Add(Activator.CreateInstance<T>());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers the logger.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public static void RegisterLogger(ILogger logger)
|
||||
{
|
||||
lock (SyncLock)
|
||||
Loggers.Add(logger);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters the logger.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException">logger.</exception>
|
||||
public static void UnregisterLogger(ILogger logger) => RemoveLogger(x => x == logger);
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters the logger.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of logger to unregister.</typeparam>
|
||||
public static void UnregisterLogger<T>() => RemoveLogger(x => x.GetType() == typeof(T));
|
||||
|
||||
/// <summary>
|
||||
/// Remove all the loggers.
|
||||
/// </summary>
|
||||
public static void NoLogging()
|
||||
{
|
||||
lock (SyncLock)
|
||||
Loggers.Clear();
|
||||
}
|
||||
|
||||
#region Debug
|
||||
|
||||
/// <summary>
|
||||
/// Logs a debug message to the console.
|
||||
/// </summary>
|
||||
/// <param name="message">The message.</param>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="extendedData">The extended data.</param>
|
||||
/// <param name="callerMemberName">Name of the caller member. This is automatically populated.</param>
|
||||
/// <param name="callerFilePath">The caller file path. This is automatically populated.</param>
|
||||
/// <param name="callerLineNumber">The caller line number. This is automatically populated.</param>
|
||||
public static void Debug(
|
||||
this string message,
|
||||
string source = null,
|
||||
object extendedData = null,
|
||||
[CallerMemberName] string callerMemberName = "",
|
||||
[CallerFilePath] string callerFilePath = "",
|
||||
[CallerLineNumber] int callerLineNumber = 0)
|
||||
{
|
||||
LogMessage(LogLevel.Debug, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs a debug message to the console.
|
||||
/// </summary>
|
||||
/// <param name="message">The message.</param>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="extendedData">The extended data.</param>
|
||||
/// <param name="callerMemberName">Name of the caller member.</param>
|
||||
/// <param name="callerFilePath">The caller file path.</param>
|
||||
/// <param name="callerLineNumber">The caller line number.</param>
|
||||
public static void Debug(
|
||||
this string message,
|
||||
Type source,
|
||||
object extendedData = null,
|
||||
[CallerMemberName] string callerMemberName = "",
|
||||
[CallerFilePath] string callerFilePath = "",
|
||||
[CallerLineNumber] int callerLineNumber = 0)
|
||||
{
|
||||
LogMessage(LogLevel.Debug, message, source?.FullName, extendedData, callerMemberName, callerFilePath, callerLineNumber);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs a debug message to the console.
|
||||
/// </summary>
|
||||
/// <param name="extendedData">The exception.</param>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="message">The message.</param>
|
||||
/// <param name="callerMemberName">Name of the caller member. This is automatically populated.</param>
|
||||
/// <param name="callerFilePath">The caller file path. This is automatically populated.</param>
|
||||
/// <param name="callerLineNumber">The caller line number. This is automatically populated.</param>
|
||||
public static void Debug(
|
||||
this Exception extendedData,
|
||||
string source,
|
||||
string message,
|
||||
[CallerMemberName] string callerMemberName = "",
|
||||
[CallerFilePath] string callerFilePath = "",
|
||||
[CallerLineNumber] int callerLineNumber = 0)
|
||||
{
|
||||
LogMessage(LogLevel.Debug, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Trace
|
||||
|
||||
/// <summary>
|
||||
/// Logs a trace message to the console.
|
||||
/// </summary>
|
||||
/// <param name="message">The text.</param>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="extendedData">The extended data.</param>
|
||||
/// <param name="callerMemberName">Name of the caller member. This is automatically populated.</param>
|
||||
/// <param name="callerFilePath">The caller file path. This is automatically populated.</param>
|
||||
/// <param name="callerLineNumber">The caller line number. This is automatically populated.</param>
|
||||
public static void Trace(
|
||||
this string message,
|
||||
string source = null,
|
||||
object extendedData = null,
|
||||
[CallerMemberName] string callerMemberName = "",
|
||||
[CallerFilePath] string callerFilePath = "",
|
||||
[CallerLineNumber] int callerLineNumber = 0)
|
||||
{
|
||||
LogMessage(LogLevel.Trace, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs a trace message to the console.
|
||||
/// </summary>
|
||||
/// <param name="message">The message.</param>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="extendedData">The extended data.</param>
|
||||
/// <param name="callerMemberName">Name of the caller member.</param>
|
||||
/// <param name="callerFilePath">The caller file path.</param>
|
||||
/// <param name="callerLineNumber">The caller line number.</param>
|
||||
public static void Trace(
|
||||
this string message,
|
||||
Type source,
|
||||
object extendedData = null,
|
||||
[CallerMemberName] string callerMemberName = "",
|
||||
[CallerFilePath] string callerFilePath = "",
|
||||
[CallerLineNumber] int callerLineNumber = 0)
|
||||
{
|
||||
LogMessage(LogLevel.Trace, message, source?.FullName, extendedData, callerMemberName, callerFilePath, callerLineNumber);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs a trace message to the console.
|
||||
/// </summary>
|
||||
/// <param name="extendedData">The extended data.</param>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="message">The message.</param>
|
||||
/// <param name="callerMemberName">Name of the caller member. This is automatically populated.</param>
|
||||
/// <param name="callerFilePath">The caller file path. This is automatically populated.</param>
|
||||
/// <param name="callerLineNumber">The caller line number. This is automatically populated.</param>
|
||||
public static void Trace(
|
||||
this Exception extendedData,
|
||||
string source,
|
||||
string message,
|
||||
[CallerMemberName] string callerMemberName = "",
|
||||
[CallerFilePath] string callerFilePath = "",
|
||||
[CallerLineNumber] int callerLineNumber = 0)
|
||||
{
|
||||
LogMessage(LogLevel.Trace, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Warn
|
||||
|
||||
/// <summary>
|
||||
/// Logs a warning message to the console.
|
||||
/// </summary>
|
||||
/// <param name="message">The text.</param>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="extendedData">The extended data.</param>
|
||||
/// <param name="callerMemberName">Name of the caller member. This is automatically populated.</param>
|
||||
/// <param name="callerFilePath">The caller file path. This is automatically populated.</param>
|
||||
/// <param name="callerLineNumber">The caller line number. This is automatically populated.</param>
|
||||
public static void Warn(
|
||||
this string message,
|
||||
string source = null,
|
||||
object extendedData = null,
|
||||
[CallerMemberName] string callerMemberName = "",
|
||||
[CallerFilePath] string callerFilePath = "",
|
||||
[CallerLineNumber] int callerLineNumber = 0)
|
||||
{
|
||||
LogMessage(LogLevel.Warning, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs a warning message to the console.
|
||||
/// </summary>
|
||||
/// <param name="message">The message.</param>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="extendedData">The extended data.</param>
|
||||
/// <param name="callerMemberName">Name of the caller member.</param>
|
||||
/// <param name="callerFilePath">The caller file path.</param>
|
||||
/// <param name="callerLineNumber">The caller line number.</param>
|
||||
public static void Warn(
|
||||
this string message,
|
||||
Type source,
|
||||
object extendedData = null,
|
||||
[CallerMemberName] string callerMemberName = "",
|
||||
[CallerFilePath] string callerFilePath = "",
|
||||
[CallerLineNumber] int callerLineNumber = 0)
|
||||
{
|
||||
LogMessage(LogLevel.Warning, message, source?.FullName, extendedData, callerMemberName, callerFilePath, callerLineNumber);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs a warning message to the console.
|
||||
/// </summary>
|
||||
/// <param name="extendedData">The extended data.</param>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="message">The message.</param>
|
||||
/// <param name="callerMemberName">Name of the caller member. This is automatically populated.</param>
|
||||
/// <param name="callerFilePath">The caller file path. This is automatically populated.</param>
|
||||
/// <param name="callerLineNumber">The caller line number. This is automatically populated.</param>
|
||||
public static void Warn(
|
||||
this Exception extendedData,
|
||||
string source,
|
||||
string message,
|
||||
[CallerMemberName] string callerMemberName = "",
|
||||
[CallerFilePath] string callerFilePath = "",
|
||||
[CallerLineNumber] int callerLineNumber = 0)
|
||||
{
|
||||
LogMessage(LogLevel.Warning, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Fatal
|
||||
|
||||
/// <summary>
|
||||
/// Logs a warning message to the console.
|
||||
/// </summary>
|
||||
/// <param name="message">The text.</param>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="extendedData">The extended data.</param>
|
||||
/// <param name="callerMemberName">Name of the caller member. This is automatically populated.</param>
|
||||
/// <param name="callerFilePath">The caller file path. This is automatically populated.</param>
|
||||
/// <param name="callerLineNumber">The caller line number. This is automatically populated.</param>
|
||||
public static void Fatal(
|
||||
this string message,
|
||||
string source = null,
|
||||
object extendedData = null,
|
||||
[CallerMemberName] string callerMemberName = "",
|
||||
[CallerFilePath] string callerFilePath = "",
|
||||
[CallerLineNumber] int callerLineNumber = 0)
|
||||
{
|
||||
LogMessage(LogLevel.Fatal, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs a warning message to the console.
|
||||
/// </summary>
|
||||
/// <param name="message">The message.</param>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="extendedData">The extended data.</param>
|
||||
/// <param name="callerMemberName">Name of the caller member.</param>
|
||||
/// <param name="callerFilePath">The caller file path.</param>
|
||||
/// <param name="callerLineNumber">The caller line number.</param>
|
||||
public static void Fatal(
|
||||
this string message,
|
||||
Type source,
|
||||
object extendedData = null,
|
||||
[CallerMemberName] string callerMemberName = "",
|
||||
[CallerFilePath] string callerFilePath = "",
|
||||
[CallerLineNumber] int callerLineNumber = 0)
|
||||
{
|
||||
LogMessage(LogLevel.Fatal, message, source?.FullName, extendedData, callerMemberName, callerFilePath, callerLineNumber);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs a warning message to the console.
|
||||
/// </summary>
|
||||
/// <param name="extendedData">The extended data.</param>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="message">The message.</param>
|
||||
/// <param name="callerMemberName">Name of the caller member. This is automatically populated.</param>
|
||||
/// <param name="callerFilePath">The caller file path. This is automatically populated.</param>
|
||||
/// <param name="callerLineNumber">The caller line number. This is automatically populated.</param>
|
||||
public static void Fatal(
|
||||
this Exception extendedData,
|
||||
string source,
|
||||
string message,
|
||||
[CallerMemberName] string callerMemberName = "",
|
||||
[CallerFilePath] string callerFilePath = "",
|
||||
[CallerLineNumber] int callerLineNumber = 0)
|
||||
{
|
||||
LogMessage(LogLevel.Fatal, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Info
|
||||
|
||||
/// <summary>
|
||||
/// Logs an info message to the console.
|
||||
/// </summary>
|
||||
/// <param name="message">The text.</param>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="extendedData">The extended data.</param>
|
||||
/// <param name="callerMemberName">Name of the caller member. This is automatically populated.</param>
|
||||
/// <param name="callerFilePath">The caller file path. This is automatically populated.</param>
|
||||
/// <param name="callerLineNumber">The caller line number. This is automatically populated.</param>
|
||||
public static void Info(
|
||||
this string message,
|
||||
string source = null,
|
||||
object extendedData = null,
|
||||
[CallerMemberName] string callerMemberName = "",
|
||||
[CallerFilePath] string callerFilePath = "",
|
||||
[CallerLineNumber] int callerLineNumber = 0)
|
||||
{
|
||||
LogMessage(LogLevel.Info, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs an info message to the console.
|
||||
/// </summary>
|
||||
/// <param name="message">The message.</param>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="extendedData">The extended data.</param>
|
||||
/// <param name="callerMemberName">Name of the caller member.</param>
|
||||
/// <param name="callerFilePath">The caller file path.</param>
|
||||
/// <param name="callerLineNumber">The caller line number.</param>
|
||||
public static void Info(
|
||||
this string message,
|
||||
Type source,
|
||||
object extendedData = null,
|
||||
[CallerMemberName] string callerMemberName = "",
|
||||
[CallerFilePath] string callerFilePath = "",
|
||||
[CallerLineNumber] int callerLineNumber = 0)
|
||||
{
|
||||
LogMessage(LogLevel.Info, message, source?.FullName, extendedData, callerMemberName, callerFilePath, callerLineNumber);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs an info message to the console.
|
||||
/// </summary>
|
||||
/// <param name="extendedData">The extended data.</param>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="message">The message.</param>
|
||||
/// <param name="callerMemberName">Name of the caller member. This is automatically populated.</param>
|
||||
/// <param name="callerFilePath">The caller file path. This is automatically populated.</param>
|
||||
/// <param name="callerLineNumber">The caller line number. This is automatically populated.</param>
|
||||
public static void Info(
|
||||
this Exception extendedData,
|
||||
string source,
|
||||
string message,
|
||||
[CallerMemberName] string callerMemberName = "",
|
||||
[CallerFilePath] string callerFilePath = "",
|
||||
[CallerLineNumber] int callerLineNumber = 0)
|
||||
{
|
||||
LogMessage(LogLevel.Info, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Error
|
||||
|
||||
/// <summary>
|
||||
/// Logs an error message to the console's standard error.
|
||||
/// </summary>
|
||||
/// <param name="message">The text.</param>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="extendedData">The extended data.</param>
|
||||
/// <param name="callerMemberName">Name of the caller member. This is automatically populated.</param>
|
||||
/// <param name="callerFilePath">The caller file path. This is automatically populated.</param>
|
||||
/// <param name="callerLineNumber">The caller line number. This is automatically populated.</param>
|
||||
public static void Error(
|
||||
this string message,
|
||||
string source = null,
|
||||
object extendedData = null,
|
||||
[CallerMemberName] string callerMemberName = "",
|
||||
[CallerFilePath] string callerFilePath = "",
|
||||
[CallerLineNumber] int callerLineNumber = 0)
|
||||
{
|
||||
LogMessage(LogLevel.Error, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs an error message to the console's standard error.
|
||||
/// </summary>
|
||||
/// <param name="message">The message.</param>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="extendedData">The extended data.</param>
|
||||
/// <param name="callerMemberName">Name of the caller member.</param>
|
||||
/// <param name="callerFilePath">The caller file path.</param>
|
||||
/// <param name="callerLineNumber">The caller line number.</param>
|
||||
public static void Error(
|
||||
this string message,
|
||||
Type source,
|
||||
object extendedData = null,
|
||||
[CallerMemberName] string callerMemberName = "",
|
||||
[CallerFilePath] string callerFilePath = "",
|
||||
[CallerLineNumber] int callerLineNumber = 0)
|
||||
{
|
||||
LogMessage(LogLevel.Error, message, source?.FullName, extendedData, callerMemberName, callerFilePath, callerLineNumber);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs an error message to the console's standard error.
|
||||
/// </summary>
|
||||
/// <param name="ex">The exception.</param>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="message">The message.</param>
|
||||
/// <param name="callerMemberName">Name of the caller member. This is automatically populated.</param>
|
||||
/// <param name="callerFilePath">The caller file path. This is automatically populated.</param>
|
||||
/// <param name="callerLineNumber">The caller line number. This is automatically populated.</param>
|
||||
public static void Error(
|
||||
this Exception ex,
|
||||
string source,
|
||||
string message,
|
||||
[CallerMemberName] string callerMemberName = "",
|
||||
[CallerFilePath] string callerFilePath = "",
|
||||
[CallerLineNumber] int callerLineNumber = 0)
|
||||
{
|
||||
LogMessage(LogLevel.Error, message, source, ex, callerMemberName, callerFilePath, callerLineNumber);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
|
||||
#region Extended Public API
|
||||
|
||||
/// <summary>
|
||||
/// Logs the specified message.
|
||||
/// </summary>
|
||||
/// <param name="message">The message.</param>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="messageType">Type of the message.</param>
|
||||
/// <param name="extendedData">The extended data.</param>
|
||||
/// <param name="callerMemberName">Name of the caller member. This is automatically populated.</param>
|
||||
/// <param name="callerFilePath">The caller file path. This is automatically populated.</param>
|
||||
/// <param name="callerLineNumber">The caller line number. This is automatically populated.</param>
|
||||
public static void Log(
|
||||
this string message,
|
||||
string source,
|
||||
LogLevel messageType,
|
||||
object extendedData = null,
|
||||
[CallerMemberName] string callerMemberName = "",
|
||||
[CallerFilePath] string callerFilePath = "",
|
||||
[CallerLineNumber] int callerLineNumber = 0)
|
||||
{
|
||||
LogMessage(messageType, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs the specified message.
|
||||
/// </summary>
|
||||
/// <param name="message">The message.</param>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="messageType">Type of the message.</param>
|
||||
/// <param name="extendedData">The extended data.</param>
|
||||
/// <param name="callerMemberName">Name of the caller member.</param>
|
||||
/// <param name="callerFilePath">The caller file path.</param>
|
||||
/// <param name="callerLineNumber">The caller line number.</param>
|
||||
public static void Log(
|
||||
this string message,
|
||||
Type source,
|
||||
LogLevel messageType,
|
||||
object extendedData = null,
|
||||
[CallerMemberName] string callerMemberName = "",
|
||||
[CallerFilePath] string callerFilePath = "",
|
||||
[CallerLineNumber] int callerLineNumber = 0)
|
||||
{
|
||||
LogMessage(messageType, message, source?.FullName, extendedData, callerMemberName, callerFilePath, callerLineNumber);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs an error message to the console's standard error.
|
||||
/// </summary>
|
||||
/// <param name="ex">The ex.</param>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="message">The message.</param>
|
||||
/// <param name="callerMemberName">Name of the caller member. This is automatically populated.</param>
|
||||
/// <param name="callerFilePath">The caller file path. This is automatically populated.</param>
|
||||
/// <param name="callerLineNumber">The caller line number. This is automatically populated.</param>
|
||||
public static void Log(
|
||||
this Exception ex,
|
||||
string source = null,
|
||||
string message = null,
|
||||
[CallerMemberName] string callerMemberName = "",
|
||||
[CallerFilePath] string callerFilePath = "",
|
||||
[CallerLineNumber] int callerLineNumber = 0)
|
||||
{
|
||||
LogMessage(LogLevel.Error, message ?? ex.Message, source ?? ex.Source, ex, callerMemberName, callerFilePath, callerLineNumber);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs an error message to the console's standard error.
|
||||
/// </summary>
|
||||
/// <param name="ex">The ex.</param>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="message">The message.</param>
|
||||
/// <param name="callerMemberName">Name of the caller member. This is automatically populated.</param>
|
||||
/// <param name="callerFilePath">The caller file path. This is automatically populated.</param>
|
||||
/// <param name="callerLineNumber">The caller line number. This is automatically populated.</param>
|
||||
public static void Log(
|
||||
this Exception ex,
|
||||
Type source = null,
|
||||
string message = null,
|
||||
[CallerMemberName] string callerMemberName = "",
|
||||
[CallerFilePath] string callerFilePath = "",
|
||||
[CallerLineNumber] int callerLineNumber = 0)
|
||||
{
|
||||
LogMessage(LogLevel.Error, message ?? ex.Message, source?.FullName ?? ex.Source, ex, callerMemberName, callerFilePath, callerLineNumber);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs a trace message showing all possible non-null properties of the given object
|
||||
/// This method is expensive as it uses Stringify internally.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object.</param>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="text">The title.</param>
|
||||
/// <param name="callerMemberName">Name of the caller member. This is automatically populated.</param>
|
||||
/// <param name="callerFilePath">The caller file path. This is automatically populated.</param>
|
||||
/// <param name="callerLineNumber">The caller line number. This is automatically populated.</param>
|
||||
public static void Dump(
|
||||
this object obj,
|
||||
string source,
|
||||
string text = "Object Dump",
|
||||
[CallerMemberName] string callerMemberName = "",
|
||||
[CallerFilePath] string callerFilePath = "",
|
||||
[CallerLineNumber] int callerLineNumber = 0)
|
||||
{
|
||||
if (obj == null) return;
|
||||
var message = $"{text} ({obj.GetType()}): {Environment.NewLine}{obj.Stringify().Indent(5)}";
|
||||
LogMessage(LogLevel.Trace, message, source, obj, callerMemberName, callerFilePath, callerLineNumber);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs a trace message showing all possible non-null properties of the given object
|
||||
/// This method is expensive as it uses Stringify internally.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object.</param>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="text">The text.</param>
|
||||
/// <param name="callerMemberName">Name of the caller member.</param>
|
||||
/// <param name="callerFilePath">The caller file path.</param>
|
||||
/// <param name="callerLineNumber">The caller line number.</param>
|
||||
public static void Dump(
|
||||
this object obj,
|
||||
Type source,
|
||||
string text = "Object Dump",
|
||||
[CallerMemberName] string callerMemberName = "",
|
||||
[CallerFilePath] string callerFilePath = "",
|
||||
[CallerLineNumber] int callerLineNumber = 0)
|
||||
{
|
||||
if (obj == null) return;
|
||||
var message = $"{text} ({obj.GetType()}): {Environment.NewLine}{obj.Stringify().Indent(5)}";
|
||||
LogMessage(LogLevel.Trace, message, source?.FullName, obj, callerMemberName, callerFilePath, callerLineNumber);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static void RemoveLogger(Func<ILogger, bool> criteria)
|
||||
{
|
||||
lock (SyncLock)
|
||||
{
|
||||
var loggerInstance = Loggers.FirstOrDefault(criteria);
|
||||
|
||||
if (loggerInstance == null)
|
||||
throw new InvalidOperationException("The logger is not registered.");
|
||||
|
||||
loggerInstance.Dispose();
|
||||
|
||||
Loggers.Remove(loggerInstance);
|
||||
}
|
||||
}
|
||||
|
||||
private static void LogMessage(
|
||||
LogLevel logLevel,
|
||||
string message,
|
||||
string sourceName,
|
||||
object extendedData,
|
||||
string callerMemberName,
|
||||
string callerFilePath,
|
||||
int callerLineNumber)
|
||||
{
|
||||
var sequence = _loggingSequence;
|
||||
var date = DateTime.UtcNow;
|
||||
_loggingSequence++;
|
||||
|
||||
var loggerMessage = string.IsNullOrWhiteSpace(message) ?
|
||||
string.Empty : message.RemoveControlCharsExcept('\n');
|
||||
|
||||
var eventArgs = new LogMessageReceivedEventArgs(
|
||||
sequence,
|
||||
logLevel,
|
||||
date,
|
||||
sourceName,
|
||||
loggerMessage,
|
||||
extendedData,
|
||||
callerMemberName,
|
||||
callerFilePath,
|
||||
callerLineNumber);
|
||||
|
||||
foreach (var logger in Loggers)
|
||||
{
|
||||
Task.Run(() =>
|
||||
{
|
||||
if (logger.LogLevel <= logLevel)
|
||||
logger.Log(eventArgs);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
89
Swan.Lite/Logging/TextLogger.cs
Normal file
89
Swan.Lite/Logging/TextLogger.cs
Normal file
@ -0,0 +1,89 @@
|
||||
using Swan.Logging;
|
||||
using System;
|
||||
|
||||
namespace Swan.Lite.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// Use this class for text-based logger.
|
||||
/// </summary>
|
||||
public abstract class TextLogger
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the logging time format.
|
||||
/// set to null or empty to prevent output.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The logging time format.
|
||||
/// </value>
|
||||
public static string LoggingTimeFormat { get; set; } = "HH:mm:ss.fff";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the color of the output of the message (the output message has a new line char in the end).
|
||||
/// </summary>
|
||||
/// <param name="logEvent">The <see cref="LogMessageReceivedEventArgs" /> instance containing the event data.</param>
|
||||
/// <returns>
|
||||
/// The output message formatted and the color of the console to be used.
|
||||
/// </returns>
|
||||
protected (string outputMessage, ConsoleColor color) GetOutputAndColor(LogMessageReceivedEventArgs logEvent)
|
||||
{
|
||||
var (prefix , color) = GetConsoleColorAndPrefix(logEvent.MessageType);
|
||||
|
||||
var loggerMessage = string.IsNullOrWhiteSpace(logEvent.Message)
|
||||
? string.Empty
|
||||
: logEvent.Message.RemoveControlCharsExcept('\n');
|
||||
|
||||
var outputMessage = CreateOutputMessage(logEvent.Source, loggerMessage, prefix, logEvent.UtcDate);
|
||||
|
||||
// Further format the output in the case there is an exception being logged
|
||||
if (logEvent.MessageType == LogLevel.Error && logEvent.Exception != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
outputMessage += $"{logEvent.Exception.Stringify().Indent()}{Environment.NewLine}";
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
return (outputMessage, color);
|
||||
}
|
||||
|
||||
private static (string Prefix, ConsoleColor color) GetConsoleColorAndPrefix(LogLevel messageType)
|
||||
{
|
||||
switch (messageType)
|
||||
{
|
||||
case LogLevel.Debug:
|
||||
return (ConsoleLogger.DebugPrefix, ConsoleLogger.DebugColor);
|
||||
case LogLevel.Error:
|
||||
return (ConsoleLogger.ErrorPrefix, ConsoleLogger.ErrorColor);
|
||||
case LogLevel.Info:
|
||||
return (ConsoleLogger.InfoPrefix, ConsoleLogger.InfoColor);
|
||||
case LogLevel.Trace:
|
||||
return (ConsoleLogger.TracePrefix, ConsoleLogger.TraceColor);
|
||||
case LogLevel.Warning:
|
||||
return (ConsoleLogger.WarnPrefix, ConsoleLogger.WarnColor);
|
||||
case LogLevel.Fatal:
|
||||
return (ConsoleLogger.FatalPrefix, ConsoleLogger.FatalColor);
|
||||
default:
|
||||
return (new string(' ', ConsoleLogger.InfoPrefix.Length), Terminal.Settings.DefaultColor);
|
||||
}
|
||||
}
|
||||
|
||||
private static string CreateOutputMessage(string sourceName, string loggerMessage, string prefix, DateTime date)
|
||||
{
|
||||
var friendlySourceName = string.IsNullOrWhiteSpace(sourceName)
|
||||
? string.Empty
|
||||
: sourceName.SliceLength(sourceName.LastIndexOf('.') + 1, sourceName.Length);
|
||||
|
||||
var outputMessage = string.IsNullOrWhiteSpace(sourceName)
|
||||
? loggerMessage
|
||||
: $"[{friendlySourceName}] {loggerMessage}";
|
||||
|
||||
return string.IsNullOrWhiteSpace(LoggingTimeFormat)
|
||||
? $" {prefix} >> {outputMessage}{Environment.NewLine}"
|
||||
: $" {date.ToLocalTime().ToString(LoggingTimeFormat)} {prefix} >> {outputMessage}{Environment.NewLine}";
|
||||
}
|
||||
}
|
||||
}
|
13
Swan.Lite/Mappers/CopyableAttribute.cs
Normal file
13
Swan.Lite/Mappers/CopyableAttribute.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using System;
|
||||
|
||||
namespace Swan.Mappers
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an attribute to select which properties are copyable between objects.
|
||||
/// </summary>
|
||||
/// <seealso cref="Attribute" />
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class CopyableAttribute : Attribute
|
||||
{
|
||||
}
|
||||
}
|
27
Swan.Lite/Mappers/IObjectMap.cs
Normal file
27
Swan.Lite/Mappers/IObjectMap.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Swan.Mappers
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface object map.
|
||||
/// </summary>
|
||||
public interface IObjectMap
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the map.
|
||||
/// </summary>
|
||||
Dictionary<PropertyInfo, List<PropertyInfo>> Map { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of the source.
|
||||
/// </summary>
|
||||
Type SourceType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of the destination.
|
||||
/// </summary>
|
||||
Type DestinationType { get; }
|
||||
}
|
||||
}
|
115
Swan.Lite/Mappers/ObjectMap.cs
Normal file
115
Swan.Lite/Mappers/ObjectMap.cs
Normal file
@ -0,0 +1,115 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Swan.Mappers
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an object map.
|
||||
/// </summary>
|
||||
/// <typeparam name="TSource">The type of the source.</typeparam>
|
||||
/// <typeparam name="TDestination">The type of the destination.</typeparam>
|
||||
/// <seealso cref="IObjectMap" />
|
||||
public class ObjectMap<TSource, TDestination> : IObjectMap
|
||||
{
|
||||
internal ObjectMap(IEnumerable<PropertyInfo> intersect)
|
||||
{
|
||||
SourceType = typeof(TSource);
|
||||
DestinationType = typeof(TDestination);
|
||||
Map = intersect.ToDictionary(
|
||||
property => DestinationType.GetProperty(property.Name),
|
||||
property => new List<PropertyInfo> {SourceType.GetProperty(property.Name)});
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Dictionary<PropertyInfo, List<PropertyInfo>> Map { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Type SourceType { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Type DestinationType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Maps the property.
|
||||
/// </summary>
|
||||
/// <typeparam name="TDestinationProperty">The type of the destination property.</typeparam>
|
||||
/// <typeparam name="TSourceProperty">The type of the source property.</typeparam>
|
||||
/// <param name="destinationProperty">The destination property.</param>
|
||||
/// <param name="sourceProperty">The source property.</param>
|
||||
/// <returns>
|
||||
/// An object map representation of type of the destination property
|
||||
/// and type of the source property.
|
||||
/// </returns>
|
||||
public ObjectMap<TSource, TDestination> MapProperty
|
||||
<TDestinationProperty, TSourceProperty>(
|
||||
Expression<Func<TDestination, TDestinationProperty>> destinationProperty,
|
||||
Expression<Func<TSource, TSourceProperty>> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the map property.
|
||||
/// </summary>
|
||||
/// <typeparam name="TDestinationProperty">The type of the destination property.</typeparam>
|
||||
/// <param name="destinationProperty">The destination property.</param>
|
||||
/// <returns>
|
||||
/// An object map representation of type of the destination property
|
||||
/// and type of the source property.
|
||||
/// </returns>
|
||||
/// <exception cref="System.Exception">Invalid destination expression.</exception>
|
||||
public ObjectMap<TSource, TDestination> RemoveMapProperty<TDestinationProperty>(
|
||||
Expression<Func<TDestination, TDestinationProperty>> 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<PropertyInfo> GetSourceMembers<TSourceProperty>(Expression<Func<TSource, TSourceProperty>> sourceProperty)
|
||||
{
|
||||
var sourceMembers = new List<PropertyInfo>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
24
Swan.Lite/Mappers/ObjectMapper.PropertyInfoComparer.cs
Normal file
24
Swan.Lite/Mappers/ObjectMapper.PropertyInfoComparer.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Swan.Mappers
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public partial class ObjectMapper
|
||||
{
|
||||
internal class PropertyInfoComparer : IEqualityComparer<PropertyInfo>
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
372
Swan.Lite/Mappers/ObjectMapper.cs
Normal file
372
Swan.Lite/Mappers/ObjectMapper.cs
Normal file
@ -0,0 +1,372 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Swan.Reflection;
|
||||
|
||||
namespace Swan.Mappers
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// The following code explains how to map an object's properties into an instance of type T.
|
||||
/// <code>
|
||||
/// using Swan.Mappers;
|
||||
///
|
||||
/// class Example
|
||||
/// {
|
||||
/// class Person
|
||||
/// {
|
||||
/// public string Name { get; set; }
|
||||
/// public int Age { get; set; }
|
||||
/// }
|
||||
///
|
||||
/// static void Main()
|
||||
/// {
|
||||
/// var obj = new { Name = "John", Age = 42 };
|
||||
///
|
||||
/// var person = Runtime.ObjectMapper.Map<Person>(obj);
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
///
|
||||
/// The following code explains how to explicitly map certain properties.
|
||||
/// <code>
|
||||
/// using Swan.Mappers;
|
||||
///
|
||||
/// class Example
|
||||
/// {
|
||||
/// class User
|
||||
/// {
|
||||
/// public string Name { get; set; }
|
||||
/// public Role Role { get; set; }
|
||||
/// }
|
||||
///
|
||||
/// public class Role
|
||||
/// {
|
||||
/// public string Name { get; set; }
|
||||
/// }
|
||||
///
|
||||
/// class UserDto
|
||||
/// {
|
||||
/// public string Name { get; set; }
|
||||
/// public string Role { get; set; }
|
||||
/// }
|
||||
///
|
||||
/// static void Main()
|
||||
/// {
|
||||
/// // create a User object
|
||||
/// var person =
|
||||
/// new User { Name = "Phillip", Role = new Role { Name = "Admin" } };
|
||||
///
|
||||
/// // create an Object Mapper
|
||||
/// var mapper = new ObjectMapper();
|
||||
///
|
||||
/// // map the User's Role.Name to UserDto's Role
|
||||
/// mapper.CreateMap<User, UserDto>()
|
||||
/// .MapProperty(d => d.Role, x => x.Role.Name);
|
||||
///
|
||||
/// // apply the previous map and retrieve a UserDto object
|
||||
/// var destination = mapper.Map<UserDto>(person);
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public partial class ObjectMapper
|
||||
{
|
||||
private static readonly Lazy<ObjectMapper> LazyInstance = new Lazy<ObjectMapper>(() => new ObjectMapper());
|
||||
|
||||
private readonly List<IObjectMap> _maps = new List<IObjectMap>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The current.
|
||||
/// </value>
|
||||
public static ObjectMapper Current => LazyInstance.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Copies the specified source.
|
||||
/// </summary>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="target">The target.</param>
|
||||
/// <param name="propertiesToCopy">The properties to copy.</param>
|
||||
/// <param name="ignoreProperties">The ignore properties.</param>
|
||||
/// <returns>
|
||||
/// Copied properties count.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// source
|
||||
/// or
|
||||
/// target.
|
||||
/// </exception>
|
||||
public static int Copy(
|
||||
object source,
|
||||
object target,
|
||||
IEnumerable<string>? propertiesToCopy = null,
|
||||
params string[]? ignoreProperties)
|
||||
{
|
||||
if (source == null)
|
||||
throw new ArgumentNullException(nameof(source));
|
||||
|
||||
if (target == null)
|
||||
throw new ArgumentNullException(nameof(target));
|
||||
|
||||
return CopyInternal(
|
||||
target,
|
||||
GetSourceMap(source),
|
||||
propertiesToCopy,
|
||||
ignoreProperties);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies the specified source.
|
||||
/// </summary>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="target">The target.</param>
|
||||
/// <param name="propertiesToCopy">The properties to copy.</param>
|
||||
/// <param name="ignoreProperties">The ignore properties.</param>
|
||||
/// <returns>
|
||||
/// Copied properties count.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// source
|
||||
/// or
|
||||
/// target.
|
||||
/// </exception>
|
||||
public static int Copy(
|
||||
IDictionary<string, object> source,
|
||||
object target,
|
||||
IEnumerable<string>? propertiesToCopy = null,
|
||||
params string[] ignoreProperties)
|
||||
{
|
||||
if (source == null)
|
||||
throw new ArgumentNullException(nameof(source));
|
||||
|
||||
if (target == null)
|
||||
throw new ArgumentNullException(nameof(target));
|
||||
|
||||
return CopyInternal(
|
||||
target,
|
||||
source.ToDictionary(
|
||||
x => x.Key.ToLowerInvariant(),
|
||||
x => Tuple.Create(typeof(object), x.Value)),
|
||||
propertiesToCopy,
|
||||
ignoreProperties);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the map.
|
||||
/// </summary>
|
||||
/// <typeparam name="TSource">The type of the source.</typeparam>
|
||||
/// <typeparam name="TDestination">The type of the destination.</typeparam>
|
||||
/// <returns>
|
||||
/// An object map representation of type of the destination property
|
||||
/// and type of the source property.
|
||||
/// </returns>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// You can't create an existing map
|
||||
/// or
|
||||
/// Types doesn't match.
|
||||
/// </exception>
|
||||
public ObjectMap<TSource, TDestination> CreateMap<TSource, TDestination>()
|
||||
{
|
||||
if (_maps.Any(x => x.SourceType == typeof(TSource) && x.DestinationType == typeof(TDestination)))
|
||||
throw new InvalidOperationException("You can't create an existing map");
|
||||
|
||||
var sourceType = PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties<TSource>(true);
|
||||
var destinationType = PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties<TDestination>(true);
|
||||
|
||||
var intersect = sourceType.Intersect(destinationType, new PropertyInfoComparer()).ToArray();
|
||||
|
||||
if (!intersect.Any())
|
||||
throw new InvalidOperationException("Types doesn't match");
|
||||
|
||||
var map = new ObjectMap<TSource, TDestination>(intersect);
|
||||
|
||||
_maps.Add(map);
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps the specified source.
|
||||
/// </summary>
|
||||
/// <typeparam name="TDestination">The type of the destination.</typeparam>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="autoResolve">if set to <c>true</c> [automatic resolve].</param>
|
||||
/// <returns>
|
||||
/// A new instance of the map.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">source.</exception>
|
||||
/// <exception cref="InvalidOperationException">You can't map from type {source.GetType().Name} to {typeof(TDestination).Name}.</exception>
|
||||
public TDestination Map<TDestination>(object source, bool autoResolve = true)
|
||||
{
|
||||
if (source == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(source));
|
||||
}
|
||||
|
||||
var destination = Activator.CreateInstance<TDestination>();
|
||||
var map = _maps
|
||||
.FirstOrDefault(x => x.SourceType == source.GetType() && x.DestinationType == typeof(TDestination));
|
||||
|
||||
if (map != null)
|
||||
{
|
||||
foreach (var property in map.Map)
|
||||
{
|
||||
var finalSource = property.Value.Aggregate(source,
|
||||
(current, sourceProperty) => sourceProperty.GetValue(current));
|
||||
|
||||
property.Key.SetValue(destination, finalSource);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!autoResolve)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"You can't map from type {source.GetType().Name} to {typeof(TDestination).Name}");
|
||||
}
|
||||
|
||||
// Missing mapping, try to use default behavior
|
||||
Copy(source, destination);
|
||||
}
|
||||
|
||||
return destination;
|
||||
}
|
||||
|
||||
private static int CopyInternal(
|
||||
object target,
|
||||
Dictionary<string, Tuple<Type, object>> sourceProperties,
|
||||
IEnumerable<string>? propertiesToCopy,
|
||||
IEnumerable<string>? ignoreProperties)
|
||||
{
|
||||
// Filter properties
|
||||
var requiredProperties = propertiesToCopy?
|
||||
.Where(p => !string.IsNullOrWhiteSpace(p))
|
||||
.Select(p => p.ToLowerInvariant());
|
||||
|
||||
var ignoredProperties = ignoreProperties?
|
||||
.Where(p => !string.IsNullOrWhiteSpace(p))
|
||||
.Select(p => p.ToLowerInvariant());
|
||||
|
||||
var properties = PropertyTypeCache.DefaultCache.Value
|
||||
.RetrieveFilteredProperties(target.GetType(), true, x => x.CanWrite);
|
||||
|
||||
return properties
|
||||
.Select(x => x.Name)
|
||||
.Distinct()
|
||||
.ToDictionary(x => x.ToLowerInvariant(), x => properties.First(y => y.Name == x))
|
||||
.Where(x => sourceProperties.Keys.Contains(x.Key))
|
||||
.When(() => requiredProperties != null, q => q.Where(y => requiredProperties.Contains(y.Key)))
|
||||
.When(() => ignoredProperties != null, q => q.Where(y => !ignoredProperties.Contains(y.Key)))
|
||||
.ToDictionary(x => x.Value, x => sourceProperties[x.Key])
|
||||
.Sum(x => TrySetValue(x.Key, x.Value, target) ? 1 : 0);
|
||||
}
|
||||
|
||||
private static bool TrySetValue(PropertyInfo propertyInfo, Tuple<Type, object> property, object target)
|
||||
{
|
||||
try
|
||||
{
|
||||
var (type, value) = property;
|
||||
|
||||
if (type.IsEnum)
|
||||
{
|
||||
propertyInfo.SetValue(target,
|
||||
Enum.ToObject(propertyInfo.PropertyType, value));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (type.IsValueType || propertyInfo.PropertyType != type)
|
||||
return propertyInfo.TrySetBasicType(value, target);
|
||||
|
||||
if (propertyInfo.PropertyType.IsArray)
|
||||
{
|
||||
propertyInfo.TrySetArray(value as IEnumerable<object>, target);
|
||||
return true;
|
||||
}
|
||||
|
||||
propertyInfo.SetValue(target, GetValue(value, propertyInfo.PropertyType));
|
||||
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// swallow
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static object? GetValue(object source, Type targetType)
|
||||
{
|
||||
if (source == null)
|
||||
return null;
|
||||
|
||||
object? target = null;
|
||||
|
||||
source.CreateTarget(targetType, false, ref target);
|
||||
|
||||
switch (source)
|
||||
{
|
||||
case string _:
|
||||
target = source;
|
||||
break;
|
||||
case IList sourceList when target is IList targetList:
|
||||
var addMethod = targetType.GetMethods()
|
||||
.FirstOrDefault(
|
||||
m => m.Name == Formatters.Json.AddMethodName && m.IsPublic && m.GetParameters().Length == 1);
|
||||
|
||||
if (addMethod == null) return target;
|
||||
|
||||
var isItemValueType = targetList.GetType().GetElementType().IsValueType;
|
||||
|
||||
foreach (var item in sourceList)
|
||||
{
|
||||
try
|
||||
{
|
||||
targetList.Add(isItemValueType
|
||||
? item
|
||||
: item.CopyPropertiesToNew<object>());
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
source.CopyPropertiesTo(target);
|
||||
break;
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
private static Dictionary<string, Tuple<Type, object>> GetSourceMap(object source)
|
||||
{
|
||||
// select distinct properties because they can be duplicated by inheritance
|
||||
var sourceProperties = PropertyTypeCache.DefaultCache.Value
|
||||
.RetrieveFilteredProperties(source.GetType(), true, x => x.CanRead)
|
||||
.ToArray();
|
||||
|
||||
return sourceProperties
|
||||
.Select(x => x.Name)
|
||||
.Distinct()
|
||||
.ToDictionary(
|
||||
x => x.ToLowerInvariant(),
|
||||
x => Tuple.Create(sourceProperties.First(y => y.Name == x).PropertyType,
|
||||
sourceProperties.First(y => y.Name == x).GetValue(source)));
|
||||
}
|
||||
}
|
||||
}
|
193
Swan.Lite/ObjectComparer.cs
Normal file
193
Swan.Lite/ObjectComparer.cs
Normal file
@ -0,0 +1,193 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Swan.Reflection;
|
||||
|
||||
namespace Swan
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a quick object comparer using the public properties of an object
|
||||
/// or the public members in a structure.
|
||||
/// </summary>
|
||||
public static class ObjectComparer
|
||||
{
|
||||
/// <summary>
|
||||
/// Compare if two variables of the same type are equal.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of objects to compare.</typeparam>
|
||||
/// <param name="left">The left.</param>
|
||||
/// <param name="right">The right.</param>
|
||||
/// <returns><c>true</c> if the variables are equal; otherwise, <c>false</c>.</returns>
|
||||
public static bool AreEqual<T>(T left, T right) => AreEqual(left, right, typeof(T));
|
||||
|
||||
/// <summary>
|
||||
/// Compare if two variables of the same type are equal.
|
||||
/// </summary>
|
||||
/// <param name="left">The left.</param>
|
||||
/// <param name="right">The right.</param>
|
||||
/// <param name="targetType">Type of the target.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the variables are equal; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">targetType.</exception>
|
||||
public static bool AreEqual(object left, object right, Type targetType)
|
||||
{
|
||||
if (targetType == null)
|
||||
throw new ArgumentNullException(nameof(targetType));
|
||||
|
||||
if (Definitions.BasicTypesInfo.Value.ContainsKey(targetType))
|
||||
return Equals(left, right);
|
||||
|
||||
return targetType.IsValueType || targetType.IsArray
|
||||
? AreStructsEqual(left, right, targetType)
|
||||
: AreObjectsEqual(left, right, targetType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compare if two objects of the same type are equal.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of objects to compare.</typeparam>
|
||||
/// <param name="left">The left.</param>
|
||||
/// <param name="right">The right.</param>
|
||||
/// <returns><c>true</c> if the objects are equal; otherwise, <c>false</c>.</returns>
|
||||
public static bool AreObjectsEqual<T>(T left, T right)
|
||||
where T : class
|
||||
{
|
||||
return AreObjectsEqual(left, right, typeof(T));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compare if two objects of the same type are equal.
|
||||
/// </summary>
|
||||
/// <param name="left">The left.</param>
|
||||
/// <param name="right">The right.</param>
|
||||
/// <param name="targetType">Type of the target.</param>
|
||||
/// <returns><c>true</c> if the objects are equal; otherwise, <c>false</c>.</returns>
|
||||
/// <exception cref="ArgumentNullException">targetType.</exception>
|
||||
public static bool AreObjectsEqual(object left, object right, Type targetType)
|
||||
{
|
||||
if (targetType == null)
|
||||
throw new ArgumentNullException(nameof(targetType));
|
||||
|
||||
var properties = PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties(targetType).ToArray();
|
||||
|
||||
foreach (var propertyTarget in properties)
|
||||
{
|
||||
var targetPropertyGetMethod = propertyTarget.GetCacheGetMethod();
|
||||
|
||||
if (propertyTarget.PropertyType.IsArray)
|
||||
{
|
||||
var leftObj = targetPropertyGetMethod(left) as IEnumerable;
|
||||
var rightObj = targetPropertyGetMethod(right) as IEnumerable;
|
||||
|
||||
if (!AreEnumerationsEquals(leftObj, rightObj))
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!Equals(targetPropertyGetMethod(left), targetPropertyGetMethod(right)))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compare if two structures of the same type are equal.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of structs to compare.</typeparam>
|
||||
/// <param name="left">The left.</param>
|
||||
/// <param name="right">The right.</param>
|
||||
/// <returns><c>true</c> if the structs are equal; otherwise, <c>false</c>.</returns>
|
||||
public static bool AreStructsEqual<T>(T left, T right)
|
||||
where T : struct
|
||||
{
|
||||
return AreStructsEqual(left, right, typeof(T));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compare if two structures of the same type are equal.
|
||||
/// </summary>
|
||||
/// <param name="left">The left.</param>
|
||||
/// <param name="right">The right.</param>
|
||||
/// <param name="targetType">Type of the target.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the structs are equal; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">targetType.</exception>
|
||||
public static bool AreStructsEqual(object left, object right, Type targetType)
|
||||
{
|
||||
if (targetType == null)
|
||||
throw new ArgumentNullException(nameof(targetType));
|
||||
|
||||
var fields = new List<MemberInfo>(FieldTypeCache.DefaultCache.Value.RetrieveAllFields(targetType))
|
||||
.Union(PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties(targetType));
|
||||
|
||||
foreach (var targetMember in fields)
|
||||
{
|
||||
switch (targetMember)
|
||||
{
|
||||
case FieldInfo field:
|
||||
if (Equals(field.GetValue(left), field.GetValue(right)) == false)
|
||||
return false;
|
||||
break;
|
||||
case PropertyInfo property:
|
||||
var targetPropertyGetMethod = property.GetCacheGetMethod();
|
||||
|
||||
if (targetPropertyGetMethod != null &&
|
||||
!Equals(targetPropertyGetMethod(left), targetPropertyGetMethod(right)))
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compare if two enumerables are equal.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of enums to compare.</typeparam>
|
||||
/// <param name="left">The left.</param>
|
||||
/// <param name="right">The right.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if two specified types are equal; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// left
|
||||
/// or
|
||||
/// right.
|
||||
/// </exception>
|
||||
public static bool AreEnumerationsEquals<T>(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<object>().ToArray();
|
||||
var rightEnumerable = right.Cast<object>().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;
|
||||
}
|
||||
}
|
||||
}
|
99
Swan.Lite/Paginator.cs
Normal file
99
Swan.Lite/Paginator.cs
Normal file
@ -0,0 +1,99 @@
|
||||
using System;
|
||||
|
||||
namespace Swan
|
||||
{
|
||||
/// <summary>
|
||||
/// A utility class to compute paging or batching offsets.
|
||||
/// </summary>
|
||||
public class Paginator
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Paginator" /> class.
|
||||
/// </summary>
|
||||
/// <param name="totalCount">The total count of items to page over.</param>
|
||||
/// <param name="pageSize">The desired size of individual pages.</param>
|
||||
public Paginator(int totalCount, int pageSize)
|
||||
{
|
||||
TotalCount = totalCount;
|
||||
PageSize = pageSize;
|
||||
PageCount = ComputePageCount();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the desired number of items per page.
|
||||
/// </summary>
|
||||
public int PageSize { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total number of items to page over.
|
||||
/// </summary>
|
||||
public int TotalCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the computed number of pages.
|
||||
/// </summary>
|
||||
public int PageCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the start item index of the given page.
|
||||
/// </summary>
|
||||
/// <param name="pageIndex">Zero-based index of the page.</param>
|
||||
/// <returns>The start item index.</returns>
|
||||
public int GetFirstItemIndex(int pageIndex)
|
||||
{
|
||||
pageIndex = FixPageIndex(pageIndex);
|
||||
return pageIndex * PageSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the end item index of the given page.
|
||||
/// </summary>
|
||||
/// <param name="pageIndex">Zero-based index of the page.</param>
|
||||
/// <returns>The end item index.</returns>
|
||||
public int GetLastItemIndex(int pageIndex)
|
||||
{
|
||||
var startIndex = GetFirstItemIndex(pageIndex);
|
||||
return Math.Min(startIndex + PageSize - 1, TotalCount - 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item count of the given page index.
|
||||
/// </summary>
|
||||
/// <param name="pageIndex">Zero-based index of the page.</param>
|
||||
/// <returns>The number of items that the page contains.</returns>
|
||||
public int GetItemCount(int pageIndex)
|
||||
{
|
||||
pageIndex = FixPageIndex(pageIndex);
|
||||
return (pageIndex >= PageCount - 1)
|
||||
? GetLastItemIndex(pageIndex) - GetFirstItemIndex(pageIndex) + 1
|
||||
: PageSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fixes the index of the page by applying bound logic.
|
||||
/// </summary>
|
||||
/// <param name="pageIndex">Index of the page.</param>
|
||||
/// <returns>A limit-bound index.</returns>
|
||||
private int FixPageIndex(int pageIndex)
|
||||
{
|
||||
if (pageIndex < 0) return 0;
|
||||
|
||||
return pageIndex >= PageCount ? PageCount - 1 : pageIndex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the number of pages for the paginator.
|
||||
/// </summary>
|
||||
/// <returns>The page count.</returns>
|
||||
private int ComputePageCount()
|
||||
{
|
||||
// include this if when you always want at least 1 page
|
||||
if (TotalCount == 0)
|
||||
return 0;
|
||||
|
||||
return TotalCount % PageSize != 0
|
||||
? (TotalCount / PageSize) + 1
|
||||
: TotalCount / PageSize;
|
||||
}
|
||||
}
|
||||
}
|
102
Swan.Lite/Parsers/ArgumentOptionAttribute.cs
Normal file
102
Swan.Lite/Parsers/ArgumentOptionAttribute.cs
Normal file
@ -0,0 +1,102 @@
|
||||
using System;
|
||||
|
||||
namespace Swan.Parsers
|
||||
{
|
||||
/// <summary>
|
||||
/// Models an option specification.
|
||||
/// Based on CommandLine (Copyright 2005-2015 Giacomo Stelluti Scala and Contributors.).
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public sealed class ArgumentOptionAttribute
|
||||
: Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ArgumentOptionAttribute"/> class.
|
||||
/// The default long name will be inferred from target property.
|
||||
/// </summary>
|
||||
public ArgumentOptionAttribute()
|
||||
: this(string.Empty, string.Empty)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ArgumentOptionAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="longName">The long name of the option.</param>
|
||||
public ArgumentOptionAttribute(string longName)
|
||||
: this(string.Empty, longName)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ArgumentOptionAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="shortName">The short name of the option.</param>
|
||||
/// <param name="longName">The long name of the option or null if not used.</param>
|
||||
public ArgumentOptionAttribute(char shortName, string longName)
|
||||
: this(new string(shortName, 1), longName)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ArgumentOptionAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="shortName">The short name of the option..</param>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets long name of this command line option. This name is usually a single English word.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The long name.
|
||||
/// </value>
|
||||
public string LongName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a short name of this command line option, made of one character.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The short name.
|
||||
/// </value>
|
||||
public string ShortName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// When applying attribute to <see cref="System.Collections.Generic.IEnumerable{T}"/> target properties,
|
||||
/// it allows you to split an argument and consume its content as a sequence.
|
||||
/// </summary>
|
||||
public char Separator { get; set; } = '\0';
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets mapped property default value.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The default value.
|
||||
/// </value>
|
||||
public object DefaultValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether a command line option is required.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if required; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public bool Required { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a short description of this command line option. Usually a sentence summary.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The help text.
|
||||
/// </value>
|
||||
public string HelpText { get; set; }
|
||||
}
|
||||
}
|
160
Swan.Lite/Parsers/ArgumentParse.Validator.cs
Normal file
160
Swan.Lite/Parsers/ArgumentParse.Validator.cs
Normal file
@ -0,0 +1,160 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Swan.Reflection;
|
||||
|
||||
namespace Swan.Parsers
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides methods to parse command line arguments.
|
||||
///
|
||||
/// Based on CommandLine (Copyright 2005-2015 Giacomo Stelluti Scala and Contributors).
|
||||
/// </summary>
|
||||
public partial class ArgumentParser
|
||||
{
|
||||
private sealed class Validator
|
||||
{
|
||||
private readonly object _instance;
|
||||
private readonly IEnumerable<string> _args;
|
||||
private readonly List<PropertyInfo> _updatedList = new List<PropertyInfo>();
|
||||
private readonly ArgumentParserSettings _settings;
|
||||
|
||||
private readonly PropertyInfo[] _properties;
|
||||
|
||||
public Validator(
|
||||
PropertyInfo[] properties,
|
||||
IEnumerable<string> args,
|
||||
object instance,
|
||||
ArgumentParserSettings settings)
|
||||
{
|
||||
_args = args;
|
||||
_instance = instance;
|
||||
_settings = settings;
|
||||
_properties = properties;
|
||||
|
||||
PopulateInstance();
|
||||
SetDefaultValues();
|
||||
GetRequiredList();
|
||||
}
|
||||
|
||||
public List<string> UnknownList { get; } = new List<string>();
|
||||
public List<string> RequiredList { get; } = new List<string>();
|
||||
|
||||
public bool IsValid() => (_settings.IgnoreUnknownArguments || !UnknownList.Any()) && !RequiredList.Any();
|
||||
|
||||
public IEnumerable<ArgumentOptionAttribute> GetPropertiesOptions()
|
||||
=> _properties.Select(p => AttributeCache.DefaultCache.Value.RetrieveOne<ArgumentOptionAttribute>(p))
|
||||
.Where(x => x != null);
|
||||
|
||||
private void GetRequiredList()
|
||||
{
|
||||
foreach (var targetProperty in _properties)
|
||||
{
|
||||
var optionAttr = AttributeCache.DefaultCache.Value.RetrieveOne<ArgumentOptionAttribute>(targetProperty);
|
||||
|
||||
if (optionAttr == null || optionAttr.Required == false)
|
||||
continue;
|
||||
|
||||
if (targetProperty.GetValue(_instance) == null)
|
||||
{
|
||||
RequiredList.Add(optionAttr.LongName ?? optionAttr.ShortName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetDefaultValues()
|
||||
{
|
||||
foreach (var targetProperty in _properties.Except(_updatedList))
|
||||
{
|
||||
var optionAttr = AttributeCache.DefaultCache.Value.RetrieveOne<ArgumentOptionAttribute>(targetProperty);
|
||||
|
||||
var defaultValue = optionAttr?.DefaultValue;
|
||||
|
||||
if (defaultValue == null)
|
||||
continue;
|
||||
|
||||
if (SetPropertyValue(targetProperty, defaultValue.ToString(), _instance, optionAttr))
|
||||
_updatedList.Add(targetProperty);
|
||||
}
|
||||
}
|
||||
|
||||
private void PopulateInstance()
|
||||
{
|
||||
const char dash = '-';
|
||||
var propertyName = string.Empty;
|
||||
|
||||
foreach (var arg in _args)
|
||||
{
|
||||
var ignoreSetValue = string.IsNullOrWhiteSpace(propertyName);
|
||||
|
||||
if (ignoreSetValue)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(arg) || arg[0] != dash) continue;
|
||||
|
||||
propertyName = arg.Substring(1);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(propertyName) && propertyName[0] == dash)
|
||||
propertyName = propertyName.Substring(1);
|
||||
}
|
||||
|
||||
var targetProperty = TryGetProperty(propertyName);
|
||||
|
||||
if (targetProperty == null)
|
||||
{
|
||||
// Skip if the property is not found
|
||||
UnknownList.Add(propertyName);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ignoreSetValue && SetPropertyValue(targetProperty, arg, _instance))
|
||||
{
|
||||
_updatedList.Add(targetProperty);
|
||||
propertyName = string.Empty;
|
||||
}
|
||||
else if (targetProperty.PropertyType == typeof(bool))
|
||||
{
|
||||
// If the arg is a boolean property set it to true.
|
||||
targetProperty.SetValue(_instance, true);
|
||||
|
||||
_updatedList.Add(targetProperty);
|
||||
propertyName = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(propertyName))
|
||||
{
|
||||
UnknownList.Add(propertyName);
|
||||
}
|
||||
}
|
||||
|
||||
private bool SetPropertyValue(
|
||||
PropertyInfo targetProperty,
|
||||
string propertyValueString,
|
||||
object result,
|
||||
ArgumentOptionAttribute optionAttr = null)
|
||||
{
|
||||
if (!targetProperty.PropertyType.IsEnum)
|
||||
{
|
||||
return targetProperty.PropertyType.IsArray
|
||||
? targetProperty.TrySetArray(propertyValueString.Split(optionAttr?.Separator ?? ','), result)
|
||||
: targetProperty.TrySetBasicType(propertyValueString, result);
|
||||
}
|
||||
|
||||
var parsedValue = Enum.Parse(
|
||||
targetProperty.PropertyType,
|
||||
propertyValueString,
|
||||
_settings.CaseInsensitiveEnumValues);
|
||||
|
||||
targetProperty.SetValue(result, Enum.ToObject(targetProperty.PropertyType, parsedValue));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private PropertyInfo TryGetProperty(string propertyName)
|
||||
=> _properties.FirstOrDefault(p =>
|
||||
string.Equals(AttributeCache.DefaultCache.Value.RetrieveOne<ArgumentOptionAttribute>(p)?.LongName, propertyName, _settings.NameComparer) ||
|
||||
string.Equals(AttributeCache.DefaultCache.Value.RetrieveOne<ArgumentOptionAttribute>(p)?.ShortName, propertyName, _settings.NameComparer));
|
||||
}
|
||||
}
|
||||
}
|
57
Swan.Lite/Parsers/ArgumentParser.TypeResolver.cs
Normal file
57
Swan.Lite/Parsers/ArgumentParser.TypeResolver.cs
Normal file
@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Swan.Reflection;
|
||||
|
||||
namespace Swan.Parsers
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides methods to parse command line arguments.
|
||||
/// </summary>
|
||||
public partial class ArgumentParser
|
||||
{
|
||||
private sealed class TypeResolver<T>
|
||||
{
|
||||
private readonly string _selectedVerb;
|
||||
|
||||
private PropertyInfo[]? _properties;
|
||||
|
||||
public TypeResolver(string selectedVerb)
|
||||
{
|
||||
_selectedVerb = selectedVerb;
|
||||
}
|
||||
|
||||
public PropertyInfo[]? Properties => _properties?.Any() == true ? _properties : null;
|
||||
|
||||
public object? GetOptionsObject(T instance)
|
||||
{
|
||||
_properties = PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties<T>(true).ToArray();
|
||||
|
||||
if (!_properties.Any(x => x.GetCustomAttributes(typeof(VerbOptionAttribute), false).Any()))
|
||||
return instance;
|
||||
|
||||
var selectedVerb = string.IsNullOrWhiteSpace(_selectedVerb)
|
||||
? null
|
||||
: _properties.FirstOrDefault(x =>
|
||||
AttributeCache.DefaultCache.Value.RetrieveOne<VerbOptionAttribute>(x).Name == _selectedVerb);
|
||||
|
||||
if (selectedVerb == null) return null;
|
||||
|
||||
var type = instance.GetType();
|
||||
|
||||
var verbProperty = type.GetProperty(selectedVerb.Name);
|
||||
|
||||
if (verbProperty?.GetValue(instance) == null)
|
||||
{
|
||||
var propertyInstance = Activator.CreateInstance(selectedVerb.PropertyType);
|
||||
verbProperty?.SetValue(instance, propertyInstance);
|
||||
}
|
||||
|
||||
_properties = PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties(selectedVerb.PropertyType, true)
|
||||
.ToArray();
|
||||
|
||||
return verbProperty?.GetValue(instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
253
Swan.Lite/Parsers/ArgumentParser.cs
Normal file
253
Swan.Lite/Parsers/ArgumentParser.cs
Normal file
@ -0,0 +1,253 @@
|
||||
using Swan.Reflection;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Swan.Parsers
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides methods to parse command line arguments.
|
||||
/// Based on CommandLine (Copyright 2005-2015 Giacomo Stelluti Scala and Contributors.).
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// The following example shows how to parse CLI arguments into objects.
|
||||
/// <code>
|
||||
/// class Example
|
||||
/// {
|
||||
/// using System;
|
||||
/// using Swan.Parsers;
|
||||
///
|
||||
/// static void Main(string[] args)
|
||||
/// {
|
||||
/// // parse the supplied command-line arguments into the options object
|
||||
/// var res = Runtime.ArgumentParser.ParseArguments(args, out var options);
|
||||
/// }
|
||||
///
|
||||
/// class Options
|
||||
/// {
|
||||
/// [ArgumentOption('v', "verbose", HelpText = "Set verbose mode.")]
|
||||
/// public bool Verbose { get; set; }
|
||||
///
|
||||
/// [ArgumentOption('u', Required = true, HelpText = "Set user name.")]
|
||||
/// public string Username { get; set; }
|
||||
///
|
||||
/// [ArgumentOption('n', "names", Separator = ',',
|
||||
/// Required = true, HelpText = "A list of files separated by a comma")]
|
||||
/// public string[] Files { get; set; }
|
||||
///
|
||||
/// [ArgumentOption('p', "port", DefaultValue = 22, HelpText = "Set port.")]
|
||||
/// public int Port { get; set; }
|
||||
///
|
||||
/// [ArgumentOption("color", DefaultValue = ConsoleColor.Red,
|
||||
/// HelpText = "Set a color.")]
|
||||
/// public ConsoleColor Color { get; set; }
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// The following code describes how to parse CLI verbs.
|
||||
/// <code>
|
||||
/// class Example2
|
||||
/// {
|
||||
/// using Swan;
|
||||
/// using Swan.Parsers;
|
||||
///
|
||||
/// static void Main(string[] args)
|
||||
/// {
|
||||
/// // create an instance of the VerbOptions class
|
||||
/// var options = new VerbOptions();
|
||||
///
|
||||
/// // parse the supplied command-line arguments into the options object
|
||||
/// var res = Runtime.ArgumentParser.ParseArguments(args, options);
|
||||
///
|
||||
/// // if there were no errors parsing
|
||||
/// if (res)
|
||||
/// {
|
||||
/// if(options.Run != null)
|
||||
/// {
|
||||
/// // run verb was selected
|
||||
/// }
|
||||
///
|
||||
/// if(options.Print != null)
|
||||
/// {
|
||||
/// // print verb was selected
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // flush all error messages
|
||||
/// Terminal.Flush();
|
||||
/// }
|
||||
///
|
||||
/// class VerbOptions
|
||||
/// {
|
||||
/// [VerbOption("run", HelpText = "Run verb.")]
|
||||
/// public RunVerbOption Run { get; set; }
|
||||
///
|
||||
/// [VerbOption("print", HelpText = "Print verb.")]
|
||||
/// public PrintVerbOption Print { get; set; }
|
||||
/// }
|
||||
///
|
||||
/// class RunVerbOption
|
||||
/// {
|
||||
/// [ArgumentOption('o', "outdir", HelpText = "Output directory",
|
||||
/// DefaultValue = "", Required = false)]
|
||||
/// public string OutDir { get; set; }
|
||||
/// }
|
||||
///
|
||||
/// class PrintVerbOption
|
||||
/// {
|
||||
/// [ArgumentOption('t', "text", HelpText = "Text to print",
|
||||
/// DefaultValue = "", Required = false)]
|
||||
/// public string Text { get; set; }
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public partial class ArgumentParser
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ArgumentParser"/> class.
|
||||
/// </summary>
|
||||
public ArgumentParser()
|
||||
: this(new ArgumentParserSettings())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ArgumentParser" /> class,
|
||||
/// configurable with <see cref="ArgumentParserSettings" /> using a delegate.
|
||||
/// </summary>
|
||||
/// <param name="parseSettings">The parse settings.</param>
|
||||
public ArgumentParser(ArgumentParserSettings parseSettings)
|
||||
{
|
||||
Settings = parseSettings ?? throw new ArgumentNullException(nameof(parseSettings));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The current.
|
||||
/// </value>
|
||||
public static ArgumentParser Current { get; } = new ArgumentParser();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the instance that implements <see cref="ArgumentParserSettings" /> in use.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The settings.
|
||||
/// </value>
|
||||
public ArgumentParserSettings Settings { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Parses a string array of command line arguments constructing values in an instance of type <typeparamref name="T" />.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the options.</typeparam>
|
||||
/// <param name="args">The arguments.</param>
|
||||
/// <param name="instance">The instance.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if was converted successfully; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// 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.
|
||||
/// </exception>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// The exception that is thrown when a method call is invalid for the object's current state.
|
||||
/// </exception>
|
||||
public bool ParseArguments<T>(IEnumerable<string> args, out T instance)
|
||||
{
|
||||
instance = Activator.CreateInstance<T>();
|
||||
return ParseArguments(args, instance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a string array of command line arguments constructing values in an instance of type <typeparamref name="T" />.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the options.</typeparam>
|
||||
/// <param name="args">The arguments.</param>
|
||||
/// <param name="instance">The instance.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if was converted successfully; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// 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.
|
||||
/// </exception>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// The exception that is thrown when a method call is invalid for the object's current state.
|
||||
/// </exception>
|
||||
public bool ParseArguments<T>(IEnumerable<string> 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<T>(args.FirstOrDefault());
|
||||
var options = typeResolver.GetOptionsObject(instance);
|
||||
|
||||
if (options == null)
|
||||
{
|
||||
ReportUnknownVerb<T>();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeResolver.Properties == null)
|
||||
throw new InvalidOperationException($"Type {typeof(T).Name} is not valid");
|
||||
|
||||
var validator = new Validator(typeResolver.Properties, args, options, Settings);
|
||||
|
||||
if (validator.IsValid())
|
||||
return true;
|
||||
|
||||
ReportIssues(validator);
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void ReportUnknownVerb<T>()
|
||||
{
|
||||
Terminal.WriteLine("No verb was specified", ConsoleColor.Red);
|
||||
Terminal.WriteLine("Valid verbs:", ConsoleColor.Cyan);
|
||||
|
||||
PropertyTypeCache.DefaultCache.Value
|
||||
.RetrieveAllProperties<T>(true)
|
||||
.Select(x => AttributeCache.DefaultCache.Value.RetrieveOne<VerbOptionAttribute>(x))
|
||||
.Where(x => x != null)
|
||||
.ToList()
|
||||
.ForEach(x => Terminal.WriteLine(x.ToString(), ConsoleColor.Cyan));
|
||||
}
|
||||
|
||||
private void ReportIssues(Validator validator)
|
||||
{
|
||||
if (Settings.WriteBanner)
|
||||
Terminal.WriteWelcomeBanner();
|
||||
|
||||
var options = validator.GetPropertiesOptions();
|
||||
|
||||
foreach (var option in options)
|
||||
{
|
||||
Terminal.WriteLine(string.Empty);
|
||||
|
||||
// TODO: If Enum list values
|
||||
var shortName = string.IsNullOrWhiteSpace(option.ShortName) ? string.Empty : $"-{option.ShortName}";
|
||||
var longName = string.IsNullOrWhiteSpace(option.LongName) ? string.Empty : $"--{option.LongName}";
|
||||
var comma = string.IsNullOrWhiteSpace(shortName) || string.IsNullOrWhiteSpace(longName)
|
||||
? string.Empty
|
||||
: ", ";
|
||||
var defaultValue = option.DefaultValue == null ? string.Empty : $"(Default: {option.DefaultValue}) ";
|
||||
|
||||
Terminal.WriteLine($" {shortName}{comma}{longName}\t\t{defaultValue}{option.HelpText}", ConsoleColor.Cyan);
|
||||
}
|
||||
|
||||
Terminal.WriteLine(string.Empty);
|
||||
Terminal.WriteLine(" --help\t\tDisplay this help screen.", ConsoleColor.Cyan);
|
||||
|
||||
if (validator.UnknownList.Any())
|
||||
Terminal.WriteLine($"Unknown arguments: {string.Join(", ", validator.UnknownList)}", ConsoleColor.Red);
|
||||
|
||||
if (validator.RequiredList.Any())
|
||||
Terminal.WriteLine($"Required arguments: {string.Join(", ", validator.RequiredList)}", ConsoleColor.Red);
|
||||
}
|
||||
}
|
||||
}
|
53
Swan.Lite/Parsers/ArgumentParserSettings.cs
Normal file
53
Swan.Lite/Parsers/ArgumentParserSettings.cs
Normal file
@ -0,0 +1,53 @@
|
||||
using System;
|
||||
|
||||
namespace Swan.Parsers
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides settings for <see cref="ArgumentParser"/>.
|
||||
/// Based on CommandLine (Copyright 2005-2015 Giacomo Stelluti Scala and Contributors.).
|
||||
/// </summary>
|
||||
public class ArgumentParserSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether [write banner].
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if [write banner]; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public bool WriteBanner { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether perform case sensitive comparisons.
|
||||
/// Note that case insensitivity only applies to <i>parameters</i>, not the values
|
||||
/// assigned to them (for example, enum parsing).
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if [case sensitive]; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public bool CaseSensitive { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether perform case sensitive comparisons of <i>values</i>.
|
||||
/// Note that case insensitivity only applies to <i>values</i>, not the parameters.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if [case insensitive enum values]; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public bool CaseInsensitiveEnumValues { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> to allow parsing the arguments with different class options that do not have all the arguments.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
public bool IgnoreUnknownArguments { get; set; } = true;
|
||||
|
||||
internal StringComparison NameComparer => CaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
|
||||
}
|
||||
}
|
117
Swan.Lite/Parsers/ExpressionParser.cs
Normal file
117
Swan.Lite/Parsers/ExpressionParser.cs
Normal file
@ -0,0 +1,117 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace Swan.Parsers
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a generic expression parser.
|
||||
/// </summary>
|
||||
public abstract class ExpressionParser
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves the expression.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of expression result.</typeparam>
|
||||
/// <param name="tokens">The tokens.</param>
|
||||
/// <returns>The representation of the expression parsed.</returns>
|
||||
public virtual T ResolveExpression<T>(IEnumerable<Token> tokens) =>
|
||||
ResolveExpression<T>(tokens, System.Globalization.CultureInfo.InvariantCulture);
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the expression.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of expression result.</typeparam>
|
||||
/// <param name="tokens">The tokens.</param>
|
||||
/// <param name="formatProvider">The format provider.</param>
|
||||
/// <returns>The representation of the expression parsed.</returns>
|
||||
public virtual T ResolveExpression<T>(IEnumerable<Token> tokens, IFormatProvider formatProvider)
|
||||
{
|
||||
var conversion = Expression.Convert(Parse(tokens,formatProvider), typeof(T));
|
||||
return Expression.Lambda<Func<T>>(conversion).Compile()();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses the specified tokens.
|
||||
/// </summary>
|
||||
/// <param name="tokens">The tokens.</param>
|
||||
/// <returns>
|
||||
/// The final expression.
|
||||
/// </returns>
|
||||
public virtual Expression Parse(IEnumerable<Token> tokens) =>
|
||||
Parse(tokens, System.Globalization.CultureInfo.InvariantCulture);
|
||||
|
||||
/// <summary>
|
||||
/// Parses the specified tokens.
|
||||
/// </summary>
|
||||
/// <param name="tokens">The tokens.</param>
|
||||
/// <param name="formatProvider">The format provider.</param>
|
||||
/// <returns>
|
||||
/// The final expression.
|
||||
/// </returns>
|
||||
public virtual Expression Parse(IEnumerable<Token> tokens, IFormatProvider formatProvider)
|
||||
{
|
||||
var expressionStack = new List<Stack<Expression>>();
|
||||
|
||||
foreach (var token in tokens)
|
||||
{
|
||||
if (expressionStack.Any() == false)
|
||||
expressionStack.Add(new Stack<Expression>());
|
||||
|
||||
switch (token.Type)
|
||||
{
|
||||
case TokenType.Wall:
|
||||
expressionStack.Add(new Stack<Expression>());
|
||||
break;
|
||||
case TokenType.Number:
|
||||
expressionStack.Last().Push(Expression.Constant(Convert.ToDecimal(token.Value, formatProvider)));
|
||||
break;
|
||||
case TokenType.Variable:
|
||||
ResolveVariable(token.Value, expressionStack.Last());
|
||||
break;
|
||||
case TokenType.String:
|
||||
expressionStack.Last().Push(Expression.Constant(token.Value));
|
||||
break;
|
||||
case TokenType.Operator:
|
||||
ResolveOperator(token.Value, expressionStack.Last());
|
||||
break;
|
||||
case TokenType.Function:
|
||||
ResolveFunction(token.Value, expressionStack.Last());
|
||||
|
||||
if (expressionStack.Count > 1 && expressionStack.Last().Count == 1)
|
||||
{
|
||||
var lastValue = expressionStack.Last().Pop();
|
||||
expressionStack.Remove(expressionStack.Last());
|
||||
expressionStack.Last().Push(lastValue);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return expressionStack.Last().Pop();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the variable.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <param name="expressionStack">The expression stack.</param>
|
||||
public abstract void ResolveVariable(string value, Stack<Expression> expressionStack);
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the operator.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <param name="expressionStack">The expression stack.</param>
|
||||
public abstract void ResolveOperator(string value, Stack<Expression> expressionStack);
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the function.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <param name="expressionStack">The expression stack.</param>
|
||||
public abstract void ResolveFunction(string value, Stack<Expression> expressionStack);
|
||||
}
|
||||
}
|
32
Swan.Lite/Parsers/Operator.cs
Normal file
32
Swan.Lite/Parsers/Operator.cs
Normal file
@ -0,0 +1,32 @@
|
||||
namespace Swan.Parsers
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an operator with precedence.
|
||||
/// </summary>
|
||||
public class Operator
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The name.
|
||||
/// </value>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the precedence.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The precedence.
|
||||
/// </value>
|
||||
public int Precedence { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether [right associative].
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if [right associative]; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public bool RightAssociative { get; set; }
|
||||
}
|
||||
}
|
35
Swan.Lite/Parsers/Token.cs
Normal file
35
Swan.Lite/Parsers/Token.cs
Normal file
@ -0,0 +1,35 @@
|
||||
namespace Swan.Parsers
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a Token structure.
|
||||
/// </summary>
|
||||
public struct Token
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Token"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="type">The type.</param>
|
||||
/// <param name="value">The value.</param>
|
||||
public Token(TokenType type, string value)
|
||||
{
|
||||
Type = type;
|
||||
Value = type == TokenType.Function || type == TokenType.Operator ? value.ToLowerInvariant() : value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The type.
|
||||
/// </value>
|
||||
public TokenType Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The value.
|
||||
/// </value>
|
||||
public string Value { get; }
|
||||
}
|
||||
}
|
48
Swan.Lite/Parsers/TokenType.cs
Normal file
48
Swan.Lite/Parsers/TokenType.cs
Normal file
@ -0,0 +1,48 @@
|
||||
namespace Swan.Parsers
|
||||
{
|
||||
/// <summary>
|
||||
/// Enums the token types.
|
||||
/// </summary>
|
||||
public enum TokenType
|
||||
{
|
||||
/// <summary>
|
||||
/// The number
|
||||
/// </summary>
|
||||
Number,
|
||||
|
||||
/// <summary>
|
||||
/// The string
|
||||
/// </summary>
|
||||
String,
|
||||
|
||||
/// <summary>
|
||||
/// The variable
|
||||
/// </summary>
|
||||
Variable,
|
||||
|
||||
/// <summary>
|
||||
/// The function
|
||||
/// </summary>
|
||||
Function,
|
||||
|
||||
/// <summary>
|
||||
/// The parenthesis
|
||||
/// </summary>
|
||||
Parenthesis,
|
||||
|
||||
/// <summary>
|
||||
/// The operator
|
||||
/// </summary>
|
||||
Operator,
|
||||
|
||||
/// <summary>
|
||||
/// The comma
|
||||
/// </summary>
|
||||
Comma,
|
||||
|
||||
/// <summary>
|
||||
/// The wall, used to specified the end of argument list of the following function
|
||||
/// </summary>
|
||||
Wall,
|
||||
}
|
||||
}
|
361
Swan.Lite/Parsers/Tokenizer.cs
Normal file
361
Swan.Lite/Parsers/Tokenizer.cs
Normal file
@ -0,0 +1,361 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Swan.Parsers
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a generic tokenizer.
|
||||
/// </summary>
|
||||
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<Operator> _operators = new List<Operator>();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Tokenizer"/> class.
|
||||
/// This constructor will use the following default operators:
|
||||
///
|
||||
/// <list type="table">
|
||||
/// <listheader>
|
||||
/// <term>Operator</term>
|
||||
/// <description>Precedence</description>
|
||||
/// </listheader>
|
||||
/// <item>
|
||||
/// <term>=</term>
|
||||
/// <description>1</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term>!=</term>
|
||||
/// <description>1</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term>></term>
|
||||
/// <description>2</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><</term>
|
||||
/// <description>2</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term>>=</term>
|
||||
/// <description>2</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><=</term>
|
||||
/// <description>2</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term>+</term>
|
||||
/// <description>3</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term>&</term>
|
||||
/// <description>3</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term>-</term>
|
||||
/// <description>3</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term>*</term>
|
||||
/// <description>4</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term>(backslash)</term>
|
||||
/// <description>4</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term>/</term>
|
||||
/// <description>4</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term>^</term>
|
||||
/// <description>4</description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
/// <param name="input">The input.</param>
|
||||
protected Tokenizer(string input)
|
||||
{
|
||||
_operators.AddRange(GetDefaultOperators());
|
||||
Tokenize(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Tokenizer" /> class.
|
||||
/// </summary>
|
||||
/// <param name="input">The input.</param>
|
||||
/// <param name="operators">The operators to use.</param>
|
||||
protected Tokenizer(string input, IEnumerable<Operator> operators)
|
||||
{
|
||||
_operators.AddRange(operators);
|
||||
Tokenize(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the tokens.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The tokens.
|
||||
/// </value>
|
||||
public List<Token> Tokens { get; } = new List<Token>();
|
||||
|
||||
/// <summary>
|
||||
/// Validates the input and return the start index for tokenizer.
|
||||
/// </summary>
|
||||
/// <param name="input">The input.</param>
|
||||
/// <param name="startIndex">The start index.</param>
|
||||
/// <returns><c>true</c> if the input is valid, otherwise <c>false</c>.</returns>
|
||||
public abstract bool ValidateInput(string input, out int startIndex);
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the type of the function or member.
|
||||
/// </summary>
|
||||
/// <param name="input">The input.</param>
|
||||
/// <returns>The token type.</returns>
|
||||
public abstract TokenType ResolveFunctionOrMemberType(string input);
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the function or member.
|
||||
/// </summary>
|
||||
/// <param name="input">The input.</param>
|
||||
/// <param name="position">The position.</param>
|
||||
/// <returns><c>true</c> if the input is a valid function or variable, otherwise <c>false</c>.</returns>
|
||||
public virtual bool EvaluateFunctionOrMember(string input, int position) => false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default operators.
|
||||
/// </summary>
|
||||
/// <returns>An array with the operators to use for the tokenizer.</returns>
|
||||
public virtual Operator[] GetDefaultOperators() => new[]
|
||||
{
|
||||
new Operator {Name = "=", Precedence = 1},
|
||||
new Operator {Name = "!=", Precedence = 1},
|
||||
new Operator {Name = ">", Precedence = 2},
|
||||
new Operator {Name = "<", Precedence = 2},
|
||||
new Operator {Name = ">=", Precedence = 2},
|
||||
new Operator {Name = "<=", Precedence = 2},
|
||||
new Operator {Name = "+", Precedence = 3},
|
||||
new Operator {Name = "&", Precedence = 3},
|
||||
new Operator {Name = "-", Precedence = 3},
|
||||
new Operator {Name = "*", Precedence = 4},
|
||||
new Operator {Name = "/", Precedence = 4},
|
||||
new Operator {Name = "\\", Precedence = 4},
|
||||
new Operator {Name = "^", Precedence = 4},
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Shunting the yard.
|
||||
/// </summary>
|
||||
/// <param name="includeFunctionStopper">if set to <c>true</c> [include function stopper] (Token type <c>Wall</c>).</param>
|
||||
/// <returns>
|
||||
/// Enumerable of the token in in.
|
||||
/// </returns>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Wrong token
|
||||
/// or
|
||||
/// Mismatched parenthesis.
|
||||
/// </exception>
|
||||
public virtual IEnumerable<Token> ShuntingYard(bool includeFunctionStopper = true)
|
||||
{
|
||||
var stack = new Stack<Token>();
|
||||
|
||||
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<string, TokenType> tokenTypeEvaluation,
|
||||
Func<char, bool> 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 };
|
||||
}
|
||||
}
|
40
Swan.Lite/Parsers/VerbOptionAttribute.cs
Normal file
40
Swan.Lite/Parsers/VerbOptionAttribute.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using System;
|
||||
|
||||
namespace Swan.Parsers
|
||||
{
|
||||
/// <summary>
|
||||
/// Models a verb option.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public sealed class VerbOptionAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="VerbOptionAttribute" /> class.
|
||||
/// </summary>
|
||||
/// <param name="name">The name.</param>
|
||||
/// <exception cref="ArgumentNullException">name.</exception>
|
||||
public VerbOptionAttribute(string name)
|
||||
{
|
||||
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the verb option.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Name.
|
||||
/// </value>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a short description of this command line verb. Usually a sentence summary.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The help text.
|
||||
/// </value>
|
||||
public string HelpText { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => $" {Name}\t\t{HelpText}";
|
||||
}
|
||||
}
|
188
Swan.Lite/Reflection/AttributeCache.cs
Normal file
188
Swan.Lite/Reflection/AttributeCache.cs
Normal file
@ -0,0 +1,188 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Swan.Reflection
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class AttributeCache
|
||||
{
|
||||
private readonly Lazy<ConcurrentDictionary<Tuple<object, Type>, IEnumerable<object>>> _data =
|
||||
new Lazy<ConcurrentDictionary<Tuple<object, Type>, IEnumerable<object>>>(() =>
|
||||
new ConcurrentDictionary<Tuple<object, Type>, IEnumerable<object>>(), true);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AttributeCache"/> class.
|
||||
/// </summary>
|
||||
/// <param name="propertyCache">The property cache object.</param>
|
||||
public AttributeCache(PropertyTypeCache? propertyCache = null)
|
||||
{
|
||||
PropertyTypeCache = propertyCache ?? PropertyTypeCache.DefaultCache.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default cache.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The default cache.
|
||||
/// </value>
|
||||
public static Lazy<AttributeCache> DefaultCache { get; } = new Lazy<AttributeCache>(() => new AttributeCache());
|
||||
|
||||
/// <summary>
|
||||
/// A PropertyTypeCache object for caching properties and their attributes.
|
||||
/// </summary>
|
||||
public PropertyTypeCache PropertyTypeCache { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether [contains] [the specified member].
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the attribute to be retrieved.</typeparam>
|
||||
/// <param name="member">The member.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if [contains] [the specified member]; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public bool Contains<T>(MemberInfo member) => _data.Value.ContainsKey(new Tuple<object, Type>(member, typeof(T)));
|
||||
|
||||
/// <summary>
|
||||
/// Gets specific attributes from a member constrained to an attribute.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the attribute to be retrieved.</typeparam>
|
||||
/// <param name="member">The member.</param>
|
||||
/// <param name="inherit"><c>true</c> to inspect the ancestors of element; otherwise, <c>false</c>.</param>
|
||||
/// <returns>An array of the attributes stored for the specified type.</returns>
|
||||
public IEnumerable<object> Retrieve<T>(MemberInfo member, bool inherit = false)
|
||||
where T : Attribute
|
||||
{
|
||||
if (member == null)
|
||||
throw new ArgumentNullException(nameof(member));
|
||||
|
||||
return Retrieve(new Tuple<object, Type>(member, typeof(T)), t => member.GetCustomAttributes<T>(inherit));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all attributes of a specific type from a member.
|
||||
/// </summary>
|
||||
/// <param name="member">The member.</param>
|
||||
/// <param name="type">The attribute type.</param>
|
||||
/// <param name="inherit"><c>true</c> to inspect the ancestors of element; otherwise, <c>false</c>.</param>
|
||||
/// <returns>An array of the attributes stored for the specified type.</returns>
|
||||
public IEnumerable<object> 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<object, Type>(member, type),
|
||||
t => member.GetCustomAttributes(type, inherit));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets one attribute of a specific type from a member.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The attribute type.</typeparam>
|
||||
/// <param name="member">The member.</param>
|
||||
/// <param name="inherit"><c>true</c> to inspect the ancestors of element; otherwise, <c>false</c>.</param>
|
||||
/// <returns>An attribute stored for the specified type.</returns>
|
||||
public T RetrieveOne<T>(MemberInfo member, bool inherit = false)
|
||||
where T : Attribute
|
||||
{
|
||||
if (member == null)
|
||||
return default;
|
||||
|
||||
var attr = Retrieve(
|
||||
new Tuple<object, Type>(member, typeof(T)),
|
||||
t => member.GetCustomAttributes(typeof(T), inherit));
|
||||
|
||||
return ConvertToAttribute<T>(attr);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets one attribute of a specific type from a generic type.
|
||||
/// </summary>
|
||||
/// <typeparam name="TAttribute">The type of the attribute.</typeparam>
|
||||
/// <typeparam name="T">The type to retrieve the attribute.</typeparam>
|
||||
/// <param name="inherit">if set to <c>true</c> [inherit].</param>
|
||||
/// <returns>An attribute stored for the specified type.</returns>
|
||||
public TAttribute RetrieveOne<TAttribute, T>(bool inherit = false)
|
||||
where TAttribute : Attribute
|
||||
{
|
||||
var attr = Retrieve(
|
||||
new Tuple<object, Type>(typeof(T), typeof(TAttribute)),
|
||||
t => typeof(T).GetCustomAttributes(typeof(TAttribute), inherit));
|
||||
|
||||
return ConvertToAttribute<TAttribute>(attr);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all properties an their attributes of a given type constrained to only attributes.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the attribute to retrieve.</typeparam>
|
||||
/// <param name="type">The type of the object.</param>
|
||||
/// <param name="inherit"><c>true</c> to inspect the ancestors of element; otherwise, <c>false</c>.</param>
|
||||
/// <returns>A dictionary of the properties and their attributes stored for the specified type.</returns>
|
||||
public Dictionary<PropertyInfo, IEnumerable<object>> Retrieve<T>(Type type, bool inherit = false)
|
||||
where T : Attribute =>
|
||||
PropertyTypeCache.RetrieveAllProperties(type, true)
|
||||
.ToDictionary(x => x, x => Retrieve<T>(x, inherit));
|
||||
|
||||
/// <summary>
|
||||
/// Gets all properties and their attributes of a given type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The object type used to extract the properties from.</typeparam>
|
||||
/// <typeparam name="TAttribute">The type of the attribute.</typeparam>
|
||||
/// <param name="inherit"><c>true</c> to inspect the ancestors of element; otherwise, <c>false</c>.</param>
|
||||
/// <returns>
|
||||
/// A dictionary of the properties and their attributes stored for the specified type.
|
||||
/// </returns>
|
||||
public Dictionary<PropertyInfo, IEnumerable<object>> RetrieveFromType<T, TAttribute>(bool inherit = false)
|
||||
=> RetrieveFromType<T>(typeof(TAttribute), inherit);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all properties and their attributes of a given type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The object type used to extract the properties from.</typeparam>
|
||||
/// <param name="attributeType">Type of the attribute.</param>
|
||||
/// <param name="inherit"><c>true</c> to inspect the ancestors of element; otherwise, <c>false</c>.</param>
|
||||
/// <returns>
|
||||
/// A dictionary of the properties and their attributes stored for the specified type.
|
||||
/// </returns>
|
||||
public Dictionary<PropertyInfo, IEnumerable<object>> RetrieveFromType<T>(Type attributeType, bool inherit = false)
|
||||
{
|
||||
if (attributeType == null)
|
||||
throw new ArgumentNullException(nameof(attributeType));
|
||||
|
||||
return PropertyTypeCache.RetrieveAllProperties<T>(true)
|
||||
.ToDictionary(x => x, x => Retrieve(x, attributeType, inherit));
|
||||
}
|
||||
|
||||
private static T ConvertToAttribute<T>(IEnumerable<object> attr)
|
||||
where T : Attribute
|
||||
{
|
||||
if (attr?.Any() != true)
|
||||
return default;
|
||||
|
||||
return attr.Count() == 1
|
||||
? (T) Convert.ChangeType(attr.First(), typeof(T))
|
||||
: throw new AmbiguousMatchException("Multiple custom attributes of the same type found.");
|
||||
}
|
||||
|
||||
private IEnumerable<object> Retrieve(Tuple<object, Type> key, Func<Tuple<object, Type>, IEnumerable<object>> factory)
|
||||
{
|
||||
if (factory == null)
|
||||
throw new ArgumentNullException(nameof(factory));
|
||||
|
||||
return _data.Value.GetOrAdd(key, k => factory.Invoke(k).Where(item => item != null));
|
||||
}
|
||||
}
|
||||
}
|
51
Swan.Lite/Reflection/ConstructorTypeCache.cs
Normal file
51
Swan.Lite/Reflection/ConstructorTypeCache.cs
Normal file
@ -0,0 +1,51 @@
|
||||
using Swan.Reflection;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Swan.Lite.Reflection
|
||||
{
|
||||
/// <summary>
|
||||
/// A thread-safe cache of constructors belonging to a given type.
|
||||
/// </summary>
|
||||
public class ConstructorTypeCache : TypeCache<Tuple<ConstructorInfo, ParameterInfo[]>>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the default cache.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The default cache.
|
||||
/// </value>
|
||||
public static Lazy<ConstructorTypeCache> DefaultCache { get; } =
|
||||
new Lazy<ConstructorTypeCache>(() => new ConstructorTypeCache());
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves all constructors order by the number of parameters ascending.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to inspect.</typeparam>
|
||||
/// <param name="includeNonPublic">if set to <c>true</c> [include non public].</param>
|
||||
/// <returns>
|
||||
/// A collection with all the constructors in the given type.
|
||||
/// </returns>
|
||||
public IEnumerable<Tuple<ConstructorInfo, ParameterInfo[]>> RetrieveAllConstructors<T>(bool includeNonPublic = false)
|
||||
=> Retrieve<T>(GetConstructors(includeNonPublic));
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves all constructors order by the number of parameters ascending.
|
||||
/// </summary>
|
||||
/// <param name="type">The type.</param>
|
||||
/// <param name="includeNonPublic">if set to <c>true</c> [include non public].</param>
|
||||
/// <returns>
|
||||
/// A collection with all the constructors in the given type.
|
||||
/// </returns>
|
||||
public IEnumerable<Tuple<ConstructorInfo, ParameterInfo[]>> RetrieveAllConstructors(Type type, bool includeNonPublic = false)
|
||||
=> Retrieve(type, GetConstructors(includeNonPublic));
|
||||
|
||||
private static Func<Type, IEnumerable<Tuple<ConstructorInfo, ParameterInfo[]>>> GetConstructors(bool includeNonPublic)
|
||||
=> t => t.GetConstructors(includeNonPublic ? BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance : BindingFlags.Public | BindingFlags.Instance)
|
||||
.Select(x => Tuple.Create(x, x.GetParameters()))
|
||||
.OrderBy(x => x.Item2.Length)
|
||||
.ToList();
|
||||
}
|
||||
}
|
107
Swan.Lite/Reflection/ExtendedPropertyInfo.cs
Normal file
107
Swan.Lite/Reflection/ExtendedPropertyInfo.cs
Normal file
@ -0,0 +1,107 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using Swan.Configuration;
|
||||
|
||||
namespace Swan.Reflection
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a Property object from a Object Reflection Property with extended values.
|
||||
/// </summary>
|
||||
public class ExtendedPropertyInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExtendedPropertyInfo"/> class.
|
||||
/// </summary>
|
||||
/// <param name="propertyInfo">The property information.</param>
|
||||
public ExtendedPropertyInfo(PropertyInfo propertyInfo)
|
||||
{
|
||||
if (propertyInfo == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(propertyInfo));
|
||||
}
|
||||
|
||||
Property = propertyInfo.Name;
|
||||
DataType = propertyInfo.PropertyType.Name;
|
||||
|
||||
foreach (PropertyDisplayAttribute display in AttributeCache.DefaultCache.Value.Retrieve<PropertyDisplayAttribute>(propertyInfo, true))
|
||||
{
|
||||
Name = display.Name;
|
||||
Description = display.Description;
|
||||
GroupName = display.GroupName;
|
||||
DefaultValue = display.DefaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the property.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The property.
|
||||
/// </value>
|
||||
public string Property { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of the data.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The type of the data.
|
||||
/// </value>
|
||||
public string DataType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The value.
|
||||
/// </value>
|
||||
public object Value { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default value.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The default value.
|
||||
/// </value>
|
||||
public object DefaultValue { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The name.
|
||||
/// </value>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the description.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The description.
|
||||
/// </value>
|
||||
public string Description { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the group.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The name of the group.
|
||||
/// </value>
|
||||
public string GroupName { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a Property object from a Object Reflection Property with extended values.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the object.</typeparam>
|
||||
public class ExtendedPropertyInfo<T> : ExtendedPropertyInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExtendedPropertyInfo{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="property">The property.</param>
|
||||
public ExtendedPropertyInfo(string property)
|
||||
: base(typeof(T).GetProperty(property))
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
267
Swan.Lite/Reflection/ExtendedTypeInfo.cs
Normal file
267
Swan.Lite/Reflection/ExtendedTypeInfo.cs
Normal file
@ -0,0 +1,267 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Swan.Reflection
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class ExtendedTypeInfo
|
||||
{
|
||||
private const string TryParseMethodName = nameof(byte.TryParse);
|
||||
private const string ToStringMethodName = nameof(ToString);
|
||||
|
||||
private static readonly Type[] NumericTypes =
|
||||
{
|
||||
typeof(byte),
|
||||
typeof(sbyte),
|
||||
typeof(decimal),
|
||||
typeof(double),
|
||||
typeof(float),
|
||||
typeof(int),
|
||||
typeof(uint),
|
||||
typeof(long),
|
||||
typeof(ulong),
|
||||
typeof(short),
|
||||
typeof(ushort),
|
||||
};
|
||||
|
||||
private readonly ParameterInfo[]? _tryParseParameters;
|
||||
private readonly int _toStringArgumentLength;
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExtendedTypeInfo"/> class.
|
||||
/// </summary>
|
||||
/// <param name="t">The t.</param>
|
||||
public ExtendedTypeInfo(Type t)
|
||||
{
|
||||
Type = t ?? throw new ArgumentNullException(nameof(t));
|
||||
IsNullableValueType = Type.IsGenericType
|
||||
&& Type.GetGenericTypeDefinition() == typeof(Nullable<>);
|
||||
|
||||
IsValueType = t.IsValueType;
|
||||
|
||||
UnderlyingType = IsNullableValueType ?
|
||||
new NullableConverter(Type).UnderlyingType :
|
||||
Type;
|
||||
|
||||
IsNumeric = NumericTypes.Contains(UnderlyingType);
|
||||
|
||||
// Extract the TryParse method info
|
||||
try
|
||||
{
|
||||
TryParseMethodInfo = UnderlyingType.GetMethod(TryParseMethodName,
|
||||
new[] { typeof(string), typeof(NumberStyles), typeof(IFormatProvider), UnderlyingType.MakeByRefType() }) ??
|
||||
UnderlyingType.GetMethod(TryParseMethodName,
|
||||
new[] { typeof(string), UnderlyingType.MakeByRefType() });
|
||||
|
||||
_tryParseParameters = TryParseMethodInfo?.GetParameters();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
||||
// Extract the ToString method Info
|
||||
try
|
||||
{
|
||||
ToStringMethodInfo = UnderlyingType.GetMethod(ToStringMethodName,
|
||||
new[] { typeof(IFormatProvider) }) ??
|
||||
UnderlyingType.GetMethod(ToStringMethodName,
|
||||
Array.Empty<Type>());
|
||||
|
||||
_toStringArgumentLength = ToStringMethodInfo?.GetParameters().Length ?? 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type this extended info class provides for.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The type.
|
||||
/// </value>
|
||||
public Type Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the type is a nullable value type.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if this instance is nullable value type; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public bool IsNullableValueType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the type or underlying type is numeric.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if this instance is numeric; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public bool IsNumeric { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the type is value type.
|
||||
/// Nullable value types have this property set to False.
|
||||
/// </summary>
|
||||
public bool IsValueType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The type of the underlying.
|
||||
/// </value>
|
||||
public Type UnderlyingType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the try parse method information. If the type does not contain
|
||||
/// a suitable TryParse static method, it will return null.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The try parse method information.
|
||||
/// </value>
|
||||
public MethodInfo TryParseMethodInfo { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ToString method info
|
||||
/// It will prefer the overload containing the IFormatProvider argument.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// To string method information.
|
||||
/// </value>
|
||||
public MethodInfo ToStringMethodInfo { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the type contains a suitable TryParse method.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if this instance can parse natively; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public bool CanParseNatively => TryParseMethodInfo != null;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="s">The s.</param>
|
||||
/// <param name="result">The result.</param>
|
||||
/// <returns><c>true</c> if parse was converted successfully; otherwise, <c>false</c>.</returns>
|
||||
public bool TryParse(string s, out object? result)
|
||||
{
|
||||
result = Type.GetDefault();
|
||||
|
||||
try
|
||||
{
|
||||
if (Type == typeof(string))
|
||||
{
|
||||
result = Convert.ChangeType(s, Type, CultureInfo.InvariantCulture);
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((IsNullableValueType && string.IsNullOrEmpty(s)) || !CanParseNatively)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Build the arguments of the TryParse method
|
||||
var dynamicArguments = new List<object?> { s };
|
||||
|
||||
for (var pi = 1; pi < _tryParseParameters.Length - 1; pi++)
|
||||
{
|
||||
var argInfo = _tryParseParameters[pi];
|
||||
if (argInfo.ParameterType == typeof(IFormatProvider))
|
||||
dynamicArguments.Add(CultureInfo.InvariantCulture);
|
||||
else if (argInfo.ParameterType == typeof(NumberStyles))
|
||||
dynamicArguments.Add(NumberStyles.Any);
|
||||
else
|
||||
dynamicArguments.Add(null);
|
||||
}
|
||||
|
||||
dynamicArguments.Add(null);
|
||||
var parseArguments = dynamicArguments.ToArray();
|
||||
|
||||
if ((bool) TryParseMethodInfo.Invoke(null, parseArguments))
|
||||
{
|
||||
result = parseArguments[parseArguments.Length - 1];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts this instance to its string representation,
|
||||
/// trying to use the CultureInfo.InvariantCulture
|
||||
/// IFormat provider if the overload is available.
|
||||
/// </summary>
|
||||
/// <param name="instance">The instance.</param>
|
||||
/// <returns>A <see cref="System.String" /> that represents the current object.</returns>
|
||||
public string ToStringInvariant(object instance)
|
||||
{
|
||||
if (instance == null)
|
||||
return string.Empty;
|
||||
|
||||
return _toStringArgumentLength != 1
|
||||
? instance.ToString()
|
||||
: ToStringMethodInfo.Invoke(instance, new object[] {CultureInfo.InvariantCulture}) as string ?? string.Empty;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of extended type information.</typeparam>
|
||||
public class ExtendedTypeInfo<T> : ExtendedTypeInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExtendedTypeInfo{T}"/> class.
|
||||
/// </summary>
|
||||
public ExtendedTypeInfo()
|
||||
: base(typeof(T))
|
||||
{
|
||||
// placeholder
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts this instance to its string representation,
|
||||
/// trying to use the CultureInfo.InvariantCulture
|
||||
/// IFormat provider if the overload is available.
|
||||
/// </summary>
|
||||
/// <param name="instance">The instance.</param>
|
||||
/// <returns>A <see cref="System.String" /> that represents the current object.</returns>
|
||||
public string ToStringInvariant(T instance) => base.ToStringInvariant(instance);
|
||||
}
|
||||
}
|
22
Swan.Lite/Reflection/IPropertyProxy.cs
Normal file
22
Swan.Lite/Reflection/IPropertyProxy.cs
Normal file
@ -0,0 +1,22 @@
|
||||
namespace Swan.Reflection
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a generic interface to store getters and setters.
|
||||
/// </summary>
|
||||
public interface IPropertyProxy
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the property value via a stored delegate.
|
||||
/// </summary>
|
||||
/// <param name="instance">The instance.</param>
|
||||
/// <returns>The property value.</returns>
|
||||
object GetValue(object instance);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the property value via a stored delegate.
|
||||
/// </summary>
|
||||
/// <param name="instance">The instance.</param>
|
||||
/// <param name="value">The value.</param>
|
||||
void SetValue(object instance, object value);
|
||||
}
|
||||
}
|
117
Swan.Lite/Reflection/MethodInfoCache.cs
Normal file
117
Swan.Lite/Reflection/MethodInfoCache.cs
Normal file
@ -0,0 +1,117 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Swan.Reflection
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a Method Info Cache.
|
||||
/// </summary>
|
||||
public class MethodInfoCache : ConcurrentDictionary<string, MethodInfo>
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of type.</typeparam>
|
||||
/// <param name="name">The name.</param>
|
||||
/// <param name="alias">The alias.</param>
|
||||
/// <param name="types">The types.</param>
|
||||
/// <returns>
|
||||
/// The cached MethodInfo.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">name
|
||||
/// or
|
||||
/// factory.</exception>
|
||||
public MethodInfo Retrieve<T>(string name, string alias, params Type[] types)
|
||||
=> Retrieve(typeof(T), name, alias, types);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the specified name.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of type.</typeparam>
|
||||
/// <param name="name">The name.</param>
|
||||
/// <param name="types">The types.</param>
|
||||
/// <returns>
|
||||
/// The cached MethodInfo.
|
||||
/// </returns>
|
||||
public MethodInfo Retrieve<T>(string name, params Type[] types)
|
||||
=> Retrieve(typeof(T), name, name, types);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the specified type.
|
||||
/// </summary>
|
||||
/// <param name="type">The type.</param>
|
||||
/// <param name="name">The name.</param>
|
||||
/// <param name="types">The types.</param>
|
||||
/// <returns>
|
||||
/// An array of the properties stored for the specified type.
|
||||
/// </returns>
|
||||
public MethodInfo Retrieve(Type type, string name, params Type[] types)
|
||||
=> Retrieve(type, name, name, types);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the specified type.
|
||||
/// </summary>
|
||||
/// <param name="type">The type.</param>
|
||||
/// <param name="name">The name.</param>
|
||||
/// <param name="alias">The alias.</param>
|
||||
/// <param name="types">The types.</param>
|
||||
/// <returns>
|
||||
/// The cached MethodInfo.
|
||||
/// </returns>
|
||||
public MethodInfo Retrieve(Type type, string name, string alias, params Type[] types)
|
||||
{
|
||||
if (type == null)
|
||||
throw new ArgumentNullException(nameof(type));
|
||||
|
||||
if (alias == null)
|
||||
throw new ArgumentNullException(nameof(alias));
|
||||
|
||||
if (name == null)
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
|
||||
return GetOrAdd(
|
||||
alias,
|
||||
x => type.GetMethod(name, types ?? Array.Empty<Type>()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the specified name.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of type.</typeparam>
|
||||
/// <param name="name">The name.</param>
|
||||
/// <returns>
|
||||
/// The cached MethodInfo.
|
||||
/// </returns>
|
||||
public MethodInfo Retrieve<T>(string name)
|
||||
=> Retrieve(typeof(T), name);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the specified type.
|
||||
/// </summary>
|
||||
/// <param name="type">The type.</param>
|
||||
/// <param name="name">The name.</param>
|
||||
/// <returns>
|
||||
/// The cached MethodInfo.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// type
|
||||
/// or
|
||||
/// name.
|
||||
/// </exception>
|
||||
public MethodInfo Retrieve(Type type, string name)
|
||||
{
|
||||
if (type == null)
|
||||
throw new ArgumentNullException(nameof(type));
|
||||
|
||||
if (name == null)
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
|
||||
return GetOrAdd(
|
||||
name,
|
||||
type.GetMethod);
|
||||
}
|
||||
}
|
||||
}
|
47
Swan.Lite/Reflection/PropertyProxy.cs
Normal file
47
Swan.Lite/Reflection/PropertyProxy.cs
Normal file
@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Swan.Reflection
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a generic class to store getters and setters.
|
||||
/// </summary>
|
||||
/// <typeparam name="TClass">The type of the class.</typeparam>
|
||||
/// <typeparam name="TProperty">The type of the property.</typeparam>
|
||||
/// <seealso cref="IPropertyProxy" />
|
||||
public sealed class PropertyProxy<TClass, TProperty> : IPropertyProxy
|
||||
where TClass : class
|
||||
{
|
||||
private readonly Func<TClass, TProperty> _getter;
|
||||
private readonly Action<TClass, TProperty> _setter;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PropertyProxy{TClass, TProperty}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="property">The property.</param>
|
||||
public PropertyProxy(PropertyInfo property)
|
||||
{
|
||||
if (property == null)
|
||||
throw new ArgumentNullException(nameof(property));
|
||||
|
||||
var getterInfo = property.GetGetMethod(false);
|
||||
if (getterInfo != null)
|
||||
_getter = (Func<TClass, TProperty>)Delegate.CreateDelegate(typeof(Func<TClass, TProperty>), getterInfo);
|
||||
|
||||
var setterInfo = property.GetSetMethod(false);
|
||||
if (setterInfo != null)
|
||||
_setter = (Action<TClass, TProperty>)Delegate.CreateDelegate(typeof(Action<TClass, TProperty>), setterInfo);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
object IPropertyProxy.GetValue(object instance) =>
|
||||
_getter(instance as TClass);
|
||||
|
||||
/// <inheritdoc />
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
void IPropertyProxy.SetValue(object instance, object value) =>
|
||||
_setter(instance as TClass, (TProperty)value);
|
||||
}
|
||||
}
|
74
Swan.Lite/Reflection/PropertyTypeCache.cs
Normal file
74
Swan.Lite/Reflection/PropertyTypeCache.cs
Normal file
@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Swan.Reflection
|
||||
{
|
||||
/// <summary>
|
||||
/// A thread-safe cache of properties belonging to a given type.
|
||||
/// </summary>
|
||||
public class PropertyTypeCache : TypeCache<PropertyInfo>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the default cache.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The default cache.
|
||||
/// </value>
|
||||
public static Lazy<PropertyTypeCache> DefaultCache { get; } = new Lazy<PropertyTypeCache>(() => new PropertyTypeCache());
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves all properties.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to inspect.</typeparam>
|
||||
/// <param name="onlyPublic">if set to <c>true</c> [only public].</param>
|
||||
/// <returns>
|
||||
/// A collection with all the properties in the given type.
|
||||
/// </returns>
|
||||
public IEnumerable<PropertyInfo> RetrieveAllProperties<T>(bool onlyPublic = false)
|
||||
=> Retrieve<T>(onlyPublic ? GetAllPublicPropertiesFunc() : GetAllPropertiesFunc());
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves all properties.
|
||||
/// </summary>
|
||||
/// <param name="type">The type.</param>
|
||||
/// <param name="onlyPublic">if set to <c>true</c> [only public].</param>
|
||||
/// <returns>
|
||||
/// A collection with all the properties in the given type.
|
||||
/// </returns>
|
||||
public IEnumerable<PropertyInfo> RetrieveAllProperties(Type type, bool onlyPublic = false)
|
||||
=> Retrieve(type, onlyPublic ? GetAllPublicPropertiesFunc() : GetAllPropertiesFunc());
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the filtered properties.
|
||||
/// </summary>
|
||||
/// <param name="type">The type.</param>
|
||||
/// <param name="onlyPublic">if set to <c>true</c> [only public].</param>
|
||||
/// <param name="filter">The filter.</param>
|
||||
/// <returns>
|
||||
/// A collection with all the properties in the given type.
|
||||
/// </returns>
|
||||
public IEnumerable<PropertyInfo> RetrieveFilteredProperties(
|
||||
Type type,
|
||||
bool onlyPublic,
|
||||
Func<PropertyInfo, bool> filter)
|
||||
=> Retrieve(type,
|
||||
onlyPublic ? GetAllPublicPropertiesFunc(filter) : GetAllPropertiesFunc(filter));
|
||||
|
||||
private static Func<Type, IEnumerable<PropertyInfo>> GetAllPropertiesFunc(
|
||||
Func<PropertyInfo, bool> filter = null)
|
||||
=> GetPropertiesFunc(
|
||||
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
|
||||
filter);
|
||||
|
||||
private static Func<Type, IEnumerable<PropertyInfo>> GetAllPublicPropertiesFunc(
|
||||
Func<PropertyInfo, bool> filter = null)
|
||||
=> GetPropertiesFunc(BindingFlags.Public | BindingFlags.Instance, filter);
|
||||
|
||||
private static Func<Type, IEnumerable<PropertyInfo>> GetPropertiesFunc(BindingFlags flags,
|
||||
Func<PropertyInfo, bool> filter = null)
|
||||
=> t => t.GetProperties(flags)
|
||||
.Where(filter ?? (p => p.CanRead || p.CanWrite));
|
||||
}
|
||||
}
|
78
Swan.Lite/Reflection/TypeCache.cs
Normal file
78
Swan.Lite/Reflection/TypeCache.cs
Normal file
@ -0,0 +1,78 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Swan.Collections;
|
||||
|
||||
namespace Swan.Reflection
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of Member to be cached.</typeparam>
|
||||
public abstract class TypeCache<T> : CollectionCacheRepository<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether the cache contains the specified type.
|
||||
/// </summary>
|
||||
/// <typeparam name="TOut">The type of the out.</typeparam>
|
||||
/// <returns>
|
||||
/// <c>true</c> if [contains]; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public bool Contains<TOut>() => ContainsKey(typeof(TOut));
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <typeparam name="TOut">The type of the out.</typeparam>
|
||||
/// <param name="factory">The factory.</param>
|
||||
/// <returns>An array of the properties stored for the specified type.</returns>
|
||||
public IEnumerable<T> Retrieve<TOut>(Func<Type, IEnumerable<T>> factory)
|
||||
=> Retrieve(typeof(TOut), factory);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class FieldTypeCache : TypeCache<FieldInfo>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the default cache.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The default cache.
|
||||
/// </value>
|
||||
public static Lazy<FieldTypeCache> DefaultCache { get; } = new Lazy<FieldTypeCache>(() => new FieldTypeCache());
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves all fields.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to inspect.</typeparam>
|
||||
/// <returns>
|
||||
/// A collection with all the fields in the given type.
|
||||
/// </returns>
|
||||
public IEnumerable<FieldInfo> RetrieveAllFields<T>()
|
||||
=> Retrieve<T>(GetAllFieldsFunc());
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves all fields.
|
||||
/// </summary>
|
||||
/// <param name="type">The type.</param>
|
||||
/// <returns>
|
||||
/// A collection with all the fields in the given type.
|
||||
/// </returns>
|
||||
public IEnumerable<FieldInfo> RetrieveAllFields(Type type)
|
||||
=> Retrieve(type, GetAllFieldsFunc());
|
||||
|
||||
private static Func<Type, IEnumerable<FieldInfo>> GetAllFieldsFunc()
|
||||
=> t => t.GetFields(BindingFlags.Public | BindingFlags.Instance);
|
||||
}
|
||||
}
|
59
Swan.Lite/SingletonBase.cs
Normal file
59
Swan.Lite/SingletonBase.cs
Normal file
@ -0,0 +1,59 @@
|
||||
using System;
|
||||
|
||||
namespace Swan
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a singleton pattern abstract class.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of class.</typeparam>
|
||||
public abstract class SingletonBase<T> : IDisposable
|
||||
where T : class
|
||||
{
|
||||
/// <summary>
|
||||
/// The static, singleton instance reference.
|
||||
/// </summary>
|
||||
protected static readonly Lazy<T> LazyInstance = new Lazy<T>(
|
||||
valueFactory: () => Activator.CreateInstance(typeof(T), true) as T,
|
||||
isThreadSafe: true);
|
||||
|
||||
private bool _isDisposing; // To detect redundant calls
|
||||
|
||||
/// <summary>
|
||||
/// Gets the instance that this singleton represents.
|
||||
/// If the instance is null, it is constructed and assigned when this member is accessed.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The instance.
|
||||
/// </value>
|
||||
public static T Instance => LazyInstance.Value;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() => Dispose(true);
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// Call the GC.SuppressFinalize if you override this method and use
|
||||
/// a non-default class finalizer (destructor).
|
||||
/// </summary>
|
||||
/// <param name="disposeManaged"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
76
Swan.Lite/StringConversionException.cs
Normal file
76
Swan.Lite/StringConversionException.cs
Normal file
@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace Swan
|
||||
{
|
||||
/// <summary>
|
||||
/// The exception that is thrown when a conversion from a string to a
|
||||
/// specified type fails.
|
||||
/// </summary>
|
||||
/// <seealso cref="FromString" />
|
||||
[Serializable]
|
||||
public class StringConversionException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StringConversionException"/> class.
|
||||
/// </summary>
|
||||
public StringConversionException()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StringConversionException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message that explains the reason for the exception.</param>
|
||||
public StringConversionException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StringConversionException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message that explains the reason for the exception.</param>
|
||||
/// <param name="innerException">The exception that is the cause of the current exception,
|
||||
/// or <see langword="null" /> if no inner exception is specified.</param>
|
||||
public StringConversionException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StringConversionException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="type">The desired resulting type of the attempted conversion.</param>
|
||||
public StringConversionException(Type type)
|
||||
: base(BuildStandardMessageForType(type))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StringConversionException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="type">The desired resulting type of the attempted conversion.</param>
|
||||
/// <param name="innerException">The exception that is the cause of the current exception,
|
||||
/// or <see langword="null" /> if no inner exception is specified.</param>
|
||||
public StringConversionException(Type type, Exception innerException)
|
||||
: base(BuildStandardMessageForType(type), innerException)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StringConversionException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="info">The <see cref="SerializationInfo" /> that holds the serialized object data
|
||||
/// about the exception being thrown.</param>
|
||||
/// <param name="context">The <see cref="StreamingContext" /> that contains contextual information
|
||||
/// about the source or destination.</param>
|
||||
protected StringConversionException(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context)
|
||||
{
|
||||
}
|
||||
|
||||
private static string BuildStandardMessageForType(Type type)
|
||||
=> $"Cannot convert a string to an instance of {type.FullName}";
|
||||
}
|
||||
}
|
30
Swan.Lite/StructEndiannessAttribute.cs
Normal file
30
Swan.Lite/StructEndiannessAttribute.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using System;
|
||||
|
||||
namespace Swan
|
||||
{
|
||||
/// <summary>
|
||||
/// An attribute used to help conversion structs back and forth into arrays of bytes via
|
||||
/// extension methods included in this library ToStruct and ToBytes.
|
||||
/// </summary>
|
||||
/// <seealso cref="Attribute" />
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Struct)]
|
||||
public class StructEndiannessAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StructEndiannessAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="endianness">The endianness.</param>
|
||||
public StructEndiannessAttribute(Endianness endianness)
|
||||
{
|
||||
Endianness = endianness;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the endianness.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The endianness.
|
||||
/// </value>
|
||||
public Endianness Endianness { get; }
|
||||
}
|
||||
}
|
18
Swan.Lite/Swan.Lite.csproj
Normal file
18
Swan.Lite/Swan.Lite.csproj
Normal file
@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>Repeating code and reinventing the wheel is generally considered bad practice. At Unosquare we are committed to beautiful code and great software. Swan is a collection of classes and extension methods that we and other good developers have developed and evolved over the years. We found ourselves copying and pasting the same code for every project every time we started it. We decide to kill that cycle once and for all. This is the result of that idea. Our philosophy is that SWAN should have no external dependencies, it should be cross-platform, and it should be useful.</Description>
|
||||
<Copyright>Copyright (c) 2016-2019 - Unosquare</Copyright>
|
||||
<AssemblyTitle>Unosquare SWAN</AssemblyTitle>
|
||||
<TargetFrameworks>netcoreapp3.0</TargetFrameworks>
|
||||
<AssemblyName>Swan.Lite</AssemblyName>
|
||||
<Version>2.4.2</Version>
|
||||
<Authors>Unosquare</Authors>
|
||||
<PackageIconUrl>https://github.com/unosquare/swan/raw/master/swan-logo-32.png</PackageIconUrl>
|
||||
<PackageProjectUrl>https://github.com/unosquare/swan</PackageProjectUrl>
|
||||
<PackageLicenseUrl>https://raw.githubusercontent.com/unosquare/swan/master/LICENSE</PackageLicenseUrl>
|
||||
<PackageTags>best-practices netcore network objectmapper json-serialization</PackageTags>
|
||||
<LangVersion>8.0</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
</Project>
|
233
Swan.Lite/SwanRuntime.cs
Normal file
233
Swan.Lite/SwanRuntime.cs
Normal file
@ -0,0 +1,233 @@
|
||||
using Swan.Logging;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
|
||||
namespace Swan
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides utility methods to retrieve information about the current application.
|
||||
/// </summary>
|
||||
public static class SwanRuntime
|
||||
{
|
||||
private static readonly Lazy<Assembly> EntryAssemblyLazy = new Lazy<Assembly>(Assembly.GetEntryAssembly);
|
||||
|
||||
private static readonly Lazy<string> CompanyNameLazy = new Lazy<string>(() =>
|
||||
{
|
||||
var attribute =
|
||||
EntryAssembly.GetCustomAttribute(typeof(AssemblyCompanyAttribute)) as AssemblyCompanyAttribute;
|
||||
return attribute?.Company ?? string.Empty;
|
||||
});
|
||||
|
||||
private static readonly Lazy<string> ProductNameLazy = new Lazy<string>(() =>
|
||||
{
|
||||
var attribute =
|
||||
EntryAssembly.GetCustomAttribute(typeof(AssemblyProductAttribute)) as AssemblyProductAttribute;
|
||||
return attribute?.Product ?? string.Empty;
|
||||
});
|
||||
|
||||
private static readonly Lazy<string> ProductTrademarkLazy = new Lazy<string>(() =>
|
||||
{
|
||||
var attribute =
|
||||
EntryAssembly.GetCustomAttribute(typeof(AssemblyTrademarkAttribute)) as AssemblyTrademarkAttribute;
|
||||
return attribute?.Trademark ?? string.Empty;
|
||||
});
|
||||
|
||||
private static readonly string ApplicationMutexName = "Global\\{{" + EntryAssembly.FullName + "}}";
|
||||
|
||||
private static readonly object SyncLock = new object();
|
||||
|
||||
private static OperatingSystem? _oS;
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current Operating System.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The os.
|
||||
/// </value>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if this application (including version number) is the only instance currently running.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if this instance is the only instance; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public static bool IsTheOnlyInstance
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (SyncLock)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Try to open existing mutex.
|
||||
Mutex.OpenExisting(ApplicationMutexName);
|
||||
}
|
||||
catch
|
||||
{
|
||||
try
|
||||
{
|
||||
// If exception occurred, there is no such mutex.
|
||||
var appMutex = new Mutex(true, ApplicationMutexName);
|
||||
$"Application Mutex created {appMutex} named '{ApplicationMutexName}'".Debug(
|
||||
typeof(SwanRuntime));
|
||||
|
||||
// Only one instance.
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Sometimes the user can't create the Global Mutex
|
||||
}
|
||||
}
|
||||
|
||||
// More than one instance.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this application instance is using the MONO runtime.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if this instance is using MONO runtime; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public static bool IsUsingMonoRuntime => Type.GetType("Mono.Runtime") != null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the assembly that started the application.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The entry assembly.
|
||||
/// </value>
|
||||
public static Assembly EntryAssembly => EntryAssemblyLazy.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the entry assembly.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The name of the entry assembly.
|
||||
/// </value>
|
||||
public static AssemblyName EntryAssemblyName => EntryAssemblyLazy.Value.GetName();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entry assembly version.
|
||||
/// </summary>
|
||||
public static Version EntryAssemblyVersion => EntryAssemblyName.Version;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the full path to the folder containing the assembly that started the application.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The entry assembly directory.
|
||||
/// </value>
|
||||
public static string EntryAssemblyDirectory
|
||||
{
|
||||
get
|
||||
{
|
||||
var uri = new UriBuilder(EntryAssembly.CodeBase);
|
||||
var path = Uri.UnescapeDataString(uri.Path);
|
||||
return Path.GetDirectoryName(path);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the company.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The name of the company.
|
||||
/// </value>
|
||||
public static string CompanyName => CompanyNameLazy.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the product.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The name of the product.
|
||||
/// </value>
|
||||
public static string ProductName => ProductNameLazy.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the trademark.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The product trademark.
|
||||
/// </value>
|
||||
public static string ProductTrademark => ProductTrademarkLazy.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a local storage path with a version.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The local storage path.
|
||||
/// </value>
|
||||
public static string LocalStoragePath
|
||||
{
|
||||
get
|
||||
{
|
||||
var localAppDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
EntryAssemblyName.Name);
|
||||
|
||||
var returnPath = Path.Combine(localAppDataPath, EntryAssemblyVersion.ToString());
|
||||
|
||||
if (!Directory.Exists(returnPath))
|
||||
{
|
||||
Directory.CreateDirectory(returnPath);
|
||||
}
|
||||
|
||||
return returnPath;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <summary>
|
||||
/// Build a full path pointing to the current user's desktop with the given filename.
|
||||
/// </summary>
|
||||
/// <param name="filename">The filename.</param>
|
||||
/// <returns>
|
||||
/// The fully qualified location of path, such as "C:\MyFile.txt".
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">filename.</exception>
|
||||
public static string GetDesktopFilePath(string filename)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(filename))
|
||||
throw new ArgumentNullException(nameof(filename));
|
||||
|
||||
var pathWithFilename = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory),
|
||||
filename);
|
||||
|
||||
return Path.GetFullPath(pathWithFilename);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
37
Swan.Lite/Terminal.Graphics.cs
Normal file
37
Swan.Lite/Terminal.Graphics.cs
Normal file
@ -0,0 +1,37 @@
|
||||
namespace Swan
|
||||
{
|
||||
/// <summary>
|
||||
/// A console terminal helper to create nicer output and receive input from the user
|
||||
/// This class is thread-safe :).
|
||||
/// </summary>
|
||||
public static partial class Terminal
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a Table to print in console.
|
||||
/// </summary>
|
||||
private static class Table
|
||||
{
|
||||
public static void Vertical() => Write('\u2502', Settings.BorderColor);
|
||||
|
||||
public static void RightTee() => Write('\u2524', Settings.BorderColor);
|
||||
|
||||
public static void TopRight() => Write('\u2510', Settings.BorderColor);
|
||||
|
||||
public static void BottomLeft() => Write('\u2514', Settings.BorderColor);
|
||||
|
||||
public static void BottomTee() => Write('\u2534', Settings.BorderColor);
|
||||
|
||||
public static void TopTee() => Write('\u252c', Settings.BorderColor);
|
||||
|
||||
public static void LeftTee() => Write('\u251c', Settings.BorderColor);
|
||||
|
||||
public static void Horizontal(int length) => Write(new string('\u2500', length), Settings.BorderColor);
|
||||
|
||||
public static void Tee() => Write('\u253c', Settings.BorderColor);
|
||||
|
||||
public static void BottomRight() => Write('\u2518', Settings.BorderColor);
|
||||
|
||||
public static void TopLeft() => Write('\u250C', Settings.BorderColor);
|
||||
}
|
||||
}
|
||||
}
|
261
Swan.Lite/Terminal.Interaction.cs
Normal file
261
Swan.Lite/Terminal.Interaction.cs
Normal file
@ -0,0 +1,261 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Swan.Lite.Logging;
|
||||
using System.Globalization;
|
||||
using Swan.Logging;
|
||||
|
||||
namespace Swan
|
||||
{
|
||||
/// <summary>
|
||||
/// A console terminal helper to create nicer output and receive input from the user
|
||||
/// This class is thread-safe :).
|
||||
/// </summary>
|
||||
public static partial class Terminal
|
||||
{
|
||||
#region ReadKey
|
||||
|
||||
/// <summary>
|
||||
/// Reads a key from the Terminal. This is the closest equivalent to Console.ReadKey.
|
||||
/// </summary>
|
||||
/// <param name="intercept">if set to <c>true</c> the pressed key will not be rendered to the output.</param>
|
||||
/// <param name="disableLocking">if set to <c>true</c> 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.</param>
|
||||
/// <returns>The console key information.</returns>
|
||||
public static ConsoleKeyInfo ReadKey(bool intercept, bool disableLocking = false)
|
||||
{
|
||||
if (!IsConsolePresent) return default;
|
||||
if (disableLocking) return Console.ReadKey(intercept);
|
||||
|
||||
lock (SyncLock)
|
||||
{
|
||||
Flush();
|
||||
InputDone.Reset();
|
||||
|
||||
try
|
||||
{
|
||||
Console.CursorVisible = true;
|
||||
return Console.ReadKey(intercept);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.CursorVisible = false;
|
||||
InputDone.Set();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a key from the Terminal.
|
||||
/// </summary>
|
||||
/// <param name="prompt">The prompt.</param>
|
||||
/// <param name="preventEcho">if set to <c>true</c> [prevent echo].</param>
|
||||
/// <returns>The console key information.</returns>
|
||||
public static ConsoleKeyInfo ReadKey(string prompt, bool preventEcho = true)
|
||||
{
|
||||
if (!IsConsolePresent) return default;
|
||||
|
||||
lock (SyncLock)
|
||||
{
|
||||
if (prompt != null)
|
||||
{
|
||||
Write($"{GetNowFormatted()}{Settings.UserInputPrefix} << {prompt} ", ConsoleColor.White);
|
||||
}
|
||||
|
||||
var input = ReadKey(true);
|
||||
var echo = preventEcho ? string.Empty : input.Key.ToString();
|
||||
WriteLine(echo);
|
||||
return input;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Other Terminal Read Methods
|
||||
|
||||
/// <summary>
|
||||
/// Clears the screen.
|
||||
/// </summary>
|
||||
public static void Clear()
|
||||
{
|
||||
Flush();
|
||||
Console.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a line of text from the console.
|
||||
/// </summary>
|
||||
/// <returns>The read line.</returns>
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a line from the input.
|
||||
/// </summary>
|
||||
/// <param name="prompt">The prompt.</param>
|
||||
/// <returns>The read line.</returns>
|
||||
public static string? ReadLine(string prompt)
|
||||
{
|
||||
if (!IsConsolePresent) return null;
|
||||
|
||||
lock (SyncLock)
|
||||
{
|
||||
Write($"{GetNowFormatted()}{Settings.UserInputPrefix} << {prompt}: ", ConsoleColor.White);
|
||||
|
||||
return ReadLine();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a number from the input. If unable to parse, it returns the default number.
|
||||
/// </summary>
|
||||
/// <param name="prompt">The prompt.</param>
|
||||
/// <param name="defaultNumber">The default number.</param>
|
||||
/// <returns>
|
||||
/// Conversions of string representation of a number to its 32-bit signed integer equivalent.
|
||||
/// </returns>
|
||||
public static int ReadNumber(string prompt, int defaultNumber)
|
||||
{
|
||||
if (!IsConsolePresent) return defaultNumber;
|
||||
|
||||
lock (SyncLock)
|
||||
{
|
||||
Write($"{GetNowFormatted()}{Settings.UserInputPrefix} << {prompt} (default is {defaultNumber}): ",
|
||||
ConsoleColor.White);
|
||||
|
||||
var input = ReadLine();
|
||||
return int.TryParse(input, out var parsedInt) ? parsedInt : defaultNumber;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a table prompt where the user can enter an option based on the options dictionary provided.
|
||||
/// </summary>
|
||||
/// <param name="title">The title.</param>
|
||||
/// <param name="options">The options.</param>
|
||||
/// <param name="anyKeyOption">Any key option.</param>
|
||||
/// <returns>
|
||||
/// A value that identifies the console key that was pressed.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">options.</exception>
|
||||
public static ConsoleKeyInfo ReadPrompt(
|
||||
string title,
|
||||
IDictionary<ConsoleKey, string> options,
|
||||
string anyKeyOption)
|
||||
{
|
||||
if (!IsConsolePresent) return default;
|
||||
|
||||
if (options == null)
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
|
||||
const ConsoleColor textColor = ConsoleColor.White;
|
||||
var lineLength = Console.WindowWidth;
|
||||
var lineAlign = -(lineLength - 2);
|
||||
var textFormat = "{0," + lineAlign + "}";
|
||||
|
||||
// lock the output as an atomic operation
|
||||
lock (SyncLock)
|
||||
{
|
||||
{
|
||||
// Top border
|
||||
Table.TopLeft();
|
||||
Table.Horizontal(-lineAlign);
|
||||
Table.TopRight();
|
||||
}
|
||||
|
||||
{
|
||||
// Title
|
||||
Table.Vertical();
|
||||
var titleText = string.Format(CultureInfo.CurrentCulture,
|
||||
textFormat,
|
||||
string.IsNullOrWhiteSpace(title) ? " Select an option from the list below." : $" {title}");
|
||||
Write(titleText, textColor);
|
||||
Table.Vertical();
|
||||
}
|
||||
|
||||
{
|
||||
// Title Bottom
|
||||
Table.LeftTee();
|
||||
Table.Horizontal(lineLength - 2);
|
||||
Table.RightTee();
|
||||
}
|
||||
|
||||
// Options
|
||||
foreach (var kvp in options)
|
||||
{
|
||||
Table.Vertical();
|
||||
Write(string.Format(
|
||||
CultureInfo.CurrentCulture,
|
||||
textFormat,
|
||||
$" {"[ " + kvp.Key + " ]",-10} {kvp.Value}"),
|
||||
textColor);
|
||||
Table.Vertical();
|
||||
}
|
||||
|
||||
// Any Key Options
|
||||
if (string.IsNullOrWhiteSpace(anyKeyOption) == false)
|
||||
{
|
||||
Table.Vertical();
|
||||
Write(string.Format(CultureInfo.CurrentCulture, textFormat, " "), ConsoleColor.Gray);
|
||||
Table.Vertical();
|
||||
|
||||
Table.Vertical();
|
||||
Write(string.Format(
|
||||
CultureInfo.CurrentCulture,
|
||||
textFormat,
|
||||
$" {" ",-10} {anyKeyOption}"),
|
||||
ConsoleColor.Gray);
|
||||
Table.Vertical();
|
||||
}
|
||||
|
||||
{
|
||||
// Input section
|
||||
Table.LeftTee();
|
||||
Table.Horizontal(lineLength - 2);
|
||||
Table.RightTee();
|
||||
|
||||
Table.Vertical();
|
||||
Write(string.Format(CultureInfo.CurrentCulture, textFormat, Settings.UserOptionText),
|
||||
ConsoleColor.Green);
|
||||
Table.Vertical();
|
||||
|
||||
Table.BottomLeft();
|
||||
Table.Horizontal(lineLength - 2);
|
||||
Table.BottomRight();
|
||||
}
|
||||
}
|
||||
|
||||
var inputLeft = Settings.UserOptionText.Length + 3;
|
||||
|
||||
SetCursorPosition(inputLeft, CursorTop - 1);
|
||||
var userInput = ReadKey(true);
|
||||
Write(userInput.Key.ToString(), ConsoleColor.Gray);
|
||||
|
||||
SetCursorPosition(0, CursorTop + 2);
|
||||
return userInput;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static string GetNowFormatted() =>
|
||||
$" {(string.IsNullOrWhiteSpace(TextLogger.LoggingTimeFormat) ? string.Empty : DateTime.Now.ToString(TextLogger.LoggingTimeFormat) + " ")}";
|
||||
}
|
||||
}
|
97
Swan.Lite/Terminal.Output.cs
Normal file
97
Swan.Lite/Terminal.Output.cs
Normal file
@ -0,0 +1,97 @@
|
||||
using System;
|
||||
|
||||
namespace Swan
|
||||
{
|
||||
/// <summary>
|
||||
/// A console terminal helper to create nicer output and receive input from the user
|
||||
/// This class is thread-safe :).
|
||||
/// </summary>
|
||||
public static partial class Terminal
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes a character a number of times, optionally adding a new line at the end.
|
||||
/// </summary>
|
||||
/// <param name="charCode">The character code.</param>
|
||||
/// <param name="color">The color.</param>
|
||||
/// <param name="count">The count.</param>
|
||||
/// <param name="newLine">if set to <c>true</c> [new line].</param>
|
||||
/// <param name="writerFlags">The writer flags.</param>
|
||||
public static void Write(char charCode, ConsoleColor? color = null, int count = 1, bool newLine = false, TerminalWriters writerFlags = TerminalWriters.StandardOutput)
|
||||
{
|
||||
lock (SyncLock)
|
||||
{
|
||||
var text = new string(charCode, count);
|
||||
|
||||
if (newLine)
|
||||
{
|
||||
text += Environment.NewLine;
|
||||
}
|
||||
|
||||
var buffer = OutputEncoding.GetBytes(text);
|
||||
var context = new OutputContext
|
||||
{
|
||||
OutputColor = color ?? Settings.DefaultColor,
|
||||
OutputText = OutputEncoding.GetChars(buffer),
|
||||
OutputWriters = writerFlags,
|
||||
};
|
||||
|
||||
EnqueueOutput(context);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the specified text in the given color.
|
||||
/// </summary>
|
||||
/// <param name="text">The text.</param>
|
||||
/// <param name="color">The color.</param>
|
||||
/// <param name="writerFlags">The writer flags.</param>
|
||||
public static void Write(string? text, ConsoleColor? color = null, TerminalWriters writerFlags = TerminalWriters.StandardOutput)
|
||||
{
|
||||
if (text == null) return;
|
||||
|
||||
lock (SyncLock)
|
||||
{
|
||||
var buffer = OutputEncoding.GetBytes(text);
|
||||
var context = new OutputContext
|
||||
{
|
||||
OutputColor = color ?? Settings.DefaultColor,
|
||||
OutputText = OutputEncoding.GetChars(buffer),
|
||||
OutputWriters = writerFlags,
|
||||
};
|
||||
|
||||
EnqueueOutput(context);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a New Line Sequence to the standard output.
|
||||
/// </summary>
|
||||
/// <param name="writerFlags">The writer flags.</param>
|
||||
public static void WriteLine(TerminalWriters writerFlags = TerminalWriters.StandardOutput)
|
||||
=> Write(Environment.NewLine, Settings.DefaultColor, writerFlags);
|
||||
|
||||
/// <summary>
|
||||
/// Writes a line of text in the current console foreground color
|
||||
/// to the standard output.
|
||||
/// </summary>
|
||||
/// <param name="text">The text.</param>
|
||||
/// <param name="color">The color.</param>
|
||||
/// <param name="writerFlags">The writer flags.</param>
|
||||
public static void WriteLine(string text, ConsoleColor? color = null, TerminalWriters writerFlags = TerminalWriters.StandardOutput)
|
||||
=> Write($"{text ?? string.Empty}{Environment.NewLine}", color, writerFlags);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="text">The text.</param>
|
||||
/// <param name="color">The color.</param>
|
||||
/// <param name="writerFlags">The writer flags.</param>
|
||||
public static void OverwriteLine(string text, ConsoleColor? color = null, TerminalWriters writerFlags = TerminalWriters.StandardOutput)
|
||||
{
|
||||
Write($"\r{text ?? string.Empty}", color, writerFlags);
|
||||
Flush();
|
||||
CursorLeft = 0;
|
||||
}
|
||||
}
|
||||
}
|
49
Swan.Lite/Terminal.Settings.cs
Normal file
49
Swan.Lite/Terminal.Settings.cs
Normal file
@ -0,0 +1,49 @@
|
||||
using System;
|
||||
|
||||
namespace Swan
|
||||
{
|
||||
/// <summary>
|
||||
/// A console terminal helper to create nicer output and receive input from the user
|
||||
/// This class is thread-safe :).
|
||||
/// </summary>
|
||||
public static partial class Terminal
|
||||
{
|
||||
/// <summary>
|
||||
/// Terminal global settings.
|
||||
/// </summary>
|
||||
public static class Settings
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the default output color.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The default color.
|
||||
/// </value>
|
||||
public static ConsoleColor DefaultColor { get; set; } = Console.ForegroundColor;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the color of the border.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The color of the border.
|
||||
/// </value>
|
||||
public static ConsoleColor BorderColor { get; } = ConsoleColor.DarkGreen;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the user input prefix.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The user input prefix.
|
||||
/// </value>
|
||||
public static string UserInputPrefix { get; set; } = "USR";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the user option text.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The user option text.
|
||||
/// </value>
|
||||
public static string UserOptionText { get; set; } = " Option: ";
|
||||
}
|
||||
}
|
||||
}
|
339
Swan.Lite/Terminal.cs
Normal file
339
Swan.Lite/Terminal.cs
Normal file
@ -0,0 +1,339 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Swan.Threading;
|
||||
|
||||
namespace Swan
|
||||
{
|
||||
/// <summary>
|
||||
/// A console terminal helper to create nicer output and receive input from the user.
|
||||
/// This class is thread-safe :).
|
||||
/// </summary>
|
||||
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<OutputContext> OutputQueue = new ConcurrentQueue<OutputContext>();
|
||||
|
||||
private static readonly ManualResetEventSlim OutputDone = new ManualResetEventSlim(false);
|
||||
private static readonly ManualResetEventSlim InputDone = new ManualResetEventSlim(true);
|
||||
|
||||
private static bool? _isConsolePresent;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes static members of the <see cref="Terminal"/> class.
|
||||
/// </summary>
|
||||
static Terminal()
|
||||
{
|
||||
lock (SyncLock)
|
||||
{
|
||||
if (DequeueOutputTimer != null) return;
|
||||
|
||||
if (IsConsolePresent)
|
||||
{
|
||||
Console.CursorVisible = false;
|
||||
}
|
||||
|
||||
// Here we start the output task, fire-and-forget
|
||||
DequeueOutputTimer = new ExclusiveTimer(DequeueOutputCycle);
|
||||
DequeueOutputTimer.Resume(OutputFlushInterval);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Synchronized Cursor Movement
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the cursor left position.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The cursor left.
|
||||
/// </value>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the cursor top position.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The cursor top.
|
||||
/// </value>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the Console is present.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if this instance is console present; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public static bool IsConsolePresent
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_isConsolePresent == null)
|
||||
{
|
||||
_isConsolePresent = true;
|
||||
try
|
||||
{
|
||||
var windowHeight = Console.WindowHeight;
|
||||
_isConsolePresent = windowHeight >= 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
_isConsolePresent = false;
|
||||
}
|
||||
}
|
||||
|
||||
return _isConsolePresent.Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the available output writers in a bitwise mask.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The available writers.
|
||||
/// </value>
|
||||
public static TerminalWriters AvailableWriters =>
|
||||
IsConsolePresent
|
||||
? TerminalWriters.StandardError | TerminalWriters.StandardOutput
|
||||
: TerminalWriters.None;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the output encoding for the current console.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The output encoding.
|
||||
/// </value>
|
||||
public static Encoding OutputEncoding
|
||||
{
|
||||
get => Console.OutputEncoding;
|
||||
set => Console.OutputEncoding = value;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="timeout">The timeout. Set the amount of time to black before this method exits.</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the cursor position.
|
||||
/// </summary>
|
||||
/// <param name="left">The left.</param>
|
||||
/// <param name="top">The top.</param>
|
||||
public static void SetCursorPosition(int left, int top)
|
||||
{
|
||||
if (!IsConsolePresent) return;
|
||||
|
||||
lock (SyncLock)
|
||||
{
|
||||
Flush();
|
||||
Console.SetCursorPosition(left.Clamp(0, left), top.Clamp(0, top));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static void BacklineCursor() => SetCursorPosition(0, CursorTop - 1);
|
||||
|
||||
/// <summary>
|
||||
/// Writes a standard banner to the standard output
|
||||
/// containing the company name, product name, assembly version and trademark.
|
||||
/// </summary>
|
||||
/// <param name="color">The color.</param>
|
||||
public static void WriteWelcomeBanner(ConsoleColor color = ConsoleColor.Gray)
|
||||
{
|
||||
WriteLine($"{SwanRuntime.CompanyName} {SwanRuntime.ProductName} [Version {SwanRuntime.EntryAssemblyVersion}]", color);
|
||||
WriteLine($"{SwanRuntime.ProductTrademark}", color);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="context">The context.</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a Terminal I/O cycle in the <see cref="ThreadPool"/> thread.
|
||||
/// </summary>
|
||||
private static void DequeueOutputCycle()
|
||||
{
|
||||
if (AvailableWriters == TerminalWriters.None)
|
||||
{
|
||||
OutputDone.Set();
|
||||
return;
|
||||
}
|
||||
|
||||
InputDone.Wait();
|
||||
|
||||
if (OutputQueue.Count <= 0)
|
||||
{
|
||||
OutputDone.Set();
|
||||
return;
|
||||
}
|
||||
|
||||
OutputDone.Reset();
|
||||
|
||||
while (OutputQueue.Count > 0)
|
||||
{
|
||||
if (!OutputQueue.TryDequeue(out var context)) continue;
|
||||
|
||||
// Process Console output and Skip over stuff we can't display so we don't stress the output too much.
|
||||
if (!IsConsolePresent) continue;
|
||||
|
||||
Console.ForegroundColor = context.OutputColor;
|
||||
|
||||
// Output to the standard output
|
||||
if (context.OutputWriters.HasFlag(TerminalWriters.StandardOutput))
|
||||
{
|
||||
Console.Out.Write(context.OutputText);
|
||||
}
|
||||
|
||||
// output to the standard error
|
||||
if (context.OutputWriters.HasFlag(TerminalWriters.StandardError))
|
||||
{
|
||||
Console.Error.Write(context.OutputText);
|
||||
}
|
||||
|
||||
Console.ResetColor();
|
||||
Console.ForegroundColor = context.OriginalColor;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Output Context
|
||||
|
||||
/// <summary>
|
||||
/// Represents an asynchronous output context.
|
||||
/// </summary>
|
||||
private sealed class OutputContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OutputContext"/> class.
|
||||
/// </summary>
|
||||
public OutputContext()
|
||||
{
|
||||
OriginalColor = Settings.DefaultColor;
|
||||
OutputWriters = IsConsolePresent
|
||||
? TerminalWriters.StandardOutput
|
||||
: TerminalWriters.None;
|
||||
}
|
||||
|
||||
public ConsoleColor OriginalColor { get; }
|
||||
public ConsoleColor OutputColor { get; set; }
|
||||
public char[] OutputText { get; set; }
|
||||
public TerminalWriters OutputWriters { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
31
Swan.Lite/TerminalWriters.Enums.cs
Normal file
31
Swan.Lite/TerminalWriters.Enums.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using System;
|
||||
|
||||
namespace Swan
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines a set of bitwise standard terminal writers.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum TerminalWriters
|
||||
{
|
||||
/// <summary>
|
||||
/// Prevents output
|
||||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Writes to the Console.Out
|
||||
/// </summary>
|
||||
StandardOutput = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Writes to the Console.Error
|
||||
/// </summary>
|
||||
StandardError = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Writes to all possible terminal writers
|
||||
/// </summary>
|
||||
All = StandardOutput | StandardError,
|
||||
}
|
||||
}
|
24
Swan.Lite/Threading/AtomicBoolean.cs
Normal file
24
Swan.Lite/Threading/AtomicBoolean.cs
Normal file
@ -0,0 +1,24 @@
|
||||
namespace Swan.Threading
|
||||
{
|
||||
/// <summary>
|
||||
/// Fast, atomic boolean combining interlocked to write value and volatile to read values.
|
||||
/// </summary>
|
||||
public sealed class AtomicBoolean : AtomicTypeBase<bool>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AtomicBoolean"/> class.
|
||||
/// </summary>
|
||||
/// <param name="initialValue">if set to <c>true</c> [initial value].</param>
|
||||
public AtomicBoolean(bool initialValue = default)
|
||||
: base(initialValue ? 1 : 0)
|
||||
{
|
||||
// placeholder
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool FromLong(long backingValue) => backingValue != 0;
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override long ToLong(bool value) => value ? 1 : 0;
|
||||
}
|
||||
}
|
26
Swan.Lite/Threading/AtomicDateTime.cs
Normal file
26
Swan.Lite/Threading/AtomicDateTime.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using System;
|
||||
|
||||
namespace Swan.Threading
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines an atomic DateTime.
|
||||
/// </summary>
|
||||
public sealed class AtomicDateTime : AtomicTypeBase<DateTime>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AtomicDateTime"/> class.
|
||||
/// </summary>
|
||||
/// <param name="initialValue">The initial value.</param>
|
||||
public AtomicDateTime(DateTime initialValue)
|
||||
: base(initialValue.Ticks)
|
||||
{
|
||||
// placeholder
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override DateTime FromLong(long backingValue) => new DateTime(backingValue);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override long ToLong(DateTime value) => value.Ticks;
|
||||
}
|
||||
}
|
28
Swan.Lite/Threading/AtomicDouble.cs
Normal file
28
Swan.Lite/Threading/AtomicDouble.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using System;
|
||||
|
||||
namespace Swan.Threading
|
||||
{
|
||||
/// <summary>
|
||||
/// Fast, atomic double combining interlocked to write value and volatile to read values.
|
||||
/// </summary>
|
||||
public sealed class AtomicDouble : AtomicTypeBase<double>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AtomicDouble"/> class.
|
||||
/// </summary>
|
||||
/// <param name="initialValue">if set to <c>true</c> [initial value].</param>
|
||||
public AtomicDouble(double initialValue = default)
|
||||
: base(BitConverter.DoubleToInt64Bits(initialValue))
|
||||
{
|
||||
// placeholder
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override double FromLong(long backingValue) =>
|
||||
BitConverter.Int64BitsToDouble(backingValue);
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override long ToLong(double value) =>
|
||||
BitConverter.DoubleToInt64Bits(value);
|
||||
}
|
||||
}
|
43
Swan.Lite/Threading/AtomicEnum.cs
Normal file
43
Swan.Lite/Threading/AtomicEnum.cs
Normal file
@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace Swan.Threading
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines an atomic generic Enum.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of enum.</typeparam>
|
||||
public sealed class AtomicEnum<T>
|
||||
where T : struct, IConvertible
|
||||
{
|
||||
private long _backingValue;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AtomicEnum{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="initialValue">The initial value.</param>
|
||||
/// <exception cref="ArgumentException">T must be an enumerated type.</exception>
|
||||
public AtomicEnum(T initialValue)
|
||||
{
|
||||
if (!Enum.IsDefined(typeof(T), initialValue))
|
||||
throw new ArgumentException("T must be an enumerated type");
|
||||
|
||||
Value = initialValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value.
|
||||
/// </summary>
|
||||
public T Value
|
||||
{
|
||||
get => (T)Enum.ToObject(typeof(T), BackingValue);
|
||||
set => BackingValue = Convert.ToInt64(value);
|
||||
}
|
||||
|
||||
private long BackingValue
|
||||
{
|
||||
get => Interlocked.Read(ref _backingValue);
|
||||
set => Interlocked.Exchange(ref _backingValue, value);
|
||||
}
|
||||
}
|
||||
}
|
28
Swan.Lite/Threading/AtomicInteger.cs
Normal file
28
Swan.Lite/Threading/AtomicInteger.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using System;
|
||||
|
||||
namespace Swan.Threading
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an atomically readable or writable integer.
|
||||
/// </summary>
|
||||
public class AtomicInteger : AtomicTypeBase<int>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AtomicInteger"/> class.
|
||||
/// </summary>
|
||||
/// <param name="initialValue">if set to <c>true</c> [initial value].</param>
|
||||
public AtomicInteger(int initialValue = default)
|
||||
: base(Convert.ToInt64(initialValue))
|
||||
{
|
||||
// placeholder
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override int FromLong(long backingValue) =>
|
||||
Convert.ToInt32(backingValue);
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override long ToLong(int value) =>
|
||||
Convert.ToInt64(value);
|
||||
}
|
||||
}
|
24
Swan.Lite/Threading/AtomicLong.cs
Normal file
24
Swan.Lite/Threading/AtomicLong.cs
Normal file
@ -0,0 +1,24 @@
|
||||
namespace Swan.Threading
|
||||
{
|
||||
/// <summary>
|
||||
/// Fast, atomic long combining interlocked to write value and volatile to read values.
|
||||
/// </summary>
|
||||
public sealed class AtomicLong : AtomicTypeBase<long>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AtomicLong"/> class.
|
||||
/// </summary>
|
||||
/// <param name="initialValue">if set to <c>true</c> [initial value].</param>
|
||||
public AtomicLong(long initialValue = default)
|
||||
: base(initialValue)
|
||||
{
|
||||
// placeholder
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override long FromLong(long backingValue) => backingValue;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override long ToLong(long value) => value;
|
||||
}
|
||||
}
|
26
Swan.Lite/Threading/AtomicTimeSpan.cs
Normal file
26
Swan.Lite/Threading/AtomicTimeSpan.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using System;
|
||||
|
||||
namespace Swan.Threading
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an atomic TimeSpan type.
|
||||
/// </summary>
|
||||
public sealed class AtomicTimeSpan : AtomicTypeBase<TimeSpan>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AtomicTimeSpan" /> class.
|
||||
/// </summary>
|
||||
/// <param name="initialValue">The initial value.</param>
|
||||
public AtomicTimeSpan(TimeSpan initialValue)
|
||||
: base(initialValue.Ticks)
|
||||
{
|
||||
// placeholder
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override TimeSpan FromLong(long backingValue) => TimeSpan.FromTicks(backingValue);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override long ToLong(TimeSpan value) => value.Ticks < 0 ? 0 : value.Ticks;
|
||||
}
|
||||
}
|
243
Swan.Lite/Threading/AtomicTypeBase.cs
Normal file
243
Swan.Lite/Threading/AtomicTypeBase.cs
Normal file
@ -0,0 +1,243 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace Swan.Threading
|
||||
{
|
||||
/// <summary>
|
||||
/// 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/.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The structure type backed by a 64-bit value.</typeparam>
|
||||
public abstract class AtomicTypeBase<T> : IComparable, IComparable<T>, IComparable<AtomicTypeBase<T>>, IEquatable<T>, IEquatable<AtomicTypeBase<T>>
|
||||
where T : struct, IComparable, IComparable<T>, IEquatable<T>
|
||||
{
|
||||
private long _backingValue;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AtomicTypeBase{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="initialValue">The initial value.</param>
|
||||
protected AtomicTypeBase(long initialValue)
|
||||
{
|
||||
BackingValue = initialValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value.
|
||||
/// </summary>
|
||||
public T Value
|
||||
{
|
||||
get => FromLong(BackingValue);
|
||||
set => BackingValue = ToLong(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the backing value.
|
||||
/// </summary>
|
||||
protected long BackingValue
|
||||
{
|
||||
get => Interlocked.Read(ref _backingValue);
|
||||
set => Interlocked.Exchange(ref _backingValue, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implements the operator ==.
|
||||
/// </summary>
|
||||
/// <param name="a">a.</param>
|
||||
/// <param name="b">The b.</param>
|
||||
/// <returns>
|
||||
/// The result of the operator.
|
||||
/// </returns>
|
||||
public static bool operator ==(AtomicTypeBase<T> a, T b) => a?.Equals(b) == true;
|
||||
|
||||
/// <summary>
|
||||
/// Implements the operator !=.
|
||||
/// </summary>
|
||||
/// <param name="a">a.</param>
|
||||
/// <param name="b">The b.</param>
|
||||
/// <returns>
|
||||
/// The result of the operator.
|
||||
/// </returns>
|
||||
public static bool operator !=(AtomicTypeBase<T> a, T b) => a?.Equals(b) == false;
|
||||
|
||||
/// <summary>
|
||||
/// Implements the operator >.
|
||||
/// </summary>
|
||||
/// <param name="a">a.</param>
|
||||
/// <param name="b">The b.</param>
|
||||
/// <returns>
|
||||
/// The result of the operator.
|
||||
/// </returns>
|
||||
public static bool operator >(AtomicTypeBase<T> a, T b) => a.CompareTo(b) > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Implements the operator <.
|
||||
/// </summary>
|
||||
/// <param name="a">a.</param>
|
||||
/// <param name="b">The b.</param>
|
||||
/// <returns>
|
||||
/// The result of the operator.
|
||||
/// </returns>
|
||||
public static bool operator <(AtomicTypeBase<T> a, T b) => a.CompareTo(b) < 0;
|
||||
|
||||
/// <summary>
|
||||
/// Implements the operator >=.
|
||||
/// </summary>
|
||||
/// <param name="a">a.</param>
|
||||
/// <param name="b">The b.</param>
|
||||
/// <returns>
|
||||
/// The result of the operator.
|
||||
/// </returns>
|
||||
public static bool operator >=(AtomicTypeBase<T> a, T b) => a.CompareTo(b) >= 0;
|
||||
|
||||
/// <summary>
|
||||
/// Implements the operator <=.
|
||||
/// </summary>
|
||||
/// <param name="a">a.</param>
|
||||
/// <param name="b">The b.</param>
|
||||
/// <returns>
|
||||
/// The result of the operator.
|
||||
/// </returns>
|
||||
public static bool operator <=(AtomicTypeBase<T> a, T b) => a.CompareTo(b) <= 0;
|
||||
|
||||
/// <summary>
|
||||
/// Implements the operator ++.
|
||||
/// </summary>
|
||||
/// <param name="instance">The instance.</param>
|
||||
/// <returns>
|
||||
/// The result of the operator.
|
||||
/// </returns>
|
||||
public static AtomicTypeBase<T> operator ++(AtomicTypeBase<T> instance)
|
||||
{
|
||||
Interlocked.Increment(ref instance._backingValue);
|
||||
return instance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implements the operator --.
|
||||
/// </summary>
|
||||
/// <param name="instance">The instance.</param>
|
||||
/// <returns>
|
||||
/// The result of the operator.
|
||||
/// </returns>
|
||||
public static AtomicTypeBase<T> operator --(AtomicTypeBase<T> instance)
|
||||
{
|
||||
Interlocked.Decrement(ref instance._backingValue);
|
||||
return instance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implements the operator -<.
|
||||
/// </summary>
|
||||
/// <param name="instance">The instance.</param>
|
||||
/// <param name="operand">The operand.</param>
|
||||
/// <returns>
|
||||
/// The result of the operator.
|
||||
/// </returns>
|
||||
public static AtomicTypeBase<T> operator +(AtomicTypeBase<T> instance, long operand)
|
||||
{
|
||||
instance.BackingValue = instance.BackingValue + operand;
|
||||
return instance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implements the operator -.
|
||||
/// </summary>
|
||||
/// <param name="instance">The instance.</param>
|
||||
/// <param name="operand">The operand.</param>
|
||||
/// <returns>
|
||||
/// The result of the operator.
|
||||
/// </returns>
|
||||
public static AtomicTypeBase<T> operator -(AtomicTypeBase<T> instance, long operand)
|
||||
{
|
||||
instance.BackingValue = instance.BackingValue - operand;
|
||||
return instance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the value to the other instance.
|
||||
/// </summary>
|
||||
/// <param name="other">The other instance.</param>
|
||||
/// <returns>0 if equal, 1 if this instance is greater, -1 if this instance is less than.</returns>
|
||||
/// <exception cref="ArgumentException">When types are incompatible.</exception>
|
||||
public int CompareTo(object other)
|
||||
{
|
||||
switch (other)
|
||||
{
|
||||
case null:
|
||||
return 1;
|
||||
case AtomicTypeBase<T> atomic:
|
||||
return BackingValue.CompareTo(atomic.BackingValue);
|
||||
case T variable:
|
||||
return Value.CompareTo(variable);
|
||||
}
|
||||
|
||||
throw new ArgumentException("Incompatible comparison types");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the value to the other instance.
|
||||
/// </summary>
|
||||
/// <param name="other">The other instance.</param>
|
||||
/// <returns>0 if equal, 1 if this instance is greater, -1 if this instance is less than.</returns>
|
||||
public int CompareTo(T other) => Value.CompareTo(other);
|
||||
|
||||
/// <summary>
|
||||
/// Compares the value to the other instance.
|
||||
/// </summary>
|
||||
/// <param name="other">The other instance.</param>
|
||||
/// <returns>0 if equal, 1 if this instance is greater, -1 if this instance is less than.</returns>
|
||||
public int CompareTo(AtomicTypeBase<T> other) => BackingValue.CompareTo(other?.BackingValue ?? default);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified <see cref="object" />, is equal to this instance.
|
||||
/// </summary>
|
||||
/// <param name="other">The <see cref="object" /> to compare with this instance.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the specified <see cref="object" /> is equal to this instance; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public override bool Equals(object other)
|
||||
{
|
||||
switch (other)
|
||||
{
|
||||
case AtomicTypeBase<T> atomic:
|
||||
return Equals(atomic);
|
||||
case T variable:
|
||||
return Equals(variable);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a hash code for this instance.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.
|
||||
/// </returns>
|
||||
public override int GetHashCode() => BackingValue.GetHashCode();
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Equals(AtomicTypeBase<T> other) =>
|
||||
BackingValue == (other?.BackingValue ?? default);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Equals(T other) => Equals(Value, other);
|
||||
|
||||
/// <summary>
|
||||
/// Converts from a long value to the target type.
|
||||
/// </summary>
|
||||
/// <param name="backingValue">The backing value.</param>
|
||||
/// <returns>The value converted form a long value.</returns>
|
||||
protected abstract T FromLong(long backingValue);
|
||||
|
||||
/// <summary>
|
||||
/// Converts from the target type to a long value.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <returns>The value converted to a long value.</returns>
|
||||
protected abstract long ToLong(T value);
|
||||
}
|
||||
}
|
68
Swan.Lite/Threading/CancellationTokenOwner.cs
Normal file
68
Swan.Lite/Threading/CancellationTokenOwner.cs
Normal file
@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace Swan.Threading
|
||||
{
|
||||
/// <summary>
|
||||
/// Acts as a <see cref="CancellationTokenSource"/> but with reusable tokens.
|
||||
/// </summary>
|
||||
public sealed class CancellationTokenOwner : IDisposable
|
||||
{
|
||||
private readonly object _syncLock = new object();
|
||||
private bool _isDisposed;
|
||||
private CancellationTokenSource _tokenSource = new CancellationTokenSource();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the token of the current.
|
||||
/// </summary>
|
||||
public CancellationToken Token
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
return _isDisposed
|
||||
? CancellationToken.None
|
||||
: _tokenSource.Token;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels the last referenced token and creates a new token source.
|
||||
/// </summary>
|
||||
public void Cancel()
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (_isDisposed) return;
|
||||
_tokenSource.Cancel();
|
||||
_tokenSource.Dispose();
|
||||
_tokenSource = new CancellationTokenSource();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() => Dispose(true);
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (_isDisposed) return;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
_tokenSource.Cancel();
|
||||
_tokenSource.Dispose();
|
||||
}
|
||||
|
||||
_isDisposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
236
Swan.Lite/Threading/ExclusiveTimer.cs
Normal file
236
Swan.Lite/Threading/ExclusiveTimer.cs
Normal file
@ -0,0 +1,236 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace Swan.Threading
|
||||
{
|
||||
/// <summary>
|
||||
/// A threading <see cref="_backingTimer"/> implementation that executes at most one cycle at a time
|
||||
/// in a <see cref="ThreadPool"/> thread. Callback execution is NOT guaranteed to be carried out
|
||||
/// on the same <see cref="ThreadPool"/> thread every time the timer fires.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExclusiveTimer"/> class.
|
||||
/// </summary>
|
||||
/// <param name="timerCallback">The timer callback.</param>
|
||||
/// <param name="state">The state.</param>
|
||||
/// <param name="dueTime">The due time.</param>
|
||||
/// <param name="period">The period.</param>
|
||||
public ExclusiveTimer(TimerCallback timerCallback, object state, int dueTime, int period)
|
||||
{
|
||||
_period = period;
|
||||
_userCallback = timerCallback;
|
||||
_backingTimer = new Timer(InternalCallback, state ?? this, dueTime, Timeout.Infinite);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExclusiveTimer"/> class.
|
||||
/// </summary>
|
||||
/// <param name="timerCallback">The timer callback.</param>
|
||||
/// <param name="state">The state.</param>
|
||||
/// <param name="dueTime">The due time.</param>
|
||||
/// <param name="period">The period.</param>
|
||||
public ExclusiveTimer(TimerCallback timerCallback, object state, TimeSpan dueTime, TimeSpan period)
|
||||
: this(timerCallback, state, Convert.ToInt32(dueTime.TotalMilliseconds), Convert.ToInt32(period.TotalMilliseconds))
|
||||
{
|
||||
// placeholder
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExclusiveTimer"/> class.
|
||||
/// </summary>
|
||||
/// <param name="timerCallback">The timer callback.</param>
|
||||
public ExclusiveTimer(TimerCallback timerCallback)
|
||||
: this(timerCallback, null, Timeout.Infinite, Timeout.Infinite)
|
||||
{
|
||||
// placeholder
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExclusiveTimer"/> class.
|
||||
/// </summary>
|
||||
/// <param name="timerCallback">The timer callback.</param>
|
||||
/// <param name="dueTime">The due time.</param>
|
||||
/// <param name="period">The period.</param>
|
||||
public ExclusiveTimer(Action timerCallback, int dueTime, int period)
|
||||
: this(s => { timerCallback?.Invoke(); }, null, dueTime, period)
|
||||
{
|
||||
// placeholder
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExclusiveTimer"/> class.
|
||||
/// </summary>
|
||||
/// <param name="timerCallback">The timer callback.</param>
|
||||
/// <param name="dueTime">The due time.</param>
|
||||
/// <param name="period">The period.</param>
|
||||
public ExclusiveTimer(Action timerCallback, TimeSpan dueTime, TimeSpan period)
|
||||
: this(s => { timerCallback?.Invoke(); }, null, dueTime, period)
|
||||
{
|
||||
// placeholder
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExclusiveTimer"/> class.
|
||||
/// </summary>
|
||||
/// <param name="timerCallback">The timer callback.</param>
|
||||
public ExclusiveTimer(Action timerCallback)
|
||||
: this(timerCallback, Timeout.Infinite, Timeout.Infinite)
|
||||
{
|
||||
// placeholder
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance is disposing.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if this instance is disposing; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public bool IsDisposing => _isDisposing.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance is disposed.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if this instance is disposed; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public bool IsDisposed => _isDisposed.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Waits until the time is elapsed.
|
||||
/// </summary>
|
||||
/// <param name="untilDate">The until date.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
public static void WaitUntil(DateTime untilDate, CancellationToken cancellationToken = default)
|
||||
{
|
||||
void Callback(IWaitEvent waitEvent)
|
||||
{
|
||||
try
|
||||
{
|
||||
waitEvent.Complete();
|
||||
waitEvent.Begin();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
using (var delayLock = WaitEventFactory.Create(true))
|
||||
{
|
||||
using (var _ = new ExclusiveTimer(() => Callback(delayLock), 0, 15))
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested && DateTime.UtcNow < untilDate)
|
||||
delayLock.Wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits the specified wait time.
|
||||
/// </summary>
|
||||
/// <param name="waitTime">The wait time.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
public static void Wait(TimeSpan waitTime, CancellationToken cancellationToken = default) =>
|
||||
WaitUntil(DateTime.UtcNow.Add(waitTime), cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Changes the start time and the interval between method invocations for the internal timer.
|
||||
/// </summary>
|
||||
/// <param name="dueTime">The due time.</param>
|
||||
/// <param name="period">The period.</param>
|
||||
public void Change(int dueTime, int period)
|
||||
{
|
||||
_period = period;
|
||||
|
||||
_backingTimer.Change(dueTime, Timeout.Infinite);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Changes the start time and the interval between method invocations for the internal timer.
|
||||
/// </summary>
|
||||
/// <param name="dueTime">The due time.</param>
|
||||
/// <param name="period">The period.</param>
|
||||
public void Change(TimeSpan dueTime, TimeSpan period)
|
||||
=> Change(Convert.ToInt32(dueTime.TotalMilliseconds), Convert.ToInt32(period.TotalMilliseconds));
|
||||
|
||||
/// <summary>
|
||||
/// Changes the interval between method invocations for the internal timer.
|
||||
/// </summary>
|
||||
/// <param name="period">The period.</param>
|
||||
public void Resume(int period) => Change(0, period);
|
||||
|
||||
/// <summary>
|
||||
/// Changes the interval between method invocations for the internal timer.
|
||||
/// </summary>
|
||||
/// <param name="period">The period.</param>
|
||||
public void Resume(TimeSpan period) => Change(TimeSpan.Zero, period);
|
||||
|
||||
/// <summary>
|
||||
/// Pauses this instance.
|
||||
/// </summary>
|
||||
public void Pause() => Change(Timeout.Infinite, Timeout.Infinite);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (_isDisposed == true || _isDisposing == true)
|
||||
return;
|
||||
|
||||
_isDisposing.Value = true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_cycleDoneEvent.Wait();
|
||||
_cycleDoneEvent.Dispose();
|
||||
Pause();
|
||||
_backingTimer.Dispose();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isDisposed.Value = true;
|
||||
_isDisposing.Value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logic that runs every time the timer hits the due time.
|
||||
/// </summary>
|
||||
/// <param name="state">The state.</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
24
Swan.Lite/Threading/ISyncLocker.cs
Normal file
24
Swan.Lite/Threading/ISyncLocker.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using System;
|
||||
|
||||
namespace Swan.Threading
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines a generic interface for synchronized locking mechanisms.
|
||||
/// </summary>
|
||||
public interface ISyncLocker : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Acquires a writer lock.
|
||||
/// The lock is released when the returned locking object is disposed.
|
||||
/// </summary>
|
||||
/// <returns>A disposable locking object.</returns>
|
||||
IDisposable AcquireWriterLock();
|
||||
|
||||
/// <summary>
|
||||
/// Acquires a reader lock.
|
||||
/// The lock is released when the returned locking object is disposed.
|
||||
/// </summary>
|
||||
/// <returns>A disposable locking object.</returns>
|
||||
IDisposable AcquireReaderLock();
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user