This commit is contained in:
2019-12-04 18:57:18 +01:00
parent 4692422c9a
commit 6263791dff
225 changed files with 33065 additions and 2 deletions
@@ -0,0 +1,743 @@
namespace Swan.DependencyInjection
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
/// <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 bool _disposed;
static DependencyContainer()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="DependencyContainer"/> class.
/// </summary>
public DependencyContainer()
{
RegisteredTypes = new TypesConcurrentDictionary(this);
Register(this);
}
private DependencyContainer(DependencyContainer parent)
: 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 (_disposed) return;
_disposed = true;
foreach (var disposable in 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, bool> registrationPredicate = null)
{
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, bool> registrationPredicate = null)
{
lock (_autoRegisterLock)
{
var types = assemblies
.SelectMany(a => a.GetAllTypes())
.Where(t => !IsIgnoredType(t, registrationPredicate))
.ToList();
var concreteTypes = types
.Where(type =>
type.IsClass && !type.IsAbstract &&
(type != GetType() && (type.DeclaringType != GetType()) && !type.IsGenericTypeDefinition))
.ToList();
foreach (var type in concreteTypes)
{
try
{
RegisteredTypes.Register(type, string.Empty, GetDefaultObjectFactory(type, type));
}
catch (MethodAccessException)
{
// Ignore methods we can't access - added for Silverlight
}
}
var abstractInterfaceTypes = types.Where(
type =>
((type.IsInterface || type.IsAbstract) && (type.DeclaringType != GetType()) &&
(!type.IsGenericTypeDefinition)));
foreach (var type in abstractInterfaceTypes)
{
var localType = type;
var 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)
{
RegisterMultiple(type, implementations);
}
}
var firstImplementation = implementations.FirstOrDefault();
if (firstImplementation == null) continue;
try
{
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 = "")
=> 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 = "") =>
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 = "") =>
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 = "")
=> 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 = "")
=> 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
{
return 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
{
return 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
{
return 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
{
return 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 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) =>
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 (var 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())
{
var queryForDuplicatedTypes = implementationTypes
.GroupBy(i => i)
.Where(j => j.Count() > 1)
.Select(j => j.Key.FullName);
var 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}");
}
var registerOptions = implementationTypes
.Select(type => 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 bool Unregister<TRegister>(string name = "") => 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 bool Unregister(Type registerType, string name = "") =>
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)
=> 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
{
return (TResolveType)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 bool CanResolve(
Type resolveType,
string name = null,
DependencyContainerResolveOptions options = null) =>
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 bool CanResolve<TResolveType>(
string name = null,
DependencyContainerResolveOptions options = null)
where TResolveType : class
{
return 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 bool TryResolve(Type resolveType, out object resolvedType)
{
try
{
resolvedType = 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 bool TryResolve(Type resolveType, DependencyContainerResolveOptions options, out object resolvedType)
{
try
{
resolvedType = 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 bool TryResolve(Type resolveType, string name, out object resolvedType)
{
try
{
resolvedType = 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 bool TryResolve(
Type resolveType,
string name,
DependencyContainerResolveOptions options,
out object resolvedType)
{
try
{
resolvedType = 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 bool TryResolve<TResolveType>(out TResolveType resolvedType)
where TResolveType : class
{
try
{
resolvedType = 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 bool TryResolve<TResolveType>(DependencyContainerResolveOptions options, out TResolveType resolvedType)
where TResolveType : class
{
try
{
resolvedType = 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 bool TryResolve<TResolveType>(string name, out TResolveType resolvedType)
where TResolveType : class
{
try
{
resolvedType = 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 bool TryResolve<TResolveType>(
string name,
DependencyContainerResolveOptions options,
out TResolveType resolvedType)
where TResolveType : class
{
try
{
resolvedType = 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, bool includeUnnamed = false)
=> 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>(bool includeUnnamed = true)
where TResolveType : class
{
return 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;
var properties = input.GetType()
.GetProperties()
.Where(property => property.GetCacheGetMethod() != null && property.GetCacheSetMethod() != null &&
!property.PropertyType.IsValueType);
foreach (var property in properties.Where(property => property.GetValue(input, null) == null))
{
try
{
property.SetValue(
input,
RegisteredTypes.ResolveInternal(new TypeRegistration(property.PropertyType), resolveOptions),
null);
}
catch (DependencyContainerResolutionException)
{
// Catch any resolution errors and ignore them
}
}
}
#endregion
#region Internal Methods
internal static bool 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 bool IsIgnoredAssembly(Assembly assembly)
{
// TODO - find a better way to remove "system" assemblies from the auto registration
var ignoreChecks = new List<Func<Assembly, bool>>
{
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 bool IsIgnoredType(Type type, Func<Type, bool> registrationPredicate)
{
// TODO - find a better way to remove "system" types from the auto registration
var ignoreChecks = new List<Func<Type, bool>>()
{
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,46 @@
namespace Swan.DependencyInjection
{
using System;
using System.Collections.Generic;
using System.Linq;
/// <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, bool isTypeFactory = false)
: base(isTypeFactory
? string.Format(RegisterErrorText, type.FullName, method)
: string.Format(ConvertErrorText, type.FullName, method))
{
}
private static string GetTypesString(IEnumerable<Type> types)
{
return string.Join(",", types.Select(type => type.FullName));
}
}
}
@@ -0,0 +1,31 @@
namespace Swan.DependencyInjection
{
using System;
/// <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,114 @@
namespace Swan.DependencyInjection
{
using System.Collections.Generic;
/// <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<string, object> ConstructorParameters { get; } = new Dictionary<string, 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,22 @@
namespace Swan.DependencyInjection
{
using System;
/// <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))
{
}
}
}
@@ -0,0 +1,423 @@
namespace Swan.DependencyInjection
{
using System;
using System.Collections.Generic;
using System.Reflection;
/// <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 bool 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(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(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(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(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)
{
return 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);
}
_registerType = registerType;
_registerImplementation = registerImplementation;
}
public override Type CreatesType => _registerImplementation;
public override ObjectFactoryBase SingletonVariant =>
new SingletonFactory(_registerType, _registerImplementation);
public override ObjectFactoryBase MultiInstanceVariant => this;
public override object GetObject(
Type requestedType,
DependencyContainer container,
DependencyContainerResolveOptions options)
{
try
{
return container.RegisteredTypes.ConstructType(_registerImplementation, Constructor, options);
}
catch (DependencyContainerResolutionException ex)
{
throw new DependencyContainerResolutionException(_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)
{
_factory = factory ?? throw new ArgumentNullException(nameof(factory));
_registerType = registerType;
}
public override bool AssumeConstruction => true;
public override Type CreatesType => _registerType;
public override ObjectFactoryBase WeakReferenceVariant => new WeakDelegateFactory(_registerType, _factory);
public override ObjectFactoryBase StrongReferenceVariant => this;
public override object GetObject(
Type requestedType,
DependencyContainer container,
DependencyContainerResolveOptions options)
{
try
{
return _factory.Invoke(container, options.ConstructorParameters);
}
catch (Exception ex)
{
throw new DependencyContainerResolutionException(_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));
_factory = new WeakReference(factory);
_registerType = registerType;
}
public override bool AssumeConstruction => true;
public override Type CreatesType => _registerType;
public override ObjectFactoryBase StrongReferenceVariant
{
get
{
if (!(_factory.Target is Func<DependencyContainer, Dictionary<string, object>, object> factory))
throw new DependencyContainerWeakReferenceException(_registerType);
return new DelegateFactory(_registerType, factory);
}
}
public override ObjectFactoryBase WeakReferenceVariant => this;
public override object GetObject(
Type requestedType,
DependencyContainer container,
DependencyContainerResolveOptions options)
{
if (!(_factory.Target is Func<DependencyContainer, Dictionary<string, object>, object> factory))
throw new DependencyContainerWeakReferenceException(_registerType);
try
{
return factory.Invoke(container, options.ConstructorParameters);
}
catch (Exception ex)
{
throw new DependencyContainerResolutionException(_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);
_registerType = registerType;
_registerImplementation = registerImplementation;
_instance = instance;
}
public override bool AssumeConstruction => true;
public override Type CreatesType => _registerImplementation;
public override ObjectFactoryBase MultiInstanceVariant =>
new MultiInstanceFactory(_registerType, _registerImplementation);
public override ObjectFactoryBase WeakReferenceVariant =>
new WeakInstanceFactory(_registerType, _registerImplementation, _instance);
public override ObjectFactoryBase StrongReferenceVariant => this;
public override object GetObject(
Type requestedType,
DependencyContainer container,
DependencyContainerResolveOptions options)
{
return _instance;
}
public void Dispose()
{
var disposable = _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);
}
_registerType = registerType;
_registerImplementation = registerImplementation;
_instance = new WeakReference(instance);
}
public override Type CreatesType => _registerImplementation;
public override ObjectFactoryBase MultiInstanceVariant =>
new MultiInstanceFactory(_registerType, _registerImplementation);
public override ObjectFactoryBase WeakReferenceVariant => this;
public override ObjectFactoryBase StrongReferenceVariant
{
get
{
var instance = _instance.Target;
if (instance == null)
throw new DependencyContainerWeakReferenceException(_registerType);
return new InstanceFactory(_registerType, _registerImplementation, instance);
}
}
public override object GetObject(
Type requestedType,
DependencyContainer container,
DependencyContainerResolveOptions options)
{
var instance = _instance.Target;
if (instance == null)
throw new DependencyContainerWeakReferenceException(_registerType);
return instance;
}
public void Dispose() => (_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);
}
_registerType = registerType;
_registerImplementation = registerImplementation;
}
public override Type CreatesType => _registerImplementation;
public override ObjectFactoryBase SingletonVariant => this;
public override ObjectFactoryBase MultiInstanceVariant =>
new MultiInstanceFactory(_registerType, _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 (_singletonLock)
{
if (_current == null)
_current = container.RegisteredTypes.ConstructType(_registerImplementation, Constructor, options);
}
return _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.
GetObject(type, parent, DependencyContainerResolveOptions.Default);
return this;
}
public void Dispose() => (_current as IDisposable)?.Dispose();
}
}
+131
View File
@@ -0,0 +1,131 @@
namespace Swan.DependencyInjection
{
using System;
using System.Collections.Generic;
using System.Linq;
/// <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)
{
_registeredTypes = registeredTypes;
_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()
{
var currentFactory = _registeredTypes.GetCurrentFactory(_registration);
if (currentFactory == null)
throw new DependencyContainerRegistrationException(_registration.Type, "singleton");
return _registeredTypes.AddUpdateRegistration(_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()
{
var currentFactory = _registeredTypes.GetCurrentFactory(_registration);
if (currentFactory == null)
throw new DependencyContainerRegistrationException(_registration.Type, "multi-instance");
return _registeredTypes.AddUpdateRegistration(_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()
{
var currentFactory = _registeredTypes.GetCurrentFactory(_registration);
if (currentFactory == null)
throw new DependencyContainerRegistrationException(_registration.Type, "weak reference");
return _registeredTypes.AddUpdateRegistration(_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()
{
var currentFactory = _registeredTypes.GetCurrentFactory(_registration);
if (currentFactory == null)
throw new DependencyContainerRegistrationException(_registration.Type, "strong reference");
return _registeredTypes.AddUpdateRegistration(_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)
{
_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()
{
_registerOptions = 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()
{
_registerOptions = ExecuteOnAllRegisterOptions(ro => ro.AsMultiInstance());
return this;
}
private IEnumerable<RegisterOptions> ExecuteOnAllRegisterOptions(
Func<RegisterOptions, RegisterOptions> action)
{
return _registerOptions.Select(action).ToList();
}
}
}
@@ -0,0 +1,67 @@
namespace Swan.DependencyInjection
{
using System;
public partial class DependencyContainer
{
/// <summary>
/// Represents a Type Registration within the IoC Container.
/// </summary>
public sealed class TypeRegistration
{
private readonly int _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)
{
Type = type;
Name = name ?? string.Empty;
_hashCode = string.Concat(Type.FullName, "|", 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 bool Equals(object obj)
{
if (!(obj is TypeRegistration typeRegistration) || typeRegistration.Type != Type)
return false;
return string.Compare(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 int GetHashCode() => _hashCode;
}
}
}
@@ -0,0 +1,351 @@
namespace Swan.DependencyInjection
{
using System;
using System.Linq.Expressions;
using System.Reflection;
using System.Collections.Generic;
using System.Linq;
using System.Collections.Concurrent;
/// <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)
{
_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, bool includeUnnamed)
{
var registrations = Keys.Where(tr => tr.Type == resolveType)
.Concat(GetParentRegistrationsForType(resolveType)).Distinct();
if (!includeUnnamed)
registrations = registrations.Where(tr => !string.IsNullOrEmpty(tr.Name));
return registrations.Select(registration =>
ResolveInternal(registration, DependencyContainerResolveOptions.Default));
}
internal ObjectFactoryBase GetCurrentFactory(DependencyContainer.TypeRegistration registration)
{
TryGetValue(registration, out var current);
return current;
}
internal RegisterOptions Register(Type registerType, string name, ObjectFactoryBase factory)
=> AddUpdateRegistration(new DependencyContainer.TypeRegistration(registerType, name), factory);
internal RegisterOptions AddUpdateRegistration(DependencyContainer.TypeRegistration typeRegistration, ObjectFactoryBase factory)
{
this[typeRegistration] = factory;
return new RegisterOptions(this, typeRegistration);
}
internal bool RemoveRegistration(DependencyContainer.TypeRegistration typeRegistration)
=> TryRemove(typeRegistration, out _);
internal object ResolveInternal(
DependencyContainer.TypeRegistration registration,
DependencyContainerResolveOptions? options = null)
{
if (options == null)
options = DependencyContainerResolveOptions.Default;
// Attempt container resolution
if (TryGetValue(registration, out var factory))
{
try
{
return factory.GetObject(registration.Type, _dependencyContainer, options);
}
catch (DependencyContainerResolutionException)
{
throw;
}
catch (Exception ex)
{
throw new DependencyContainerResolutionException(registration.Type, ex);
}
}
// Attempt to get a factory from parent if we can
var bubbledObjectFactory = GetParentObjectFactory(registration);
if (bubbledObjectFactory != null)
{
try
{
return bubbledObjectFactory.GetObject(registration.Type, _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 (TryGetValue(new DependencyContainer.TypeRegistration(registration.Type, string.Empty), out factory))
{
try
{
return factory.GetObject(registration.Type, _dependencyContainer, options);
}
catch (DependencyContainerResolutionException)
{
throw;
}
catch (Exception ex)
{
throw new DependencyContainerResolutionException(registration.Type, ex);
}
}
}
// Attempt unregistered construction if possible and requested
var isValid = (options.UnregisteredResolutionAction ==
DependencyContainerUnregisteredResolutionAction.AttemptResolve) ||
(registration.Type.IsGenericType && options.UnregisteredResolutionAction ==
DependencyContainerUnregisteredResolutionAction.GenericsOnly);
return isValid && !registration.Type.IsAbstract && !registration.Type.IsInterface
? ConstructType(registration.Type, null, options)
: throw new DependencyContainerResolutionException(registration.Type);
}
internal bool CanResolve(
DependencyContainer.TypeRegistration registration,
DependencyContainerResolveOptions? options = null)
{
if (options == null)
options = DependencyContainerResolveOptions.Default;
var checkType = registration.Type;
var name = registration.Name;
if (TryGetValue(new DependencyContainer.TypeRegistration(checkType, name), out var factory))
{
if (factory.AssumeConstruction)
return true;
if (factory.Constructor == null)
return GetBestConstructor(factory.CreatesType, options) != null;
return 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 _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 (TryGetValue(new DependencyContainer.TypeRegistration(checkType), out factory))
{
if (factory.AssumeConstruction)
return true;
return 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 (GetBestConstructor(checkType, options) != null) ||
(_dependencyContainer.Parent?.RegisteredTypes.CanResolve(registration, options.Clone()) ?? false);
}
// Bubble resolution up the container tree if we have a parent
return _dependencyContainer.Parent != null && _dependencyContainer.Parent.RegisteredTypes.CanResolve(registration, options.Clone());
}
internal object ConstructType(
Type implementationType,
ConstructorInfo constructor,
DependencyContainerResolveOptions? options = null)
{
var 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 = GetBestConstructor(typeToConstruct, options) ??
GetTypeConstructors(typeToConstruct).LastOrDefault();
}
if (constructor == null)
throw new DependencyContainerResolutionException(typeToConstruct);
var ctorParams = constructor.GetParameters();
var args = new object?[ctorParams.Length];
for (var parameterIndex = 0; parameterIndex < ctorParams.Length; parameterIndex++)
{
var currentParam = ctorParams[parameterIndex];
try
{
args[parameterIndex] = options?.ConstructorParameters.GetValueOrDefault(currentParam.Name, 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 var 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.
var constructorParams = constructor.GetParameters();
var lambdaParams = Expression.Parameter(typeof(object[]), "parameters");
var newParams = new Expression[constructorParams.Length];
for (var i = 0; i < constructorParams.Length; i++)
{
var paramsParameter = Expression.ArrayIndex(lambdaParams, Expression.Constant(i));
newParams[i] = Expression.Convert(paramsParameter, constructorParams[i].ParameterType);
}
var newExpression = Expression.New(constructor, newParams);
var 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 bool IsAutomaticLazyFactoryRequest(Type type)
{
if (!type.IsGenericType)
return false;
var 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)
{
if (_dependencyContainer.Parent == null)
return null;
return _dependencyContainer.Parent.RegisteredTypes.TryGetValue(registration, out var factory)
? factory.GetFactoryForChildContainer(registration.Type, _dependencyContainer.Parent, _dependencyContainer)
: _dependencyContainer.Parent.RegisteredTypes.GetParentObjectFactory(registration);
}
private ConstructorInfo? GetBestConstructor(
Type type,
DependencyContainerResolveOptions options)
=> type.IsValueType ? null : GetTypeConstructors(type).FirstOrDefault(ctor => CanConstruct(ctor, options));
private bool CanConstruct(
MethodBase ctor,
DependencyContainerResolveOptions? options)
{
foreach (var parameter in ctor.GetParameters())
{
if (string.IsNullOrEmpty(parameter.Name))
return false;
var isParameterOverload = options.ConstructorParameters.ContainsKey(parameter.Name);
if (parameter.ParameterType.IsPrimitive && !isParameterOverload)
return false;
if (!isParameterOverload &&
!CanResolve(new DependencyContainer.TypeRegistration(parameter.ParameterType), options.Clone()))
return false;
}
return true;
}
private IEnumerable<DependencyContainer.TypeRegistration> GetParentRegistrationsForType(Type resolveType)
=> _dependencyContainer.Parent == null
? Array.Empty<DependencyContainer.TypeRegistration>()
: _dependencyContainer.Parent.RegisteredTypes.Keys.Where(tr => tr.Type == resolveType).Concat(_dependencyContainer.Parent.RegisteredTypes.GetParentRegistrationsForType(resolveType));
}
}
+143
View File
@@ -0,0 +1,143 @@
namespace Swan.Diagnostics
{
using System;
using System.Diagnostics;
using Threading;
/// <summary>
/// A time measurement artifact.
/// </summary>
internal sealed class RealTimeClock : IDisposable
{
private readonly Stopwatch _chrono = new Stopwatch();
private ISyncLocker? _locker = SyncLockerFactory.Create(useSlim: true);
private long _offsetTicks;
private double _speedRatio = 1.0d;
private bool _isDisposed;
/// <summary>
/// Initializes a new instance of the <see cref="RealTimeClock"/> class.
/// The clock starts paused and at the 0 position.
/// </summary>
public RealTimeClock()
{
Reset();
}
/// <summary>
/// Gets or sets the clock position.
/// </summary>
public TimeSpan Position
{
get
{
using (_locker?.AcquireReaderLock())
{
return TimeSpan.FromTicks(
_offsetTicks + Convert.ToInt64(_chrono.Elapsed.Ticks * SpeedRatio));
}
}
}
/// <summary>
/// Gets a value indicating whether the clock is running.
/// </summary>
public bool IsRunning
{
get
{
using (_locker?.AcquireReaderLock())
{
return _chrono.IsRunning;
}
}
}
/// <summary>
/// Gets or sets the speed ratio at which the clock runs.
/// </summary>
public double SpeedRatio
{
get
{
using (_locker?.AcquireReaderLock())
{
return _speedRatio;
}
}
set
{
using (_locker?.AcquireWriterLock())
{
if (value < 0d) value = 0d;
// Capture the initial position se we set it even after the Speed Ratio has changed
// this ensures a smooth position transition
var initialPosition = Position;
_speedRatio = value;
Update(initialPosition);
}
}
}
/// <summary>
/// Sets a new position value atomically.
/// </summary>
/// <param name="value">The new value that the position property will hold.</param>
public void Update(TimeSpan value)
{
using (_locker?.AcquireWriterLock())
{
var resume = _chrono.IsRunning;
_chrono.Reset();
_offsetTicks = value.Ticks;
if (resume) _chrono.Start();
}
}
/// <summary>
/// Starts or resumes the clock.
/// </summary>
public void Play()
{
using (_locker?.AcquireWriterLock())
{
if (_chrono.IsRunning) return;
_chrono.Start();
}
}
/// <summary>
/// Pauses the clock.
/// </summary>
public void Pause()
{
using (_locker?.AcquireWriterLock())
{
_chrono.Stop();
}
}
/// <summary>
/// Sets the clock position to 0 and stops it.
/// The speed ratio is not modified.
/// </summary>
public void Reset()
{
using (_locker?.AcquireWriterLock())
{
_offsetTicks = 0;
_chrono.Reset();
}
}
/// <inheritdoc />
public void Dispose()
{
if (_isDisposed) return;
_isDisposed = true;
_locker?.Dispose();
_locker = null;
}
}
}
+56
View File
@@ -0,0 +1,56 @@
namespace Swan
{
using System;
using System.IO;
using System.Net.Mail;
using System.Reflection;
/// <summary>
/// Extension methods.
/// </summary>
public static class SmtpExtensions
{
private static readonly BindingFlags PrivateInstanceFlags = BindingFlags.Instance | BindingFlags.NonPublic;
/// <summary>
/// The raw contents of this MailMessage as a MemoryStream.
/// </summary>
/// <param name="this">The caller.</param>
/// <returns>A MemoryStream with the raw contents of this MailMessage.</returns>
public static MemoryStream ToMimeMessage(this MailMessage @this)
{
if (@this == null)
throw new ArgumentNullException(nameof(@this));
var result = new MemoryStream();
var mailWriter = MimeMessageConstants.MailWriterConstructor.Invoke(new object[] { result });
MimeMessageConstants.SendMethod.Invoke(
@this,
PrivateInstanceFlags,
null,
MimeMessageConstants.IsRunningInDotNetFourPointFive ? new[] { mailWriter, true, true } : new[] { mailWriter, true },
null);
result = new MemoryStream(result.ToArray());
MimeMessageConstants.CloseMethod.Invoke(
mailWriter,
PrivateInstanceFlags,
null,
Array.Empty<object>(),
null);
result.Position = 0;
return result;
}
internal static class MimeMessageConstants
{
#pragma warning disable DE0005 // API is deprecated
public static readonly Type MailWriter = typeof(SmtpClient).Assembly.GetType("System.Net.Mail.MailWriter");
#pragma warning restore DE0005 // API is deprecated
public static readonly ConstructorInfo MailWriterConstructor = MailWriter.GetConstructor(PrivateInstanceFlags, null, new[] { typeof(Stream) }, null);
public static readonly MethodInfo CloseMethod = MailWriter.GetMethod("Close", PrivateInstanceFlags);
public static readonly MethodInfo SendMethod = typeof(MailMessage).GetMethod("Send", PrivateInstanceFlags);
public static readonly bool IsRunningInDotNetFourPointFive = SendMethod.GetParameters().Length == 3;
}
}
}
+58
View File
@@ -0,0 +1,58 @@
namespace Swan
{
using System;
using System.Linq;
using System.Net;
using System.Net.Sockets;
/// <summary>
/// Provides various extension methods for networking-related tasks.
/// </summary>
public static class NetworkExtensions
{
/// <summary>
/// Determines whether the IP address is private.
/// </summary>
/// <param name="this">The IP address.</param>
/// <returns>
/// True if the IP Address is private; otherwise, false.
/// </returns>
/// <exception cref="ArgumentNullException">address.</exception>
public static bool IsPrivateAddress(this IPAddress @this)
{
if (@this == null)
throw new ArgumentNullException(nameof(@this));
var octets = @this.ToString().Split(new[] { "." }, StringSplitOptions.RemoveEmptyEntries).Select(byte.Parse).ToArray();
var is24Bit = octets[0] == 10;
var is20Bit = octets[0] == 172 && (octets[1] >= 16 && octets[1] <= 31);
var is16Bit = octets[0] == 192 && octets[1] == 168;
return is24Bit || is20Bit || is16Bit;
}
/// <summary>
/// Converts an IPv4 Address to its Unsigned, 32-bit integer representation.
/// </summary>
/// <param name="this">The address.</param>
/// <returns>
/// A 32-bit unsigned integer converted from four bytes at a specified position in a byte array.
/// </returns>
/// <exception cref="ArgumentNullException">address.</exception>
/// <exception cref="ArgumentException">InterNetwork - address.</exception>
public static uint ToUInt32(this IPAddress @this)
{
if (@this == null)
throw new ArgumentNullException(nameof(@this));
if (@this.AddressFamily != AddressFamily.InterNetwork)
throw new ArgumentException($"Address has to be of family '{nameof(AddressFamily.InterNetwork)}'", nameof(@this));
var addressBytes = @this.GetAddressBytes();
if (BitConverter.IsLittleEndian)
Array.Reverse(addressBytes);
return BitConverter.ToUInt32(addressBytes, 0);
}
}
}
+89
View File
@@ -0,0 +1,89 @@
namespace Swan
{
using Logging;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
#if NET461
using System.ServiceProcess;
#else
using Services;
#endif
/// <summary>
/// Extension methods.
/// </summary>
public static class WindowsServicesExtensions
{
/// <summary>
/// Runs a service in console mode.
/// </summary>
/// <param name="this">The service to run.</param>
/// <param name="loggerSource">The logger source.</param>
/// <exception cref="ArgumentNullException">this.</exception>
[Obsolete("This extension method will be removed in version 3.0")]
public static void RunInConsoleMode(this ServiceBase @this, string loggerSource = null)
{
if (@this == null)
throw new ArgumentNullException(nameof(@this));
RunInConsoleMode(new[] { @this }, loggerSource);
}
/// <summary>
/// Runs a set of services in console mode.
/// </summary>
/// <param name="this">The services to run.</param>
/// <param name="loggerSource">The logger source.</param>
/// <exception cref="ArgumentNullException">this.</exception>
/// <exception cref="InvalidOperationException">The ServiceBase class isn't available.</exception>
[Obsolete("This extension method will be removed in version 3.0")]
public static void RunInConsoleMode(this ServiceBase[] @this, string loggerSource = null)
{
if (@this == null)
throw new ArgumentNullException(nameof(@this));
const string onStartMethodName = "OnStart";
const string onStopMethodName = "OnStop";
var onStartMethod = typeof(ServiceBase).GetMethod(onStartMethodName,
BindingFlags.Instance | BindingFlags.NonPublic);
var onStopMethod = typeof(ServiceBase).GetMethod(onStopMethodName,
BindingFlags.Instance | BindingFlags.NonPublic);
if (onStartMethod == null || onStopMethod == null)
throw new InvalidOperationException("The ServiceBase class isn't available.");
var serviceThreads = new List<Thread>();
"Starting services . . .".Info(loggerSource ?? SwanRuntime.EntryAssemblyName.Name);
foreach (var service in @this)
{
var thread = new Thread(() =>
{
onStartMethod.Invoke(service, new object[] { Array.Empty<string>() });
$"Started service '{service.GetType().Name}'".Info(loggerSource ?? service.GetType().Name);
});
serviceThreads.Add(thread);
thread.Start();
}
"Press any key to stop all services.".Info(loggerSource ?? SwanRuntime.EntryAssemblyName.Name);
Terminal.ReadKey(true, true);
"Stopping services . . .".Info(SwanRuntime.EntryAssemblyName.Name);
foreach (var service in @this)
{
onStopMethod.Invoke(service, null);
$"Stopped service '{service.GetType().Name}'".Info(loggerSource ?? service.GetType().Name);
}
foreach (var thread in serviceThreads)
thread.Join();
"Stopped all services.".Info(loggerSource ?? SwanRuntime.EntryAssemblyName.Name);
}
}
}
+13
View File
@@ -0,0 +1,13 @@
namespace Swan.Messaging
{
/// <summary>
/// A Message to be published/delivered by Messenger.
/// </summary>
public interface IMessageHubMessage
{
/// <summary>
/// The sender of the message, or null if not supported by the message implementation.
/// </summary>
object Sender { get; }
}
}
+26
View File
@@ -0,0 +1,26 @@
namespace Swan.Messaging
{
/// <summary>
/// Represents a message subscription.
/// </summary>
public interface IMessageHubSubscription
{
/// <summary>
/// Token returned to the subscribed to reference this subscription.
/// </summary>
MessageHubSubscriptionToken SubscriptionToken { get; }
/// <summary>
/// Whether delivery should be attempted.
/// </summary>
/// <param name="message">Message that may potentially be delivered.</param>
/// <returns><c>true</c> - ok to send, <c>false</c> - should not attempt to send.</returns>
bool ShouldAttemptDelivery(IMessageHubMessage message);
/// <summary>
/// Deliver the message.
/// </summary>
/// <param name="message">Message to deliver.</param>
void Deliver(IMessageHubMessage message);
}
}
+442
View File
@@ -0,0 +1,442 @@
// ===============================================================================
// TinyIoC - TinyMessenger
//
// A simple messenger/event aggregator.
//
// https://github.com/grumpydev/TinyIoC/blob/master/src/TinyIoC/TinyMessenger.cs
// ===============================================================================
// Copyright © Steven Robbins. All rights reserved.
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY
// OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT
// LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
// FITNESS FOR A PARTICULAR PURPOSE.
// ===============================================================================
namespace Swan.Messaging
{
using System.Threading.Tasks;
using System;
using System.Collections.Generic;
using System.Linq;
#region Message Types / Interfaces
/// <summary>
/// Message proxy definition.
///
/// A message proxy can be used to intercept/alter messages and/or
/// marshal delivery actions onto a particular thread.
/// </summary>
public interface IMessageHubProxy
{
/// <summary>
/// Delivers the specified message.
/// </summary>
/// <param name="message">The message.</param>
/// <param name="subscription">The subscription.</param>
void Deliver(IMessageHubMessage message, IMessageHubSubscription subscription);
}
/// <summary>
/// Default "pass through" proxy.
///
/// Does nothing other than deliver the message.
/// </summary>
public sealed class MessageHubDefaultProxy : IMessageHubProxy
{
private MessageHubDefaultProxy()
{
// placeholder
}
/// <summary>
/// Singleton instance of the proxy.
/// </summary>
public static MessageHubDefaultProxy Instance { get; } = new MessageHubDefaultProxy();
/// <summary>
/// Delivers the specified message.
/// </summary>
/// <param name="message">The message.</param>
/// <param name="subscription">The subscription.</param>
public void Deliver(IMessageHubMessage message, IMessageHubSubscription subscription)
=> subscription.Deliver(message);
}
#endregion
#region Hub Interface
/// <summary>
/// Messenger hub responsible for taking subscriptions/publications and delivering of messages.
/// </summary>
public interface IMessageHub
{
/// <summary>
/// Subscribe to a message type with the given destination and delivery action.
/// Messages will be delivered via the specified proxy.
///
/// All messages of this type will be delivered.
/// </summary>
/// <typeparam name="TMessage">Type of message.</typeparam>
/// <param name="deliveryAction">Action to invoke when message is delivered.</param>
/// <param name="useStrongReferences">Use strong references to destination and deliveryAction.</param>
/// <param name="proxy">Proxy to use when delivering the messages.</param>
/// <returns>MessageSubscription used to unsubscribing.</returns>
MessageHubSubscriptionToken Subscribe<TMessage>(
Action<TMessage> deliveryAction,
bool useStrongReferences,
IMessageHubProxy proxy)
where TMessage : class, IMessageHubMessage;
/// <summary>
/// Subscribe to a message type with the given destination and delivery action with the given filter.
/// Messages will be delivered via the specified proxy.
/// All references are held with WeakReferences
/// Only messages that "pass" the filter will be delivered.
/// </summary>
/// <typeparam name="TMessage">Type of message.</typeparam>
/// <param name="deliveryAction">Action to invoke when message is delivered.</param>
/// <param name="messageFilter">The message filter.</param>
/// <param name="useStrongReferences">Use strong references to destination and deliveryAction.</param>
/// <param name="proxy">Proxy to use when delivering the messages.</param>
/// <returns>
/// MessageSubscription used to unsubscribing.
/// </returns>
MessageHubSubscriptionToken Subscribe<TMessage>(
Action<TMessage> deliveryAction,
Func<TMessage, bool> messageFilter,
bool useStrongReferences,
IMessageHubProxy proxy)
where TMessage : class, IMessageHubMessage;
/// <summary>
/// Unsubscribe from a particular message type.
///
/// Does not throw an exception if the subscription is not found.
/// </summary>
/// <typeparam name="TMessage">Type of message.</typeparam>
/// <param name="subscriptionToken">Subscription token received from Subscribe.</param>
void Unsubscribe<TMessage>(MessageHubSubscriptionToken subscriptionToken)
where TMessage : class, IMessageHubMessage;
/// <summary>
/// Publish a message to any subscribers.
/// </summary>
/// <typeparam name="TMessage">Type of message.</typeparam>
/// <param name="message">Message to deliver.</param>
void Publish<TMessage>(TMessage message)
where TMessage : class, IMessageHubMessage;
/// <summary>
/// Publish a message to any subscribers asynchronously.
/// </summary>
/// <typeparam name="TMessage">Type of message.</typeparam>
/// <param name="message">Message to deliver.</param>
/// <returns>A task from Publish action.</returns>
Task PublishAsync<TMessage>(TMessage message)
where TMessage : class, IMessageHubMessage;
}
#endregion
#region Hub Implementation
/// <inheritdoc />
/// <example>
/// The following code describes how to use a MessageHub. Both the
/// subscription and the message sending are done in the same place but this is only for explanatory purposes.
/// <code>
/// class Example
/// {
/// using Swan;
/// using Swan.Components;
///
/// static void Main()
/// {
/// // using DependencyContainer to create an instance of MessageHub
/// var messageHub = DependencyContainer
/// .Current
/// .Resolve&lt;IMessageHub&gt;() as MessageHub;
///
/// // create an instance of the publisher class
/// // which has a string as its content
/// var message = new MessageHubGenericMessage&lt;string&gt;(new object(), "SWAN");
///
/// // subscribe to the publisher's event
/// // and just print out the content which is a string
/// // a token is returned which can be used to unsubscribe later on
/// var token = messageHub
/// .Subscribe&lt;MessageHubGenericMessage&lt;string&gt;&gt;(m =&gt; m.Content.Info());
///
/// // publish the message described above which is
/// // the string 'SWAN'
/// messageHub.Publish(message);
///
/// // unsuscribe, we will no longer receive any messages
/// messageHub.Unsubscribe&lt;MessageHubGenericMessage&lt;string&gt;&gt;(token);
///
/// Terminal.Flush();
/// }
///
/// }
/// </code>
/// </example>
public sealed class MessageHub : IMessageHub
{
#region Private Types and Interfaces
private readonly object _subscriptionsPadlock = new object();
private readonly Dictionary<Type, List<SubscriptionItem>> _subscriptions =
new Dictionary<Type, List<SubscriptionItem>>();
private class WeakMessageSubscription<TMessage> : IMessageHubSubscription
where TMessage : class, IMessageHubMessage
{
private readonly WeakReference _deliveryAction;
private readonly WeakReference _messageFilter;
/// <summary>
/// Initializes a new instance of the <see cref="WeakMessageSubscription{TMessage}" /> class.
/// </summary>
/// <param name="subscriptionToken">The subscription token.</param>
/// <param name="deliveryAction">The delivery action.</param>
/// <param name="messageFilter">The message filter.</param>
/// <exception cref="ArgumentNullException">subscriptionToken
/// or
/// deliveryAction
/// or
/// messageFilter.</exception>
public WeakMessageSubscription(
MessageHubSubscriptionToken subscriptionToken,
Action<TMessage> deliveryAction,
Func<TMessage, bool> messageFilter)
{
SubscriptionToken = subscriptionToken ?? throw new ArgumentNullException(nameof(subscriptionToken));
_deliveryAction = new WeakReference(deliveryAction);
_messageFilter = new WeakReference(messageFilter);
}
public MessageHubSubscriptionToken SubscriptionToken { get; }
public bool ShouldAttemptDelivery(IMessageHubMessage message)
{
return _deliveryAction.IsAlive && _messageFilter.IsAlive &&
((Func<TMessage, bool>) _messageFilter.Target).Invoke((TMessage) message);
}
public void Deliver(IMessageHubMessage message)
{
if (_deliveryAction.IsAlive)
{
((Action<TMessage>) _deliveryAction.Target).Invoke((TMessage) message);
}
}
}
private class StrongMessageSubscription<TMessage> : IMessageHubSubscription
where TMessage : class, IMessageHubMessage
{
private readonly Action<TMessage> _deliveryAction;
private readonly Func<TMessage, bool> _messageFilter;
/// <summary>
/// Initializes a new instance of the <see cref="StrongMessageSubscription{TMessage}" /> class.
/// </summary>
/// <param name="subscriptionToken">The subscription token.</param>
/// <param name="deliveryAction">The delivery action.</param>
/// <param name="messageFilter">The message filter.</param>
/// <exception cref="ArgumentNullException">subscriptionToken
/// or
/// deliveryAction
/// or
/// messageFilter.</exception>
public StrongMessageSubscription(
MessageHubSubscriptionToken subscriptionToken,
Action<TMessage> deliveryAction,
Func<TMessage, bool> messageFilter)
{
SubscriptionToken = subscriptionToken ?? throw new ArgumentNullException(nameof(subscriptionToken));
_deliveryAction = deliveryAction;
_messageFilter = messageFilter;
}
public MessageHubSubscriptionToken SubscriptionToken { get; }
public bool ShouldAttemptDelivery(IMessageHubMessage message) => _messageFilter.Invoke((TMessage) message);
public void Deliver(IMessageHubMessage message) => _deliveryAction.Invoke((TMessage) message);
}
#endregion
#region Subscription dictionary
private class SubscriptionItem
{
public SubscriptionItem(IMessageHubProxy proxy, IMessageHubSubscription subscription)
{
Proxy = proxy;
Subscription = subscription;
}
public IMessageHubProxy Proxy { get; }
public IMessageHubSubscription Subscription { get; }
}
#endregion
#region Public API
/// <summary>
/// Subscribe to a message type with the given destination and delivery action.
/// Messages will be delivered via the specified proxy.
///
/// All messages of this type will be delivered.
/// </summary>
/// <typeparam name="TMessage">Type of message.</typeparam>
/// <param name="deliveryAction">Action to invoke when message is delivered.</param>
/// <param name="useStrongReferences">Use strong references to destination and deliveryAction. </param>
/// <param name="proxy">Proxy to use when delivering the messages.</param>
/// <returns>MessageSubscription used to unsubscribing.</returns>
public MessageHubSubscriptionToken Subscribe<TMessage>(
Action<TMessage> deliveryAction,
bool useStrongReferences = true,
IMessageHubProxy? proxy = null)
where TMessage : class, IMessageHubMessage
{
return Subscribe(deliveryAction, m => true, useStrongReferences, proxy);
}
/// <summary>
/// Subscribe to a message type with the given destination and delivery action with the given filter.
/// Messages will be delivered via the specified proxy.
/// All references are held with WeakReferences
/// Only messages that "pass" the filter will be delivered.
/// </summary>
/// <typeparam name="TMessage">Type of message.</typeparam>
/// <param name="deliveryAction">Action to invoke when message is delivered.</param>
/// <param name="messageFilter">The message filter.</param>
/// <param name="useStrongReferences">Use strong references to destination and deliveryAction.</param>
/// <param name="proxy">Proxy to use when delivering the messages.</param>
/// <returns>
/// MessageSubscription used to unsubscribing.
/// </returns>
public MessageHubSubscriptionToken Subscribe<TMessage>(
Action<TMessage> deliveryAction,
Func<TMessage, bool> messageFilter,
bool useStrongReferences = true,
IMessageHubProxy? proxy = null)
where TMessage : class, IMessageHubMessage
{
if (deliveryAction == null)
throw new ArgumentNullException(nameof(deliveryAction));
if (messageFilter == null)
throw new ArgumentNullException(nameof(messageFilter));
lock (_subscriptionsPadlock)
{
if (!_subscriptions.TryGetValue(typeof(TMessage), out var currentSubscriptions))
{
currentSubscriptions = new List<SubscriptionItem>();
_subscriptions[typeof(TMessage)] = currentSubscriptions;
}
var subscriptionToken = new MessageHubSubscriptionToken(this, typeof(TMessage));
IMessageHubSubscription subscription;
if (useStrongReferences)
{
subscription = new StrongMessageSubscription<TMessage>(
subscriptionToken,
deliveryAction,
messageFilter);
}
else
{
subscription = new WeakMessageSubscription<TMessage>(
subscriptionToken,
deliveryAction,
messageFilter);
}
currentSubscriptions.Add(new SubscriptionItem(proxy ?? MessageHubDefaultProxy.Instance, subscription));
return subscriptionToken;
}
}
/// <inheritdoc />
public void Unsubscribe<TMessage>(MessageHubSubscriptionToken subscriptionToken)
where TMessage : class, IMessageHubMessage
{
if (subscriptionToken == null)
throw new ArgumentNullException(nameof(subscriptionToken));
lock (_subscriptionsPadlock)
{
if (!_subscriptions.TryGetValue(typeof(TMessage), out var currentSubscriptions))
return;
var currentlySubscribed = currentSubscriptions
.Where(sub => ReferenceEquals(sub.Subscription.SubscriptionToken, subscriptionToken))
.ToList();
currentlySubscribed.ForEach(sub => currentSubscriptions.Remove(sub));
}
}
/// <summary>
/// Publish a message to any subscribers.
/// </summary>
/// <typeparam name="TMessage">Type of message.</typeparam>
/// <param name="message">Message to deliver.</param>
public void Publish<TMessage>(TMessage message)
where TMessage : class, IMessageHubMessage
{
if (message == null)
throw new ArgumentNullException(nameof(message));
List<SubscriptionItem> currentlySubscribed;
lock (_subscriptionsPadlock)
{
if (!_subscriptions.TryGetValue(typeof(TMessage), out var currentSubscriptions))
return;
currentlySubscribed = currentSubscriptions
.Where(sub => sub.Subscription.ShouldAttemptDelivery(message))
.ToList();
}
currentlySubscribed.ForEach(sub =>
{
try
{
sub.Proxy.Deliver(message, sub.Subscription);
}
catch
{
// Ignore any errors and carry on
}
});
}
/// <summary>
/// Publish a message to any subscribers asynchronously.
/// </summary>
/// <typeparam name="TMessage">Type of message.</typeparam>
/// <param name="message">Message to deliver.</param>
/// <returns>A task with the publish.</returns>
public Task PublishAsync<TMessage>(TMessage message)
where TMessage : class, IMessageHubMessage
{
return Task.Run(() => Publish(message));
}
#endregion
}
#endregion
}
+57
View File
@@ -0,0 +1,57 @@
namespace Swan.Messaging
{
using System;
/// <summary>
/// Base class for messages that provides weak reference storage of the sender.
/// </summary>
public abstract class MessageHubMessageBase
: IMessageHubMessage
{
/// <summary>
/// Store a WeakReference to the sender just in case anyone is daft enough to
/// keep the message around and prevent the sender from being collected.
/// </summary>
private readonly WeakReference _sender;
/// <summary>
/// Initializes a new instance of the <see cref="MessageHubMessageBase"/> class.
/// </summary>
/// <param name="sender">The sender.</param>
/// <exception cref="System.ArgumentNullException">sender.</exception>
protected MessageHubMessageBase(object sender)
{
if (sender == null)
throw new ArgumentNullException(nameof(sender));
_sender = new WeakReference(sender);
}
/// <inheritdoc />
public object Sender => _sender.Target;
}
/// <summary>
/// Generic message with user specified content.
/// </summary>
/// <typeparam name="TContent">Content type to store.</typeparam>
public class MessageHubGenericMessage<TContent>
: MessageHubMessageBase
{
/// <summary>
/// Initializes a new instance of the <see cref="MessageHubGenericMessage{TContent}"/> class.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="content">The content.</param>
public MessageHubGenericMessage(object sender, TContent content)
: base(sender)
{
Content = content;
}
/// <summary>
/// Contents of the message.
/// </summary>
public TContent Content { get; protected set; }
}
}
@@ -0,0 +1,51 @@
namespace Swan.Messaging
{
using System;
/// <summary>
/// Represents an active subscription to a message.
/// </summary>
public sealed class MessageHubSubscriptionToken
: IDisposable
{
private readonly WeakReference _hub;
private readonly Type _messageType;
/// <summary>
/// Initializes a new instance of the <see cref="MessageHubSubscriptionToken"/> class.
/// </summary>
/// <param name="hub">The hub.</param>
/// <param name="messageType">Type of the message.</param>
/// <exception cref="System.ArgumentNullException">hub.</exception>
/// <exception cref="System.ArgumentOutOfRangeException">messageType.</exception>
public MessageHubSubscriptionToken(IMessageHub hub, Type messageType)
{
if (hub == null)
{
throw new ArgumentNullException(nameof(hub));
}
if (!typeof(IMessageHubMessage).IsAssignableFrom(messageType))
{
throw new ArgumentOutOfRangeException(nameof(messageType));
}
_hub = new WeakReference(hub);
_messageType = messageType;
}
/// <inheritdoc />
public void Dispose()
{
if (_hub.IsAlive && _hub.Target is IMessageHub hub)
{
var unsubscribeMethod = typeof(IMessageHub).GetMethod(nameof(IMessageHub.Unsubscribe),
new[] {typeof(MessageHubSubscriptionToken)});
unsubscribeMethod = unsubscribeMethod.MakeGenericMethod(_messageType);
unsubscribeMethod.Invoke(hub, new object[] {this});
}
GC.SuppressFinalize(this);
}
}
}
+886
View File
@@ -0,0 +1,886 @@
namespace Swan.Net
{
using Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
/// <summary>
/// Represents a network connection either on the server or on the client. It wraps a TcpClient
/// and its corresponding network streams. It is capable of working in 2 modes. Typically on the server side
/// you will need to enable continuous reading and events. On the client side you may want to disable continuous reading
/// and use the Read methods available. In continuous reading mode Read methods are not available and will throw
/// an invalid operation exceptions if they are used.
/// Continuous Reading Mode: Subscribe to data reception events, it runs a background thread, don't use Read methods
/// Manual Reading Mode: Data reception events are NEVER fired. No background threads are used. Use Read methods to receive data.
/// </summary>
/// <seealso cref="System.IDisposable" />
/// <example>
/// The following code explains how to create a TCP server.
/// <code>
/// using System.Text;
/// using Swan.Net;
///
/// class Example
/// {
/// static void Main()
/// {
/// // create a new connection listener on a specific port
/// var connectionListener = new ConnectionListener(1337);
///
/// // handle the OnConnectionAccepting event
/// connectionListener.OnConnectionAccepted += async (s, e) =>
/// {
/// // create a new connection
/// using (var con = new Connection(e.Client))
/// {
/// await con.WriteLineAsync("Hello world!");
/// }
/// };
///
/// connectionListener.Start();
/// Console.ReadLine)=ñ
/// }
/// }
/// </code>
/// The following code describes how to create a TCP client.
/// <code>
/// using System.Net.Sockets;
/// using System.Text;
/// using System.Threading.Tasks;
/// using Swan.Net;
///
/// class Example
/// {
/// static async Task Main()
/// {
/// // create a new TcpClient object
/// var client = new TcpClient();
///
/// // connect to a specific address and port
/// client.Connect("localhost", 1337);
///
/// //create a new connection with specific encoding,
/// //new line sequence and continuous reading disabled
/// using (var cn = new Connection(client, Encoding.UTF8, "\r\n", true, 0))
/// {
/// var response = await cn.ReadTextAsync();
/// }
/// }
/// }
/// </code>
/// </example>
public sealed class Connection : IDisposable
{
// New Line definitions for reading. This applies to both, events and read methods
private readonly string _newLineSequence;
private readonly byte[] _newLineSequenceBytes;
private readonly char[] _newLineSequenceChars;
private readonly string[] _newLineSequenceLineSplitter;
private readonly byte[] _receiveBuffer;
private readonly TimeSpan _continuousReadingInterval = TimeSpan.FromMilliseconds(5);
private readonly Queue<string> _readLineBuffer = new Queue<string>();
private readonly ManualResetEvent _writeDone = new ManualResetEvent(true);
// Disconnect and Dispose
private bool _hasDisposed;
private int _disconnectCalls;
// Continuous Reading
private Thread _continuousReadingThread;
private int _receiveBufferPointer;
// Reading and writing
private Task<int> _readTask;
/// <summary>
/// Initializes a new instance of the <see cref="Connection"/> class.
/// </summary>
/// <param name="client">The client.</param>
/// <param name="textEncoding">The text encoding.</param>
/// <param name="newLineSequence">The new line sequence used for read and write operations.</param>
/// <param name="disableContinuousReading">if set to <c>true</c> [disable continuous reading].</param>
/// <param name="blockSize">Size of the block. -- set to 0 or less to disable.</param>
public Connection(
TcpClient client,
Encoding textEncoding,
string newLineSequence,
bool disableContinuousReading,
int blockSize)
{
// Setup basic properties
Id = Guid.NewGuid();
TextEncoding = textEncoding;
// Setup new line sequence
if (string.IsNullOrEmpty(newLineSequence))
throw new ArgumentException("Argument cannot be null", nameof(newLineSequence));
_newLineSequence = newLineSequence;
_newLineSequenceBytes = TextEncoding.GetBytes(_newLineSequence);
_newLineSequenceChars = _newLineSequence.ToCharArray();
_newLineSequenceLineSplitter = new[] { _newLineSequence };
// Setup Connection timers
ConnectionStartTimeUtc = DateTime.UtcNow;
DataReceivedLastTimeUtc = ConnectionStartTimeUtc;
DataSentLastTimeUtc = ConnectionStartTimeUtc;
// Setup connection properties
RemoteClient = client;
LocalEndPoint = client.Client.LocalEndPoint as IPEndPoint;
NetworkStream = RemoteClient.GetStream();
RemoteEndPoint = RemoteClient.Client.RemoteEndPoint as IPEndPoint;
// Setup buffers
_receiveBuffer = new byte[RemoteClient.ReceiveBufferSize * 2];
ProtocolBlockSize = blockSize;
_receiveBufferPointer = 0;
// Setup continuous reading mode if enabled
if (disableContinuousReading) return;
ThreadPool.GetAvailableThreads(out var availableWorkerThreads, out _);
ThreadPool.GetMaxThreads(out var maxWorkerThreads, out _);
var activeThreadPoolTreads = maxWorkerThreads - availableWorkerThreads;
if (activeThreadPoolTreads < Environment.ProcessorCount / 4)
{
ThreadPool.QueueUserWorkItem(PerformContinuousReading, this);
}
else
{
new Thread(PerformContinuousReading) { IsBackground = true }.Start();
}
}
/// <summary>
/// Initializes a new instance of the <see cref="Connection"/> class in continuous reading mode.
/// It uses UTF8 encoding, CRLF as a new line sequence and disables a protocol block size.
/// </summary>
/// <param name="client">The client.</param>
public Connection(TcpClient client)
: this(client, Encoding.UTF8, "\r\n", false, 0)
{
// placeholder
}
/// <summary>
/// Initializes a new instance of the <see cref="Connection"/> class in continuous reading mode.
/// It uses UTF8 encoding, disables line sequences, and uses a protocol block size instead.
/// </summary>
/// <param name="client">The client.</param>
/// <param name="blockSize">Size of the block.</param>
public Connection(TcpClient client, int blockSize)
: this(client, Encoding.UTF8, new string('\n', blockSize + 1), false, blockSize)
{
// placeholder
}
#region Events
/// <summary>
/// Occurs when the receive buffer has encounters a new line sequence, the buffer is flushed or the buffer is full.
/// </summary>
public event EventHandler<ConnectionDataReceivedEventArgs> DataReceived = (s, e) => { };
/// <summary>
/// Occurs when an error occurs while upgrading, sending, or receiving data in this client
/// </summary>
public event EventHandler<ConnectionFailureEventArgs> ConnectionFailure = (s, e) => { };
/// <summary>
/// Occurs when a client is disconnected
/// </summary>
public event EventHandler ClientDisconnected = (s, e) => { };
#endregion
#region Properties
/// <summary>
/// Gets the unique identifier of this connection.
/// This field is filled out upon instantiation of this class.
/// </summary>
/// <value>
/// The identifier.
/// </value>
public Guid Id { get; }
/// <summary>
/// Gets the active stream. Returns an SSL stream if the connection is secure, otherwise returns
/// the underlying NetworkStream.
/// </summary>
/// <value>
/// The active stream.
/// </value>
public Stream ActiveStream => SecureStream ?? NetworkStream as Stream;
/// <summary>
/// Gets a value indicating whether the current connection stream is an SSL stream.
/// </summary>
/// <value>
/// <c>true</c> if this instance is active stream secure; otherwise, <c>false</c>.
/// </value>
public bool IsActiveStreamSecure => SecureStream != null;
/// <summary>
/// Gets the text encoding for send and receive operations.
/// </summary>
/// <value>
/// The text encoding.
/// </value>
public Encoding TextEncoding { get; }
/// <summary>
/// Gets the remote end point of this TCP connection.
/// </summary>
/// <value>
/// The remote end point.
/// </value>
public IPEndPoint RemoteEndPoint { get; }
/// <summary>
/// Gets the local end point of this TCP connection.
/// </summary>
/// <value>
/// The local end point.
/// </value>
public IPEndPoint LocalEndPoint { get; }
/// <summary>
/// Gets the remote client of this TCP connection.
/// </summary>
/// <value>
/// The remote client.
/// </value>
public TcpClient RemoteClient { get; private set; }
/// <summary>
/// When in continuous reading mode, and if set to greater than 0,
/// a Data reception event will be fired whenever the amount of bytes
/// determined by this property has been received. Useful for fixed-length message protocols.
/// </summary>
/// <value>
/// The size of the protocol block.
/// </value>
public int ProtocolBlockSize { get; }
/// <summary>
/// Gets a value indicating whether this connection is in continuous reading mode.
/// Remark: Whenever a disconnect event occurs, the background thread is terminated
/// and this property will return false whenever the reading thread is not active.
/// Therefore, even if continuous reading was not disabled in the constructor, this property
/// might return false.
/// </summary>
/// <value>
/// <c>true</c> if this instance is continuous reading enabled; otherwise, <c>false</c>.
/// </value>
public bool IsContinuousReadingEnabled => _continuousReadingThread != null;
/// <summary>
/// Gets the start time at which the connection was started in UTC.
/// </summary>
/// <value>
/// The connection start time UTC.
/// </value>
public DateTime ConnectionStartTimeUtc { get; }
/// <summary>
/// Gets the start time at which the connection was started in local time.
/// </summary>
/// <value>
/// The connection start time.
/// </value>
public DateTime ConnectionStartTime => ConnectionStartTimeUtc.ToLocalTime();
/// <summary>
/// Gets the duration of the connection.
/// </summary>
/// <value>
/// The duration of the connection.
/// </value>
public TimeSpan ConnectionDuration => DateTime.UtcNow.Subtract(ConnectionStartTimeUtc);
/// <summary>
/// Gets the last time data was received at in UTC.
/// </summary>
/// <value>
/// The data received last time UTC.
/// </value>
public DateTime DataReceivedLastTimeUtc { get; private set; }
/// <summary>
/// Gets how long has elapsed since data was last received.
/// </summary>
public TimeSpan DataReceivedIdleDuration => DateTime.UtcNow.Subtract(DataReceivedLastTimeUtc);
/// <summary>
/// Gets the last time at which data was sent in UTC.
/// </summary>
/// <value>
/// The data sent last time UTC.
/// </value>
public DateTime DataSentLastTimeUtc { get; private set; }
/// <summary>
/// Gets how long has elapsed since data was last sent.
/// </summary>
/// <value>
/// The duration of the data sent idle.
/// </value>
public TimeSpan DataSentIdleDuration => DateTime.UtcNow.Subtract(DataSentLastTimeUtc);
/// <summary>
/// Gets a value indicating whether this connection is connected.
/// Remarks: This property polls the socket internally and checks if it is available to read data from it.
/// If disconnect has been called, then this property will return false.
/// </summary>
/// <value>
/// <c>true</c> if this instance is connected; otherwise, <c>false</c>.
/// </value>
public bool IsConnected
{
get
{
if (_disconnectCalls > 0)
return false;
try
{
var socket = RemoteClient.Client;
var pollResult = !((socket.Poll(1000, SelectMode.SelectRead)
&& (NetworkStream.DataAvailable == false)) || !socket.Connected);
if (pollResult == false)
Disconnect();
return pollResult;
}
catch
{
Disconnect();
return false;
}
}
}
private NetworkStream NetworkStream { get; set; }
private SslStream SecureStream { get; set; }
#endregion
#region Read Methods
/// <summary>
/// Reads data from the remote client asynchronously and with the given timeout.
/// </summary>
/// <param name="timeout">The timeout.</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="InvalidOperationException">Read methods have been disabled because continuous reading is enabled.</exception>
/// <exception cref="TimeoutException">Reading data from {ActiveStream} timed out in {timeout.TotalMilliseconds} m.</exception>
public async Task<byte[]> ReadDataAsync(TimeSpan timeout, CancellationToken cancellationToken = default)
{
if (IsContinuousReadingEnabled)
{
throw new InvalidOperationException(
"Read methods have been disabled because continuous reading is enabled.");
}
if (RemoteClient == null)
{
throw new InvalidOperationException("An open connection is required");
}
var receiveBuffer = new byte[RemoteClient.ReceiveBufferSize * 2];
var receiveBuilder = new List<byte>(receiveBuffer.Length);
try
{
var startTime = DateTime.UtcNow;
while (receiveBuilder.Count <= 0)
{
if (DateTime.UtcNow.Subtract(startTime) >= timeout)
{
throw new TimeoutException(
$"Reading data from {ActiveStream} timed out in {timeout.TotalMilliseconds} ms");
}
if (_readTask == null)
_readTask = ActiveStream.ReadAsync(receiveBuffer, 0, receiveBuffer.Length, cancellationToken);
if (_readTask.Wait(_continuousReadingInterval))
{
var bytesReceivedCount = _readTask.Result;
if (bytesReceivedCount > 0)
{
DataReceivedLastTimeUtc = DateTime.UtcNow;
var buffer = new byte[bytesReceivedCount];
Array.Copy(receiveBuffer, 0, buffer, 0, bytesReceivedCount);
receiveBuilder.AddRange(buffer);
}
_readTask = null;
}
else
{
await Task.Delay(_continuousReadingInterval, cancellationToken).ConfigureAwait(false);
}
}
}
catch (Exception ex)
{
ex.Error(typeof(Connection).FullName, "Error while reading network stream data asynchronously.");
throw;
}
return receiveBuilder.ToArray();
}
/// <summary>
/// Reads data asynchronously from the remote stream with a 5000 millisecond timeout.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// A byte array containing the results the specified sequence of bytes.
/// </returns>
public Task<byte[]> ReadDataAsync(CancellationToken cancellationToken = default)
=> ReadDataAsync(TimeSpan.FromSeconds(5), cancellationToken);
/// <summary>
/// Asynchronously reads data as text with the given timeout.
/// </summary>
/// <param name="timeout">The timeout.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// A <see cref="System.String" /> that contains the results of decoding the specified sequence of bytes.
/// </returns>
public async Task<string?> ReadTextAsync(TimeSpan timeout, CancellationToken cancellationToken = default)
{
var buffer = await ReadDataAsync(timeout, cancellationToken).ConfigureAwait(false);
return buffer == null ? null : TextEncoding.GetString(buffer);
}
/// <summary>
/// Asynchronously reads data as text with a 5000 millisecond timeout.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// When this method completes successfully, it returns the contents of the file as a text string.
/// </returns>
public Task<string?> ReadTextAsync(CancellationToken cancellationToken = default)
=> ReadTextAsync(TimeSpan.FromSeconds(5), cancellationToken);
/// <summary>
/// Performs the same task as this method's overload but it defaults to a read timeout of 30 seconds.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// A task that represents the asynchronous read operation. The value of the TResult parameter
/// contains the next line from the stream, or is null if all the characters have been read.
/// </returns>
public Task<string?> ReadLineAsync(CancellationToken cancellationToken = default)
=> ReadLineAsync(TimeSpan.FromSeconds(30), cancellationToken);
/// <summary>
/// Reads the next available line of text in queue. Return null when no text is read.
/// This method differs from the rest of the read methods because it keeps an internal
/// queue of lines that are read from the stream and only returns the one line next in the queue.
/// It is only recommended to use this method when you are working with text-based protocols
/// and the rest of the read methods are not called.
/// </summary>
/// <param name="timeout">The timeout.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task with a string line from the queue.</returns>
/// <exception cref="InvalidOperationException">Read methods have been disabled because continuous reading is enabled.</exception>
public async Task<string?> ReadLineAsync(TimeSpan timeout, CancellationToken cancellationToken = default)
{
if (IsContinuousReadingEnabled)
{
throw new InvalidOperationException(
"Read methods have been disabled because continuous reading is enabled.");
}
if (_readLineBuffer.Count > 0)
return _readLineBuffer.Dequeue();
var builder = new StringBuilder();
while (true)
{
var text = await ReadTextAsync(timeout, cancellationToken).ConfigureAwait(false);
if (string.IsNullOrEmpty(text))
break;
builder.Append(text);
if (!text.EndsWith(_newLineSequence)) continue;
var lines = builder.ToString().TrimEnd(_newLineSequenceChars)
.Split(_newLineSequenceLineSplitter, StringSplitOptions.None);
foreach (var item in lines)
_readLineBuffer.Enqueue(item);
break;
}
return _readLineBuffer.Count > 0 ? _readLineBuffer.Dequeue() : null;
}
#endregion
#region Write Methods
/// <summary>
/// Writes data asynchronously.
/// </summary>
/// <param name="buffer">The buffer.</param>
/// <param name="forceFlush">if set to <c>true</c> [force flush].</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
public async Task WriteDataAsync(byte[] buffer, bool forceFlush, CancellationToken cancellationToken = default)
{
try
{
_writeDone.WaitOne();
_writeDone.Reset();
await ActiveStream.WriteAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
if (forceFlush)
await ActiveStream.FlushAsync(cancellationToken).ConfigureAwait(false);
DataSentLastTimeUtc = DateTime.UtcNow;
}
finally
{
_writeDone.Set();
}
}
/// <summary>
/// Writes text asynchronously.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
public Task WriteTextAsync(string text, CancellationToken cancellationToken = default)
=> WriteTextAsync(text, TextEncoding, cancellationToken);
/// <summary>
/// Writes text asynchronously.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="encoding">The encoding.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
public Task WriteTextAsync(string text, Encoding encoding, CancellationToken cancellationToken = default)
=> WriteDataAsync(encoding.GetBytes(text), true, cancellationToken);
/// <summary>
/// Writes a line of text asynchronously.
/// The new line sequence is added automatically at the end of the line.
/// </summary>
/// <param name="line">The line.</param>
/// <param name="encoding">The encoding.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
public Task WriteLineAsync(string line, Encoding encoding, CancellationToken cancellationToken = default)
=> WriteDataAsync(encoding.GetBytes($"{line}{_newLineSequence}"), true, cancellationToken);
/// <summary>
/// Writes a line of text asynchronously.
/// The new line sequence is added automatically at the end of the line.
/// </summary>
/// <param name="line">The line.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
public Task WriteLineAsync(string line, CancellationToken cancellationToken = default)
=> WriteLineAsync(line, TextEncoding, cancellationToken);
#endregion
#region Socket Methods
/// <summary>
/// Upgrades the active stream to an SSL stream if this connection object is hosted in the server.
/// </summary>
/// <param name="serverCertificate">The server certificate.</param>
/// <returns><c>true</c> if the object is hosted in the server; otherwise, <c>false</c>.</returns>
public async Task<bool> UpgradeToSecureAsServerAsync(X509Certificate2 serverCertificate)
{
if (IsActiveStreamSecure)
return true;
_writeDone.WaitOne();
SslStream? secureStream = null;
try
{
secureStream = new SslStream(NetworkStream, true);
await secureStream.AuthenticateAsServerAsync(serverCertificate).ConfigureAwait(false);
SecureStream = secureStream;
return true;
}
catch (Exception ex)
{
ConnectionFailure(this, new ConnectionFailureEventArgs(ex));
secureStream?.Dispose();
return false;
}
}
/// <summary>
/// Upgrades the active stream to an SSL stream if this connection object is hosted in the client.
/// </summary>
/// <param name="hostname">The hostname.</param>
/// <param name="callback">The callback.</param>
/// <returns>A tasks with <c>true</c> if the upgrade to SSL was successful; otherwise, <c>false</c>.</returns>
public async Task<bool> UpgradeToSecureAsClientAsync(
string? hostname = null,
RemoteCertificateValidationCallback? callback = null)
{
if (IsActiveStreamSecure)
return true;
var secureStream = callback == null
? new SslStream(NetworkStream, true)
: new SslStream(NetworkStream, true, callback);
try
{
await secureStream.AuthenticateAsClientAsync(hostname ?? Network.HostName.ToLowerInvariant()).ConfigureAwait(false);
SecureStream = secureStream;
}
catch (Exception ex)
{
secureStream.Dispose();
ConnectionFailure(this, new ConnectionFailureEventArgs(ex));
return false;
}
return true;
}
/// <summary>
/// Disconnects this connection.
/// </summary>
public void Disconnect()
{
if (_disconnectCalls > 0)
return;
_disconnectCalls++;
_writeDone.WaitOne();
try
{
ClientDisconnected(this, EventArgs.Empty);
}
catch
{
// ignore
}
try
{
#if !NET461
RemoteClient.Dispose();
SecureStream?.Dispose();
NetworkStream?.Dispose();
#else
RemoteClient.Close();
SecureStream?.Close();
NetworkStream?.Close();
#endif
}
finally
{
NetworkStream = null;
SecureStream = null;
RemoteClient = null;
_continuousReadingThread = null;
}
}
#endregion
#region Dispose
/// <inheritdoc />
public void Dispose()
{
if (_hasDisposed)
return;
// Release managed resources
Disconnect();
_continuousReadingThread = null;
_writeDone.Dispose();
_hasDisposed = true;
}
#endregion
#region Continuous Read Methods
private void RaiseReceiveBufferEvents(IEnumerable<byte> receivedData)
{
var moreAvailable = RemoteClient.Available > 0;
foreach (var data in receivedData)
{
ProcessReceivedBlock(data, moreAvailable);
}
// Check if we are left with some more stuff to handle
if (_receiveBufferPointer <= 0)
return;
// Extract the segments split by newline terminated bytes
var sequences = _receiveBuffer.Skip(0).Take(_receiveBufferPointer).ToArray()
.Split(0, _newLineSequenceBytes);
// Something really wrong happened
if (sequences.Count == 0)
throw new InvalidOperationException("Split function failed! This is terribly wrong!");
// We only have one sequence and it is not newline-terminated
// we don't have to do anything.
if (sequences.Count == 1 && sequences[0].EndsWith(_newLineSequenceBytes) == false)
return;
// Process the events for each sequence
for (var i = 0; i < sequences.Count; i++)
{
var sequenceBytes = sequences[i];
var isNewLineTerminated = sequences[i].EndsWith(_newLineSequenceBytes);
var isLast = i == sequences.Count - 1;
if (isNewLineTerminated)
{
var eventArgs = new ConnectionDataReceivedEventArgs(
sequenceBytes,
ConnectionDataReceivedTrigger.NewLineSequenceEncountered,
isLast == false);
DataReceived(this, eventArgs);
}
// Depending on the last segment determine what to do with the receive buffer
if (!isLast) continue;
if (isNewLineTerminated)
{
// Simply reset the buffer pointer if the last segment was also terminated
_receiveBufferPointer = 0;
}
else
{
// If we have not received the termination sequence, then just shift the receive buffer to the left
// and adjust the pointer
Array.Copy(sequenceBytes, _receiveBuffer, sequenceBytes.Length);
_receiveBufferPointer = sequenceBytes.Length;
}
}
}
private void ProcessReceivedBlock(byte data, bool moreAvailable)
{
_receiveBuffer[_receiveBufferPointer] = data;
_receiveBufferPointer++;
// Block size reached
if (ProtocolBlockSize > 0 && _receiveBufferPointer >= ProtocolBlockSize)
{
SendBuffer(moreAvailable, ConnectionDataReceivedTrigger.BlockSizeReached);
return;
}
// The receive buffer is full. Time to flush
if (_receiveBufferPointer >= _receiveBuffer.Length)
{
SendBuffer(moreAvailable, ConnectionDataReceivedTrigger.BufferFull);
}
}
private void SendBuffer(bool moreAvailable, ConnectionDataReceivedTrigger trigger)
{
var eventBuffer = new byte[_receiveBuffer.Length];
Array.Copy(_receiveBuffer, eventBuffer, eventBuffer.Length);
DataReceived(this,
new ConnectionDataReceivedEventArgs(
eventBuffer,
trigger,
moreAvailable));
_receiveBufferPointer = 0;
}
private void PerformContinuousReading(object threadContext)
{
_continuousReadingThread = Thread.CurrentThread;
// Check if the RemoteClient is still there
if (RemoteClient == null) return;
var receiveBuffer = new byte[RemoteClient.ReceiveBufferSize * 2];
while (IsConnected && _disconnectCalls <= 0)
{
var doThreadSleep = false;
try
{
if (_readTask == null)
_readTask = ActiveStream.ReadAsync(receiveBuffer, 0, receiveBuffer.Length);
if (_readTask.Wait(_continuousReadingInterval))
{
var bytesReceivedCount = _readTask.Result;
if (bytesReceivedCount > 0)
{
DataReceivedLastTimeUtc = DateTime.UtcNow;
var buffer = new byte[bytesReceivedCount];
Array.Copy(receiveBuffer, 0, buffer, 0, bytesReceivedCount);
RaiseReceiveBufferEvents(buffer);
}
_readTask = null;
}
else
{
doThreadSleep = _disconnectCalls <= 0;
}
}
catch (Exception ex)
{
ex.Log(nameof(PerformContinuousReading), "Continuous Read operation errored");
}
finally
{
if (doThreadSleep)
Thread.Sleep(_continuousReadingInterval);
}
}
}
#endregion
}
}
+28
View File
@@ -0,0 +1,28 @@
namespace Swan
{
/// <summary>
/// Enumerates the possible causes of the DataReceived event occurring.
/// </summary>
public enum ConnectionDataReceivedTrigger
{
/// <summary>
/// The trigger was a forceful flush of the buffer
/// </summary>
Flush,
/// <summary>
/// The new line sequence bytes were received
/// </summary>
NewLineSequenceEncountered,
/// <summary>
/// The buffer was full
/// </summary>
BufferFull,
/// <summary>
/// The block size reached
/// </summary>
BlockSizeReached,
}
}
+253
View File
@@ -0,0 +1,253 @@
namespace Swan.Net
{
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
/// <summary>
/// TCP Listener manager with built-in events and asynchronous functionality.
/// This networking component is typically used when writing server software.
/// </summary>
/// <seealso cref="System.IDisposable" />
public sealed class ConnectionListener : IDisposable
{
private readonly object _stateLock = new object();
private TcpListener _listenerSocket;
private bool _cancellationPending;
private CancellationTokenSource _cancelListening;
private Task? _backgroundWorkerTask;
private bool _hasDisposed;
#region Events
/// <summary>
/// Occurs when a new connection requests a socket from the listener.
/// Set Cancel = true to prevent the TCP client from being accepted.
/// </summary>
public event EventHandler<ConnectionAcceptingEventArgs> OnConnectionAccepting = (s, e) => { };
/// <summary>
/// Occurs when a new connection is accepted.
/// </summary>
public event EventHandler<ConnectionAcceptedEventArgs> OnConnectionAccepted = (s, e) => { };
/// <summary>
/// Occurs when a connection fails to get accepted
/// </summary>
public event EventHandler<ConnectionFailureEventArgs> OnConnectionFailure = (s, e) => { };
/// <summary>
/// Occurs when the listener stops.
/// </summary>
public event EventHandler<ConnectionListenerStoppedEventArgs> OnListenerStopped = (s, e) => { };
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="ConnectionListener"/> class.
/// </summary>
/// <param name="listenEndPoint">The listen end point.</param>
public ConnectionListener(IPEndPoint listenEndPoint)
{
Id = Guid.NewGuid();
LocalEndPoint = listenEndPoint ?? throw new ArgumentNullException(nameof(listenEndPoint));
}
/// <summary>
/// Initializes a new instance of the <see cref="ConnectionListener"/> class.
/// It uses the loopback address for listening.
/// </summary>
/// <param name="listenPort">The listen port.</param>
public ConnectionListener(int listenPort)
: this(new IPEndPoint(IPAddress.Loopback, listenPort))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ConnectionListener"/> class.
/// </summary>
/// <param name="listenAddress">The listen address.</param>
/// <param name="listenPort">The listen port.</param>
public ConnectionListener(IPAddress listenAddress, int listenPort)
: this(new IPEndPoint(listenAddress, listenPort))
{
}
/// <summary>
/// Finalizes an instance of the <see cref="ConnectionListener"/> class.
/// </summary>
~ConnectionListener()
{
Dispose(false);
}
#endregion
#region Public Properties
/// <summary>
/// Gets the local end point on which we are listening.
/// </summary>
/// <value>
/// The local end point.
/// </value>
public IPEndPoint LocalEndPoint { get; }
/// <summary>
/// Gets a value indicating whether this listener is active.
/// </summary>
/// <value>
/// <c>true</c> if this instance is listening; otherwise, <c>false</c>.
/// </value>
public bool IsListening => _backgroundWorkerTask != null;
/// <summary>
/// Gets a unique identifier that gets automatically assigned upon instantiation of this class.
/// </summary>
/// <value>
/// The unique identifier.
/// </value>
public Guid Id { get; }
#endregion
#region Start and Stop
/// <summary>
/// Starts the listener in an asynchronous, non-blocking fashion.
/// Subscribe to the events of this class to gain access to connected client sockets.
/// </summary>
/// <exception cref="System.InvalidOperationException">Cancellation has already been requested. This listener is not reusable.</exception>
public void Start()
{
lock (_stateLock)
{
if (_backgroundWorkerTask != null)
{
return;
}
if (_cancellationPending)
{
throw new InvalidOperationException(
"Cancellation has already been requested. This listener is not reusable.");
}
_backgroundWorkerTask = DoWorkAsync();
}
}
/// <summary>
/// Stops the listener from receiving new connections.
/// This does not prevent the listener from .
/// </summary>
public void Stop()
{
lock (_stateLock)
{
_cancellationPending = true;
_listenerSocket?.Stop();
_cancelListening?.Cancel();
_backgroundWorkerTask?.Wait();
_backgroundWorkerTask = null;
_cancellationPending = false;
}
}
/// <summary>
/// Returns a <see cref="System.String" /> that represents this instance.
/// </summary>
/// <returns>
/// A <see cref="System.String" /> that represents this instance.
/// </returns>
public override string ToString() => LocalEndPoint.ToString();
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
private void Dispose(bool disposing)
{
if (_hasDisposed)
return;
if (disposing)
{
// Release managed resources
Stop();
}
_hasDisposed = true;
}
/// <summary>
/// Continuously checks for client connections until the Close method has been called.
/// </summary>
/// <returns>A task that represents the asynchronous connection operation.</returns>
private async Task DoWorkAsync()
{
_cancellationPending = false;
_listenerSocket = new TcpListener(LocalEndPoint);
_listenerSocket.Start();
_cancelListening = new CancellationTokenSource();
try
{
while (_cancellationPending == false)
{
try
{
var client = await Task.Run(() => _listenerSocket.AcceptTcpClientAsync(), _cancelListening.Token).ConfigureAwait(false);
var acceptingArgs = new ConnectionAcceptingEventArgs(client);
OnConnectionAccepting(this, acceptingArgs);
if (acceptingArgs.Cancel)
{
#if !NET461
client.Dispose();
#else
client.Close();
#endif
continue;
}
OnConnectionAccepted(this, new ConnectionAcceptedEventArgs(client));
}
catch (Exception ex)
{
OnConnectionFailure(this, new ConnectionFailureEventArgs(ex));
}
}
OnListenerStopped(this, new ConnectionListenerStoppedEventArgs(LocalEndPoint));
}
catch (ObjectDisposedException)
{
OnListenerStopped(this, new ConnectionListenerStoppedEventArgs(LocalEndPoint));
}
catch (Exception ex)
{
OnListenerStopped(this,
new ConnectionListenerStoppedEventArgs(LocalEndPoint, _cancellationPending ? null : ex));
}
finally
{
_backgroundWorkerTask = null;
_cancellationPending = false;
}
}
#endregion
}
}
+62
View File
@@ -0,0 +1,62 @@
namespace Swan.Net.Dns
{
using System;
using System.Threading.Tasks;
using System.Collections.Generic;
/// <summary>
/// DnsClient public interfaces.
/// </summary>
internal partial class DnsClient
{
public interface IDnsMessage
{
IList<DnsQuestion> Questions { get; }
int Size { get; }
byte[] ToArray();
}
public interface IDnsMessageEntry
{
DnsDomain Name { get; }
DnsRecordType Type { get; }
DnsRecordClass Class { get; }
int Size { get; }
byte[] ToArray();
}
public interface IDnsResourceRecord : IDnsMessageEntry
{
TimeSpan TimeToLive { get; }
int DataLength { get; }
byte[] Data { get; }
}
public interface IDnsRequest : IDnsMessage
{
int Id { get; set; }
DnsOperationCode OperationCode { get; set; }
bool RecursionDesired { get; set; }
}
public interface IDnsResponse : IDnsMessage
{
int Id { get; set; }
IList<IDnsResourceRecord> AnswerRecords { get; }
IList<IDnsResourceRecord> AuthorityRecords { get; }
IList<IDnsResourceRecord> AdditionalRecords { get; }
bool IsRecursionAvailable { get; set; }
bool IsAuthorativeServer { get; set; }
bool IsTruncated { get; set; }
DnsOperationCode OperationCode { get; set; }
DnsResponseCode ResponseCode { get; set; }
}
public interface IDnsRequestResolver
{
Task<DnsClientResponse> Request(DnsClientRequest request);
}
}
}
+681
View File
@@ -0,0 +1,681 @@
namespace Swan.Net.Dns
{
using 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;
/// <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)
{
Dns = dns;
_request = request == null ? new DnsRequest() : new DnsRequest(request);
_resolver = resolver ?? new DnsUdpRequestResolver();
}
public int Id
{
get => _request.Id;
set => _request.Id = value;
}
public DnsOperationCode OperationCode
{
get => _request.OperationCode;
set => _request.OperationCode = value;
}
public bool RecursionDesired
{
get => _request.RecursionDesired;
set => _request.RecursionDesired = value;
}
public IList<DnsQuestion> Questions => _request.Questions;
public int Size => _request.Size;
public IPEndPoint Dns { get; set; }
public byte[] ToArray() => _request.ToArray();
public override string ToString() => _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
{
var response = await _resolver.Request(this).ConfigureAwait(false);
if (response.Id != 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()
{
Questions = new List<DnsQuestion>();
header = new DnsHeader
{
OperationCode = DnsOperationCode.Query,
Response = false,
Id = Random.Next(ushort.MaxValue),
};
}
public DnsRequest(IDnsRequest request)
{
header = new DnsHeader();
Questions = new List<DnsQuestion>(request.Questions);
header.Response = false;
Id = request.Id;
OperationCode = request.OperationCode;
RecursionDesired = request.RecursionDesired;
}
public IList<DnsQuestion> Questions { get; }
public int Size => header.Size + Questions.Sum(q => q.Size);
public int Id
{
get => header.Id;
set => header.Id = value;
}
public DnsOperationCode OperationCode
{
get => header.OperationCode;
set => header.OperationCode = value;
}
public bool RecursionDesired
{
get => header.RecursionDesired;
set => header.RecursionDesired = value;
}
public byte[] ToArray()
{
UpdateHeader();
using var result = new MemoryStream(Size);
return result
.Append(header.ToArray())
.Append(Questions.Select(q => q.ToArray()))
.ToArray();
}
public override string ToString()
{
UpdateHeader();
return Json.Serialize(this, true);
}
private void UpdateHeader()
{
header.QuestionCount = Questions.Count;
}
}
public class DnsTcpRequestResolver : IDnsRequestResolver
{
public async Task<DnsClientResponse> Request(DnsClientRequest request)
{
var tcp = new TcpClient();
try
{
#if !NET461
await tcp.Client.ConnectAsync(request.Dns).ConfigureAwait(false);
#else
tcp.Client.Connect(request.Dns);
#endif
var stream = tcp.GetStream();
var buffer = request.ToArray();
var length = BitConverter.GetBytes((ushort)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);
var response = DnsResponse.FromArray(buffer);
return new DnsClientResponse(request, response, buffer);
}
finally
{
#if NET461
tcp.Close();
#else
tcp.Dispose();
#endif
}
}
private static async Task Read(Stream stream, byte[] buffer)
{
var length = buffer.Length;
var offset = 0;
int 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)
{
_fallback = fallback;
}
public DnsUdpRequestResolver()
{
_fallback = new DnsNullRequestResolver();
}
public async Task<DnsClientResponse> Request(DnsClientRequest request)
{
var udp = new UdpClient();
var dns = request.Dns;
try
{
udp.Client.SendTimeout = 7000;
udp.Client.ReceiveTimeout = 7000;
#if !NET461
await udp.Client.ConnectAsync(dns).ConfigureAwait(false);
#else
udp.Client.Connect(dns);
#endif
await udp.SendAsync(request.ToArray(), request.Size).ConfigureAwait(false);
var bufferList = new List<byte>();
do
{
var tempBuffer = new byte[1024];
var receiveCount = udp.Client.Receive(tempBuffer);
bufferList.AddRange(tempBuffer.Skip(0).Take(receiveCount));
}
while (udp.Client.Available > 0 || bufferList.Count == 0);
var buffer = bufferList.ToArray();
var response = DnsResponse.FromArray(buffer);
return response.IsTruncated
? await _fallback.Request(request).ConfigureAwait(false)
: new DnsClientResponse(request, response, buffer);
}
finally
{
#if NET461
udp.Close();
#else
udp.Dispose();
#endif
}
}
}
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 int SIZE = 12;
private ushort id;
private byte flag0;
private byte flag1;
// Question count: number of questions in the Question section
private ushort questionCount;
// Answer record count: number of records in the Answer section
private ushort answerCount;
// Authority record count: number of records in the Authority section
private ushort authorityCount;
// Additional record count: number of records in the Additional section
private ushort addtionalCount;
public int Id
{
get => id;
set => id = (ushort)value;
}
public int QuestionCount
{
get => questionCount;
set => questionCount = (ushort)value;
}
public int AnswerRecordCount
{
get => answerCount;
set => answerCount = (ushort)value;
}
public int AuthorityRecordCount
{
get => authorityCount;
set => authorityCount = (ushort)value;
}
public int AdditionalRecordCount
{
get => addtionalCount;
set => addtionalCount = (ushort)value;
}
public bool Response
{
get => Qr == 1;
set => Qr = Convert.ToByte(value);
}
public DnsOperationCode OperationCode
{
get => (DnsOperationCode)Opcode;
set => Opcode = (byte)value;
}
public bool AuthorativeServer
{
get => Aa == 1;
set => Aa = Convert.ToByte(value);
}
public bool Truncated
{
get => Tc == 1;
set => Tc = Convert.ToByte(value);
}
public bool RecursionDesired
{
get => Rd == 1;
set => Rd = Convert.ToByte(value);
}
public bool RecursionAvailable
{
get => Ra == 1;
set => Ra = Convert.ToByte(value);
}
public DnsResponseCode ResponseCode
{
get => (DnsResponseCode)RCode;
set => RCode = (byte)value;
}
public int Size => SIZE;
// Query/Response Flag
private byte Qr
{
get => Flag0.GetBitValueAt(7);
set => Flag0 = Flag0.SetBitValueAt(7, 1, value);
}
// Operation Code
private byte Opcode
{
get => Flag0.GetBitValueAt(3, 4);
set => Flag0 = Flag0.SetBitValueAt(3, 4, value);
}
// Authorative Answer Flag
private byte Aa
{
get => Flag0.GetBitValueAt(2);
set => Flag0 = Flag0.SetBitValueAt(2, 1, value);
}
// Truncation Flag
private byte Tc
{
get => Flag0.GetBitValueAt(1);
set => Flag0 = Flag0.SetBitValueAt(1, 1, value);
}
// Recursion Desired
private byte Rd
{
get => Flag0.GetBitValueAt(0);
set => Flag0 = Flag0.SetBitValueAt(0, 1, value);
}
// Recursion Available
private byte Ra
{
get => Flag1.GetBitValueAt(7);
set => Flag1 = Flag1.SetBitValueAt(7, 1, value);
}
// Zero (Reserved)
private byte Z
{
get => Flag1.GetBitValueAt(4, 3);
set { }
}
// Response Code
private byte RCode
{
get => Flag1.GetBitValueAt(0, 4);
set => Flag1 = Flag1.SetBitValueAt(0, 4, value);
}
private byte Flag0
{
get => flag0;
set => flag0 = value;
}
private byte Flag1
{
get => flag1;
set => flag1 = value;
}
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(Size));
}
public class DnsDomain : IComparable<DnsDomain>
{
private readonly string[] _labels;
public DnsDomain(string domain)
: this(domain.Split('.'))
{
}
public DnsDomain(string[] labels)
{
_labels = labels;
}
public int Size => _labels.Sum(l => l.Length) + _labels.Length + 1;
public static DnsDomain FromArray(byte[] message, int offset)
=> FromArray(message, offset, out offset);
public static DnsDomain FromArray(byte[] message, int offset, out int endOffset)
{
var labels = new List<byte[]>();
var 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;
}
ushort 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");
}
var length = lengthOrPointer;
var 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()
{
var result = new byte[Size];
var offset = 0;
foreach (var l in _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(".", _labels);
public int CompareTo(DnsDomain other)
=> string.Compare(ToString(), other.ToString(), StringComparison.Ordinal);
public override bool Equals(object obj)
=> obj is DnsDomain domain && CompareTo(domain) == 0;
public override int GetHashCode() => ToString().GetHashCode();
private static string FormatReverseIP(IPAddress ip)
{
var address = ip.GetAddressBytes();
if (address.Length == 4)
{
return string.Join(".", address.Reverse().Select(b => b.ToString())) + ".in-addr.arpa";
}
var nibbles = new byte[address.Length * 2];
for (int i = 0, j = 0; i < address.Length; i++, j = 2 * i)
{
var 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
{
private readonly DnsRecordType _type;
private readonly DnsRecordClass _klass;
public static IList<DnsQuestion> GetAllFromArray(byte[] message, int offset, int questionCount) =>
GetAllFromArray(message, offset, questionCount, out offset);
public static IList<DnsQuestion> GetAllFromArray(
byte[] message,
int offset,
int questionCount,
out int endOffset)
{
IList<DnsQuestion> questions = new List<DnsQuestion>(questionCount);
for (var i = 0; i < questionCount; i++)
{
questions.Add(FromArray(message, offset, out offset));
}
endOffset = offset;
return questions;
}
public static DnsQuestion FromArray(byte[] message, int offset, out int endOffset)
{
var domain = DnsDomain.FromArray(message, offset, out offset);
var 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)
{
Name = domain;
_type = type;
_klass = klass;
}
public DnsDomain Name { get; }
public DnsRecordType Type => _type;
public DnsRecordClass Class => _klass;
public int Size => Name.Size + Tail.SIZE;
public byte[] ToArray() =>
new MemoryStream(Size)
.Append(Name.ToArray())
.Append(new Tail { Type = Type, Class = Class }.ToBytes())
.ToArray();
public override string ToString()
=> Json.SerializeOnly(this, true, nameof(Name), nameof(Type), nameof(Class));
[StructEndianness(Endianness.Big)]
[StructLayout(LayoutKind.Sequential, Pack = 2)]
private struct Tail
{
public const int SIZE = 4;
private ushort type;
private ushort klass;
public DnsRecordType Type
{
get => (DnsRecordType)type;
set => type = (ushort)value;
}
public DnsRecordClass Class
{
get => (DnsRecordClass)klass;
set => klass = (ushort)value;
}
}
}
}
}
+419
View File
@@ -0,0 +1,419 @@
namespace Swan.Net.Dns
{
using Formatters;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Runtime.InteropServices;
/// <summary>
/// DnsClient public methods.
/// </summary>
internal partial class DnsClient
{
public abstract class DnsResourceRecordBase : IDnsResourceRecord
{
private readonly IDnsResourceRecord _record;
protected DnsResourceRecordBase(IDnsResourceRecord record)
{
_record = record;
}
public DnsDomain Name => _record.Name;
public DnsRecordType Type => _record.Type;
public DnsRecordClass Class => _record.Class;
public TimeSpan TimeToLive => _record.TimeToLive;
public int DataLength => _record.DataLength;
public byte[] Data => _record.Data;
public int Size => _record.Size;
protected virtual string[] IncludedProperties
=> new[] {nameof(Name), nameof(Type), nameof(Class), nameof(TimeToLive), nameof(DataLength)};
public byte[] ToArray() => _record.ToArray();
public override string ToString()
=> Json.SerializeOnly(this, true, IncludedProperties);
}
public class DnsResourceRecord : IDnsResourceRecord
{
public DnsResourceRecord(
DnsDomain domain,
byte[] data,
DnsRecordType type,
DnsRecordClass klass = DnsRecordClass.IN,
TimeSpan ttl = default)
{
Name = domain;
Type = type;
Class = klass;
TimeToLive = ttl;
Data = data;
}
public DnsDomain Name { get; }
public DnsRecordType Type { get; }
public DnsRecordClass Class { get; }
public TimeSpan TimeToLive { get; }
public int DataLength => Data.Length;
public byte[] Data { get; }
public int Size => Name.Size + Tail.SIZE + Data.Length;
public static DnsResourceRecord FromArray(byte[] message, int offset, out int endOffset)
{
var domain = DnsDomain.FromArray(message, offset, out offset);
var tail = message.ToStruct<Tail>(offset, Tail.SIZE);
var 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(Size)
.Append(Name.ToArray())
.Append(new Tail()
{
Type = Type,
Class = Class,
TimeToLive = TimeToLive,
DataLength = Data.Length,
}.ToBytes())
.Append(Data)
.ToArray();
public override string ToString()
{
return Json.SerializeOnly(
this,
true,
nameof(Name),
nameof(Type),
nameof(Class),
nameof(TimeToLive),
nameof(DataLength));
}
[StructEndianness(Endianness.Big)]
[StructLayout(LayoutKind.Sequential, Pack = 2)]
private struct Tail
{
public const int SIZE = 10;
private ushort type;
private ushort klass;
private uint ttl;
private ushort dataLength;
public DnsRecordType Type
{
get => (DnsRecordType) type;
set => type = (ushort) value;
}
public DnsRecordClass Class
{
get => (DnsRecordClass) klass;
set => klass = (ushort) value;
}
public TimeSpan TimeToLive
{
get => TimeSpan.FromSeconds(ttl);
set => ttl = (uint) value.TotalSeconds;
}
public int DataLength
{
get => dataLength;
set => dataLength = (ushort) value;
}
}
}
public class DnsPointerResourceRecord : DnsResourceRecordBase
{
public DnsPointerResourceRecord(IDnsResourceRecord record, byte[] message, int dataOffset)
: base(record)
{
PointerDomainName = DnsDomain.FromArray(message, dataOffset);
}
public DnsDomain PointerDomainName { get; }
protected override string[] IncludedProperties
{
get
{
var temp = new List<string>(base.IncludedProperties) {nameof(PointerDomainName)};
return temp.ToArray();
}
}
}
public class DnsIPAddressResourceRecord : DnsResourceRecordBase
{
public DnsIPAddressResourceRecord(IDnsResourceRecord record)
: base(record)
{
IPAddress = new IPAddress(Data);
}
public IPAddress IPAddress { get; }
protected override string[] IncludedProperties
=> new List<string>(base.IncludedProperties) {nameof(IPAddress)}.ToArray();
}
public class DnsNameServerResourceRecord : DnsResourceRecordBase
{
public DnsNameServerResourceRecord(IDnsResourceRecord record, byte[] message, int dataOffset)
: base(record)
{
NSDomainName = DnsDomain.FromArray(message, dataOffset);
}
public DnsDomain NSDomainName { get; }
protected override string[] IncludedProperties
=> new List<string>(base.IncludedProperties) {nameof(NSDomainName)}.ToArray();
}
public class DnsCanonicalNameResourceRecord : DnsResourceRecordBase
{
public DnsCanonicalNameResourceRecord(IDnsResourceRecord record, byte[] message, int dataOffset)
: base(record)
{
CanonicalDomainName = DnsDomain.FromArray(message, dataOffset);
}
public DnsDomain CanonicalDomainName { get; }
protected override string[] IncludedProperties
=> new List<string>(base.IncludedProperties) {nameof(CanonicalDomainName)}.ToArray();
}
public class DnsMailExchangeResourceRecord : DnsResourceRecordBase
{
private const int PreferenceSize = 2;
public DnsMailExchangeResourceRecord(
IDnsResourceRecord record,
byte[] message,
int dataOffset)
: base(record)
{
var preference = new byte[PreferenceSize];
Array.Copy(message, dataOffset, preference, 0, preference.Length);
if (BitConverter.IsLittleEndian)
{
Array.Reverse(preference);
}
dataOffset += PreferenceSize;
Preference = BitConverter.ToUInt16(preference, 0);
ExchangeDomainName = DnsDomain.FromArray(message, dataOffset);
}
public int Preference { get; }
public DnsDomain ExchangeDomainName { get; }
protected override string[] IncludedProperties => new List<string>(base.IncludedProperties)
{
nameof(Preference),
nameof(ExchangeDomainName),
}.ToArray();
}
public class DnsStartOfAuthorityResourceRecord : DnsResourceRecordBase
{
public DnsStartOfAuthorityResourceRecord(IDnsResourceRecord record, byte[] message, int dataOffset)
: base(record)
{
MasterDomainName = DnsDomain.FromArray(message, dataOffset, out dataOffset);
ResponsibleDomainName = DnsDomain.FromArray(message, dataOffset, out dataOffset);
var tail = message.ToStruct<Options>(dataOffset, Options.SIZE);
SerialNumber = tail.SerialNumber;
RefreshInterval = tail.RefreshInterval;
RetryInterval = tail.RetryInterval;
ExpireInterval = tail.ExpireInterval;
MinimumTimeToLive = tail.MinimumTimeToLive;
}
public DnsStartOfAuthorityResourceRecord(
DnsDomain domain,
DnsDomain master,
DnsDomain responsible,
long serial,
TimeSpan refresh,
TimeSpan retry,
TimeSpan expire,
TimeSpan minTtl,
TimeSpan ttl = default)
: base(Create(domain, master, responsible, serial, refresh, retry, expire, minTtl, ttl))
{
MasterDomainName = master;
ResponsibleDomainName = responsible;
SerialNumber = serial;
RefreshInterval = refresh;
RetryInterval = retry;
ExpireInterval = expire;
MinimumTimeToLive = minTtl;
}
public DnsDomain MasterDomainName { get; }
public DnsDomain ResponsibleDomainName { get; }
public long 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(MasterDomainName),
nameof(ResponsibleDomainName),
nameof(SerialNumber),
}.ToArray();
private static IDnsResourceRecord Create(
DnsDomain domain,
DnsDomain master,
DnsDomain responsible,
long serial,
TimeSpan refresh,
TimeSpan retry,
TimeSpan expire,
TimeSpan minTtl,
TimeSpan ttl)
{
var data = new MemoryStream(Options.SIZE + master.Size + responsible.Size);
var 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 int SIZE = 20;
private uint serialNumber;
private uint refreshInterval;
private uint retryInterval;
private uint expireInterval;
private uint ttl;
public long SerialNumber
{
get => serialNumber;
set => serialNumber = (uint) value;
}
public TimeSpan RefreshInterval
{
get => TimeSpan.FromSeconds(refreshInterval);
set => refreshInterval = (uint) value.TotalSeconds;
}
public TimeSpan RetryInterval
{
get => TimeSpan.FromSeconds(retryInterval);
set => retryInterval = (uint) value.TotalSeconds;
}
public TimeSpan ExpireInterval
{
get => TimeSpan.FromSeconds(expireInterval);
set => expireInterval = (uint) value.TotalSeconds;
}
public TimeSpan MinimumTimeToLive
{
get => TimeSpan.FromSeconds(ttl);
set => ttl = (uint) value.TotalSeconds;
}
}
}
private static class DnsResourceRecordFactory
{
public static IList<IDnsResourceRecord> GetAllFromArray(
byte[] message,
int offset,
int count,
out int endOffset)
{
var result = new List<IDnsResourceRecord>(count);
for (var i = 0; i < count; i++)
{
result.Add(GetFromArray(message, offset, out offset));
}
endOffset = offset;
return result;
}
private static IDnsResourceRecord GetFromArray(byte[] message, int offset, out int endOffset)
{
var record = DnsResourceRecord.FromArray(message, offset, out endOffset);
var dataOffset = endOffset - record.DataLength;
return record.Type switch
{
DnsRecordType.A => (IDnsResourceRecord) 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
};
}
}
}
}
+215
View File
@@ -0,0 +1,215 @@
namespace Swan.Net.Dns
{
using Formatters;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
/// <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)
{
Request = request;
_message = message;
_response = response;
}
public DnsClientRequest Request { get; }
public int Id
{
get { return _response.Id; }
set { }
}
public IList<IDnsResourceRecord> AnswerRecords => _response.AnswerRecords;
public IList<IDnsResourceRecord> AuthorityRecords =>
new ReadOnlyCollection<IDnsResourceRecord>(_response.AuthorityRecords);
public IList<IDnsResourceRecord> AdditionalRecords =>
new ReadOnlyCollection<IDnsResourceRecord>(_response.AdditionalRecords);
public bool IsRecursionAvailable
{
get { return _response.IsRecursionAvailable; }
set { }
}
public bool IsAuthorativeServer
{
get { return _response.IsAuthorativeServer; }
set { }
}
public bool IsTruncated
{
get { return _response.IsTruncated; }
set { }
}
public DnsOperationCode OperationCode
{
get { return _response.OperationCode; }
set { }
}
public DnsResponseCode ResponseCode
{
get { return _response.ResponseCode; }
set { }
}
public IList<DnsQuestion> Questions => new ReadOnlyCollection<DnsQuestion>(_response.Questions);
public int Size => _message.Length;
public byte[] ToArray() => _message;
public override string ToString() => _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)
{
_header = header;
Questions = questions;
AnswerRecords = answers;
AuthorityRecords = authority;
AdditionalRecords = additional;
}
public IList<DnsQuestion> Questions { get; }
public IList<IDnsResourceRecord> AnswerRecords { get; }
public IList<IDnsResourceRecord> AuthorityRecords { get; }
public IList<IDnsResourceRecord> AdditionalRecords { get; }
public int Id
{
get => _header.Id;
set => _header.Id = value;
}
public bool IsRecursionAvailable
{
get => _header.RecursionAvailable;
set => _header.RecursionAvailable = value;
}
public bool IsAuthorativeServer
{
get => _header.AuthorativeServer;
set => _header.AuthorativeServer = value;
}
public bool IsTruncated
{
get => _header.Truncated;
set => _header.Truncated = value;
}
public DnsOperationCode OperationCode
{
get => _header.OperationCode;
set => _header.OperationCode = value;
}
public DnsResponseCode ResponseCode
{
get => _header.ResponseCode;
set => _header.ResponseCode = value;
}
public int Size
=> _header.Size +
Questions.Sum(q => q.Size) +
AnswerRecords.Sum(a => a.Size) +
AuthorityRecords.Sum(a => a.Size) +
AdditionalRecords.Sum(a => a.Size);
public static DnsResponse FromArray(byte[] message)
{
var header = DnsHeader.FromArray(message);
var offset = header.Size;
if (!header.Response || header.QuestionCount == 0)
{
throw new ArgumentException("Invalid response message");
}
if (header.Truncated)
{
return new DnsResponse(header,
DnsQuestion.GetAllFromArray(message, offset, header.QuestionCount),
new List<IDnsResourceRecord>(),
new List<IDnsResourceRecord>(),
new List<IDnsResourceRecord>());
}
return 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 offset));
}
public byte[] ToArray()
{
UpdateHeader();
var result = new MemoryStream(Size);
result
.Append(_header.ToArray())
.Append(Questions.Select(q => q.ToArray()))
.Append(AnswerRecords.Select(a => a.ToArray()))
.Append(AuthorityRecords.Select(a => a.ToArray()))
.Append(AdditionalRecords.Select(a => a.ToArray()));
return result.ToArray();
}
public override string ToString()
{
UpdateHeader();
return Json.SerializeOnly(
this,
true,
nameof(Questions),
nameof(AnswerRecords),
nameof(AuthorityRecords),
nameof(AdditionalRecords));
}
private void UpdateHeader()
{
_header.QuestionCount = Questions.Count;
_header.AnswerRecordCount = AnswerRecords.Count;
_header.AuthorityRecordCount = AuthorityRecords.Count;
_header.AdditionalRecordCount = AdditionalRecords.Count;
}
}
}
}
+79
View File
@@ -0,0 +1,79 @@
namespace Swan.Net.Dns
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
/// <summary>
/// DnsClient public methods.
/// </summary>
internal partial class DnsClient
{
private readonly IPEndPoint _dns;
private readonly IDnsRequestResolver _resolver;
public DnsClient(IPEndPoint dns, IDnsRequestResolver? resolver = null)
{
_dns = dns;
_resolver = resolver ?? new DnsUdpRequestResolver(new DnsTcpRequestResolver());
}
public DnsClient(IPAddress ip, int port = Network.DnsDefaultPort, IDnsRequestResolver? resolver = null)
: this(new IPEndPoint(ip, port), resolver)
{
}
public DnsClientRequest Create(IDnsRequest? request = null)
=> new DnsClientRequest(_dns, request, _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);
}
var response = await Resolve(domain, type).ConfigureAwait(false);
var 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));
var response = await Resolve(DnsDomain.PointerName(ip), DnsRecordType.PTR);
var 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) =>
Resolve(new DnsDomain(domain), type);
public Task<DnsClientResponse> Resolve(DnsDomain domain, DnsRecordType type)
{
var request = Create();
var question = new DnsQuestion(domain, type);
request.Questions.Add(question);
request.OperationCode = DnsOperationCode.Query;
request.RecursionDesired = true;
return request.Resolve();
}
}
}
+37
View File
@@ -0,0 +1,37 @@
namespace Swan.Net.Dns
{
using System;
/// <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)
{
Response = response;
}
internal DnsClient.IDnsResponse? Response { get; }
private static string Format(DnsClient.IDnsResponse response) => $"Invalid response received with code {response.ResponseCode}";
}
}
+123
View File
@@ -0,0 +1,123 @@
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()
{
Id = response.Id;
IsAuthoritativeServer = response.IsAuthorativeServer;
IsRecursionAvailable = response.IsRecursionAvailable;
IsTruncated = response.IsTruncated;
OperationCode = response.OperationCode;
ResponseCode = response.ResponseCode;
if (response.AnswerRecords != null)
{
foreach (var record in response.AnswerRecords)
AnswerRecords.Add(new DnsRecord(record));
}
if (response.AuthorityRecords != null)
{
foreach (var record in response.AuthorityRecords)
AuthorityRecords.Add(new DnsRecord(record));
}
if (response.AdditionalRecords != null)
{
foreach (var record in response.AdditionalRecords)
AdditionalRecords.Add(new DnsRecord(record));
}
}
private DnsQueryResult()
{
}
/// <summary>
/// Gets the identifier.
/// </summary>
/// <value>
/// The identifier.
/// </value>
public int 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 bool 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 bool 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 bool 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 => _mAnswerRecords;
/// <summary>
/// Gets the additional records.
/// </summary>
/// <value>
/// The additional records.
/// </value>
public IList<DnsRecord> AdditionalRecords => _mAdditionalRecords;
/// <summary>
/// Gets the authority records.
/// </summary>
/// <value>
/// The authority records.
/// </value>
public IList<DnsRecord> AuthorityRecords => _mAuthorityRecords;
}
}
+208
View File
@@ -0,0 +1,208 @@
namespace Swan.Net.Dns
{
using System;
using System.Net;
using System.Text;
/// <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()
{
Name = record.Name.ToString();
Type = record.Type;
Class = record.Class;
TimeToLive = record.TimeToLive;
Data = record.Data;
// PTR
PointerDomainName = (record as DnsClient.DnsPointerResourceRecord)?.PointerDomainName?.ToString();
// A
IPAddress = (record as DnsClient.DnsIPAddressResourceRecord)?.IPAddress;
// NS
NameServerDomainName = (record as DnsClient.DnsNameServerResourceRecord)?.NSDomainName?.ToString();
// CNAME
CanonicalDomainName = (record as DnsClient.DnsCanonicalNameResourceRecord)?.CanonicalDomainName.ToString();
// MX
MailExchangerDomainName = (record as DnsClient.DnsMailExchangeResourceRecord)?.ExchangeDomainName.ToString();
MailExchangerPreference = (record as DnsClient.DnsMailExchangeResourceRecord)?.Preference;
// SOA
SoaMasterDomainName = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.MasterDomainName.ToString();
SoaResponsibleDomainName = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.ResponsibleDomainName.ToString();
SoaSerialNumber = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.SerialNumber;
SoaRefreshInterval = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.RefreshInterval;
SoaRetryInterval = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.RetryInterval;
SoaExpireInterval = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.ExpireInterval;
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 => Data == null ? string.Empty : Encoding.ASCII.GetString(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 int? 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 long? 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; }
}
}
+172
View File
@@ -0,0 +1,172 @@
// 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,
}
}
+158
View File
@@ -0,0 +1,158 @@
namespace Swan.Net
{
using System;
using System.Net;
using System.Net.Sockets;
/// <summary>
/// The event arguments for when connections are accepted.
/// </summary>
/// <seealso cref="System.EventArgs" />
public class ConnectionAcceptedEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="ConnectionAcceptedEventArgs" /> class.
/// </summary>
/// <param name="client">The client.</param>
/// <exception cref="ArgumentNullException">client.</exception>
public ConnectionAcceptedEventArgs(TcpClient client)
{
Client = client ?? throw new ArgumentNullException(nameof(client));
}
/// <summary>
/// Gets the client.
/// </summary>
/// <value>
/// The client.
/// </value>
public TcpClient Client { get; }
}
/// <summary>
/// Occurs before a connection is accepted. Set the Cancel property to true to prevent the connection from being accepted.
/// </summary>
/// <seealso cref="ConnectionAcceptedEventArgs" />
public class ConnectionAcceptingEventArgs : ConnectionAcceptedEventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="ConnectionAcceptingEventArgs"/> class.
/// </summary>
/// <param name="client">The client.</param>
public ConnectionAcceptingEventArgs(TcpClient client)
: base(client)
{
}
/// <summary>
/// Setting Cancel to true rejects the new TcpClient.
/// </summary>
/// <value>
/// <c>true</c> if cancel; otherwise, <c>false</c>.
/// </value>
public bool Cancel { get; set; }
}
/// <summary>
/// Event arguments for when a server listener is started.
/// </summary>
/// <seealso cref="System.EventArgs" />
public class ConnectionListenerStartedEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="ConnectionListenerStartedEventArgs" /> class.
/// </summary>
/// <param name="listenerEndPoint">The listener end point.</param>
/// <exception cref="ArgumentNullException">listenerEndPoint.</exception>
public ConnectionListenerStartedEventArgs(IPEndPoint listenerEndPoint)
{
EndPoint = listenerEndPoint ?? throw new ArgumentNullException(nameof(listenerEndPoint));
}
/// <summary>
/// Gets the end point.
/// </summary>
/// <value>
/// The end point.
/// </value>
public IPEndPoint EndPoint { get; }
}
/// <summary>
/// Event arguments for when a server listener fails to start.
/// </summary>
/// <seealso cref="System.EventArgs" />
public class ConnectionListenerFailedEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="ConnectionListenerFailedEventArgs" /> class.
/// </summary>
/// <param name="listenerEndPoint">The listener end point.</param>
/// <param name="ex">The ex.</param>
/// <exception cref="ArgumentNullException">
/// listenerEndPoint
/// or
/// ex.
/// </exception>
public ConnectionListenerFailedEventArgs(IPEndPoint listenerEndPoint, Exception ex)
{
EndPoint = listenerEndPoint ?? throw new ArgumentNullException(nameof(listenerEndPoint));
Error = ex ?? throw new ArgumentNullException(nameof(ex));
}
/// <summary>
/// Gets the end point.
/// </summary>
/// <value>
/// The end point.
/// </value>
public IPEndPoint EndPoint { get; }
/// <summary>
/// Gets the error.
/// </summary>
/// <value>
/// The error.
/// </value>
public Exception Error { get; }
}
/// <summary>
/// Event arguments for when a server listener stopped.
/// </summary>
/// <seealso cref="System.EventArgs" />
public class ConnectionListenerStoppedEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="ConnectionListenerStoppedEventArgs" /> class.
/// </summary>
/// <param name="listenerEndPoint">The listener end point.</param>
/// <param name="ex">The ex.</param>
/// <exception cref="ArgumentNullException">
/// listenerEndPoint
/// or
/// ex.
/// </exception>
public ConnectionListenerStoppedEventArgs(IPEndPoint listenerEndPoint, Exception? ex = null)
{
EndPoint = listenerEndPoint ?? throw new ArgumentNullException(nameof(listenerEndPoint));
Error = ex;
}
/// <summary>
/// Gets the end point.
/// </summary>
/// <value>
/// The end point.
/// </value>
public IPEndPoint EndPoint { get; }
/// <summary>
/// Gets the error.
/// </summary>
/// <value>
/// The error.
/// </value>
public Exception? Error { get; }
}
}
+84
View File
@@ -0,0 +1,84 @@
namespace Swan.Net
{
using System;
using System.Text;
/// <summary>
/// The event arguments for connection failure events.
/// </summary>
/// <seealso cref="System.EventArgs" />
public class ConnectionFailureEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="ConnectionFailureEventArgs"/> class.
/// </summary>
/// <param name="ex">The ex.</param>
public ConnectionFailureEventArgs(Exception ex)
{
Error = ex;
}
/// <summary>
/// Gets the error.
/// </summary>
/// <value>
/// The error.
/// </value>
public Exception Error { get; }
}
/// <summary>
/// Event arguments for when data is received.
/// </summary>
/// <seealso cref="System.EventArgs" />
public class ConnectionDataReceivedEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="ConnectionDataReceivedEventArgs"/> class.
/// </summary>
/// <param name="buffer">The buffer.</param>
/// <param name="trigger">The trigger.</param>
/// <param name="moreAvailable">if set to <c>true</c> [more available].</param>
public ConnectionDataReceivedEventArgs(byte[] buffer, ConnectionDataReceivedTrigger trigger, bool moreAvailable)
{
Buffer = buffer ?? throw new ArgumentNullException(nameof(buffer));
Trigger = trigger;
HasMoreAvailable = moreAvailable;
}
/// <summary>
/// Gets the buffer.
/// </summary>
/// <value>
/// The buffer.
/// </value>
public byte[] Buffer { get; }
/// <summary>
/// Gets the cause as to why this event was thrown.
/// </summary>
/// <value>
/// The trigger.
/// </value>
public ConnectionDataReceivedTrigger Trigger { get; }
/// <summary>
/// Gets a value indicating whether the receive buffer has more bytes available.
/// </summary>
/// <value>
/// <c>true</c> if this instance has more available; otherwise, <c>false</c>.
/// </value>
public bool HasMoreAvailable { get; }
/// <summary>
/// Gets the string from buffer.
/// </summary>
/// <param name="encoding">The encoding.</param>
/// <returns>
/// A <see cref="System.String" /> that contains the results of decoding the specified sequence of bytes.
/// </returns>
/// <exception cref="ArgumentNullException">encoding</exception>
public string GetStringFromBuffer(Encoding encoding)
=> encoding?.GetString(Buffer).TrimEnd('\r', '\n') ?? throw new ArgumentNullException(nameof(encoding));
}
}
+418
View File
@@ -0,0 +1,418 @@
namespace Swan.Net
{
using Formatters;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
/// <summary>
/// Represents a HttpClient with extended methods to use with JSON payloads
/// and bearer tokens authentication.
/// </summary>
public static class JsonClient
{
private const string JsonMimeType = "application/json";
private const string FormType = "application/x-www-form-urlencoded";
private static readonly HttpClient HttpClient = new HttpClient();
/// <summary>
/// Post a object as JSON with optional authorization token.
/// </summary>
/// <typeparam name="T">The type of response object.</typeparam>
/// <param name="requestUri">The request URI.</param>
/// <param name="payload">The payload.</param>
/// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// A task with a result of the requested type.
/// </returns>
public static async Task<T> Post<T>(
Uri requestUri,
object payload,
string? authorization = null,
CancellationToken cancellationToken = default)
{
var jsonString = await PostString(requestUri, payload, authorization, cancellationToken)
.ConfigureAwait(false);
return !string.IsNullOrEmpty(jsonString) ? Json.Deserialize<T>(jsonString) : default;
}
/// <summary>
/// Posts the specified URL.
/// </summary>
/// <param name="requestUri">The request URI.</param>
/// <param name="payload">The payload.</param>
/// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// A task with a result as a collection of key/value pairs.
/// </returns>
public static async Task<IDictionary<string, object>?> Post(
Uri requestUri,
object payload,
string? authorization = null,
CancellationToken cancellationToken = default)
{
var jsonString = await PostString(requestUri, payload, authorization, cancellationToken)
.ConfigureAwait(false);
return string.IsNullOrWhiteSpace(jsonString)
? default
: Json.Deserialize(jsonString) as IDictionary<string, object>;
}
/// <summary>
/// Posts the specified URL.
/// </summary>
/// <param name="requestUri">The request URI.</param>
/// <param name="payload">The payload.</param>
/// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// A task with a result of the requested string.
/// </returns>
/// <exception cref="ArgumentNullException">url.</exception>
/// <exception cref="JsonRequestException">Error POST JSON.</exception>
public static Task<string> PostString(
Uri requestUri,
object payload,
string? authorization = null,
CancellationToken cancellationToken = default)
=> SendAsync(HttpMethod.Post, requestUri, payload, authorization, cancellationToken);
/// <summary>
/// Puts the specified URL.
/// </summary>
/// <typeparam name="T">The type of response object.</typeparam>
/// <param name="requestUri">The request URI.</param>
/// <param name="payload">The payload.</param>
/// <param name="authorization">The authorization.</param>
/// <param name="ct">The cancellation token.</param>
/// <returns>
/// A task with a result of the requested type.
/// </returns>
public static async Task<T> Put<T>(
Uri requestUri,
object payload,
string? authorization = null,
CancellationToken ct = default)
{
var jsonString = await PutString(requestUri, payload, authorization, ct)
.ConfigureAwait(false);
return !string.IsNullOrEmpty(jsonString) ? Json.Deserialize<T>(jsonString) : default;
}
/// <summary>
/// Puts the specified URL.
/// </summary>
/// <param name="requestUri">The request URI.</param>
/// <param name="payload">The payload.</param>
/// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// A task with a result of the requested collection of key/value pairs.
/// </returns>
public static async Task<IDictionary<string, object>?> Put(
Uri requestUri,
object payload,
string? authorization = null,
CancellationToken cancellationToken = default)
{
var response = await Put<object>(requestUri, payload, authorization, cancellationToken)
.ConfigureAwait(false);
return response as IDictionary<string, object>;
}
/// <summary>
/// Puts as string.
/// </summary>
/// <param name="requestUri">The request URI.</param>
/// <param name="payload">The payload.</param>
/// <param name="authorization">The authorization.</param>
/// <param name="ct">The cancellation token.</param>
/// <returns>
/// A task with a result of the requested string.
/// </returns>
/// <exception cref="ArgumentNullException">url.</exception>
/// <exception cref="JsonRequestException">Error PUT JSON.</exception>
public static Task<string> PutString(
Uri requestUri,
object payload,
string? authorization = null,
CancellationToken ct = default) => SendAsync(HttpMethod.Put, requestUri, payload, authorization, ct);
/// <summary>
/// Gets as string.
/// </summary>
/// <param name="requestUri">The request URI.</param>
/// <param name="authorization">The authorization.</param>
/// <param name="ct">The cancellation token.</param>
/// <returns>
/// A task with a result of the requested string.
/// </returns>
/// <exception cref="ArgumentNullException">url.</exception>
/// <exception cref="JsonRequestException">Error GET JSON.</exception>
public static Task<string> GetString(
Uri requestUri,
string? authorization = null,
CancellationToken ct = default)
=> GetString(requestUri, null, authorization, ct);
/// <summary>
/// Gets the string.
/// </summary>
/// <param name="uri">The URI.</param>
/// <param name="headers">The headers.</param>
/// <param name="authorization">The authorization.</param>
/// <param name="ct">The ct.</param>
/// <returns>
/// A task with a result of the requested string.
/// </returns>
public static async Task<string> GetString(
Uri uri,
IDictionary<string, IEnumerable<string>>? headers,
string? authorization = null,
CancellationToken ct = default)
{
var response = await GetHttpContent(uri, ct, authorization, headers)
.ConfigureAwait(false);
return await response.ReadAsStringAsync()
.ConfigureAwait(false);
}
/// <summary>
/// Gets the specified URL and return the JSON data as object
/// with optional authorization token.
/// </summary>
/// <typeparam name="T">The response type.</typeparam>
/// <param name="requestUri">The request URI.</param>
/// <param name="authorization">The authorization.</param>
/// <param name="ct">The cancellation token.</param>
/// <returns>
/// A task with a result of the requested type.
/// </returns>
public static async Task<T> Get<T>(
Uri requestUri,
string? authorization = null,
CancellationToken ct = default)
{
var jsonString = await GetString(requestUri, authorization, ct)
.ConfigureAwait(false);
return !string.IsNullOrEmpty(jsonString) ? Json.Deserialize<T>(jsonString) : default;
}
/// <summary>
/// Gets the specified URL and return the JSON data as object
/// with optional authorization token.
/// </summary>
/// <typeparam name="T">The response type.</typeparam>
/// <param name="requestUri">The request URI.</param>
/// <param name="headers">The headers.</param>
/// <param name="authorization">The authorization.</param>
/// <param name="ct">The cancellation token.</param>
/// <returns>
/// A task with a result of the requested type.
/// </returns>
public static async Task<T> Get<T>(
Uri requestUri,
IDictionary<string, IEnumerable<string>>? headers,
string? authorization = null,
CancellationToken ct = default)
{
var jsonString = await GetString(requestUri, headers, authorization, ct)
.ConfigureAwait(false);
return !string.IsNullOrEmpty(jsonString) ? Json.Deserialize<T>(jsonString) : default;
}
/// <summary>
/// Gets the binary.
/// </summary>
/// <param name="requestUri">The request URI.</param>
/// <param name="authorization">The authorization.</param>
/// <param name="ct">The cancellation token.</param>
/// <returns>
/// A task with a result of the requested byte array.
/// </returns>
/// <exception cref="ArgumentNullException">url.</exception>
/// <exception cref="JsonRequestException">Error GET Binary.</exception>
public static async Task<byte[]> GetBinary(
Uri requestUri,
string? authorization = null,
CancellationToken ct = default)
{
var response = await GetHttpContent(requestUri, ct, authorization)
.ConfigureAwait(false);
return await response.ReadAsByteArrayAsync()
.ConfigureAwait(false);
}
/// <summary>
/// Authenticate against a web server using Bearer Token.
/// </summary>
/// <param name="requestUri">The request URI.</param>
/// <param name="username">The username.</param>
/// <param name="password">The password.</param>
/// <param name="ct">The cancellation token.</param>
/// <returns>
/// A task with a Dictionary with authentication data.
/// </returns>
/// <exception cref="ArgumentNullException">url
/// or
/// username.</exception>
/// <exception cref="SecurityException">Error Authenticating.</exception>
public static async Task<IDictionary<string, object>?> Authenticate(
Uri requestUri,
string username,
string password,
CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(username))
throw new ArgumentNullException(nameof(username));
// ignore empty password for now
var content = $"grant_type=password&username={username}&password={password}";
using var requestContent = new StringContent(content, Encoding.UTF8, FormType);
var response = await HttpClient.PostAsync(requestUri, requestContent, ct).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
throw new SecurityException($"Error Authenticating. Status code: {response.StatusCode}.");
var jsonPayload = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
return Json.Deserialize(jsonPayload) as IDictionary<string, object>;
}
/// <summary>
/// Posts the file.
/// </summary>
/// <param name="requestUri">The request URI.</param>
/// <param name="buffer">The buffer.</param>
/// <param name="fileName">Name of the file.</param>
/// <param name="authorization">The authorization.</param>
/// <param name="ct">The cancellation token.</param>
/// <returns>
/// A task with a result of the requested string.
/// </returns>
public static Task<string> PostFileString(
Uri requestUri,
byte[] buffer,
string fileName,
string? authorization = null,
CancellationToken ct = default) =>
PostString(requestUri, new { Filename = fileName, Data = buffer }, authorization, ct);
/// <summary>
/// Posts the file.
/// </summary>
/// <typeparam name="T">The response type.</typeparam>
/// <param name="requestUri">The request URI.</param>
/// <param name="buffer">The buffer.</param>
/// <param name="fileName">Name of the file.</param>
/// <param name="authorization">The authorization.</param>
/// <param name="ct">The cancellation token.</param>
/// <returns>
/// A task with a result of the requested string.
/// </returns>
public static Task<T> PostFile<T>(
Uri requestUri,
byte[] buffer,
string fileName,
string? authorization = null,
CancellationToken ct = default) =>
Post<T>(requestUri, new { Filename = fileName, Data = buffer }, authorization, ct);
/// <summary>
/// Sends the asynchronous request.
/// </summary>
/// <param name="method">The method.</param>
/// <param name="requestUri">The request URI.</param>
/// <param name="payload">The payload.</param>
/// <param name="authorization">The authorization.</param>
/// <param name="ct">The cancellation token.</param>
/// <returns>
/// A task with a result of the requested string.
/// </returns>
/// <exception cref="ArgumentNullException">requestUri.</exception>
/// <exception cref="JsonRequestException">Error {method} JSON.</exception>
public static async Task<string> SendAsync(
HttpMethod method,
Uri requestUri,
object payload,
string? authorization = null,
CancellationToken ct = default)
{
using var response = await GetResponse(requestUri, authorization, null, payload, method, ct).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
throw new JsonRequestException(
$"Error {method} JSON",
(int)response.StatusCode,
await response.Content.ReadAsStringAsync().ConfigureAwait(false));
}
return await response.Content.ReadAsStringAsync()
.ConfigureAwait(false);
}
private static async Task<HttpContent> GetHttpContent(
Uri uri,
CancellationToken ct,
string? authorization = null,
IDictionary<string, IEnumerable<string>>? headers = null)
{
var response = await GetResponse(uri, authorization, headers, ct: ct)
.ConfigureAwait(false);
return response.IsSuccessStatusCode
? response.Content
: throw new JsonRequestException("Error GET", (int)response.StatusCode);
}
private static async Task<HttpResponseMessage> GetResponse(
Uri uri,
string? authorization,
IDictionary<string, IEnumerable<string>>? headers,
object? payload = null,
HttpMethod? method = default,
CancellationToken ct = default)
{
if (uri == null)
throw new ArgumentNullException(nameof(uri));
using var requestMessage = new HttpRequestMessage(method ?? HttpMethod.Get, uri);
if (!string.IsNullOrWhiteSpace(authorization))
{
requestMessage.Headers.Authorization
= new AuthenticationHeaderValue("Bearer", authorization);
}
if (headers != null)
{
foreach (var header in headers)
requestMessage.Headers.Add(header.Key, header.Value);
}
if (payload != null && requestMessage.Method != HttpMethod.Get)
{
requestMessage.Content = new StringContent(Json.Serialize(payload), Encoding.UTF8, JsonMimeType);
}
return await HttpClient.SendAsync(requestMessage, ct)
.ConfigureAwait(false);
}
}
}
+47
View File
@@ -0,0 +1,47 @@
namespace Swan.Net
{
using System;
/// <summary>
/// Represents errors that occurs requesting a JSON file through HTTP.
/// </summary>
/// <seealso cref="System.Exception" />
[Serializable]
public class JsonRequestException
: Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="JsonRequestException"/> class.
/// </summary>
/// <param name="message">The message.</param>
/// <param name="httpErrorCode">The HTTP error code.</param>
/// <param name="errorContent">Content of the error.</param>
public JsonRequestException(string message, int httpErrorCode = 500, string errorContent = null)
: base(message)
{
HttpErrorCode = httpErrorCode;
HttpErrorContent = errorContent;
}
/// <summary>
/// Gets the HTTP error code.
/// </summary>
/// <value>
/// The HTTP error code.
/// </value>
public int HttpErrorCode { get; }
/// <summary>
/// Gets the content of the HTTP error.
/// </summary>
/// <value>
/// The content of the HTTP error.
/// </value>
public string HttpErrorContent { get; }
/// <inheritdoc />
public override string ToString() => string.IsNullOrEmpty(HttpErrorContent)
? $"HTTP Response Status Code {HttpErrorCode} Error Message: {HttpErrorContent}"
: base.ToString();
}
}
+328
View File
@@ -0,0 +1,328 @@
namespace Swan.Net
{
using 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;
/// <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 int DnsDefaultPort = 53;
/// <summary>
/// The NTP default port.
/// </summary>
public const int 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
var zeroConf = new IPAddress(0);
var adapters = NetworkInterface.GetAllNetworkInterfaces()
.Where(network =>
network.OperationalStatus == OperationalStatus.Up
&& network.NetworkInterfaceType != NetworkInterfaceType.Unknown
&& network.NetworkInterfaceType != NetworkInterfaceType.Loopback)
.ToArray();
var result = new Dictionary<NetworkInterface, IPInterfaceProperties>();
foreach (var adapter in adapters)
{
var 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(bool 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,
bool skipTypeFilter = false,
bool includeLoopback = false)
{
var addressList = new List<IPAddress>();
var interfaces = NetworkInterface.GetAllNetworkInterfaces()
.Where(ni =>
#if NET461
ni.IsReceiveOnly == false &&
#endif
(skipTypeFilter || ni.NetworkInterfaceType == interfaceType) &&
ni.OperationalStatus == OperationalStatus.Up)
.ToArray();
foreach (var networkInterface in interfaces)
{
var 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 var client = new HttpClient();
var 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)
{
var 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, int 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.Substring(0, fqdn.Length - 1);
}
var client = new DnsClient(dnsServer, port);
var 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, int port)
{
var 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)
{
var 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, int port)
{
if (query == null)
throw new ArgumentNullException(nameof(query));
var client = new DnsClient(dnsServer, port);
var 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, int port = NtpDefaultPort)
{
if (ntpServerAddress == null)
throw new ArgumentNullException(nameof(ntpServerAddress));
// NTP message size - 16 bytes of the digest (RFC 2030)
var 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
var endPoint = new IPEndPoint(ntpServerAddress, port);
var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
#if !NET461
await socket.ConnectAsync(endPoint).ConfigureAwait(false);
#else
socket.Connect(endPoint);
#endif
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
ulong intPart = BitConverter.ToUInt32(ntpData, serverReplyTime);
// Get the seconds fraction
ulong 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();
}
var milliseconds = (intPart * 1000) + ((fractPart * 1000) / 0x100000000L);
// The time is given in UTC
return new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds((long) 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",
int port = NtpDefaultPort)
{
var addresses = await GetDnsHostEntryAsync(ntpServerName).ConfigureAwait(false);
return await GetNetworkTimeUtcAsync(addresses.First(), port).ConfigureAwait(false);
}
#endregion
}
}
+166
View File
@@ -0,0 +1,166 @@
// ReSharper disable InconsistentNaming
namespace Swan.Net.Smtp
{
/// <summary>
/// Enumerates all of the well-known SMTP command names.
/// </summary>
public enum SmtpCommandNames
{
/// <summary>
/// An unknown command
/// </summary>
Unknown,
/// <summary>
/// The helo command
/// </summary>
HELO,
/// <summary>
/// The ehlo command
/// </summary>
EHLO,
/// <summary>
/// The quit command
/// </summary>
QUIT,
/// <summary>
/// The help command
/// </summary>
HELP,
/// <summary>
/// The noop command
/// </summary>
NOOP,
/// <summary>
/// The rset command
/// </summary>
RSET,
/// <summary>
/// The mail command
/// </summary>
MAIL,
/// <summary>
/// The data command
/// </summary>
DATA,
/// <summary>
/// The send command
/// </summary>
SEND,
/// <summary>
/// The soml command
/// </summary>
SOML,
/// <summary>
/// The saml command
/// </summary>
SAML,
/// <summary>
/// The RCPT command
/// </summary>
RCPT,
/// <summary>
/// The vrfy command
/// </summary>
VRFY,
/// <summary>
/// The expn command
/// </summary>
EXPN,
/// <summary>
/// The starttls command
/// </summary>
STARTTLS,
/// <summary>
/// The authentication command
/// </summary>
AUTH,
}
/// <summary>
/// Enumerates the reply code severities.
/// </summary>
public enum SmtpReplyCodeSeverities
{
/// <summary>
/// The unknown severity
/// </summary>
Unknown = 0,
/// <summary>
/// The positive completion severity
/// </summary>
PositiveCompletion = 200,
/// <summary>
/// The positive intermediate severity
/// </summary>
PositiveIntermediate = 300,
/// <summary>
/// The transient negative severity
/// </summary>
TransientNegative = 400,
/// <summary>
/// The permanent negative severity
/// </summary>
PermanentNegative = 500,
}
/// <summary>
/// Enumerates the reply code categories.
/// </summary>
public enum SmtpReplyCodeCategories
{
/// <summary>
/// The unknown category
/// </summary>
Unknown = -1,
/// <summary>
/// The syntax category
/// </summary>
Syntax = 0,
/// <summary>
/// The information category
/// </summary>
Information = 1,
/// <summary>
/// The connections category
/// </summary>
Connections = 2,
/// <summary>
/// The unspecified a category
/// </summary>
UnspecifiedA = 3,
/// <summary>
/// The unspecified b category
/// </summary>
UnspecifiedB = 4,
/// <summary>
/// The system category
/// </summary>
System = 5,
}
}
+388
View File
@@ -0,0 +1,388 @@
namespace Swan.Net.Smtp
{
using System.Threading;
using System;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Security;
using System.Text;
using System.Net.Security;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Net.Mail;
/// <summary>
/// Represents a basic SMTP client that is capable of submitting messages to an SMTP server.
/// </summary>
/// <example>
/// The following code explains how to send a simple e-mail.
/// <code>
/// using System.Net.Mail;
///
/// class Example
/// {
/// static void Main()
/// {
/// // create a new smtp client using google's smtp server
/// var client = new Swan.Net.Smtp.SmtpClient("smtp.gmail.com", 587);
///
/// // send an email
/// client.SendMailAsync(
/// new MailMessage("sender@test.com", "recipient@test.cm", "Subject", "Body"));
/// }
/// }
/// </code>
///
/// The following code demonstrates how to sent an e-mail using a SmtpSessionState:
/// <code>
/// using Swan.Net.Smtp;
///
/// class Example
/// {
/// static void Main()
/// {
/// // create a new smtp client using google's smtp server
/// var client = new SmtpClient("smtp.gmail.com", 587);
///
/// // create a new session state with a sender address
/// var session = new SmtpSessionState { SenderAddress = "sender@test.com" };
///
/// // add a recipient
/// session.Recipients.Add("recipient@test.cm");
///
/// // send
/// client.SendMailAsync(session);
/// }
/// }
/// </code>
///
/// The following code shows how to send an e-mail with an attachment using MimeKit:
/// <code>
/// using MimeKit;
/// using Swan.Net.Smtp;
///
/// class Example
/// {
/// static void Main()
/// {
/// // create a new smtp client using google's smtp server
/// var client = new SmtpClient("smtp.gmail.com", 587);
///
/// // create a new session state with a sender address
/// var session = new SmtpSessionState { SenderAddress = "sender@test.com" };
///
/// // add a recipient
/// session.Recipients.Add("recipient@test.cm");
///
/// // load a file as an attachment
/// var attachment = new MimePart("image", "gif")
/// {
/// Content = new
/// MimeContent(File.OpenRead("meme.gif"), ContentEncoding.Default),
/// ContentDisposition =
/// new ContentDisposition(ContentDisposition.Attachment),
/// ContentTransferEncoding = ContentEncoding.Base64,
/// FileName = Path.GetFileName("meme.gif")
/// };
///
/// // send
/// client.SendMailAsync(session);
/// }
/// }
/// </code>
/// </example>
public class SmtpClient
{
/// <summary>
/// Initializes a new instance of the <see cref="SmtpClient" /> class.
/// </summary>
/// <param name="host">The host.</param>
/// <param name="port">The port.</param>
/// <exception cref="ArgumentNullException">host.</exception>
public SmtpClient(string host, int port)
{
Host = host ?? throw new ArgumentNullException(nameof(host));
Port = port;
ClientHostname = Network.HostName;
}
/// <summary>
/// Gets or sets the credentials. No credentials will be used if set to null.
/// </summary>
/// <value>
/// The credentials.
/// </value>
public NetworkCredential Credentials { get; set; }
/// <summary>
/// Gets the host.
/// </summary>
/// <value>
/// The host.
/// </value>
public string Host { get; }
/// <summary>
/// Gets the port.
/// </summary>
/// <value>
/// The port.
/// </value>
public int Port { get; }
/// <summary>
/// Gets or sets a value indicating whether the SSL is enabled.
/// If set to false, communication between client and server will not be secured.
/// </summary>
/// <value>
/// <c>true</c> if [enable SSL]; otherwise, <c>false</c>.
/// </value>
public bool EnableSsl { get; set; }
/// <summary>
/// Gets or sets the name of the client that gets announced to the server.
/// </summary>
/// <value>
/// The client hostname.
/// </value>
public string ClientHostname { get; set; }
/// <summary>
/// Sends an email message asynchronously.
/// </summary>
/// <param name="message">The message.</param>
/// <param name="sessionId">The session identifier.</param>
/// <param name="callback">The callback.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// A task that represents the asynchronous of send email operation.
/// </returns>
/// <exception cref="ArgumentNullException">message.</exception>
public Task SendMailAsync(
MailMessage message,
string? sessionId = null,
RemoteCertificateValidationCallback? callback = null,
CancellationToken cancellationToken = default)
{
if (message == null)
throw new ArgumentNullException(nameof(message));
var state = new SmtpSessionState
{
AuthMode = Credentials == null ? string.Empty : SmtpDefinitions.SmtpAuthMethods.Login,
ClientHostname = ClientHostname,
IsChannelSecure = EnableSsl,
SenderAddress = message.From.Address,
};
if (Credentials != null)
{
state.Username = Credentials.UserName;
state.Password = Credentials.Password;
}
foreach (var recipient in message.To)
{
state.Recipients.Add(recipient.Address);
}
state.DataBuffer.AddRange(message.ToMimeMessage().ToArray());
return SendMailAsync(state, sessionId, callback, cancellationToken);
}
/// <summary>
/// Sends an email message using a session state object.
/// Credentials, Enable SSL and Client Hostname are NOT taken from the state object but
/// rather from the properties of this class.
/// </summary>
/// <param name="sessionState">The state.</param>
/// <param name="sessionId">The session identifier.</param>
/// <param name="callback">The callback.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// A task that represents the asynchronous of send email operation.
/// </returns>
/// <exception cref="ArgumentNullException">sessionState.</exception>
public Task SendMailAsync(
SmtpSessionState sessionState,
string? sessionId = null,
RemoteCertificateValidationCallback? callback = null,
CancellationToken cancellationToken = default)
{
if (sessionState == null)
throw new ArgumentNullException(nameof(sessionState));
return SendMailAsync(new[] { sessionState }, sessionId, callback, cancellationToken);
}
/// <summary>
/// Sends an array of email messages using a session state object.
/// Credentials, Enable SSL and Client Hostname are NOT taken from the state object but
/// rather from the properties of this class.
/// </summary>
/// <param name="sessionStates">The session states.</param>
/// <param name="sessionId">The session identifier.</param>
/// <param name="callback">The callback.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// A task that represents the asynchronous of send email operation.
/// </returns>
/// <exception cref="ArgumentNullException">sessionStates.</exception>
/// <exception cref="SecurityException">Could not upgrade the channel to SSL.</exception>
/// <exception cref="SmtpException">Defines an SMTP Exceptions class.</exception>
public async Task SendMailAsync(
IEnumerable<SmtpSessionState> sessionStates,
string? sessionId = null,
RemoteCertificateValidationCallback? callback = null,
CancellationToken cancellationToken = default)
{
if (sessionStates == null)
throw new ArgumentNullException(nameof(sessionStates));
using var tcpClient = new TcpClient();
await tcpClient.ConnectAsync(Host, Port).ConfigureAwait(false);
using var connection = new Connection(tcpClient, Encoding.UTF8, "\r\n", true, 1000);
var sender = new SmtpSender(sessionId);
try
{
// Read the greeting message
sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false);
// EHLO 1
await SendEhlo(sender, connection, cancellationToken).ConfigureAwait(false);
// STARTTLS
if (EnableSsl)
{
sender.RequestText = $"{SmtpCommandNames.STARTTLS}";
await connection.WriteLineAsync(sender.RequestText, cancellationToken).ConfigureAwait(false);
sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false);
sender.ValidateReply();
if (await connection.UpgradeToSecureAsClientAsync(callback: callback).ConfigureAwait(false) == false)
throw new SecurityException("Could not upgrade the channel to SSL.");
}
// EHLO 2
await SendEhlo(sender, connection, cancellationToken).ConfigureAwait(false);
// AUTH
if (Credentials != null)
{
var auth = new ConnectionAuth(connection, sender, Credentials);
await auth.AuthenticateAsync(cancellationToken).ConfigureAwait(false);
}
foreach (var sessionState in sessionStates)
{
{
// MAIL FROM
sender.RequestText = $"{SmtpCommandNames.MAIL} FROM:<{sessionState.SenderAddress}>";
await connection.WriteLineAsync(sender.RequestText, cancellationToken).ConfigureAwait(false);
sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false);
sender.ValidateReply();
}
// RCPT TO
foreach (var recipient in sessionState.Recipients)
{
sender.RequestText = $"{SmtpCommandNames.RCPT} TO:<{recipient}>";
await connection.WriteLineAsync(sender.RequestText, cancellationToken).ConfigureAwait(false);
sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false);
sender.ValidateReply();
}
{
// DATA
sender.RequestText = $"{SmtpCommandNames.DATA}";
await connection.WriteLineAsync(sender.RequestText, cancellationToken).ConfigureAwait(false);
sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false);
sender.ValidateReply();
}
{
// CONTENT
var dataTerminator = sessionState.DataBuffer
.Skip(sessionState.DataBuffer.Count - 5)
.ToText();
sender.RequestText = $"Buffer ({sessionState.DataBuffer.Count} bytes)";
await connection.WriteDataAsync(sessionState.DataBuffer.ToArray(), true, cancellationToken).ConfigureAwait(false);
if (!dataTerminator.EndsWith(SmtpDefinitions.SmtpDataCommandTerminator))
await connection.WriteTextAsync(SmtpDefinitions.SmtpDataCommandTerminator, cancellationToken).ConfigureAwait(false);
sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false);
sender.ValidateReply();
}
}
{
// QUIT
sender.RequestText = $"{SmtpCommandNames.QUIT}";
await connection.WriteLineAsync(sender.RequestText, cancellationToken).ConfigureAwait(false);
sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false);
sender.ValidateReply();
}
}
catch (Exception ex)
{
throw new SmtpException($"Could not send email - Session ID {sessionId}. {ex.Message}\r\n Last Request: {sender.RequestText}\r\n Last Reply: {sender.ReplyText}");
}
}
private async Task SendEhlo(SmtpSender sender, Connection connection, CancellationToken cancellationToken)
{
sender.RequestText = $"{SmtpCommandNames.EHLO} {ClientHostname}";
await connection.WriteLineAsync(sender.RequestText, cancellationToken).ConfigureAwait(false);
do
{
sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false);
}
while (!sender.IsReplyOk);
sender.ValidateReply();
}
private class ConnectionAuth
{
private readonly SmtpSender _sender;
private readonly Connection _connection;
private readonly NetworkCredential _credentials;
public ConnectionAuth(Connection connection, SmtpSender sender, NetworkCredential credentials)
{
_connection = connection;
_sender = sender;
_credentials = credentials;
}
public async Task AuthenticateAsync(CancellationToken ct)
{
_sender.RequestText =
$"{SmtpCommandNames.AUTH} {SmtpDefinitions.SmtpAuthMethods.Login} {Convert.ToBase64String(Encoding.UTF8.GetBytes(_credentials.UserName))}";
await _connection.WriteLineAsync(_sender.RequestText, ct).ConfigureAwait(false);
_sender.ReplyText = await _connection.ReadLineAsync(ct).ConfigureAwait(false);
_sender.ValidateReply();
_sender.RequestText = Convert.ToBase64String(Encoding.UTF8.GetBytes(_credentials.Password));
await _connection.WriteLineAsync(_sender.RequestText, ct).ConfigureAwait(false);
_sender.ReplyText = await _connection.ReadLineAsync(ct).ConfigureAwait(false);
_sender.ValidateReply();
}
}
}
}
+29
View File
@@ -0,0 +1,29 @@
namespace Swan.Net.Smtp
{
/// <summary>
/// Contains useful constants and definitions.
/// </summary>
public static class SmtpDefinitions
{
/// <summary>
/// The string sequence that delimits the end of the DATA command.
/// </summary>
public const string SmtpDataCommandTerminator = "\r\n.\r\n";
/// <summary>
/// Lists the AUTH methods supported by default.
/// </summary>
public static class SmtpAuthMethods
{
/// <summary>
/// The plain method.
/// </summary>
public const string Plain = "PLAIN";
/// <summary>
/// The login method.
/// </summary>
public const string Login = "LOGIN";
}
}
}
+60
View File
@@ -0,0 +1,60 @@
namespace Swan.Net.Smtp
{
using Logging;
using System;
using System.Linq;
using System.Net.Mail;
/// <summary>
/// Use this class to store the sender session data.
/// </summary>
internal class SmtpSender
{
private readonly string _sessionId;
private string _requestText;
public SmtpSender(string sessionId)
{
_sessionId = sessionId;
}
public string RequestText
{
get => _requestText;
set
{
_requestText = value;
$" TX {_requestText}".Trace(typeof(SmtpClient), _sessionId);
}
}
public string ReplyText { get; set; }
public bool IsReplyOk => ReplyText.StartsWith("250 ", StringComparison.OrdinalIgnoreCase);
public void ValidateReply()
{
if (ReplyText == null)
throw new SmtpException("There was no response from the server");
try
{
var response = SmtpServerReply.Parse(ReplyText);
$" RX {ReplyText} - {response.IsPositive}".Trace(typeof(SmtpClient), _sessionId);
if (response.IsPositive) return;
var responseContent = response.Content.Any()
? string.Join(";", response.Content.ToArray())
: string.Empty;
throw new SmtpException((SmtpStatusCode)response.ReplyCode, responseContent);
}
catch (Exception ex)
{
if (!(ex is SmtpException))
throw new SmtpException($"Could not parse server response: {ReplyText}");
}
}
}
}
+243
View File
@@ -0,0 +1,243 @@
namespace Swan.Net.Smtp
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
/// <summary>
/// Represents an SMTP server response object.
/// </summary>
public class SmtpServerReply
{
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="SmtpServerReply"/> class.
/// </summary>
/// <param name="responseCode">The response code.</param>
/// <param name="statusCode">The status code.</param>
/// <param name="content">The content.</param>
public SmtpServerReply(int responseCode, string statusCode, params string[] content)
{
Content = new List<string>();
ReplyCode = responseCode;
EnhancedStatusCode = statusCode;
Content.AddRange(content);
IsValid = responseCode >= 200 && responseCode < 600;
ReplyCodeSeverity = SmtpReplyCodeSeverities.Unknown;
ReplyCodeCategory = SmtpReplyCodeCategories.Unknown;
if (!IsValid) return;
if (responseCode >= 200) ReplyCodeSeverity = SmtpReplyCodeSeverities.PositiveCompletion;
if (responseCode >= 300) ReplyCodeSeverity = SmtpReplyCodeSeverities.PositiveIntermediate;
if (responseCode >= 400) ReplyCodeSeverity = SmtpReplyCodeSeverities.TransientNegative;
if (responseCode >= 500) ReplyCodeSeverity = SmtpReplyCodeSeverities.PermanentNegative;
if (responseCode >= 600) ReplyCodeSeverity = SmtpReplyCodeSeverities.Unknown;
if (int.TryParse(responseCode.ToString(CultureInfo.InvariantCulture).Substring(1, 1), out var middleDigit))
{
if (middleDigit >= 0 && middleDigit <= 5)
ReplyCodeCategory = (SmtpReplyCodeCategories) middleDigit;
}
}
/// <summary>
/// Initializes a new instance of the <see cref="SmtpServerReply"/> class.
/// </summary>
public SmtpServerReply()
: this(0, string.Empty, string.Empty)
{
// placeholder
}
/// <summary>
/// Initializes a new instance of the <see cref="SmtpServerReply"/> class.
/// </summary>
/// <param name="responseCode">The response code.</param>
/// <param name="statusCode">The status code.</param>
/// <param name="content">The content.</param>
public SmtpServerReply(int responseCode, string statusCode, string content)
: this(responseCode, statusCode, new[] {content})
{
}
/// <summary>
/// Initializes a new instance of the <see cref="SmtpServerReply"/> class.
/// </summary>
/// <param name="responseCode">The response code.</param>
/// <param name="content">The content.</param>
public SmtpServerReply(int responseCode, string content)
: this(responseCode, string.Empty, content)
{
}
#endregion
#region Pre-built responses (https://tools.ietf.org/html/rfc5321#section-4.2.2)
/// <summary>
/// Gets the command unrecognized reply.
/// </summary>
public static SmtpServerReply CommandUnrecognized =>
new SmtpServerReply(500, "Syntax error, command unrecognized");
/// <summary>
/// Gets the syntax error arguments reply.
/// </summary>
public static SmtpServerReply SyntaxErrorArguments =>
new SmtpServerReply(501, "Syntax error in parameters or arguments");
/// <summary>
/// Gets the command not implemented reply.
/// </summary>
public static SmtpServerReply CommandNotImplemented => new SmtpServerReply(502, "Command not implemented");
/// <summary>
/// Gets the bad sequence of commands reply.
/// </summary>
public static SmtpServerReply BadSequenceOfCommands => new SmtpServerReply(503, "Bad sequence of commands");
/// <summary>
/// Gets the protocol violation reply.
/// </summary>=
public static SmtpServerReply ProtocolViolation =>
new SmtpServerReply(451, "Requested action aborted: error in processing");
/// <summary>
/// Gets the system status bye reply.
/// </summary>
public static SmtpServerReply SystemStatusBye =>
new SmtpServerReply(221, "Service closing transmission channel");
/// <summary>
/// Gets the system status help reply.
/// </summary>=
public static SmtpServerReply SystemStatusHelp => new SmtpServerReply(221, "Refer to RFC 5321");
/// <summary>
/// Gets the bad syntax command empty reply.
/// </summary>
public static SmtpServerReply BadSyntaxCommandEmpty => new SmtpServerReply(400, "Error: bad syntax");
/// <summary>
/// Gets the OK reply.
/// </summary>
public static SmtpServerReply Ok => new SmtpServerReply(250, "OK");
/// <summary>
/// Gets the authorization required reply.
/// </summary>
public static SmtpServerReply AuthorizationRequired => new SmtpServerReply(530, "Authorization Required");
#endregion
#region Properties
/// <summary>
/// Gets the response severity.
/// </summary>
public SmtpReplyCodeSeverities ReplyCodeSeverity { get; }
/// <summary>
/// Gets the response category.
/// </summary>
public SmtpReplyCodeCategories ReplyCodeCategory { get; }
/// <summary>
/// Gets the numeric response code.
/// </summary>
public int ReplyCode { get; }
/// <summary>
/// Gets the enhanced status code.
/// </summary>
public string EnhancedStatusCode { get; }
/// <summary>
/// Gets the content.
/// </summary>
public List<string> Content { get; }
/// <summary>
/// Returns true if the response code is between 200 and 599.
/// </summary>
public bool IsValid { get; }
/// <summary>
/// Gets a value indicating whether this instance is positive.
/// </summary>
public bool IsPositive => ReplyCode >= 200 && ReplyCode <= 399;
#endregion
#region Methods
/// <summary>
/// Parses the specified text into a Server Reply for thorough analysis.
/// </summary>
/// <param name="text">The text.</param>
/// <returns>A new instance of SMTP server response object.</returns>
public static SmtpServerReply Parse(string text)
{
var lines = text.Split(new[] {"\r\n"}, StringSplitOptions.RemoveEmptyEntries);
if (lines.Length == 0) return new SmtpServerReply();
var lastLineParts = lines.Last().Split(new[] {" "}, StringSplitOptions.RemoveEmptyEntries);
var enhancedStatusCode = string.Empty;
int.TryParse(lastLineParts[0], out var responseCode);
if (lastLineParts.Length > 1)
{
if (lastLineParts[1].Split('.').Length == 3)
enhancedStatusCode = lastLineParts[1];
}
var content = new List<string>();
for (var i = 0; i < lines.Length; i++)
{
var splitChar = i == lines.Length - 1 ? " " : "-";
var lineParts = lines[i].Split(new[] {splitChar}, 2, StringSplitOptions.None);
var lineContent = lineParts.Last();
if (string.IsNullOrWhiteSpace(enhancedStatusCode) == false)
lineContent = lineContent.Replace(enhancedStatusCode, string.Empty).Trim();
content.Add(lineContent);
}
return new SmtpServerReply(responseCode, enhancedStatusCode, content.ToArray());
}
/// <summary>
/// Returns a <see cref="System.String" /> that represents this instance.
/// </summary>
/// <returns>
/// A <see cref="System.String" /> that represents this instance.
/// </returns>
public override string ToString()
{
var responseCodeText = ReplyCode.ToString(CultureInfo.InvariantCulture);
var statusCodeText = string.IsNullOrWhiteSpace(EnhancedStatusCode)
? string.Empty
: $" {EnhancedStatusCode.Trim()}";
if (Content.Count == 0) return $"{responseCodeText}{statusCodeText}";
var builder = new StringBuilder();
for (var i = 0; i < Content.Count; i++)
{
var isLastLine = i == Content.Count - 1;
builder.Append(isLastLine
? $"{responseCodeText}{statusCodeText} {Content[i]}"
: $"{responseCodeText}-{Content[i]}\r\n");
}
return builder.ToString();
}
#endregion
}
}
+158
View File
@@ -0,0 +1,158 @@
namespace Swan.Net.Smtp
{
using System.Collections.Generic;
/// <summary>
/// Represents the state of an SMTP session associated with a client.
/// </summary>
public class SmtpSessionState
{
/// <summary>
/// Initializes a new instance of the <see cref="SmtpSessionState"/> class.
/// </summary>
public SmtpSessionState()
{
DataBuffer = new List<byte>();
Reset(true);
ResetAuthentication();
}
#region Properties
/// <summary>
/// Gets the contents of the data buffer.
/// </summary>
public List<byte> DataBuffer { get; protected set; }
/// <summary>
/// Gets or sets a value indicating whether this instance has initiated.
/// </summary>
public bool HasInitiated { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the current session supports extensions.
/// </summary>
public bool SupportsExtensions { get; set; }
/// <summary>
/// Gets or sets the client hostname.
/// </summary>
public string ClientHostname { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the session is currently receiving DATA.
/// </summary>
public bool IsInDataMode { get; set; }
/// <summary>
/// Gets or sets the sender address.
/// </summary>
public string SenderAddress { get; set; }
/// <summary>
/// Gets the recipients.
/// </summary>
public List<string> Recipients { get; } = new List<string>();
/// <summary>
/// Gets or sets the extended data supporting any additional field for storage by a responder implementation.
/// </summary>
public object ExtendedData { get; set; }
#endregion
#region AUTH State
/// <summary>
/// Gets or sets a value indicating whether this instance is in authentication mode.
/// </summary>
public bool IsInAuthMode { get; set; }
/// <summary>
/// Gets or sets the username.
/// </summary>
public string Username { get; set; }
/// <summary>
/// Gets or sets the password.
/// </summary>
public string Password { get; set; }
/// <summary>
/// Gets a value indicating whether this instance has provided username.
/// </summary>
public bool HasProvidedUsername => string.IsNullOrWhiteSpace(Username) == false;
/// <summary>
/// Gets or sets a value indicating whether this instance is authenticated.
/// </summary>
public bool IsAuthenticated { get; set; }
/// <summary>
/// Gets or sets the authentication mode.
/// </summary>
public string AuthMode { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this instance is channel secure.
/// </summary>
public bool IsChannelSecure { get; set; }
/// <summary>
/// Resets the authentication state.
/// </summary>
public void ResetAuthentication()
{
Username = string.Empty;
Password = string.Empty;
AuthMode = string.Empty;
IsInAuthMode = false;
IsAuthenticated = false;
}
#endregion
#region Methods
/// <summary>
/// Resets the data mode to false, clears the recipients, the sender address and the data buffer.
/// </summary>
public void ResetEmail()
{
IsInDataMode = false;
Recipients.Clear();
SenderAddress = string.Empty;
DataBuffer.Clear();
}
/// <summary>
/// Resets the state table entirely.
/// </summary>
/// <param name="clearExtensionData">if set to <c>true</c> [clear extension data].</param>
public void Reset(bool clearExtensionData)
{
HasInitiated = false;
SupportsExtensions = false;
ClientHostname = string.Empty;
ResetEmail();
if (clearExtensionData)
ExtendedData = null;
}
/// <summary>
/// Creates a new object that is a copy of the current instance.
/// </summary>
/// <returns>A clone.</returns>
public virtual SmtpSessionState Clone()
{
var clonedState = this.CopyPropertiesToNew<SmtpSessionState>(new[] {nameof(DataBuffer)});
clonedState.DataBuffer.AddRange(DataBuffer);
clonedState.Recipients.AddRange(Recipients);
return clonedState;
}
#endregion
}
}
+46
View File
@@ -0,0 +1,46 @@
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(int exitCode, string standardOutput, string standardError)
{
ExitCode = exitCode;
StandardOutput = standardOutput;
StandardError = standardError;
}
/// <summary>
/// Gets the exit code.
/// </summary>
/// <value>
/// The exit code.
/// </value>
public int 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; }
}
}
+443
View File
@@ -0,0 +1,443 @@
namespace Swan
{
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
/// <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)
{
var 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)
{
var 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;
var standardOutputBuilder = new StringBuilder();
var standardErrorBuilder = new StringBuilder();
var 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<int> RunProcessAsync(
string filename,
string arguments,
string? workingDirectory,
ProcessDataReceivedCallback onOutputData,
ProcessDataReceivedCallback onErrorData,
Encoding encoding,
bool syncEvents = true,
CancellationToken cancellationToken = default)
{
if (filename == null)
throw new ArgumentNullException(nameof(filename));
return Task.Run(() =>
{
// Setup the process and its corresponding start info
var 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 NET461
WindowStyle = ProcessWindowStyle.Hidden,
#endif
},
};
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
var 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, bool, 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<int> RunProcessAsync(
string filename,
string arguments,
ProcessDataReceivedCallback onOutputData,
ProcessDataReceivedCallback onErrorData,
bool 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<ulong> CopyStreamAsync(
Process process,
Stream baseStream,
ProcessDataReceivedCallback onDataCallback,
bool syncEvents,
CancellationToken ct) =>
Task.Run(async () =>
{
// define some state variables
var swapBuffer = new byte[2048]; // the buffer to copy data from one stream to the next
ulong totalCount = 0; // the total amount of bytes read
var 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.
int 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 += (ulong) 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 += (ulong) readCount;
if (onDataCallback == null) continue;
// Create the buffer to pass to the callback
var eventBuffer = swapBuffer.Skip(0).Take(readCount).ToArray();
// Create the data processing callback invocation
var 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);
}
}
+92
View File
@@ -0,0 +1,92 @@
using System;
#if !NET461
namespace Swan.Services
{
/// <summary>
/// Mimic a Windows ServiceBase class. Useful to keep compatibility with applications
/// running as services in OS different to Windows.
/// </summary>
[Obsolete("This abstract class will be removed in version 3.0")]
public abstract class ServiceBase
{
/// <summary>
/// Gets or sets a value indicating whether the service can be stopped once it has started.
/// </summary>
/// <value>
/// <c>true</c> if this instance can stop; otherwise, <c>false</c>.
/// </value>
public bool CanStop { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether the service should be notified when the system is shutting down.
/// </summary>
/// <value>
/// <c>true</c> if this instance can shutdown; otherwise, <c>false</c>.
/// </value>
public bool CanShutdown { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the service can be paused and resumed.
/// </summary>
/// <value>
/// <c>true</c> if this instance can pause and continue; otherwise, <c>false</c>.
/// </value>
public bool CanPauseAndContinue { get; set; }
/// <summary>
/// Gets or sets the exit code.
/// </summary>
/// <value>
/// The exit code.
/// </value>
public int ExitCode { get; set; }
/// <summary>
/// Indicates whether to report Start, Stop, Pause, and Continue commands in the event log.
/// </summary>
/// <value>
/// <c>true</c> if [automatic log]; otherwise, <c>false</c>.
/// </value>
public bool AutoLog { get; set; }
/// <summary>
/// Gets or sets the name of the service.
/// </summary>
/// <value>
/// The name of the service.
/// </value>
public string ServiceName { get; set; }
/// <summary>
/// Stops the executing service.
/// </summary>
public void Stop()
{
if (!CanStop) return;
CanStop = false;
OnStop();
}
/// <summary>
/// When implemented in a derived class, executes when a Start command is sent to the service by the Service Control Manager (SCM)
/// or when the operating system starts (for a service that starts automatically). Specifies actions to take when the service starts.
/// </summary>
/// <param name="args">The arguments.</param>
protected virtual void OnStart(string[] args)
{
// do nothing
}
/// <summary>
/// When implemented in a derived class, executes when a Stop command is sent to the service by the Service Control Manager (SCM).
/// Specifies actions to take when a service stops running.
/// </summary>
protected virtual void OnStop()
{
// do nothing
}
}
}
#endif
+22
View File
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>Repeating code and reinventing the wheel is generally considered bad practice. At Unosquare we are committed to beautiful code and great software. Swan is a collection of classes and extension methods that we and other good developers have developed and evolved over the years. We found ourselves copying and pasting the same code for every project every time we started it. We decide to kill that cycle once and for all. This is the result of that idea. Our philosophy is that SWAN should have no external dependencies, it should be cross-platform, and it should be useful.</Description>
<Copyright>Copyright (c) 2016-2019 - Unosquare</Copyright>
<AssemblyTitle>Unosquare SWAN</AssemblyTitle>
<TargetFrameworks>netcoreapp3.0</TargetFrameworks>
<AssemblyName>Swan</AssemblyName>
<Version>2.4.2</Version>
<Authors>Unosquare</Authors>
<PackageIconUrl>https://github.com/unosquare/swan/raw/master/swan-logo-32.png</PackageIconUrl>
<PackageProjectUrl>https://github.com/unosquare/swan</PackageProjectUrl>
<PackageLicenseUrl>https://raw.githubusercontent.com/unosquare/swan/master/LICENSE</PackageLicenseUrl>
<PackageTags>best-practices netcore network objectmapper json-serialization</PackageTags>
<LangVersion>8.0</LangVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Swan.Lite\Swan.Lite.csproj" />
</ItemGroup>
</Project>
+141
View File
@@ -0,0 +1,141 @@
namespace Swan.Threading
{
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
/// <summary>
/// Represents logic providing several delay mechanisms.
/// </summary>
/// <example>
/// The following example shows how to implement delay mechanisms.
/// <code>
/// using Swan.Threading;
///
/// public class Example
/// {
/// public static void Main()
/// {
/// // using the ThreadSleep strategy
/// using (var delay = new DelayProvider(DelayProvider.DelayStrategy.ThreadSleep))
/// {
/// // retrieve how much time was delayed
/// var time = delay.WaitOne();
/// }
/// }
/// }
/// </code>
/// </example>
public sealed class DelayProvider : IDisposable
{
private readonly object _syncRoot = new object();
private readonly Stopwatch _delayStopwatch = new Stopwatch();
private bool _isDisposed;
private IWaitEvent _delayEvent;
/// <summary>
/// Initializes a new instance of the <see cref="DelayProvider"/> class.
/// </summary>
/// <param name="strategy">The strategy.</param>
public DelayProvider(DelayStrategy strategy = DelayStrategy.TaskDelay)
{
Strategy = strategy;
}
/// <summary>
/// Enumerates the different ways of providing delays.
/// </summary>
public enum DelayStrategy
{
/// <summary>
/// Using the Thread.Sleep(15) mechanism.
/// </summary>
ThreadSleep,
/// <summary>
/// Using the Task.Delay(1).Wait mechanism.
/// </summary>
TaskDelay,
/// <summary>
/// Using a wait event that completes in a background ThreadPool thread.
/// </summary>
ThreadPool,
}
/// <summary>
/// Gets the selected delay strategy.
/// </summary>
public DelayStrategy Strategy { get; }
/// <summary>
/// Creates the smallest possible, synchronous delay based on the selected strategy.
/// </summary>
/// <returns>The elapsed time of the delay.</returns>
public TimeSpan WaitOne()
{
lock (_syncRoot)
{
if (_isDisposed) return TimeSpan.Zero;
_delayStopwatch.Restart();
switch (Strategy)
{
case DelayStrategy.ThreadSleep:
DelaySleep();
break;
case DelayStrategy.TaskDelay:
DelayTask();
break;
case DelayStrategy.ThreadPool:
DelayThreadPool();
break;
}
return _delayStopwatch.Elapsed;
}
}
#region Dispose Pattern
/// <inheritdoc />
public void Dispose()
{
lock (_syncRoot)
{
if (_isDisposed) return;
_isDisposed = true;
_delayEvent?.Dispose();
}
}
#endregion
#region Private Delay Mechanisms
private static void DelaySleep() => Thread.Sleep(15);
private static void DelayTask() => Task.Delay(1).Wait();
private void DelayThreadPool()
{
if (_delayEvent == null)
_delayEvent = WaitEventFactory.Create(isCompleted: true, useSlim: true);
_delayEvent.Begin();
ThreadPool.QueueUserWorkItem(s =>
{
DelaySleep();
_delayEvent.Complete();
});
_delayEvent.Wait();
}
#endregion
}
}
+292
View File
@@ -0,0 +1,292 @@
namespace Swan.Threading
{
using System;
using System.Threading;
using System.Threading.Tasks;
/// <summary>
/// Provides a base implementation for application workers
/// that perform continuous, long-running tasks. This class
/// provides the ability to perform fine-grained control on these tasks.
/// </summary>
/// <seealso cref="IWorker" />
public abstract class ThreadWorkerBase : WorkerBase
{
private readonly object _syncLock = new object();
private readonly Thread _thread;
/// <summary>
/// Initializes a new instance of the <see cref="ThreadWorkerBase"/> class.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="priority">The thread priority.</param>
/// <param name="period">The interval of cycle execution.</param>
/// <param name="delayProvider">The cycle delay provide implementation.</param>
protected ThreadWorkerBase(string name, ThreadPriority priority, TimeSpan period, IWorkerDelayProvider delayProvider)
: base(name, period)
{
DelayProvider = delayProvider;
_thread = new Thread(RunWorkerLoop)
{
IsBackground = true,
Priority = priority,
Name = name,
};
}
/// <summary>
/// Initializes a new instance of the <see cref="ThreadWorkerBase"/> class.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="period">The execution interval.</param>
protected ThreadWorkerBase(string name, TimeSpan period)
: this(name, ThreadPriority.Normal, period, WorkerDelayProvider.Default)
{
// placeholder
}
/// <summary>
/// Provides an implementation on a cycle delay provider.
/// </summary>
protected IWorkerDelayProvider DelayProvider { get; }
/// <inheritdoc />
public override Task<WorkerState> StartAsync()
{
lock (_syncLock)
{
if (WorkerState == WorkerState.Paused || WorkerState == WorkerState.Waiting)
return ResumeAsync();
if (WorkerState != WorkerState.Created)
return Task.FromResult(WorkerState);
if (IsStopRequested)
return Task.FromResult(WorkerState);
var task = QueueStateChange(StateChangeRequest.Start);
_thread.Start();
return task;
}
}
/// <inheritdoc />
public override Task<WorkerState> PauseAsync()
{
lock (_syncLock)
{
if (WorkerState != WorkerState.Running && WorkerState != WorkerState.Waiting)
return Task.FromResult(WorkerState);
return IsStopRequested ? Task.FromResult(WorkerState) : QueueStateChange(StateChangeRequest.Pause);
}
}
/// <inheritdoc />
public override Task<WorkerState> ResumeAsync()
{
lock (_syncLock)
{
if (WorkerState == WorkerState.Created)
return StartAsync();
if (WorkerState != WorkerState.Paused && WorkerState != WorkerState.Waiting)
return Task.FromResult(WorkerState);
return IsStopRequested ? Task.FromResult(WorkerState) : QueueStateChange(StateChangeRequest.Resume);
}
}
/// <inheritdoc />
public override Task<WorkerState> StopAsync()
{
lock (_syncLock)
{
if (WorkerState == WorkerState.Stopped || WorkerState == WorkerState.Created)
{
WorkerState = WorkerState.Stopped;
return Task.FromResult(WorkerState);
}
return QueueStateChange(StateChangeRequest.Stop);
}
}
/// <summary>
/// Suspends execution queues a new new cycle for execution. The delay is given in
/// milliseconds. When overridden in a derived class the wait handle will be set
/// whenever an interrupt is received.
/// </summary>
/// <param name="wantedDelay">The remaining delay to wait for in the cycle.</param>
/// <param name="delayTask">Contains a reference to a task with the scheduled period delay.</param>
/// <param name="token">The cancellation token to cancel waiting.</param>
protected virtual void ExecuteCycleDelay(int wantedDelay, Task delayTask, CancellationToken token) =>
DelayProvider?.ExecuteCycleDelay(wantedDelay, delayTask, token);
/// <inheritdoc />
protected override void OnDisposing()
{
lock (_syncLock)
{
if ((_thread.ThreadState & ThreadState.Unstarted) != ThreadState.Unstarted)
_thread.Join();
}
}
/// <summary>
/// Implements worker control, execution and delay logic in a loop.
/// </summary>
private void RunWorkerLoop()
{
while (WorkerState != WorkerState.Stopped && !IsDisposing && !IsDisposed)
{
CycleStopwatch.Restart();
var interruptToken = CycleCancellation.Token;
var period = Period.TotalMilliseconds >= int.MaxValue ? -1 : Convert.ToInt32(Math.Floor(Period.TotalMilliseconds));
var delayTask = Task.Delay(period, interruptToken);
var initialWorkerState = WorkerState;
// Lock the cycle and capture relevant state valid for this cycle
CycleCompletedEvent.Reset();
// Process the tasks that are awaiting
if (ProcessStateChangeRequests())
continue;
try
{
if (initialWorkerState == WorkerState.Waiting &&
!interruptToken.IsCancellationRequested)
{
// Mark the state as Running
WorkerState = WorkerState.Running;
// Call the execution logic
ExecuteCycleLogic(interruptToken);
}
}
catch (Exception ex)
{
OnCycleException(ex);
}
finally
{
// Update the state
WorkerState = initialWorkerState == WorkerState.Paused
? WorkerState.Paused
: WorkerState.Waiting;
// Signal the cycle has been completed so new cycles can be executed
CycleCompletedEvent.Set();
if (!interruptToken.IsCancellationRequested)
{
var cycleDelay = ComputeCycleDelay(initialWorkerState);
if (cycleDelay == Timeout.Infinite)
delayTask = Task.Delay(Timeout.Infinite, interruptToken);
ExecuteCycleDelay(
cycleDelay,
delayTask,
CycleCancellation.Token);
}
}
}
ClearStateChangeRequests();
WorkerState = WorkerState.Stopped;
}
/// <summary>
/// Queues a transition in worker state for processing. Returns a task that can be awaited
/// when the operation completes.
/// </summary>
/// <param name="request">The request.</param>
/// <returns>The awaitable task.</returns>
private Task<WorkerState> QueueStateChange(StateChangeRequest request)
{
lock (_syncLock)
{
if (StateChangeTask != null)
return StateChangeTask;
var waitingTask = new Task<WorkerState>(() =>
{
StateChangedEvent.Wait();
lock (_syncLock)
{
StateChangeTask = null;
return WorkerState;
}
});
StateChangeTask = waitingTask;
StateChangedEvent.Reset();
StateChangeRequests[request] = true;
waitingTask.Start();
CycleCancellation.Cancel();
return waitingTask;
}
}
/// <summary>
/// Processes the state change request by checking pending events and scheduling
/// cycle execution accordingly. The <see cref="WorkerState"/> is also updated.
/// </summary>
/// <returns>Returns <c>true</c> if the execution should be terminated. <c>false</c> otherwise.</returns>
private bool ProcessStateChangeRequests()
{
lock (_syncLock)
{
var hasRequest = false;
var currentState = WorkerState;
// Update the state in the given priority
if (StateChangeRequests[StateChangeRequest.Stop] || IsDisposing || IsDisposed)
{
hasRequest = true;
WorkerState = WorkerState.Stopped;
}
else if (StateChangeRequests[StateChangeRequest.Pause])
{
hasRequest = true;
WorkerState = WorkerState.Paused;
}
else if (StateChangeRequests[StateChangeRequest.Start] || StateChangeRequests[StateChangeRequest.Resume])
{
hasRequest = true;
WorkerState = WorkerState.Waiting;
}
// Signals all state changes to continue
// as a command has been handled.
if (hasRequest)
{
ClearStateChangeRequests();
OnStateChangeProcessed(currentState, WorkerState);
}
return hasRequest;
}
}
/// <summary>
/// Signals all state change requests to set.
/// </summary>
private void ClearStateChangeRequests()
{
lock (_syncLock)
{
// Mark all events as completed
StateChangeRequests[StateChangeRequest.Start] = false;
StateChangeRequests[StateChangeRequest.Pause] = false;
StateChangeRequests[StateChangeRequest.Resume] = false;
StateChangeRequests[StateChangeRequest.Stop] = false;
StateChangedEvent.Set();
CycleCompletedEvent.Set();
}
}
}
}
+328
View File
@@ -0,0 +1,328 @@
namespace Swan.Threading
{
using System;
using System.Threading;
using System.Threading.Tasks;
/// <summary>
/// Provides a base implementation for application workers.
/// </summary>
/// <seealso cref="IWorker" />
public abstract class TimerWorkerBase : WorkerBase
{
private readonly object _syncLock = new object();
private readonly Timer _timer;
private bool _isTimerAlive = true;
/// <summary>
/// Initializes a new instance of the <see cref="TimerWorkerBase"/> class.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="period">The execution interval.</param>
protected TimerWorkerBase(string name, TimeSpan period)
: base(name, period)
{
// Instantiate the timer that will be used to schedule cycles
_timer = new Timer(
ExecuteTimerCallback,
this,
Timeout.Infinite,
Timeout.Infinite);
}
/// <inheritdoc />
public override Task<WorkerState> StartAsync()
{
lock (_syncLock)
{
if (WorkerState == WorkerState.Paused || WorkerState == WorkerState.Waiting)
return ResumeAsync();
if (WorkerState != WorkerState.Created)
return Task.FromResult(WorkerState);
if (IsStopRequested)
return Task.FromResult(WorkerState);
var task = QueueStateChange(StateChangeRequest.Start);
Interrupt();
return task;
}
}
/// <inheritdoc />
public override Task<WorkerState> PauseAsync()
{
lock (_syncLock)
{
if (WorkerState != WorkerState.Running && WorkerState != WorkerState.Waiting)
return Task.FromResult(WorkerState);
if (IsStopRequested)
return Task.FromResult(WorkerState);
var task = QueueStateChange(StateChangeRequest.Pause);
Interrupt();
return task;
}
}
/// <inheritdoc />
public override Task<WorkerState> ResumeAsync()
{
lock (_syncLock)
{
if (WorkerState == WorkerState.Created)
return StartAsync();
if (WorkerState != WorkerState.Paused && WorkerState != WorkerState.Waiting)
return Task.FromResult(WorkerState);
if (IsStopRequested)
return Task.FromResult(WorkerState);
var task = QueueStateChange(StateChangeRequest.Resume);
Interrupt();
return task;
}
}
/// <inheritdoc />
public override Task<WorkerState> StopAsync()
{
lock (_syncLock)
{
if (WorkerState == WorkerState.Stopped || WorkerState == WorkerState.Created)
{
WorkerState = WorkerState.Stopped;
return Task.FromResult(WorkerState);
}
var task = QueueStateChange(StateChangeRequest.Stop);
Interrupt();
return task;
}
}
/// <summary>
/// Schedules a new cycle for execution. The delay is given in
/// milliseconds. Passing a delay of 0 means a new cycle should be executed
/// immediately.
/// </summary>
/// <param name="delay">The delay.</param>
protected void ScheduleCycle(int delay)
{
lock (_syncLock)
{
if (!_isTimerAlive) return;
_timer.Change(delay, Timeout.Infinite);
}
}
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
lock (_syncLock)
{
if (!_isTimerAlive) return;
_isTimerAlive = false;
_timer.Dispose();
}
}
/// <summary>
/// Cancels the current token and schedules a new cycle immediately.
/// </summary>
private void Interrupt()
{
lock (_syncLock)
{
if (WorkerState == WorkerState.Stopped)
return;
CycleCancellation.Cancel();
ScheduleCycle(0);
}
}
/// <summary>
/// Executes the worker cycle control logic.
/// This includes processing state change requests,
/// the execution of use cycle code,
/// and the scheduling of new cycles.
/// </summary>
private void ExecuteWorkerCycle()
{
CycleStopwatch.Restart();
lock (_syncLock)
{
if (IsDisposing || IsDisposed)
{
WorkerState = WorkerState.Stopped;
// Cancel any awaiters
try { StateChangedEvent.Set(); }
catch { /* Ignore */ }
return;
}
// Prevent running another instance of the cycle
if (CycleCompletedEvent.IsSet == false) return;
// Lock the cycle and capture relevant state valid for this cycle
CycleCompletedEvent.Reset();
}
var interruptToken = CycleCancellation.Token;
var initialWorkerState = WorkerState;
// Process the tasks that are awaiting
if (ProcessStateChangeRequests())
return;
try
{
if (initialWorkerState == WorkerState.Waiting &&
!interruptToken.IsCancellationRequested)
{
// Mark the state as Running
WorkerState = WorkerState.Running;
// Call the execution logic
ExecuteCycleLogic(interruptToken);
}
}
catch (Exception ex)
{
OnCycleException(ex);
}
finally
{
// Update the state
WorkerState = initialWorkerState == WorkerState.Paused
? WorkerState.Paused
: WorkerState.Waiting;
lock (_syncLock)
{
// Signal the cycle has been completed so new cycles can be executed
CycleCompletedEvent.Set();
// Schedule a new cycle
ScheduleCycle(!interruptToken.IsCancellationRequested
? ComputeCycleDelay(initialWorkerState)
: 0);
}
}
}
/// <summary>
/// Represents the callback that is executed when the <see cref="_timer"/> ticks.
/// </summary>
/// <param name="state">The state -- this contains the worker.</param>
private void ExecuteTimerCallback(object state) => ExecuteWorkerCycle();
/// <summary>
/// Queues a transition in worker state for processing. Returns a task that can be awaited
/// when the operation completes.
/// </summary>
/// <param name="request">The request.</param>
/// <returns>The awaitable task.</returns>
private Task<WorkerState> QueueStateChange(StateChangeRequest request)
{
lock (_syncLock)
{
if (StateChangeTask != null)
return StateChangeTask;
var waitingTask = new Task<WorkerState>(() =>
{
StateChangedEvent.Wait();
lock (_syncLock)
{
StateChangeTask = null;
return WorkerState;
}
});
StateChangeTask = waitingTask;
StateChangedEvent.Reset();
StateChangeRequests[request] = true;
waitingTask.Start();
CycleCancellation.Cancel();
return waitingTask;
}
}
/// <summary>
/// Processes the state change queue by checking pending events and scheduling
/// cycle execution accordingly. The <see cref="WorkerState"/> is also updated.
/// </summary>
/// <returns>Returns <c>true</c> if the execution should be terminated. <c>false</c> otherwise.</returns>
private bool ProcessStateChangeRequests()
{
lock (_syncLock)
{
var currentState = WorkerState;
var hasRequest = false;
var schedule = 0;
// Update the state according to request priority
if (StateChangeRequests[StateChangeRequest.Stop] || IsDisposing || IsDisposed)
{
hasRequest = true;
WorkerState = WorkerState.Stopped;
schedule = StateChangeRequests[StateChangeRequest.Stop] ? Timeout.Infinite : 0;
}
else if (StateChangeRequests[StateChangeRequest.Pause])
{
hasRequest = true;
WorkerState = WorkerState.Paused;
schedule = Timeout.Infinite;
}
else if (StateChangeRequests[StateChangeRequest.Start] || StateChangeRequests[StateChangeRequest.Resume])
{
hasRequest = true;
WorkerState = WorkerState.Waiting;
}
// Signals all state changes to continue
// as a command has been handled.
if (hasRequest)
{
ClearStateChangeRequests(schedule, currentState, WorkerState);
}
return hasRequest;
}
}
/// <summary>
/// Signals all state change requests to set.
/// </summary>
/// <param name="schedule">The cycle schedule.</param>
/// <param name="oldState">The previous worker state.</param>
/// <param name="newState">The new worker state.</param>
private void ClearStateChangeRequests(int schedule, WorkerState oldState, WorkerState newState)
{
lock (_syncLock)
{
// Mark all events as completed
StateChangeRequests[StateChangeRequest.Start] = false;
StateChangeRequests[StateChangeRequest.Pause] = false;
StateChangeRequests[StateChangeRequest.Resume] = false;
StateChangeRequests[StateChangeRequest.Stop] = false;
StateChangedEvent.Set();
CycleCompletedEvent.Set();
OnStateChangeProcessed(oldState, newState);
ScheduleCycle(schedule);
}
}
}
}
+240
View File
@@ -0,0 +1,240 @@
namespace Swan.Threading
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
/// <summary>
/// Provides base infrastructure for Timer and Thread workers.
/// </summary>
/// <seealso cref="IWorker" />
public abstract class WorkerBase : IWorker, IDisposable
{
// Since these are API property backers, we use interlocked to read from them
// to avoid deadlocked reads
private readonly object _syncLock = new object();
private readonly AtomicBoolean _isDisposed = new AtomicBoolean();
private readonly AtomicBoolean _isDisposing = new AtomicBoolean();
private readonly AtomicEnum<WorkerState> _workerState = new AtomicEnum<WorkerState>(WorkerState.Created);
private readonly AtomicTimeSpan _timeSpan;
/// <summary>
/// Initializes a new instance of the <see cref="WorkerBase"/> class.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="period">The execution interval.</param>
protected WorkerBase(string name, TimeSpan period)
{
Name = name;
_timeSpan = new AtomicTimeSpan(period);
StateChangeRequests = new Dictionary<StateChangeRequest, bool>(5)
{
[StateChangeRequest.Start] = false,
[StateChangeRequest.Pause] = false,
[StateChangeRequest.Resume] = false,
[StateChangeRequest.Stop] = false,
};
}
/// <summary>
/// Enumerates all the different state change requests.
/// </summary>
protected enum StateChangeRequest
{
/// <summary>
/// No state change request.
/// </summary>
None,
/// <summary>
/// Start state change request
/// </summary>
Start,
/// <summary>
/// Pause state change request
/// </summary>
Pause,
/// <summary>
/// Resume state change request
/// </summary>
Resume,
/// <summary>
/// Stop state change request
/// </summary>
Stop,
}
/// <inheritdoc />
public string Name { get; }
/// <inheritdoc />
public TimeSpan Period
{
get => _timeSpan.Value;
set => _timeSpan.Value = value;
}
/// <inheritdoc />
public WorkerState WorkerState
{
get => _workerState.Value;
protected set => _workerState.Value = value;
}
/// <inheritdoc />
public bool IsDisposed
{
get => _isDisposed.Value;
protected set => _isDisposed.Value = value;
}
/// <inheritdoc />
public bool IsDisposing
{
get => _isDisposing.Value;
protected set => _isDisposing.Value = value;
}
/// <summary>
/// Gets the default period of 15 milliseconds which is the default precision for timers.
/// </summary>
protected static TimeSpan DefaultPeriod { get; } = TimeSpan.FromMilliseconds(15);
/// <summary>
/// Gets a value indicating whether stop has been requested.
/// This is useful to prevent more requests from being issued.
/// </summary>
protected bool IsStopRequested => StateChangeRequests[StateChangeRequest.Stop];
/// <summary>
/// Gets the cycle stopwatch.
/// </summary>
protected Stopwatch CycleStopwatch { get; } = new Stopwatch();
/// <summary>
/// Gets the state change requests.
/// </summary>
protected Dictionary<StateChangeRequest, bool> StateChangeRequests { get; }
/// <summary>
/// Gets the cycle completed event.
/// </summary>
protected ManualResetEventSlim CycleCompletedEvent { get; } = new ManualResetEventSlim(true);
/// <summary>
/// Gets the state changed event.
/// </summary>
protected ManualResetEventSlim StateChangedEvent { get; } = new ManualResetEventSlim(true);
/// <summary>
/// Gets the cycle logic cancellation owner.
/// </summary>
protected CancellationTokenOwner CycleCancellation { get; } = new CancellationTokenOwner();
/// <summary>
/// Gets or sets the state change task.
/// </summary>
protected Task<WorkerState>? StateChangeTask { get; set; }
/// <inheritdoc />
public abstract Task<WorkerState> StartAsync();
/// <inheritdoc />
public abstract Task<WorkerState> PauseAsync();
/// <inheritdoc />
public abstract Task<WorkerState> ResumeAsync();
/// <inheritdoc />
public abstract Task<WorkerState> StopAsync();
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
protected virtual void Dispose(bool disposing)
{
lock (_syncLock)
{
if (IsDisposed || IsDisposing) return;
IsDisposing = true;
}
// This also ensures the state change queue gets cleared
StopAsync().Wait();
StateChangedEvent.Set();
CycleCompletedEvent.Set();
OnDisposing();
CycleStopwatch.Stop();
StateChangedEvent.Dispose();
CycleCompletedEvent.Dispose();
CycleCancellation.Dispose();
IsDisposed = true;
IsDisposing = false;
}
/// <summary>
/// Handles the cycle logic exceptions.
/// </summary>
/// <param name="ex">The exception that was thrown.</param>
protected abstract void OnCycleException(Exception ex);
/// <summary>
/// Represents the user defined logic to be executed on a single worker cycle.
/// Check the cancellation token continuously if you need responsive interrupts.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
protected abstract void ExecuteCycleLogic(CancellationToken cancellationToken);
/// <summary>
/// This method is called automatically when <see cref="Dispose()"/> is called.
/// Makes sure you release all resources within this call.
/// </summary>
protected abstract void OnDisposing();
/// <summary>
/// Called when a state change request is processed.
/// </summary>
/// <param name="previousState">The state before the change.</param>
/// <param name="newState">The new state.</param>
protected virtual void OnStateChangeProcessed(WorkerState previousState, WorkerState newState)
{
// placeholder
}
/// <summary>
/// Computes the cycle delay.
/// </summary>
/// <param name="initialWorkerState">Initial state of the worker.</param>
/// <returns>The number of milliseconds to delay for.</returns>
protected int ComputeCycleDelay(WorkerState initialWorkerState)
{
var elapsedMillis = CycleStopwatch.ElapsedMilliseconds;
var period = Period;
var periodMillis = period.TotalMilliseconds;
var delayMillis = periodMillis - elapsedMillis;
if (initialWorkerState == WorkerState.Paused || period == TimeSpan.MaxValue || delayMillis >= int.MaxValue)
return Timeout.Infinite;
return elapsedMillis >= periodMillis ? 0 : Convert.ToInt32(Math.Floor(delayMillis));
}
}
}
+151
View File
@@ -0,0 +1,151 @@
namespace Swan.Threading
{
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
/// <summary>
/// Represents a class that implements delay logic for thread workers.
/// </summary>
public static class WorkerDelayProvider
{
/// <summary>
/// Gets the default delay provider.
/// </summary>
public static IWorkerDelayProvider Default => TokenTimeout;
/// <summary>
/// Provides a delay implementation which simply waits on the task and cancels on
/// the cancellation token.
/// </summary>
public static IWorkerDelayProvider Token => new TokenCancellableDelay();
/// <summary>
/// Provides a delay implementation which waits on the task and cancels on both,
/// the cancellation token and a wanted delay timeout.
/// </summary>
public static IWorkerDelayProvider TokenTimeout => new TokenTimeoutCancellableDelay();
/// <summary>
/// Provides a delay implementation which uses short sleep intervals of 5ms.
/// </summary>
public static IWorkerDelayProvider TokenSleep => new TokenSleepDelay();
/// <summary>
/// Provides a delay implementation which uses short delay intervals of 5ms and
/// a wait on the delay task in the final loop.
/// </summary>
public static IWorkerDelayProvider SteppedToken => new SteppedTokenDelay();
private class TokenCancellableDelay : IWorkerDelayProvider
{
public void ExecuteCycleDelay(int wantedDelay, Task delayTask, CancellationToken token)
{
if (wantedDelay == 0 || wantedDelay < -1)
return;
// for wanted delays of less than 30ms it is not worth
// passing a timeout or a token as it only adds unnecessary
// overhead.
if (wantedDelay <= 30)
{
try { delayTask.Wait(token); }
catch { /* ignore */ }
return;
}
// only wait on the cancellation token
// or until the task completes normally
try { delayTask.Wait(token); }
catch { /* ignore */ }
}
}
private class TokenTimeoutCancellableDelay : IWorkerDelayProvider
{
public void ExecuteCycleDelay(int wantedDelay, Task delayTask, CancellationToken token)
{
if (wantedDelay == 0 || wantedDelay < -1)
return;
// for wanted delays of less than 30ms it is not worth
// passing a timeout or a token as it only adds unnecessary
// overhead.
if (wantedDelay <= 30)
{
try { delayTask.Wait(token); }
catch { /* ignore */ }
return;
}
try { delayTask.Wait(wantedDelay, token); }
catch { /* ignore */ }
}
}
private class TokenSleepDelay : IWorkerDelayProvider
{
private readonly Stopwatch _elapsedWait = new Stopwatch();
public void ExecuteCycleDelay(int wantedDelay, Task delayTask, CancellationToken token)
{
_elapsedWait.Restart();
if (wantedDelay == 0 || wantedDelay < -1)
return;
while (!token.IsCancellationRequested)
{
Thread.Sleep(5);
if (wantedDelay != Timeout.Infinite && _elapsedWait.ElapsedMilliseconds >= wantedDelay)
break;
}
}
}
private class SteppedTokenDelay : IWorkerDelayProvider
{
private const int StepMilliseconds = 15;
private readonly Stopwatch _elapsedWait = new Stopwatch();
public void ExecuteCycleDelay(int wantedDelay, Task delayTask, CancellationToken token)
{
_elapsedWait.Restart();
if (wantedDelay == 0 || wantedDelay < -1)
return;
if (wantedDelay == Timeout.Infinite)
{
try { delayTask.Wait(wantedDelay, token); }
catch { /* Ignore cancelled tasks */ }
return;
}
while (!token.IsCancellationRequested)
{
var remainingWaitTime = wantedDelay - Convert.ToInt32(_elapsedWait.ElapsedMilliseconds);
// Exit for no remaining wait time
if (remainingWaitTime <= 0)
break;
if (remainingWaitTime >= StepMilliseconds)
{
Task.Delay(StepMilliseconds, token).Wait(token);
}
else
{
try { delayTask.Wait(remainingWaitTime); }
catch { /* ignore cancellation of task exception */ }
}
if (_elapsedWait.ElapsedMilliseconds >= wantedDelay)
break;
}
}
}
}
}
+124
View File
@@ -0,0 +1,124 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
namespace Swan
{
/// <summary>
/// A base class for implementing models that fire notifications when their properties change.
/// This class is ideal for implementing MVVM driven UIs.
/// </summary>
/// <seealso cref="INotifyPropertyChanged" />
public abstract class ViewModelBase : INotifyPropertyChanged
{
private readonly ConcurrentDictionary<string, bool> _queuedNotifications = new ConcurrentDictionary<string, bool>();
private readonly bool _useDeferredNotifications;
/// <summary>
/// Initializes a new instance of the <see cref="ViewModelBase"/> class.
/// </summary>
/// <param name="useDeferredNotifications">Set to <c>true</c> to use deferred notifications in the background.</param>
protected ViewModelBase(bool useDeferredNotifications)
{
_useDeferredNotifications = useDeferredNotifications;
}
/// <summary>
/// Initializes a new instance of the <see cref="ViewModelBase"/> class.
/// </summary>
protected ViewModelBase()
: this(false)
{
// placeholder
}
/// <inheritdoc />
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>Checks if a property already matches a desired value. Sets the property and
/// notifies listeners only when necessary.</summary>
/// <typeparam name="T">Type of the property.</typeparam>
/// <param name="storage">Reference to a property with both getter and setter.</param>
/// <param name="value">Desired value for the property.</param>
/// <param name="propertyName">Name of the property used to notify listeners. This
/// value is optional and can be provided automatically when invoked from compilers that
/// support CallerMemberName.</param>
/// <param name="notifyAlso">An array of property names to notify in addition to notifying the changes on the current property name.</param>
/// <returns>True if the value was changed, false if the existing value matched the
/// desired value.</returns>
protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = "", string[] notifyAlso = null)
{
if (EqualityComparer<T>.Default.Equals(storage, value))
return false;
storage = value;
NotifyPropertyChanged(propertyName, notifyAlso);
return true;
}
/// <summary>
/// Notifies one or more properties changed.
/// </summary>
/// <param name="propertyNames">The property names.</param>
protected void NotifyPropertyChanged(params string[] propertyNames) => NotifyPropertyChanged(null, propertyNames);
/// <summary>
/// Notifies one or more properties changed.
/// </summary>
/// <param name="mainProperty">The main property.</param>
/// <param name="auxiliaryProperties">The auxiliary properties.</param>
private void NotifyPropertyChanged(string mainProperty, string[] auxiliaryProperties)
{
// Queue property notification
if (string.IsNullOrWhiteSpace(mainProperty) == false)
_queuedNotifications[mainProperty] = true;
// Set the state for notification properties
if (auxiliaryProperties != null)
{
foreach (var property in auxiliaryProperties)
{
if (string.IsNullOrWhiteSpace(property) == false)
_queuedNotifications[property] = true;
}
}
// Depending on operation mode, either fire the notifications in the background
// or fire them immediately
if (_useDeferredNotifications)
Task.Run(NotifyQueuedProperties);
else
NotifyQueuedProperties();
}
/// <summary>
/// Notifies the queued properties and resets the property name to a non-queued stated.
/// </summary>
private void NotifyQueuedProperties()
{
// get a snapshot of property names.
var propertyNames = _queuedNotifications.Keys.ToArray();
// Iterate through the properties
foreach (var property in propertyNames)
{
// don't notify if we don't have a change
if (!_queuedNotifications[property]) continue;
// notify and reset queued state to false
try { OnPropertyChanged(property); }
finally { _queuedNotifications[property] = false; }
}
}
/// <summary>
/// Called when a property changes its backing value.
/// </summary>
/// <param name="propertyName">Name of the property.</param>
private void OnPropertyChanged(string propertyName) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName ?? string.Empty));
}
}