Strip Swan library Part 1
This commit is contained in:
parent
d0b26111dd
commit
f1c7a29b38
49
Swan.Tiny/Collections/CollectionCacheRepository.cs
Normal file
49
Swan.Tiny/Collections/CollectionCacheRepository.cs
Normal file
@ -0,0 +1,49 @@
|
||||
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 Boolean ContainsKey(Type key) => this._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 this._data.Value.GetOrAdd(key, k => factory.Invoke(k).Where(item => item != null));
|
||||
}
|
||||
}
|
||||
}
|
62
Swan.Tiny/Configuration/PropertyDisplayAttribute.cs
Normal file
62
Swan.Tiny/Configuration/PropertyDisplayAttribute.cs
Normal file
@ -0,0 +1,62 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
125
Swan.Tiny/Definitions.Types.cs
Normal file
125
Swan.Tiny/Definitions.Types.cs
Normal file
@ -0,0 +1,125 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
|
||||
using Swan.Reflection;
|
||||
|
||||
namespace Swan {
|
||||
/// <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(Int32), new ExtendedTypeInfo<Int32>()},
|
||||
{typeof(UInt32), new ExtendedTypeInfo<UInt32>()},
|
||||
{typeof(Int16), new ExtendedTypeInfo<Int16>()},
|
||||
{typeof(UInt16), new ExtendedTypeInfo<UInt16>()},
|
||||
{typeof(Int64), new ExtendedTypeInfo<Int64>()},
|
||||
{typeof(UInt64), new ExtendedTypeInfo<UInt64>()},
|
||||
{typeof(Single), new ExtendedTypeInfo<Single>()},
|
||||
{typeof(Double), new ExtendedTypeInfo<Double>()},
|
||||
{typeof(Char), new ExtendedTypeInfo<Char>()},
|
||||
{typeof(Boolean), new ExtendedTypeInfo<Boolean>()},
|
||||
{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(Int32?), new ExtendedTypeInfo<Int32?>()},
|
||||
{typeof(UInt32?), new ExtendedTypeInfo<UInt32?>()},
|
||||
{typeof(Int16?), new ExtendedTypeInfo<Int16?>()},
|
||||
{typeof(UInt16?), new ExtendedTypeInfo<UInt16?>()},
|
||||
{typeof(Int64?), new ExtendedTypeInfo<Int64?>()},
|
||||
{typeof(UInt64?), new ExtendedTypeInfo<UInt64?>()},
|
||||
{typeof(Single?), new ExtendedTypeInfo<Single?>()},
|
||||
{typeof(Double?), new ExtendedTypeInfo<Double?>()},
|
||||
{typeof(Char?), new ExtendedTypeInfo<Char?>()},
|
||||
{typeof(Boolean?), new ExtendedTypeInfo<Boolean?>()},
|
||||
{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());
|
||||
}
|
||||
}
|
34
Swan.Tiny/Definitions.cs
Normal file
34
Swan.Tiny/Definitions.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using System;
|
||||
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(Int32));
|
||||
try {
|
||||
Windows1252Encoding = Encoding.GetEncoding(1252);
|
||||
} catch {
|
||||
// ignore, the codepage is not available use default
|
||||
Windows1252Encoding = CurrentAnsiEncoding;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
575
Swan.Tiny/DependencyInjection/DependencyContainer.cs
Normal file
575
Swan.Tiny/DependencyInjection/DependencyContainer.cs
Normal file
@ -0,0 +1,575 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Swan.DependencyInjection {
|
||||
/// <summary>
|
||||
/// The concrete implementation of a simple IoC container
|
||||
/// based largely on TinyIoC (https://github.com/grumpydev/TinyIoC).
|
||||
/// </summary>
|
||||
/// <seealso cref="System.IDisposable" />
|
||||
public partial class DependencyContainer : IDisposable {
|
||||
private readonly Object _autoRegisterLock = new Object();
|
||||
|
||||
private Boolean _disposed;
|
||||
|
||||
static DependencyContainer() {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DependencyContainer"/> class.
|
||||
/// </summary>
|
||||
public DependencyContainer() {
|
||||
this.RegisteredTypes = new TypesConcurrentDictionary(this);
|
||||
_ = this.Register(this);
|
||||
}
|
||||
|
||||
private DependencyContainer(DependencyContainer parent) : this() => this.Parent = parent;
|
||||
|
||||
/// <summary>
|
||||
/// Lazy created Singleton instance of the container for simple scenarios.
|
||||
/// </summary>
|
||||
public static DependencyContainer Current { get; } = new DependencyContainer();
|
||||
|
||||
internal DependencyContainer Parent {
|
||||
get;
|
||||
}
|
||||
|
||||
internal TypesConcurrentDictionary RegisteredTypes {
|
||||
get;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() {
|
||||
if(this._disposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._disposed = true;
|
||||
|
||||
foreach(IDisposable disposable in this.RegisteredTypes.Values.Select(item => item as IDisposable)) {
|
||||
disposable?.Dispose();
|
||||
}
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the child container.
|
||||
/// </summary>
|
||||
/// <returns>A new instance of the <see cref="DependencyContainer"/> class.</returns>
|
||||
public DependencyContainer GetChildContainer() => new DependencyContainer(this);
|
||||
|
||||
#region Registration
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to automatically register all non-generic classes and interfaces in the current app domain.
|
||||
/// Types will only be registered if they pass the supplied registration predicate.
|
||||
/// </summary>
|
||||
/// <param name="duplicateAction">What action to take when encountering duplicate implementations of an interface/base class.</param>
|
||||
/// <param name="registrationPredicate">Predicate to determine if a particular type should be registered.</param>
|
||||
public void AutoRegister(DependencyContainerDuplicateImplementationAction duplicateAction = DependencyContainerDuplicateImplementationAction.RegisterSingle, Func<Type, Boolean> registrationPredicate = null) => this.AutoRegister(AppDomain.CurrentDomain.GetAssemblies().Where(a => !IsIgnoredAssembly(a)), duplicateAction, registrationPredicate);
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to automatically register all non-generic classes and interfaces in the specified assemblies
|
||||
/// Types will only be registered if they pass the supplied registration predicate.
|
||||
/// </summary>
|
||||
/// <param name="assemblies">Assemblies to process.</param>
|
||||
/// <param name="duplicateAction">What action to take when encountering duplicate implementations of an interface/base class.</param>
|
||||
/// <param name="registrationPredicate">Predicate to determine if a particular type should be registered.</param>
|
||||
public void AutoRegister(IEnumerable<Assembly> assemblies, DependencyContainerDuplicateImplementationAction duplicateAction = DependencyContainerDuplicateImplementationAction.RegisterSingle, Func<Type, Boolean> registrationPredicate = null) {
|
||||
lock(this._autoRegisterLock) {
|
||||
List<Type> types = assemblies.SelectMany(a => a.GetAllTypes()).Where(t => !IsIgnoredType(t, registrationPredicate)).ToList();
|
||||
|
||||
List<Type> concreteTypes = types.Where(type => type.IsClass && !type.IsAbstract && type != this.GetType() && type.DeclaringType != this.GetType() && !type.IsGenericTypeDefinition).ToList();
|
||||
|
||||
foreach(Type type in concreteTypes) {
|
||||
try {
|
||||
_ = this.RegisteredTypes.Register(type, String.Empty, GetDefaultObjectFactory(type, type));
|
||||
} catch(MethodAccessException) {
|
||||
// Ignore methods we can't access - added for Silverlight
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerable<Type> abstractInterfaceTypes = types.Where(type => (type.IsInterface || type.IsAbstract) && type.DeclaringType != this.GetType() && !type.IsGenericTypeDefinition);
|
||||
|
||||
foreach(Type type in abstractInterfaceTypes) {
|
||||
Type localType = type;
|
||||
List<Type> implementations = concreteTypes.Where(implementationType => localType.IsAssignableFrom(implementationType)).ToList();
|
||||
|
||||
if(implementations.Skip(1).Any()) {
|
||||
if(duplicateAction == DependencyContainerDuplicateImplementationAction.Fail) {
|
||||
throw new DependencyContainerRegistrationException(type, implementations);
|
||||
}
|
||||
|
||||
if(duplicateAction == DependencyContainerDuplicateImplementationAction.RegisterMultiple) {
|
||||
_ = this.RegisterMultiple(type, implementations);
|
||||
}
|
||||
}
|
||||
|
||||
Type firstImplementation = implementations.FirstOrDefault();
|
||||
|
||||
if(firstImplementation == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
_ = this.RegisteredTypes.Register(type, String.Empty, GetDefaultObjectFactory(type, firstImplementation));
|
||||
} catch(MethodAccessException) {
|
||||
// Ignore methods we can't access - added for Silverlight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates/replaces a named container class registration with default options.
|
||||
/// </summary>
|
||||
/// <param name="registerType">Type to register.</param>
|
||||
/// <param name="name">Name of registration.</param>
|
||||
/// <returns>RegisterOptions for fluent API.</returns>
|
||||
public RegisterOptions Register(Type registerType, String name = "") => this.RegisteredTypes.Register(registerType, name, GetDefaultObjectFactory(registerType, registerType));
|
||||
|
||||
/// <summary>
|
||||
/// Creates/replaces a named container class registration with a given implementation and default options.
|
||||
/// </summary>
|
||||
/// <param name="registerType">Type to register.</param>
|
||||
/// <param name="registerImplementation">Type to instantiate that implements RegisterType.</param>
|
||||
/// <param name="name">Name of registration.</param>
|
||||
/// <returns>RegisterOptions for fluent API.</returns>
|
||||
public RegisterOptions Register(Type registerType, Type registerImplementation, String name = "") => this.RegisteredTypes.Register(registerType, name, GetDefaultObjectFactory(registerType, registerImplementation));
|
||||
|
||||
/// <summary>
|
||||
/// Creates/replaces a named container class registration with a specific, strong referenced, instance.
|
||||
/// </summary>
|
||||
/// <param name="registerType">Type to register.</param>
|
||||
/// <param name="instance">Instance of RegisterType to register.</param>
|
||||
/// <param name="name">Name of registration.</param>
|
||||
/// <returns>RegisterOptions for fluent API.</returns>
|
||||
public RegisterOptions Register(Type registerType, Object instance, String name = "") => this.RegisteredTypes.Register(registerType, name, new InstanceFactory(registerType, registerType, instance));
|
||||
|
||||
/// <summary>
|
||||
/// Creates/replaces a named container class registration with a specific, strong referenced, instance.
|
||||
/// </summary>
|
||||
/// <param name="registerType">Type to register.</param>
|
||||
/// <param name="registerImplementation">Type of instance to register that implements RegisterType.</param>
|
||||
/// <param name="instance">Instance of RegisterImplementation to register.</param>
|
||||
/// <param name="name">Name of registration.</param>
|
||||
/// <returns>RegisterOptions for fluent API.</returns>
|
||||
public RegisterOptions Register(Type registerType, Type registerImplementation, Object instance, String name = "") => this.RegisteredTypes.Register(registerType, name, new InstanceFactory(registerType, registerImplementation, instance));
|
||||
|
||||
/// <summary>
|
||||
/// Creates/replaces a container class registration with a user specified factory.
|
||||
/// </summary>
|
||||
/// <param name="registerType">Type to register.</param>
|
||||
/// <param name="factory">Factory/lambda that returns an instance of RegisterType.</param>
|
||||
/// <param name="name">Name of registration.</param>
|
||||
/// <returns>RegisterOptions for fluent API.</returns>
|
||||
public RegisterOptions Register(Type registerType, Func<DependencyContainer, Dictionary<String, Object>, Object> factory, String name = "") => this.RegisteredTypes.Register(registerType, name, new DelegateFactory(registerType, factory));
|
||||
|
||||
/// <summary>
|
||||
/// Creates/replaces a named container class registration with default options.
|
||||
/// </summary>
|
||||
/// <typeparam name="TRegister">Type to register.</typeparam>
|
||||
/// <param name="name">Name of registration.</param>
|
||||
/// <returns>RegisterOptions for fluent API.</returns>
|
||||
public RegisterOptions Register<TRegister>(String name = "") where TRegister : class => this.Register(typeof(TRegister), name);
|
||||
|
||||
/// <summary>
|
||||
/// Creates/replaces a named container class registration with a given implementation and default options.
|
||||
/// </summary>
|
||||
/// <typeparam name="TRegister">Type to register.</typeparam>
|
||||
/// <typeparam name="TRegisterImplementation">Type to instantiate that implements RegisterType.</typeparam>
|
||||
/// <param name="name">Name of registration.</param>
|
||||
/// <returns>RegisterOptions for fluent API.</returns>
|
||||
public RegisterOptions Register<TRegister, TRegisterImplementation>(String name = "") where TRegister : class where TRegisterImplementation : class, TRegister => this.Register(typeof(TRegister), typeof(TRegisterImplementation), name);
|
||||
|
||||
/// <summary>
|
||||
/// Creates/replaces a named container class registration with a specific, strong referenced, instance.
|
||||
/// </summary>
|
||||
/// <typeparam name="TRegister">Type to register.</typeparam>
|
||||
/// <param name="instance">Instance of RegisterType to register.</param>
|
||||
/// <param name="name">Name of registration.</param>
|
||||
/// <returns>RegisterOptions for fluent API.</returns>
|
||||
public RegisterOptions Register<TRegister>(TRegister instance, String name = "") where TRegister : class => this.Register(typeof(TRegister), instance, name);
|
||||
|
||||
/// <summary>
|
||||
/// Creates/replaces a named container class registration with a specific, strong referenced, instance.
|
||||
/// </summary>
|
||||
/// <typeparam name="TRegister">Type to register.</typeparam>
|
||||
/// <typeparam name="TRegisterImplementation">Type of instance to register that implements RegisterType.</typeparam>
|
||||
/// <param name="instance">Instance of RegisterImplementation to register.</param>
|
||||
/// <param name="name">Name of registration.</param>
|
||||
/// <returns>RegisterOptions for fluent API.</returns>
|
||||
public RegisterOptions Register<TRegister, TRegisterImplementation>(TRegisterImplementation instance, String name = "") where TRegister : class where TRegisterImplementation : class, TRegister => this.Register(typeof(TRegister), typeof(TRegisterImplementation), instance, name);
|
||||
|
||||
/// <summary>
|
||||
/// Creates/replaces a named container class registration with a user specified factory.
|
||||
/// </summary>
|
||||
/// <typeparam name="TRegister">Type to register.</typeparam>
|
||||
/// <param name="factory">Factory/lambda that returns an instance of RegisterType.</param>
|
||||
/// <param name="name">Name of registration.</param>
|
||||
/// <returns>RegisterOptions for fluent API.</returns>
|
||||
public RegisterOptions Register<TRegister>(Func<DependencyContainer, Dictionary<String, Object>, TRegister> factory, String name = "") where TRegister : class {
|
||||
if(factory == null) {
|
||||
throw new ArgumentNullException(nameof(factory));
|
||||
}
|
||||
|
||||
return this.Register(typeof(TRegister), factory, name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register multiple implementations of a type.
|
||||
///
|
||||
/// Internally this registers each implementation using the full name of the class as its registration name.
|
||||
/// </summary>
|
||||
/// <typeparam name="TRegister">Type that each implementation implements.</typeparam>
|
||||
/// <param name="implementationTypes">Types that implement RegisterType.</param>
|
||||
/// <returns>MultiRegisterOptions for the fluent API.</returns>
|
||||
public MultiRegisterOptions RegisterMultiple<TRegister>(IEnumerable<Type> implementationTypes) => this.RegisterMultiple(typeof(TRegister), implementationTypes);
|
||||
|
||||
/// <summary>
|
||||
/// Register multiple implementations of a type.
|
||||
///
|
||||
/// Internally this registers each implementation using the full name of the class as its registration name.
|
||||
/// </summary>
|
||||
/// <param name="registrationType">Type that each implementation implements.</param>
|
||||
/// <param name="implementationTypes">Types that implement RegisterType.</param>
|
||||
/// <returns>MultiRegisterOptions for the fluent API.</returns>
|
||||
public MultiRegisterOptions RegisterMultiple(Type registrationType, IEnumerable<Type> implementationTypes) {
|
||||
if(implementationTypes == null) {
|
||||
throw new ArgumentNullException(nameof(implementationTypes), "types is null.");
|
||||
}
|
||||
|
||||
foreach(Type type in implementationTypes.Where(type => !registrationType.IsAssignableFrom(type))) {
|
||||
throw new ArgumentException($"types: The type {registrationType.FullName} is not assignable from {type.FullName}");
|
||||
}
|
||||
|
||||
if(implementationTypes.Count() != implementationTypes.Distinct().Count()) {
|
||||
IEnumerable<String> queryForDuplicatedTypes = implementationTypes.GroupBy(i => i).Where(j => j.Count() > 1).Select(j => j.Key.FullName);
|
||||
|
||||
String fullNamesOfDuplicatedTypes = String.Join(",\n", queryForDuplicatedTypes.ToArray());
|
||||
|
||||
throw new ArgumentException($"types: The same implementation type cannot be specified multiple times for {registrationType.FullName}\n\n{fullNamesOfDuplicatedTypes}");
|
||||
}
|
||||
|
||||
List<RegisterOptions> registerOptions = implementationTypes.Select(type => this.Register(registrationType, type, type.FullName)).ToList();
|
||||
|
||||
return new MultiRegisterOptions(registerOptions);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Unregistration
|
||||
|
||||
/// <summary>
|
||||
/// Remove a named container class registration.
|
||||
/// </summary>
|
||||
/// <typeparam name="TRegister">Type to unregister.</typeparam>
|
||||
/// <param name="name">Name of registration.</param>
|
||||
/// <returns><c>true</c> if the registration is successfully found and removed; otherwise, <c>false</c>.</returns>
|
||||
public Boolean Unregister<TRegister>(String name = "") => this.Unregister(typeof(TRegister), name);
|
||||
|
||||
/// <summary>
|
||||
/// Remove a named container class registration.
|
||||
/// </summary>
|
||||
/// <param name="registerType">Type to unregister.</param>
|
||||
/// <param name="name">Name of registration.</param>
|
||||
/// <returns><c>true</c> if the registration is successfully found and removed; otherwise, <c>false</c>.</returns>
|
||||
public Boolean Unregister(Type registerType, String name = "") => this.RegisteredTypes.RemoveRegistration(new TypeRegistration(registerType, name));
|
||||
|
||||
#endregion
|
||||
|
||||
#region Resolution
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to resolve a named type using specified options and the supplied constructor parameters.
|
||||
///
|
||||
/// Parameters are used in conjunction with normal container resolution to find the most suitable constructor (if one exists).
|
||||
/// All user supplied parameters must exist in at least one resolvable constructor of RegisterType or resolution will fail.
|
||||
/// </summary>
|
||||
/// <param name="resolveType">Type to resolve.</param>
|
||||
/// <param name="name">Name of registration.</param>
|
||||
/// <param name="options">Resolution options.</param>
|
||||
/// <returns>Instance of type.</returns>
|
||||
/// <exception cref="DependencyContainerResolutionException">Unable to resolve the type.</exception>
|
||||
public Object Resolve(Type resolveType, String name = null, DependencyContainerResolveOptions options = null) => this.RegisteredTypes.ResolveInternal(new TypeRegistration(resolveType, name), options ?? DependencyContainerResolveOptions.Default);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to resolve a named type using specified options and the supplied constructor parameters.
|
||||
///
|
||||
/// Parameters are used in conjunction with normal container resolution to find the most suitable constructor (if one exists).
|
||||
/// All user supplied parameters must exist in at least one resolvable constructor of RegisterType or resolution will fail.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResolveType">Type to resolve.</typeparam>
|
||||
/// <param name="name">Name of registration.</param>
|
||||
/// <param name="options">Resolution options.</param>
|
||||
/// <returns>Instance of type.</returns>
|
||||
/// <exception cref="DependencyContainerResolutionException">Unable to resolve the type.</exception>
|
||||
public TResolveType Resolve<TResolveType>(String name = null, DependencyContainerResolveOptions options = null) where TResolveType : class => (TResolveType)this.Resolve(typeof(TResolveType), name, options);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to predict whether a given type can be resolved with the supplied constructor parameters options.
|
||||
/// Parameters are used in conjunction with normal container resolution to find the most suitable constructor (if one exists).
|
||||
/// All user supplied parameters must exist in at least one resolvable constructor of RegisterType or resolution will fail.
|
||||
/// Note: Resolution may still fail if user defined factory registrations fail to construct objects when called.
|
||||
/// </summary>
|
||||
/// <param name="resolveType">Type to resolve.</param>
|
||||
/// <param name="name">The name.</param>
|
||||
/// <param name="options">Resolution options.</param>
|
||||
/// <returns>
|
||||
/// Bool indicating whether the type can be resolved.
|
||||
/// </returns>
|
||||
public Boolean CanResolve(Type resolveType, String name = null, DependencyContainerResolveOptions options = null) => this.RegisteredTypes.CanResolve(new TypeRegistration(resolveType, name), options);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to predict whether a given named type can be resolved with the supplied constructor parameters options.
|
||||
///
|
||||
/// Parameters are used in conjunction with normal container resolution to find the most suitable constructor (if one exists).
|
||||
/// All user supplied parameters must exist in at least one resolvable constructor of RegisterType or resolution will fail.
|
||||
///
|
||||
/// Note: Resolution may still fail if user defined factory registrations fail to construct objects when called.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResolveType">Type to resolve.</typeparam>
|
||||
/// <param name="name">Name of registration.</param>
|
||||
/// <param name="options">Resolution options.</param>
|
||||
/// <returns>Bool indicating whether the type can be resolved.</returns>
|
||||
public Boolean CanResolve<TResolveType>(String name = null, DependencyContainerResolveOptions options = null) where TResolveType : class => this.CanResolve(typeof(TResolveType), name, options);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to resolve a type using the default options.
|
||||
/// </summary>
|
||||
/// <param name="resolveType">Type to resolve.</param>
|
||||
/// <param name="resolvedType">Resolved type or default if resolve fails.</param>
|
||||
/// <returns><c>true</c> if resolved successfully, <c>false</c> otherwise.</returns>
|
||||
public Boolean TryResolve(Type resolveType, out Object resolvedType) {
|
||||
try {
|
||||
resolvedType = this.Resolve(resolveType);
|
||||
return true;
|
||||
} catch(DependencyContainerResolutionException) {
|
||||
resolvedType = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to resolve a type using the given options.
|
||||
/// </summary>
|
||||
/// <param name="resolveType">Type to resolve.</param>
|
||||
/// <param name="options">Resolution options.</param>
|
||||
/// <param name="resolvedType">Resolved type or default if resolve fails.</param>
|
||||
/// <returns><c>true</c> if resolved successfully, <c>false</c> otherwise.</returns>
|
||||
public Boolean TryResolve(Type resolveType, DependencyContainerResolveOptions options, out Object resolvedType) {
|
||||
try {
|
||||
resolvedType = this.Resolve(resolveType, options: options);
|
||||
return true;
|
||||
} catch(DependencyContainerResolutionException) {
|
||||
resolvedType = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to resolve a type using the default options and given name.
|
||||
/// </summary>
|
||||
/// <param name="resolveType">Type to resolve.</param>
|
||||
/// <param name="name">Name of registration.</param>
|
||||
/// <param name="resolvedType">Resolved type or default if resolve fails.</param>
|
||||
/// <returns><c>true</c> if resolved successfully, <c>false</c> otherwise.</returns>
|
||||
public Boolean TryResolve(Type resolveType, String name, out Object resolvedType) {
|
||||
try {
|
||||
resolvedType = this.Resolve(resolveType, name);
|
||||
return true;
|
||||
} catch(DependencyContainerResolutionException) {
|
||||
resolvedType = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to resolve a type using the given options and name.
|
||||
/// </summary>
|
||||
/// <param name="resolveType">Type to resolve.</param>
|
||||
/// <param name="name">Name of registration.</param>
|
||||
/// <param name="options">Resolution options.</param>
|
||||
/// <param name="resolvedType">Resolved type or default if resolve fails.</param>
|
||||
/// <returns><c>true</c> if resolved successfully, <c>false</c> otherwise.</returns>
|
||||
public Boolean TryResolve(Type resolveType, String name, DependencyContainerResolveOptions options, out Object resolvedType) {
|
||||
try {
|
||||
resolvedType = this.Resolve(resolveType, name, options);
|
||||
return true;
|
||||
} catch(DependencyContainerResolutionException) {
|
||||
resolvedType = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to resolve a type using the default options.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResolveType">Type to resolve.</typeparam>
|
||||
/// <param name="resolvedType">Resolved type or default if resolve fails.</param>
|
||||
/// <returns><c>true</c> if resolved successfully, <c>false</c> otherwise.</returns>
|
||||
public Boolean TryResolve<TResolveType>(out TResolveType resolvedType) where TResolveType : class {
|
||||
try {
|
||||
resolvedType = this.Resolve<TResolveType>();
|
||||
return true;
|
||||
} catch(DependencyContainerResolutionException) {
|
||||
resolvedType = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to resolve a type using the given options.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResolveType">Type to resolve.</typeparam>
|
||||
/// <param name="options">Resolution options.</param>
|
||||
/// <param name="resolvedType">Resolved type or default if resolve fails.</param>
|
||||
/// <returns><c>true</c> if resolved successfully, <c>false</c> otherwise.</returns>
|
||||
public Boolean TryResolve<TResolveType>(DependencyContainerResolveOptions options, out TResolveType resolvedType) where TResolveType : class {
|
||||
try {
|
||||
resolvedType = this.Resolve<TResolveType>(options: options);
|
||||
return true;
|
||||
} catch(DependencyContainerResolutionException) {
|
||||
resolvedType = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to resolve a type using the default options and given name.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResolveType">Type to resolve.</typeparam>
|
||||
/// <param name="name">Name of registration.</param>
|
||||
/// <param name="resolvedType">Resolved type or default if resolve fails.</param>
|
||||
/// <returns><c>true</c> if resolved successfully, <c>false</c> otherwise.</returns>
|
||||
public Boolean TryResolve<TResolveType>(String name, out TResolveType resolvedType) where TResolveType : class {
|
||||
try {
|
||||
resolvedType = this.Resolve<TResolveType>(name);
|
||||
return true;
|
||||
} catch(DependencyContainerResolutionException) {
|
||||
resolvedType = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to resolve a type using the given options and name.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResolveType">Type to resolve.</typeparam>
|
||||
/// <param name="name">Name of registration.</param>
|
||||
/// <param name="options">Resolution options.</param>
|
||||
/// <param name="resolvedType">Resolved type or default if resolve fails.</param>
|
||||
/// <returns><c>true</c> if resolved successfully, <c>false</c> otherwise.</returns>
|
||||
public Boolean TryResolve<TResolveType>(String name, DependencyContainerResolveOptions options, out TResolveType resolvedType) where TResolveType : class {
|
||||
try {
|
||||
resolvedType = this.Resolve<TResolveType>(name, options);
|
||||
return true;
|
||||
} catch(DependencyContainerResolutionException) {
|
||||
resolvedType = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all registrations of a type.
|
||||
/// </summary>
|
||||
/// <param name="resolveType">Type to resolveAll.</param>
|
||||
/// <param name="includeUnnamed">Whether to include un-named (default) registrations.</param>
|
||||
/// <returns>IEnumerable.</returns>
|
||||
public IEnumerable<Object> ResolveAll(Type resolveType, Boolean includeUnnamed = false) => this.RegisteredTypes.Resolve(resolveType, includeUnnamed);
|
||||
|
||||
/// <summary>
|
||||
/// Returns all registrations of a type.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResolveType">Type to resolveAll.</typeparam>
|
||||
/// <param name="includeUnnamed">Whether to include un-named (default) registrations.</param>
|
||||
/// <returns>IEnumerable.</returns>
|
||||
public IEnumerable<TResolveType> ResolveAll<TResolveType>(Boolean includeUnnamed = true) where TResolveType : class => this.ResolveAll(typeof(TResolveType), includeUnnamed).Cast<TResolveType>();
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to resolve all public property dependencies on the given object using the given resolve options.
|
||||
/// </summary>
|
||||
/// <param name="input">Object to "build up".</param>
|
||||
/// <param name="resolveOptions">Resolve options to use.</param>
|
||||
public void BuildUp(Object input, DependencyContainerResolveOptions resolveOptions = null) {
|
||||
if(resolveOptions == null) {
|
||||
resolveOptions = DependencyContainerResolveOptions.Default;
|
||||
}
|
||||
|
||||
IEnumerable<PropertyInfo> properties = input.GetType().GetProperties().Where(property => property.GetCacheGetMethod() != null && property.GetCacheSetMethod() != null && !property.PropertyType.IsValueType);
|
||||
|
||||
foreach(PropertyInfo property in properties.Where(property => property.GetValue(input, null) == null)) {
|
||||
try {
|
||||
property.SetValue(input, this.RegisteredTypes.ResolveInternal(new TypeRegistration(property.PropertyType), resolveOptions), null);
|
||||
} catch(DependencyContainerResolutionException) {
|
||||
// Catch any resolution errors and ignore them
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Internal Methods
|
||||
|
||||
internal static Boolean IsValidAssignment(Type registerType, Type registerImplementation) {
|
||||
if(!registerType.IsGenericTypeDefinition) {
|
||||
if(!registerType.IsAssignableFrom(registerImplementation)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if(registerType.IsInterface && registerImplementation.GetInterfaces().All(t => t.Name != registerType.Name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(registerType.IsAbstract && registerImplementation.BaseType != registerType) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Boolean IsIgnoredAssembly(Assembly assembly) {
|
||||
// TODO - find a better way to remove "system" assemblies from the auto registration
|
||||
List<Func<Assembly, Boolean>> ignoreChecks = new List<Func<Assembly, Boolean>>
|
||||
{
|
||||
asm => asm.FullName.StartsWith("Microsoft.", StringComparison.Ordinal),
|
||||
asm => asm.FullName.StartsWith("System.", StringComparison.Ordinal),
|
||||
asm => asm.FullName.StartsWith("System,", StringComparison.Ordinal),
|
||||
asm => asm.FullName.StartsWith("CR_ExtUnitTest", StringComparison.Ordinal),
|
||||
asm => asm.FullName.StartsWith("mscorlib,", StringComparison.Ordinal),
|
||||
asm => asm.FullName.StartsWith("CR_VSTest", StringComparison.Ordinal),
|
||||
asm => asm.FullName.StartsWith("DevExpress.CodeRush", StringComparison.Ordinal),
|
||||
asm => asm.FullName.StartsWith("xunit.", StringComparison.Ordinal),
|
||||
};
|
||||
|
||||
return ignoreChecks.Any(check => check(assembly));
|
||||
}
|
||||
|
||||
private static Boolean IsIgnoredType(Type type, Func<Type, Boolean> registrationPredicate) {
|
||||
// TODO - find a better way to remove "system" types from the auto registration
|
||||
List<Func<Type, Boolean>> ignoreChecks = new List<Func<Type, Boolean>>()
|
||||
{
|
||||
t => t.FullName?.StartsWith("System.", StringComparison.Ordinal) ?? false,
|
||||
t => t.FullName?.StartsWith("Microsoft.", StringComparison.Ordinal) ?? false,
|
||||
t => t.IsPrimitive,
|
||||
t => t.IsGenericTypeDefinition,
|
||||
t => t.GetConstructors(BindingFlags.Instance | BindingFlags.Public).Length == 0 &&
|
||||
!(t.IsInterface || t.IsAbstract),
|
||||
};
|
||||
|
||||
if(registrationPredicate != null) {
|
||||
ignoreChecks.Add(t => !registrationPredicate(t));
|
||||
}
|
||||
|
||||
return ignoreChecks.Any(check => check(type));
|
||||
}
|
||||
|
||||
private static ObjectFactoryBase GetDefaultObjectFactory(Type registerType, Type registerImplementation) => registerType.IsInterface || registerType.IsAbstract ? (ObjectFactoryBase)new SingletonFactory(registerType, registerImplementation) : new MultiInstanceFactory(registerType, registerImplementation);
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Swan.DependencyInjection {
|
||||
/// <summary>
|
||||
/// Generic Constraint Registration Exception.
|
||||
/// </summary>
|
||||
/// <seealso cref="Exception" />
|
||||
public class DependencyContainerRegistrationException : Exception {
|
||||
private const String ConvertErrorText = "Cannot convert current registration of {0} to {1}";
|
||||
private const String RegisterErrorText = "Cannot register type {0} - abstract classes or interfaces are not valid implementation types for {1}.";
|
||||
private const String ErrorText = "Duplicate implementation of type {0} found ({1}).";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DependencyContainerRegistrationException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="registerType">Type of the register.</param>
|
||||
/// <param name="types">The types.</param>
|
||||
public DependencyContainerRegistrationException(Type registerType, IEnumerable<Type> types) : base(String.Format(ErrorText, registerType, GetTypesString(types))) {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DependencyContainerRegistrationException" /> class.
|
||||
/// </summary>
|
||||
/// <param name="type">The type.</param>
|
||||
/// <param name="method">The method.</param>
|
||||
/// <param name="isTypeFactory">if set to <c>true</c> [is type factory].</param>
|
||||
public DependencyContainerRegistrationException(Type type, String method, Boolean isTypeFactory = false) : base(isTypeFactory ? String.Format(RegisterErrorText, type.FullName, method) : String.Format(ConvertErrorText, type.FullName, method)) {
|
||||
}
|
||||
|
||||
private static String GetTypesString(IEnumerable<Type> types) => String.Join(",", types.Select(type => type.FullName));
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
using System;
|
||||
|
||||
namespace Swan.DependencyInjection {
|
||||
/// <summary>
|
||||
/// An exception for dependency resolutions.
|
||||
/// </summary>
|
||||
/// <seealso cref="System.Exception" />
|
||||
[Serializable]
|
||||
public class DependencyContainerResolutionException : Exception {
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DependencyContainerResolutionException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="type">The type.</param>
|
||||
public DependencyContainerResolutionException(Type type) : base($"Unable to resolve type: {type.FullName}") {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DependencyContainerResolutionException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="type">The type.</param>
|
||||
/// <param name="innerException">The inner exception.</param>
|
||||
public DependencyContainerResolutionException(Type type, Exception innerException) : base($"Unable to resolve type: {type.FullName}", innerException) {
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,106 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Swan.DependencyInjection {
|
||||
/// <summary>
|
||||
/// Resolution settings.
|
||||
/// </summary>
|
||||
public class DependencyContainerResolveOptions {
|
||||
/// <summary>
|
||||
/// Gets the default options (attempt resolution of unregistered types, fail on named resolution if name not found).
|
||||
/// </summary>
|
||||
public static DependencyContainerResolveOptions Default { get; } = new DependencyContainerResolveOptions();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the unregistered resolution action.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The unregistered resolution action.
|
||||
/// </value>
|
||||
public DependencyContainerUnregisteredResolutionAction UnregisteredResolutionAction { get; set; } = DependencyContainerUnregisteredResolutionAction.AttemptResolve;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the named resolution failure action.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The named resolution failure action.
|
||||
/// </value>
|
||||
public DependencyContainerNamedResolutionFailureAction NamedResolutionFailureAction { get; set; } = DependencyContainerNamedResolutionFailureAction.Fail;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the constructor parameters.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The constructor parameters.
|
||||
/// </value>
|
||||
public Dictionary<System.String, System.Object> ConstructorParameters { get; } = new Dictionary<System.String, System.Object>();
|
||||
|
||||
/// <summary>
|
||||
/// Clones this instance.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public DependencyContainerResolveOptions Clone() => new DependencyContainerResolveOptions {
|
||||
NamedResolutionFailureAction = NamedResolutionFailureAction,
|
||||
UnregisteredResolutionAction = UnregisteredResolutionAction,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines Resolution actions.
|
||||
/// </summary>
|
||||
public enum DependencyContainerUnregisteredResolutionAction {
|
||||
/// <summary>
|
||||
/// Attempt to resolve type, even if the type isn't registered.
|
||||
///
|
||||
/// Registered types/options will always take precedence.
|
||||
/// </summary>
|
||||
AttemptResolve,
|
||||
|
||||
/// <summary>
|
||||
/// Fail resolution if type not explicitly registered
|
||||
/// </summary>
|
||||
Fail,
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to resolve unregistered type if requested type is generic
|
||||
/// and no registration exists for the specific generic parameters used.
|
||||
///
|
||||
/// Registered types/options will always take precedence.
|
||||
/// </summary>
|
||||
GenericsOnly,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates failure actions.
|
||||
/// </summary>
|
||||
public enum DependencyContainerNamedResolutionFailureAction {
|
||||
/// <summary>
|
||||
/// The attempt unnamed resolution
|
||||
/// </summary>
|
||||
AttemptUnnamedResolution,
|
||||
|
||||
/// <summary>
|
||||
/// The fail
|
||||
/// </summary>
|
||||
Fail,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates duplicate definition actions.
|
||||
/// </summary>
|
||||
public enum DependencyContainerDuplicateImplementationAction {
|
||||
/// <summary>
|
||||
/// The register single
|
||||
/// </summary>
|
||||
RegisterSingle,
|
||||
|
||||
/// <summary>
|
||||
/// The register multiple
|
||||
/// </summary>
|
||||
RegisterMultiple,
|
||||
|
||||
/// <summary>
|
||||
/// The fail
|
||||
/// </summary>
|
||||
Fail,
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
using System;
|
||||
|
||||
namespace Swan.DependencyInjection {
|
||||
/// <summary>
|
||||
/// Weak Reference Exception.
|
||||
/// </summary>
|
||||
/// <seealso cref="System.Exception" />
|
||||
public class DependencyContainerWeakReferenceException : Exception {
|
||||
private const String ErrorText = "Unable to instantiate {0} - referenced object has been reclaimed";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DependencyContainerWeakReferenceException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="type">The type.</param>
|
||||
public DependencyContainerWeakReferenceException(Type type) : base(String.Format(ErrorText, type.FullName)) {
|
||||
}
|
||||
}
|
||||
}
|
352
Swan.Tiny/DependencyInjection/ObjectFactoryBase.cs
Normal file
352
Swan.Tiny/DependencyInjection/ObjectFactoryBase.cs
Normal file
@ -0,0 +1,352 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Swan.DependencyInjection {
|
||||
/// <summary>
|
||||
/// Represents an abstract class for Object Factory.
|
||||
/// </summary>
|
||||
public abstract class ObjectFactoryBase {
|
||||
/// <summary>
|
||||
/// Whether to assume this factory successfully constructs its objects
|
||||
///
|
||||
/// Generally set to true for delegate style factories as CanResolve cannot delve
|
||||
/// into the delegates they contain.
|
||||
/// </summary>
|
||||
public virtual Boolean AssumeConstruction => false;
|
||||
|
||||
/// <summary>
|
||||
/// The type the factory instantiates.
|
||||
/// </summary>
|
||||
public abstract Type CreatesType {
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor to use, if specified.
|
||||
/// </summary>
|
||||
public ConstructorInfo Constructor {
|
||||
get; private set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the singleton variant.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The singleton variant.
|
||||
/// </value>
|
||||
/// <exception cref="DependencyContainerRegistrationException">singleton.</exception>
|
||||
public virtual ObjectFactoryBase SingletonVariant => throw new DependencyContainerRegistrationException(this.GetType(), "singleton");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the multi instance variant.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The multi instance variant.
|
||||
/// </value>
|
||||
/// <exception cref="DependencyContainerRegistrationException">multi-instance.</exception>
|
||||
public virtual ObjectFactoryBase MultiInstanceVariant => throw new DependencyContainerRegistrationException(this.GetType(), "multi-instance");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the strong reference variant.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The strong reference variant.
|
||||
/// </value>
|
||||
/// <exception cref="DependencyContainerRegistrationException">strong reference.</exception>
|
||||
public virtual ObjectFactoryBase StrongReferenceVariant => throw new DependencyContainerRegistrationException(this.GetType(), "strong reference");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the weak reference variant.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The weak reference variant.
|
||||
/// </value>
|
||||
/// <exception cref="DependencyContainerRegistrationException">weak reference.</exception>
|
||||
public virtual ObjectFactoryBase WeakReferenceVariant => throw new DependencyContainerRegistrationException(this.GetType(), "weak reference");
|
||||
|
||||
/// <summary>
|
||||
/// Create the type.
|
||||
/// </summary>
|
||||
/// <param name="requestedType">Type user requested to be resolved.</param>
|
||||
/// <param name="container">Container that requested the creation.</param>
|
||||
/// <param name="options">The options.</param>
|
||||
/// <returns> Instance of type. </returns>
|
||||
public abstract Object GetObject(Type requestedType, DependencyContainer container, DependencyContainerResolveOptions options);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the factory for child container.
|
||||
/// </summary>
|
||||
/// <param name="type">The type.</param>
|
||||
/// <param name="parent">The parent.</param>
|
||||
/// <param name="child">The child.</param>
|
||||
/// <returns></returns>
|
||||
public virtual ObjectFactoryBase GetFactoryForChildContainer(Type type, DependencyContainer parent, DependencyContainer child) => this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// IObjectFactory that creates new instances of types for each resolution.
|
||||
/// </summary>
|
||||
internal class MultiInstanceFactory : ObjectFactoryBase {
|
||||
private readonly Type _registerType;
|
||||
private readonly Type _registerImplementation;
|
||||
|
||||
public MultiInstanceFactory(Type registerType, Type registerImplementation) {
|
||||
if(registerImplementation.IsAbstract || registerImplementation.IsInterface) {
|
||||
throw new DependencyContainerRegistrationException(registerImplementation, "MultiInstanceFactory", true);
|
||||
}
|
||||
|
||||
if(!DependencyContainer.IsValidAssignment(registerType, registerImplementation)) {
|
||||
throw new DependencyContainerRegistrationException(registerImplementation, "MultiInstanceFactory", true);
|
||||
}
|
||||
|
||||
this._registerType = registerType;
|
||||
this._registerImplementation = registerImplementation;
|
||||
}
|
||||
|
||||
public override Type CreatesType => this._registerImplementation;
|
||||
|
||||
public override ObjectFactoryBase SingletonVariant =>
|
||||
new SingletonFactory(this._registerType, this._registerImplementation);
|
||||
|
||||
public override ObjectFactoryBase MultiInstanceVariant => this;
|
||||
|
||||
public override Object GetObject(Type requestedType, DependencyContainer container, DependencyContainerResolveOptions options) {
|
||||
try {
|
||||
return container.RegisteredTypes.ConstructType(this._registerImplementation, this.Constructor, options);
|
||||
} catch(DependencyContainerResolutionException ex) {
|
||||
throw new DependencyContainerResolutionException(this._registerType, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// IObjectFactory that invokes a specified delegate to construct the object.
|
||||
/// </summary>
|
||||
internal class DelegateFactory : ObjectFactoryBase {
|
||||
private readonly Type _registerType;
|
||||
|
||||
private readonly Func<DependencyContainer, Dictionary<String, Object>, Object> _factory;
|
||||
|
||||
public DelegateFactory(
|
||||
Type registerType,
|
||||
Func<DependencyContainer, Dictionary<String, Object>, Object> factory) {
|
||||
this._factory = factory ?? throw new ArgumentNullException(nameof(factory));
|
||||
|
||||
this._registerType = registerType;
|
||||
}
|
||||
|
||||
public override Boolean AssumeConstruction => true;
|
||||
|
||||
public override Type CreatesType => this._registerType;
|
||||
|
||||
public override ObjectFactoryBase WeakReferenceVariant => new WeakDelegateFactory(this._registerType, this._factory);
|
||||
|
||||
public override ObjectFactoryBase StrongReferenceVariant => this;
|
||||
|
||||
public override Object GetObject(Type requestedType, DependencyContainer container, DependencyContainerResolveOptions options) {
|
||||
try {
|
||||
return this._factory.Invoke(container, options.ConstructorParameters);
|
||||
} catch(Exception ex) {
|
||||
throw new DependencyContainerResolutionException(this._registerType, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// IObjectFactory that invokes a specified delegate to construct the object
|
||||
/// Holds the delegate using a weak reference.
|
||||
/// </summary>
|
||||
internal class WeakDelegateFactory : ObjectFactoryBase {
|
||||
private readonly Type _registerType;
|
||||
|
||||
private readonly WeakReference _factory;
|
||||
|
||||
public WeakDelegateFactory(Type registerType, Func<DependencyContainer, Dictionary<String, Object>, Object> factory) {
|
||||
if(factory == null) {
|
||||
throw new ArgumentNullException(nameof(factory));
|
||||
}
|
||||
|
||||
this._factory = new WeakReference(factory);
|
||||
|
||||
this._registerType = registerType;
|
||||
}
|
||||
|
||||
public override Boolean AssumeConstruction => true;
|
||||
|
||||
public override Type CreatesType => this._registerType;
|
||||
|
||||
public override ObjectFactoryBase StrongReferenceVariant {
|
||||
get {
|
||||
if(!(this._factory.Target is Func<DependencyContainer, Dictionary<global::System.String, global::System.Object>, global::System.Object> factory)) {
|
||||
throw new DependencyContainerWeakReferenceException(this._registerType);
|
||||
}
|
||||
|
||||
return new DelegateFactory(this._registerType, factory);
|
||||
}
|
||||
}
|
||||
|
||||
public override ObjectFactoryBase WeakReferenceVariant => this;
|
||||
|
||||
public override Object GetObject(Type requestedType, DependencyContainer container, DependencyContainerResolveOptions options) {
|
||||
if(!(this._factory.Target is Func<DependencyContainer, Dictionary<global::System.String, global::System.Object>, global::System.Object> factory)) {
|
||||
throw new DependencyContainerWeakReferenceException(this._registerType);
|
||||
}
|
||||
|
||||
try {
|
||||
return factory.Invoke(container, options.ConstructorParameters);
|
||||
} catch(Exception ex) {
|
||||
throw new DependencyContainerResolutionException(this._registerType, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores an particular instance to return for a type.
|
||||
/// </summary>
|
||||
internal class InstanceFactory : ObjectFactoryBase, IDisposable {
|
||||
private readonly Type _registerType;
|
||||
private readonly Type _registerImplementation;
|
||||
private readonly Object _instance;
|
||||
|
||||
public InstanceFactory(Type registerType, Type registerImplementation, Object instance) {
|
||||
if(!DependencyContainer.IsValidAssignment(registerType, registerImplementation)) {
|
||||
throw new DependencyContainerRegistrationException(registerImplementation, "InstanceFactory", true);
|
||||
}
|
||||
|
||||
this._registerType = registerType;
|
||||
this._registerImplementation = registerImplementation;
|
||||
this._instance = instance;
|
||||
}
|
||||
|
||||
public override Boolean AssumeConstruction => true;
|
||||
|
||||
public override Type CreatesType => this._registerImplementation;
|
||||
|
||||
public override ObjectFactoryBase MultiInstanceVariant => new MultiInstanceFactory(this._registerType, this._registerImplementation);
|
||||
|
||||
public override ObjectFactoryBase WeakReferenceVariant => new WeakInstanceFactory(this._registerType, this._registerImplementation, this._instance);
|
||||
|
||||
public override ObjectFactoryBase StrongReferenceVariant => this;
|
||||
|
||||
public override Object GetObject(Type requestedType, DependencyContainer container, DependencyContainerResolveOptions options) => this._instance;
|
||||
|
||||
public void Dispose() {
|
||||
IDisposable disposable = this._instance as IDisposable;
|
||||
|
||||
disposable?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores the instance with a weak reference.
|
||||
/// </summary>
|
||||
internal class WeakInstanceFactory : ObjectFactoryBase, IDisposable {
|
||||
private readonly Type _registerType;
|
||||
private readonly Type _registerImplementation;
|
||||
private readonly WeakReference _instance;
|
||||
|
||||
public WeakInstanceFactory(Type registerType, Type registerImplementation, Object instance) {
|
||||
if(!DependencyContainer.IsValidAssignment(registerType, registerImplementation)) {
|
||||
throw new DependencyContainerRegistrationException(registerImplementation, "WeakInstanceFactory", true);
|
||||
}
|
||||
|
||||
this._registerType = registerType;
|
||||
this._registerImplementation = registerImplementation;
|
||||
this._instance = new WeakReference(instance);
|
||||
}
|
||||
|
||||
public override Type CreatesType => this._registerImplementation;
|
||||
|
||||
public override ObjectFactoryBase MultiInstanceVariant => new MultiInstanceFactory(this._registerType, this._registerImplementation);
|
||||
|
||||
public override ObjectFactoryBase WeakReferenceVariant => this;
|
||||
|
||||
public override ObjectFactoryBase StrongReferenceVariant {
|
||||
get {
|
||||
Object instance = this._instance.Target;
|
||||
|
||||
if(instance == null) {
|
||||
throw new DependencyContainerWeakReferenceException(this._registerType);
|
||||
}
|
||||
|
||||
return new InstanceFactory(this._registerType, this._registerImplementation, instance);
|
||||
}
|
||||
}
|
||||
|
||||
public override Object GetObject(Type requestedType, DependencyContainer container, DependencyContainerResolveOptions options) {
|
||||
Object instance = this._instance.Target;
|
||||
|
||||
if(instance == null) {
|
||||
throw new DependencyContainerWeakReferenceException(this._registerType);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
public void Dispose() => (this._instance.Target as IDisposable)?.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A factory that lazy instantiates a type and always returns the same instance.
|
||||
/// </summary>
|
||||
internal class SingletonFactory : ObjectFactoryBase, IDisposable {
|
||||
private readonly Type _registerType;
|
||||
private readonly Type _registerImplementation;
|
||||
private readonly Object _singletonLock = new Object();
|
||||
private Object _current;
|
||||
|
||||
public SingletonFactory(Type registerType, Type registerImplementation) {
|
||||
if(registerImplementation.IsAbstract || registerImplementation.IsInterface) {
|
||||
throw new DependencyContainerRegistrationException(registerImplementation, nameof(SingletonFactory), true);
|
||||
}
|
||||
|
||||
if(!DependencyContainer.IsValidAssignment(registerType, registerImplementation)) {
|
||||
throw new DependencyContainerRegistrationException(registerImplementation, nameof(SingletonFactory), true);
|
||||
}
|
||||
|
||||
this._registerType = registerType;
|
||||
this._registerImplementation = registerImplementation;
|
||||
}
|
||||
|
||||
public override Type CreatesType => this._registerImplementation;
|
||||
|
||||
public override ObjectFactoryBase SingletonVariant => this;
|
||||
|
||||
public override ObjectFactoryBase MultiInstanceVariant =>
|
||||
new MultiInstanceFactory(this._registerType, this._registerImplementation);
|
||||
|
||||
public override Object GetObject(
|
||||
Type requestedType,
|
||||
DependencyContainer container,
|
||||
DependencyContainerResolveOptions options) {
|
||||
if(options.ConstructorParameters.Count != 0) {
|
||||
throw new ArgumentException("Cannot specify parameters for singleton types");
|
||||
}
|
||||
|
||||
lock(this._singletonLock) {
|
||||
if(this._current == null) {
|
||||
this._current = container.RegisteredTypes.ConstructType(this._registerImplementation, this.Constructor, options);
|
||||
}
|
||||
}
|
||||
|
||||
return this._current;
|
||||
}
|
||||
|
||||
public override ObjectFactoryBase GetFactoryForChildContainer(
|
||||
Type type,
|
||||
DependencyContainer parent,
|
||||
DependencyContainer child) {
|
||||
// We make sure that the singleton is constructed before the child container takes the factory.
|
||||
// Otherwise the results would vary depending on whether or not the parent container had resolved
|
||||
// the type before the child container does.
|
||||
_ = this.GetObject(type, parent, DependencyContainerResolveOptions.Default);
|
||||
return this;
|
||||
}
|
||||
|
||||
public void Dispose() => (this._current as IDisposable)?.Dispose();
|
||||
}
|
||||
}
|
119
Swan.Tiny/DependencyInjection/RegisterOptions.cs
Normal file
119
Swan.Tiny/DependencyInjection/RegisterOptions.cs
Normal file
@ -0,0 +1,119 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Swan.DependencyInjection {
|
||||
/// <summary>
|
||||
/// Registration options for "fluent" API.
|
||||
/// </summary>
|
||||
public sealed class RegisterOptions {
|
||||
private readonly TypesConcurrentDictionary _registeredTypes;
|
||||
private readonly DependencyContainer.TypeRegistration _registration;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RegisterOptions" /> class.
|
||||
/// </summary>
|
||||
/// <param name="registeredTypes">The registered types.</param>
|
||||
/// <param name="registration">The registration.</param>
|
||||
public RegisterOptions(TypesConcurrentDictionary registeredTypes, DependencyContainer.TypeRegistration registration) {
|
||||
this._registeredTypes = registeredTypes;
|
||||
this._registration = registration;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Make registration a singleton (single instance) if possible.
|
||||
/// </summary>
|
||||
/// <returns>A registration options for fluent API.</returns>
|
||||
/// <exception cref="DependencyContainerRegistrationException">Generic constraint registration exception.</exception>
|
||||
public RegisterOptions AsSingleton() {
|
||||
ObjectFactoryBase currentFactory = this._registeredTypes.GetCurrentFactory(this._registration);
|
||||
|
||||
if(currentFactory == null) {
|
||||
throw new DependencyContainerRegistrationException(this._registration.Type, "singleton");
|
||||
}
|
||||
|
||||
return this._registeredTypes.AddUpdateRegistration(this._registration, currentFactory.SingletonVariant);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Make registration multi-instance if possible.
|
||||
/// </summary>
|
||||
/// <returns>A registration options for fluent API.</returns>
|
||||
/// <exception cref="DependencyContainerRegistrationException">Generic constraint registration exception.</exception>
|
||||
public RegisterOptions AsMultiInstance() {
|
||||
ObjectFactoryBase currentFactory = this._registeredTypes.GetCurrentFactory(this._registration);
|
||||
|
||||
if(currentFactory == null) {
|
||||
throw new DependencyContainerRegistrationException(this._registration.Type, "multi-instance");
|
||||
}
|
||||
|
||||
return this._registeredTypes.AddUpdateRegistration(this._registration, currentFactory.MultiInstanceVariant);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Make registration hold a weak reference if possible.
|
||||
/// </summary>
|
||||
/// <returns>A registration options for fluent API.</returns>
|
||||
/// <exception cref="DependencyContainerRegistrationException">Generic constraint registration exception.</exception>
|
||||
public RegisterOptions WithWeakReference() {
|
||||
ObjectFactoryBase currentFactory = this._registeredTypes.GetCurrentFactory(this._registration);
|
||||
|
||||
if(currentFactory == null) {
|
||||
throw new DependencyContainerRegistrationException(this._registration.Type, "weak reference");
|
||||
}
|
||||
|
||||
return this._registeredTypes.AddUpdateRegistration(this._registration, currentFactory.WeakReferenceVariant);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Make registration hold a strong reference if possible.
|
||||
/// </summary>
|
||||
/// <returns>A registration options for fluent API.</returns>
|
||||
/// <exception cref="DependencyContainerRegistrationException">Generic constraint registration exception.</exception>
|
||||
public RegisterOptions WithStrongReference() {
|
||||
ObjectFactoryBase currentFactory = this._registeredTypes.GetCurrentFactory(this._registration);
|
||||
|
||||
if(currentFactory == null) {
|
||||
throw new DependencyContainerRegistrationException(this._registration.Type, "strong reference");
|
||||
}
|
||||
|
||||
return this._registeredTypes.AddUpdateRegistration(this._registration, currentFactory.StrongReferenceVariant);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registration options for "fluent" API when registering multiple implementations.
|
||||
/// </summary>
|
||||
public sealed class MultiRegisterOptions {
|
||||
private IEnumerable<RegisterOptions> _registerOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MultiRegisterOptions"/> class.
|
||||
/// </summary>
|
||||
/// <param name="registerOptions">The register options.</param>
|
||||
public MultiRegisterOptions(IEnumerable<RegisterOptions> registerOptions) => this._registerOptions = registerOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Make registration a singleton (single instance) if possible.
|
||||
/// </summary>
|
||||
/// <returns>A registration multi-instance for fluent API.</returns>
|
||||
/// <exception cref="DependencyContainerRegistrationException">Generic Constraint Registration Exception.</exception>
|
||||
public MultiRegisterOptions AsSingleton() {
|
||||
this._registerOptions = this.ExecuteOnAllRegisterOptions(ro => ro.AsSingleton());
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Make registration multi-instance if possible.
|
||||
/// </summary>
|
||||
/// <returns>A registration multi-instance for fluent API.</returns>
|
||||
/// <exception cref="DependencyContainerRegistrationException">Generic Constraint Registration Exception.</exception>
|
||||
public MultiRegisterOptions AsMultiInstance() {
|
||||
this._registerOptions = this.ExecuteOnAllRegisterOptions(ro => ro.AsMultiInstance());
|
||||
return this;
|
||||
}
|
||||
|
||||
private IEnumerable<RegisterOptions> ExecuteOnAllRegisterOptions(
|
||||
Func<RegisterOptions, RegisterOptions> action) => this._registerOptions.Select(action).ToList();
|
||||
}
|
||||
}
|
61
Swan.Tiny/DependencyInjection/TypeRegistration.cs
Normal file
61
Swan.Tiny/DependencyInjection/TypeRegistration.cs
Normal file
@ -0,0 +1,61 @@
|
||||
using System;
|
||||
|
||||
namespace Swan.DependencyInjection {
|
||||
public partial class DependencyContainer {
|
||||
/// <summary>
|
||||
/// Represents a Type Registration within the IoC Container.
|
||||
/// </summary>
|
||||
public sealed class TypeRegistration {
|
||||
private readonly Int32 _hashCode;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TypeRegistration"/> class.
|
||||
/// </summary>
|
||||
/// <param name="type">The type.</param>
|
||||
/// <param name="name">The name.</param>
|
||||
public TypeRegistration(Type type, String name = null) {
|
||||
this.Type = type;
|
||||
this.Name = name ?? String.Empty;
|
||||
|
||||
this._hashCode = String.Concat(this.Type.FullName, "|", this.Name).GetHashCode();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The type.
|
||||
/// </value>
|
||||
public Type Type {
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The name.
|
||||
/// </value>
|
||||
public String Name {
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified <see cref="System.Object" />, is equal to this instance.
|
||||
/// </summary>
|
||||
/// <param name="obj">The <see cref="System.Object" /> to compare with this instance.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the specified <see cref="System.Object" /> is equal to this instance; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public override Boolean Equals(Object obj) => !(obj is TypeRegistration typeRegistration) || typeRegistration.Type != this.Type ? false : String.Compare(this.Name, typeRegistration.Name, StringComparison.Ordinal) == 0;
|
||||
|
||||
/// <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 Int32 GetHashCode() => this._hashCode;
|
||||
}
|
||||
}
|
||||
}
|
265
Swan.Tiny/DependencyInjection/TypesConcurrentDictionary.cs
Normal file
265
Swan.Tiny/DependencyInjection/TypesConcurrentDictionary.cs
Normal file
@ -0,0 +1,265 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Swan.DependencyInjection {
|
||||
/// <summary>
|
||||
/// Represents a Concurrent Dictionary for TypeRegistration.
|
||||
/// </summary>
|
||||
public class TypesConcurrentDictionary : ConcurrentDictionary<DependencyContainer.TypeRegistration, ObjectFactoryBase> {
|
||||
private static readonly ConcurrentDictionary<ConstructorInfo, ObjectConstructor> ObjectConstructorCache = new ConcurrentDictionary<ConstructorInfo, ObjectConstructor>();
|
||||
|
||||
private readonly DependencyContainer _dependencyContainer;
|
||||
|
||||
internal TypesConcurrentDictionary(DependencyContainer dependencyContainer) => this._dependencyContainer = dependencyContainer;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a delegate to build an object with the parameters.
|
||||
/// </summary>
|
||||
/// <param name="parameters">The parameters.</param>
|
||||
/// <returns>The built object.</returns>
|
||||
public delegate Object ObjectConstructor(params Object?[] parameters);
|
||||
|
||||
internal IEnumerable<Object> Resolve(Type resolveType, Boolean includeUnnamed) {
|
||||
IEnumerable<DependencyContainer.TypeRegistration> registrations = this.Keys.Where(tr => tr.Type == resolveType).Concat(this.GetParentRegistrationsForType(resolveType)).Distinct();
|
||||
|
||||
if(!includeUnnamed) {
|
||||
registrations = registrations.Where(tr => !String.IsNullOrEmpty(tr.Name));
|
||||
}
|
||||
|
||||
return registrations.Select(registration => this.ResolveInternal(registration, DependencyContainerResolveOptions.Default));
|
||||
}
|
||||
|
||||
internal ObjectFactoryBase GetCurrentFactory(DependencyContainer.TypeRegistration registration) {
|
||||
_ = this.TryGetValue(registration, out ObjectFactoryBase? current);
|
||||
|
||||
return current!;
|
||||
}
|
||||
|
||||
internal RegisterOptions Register(Type registerType, String name, ObjectFactoryBase factory) => this.AddUpdateRegistration(new DependencyContainer.TypeRegistration(registerType, name), factory);
|
||||
|
||||
internal RegisterOptions AddUpdateRegistration(DependencyContainer.TypeRegistration typeRegistration, ObjectFactoryBase factory) {
|
||||
this[typeRegistration] = factory;
|
||||
|
||||
return new RegisterOptions(this, typeRegistration);
|
||||
}
|
||||
|
||||
internal Boolean RemoveRegistration(DependencyContainer.TypeRegistration typeRegistration) => this.TryRemove(typeRegistration, out _);
|
||||
|
||||
internal Object ResolveInternal(DependencyContainer.TypeRegistration registration, DependencyContainerResolveOptions? options = null) {
|
||||
if(options == null) {
|
||||
options = DependencyContainerResolveOptions.Default;
|
||||
}
|
||||
|
||||
// Attempt container resolution
|
||||
if(this.TryGetValue(registration, out ObjectFactoryBase? factory)) {
|
||||
try {
|
||||
return factory.GetObject(registration.Type, this._dependencyContainer, options);
|
||||
} catch(DependencyContainerResolutionException) {
|
||||
throw;
|
||||
} catch(Exception ex) {
|
||||
throw new DependencyContainerResolutionException(registration.Type, ex);
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to get a factory from parent if we can
|
||||
ObjectFactoryBase? bubbledObjectFactory = this.GetParentObjectFactory(registration);
|
||||
if(bubbledObjectFactory != null) {
|
||||
try {
|
||||
return bubbledObjectFactory.GetObject(registration.Type, this._dependencyContainer, options);
|
||||
} catch(DependencyContainerResolutionException) {
|
||||
throw;
|
||||
} catch(Exception ex) {
|
||||
throw new DependencyContainerResolutionException(registration.Type, ex);
|
||||
}
|
||||
}
|
||||
|
||||
// Fail if requesting named resolution and settings set to fail if unresolved
|
||||
if(!String.IsNullOrEmpty(registration.Name) && options.NamedResolutionFailureAction == DependencyContainerNamedResolutionFailureAction.Fail) {
|
||||
throw new DependencyContainerResolutionException(registration.Type);
|
||||
}
|
||||
|
||||
// Attempted unnamed fallback container resolution if relevant and requested
|
||||
if(!String.IsNullOrEmpty(registration.Name) && options.NamedResolutionFailureAction == DependencyContainerNamedResolutionFailureAction.AttemptUnnamedResolution) {
|
||||
if(this.TryGetValue(new DependencyContainer.TypeRegistration(registration.Type, String.Empty), out factory)) {
|
||||
try {
|
||||
return factory.GetObject(registration.Type, this._dependencyContainer, options);
|
||||
} catch(DependencyContainerResolutionException) {
|
||||
throw;
|
||||
} catch(Exception ex) {
|
||||
throw new DependencyContainerResolutionException(registration.Type, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt unregistered construction if possible and requested
|
||||
Boolean isValid = options.UnregisteredResolutionAction == DependencyContainerUnregisteredResolutionAction.AttemptResolve || registration.Type.IsGenericType && options.UnregisteredResolutionAction == DependencyContainerUnregisteredResolutionAction.GenericsOnly;
|
||||
|
||||
return isValid && !registration.Type.IsAbstract && !registration.Type.IsInterface ? this.ConstructType(registration.Type, null, options) : throw new DependencyContainerResolutionException(registration.Type);
|
||||
}
|
||||
|
||||
internal Boolean CanResolve(DependencyContainer.TypeRegistration registration, DependencyContainerResolveOptions? options = null) {
|
||||
if(options == null) {
|
||||
options = DependencyContainerResolveOptions.Default;
|
||||
}
|
||||
|
||||
Type checkType = registration.Type;
|
||||
String name = registration.Name;
|
||||
|
||||
if(this.TryGetValue(new DependencyContainer.TypeRegistration(checkType, name), out ObjectFactoryBase? factory)) {
|
||||
return factory.AssumeConstruction ? true : factory.Constructor == null ? this.GetBestConstructor(factory.CreatesType, options) != null : this.CanConstruct(factory.Constructor, options);
|
||||
}
|
||||
|
||||
// Fail if requesting named resolution and settings set to fail if unresolved
|
||||
// Or bubble up if we have a parent
|
||||
if(!String.IsNullOrEmpty(name) && options.NamedResolutionFailureAction == DependencyContainerNamedResolutionFailureAction.Fail) {
|
||||
return this._dependencyContainer.Parent?.RegisteredTypes.CanResolve(registration, options.Clone()) ?? false;
|
||||
}
|
||||
|
||||
// Attempted unnamed fallback container resolution if relevant and requested
|
||||
if(!String.IsNullOrEmpty(name) && options.NamedResolutionFailureAction == DependencyContainerNamedResolutionFailureAction.AttemptUnnamedResolution) {
|
||||
if(this.TryGetValue(new DependencyContainer.TypeRegistration(checkType), out factory)) {
|
||||
return factory.AssumeConstruction ? true : this.GetBestConstructor(factory.CreatesType, options) != null;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if type is an automatic lazy factory request or an IEnumerable<ResolveType>
|
||||
if(IsAutomaticLazyFactoryRequest(checkType) || registration.Type.IsIEnumerable()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Attempt unregistered construction if possible and requested
|
||||
// If we cant', bubble if we have a parent
|
||||
if(options.UnregisteredResolutionAction == DependencyContainerUnregisteredResolutionAction.AttemptResolve || checkType.IsGenericType && options.UnregisteredResolutionAction == DependencyContainerUnregisteredResolutionAction.GenericsOnly) {
|
||||
return this.GetBestConstructor(checkType, options) != null || (this._dependencyContainer.Parent?.RegisteredTypes.CanResolve(registration, options.Clone()) ?? false);
|
||||
}
|
||||
|
||||
// Bubble resolution up the container tree if we have a parent
|
||||
return this._dependencyContainer.Parent != null && this._dependencyContainer.Parent.RegisteredTypes.CanResolve(registration, options.Clone());
|
||||
}
|
||||
|
||||
internal Object ConstructType(Type implementationType, ConstructorInfo? constructor, DependencyContainerResolveOptions? options = null) {
|
||||
Type typeToConstruct = implementationType;
|
||||
|
||||
if(constructor == null) {
|
||||
// Try and get the best constructor that we can construct
|
||||
// if we can't construct any then get the constructor
|
||||
// with the least number of parameters so we can throw a meaningful
|
||||
// resolve exception
|
||||
constructor = this.GetBestConstructor(typeToConstruct, options) ?? GetTypeConstructors(typeToConstruct).LastOrDefault();
|
||||
}
|
||||
|
||||
if(constructor == null) {
|
||||
throw new DependencyContainerResolutionException(typeToConstruct);
|
||||
}
|
||||
|
||||
ParameterInfo[] ctorParams = constructor.GetParameters();
|
||||
Object?[] args = new Object?[ctorParams.Length];
|
||||
|
||||
for(Int32 parameterIndex = 0; parameterIndex < ctorParams.Length; parameterIndex++) {
|
||||
ParameterInfo currentParam = ctorParams[parameterIndex];
|
||||
|
||||
try {
|
||||
args[parameterIndex] = options?.ConstructorParameters.GetValueOrDefault(currentParam.Name, this.ResolveInternal(new DependencyContainer.TypeRegistration(currentParam.ParameterType), options.Clone()));
|
||||
} catch(DependencyContainerResolutionException ex) {
|
||||
// If a constructor parameter can't be resolved
|
||||
// it will throw, so wrap it and throw that this can't
|
||||
// be resolved.
|
||||
throw new DependencyContainerResolutionException(typeToConstruct, ex);
|
||||
} catch(Exception ex) {
|
||||
throw new DependencyContainerResolutionException(typeToConstruct, ex);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return CreateObjectConstructionDelegateWithCache(constructor).Invoke(args);
|
||||
} catch(Exception ex) {
|
||||
throw new DependencyContainerResolutionException(typeToConstruct, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static ObjectConstructor CreateObjectConstructionDelegateWithCache(ConstructorInfo constructor) {
|
||||
if(ObjectConstructorCache.TryGetValue(constructor, out ObjectConstructor? objectConstructor)) {
|
||||
return objectConstructor;
|
||||
}
|
||||
|
||||
// We could lock the cache here, but there's no real side
|
||||
// effect to two threads creating the same ObjectConstructor
|
||||
// at the same time, compared to the cost of a lock for
|
||||
// every creation.
|
||||
ParameterInfo[] constructorParams = constructor.GetParameters();
|
||||
ParameterExpression lambdaParams = Expression.Parameter(typeof(Object[]), "parameters");
|
||||
Expression[] newParams = new Expression[constructorParams.Length];
|
||||
|
||||
for(Int32 i = 0; i < constructorParams.Length; i++) {
|
||||
BinaryExpression paramsParameter = Expression.ArrayIndex(lambdaParams, Expression.Constant(i));
|
||||
|
||||
newParams[i] = Expression.Convert(paramsParameter, constructorParams[i].ParameterType);
|
||||
}
|
||||
|
||||
NewExpression newExpression = Expression.New(constructor, newParams);
|
||||
|
||||
LambdaExpression constructionLambda = Expression.Lambda(typeof(ObjectConstructor), newExpression, lambdaParams);
|
||||
|
||||
objectConstructor = (ObjectConstructor)constructionLambda.Compile();
|
||||
|
||||
ObjectConstructorCache[constructor] = objectConstructor;
|
||||
return objectConstructor;
|
||||
}
|
||||
|
||||
private static IEnumerable<ConstructorInfo> GetTypeConstructors(Type type) => type.GetConstructors().OrderByDescending(ctor => ctor.GetParameters().Length);
|
||||
|
||||
private static Boolean IsAutomaticLazyFactoryRequest(Type type) {
|
||||
if(!type.IsGenericType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Type genericType = type.GetGenericTypeDefinition();
|
||||
|
||||
// Just a func
|
||||
if(genericType == typeof(Func<>)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2 parameter func with string as first parameter (name)
|
||||
if(genericType == typeof(Func<,>) && type.GetGenericArguments()[0] == typeof(String)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 3 parameter func with string as first parameter (name) and IDictionary<string, object> as second (parameters)
|
||||
return genericType == typeof(Func<,,>) && type.GetGenericArguments()[0] == typeof(String) && type.GetGenericArguments()[1] == typeof(IDictionary<String, Object>);
|
||||
}
|
||||
|
||||
private ObjectFactoryBase? GetParentObjectFactory(DependencyContainer.TypeRegistration registration) => this._dependencyContainer.Parent == null
|
||||
? null
|
||||
: this._dependencyContainer.Parent.RegisteredTypes.TryGetValue(registration, out ObjectFactoryBase? factory) ? factory.GetFactoryForChildContainer(registration.Type, this._dependencyContainer.Parent, this._dependencyContainer) : this._dependencyContainer.Parent.RegisteredTypes.GetParentObjectFactory(registration);
|
||||
|
||||
private ConstructorInfo? GetBestConstructor(Type type, DependencyContainerResolveOptions? options) => type.IsValueType ? null : GetTypeConstructors(type).FirstOrDefault(ctor => this.CanConstruct(ctor, options));
|
||||
|
||||
private Boolean CanConstruct(MethodBase ctor, DependencyContainerResolveOptions? options) {
|
||||
foreach(ParameterInfo parameter in ctor.GetParameters()) {
|
||||
if(String.IsNullOrEmpty(parameter.Name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Boolean isParameterOverload = options!.ConstructorParameters.ContainsKey(parameter.Name);
|
||||
|
||||
if(parameter.ParameterType.IsPrimitive && !isParameterOverload) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!isParameterOverload && !this.CanResolve(new DependencyContainer.TypeRegistration(parameter.ParameterType), options.Clone())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private IEnumerable<DependencyContainer.TypeRegistration> GetParentRegistrationsForType(Type resolveType) => this._dependencyContainer.Parent == null ? Array.Empty<DependencyContainer.TypeRegistration>() : this._dependencyContainer.Parent.RegisteredTypes.Keys.Where(tr => tr.Type == resolveType).Concat(this._dependencyContainer.Parent.RegisteredTypes.GetParentRegistrationsForType(resolveType));
|
||||
}
|
||||
}
|
30
Swan.Tiny/Diagnostics/HighResolutionTimer.cs
Normal file
30
Swan.Tiny/Diagnostics/HighResolutionTimer.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Swan.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 Int64 ElapsedMicroseconds => (Int64)(this.ElapsedTicks * MicrosecondsPerTick);
|
||||
}
|
||||
}
|
61
Swan.Tiny/Enums.cs
Normal file
61
Swan.Tiny/Enums.cs
Normal file
@ -0,0 +1,61 @@
|
||||
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,
|
||||
}
|
||||
}
|
498
Swan.Tiny/Extensions.ByteArrays.cs
Normal file
498
Swan.Tiny/Extensions.ByteArrays.cs
Normal file
@ -0,0 +1,498 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Swan {
|
||||
/// <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, Boolean 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, Boolean 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) {
|
||||
Int32 mask = ~(0xff << length);
|
||||
Byte 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, Int32 offset, params Byte[] sequence) {
|
||||
if(@this == null) {
|
||||
throw new ArgumentNullException(nameof(@this));
|
||||
}
|
||||
|
||||
if(sequence == null) {
|
||||
throw new ArgumentNullException(nameof(sequence));
|
||||
}
|
||||
|
||||
Int32 seqOffset = offset.Clamp(0, @this.Length - 1);
|
||||
|
||||
List<Byte[]> result = new List<Byte[]>();
|
||||
|
||||
while(seqOffset < @this.Length) {
|
||||
Int32 separatorStartIndex = @this.GetIndexOf(sequence, seqOffset);
|
||||
|
||||
if(separatorStartIndex >= 0) {
|
||||
Byte[] item = new Byte[separatorStartIndex - seqOffset + sequence.Length];
|
||||
Array.Copy(@this, seqOffset, item, 0, item.Length);
|
||||
result.Add(item);
|
||||
seqOffset += item.Length;
|
||||
} else {
|
||||
Byte[] item = new Byte[@this.Length - seqOffset];
|
||||
Array.Copy(@this, seqOffset, item, 0, item.Length);
|
||||
result.Add(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <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));
|
||||
}
|
||||
|
||||
Byte[] 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();
|
||||
}
|
||||
|
||||
Byte[] 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();
|
||||
}
|
||||
|
||||
Byte[] 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) {
|
||||
Byte[] 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 Boolean EndsWith(this Byte[] buffer, params Byte[] sequence) {
|
||||
if(buffer == null) {
|
||||
throw new ArgumentNullException(nameof(buffer));
|
||||
}
|
||||
|
||||
Int32 startIndex = buffer.Length - sequence.Length;
|
||||
return buffer.GetIndexOf(sequence, startIndex) == startIndex;
|
||||
}
|
||||
|
||||
/// <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 Boolean 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 Boolean 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 Boolean IsEqualTo(this Byte[] buffer, params Byte[] sequence) {
|
||||
if(ReferenceEquals(buffer, sequence)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if(buffer == null) {
|
||||
throw new ArgumentNullException(nameof(buffer));
|
||||
}
|
||||
|
||||
return buffer.Length == sequence.Length && buffer.GetIndexOf(sequence) == 0;
|
||||
}
|
||||
|
||||
/// <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 Int32 GetIndexOf(this Byte[] buffer, Byte[] sequence, Int32 offset = 0) {
|
||||
if(buffer == null) {
|
||||
throw new ArgumentNullException(nameof(buffer));
|
||||
}
|
||||
|
||||
if(sequence == null) {
|
||||
throw new ArgumentNullException(nameof(sequence));
|
||||
}
|
||||
|
||||
if(sequence.Length == 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(sequence.Length > buffer.Length) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
Int32 seqOffset = offset < 0 ? 0 : offset;
|
||||
|
||||
Int32 matchedCount = 0;
|
||||
for(Int32 i = seqOffset; i < buffer.Length; i++) {
|
||||
if(buffer[i] == sequence[matchedCount]) {
|
||||
matchedCount++;
|
||||
} else {
|
||||
matchedCount = 0;
|
||||
}
|
||||
|
||||
if(matchedCount == sequence.Length) {
|
||||
return i - (matchedCount - 1);
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <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(Byte[] 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, Int64 length, Int32 bufferLength, CancellationToken cancellationToken = default) {
|
||||
if(stream == null) {
|
||||
throw new ArgumentNullException(nameof(stream));
|
||||
}
|
||||
|
||||
using MemoryStream dest = new MemoryStream();
|
||||
try {
|
||||
Byte[] buff = new Byte[bufferLength];
|
||||
while(length > 0) {
|
||||
if(length < bufferLength) {
|
||||
bufferLength = (Int32)length;
|
||||
}
|
||||
|
||||
Int32 nread = await stream.ReadAsync(buff, 0, bufferLength, cancellationToken).ConfigureAwait(false);
|
||||
if(nread == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
dest.Write(buff, 0, nread);
|
||||
length -= nread;
|
||||
}
|
||||
} catch {
|
||||
// ignored
|
||||
}
|
||||
|
||||
return dest.ToArray();
|
||||
}
|
||||
|
||||
/// <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, Int32 length, CancellationToken cancellationToken = default) {
|
||||
if(stream == null) {
|
||||
throw new ArgumentNullException(nameof(stream));
|
||||
}
|
||||
|
||||
Byte[] buff = new Byte[length];
|
||||
Int32 offset = 0;
|
||||
try {
|
||||
while(length > 0) {
|
||||
Int32 nread = await stream.ReadAsync(buff, offset, length, cancellationToken).ConfigureAwait(false);
|
||||
if(nread == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
offset += nread;
|
||||
length -= nread;
|
||||
}
|
||||
} catch {
|
||||
// ignored
|
||||
}
|
||||
|
||||
return new ArraySegment<Byte>(buff, 0, offset).ToArray();
|
||||
}
|
||||
|
||||
private static String ToHex(Byte[] bytes, Boolean addPrefix, String format) {
|
||||
if(bytes == null) {
|
||||
throw new ArgumentNullException(nameof(bytes));
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder(bytes.Length * 2);
|
||||
|
||||
foreach(Byte item in bytes) {
|
||||
_ = sb.Append(item.ToString(format, CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
return $"{(addPrefix ? "0x" : String.Empty)}{sb}";
|
||||
}
|
||||
}
|
||||
}
|
86
Swan.Tiny/Extensions.Dictionaries.cs
Normal file
86
Swan.Tiny/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)) {
|
||||
TValue 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(KeyValuePair<TKey, TValue> kvp in dict) {
|
||||
itemAction(kvp.Key, kvp.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
173
Swan.Tiny/Extensions.Functional.cs
Normal file
173
Swan.Tiny/Extensions.Functional.cs
Normal file
@ -0,0 +1,173 @@
|
||||
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<Boolean> 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<Boolean> 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<Boolean> 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, Boolean 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<Boolean> 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;
|
||||
}
|
||||
}
|
||||
}
|
411
Swan.Tiny/Extensions.Reflection.cs
Normal file
411
Swan.Tiny/Extensions.Reflection.cs
Normal file
@ -0,0 +1,411 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
using Swan.Configuration;
|
||||
using Swan.Reflection;
|
||||
|
||||
namespace Swan {
|
||||
/// <summary>
|
||||
/// Provides various extension methods for Reflection and Types.
|
||||
/// </summary>
|
||||
public static class ReflectionExtensions {
|
||||
private static readonly Lazy<ConcurrentDictionary<Tuple<Boolean, PropertyInfo>, Func<Object, Object>>> CacheGetMethods = new Lazy<ConcurrentDictionary<Tuple<Boolean, PropertyInfo>, Func<Object, Object>>>(() => new ConcurrentDictionary<Tuple<Boolean, PropertyInfo>, Func<Object, Object>>(), true);
|
||||
|
||||
private static readonly Lazy<ConcurrentDictionary<Tuple<Boolean, PropertyInfo>, Action<Object, Object[]>>> CacheSetMethods = new Lazy<ConcurrentDictionary<Tuple<Boolean, PropertyInfo>, Action<Object, Object[]>>>(() => new ConcurrentDictionary<Tuple<Boolean, 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 Boolean 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));
|
||||
}
|
||||
|
||||
List<MethodInfo> 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 Boolean 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 Boolean TryParseBasicType(this Type type, Object value, out Object? result) {
|
||||
if(type == null) {
|
||||
throw new ArgumentNullException(nameof(type));
|
||||
}
|
||||
|
||||
if(type != typeof(Boolean)) {
|
||||
return TryParseBasicType(type, value.ToStringInvariant(), out result);
|
||||
}
|
||||
|
||||
result = value.ToBoolean();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <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 Boolean TryParseBasicType(this Type type, String value, out Object? result) {
|
||||
if(type == null) {
|
||||
throw new ArgumentNullException(nameof(type));
|
||||
}
|
||||
|
||||
result = null;
|
||||
|
||||
return Definitions.BasicTypesInfo.Value.ContainsKey(type) && Definitions.BasicTypesInfo.Value[type].TryParse(value, out result);
|
||||
}
|
||||
|
||||
/// <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 Boolean TrySetBasicType(this PropertyInfo propertyInfo, Object value, Object target) {
|
||||
if(propertyInfo == null) {
|
||||
throw new ArgumentNullException(nameof(propertyInfo));
|
||||
}
|
||||
|
||||
try {
|
||||
if(propertyInfo.PropertyType.TryParseBasicType(value, out Object? propertyValue)) {
|
||||
propertyInfo.SetValue(target, propertyValue);
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// swallow
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <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 Boolean TrySetArrayBasicType(this Type type, Object value, Array target, Int32 index) {
|
||||
if(type == null) {
|
||||
throw new ArgumentNullException(nameof(type));
|
||||
}
|
||||
|
||||
if(target == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if(value == null) {
|
||||
target.SetValue(null, index);
|
||||
return true;
|
||||
}
|
||||
|
||||
if(type.TryParseBasicType(value, out Object? propertyValue)) {
|
||||
target.SetValue(propertyValue, index);
|
||||
return true;
|
||||
}
|
||||
|
||||
if(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) {
|
||||
target.SetValue(null, index);
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// swallow
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <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 Boolean TrySetArray(this PropertyInfo propertyInfo, IEnumerable<Object>? value, Object obj) {
|
||||
if(propertyInfo == null) {
|
||||
throw new ArgumentNullException(nameof(propertyInfo));
|
||||
}
|
||||
|
||||
Type? elementType = propertyInfo.PropertyType.GetElementType();
|
||||
|
||||
if(elementType == null || value == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Array targetArray = Array.CreateInstance(elementType, value.Count());
|
||||
|
||||
Int32 i = 0;
|
||||
|
||||
foreach(Object sourceElement in value) {
|
||||
Boolean result = elementType.TrySetArrayBasicType(sourceElement, targetArray, i++);
|
||||
|
||||
if(!result) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
propertyInfo.SetValue(obj, targetArray);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <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 {
|
||||
Object? value = propertyInfo.GetValue(target);
|
||||
PropertyDisplayAttribute attr = AttributeCache.DefaultCache.Value.RetrieveOne<PropertyDisplayAttribute>(propertyInfo);
|
||||
|
||||
if(attr == null) {
|
||||
return value?.ToString() ?? String.Empty;
|
||||
}
|
||||
|
||||
Object valueToFormat = value ?? attr.DefaultValue;
|
||||
|
||||
return String.IsNullOrEmpty(attr.Format) ? (valueToFormat?.ToString() ?? String.Empty) : ConvertObjectAndFormat(propertyInfo.PropertyType, valueToFormat, attr.Format);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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, Boolean nonPublic = false) {
|
||||
Tuple<Boolean, PropertyInfo> 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, Boolean nonPublic = false) {
|
||||
Tuple<Boolean, PropertyInfo> 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 Boolean 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));
|
||||
}
|
||||
|
||||
Type 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 Boolean ToBoolean(this Object value) => value.ToStringInvariant().ToBoolean();
|
||||
|
||||
private static String ConvertObjectAndFormat(Type propertyType, Object value, String format) =>
|
||||
propertyType == typeof(DateTime) || propertyType == typeof(DateTime?)
|
||||
? Convert.ToDateTime(value, CultureInfo.InvariantCulture).ToString(format)
|
||||
: propertyType == typeof(Int32) || propertyType == typeof(Int32?)
|
||||
? Convert.ToInt32(value, CultureInfo.InvariantCulture).ToString(format)
|
||||
: propertyType == typeof(Decimal) || propertyType == typeof(Decimal?)
|
||||
? Convert.ToDecimal(value, CultureInfo.InvariantCulture).ToString(format)
|
||||
: propertyType == typeof(Double) || propertyType == typeof(Double?)
|
||||
? Convert.ToDouble(value, CultureInfo.InvariantCulture).ToString(format)
|
||||
: propertyType == typeof(Byte) || propertyType == typeof(Byte?)
|
||||
? Convert.ToByte(value, CultureInfo.InvariantCulture).ToString(format)
|
||||
: value?.ToString() ?? String.Empty;
|
||||
}
|
||||
}
|
364
Swan.Tiny/Extensions.Strings.cs
Normal file
364
Swan.Tiny/Extensions.Strings.cs
Normal file
@ -0,0 +1,364 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using Swan.Formatters;
|
||||
|
||||
namespace Swan {
|
||||
/// <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 => {
|
||||
String x = m.ToString();
|
||||
return x[0] + " " + x[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;
|
||||
}
|
||||
|
||||
Type itemType = @this.GetType();
|
||||
|
||||
return itemType == typeof(String) ? @this as String ?? String.Empty : Definitions.BasicTypesInfo.Value.ContainsKey(itemType) ? Definitions.BasicTypesInfo.Value[itemType].ToStringInvariant(@this) : @this.ToString()!;
|
||||
}
|
||||
|
||||
/// <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, Boolean 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 {
|
||||
String jsonText = Json.Serialize(@this, false, "$type");
|
||||
Object? 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, Int32 startIndex, Int32 endIndex) {
|
||||
if(@this == null) {
|
||||
return String.Empty;
|
||||
}
|
||||
|
||||
Int32 end = endIndex.Clamp(startIndex, @this.Length - 1);
|
||||
|
||||
return startIndex >= end ? String.Empty : @this.Substring(startIndex, end - startIndex + 1);
|
||||
}
|
||||
|
||||
/// <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, Int32 startIndex, Int32 length) {
|
||||
if(@this == null) {
|
||||
return String.Empty;
|
||||
}
|
||||
|
||||
Int32 start = startIndex.Clamp(0, @this.Length - 1);
|
||||
Int32 len = length.Clamp(0, @this.Length - start);
|
||||
|
||||
return len == 0 ? String.Empty : @this.Substring(start, len);
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
|
||||
String 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 Boolean 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(),
|
||||
Boolean 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, Int32 spaces = 4) {
|
||||
if(value == null) {
|
||||
value = String.Empty;
|
||||
}
|
||||
|
||||
if(spaces <= 0) {
|
||||
return value;
|
||||
}
|
||||
|
||||
String[] lines = value.ToLines();
|
||||
StringBuilder builder = new StringBuilder();
|
||||
String indentStr = new String(' ', spaces);
|
||||
|
||||
foreach(String line in lines) {
|
||||
_ = builder.AppendLine($"{indentStr}{line}");
|
||||
}
|
||||
|
||||
return builder.ToString().TrimEnd();
|
||||
}
|
||||
|
||||
/// <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<Int32, Int32> TextPositionAt(this String value, Int32 charIndex) {
|
||||
if(value == null) {
|
||||
return Tuple.Create(0, 0);
|
||||
}
|
||||
|
||||
Int32 index = charIndex.Clamp(0, value.Length - 1);
|
||||
|
||||
Int32 lineIndex = 0;
|
||||
Int32 colNumber = 0;
|
||||
|
||||
for(Int32 i = 0; i <= index; i++) {
|
||||
if(value[i] == '\n') {
|
||||
lineIndex++;
|
||||
colNumber = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
if(value[i] != '\r') {
|
||||
colNumber++;
|
||||
}
|
||||
}
|
||||
|
||||
return Tuple.Create(lineIndex + 1, colNumber);
|
||||
}
|
||||
|
||||
/// <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 Int64 bytes) => ((UInt64)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 UInt64 bytes) {
|
||||
Int32 i;
|
||||
Double dblSByte = bytes;
|
||||
|
||||
for(i = 0; i < ByteSuffixes.Length && bytes >= 1024; i++, bytes /= 1024) {
|
||||
dblSByte = bytes / 1024.0;
|
||||
}
|
||||
|
||||
return $"{dblSByte:0.##} {ByteSuffixes[i]}";
|
||||
}
|
||||
|
||||
/// <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, Int32 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, Int32 maximumLength, String omission) => value == null ? null : 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 Boolean Contains(this String value, params Char[] chars) => chars?.Length == 0 || !String.IsNullOrEmpty(value) && chars != null && 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 Int32 Hex2Int(this Char value) => value >= '0' && value <= '9' ? value - '0' : value >= 'A' && value <= 'F' ? value - 'A' + 10 : value >= 'a' && value <= 'f' ? value - 'a' + 10 : -1;
|
||||
}
|
||||
}
|
135
Swan.Tiny/Extensions.ValueTypes.cs
Normal file
135
Swan.Tiny/Extensions.ValueTypes.cs
Normal file
@ -0,0 +1,135 @@
|
||||
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 => @this.CompareTo(min) < 0 ? min : @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 Int32 Clamp(this Int32 @this, Int32 min, Int32 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 Boolean IsBetween<T>(this T @this, T min, T max) where T : struct, IComparable => @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 => @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, Int32 offset, Int32 length) where T : struct {
|
||||
if(@this == null) {
|
||||
throw new ArgumentNullException(nameof(@this));
|
||||
}
|
||||
|
||||
Byte[] buffer = new Byte[length];
|
||||
Array.Copy(@this, offset, buffer, 0, buffer.Length);
|
||||
GCHandle handle = GCHandle.Alloc(GetStructBytes<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 {
|
||||
Byte[] data = new Byte[Marshal.SizeOf(@this)];
|
||||
GCHandle 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 UInt32 SwapEndianness(this UInt64 @this) => (UInt32)(((@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));
|
||||
}
|
||||
|
||||
FieldInfo[] fields = typeof(T).GetTypeInfo()
|
||||
.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
StructEndiannessAttribute endian = AttributeCache.DefaultCache.Value.RetrieveOne<StructEndiannessAttribute, T>();
|
||||
|
||||
foreach(FieldInfo field in fields) {
|
||||
if(endian == null && !field.IsDefined(typeof(StructEndiannessAttribute), false)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Int32 offset = Marshal.OffsetOf<T>(field.Name).ToInt32();
|
||||
Int32 length = Marshal.SizeOf(field.FieldType);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
230
Swan.Tiny/Extensions.cs
Normal file
230
Swan.Tiny/Extensions.cs
Normal file
@ -0,0 +1,230 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Swan.Lite.Reflection;
|
||||
using Swan.Mappers;
|
||||
using Swan.Reflection;
|
||||
|
||||
namespace Swan {
|
||||
/// <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 Int32 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 Int32 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));
|
||||
}
|
||||
|
||||
T 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));
|
||||
}
|
||||
|
||||
T 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 Int32 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));
|
||||
}
|
||||
|
||||
T 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, Int32 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, Int32 retryCount = 3) {
|
||||
if(action == null) {
|
||||
throw new ArgumentNullException(nameof(action));
|
||||
}
|
||||
|
||||
if(retryInterval == default) {
|
||||
retryInterval = TimeSpan.FromSeconds(1);
|
||||
}
|
||||
|
||||
global::System.Collections.Generic.List<global::System.Exception> exceptions = new List<Exception>();
|
||||
|
||||
for(Int32 retry = 0; retry < retryCount; retry++) {
|
||||
try {
|
||||
if(retry > 0) {
|
||||
Task.Delay(retryInterval).Wait();
|
||||
}
|
||||
|
||||
return action();
|
||||
} catch(Exception ex) {
|
||||
exceptions.Add(ex);
|
||||
}
|
||||
}
|
||||
|
||||
throw new AggregateException(exceptions);
|
||||
}
|
||||
|
||||
/// <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));
|
||||
}
|
||||
|
||||
global::System.Collections.Generic.IEnumerable<global::System.Reflection.PropertyInfo> collection = PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties(@this.GetType(), true);
|
||||
|
||||
global::System.Collections.Generic.IEnumerable<global::System.String> 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, Boolean includeNonPublic, ref Object? target) {
|
||||
switch(source) {
|
||||
// do nothing. Simply skip creation
|
||||
case String _:
|
||||
break;
|
||||
// When using arrays, there is no default constructor, attempt to build a compatible array
|
||||
case IList sourceObjectList when targetType.IsArray:
|
||||
Type? elementType = targetType.GetElementType();
|
||||
|
||||
if(elementType != null) {
|
||||
target = Array.CreateInstance(elementType, sourceObjectList.Count);
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
IEnumerable<Tuple<System.Reflection.ConstructorInfo, System.Reflection.ParameterInfo[]>> constructors = ConstructorTypeCache.DefaultCache.Value.RetrieveAllConstructors(targetType, includeNonPublic);
|
||||
|
||||
// Try to check if empty constructor is available
|
||||
if(constructors.Any(x => x.Item2.Length == 0)) {
|
||||
target = Activator.CreateInstance(targetType, includeNonPublic);
|
||||
} else {
|
||||
Tuple<System.Reflection.ConstructorInfo, System.Reflection.ParameterInfo[]> 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)
|
||||
};
|
||||
}
|
||||
}
|
126
Swan.Tiny/Formatters/HumanizeJson.cs
Normal file
126
Swan.Tiny/Formatters/HumanizeJson.cs
Normal file
@ -0,0 +1,126 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System;
|
||||
|
||||
namespace Swan.Formatters {
|
||||
internal class HumanizeJson {
|
||||
private readonly StringBuilder _builder = new StringBuilder();
|
||||
private readonly Int32 _indent;
|
||||
private readonly String _indentStr;
|
||||
private readonly Object _obj;
|
||||
|
||||
public HumanizeJson(Object obj, Int32 indent) {
|
||||
if(obj == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._indent = indent;
|
||||
this._indentStr = new String(' ', indent * 4);
|
||||
this._obj = obj;
|
||||
|
||||
this.ParseObject();
|
||||
}
|
||||
|
||||
public String GetResult() => this._builder == null ? String.Empty : this._builder.ToString().TrimEnd();
|
||||
|
||||
private void ParseObject() {
|
||||
switch(this._obj) {
|
||||
case Dictionary<String, Object> dictionary:
|
||||
this.AppendDictionary(dictionary);
|
||||
break;
|
||||
case List<Object> list:
|
||||
this.AppendList(list);
|
||||
break;
|
||||
default:
|
||||
this.AppendString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void AppendDictionary(Dictionary<String, Object> objects) {
|
||||
foreach(KeyValuePair<String, Object> kvp in objects) {
|
||||
if(kvp.Value == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Boolean writeOutput = false;
|
||||
|
||||
switch(kvp.Value) {
|
||||
case Dictionary<String, Object> valueDictionary:
|
||||
if(valueDictionary.Count > 0) {
|
||||
writeOutput = true;
|
||||
_ = this._builder.Append($"{this._indentStr}{kvp.Key,-16}: object").AppendLine();
|
||||
}
|
||||
|
||||
break;
|
||||
case List<Object> valueList:
|
||||
if(valueList.Count > 0) {
|
||||
writeOutput = true;
|
||||
_ = this._builder.Append($"{this._indentStr}{kvp.Key,-16}: array[{valueList.Count}]").AppendLine();
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
writeOutput = true;
|
||||
_ = this._builder.Append($"{this._indentStr}{kvp.Key,-16}: ");
|
||||
break;
|
||||
}
|
||||
|
||||
if(writeOutput) {
|
||||
_ = this._builder.AppendLine(new HumanizeJson(kvp.Value, this._indent + 1).GetResult());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AppendList(List<Object> objects) {
|
||||
Int32 index = 0;
|
||||
foreach(Object value in objects) {
|
||||
Boolean writeOutput = false;
|
||||
|
||||
switch(value) {
|
||||
case Dictionary<String, Object> valueDictionary:
|
||||
if(valueDictionary.Count > 0) {
|
||||
writeOutput = true;
|
||||
_ = this._builder.Append($"{this._indentStr}[{index}]: object").AppendLine();
|
||||
}
|
||||
|
||||
break;
|
||||
case List<Object> valueList:
|
||||
if(valueList.Count > 0) {
|
||||
writeOutput = true;
|
||||
_ = this._builder.Append($"{this._indentStr}[{index}]: array[{valueList.Count}]").AppendLine();
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
writeOutput = true;
|
||||
_ = this._builder.Append($"{this._indentStr}[{index}]: ");
|
||||
break;
|
||||
}
|
||||
|
||||
index++;
|
||||
|
||||
if(writeOutput) {
|
||||
_ = this._builder.AppendLine(new HumanizeJson(value, this._indent + 1).GetResult());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AppendString() {
|
||||
String stringValue = this._obj.ToString();
|
||||
|
||||
if(stringValue.Length + this._indentStr.Length > 96 || stringValue.IndexOf('\r') >= 0 ||
|
||||
stringValue.IndexOf('\n') >= 0) {
|
||||
_ = this._builder.AppendLine();
|
||||
IEnumerable<String> stringLines = stringValue.ToLines().Select(l => l.Trim());
|
||||
|
||||
foreach(String line in stringLines) {
|
||||
_ = this._builder.AppendLine($"{this._indentStr}{line}");
|
||||
}
|
||||
} else {
|
||||
_ = this._builder.Append($"{stringValue}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
259
Swan.Tiny/Formatters/Json.Converter.cs
Normal file
259
Swan.Tiny/Formatters/Json.Converter.cs
Normal file
@ -0,0 +1,259 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using Swan.Reflection;
|
||||
|
||||
namespace Swan.Formatters {
|
||||
/// <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, global::System.String>();
|
||||
|
||||
private static readonly ConcurrentDictionary<Type, Type> ListAddMethodCache = new ConcurrentDictionary<Type, Type>();
|
||||
|
||||
private readonly Object? _target;
|
||||
private readonly Type _targetType;
|
||||
private readonly Boolean _includeNonPublic;
|
||||
private readonly JsonSerializerCase _jsonSerializerCase;
|
||||
|
||||
private Converter(Object? source, Type targetType, ref Object? targetInstance, Boolean includeNonPublic, JsonSerializerCase jsonSerializerCase) {
|
||||
this._targetType = targetInstance != null ? targetInstance.GetType() : targetType;
|
||||
this._includeNonPublic = includeNonPublic;
|
||||
this._jsonSerializerCase = jsonSerializerCase;
|
||||
|
||||
if(source == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Type sourceType = source.GetType();
|
||||
|
||||
if(this._targetType == null || this._targetType == typeof(Object)) {
|
||||
this._targetType = sourceType;
|
||||
}
|
||||
|
||||
if(sourceType == this._targetType) {
|
||||
this._target = source;
|
||||
return;
|
||||
}
|
||||
|
||||
if(!this.TrySetInstance(targetInstance, source, ref this._target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.ResolveObject(source, ref this._target);
|
||||
}
|
||||
|
||||
internal static Object? FromJsonResult(Object? source, JsonSerializerCase jsonSerializerCase, Type? targetType = null, Boolean includeNonPublic = false) {
|
||||
Object? nullRef = null;
|
||||
return new Converter(source, targetType ?? typeof(Object), ref nullRef, includeNonPublic, jsonSerializerCase).GetResult();
|
||||
}
|
||||
|
||||
private static Object? FromJsonResult(Object source, Type targetType, ref Object? targetInstance, Boolean includeNonPublic) => new Converter(source, targetType, ref targetInstance, includeNonPublic, JsonSerializerCase.None).GetResult();
|
||||
|
||||
private static Type? GetAddMethodParameterType(Type targetType) => ListAddMethodCache.GetOrAdd(targetType, x => x.GetMethods().FirstOrDefault(m => m.Name == AddMethodName && m.IsPublic && m.GetParameters().Length == 1)?.GetParameters()[0].ParameterType!);
|
||||
|
||||
private static void GetByteArray(String sourceString, ref Object? target) {
|
||||
try {
|
||||
target = Convert.FromBase64String(sourceString);
|
||||
} // Try conversion from Base 64
|
||||
catch(FormatException) {
|
||||
target = Encoding.UTF8.GetBytes(sourceString);
|
||||
} // Get the string bytes in UTF8
|
||||
}
|
||||
|
||||
private Object GetSourcePropertyValue(IDictionary<String, Object> sourceProperties, MemberInfo targetProperty) {
|
||||
String targetPropertyName = MemberInfoNameCache.GetOrAdd(targetProperty, x => AttributeCache.DefaultCache.Value.RetrieveOne<JsonPropertyAttribute>(x)?.PropertyName ?? x.Name.GetNameWithCase(this._jsonSerializerCase));
|
||||
|
||||
return sourceProperties.GetValueOrDefault(targetPropertyName);
|
||||
}
|
||||
|
||||
private Boolean TrySetInstance(Object? targetInstance, Object source, ref Object? target) {
|
||||
if(targetInstance == null) {
|
||||
// Try to create a default instance
|
||||
try {
|
||||
source.CreateTarget(this._targetType, this._includeNonPublic, ref target);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
target = targetInstance;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private Object? GetResult() => this._target ?? this._targetType.GetDefault();
|
||||
|
||||
private void ResolveObject(Object source, ref Object? target) {
|
||||
switch(source) {
|
||||
// Case 0: Special Cases Handling (Source and Target are of specific convertible types)
|
||||
// Case 0.1: Source is string, Target is byte[]
|
||||
case String sourceString when this._targetType == typeof(Byte[]):
|
||||
GetByteArray(sourceString, ref target);
|
||||
break;
|
||||
|
||||
// Case 1.1: Source is Dictionary, Target is IDictionary
|
||||
case Dictionary<String, Object> sourceProperties when target is IDictionary targetDictionary:
|
||||
this.PopulateDictionary(sourceProperties, targetDictionary);
|
||||
break;
|
||||
|
||||
// Case 1.2: Source is Dictionary, Target is not IDictionary (i.e. it is a complex type)
|
||||
case Dictionary<String, Object> sourceProperties:
|
||||
this.PopulateObject(sourceProperties);
|
||||
break;
|
||||
|
||||
// Case 2.1: Source is List, Target is Array
|
||||
case List<Object> sourceList when target is Array targetArray:
|
||||
this.PopulateArray(sourceList, targetArray);
|
||||
break;
|
||||
|
||||
// Case 2.2: Source is List, Target is IList
|
||||
case List<Object> sourceList when target is IList targetList:
|
||||
this.PopulateIList(sourceList, targetList);
|
||||
break;
|
||||
|
||||
// Case 3: Source is a simple type; Attempt conversion
|
||||
default:
|
||||
String sourceStringValue = source.ToStringInvariant();
|
||||
|
||||
// Handle basic types or enumerations if not
|
||||
if(!this._targetType.TryParseBasicType(sourceStringValue, out target)) {
|
||||
this.GetEnumValue(sourceStringValue, ref target);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void PopulateIList(IEnumerable<Object> objects, IList list) {
|
||||
Type? parameterType = GetAddMethodParameterType(this._targetType);
|
||||
if(parameterType == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach(Object item in objects) {
|
||||
try {
|
||||
_ = list.Add(FromJsonResult(item, this._jsonSerializerCase, parameterType, this._includeNonPublic));
|
||||
} catch {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void PopulateArray(IList<Object> objects, Array array) {
|
||||
Type? elementType = this._targetType.GetElementType();
|
||||
|
||||
for(Int32 i = 0; i < objects.Count; i++) {
|
||||
try {
|
||||
Object? targetItem = FromJsonResult(objects[i], this._jsonSerializerCase, elementType, this._includeNonPublic);
|
||||
array.SetValue(targetItem, i);
|
||||
} catch {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void GetEnumValue(String sourceStringValue, ref Object? target) {
|
||||
Type? enumType = Nullable.GetUnderlyingType(this._targetType);
|
||||
if(enumType == null && this._targetType.IsEnum) {
|
||||
enumType = this._targetType;
|
||||
}
|
||||
|
||||
if(enumType == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
target = Enum.Parse(enumType, sourceStringValue);
|
||||
} catch {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
private void PopulateDictionary(IDictionary<String, Object> sourceProperties, IDictionary targetDictionary) {
|
||||
// find the add method of the target dictionary
|
||||
MethodInfo addMethod = this._targetType.GetMethods().FirstOrDefault(m => m.Name == AddMethodName && m.IsPublic && m.GetParameters().Length == 2);
|
||||
|
||||
// skip if we don't have a compatible add method
|
||||
if(addMethod == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
global::System.Reflection.ParameterInfo[] addMethodParameters = addMethod.GetParameters();
|
||||
if(addMethodParameters[0].ParameterType != typeof(String)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Retrieve the target entry type
|
||||
Type targetEntryType = addMethodParameters[1].ParameterType;
|
||||
|
||||
// Add the items to the target dictionary
|
||||
foreach(KeyValuePair<String, Object> sourceProperty in sourceProperties) {
|
||||
try {
|
||||
Object? targetEntryValue = FromJsonResult(sourceProperty.Value, this._jsonSerializerCase, targetEntryType, this._includeNonPublic);
|
||||
targetDictionary.Add(sourceProperty.Key, targetEntryValue);
|
||||
} catch {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void PopulateObject(IDictionary<String, Object> sourceProperties) {
|
||||
if(this._targetType.IsValueType) {
|
||||
this.PopulateFields(sourceProperties);
|
||||
}
|
||||
|
||||
this.PopulateProperties(sourceProperties);
|
||||
}
|
||||
|
||||
private void PopulateProperties(IDictionary<String, Object> sourceProperties) {
|
||||
global::System.Collections.Generic.IEnumerable<global::System.Reflection.PropertyInfo> properties = PropertyTypeCache.DefaultCache.Value.RetrieveFilteredProperties(this._targetType, false, p => p.CanWrite);
|
||||
|
||||
foreach(PropertyInfo property in properties) {
|
||||
Object sourcePropertyValue = this.GetSourcePropertyValue(sourceProperties, property);
|
||||
if(sourcePropertyValue == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
Object? currentPropertyValue = !property.PropertyType.IsArray ? property?.GetCacheGetMethod(this._includeNonPublic)!(this._target!) : null;
|
||||
|
||||
Object? targetPropertyValue = FromJsonResult(sourcePropertyValue, property.PropertyType, ref currentPropertyValue, this._includeNonPublic);
|
||||
|
||||
property?.GetCacheSetMethod(this._includeNonPublic)!(this._target!, new[] { targetPropertyValue }!);
|
||||
} catch {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void PopulateFields(IDictionary<String, Object> sourceProperties) {
|
||||
foreach(FieldInfo field in FieldTypeCache.DefaultCache.Value.RetrieveAllFields(this._targetType)) {
|
||||
Object sourcePropertyValue = this.GetSourcePropertyValue(sourceProperties, field);
|
||||
if(sourcePropertyValue == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Object? targetPropertyValue = FromJsonResult(sourcePropertyValue, this._jsonSerializerCase, field.FieldType, this._includeNonPublic);
|
||||
|
||||
try {
|
||||
field.SetValue(this._target, targetPropertyValue);
|
||||
} catch {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
332
Swan.Tiny/Formatters/Json.Deserializer.cs
Normal file
332
Swan.Tiny/Formatters/Json.Deserializer.cs
Normal file
@ -0,0 +1,332 @@
|
||||
#nullable enable
|
||||
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 Int32 _index;
|
||||
|
||||
#endregion
|
||||
|
||||
private Deserializer(String? json, Int32 startIndex) {
|
||||
if(json == null) {
|
||||
this._json = "";
|
||||
return;
|
||||
}
|
||||
this._json = json;
|
||||
|
||||
for(this._index = startIndex; this._index < this._json.Length; this._index++) {
|
||||
switch(this._state) {
|
||||
case ReadState.WaitingForRootOpen:
|
||||
this.WaitForRootOpen();
|
||||
continue;
|
||||
case ReadState.WaitingForField when Char.IsWhiteSpace(this._json, this._index):
|
||||
continue;
|
||||
case ReadState.WaitingForField when this._resultObject != null && this._json[this._index] == CloseObjectChar || this._resultArray != null && this._json[this._index] == CloseArrayChar:
|
||||
// Handle empty arrays and empty objects
|
||||
this._result = this._resultObject ?? this._resultArray as Object;
|
||||
return;
|
||||
case ReadState.WaitingForField when this._json[this._index] != StringQuotedChar:
|
||||
throw this.CreateParserException($"'{StringQuotedChar}'");
|
||||
case ReadState.WaitingForField: {
|
||||
Int32 charCount = this.GetFieldNameCount();
|
||||
|
||||
this._currentFieldName = Unescape(this._json.SliceLength(this._index + 1, charCount));
|
||||
this._index += charCount + 1;
|
||||
this._state = ReadState.WaitingForColon;
|
||||
continue;
|
||||
}
|
||||
|
||||
case ReadState.WaitingForColon when Char.IsWhiteSpace(this._json, this._index):
|
||||
continue;
|
||||
case ReadState.WaitingForColon when this._json[this._index] != ValueSeparatorChar:
|
||||
throw this.CreateParserException($"'{ValueSeparatorChar}'");
|
||||
case ReadState.WaitingForColon:
|
||||
this._state = ReadState.WaitingForValue;
|
||||
continue;
|
||||
case ReadState.WaitingForValue when Char.IsWhiteSpace(this._json, this._index):
|
||||
continue;
|
||||
case ReadState.WaitingForValue when this._resultObject != null && this._json[this._index] == CloseObjectChar || this._resultArray != null && this._json[this._index] == CloseArrayChar:
|
||||
// Handle empty arrays and empty objects
|
||||
this._result = this._resultObject ?? this._resultArray as Object;
|
||||
return;
|
||||
case ReadState.WaitingForValue:
|
||||
this.ExtractValue();
|
||||
continue;
|
||||
}
|
||||
|
||||
if(this._state != ReadState.WaitingForNextOrRootClose || Char.IsWhiteSpace(this._json, this._index)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if(this._json[this._index] == FieldSeparatorChar) {
|
||||
if(this._resultObject != null) {
|
||||
this._state = ReadState.WaitingForField;
|
||||
this._currentFieldName = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
this._state = ReadState.WaitingForValue;
|
||||
continue;
|
||||
}
|
||||
|
||||
if((this._resultObject == null || this._json[this._index] != CloseObjectChar) && (this._resultArray == null || this._json[this._index] != CloseArrayChar)) {
|
||||
throw this.CreateParserException($"'{FieldSeparatorChar}' '{CloseObjectChar}' or '{CloseArrayChar}'");
|
||||
}
|
||||
|
||||
this._result = this._resultObject ?? this._resultArray as Object;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
internal static Object? DeserializeInternal(String? json) => new Deserializer(json, 0)._result;
|
||||
|
||||
private void WaitForRootOpen() {
|
||||
if(Char.IsWhiteSpace(this._json, this._index)) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch(this._json[this._index]) {
|
||||
case OpenObjectChar:
|
||||
this._resultObject = new Dictionary<String, Object?>();
|
||||
this._state = ReadState.WaitingForField;
|
||||
return;
|
||||
case OpenArrayChar:
|
||||
this._resultArray = new List<Object?>();
|
||||
this._state = ReadState.WaitingForValue;
|
||||
return;
|
||||
default:
|
||||
throw this.CreateParserException($"'{OpenObjectChar}' or '{OpenArrayChar}'");
|
||||
}
|
||||
}
|
||||
|
||||
private void ExtractValue() {
|
||||
// determine the value based on what it starts with
|
||||
switch(this._json[this._index]) {
|
||||
case StringQuotedChar: // expect a string
|
||||
this.ExtractStringQuoted();
|
||||
break;
|
||||
|
||||
case OpenObjectChar: // expect object
|
||||
case OpenArrayChar: // expect array
|
||||
this.ExtractObject();
|
||||
break;
|
||||
|
||||
case 't': // expect true
|
||||
this.ExtractConstant(TrueLiteral, true);
|
||||
break;
|
||||
|
||||
case 'f': // expect false
|
||||
this.ExtractConstant(FalseLiteral, false);
|
||||
break;
|
||||
|
||||
case 'n': // expect null
|
||||
this.ExtractConstant(NullLiteral, null);
|
||||
break;
|
||||
|
||||
default: // expect number
|
||||
this.ExtractNumber();
|
||||
break;
|
||||
}
|
||||
|
||||
this._currentFieldName = null;
|
||||
this._state = ReadState.WaitingForNextOrRootClose;
|
||||
}
|
||||
|
||||
private static String Unescape(String str) {
|
||||
// check if we need to unescape at all
|
||||
if(str.IndexOf(StringEscapeChar) < 0) {
|
||||
return str;
|
||||
}
|
||||
|
||||
StringBuilder builder = new StringBuilder(str.Length);
|
||||
for(Int32 i = 0; i < str.Length; i++) {
|
||||
if(str[i] != StringEscapeChar) {
|
||||
_ = builder.Append(str[i]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if(i + 1 > str.Length - 1) {
|
||||
break;
|
||||
}
|
||||
|
||||
// escape sequence begins here
|
||||
switch(str[i + 1]) {
|
||||
case 'u':
|
||||
i = ExtractEscapeSequence(str, i, builder);
|
||||
break;
|
||||
case 'b':
|
||||
_ = builder.Append('\b');
|
||||
i += 1;
|
||||
break;
|
||||
case 't':
|
||||
_ = builder.Append('\t');
|
||||
i += 1;
|
||||
break;
|
||||
case 'n':
|
||||
_ = builder.Append('\n');
|
||||
i += 1;
|
||||
break;
|
||||
case 'f':
|
||||
_ = builder.Append('\f');
|
||||
i += 1;
|
||||
break;
|
||||
case 'r':
|
||||
_ = builder.Append('\r');
|
||||
i += 1;
|
||||
break;
|
||||
default:
|
||||
_ = builder.Append(str[i + 1]);
|
||||
i += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static Int32 ExtractEscapeSequence(String str, Int32 i, StringBuilder builder) {
|
||||
Int32 startIndex = i + 2;
|
||||
Int32 endIndex = i + 5;
|
||||
if(endIndex > str.Length - 1) {
|
||||
_ = builder.Append(str[i + 1]);
|
||||
i += 1;
|
||||
return i;
|
||||
}
|
||||
|
||||
Byte[] hexCode = str.Slice(startIndex, endIndex).ConvertHexadecimalToBytes();
|
||||
_ = builder.Append(Encoding.BigEndianUnicode.GetChars(hexCode));
|
||||
i += 5;
|
||||
return i;
|
||||
}
|
||||
|
||||
private Int32 GetFieldNameCount() {
|
||||
Int32 charCount = 0;
|
||||
for(Int32 j = this._index + 1; j < this._json.Length; j++) {
|
||||
if(this._json[j] == StringQuotedChar && this._json[j - 1] != StringEscapeChar) {
|
||||
break;
|
||||
}
|
||||
|
||||
charCount++;
|
||||
}
|
||||
|
||||
return charCount;
|
||||
}
|
||||
|
||||
private void ExtractObject() {
|
||||
// Extract and set the value
|
||||
Deserializer deserializer = new Deserializer(this._json, this._index);
|
||||
|
||||
if(this._currentFieldName != null) {
|
||||
this._resultObject![this._currentFieldName] = deserializer._result!;
|
||||
} else {
|
||||
this._resultArray!.Add(deserializer._result!);
|
||||
}
|
||||
|
||||
this._index = deserializer._index;
|
||||
}
|
||||
|
||||
private void ExtractNumber() {
|
||||
Int32 charCount = 0;
|
||||
for(Int32 j = this._index; j < this._json.Length; j++) {
|
||||
if(Char.IsWhiteSpace(this._json[j]) || this._json[j] == FieldSeparatorChar || this._resultObject != null && this._json[j] == CloseObjectChar || this._resultArray != null && this._json[j] == CloseArrayChar) {
|
||||
break;
|
||||
}
|
||||
|
||||
charCount++;
|
||||
}
|
||||
|
||||
// Extract and set the value
|
||||
String stringValue = this._json.SliceLength(this._index, charCount);
|
||||
|
||||
if(Decimal.TryParse(stringValue, System.Globalization.NumberStyles.Number, System.Globalization.CultureInfo.InvariantCulture, out Decimal value) == false) {
|
||||
throw this.CreateParserException("[number]");
|
||||
}
|
||||
|
||||
if(this._currentFieldName != null) {
|
||||
this._resultObject![this._currentFieldName] = value;
|
||||
} else {
|
||||
this._resultArray!.Add(value);
|
||||
}
|
||||
|
||||
this._index += charCount - 1;
|
||||
}
|
||||
|
||||
private void ExtractConstant(String boolValue, Boolean? value) {
|
||||
if(this._json.SliceLength(this._index, boolValue.Length) != boolValue) {
|
||||
throw this.CreateParserException($"'{ValueSeparatorChar}'");
|
||||
}
|
||||
|
||||
// Extract and set the value
|
||||
if(this._currentFieldName != null) {
|
||||
this._resultObject![this._currentFieldName] = value;
|
||||
} else {
|
||||
this._resultArray!.Add(value);
|
||||
}
|
||||
|
||||
this._index += boolValue.Length - 1;
|
||||
}
|
||||
|
||||
private void ExtractStringQuoted() {
|
||||
Int32 charCount = 0;
|
||||
Boolean escapeCharFound = false;
|
||||
for(Int32 j = this._index + 1; j < this._json.Length; j++) {
|
||||
if(this._json[j] == StringQuotedChar && !escapeCharFound) {
|
||||
break;
|
||||
}
|
||||
|
||||
escapeCharFound = this._json[j] == StringEscapeChar && !escapeCharFound;
|
||||
charCount++;
|
||||
}
|
||||
|
||||
// Extract and set the value
|
||||
String value = Unescape(this._json.SliceLength(this._index + 1, charCount));
|
||||
if(this._currentFieldName != null) {
|
||||
this._resultObject![this._currentFieldName] = value;
|
||||
} else {
|
||||
this._resultArray!.Add(value);
|
||||
}
|
||||
|
||||
this._index += charCount + 1;
|
||||
}
|
||||
|
||||
private FormatException CreateParserException(String expected) {
|
||||
Tuple<Int32, Int32> textPosition = this._json.TextPositionAt(this._index);
|
||||
return new FormatException($"Parser error (Line {textPosition.Item1}, Col {textPosition.Item2}, State {this._state}): Expected {expected} but got '{this._json[this._index]}'.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines the different JSON read states.
|
||||
/// </summary>
|
||||
private enum ReadState {
|
||||
WaitingForRootOpen,
|
||||
WaitingForField,
|
||||
WaitingForColon,
|
||||
WaitingForValue,
|
||||
WaitingForNextOrRootClose,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
331
Swan.Tiny/Formatters/Json.Serializer.cs
Normal file
331
Swan.Tiny/Formatters/Json.Serializer.cs
Normal file
@ -0,0 +1,331 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace Swan.Formatters {
|
||||
/// <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<Int32, String> IndentStrings = new Dictionary<global::System.Int32, global::System.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, Int32 depth, SerializerOptions options) {
|
||||
if(depth > 20) {
|
||||
throw new InvalidOperationException("The max depth (20) has been reached. Serializer can not continue.");
|
||||
}
|
||||
|
||||
// Basic Type Handling (nulls, strings, number, date and bool)
|
||||
this._result = ResolveBasicType(obj);
|
||||
|
||||
if(!String.IsNullOrWhiteSpace(this._result)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._options = options;
|
||||
|
||||
// Handle circular references correctly and avoid them
|
||||
if(options.IsObjectPresent(obj!)) {
|
||||
this._result = $"{{ \"$circref\": \"{Escape(obj!.GetHashCode().ToStringInvariant(), false)}\" }}";
|
||||
return;
|
||||
}
|
||||
|
||||
// At this point, we will need to construct the object with a StringBuilder.
|
||||
this._lastCommaSearch = FieldSeparatorChar + (this._options.Format ? Environment.NewLine : String.Empty);
|
||||
this._builder = new StringBuilder();
|
||||
|
||||
this._result = obj switch
|
||||
{
|
||||
IDictionary itemsZero when itemsZero.Count == 0 => EmptyObjectLiteral,
|
||||
IDictionary items => this.ResolveDictionary(items, depth),
|
||||
IEnumerable enumerableZero when !enumerableZero.Cast<Object>().Any() => EmptyArrayLiteral,
|
||||
IEnumerable enumerableBytes when enumerableBytes is Byte[] bytes => Serialize(bytes.ToBase64(), depth, this._options),
|
||||
IEnumerable enumerable => this.ResolveEnumerable(enumerable, depth),
|
||||
_ => this.ResolveObject(obj!, depth)
|
||||
};
|
||||
}
|
||||
|
||||
internal static String Serialize(Object? obj, Int32 depth, SerializerOptions options) => new Serializer(obj, depth, options)._result;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private static String ResolveBasicType(Object? obj) {
|
||||
switch(obj) {
|
||||
case null:
|
||||
return NullLiteral;
|
||||
case String s:
|
||||
return Escape(s, true);
|
||||
case Boolean b:
|
||||
return b ? TrueLiteral : FalseLiteral;
|
||||
case Type _:
|
||||
case Assembly _:
|
||||
case MethodInfo _:
|
||||
case PropertyInfo _:
|
||||
case EventInfo _:
|
||||
return Escape(obj.ToString()!, true);
|
||||
case DateTime d:
|
||||
return $"{StringQuotedChar}{d:s}{StringQuotedChar}";
|
||||
default:
|
||||
Type targetType = obj.GetType();
|
||||
|
||||
if(!Definitions.BasicTypesInfo.Value.ContainsKey(targetType)) {
|
||||
return String.Empty;
|
||||
}
|
||||
|
||||
String escapedValue = Escape(Definitions.BasicTypesInfo.Value[targetType].ToStringInvariant(obj), false);
|
||||
|
||||
return Decimal.TryParse(escapedValue, out _) ? $"{escapedValue}" : $"{StringQuotedChar}{escapedValue}{StringQuotedChar}";
|
||||
}
|
||||
}
|
||||
|
||||
private static Boolean IsNonEmptyJsonArrayOrObject(String serialized) {
|
||||
if(serialized == EmptyObjectLiteral || serialized == EmptyArrayLiteral) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// find the first position the character is not a space
|
||||
return serialized.Where(c => c != ' ').Select(c => c == OpenObjectChar || c == OpenArrayChar).FirstOrDefault();
|
||||
}
|
||||
|
||||
private static String Escape(String str, Boolean quoted) {
|
||||
if(str == null) {
|
||||
return String.Empty;
|
||||
}
|
||||
|
||||
StringBuilder builder = new StringBuilder(str.Length * 2);
|
||||
if(quoted) {
|
||||
_ = builder.Append(StringQuotedChar);
|
||||
}
|
||||
|
||||
Escape(str, builder);
|
||||
if(quoted) {
|
||||
_ = builder.Append(StringQuotedChar);
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static void Escape(String str, StringBuilder builder) {
|
||||
foreach(Char currentChar in str) {
|
||||
switch(currentChar) {
|
||||
case '\\':
|
||||
case '"':
|
||||
case '/':
|
||||
_ = builder
|
||||
.Append('\\')
|
||||
.Append(currentChar);
|
||||
break;
|
||||
case '\b':
|
||||
_ = builder.Append("\\b");
|
||||
break;
|
||||
case '\t':
|
||||
_ = builder.Append("\\t");
|
||||
break;
|
||||
case '\n':
|
||||
_ = builder.Append("\\n");
|
||||
break;
|
||||
case '\f':
|
||||
_ = builder.Append("\\f");
|
||||
break;
|
||||
case '\r':
|
||||
_ = builder.Append("\\r");
|
||||
break;
|
||||
default:
|
||||
if(currentChar < ' ') {
|
||||
Byte[] escapeBytes = BitConverter.GetBytes((UInt16)currentChar);
|
||||
if(BitConverter.IsLittleEndian == false) {
|
||||
Array.Reverse(escapeBytes);
|
||||
}
|
||||
|
||||
_ = builder.Append("\\u")
|
||||
.Append(escapeBytes[1].ToString("X").PadLeft(2, '0'))
|
||||
.Append(escapeBytes[0].ToString("X").PadLeft(2, '0'));
|
||||
} else {
|
||||
_ = builder.Append(currentChar);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<String, Object?> CreateDictionary(Dictionary<String, MemberInfo> fields, String targetType, Object target) {
|
||||
// Create the dictionary and extract the properties
|
||||
global::System.Collections.Generic.Dictionary<global::System.String, global::System.Object?> objectDictionary = new Dictionary<global::System.String, global::System.Object?>();
|
||||
|
||||
if(String.IsNullOrWhiteSpace(this._options?.TypeSpecifier) == false) {
|
||||
objectDictionary[this._options?.TypeSpecifier!] = targetType;
|
||||
}
|
||||
|
||||
foreach(global::System.Collections.Generic.KeyValuePair<global::System.String, global::System.Reflection.MemberInfo> field in fields) {
|
||||
// Build the dictionary using property names and values
|
||||
// Note: used to be: property.GetValue(target); but we would be reading private properties
|
||||
try {
|
||||
objectDictionary[field.Key] = field.Value is PropertyInfo property ? property.GetCacheGetMethod((Boolean)(this._options?.IncludeNonPublic)!)?.Invoke(target) : (field.Value as FieldInfo)?.GetValue(target);
|
||||
} catch {
|
||||
/* ignored */
|
||||
}
|
||||
}
|
||||
|
||||
return objectDictionary;
|
||||
}
|
||||
|
||||
private String ResolveDictionary(IDictionary items, Int32 depth) {
|
||||
this.Append(OpenObjectChar, depth);
|
||||
this.AppendLine();
|
||||
|
||||
// Iterate through the elements and output recursively
|
||||
Int32 writeCount = 0;
|
||||
foreach(Object? key in items.Keys) {
|
||||
// Serialize and append the key (first char indented)
|
||||
this.Append(StringQuotedChar, depth + 1);
|
||||
Escape(key?.ToString()!, this._builder!);
|
||||
_ = this._builder?.Append(StringQuotedChar).Append(ValueSeparatorChar).Append(" ");
|
||||
|
||||
// Serialize and append the value
|
||||
String serializedValue = Serialize(items[key!], depth + 1, this._options!);
|
||||
|
||||
if(IsNonEmptyJsonArrayOrObject(serializedValue)) {
|
||||
this.AppendLine();
|
||||
}
|
||||
|
||||
this.Append(serializedValue, 0);
|
||||
|
||||
// Add a comma and start a new line -- We will remove the last one when we are done writing the elements
|
||||
this.Append(FieldSeparatorChar, 0);
|
||||
this.AppendLine();
|
||||
writeCount++;
|
||||
}
|
||||
|
||||
// Output the end of the object and set the result
|
||||
this.RemoveLastComma();
|
||||
this.Append(CloseObjectChar, writeCount > 0 ? depth : 0);
|
||||
return this._builder!.ToString();
|
||||
}
|
||||
|
||||
private String ResolveObject(Object target, Int32 depth) {
|
||||
Type targetType = target.GetType();
|
||||
|
||||
if(targetType.IsEnum) {
|
||||
return Convert.ToInt64(target, System.Globalization.CultureInfo.InvariantCulture).ToString();
|
||||
}
|
||||
|
||||
global::System.Collections.Generic.Dictionary<global::System.String, global::System.Reflection.MemberInfo> fields = this._options!.GetProperties(targetType);
|
||||
|
||||
if(fields.Count == 0 && String.IsNullOrWhiteSpace(this._options.TypeSpecifier)) {
|
||||
return EmptyObjectLiteral;
|
||||
}
|
||||
|
||||
// If we arrive here, then we convert the object into a
|
||||
// dictionary of property names and values and call the serialization
|
||||
// function again
|
||||
global::System.Collections.Generic.Dictionary<global::System.String, global::System.Object?> objectDictionary = this.CreateDictionary(fields, targetType.ToString(), target);
|
||||
|
||||
return Serialize(objectDictionary, depth, this._options);
|
||||
}
|
||||
|
||||
private String ResolveEnumerable(IEnumerable target, Int32 depth) {
|
||||
// Cast the items as a generic object array
|
||||
global::System.Collections.Generic.IEnumerable<global::System.Object> items = target.Cast<global::System.Object>();
|
||||
|
||||
this.Append(OpenArrayChar, depth);
|
||||
this.AppendLine();
|
||||
|
||||
// Iterate through the elements and output recursively
|
||||
Int32 writeCount = 0;
|
||||
foreach(Object entry in items) {
|
||||
String serializedValue = Serialize(entry, depth + 1, this._options!);
|
||||
|
||||
if(IsNonEmptyJsonArrayOrObject(serializedValue)) {
|
||||
this.Append(serializedValue, 0);
|
||||
} else {
|
||||
this.Append(serializedValue, depth + 1);
|
||||
}
|
||||
|
||||
this.Append(FieldSeparatorChar, 0);
|
||||
this.AppendLine();
|
||||
writeCount++;
|
||||
}
|
||||
|
||||
// Output the end of the array and set the result
|
||||
this.RemoveLastComma();
|
||||
this.Append(CloseArrayChar, writeCount > 0 ? depth : 0);
|
||||
return this._builder!.ToString();
|
||||
}
|
||||
|
||||
private void SetIndent(Int32 depth) {
|
||||
if(this._options!.Format == false || depth <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
_ = this._builder!.Append(IndentStrings.GetOrAdd(depth, x => new String(' ', x * 4)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the last comma in the current string builder.
|
||||
/// </summary>
|
||||
private void RemoveLastComma() {
|
||||
if(this._builder!.Length < this._lastCommaSearch!.Length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(this._lastCommaSearch.Where((t, i) => this._builder[this._builder.Length - this._lastCommaSearch.Length + i] != t).Any()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we got this far, we simply remove the comma character
|
||||
_ = this._builder.Remove(this._builder.Length - this._lastCommaSearch.Length, 1);
|
||||
}
|
||||
|
||||
private void Append(String text, Int32 depth) {
|
||||
this.SetIndent(depth);
|
||||
_ = this._builder!.Append(text);
|
||||
}
|
||||
|
||||
private void Append(Char text, Int32 depth) {
|
||||
this.SetIndent(depth);
|
||||
_ = this._builder!.Append(text);
|
||||
}
|
||||
|
||||
private void AppendLine() {
|
||||
if(this._options!.Format == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
_ = this._builder!.Append(Environment.NewLine);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
133
Swan.Tiny/Formatters/Json.SerializerOptions.cs
Normal file
133
Swan.Tiny/Formatters/Json.SerializerOptions.cs
Normal file
@ -0,0 +1,133 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Swan.Reflection;
|
||||
|
||||
namespace Swan.Formatters {
|
||||
/// <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<Int32, List<WeakReference>> _parentReferences = new Dictionary<Int32, 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(Boolean format, String? typeSpecifier, String[]? includeProperties, String[]? excludeProperties = null, Boolean includeNonPublic = true, IReadOnlyCollection<WeakReference>? parentReferences = null, JsonSerializerCase jsonSerializerCase = JsonSerializerCase.None) {
|
||||
this._includeProperties = includeProperties;
|
||||
this._excludeProperties = excludeProperties;
|
||||
|
||||
this.IncludeNonPublic = includeNonPublic;
|
||||
this.Format = format;
|
||||
this.TypeSpecifier = typeSpecifier;
|
||||
this.JsonSerializerCase = jsonSerializerCase;
|
||||
|
||||
if(parentReferences == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach(WeakReference parentReference in parentReferences.Where(x => x.IsAlive)) {
|
||||
_ = this.IsObjectPresent(parentReference.Target);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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 Boolean 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 Boolean IncludeNonPublic {
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the json serializer case.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The json serializer case.
|
||||
/// </value>
|
||||
public JsonSerializerCase JsonSerializerCase {
|
||||
get;
|
||||
}
|
||||
|
||||
internal Boolean IsObjectPresent(Object? target) {
|
||||
if(target == null) {
|
||||
return false;
|
||||
}
|
||||
Int32 hashCode = target.GetHashCode();
|
||||
|
||||
if(this._parentReferences.ContainsKey(hashCode)) {
|
||||
if(this._parentReferences[hashCode].Any(p => ReferenceEquals(p.Target, target))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
this._parentReferences[hashCode].Add(new WeakReference(target));
|
||||
return false;
|
||||
}
|
||||
|
||||
this._parentReferences.Add(hashCode, new List<WeakReference> { new WeakReference(target) });
|
||||
return false;
|
||||
}
|
||||
|
||||
internal Dictionary<String, MemberInfo> GetProperties(Type targetType) => this.GetPropertiesCache(targetType).When(() => this._includeProperties?.Length > 0, query => query.Where(p => this._includeProperties.Contains(p.Key.Item1))).When(() => this._excludeProperties?.Length > 0, query => query.Where(p => !this._excludeProperties.Contains(p.Key.Item1))).ToDictionary(x => x.Key.Item2, x => x.Value);
|
||||
|
||||
private Dictionary<Tuple<String, String>, MemberInfo> GetPropertiesCache(Type targetType) {
|
||||
if(TypeCache.TryGetValue(targetType, out Dictionary<Tuple<String, String>, MemberInfo>? current)) {
|
||||
return current;
|
||||
}
|
||||
|
||||
List<MemberInfo> 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));
|
||||
}
|
||||
|
||||
Dictionary<Tuple<String, String>, MemberInfo> value = fields.ToDictionary(x => Tuple.Create(x.Name, x.GetCustomAttribute<JsonPropertyAttribute>()?.PropertyName ?? x.Name.GetNameWithCase(this.JsonSerializerCase)), x => x);
|
||||
|
||||
TypeCache.TryAdd(targetType, value);
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
340
Swan.Tiny/Formatters/Json.cs
Normal file
340
Swan.Tiny/Formatters/Json.cs
Normal file
@ -0,0 +1,340 @@
|
||||
#nullable enable
|
||||
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<global::System.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, Boolean format = false, String? typeSpecifier = null, Boolean includeNonPublic = false, String[]? includedNames = null, params String[] excludedNames) => Serialize(obj, format, typeSpecifier, includeNonPublic, includedNames, excludedNames, null, JsonSerializerCase.None);
|
||||
|
||||
/// <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, Boolean 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, Boolean format, String? typeSpecifier, Boolean includeNonPublic, String[]? includedNames, String[]? excludedNames, List<WeakReference>? parentReferences, JsonSerializerCase jsonSerializerCase) {
|
||||
if(obj != null && (obj is String || Definitions.AllBasicValueTypes.Contains(obj.GetType()))) {
|
||||
return SerializePrimitiveValue(obj);
|
||||
}
|
||||
|
||||
SerializerOptions options = new SerializerOptions(format, typeSpecifier, includedNames, GetExcludedNames(obj?.GetType(), excludedNames), includeNonPublic, parentReferences, jsonSerializerCase);
|
||||
|
||||
return Serialize(obj, options);
|
||||
}
|
||||
|
||||
/// <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, Boolean 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, Boolean 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) where T : notnull => (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, Boolean includeNonPublic) where T : notnull => (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, Boolean includeNonPublic = false, JsonSerializerCase jsonSerializerCase = JsonSerializerCase.None) => Converter.FromJsonResult(Deserializer.DeserializeInternal(json), jsonSerializerCase, resultType, includeNonPublic);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private API
|
||||
|
||||
private static String[]? GetExcludedNames(Type? type, String[]? excludedNames) {
|
||||
if(type == null) {
|
||||
return excludedNames;
|
||||
}
|
||||
|
||||
global::System.Collections.Generic.IEnumerable<global::System.String> 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,
|
||||
Boolean boolValue => boolValue ? TrueLiteral : FalseLiteral,
|
||||
_ => obj.ToString()!
|
||||
};
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
40
Swan.Tiny/Formatters/JsonPropertyAttribute.cs
Normal file
40
Swan.Tiny/Formatters/JsonPropertyAttribute.cs
Normal file
@ -0,0 +1,40 @@
|
||||
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, Boolean ignored = false) {
|
||||
this.PropertyName = propertyName ?? throw new ArgumentNullException(nameof(propertyName));
|
||||
this.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 Boolean Ignored {
|
||||
get;
|
||||
}
|
||||
}
|
||||
}
|
139
Swan.Tiny/Logging/ConsoleLogger.cs
Normal file
139
Swan.Tiny/Logging/ConsoleLogger.cs
Normal file
@ -0,0 +1,139 @@
|
||||
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
|
||||
TerminalWriters writer = logEvent.MessageType == LogLevel.Error ? TerminalWriters.StandardError : TerminalWriters.StandardOutput;
|
||||
|
||||
(String outputMessage, ConsoleColor color) = this.GetOutputAndColor(logEvent);
|
||||
|
||||
Terminal.Write(outputMessage, color, writer);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
}
|
49
Swan.Tiny/Logging/DebugLogger.cs
Normal file
49
Swan.Tiny/Logging/DebugLogger.cs
Normal file
@ -0,0 +1,49 @@
|
||||
using System;
|
||||
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 Boolean IsDebuggerAttached => System.Diagnostics.Debugger.IsAttached;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public LogLevel LogLevel { get; set; } = IsDebuggerAttached ? LogLevel.Trace : LogLevel.None;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Log(LogMessageReceivedEventArgs logEvent) {
|
||||
(String outputMessage, ConsoleColor _) = this.GetOutputAndColor(logEvent);
|
||||
|
||||
System.Diagnostics.Debug.Write(outputMessage);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose() {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
24
Swan.Tiny/Logging/ILogger.cs
Normal file
24
Swan.Tiny/Logging/ILogger.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using System;
|
||||
|
||||
namespace Swan.Logging {
|
||||
/// <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);
|
||||
}
|
||||
}
|
41
Swan.Tiny/Logging/LogLevel.cs
Normal file
41
Swan.Tiny/Logging/LogLevel.cs
Normal file
@ -0,0 +1,41 @@
|
||||
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,
|
||||
}
|
||||
}
|
147
Swan.Tiny/Logging/LogMessageReceivedEventArgs.cs
Normal file
147
Swan.Tiny/Logging/LogMessageReceivedEventArgs.cs
Normal file
@ -0,0 +1,147 @@
|
||||
#nullable enable
|
||||
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(
|
||||
UInt64 sequence,
|
||||
LogLevel messageType,
|
||||
DateTime utcDate,
|
||||
String source,
|
||||
String message,
|
||||
Object? extendedData,
|
||||
String callerMemberName,
|
||||
String callerFilePath,
|
||||
Int32 callerLineNumber) {
|
||||
this.Sequence = sequence;
|
||||
this.MessageType = messageType;
|
||||
this.UtcDate = utcDate;
|
||||
this.Source = source;
|
||||
this.Message = message;
|
||||
this.CallerMemberName = callerMemberName;
|
||||
this.CallerFilePath = callerFilePath;
|
||||
this.CallerLineNumber = callerLineNumber;
|
||||
this.ExtendedData = extendedData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets logging message sequence.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The sequence.
|
||||
/// </value>
|
||||
public UInt64 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 Int32 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 => this.ExtendedData as Exception;
|
||||
}
|
||||
}
|
423
Swan.Tiny/Logging/Logger.cs
Normal file
423
Swan.Tiny/Logging/Logger.cs
Normal file
@ -0,0 +1,423 @@
|
||||
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 UInt64 _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) {
|
||||
ILogger loggerInstance = Loggers.FirstOrDefault(x => x.GetType() == typeof(T));
|
||||
|
||||
if(loggerInstance != null) {
|
||||
throw new InvalidOperationException("There is already a logger with that class registered.");
|
||||
}
|
||||
|
||||
Loggers.Add(Activator.CreateInstance<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] Int32 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] Int32 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] Int32 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] Int32 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] Int32 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] Int32 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] Int32 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] Int32 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] Int32 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] Int32 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] Int32 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] Int32 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] Int32 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] Int32 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] Int32 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] Int32 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] Int32 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] Int32 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] Int32 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] Int32 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] Int32 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] Int32 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] Int32 callerLineNumber = 0) {
|
||||
if(obj == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String message = $"{text} ({obj.GetType()}): {Environment.NewLine}{obj.Stringify().Indent(5)}";
|
||||
LogMessage(LogLevel.Trace, message, source, obj, callerMemberName, callerFilePath, callerLineNumber);
|
||||
}
|
||||
|
||||
/// <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] Int32 callerLineNumber = 0) {
|
||||
if(obj == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String message = $"{text} ({obj.GetType()}): {Environment.NewLine}{obj.Stringify().Indent(5)}";
|
||||
LogMessage(LogLevel.Trace, message, source?.FullName, obj, callerMemberName, callerFilePath, callerLineNumber);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static void RemoveLogger(Func<ILogger, Boolean> criteria) {
|
||||
lock(SyncLock) {
|
||||
ILogger loggerInstance = Loggers.FirstOrDefault(criteria);
|
||||
|
||||
if(loggerInstance == null) {
|
||||
throw new InvalidOperationException("The logger is not registered.");
|
||||
}
|
||||
|
||||
loggerInstance.Dispose();
|
||||
|
||||
_ = Loggers.Remove(loggerInstance);
|
||||
}
|
||||
}
|
||||
|
||||
private static void LogMessage(LogLevel logLevel, String message, String sourceName, Object extendedData, String callerMemberName, String callerFilePath, Int32 callerLineNumber) {
|
||||
UInt64 sequence = _loggingSequence;
|
||||
DateTime date = DateTime.UtcNow;
|
||||
_loggingSequence++;
|
||||
|
||||
String loggerMessage = String.IsNullOrWhiteSpace(message) ? String.Empty : message.RemoveControlCharsExcept('\n');
|
||||
|
||||
LogMessageReceivedEventArgs eventArgs = new LogMessageReceivedEventArgs(sequence, logLevel, date, sourceName, loggerMessage, extendedData, callerMemberName, callerFilePath, callerLineNumber);
|
||||
|
||||
foreach(ILogger logger in Loggers) {
|
||||
_ = Task.Run(() => {
|
||||
if(logger.LogLevel <= logLevel) {
|
||||
logger.Log(eventArgs);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
63
Swan.Tiny/Logging/TextLogger.cs
Normal file
63
Swan.Tiny/Logging/TextLogger.cs
Normal file
@ -0,0 +1,63 @@
|
||||
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) {
|
||||
(String prefix, ConsoleColor color) = GetConsoleColorAndPrefix(logEvent.MessageType);
|
||||
|
||||
String loggerMessage = String.IsNullOrWhiteSpace(logEvent.Message) ? String.Empty : logEvent.Message.RemoveControlCharsExcept('\n');
|
||||
|
||||
String outputMessage = CreateOutputMessage(logEvent.Source, loggerMessage, prefix, logEvent.UtcDate);
|
||||
|
||||
// Further format the output in the case there is an exception being logged
|
||||
if(logEvent.MessageType == LogLevel.Error && logEvent.Exception != null) {
|
||||
try {
|
||||
outputMessage += $"{logEvent.Exception.Stringify().Indent()}{Environment.NewLine}";
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
return (outputMessage, color);
|
||||
}
|
||||
|
||||
private static (String Prefix, ConsoleColor color) GetConsoleColorAndPrefix(LogLevel messageType) => messageType switch
|
||||
{
|
||||
LogLevel.Debug => (ConsoleLogger.DebugPrefix, ConsoleLogger.DebugColor),
|
||||
LogLevel.Error => (ConsoleLogger.ErrorPrefix, ConsoleLogger.ErrorColor),
|
||||
LogLevel.Info => (ConsoleLogger.InfoPrefix, ConsoleLogger.InfoColor),
|
||||
LogLevel.Trace => (ConsoleLogger.TracePrefix, ConsoleLogger.TraceColor),
|
||||
LogLevel.Warning => (ConsoleLogger.WarnPrefix, ConsoleLogger.WarnColor),
|
||||
LogLevel.Fatal => (ConsoleLogger.FatalPrefix, ConsoleLogger.FatalColor),
|
||||
_ => (new String(' ', ConsoleLogger.InfoPrefix.Length), Terminal.Settings.DefaultColor),
|
||||
};
|
||||
|
||||
private static String CreateOutputMessage(String sourceName, String loggerMessage, String prefix, DateTime date) {
|
||||
String friendlySourceName = String.IsNullOrWhiteSpace(sourceName) ? String.Empty : sourceName.SliceLength(sourceName.LastIndexOf('.') + 1, sourceName.Length);
|
||||
|
||||
String outputMessage = String.IsNullOrWhiteSpace(sourceName) ? loggerMessage : $"[{friendlySourceName}] {loggerMessage}";
|
||||
|
||||
return String.IsNullOrWhiteSpace(LoggingTimeFormat) ? $" {prefix} >> {outputMessage}{Environment.NewLine}" : $" {date.ToLocalTime().ToString(LoggingTimeFormat)} {prefix} >> {outputMessage}{Environment.NewLine}";
|
||||
}
|
||||
}
|
||||
}
|
11
Swan.Tiny/Mappers/CopyableAttribute.cs
Normal file
11
Swan.Tiny/Mappers/CopyableAttribute.cs
Normal file
@ -0,0 +1,11 @@
|
||||
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 {
|
||||
}
|
||||
}
|
31
Swan.Tiny/Mappers/IObjectMap.cs
Normal file
31
Swan.Tiny/Mappers/IObjectMap.cs
Normal file
@ -0,0 +1,31 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
109
Swan.Tiny/Mappers/ObjectMap.cs
Normal file
109
Swan.Tiny/Mappers/ObjectMap.cs
Normal file
@ -0,0 +1,109 @@
|
||||
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) {
|
||||
this.SourceType = typeof(TSource);
|
||||
this.DestinationType = typeof(TDestination);
|
||||
this.Map = intersect.ToDictionary(property => this.DestinationType.GetProperty(property.Name), property => new List<PropertyInfo> { this.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) {
|
||||
PropertyInfo propertyDestinationInfo = (destinationProperty.Body as MemberExpression)?.Member as PropertyInfo;
|
||||
|
||||
if(propertyDestinationInfo == null) {
|
||||
throw new ArgumentException("Invalid destination expression", nameof(destinationProperty));
|
||||
}
|
||||
|
||||
List<PropertyInfo> sourceMembers = GetSourceMembers(sourceProperty);
|
||||
|
||||
if(sourceMembers.Any() == false) {
|
||||
throw new ArgumentException("Invalid source expression", nameof(sourceProperty));
|
||||
}
|
||||
|
||||
// reverse order
|
||||
sourceMembers.Reverse();
|
||||
this.Map[propertyDestinationInfo] = sourceMembers;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <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) {
|
||||
PropertyInfo propertyDestinationInfo = (destinationProperty.Body as MemberExpression)?.Member as PropertyInfo;
|
||||
|
||||
if(propertyDestinationInfo == null) {
|
||||
throw new ArgumentException("Invalid destination expression", nameof(destinationProperty));
|
||||
}
|
||||
|
||||
if(this.Map.ContainsKey(propertyDestinationInfo)) {
|
||||
_ = this.Map.Remove(propertyDestinationInfo);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
private static List<PropertyInfo> GetSourceMembers<TSourceProperty>(Expression<Func<TSource, TSourceProperty>> sourceProperty) {
|
||||
List<PropertyInfo> sourceMembers = new List<PropertyInfo>();
|
||||
MemberExpression initialExpression = sourceProperty.Body as MemberExpression;
|
||||
|
||||
while(true) {
|
||||
PropertyInfo propertySourceInfo = initialExpression?.Member as PropertyInfo;
|
||||
|
||||
if(propertySourceInfo == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
sourceMembers.Add(propertySourceInfo);
|
||||
initialExpression = initialExpression.Expression as MemberExpression;
|
||||
}
|
||||
|
||||
return sourceMembers;
|
||||
}
|
||||
}
|
||||
}
|
20
Swan.Tiny/Mappers/ObjectMapper.PropertyInfoComparer.cs
Normal file
20
Swan.Tiny/Mappers/ObjectMapper.PropertyInfoComparer.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using System;
|
||||
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 Boolean Equals(PropertyInfo x, PropertyInfo y) => x != null && y != null && x.Name == y.Name && x.PropertyType == y.PropertyType;
|
||||
|
||||
public Int32 GetHashCode(PropertyInfo obj) => obj.Name.GetHashCode() + obj.PropertyType.Name.GetHashCode();
|
||||
}
|
||||
}
|
||||
}
|
309
Swan.Tiny/Mappers/ObjectMapper.cs
Normal file
309
Swan.Tiny/Mappers/ObjectMapper.cs
Normal file
@ -0,0 +1,309 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Swan.Reflection;
|
||||
|
||||
namespace Swan.Mappers {
|
||||
/// <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 Int32 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 Int32 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(this._maps.Any(x => x.SourceType == typeof(TSource) && x.DestinationType == typeof(TDestination))) {
|
||||
throw new InvalidOperationException("You can't create an existing map");
|
||||
}
|
||||
|
||||
IEnumerable<PropertyInfo> sourceType = PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties<TSource>(true);
|
||||
IEnumerable<PropertyInfo> destinationType = PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties<TDestination>(true);
|
||||
|
||||
PropertyInfo[] intersect = sourceType.Intersect(destinationType, new PropertyInfoComparer()).ToArray();
|
||||
|
||||
if(!intersect.Any()) {
|
||||
throw new InvalidOperationException("Types doesn't match");
|
||||
}
|
||||
|
||||
ObjectMap<TSource, TDestination> map = new ObjectMap<TSource, TDestination>(intersect);
|
||||
|
||||
this._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, Boolean autoResolve = true) {
|
||||
if(source == null) {
|
||||
throw new ArgumentNullException(nameof(source));
|
||||
}
|
||||
|
||||
TDestination destination = Activator.CreateInstance<TDestination>();
|
||||
IObjectMap map = this._maps.FirstOrDefault(x => x.SourceType == source.GetType() && x.DestinationType == typeof(TDestination));
|
||||
|
||||
if(map != null) {
|
||||
foreach(KeyValuePair<PropertyInfo, List<PropertyInfo>> property in map.Map) {
|
||||
Object finalSource = property.Value.Aggregate(source, (current, sourceProperty) => sourceProperty.GetValue(current)!);
|
||||
|
||||
property.Key.SetValue(destination, finalSource);
|
||||
}
|
||||
} else {
|
||||
if(!autoResolve) {
|
||||
throw new InvalidOperationException($"You can't map from type {source.GetType().Name} to {typeof(TDestination).Name}");
|
||||
}
|
||||
|
||||
// Missing mapping, try to use default behavior
|
||||
_ = Copy(source, destination);
|
||||
}
|
||||
|
||||
return destination;
|
||||
}
|
||||
|
||||
private static Int32 CopyInternal(Object target, Dictionary<String, Tuple<Type, Object>> sourceProperties, IEnumerable<String>? propertiesToCopy, IEnumerable<String>? ignoreProperties) {
|
||||
// Filter properties
|
||||
IEnumerable<String>? requiredProperties = propertiesToCopy?.Where(p => !String.IsNullOrWhiteSpace(p)).Select(p => p.ToLowerInvariant());
|
||||
|
||||
IEnumerable<String>? ignoredProperties = ignoreProperties?.Where(p => !String.IsNullOrWhiteSpace(p)).Select(p => p.ToLowerInvariant());
|
||||
|
||||
IEnumerable<PropertyInfo> properties = PropertyTypeCache.DefaultCache.Value.RetrieveFilteredProperties(target.GetType(), true, x => x.CanWrite);
|
||||
|
||||
return properties.Select(x => x.Name).Distinct().ToDictionary(x => x.ToLowerInvariant(), x => properties.First(y => y.Name == x)).Where(x => sourceProperties.Keys.Contains(x.Key)).When(() => requiredProperties != null, q => q.Where(y => requiredProperties.Contains(y.Key))).When(() => ignoredProperties != null, q => q.Where(y => !ignoredProperties.Contains(y.Key))).ToDictionary(x => x.Value, x => sourceProperties[x.Key]).Sum(x => TrySetValue(x.Key, x.Value, target) ? 1 : 0);
|
||||
}
|
||||
|
||||
private static Boolean TrySetValue(PropertyInfo propertyInfo, Tuple<Type, Object> property, Object target) {
|
||||
try {
|
||||
(Type type, Object value) = property;
|
||||
|
||||
if(type.IsEnum) {
|
||||
propertyInfo.SetValue(target,
|
||||
Enum.ToObject(propertyInfo.PropertyType, value));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if(type.IsValueType || propertyInfo.PropertyType != type) {
|
||||
return propertyInfo.TrySetBasicType(value, target);
|
||||
}
|
||||
|
||||
if(propertyInfo.PropertyType.IsArray) {
|
||||
_ = propertyInfo.TrySetArray(value as IEnumerable<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:
|
||||
MethodInfo addMethod = targetType.GetMethods().FirstOrDefault(m => m.Name == Formatters.Json.AddMethodName && m.IsPublic && m.GetParameters().Length == 1);
|
||||
|
||||
if(addMethod == null) {
|
||||
return target;
|
||||
}
|
||||
|
||||
Boolean? isItemValueType = targetList.GetType().GetElementType()?.IsValueType;
|
||||
|
||||
foreach(Object? item in sourceList) {
|
||||
try {
|
||||
if(isItemValueType != null) {
|
||||
_ = targetList.Add((Boolean)isItemValueType ? item : item?.CopyPropertiesToNew<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
|
||||
PropertyInfo[] sourceProperties = PropertyTypeCache.DefaultCache.Value.RetrieveFilteredProperties(source.GetType(), true, x => x.CanRead).ToArray();
|
||||
|
||||
return sourceProperties.Select(x => x.Name).Distinct().ToDictionary(x => x.ToLowerInvariant(), x => Tuple.Create(sourceProperties.First(y => y.Name == x).PropertyType, sourceProperties.First(y => y.Name == x).GetValue(source)))!;
|
||||
}
|
||||
}
|
||||
}
|
96
Swan.Tiny/Net/Dns/DnsClient.Interfaces.cs
Normal file
96
Swan.Tiny/Net/Dns/DnsClient.Interfaces.cs
Normal file
@ -0,0 +1,96 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Swan.Net.Dns {
|
||||
/// <summary>
|
||||
/// DnsClient public interfaces.
|
||||
/// </summary>
|
||||
internal partial class DnsClient {
|
||||
public interface IDnsMessage {
|
||||
IList<DnsQuestion> Questions {
|
||||
get;
|
||||
}
|
||||
|
||||
Int32 Size {
|
||||
get;
|
||||
}
|
||||
Byte[] ToArray();
|
||||
}
|
||||
|
||||
public interface IDnsMessageEntry {
|
||||
DnsDomain Name {
|
||||
get;
|
||||
}
|
||||
DnsRecordType Type {
|
||||
get;
|
||||
}
|
||||
DnsRecordClass Class {
|
||||
get;
|
||||
}
|
||||
|
||||
Int32 Size {
|
||||
get;
|
||||
}
|
||||
Byte[] ToArray();
|
||||
}
|
||||
|
||||
public interface IDnsResourceRecord : IDnsMessageEntry {
|
||||
TimeSpan TimeToLive {
|
||||
get;
|
||||
}
|
||||
Int32 DataLength {
|
||||
get;
|
||||
}
|
||||
Byte[] Data {
|
||||
get;
|
||||
}
|
||||
}
|
||||
|
||||
public interface IDnsRequest : IDnsMessage {
|
||||
Int32 Id {
|
||||
get; set;
|
||||
}
|
||||
DnsOperationCode OperationCode {
|
||||
get; set;
|
||||
}
|
||||
Boolean RecursionDesired {
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
|
||||
public interface IDnsResponse : IDnsMessage {
|
||||
Int32 Id {
|
||||
get; set;
|
||||
}
|
||||
IList<IDnsResourceRecord> AnswerRecords {
|
||||
get;
|
||||
}
|
||||
IList<IDnsResourceRecord> AuthorityRecords {
|
||||
get;
|
||||
}
|
||||
IList<IDnsResourceRecord> AdditionalRecords {
|
||||
get;
|
||||
}
|
||||
Boolean IsRecursionAvailable {
|
||||
get; set;
|
||||
}
|
||||
Boolean IsAuthorativeServer {
|
||||
get; set;
|
||||
}
|
||||
Boolean IsTruncated {
|
||||
get; set;
|
||||
}
|
||||
DnsOperationCode OperationCode {
|
||||
get; set;
|
||||
}
|
||||
DnsResponseCode ResponseCode {
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
|
||||
public interface IDnsRequestResolver {
|
||||
Task<DnsClientResponse> Request(DnsClientRequest request);
|
||||
}
|
||||
}
|
||||
}
|
558
Swan.Tiny/Net/Dns/DnsClient.Request.cs
Normal file
558
Swan.Tiny/Net/Dns/DnsClient.Request.cs
Normal file
@ -0,0 +1,558 @@
|
||||
#nullable enable
|
||||
using Swan.Formatters;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Swan.Net.Dns {
|
||||
/// <summary>
|
||||
/// DnsClient Request inner class.
|
||||
/// </summary>
|
||||
internal partial class DnsClient {
|
||||
public class DnsClientRequest : IDnsRequest {
|
||||
private readonly IDnsRequestResolver _resolver;
|
||||
private readonly IDnsRequest _request;
|
||||
|
||||
public DnsClientRequest(IPEndPoint dns, IDnsRequest? request = null, IDnsRequestResolver? resolver = null) {
|
||||
this.Dns = dns;
|
||||
this._request = request == null ? new DnsRequest() : new DnsRequest(request);
|
||||
this._resolver = resolver ?? new DnsUdpRequestResolver();
|
||||
}
|
||||
|
||||
public Int32 Id {
|
||||
get => this._request.Id;
|
||||
set => this._request.Id = value;
|
||||
}
|
||||
|
||||
public DnsOperationCode OperationCode {
|
||||
get => this._request.OperationCode;
|
||||
set => this._request.OperationCode = value;
|
||||
}
|
||||
|
||||
public Boolean RecursionDesired {
|
||||
get => this._request.RecursionDesired;
|
||||
set => this._request.RecursionDesired = value;
|
||||
}
|
||||
|
||||
public IList<DnsQuestion> Questions => this._request.Questions;
|
||||
|
||||
public Int32 Size => this._request.Size;
|
||||
|
||||
public IPEndPoint Dns {
|
||||
get; set;
|
||||
}
|
||||
|
||||
public Byte[] ToArray() => this._request.ToArray();
|
||||
|
||||
public override String ToString() => this._request.ToString()!;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves this request into a response using the provided DNS information. The given
|
||||
/// request strategy is used to retrieve the response.
|
||||
/// </summary>
|
||||
/// <exception cref="DnsQueryException">Throw if a malformed response is received from the server.</exception>
|
||||
/// <exception cref="IOException">Thrown if a IO error occurs.</exception>
|
||||
/// <exception cref="SocketException">Thrown if a the reading or writing to the socket fails.</exception>
|
||||
/// <returns>The response received from server.</returns>
|
||||
public async Task<DnsClientResponse> Resolve() {
|
||||
try {
|
||||
DnsClientResponse response = await this._resolver.Request(this).ConfigureAwait(false);
|
||||
|
||||
if(response.Id != this.Id) {
|
||||
throw new DnsQueryException(response, "Mismatching request/response IDs");
|
||||
}
|
||||
|
||||
if(response.ResponseCode != DnsResponseCode.NoError) {
|
||||
throw new DnsQueryException(response);
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch(Exception e) {
|
||||
if(e is ArgumentException || e is SocketException) {
|
||||
throw new DnsQueryException("Invalid response", e);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class DnsRequest : IDnsRequest {
|
||||
private static readonly Random Random = new Random();
|
||||
|
||||
private DnsHeader header;
|
||||
|
||||
public DnsRequest() {
|
||||
this.Questions = new List<DnsQuestion>();
|
||||
this.header = new DnsHeader {
|
||||
OperationCode = DnsOperationCode.Query,
|
||||
Response = false,
|
||||
Id = Random.Next(UInt16.MaxValue),
|
||||
};
|
||||
}
|
||||
|
||||
public DnsRequest(IDnsRequest request) {
|
||||
this.header = new DnsHeader();
|
||||
this.Questions = new List<DnsQuestion>(request.Questions);
|
||||
|
||||
this.header.Response = false;
|
||||
|
||||
this.Id = request.Id;
|
||||
this.OperationCode = request.OperationCode;
|
||||
this.RecursionDesired = request.RecursionDesired;
|
||||
}
|
||||
|
||||
public IList<DnsQuestion> Questions {
|
||||
get;
|
||||
}
|
||||
|
||||
public Int32 Size => this.header.Size + this.Questions.Sum(q => q.Size);
|
||||
|
||||
public Int32 Id {
|
||||
get => this.header.Id;
|
||||
set => this.header.Id = value;
|
||||
}
|
||||
|
||||
public DnsOperationCode OperationCode {
|
||||
get => this.header.OperationCode;
|
||||
set => this.header.OperationCode = value;
|
||||
}
|
||||
|
||||
public Boolean RecursionDesired {
|
||||
get => this.header.RecursionDesired;
|
||||
set => this.header.RecursionDesired = value;
|
||||
}
|
||||
|
||||
public Byte[] ToArray() {
|
||||
this.UpdateHeader();
|
||||
using MemoryStream result = new MemoryStream(this.Size);
|
||||
|
||||
return result.Append(this.header.ToArray()).Append(this.Questions.Select(q => q.ToArray())).ToArray();
|
||||
}
|
||||
|
||||
public override String ToString() {
|
||||
this.UpdateHeader();
|
||||
|
||||
return Json.Serialize(this, true);
|
||||
}
|
||||
|
||||
private void UpdateHeader() => this.header.QuestionCount = this.Questions.Count;
|
||||
}
|
||||
|
||||
public class DnsTcpRequestResolver : IDnsRequestResolver {
|
||||
public async Task<DnsClientResponse> Request(DnsClientRequest request) {
|
||||
TcpClient tcp = new TcpClient();
|
||||
|
||||
try {
|
||||
await tcp.Client.ConnectAsync(request.Dns).ConfigureAwait(false);
|
||||
|
||||
NetworkStream stream = tcp.GetStream();
|
||||
Byte[] buffer = request.ToArray();
|
||||
Byte[] length = BitConverter.GetBytes((UInt16)buffer.Length);
|
||||
|
||||
if(BitConverter.IsLittleEndian) {
|
||||
Array.Reverse(length);
|
||||
}
|
||||
|
||||
await stream.WriteAsync(length, 0, length.Length).ConfigureAwait(false);
|
||||
await stream.WriteAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
|
||||
|
||||
buffer = new Byte[2];
|
||||
await Read(stream, buffer).ConfigureAwait(false);
|
||||
|
||||
if(BitConverter.IsLittleEndian) {
|
||||
Array.Reverse(buffer);
|
||||
}
|
||||
|
||||
buffer = new Byte[BitConverter.ToUInt16(buffer, 0)];
|
||||
await Read(stream, buffer).ConfigureAwait(false);
|
||||
|
||||
DnsResponse response = DnsResponse.FromArray(buffer);
|
||||
|
||||
return new DnsClientResponse(request, response, buffer);
|
||||
} finally {
|
||||
tcp.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task Read(Stream stream, Byte[] buffer) {
|
||||
Int32 length = buffer.Length;
|
||||
Int32 offset = 0;
|
||||
Int32 size;
|
||||
|
||||
while(length > 0 && (size = await stream.ReadAsync(buffer, offset, length).ConfigureAwait(false)) > 0) {
|
||||
offset += size;
|
||||
length -= size;
|
||||
}
|
||||
|
||||
if(length > 0) {
|
||||
throw new IOException("Unexpected end of stream");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class DnsUdpRequestResolver : IDnsRequestResolver {
|
||||
private readonly IDnsRequestResolver _fallback;
|
||||
|
||||
public DnsUdpRequestResolver(IDnsRequestResolver fallback) => this._fallback = fallback;
|
||||
|
||||
public DnsUdpRequestResolver() => this._fallback = new DnsNullRequestResolver();
|
||||
|
||||
public async Task<DnsClientResponse> Request(DnsClientRequest request) {
|
||||
UdpClient udp = new UdpClient();
|
||||
IPEndPoint dns = request.Dns;
|
||||
|
||||
try {
|
||||
udp.Client.SendTimeout = 7000;
|
||||
udp.Client.ReceiveTimeout = 7000;
|
||||
|
||||
await udp.Client.ConnectAsync(dns).ConfigureAwait(false);
|
||||
|
||||
|
||||
_ = await udp.SendAsync(request.ToArray(), request.Size).ConfigureAwait(false);
|
||||
|
||||
List<Byte> bufferList = new List<Byte>();
|
||||
|
||||
do {
|
||||
Byte[] tempBuffer = new Byte[1024];
|
||||
Int32 receiveCount = udp.Client.Receive(tempBuffer);
|
||||
bufferList.AddRange(tempBuffer.Skip(0).Take(receiveCount));
|
||||
}
|
||||
while(udp.Client.Available > 0 || bufferList.Count == 0);
|
||||
|
||||
Byte[] buffer = bufferList.ToArray();
|
||||
DnsResponse response = DnsResponse.FromArray(buffer);
|
||||
|
||||
return response.IsTruncated
|
||||
? await this._fallback.Request(request).ConfigureAwait(false)
|
||||
: new DnsClientResponse(request, response, buffer);
|
||||
} finally {
|
||||
udp.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class DnsNullRequestResolver : IDnsRequestResolver {
|
||||
public Task<DnsClientResponse> Request(DnsClientRequest request) => throw new DnsQueryException("Request failed");
|
||||
}
|
||||
|
||||
// 12 bytes message header
|
||||
[StructEndianness(Endianness.Big)]
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public struct DnsHeader {
|
||||
public const Int32 SIZE = 12;
|
||||
|
||||
private UInt16 id;
|
||||
|
||||
// Question count: number of questions in the Question section
|
||||
private UInt16 questionCount;
|
||||
|
||||
// Answer record count: number of records in the Answer section
|
||||
private UInt16 answerCount;
|
||||
|
||||
// Authority record count: number of records in the Authority section
|
||||
private UInt16 authorityCount;
|
||||
|
||||
// Additional record count: number of records in the Additional section
|
||||
private UInt16 addtionalCount;
|
||||
|
||||
public Int32 Id {
|
||||
get => this.id;
|
||||
set => this.id = (UInt16)value;
|
||||
}
|
||||
|
||||
public Int32 QuestionCount {
|
||||
get => this.questionCount;
|
||||
set => this.questionCount = (UInt16)value;
|
||||
}
|
||||
|
||||
public Int32 AnswerRecordCount {
|
||||
get => this.answerCount;
|
||||
set => this.answerCount = (UInt16)value;
|
||||
}
|
||||
|
||||
public Int32 AuthorityRecordCount {
|
||||
get => this.authorityCount;
|
||||
set => this.authorityCount = (UInt16)value;
|
||||
}
|
||||
|
||||
public Int32 AdditionalRecordCount {
|
||||
get => this.addtionalCount;
|
||||
set => this.addtionalCount = (UInt16)value;
|
||||
}
|
||||
|
||||
public Boolean Response {
|
||||
get => this.Qr == 1;
|
||||
set => this.Qr = Convert.ToByte(value);
|
||||
}
|
||||
|
||||
public DnsOperationCode OperationCode {
|
||||
get => (DnsOperationCode)this.Opcode;
|
||||
set => this.Opcode = (Byte)value;
|
||||
}
|
||||
|
||||
public Boolean AuthorativeServer {
|
||||
get => this.Aa == 1;
|
||||
set => this.Aa = Convert.ToByte(value);
|
||||
}
|
||||
|
||||
public Boolean Truncated {
|
||||
get => this.Tc == 1;
|
||||
set => this.Tc = Convert.ToByte(value);
|
||||
}
|
||||
|
||||
public Boolean RecursionDesired {
|
||||
get => this.Rd == 1;
|
||||
set => this.Rd = Convert.ToByte(value);
|
||||
}
|
||||
|
||||
public Boolean RecursionAvailable {
|
||||
get => this.Ra == 1;
|
||||
set => this.Ra = Convert.ToByte(value);
|
||||
}
|
||||
|
||||
public DnsResponseCode ResponseCode {
|
||||
get => (DnsResponseCode)this.RCode;
|
||||
set => this.RCode = (Byte)value;
|
||||
}
|
||||
|
||||
public Int32 Size => SIZE;
|
||||
|
||||
// Query/Response Flag
|
||||
private Byte Qr {
|
||||
get => this.Flag0.GetBitValueAt(7);
|
||||
set => this.Flag0 = this.Flag0.SetBitValueAt(7, 1, value);
|
||||
}
|
||||
|
||||
// Operation Code
|
||||
private Byte Opcode {
|
||||
get => this.Flag0.GetBitValueAt(3, 4);
|
||||
set => this.Flag0 = this.Flag0.SetBitValueAt(3, 4, value);
|
||||
}
|
||||
|
||||
// Authorative Answer Flag
|
||||
private Byte Aa {
|
||||
get => this.Flag0.GetBitValueAt(2);
|
||||
set => this.Flag0 = this.Flag0.SetBitValueAt(2, 1, value);
|
||||
}
|
||||
|
||||
// Truncation Flag
|
||||
private Byte Tc {
|
||||
get => this.Flag0.GetBitValueAt(1);
|
||||
set => this.Flag0 = this.Flag0.SetBitValueAt(1, 1, value);
|
||||
}
|
||||
|
||||
// Recursion Desired
|
||||
private Byte Rd {
|
||||
get => this.Flag0.GetBitValueAt(0);
|
||||
set => this.Flag0 = this.Flag0.SetBitValueAt(0, 1, value);
|
||||
}
|
||||
|
||||
// Recursion Available
|
||||
private Byte Ra {
|
||||
get => this.Flag1.GetBitValueAt(7);
|
||||
set => this.Flag1 = this.Flag1.SetBitValueAt(7, 1, value);
|
||||
}
|
||||
|
||||
// Zero (Reserved)
|
||||
private Byte Z {
|
||||
get => this.Flag1.GetBitValueAt(4, 3);
|
||||
set {
|
||||
}
|
||||
}
|
||||
|
||||
// Response Code
|
||||
private Byte RCode {
|
||||
get => this.Flag1.GetBitValueAt(0, 4);
|
||||
set => this.Flag1 = this.Flag1.SetBitValueAt(0, 4, value);
|
||||
}
|
||||
|
||||
private Byte Flag0 {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
private Byte Flag1 {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public static DnsHeader FromArray(Byte[] header) => header.Length < SIZE ? throw new ArgumentException("Header length too small") : header.ToStruct<DnsHeader>(0, SIZE);
|
||||
|
||||
public Byte[] ToArray() => this.ToBytes();
|
||||
|
||||
public override String ToString() => Json.SerializeExcluding(this, true, nameof(this.Size));
|
||||
}
|
||||
|
||||
public class DnsDomain : IComparable<DnsDomain> {
|
||||
private readonly String[] _labels;
|
||||
|
||||
public DnsDomain(String domain) : this(domain.Split('.')) {
|
||||
}
|
||||
|
||||
public DnsDomain(String[] labels) => this._labels = labels;
|
||||
|
||||
public Int32 Size => this._labels.Sum(l => l.Length) + this._labels.Length + 1;
|
||||
|
||||
public static DnsDomain FromArray(Byte[] message, Int32 offset) => FromArray(message, offset, out _);
|
||||
|
||||
public static DnsDomain FromArray(Byte[] message, Int32 offset, out Int32 endOffset) {
|
||||
List<Byte[]> labels = new List<Byte[]>();
|
||||
Boolean endOffsetAssigned = false;
|
||||
endOffset = 0;
|
||||
Byte lengthOrPointer;
|
||||
|
||||
while((lengthOrPointer = message[offset++]) > 0) {
|
||||
// Two heighest bits are set (pointer)
|
||||
if(lengthOrPointer.GetBitValueAt(6, 2) == 3) {
|
||||
if(!endOffsetAssigned) {
|
||||
endOffsetAssigned = true;
|
||||
endOffset = offset + 1;
|
||||
}
|
||||
|
||||
UInt16 pointer = lengthOrPointer.GetBitValueAt(0, 6);
|
||||
offset = (pointer << 8) | message[offset];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if(lengthOrPointer.GetBitValueAt(6, 2) != 0) {
|
||||
throw new ArgumentException("Unexpected bit pattern in label length");
|
||||
}
|
||||
|
||||
Byte length = lengthOrPointer;
|
||||
Byte[] label = new Byte[length];
|
||||
Array.Copy(message, offset, label, 0, length);
|
||||
|
||||
labels.Add(label);
|
||||
|
||||
offset += length;
|
||||
}
|
||||
|
||||
if(!endOffsetAssigned) {
|
||||
endOffset = offset;
|
||||
}
|
||||
|
||||
return new DnsDomain(labels.Select(l => l.ToText(Encoding.ASCII)).ToArray());
|
||||
}
|
||||
|
||||
public static DnsDomain PointerName(IPAddress ip) => new DnsDomain(FormatReverseIP(ip));
|
||||
|
||||
public Byte[] ToArray() {
|
||||
Byte[] result = new Byte[this.Size];
|
||||
Int32 offset = 0;
|
||||
|
||||
foreach(Byte[] l in this._labels.Select(label => Encoding.ASCII.GetBytes(label))) {
|
||||
result[offset++] = (Byte)l.Length;
|
||||
l.CopyTo(result, offset);
|
||||
|
||||
offset += l.Length;
|
||||
}
|
||||
|
||||
result[offset] = 0;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public override String ToString() => String.Join(".", this._labels);
|
||||
|
||||
public Int32 CompareTo(DnsDomain other) => String.Compare(this.ToString(), other.ToString(), StringComparison.Ordinal);
|
||||
|
||||
public override Boolean Equals(Object? obj) => obj is DnsDomain domain && this.CompareTo(domain) == 0;
|
||||
|
||||
public override Int32 GetHashCode() => this.ToString().GetHashCode();
|
||||
|
||||
private static String FormatReverseIP(IPAddress ip) {
|
||||
Byte[] address = ip.GetAddressBytes();
|
||||
|
||||
if(address.Length == 4) {
|
||||
return String.Join(".", address.Reverse().Select(b => b.ToString())) + ".in-addr.arpa";
|
||||
}
|
||||
|
||||
Byte[] nibbles = new Byte[address.Length * 2];
|
||||
|
||||
for(Int32 i = 0, j = 0; i < address.Length; i++, j = 2 * i) {
|
||||
Byte b = address[i];
|
||||
|
||||
nibbles[j] = b.GetBitValueAt(4, 4);
|
||||
nibbles[j + 1] = b.GetBitValueAt(0, 4);
|
||||
}
|
||||
|
||||
return String.Join(".", nibbles.Reverse().Select(b => b.ToString("x"))) + ".ip6.arpa";
|
||||
}
|
||||
}
|
||||
|
||||
public class DnsQuestion : IDnsMessageEntry {
|
||||
public static IList<DnsQuestion> GetAllFromArray(Byte[] message, Int32 offset, Int32 questionCount) => GetAllFromArray(message, offset, questionCount, out _);
|
||||
|
||||
public static IList<DnsQuestion> GetAllFromArray(Byte[] message, Int32 offset, Int32 questionCount, out Int32 endOffset) {
|
||||
IList<DnsQuestion> questions = new List<DnsQuestion>(questionCount);
|
||||
|
||||
for(Int32 i = 0; i < questionCount; i++) {
|
||||
questions.Add(FromArray(message, offset, out offset));
|
||||
}
|
||||
|
||||
endOffset = offset;
|
||||
return questions;
|
||||
}
|
||||
|
||||
public static DnsQuestion FromArray(Byte[] message, Int32 offset, out Int32 endOffset) {
|
||||
DnsDomain domain = DnsDomain.FromArray(message, offset, out offset);
|
||||
Tail tail = message.ToStruct<Tail>(offset, Tail.SIZE);
|
||||
|
||||
endOffset = offset + Tail.SIZE;
|
||||
|
||||
return new DnsQuestion(domain, tail.Type, tail.Class);
|
||||
}
|
||||
|
||||
public DnsQuestion(DnsDomain domain, DnsRecordType type = DnsRecordType.A, DnsRecordClass klass = DnsRecordClass.IN) {
|
||||
this.Name = domain;
|
||||
this.Type = type;
|
||||
this.Class = klass;
|
||||
}
|
||||
|
||||
public DnsDomain Name {
|
||||
get;
|
||||
}
|
||||
|
||||
public DnsRecordType Type {
|
||||
get;
|
||||
}
|
||||
|
||||
public DnsRecordClass Class {
|
||||
get;
|
||||
}
|
||||
|
||||
public Int32 Size => this.Name.Size + Tail.SIZE;
|
||||
|
||||
public Byte[] ToArray() => new MemoryStream(this.Size).Append(this.Name.ToArray()).Append(new Tail { Type = Type, Class = Class }.ToBytes()).ToArray();
|
||||
|
||||
public override String ToString() => Json.SerializeOnly(this, true, nameof(this.Name), nameof(this.Type), nameof(this.Class));
|
||||
|
||||
[StructEndianness(Endianness.Big)]
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 2)]
|
||||
private struct Tail {
|
||||
public const Int32 SIZE = 4;
|
||||
|
||||
private UInt16 type;
|
||||
private UInt16 klass;
|
||||
|
||||
public DnsRecordType Type {
|
||||
get => (DnsRecordType)this.type;
|
||||
set => this.type = (UInt16)value;
|
||||
}
|
||||
|
||||
public DnsRecordClass Class {
|
||||
get => (DnsRecordClass)this.klass;
|
||||
set => this.klass = (UInt16)value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
344
Swan.Tiny/Net/Dns/DnsClient.ResourceRecords.cs
Normal file
344
Swan.Tiny/Net/Dns/DnsClient.ResourceRecords.cs
Normal file
@ -0,0 +1,344 @@
|
||||
using Swan.Formatters;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Swan.Net.Dns {
|
||||
/// <summary>
|
||||
/// DnsClient public methods.
|
||||
/// </summary>
|
||||
internal partial class DnsClient {
|
||||
public abstract class DnsResourceRecordBase : IDnsResourceRecord {
|
||||
private readonly IDnsResourceRecord _record;
|
||||
|
||||
protected DnsResourceRecordBase(IDnsResourceRecord record) => this._record = record;
|
||||
|
||||
public DnsDomain Name => this._record.Name;
|
||||
|
||||
public DnsRecordType Type => this._record.Type;
|
||||
|
||||
public DnsRecordClass Class => this._record.Class;
|
||||
|
||||
public TimeSpan TimeToLive => this._record.TimeToLive;
|
||||
|
||||
public Int32 DataLength => this._record.DataLength;
|
||||
|
||||
public Byte[] Data => this._record.Data;
|
||||
|
||||
public Int32 Size => this._record.Size;
|
||||
|
||||
protected virtual String[] IncludedProperties => new[] { nameof(this.Name), nameof(this.Type), nameof(this.Class), nameof(this.TimeToLive), nameof(this.DataLength) };
|
||||
|
||||
public Byte[] ToArray() => this._record.ToArray();
|
||||
|
||||
public override String ToString() => Json.SerializeOnly(this, true, this.IncludedProperties);
|
||||
}
|
||||
|
||||
public class DnsResourceRecord : IDnsResourceRecord {
|
||||
public DnsResourceRecord(DnsDomain domain, Byte[] data, DnsRecordType type, DnsRecordClass klass = DnsRecordClass.IN, TimeSpan ttl = default) {
|
||||
this.Name = domain;
|
||||
this.Type = type;
|
||||
this.Class = klass;
|
||||
this.TimeToLive = ttl;
|
||||
this.Data = data;
|
||||
}
|
||||
|
||||
public DnsDomain Name {
|
||||
get;
|
||||
}
|
||||
|
||||
public DnsRecordType Type {
|
||||
get;
|
||||
}
|
||||
|
||||
public DnsRecordClass Class {
|
||||
get;
|
||||
}
|
||||
|
||||
public TimeSpan TimeToLive {
|
||||
get;
|
||||
}
|
||||
|
||||
public Int32 DataLength => this.Data.Length;
|
||||
|
||||
public Byte[] Data {
|
||||
get;
|
||||
}
|
||||
|
||||
public Int32 Size => this.Name.Size + Tail.SIZE + this.Data.Length;
|
||||
|
||||
public static DnsResourceRecord FromArray(Byte[] message, Int32 offset, out Int32 endOffset) {
|
||||
DnsDomain domain = DnsDomain.FromArray(message, offset, out offset);
|
||||
Tail tail = message.ToStruct<Tail>(offset, Tail.SIZE);
|
||||
|
||||
Byte[] data = new Byte[tail.DataLength];
|
||||
|
||||
offset += Tail.SIZE;
|
||||
Array.Copy(message, offset, data, 0, data.Length);
|
||||
|
||||
endOffset = offset + data.Length;
|
||||
|
||||
return new DnsResourceRecord(domain, data, tail.Type, tail.Class, tail.TimeToLive);
|
||||
}
|
||||
|
||||
public Byte[] ToArray() => new MemoryStream(this.Size).Append(this.Name.ToArray()).Append(new Tail() { Type = Type, Class = Class, TimeToLive = TimeToLive, DataLength = this.Data.Length, }.ToBytes()).Append(this.Data).ToArray();
|
||||
|
||||
public override String ToString() => Json.SerializeOnly(this, true, nameof(this.Name), nameof(this.Type), nameof(this.Class), nameof(this.TimeToLive), nameof(this.DataLength));
|
||||
|
||||
[StructEndianness(Endianness.Big)]
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 2)]
|
||||
private struct Tail {
|
||||
public const Int32 SIZE = 10;
|
||||
|
||||
private UInt16 type;
|
||||
private UInt16 klass;
|
||||
private UInt32 ttl;
|
||||
private UInt16 dataLength;
|
||||
|
||||
public DnsRecordType Type {
|
||||
get => (DnsRecordType)this.type;
|
||||
set => this.type = (UInt16)value;
|
||||
}
|
||||
|
||||
public DnsRecordClass Class {
|
||||
get => (DnsRecordClass)this.klass;
|
||||
set => this.klass = (UInt16)value;
|
||||
}
|
||||
|
||||
public TimeSpan TimeToLive {
|
||||
get => TimeSpan.FromSeconds(this.ttl);
|
||||
set => this.ttl = (UInt32)value.TotalSeconds;
|
||||
}
|
||||
|
||||
public Int32 DataLength {
|
||||
get => this.dataLength;
|
||||
set => this.dataLength = (UInt16)value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class DnsPointerResourceRecord : DnsResourceRecordBase {
|
||||
public DnsPointerResourceRecord(IDnsResourceRecord record, Byte[] message, Int32 dataOffset) : base(record) => this.PointerDomainName = DnsDomain.FromArray(message, dataOffset);
|
||||
|
||||
public DnsDomain PointerDomainName {
|
||||
get;
|
||||
}
|
||||
|
||||
protected override String[] IncludedProperties {
|
||||
get {
|
||||
List<String> temp = new List<String>(base.IncludedProperties) { nameof(this.PointerDomainName) };
|
||||
return temp.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class DnsIPAddressResourceRecord : DnsResourceRecordBase {
|
||||
public DnsIPAddressResourceRecord(IDnsResourceRecord record) : base(record) => this.IPAddress = new IPAddress(this.Data);
|
||||
|
||||
public IPAddress IPAddress {
|
||||
get;
|
||||
}
|
||||
|
||||
protected override String[] IncludedProperties => new List<String>(base.IncludedProperties) { nameof(this.IPAddress) }.ToArray();
|
||||
}
|
||||
|
||||
public class DnsNameServerResourceRecord : DnsResourceRecordBase {
|
||||
public DnsNameServerResourceRecord(IDnsResourceRecord record, Byte[] message, Int32 dataOffset) : base(record) => this.NSDomainName = DnsDomain.FromArray(message, dataOffset);
|
||||
|
||||
public DnsDomain NSDomainName {
|
||||
get;
|
||||
}
|
||||
|
||||
protected override String[] IncludedProperties => new List<String>(base.IncludedProperties) { nameof(this.NSDomainName) }.ToArray();
|
||||
}
|
||||
|
||||
public class DnsCanonicalNameResourceRecord : DnsResourceRecordBase {
|
||||
public DnsCanonicalNameResourceRecord(IDnsResourceRecord record, Byte[] message, Int32 dataOffset) : base(record) => this.CanonicalDomainName = DnsDomain.FromArray(message, dataOffset);
|
||||
|
||||
public DnsDomain CanonicalDomainName {
|
||||
get;
|
||||
}
|
||||
|
||||
protected override String[] IncludedProperties => new List<String>(base.IncludedProperties) { nameof(this.CanonicalDomainName) }.ToArray();
|
||||
}
|
||||
|
||||
public class DnsMailExchangeResourceRecord : DnsResourceRecordBase {
|
||||
private const Int32 PreferenceSize = 2;
|
||||
|
||||
public DnsMailExchangeResourceRecord(IDnsResourceRecord record, Byte[] message, Int32 dataOffset)
|
||||
: base(record) {
|
||||
Byte[] preference = new Byte[PreferenceSize];
|
||||
Array.Copy(message, dataOffset, preference, 0, preference.Length);
|
||||
|
||||
if(BitConverter.IsLittleEndian) {
|
||||
Array.Reverse(preference);
|
||||
}
|
||||
|
||||
dataOffset += PreferenceSize;
|
||||
|
||||
this.Preference = BitConverter.ToUInt16(preference, 0);
|
||||
this.ExchangeDomainName = DnsDomain.FromArray(message, dataOffset);
|
||||
}
|
||||
|
||||
public Int32 Preference {
|
||||
get;
|
||||
}
|
||||
|
||||
public DnsDomain ExchangeDomainName {
|
||||
get;
|
||||
}
|
||||
|
||||
protected override String[] IncludedProperties => new List<String>(base.IncludedProperties)
|
||||
{
|
||||
nameof(this.Preference),
|
||||
nameof(this.ExchangeDomainName),
|
||||
}.ToArray();
|
||||
}
|
||||
|
||||
public class DnsStartOfAuthorityResourceRecord : DnsResourceRecordBase {
|
||||
public DnsStartOfAuthorityResourceRecord(IDnsResourceRecord record, Byte[] message, Int32 dataOffset) : base(record) {
|
||||
this.MasterDomainName = DnsDomain.FromArray(message, dataOffset, out dataOffset);
|
||||
this.ResponsibleDomainName = DnsDomain.FromArray(message, dataOffset, out dataOffset);
|
||||
|
||||
Options tail = message.ToStruct<Options>(dataOffset, Options.SIZE);
|
||||
|
||||
this.SerialNumber = tail.SerialNumber;
|
||||
this.RefreshInterval = tail.RefreshInterval;
|
||||
this.RetryInterval = tail.RetryInterval;
|
||||
this.ExpireInterval = tail.ExpireInterval;
|
||||
this.MinimumTimeToLive = tail.MinimumTimeToLive;
|
||||
}
|
||||
|
||||
public DnsStartOfAuthorityResourceRecord(DnsDomain domain, DnsDomain master, DnsDomain responsible, Int64 serial, TimeSpan refresh, TimeSpan retry, TimeSpan expire, TimeSpan minTtl, TimeSpan ttl = default)
|
||||
: base(Create(domain, master, responsible, serial, refresh, retry, expire, minTtl, ttl)) {
|
||||
this.MasterDomainName = master;
|
||||
this.ResponsibleDomainName = responsible;
|
||||
|
||||
this.SerialNumber = serial;
|
||||
this.RefreshInterval = refresh;
|
||||
this.RetryInterval = retry;
|
||||
this.ExpireInterval = expire;
|
||||
this.MinimumTimeToLive = minTtl;
|
||||
}
|
||||
|
||||
public DnsDomain MasterDomainName {
|
||||
get;
|
||||
}
|
||||
|
||||
public DnsDomain ResponsibleDomainName {
|
||||
get;
|
||||
}
|
||||
|
||||
public Int64 SerialNumber {
|
||||
get;
|
||||
}
|
||||
|
||||
public TimeSpan RefreshInterval {
|
||||
get;
|
||||
}
|
||||
|
||||
public TimeSpan RetryInterval {
|
||||
get;
|
||||
}
|
||||
|
||||
public TimeSpan ExpireInterval {
|
||||
get;
|
||||
}
|
||||
|
||||
public TimeSpan MinimumTimeToLive {
|
||||
get;
|
||||
}
|
||||
|
||||
protected override String[] IncludedProperties => new List<String>(base.IncludedProperties)
|
||||
{
|
||||
nameof(this.MasterDomainName),
|
||||
nameof(this.ResponsibleDomainName),
|
||||
nameof(this.SerialNumber),
|
||||
}.ToArray();
|
||||
|
||||
private static IDnsResourceRecord Create(DnsDomain domain, DnsDomain master, DnsDomain responsible, Int64 serial, TimeSpan refresh, TimeSpan retry, TimeSpan expire, TimeSpan minTtl, TimeSpan ttl) {
|
||||
MemoryStream data = new MemoryStream(Options.SIZE + master.Size + responsible.Size);
|
||||
Options tail = new Options {
|
||||
SerialNumber = serial,
|
||||
RefreshInterval = refresh,
|
||||
RetryInterval = retry,
|
||||
ExpireInterval = expire,
|
||||
MinimumTimeToLive = minTtl,
|
||||
};
|
||||
|
||||
_ = data.Append(master.ToArray()).Append(responsible.ToArray()).Append(tail.ToBytes());
|
||||
|
||||
return new DnsResourceRecord(domain, data.ToArray(), DnsRecordType.SOA, DnsRecordClass.IN, ttl);
|
||||
}
|
||||
|
||||
[StructEndianness(Endianness.Big)]
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||
public struct Options {
|
||||
public const Int32 SIZE = 20;
|
||||
|
||||
private UInt32 serialNumber;
|
||||
private UInt32 refreshInterval;
|
||||
private UInt32 retryInterval;
|
||||
private UInt32 expireInterval;
|
||||
private UInt32 ttl;
|
||||
|
||||
public Int64 SerialNumber {
|
||||
get => this.serialNumber;
|
||||
set => this.serialNumber = (UInt32)value;
|
||||
}
|
||||
|
||||
public TimeSpan RefreshInterval {
|
||||
get => TimeSpan.FromSeconds(this.refreshInterval);
|
||||
set => this.refreshInterval = (UInt32)value.TotalSeconds;
|
||||
}
|
||||
|
||||
public TimeSpan RetryInterval {
|
||||
get => TimeSpan.FromSeconds(this.retryInterval);
|
||||
set => this.retryInterval = (UInt32)value.TotalSeconds;
|
||||
}
|
||||
|
||||
public TimeSpan ExpireInterval {
|
||||
get => TimeSpan.FromSeconds(this.expireInterval);
|
||||
set => this.expireInterval = (UInt32)value.TotalSeconds;
|
||||
}
|
||||
|
||||
public TimeSpan MinimumTimeToLive {
|
||||
get => TimeSpan.FromSeconds(this.ttl);
|
||||
set => this.ttl = (UInt32)value.TotalSeconds;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class DnsResourceRecordFactory {
|
||||
public static IList<IDnsResourceRecord> GetAllFromArray(Byte[] message, Int32 offset, Int32 count, out Int32 endOffset) {
|
||||
List<IDnsResourceRecord> result = new List<IDnsResourceRecord>(count);
|
||||
|
||||
for(Int32 i = 0; i < count; i++) {
|
||||
result.Add(GetFromArray(message, offset, out offset));
|
||||
}
|
||||
|
||||
endOffset = offset;
|
||||
return result;
|
||||
}
|
||||
|
||||
private static IDnsResourceRecord GetFromArray(Byte[] message, Int32 offset, out Int32 endOffset) {
|
||||
DnsResourceRecord record = DnsResourceRecord.FromArray(message, offset, out endOffset);
|
||||
Int32 dataOffset = endOffset - record.DataLength;
|
||||
|
||||
return record.Type switch
|
||||
{
|
||||
DnsRecordType.A => (new DnsIPAddressResourceRecord(record)),
|
||||
DnsRecordType.AAAA => new DnsIPAddressResourceRecord(record),
|
||||
DnsRecordType.NS => new DnsNameServerResourceRecord(record, message, dataOffset),
|
||||
DnsRecordType.CNAME => new DnsCanonicalNameResourceRecord(record, message, dataOffset),
|
||||
DnsRecordType.SOA => new DnsStartOfAuthorityResourceRecord(record, message, dataOffset),
|
||||
DnsRecordType.PTR => new DnsPointerResourceRecord(record, message, dataOffset),
|
||||
DnsRecordType.MX => new DnsMailExchangeResourceRecord(record, message, dataOffset),
|
||||
_ => record
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
174
Swan.Tiny/Net/Dns/DnsClient.Response.cs
Normal file
174
Swan.Tiny/Net/Dns/DnsClient.Response.cs
Normal file
@ -0,0 +1,174 @@
|
||||
using Swan.Formatters;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace Swan.Net.Dns {
|
||||
/// <summary>
|
||||
/// DnsClient Response inner class.
|
||||
/// </summary>
|
||||
internal partial class DnsClient {
|
||||
public class DnsClientResponse : IDnsResponse {
|
||||
private readonly DnsResponse _response;
|
||||
private readonly Byte[] _message;
|
||||
|
||||
internal DnsClientResponse(DnsClientRequest request, DnsResponse response, Byte[] message) {
|
||||
this.Request = request;
|
||||
|
||||
this._message = message;
|
||||
this._response = response;
|
||||
}
|
||||
|
||||
public DnsClientRequest Request {
|
||||
get;
|
||||
}
|
||||
|
||||
public Int32 Id {
|
||||
get => this._response.Id;
|
||||
set {
|
||||
}
|
||||
}
|
||||
|
||||
public IList<IDnsResourceRecord> AnswerRecords => this._response.AnswerRecords;
|
||||
|
||||
public IList<IDnsResourceRecord> AuthorityRecords => new ReadOnlyCollection<IDnsResourceRecord>(this._response.AuthorityRecords);
|
||||
|
||||
public IList<IDnsResourceRecord> AdditionalRecords => new ReadOnlyCollection<IDnsResourceRecord>(this._response.AdditionalRecords);
|
||||
|
||||
public Boolean IsRecursionAvailable {
|
||||
get => this._response.IsRecursionAvailable;
|
||||
set {
|
||||
}
|
||||
}
|
||||
|
||||
public Boolean IsAuthorativeServer {
|
||||
get => this._response.IsAuthorativeServer;
|
||||
set {
|
||||
}
|
||||
}
|
||||
|
||||
public Boolean IsTruncated {
|
||||
get => this._response.IsTruncated;
|
||||
set {
|
||||
}
|
||||
}
|
||||
|
||||
public DnsOperationCode OperationCode {
|
||||
get => this._response.OperationCode;
|
||||
set {
|
||||
}
|
||||
}
|
||||
|
||||
public DnsResponseCode ResponseCode {
|
||||
get => this._response.ResponseCode;
|
||||
set {
|
||||
}
|
||||
}
|
||||
|
||||
public IList<DnsQuestion> Questions => new ReadOnlyCollection<DnsQuestion>(this._response.Questions);
|
||||
|
||||
public Int32 Size => this._message.Length;
|
||||
|
||||
public Byte[] ToArray() => this._message;
|
||||
|
||||
public override String ToString() => this._response.ToString();
|
||||
}
|
||||
|
||||
public class DnsResponse : IDnsResponse {
|
||||
private DnsHeader _header;
|
||||
|
||||
public DnsResponse(DnsHeader header, IList<DnsQuestion> questions, IList<IDnsResourceRecord> answers, IList<IDnsResourceRecord> authority, IList<IDnsResourceRecord> additional) {
|
||||
this._header = header;
|
||||
this.Questions = questions;
|
||||
this.AnswerRecords = answers;
|
||||
this.AuthorityRecords = authority;
|
||||
this.AdditionalRecords = additional;
|
||||
}
|
||||
|
||||
public IList<DnsQuestion> Questions {
|
||||
get;
|
||||
}
|
||||
|
||||
public IList<IDnsResourceRecord> AnswerRecords {
|
||||
get;
|
||||
}
|
||||
|
||||
public IList<IDnsResourceRecord> AuthorityRecords {
|
||||
get;
|
||||
}
|
||||
|
||||
public IList<IDnsResourceRecord> AdditionalRecords {
|
||||
get;
|
||||
}
|
||||
|
||||
public Int32 Id {
|
||||
get => this._header.Id;
|
||||
set => this._header.Id = value;
|
||||
}
|
||||
|
||||
public Boolean IsRecursionAvailable {
|
||||
get => this._header.RecursionAvailable;
|
||||
set => this._header.RecursionAvailable = value;
|
||||
}
|
||||
|
||||
public Boolean IsAuthorativeServer {
|
||||
get => this._header.AuthorativeServer;
|
||||
set => this._header.AuthorativeServer = value;
|
||||
}
|
||||
|
||||
public Boolean IsTruncated {
|
||||
get => this._header.Truncated;
|
||||
set => this._header.Truncated = value;
|
||||
}
|
||||
|
||||
public DnsOperationCode OperationCode {
|
||||
get => this._header.OperationCode;
|
||||
set => this._header.OperationCode = value;
|
||||
}
|
||||
|
||||
public DnsResponseCode ResponseCode {
|
||||
get => this._header.ResponseCode;
|
||||
set => this._header.ResponseCode = value;
|
||||
}
|
||||
|
||||
public Int32 Size => this._header.Size + this.Questions.Sum(q => q.Size) + this.AnswerRecords.Sum(a => a.Size) + this.AuthorityRecords.Sum(a => a.Size) + this.AdditionalRecords.Sum(a => a.Size);
|
||||
|
||||
public static DnsResponse FromArray(Byte[] message) {
|
||||
DnsHeader header = DnsHeader.FromArray(message);
|
||||
Int32 offset = header.Size;
|
||||
|
||||
if(!header.Response || header.QuestionCount == 0) {
|
||||
throw new ArgumentException("Invalid response message");
|
||||
}
|
||||
|
||||
return header.Truncated
|
||||
? new DnsResponse(header, DnsQuestion.GetAllFromArray(message, offset, header.QuestionCount), new List<IDnsResourceRecord>(), new List<IDnsResourceRecord>(), new List<IDnsResourceRecord>())
|
||||
: new DnsResponse(header, DnsQuestion.GetAllFromArray(message, offset, header.QuestionCount, out offset), DnsResourceRecordFactory.GetAllFromArray(message, offset, header.AnswerRecordCount, out offset), DnsResourceRecordFactory.GetAllFromArray(message, offset, header.AuthorityRecordCount, out offset), DnsResourceRecordFactory.GetAllFromArray(message, offset, header.AdditionalRecordCount, out _));
|
||||
}
|
||||
|
||||
public Byte[] ToArray() {
|
||||
this.UpdateHeader();
|
||||
MemoryStream result = new MemoryStream(this.Size);
|
||||
|
||||
_ = result.Append(this._header.ToArray()).Append(this.Questions.Select(q => q.ToArray())).Append(this.AnswerRecords.Select(a => a.ToArray())).Append(this.AuthorityRecords.Select(a => a.ToArray())).Append(this.AdditionalRecords.Select(a => a.ToArray()));
|
||||
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
public override String ToString() {
|
||||
this.UpdateHeader();
|
||||
|
||||
return Json.SerializeOnly(this, true, nameof(this.Questions), nameof(this.AnswerRecords), nameof(this.AuthorityRecords), nameof(this.AdditionalRecords));
|
||||
}
|
||||
|
||||
private void UpdateHeader() {
|
||||
this._header.QuestionCount = this.Questions.Count;
|
||||
this._header.AnswerRecordCount = this.AnswerRecords.Count;
|
||||
this._header.AuthorityRecordCount = this.AuthorityRecords.Count;
|
||||
this._header.AdditionalRecordCount = this.AdditionalRecords.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
65
Swan.Tiny/Net/Dns/DnsClient.cs
Normal file
65
Swan.Tiny/Net/Dns/DnsClient.cs
Normal file
@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
#nullable enable
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Swan.Net.Dns {
|
||||
/// <summary>
|
||||
/// DnsClient public methods.
|
||||
/// </summary>
|
||||
internal partial class DnsClient {
|
||||
private readonly IPEndPoint _dns;
|
||||
private readonly IDnsRequestResolver _resolver;
|
||||
|
||||
public DnsClient(IPEndPoint dns, IDnsRequestResolver? resolver = null) {
|
||||
this._dns = dns;
|
||||
this._resolver = resolver ?? new DnsUdpRequestResolver(new DnsTcpRequestResolver());
|
||||
}
|
||||
|
||||
public DnsClient(IPAddress ip, Int32 port = Network.DnsDefaultPort, IDnsRequestResolver? resolver = null) : this(new IPEndPoint(ip, port), resolver) {
|
||||
}
|
||||
|
||||
public DnsClientRequest Create(IDnsRequest? request = null) => new DnsClientRequest(this._dns, request, this._resolver);
|
||||
|
||||
public async Task<IList<IPAddress>> Lookup(String domain, DnsRecordType type = DnsRecordType.A) {
|
||||
if(String.IsNullOrWhiteSpace(domain)) {
|
||||
throw new ArgumentNullException(nameof(domain));
|
||||
}
|
||||
|
||||
if(type != DnsRecordType.A && type != DnsRecordType.AAAA) {
|
||||
throw new ArgumentException("Invalid record type " + type);
|
||||
}
|
||||
|
||||
DnsClientResponse response = await this.Resolve(domain, type).ConfigureAwait(false);
|
||||
List<IPAddress> ips = response.AnswerRecords.Where(r => r.Type == type).Cast<DnsIPAddressResourceRecord>().Select(r => r.IPAddress).ToList();
|
||||
|
||||
return ips.Count == 0 ? throw new DnsQueryException(response, "No matching records") : ips;
|
||||
}
|
||||
|
||||
public async Task<String> Reverse(IPAddress ip) {
|
||||
if(ip == null) {
|
||||
throw new ArgumentNullException(nameof(ip));
|
||||
}
|
||||
|
||||
DnsClientResponse response = await this.Resolve(DnsDomain.PointerName(ip), DnsRecordType.PTR);
|
||||
IDnsResourceRecord ptr = response.AnswerRecords.FirstOrDefault(r => r.Type == DnsRecordType.PTR);
|
||||
|
||||
return ptr == null ? throw new DnsQueryException(response, "No matching records") : ((DnsPointerResourceRecord)ptr).PointerDomainName.ToString();
|
||||
}
|
||||
|
||||
public Task<DnsClientResponse> Resolve(String domain, DnsRecordType type) => this.Resolve(new DnsDomain(domain), type);
|
||||
|
||||
public Task<DnsClientResponse> Resolve(DnsDomain domain, DnsRecordType type) {
|
||||
DnsClientRequest request = this.Create();
|
||||
DnsQuestion question = new DnsQuestion(domain, type);
|
||||
|
||||
request.Questions.Add(question);
|
||||
request.OperationCode = DnsOperationCode.Query;
|
||||
request.RecursionDesired = true;
|
||||
|
||||
return request.Resolve();
|
||||
}
|
||||
}
|
||||
}
|
28
Swan.Tiny/Net/Dns/DnsQueryException.cs
Normal file
28
Swan.Tiny/Net/Dns/DnsQueryException.cs
Normal file
@ -0,0 +1,28 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
|
||||
namespace Swan.Net.Dns {
|
||||
/// <summary>
|
||||
/// An exception thrown when the DNS query fails.
|
||||
/// </summary>
|
||||
/// <seealso cref="Exception" />
|
||||
[Serializable]
|
||||
public class DnsQueryException : Exception {
|
||||
internal DnsQueryException(String message) : base(message) {
|
||||
}
|
||||
|
||||
internal DnsQueryException(String message, Exception e) : base(message, e) {
|
||||
}
|
||||
|
||||
internal DnsQueryException(DnsClient.IDnsResponse response) : this(response, Format(response)) {
|
||||
}
|
||||
|
||||
internal DnsQueryException(DnsClient.IDnsResponse response, String message) : base(message) => this.Response = response;
|
||||
|
||||
internal DnsClient.IDnsResponse? Response {
|
||||
get;
|
||||
}
|
||||
|
||||
private static String Format(DnsClient.IDnsResponse response) => $"Invalid response received with code {response.ResponseCode}";
|
||||
}
|
||||
}
|
130
Swan.Tiny/Net/Dns/DnsQueryResult.cs
Normal file
130
Swan.Tiny/Net/Dns/DnsQueryResult.cs
Normal file
@ -0,0 +1,130 @@
|
||||
namespace Swan.Net.Dns {
|
||||
using System.Collections.Generic;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a response from a DNS server.
|
||||
/// </summary>
|
||||
public class DnsQueryResult {
|
||||
private readonly List<DnsRecord> _mAnswerRecords = new List<DnsRecord>();
|
||||
private readonly List<DnsRecord> _mAdditionalRecords = new List<DnsRecord>();
|
||||
private readonly List<DnsRecord> _mAuthorityRecords = new List<DnsRecord>();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DnsQueryResult"/> class.
|
||||
/// </summary>
|
||||
/// <param name="response">The response.</param>
|
||||
internal DnsQueryResult(DnsClient.IDnsResponse response) : this() {
|
||||
this.Id = response.Id;
|
||||
this.IsAuthoritativeServer = response.IsAuthorativeServer;
|
||||
this.IsRecursionAvailable = response.IsRecursionAvailable;
|
||||
this.IsTruncated = response.IsTruncated;
|
||||
this.OperationCode = response.OperationCode;
|
||||
this.ResponseCode = response.ResponseCode;
|
||||
|
||||
if(response.AnswerRecords != null) {
|
||||
foreach(DnsClient.IDnsResourceRecord record in response.AnswerRecords) {
|
||||
this.AnswerRecords.Add(new DnsRecord(record));
|
||||
}
|
||||
}
|
||||
|
||||
if(response.AuthorityRecords != null) {
|
||||
foreach(DnsClient.IDnsResourceRecord record in response.AuthorityRecords) {
|
||||
this.AuthorityRecords.Add(new DnsRecord(record));
|
||||
}
|
||||
}
|
||||
|
||||
if(response.AdditionalRecords != null) {
|
||||
foreach(DnsClient.IDnsResourceRecord record in response.AdditionalRecords) {
|
||||
this.AdditionalRecords.Add(new DnsRecord(record));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private DnsQueryResult() {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the identifier.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The identifier.
|
||||
/// </value>
|
||||
public System.Int32 Id {
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance is authoritative server.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if this instance is authoritative server; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public System.Boolean IsAuthoritativeServer {
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance is truncated.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if this instance is truncated; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public System.Boolean IsTruncated {
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance is recursion available.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if this instance is recursion available; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public System.Boolean IsRecursionAvailable {
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the operation code.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The operation code.
|
||||
/// </value>
|
||||
public DnsOperationCode OperationCode {
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the response code.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The response code.
|
||||
/// </value>
|
||||
public DnsResponseCode ResponseCode {
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the answer records.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The answer records.
|
||||
/// </value>
|
||||
public IList<DnsRecord> AnswerRecords => this._mAnswerRecords;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the additional records.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The additional records.
|
||||
/// </value>
|
||||
public IList<DnsRecord> AdditionalRecords => this._mAdditionalRecords;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the authority records.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The authority records.
|
||||
/// </value>
|
||||
public IList<DnsRecord> AuthorityRecords => this._mAuthorityRecords;
|
||||
}
|
||||
}
|
239
Swan.Tiny/Net/Dns/DnsRecord.cs
Normal file
239
Swan.Tiny/Net/Dns/DnsRecord.cs
Normal file
@ -0,0 +1,239 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
|
||||
namespace Swan.Net.Dns {
|
||||
/// <summary>
|
||||
/// Represents a DNS record entry.
|
||||
/// </summary>
|
||||
public class DnsRecord {
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DnsRecord"/> class.
|
||||
/// </summary>
|
||||
/// <param name="record">The record.</param>
|
||||
internal DnsRecord(DnsClient.IDnsResourceRecord record) : this() {
|
||||
this.Name = record.Name.ToString();
|
||||
this.Type = record.Type;
|
||||
this.Class = record.Class;
|
||||
this.TimeToLive = record.TimeToLive;
|
||||
this.Data = record.Data;
|
||||
|
||||
// PTR
|
||||
this.PointerDomainName = (record as DnsClient.DnsPointerResourceRecord)?.PointerDomainName?.ToString();
|
||||
|
||||
// A
|
||||
this.IPAddress = (record as DnsClient.DnsIPAddressResourceRecord)?.IPAddress;
|
||||
|
||||
// NS
|
||||
this.NameServerDomainName = (record as DnsClient.DnsNameServerResourceRecord)?.NSDomainName?.ToString();
|
||||
|
||||
// CNAME
|
||||
this.CanonicalDomainName = (record as DnsClient.DnsCanonicalNameResourceRecord)?.CanonicalDomainName.ToString();
|
||||
|
||||
// MX
|
||||
this.MailExchangerDomainName = (record as DnsClient.DnsMailExchangeResourceRecord)?.ExchangeDomainName.ToString();
|
||||
this.MailExchangerPreference = (record as DnsClient.DnsMailExchangeResourceRecord)?.Preference;
|
||||
|
||||
// SOA
|
||||
this.SoaMasterDomainName = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.MasterDomainName.ToString();
|
||||
this.SoaResponsibleDomainName = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.ResponsibleDomainName.ToString();
|
||||
this.SoaSerialNumber = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.SerialNumber;
|
||||
this.SoaRefreshInterval = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.RefreshInterval;
|
||||
this.SoaRetryInterval = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.RetryInterval;
|
||||
this.SoaExpireInterval = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.ExpireInterval;
|
||||
this.SoaMinimumTimeToLive = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.MinimumTimeToLive;
|
||||
}
|
||||
|
||||
private DnsRecord() {
|
||||
// placeholder
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The name.
|
||||
/// </value>
|
||||
public String Name {
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The type.
|
||||
/// </value>
|
||||
public DnsRecordType Type {
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the class.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The class.
|
||||
/// </value>
|
||||
public DnsRecordClass Class {
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the time to live.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The time to live.
|
||||
/// </value>
|
||||
public TimeSpan TimeToLive {
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the raw data of the record.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The data.
|
||||
/// </value>
|
||||
public Byte[] Data {
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the data text bytes in ASCII encoding.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The data text.
|
||||
/// </value>
|
||||
public String DataText => this.Data == null ? String.Empty : Encoding.ASCII.GetString(this.Data);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the pointer domain.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The name of the pointer domain.
|
||||
/// </value>
|
||||
public String PointerDomainName {
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ip address.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The ip address.
|
||||
/// </value>
|
||||
public IPAddress IPAddress {
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the name server domain.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The name of the name server domain.
|
||||
/// </value>
|
||||
public String NameServerDomainName {
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the canonical domain.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The name of the canonical domain.
|
||||
/// </value>
|
||||
public String CanonicalDomainName {
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the mail exchanger preference.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The mail exchanger preference.
|
||||
/// </value>
|
||||
public Int32? MailExchangerPreference {
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the mail exchanger domain.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The name of the mail exchanger domain.
|
||||
/// </value>
|
||||
public String MailExchangerDomainName {
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the soa master domain.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The name of the soa master domain.
|
||||
/// </value>
|
||||
public String SoaMasterDomainName {
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the soa responsible domain.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The name of the soa responsible domain.
|
||||
/// </value>
|
||||
public String SoaResponsibleDomainName {
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the soa serial number.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The soa serial number.
|
||||
/// </value>
|
||||
public Int64? SoaSerialNumber {
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the soa refresh interval.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The soa refresh interval.
|
||||
/// </value>
|
||||
public TimeSpan? SoaRefreshInterval {
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the soa retry interval.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The soa retry interval.
|
||||
/// </value>
|
||||
public TimeSpan? SoaRetryInterval {
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the soa expire interval.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The soa expire interval.
|
||||
/// </value>
|
||||
public TimeSpan? SoaExpireInterval {
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the soa minimum time to live.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The soa minimum time to live.
|
||||
/// </value>
|
||||
public TimeSpan? SoaMinimumTimeToLive {
|
||||
get;
|
||||
}
|
||||
}
|
||||
}
|
167
Swan.Tiny/Net/Dns/Enums.Dns.cs
Normal file
167
Swan.Tiny/Net/Dns/Enums.Dns.cs
Normal file
@ -0,0 +1,167 @@
|
||||
// ReSharper disable InconsistentNaming
|
||||
namespace Swan.Net.Dns {
|
||||
/// <summary>
|
||||
/// Enumerates the different DNS record types.
|
||||
/// </summary>
|
||||
public enum DnsRecordType {
|
||||
/// <summary>
|
||||
/// A records
|
||||
/// </summary>
|
||||
A = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Nameserver records
|
||||
/// </summary>
|
||||
NS = 2,
|
||||
|
||||
/// <summary>
|
||||
/// CNAME records
|
||||
/// </summary>
|
||||
CNAME = 5,
|
||||
|
||||
/// <summary>
|
||||
/// SOA records
|
||||
/// </summary>
|
||||
SOA = 6,
|
||||
|
||||
/// <summary>
|
||||
/// WKS records
|
||||
/// </summary>
|
||||
WKS = 11,
|
||||
|
||||
/// <summary>
|
||||
/// PTR records
|
||||
/// </summary>
|
||||
PTR = 12,
|
||||
|
||||
/// <summary>
|
||||
/// MX records
|
||||
/// </summary>
|
||||
MX = 15,
|
||||
|
||||
/// <summary>
|
||||
/// TXT records
|
||||
/// </summary>
|
||||
TXT = 16,
|
||||
|
||||
/// <summary>
|
||||
/// A records fot IPv6
|
||||
/// </summary>
|
||||
AAAA = 28,
|
||||
|
||||
/// <summary>
|
||||
/// SRV records
|
||||
/// </summary>
|
||||
SRV = 33,
|
||||
|
||||
/// <summary>
|
||||
/// ANY records
|
||||
/// </summary>
|
||||
ANY = 255,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates the different DNS record classes.
|
||||
/// </summary>
|
||||
public enum DnsRecordClass {
|
||||
/// <summary>
|
||||
/// IN records
|
||||
/// </summary>
|
||||
IN = 1,
|
||||
|
||||
/// <summary>
|
||||
/// ANY records
|
||||
/// </summary>
|
||||
ANY = 255,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates the different DNS operation codes.
|
||||
/// </summary>
|
||||
public enum DnsOperationCode {
|
||||
/// <summary>
|
||||
/// Query operation
|
||||
/// </summary>
|
||||
Query = 0,
|
||||
|
||||
/// <summary>
|
||||
/// IQuery operation
|
||||
/// </summary>
|
||||
IQuery,
|
||||
|
||||
/// <summary>
|
||||
/// Status operation
|
||||
/// </summary>
|
||||
Status,
|
||||
|
||||
/// <summary>
|
||||
/// Notify operation
|
||||
/// </summary>
|
||||
Notify = 4,
|
||||
|
||||
/// <summary>
|
||||
/// Update operation
|
||||
/// </summary>
|
||||
Update,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates the different DNS query response codes.
|
||||
/// </summary>
|
||||
public enum DnsResponseCode {
|
||||
/// <summary>
|
||||
/// No error
|
||||
/// </summary>
|
||||
NoError = 0,
|
||||
|
||||
/// <summary>
|
||||
/// No error
|
||||
/// </summary>
|
||||
FormatError,
|
||||
|
||||
/// <summary>
|
||||
/// Format error
|
||||
/// </summary>
|
||||
ServerFailure,
|
||||
|
||||
/// <summary>
|
||||
/// Server failure error
|
||||
/// </summary>
|
||||
NameError,
|
||||
|
||||
/// <summary>
|
||||
/// Name error
|
||||
/// </summary>
|
||||
NotImplemented,
|
||||
|
||||
/// <summary>
|
||||
/// Not implemented error
|
||||
/// </summary>
|
||||
Refused,
|
||||
|
||||
/// <summary>
|
||||
/// Refused error
|
||||
/// </summary>
|
||||
YXDomain,
|
||||
|
||||
/// <summary>
|
||||
/// YXRR error
|
||||
/// </summary>
|
||||
YXRRSet,
|
||||
|
||||
/// <summary>
|
||||
/// NXRR Set error
|
||||
/// </summary>
|
||||
NXRRSet,
|
||||
|
||||
/// <summary>
|
||||
/// Not authorized error
|
||||
/// </summary>
|
||||
NotAuth,
|
||||
|
||||
/// <summary>
|
||||
/// Not zone error
|
||||
/// </summary>
|
||||
NotZone,
|
||||
}
|
||||
}
|
289
Swan.Tiny/Net/Network.cs
Normal file
289
Swan.Tiny/Net/Network.cs
Normal file
@ -0,0 +1,289 @@
|
||||
using Swan.Net.Dns;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Swan.Net {
|
||||
/// <summary>
|
||||
/// Provides miscellaneous network utilities such as a Public IP finder,
|
||||
/// a DNS client to query DNS records of any kind, and an NTP client.
|
||||
/// </summary>
|
||||
public static class Network {
|
||||
/// <summary>
|
||||
/// The DNS default port.
|
||||
/// </summary>
|
||||
public const Int32 DnsDefaultPort = 53;
|
||||
|
||||
/// <summary>
|
||||
/// The NTP default port.
|
||||
/// </summary>
|
||||
public const Int32 NtpDefaultPort = 123;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the host.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The name of the host.
|
||||
/// </value>
|
||||
public static String HostName => IPGlobalProperties.GetIPGlobalProperties().HostName;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the network domain.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The name of the network domain.
|
||||
/// </value>
|
||||
public static String DomainName => IPGlobalProperties.GetIPGlobalProperties().DomainName;
|
||||
|
||||
#region IP Addresses and Adapters Information Methods
|
||||
|
||||
/// <summary>
|
||||
/// Gets the active IPv4 interfaces.
|
||||
/// Only those interfaces with a valid unicast address and a valid gateway will be returned in the collection.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// A collection of NetworkInterface/IPInterfaceProperties pairs
|
||||
/// that represents the active IPv4 interfaces.
|
||||
/// </returns>
|
||||
public static Dictionary<NetworkInterface, IPInterfaceProperties> GetIPv4Interfaces() {
|
||||
// zero conf ip address
|
||||
IPAddress zeroConf = new IPAddress(0);
|
||||
|
||||
NetworkInterface[] adapters = NetworkInterface.GetAllNetworkInterfaces().Where(network => network.OperationalStatus == OperationalStatus.Up && network.NetworkInterfaceType != NetworkInterfaceType.Unknown && network.NetworkInterfaceType != NetworkInterfaceType.Loopback).ToArray();
|
||||
|
||||
Dictionary<NetworkInterface, IPInterfaceProperties> result = new Dictionary<NetworkInterface, IPInterfaceProperties>();
|
||||
|
||||
foreach(NetworkInterface adapter in adapters) {
|
||||
IPInterfaceProperties properties = adapter.GetIPProperties();
|
||||
if(properties == null || properties.GatewayAddresses.Count == 0 || properties.GatewayAddresses.All(gateway => Equals(gateway.Address, zeroConf)) || properties.UnicastAddresses.Count == 0 || properties.GatewayAddresses.All(address => Equals(address.Address, zeroConf)) || properties.UnicastAddresses.Any(a => a.Address.AddressFamily == AddressFamily.InterNetwork) == false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result[adapter] = properties;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the local ip addresses.
|
||||
/// </summary>
|
||||
/// <param name="includeLoopback">if set to <c>true</c> [include loopback].</param>
|
||||
/// <returns>An array of local ip addresses.</returns>
|
||||
public static IPAddress[] GetIPv4Addresses(Boolean includeLoopback = true) => GetIPv4Addresses(NetworkInterfaceType.Unknown, true, includeLoopback);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the local ip addresses.
|
||||
/// </summary>
|
||||
/// <param name="interfaceType">Type of the interface.</param>
|
||||
/// <param name="skipTypeFilter">if set to <c>true</c> [skip type filter].</param>
|
||||
/// <param name="includeLoopback">if set to <c>true</c> [include loopback].</param>
|
||||
/// <returns>An array of local ip addresses.</returns>
|
||||
public static IPAddress[] GetIPv4Addresses(NetworkInterfaceType interfaceType, Boolean skipTypeFilter = false, Boolean includeLoopback = false) {
|
||||
List<IPAddress> addressList = new List<IPAddress>();
|
||||
NetworkInterface[] interfaces = NetworkInterface.GetAllNetworkInterfaces()
|
||||
.Where(ni => (skipTypeFilter || ni.NetworkInterfaceType == interfaceType) && ni.OperationalStatus == OperationalStatus.Up).ToArray();
|
||||
|
||||
foreach(NetworkInterface networkInterface in interfaces) {
|
||||
IPInterfaceProperties properties = networkInterface.GetIPProperties();
|
||||
|
||||
if(properties.GatewayAddresses.All(g => g.Address.AddressFamily != AddressFamily.InterNetwork)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
addressList.AddRange(properties.UnicastAddresses.Where(i => i.Address.AddressFamily == AddressFamily.InterNetwork).Select(i => i.Address));
|
||||
}
|
||||
|
||||
if(includeLoopback || interfaceType == NetworkInterfaceType.Loopback) {
|
||||
addressList.Add(IPAddress.Loopback);
|
||||
}
|
||||
|
||||
return addressList.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the public IP address using ipify.org.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>A public IP address of the result produced by this Task.</returns>
|
||||
public static async Task<IPAddress> GetPublicIPAddressAsync(CancellationToken cancellationToken = default) {
|
||||
using HttpClient client = new HttpClient();
|
||||
HttpResponseMessage response = await client.GetAsync("https://api.ipify.org", cancellationToken).ConfigureAwait(false);
|
||||
return IPAddress.Parse(await response.Content.ReadAsStringAsync().ConfigureAwait(false));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configured IPv4 DNS servers for the active network interfaces.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// A collection of NetworkInterface/IPInterfaceProperties pairs
|
||||
/// that represents the active IPv4 interfaces.
|
||||
/// </returns>
|
||||
public static IPAddress[] GetIPv4DnsServers() => GetIPv4Interfaces().Select(a => a.Value.DnsAddresses.Where(d => d.AddressFamily == AddressFamily.InterNetwork)).SelectMany(d => d).ToArray();
|
||||
|
||||
#endregion
|
||||
|
||||
#region DNS and NTP Clients
|
||||
|
||||
/// <summary>
|
||||
/// Gets the DNS host entry (a list of IP addresses) for the domain name.
|
||||
/// </summary>
|
||||
/// <param name="fqdn">The FQDN.</param>
|
||||
/// <returns>An array of local ip addresses of the result produced by this task.</returns>
|
||||
public static Task<IPAddress[]> GetDnsHostEntryAsync(String fqdn) {
|
||||
IPAddress dnsServer = GetIPv4DnsServers().FirstOrDefault() ?? IPAddress.Parse("8.8.8.8");
|
||||
return GetDnsHostEntryAsync(fqdn, dnsServer, DnsDefaultPort);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the DNS host entry (a list of IP addresses) for the domain name.
|
||||
/// </summary>
|
||||
/// <param name="fqdn">The FQDN.</param>
|
||||
/// <param name="dnsServer">The DNS server.</param>
|
||||
/// <param name="port">The port.</param>
|
||||
/// <returns>
|
||||
/// An array of local ip addresses of the result produced by this task.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">fqdn.</exception>
|
||||
public static async Task<IPAddress[]> GetDnsHostEntryAsync(String fqdn, IPAddress dnsServer, Int32 port) {
|
||||
if(fqdn == null) {
|
||||
throw new ArgumentNullException(nameof(fqdn));
|
||||
}
|
||||
|
||||
if(fqdn.IndexOf(".", StringComparison.Ordinal) == -1) {
|
||||
fqdn += "." + IPGlobalProperties.GetIPGlobalProperties().DomainName;
|
||||
}
|
||||
|
||||
while(true) {
|
||||
if(!fqdn.EndsWith(".", StringComparison.OrdinalIgnoreCase)) {
|
||||
break;
|
||||
}
|
||||
|
||||
fqdn = fqdn[0..^1];
|
||||
}
|
||||
|
||||
DnsClient client = new DnsClient(dnsServer, port);
|
||||
IList<IPAddress> result = await client.Lookup(fqdn).ConfigureAwait(false);
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the reverse lookup FQDN of the given IP Address.
|
||||
/// </summary>
|
||||
/// <param name="query">The query.</param>
|
||||
/// <param name="dnsServer">The DNS server.</param>
|
||||
/// <param name="port">The port.</param>
|
||||
/// <returns>A <see cref="System.String" /> that represents the current object.</returns>
|
||||
public static Task<String> GetDnsPointerEntryAsync(IPAddress query, IPAddress dnsServer, Int32 port) {
|
||||
DnsClient client = new DnsClient(dnsServer, port);
|
||||
return client.Reverse(query);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the reverse lookup FQDN of the given IP Address.
|
||||
/// </summary>
|
||||
/// <param name="query">The query.</param>
|
||||
/// <returns>A <see cref="System.String" /> that represents the current object.</returns>
|
||||
public static Task<String> GetDnsPointerEntryAsync(IPAddress query) {
|
||||
DnsClient client = new DnsClient(GetIPv4DnsServers().FirstOrDefault());
|
||||
return client.Reverse(query);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queries the DNS server for the specified record type.
|
||||
/// </summary>
|
||||
/// <param name="query">The query.</param>
|
||||
/// <param name="recordType">Type of the record.</param>
|
||||
/// <param name="dnsServer">The DNS server.</param>
|
||||
/// <param name="port">The port.</param>
|
||||
/// <returns>Queries the DNS server for the specified record type of the result produced by this Task.</returns>
|
||||
public static async Task<DnsQueryResult> QueryDnsAsync(String query, DnsRecordType recordType, IPAddress dnsServer, Int32 port) {
|
||||
if(query == null) {
|
||||
throw new ArgumentNullException(nameof(query));
|
||||
}
|
||||
|
||||
DnsClient client = new DnsClient(dnsServer, port);
|
||||
DnsClient.DnsClientResponse response = await client.Resolve(query, recordType).ConfigureAwait(false);
|
||||
return new DnsQueryResult(response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queries the DNS server for the specified record type.
|
||||
/// </summary>
|
||||
/// <param name="query">The query.</param>
|
||||
/// <param name="recordType">Type of the record.</param>
|
||||
/// <returns>Queries the DNS server for the specified record type of the result produced by this Task.</returns>
|
||||
public static Task<DnsQueryResult> QueryDnsAsync(String query, DnsRecordType recordType) => QueryDnsAsync(query, recordType, GetIPv4DnsServers().FirstOrDefault(), DnsDefaultPort);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the UTC time by querying from an NTP server.
|
||||
/// </summary>
|
||||
/// <param name="ntpServerAddress">The NTP server address.</param>
|
||||
/// <param name="port">The port.</param>
|
||||
/// <returns>The UTC time by querying from an NTP server of the result produced by this Task.</returns>
|
||||
public static async Task<DateTime> GetNetworkTimeUtcAsync(IPAddress ntpServerAddress, Int32 port = NtpDefaultPort) {
|
||||
if(ntpServerAddress == null) {
|
||||
throw new ArgumentNullException(nameof(ntpServerAddress));
|
||||
}
|
||||
|
||||
// NTP message size - 16 bytes of the digest (RFC 2030)
|
||||
Byte[] ntpData = new Byte[48];
|
||||
|
||||
// Setting the Leap Indicator, Version Number and Mode values
|
||||
ntpData[0] = 0x1B; // LI = 0 (no warning), VN = 3 (IPv4 only), Mode = 3 (Client Mode)
|
||||
|
||||
// The UDP port number assigned to NTP is 123
|
||||
IPEndPoint endPoint = new IPEndPoint(ntpServerAddress, port);
|
||||
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
|
||||
|
||||
|
||||
await socket.ConnectAsync(endPoint).ConfigureAwait(false);
|
||||
|
||||
|
||||
socket.ReceiveTimeout = 3000; // Stops code hang if NTP is blocked
|
||||
_ = socket.Send(ntpData);
|
||||
_ = socket.Receive(ntpData);
|
||||
socket.Dispose();
|
||||
|
||||
// Offset to get to the "Transmit Timestamp" field (time at which the reply
|
||||
// departed the server for the client, in 64-bit timestamp format."
|
||||
const Byte serverReplyTime = 40;
|
||||
|
||||
// Get the seconds part
|
||||
UInt64 intPart = BitConverter.ToUInt32(ntpData, serverReplyTime);
|
||||
|
||||
// Get the seconds fraction
|
||||
UInt64 fractPart = BitConverter.ToUInt32(ntpData, serverReplyTime + 4);
|
||||
|
||||
// Convert From big-endian to little-endian to match the platform
|
||||
if(BitConverter.IsLittleEndian) {
|
||||
intPart = intPart.SwapEndianness();
|
||||
fractPart = intPart.SwapEndianness();
|
||||
}
|
||||
|
||||
UInt64 milliseconds = intPart * 1000 + fractPart * 1000 / 0x100000000L;
|
||||
|
||||
// The time is given in UTC
|
||||
return new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds((Int64)milliseconds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the UTC time by querying from an NTP server.
|
||||
/// </summary>
|
||||
/// <param name="ntpServerName">The NTP server, by default pool.ntp.org.</param>
|
||||
/// <param name="port">The port, by default NTP 123.</param>
|
||||
/// <returns>The UTC time by querying from an NTP server of the result produced by this Task.</returns>
|
||||
public static async Task<DateTime> GetNetworkTimeUtcAsync(String ntpServerName = "pool.ntp.org", Int32 port = NtpDefaultPort) {
|
||||
IPAddress[] addresses = await GetDnsHostEntryAsync(ntpServerName).ConfigureAwait(false);
|
||||
return await GetNetworkTimeUtcAsync(addresses.First(), port).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
51
Swan.Tiny/ProcessResult.cs
Normal file
51
Swan.Tiny/ProcessResult.cs
Normal file
@ -0,0 +1,51 @@
|
||||
using System;
|
||||
|
||||
namespace Swan {
|
||||
/// <summary>
|
||||
/// Represents the text of the standard output and standard error
|
||||
/// of a process, including its exit code.
|
||||
/// </summary>
|
||||
public class ProcessResult {
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ProcessResult" /> class.
|
||||
/// </summary>
|
||||
/// <param name="exitCode">The exit code.</param>
|
||||
/// <param name="standardOutput">The standard output.</param>
|
||||
/// <param name="standardError">The standard error.</param>
|
||||
public ProcessResult(Int32 exitCode, String standardOutput, String standardError) {
|
||||
this.ExitCode = exitCode;
|
||||
this.StandardOutput = standardOutput;
|
||||
this.StandardError = standardError;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the exit code.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The exit code.
|
||||
/// </value>
|
||||
public Int32 ExitCode {
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the text of the standard output.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The standard output.
|
||||
/// </value>
|
||||
public String StandardOutput {
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the text of the standard error.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The standard error.
|
||||
/// </value>
|
||||
public String StandardError {
|
||||
get;
|
||||
}
|
||||
}
|
||||
}
|
353
Swan.Tiny/ProcessRunner.cs
Normal file
353
Swan.Tiny/ProcessRunner.cs
Normal file
@ -0,0 +1,353 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Swan {
|
||||
/// <summary>
|
||||
/// Provides methods to help create external processes, and efficiently capture the
|
||||
/// standard error and standard output streams.
|
||||
/// </summary>
|
||||
public static class ProcessRunner {
|
||||
/// <summary>
|
||||
/// Defines a delegate to handle binary data reception from the standard
|
||||
/// output or standard error streams from a process.
|
||||
/// </summary>
|
||||
/// <param name="processData">The process data.</param>
|
||||
/// <param name="process">The process.</param>
|
||||
public delegate void ProcessDataReceivedCallback(Byte[] processData, Process process);
|
||||
|
||||
/// <summary>
|
||||
/// Runs the process asynchronously and if the exit code is 0,
|
||||
/// returns all of the standard output text. If the exit code is something other than 0
|
||||
/// it returns the contents of standard error.
|
||||
/// This method is meant to be used for programs that output a relatively small amount of text.
|
||||
/// </summary>
|
||||
/// <param name="filename">The filename.</param>
|
||||
/// <param name="arguments">The arguments.</param>
|
||||
/// <param name="workingDirectory">The working directory.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The type of the result produced by this Task.</returns>
|
||||
/// <example>
|
||||
/// The following code explains how to run an external process using the
|
||||
/// <see cref="GetProcessOutputAsync(String, String, CancellationToken)"/> method.
|
||||
/// <code>
|
||||
/// class Example
|
||||
/// {
|
||||
/// using System.Threading.Tasks;
|
||||
/// using Swan;
|
||||
///
|
||||
/// static async Task Main()
|
||||
/// {
|
||||
/// // execute a process and save its output
|
||||
/// var data = await ProcessRunner.
|
||||
/// GetProcessOutputAsync("dotnet", "--help");
|
||||
///
|
||||
/// // print the output
|
||||
/// data.WriteLine();
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public static async Task<String> GetProcessOutputAsync(String filename, String arguments = "", String? workingDirectory = null, CancellationToken cancellationToken = default) {
|
||||
ProcessResult result = await GetProcessResultAsync(filename, arguments, workingDirectory, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
return result.ExitCode == 0 ? result.StandardOutput : result.StandardError;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the process asynchronously and if the exit code is 0,
|
||||
/// returns all of the standard output text. If the exit code is something other than 0
|
||||
/// it returns the contents of standard error.
|
||||
/// This method is meant to be used for programs that output a relatively small amount
|
||||
/// of text using a different encoder.
|
||||
/// </summary>
|
||||
/// <param name="filename">The filename.</param>
|
||||
/// <param name="arguments">The arguments.</param>
|
||||
/// <param name="encoding">The encoding.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>
|
||||
/// The type of the result produced by this Task.
|
||||
/// </returns>
|
||||
public static async Task<String> GetProcessEncodedOutputAsync(String filename, String arguments = "", Encoding? encoding = null, CancellationToken cancellationToken = default) {
|
||||
ProcessResult result = await GetProcessResultAsync(filename, arguments, null, encoding, cancellationToken).ConfigureAwait(false);
|
||||
return result.ExitCode == 0 ? result.StandardOutput : result.StandardError;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes a process asynchronously and returns the text of the standard output and standard error streams
|
||||
/// along with the exit code. This method is meant to be used for programs that output a relatively small
|
||||
/// amount of text.
|
||||
/// </summary>
|
||||
/// <param name="filename">The filename.</param>
|
||||
/// <param name="arguments">The arguments.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>
|
||||
/// Text of the standard output and standard error streams along with the exit code as a <see cref="ProcessResult" /> instance.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">filename.</exception>
|
||||
public static Task<ProcessResult> GetProcessResultAsync(String filename, String arguments = "", CancellationToken cancellationToken = default) => GetProcessResultAsync(filename, arguments, null, Definitions.CurrentAnsiEncoding, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Executes a process asynchronously and returns the text of the standard output and standard error streams
|
||||
/// along with the exit code. This method is meant to be used for programs that output a relatively small
|
||||
/// amount of text.
|
||||
/// </summary>
|
||||
/// <param name="filename">The filename.</param>
|
||||
/// <param name="arguments">The arguments.</param>
|
||||
/// <param name="workingDirectory">The working directory.</param>
|
||||
/// <param name="encoding">The encoding.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>
|
||||
/// Text of the standard output and standard error streams along with the exit code as a <see cref="ProcessResult" /> instance.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">filename.</exception>
|
||||
/// <example>
|
||||
/// The following code describes how to run an external process using the <see cref="GetProcessResultAsync(String, String, String, Encoding, CancellationToken)" /> method.
|
||||
/// <code>
|
||||
/// class Example
|
||||
/// {
|
||||
/// using System.Threading.Tasks;
|
||||
/// using Swan;
|
||||
///
|
||||
/// static async Task Main()
|
||||
/// {
|
||||
/// // Execute a process asynchronously
|
||||
/// var data = await ProcessRunner.GetProcessResultAsync("dotnet", "--help");
|
||||
///
|
||||
/// // print out the exit code
|
||||
/// $"{data.ExitCode}".WriteLine();
|
||||
///
|
||||
/// // print out the output
|
||||
/// data.StandardOutput.WriteLine();
|
||||
/// // and the error if exists
|
||||
/// data.StandardError.Error();
|
||||
/// }
|
||||
/// }
|
||||
/// </code></example>
|
||||
public static async Task<ProcessResult> GetProcessResultAsync(String filename, String arguments, String? workingDirectory, Encoding? encoding = null, CancellationToken cancellationToken = default) {
|
||||
if(filename == null) {
|
||||
throw new ArgumentNullException(nameof(filename));
|
||||
}
|
||||
|
||||
if(encoding == null) {
|
||||
encoding = Definitions.CurrentAnsiEncoding;
|
||||
}
|
||||
|
||||
StringBuilder standardOutputBuilder = new StringBuilder();
|
||||
StringBuilder standardErrorBuilder = new StringBuilder();
|
||||
|
||||
Int32 processReturn = await RunProcessAsync(filename, arguments, workingDirectory, (data, proc) => standardOutputBuilder.Append(encoding.GetString(data)), (data, proc) => standardErrorBuilder.Append(encoding.GetString(data)), encoding, true, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return new ProcessResult(processReturn, standardOutputBuilder.ToString(), standardErrorBuilder.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs an external process asynchronously, providing callbacks to
|
||||
/// capture binary data from the standard error and standard output streams.
|
||||
/// The callbacks contain a reference to the process so you can respond to output or
|
||||
/// error streams by writing to the process' input stream.
|
||||
/// The exit code (return value) will be -1 for forceful termination of the process.
|
||||
/// </summary>
|
||||
/// <param name="filename">The filename.</param>
|
||||
/// <param name="arguments">The arguments.</param>
|
||||
/// <param name="workingDirectory">The working directory.</param>
|
||||
/// <param name="onOutputData">The on output data.</param>
|
||||
/// <param name="onErrorData">The on error data.</param>
|
||||
/// <param name="encoding">The encoding.</param>
|
||||
/// <param name="syncEvents">if set to <c>true</c> the next data callback will wait until the current one completes.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>
|
||||
/// Value type will be -1 for forceful termination of the process.
|
||||
/// </returns>
|
||||
public static Task<Int32> RunProcessAsync(String filename, String arguments, String? workingDirectory, ProcessDataReceivedCallback onOutputData, ProcessDataReceivedCallback? onErrorData, Encoding encoding, Boolean syncEvents = true, CancellationToken cancellationToken = default) {
|
||||
if(filename == null) {
|
||||
throw new ArgumentNullException(nameof(filename));
|
||||
}
|
||||
|
||||
return Task.Run(() => {
|
||||
// Setup the process and its corresponding start info
|
||||
Process process = new Process {
|
||||
EnableRaisingEvents = false,
|
||||
StartInfo = new ProcessStartInfo {
|
||||
Arguments = arguments,
|
||||
CreateNoWindow = true,
|
||||
FileName = filename,
|
||||
RedirectStandardError = true,
|
||||
StandardErrorEncoding = encoding,
|
||||
RedirectStandardOutput = true,
|
||||
StandardOutputEncoding = encoding,
|
||||
UseShellExecute = false,
|
||||
},
|
||||
};
|
||||
|
||||
if(!String.IsNullOrWhiteSpace(workingDirectory)) {
|
||||
process.StartInfo.WorkingDirectory = workingDirectory;
|
||||
}
|
||||
|
||||
// Launch the process and discard any buffered data for standard error and standard output
|
||||
_ = process.Start();
|
||||
process.StandardError.DiscardBufferedData();
|
||||
process.StandardOutput.DiscardBufferedData();
|
||||
|
||||
// Launch the asynchronous stream reading tasks
|
||||
Task[] readTasks = new Task[2];
|
||||
readTasks[0] = CopyStreamAsync(process, process.StandardOutput.BaseStream, onOutputData, syncEvents, cancellationToken);
|
||||
readTasks[1] = CopyStreamAsync(process, process.StandardError.BaseStream, onErrorData, syncEvents, cancellationToken);
|
||||
|
||||
try {
|
||||
// Wait for all tasks to complete
|
||||
Task.WaitAll(readTasks, cancellationToken);
|
||||
} catch(TaskCanceledException) {
|
||||
// ignore
|
||||
} finally {
|
||||
// Wait for the process to exit
|
||||
while(cancellationToken.IsCancellationRequested == false) {
|
||||
if(process.HasExited || process.WaitForExit(5)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Forcefully kill the process if it do not exit
|
||||
try {
|
||||
if(process.HasExited == false) {
|
||||
process.Kill();
|
||||
}
|
||||
} catch {
|
||||
// swallow
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Retrieve and return the exit code.
|
||||
// -1 signals error
|
||||
return process.HasExited ? process.ExitCode : -1;
|
||||
} catch {
|
||||
return -1;
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs an external process asynchronously, providing callbacks to
|
||||
/// capture binary data from the standard error and standard output streams.
|
||||
/// The callbacks contain a reference to the process so you can respond to output or
|
||||
/// error streams by writing to the process' input stream.
|
||||
/// The exit code (return value) will be -1 for forceful termination of the process.
|
||||
/// </summary>
|
||||
/// <param name="filename">The filename.</param>
|
||||
/// <param name="arguments">The arguments.</param>
|
||||
/// <param name="onOutputData">The on output data.</param>
|
||||
/// <param name="onErrorData">The on error data.</param>
|
||||
/// <param name="syncEvents">if set to <c>true</c> the next data callback will wait until the current one completes.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Value type will be -1 for forceful termination of the process.</returns>
|
||||
/// <example>
|
||||
/// The following example illustrates how to run an external process using the
|
||||
/// <see cref="RunProcessAsync(String, String, ProcessDataReceivedCallback, ProcessDataReceivedCallback, Boolean, CancellationToken)"/>
|
||||
/// method.
|
||||
/// <code>
|
||||
/// class Example
|
||||
/// {
|
||||
/// using System.Diagnostics;
|
||||
/// using System.Text;
|
||||
/// using System.Threading.Tasks;
|
||||
/// using Swan;
|
||||
///
|
||||
/// static async Task Main()
|
||||
/// {
|
||||
/// // Execute a process asynchronously
|
||||
/// var data = await ProcessRunner
|
||||
/// .RunProcessAsync("dotnet", "--help", Print, Print);
|
||||
///
|
||||
/// // flush all messages
|
||||
/// Terminal.Flush();
|
||||
/// }
|
||||
///
|
||||
/// // a callback to print both output or errors
|
||||
/// static void Print(byte[] data, Process proc) =>
|
||||
/// Encoding.GetEncoding(0).GetString(data).WriteLine();
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public static Task<Int32> RunProcessAsync(String filename, String arguments, ProcessDataReceivedCallback onOutputData, ProcessDataReceivedCallback? onErrorData, Boolean syncEvents = true, CancellationToken cancellationToken = default) => RunProcessAsync(filename, arguments, null, onOutputData, onErrorData, Definitions.CurrentAnsiEncoding, syncEvents, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Copies the stream asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="process">The process.</param>
|
||||
/// <param name="baseStream">The source stream.</param>
|
||||
/// <param name="onDataCallback">The on data callback.</param>
|
||||
/// <param name="syncEvents">if set to <c>true</c> [synchronize events].</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>Total copies stream.</returns>
|
||||
private static Task<UInt64> CopyStreamAsync(Process process, Stream baseStream, ProcessDataReceivedCallback? onDataCallback, Boolean syncEvents, CancellationToken ct) => Task.Run(async () => {
|
||||
// define some state variables
|
||||
Byte[] swapBuffer = new Byte[2048]; // the buffer to copy data from one stream to the next
|
||||
UInt64 totalCount = 0; // the total amount of bytes read
|
||||
Boolean hasExited = false;
|
||||
|
||||
while(ct.IsCancellationRequested == false) {
|
||||
try {
|
||||
// Check if process is no longer valid
|
||||
// if this condition holds, simply read the last bits of data available.
|
||||
Int32 readCount; // the bytes read in any given event
|
||||
if(process.HasExited || process.WaitForExit(1)) {
|
||||
while(true) {
|
||||
try {
|
||||
readCount = await baseStream.ReadAsync(swapBuffer, 0, swapBuffer.Length, ct);
|
||||
|
||||
if(readCount > 0) {
|
||||
totalCount += (UInt64)readCount;
|
||||
onDataCallback?.Invoke(swapBuffer.Skip(0).Take(readCount).ToArray(), process);
|
||||
} else {
|
||||
hasExited = true;
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
hasExited = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(hasExited) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Try reading from the stream. < 0 means no read occurred.
|
||||
readCount = await baseStream.ReadAsync(swapBuffer, 0, swapBuffer.Length, ct).ConfigureAwait(false);
|
||||
|
||||
// When no read is done, we need to let is rest for a bit
|
||||
if(readCount <= 0) {
|
||||
await Task.Delay(1, ct).ConfigureAwait(false); // do not hog CPU cycles doing nothing.
|
||||
continue;
|
||||
}
|
||||
|
||||
totalCount += (UInt64)readCount;
|
||||
if(onDataCallback == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create the buffer to pass to the callback
|
||||
Byte[] eventBuffer = swapBuffer.Skip(0).Take(readCount).ToArray();
|
||||
|
||||
// Create the data processing callback invocation
|
||||
Task eventTask = Task.Run(() => onDataCallback.Invoke(eventBuffer, process), ct);
|
||||
|
||||
// wait for the event to process before the next read occurs
|
||||
if(syncEvents) {
|
||||
eventTask.Wait(ct);
|
||||
}
|
||||
} catch {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return totalCount;
|
||||
}, ct);
|
||||
}
|
||||
}
|
161
Swan.Tiny/Reflection/AttributeCache.cs
Normal file
161
Swan.Tiny/Reflection/AttributeCache.cs
Normal file
@ -0,0 +1,161 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Swan.Reflection {
|
||||
/// <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) => this.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 Boolean Contains<T>(MemberInfo member) => this._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, Boolean inherit = false) where T : Attribute {
|
||||
if(member == null) {
|
||||
throw new ArgumentNullException(nameof(member));
|
||||
}
|
||||
|
||||
return this.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, Boolean inherit = false) {
|
||||
if(member == null) {
|
||||
throw new ArgumentNullException(nameof(member));
|
||||
}
|
||||
|
||||
if(type == null) {
|
||||
throw new ArgumentNullException(nameof(type));
|
||||
}
|
||||
|
||||
return this.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, Boolean inherit = false) where T : Attribute {
|
||||
if(member == null) {
|
||||
return default!;
|
||||
}
|
||||
|
||||
IEnumerable<Object> attr = this.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>(Boolean inherit = false) where TAttribute : Attribute {
|
||||
IEnumerable<Object> attr = this.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, Boolean inherit = false) where T : Attribute => this.PropertyTypeCache.RetrieveAllProperties(type, true).ToDictionary(x => x, x => this.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>(Boolean inherit = false) => this.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, Boolean inherit = false) {
|
||||
if(attributeType == null) {
|
||||
throw new ArgumentNullException(nameof(attributeType));
|
||||
}
|
||||
|
||||
return this.PropertyTypeCache.RetrieveAllProperties<T>(true).ToDictionary(x => x, x => this.Retrieve(x, attributeType, inherit));
|
||||
}
|
||||
|
||||
private static T ConvertToAttribute<T>(IEnumerable<Object> attr) where T : Attribute => attr?.Any() != true ? (default!) : attr.Count() == 1 ? (T)Convert.ChangeType(attr.First(), typeof(T)) : throw new AmbiguousMatchException("Multiple custom attributes of the same type found.");
|
||||
|
||||
private IEnumerable<Object> Retrieve(Tuple<Object, Type> key, Func<Tuple<Object, Type>, IEnumerable<Object>> factory) {
|
||||
if(factory == null) {
|
||||
throw new ArgumentNullException(nameof(factory));
|
||||
}
|
||||
|
||||
return this._data.Value.GetOrAdd(key, k => factory.Invoke(k).Where(item => item != null));
|
||||
}
|
||||
}
|
||||
}
|
42
Swan.Tiny/Reflection/ConstructorTypeCache.cs
Normal file
42
Swan.Tiny/Reflection/ConstructorTypeCache.cs
Normal file
@ -0,0 +1,42 @@
|
||||
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>(Boolean includeNonPublic = false) => this.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, Boolean includeNonPublic = false) => this.Retrieve(type, GetConstructors(includeNonPublic));
|
||||
|
||||
private static Func<Type, IEnumerable<Tuple<ConstructorInfo, ParameterInfo[]>>> GetConstructors(Boolean includeNonPublic) => t => t.GetConstructors(includeNonPublic ? BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance : BindingFlags.Public | BindingFlags.Instance).Select(x => Tuple.Create(x, x.GetParameters())).OrderBy(x => x.Item2.Length).ToList();
|
||||
}
|
||||
}
|
247
Swan.Tiny/Reflection/ExtendedTypeInfo.cs
Normal file
247
Swan.Tiny/Reflection/ExtendedTypeInfo.cs
Normal file
@ -0,0 +1,247 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Swan.Reflection {
|
||||
/// <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(Single),
|
||||
typeof(Int32),
|
||||
typeof(UInt32),
|
||||
typeof(Int64),
|
||||
typeof(UInt64),
|
||||
typeof(Int16),
|
||||
typeof(UInt16),
|
||||
};
|
||||
|
||||
private readonly ParameterInfo[]? _tryParseParameters;
|
||||
private readonly Int32 _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) {
|
||||
this.Type = t ?? throw new ArgumentNullException(nameof(t));
|
||||
this.IsNullableValueType = this.Type.IsGenericType && this.Type.GetGenericTypeDefinition() == typeof(Nullable<>);
|
||||
|
||||
this.IsValueType = t.IsValueType;
|
||||
|
||||
this.UnderlyingType = this.IsNullableValueType ? new NullableConverter(this.Type).UnderlyingType : this.Type;
|
||||
|
||||
this.IsNumeric = NumericTypes.Contains(this.UnderlyingType);
|
||||
|
||||
// Extract the TryParse method info
|
||||
try {
|
||||
this.TryParseMethodInfo = this.UnderlyingType.GetMethod(TryParseMethodName, new[] { typeof(String), typeof(NumberStyles), typeof(IFormatProvider), this.UnderlyingType.MakeByRefType() }) ?? this.UnderlyingType.GetMethod(TryParseMethodName, new[] { typeof(String), this.UnderlyingType.MakeByRefType() });
|
||||
|
||||
this._tryParseParameters = this.TryParseMethodInfo?.GetParameters();
|
||||
} catch {
|
||||
// ignored
|
||||
}
|
||||
|
||||
// Extract the ToString method Info
|
||||
try {
|
||||
this.ToStringMethodInfo = this.UnderlyingType.GetMethod(ToStringMethodName, new[] { typeof(IFormatProvider) }) ?? this.UnderlyingType.GetMethod(ToStringMethodName, Array.Empty<Type>());
|
||||
|
||||
this._toStringArgumentLength = this.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 Boolean 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 Boolean IsNumeric {
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the type is value type.
|
||||
/// Nullable value types have this property set to False.
|
||||
/// </summary>
|
||||
public Boolean 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 Boolean CanParseNatively => this.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 Boolean TryParse(String s, out Object? result) {
|
||||
result = this.Type.GetDefault();
|
||||
|
||||
try {
|
||||
if(this.Type == typeof(String)) {
|
||||
result = Convert.ChangeType(s, this.Type, CultureInfo.InvariantCulture);
|
||||
return true;
|
||||
}
|
||||
|
||||
if(this.IsNullableValueType && String.IsNullOrEmpty(s) || !this.CanParseNatively) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Build the arguments of the TryParse method
|
||||
List<Object?> dynamicArguments = new List<Object?> { s };
|
||||
if(this._tryParseParameters != null) {
|
||||
for(Int32 pi = 1; pi < this._tryParseParameters.Length - 1; pi++) {
|
||||
ParameterInfo argInfo = this._tryParseParameters[pi];
|
||||
if(argInfo.ParameterType == typeof(IFormatProvider)) {
|
||||
dynamicArguments.Add(CultureInfo.InvariantCulture);
|
||||
} else if(argInfo.ParameterType == typeof(NumberStyles)) {
|
||||
dynamicArguments.Add(NumberStyles.Any);
|
||||
} else {
|
||||
dynamicArguments.Add(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dynamicArguments.Add(null);
|
||||
Object?[] parseArguments = dynamicArguments.ToArray();
|
||||
|
||||
if((Boolean)this.TryParseMethodInfo?.Invoke(null, parseArguments)!) {
|
||||
result = parseArguments[^1];
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <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) => instance == null ? String.Empty : this._toStringArgumentLength != 1 ? instance.ToString()! : this.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.Tiny/Reflection/IPropertyProxy.cs
Normal file
22
Swan.Tiny/Reflection/IPropertyProxy.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using System;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
44
Swan.Tiny/Reflection/PropertyProxy.cs
Normal file
44
Swan.Tiny/Reflection/PropertyProxy.cs
Normal file
@ -0,0 +1,44 @@
|
||||
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));
|
||||
}
|
||||
|
||||
MethodInfo getterInfo = property.GetGetMethod(false);
|
||||
if(getterInfo != null) {
|
||||
this._getter = (Func<TClass, TProperty>)Delegate.CreateDelegate(typeof(Func<TClass, TProperty>), getterInfo);
|
||||
}
|
||||
|
||||
MethodInfo setterInfo = property.GetSetMethod(false);
|
||||
if(setterInfo != null) {
|
||||
this._setter = (Action<TClass, TProperty>)Delegate.CreateDelegate(typeof(Action<TClass, TProperty>), setterInfo);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
Object IPropertyProxy.GetValue(Object instance) => this._getter(instance as TClass);
|
||||
|
||||
/// <inheritdoc />
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
void IPropertyProxy.SetValue(Object instance, Object value) => this._setter(instance as TClass, (TProperty)value);
|
||||
}
|
||||
}
|
56
Swan.Tiny/Reflection/PropertyTypeCache.cs
Normal file
56
Swan.Tiny/Reflection/PropertyTypeCache.cs
Normal file
@ -0,0 +1,56 @@
|
||||
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>(Boolean onlyPublic = false) => this.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, Boolean onlyPublic = false) => this.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, Boolean onlyPublic, Func<PropertyInfo, Boolean> filter) => this.Retrieve(type, onlyPublic ? GetAllPublicPropertiesFunc(filter) : GetAllPropertiesFunc(filter));
|
||||
|
||||
private static Func<Type, IEnumerable<PropertyInfo>> GetAllPropertiesFunc(Func<PropertyInfo, Boolean> filter = null) => GetPropertiesFunc(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, filter);
|
||||
|
||||
private static Func<Type, IEnumerable<PropertyInfo>> GetAllPublicPropertiesFunc(Func<PropertyInfo, Boolean> filter = null) => GetPropertiesFunc(BindingFlags.Public | BindingFlags.Instance, filter);
|
||||
|
||||
private static Func<Type, IEnumerable<PropertyInfo>> GetPropertiesFunc(BindingFlags flags, Func<PropertyInfo, Boolean> filter = null) => t => t.GetProperties(flags).Where(filter ?? (p => p.CanRead || p.CanWrite));
|
||||
}
|
||||
}
|
71
Swan.Tiny/Reflection/TypeCache.cs
Normal file
71
Swan.Tiny/Reflection/TypeCache.cs
Normal file
@ -0,0 +1,71 @@
|
||||
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 Boolean Contains<TOut>() => this.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) => this.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>() => this.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) => this.Retrieve(type, GetAllFieldsFunc());
|
||||
|
||||
private static Func<Type, IEnumerable<FieldInfo>> GetAllFieldsFunc() => t => t.GetFields(BindingFlags.Public | BindingFlags.Instance);
|
||||
}
|
||||
}
|
54
Swan.Tiny/SingletonBase.cs
Normal file
54
Swan.Tiny/SingletonBase.cs
Normal file
@ -0,0 +1,54 @@
|
||||
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 Boolean _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() => this.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(Boolean disposeManaged) {
|
||||
if(this._isDisposing) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._isDisposing = true;
|
||||
|
||||
// free managed resources
|
||||
if(LazyInstance == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
IDisposable disposableInstance = LazyInstance.Value as IDisposable;
|
||||
disposableInstance?.Dispose();
|
||||
} catch {
|
||||
// swallow
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
27
Swan.Tiny/StructEndiannessAttribute.cs
Normal file
27
Swan.Tiny/StructEndiannessAttribute.cs
Normal file
@ -0,0 +1,27 @@
|
||||
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) => this.Endianness = endianness;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the endianness.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The endianness.
|
||||
/// </value>
|
||||
public Endianness Endianness {
|
||||
get;
|
||||
}
|
||||
}
|
||||
}
|
7
Swan.Tiny/Swan.Tiny.csproj
Normal file
7
Swan.Tiny/Swan.Tiny.csproj
Normal file
@ -0,0 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
200
Swan.Tiny/SwanRuntime.cs
Normal file
200
Swan.Tiny/SwanRuntime.cs
Normal file
@ -0,0 +1,200 @@
|
||||
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>(() => {
|
||||
AssemblyCompanyAttribute attribute = EntryAssembly.GetCustomAttribute(typeof(AssemblyCompanyAttribute)) as AssemblyCompanyAttribute;
|
||||
return attribute?.Company ?? String.Empty;
|
||||
});
|
||||
|
||||
private static readonly Lazy<String> ProductNameLazy = new Lazy<String>(() => {
|
||||
AssemblyProductAttribute attribute = EntryAssembly.GetCustomAttribute(typeof(AssemblyProductAttribute)) as AssemblyProductAttribute;
|
||||
return attribute?.Product ?? String.Empty;
|
||||
});
|
||||
|
||||
private static readonly Lazy<String> ProductTrademarkLazy = new Lazy<String>(() => {
|
||||
AssemblyTrademarkAttribute attribute = EntryAssembly.GetCustomAttribute(typeof(AssemblyTrademarkAttribute)) as AssemblyTrademarkAttribute;
|
||||
return attribute?.Trademark ?? String.Empty;
|
||||
});
|
||||
|
||||
private static readonly String ApplicationMutexName = "Global\\{{" + EntryAssembly.FullName + "}}";
|
||||
|
||||
private static readonly Object SyncLock = new Object();
|
||||
|
||||
private static OperatingSystem? _oS;
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current Operating System.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The os.
|
||||
/// </value>
|
||||
public static OperatingSystem OS {
|
||||
get {
|
||||
if(_oS.HasValue == false) {
|
||||
String windowsDirectory = Environment.GetEnvironmentVariable("windir");
|
||||
_oS = String.IsNullOrEmpty(windowsDirectory) == false && windowsDirectory.Contains(@"\") && Directory.Exists(windowsDirectory)
|
||||
? (OperatingSystem?)OperatingSystem.Windows
|
||||
: (OperatingSystem?)(File.Exists(@"/proc/sys/kernel/ostype") ? OperatingSystem.Unix : OperatingSystem.Osx);
|
||||
}
|
||||
|
||||
return _oS ?? OperatingSystem.Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <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>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Codequalität", "IDE0067:Objekte verwerfen, bevor Bereich verloren geht", Justification = "<Ausstehend>")]
|
||||
public static Boolean IsTheOnlyInstance {
|
||||
get {
|
||||
lock(SyncLock) {
|
||||
try {
|
||||
// Try to open existing mutex.
|
||||
_ = Mutex.OpenExisting(ApplicationMutexName);
|
||||
} catch {
|
||||
try {
|
||||
// If exception occurred, there is no such mutex.
|
||||
Mutex appMutex = new Mutex(true, ApplicationMutexName);
|
||||
$"Application Mutex created {appMutex} named '{ApplicationMutexName}'".Debug(typeof(SwanRuntime));
|
||||
|
||||
// Only one instance.
|
||||
return true;
|
||||
} catch {
|
||||
// Sometimes the user can't create the Global Mutex
|
||||
}
|
||||
}
|
||||
|
||||
// More than one instance.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <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 Boolean 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 {
|
||||
UriBuilder uri = new UriBuilder(EntryAssembly.CodeBase);
|
||||
String 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 {
|
||||
String localAppDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), EntryAssemblyName.Name);
|
||||
|
||||
String returnPath = Path.Combine(localAppDataPath, EntryAssemblyVersion.ToString());
|
||||
|
||||
if(!Directory.Exists(returnPath)) {
|
||||
_ = Directory.CreateDirectory(returnPath);
|
||||
}
|
||||
|
||||
return returnPath;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <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));
|
||||
}
|
||||
|
||||
String pathWithFilename = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), filename);
|
||||
|
||||
return Path.GetFullPath(pathWithFilename);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
88
Swan.Tiny/Terminal.Output.cs
Normal file
88
Swan.Tiny/Terminal.Output.cs
Normal file
@ -0,0 +1,88 @@
|
||||
#nullable enable
|
||||
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, Int32 count = 1, Boolean newLine = false, TerminalWriters writerFlags = TerminalWriters.StandardOutput) {
|
||||
lock(SyncLock) {
|
||||
String text = new String(charCode, count);
|
||||
|
||||
if(newLine) {
|
||||
text += Environment.NewLine;
|
||||
}
|
||||
|
||||
Byte[] buffer = OutputEncoding.GetBytes(text);
|
||||
OutputContext context = new OutputContext {
|
||||
OutputColor = color ?? Settings.DefaultColor,
|
||||
OutputText = OutputEncoding.GetChars(buffer),
|
||||
OutputWriters = writerFlags,
|
||||
};
|
||||
|
||||
EnqueueOutput(context);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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) {
|
||||
Byte[] buffer = OutputEncoding.GetBytes(text);
|
||||
OutputContext 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;
|
||||
}
|
||||
}
|
||||
}
|
46
Swan.Tiny/Terminal.Settings.cs
Normal file
46
Swan.Tiny/Terminal.Settings.cs
Normal file
@ -0,0 +1,46 @@
|
||||
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: ";
|
||||
}
|
||||
}
|
||||
}
|
330
Swan.Tiny/Terminal.cs
Normal file
330
Swan.Tiny/Terminal.cs
Normal file
@ -0,0 +1,330 @@
|
||||
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 Int32 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 Boolean? _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 Int32 CursorLeft {
|
||||
get {
|
||||
if(IsConsolePresent == false) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
lock(SyncLock) {
|
||||
Flush();
|
||||
return Console.CursorLeft;
|
||||
}
|
||||
}
|
||||
set {
|
||||
if(IsConsolePresent == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
lock(SyncLock) {
|
||||
Flush();
|
||||
Console.CursorLeft = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the cursor top position.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The cursor top.
|
||||
/// </value>
|
||||
public static Int32 CursorTop {
|
||||
get {
|
||||
if(IsConsolePresent == false) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
lock(SyncLock) {
|
||||
Flush();
|
||||
return Console.CursorTop;
|
||||
}
|
||||
}
|
||||
set {
|
||||
if(IsConsolePresent == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
lock(SyncLock) {
|
||||
Flush();
|
||||
Console.CursorTop = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <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 Boolean IsConsolePresent {
|
||||
get {
|
||||
if(_isConsolePresent == null) {
|
||||
_isConsolePresent = true;
|
||||
try {
|
||||
Int32 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;
|
||||
}
|
||||
|
||||
DateTime startTime = DateTime.UtcNow;
|
||||
|
||||
while(OutputQueue.Count > 0) {
|
||||
// Manually trigger a timer cycle to run immediately
|
||||
DequeueOutputTimer.Change(0, OutputFlushInterval);
|
||||
|
||||
// Wait for the output to finish
|
||||
if(OutputDone.Wait(OutputFlushInterval)) {
|
||||
break;
|
||||
}
|
||||
|
||||
// infinite timeout
|
||||
if(timeout.Value == TimeSpan.Zero) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// break if we have reached a timeout condition
|
||||
if(DateTime.UtcNow.Subtract(startTime) >= timeout.Value) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the cursor position.
|
||||
/// </summary>
|
||||
/// <param name="left">The left.</param>
|
||||
/// <param name="top">The top.</param>
|
||||
public static void SetCursorPosition(Int32 left, Int32 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) {
|
||||
TerminalWriters availableWriters = AvailableWriters;
|
||||
|
||||
if(availableWriters == TerminalWriters.None || context.OutputWriters == TerminalWriters.None) {
|
||||
OutputDone.Set();
|
||||
return;
|
||||
}
|
||||
|
||||
if((context.OutputWriters & availableWriters) == TerminalWriters.None) {
|
||||
return;
|
||||
}
|
||||
|
||||
OutputDone.Reset();
|
||||
OutputQueue.Enqueue(context);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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 OutputContext context)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Process Console output and Skip over stuff we can't display so we don't stress the output too much.
|
||||
if(!IsConsolePresent) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Console.ForegroundColor = context.OutputColor;
|
||||
|
||||
// Output to the standard output
|
||||
if(context.OutputWriters.HasFlag(TerminalWriters.StandardOutput)) {
|
||||
Console.Out.Write(context.OutputText);
|
||||
}
|
||||
|
||||
// output to the standard error
|
||||
if(context.OutputWriters.HasFlag(TerminalWriters.StandardError)) {
|
||||
Console.Error.Write(context.OutputText);
|
||||
}
|
||||
|
||||
Console.ResetColor();
|
||||
Console.ForegroundColor = context.OriginalColor;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Output Context
|
||||
|
||||
/// <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() {
|
||||
this.OriginalColor = Settings.DefaultColor;
|
||||
this.OutputWriters = IsConsolePresent ? TerminalWriters.StandardOutput : TerminalWriters.None;
|
||||
}
|
||||
|
||||
public ConsoleColor OriginalColor {
|
||||
get;
|
||||
}
|
||||
public ConsoleColor OutputColor {
|
||||
get; set;
|
||||
}
|
||||
public Char[] OutputText {
|
||||
get; set;
|
||||
}
|
||||
public TerminalWriters OutputWriters {
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
29
Swan.Tiny/TerminalWriters.Enums.cs
Normal file
29
Swan.Tiny/TerminalWriters.Enums.cs
Normal file
@ -0,0 +1,29 @@
|
||||
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,
|
||||
}
|
||||
}
|
22
Swan.Tiny/Threading/AtomicBoolean.cs
Normal file
22
Swan.Tiny/Threading/AtomicBoolean.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using System;
|
||||
|
||||
namespace Swan.Threading {
|
||||
/// <summary>
|
||||
/// Fast, atomic boolean combining interlocked to write value and volatile to read values.
|
||||
/// </summary>
|
||||
public sealed class AtomicBoolean : AtomicTypeBase<Boolean> {
|
||||
/// <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(Boolean initialValue = default) : base(initialValue ? 1 : 0) {
|
||||
// placeholder
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Boolean FromLong(Int64 backingValue) => backingValue != 0;
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Int64 ToLong(Boolean value) => value ? 1 : 0;
|
||||
}
|
||||
}
|
219
Swan.Tiny/Threading/AtomicTypeBase.cs
Normal file
219
Swan.Tiny/Threading/AtomicTypeBase.cs
Normal file
@ -0,0 +1,219 @@
|
||||
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 Int64 _backingValue;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AtomicTypeBase{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="initialValue">The initial value.</param>
|
||||
protected AtomicTypeBase(Int64 initialValue) => this.BackingValue = initialValue;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value.
|
||||
/// </summary>
|
||||
public T Value {
|
||||
get => this.FromLong(this.BackingValue);
|
||||
set => this.BackingValue = this.ToLong(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the backing value.
|
||||
/// </summary>
|
||||
protected Int64 BackingValue {
|
||||
get => Interlocked.Read(ref this._backingValue);
|
||||
set => Interlocked.Exchange(ref this._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 Boolean 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 Boolean 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 Boolean 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 Boolean 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 Boolean 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 Boolean 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, Int64 operand) {
|
||||
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, Int64 operand) {
|
||||
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 Int32 CompareTo(Object other) => other switch
|
||||
{
|
||||
null => 1,
|
||||
AtomicTypeBase<T> atomic => this.BackingValue.CompareTo(atomic.BackingValue),
|
||||
T variable => this.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 Int32 CompareTo(T other) => this.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 Int32 CompareTo(AtomicTypeBase<T> other) => this.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 Boolean Equals(Object other) => other switch
|
||||
{
|
||||
AtomicTypeBase<T> atomic => this.Equals(atomic),
|
||||
T variable => this.Equals(variable),
|
||||
|
||||
_ => 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 Int32 GetHashCode() => this.BackingValue.GetHashCode();
|
||||
|
||||
/// <inheritdoc />
|
||||
public Boolean Equals(AtomicTypeBase<T> other) => this.BackingValue == (other?.BackingValue ?? default);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Boolean Equals(T other) => Equals(this.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(Int64 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 Int64 ToLong(T value);
|
||||
}
|
||||
}
|
206
Swan.Tiny/Threading/ExclusiveTimer.cs
Normal file
206
Swan.Tiny/Threading/ExclusiveTimer.cs
Normal file
@ -0,0 +1,206 @@
|
||||
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 Int32 _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, Int32 dueTime, Int32 period) {
|
||||
this._period = period;
|
||||
this._userCallback = timerCallback;
|
||||
this._backingTimer = new Timer(this.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, Int32 dueTime, Int32 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 Boolean IsDisposing => this._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 Boolean IsDisposed => this._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) {
|
||||
static void Callback(IWaitEvent waitEvent) {
|
||||
try {
|
||||
waitEvent.Complete();
|
||||
waitEvent.Begin();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
using IWaitEvent delayLock = WaitEventFactory.Create(true);
|
||||
using ExclusiveTimer _ = new ExclusiveTimer(() => Callback(delayLock), 0, 15);
|
||||
while(!cancellationToken.IsCancellationRequested && DateTime.UtcNow < untilDate) {
|
||||
delayLock.Wait();
|
||||
}
|
||||
}
|
||||
|
||||
/// <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(Int32 dueTime, Int32 period) {
|
||||
this._period = period;
|
||||
|
||||
_ = this._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) => this.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(Int32 period) => this.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) => this.Change(TimeSpan.Zero, period);
|
||||
|
||||
/// <summary>
|
||||
/// Pauses this instance.
|
||||
/// </summary>
|
||||
public void Pause() => this.Change(Timeout.Infinite, Timeout.Infinite);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() {
|
||||
lock(this._syncLock) {
|
||||
if(this._isDisposed == true || this._isDisposing == true) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._isDisposing.Value = true;
|
||||
}
|
||||
|
||||
try {
|
||||
this._cycleDoneEvent.Wait();
|
||||
this._cycleDoneEvent.Dispose();
|
||||
this.Pause();
|
||||
this._backingTimer.Dispose();
|
||||
} finally {
|
||||
this._isDisposed.Value = true;
|
||||
this._isDisposing.Value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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(this._syncLock) {
|
||||
if(this.IsDisposed || this.IsDisposing) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if(this._cycleDoneEvent.IsSet == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._cycleDoneEvent.Reset();
|
||||
|
||||
try {
|
||||
this._userCallback(state);
|
||||
} finally {
|
||||
this._cycleDoneEvent?.Set();
|
||||
_ = this._backingTimer?.Change(this._period, Timeout.Infinite);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
63
Swan.Tiny/Threading/IWaitEvent.cs
Normal file
63
Swan.Tiny/Threading/IWaitEvent.cs
Normal file
@ -0,0 +1,63 @@
|
||||
using System;
|
||||
|
||||
namespace Swan.Threading {
|
||||
/// <summary>
|
||||
/// Provides a generalized API for ManualResetEvent and ManualResetEventSlim.
|
||||
/// </summary>
|
||||
/// <seealso cref="IDisposable" />
|
||||
public interface IWaitEvent : IDisposable {
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the event is in the completed state.
|
||||
/// </summary>
|
||||
Boolean IsCompleted {
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the Begin method has been called.
|
||||
/// It returns false after the Complete method is called.
|
||||
/// </summary>
|
||||
Boolean IsInProgress {
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the underlying handle is not closed and it is still valid.
|
||||
/// </summary>
|
||||
Boolean IsValid {
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance is disposed.
|
||||
/// </summary>
|
||||
Boolean IsDisposed {
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enters the state in which waiters need to wait.
|
||||
/// All future waiters will block when they call the Wait method.
|
||||
/// </summary>
|
||||
void Begin();
|
||||
|
||||
/// <summary>
|
||||
/// Leaves the state in which waiters need to wait.
|
||||
/// All current waiters will continue.
|
||||
/// </summary>
|
||||
void Complete();
|
||||
|
||||
/// <summary>
|
||||
/// Waits for the event to be completed.
|
||||
/// </summary>
|
||||
void Wait();
|
||||
|
||||
/// <summary>
|
||||
/// Waits for the event to be completed.
|
||||
/// Returns <c>true</c> when there was no timeout. False if the timeout was reached.
|
||||
/// </summary>
|
||||
/// <param name="timeout">The maximum amount of time to wait for.</param>
|
||||
/// <returns><c>true</c> when there was no timeout. <c>false</c> if the timeout was reached.</returns>
|
||||
Boolean Wait(TimeSpan timeout);
|
||||
}
|
||||
}
|
188
Swan.Tiny/Threading/WaitEventFactory.cs
Normal file
188
Swan.Tiny/Threading/WaitEventFactory.cs
Normal file
@ -0,0 +1,188 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace Swan.Threading {
|
||||
/// <summary>
|
||||
/// Provides a Manual Reset Event factory with a unified API.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// The following example shows how to use the WaitEventFactory class.
|
||||
/// <code>
|
||||
/// using Swan.Threading;
|
||||
///
|
||||
/// public class Example
|
||||
/// {
|
||||
/// // create a WaitEvent using the slim version
|
||||
/// private static readonly IWaitEvent waitEvent = WaitEventFactory.CreateSlim(false);
|
||||
///
|
||||
/// public static void Main()
|
||||
/// {
|
||||
/// Task.Factory.StartNew(() =>
|
||||
/// {
|
||||
/// DoWork(1);
|
||||
/// });
|
||||
///
|
||||
/// Task.Factory.StartNew(() =>
|
||||
/// {
|
||||
/// DoWork(2);
|
||||
/// });
|
||||
///
|
||||
/// // send first signal
|
||||
/// waitEvent.Complete();
|
||||
/// waitEvent.Begin();
|
||||
///
|
||||
/// Thread.Sleep(TimeSpan.FromSeconds(2));
|
||||
///
|
||||
/// // send second signal
|
||||
/// waitEvent.Complete();
|
||||
///
|
||||
/// Terminal.Readline();
|
||||
/// }
|
||||
///
|
||||
/// public static void DoWork(int taskNumber)
|
||||
/// {
|
||||
/// $"Data retrieved:{taskNumber}".WriteLine();
|
||||
/// waitEvent.Wait();
|
||||
///
|
||||
/// Thread.Sleep(TimeSpan.FromSeconds(2));
|
||||
/// $"All finished up {taskNumber}".WriteLine();
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public static class WaitEventFactory {
|
||||
#region Factory Methods
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Wait Event backed by a standard ManualResetEvent.
|
||||
/// </summary>
|
||||
/// <param name="isCompleted">if initially set to completed. Generally true.</param>
|
||||
/// <returns>The Wait Event.</returns>
|
||||
public static IWaitEvent Create(Boolean isCompleted) => new WaitEvent(isCompleted);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Wait Event backed by a ManualResetEventSlim.
|
||||
/// </summary>
|
||||
/// <param name="isCompleted">if initially set to completed. Generally true.</param>
|
||||
/// <returns>The Wait Event.</returns>
|
||||
public static IWaitEvent CreateSlim(Boolean isCompleted) => new WaitEventSlim(isCompleted);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Wait Event backed by a ManualResetEventSlim.
|
||||
/// </summary>
|
||||
/// <param name="isCompleted">if initially set to completed. Generally true.</param>
|
||||
/// <param name="useSlim">if set to <c>true</c> creates a slim version of the wait event.</param>
|
||||
/// <returns>The Wait Event.</returns>
|
||||
public static IWaitEvent Create(Boolean isCompleted, Boolean useSlim) => useSlim ? CreateSlim(isCompleted) : Create(isCompleted);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Backing Classes
|
||||
|
||||
/// <summary>
|
||||
/// Defines a WaitEvent backed by a ManualResetEvent.
|
||||
/// </summary>
|
||||
private class WaitEvent : IWaitEvent {
|
||||
private ManualResetEvent _event;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WaitEvent"/> class.
|
||||
/// </summary>
|
||||
/// <param name="isCompleted">if set to <c>true</c> [is completed].</param>
|
||||
public WaitEvent(Boolean isCompleted) => this._event = new ManualResetEvent(isCompleted);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Boolean IsDisposed {
|
||||
get; private set;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Boolean IsValid => this.IsDisposed || this._event == null ? false : this._event?.SafeWaitHandle?.IsClosed ?? true ? false : !(this._event?.SafeWaitHandle?.IsInvalid ?? true);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Boolean IsCompleted => this.IsValid == false ? true : this._event?.WaitOne(0) ?? true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Boolean IsInProgress => !this.IsCompleted;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Begin() => this._event?.Reset();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Complete() => this._event?.Set();
|
||||
|
||||
/// <inheritdoc />
|
||||
void IDisposable.Dispose() {
|
||||
if(this.IsDisposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.IsDisposed = true;
|
||||
|
||||
_ = this._event?.Set();
|
||||
this._event?.Dispose();
|
||||
this._event = null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Wait() => this._event?.WaitOne();
|
||||
|
||||
/// <inheritdoc />
|
||||
public Boolean Wait(TimeSpan timeout) => this._event?.WaitOne(timeout) ?? true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines a WaitEvent backed by a ManualResetEventSlim.
|
||||
/// </summary>
|
||||
private class WaitEventSlim : IWaitEvent {
|
||||
private ManualResetEventSlim _event;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WaitEventSlim"/> class.
|
||||
/// </summary>
|
||||
/// <param name="isCompleted">if set to <c>true</c> [is completed].</param>
|
||||
public WaitEventSlim(Boolean isCompleted) => this._event = new ManualResetEventSlim(isCompleted);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Boolean IsDisposed {
|
||||
get; private set;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Boolean IsValid => this.IsDisposed || this._event?.WaitHandle?.SafeWaitHandle == null ? false : !this._event.WaitHandle.SafeWaitHandle.IsClosed && !this._event.WaitHandle.SafeWaitHandle.IsInvalid;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Boolean IsCompleted => this.IsValid == false || this._event.IsSet;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Boolean IsInProgress => !this.IsCompleted;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Begin() => this._event?.Reset();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Complete() => this._event?.Set();
|
||||
|
||||
/// <inheritdoc />
|
||||
void IDisposable.Dispose() {
|
||||
if(this.IsDisposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.IsDisposed = true;
|
||||
|
||||
this._event?.Set();
|
||||
this._event?.Dispose();
|
||||
this._event = null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Wait() => this._event?.Wait();
|
||||
|
||||
/// <inheritdoc />
|
||||
public Boolean Wait(TimeSpan timeout) => this._event?.Wait(timeout) ?? true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user