Codingstyle nullable
This commit is contained in:
parent
aa9fcd4a36
commit
d0b26111dd
File diff suppressed because it is too large
Load Diff
@ -1,46 +1,34 @@
|
|||||||
namespace Swan.DependencyInjection
|
using System;
|
||||||
{
|
using System.Collections.Generic;
|
||||||
using System;
|
using System.Linq;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
namespace Swan.DependencyInjection {
|
||||||
|
/// <summary>
|
||||||
|
/// Generic Constraint Registration Exception.
|
||||||
|
/// </summary>
|
||||||
|
/// <seealso cref="Exception" />
|
||||||
|
public class DependencyContainerRegistrationException : Exception {
|
||||||
|
private const String ConvertErrorText = "Cannot convert current registration of {0} to {1}";
|
||||||
|
private const String RegisterErrorText = "Cannot register type {0} - abstract classes or interfaces are not valid implementation types for {1}.";
|
||||||
|
private const String ErrorText = "Duplicate implementation of type {0} found ({1}).";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Generic Constraint Registration Exception.
|
/// Initializes a new instance of the <see cref="DependencyContainerRegistrationException"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <seealso cref="Exception" />
|
/// <param name="registerType">Type of the register.</param>
|
||||||
public class DependencyContainerRegistrationException : Exception
|
/// <param name="types">The types.</param>
|
||||||
{
|
public DependencyContainerRegistrationException(Type registerType, IEnumerable<Type> types) : base(String.Format(ErrorText, registerType, GetTypesString(types))) {
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="DependencyContainerRegistrationException" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The type.</param>
|
||||||
|
/// <param name="method">The method.</param>
|
||||||
|
/// <param name="isTypeFactory">if set to <c>true</c> [is type factory].</param>
|
||||||
|
public DependencyContainerRegistrationException(Type type, String method, Boolean isTypeFactory = false) : base(isTypeFactory ? String.Format(RegisterErrorText, type.FullName, method) : String.Format(ConvertErrorText, type.FullName, method)) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String GetTypesString(IEnumerable<Type> types) => String.Join(",", types.Select(type => type.FullName));
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,31 +1,25 @@
|
|||||||
namespace Swan.DependencyInjection
|
using System;
|
||||||
{
|
|
||||||
using System;
|
namespace Swan.DependencyInjection {
|
||||||
|
/// <summary>
|
||||||
|
/// An exception for dependency resolutions.
|
||||||
|
/// </summary>
|
||||||
|
/// <seealso cref="System.Exception" />
|
||||||
|
[Serializable]
|
||||||
|
public class DependencyContainerResolutionException : Exception {
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="DependencyContainerResolutionException"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The type.</param>
|
||||||
|
public DependencyContainerResolutionException(Type type) : base($"Unable to resolve type: {type.FullName}") {
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// An exception for dependency resolutions.
|
/// Initializes a new instance of the <see cref="DependencyContainerResolutionException"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <seealso cref="System.Exception" />
|
/// <param name="type">The type.</param>
|
||||||
[Serializable]
|
/// <param name="innerException">The inner exception.</param>
|
||||||
public class DependencyContainerResolutionException : Exception
|
public DependencyContainerResolutionException(Type type, Exception innerException) : base($"Unable to resolve type: {type.FullName}", innerException) {
|
||||||
{
|
|
||||||
/// <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)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,114 +1,106 @@
|
|||||||
namespace Swan.DependencyInjection
|
using System.Collections.Generic;
|
||||||
{
|
|
||||||
using System.Collections.Generic;
|
namespace Swan.DependencyInjection {
|
||||||
|
/// <summary>
|
||||||
|
/// Resolution settings.
|
||||||
|
/// </summary>
|
||||||
|
public class DependencyContainerResolveOptions {
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the default options (attempt resolution of unregistered types, fail on named resolution if name not found).
|
||||||
|
/// </summary>
|
||||||
|
public static DependencyContainerResolveOptions Default { get; } = new DependencyContainerResolveOptions();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolution settings.
|
/// Gets or sets the unregistered resolution action.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DependencyContainerResolveOptions
|
/// <value>
|
||||||
{
|
/// The unregistered resolution action.
|
||||||
/// <summary>
|
/// </value>
|
||||||
/// Gets the default options (attempt resolution of unregistered types, fail on named resolution if name not found).
|
public DependencyContainerUnregisteredResolutionAction UnregisteredResolutionAction { get; set; } = DependencyContainerUnregisteredResolutionAction.AttemptResolve;
|
||||||
/// </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>
|
/// <summary>
|
||||||
/// Defines Resolution actions.
|
/// Gets or sets the named resolution failure action.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum DependencyContainerUnregisteredResolutionAction
|
/// <value>
|
||||||
{
|
/// The named resolution failure action.
|
||||||
/// <summary>
|
/// </value>
|
||||||
/// Attempt to resolve type, even if the type isn't registered.
|
public DependencyContainerNamedResolutionFailureAction NamedResolutionFailureAction { get; set; } = DependencyContainerNamedResolutionFailureAction.Fail;
|
||||||
///
|
|
||||||
/// 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>
|
/// <summary>
|
||||||
/// Enumerates failure actions.
|
/// Gets the constructor parameters.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum DependencyContainerNamedResolutionFailureAction
|
/// <value>
|
||||||
{
|
/// The constructor parameters.
|
||||||
/// <summary>
|
/// </value>
|
||||||
/// The attempt unnamed resolution
|
public Dictionary<System.String, System.Object> ConstructorParameters { get; } = new Dictionary<System.String, System.Object>();
|
||||||
/// </summary>
|
|
||||||
AttemptUnnamedResolution,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The fail
|
|
||||||
/// </summary>
|
|
||||||
Fail,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Enumerates duplicate definition actions.
|
/// Clones this instance.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum DependencyContainerDuplicateImplementationAction
|
/// <returns></returns>
|
||||||
{
|
public DependencyContainerResolveOptions Clone() => new DependencyContainerResolveOptions {
|
||||||
/// <summary>
|
NamedResolutionFailureAction = NamedResolutionFailureAction,
|
||||||
/// The register single
|
UnregisteredResolutionAction = UnregisteredResolutionAction,
|
||||||
/// </summary>
|
};
|
||||||
RegisterSingle,
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The register multiple
|
/// Defines Resolution actions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
RegisterMultiple,
|
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>
|
/// <summary>
|
||||||
/// The fail
|
/// Fail resolution if type not explicitly registered
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Fail,
|
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,
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,22 +1,18 @@
|
|||||||
namespace Swan.DependencyInjection
|
using System;
|
||||||
{
|
|
||||||
using System;
|
namespace Swan.DependencyInjection {
|
||||||
|
/// <summary>
|
||||||
|
/// Weak Reference Exception.
|
||||||
|
/// </summary>
|
||||||
|
/// <seealso cref="System.Exception" />
|
||||||
|
public class DependencyContainerWeakReferenceException : Exception {
|
||||||
|
private const String ErrorText = "Unable to instantiate {0} - referenced object has been reclaimed";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Weak Reference Exception.
|
/// Initializes a new instance of the <see cref="DependencyContainerWeakReferenceException"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <seealso cref="System.Exception" />
|
/// <param name="type">The type.</param>
|
||||||
public class DependencyContainerWeakReferenceException : Exception
|
public DependencyContainerWeakReferenceException(Type type) : base(String.Format(ErrorText, type.FullName)) {
|
||||||
{
|
|
||||||
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))
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,423 +1,352 @@
|
|||||||
namespace Swan.DependencyInjection
|
using System;
|
||||||
{
|
using System.Collections.Generic;
|
||||||
using System;
|
using System.Reflection;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Reflection;
|
namespace Swan.DependencyInjection {
|
||||||
|
/// <summary>
|
||||||
|
/// Represents an abstract class for Object Factory.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class ObjectFactoryBase {
|
||||||
|
/// <summary>
|
||||||
|
/// Whether to assume this factory successfully constructs its objects
|
||||||
|
///
|
||||||
|
/// Generally set to true for delegate style factories as CanResolve cannot delve
|
||||||
|
/// into the delegates they contain.
|
||||||
|
/// </summary>
|
||||||
|
public virtual Boolean AssumeConstruction => false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents an abstract class for Object Factory.
|
/// The type the factory instantiates.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class ObjectFactoryBase
|
public abstract Type CreatesType {
|
||||||
{
|
get;
|
||||||
/// <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>
|
/// <summary>
|
||||||
/// Stores an particular instance to return for a type.
|
/// Constructor to use, if specified.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal class InstanceFactory : ObjectFactoryBase, IDisposable
|
public ConstructorInfo Constructor {
|
||||||
{
|
get; private set;
|
||||||
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>
|
/// <summary>
|
||||||
/// Stores the instance with a weak reference.
|
/// Gets the singleton variant.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal class WeakInstanceFactory : ObjectFactoryBase, IDisposable
|
/// <value>
|
||||||
{
|
/// The singleton variant.
|
||||||
private readonly Type _registerType;
|
/// </value>
|
||||||
private readonly Type _registerImplementation;
|
/// <exception cref="DependencyContainerRegistrationException">singleton.</exception>
|
||||||
private readonly WeakReference _instance;
|
public virtual ObjectFactoryBase SingletonVariant => throw new DependencyContainerRegistrationException(this.GetType(), "singleton");
|
||||||
|
|
||||||
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>
|
/// <summary>
|
||||||
/// A factory that lazy instantiates a type and always returns the same instance.
|
/// Gets the multi instance variant.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal class SingletonFactory : ObjectFactoryBase, IDisposable
|
/// <value>
|
||||||
{
|
/// The multi instance variant.
|
||||||
private readonly Type _registerType;
|
/// </value>
|
||||||
private readonly Type _registerImplementation;
|
/// <exception cref="DependencyContainerRegistrationException">multi-instance.</exception>
|
||||||
private readonly object _singletonLock = new object();
|
public virtual ObjectFactoryBase MultiInstanceVariant => throw new DependencyContainerRegistrationException(this.GetType(), "multi-instance");
|
||||||
private object _current;
|
|
||||||
|
|
||||||
public SingletonFactory(Type registerType, Type registerImplementation)
|
/// <summary>
|
||||||
{
|
/// Gets the strong reference variant.
|
||||||
if (registerImplementation.IsAbstract || registerImplementation.IsInterface)
|
/// </summary>
|
||||||
{
|
/// <value>
|
||||||
throw new DependencyContainerRegistrationException(registerImplementation, nameof(SingletonFactory), true);
|
/// The strong reference variant.
|
||||||
}
|
/// </value>
|
||||||
|
/// <exception cref="DependencyContainerRegistrationException">strong reference.</exception>
|
||||||
|
public virtual ObjectFactoryBase StrongReferenceVariant => throw new DependencyContainerRegistrationException(this.GetType(), "strong reference");
|
||||||
|
|
||||||
if (!DependencyContainer.IsValidAssignment(registerType, registerImplementation))
|
/// <summary>
|
||||||
{
|
/// Gets the weak reference variant.
|
||||||
throw new DependencyContainerRegistrationException(registerImplementation, nameof(SingletonFactory), true);
|
/// </summary>
|
||||||
}
|
/// <value>
|
||||||
|
/// The weak reference variant.
|
||||||
|
/// </value>
|
||||||
|
/// <exception cref="DependencyContainerRegistrationException">weak reference.</exception>
|
||||||
|
public virtual ObjectFactoryBase WeakReferenceVariant => throw new DependencyContainerRegistrationException(this.GetType(), "weak reference");
|
||||||
|
|
||||||
_registerType = registerType;
|
/// <summary>
|
||||||
_registerImplementation = registerImplementation;
|
/// 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);
|
||||||
|
|
||||||
public override Type CreatesType => _registerImplementation;
|
/// <summary>
|
||||||
|
/// Gets the factory for child container.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The type.</param>
|
||||||
|
/// <param name="parent">The parent.</param>
|
||||||
|
/// <param name="child">The child.</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public virtual ObjectFactoryBase GetFactoryForChildContainer(Type type, DependencyContainer parent, DependencyContainer child) => this;
|
||||||
|
}
|
||||||
|
|
||||||
public override ObjectFactoryBase SingletonVariant => 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 override ObjectFactoryBase MultiInstanceVariant =>
|
public MultiInstanceFactory(Type registerType, Type registerImplementation) {
|
||||||
new MultiInstanceFactory(_registerType, _registerImplementation);
|
if(registerImplementation.IsAbstract || registerImplementation.IsInterface) {
|
||||||
|
throw new DependencyContainerRegistrationException(registerImplementation, "MultiInstanceFactory", true);
|
||||||
|
}
|
||||||
|
|
||||||
public override object GetObject(
|
if(!DependencyContainer.IsValidAssignment(registerType, registerImplementation)) {
|
||||||
Type requestedType,
|
throw new DependencyContainerRegistrationException(registerImplementation, "MultiInstanceFactory", true);
|
||||||
DependencyContainer container,
|
}
|
||||||
DependencyContainerResolveOptions options)
|
|
||||||
{
|
|
||||||
if (options.ConstructorParameters.Count != 0)
|
|
||||||
throw new ArgumentException("Cannot specify parameters for singleton types");
|
|
||||||
|
|
||||||
lock (_singletonLock)
|
this._registerType = registerType;
|
||||||
{
|
this._registerImplementation = registerImplementation;
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override Type CreatesType => this._registerImplementation;
|
||||||
|
|
||||||
|
public override ObjectFactoryBase SingletonVariant =>
|
||||||
|
new SingletonFactory(this._registerType, this._registerImplementation);
|
||||||
|
|
||||||
|
public override ObjectFactoryBase MultiInstanceVariant => this;
|
||||||
|
|
||||||
|
public override Object GetObject(Type requestedType, DependencyContainer container, DependencyContainerResolveOptions options) {
|
||||||
|
try {
|
||||||
|
return container.RegisteredTypes.ConstructType(this._registerImplementation, this.Constructor, options);
|
||||||
|
} catch(DependencyContainerResolutionException ex) {
|
||||||
|
throw new DependencyContainerResolutionException(this._registerType, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
/// <summary>
|
||||||
|
/// IObjectFactory that invokes a specified delegate to construct the object.
|
||||||
|
/// </summary>
|
||||||
|
internal class DelegateFactory : ObjectFactoryBase {
|
||||||
|
private readonly Type _registerType;
|
||||||
|
|
||||||
|
private readonly Func<DependencyContainer, Dictionary<String, Object>, Object> _factory;
|
||||||
|
|
||||||
|
public DelegateFactory(
|
||||||
|
Type registerType,
|
||||||
|
Func<DependencyContainer, Dictionary<String, Object>, Object> factory) {
|
||||||
|
this._factory = factory ?? throw new ArgumentNullException(nameof(factory));
|
||||||
|
|
||||||
|
this._registerType = registerType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Boolean AssumeConstruction => true;
|
||||||
|
|
||||||
|
public override Type CreatesType => this._registerType;
|
||||||
|
|
||||||
|
public override ObjectFactoryBase WeakReferenceVariant => new WeakDelegateFactory(this._registerType, this._factory);
|
||||||
|
|
||||||
|
public override ObjectFactoryBase StrongReferenceVariant => this;
|
||||||
|
|
||||||
|
public override Object GetObject(Type requestedType, DependencyContainer container, DependencyContainerResolveOptions options) {
|
||||||
|
try {
|
||||||
|
return this._factory.Invoke(container, options.ConstructorParameters);
|
||||||
|
} catch(Exception ex) {
|
||||||
|
throw new DependencyContainerResolutionException(this._registerType, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
/// <summary>
|
||||||
|
/// IObjectFactory that invokes a specified delegate to construct the object
|
||||||
|
/// Holds the delegate using a weak reference.
|
||||||
|
/// </summary>
|
||||||
|
internal class WeakDelegateFactory : ObjectFactoryBase {
|
||||||
|
private readonly Type _registerType;
|
||||||
|
|
||||||
|
private readonly WeakReference _factory;
|
||||||
|
|
||||||
|
public WeakDelegateFactory(Type registerType, Func<DependencyContainer, Dictionary<String, Object>, Object> factory) {
|
||||||
|
if(factory == null) {
|
||||||
|
throw new ArgumentNullException(nameof(factory));
|
||||||
|
}
|
||||||
|
|
||||||
|
this._factory = new WeakReference(factory);
|
||||||
|
|
||||||
|
this._registerType = registerType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Boolean AssumeConstruction => true;
|
||||||
|
|
||||||
|
public override Type CreatesType => this._registerType;
|
||||||
|
|
||||||
|
public override ObjectFactoryBase StrongReferenceVariant {
|
||||||
|
get {
|
||||||
|
if(!(this._factory.Target is Func<DependencyContainer, Dictionary<global::System.String, global::System.Object>, global::System.Object> factory)) {
|
||||||
|
throw new DependencyContainerWeakReferenceException(this._registerType);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DelegateFactory(this._registerType, factory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override ObjectFactoryBase WeakReferenceVariant => this;
|
||||||
|
|
||||||
|
public override Object GetObject(Type requestedType, DependencyContainer container, DependencyContainerResolveOptions options) {
|
||||||
|
if(!(this._factory.Target is Func<DependencyContainer, Dictionary<global::System.String, global::System.Object>, global::System.Object> factory)) {
|
||||||
|
throw new DependencyContainerWeakReferenceException(this._registerType);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return factory.Invoke(container, options.ConstructorParameters);
|
||||||
|
} catch(Exception ex) {
|
||||||
|
throw new DependencyContainerResolutionException(this._registerType, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stores an particular instance to return for a type.
|
||||||
|
/// </summary>
|
||||||
|
internal class InstanceFactory : ObjectFactoryBase, IDisposable {
|
||||||
|
private readonly Type _registerType;
|
||||||
|
private readonly Type _registerImplementation;
|
||||||
|
private readonly Object _instance;
|
||||||
|
|
||||||
|
public InstanceFactory(Type registerType, Type registerImplementation, Object instance) {
|
||||||
|
if(!DependencyContainer.IsValidAssignment(registerType, registerImplementation)) {
|
||||||
|
throw new DependencyContainerRegistrationException(registerImplementation, "InstanceFactory", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._registerType = registerType;
|
||||||
|
this._registerImplementation = registerImplementation;
|
||||||
|
this._instance = instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Boolean AssumeConstruction => true;
|
||||||
|
|
||||||
|
public override Type CreatesType => this._registerImplementation;
|
||||||
|
|
||||||
|
public override ObjectFactoryBase MultiInstanceVariant => new MultiInstanceFactory(this._registerType, this._registerImplementation);
|
||||||
|
|
||||||
|
public override ObjectFactoryBase WeakReferenceVariant => new WeakInstanceFactory(this._registerType, this._registerImplementation, this._instance);
|
||||||
|
|
||||||
|
public override ObjectFactoryBase StrongReferenceVariant => this;
|
||||||
|
|
||||||
|
public override Object GetObject(Type requestedType, DependencyContainer container, DependencyContainerResolveOptions options) => this._instance;
|
||||||
|
|
||||||
|
public void Dispose() {
|
||||||
|
IDisposable disposable = this._instance as IDisposable;
|
||||||
|
|
||||||
|
disposable?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stores the instance with a weak reference.
|
||||||
|
/// </summary>
|
||||||
|
internal class WeakInstanceFactory : ObjectFactoryBase, IDisposable {
|
||||||
|
private readonly Type _registerType;
|
||||||
|
private readonly Type _registerImplementation;
|
||||||
|
private readonly WeakReference _instance;
|
||||||
|
|
||||||
|
public WeakInstanceFactory(Type registerType, Type registerImplementation, Object instance) {
|
||||||
|
if(!DependencyContainer.IsValidAssignment(registerType, registerImplementation)) {
|
||||||
|
throw new DependencyContainerRegistrationException(registerImplementation, "WeakInstanceFactory", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._registerType = registerType;
|
||||||
|
this._registerImplementation = registerImplementation;
|
||||||
|
this._instance = new WeakReference(instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Type CreatesType => this._registerImplementation;
|
||||||
|
|
||||||
|
public override ObjectFactoryBase MultiInstanceVariant => new MultiInstanceFactory(this._registerType, this._registerImplementation);
|
||||||
|
|
||||||
|
public override ObjectFactoryBase WeakReferenceVariant => this;
|
||||||
|
|
||||||
|
public override ObjectFactoryBase StrongReferenceVariant {
|
||||||
|
get {
|
||||||
|
Object instance = this._instance.Target;
|
||||||
|
|
||||||
|
if(instance == null) {
|
||||||
|
throw new DependencyContainerWeakReferenceException(this._registerType);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new InstanceFactory(this._registerType, this._registerImplementation, instance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Object GetObject(Type requestedType, DependencyContainer container, DependencyContainerResolveOptions options) {
|
||||||
|
Object instance = this._instance.Target;
|
||||||
|
|
||||||
|
if(instance == null) {
|
||||||
|
throw new DependencyContainerWeakReferenceException(this._registerType);
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() => (this._instance.Target as IDisposable)?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A factory that lazy instantiates a type and always returns the same instance.
|
||||||
|
/// </summary>
|
||||||
|
internal class SingletonFactory : ObjectFactoryBase, IDisposable {
|
||||||
|
private readonly Type _registerType;
|
||||||
|
private readonly Type _registerImplementation;
|
||||||
|
private readonly Object _singletonLock = new Object();
|
||||||
|
private Object _current;
|
||||||
|
|
||||||
|
public SingletonFactory(Type registerType, Type registerImplementation) {
|
||||||
|
if(registerImplementation.IsAbstract || registerImplementation.IsInterface) {
|
||||||
|
throw new DependencyContainerRegistrationException(registerImplementation, nameof(SingletonFactory), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!DependencyContainer.IsValidAssignment(registerType, registerImplementation)) {
|
||||||
|
throw new DependencyContainerRegistrationException(registerImplementation, nameof(SingletonFactory), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._registerType = registerType;
|
||||||
|
this._registerImplementation = registerImplementation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Type CreatesType => this._registerImplementation;
|
||||||
|
|
||||||
|
public override ObjectFactoryBase SingletonVariant => this;
|
||||||
|
|
||||||
|
public override ObjectFactoryBase MultiInstanceVariant =>
|
||||||
|
new MultiInstanceFactory(this._registerType, this._registerImplementation);
|
||||||
|
|
||||||
|
public override Object GetObject(
|
||||||
|
Type requestedType,
|
||||||
|
DependencyContainer container,
|
||||||
|
DependencyContainerResolveOptions options) {
|
||||||
|
if(options.ConstructorParameters.Count != 0) {
|
||||||
|
throw new ArgumentException("Cannot specify parameters for singleton types");
|
||||||
|
}
|
||||||
|
|
||||||
|
lock(this._singletonLock) {
|
||||||
|
if(this._current == null) {
|
||||||
|
this._current = container.RegisteredTypes.ConstructType(this._registerImplementation, this.Constructor, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._current;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override ObjectFactoryBase GetFactoryForChildContainer(
|
||||||
|
Type type,
|
||||||
|
DependencyContainer parent,
|
||||||
|
DependencyContainer child) {
|
||||||
|
// We make sure that the singleton is constructed before the child container takes the factory.
|
||||||
|
// Otherwise the results would vary depending on whether or not the parent container had resolved
|
||||||
|
// the type before the child container does.
|
||||||
|
_ = this.GetObject(type, parent, DependencyContainerResolveOptions.Default);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() => (this._current as IDisposable)?.Dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,131 +1,119 @@
|
|||||||
namespace Swan.DependencyInjection
|
using System;
|
||||||
{
|
using System.Collections.Generic;
|
||||||
using System;
|
using System.Linq;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
namespace Swan.DependencyInjection {
|
||||||
|
/// <summary>
|
||||||
|
/// Registration options for "fluent" API.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class RegisterOptions {
|
||||||
|
private readonly TypesConcurrentDictionary _registeredTypes;
|
||||||
|
private readonly DependencyContainer.TypeRegistration _registration;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Registration options for "fluent" API.
|
/// Initializes a new instance of the <see cref="RegisterOptions" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class RegisterOptions
|
/// <param name="registeredTypes">The registered types.</param>
|
||||||
{
|
/// <param name="registration">The registration.</param>
|
||||||
private readonly TypesConcurrentDictionary _registeredTypes;
|
public RegisterOptions(TypesConcurrentDictionary registeredTypes, DependencyContainer.TypeRegistration registration) {
|
||||||
private readonly DependencyContainer.TypeRegistration _registration;
|
this._registeredTypes = registeredTypes;
|
||||||
|
this._registration = 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>
|
/// <summary>
|
||||||
/// Registration options for "fluent" API when registering multiple implementations.
|
/// Make registration a singleton (single instance) if possible.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class MultiRegisterOptions
|
/// <returns>A registration options for fluent API.</returns>
|
||||||
{
|
/// <exception cref="DependencyContainerRegistrationException">Generic constraint registration exception.</exception>
|
||||||
private IEnumerable<RegisterOptions> _registerOptions;
|
public RegisterOptions AsSingleton() {
|
||||||
|
ObjectFactoryBase currentFactory = this._registeredTypes.GetCurrentFactory(this._registration);
|
||||||
|
|
||||||
/// <summary>
|
if(currentFactory == null) {
|
||||||
/// Initializes a new instance of the <see cref="MultiRegisterOptions"/> class.
|
throw new DependencyContainerRegistrationException(this._registration.Type, "singleton");
|
||||||
/// </summary>
|
}
|
||||||
/// <param name="registerOptions">The register options.</param>
|
|
||||||
public MultiRegisterOptions(IEnumerable<RegisterOptions> registerOptions)
|
|
||||||
{
|
|
||||||
_registerOptions = registerOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
return this._registeredTypes.AddUpdateRegistration(this._registration, currentFactory.SingletonVariant);
|
||||||
/// 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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Make registration multi-instance if possible.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A registration options for fluent API.</returns>
|
||||||
|
/// <exception cref="DependencyContainerRegistrationException">Generic constraint registration exception.</exception>
|
||||||
|
public RegisterOptions AsMultiInstance() {
|
||||||
|
ObjectFactoryBase currentFactory = this._registeredTypes.GetCurrentFactory(this._registration);
|
||||||
|
|
||||||
|
if(currentFactory == null) {
|
||||||
|
throw new DependencyContainerRegistrationException(this._registration.Type, "multi-instance");
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._registeredTypes.AddUpdateRegistration(this._registration, currentFactory.MultiInstanceVariant);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Make registration hold a weak reference if possible.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A registration options for fluent API.</returns>
|
||||||
|
/// <exception cref="DependencyContainerRegistrationException">Generic constraint registration exception.</exception>
|
||||||
|
public RegisterOptions WithWeakReference() {
|
||||||
|
ObjectFactoryBase currentFactory = this._registeredTypes.GetCurrentFactory(this._registration);
|
||||||
|
|
||||||
|
if(currentFactory == null) {
|
||||||
|
throw new DependencyContainerRegistrationException(this._registration.Type, "weak reference");
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._registeredTypes.AddUpdateRegistration(this._registration, currentFactory.WeakReferenceVariant);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Make registration hold a strong reference if possible.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A registration options for fluent API.</returns>
|
||||||
|
/// <exception cref="DependencyContainerRegistrationException">Generic constraint registration exception.</exception>
|
||||||
|
public RegisterOptions WithStrongReference() {
|
||||||
|
ObjectFactoryBase currentFactory = this._registeredTypes.GetCurrentFactory(this._registration);
|
||||||
|
|
||||||
|
if(currentFactory == null) {
|
||||||
|
throw new DependencyContainerRegistrationException(this._registration.Type, "strong reference");
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._registeredTypes.AddUpdateRegistration(this._registration, currentFactory.StrongReferenceVariant);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registration options for "fluent" API when registering multiple implementations.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MultiRegisterOptions {
|
||||||
|
private IEnumerable<RegisterOptions> _registerOptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="MultiRegisterOptions"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="registerOptions">The register options.</param>
|
||||||
|
public MultiRegisterOptions(IEnumerable<RegisterOptions> registerOptions) => this._registerOptions = registerOptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Make registration a singleton (single instance) if possible.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A registration multi-instance for fluent API.</returns>
|
||||||
|
/// <exception cref="DependencyContainerRegistrationException">Generic Constraint Registration Exception.</exception>
|
||||||
|
public MultiRegisterOptions AsSingleton() {
|
||||||
|
this._registerOptions = this.ExecuteOnAllRegisterOptions(ro => ro.AsSingleton());
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Make registration multi-instance if possible.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A registration multi-instance for fluent API.</returns>
|
||||||
|
/// <exception cref="DependencyContainerRegistrationException">Generic Constraint Registration Exception.</exception>
|
||||||
|
public MultiRegisterOptions AsMultiInstance() {
|
||||||
|
this._registerOptions = this.ExecuteOnAllRegisterOptions(ro => ro.AsMultiInstance());
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<RegisterOptions> ExecuteOnAllRegisterOptions(
|
||||||
|
Func<RegisterOptions, RegisterOptions> action) => this._registerOptions.Select(action).ToList();
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,67 +1,61 @@
|
|||||||
namespace Swan.DependencyInjection
|
using System;
|
||||||
{
|
|
||||||
using System;
|
|
||||||
|
|
||||||
public partial class DependencyContainer
|
namespace Swan.DependencyInjection {
|
||||||
{
|
public partial class DependencyContainer {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a Type Registration within the IoC Container.
|
/// Represents a Type Registration within the IoC Container.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class TypeRegistration
|
public sealed class TypeRegistration {
|
||||||
{
|
private readonly Int32 _hashCode;
|
||||||
private readonly int _hashCode;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="TypeRegistration"/> class.
|
/// Initializes a new instance of the <see cref="TypeRegistration"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="type">The type.</param>
|
/// <param name="type">The type.</param>
|
||||||
/// <param name="name">The name.</param>
|
/// <param name="name">The name.</param>
|
||||||
public TypeRegistration(Type type, string name = null)
|
public TypeRegistration(Type type, String name = null) {
|
||||||
{
|
this.Type = type;
|
||||||
Type = type;
|
this.Name = name ?? String.Empty;
|
||||||
Name = name ?? string.Empty;
|
|
||||||
|
|
||||||
_hashCode = string.Concat(Type.FullName, "|", Name).GetHashCode();
|
this._hashCode = String.Concat(this.Type.FullName, "|", this.Name).GetHashCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the type.
|
/// Gets the type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
/// <value>
|
||||||
/// The type.
|
/// The type.
|
||||||
/// </value>
|
/// </value>
|
||||||
public Type Type { get; }
|
public Type Type {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the name.
|
/// Gets the name.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
/// <value>
|
||||||
/// The name.
|
/// The name.
|
||||||
/// </value>
|
/// </value>
|
||||||
public string Name { get; }
|
public String Name {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Determines whether the specified <see cref="System.Object" />, is equal to this instance.
|
/// Determines whether the specified <see cref="System.Object" />, is equal to this instance.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="obj">The <see cref="System.Object" /> to compare with this instance.</param>
|
/// <param name="obj">The <see cref="System.Object" /> to compare with this instance.</param>
|
||||||
/// <returns>
|
/// <returns>
|
||||||
/// <c>true</c> if the specified <see cref="System.Object" /> is equal to this instance; otherwise, <c>false</c>.
|
/// <c>true</c> if the specified <see cref="System.Object" /> is equal to this instance; otherwise, <c>false</c>.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
public override bool Equals(object obj)
|
public override Boolean Equals(Object obj) => !(obj is TypeRegistration typeRegistration) || typeRegistration.Type != this.Type ? false : String.Compare(this.Name, typeRegistration.Name, StringComparison.Ordinal) == 0;
|
||||||
{
|
|
||||||
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>
|
||||||
/// <summary>
|
/// <returns>
|
||||||
/// Returns a hash code for this instance.
|
/// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.
|
||||||
/// </summary>
|
/// </returns>
|
||||||
/// <returns>
|
public override Int32 GetHashCode() => this._hashCode;
|
||||||
/// 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,351 +1,265 @@
|
|||||||
namespace Swan.DependencyInjection
|
#nullable enable
|
||||||
{
|
using System;
|
||||||
using System;
|
using System.Linq.Expressions;
|
||||||
using System.Linq.Expressions;
|
using System.Reflection;
|
||||||
using System.Reflection;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Generic;
|
using System.Linq;
|
||||||
using System.Linq;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Concurrent;
|
|
||||||
|
namespace Swan.DependencyInjection {
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a Concurrent Dictionary for TypeRegistration.
|
||||||
|
/// </summary>
|
||||||
|
public class TypesConcurrentDictionary : ConcurrentDictionary<DependencyContainer.TypeRegistration, ObjectFactoryBase> {
|
||||||
|
private static readonly ConcurrentDictionary<ConstructorInfo, ObjectConstructor> ObjectConstructorCache = new ConcurrentDictionary<ConstructorInfo, ObjectConstructor>();
|
||||||
|
|
||||||
|
private readonly DependencyContainer _dependencyContainer;
|
||||||
|
|
||||||
|
internal TypesConcurrentDictionary(DependencyContainer dependencyContainer) => this._dependencyContainer = dependencyContainer;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a Concurrent Dictionary for TypeRegistration.
|
/// Represents a delegate to build an object with the parameters.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class TypesConcurrentDictionary : ConcurrentDictionary<DependencyContainer.TypeRegistration, ObjectFactoryBase>
|
/// <param name="parameters">The parameters.</param>
|
||||||
{
|
/// <returns>The built object.</returns>
|
||||||
private static readonly ConcurrentDictionary<ConstructorInfo, ObjectConstructor> ObjectConstructorCache =
|
public delegate Object ObjectConstructor(params Object?[] parameters);
|
||||||
new ConcurrentDictionary<ConstructorInfo, ObjectConstructor>();
|
|
||||||
|
|
||||||
private readonly DependencyContainer _dependencyContainer;
|
internal IEnumerable<Object> Resolve(Type resolveType, Boolean includeUnnamed) {
|
||||||
|
IEnumerable<DependencyContainer.TypeRegistration> registrations = this.Keys.Where(tr => tr.Type == resolveType).Concat(this.GetParentRegistrationsForType(resolveType)).Distinct();
|
||||||
|
|
||||||
internal TypesConcurrentDictionary(DependencyContainer dependencyContainer)
|
if(!includeUnnamed) {
|
||||||
{
|
registrations = registrations.Where(tr => !String.IsNullOrEmpty(tr.Name));
|
||||||
_dependencyContainer = dependencyContainer;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
return registrations.Select(registration => this.ResolveInternal(registration, DependencyContainerResolveOptions.Default));
|
||||||
/// 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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal ObjectFactoryBase GetCurrentFactory(DependencyContainer.TypeRegistration registration) {
|
||||||
|
_ = this.TryGetValue(registration, out ObjectFactoryBase? current);
|
||||||
|
|
||||||
|
return current!;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal RegisterOptions Register(Type registerType, String name, ObjectFactoryBase factory) => this.AddUpdateRegistration(new DependencyContainer.TypeRegistration(registerType, name), factory);
|
||||||
|
|
||||||
|
internal RegisterOptions AddUpdateRegistration(DependencyContainer.TypeRegistration typeRegistration, ObjectFactoryBase factory) {
|
||||||
|
this[typeRegistration] = factory;
|
||||||
|
|
||||||
|
return new RegisterOptions(this, typeRegistration);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal Boolean RemoveRegistration(DependencyContainer.TypeRegistration typeRegistration) => this.TryRemove(typeRegistration, out _);
|
||||||
|
|
||||||
|
internal Object ResolveInternal(DependencyContainer.TypeRegistration registration, DependencyContainerResolveOptions? options = null) {
|
||||||
|
if(options == null) {
|
||||||
|
options = DependencyContainerResolveOptions.Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt container resolution
|
||||||
|
if(this.TryGetValue(registration, out ObjectFactoryBase? factory)) {
|
||||||
|
try {
|
||||||
|
return factory.GetObject(registration.Type, this._dependencyContainer, options);
|
||||||
|
} catch(DependencyContainerResolutionException) {
|
||||||
|
throw;
|
||||||
|
} catch(Exception ex) {
|
||||||
|
throw new DependencyContainerResolutionException(registration.Type, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to get a factory from parent if we can
|
||||||
|
ObjectFactoryBase? bubbledObjectFactory = this.GetParentObjectFactory(registration);
|
||||||
|
if(bubbledObjectFactory != null) {
|
||||||
|
try {
|
||||||
|
return bubbledObjectFactory.GetObject(registration.Type, this._dependencyContainer, options);
|
||||||
|
} catch(DependencyContainerResolutionException) {
|
||||||
|
throw;
|
||||||
|
} catch(Exception ex) {
|
||||||
|
throw new DependencyContainerResolutionException(registration.Type, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fail if requesting named resolution and settings set to fail if unresolved
|
||||||
|
if(!String.IsNullOrEmpty(registration.Name) && options.NamedResolutionFailureAction == DependencyContainerNamedResolutionFailureAction.Fail) {
|
||||||
|
throw new DependencyContainerResolutionException(registration.Type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempted unnamed fallback container resolution if relevant and requested
|
||||||
|
if(!String.IsNullOrEmpty(registration.Name) && options.NamedResolutionFailureAction == DependencyContainerNamedResolutionFailureAction.AttemptUnnamedResolution) {
|
||||||
|
if(this.TryGetValue(new DependencyContainer.TypeRegistration(registration.Type, String.Empty), out factory)) {
|
||||||
|
try {
|
||||||
|
return factory.GetObject(registration.Type, this._dependencyContainer, options);
|
||||||
|
} catch(DependencyContainerResolutionException) {
|
||||||
|
throw;
|
||||||
|
} catch(Exception ex) {
|
||||||
|
throw new DependencyContainerResolutionException(registration.Type, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt unregistered construction if possible and requested
|
||||||
|
Boolean isValid = options.UnregisteredResolutionAction == DependencyContainerUnregisteredResolutionAction.AttemptResolve || registration.Type.IsGenericType && options.UnregisteredResolutionAction == DependencyContainerUnregisteredResolutionAction.GenericsOnly;
|
||||||
|
|
||||||
|
return isValid && !registration.Type.IsAbstract && !registration.Type.IsInterface ? this.ConstructType(registration.Type, null, options) : throw new DependencyContainerResolutionException(registration.Type);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal Boolean CanResolve(DependencyContainer.TypeRegistration registration, DependencyContainerResolveOptions? options = null) {
|
||||||
|
if(options == null) {
|
||||||
|
options = DependencyContainerResolveOptions.Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
Type checkType = registration.Type;
|
||||||
|
String name = registration.Name;
|
||||||
|
|
||||||
|
if(this.TryGetValue(new DependencyContainer.TypeRegistration(checkType, name), out ObjectFactoryBase? factory)) {
|
||||||
|
return factory.AssumeConstruction ? true : factory.Constructor == null ? this.GetBestConstructor(factory.CreatesType, options) != null : this.CanConstruct(factory.Constructor, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fail if requesting named resolution and settings set to fail if unresolved
|
||||||
|
// Or bubble up if we have a parent
|
||||||
|
if(!String.IsNullOrEmpty(name) && options.NamedResolutionFailureAction == DependencyContainerNamedResolutionFailureAction.Fail) {
|
||||||
|
return this._dependencyContainer.Parent?.RegisteredTypes.CanResolve(registration, options.Clone()) ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempted unnamed fallback container resolution if relevant and requested
|
||||||
|
if(!String.IsNullOrEmpty(name) && options.NamedResolutionFailureAction == DependencyContainerNamedResolutionFailureAction.AttemptUnnamedResolution) {
|
||||||
|
if(this.TryGetValue(new DependencyContainer.TypeRegistration(checkType), out factory)) {
|
||||||
|
return factory.AssumeConstruction ? true : this.GetBestConstructor(factory.CreatesType, options) != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if type is an automatic lazy factory request or an IEnumerable<ResolveType>
|
||||||
|
if(IsAutomaticLazyFactoryRequest(checkType) || registration.Type.IsIEnumerable()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt unregistered construction if possible and requested
|
||||||
|
// If we cant', bubble if we have a parent
|
||||||
|
if(options.UnregisteredResolutionAction == DependencyContainerUnregisteredResolutionAction.AttemptResolve || checkType.IsGenericType && options.UnregisteredResolutionAction == DependencyContainerUnregisteredResolutionAction.GenericsOnly) {
|
||||||
|
return this.GetBestConstructor(checkType, options) != null || (this._dependencyContainer.Parent?.RegisteredTypes.CanResolve(registration, options.Clone()) ?? false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bubble resolution up the container tree if we have a parent
|
||||||
|
return this._dependencyContainer.Parent != null && this._dependencyContainer.Parent.RegisteredTypes.CanResolve(registration, options.Clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
internal Object ConstructType(Type implementationType, ConstructorInfo? constructor, DependencyContainerResolveOptions? options = null) {
|
||||||
|
Type typeToConstruct = implementationType;
|
||||||
|
|
||||||
|
if(constructor == null) {
|
||||||
|
// Try and get the best constructor that we can construct
|
||||||
|
// if we can't construct any then get the constructor
|
||||||
|
// with the least number of parameters so we can throw a meaningful
|
||||||
|
// resolve exception
|
||||||
|
constructor = this.GetBestConstructor(typeToConstruct, options) ?? GetTypeConstructors(typeToConstruct).LastOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(constructor == null) {
|
||||||
|
throw new DependencyContainerResolutionException(typeToConstruct);
|
||||||
|
}
|
||||||
|
|
||||||
|
ParameterInfo[] ctorParams = constructor.GetParameters();
|
||||||
|
Object?[] args = new Object?[ctorParams.Length];
|
||||||
|
|
||||||
|
for(Int32 parameterIndex = 0; parameterIndex < ctorParams.Length; parameterIndex++) {
|
||||||
|
ParameterInfo currentParam = ctorParams[parameterIndex];
|
||||||
|
|
||||||
|
try {
|
||||||
|
args[parameterIndex] = options?.ConstructorParameters.GetValueOrDefault(currentParam.Name, this.ResolveInternal(new DependencyContainer.TypeRegistration(currentParam.ParameterType), options.Clone()));
|
||||||
|
} catch(DependencyContainerResolutionException ex) {
|
||||||
|
// If a constructor parameter can't be resolved
|
||||||
|
// it will throw, so wrap it and throw that this can't
|
||||||
|
// be resolved.
|
||||||
|
throw new DependencyContainerResolutionException(typeToConstruct, ex);
|
||||||
|
} catch(Exception ex) {
|
||||||
|
throw new DependencyContainerResolutionException(typeToConstruct, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return CreateObjectConstructionDelegateWithCache(constructor).Invoke(args);
|
||||||
|
} catch(Exception ex) {
|
||||||
|
throw new DependencyContainerResolutionException(typeToConstruct, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ObjectConstructor CreateObjectConstructionDelegateWithCache(ConstructorInfo constructor) {
|
||||||
|
if(ObjectConstructorCache.TryGetValue(constructor, out ObjectConstructor? objectConstructor)) {
|
||||||
|
return objectConstructor;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We could lock the cache here, but there's no real side
|
||||||
|
// effect to two threads creating the same ObjectConstructor
|
||||||
|
// at the same time, compared to the cost of a lock for
|
||||||
|
// every creation.
|
||||||
|
ParameterInfo[] constructorParams = constructor.GetParameters();
|
||||||
|
ParameterExpression lambdaParams = Expression.Parameter(typeof(Object[]), "parameters");
|
||||||
|
Expression[] newParams = new Expression[constructorParams.Length];
|
||||||
|
|
||||||
|
for(Int32 i = 0; i < constructorParams.Length; i++) {
|
||||||
|
BinaryExpression paramsParameter = Expression.ArrayIndex(lambdaParams, Expression.Constant(i));
|
||||||
|
|
||||||
|
newParams[i] = Expression.Convert(paramsParameter, constructorParams[i].ParameterType);
|
||||||
|
}
|
||||||
|
|
||||||
|
NewExpression newExpression = Expression.New(constructor, newParams);
|
||||||
|
|
||||||
|
LambdaExpression constructionLambda = Expression.Lambda(typeof(ObjectConstructor), newExpression, lambdaParams);
|
||||||
|
|
||||||
|
objectConstructor = (ObjectConstructor)constructionLambda.Compile();
|
||||||
|
|
||||||
|
ObjectConstructorCache[constructor] = objectConstructor;
|
||||||
|
return objectConstructor;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<ConstructorInfo> GetTypeConstructors(Type type) => type.GetConstructors().OrderByDescending(ctor => ctor.GetParameters().Length);
|
||||||
|
|
||||||
|
private static Boolean IsAutomaticLazyFactoryRequest(Type type) {
|
||||||
|
if(!type.IsGenericType) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Type genericType = type.GetGenericTypeDefinition();
|
||||||
|
|
||||||
|
// Just a func
|
||||||
|
if(genericType == typeof(Func<>)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2 parameter func with string as first parameter (name)
|
||||||
|
if(genericType == typeof(Func<,>) && type.GetGenericArguments()[0] == typeof(String)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3 parameter func with string as first parameter (name) and IDictionary<string, object> as second (parameters)
|
||||||
|
return genericType == typeof(Func<,,>) && type.GetGenericArguments()[0] == typeof(String) && type.GetGenericArguments()[1] == typeof(IDictionary<String, Object>);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ObjectFactoryBase? GetParentObjectFactory(DependencyContainer.TypeRegistration registration) => this._dependencyContainer.Parent == null
|
||||||
|
? null
|
||||||
|
: this._dependencyContainer.Parent.RegisteredTypes.TryGetValue(registration, out ObjectFactoryBase? factory) ? factory.GetFactoryForChildContainer(registration.Type, this._dependencyContainer.Parent, this._dependencyContainer) : this._dependencyContainer.Parent.RegisteredTypes.GetParentObjectFactory(registration);
|
||||||
|
|
||||||
|
private ConstructorInfo? GetBestConstructor(Type type, DependencyContainerResolveOptions? options) => type.IsValueType ? null : GetTypeConstructors(type).FirstOrDefault(ctor => this.CanConstruct(ctor, options));
|
||||||
|
|
||||||
|
private Boolean CanConstruct(MethodBase ctor, DependencyContainerResolveOptions? options) {
|
||||||
|
foreach(ParameterInfo parameter in ctor.GetParameters()) {
|
||||||
|
if(String.IsNullOrEmpty(parameter.Name)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Boolean isParameterOverload = options!.ConstructorParameters.ContainsKey(parameter.Name);
|
||||||
|
|
||||||
|
if(parameter.ParameterType.IsPrimitive && !isParameterOverload) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!isParameterOverload && !this.CanResolve(new DependencyContainer.TypeRegistration(parameter.ParameterType), options.Clone())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<DependencyContainer.TypeRegistration> GetParentRegistrationsForType(Type resolveType) => this._dependencyContainer.Parent == null ? Array.Empty<DependencyContainer.TypeRegistration>() : this._dependencyContainer.Parent.RegisteredTypes.Keys.Where(tr => tr.Type == resolveType).Concat(this._dependencyContainer.Parent.RegisteredTypes.GetParentRegistrationsForType(resolveType));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,143 +1,128 @@
|
|||||||
namespace Swan.Diagnostics
|
#nullable enable
|
||||||
{
|
using System;
|
||||||
using System;
|
using System.Diagnostics;
|
||||||
using System.Diagnostics;
|
using Swan.Threading;
|
||||||
using Threading;
|
|
||||||
|
namespace Swan.Diagnostics {
|
||||||
|
/// <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 Int64 _offsetTicks;
|
||||||
|
private Double _speedRatio = 1.0d;
|
||||||
|
private Boolean _isDisposed;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A time measurement artifact.
|
/// Initializes a new instance of the <see cref="RealTimeClock"/> class.
|
||||||
|
/// The clock starts paused and at the 0 position.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class RealTimeClock : IDisposable
|
public RealTimeClock() => this.Reset();
|
||||||
{
|
|
||||||
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>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="RealTimeClock"/> class.
|
/// Gets or sets the clock position.
|
||||||
/// The clock starts paused and at the 0 position.
|
/// </summary>
|
||||||
/// </summary>
|
public TimeSpan Position {
|
||||||
public RealTimeClock()
|
get {
|
||||||
{
|
using(this._locker?.AcquireReaderLock()) {
|
||||||
Reset();
|
return TimeSpan.FromTicks(this._offsetTicks + Convert.ToInt64(this._chrono.Elapsed.Ticks * this.SpeedRatio));
|
||||||
}
|
|
||||||
|
|
||||||
/// <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;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether the clock is running.
|
||||||
|
/// </summary>
|
||||||
|
public Boolean IsRunning {
|
||||||
|
get {
|
||||||
|
using(this._locker?.AcquireReaderLock()) {
|
||||||
|
return this._chrono.IsRunning;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the speed ratio at which the clock runs.
|
||||||
|
/// </summary>
|
||||||
|
public Double SpeedRatio {
|
||||||
|
get {
|
||||||
|
using(this._locker?.AcquireReaderLock()) {
|
||||||
|
return this._speedRatio;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
using(this._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
|
||||||
|
TimeSpan initialPosition = this.Position;
|
||||||
|
this._speedRatio = value;
|
||||||
|
this.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(this._locker?.AcquireWriterLock()) {
|
||||||
|
Boolean resume = this._chrono.IsRunning;
|
||||||
|
this._chrono.Reset();
|
||||||
|
this._offsetTicks = value.Ticks;
|
||||||
|
if(resume) {
|
||||||
|
this._chrono.Start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Starts or resumes the clock.
|
||||||
|
/// </summary>
|
||||||
|
public void Play() {
|
||||||
|
using(this._locker?.AcquireWriterLock()) {
|
||||||
|
if(this._chrono.IsRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._chrono.Start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pauses the clock.
|
||||||
|
/// </summary>
|
||||||
|
public void Pause() {
|
||||||
|
using(this._locker?.AcquireWriterLock()) {
|
||||||
|
this._chrono.Stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the clock position to 0 and stops it.
|
||||||
|
/// The speed ratio is not modified.
|
||||||
|
/// </summary>
|
||||||
|
public void Reset() {
|
||||||
|
using(this._locker?.AcquireWriterLock()) {
|
||||||
|
this._offsetTicks = 0;
|
||||||
|
this._chrono.Reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Dispose() {
|
||||||
|
if(this._isDisposed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._isDisposed = true;
|
||||||
|
this._locker?.Dispose();
|
||||||
|
this._locker = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,56 +1,43 @@
|
|||||||
namespace Swan
|
using System;
|
||||||
{
|
using System.IO;
|
||||||
using System;
|
using System.Net.Mail;
|
||||||
using System.IO;
|
using System.Reflection;
|
||||||
using System.Net.Mail;
|
|
||||||
using System.Reflection;
|
namespace Swan {
|
||||||
|
/// <summary>
|
||||||
|
/// Extension methods.
|
||||||
|
/// </summary>
|
||||||
|
public static class SmtpExtensions {
|
||||||
|
private static readonly BindingFlags PrivateInstanceFlags = BindingFlags.Instance | BindingFlags.NonPublic;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Extension methods.
|
/// The raw contents of this MailMessage as a MemoryStream.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class SmtpExtensions
|
/// <param name="this">The caller.</param>
|
||||||
{
|
/// <returns>A MemoryStream with the raw contents of this MailMessage.</returns>
|
||||||
private static readonly BindingFlags PrivateInstanceFlags = BindingFlags.Instance | BindingFlags.NonPublic;
|
public static MemoryStream ToMimeMessage(this MailMessage @this) {
|
||||||
|
if(@this == null) {
|
||||||
|
throw new ArgumentNullException(nameof(@this));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
MemoryStream result = new MemoryStream();
|
||||||
/// The raw contents of this MailMessage as a MemoryStream.
|
Object mailWriter = MimeMessageConstants.MailWriterConstructor.Invoke(new Object[] { result });
|
||||||
/// </summary>
|
_ = MimeMessageConstants.SendMethod.Invoke(@this, PrivateInstanceFlags, null, MimeMessageConstants.IsRunningInDotNetFourPointFive ? new[] { mailWriter, true, true } : new[] { mailWriter, true }, null);
|
||||||
/// <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();
|
result = new MemoryStream(result.ToArray());
|
||||||
var mailWriter = MimeMessageConstants.MailWriterConstructor.Invoke(new object[] { result });
|
_ = MimeMessageConstants.CloseMethod.Invoke(mailWriter, PrivateInstanceFlags, null, Array.Empty<Object>(), null);
|
||||||
MimeMessageConstants.SendMethod.Invoke(
|
result.Position = 0;
|
||||||
@this,
|
return result;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 Boolean IsRunningInDotNetFourPointFive = SendMethod.GetParameters().Length == 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,58 +1,58 @@
|
|||||||
namespace Swan
|
using System;
|
||||||
{
|
using System.Linq;
|
||||||
using System;
|
using System.Net;
|
||||||
using System.Linq;
|
using System.Net.Sockets;
|
||||||
using System.Net;
|
|
||||||
using System.Net.Sockets;
|
namespace Swan {
|
||||||
|
/// <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 Boolean IsPrivateAddress(this IPAddress @this) {
|
||||||
|
if(@this == null) {
|
||||||
|
throw new ArgumentNullException(nameof(@this));
|
||||||
|
}
|
||||||
|
|
||||||
|
Byte[] octets = @this.ToString().Split(new[] { "." }, StringSplitOptions.RemoveEmptyEntries).Select(Byte.Parse).ToArray();
|
||||||
|
Boolean is24Bit = octets[0] == 10;
|
||||||
|
Boolean is20Bit = octets[0] == 172 && octets[1] >= 16 && octets[1] <= 31;
|
||||||
|
Boolean is16Bit = octets[0] == 192 && octets[1] == 168;
|
||||||
|
|
||||||
|
return is24Bit || is20Bit || is16Bit;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides various extension methods for networking-related tasks.
|
/// Converts an IPv4 Address to its Unsigned, 32-bit integer representation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class NetworkExtensions
|
/// <param name="this">The address.</param>
|
||||||
{
|
/// <returns>
|
||||||
/// <summary>
|
/// A 32-bit unsigned integer converted from four bytes at a specified position in a byte array.
|
||||||
/// Determines whether the IP address is private.
|
/// </returns>
|
||||||
/// </summary>
|
/// <exception cref="ArgumentNullException">address.</exception>
|
||||||
/// <param name="this">The IP address.</param>
|
/// <exception cref="ArgumentException">InterNetwork - address.</exception>
|
||||||
/// <returns>
|
public static UInt32 ToUInt32(this IPAddress @this) {
|
||||||
/// True if the IP Address is private; otherwise, false.
|
if(@this == null) {
|
||||||
/// </returns>
|
throw new ArgumentNullException(nameof(@this));
|
||||||
/// <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();
|
if(@this.AddressFamily != AddressFamily.InterNetwork) {
|
||||||
var is24Bit = octets[0] == 10;
|
throw new ArgumentException($"Address has to be of family '{nameof(AddressFamily.InterNetwork)}'", nameof(@this));
|
||||||
var is20Bit = octets[0] == 172 && (octets[1] >= 16 && octets[1] <= 31);
|
}
|
||||||
var is16Bit = octets[0] == 192 && octets[1] == 168;
|
|
||||||
|
|
||||||
return is24Bit || is20Bit || is16Bit;
|
Byte[] addressBytes = @this.GetAddressBytes();
|
||||||
}
|
if(BitConverter.IsLittleEndian) {
|
||||||
|
Array.Reverse(addressBytes);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
return BitConverter.ToUInt32(addressBytes, 0);
|
||||||
/// 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,89 +1,81 @@
|
|||||||
namespace Swan
|
using Swan.Logging;
|
||||||
{
|
using System;
|
||||||
using Logging;
|
using System.Collections.Generic;
|
||||||
using System;
|
using System.Reflection;
|
||||||
using System.Collections.Generic;
|
using System.Threading;
|
||||||
using System.Reflection;
|
|
||||||
using System.Threading;
|
using Swan.Services;
|
||||||
#if NET461
|
|
||||||
using System.ServiceProcess;
|
namespace Swan {
|
||||||
#else
|
/// <summary>
|
||||||
using Services;
|
/// Extension methods.
|
||||||
#endif
|
/// </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>
|
/// <summary>
|
||||||
/// Extension methods.
|
/// Runs a set of services in console mode.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class WindowsServicesExtensions
|
/// <param name="this">The services to run.</param>
|
||||||
{
|
/// <param name="loggerSource">The logger source.</param>
|
||||||
/// <summary>
|
/// <exception cref="ArgumentNullException">this.</exception>
|
||||||
/// Runs a service in console mode.
|
/// <exception cref="InvalidOperationException">The ServiceBase class isn't available.</exception>
|
||||||
/// </summary>
|
[Obsolete("This extension method will be removed in version 3.0")]
|
||||||
/// <param name="this">The service to run.</param>
|
public static void RunInConsoleMode(this ServiceBase[] @this, String loggerSource = null) {
|
||||||
/// <param name="loggerSource">The logger source.</param>
|
if(@this == null) {
|
||||||
/// <exception cref="ArgumentNullException">this.</exception>
|
throw new ArgumentNullException(nameof(@this));
|
||||||
[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);
|
const String onStartMethodName = "OnStart";
|
||||||
}
|
const String onStopMethodName = "OnStop";
|
||||||
|
|
||||||
/// <summary>
|
MethodInfo onStartMethod = typeof(ServiceBase).GetMethod(onStartMethodName, BindingFlags.Instance | BindingFlags.NonPublic);
|
||||||
/// Runs a set of services in console mode.
|
MethodInfo onStopMethod = typeof(ServiceBase).GetMethod(onStopMethodName, BindingFlags.Instance | BindingFlags.NonPublic);
|
||||||
/// </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";
|
if(onStartMethod == null || onStopMethod == null) {
|
||||||
const string onStopMethodName = "OnStop";
|
throw new InvalidOperationException("The ServiceBase class isn't available.");
|
||||||
|
}
|
||||||
|
|
||||||
var onStartMethod = typeof(ServiceBase).GetMethod(onStartMethodName,
|
List<Thread> serviceThreads = new List<Thread>();
|
||||||
BindingFlags.Instance | BindingFlags.NonPublic);
|
"Starting services . . .".Info(loggerSource ?? SwanRuntime.EntryAssemblyName.Name);
|
||||||
var onStopMethod = typeof(ServiceBase).GetMethod(onStopMethodName,
|
|
||||||
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
||||||
|
|
||||||
if (onStartMethod == null || onStopMethod == null)
|
foreach(ServiceBase service in @this) {
|
||||||
throw new InvalidOperationException("The ServiceBase class isn't available.");
|
Thread thread = new Thread(() => {
|
||||||
|
_ = onStartMethod.Invoke(service, new Object[] { Array.Empty<String>() });
|
||||||
|
$"Started service '{service.GetType().Name}'".Info(loggerSource ?? service.GetType().Name);
|
||||||
|
});
|
||||||
|
|
||||||
var serviceThreads = new List<Thread>();
|
serviceThreads.Add(thread);
|
||||||
"Starting services . . .".Info(loggerSource ?? SwanRuntime.EntryAssemblyName.Name);
|
thread.Start();
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var service in @this)
|
"Press any key to stop all services.".Info(loggerSource ?? SwanRuntime.EntryAssemblyName.Name);
|
||||||
{
|
_ = Terminal.ReadKey(true, true);
|
||||||
var thread = new Thread(() =>
|
"Stopping services . . .".Info(SwanRuntime.EntryAssemblyName.Name);
|
||||||
{
|
|
||||||
onStartMethod.Invoke(service, new object[] { Array.Empty<string>() });
|
|
||||||
$"Started service '{service.GetType().Name}'".Info(loggerSource ?? service.GetType().Name);
|
|
||||||
});
|
|
||||||
|
|
||||||
serviceThreads.Add(thread);
|
foreach(ServiceBase service in @this) {
|
||||||
thread.Start();
|
_ = onStopMethod.Invoke(service, null);
|
||||||
}
|
$"Stopped service '{service.GetType().Name}'".Info(loggerSource ?? service.GetType().Name);
|
||||||
|
}
|
||||||
|
|
||||||
"Press any key to stop all services.".Info(loggerSource ?? SwanRuntime.EntryAssemblyName.Name);
|
foreach(Thread thread in serviceThreads) {
|
||||||
Terminal.ReadKey(true, true);
|
thread.Join();
|
||||||
"Stopping services . . .".Info(SwanRuntime.EntryAssemblyName.Name);
|
}
|
||||||
|
|
||||||
foreach (var service in @this)
|
"Stopped all services.".Info(loggerSource ?? SwanRuntime.EntryAssemblyName.Name);
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
namespace Swan.Messaging
|
using System;
|
||||||
{
|
|
||||||
|
namespace Swan.Messaging {
|
||||||
|
/// <summary>
|
||||||
|
/// A Message to be published/delivered by Messenger.
|
||||||
|
/// </summary>
|
||||||
|
public interface IMessageHubMessage {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A Message to be published/delivered by Messenger.
|
/// The sender of the message, or null if not supported by the message implementation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IMessageHubMessage
|
Object Sender {
|
||||||
{
|
get;
|
||||||
/// <summary>
|
|
||||||
/// The sender of the message, or null if not supported by the message implementation.
|
|
||||||
/// </summary>
|
|
||||||
object Sender { get; }
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,26 +1,28 @@
|
|||||||
namespace Swan.Messaging
|
using System;
|
||||||
{
|
|
||||||
|
namespace Swan.Messaging {
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a message subscription.
|
||||||
|
/// </summary>
|
||||||
|
public interface IMessageHubSubscription {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a message subscription.
|
/// Token returned to the subscribed to reference this subscription.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IMessageHubSubscription
|
MessageHubSubscriptionToken SubscriptionToken {
|
||||||
{
|
get;
|
||||||
/// <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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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>
|
||||||
|
Boolean ShouldAttemptDelivery(IMessageHubMessage message);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deliver the message.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">Message to deliver.</param>
|
||||||
|
void Deliver(IMessageHubMessage message);
|
||||||
|
}
|
||||||
}
|
}
|
@ -11,432 +11,361 @@
|
|||||||
// LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
// LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE.
|
// FITNESS FOR A PARTICULAR PURPOSE.
|
||||||
// ===============================================================================
|
// ===============================================================================
|
||||||
|
#nullable enable
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace Swan.Messaging
|
namespace Swan.Messaging {
|
||||||
{
|
#region Message Types / Interfaces
|
||||||
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>
|
/// <summary>
|
||||||
/// Message proxy definition.
|
/// Delivers the specified message.
|
||||||
///
|
|
||||||
/// A message proxy can be used to intercept/alter messages and/or
|
|
||||||
/// marshal delivery actions onto a particular thread.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IMessageHubProxy
|
/// <param name="message">The message.</param>
|
||||||
{
|
/// <param name="subscription">The subscription.</param>
|
||||||
/// <summary>
|
void Deliver(IMessageHubMessage message, IMessageHubSubscription subscription);
|
||||||
/// Delivers the specified message.
|
}
|
||||||
/// </summary>
|
|
||||||
/// <param name="message">The message.</param>
|
/// <summary>
|
||||||
/// <param name="subscription">The subscription.</param>
|
/// Default "pass through" proxy.
|
||||||
void Deliver(IMessageHubMessage message, IMessageHubSubscription subscription);
|
///
|
||||||
|
/// Does nothing other than deliver the message.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MessageHubDefaultProxy : IMessageHubProxy {
|
||||||
|
private MessageHubDefaultProxy() {
|
||||||
|
// placeholder
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Default "pass through" proxy.
|
/// Singleton instance of the proxy.
|
||||||
///
|
|
||||||
/// Does nothing other than deliver the message.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class MessageHubDefaultProxy : IMessageHubProxy
|
public static MessageHubDefaultProxy Instance { get; } = new MessageHubDefaultProxy();
|
||||||
{
|
|
||||||
private MessageHubDefaultProxy()
|
/// <summary>
|
||||||
{
|
/// Delivers the specified message.
|
||||||
// placeholder
|
/// </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, Boolean 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, Boolean> messageFilter, Boolean 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<IMessageHub>() as MessageHub;
|
||||||
|
///
|
||||||
|
/// // create an instance of the publisher class
|
||||||
|
/// // which has a string as its content
|
||||||
|
/// var message = new MessageHubGenericMessage<string>(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<MessageHubGenericMessage<string>>(m => 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<MessageHubGenericMessage<string>>(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, Boolean> messageFilter) {
|
||||||
|
this.SubscriptionToken = subscriptionToken ?? throw new ArgumentNullException(nameof(subscriptionToken));
|
||||||
|
this._deliveryAction = new WeakReference(deliveryAction);
|
||||||
|
this._messageFilter = new WeakReference(messageFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MessageHubSubscriptionToken SubscriptionToken {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean ShouldAttemptDelivery(IMessageHubMessage message) => this._deliveryAction.IsAlive && this._messageFilter.IsAlive && ((Func<TMessage, Boolean>)this._messageFilter.Target!).Invoke((TMessage)message);
|
||||||
|
|
||||||
|
public void Deliver(IMessageHubMessage message) {
|
||||||
|
if(this._deliveryAction.IsAlive) {
|
||||||
|
((Action<TMessage>)this._deliveryAction.Target!).Invoke((TMessage)message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class StrongMessageSubscription<TMessage> : IMessageHubSubscription where TMessage : class, IMessageHubMessage {
|
||||||
|
private readonly Action<TMessage> _deliveryAction;
|
||||||
|
private readonly Func<TMessage, Boolean> _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, Boolean> messageFilter) {
|
||||||
|
this.SubscriptionToken = subscriptionToken ?? throw new ArgumentNullException(nameof(subscriptionToken));
|
||||||
|
this._deliveryAction = deliveryAction;
|
||||||
|
this._messageFilter = messageFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MessageHubSubscriptionToken SubscriptionToken {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean ShouldAttemptDelivery(IMessageHubMessage message) => this._messageFilter.Invoke((TMessage)message);
|
||||||
|
|
||||||
|
public void Deliver(IMessageHubMessage message) => this._deliveryAction.Invoke((TMessage)message);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Subscription dictionary
|
||||||
|
|
||||||
|
private class SubscriptionItem {
|
||||||
|
public SubscriptionItem(IMessageHubProxy proxy, IMessageHubSubscription subscription) {
|
||||||
|
this.Proxy = proxy;
|
||||||
|
this.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, Boolean useStrongReferences = true, IMessageHubProxy? proxy = null) where TMessage : class, IMessageHubMessage => this.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>
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("Codequalität", "IDE0068:Empfohlenes Dispose-Muster verwenden", Justification = "<Ausstehend>")]
|
||||||
|
public MessageHubSubscriptionToken Subscribe<TMessage>(Action<TMessage> deliveryAction, Func<TMessage, Boolean> messageFilter, Boolean 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(this._subscriptionsPadlock) {
|
||||||
|
if(!this._subscriptions.TryGetValue(typeof(TMessage), out List<SubscriptionItem>? currentSubscriptions)) {
|
||||||
|
currentSubscriptions = new List<SubscriptionItem>();
|
||||||
|
this._subscriptions[typeof(TMessage)] = currentSubscriptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
MessageHubSubscriptionToken subscriptionToken = new MessageHubSubscriptionToken(this, typeof(TMessage));
|
||||||
/// Singleton instance of the proxy.
|
|
||||||
/// </summary>
|
|
||||||
public static MessageHubDefaultProxy Instance { get; } = new MessageHubDefaultProxy();
|
|
||||||
|
|
||||||
/// <summary>
|
IMessageHubSubscription subscription = useStrongReferences ? new StrongMessageSubscription<TMessage>(subscriptionToken, deliveryAction, messageFilter) : (IMessageHubSubscription)new WeakMessageSubscription<TMessage>(subscriptionToken, deliveryAction, messageFilter);
|
||||||
/// Delivers the specified message.
|
|
||||||
/// </summary>
|
currentSubscriptions.Add(new SubscriptionItem(proxy ?? MessageHubDefaultProxy.Instance, subscription));
|
||||||
/// <param name="message">The message.</param>
|
|
||||||
/// <param name="subscription">The subscription.</param>
|
return subscriptionToken;
|
||||||
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 />
|
/// <inheritdoc />
|
||||||
/// <example>
|
public void Unsubscribe<TMessage>(MessageHubSubscriptionToken subscriptionToken) where TMessage : class, IMessageHubMessage {
|
||||||
/// The following code describes how to use a MessageHub. Both the
|
if(subscriptionToken == null) {
|
||||||
/// subscription and the message sending are done in the same place but this is only for explanatory purposes.
|
throw new ArgumentNullException(nameof(subscriptionToken));
|
||||||
/// <code>
|
}
|
||||||
/// class Example
|
|
||||||
/// {
|
|
||||||
/// using Swan;
|
|
||||||
/// using Swan.Components;
|
|
||||||
///
|
|
||||||
/// static void Main()
|
|
||||||
/// {
|
|
||||||
/// // using DependencyContainer to create an instance of MessageHub
|
|
||||||
/// var messageHub = DependencyContainer
|
|
||||||
/// .Current
|
|
||||||
/// .Resolve<IMessageHub>() as MessageHub;
|
|
||||||
///
|
|
||||||
/// // create an instance of the publisher class
|
|
||||||
/// // which has a string as its content
|
|
||||||
/// var message = new MessageHubGenericMessage<string>(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<MessageHubGenericMessage<string>>(m => 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<MessageHubGenericMessage<string>>(token);
|
|
||||||
///
|
|
||||||
/// Terminal.Flush();
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// }
|
|
||||||
/// </code>
|
|
||||||
/// </example>
|
|
||||||
public sealed class MessageHub : IMessageHub
|
|
||||||
{
|
|
||||||
#region Private Types and Interfaces
|
|
||||||
|
|
||||||
private readonly object _subscriptionsPadlock = new object();
|
lock(this._subscriptionsPadlock) {
|
||||||
|
if(!this._subscriptions.TryGetValue(typeof(TMessage), out List<SubscriptionItem>? currentSubscriptions)) {
|
||||||
private readonly Dictionary<Type, List<SubscriptionItem>> _subscriptions =
|
return;
|
||||||
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
|
List<SubscriptionItem> currentlySubscribed = currentSubscriptions.Where(sub => ReferenceEquals(sub.Subscription.SubscriptionToken, subscriptionToken)).ToList();
|
||||||
where TMessage : class, IMessageHubMessage
|
|
||||||
{
|
|
||||||
private readonly Action<TMessage> _deliveryAction;
|
|
||||||
private readonly Func<TMessage, bool> _messageFilter;
|
|
||||||
|
|
||||||
/// <summary>
|
currentlySubscribed.ForEach(sub => currentSubscriptions.Remove(sub));
|
||||||
/// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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(this._subscriptionsPadlock) {
|
||||||
|
if(!this._subscriptions.TryGetValue(typeof(TMessage), out List<SubscriptionItem>? 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 => Task.Run(() => this.Publish(message));
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
|
@ -1,57 +1,50 @@
|
|||||||
namespace Swan.Messaging
|
using System;
|
||||||
{
|
|
||||||
using System;
|
namespace Swan.Messaging {
|
||||||
|
/// <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>
|
/// <summary>
|
||||||
/// Base class for messages that provides weak reference storage of the sender.
|
/// Initializes a new instance of the <see cref="MessageHubMessageBase"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class MessageHubMessageBase
|
/// <param name="sender">The sender.</param>
|
||||||
: IMessageHubMessage
|
/// <exception cref="System.ArgumentNullException">sender.</exception>
|
||||||
{
|
protected MessageHubMessageBase(Object sender) {
|
||||||
/// <summary>
|
if(sender == null) {
|
||||||
/// Store a WeakReference to the sender just in case anyone is daft enough to
|
throw new ArgumentNullException(nameof(sender));
|
||||||
/// keep the message around and prevent the sender from being collected.
|
}
|
||||||
/// </summary>
|
|
||||||
private readonly WeakReference _sender;
|
|
||||||
|
|
||||||
/// <summary>
|
this._sender = new WeakReference(sender);
|
||||||
/// 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Object Sender => this._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) => this.Content = content;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Generic message with user specified content.
|
/// Contents of the message.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TContent">Content type to store.</typeparam>
|
public TContent Content {
|
||||||
public class MessageHubGenericMessage<TContent>
|
get; protected set;
|
||||||
: 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; }
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,51 +1,43 @@
|
|||||||
namespace Swan.Messaging
|
using System;
|
||||||
{
|
using System.Reflection;
|
||||||
using System;
|
|
||||||
|
namespace Swan.Messaging {
|
||||||
|
/// <summary>
|
||||||
|
/// Represents an active subscription to a message.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MessageHubSubscriptionToken : IDisposable {
|
||||||
|
private readonly WeakReference _hub;
|
||||||
|
private readonly Type _messageType;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents an active subscription to a message.
|
/// Initializes a new instance of the <see cref="MessageHubSubscriptionToken"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class MessageHubSubscriptionToken
|
/// <param name="hub">The hub.</param>
|
||||||
: IDisposable
|
/// <param name="messageType">Type of the message.</param>
|
||||||
{
|
/// <exception cref="System.ArgumentNullException">hub.</exception>
|
||||||
private readonly WeakReference _hub;
|
/// <exception cref="System.ArgumentOutOfRangeException">messageType.</exception>
|
||||||
private readonly Type _messageType;
|
public MessageHubSubscriptionToken(IMessageHub hub, Type messageType) {
|
||||||
|
if(hub == null) {
|
||||||
|
throw new ArgumentNullException(nameof(hub));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
if(!typeof(IMessageHubMessage).IsAssignableFrom(messageType)) {
|
||||||
/// Initializes a new instance of the <see cref="MessageHubSubscriptionToken"/> class.
|
throw new ArgumentOutOfRangeException(nameof(messageType));
|
||||||
/// </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))
|
this._hub = new WeakReference(hub);
|
||||||
{
|
this._messageType = 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Dispose() {
|
||||||
|
if(this._hub.IsAlive && this._hub.Target is IMessageHub hub) {
|
||||||
|
MethodInfo unsubscribeMethod = typeof(IMessageHub).GetMethod(nameof(IMessageHub.Unsubscribe), new[] { typeof(MessageHubSubscriptionToken) });
|
||||||
|
unsubscribeMethod = unsubscribeMethod.MakeGenericMethod(this._messageType);
|
||||||
|
_ = unsubscribeMethod.Invoke(hub, new Object[] { this });
|
||||||
|
}
|
||||||
|
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
File diff suppressed because it is too large
Load Diff
@ -1,28 +1,26 @@
|
|||||||
namespace Swan
|
namespace Swan {
|
||||||
{
|
/// <summary>
|
||||||
|
/// Enumerates the possible causes of the DataReceived event occurring.
|
||||||
|
/// </summary>
|
||||||
|
public enum ConnectionDataReceivedTrigger {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Enumerates the possible causes of the DataReceived event occurring.
|
/// The trigger was a forceful flush of the buffer
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum ConnectionDataReceivedTrigger
|
Flush,
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The trigger was a forceful flush of the buffer
|
|
||||||
/// </summary>
|
|
||||||
Flush,
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The new line sequence bytes were received
|
/// The new line sequence bytes were received
|
||||||
/// </summary>
|
/// </summary>
|
||||||
NewLineSequenceEncountered,
|
NewLineSequenceEncountered,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The buffer was full
|
/// The buffer was full
|
||||||
/// </summary>
|
/// </summary>
|
||||||
BufferFull,
|
BufferFull,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The block size reached
|
/// The block size reached
|
||||||
/// </summary>
|
/// </summary>
|
||||||
BlockSizeReached,
|
BlockSizeReached,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,253 +1,226 @@
|
|||||||
namespace Swan.Net
|
#nullable enable
|
||||||
{
|
using System;
|
||||||
using System;
|
using System.Net;
|
||||||
using System.Net;
|
using System.Net.Sockets;
|
||||||
using System.Net.Sockets;
|
using System.Threading;
|
||||||
using System.Threading;
|
using System.Threading.Tasks;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
namespace Swan.Net {
|
||||||
|
/// <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 Boolean _cancellationPending;
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("Codequalität", "IDE0069:Verwerfbare Felder verwerfen", Justification = "<Ausstehend>")]
|
||||||
|
private CancellationTokenSource? _cancelListening;
|
||||||
|
private Task? _backgroundWorkerTask;
|
||||||
|
private Boolean _hasDisposed;
|
||||||
|
|
||||||
|
#region Events
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// TCP Listener manager with built-in events and asynchronous functionality.
|
/// Occurs when a new connection requests a socket from the listener.
|
||||||
/// This networking component is typically used when writing server software.
|
/// Set Cancel = true to prevent the TCP client from being accepted.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <seealso cref="System.IDisposable" />
|
public event EventHandler<ConnectionAcceptingEventArgs> OnConnectionAccepting = (s, e) => { };
|
||||||
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 is accepted.
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler<ConnectionAcceptedEventArgs> OnConnectionAccepted = (s, e) => { };
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Occurs when a new connection requests a socket from the listener.
|
/// Occurs when a connection fails to get accepted
|
||||||
/// Set Cancel = true to prevent the TCP client from being accepted.
|
/// </summary>
|
||||||
/// </summary>
|
public event EventHandler<ConnectionFailureEventArgs> OnConnectionFailure = (s, e) => { };
|
||||||
public event EventHandler<ConnectionAcceptingEventArgs> OnConnectionAccepting = (s, e) => { };
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Occurs when a new connection is accepted.
|
/// Occurs when the listener stops.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public event EventHandler<ConnectionAcceptedEventArgs> OnConnectionAccepted = (s, e) => { };
|
public event EventHandler<ConnectionListenerStoppedEventArgs> OnListenerStopped = (s, e) => { };
|
||||||
|
|
||||||
/// <summary>
|
#endregion
|
||||||
/// Occurs when a connection fails to get accepted
|
|
||||||
/// </summary>
|
|
||||||
public event EventHandler<ConnectionFailureEventArgs> OnConnectionFailure = (s, e) => { };
|
|
||||||
|
|
||||||
/// <summary>
|
#region Constructors
|
||||||
/// Occurs when the listener stops.
|
|
||||||
/// </summary>
|
|
||||||
public event EventHandler<ConnectionListenerStoppedEventArgs> OnListenerStopped = (s, e) => { };
|
|
||||||
|
|
||||||
#endregion
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ConnectionListener"/> class.
|
||||||
#region Constructors
|
/// </summary>
|
||||||
|
/// <param name="listenEndPoint">The listen end point.</param>
|
||||||
/// <summary>
|
public ConnectionListener(IPEndPoint listenEndPoint) {
|
||||||
/// Initializes a new instance of the <see cref="ConnectionListener"/> class.
|
this.Id = Guid.NewGuid();
|
||||||
/// </summary>
|
this.LocalEndPoint = listenEndPoint ?? throw new ArgumentNullException(nameof(listenEndPoint));
|
||||||
/// <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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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(Int32 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, Int32 listenPort) : this(new IPEndPoint(listenAddress, listenPort)) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finalizes an instance of the <see cref="ConnectionListener"/> class.
|
||||||
|
/// </summary>
|
||||||
|
~ConnectionListener() {
|
||||||
|
this.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 Boolean IsListening => this._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(this._stateLock) {
|
||||||
|
if(this._backgroundWorkerTask != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this._cancellationPending) {
|
||||||
|
throw new InvalidOperationException("Cancellation has already been requested. This listener is not reusable.");
|
||||||
|
}
|
||||||
|
|
||||||
|
this._backgroundWorkerTask = this.DoWorkAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stops the listener from receiving new connections.
|
||||||
|
/// This does not prevent the listener from .
|
||||||
|
/// </summary>
|
||||||
|
public void Stop() {
|
||||||
|
lock(this._stateLock) {
|
||||||
|
this._cancellationPending = true;
|
||||||
|
this._listenerSocket?.Stop();
|
||||||
|
this._cancelListening?.Cancel();
|
||||||
|
this._backgroundWorkerTask?.Wait();
|
||||||
|
this._backgroundWorkerTask = null;
|
||||||
|
this._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() => this.LocalEndPoint.ToString();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Dispose() {
|
||||||
|
this.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(Boolean disposing) {
|
||||||
|
if(this._hasDisposed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(disposing) {
|
||||||
|
// Release managed resources
|
||||||
|
this.Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
this._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() {
|
||||||
|
this._cancellationPending = false;
|
||||||
|
this._listenerSocket = new TcpListener(this.LocalEndPoint);
|
||||||
|
this._listenerSocket.Start();
|
||||||
|
this._cancelListening = new CancellationTokenSource();
|
||||||
|
|
||||||
|
try {
|
||||||
|
while(this._cancellationPending == false) {
|
||||||
|
try {
|
||||||
|
TcpClient client = await Task.Run(() => this._listenerSocket.AcceptTcpClientAsync(), this._cancelListening.Token).ConfigureAwait(false);
|
||||||
|
ConnectionAcceptingEventArgs acceptingArgs = new ConnectionAcceptingEventArgs(client);
|
||||||
|
OnConnectionAccepting(this, acceptingArgs);
|
||||||
|
|
||||||
|
if(acceptingArgs.Cancel) {
|
||||||
|
client.Dispose();
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
OnConnectionAccepted(this, new ConnectionAcceptedEventArgs(client));
|
||||||
|
} catch(Exception ex) {
|
||||||
|
OnConnectionFailure(this, new ConnectionFailureEventArgs(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OnListenerStopped(this, new ConnectionListenerStoppedEventArgs(this.LocalEndPoint));
|
||||||
|
} catch(ObjectDisposedException) {
|
||||||
|
OnListenerStopped(this, new ConnectionListenerStoppedEventArgs(this.LocalEndPoint));
|
||||||
|
} catch(Exception ex) {
|
||||||
|
OnListenerStopped(this,
|
||||||
|
new ConnectionListenerStoppedEventArgs(this.LocalEndPoint, this._cancellationPending ? null : ex));
|
||||||
|
} finally {
|
||||||
|
this._backgroundWorkerTask = null;
|
||||||
|
this._cancellationPending = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,62 +1,96 @@
|
|||||||
namespace Swan.Net.Dns
|
using System;
|
||||||
{
|
using System.Threading.Tasks;
|
||||||
using System;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
/// <summary>
|
namespace Swan.Net.Dns {
|
||||||
/// DnsClient public interfaces.
|
/// <summary>
|
||||||
/// </summary>
|
/// DnsClient public interfaces.
|
||||||
internal partial class DnsClient
|
/// </summary>
|
||||||
{
|
internal partial class DnsClient {
|
||||||
public interface IDnsMessage
|
public interface IDnsMessage {
|
||||||
{
|
IList<DnsQuestion> Questions {
|
||||||
IList<DnsQuestion> Questions { get; }
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
int Size { get; }
|
Int32 Size {
|
||||||
byte[] ToArray();
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public interface IDnsMessageEntry {
|
||||||
|
DnsDomain Name {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
DnsRecordType Type {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
DnsRecordClass Class {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
Int32 Size {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
Byte[] ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IDnsResourceRecord : IDnsMessageEntry {
|
||||||
|
TimeSpan TimeToLive {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
Int32 DataLength {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
Byte[] Data {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IDnsRequest : IDnsMessage {
|
||||||
|
Int32 Id {
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
DnsOperationCode OperationCode {
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
Boolean RecursionDesired {
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IDnsResponse : IDnsMessage {
|
||||||
|
Int32 Id {
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
IList<IDnsResourceRecord> AnswerRecords {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
IList<IDnsResourceRecord> AuthorityRecords {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
IList<IDnsResourceRecord> AdditionalRecords {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
Boolean IsRecursionAvailable {
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
Boolean IsAuthorativeServer {
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
Boolean IsTruncated {
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
DnsOperationCode OperationCode {
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
DnsResponseCode ResponseCode {
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IDnsRequestResolver {
|
||||||
|
Task<DnsClientResponse> Request(DnsClientRequest request);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,419 +1,344 @@
|
|||||||
namespace Swan.Net.Dns
|
using Swan.Formatters;
|
||||||
{
|
using System;
|
||||||
using Formatters;
|
using System.Collections.Generic;
|
||||||
using System;
|
using System.IO;
|
||||||
using System.Collections.Generic;
|
using System.Net;
|
||||||
using System.IO;
|
using System.Runtime.InteropServices;
|
||||||
using System.Net;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
/// <summary>
|
namespace Swan.Net.Dns {
|
||||||
/// DnsClient public methods.
|
/// <summary>
|
||||||
/// </summary>
|
/// DnsClient public methods.
|
||||||
internal partial class DnsClient
|
/// </summary>
|
||||||
{
|
internal partial class DnsClient {
|
||||||
public abstract class DnsResourceRecordBase : IDnsResourceRecord
|
public abstract class DnsResourceRecordBase : IDnsResourceRecord {
|
||||||
{
|
private readonly IDnsResourceRecord _record;
|
||||||
private readonly IDnsResourceRecord _record;
|
|
||||||
|
|
||||||
protected DnsResourceRecordBase(IDnsResourceRecord record)
|
protected DnsResourceRecordBase(IDnsResourceRecord record) => this._record = record;
|
||||||
{
|
|
||||||
_record = record;
|
|
||||||
}
|
|
||||||
|
|
||||||
public DnsDomain Name => _record.Name;
|
public DnsDomain Name => this._record.Name;
|
||||||
|
|
||||||
public DnsRecordType Type => _record.Type;
|
public DnsRecordType Type => this._record.Type;
|
||||||
|
|
||||||
public DnsRecordClass Class => _record.Class;
|
public DnsRecordClass Class => this._record.Class;
|
||||||
|
|
||||||
public TimeSpan TimeToLive => _record.TimeToLive;
|
public TimeSpan TimeToLive => this._record.TimeToLive;
|
||||||
|
|
||||||
public int DataLength => _record.DataLength;
|
public Int32 DataLength => this._record.DataLength;
|
||||||
|
|
||||||
public byte[] Data => _record.Data;
|
public Byte[] Data => this._record.Data;
|
||||||
|
|
||||||
public int Size => _record.Size;
|
public Int32 Size => this._record.Size;
|
||||||
|
|
||||||
protected virtual string[] IncludedProperties
|
protected virtual String[] IncludedProperties => new[] { nameof(this.Name), nameof(this.Type), nameof(this.Class), nameof(this.TimeToLive), nameof(this.DataLength) };
|
||||||
=> new[] {nameof(Name), nameof(Type), nameof(Class), nameof(TimeToLive), nameof(DataLength)};
|
|
||||||
|
|
||||||
public byte[] ToArray() => _record.ToArray();
|
public Byte[] ToArray() => this._record.ToArray();
|
||||||
|
|
||||||
public override string ToString()
|
public override String ToString() => Json.SerializeOnly(this, true, this.IncludedProperties);
|
||||||
=> 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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class DnsResourceRecord : IDnsResourceRecord {
|
||||||
|
public DnsResourceRecord(DnsDomain domain, Byte[] data, DnsRecordType type, DnsRecordClass klass = DnsRecordClass.IN, TimeSpan ttl = default) {
|
||||||
|
this.Name = domain;
|
||||||
|
this.Type = type;
|
||||||
|
this.Class = klass;
|
||||||
|
this.TimeToLive = ttl;
|
||||||
|
this.Data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DnsDomain Name {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DnsRecordType Type {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DnsRecordClass Class {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TimeSpan TimeToLive {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Int32 DataLength => this.Data.Length;
|
||||||
|
|
||||||
|
public Byte[] Data {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Int32 Size => this.Name.Size + Tail.SIZE + this.Data.Length;
|
||||||
|
|
||||||
|
public static DnsResourceRecord FromArray(Byte[] message, Int32 offset, out Int32 endOffset) {
|
||||||
|
DnsDomain domain = DnsDomain.FromArray(message, offset, out offset);
|
||||||
|
Tail tail = message.ToStruct<Tail>(offset, Tail.SIZE);
|
||||||
|
|
||||||
|
Byte[] data = new Byte[tail.DataLength];
|
||||||
|
|
||||||
|
offset += Tail.SIZE;
|
||||||
|
Array.Copy(message, offset, data, 0, data.Length);
|
||||||
|
|
||||||
|
endOffset = offset + data.Length;
|
||||||
|
|
||||||
|
return new DnsResourceRecord(domain, data, tail.Type, tail.Class, tail.TimeToLive);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Byte[] ToArray() => new MemoryStream(this.Size).Append(this.Name.ToArray()).Append(new Tail() { Type = Type, Class = Class, TimeToLive = TimeToLive, DataLength = this.Data.Length, }.ToBytes()).Append(this.Data).ToArray();
|
||||||
|
|
||||||
|
public override String ToString() => Json.SerializeOnly(this, true, nameof(this.Name), nameof(this.Type), nameof(this.Class), nameof(this.TimeToLive), nameof(this.DataLength));
|
||||||
|
|
||||||
|
[StructEndianness(Endianness.Big)]
|
||||||
|
[StructLayout(LayoutKind.Sequential, Pack = 2)]
|
||||||
|
private struct Tail {
|
||||||
|
public const Int32 SIZE = 10;
|
||||||
|
|
||||||
|
private UInt16 type;
|
||||||
|
private UInt16 klass;
|
||||||
|
private UInt32 ttl;
|
||||||
|
private UInt16 dataLength;
|
||||||
|
|
||||||
|
public DnsRecordType Type {
|
||||||
|
get => (DnsRecordType)this.type;
|
||||||
|
set => this.type = (UInt16)value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DnsRecordClass Class {
|
||||||
|
get => (DnsRecordClass)this.klass;
|
||||||
|
set => this.klass = (UInt16)value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TimeSpan TimeToLive {
|
||||||
|
get => TimeSpan.FromSeconds(this.ttl);
|
||||||
|
set => this.ttl = (UInt32)value.TotalSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Int32 DataLength {
|
||||||
|
get => this.dataLength;
|
||||||
|
set => this.dataLength = (UInt16)value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DnsPointerResourceRecord : DnsResourceRecordBase {
|
||||||
|
public DnsPointerResourceRecord(IDnsResourceRecord record, Byte[] message, Int32 dataOffset) : base(record) => this.PointerDomainName = DnsDomain.FromArray(message, dataOffset);
|
||||||
|
|
||||||
|
public DnsDomain PointerDomainName {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override String[] IncludedProperties {
|
||||||
|
get {
|
||||||
|
List<String> temp = new List<String>(base.IncludedProperties) { nameof(this.PointerDomainName) };
|
||||||
|
return temp.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DnsIPAddressResourceRecord : DnsResourceRecordBase {
|
||||||
|
public DnsIPAddressResourceRecord(IDnsResourceRecord record) : base(record) => this.IPAddress = new IPAddress(this.Data);
|
||||||
|
|
||||||
|
public IPAddress IPAddress {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override String[] IncludedProperties => new List<String>(base.IncludedProperties) { nameof(this.IPAddress) }.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DnsNameServerResourceRecord : DnsResourceRecordBase {
|
||||||
|
public DnsNameServerResourceRecord(IDnsResourceRecord record, Byte[] message, Int32 dataOffset) : base(record) => this.NSDomainName = DnsDomain.FromArray(message, dataOffset);
|
||||||
|
|
||||||
|
public DnsDomain NSDomainName {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override String[] IncludedProperties => new List<String>(base.IncludedProperties) { nameof(this.NSDomainName) }.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DnsCanonicalNameResourceRecord : DnsResourceRecordBase {
|
||||||
|
public DnsCanonicalNameResourceRecord(IDnsResourceRecord record, Byte[] message, Int32 dataOffset) : base(record) => this.CanonicalDomainName = DnsDomain.FromArray(message, dataOffset);
|
||||||
|
|
||||||
|
public DnsDomain CanonicalDomainName {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override String[] IncludedProperties => new List<String>(base.IncludedProperties) { nameof(this.CanonicalDomainName) }.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DnsMailExchangeResourceRecord : DnsResourceRecordBase {
|
||||||
|
private const Int32 PreferenceSize = 2;
|
||||||
|
|
||||||
|
public DnsMailExchangeResourceRecord(IDnsResourceRecord record, Byte[] message, Int32 dataOffset)
|
||||||
|
: base(record) {
|
||||||
|
Byte[] preference = new Byte[PreferenceSize];
|
||||||
|
Array.Copy(message, dataOffset, preference, 0, preference.Length);
|
||||||
|
|
||||||
|
if(BitConverter.IsLittleEndian) {
|
||||||
|
Array.Reverse(preference);
|
||||||
|
}
|
||||||
|
|
||||||
|
dataOffset += PreferenceSize;
|
||||||
|
|
||||||
|
this.Preference = BitConverter.ToUInt16(preference, 0);
|
||||||
|
this.ExchangeDomainName = DnsDomain.FromArray(message, dataOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Int32 Preference {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DnsDomain ExchangeDomainName {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override String[] IncludedProperties => new List<String>(base.IncludedProperties)
|
||||||
|
{
|
||||||
|
nameof(this.Preference),
|
||||||
|
nameof(this.ExchangeDomainName),
|
||||||
|
}.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DnsStartOfAuthorityResourceRecord : DnsResourceRecordBase {
|
||||||
|
public DnsStartOfAuthorityResourceRecord(IDnsResourceRecord record, Byte[] message, Int32 dataOffset) : base(record) {
|
||||||
|
this.MasterDomainName = DnsDomain.FromArray(message, dataOffset, out dataOffset);
|
||||||
|
this.ResponsibleDomainName = DnsDomain.FromArray(message, dataOffset, out dataOffset);
|
||||||
|
|
||||||
|
Options tail = message.ToStruct<Options>(dataOffset, Options.SIZE);
|
||||||
|
|
||||||
|
this.SerialNumber = tail.SerialNumber;
|
||||||
|
this.RefreshInterval = tail.RefreshInterval;
|
||||||
|
this.RetryInterval = tail.RetryInterval;
|
||||||
|
this.ExpireInterval = tail.ExpireInterval;
|
||||||
|
this.MinimumTimeToLive = tail.MinimumTimeToLive;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DnsStartOfAuthorityResourceRecord(DnsDomain domain, DnsDomain master, DnsDomain responsible, Int64 serial, TimeSpan refresh, TimeSpan retry, TimeSpan expire, TimeSpan minTtl, TimeSpan ttl = default)
|
||||||
|
: base(Create(domain, master, responsible, serial, refresh, retry, expire, minTtl, ttl)) {
|
||||||
|
this.MasterDomainName = master;
|
||||||
|
this.ResponsibleDomainName = responsible;
|
||||||
|
|
||||||
|
this.SerialNumber = serial;
|
||||||
|
this.RefreshInterval = refresh;
|
||||||
|
this.RetryInterval = retry;
|
||||||
|
this.ExpireInterval = expire;
|
||||||
|
this.MinimumTimeToLive = minTtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DnsDomain MasterDomainName {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DnsDomain ResponsibleDomainName {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Int64 SerialNumber {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TimeSpan RefreshInterval {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TimeSpan RetryInterval {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TimeSpan ExpireInterval {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TimeSpan MinimumTimeToLive {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override String[] IncludedProperties => new List<String>(base.IncludedProperties)
|
||||||
|
{
|
||||||
|
nameof(this.MasterDomainName),
|
||||||
|
nameof(this.ResponsibleDomainName),
|
||||||
|
nameof(this.SerialNumber),
|
||||||
|
}.ToArray();
|
||||||
|
|
||||||
|
private static IDnsResourceRecord Create(DnsDomain domain, DnsDomain master, DnsDomain responsible, Int64 serial, TimeSpan refresh, TimeSpan retry, TimeSpan expire, TimeSpan minTtl, TimeSpan ttl) {
|
||||||
|
MemoryStream data = new MemoryStream(Options.SIZE + master.Size + responsible.Size);
|
||||||
|
Options tail = new Options {
|
||||||
|
SerialNumber = serial,
|
||||||
|
RefreshInterval = refresh,
|
||||||
|
RetryInterval = retry,
|
||||||
|
ExpireInterval = expire,
|
||||||
|
MinimumTimeToLive = minTtl,
|
||||||
|
};
|
||||||
|
|
||||||
|
_ = data.Append(master.ToArray()).Append(responsible.ToArray()).Append(tail.ToBytes());
|
||||||
|
|
||||||
|
return new DnsResourceRecord(domain, data.ToArray(), DnsRecordType.SOA, DnsRecordClass.IN, ttl);
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructEndianness(Endianness.Big)]
|
||||||
|
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||||
|
public struct Options {
|
||||||
|
public const Int32 SIZE = 20;
|
||||||
|
|
||||||
|
private UInt32 serialNumber;
|
||||||
|
private UInt32 refreshInterval;
|
||||||
|
private UInt32 retryInterval;
|
||||||
|
private UInt32 expireInterval;
|
||||||
|
private UInt32 ttl;
|
||||||
|
|
||||||
|
public Int64 SerialNumber {
|
||||||
|
get => this.serialNumber;
|
||||||
|
set => this.serialNumber = (UInt32)value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TimeSpan RefreshInterval {
|
||||||
|
get => TimeSpan.FromSeconds(this.refreshInterval);
|
||||||
|
set => this.refreshInterval = (UInt32)value.TotalSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TimeSpan RetryInterval {
|
||||||
|
get => TimeSpan.FromSeconds(this.retryInterval);
|
||||||
|
set => this.retryInterval = (UInt32)value.TotalSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TimeSpan ExpireInterval {
|
||||||
|
get => TimeSpan.FromSeconds(this.expireInterval);
|
||||||
|
set => this.expireInterval = (UInt32)value.TotalSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TimeSpan MinimumTimeToLive {
|
||||||
|
get => TimeSpan.FromSeconds(this.ttl);
|
||||||
|
set => this.ttl = (UInt32)value.TotalSeconds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class DnsResourceRecordFactory {
|
||||||
|
public static IList<IDnsResourceRecord> GetAllFromArray(Byte[] message, Int32 offset, Int32 count, out Int32 endOffset) {
|
||||||
|
List<IDnsResourceRecord> result = new List<IDnsResourceRecord>(count);
|
||||||
|
|
||||||
|
for(Int32 i = 0; i < count; i++) {
|
||||||
|
result.Add(GetFromArray(message, offset, out offset));
|
||||||
|
}
|
||||||
|
|
||||||
|
endOffset = offset;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IDnsResourceRecord GetFromArray(Byte[] message, Int32 offset, out Int32 endOffset) {
|
||||||
|
DnsResourceRecord record = DnsResourceRecord.FromArray(message, offset, out endOffset);
|
||||||
|
Int32 dataOffset = endOffset - record.DataLength;
|
||||||
|
|
||||||
|
return record.Type switch
|
||||||
|
{
|
||||||
|
DnsRecordType.A => (new DnsIPAddressResourceRecord(record)),
|
||||||
|
DnsRecordType.AAAA => new DnsIPAddressResourceRecord(record),
|
||||||
|
DnsRecordType.NS => new DnsNameServerResourceRecord(record, message, dataOffset),
|
||||||
|
DnsRecordType.CNAME => new DnsCanonicalNameResourceRecord(record, message, dataOffset),
|
||||||
|
DnsRecordType.SOA => new DnsStartOfAuthorityResourceRecord(record, message, dataOffset),
|
||||||
|
DnsRecordType.PTR => new DnsPointerResourceRecord(record, message, dataOffset),
|
||||||
|
DnsRecordType.MX => new DnsMailExchangeResourceRecord(record, message, dataOffset),
|
||||||
|
_ => record
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,215 +1,174 @@
|
|||||||
namespace Swan.Net.Dns
|
using Swan.Formatters;
|
||||||
{
|
using System;
|
||||||
using Formatters;
|
using System.Collections.Generic;
|
||||||
using System;
|
using System.Collections.ObjectModel;
|
||||||
using System.Collections.Generic;
|
using System.IO;
|
||||||
using System.Collections.ObjectModel;
|
using System.Linq;
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
/// <summary>
|
namespace Swan.Net.Dns {
|
||||||
/// DnsClient Response inner class.
|
/// <summary>
|
||||||
/// </summary>
|
/// DnsClient Response inner class.
|
||||||
internal partial class DnsClient
|
/// </summary>
|
||||||
{
|
internal partial class DnsClient {
|
||||||
public class DnsClientResponse : IDnsResponse
|
public class DnsClientResponse : IDnsResponse {
|
||||||
{
|
private readonly DnsResponse _response;
|
||||||
private readonly DnsResponse _response;
|
private readonly Byte[] _message;
|
||||||
private readonly byte[] _message;
|
|
||||||
|
|
||||||
internal DnsClientResponse(DnsClientRequest request, DnsResponse response, byte[] message)
|
internal DnsClientResponse(DnsClientRequest request, DnsResponse response, Byte[] message) {
|
||||||
{
|
this.Request = request;
|
||||||
Request = request;
|
|
||||||
|
|
||||||
_message = message;
|
this._message = message;
|
||||||
_response = response;
|
this._response = response;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DnsClientRequest Request { get; }
|
public DnsClientRequest Request {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
public int Id
|
public Int32 Id {
|
||||||
{
|
get => this._response.Id;
|
||||||
get { return _response.Id; }
|
set {
|
||||||
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
|
public IList<IDnsResourceRecord> AnswerRecords => this._response.AnswerRecords;
|
||||||
{
|
|
||||||
private DnsHeader _header;
|
|
||||||
|
|
||||||
public DnsResponse(
|
public IList<IDnsResourceRecord> AuthorityRecords => new ReadOnlyCollection<IDnsResourceRecord>(this._response.AuthorityRecords);
|
||||||
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> AdditionalRecords => new ReadOnlyCollection<IDnsResourceRecord>(this._response.AdditionalRecords);
|
||||||
|
|
||||||
public IList<IDnsResourceRecord> AnswerRecords { get; }
|
public Boolean IsRecursionAvailable {
|
||||||
|
get => this._response.IsRecursionAvailable;
|
||||||
public IList<IDnsResourceRecord> AuthorityRecords { get; }
|
set {
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean IsAuthorativeServer {
|
||||||
|
get => this._response.IsAuthorativeServer;
|
||||||
|
set {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean IsTruncated {
|
||||||
|
get => this._response.IsTruncated;
|
||||||
|
set {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DnsOperationCode OperationCode {
|
||||||
|
get => this._response.OperationCode;
|
||||||
|
set {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DnsResponseCode ResponseCode {
|
||||||
|
get => this._response.ResponseCode;
|
||||||
|
set {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IList<DnsQuestion> Questions => new ReadOnlyCollection<DnsQuestion>(this._response.Questions);
|
||||||
|
|
||||||
|
public Int32 Size => this._message.Length;
|
||||||
|
|
||||||
|
public Byte[] ToArray() => this._message;
|
||||||
|
|
||||||
|
public override String ToString() => this._response.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class DnsResponse : IDnsResponse {
|
||||||
|
private DnsHeader _header;
|
||||||
|
|
||||||
|
public DnsResponse(DnsHeader header, IList<DnsQuestion> questions, IList<IDnsResourceRecord> answers, IList<IDnsResourceRecord> authority, IList<IDnsResourceRecord> additional) {
|
||||||
|
this._header = header;
|
||||||
|
this.Questions = questions;
|
||||||
|
this.AnswerRecords = answers;
|
||||||
|
this.AuthorityRecords = authority;
|
||||||
|
this.AdditionalRecords = additional;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IList<DnsQuestion> Questions {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IList<IDnsResourceRecord> AnswerRecords {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IList<IDnsResourceRecord> AuthorityRecords {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IList<IDnsResourceRecord> AdditionalRecords {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Int32 Id {
|
||||||
|
get => this._header.Id;
|
||||||
|
set => this._header.Id = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean IsRecursionAvailable {
|
||||||
|
get => this._header.RecursionAvailable;
|
||||||
|
set => this._header.RecursionAvailable = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean IsAuthorativeServer {
|
||||||
|
get => this._header.AuthorativeServer;
|
||||||
|
set => this._header.AuthorativeServer = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean IsTruncated {
|
||||||
|
get => this._header.Truncated;
|
||||||
|
set => this._header.Truncated = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DnsOperationCode OperationCode {
|
||||||
|
get => this._header.OperationCode;
|
||||||
|
set => this._header.OperationCode = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DnsResponseCode ResponseCode {
|
||||||
|
get => this._header.ResponseCode;
|
||||||
|
set => this._header.ResponseCode = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Int32 Size => this._header.Size + this.Questions.Sum(q => q.Size) + this.AnswerRecords.Sum(a => a.Size) + this.AuthorityRecords.Sum(a => a.Size) + this.AdditionalRecords.Sum(a => a.Size);
|
||||||
|
|
||||||
|
public static DnsResponse FromArray(Byte[] message) {
|
||||||
|
DnsHeader header = DnsHeader.FromArray(message);
|
||||||
|
Int32 offset = header.Size;
|
||||||
|
|
||||||
|
if(!header.Response || header.QuestionCount == 0) {
|
||||||
|
throw new ArgumentException("Invalid response message");
|
||||||
|
}
|
||||||
|
|
||||||
|
return header.Truncated
|
||||||
|
? new DnsResponse(header, DnsQuestion.GetAllFromArray(message, offset, header.QuestionCount), new List<IDnsResourceRecord>(), new List<IDnsResourceRecord>(), new List<IDnsResourceRecord>())
|
||||||
|
: new DnsResponse(header, DnsQuestion.GetAllFromArray(message, offset, header.QuestionCount, out offset), DnsResourceRecordFactory.GetAllFromArray(message, offset, header.AnswerRecordCount, out offset), DnsResourceRecordFactory.GetAllFromArray(message, offset, header.AuthorityRecordCount, out offset), DnsResourceRecordFactory.GetAllFromArray(message, offset, header.AdditionalRecordCount, out _));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Byte[] ToArray() {
|
||||||
|
this.UpdateHeader();
|
||||||
|
MemoryStream result = new MemoryStream(this.Size);
|
||||||
|
|
||||||
|
_ = result.Append(this._header.ToArray()).Append(this.Questions.Select(q => q.ToArray())).Append(this.AnswerRecords.Select(a => a.ToArray())).Append(this.AuthorityRecords.Select(a => a.ToArray())).Append(this.AdditionalRecords.Select(a => a.ToArray()));
|
||||||
|
|
||||||
|
return result.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override String ToString() {
|
||||||
|
this.UpdateHeader();
|
||||||
|
|
||||||
|
return Json.SerializeOnly(this, true, nameof(this.Questions), nameof(this.AnswerRecords), nameof(this.AuthorityRecords), nameof(this.AdditionalRecords));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateHeader() {
|
||||||
|
this._header.QuestionCount = this.Questions.Count;
|
||||||
|
this._header.AnswerRecordCount = this.AnswerRecords.Count;
|
||||||
|
this._header.AuthorityRecordCount = this.AuthorityRecords.Count;
|
||||||
|
this._header.AdditionalRecordCount = this.AdditionalRecords.Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,79 +1,65 @@
|
|||||||
namespace Swan.Net.Dns
|
using System;
|
||||||
{
|
using System.Collections.Generic;
|
||||||
using System;
|
using System.Linq;
|
||||||
using System.Collections.Generic;
|
#nullable enable
|
||||||
using System.Linq;
|
using System.Net;
|
||||||
using System.Net;
|
using System.Threading.Tasks;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
/// <summary>
|
namespace Swan.Net.Dns {
|
||||||
/// DnsClient public methods.
|
/// <summary>
|
||||||
/// </summary>
|
/// DnsClient public methods.
|
||||||
internal partial class DnsClient
|
/// </summary>
|
||||||
{
|
internal partial class DnsClient {
|
||||||
private readonly IPEndPoint _dns;
|
private readonly IPEndPoint _dns;
|
||||||
private readonly IDnsRequestResolver _resolver;
|
private readonly IDnsRequestResolver _resolver;
|
||||||
|
|
||||||
public DnsClient(IPEndPoint dns, IDnsRequestResolver? resolver = null)
|
public DnsClient(IPEndPoint dns, IDnsRequestResolver? resolver = null) {
|
||||||
{
|
this._dns = dns;
|
||||||
_dns = dns;
|
this._resolver = resolver ?? new DnsUdpRequestResolver(new DnsTcpRequestResolver());
|
||||||
_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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DnsClient(IPAddress ip, Int32 port = Network.DnsDefaultPort, IDnsRequestResolver? resolver = null) : this(new IPEndPoint(ip, port), resolver) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public DnsClientRequest Create(IDnsRequest? request = null) => new DnsClientRequest(this._dns, request, this._resolver);
|
||||||
|
|
||||||
|
public async Task<IList<IPAddress>> Lookup(String domain, DnsRecordType type = DnsRecordType.A) {
|
||||||
|
if(String.IsNullOrWhiteSpace(domain)) {
|
||||||
|
throw new ArgumentNullException(nameof(domain));
|
||||||
|
}
|
||||||
|
|
||||||
|
if(type != DnsRecordType.A && type != DnsRecordType.AAAA) {
|
||||||
|
throw new ArgumentException("Invalid record type " + type);
|
||||||
|
}
|
||||||
|
|
||||||
|
DnsClientResponse response = await this.Resolve(domain, type).ConfigureAwait(false);
|
||||||
|
List<IPAddress> ips = response.AnswerRecords.Where(r => r.Type == type).Cast<DnsIPAddressResourceRecord>().Select(r => r.IPAddress).ToList();
|
||||||
|
|
||||||
|
return ips.Count == 0 ? throw new DnsQueryException(response, "No matching records") : ips;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<String> Reverse(IPAddress ip) {
|
||||||
|
if(ip == null) {
|
||||||
|
throw new ArgumentNullException(nameof(ip));
|
||||||
|
}
|
||||||
|
|
||||||
|
DnsClientResponse response = await this.Resolve(DnsDomain.PointerName(ip), DnsRecordType.PTR);
|
||||||
|
IDnsResourceRecord ptr = response.AnswerRecords.FirstOrDefault(r => r.Type == DnsRecordType.PTR);
|
||||||
|
|
||||||
|
return ptr == null ? throw new DnsQueryException(response, "No matching records") : ((DnsPointerResourceRecord)ptr).PointerDomainName.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<DnsClientResponse> Resolve(String domain, DnsRecordType type) => this.Resolve(new DnsDomain(domain), type);
|
||||||
|
|
||||||
|
public Task<DnsClientResponse> Resolve(DnsDomain domain, DnsRecordType type) {
|
||||||
|
DnsClientRequest request = this.Create();
|
||||||
|
DnsQuestion question = new DnsQuestion(domain, type);
|
||||||
|
|
||||||
|
request.Questions.Add(question);
|
||||||
|
request.OperationCode = DnsOperationCode.Query;
|
||||||
|
request.RecursionDesired = true;
|
||||||
|
|
||||||
|
return request.Resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,37 +1,28 @@
|
|||||||
namespace Swan.Net.Dns
|
#nullable enable
|
||||||
{
|
using System;
|
||||||
using System;
|
|
||||||
|
|
||||||
/// <summary>
|
namespace Swan.Net.Dns {
|
||||||
/// An exception thrown when the DNS query fails.
|
/// <summary>
|
||||||
/// </summary>
|
/// An exception thrown when the DNS query fails.
|
||||||
/// <seealso cref="Exception" />
|
/// </summary>
|
||||||
[Serializable]
|
/// <seealso cref="Exception" />
|
||||||
public class DnsQueryException : Exception
|
[Serializable]
|
||||||
{
|
public class DnsQueryException : Exception {
|
||||||
internal DnsQueryException(string message)
|
internal DnsQueryException(String message) : base(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}";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal DnsQueryException(String message, Exception e) : base(message, e) {
|
||||||
|
}
|
||||||
|
|
||||||
|
internal DnsQueryException(DnsClient.IDnsResponse response) : this(response, Format(response)) {
|
||||||
|
}
|
||||||
|
|
||||||
|
internal DnsQueryException(DnsClient.IDnsResponse response, String message) : base(message) => this.Response = response;
|
||||||
|
|
||||||
|
internal DnsClient.IDnsResponse? Response {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String Format(DnsClient.IDnsResponse response) => $"Invalid response received with code {response.ResponseCode}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,123 +1,130 @@
|
|||||||
namespace Swan.Net.Dns
|
namespace Swan.Net.Dns {
|
||||||
{
|
using System.Collections.Generic;
|
||||||
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>
|
/// <summary>
|
||||||
/// Represents a response from a DNS server.
|
/// Initializes a new instance of the <see cref="DnsQueryResult"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DnsQueryResult
|
/// <param name="response">The response.</param>
|
||||||
{
|
internal DnsQueryResult(DnsClient.IDnsResponse response) : this() {
|
||||||
private readonly List<DnsRecord> _mAnswerRecords = new List<DnsRecord>();
|
this.Id = response.Id;
|
||||||
private readonly List<DnsRecord> _mAdditionalRecords = new List<DnsRecord>();
|
this.IsAuthoritativeServer = response.IsAuthorativeServer;
|
||||||
private readonly List<DnsRecord> _mAuthorityRecords = new List<DnsRecord>();
|
this.IsRecursionAvailable = response.IsRecursionAvailable;
|
||||||
|
this.IsTruncated = response.IsTruncated;
|
||||||
|
this.OperationCode = response.OperationCode;
|
||||||
|
this.ResponseCode = response.ResponseCode;
|
||||||
|
|
||||||
/// <summary>
|
if(response.AnswerRecords != null) {
|
||||||
/// Initializes a new instance of the <see cref="DnsQueryResult"/> class.
|
foreach(DnsClient.IDnsResourceRecord record in response.AnswerRecords) {
|
||||||
/// </summary>
|
this.AnswerRecords.Add(new DnsRecord(record));
|
||||||
/// <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()
|
if(response.AuthorityRecords != null) {
|
||||||
{
|
foreach(DnsClient.IDnsResourceRecord record in response.AuthorityRecords) {
|
||||||
|
this.AuthorityRecords.Add(new DnsRecord(record));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
if(response.AdditionalRecords != null) {
|
||||||
/// Gets the identifier.
|
foreach(DnsClient.IDnsResourceRecord record in response.AdditionalRecords) {
|
||||||
/// </summary>
|
this.AdditionalRecords.Add(new DnsRecord(record));
|
||||||
/// <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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private DnsQueryResult() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the identifier.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// The identifier.
|
||||||
|
/// </value>
|
||||||
|
public System.Int32 Id {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether this instance is authoritative server.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// <c>true</c> if this instance is authoritative server; otherwise, <c>false</c>.
|
||||||
|
/// </value>
|
||||||
|
public System.Boolean IsAuthoritativeServer {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether this instance is truncated.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// <c>true</c> if this instance is truncated; otherwise, <c>false</c>.
|
||||||
|
/// </value>
|
||||||
|
public System.Boolean IsTruncated {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether this instance is recursion available.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// <c>true</c> if this instance is recursion available; otherwise, <c>false</c>.
|
||||||
|
/// </value>
|
||||||
|
public System.Boolean IsRecursionAvailable {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the operation code.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// The operation code.
|
||||||
|
/// </value>
|
||||||
|
public DnsOperationCode OperationCode {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the response code.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// The response code.
|
||||||
|
/// </value>
|
||||||
|
public DnsResponseCode ResponseCode {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the answer records.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// The answer records.
|
||||||
|
/// </value>
|
||||||
|
public IList<DnsRecord> AnswerRecords => this._mAnswerRecords;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the additional records.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// The additional records.
|
||||||
|
/// </value>
|
||||||
|
public IList<DnsRecord> AdditionalRecords => this._mAdditionalRecords;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the authority records.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// The authority records.
|
||||||
|
/// </value>
|
||||||
|
public IList<DnsRecord> AuthorityRecords => this._mAuthorityRecords;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,208 +1,239 @@
|
|||||||
namespace Swan.Net.Dns
|
using System;
|
||||||
{
|
using System.Net;
|
||||||
using System;
|
using System.Text;
|
||||||
using System.Net;
|
|
||||||
using System.Text;
|
namespace Swan.Net.Dns {
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a DNS record entry.
|
||||||
|
/// </summary>
|
||||||
|
public class DnsRecord {
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="DnsRecord"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="record">The record.</param>
|
||||||
|
internal DnsRecord(DnsClient.IDnsResourceRecord record) : this() {
|
||||||
|
this.Name = record.Name.ToString();
|
||||||
|
this.Type = record.Type;
|
||||||
|
this.Class = record.Class;
|
||||||
|
this.TimeToLive = record.TimeToLive;
|
||||||
|
this.Data = record.Data;
|
||||||
|
|
||||||
|
// PTR
|
||||||
|
this.PointerDomainName = (record as DnsClient.DnsPointerResourceRecord)?.PointerDomainName?.ToString();
|
||||||
|
|
||||||
|
// A
|
||||||
|
this.IPAddress = (record as DnsClient.DnsIPAddressResourceRecord)?.IPAddress;
|
||||||
|
|
||||||
|
// NS
|
||||||
|
this.NameServerDomainName = (record as DnsClient.DnsNameServerResourceRecord)?.NSDomainName?.ToString();
|
||||||
|
|
||||||
|
// CNAME
|
||||||
|
this.CanonicalDomainName = (record as DnsClient.DnsCanonicalNameResourceRecord)?.CanonicalDomainName.ToString();
|
||||||
|
|
||||||
|
// MX
|
||||||
|
this.MailExchangerDomainName = (record as DnsClient.DnsMailExchangeResourceRecord)?.ExchangeDomainName.ToString();
|
||||||
|
this.MailExchangerPreference = (record as DnsClient.DnsMailExchangeResourceRecord)?.Preference;
|
||||||
|
|
||||||
|
// SOA
|
||||||
|
this.SoaMasterDomainName = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.MasterDomainName.ToString();
|
||||||
|
this.SoaResponsibleDomainName = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.ResponsibleDomainName.ToString();
|
||||||
|
this.SoaSerialNumber = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.SerialNumber;
|
||||||
|
this.SoaRefreshInterval = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.RefreshInterval;
|
||||||
|
this.SoaRetryInterval = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.RetryInterval;
|
||||||
|
this.SoaExpireInterval = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.ExpireInterval;
|
||||||
|
this.SoaMinimumTimeToLive = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.MinimumTimeToLive;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DnsRecord() {
|
||||||
|
// placeholder
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a DNS record entry.
|
/// Gets the name.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DnsRecord
|
/// <value>
|
||||||
{
|
/// The name.
|
||||||
/// <summary>
|
/// </value>
|
||||||
/// Initializes a new instance of the <see cref="DnsRecord"/> class.
|
public String Name {
|
||||||
/// </summary>
|
get;
|
||||||
/// <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; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the type.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// The type.
|
||||||
|
/// </value>
|
||||||
|
public DnsRecordType Type {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the class.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// The class.
|
||||||
|
/// </value>
|
||||||
|
public DnsRecordClass Class {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the time to live.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// The time to live.
|
||||||
|
/// </value>
|
||||||
|
public TimeSpan TimeToLive {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the raw data of the record.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// The data.
|
||||||
|
/// </value>
|
||||||
|
public Byte[] Data {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the data text bytes in ASCII encoding.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// The data text.
|
||||||
|
/// </value>
|
||||||
|
public String DataText => this.Data == null ? String.Empty : Encoding.ASCII.GetString(this.Data);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the name of the pointer domain.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// The name of the pointer domain.
|
||||||
|
/// </value>
|
||||||
|
public String PointerDomainName {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the ip address.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// The ip address.
|
||||||
|
/// </value>
|
||||||
|
public IPAddress IPAddress {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the name of the name server domain.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// The name of the name server domain.
|
||||||
|
/// </value>
|
||||||
|
public String NameServerDomainName {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the name of the canonical domain.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// The name of the canonical domain.
|
||||||
|
/// </value>
|
||||||
|
public String CanonicalDomainName {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the mail exchanger preference.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// The mail exchanger preference.
|
||||||
|
/// </value>
|
||||||
|
public Int32? MailExchangerPreference {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the name of the mail exchanger domain.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// The name of the mail exchanger domain.
|
||||||
|
/// </value>
|
||||||
|
public String MailExchangerDomainName {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the name of the soa master domain.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// The name of the soa master domain.
|
||||||
|
/// </value>
|
||||||
|
public String SoaMasterDomainName {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the name of the soa responsible domain.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// The name of the soa responsible domain.
|
||||||
|
/// </value>
|
||||||
|
public String SoaResponsibleDomainName {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the soa serial number.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// The soa serial number.
|
||||||
|
/// </value>
|
||||||
|
public Int64? SoaSerialNumber {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the soa refresh interval.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// The soa refresh interval.
|
||||||
|
/// </value>
|
||||||
|
public TimeSpan? SoaRefreshInterval {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the soa retry interval.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// The soa retry interval.
|
||||||
|
/// </value>
|
||||||
|
public TimeSpan? SoaRetryInterval {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the soa expire interval.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// The soa expire interval.
|
||||||
|
/// </value>
|
||||||
|
public TimeSpan? SoaExpireInterval {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the soa minimum time to live.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// The soa minimum time to live.
|
||||||
|
/// </value>
|
||||||
|
public TimeSpan? SoaMinimumTimeToLive {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,172 +1,167 @@
|
|||||||
// ReSharper disable InconsistentNaming
|
// ReSharper disable InconsistentNaming
|
||||||
namespace Swan.Net.Dns
|
namespace Swan.Net.Dns {
|
||||||
{
|
/// <summary>
|
||||||
|
/// Enumerates the different DNS record types.
|
||||||
|
/// </summary>
|
||||||
|
public enum DnsRecordType {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Enumerates the different DNS record types.
|
/// A records
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum DnsRecordType
|
A = 1,
|
||||||
{
|
|
||||||
/// <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>
|
/// <summary>
|
||||||
/// Enumerates the different DNS record classes.
|
/// Nameserver records
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum DnsRecordClass
|
NS = 2,
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// IN records
|
|
||||||
/// </summary>
|
|
||||||
IN = 1,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ANY records
|
|
||||||
/// </summary>
|
|
||||||
ANY = 255,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Enumerates the different DNS operation codes.
|
/// CNAME records
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum DnsOperationCode
|
CNAME = 5,
|
||||||
{
|
|
||||||
/// <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>
|
/// <summary>
|
||||||
/// Enumerates the different DNS query response codes.
|
/// SOA records
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum DnsResponseCode
|
SOA = 6,
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// No error
|
|
||||||
/// </summary>
|
|
||||||
NoError = 0,
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// No error
|
/// WKS records
|
||||||
/// </summary>
|
/// </summary>
|
||||||
FormatError,
|
WKS = 11,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Format error
|
/// PTR records
|
||||||
/// </summary>
|
/// </summary>
|
||||||
ServerFailure,
|
PTR = 12,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Server failure error
|
/// MX records
|
||||||
/// </summary>
|
/// </summary>
|
||||||
NameError,
|
MX = 15,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Name error
|
/// TXT records
|
||||||
/// </summary>
|
/// </summary>
|
||||||
NotImplemented,
|
TXT = 16,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Not implemented error
|
/// A records fot IPv6
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Refused,
|
AAAA = 28,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Refused error
|
/// SRV records
|
||||||
/// </summary>
|
/// </summary>
|
||||||
YXDomain,
|
SRV = 33,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// YXRR error
|
/// ANY records
|
||||||
/// </summary>
|
/// </summary>
|
||||||
YXRRSet,
|
ANY = 255,
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// NXRR Set error
|
/// Enumerates the different DNS record classes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
NXRRSet,
|
public enum DnsRecordClass {
|
||||||
|
/// <summary>
|
||||||
|
/// IN records
|
||||||
|
/// </summary>
|
||||||
|
IN = 1,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Not authorized error
|
/// ANY records
|
||||||
/// </summary>
|
/// </summary>
|
||||||
NotAuth,
|
ANY = 255,
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Not zone error
|
/// Enumerates the different DNS operation codes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
NotZone,
|
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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,158 +1,157 @@
|
|||||||
namespace Swan.Net
|
#nullable enable
|
||||||
{
|
using System;
|
||||||
using System;
|
using System.Net;
|
||||||
using System.Net;
|
using System.Net.Sockets;
|
||||||
using System.Net.Sockets;
|
|
||||||
|
namespace Swan.Net {
|
||||||
|
/// <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) => this.Client = client ?? throw new ArgumentNullException(nameof(client));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The event arguments for when connections are accepted.
|
/// Gets the client.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <seealso cref="System.EventArgs" />
|
/// <value>
|
||||||
public class ConnectionAcceptedEventArgs : EventArgs
|
/// The client.
|
||||||
{
|
/// </value>
|
||||||
/// <summary>
|
public TcpClient Client {
|
||||||
/// Initializes a new instance of the <see cref="ConnectionAcceptedEventArgs" /> class.
|
get;
|
||||||
/// </summary>
|
}
|
||||||
/// <param name="client">The client.</param>
|
}
|
||||||
/// <exception cref="ArgumentNullException">client.</exception>
|
|
||||||
public ConnectionAcceptedEventArgs(TcpClient client)
|
|
||||||
{
|
|
||||||
Client = client ?? throw new ArgumentNullException(nameof(client));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the client.
|
/// Occurs before a connection is accepted. Set the Cancel property to true to prevent the connection from being accepted.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
/// <seealso cref="ConnectionAcceptedEventArgs" />
|
||||||
/// The client.
|
public class ConnectionAcceptingEventArgs : ConnectionAcceptedEventArgs {
|
||||||
/// </value>
|
/// <summary>
|
||||||
public TcpClient Client { get; }
|
/// Initializes a new instance of the <see cref="ConnectionAcceptingEventArgs"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client">The client.</param>
|
||||||
|
public ConnectionAcceptingEventArgs(TcpClient client) : base(client) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Occurs before a connection is accepted. Set the Cancel property to true to prevent the connection from being accepted.
|
/// Setting Cancel to true rejects the new TcpClient.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <seealso cref="ConnectionAcceptedEventArgs" />
|
/// <value>
|
||||||
public class ConnectionAcceptingEventArgs : ConnectionAcceptedEventArgs
|
/// <c>true</c> if cancel; otherwise, <c>false</c>.
|
||||||
{
|
/// </value>
|
||||||
/// <summary>
|
public Boolean Cancel {
|
||||||
/// Initializes a new instance of the <see cref="ConnectionAcceptingEventArgs"/> class.
|
get; set;
|
||||||
/// </summary>
|
}
|
||||||
/// <param name="client">The client.</param>
|
}
|
||||||
public ConnectionAcceptingEventArgs(TcpClient client)
|
|
||||||
: base(client)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Setting Cancel to true rejects the new TcpClient.
|
/// Event arguments for when a server listener is started.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
/// <seealso cref="System.EventArgs" />
|
||||||
/// <c>true</c> if cancel; otherwise, <c>false</c>.
|
public class ConnectionListenerStartedEventArgs : EventArgs {
|
||||||
/// </value>
|
/// <summary>
|
||||||
public bool Cancel { get; set; }
|
/// 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) => this.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) {
|
||||||
|
this.EndPoint = listenerEndPoint ?? throw new ArgumentNullException(nameof(listenerEndPoint));
|
||||||
|
this.Error = ex ?? throw new ArgumentNullException(nameof(ex));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Event arguments for when a server listener is started.
|
/// Gets the end point.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <seealso cref="System.EventArgs" />
|
/// <value>
|
||||||
public class ConnectionListenerStartedEventArgs : EventArgs
|
/// The end point.
|
||||||
{
|
/// </value>
|
||||||
/// <summary>
|
public IPEndPoint EndPoint {
|
||||||
/// Initializes a new instance of the <see cref="ConnectionListenerStartedEventArgs" /> class.
|
get;
|
||||||
/// </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>
|
/// <summary>
|
||||||
/// Event arguments for when a server listener fails to start.
|
/// Gets the error.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <seealso cref="System.EventArgs" />
|
/// <value>
|
||||||
public class ConnectionListenerFailedEventArgs : EventArgs
|
/// The error.
|
||||||
{
|
/// </value>
|
||||||
/// <summary>
|
public Exception Error {
|
||||||
/// Initializes a new instance of the <see cref="ConnectionListenerFailedEventArgs" /> class.
|
get;
|
||||||
/// </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>
|
/// <summary>
|
||||||
/// Gets the end point.
|
/// Event arguments for when a server listener stopped.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
/// <seealso cref="System.EventArgs" />
|
||||||
/// The end point.
|
public class ConnectionListenerStoppedEventArgs : EventArgs {
|
||||||
/// </value>
|
/// <summary>
|
||||||
public IPEndPoint EndPoint { get; }
|
/// Initializes a new instance of the <see cref="ConnectionListenerStoppedEventArgs" /> class.
|
||||||
|
/// </summary>
|
||||||
/// <summary>
|
/// <param name="listenerEndPoint">The listener end point.</param>
|
||||||
/// Gets the error.
|
/// <param name="ex">The ex.</param>
|
||||||
/// </summary>
|
/// <exception cref="ArgumentNullException">
|
||||||
/// <value>
|
/// listenerEndPoint
|
||||||
/// The error.
|
/// or
|
||||||
/// </value>
|
/// ex.
|
||||||
public Exception Error { get; }
|
/// </exception>
|
||||||
|
public ConnectionListenerStoppedEventArgs(IPEndPoint listenerEndPoint, Exception? ex = null) {
|
||||||
|
this.EndPoint = listenerEndPoint ?? throw new ArgumentNullException(nameof(listenerEndPoint));
|
||||||
|
this.Error = ex;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Event arguments for when a server listener stopped.
|
/// Gets the end point.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <seealso cref="System.EventArgs" />
|
/// <value>
|
||||||
public class ConnectionListenerStoppedEventArgs : EventArgs
|
/// The end point.
|
||||||
{
|
/// </value>
|
||||||
/// <summary>
|
public IPEndPoint EndPoint {
|
||||||
/// Initializes a new instance of the <see cref="ConnectionListenerStoppedEventArgs" /> class.
|
get;
|
||||||
/// </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; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the error.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// The error.
|
||||||
|
/// </value>
|
||||||
|
public Exception? Error {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,84 +1,84 @@
|
|||||||
namespace Swan.Net
|
using System;
|
||||||
{
|
using System.Text;
|
||||||
using System;
|
|
||||||
using System.Text;
|
namespace Swan.Net {
|
||||||
|
/// <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) => this.Error = ex;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The event arguments for connection failure events.
|
/// Gets the error.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <seealso cref="System.EventArgs" />
|
/// <value>
|
||||||
public class ConnectionFailureEventArgs : EventArgs
|
/// The error.
|
||||||
{
|
/// </value>
|
||||||
/// <summary>
|
public Exception Error {
|
||||||
/// Initializes a new instance of the <see cref="ConnectionFailureEventArgs"/> class.
|
get;
|
||||||
/// </summary>
|
}
|
||||||
/// <param name="ex">The ex.</param>
|
}
|
||||||
public ConnectionFailureEventArgs(Exception ex)
|
|
||||||
{
|
|
||||||
Error = ex;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the error.
|
/// Event arguments for when data is received.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
/// <seealso cref="System.EventArgs" />
|
||||||
/// The error.
|
public class ConnectionDataReceivedEventArgs : EventArgs {
|
||||||
/// </value>
|
/// <summary>
|
||||||
public Exception Error { get; }
|
/// 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, Boolean moreAvailable) {
|
||||||
|
this.Buffer = buffer ?? throw new ArgumentNullException(nameof(buffer));
|
||||||
|
this.Trigger = trigger;
|
||||||
|
this.HasMoreAvailable = moreAvailable;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Event arguments for when data is received.
|
/// Gets the buffer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <seealso cref="System.EventArgs" />
|
/// <value>
|
||||||
public class ConnectionDataReceivedEventArgs : EventArgs
|
/// The buffer.
|
||||||
{
|
/// </value>
|
||||||
/// <summary>
|
public Byte[] Buffer {
|
||||||
/// Initializes a new instance of the <see cref="ConnectionDataReceivedEventArgs"/> class.
|
get;
|
||||||
/// </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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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 Boolean 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(this.Buffer).TrimEnd('\r', '\n') ?? throw new ArgumentNullException(nameof(encoding));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,418 +1,313 @@
|
|||||||
namespace Swan.Net
|
#nullable enable
|
||||||
{
|
using Swan.Formatters;
|
||||||
using Formatters;
|
using System;
|
||||||
using System;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Generic;
|
using System.Net.Http;
|
||||||
using System.Net.Http;
|
using System.Net.Http.Headers;
|
||||||
using System.Net.Http.Headers;
|
using System.Security;
|
||||||
using System.Security;
|
using System.Text;
|
||||||
using System.Text;
|
using System.Threading;
|
||||||
using System.Threading;
|
using System.Threading.Tasks;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
namespace Swan.Net {
|
||||||
|
/// <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>
|
/// <summary>
|
||||||
/// Represents a HttpClient with extended methods to use with JSON payloads
|
/// Post a object as JSON with optional authorization token.
|
||||||
/// and bearer tokens authentication.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class JsonClient
|
/// <typeparam name="T">The type of response object.</typeparam>
|
||||||
{
|
/// <param name="requestUri">The request URI.</param>
|
||||||
private const string JsonMimeType = "application/json";
|
/// <param name="payload">The payload.</param>
|
||||||
private const string FormType = "application/x-www-form-urlencoded";
|
/// <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) where T : notnull {
|
||||||
|
String jsonString = await PostString(requestUri, payload, authorization, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
private static readonly HttpClient HttpClient = new HttpClient();
|
return !String.IsNullOrEmpty(jsonString) ? Json.Deserialize<T>(jsonString) : default;
|
||||||
|
|
||||||
/// <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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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) {
|
||||||
|
String 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) where T : notnull {
|
||||||
|
String 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) {
|
||||||
|
Object 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) {
|
||||||
|
HttpContent 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) where T : notnull {
|
||||||
|
String 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) where T : notnull {
|
||||||
|
String 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) {
|
||||||
|
HttpContent 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
|
||||||
|
String content = $"grant_type=password&username={username}&password={password}";
|
||||||
|
using StringContent requestContent = new StringContent(content, Encoding.UTF8, FormType);
|
||||||
|
HttpResponseMessage response = await HttpClient.PostAsync(requestUri, requestContent, ct).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if(!response.IsSuccessStatusCode) {
|
||||||
|
throw new SecurityException($"Error Authenticating. Status code: {response.StatusCode}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
String 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) where T : notnull => 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 HttpResponseMessage response = await GetResponse(requestUri, authorization, null, payload, method, ct).ConfigureAwait(false);
|
||||||
|
if(!response.IsSuccessStatusCode) {
|
||||||
|
throw new JsonRequestException(
|
||||||
|
$"Error {method} JSON",
|
||||||
|
(Int32)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) {
|
||||||
|
HttpResponseMessage response = await GetResponse(uri, authorization, headers, ct: ct).ConfigureAwait(false);
|
||||||
|
|
||||||
|
return response.IsSuccessStatusCode ? response.Content : throw new JsonRequestException("Error GET", (Int32)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 HttpRequestMessage requestMessage = new HttpRequestMessage(method ?? HttpMethod.Get, uri);
|
||||||
|
|
||||||
|
if(!String.IsNullOrWhiteSpace(authorization)) {
|
||||||
|
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authorization);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(headers != null) {
|
||||||
|
foreach(KeyValuePair<String, IEnumerable<String>> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,47 +1,44 @@
|
|||||||
namespace Swan.Net
|
using System;
|
||||||
{
|
|
||||||
using System;
|
namespace Swan.Net {
|
||||||
|
/// <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, Int32 httpErrorCode = 500, String errorContent = null) : base(message) {
|
||||||
|
this.HttpErrorCode = httpErrorCode;
|
||||||
|
this.HttpErrorContent = errorContent;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents errors that occurs requesting a JSON file through HTTP.
|
/// Gets the HTTP error code.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <seealso cref="System.Exception" />
|
/// <value>
|
||||||
[Serializable]
|
/// The HTTP error code.
|
||||||
public class JsonRequestException
|
/// </value>
|
||||||
: Exception
|
public Int32 HttpErrorCode {
|
||||||
{
|
get;
|
||||||
/// <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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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(this.HttpErrorContent) ? $"HTTP Response Status Code {this.HttpErrorCode} Error Message: {this.HttpErrorContent}" : base.ToString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,328 +1,289 @@
|
|||||||
namespace Swan.Net
|
using Swan.Net.Dns;
|
||||||
{
|
using System;
|
||||||
using Net.Dns;
|
using System.Collections.Generic;
|
||||||
using System;
|
using System.Linq;
|
||||||
using System.Collections.Generic;
|
using System.Net;
|
||||||
using System.Linq;
|
using System.Net.Http;
|
||||||
using System.Net;
|
using System.Net.NetworkInformation;
|
||||||
using System.Net.Http;
|
using System.Net.Sockets;
|
||||||
using System.Net.NetworkInformation;
|
using System.Threading;
|
||||||
using System.Net.Sockets;
|
using System.Threading.Tasks;
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
namespace Swan.Net {
|
||||||
|
/// <summary>
|
||||||
|
/// Provides miscellaneous network utilities such as a Public IP finder,
|
||||||
|
/// a DNS client to query DNS records of any kind, and an NTP client.
|
||||||
|
/// </summary>
|
||||||
|
public static class Network {
|
||||||
|
/// <summary>
|
||||||
|
/// The DNS default port.
|
||||||
|
/// </summary>
|
||||||
|
public const Int32 DnsDefaultPort = 53;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides miscellaneous network utilities such as a Public IP finder,
|
/// The NTP default port.
|
||||||
/// a DNS client to query DNS records of any kind, and an NTP client.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class Network
|
public const Int32 NtpDefaultPort = 123;
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The DNS default port.
|
|
||||||
/// </summary>
|
|
||||||
public const int DnsDefaultPort = 53;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The NTP default port.
|
/// Gets the name of the host.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const int NtpDefaultPort = 123;
|
/// <value>
|
||||||
|
/// The name of the host.
|
||||||
|
/// </value>
|
||||||
|
public static String HostName => IPGlobalProperties.GetIPGlobalProperties().HostName;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the name of the host.
|
/// Gets the name of the network domain.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
/// <value>
|
||||||
/// The name of the host.
|
/// The name of the network domain.
|
||||||
/// </value>
|
/// </value>
|
||||||
public static string HostName => IPGlobalProperties.GetIPGlobalProperties().HostName;
|
public static String DomainName => IPGlobalProperties.GetIPGlobalProperties().DomainName;
|
||||||
|
|
||||||
/// <summary>
|
#region IP Addresses and Adapters Information Methods
|
||||||
/// Gets the name of the network domain.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// The name of the network domain.
|
|
||||||
/// </value>
|
|
||||||
public static string DomainName => IPGlobalProperties.GetIPGlobalProperties().DomainName;
|
|
||||||
|
|
||||||
#region IP Addresses and Adapters Information Methods
|
/// <summary>
|
||||||
|
/// Gets the active IPv4 interfaces.
|
||||||
|
/// Only those interfaces with a valid unicast address and a valid gateway will be returned in the collection.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// A collection of NetworkInterface/IPInterfaceProperties pairs
|
||||||
|
/// that represents the active IPv4 interfaces.
|
||||||
|
/// </returns>
|
||||||
|
public static Dictionary<NetworkInterface, IPInterfaceProperties> GetIPv4Interfaces() {
|
||||||
|
// zero conf ip address
|
||||||
|
IPAddress zeroConf = new IPAddress(0);
|
||||||
|
|
||||||
/// <summary>
|
NetworkInterface[] adapters = NetworkInterface.GetAllNetworkInterfaces().Where(network => network.OperationalStatus == OperationalStatus.Up && network.NetworkInterfaceType != NetworkInterfaceType.Unknown && network.NetworkInterfaceType != NetworkInterfaceType.Loopback).ToArray();
|
||||||
/// 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()
|
Dictionary<NetworkInterface, IPInterfaceProperties> result = new Dictionary<NetworkInterface, IPInterfaceProperties>();
|
||||||
.Where(network =>
|
|
||||||
network.OperationalStatus == OperationalStatus.Up
|
|
||||||
&& network.NetworkInterfaceType != NetworkInterfaceType.Unknown
|
|
||||||
&& network.NetworkInterfaceType != NetworkInterfaceType.Loopback)
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
var result = new Dictionary<NetworkInterface, IPInterfaceProperties>();
|
foreach(NetworkInterface adapter in adapters) {
|
||||||
|
IPInterfaceProperties properties = adapter.GetIPProperties();
|
||||||
foreach (var adapter in adapters)
|
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;
|
||||||
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>
|
result[adapter] = properties;
|
||||||
/// 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>
|
return result;
|
||||||
/// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the local ip addresses.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="includeLoopback">if set to <c>true</c> [include loopback].</param>
|
||||||
|
/// <returns>An array of local ip addresses.</returns>
|
||||||
|
public static IPAddress[] GetIPv4Addresses(Boolean includeLoopback = true) => GetIPv4Addresses(NetworkInterfaceType.Unknown, true, includeLoopback);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the local ip addresses.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="interfaceType">Type of the interface.</param>
|
||||||
|
/// <param name="skipTypeFilter">if set to <c>true</c> [skip type filter].</param>
|
||||||
|
/// <param name="includeLoopback">if set to <c>true</c> [include loopback].</param>
|
||||||
|
/// <returns>An array of local ip addresses.</returns>
|
||||||
|
public static IPAddress[] GetIPv4Addresses(NetworkInterfaceType interfaceType, Boolean skipTypeFilter = false, Boolean includeLoopback = false) {
|
||||||
|
List<IPAddress> addressList = new List<IPAddress>();
|
||||||
|
NetworkInterface[] interfaces = NetworkInterface.GetAllNetworkInterfaces()
|
||||||
|
.Where(ni => (skipTypeFilter || ni.NetworkInterfaceType == interfaceType) && ni.OperationalStatus == OperationalStatus.Up).ToArray();
|
||||||
|
|
||||||
|
foreach(NetworkInterface networkInterface in interfaces) {
|
||||||
|
IPInterfaceProperties properties = networkInterface.GetIPProperties();
|
||||||
|
|
||||||
|
if(properties.GatewayAddresses.All(g => g.Address.AddressFamily != AddressFamily.InterNetwork)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
addressList.AddRange(properties.UnicastAddresses.Where(i => i.Address.AddressFamily == AddressFamily.InterNetwork).Select(i => i.Address));
|
||||||
|
}
|
||||||
|
|
||||||
|
if(includeLoopback || interfaceType == NetworkInterfaceType.Loopback) {
|
||||||
|
addressList.Add(IPAddress.Loopback);
|
||||||
|
}
|
||||||
|
|
||||||
|
return addressList.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the public IP address using ipify.org.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
|
/// <returns>A public IP address of the result produced by this Task.</returns>
|
||||||
|
public static async Task<IPAddress> GetPublicIPAddressAsync(CancellationToken cancellationToken = default) {
|
||||||
|
using HttpClient client = new HttpClient();
|
||||||
|
HttpResponseMessage response = await client.GetAsync("https://api.ipify.org", cancellationToken).ConfigureAwait(false);
|
||||||
|
return IPAddress.Parse(await response.Content.ReadAsStringAsync().ConfigureAwait(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the configured IPv4 DNS servers for the active network interfaces.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// A collection of NetworkInterface/IPInterfaceProperties pairs
|
||||||
|
/// that represents the active IPv4 interfaces.
|
||||||
|
/// </returns>
|
||||||
|
public static IPAddress[] GetIPv4DnsServers() => GetIPv4Interfaces().Select(a => a.Value.DnsAddresses.Where(d => d.AddressFamily == AddressFamily.InterNetwork)).SelectMany(d => d).ToArray();
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region DNS and NTP Clients
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the DNS host entry (a list of IP addresses) for the domain name.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="fqdn">The FQDN.</param>
|
||||||
|
/// <returns>An array of local ip addresses of the result produced by this task.</returns>
|
||||||
|
public static Task<IPAddress[]> GetDnsHostEntryAsync(String fqdn) {
|
||||||
|
IPAddress dnsServer = GetIPv4DnsServers().FirstOrDefault() ?? IPAddress.Parse("8.8.8.8");
|
||||||
|
return GetDnsHostEntryAsync(fqdn, dnsServer, DnsDefaultPort);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the DNS host entry (a list of IP addresses) for the domain name.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="fqdn">The FQDN.</param>
|
||||||
|
/// <param name="dnsServer">The DNS server.</param>
|
||||||
|
/// <param name="port">The port.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// An array of local ip addresses of the result produced by this task.
|
||||||
|
/// </returns>
|
||||||
|
/// <exception cref="ArgumentNullException">fqdn.</exception>
|
||||||
|
public static async Task<IPAddress[]> GetDnsHostEntryAsync(String fqdn, IPAddress dnsServer, Int32 port) {
|
||||||
|
if(fqdn == null) {
|
||||||
|
throw new ArgumentNullException(nameof(fqdn));
|
||||||
|
}
|
||||||
|
|
||||||
|
if(fqdn.IndexOf(".", StringComparison.Ordinal) == -1) {
|
||||||
|
fqdn += "." + IPGlobalProperties.GetIPGlobalProperties().DomainName;
|
||||||
|
}
|
||||||
|
|
||||||
|
while(true) {
|
||||||
|
if(!fqdn.EndsWith(".", StringComparison.OrdinalIgnoreCase)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
fqdn = fqdn[0..^1];
|
||||||
|
}
|
||||||
|
|
||||||
|
DnsClient client = new DnsClient(dnsServer, port);
|
||||||
|
IList<IPAddress> result = await client.Lookup(fqdn).ConfigureAwait(false);
|
||||||
|
return result.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the reverse lookup FQDN of the given IP Address.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="query">The query.</param>
|
||||||
|
/// <param name="dnsServer">The DNS server.</param>
|
||||||
|
/// <param name="port">The port.</param>
|
||||||
|
/// <returns>A <see cref="System.String" /> that represents the current object.</returns>
|
||||||
|
public static Task<String> GetDnsPointerEntryAsync(IPAddress query, IPAddress dnsServer, Int32 port) {
|
||||||
|
DnsClient client = new DnsClient(dnsServer, port);
|
||||||
|
return client.Reverse(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the reverse lookup FQDN of the given IP Address.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="query">The query.</param>
|
||||||
|
/// <returns>A <see cref="System.String" /> that represents the current object.</returns>
|
||||||
|
public static Task<String> GetDnsPointerEntryAsync(IPAddress query) {
|
||||||
|
DnsClient client = new DnsClient(GetIPv4DnsServers().FirstOrDefault());
|
||||||
|
return client.Reverse(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Queries the DNS server for the specified record type.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="query">The query.</param>
|
||||||
|
/// <param name="recordType">Type of the record.</param>
|
||||||
|
/// <param name="dnsServer">The DNS server.</param>
|
||||||
|
/// <param name="port">The port.</param>
|
||||||
|
/// <returns>Queries the DNS server for the specified record type of the result produced by this Task.</returns>
|
||||||
|
public static async Task<DnsQueryResult> QueryDnsAsync(String query, DnsRecordType recordType, IPAddress dnsServer, Int32 port) {
|
||||||
|
if(query == null) {
|
||||||
|
throw new ArgumentNullException(nameof(query));
|
||||||
|
}
|
||||||
|
|
||||||
|
DnsClient client = new DnsClient(dnsServer, port);
|
||||||
|
DnsClient.DnsClientResponse response = await client.Resolve(query, recordType).ConfigureAwait(false);
|
||||||
|
return new DnsQueryResult(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Queries the DNS server for the specified record type.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="query">The query.</param>
|
||||||
|
/// <param name="recordType">Type of the record.</param>
|
||||||
|
/// <returns>Queries the DNS server for the specified record type of the result produced by this Task.</returns>
|
||||||
|
public static Task<DnsQueryResult> QueryDnsAsync(String query, DnsRecordType recordType) => QueryDnsAsync(query, recordType, GetIPv4DnsServers().FirstOrDefault(), DnsDefaultPort);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the UTC time by querying from an NTP server.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ntpServerAddress">The NTP server address.</param>
|
||||||
|
/// <param name="port">The port.</param>
|
||||||
|
/// <returns>The UTC time by querying from an NTP server of the result produced by this Task.</returns>
|
||||||
|
public static async Task<DateTime> GetNetworkTimeUtcAsync(IPAddress ntpServerAddress, Int32 port = NtpDefaultPort) {
|
||||||
|
if(ntpServerAddress == null) {
|
||||||
|
throw new ArgumentNullException(nameof(ntpServerAddress));
|
||||||
|
}
|
||||||
|
|
||||||
|
// NTP message size - 16 bytes of the digest (RFC 2030)
|
||||||
|
Byte[] ntpData = new Byte[48];
|
||||||
|
|
||||||
|
// Setting the Leap Indicator, Version Number and Mode values
|
||||||
|
ntpData[0] = 0x1B; // LI = 0 (no warning), VN = 3 (IPv4 only), Mode = 3 (Client Mode)
|
||||||
|
|
||||||
|
// The UDP port number assigned to NTP is 123
|
||||||
|
IPEndPoint endPoint = new IPEndPoint(ntpServerAddress, port);
|
||||||
|
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
|
||||||
|
|
||||||
|
|
||||||
|
await socket.ConnectAsync(endPoint).ConfigureAwait(false);
|
||||||
|
|
||||||
|
|
||||||
|
socket.ReceiveTimeout = 3000; // Stops code hang if NTP is blocked
|
||||||
|
_ = socket.Send(ntpData);
|
||||||
|
_ = socket.Receive(ntpData);
|
||||||
|
socket.Dispose();
|
||||||
|
|
||||||
|
// Offset to get to the "Transmit Timestamp" field (time at which the reply
|
||||||
|
// departed the server for the client, in 64-bit timestamp format."
|
||||||
|
const Byte serverReplyTime = 40;
|
||||||
|
|
||||||
|
// Get the seconds part
|
||||||
|
UInt64 intPart = BitConverter.ToUInt32(ntpData, serverReplyTime);
|
||||||
|
|
||||||
|
// Get the seconds fraction
|
||||||
|
UInt64 fractPart = BitConverter.ToUInt32(ntpData, serverReplyTime + 4);
|
||||||
|
|
||||||
|
// Convert From big-endian to little-endian to match the platform
|
||||||
|
if(BitConverter.IsLittleEndian) {
|
||||||
|
intPart = intPart.SwapEndianness();
|
||||||
|
fractPart = intPart.SwapEndianness();
|
||||||
|
}
|
||||||
|
|
||||||
|
UInt64 milliseconds = intPart * 1000 + fractPart * 1000 / 0x100000000L;
|
||||||
|
|
||||||
|
// The time is given in UTC
|
||||||
|
return new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds((Int64)milliseconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the UTC time by querying from an NTP server.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ntpServerName">The NTP server, by default pool.ntp.org.</param>
|
||||||
|
/// <param name="port">The port, by default NTP 123.</param>
|
||||||
|
/// <returns>The UTC time by querying from an NTP server of the result produced by this Task.</returns>
|
||||||
|
public static async Task<DateTime> GetNetworkTimeUtcAsync(String ntpServerName = "pool.ntp.org", Int32 port = NtpDefaultPort) {
|
||||||
|
IPAddress[] addresses = await GetDnsHostEntryAsync(ntpServerName).ConfigureAwait(false);
|
||||||
|
return await GetNetworkTimeUtcAsync(addresses.First(), port).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,166 +1,162 @@
|
|||||||
// ReSharper disable InconsistentNaming
|
// ReSharper disable InconsistentNaming
|
||||||
namespace Swan.Net.Smtp
|
namespace Swan.Net.Smtp {
|
||||||
{
|
/// <summary>
|
||||||
|
/// Enumerates all of the well-known SMTP command names.
|
||||||
|
/// </summary>
|
||||||
|
public enum SmtpCommandNames {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Enumerates all of the well-known SMTP command names.
|
/// An unknown command
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum SmtpCommandNames
|
Unknown,
|
||||||
{
|
|
||||||
/// <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>
|
/// <summary>
|
||||||
/// Enumerates the reply code severities.
|
/// The helo command
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum SmtpReplyCodeSeverities
|
HELO,
|
||||||
{
|
|
||||||
/// <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>
|
/// <summary>
|
||||||
/// Enumerates the reply code categories.
|
/// The ehlo command
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum SmtpReplyCodeCategories
|
EHLO,
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The unknown category
|
|
||||||
/// </summary>
|
|
||||||
Unknown = -1,
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The syntax category
|
/// The quit command
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Syntax = 0,
|
QUIT,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The information category
|
/// The help command
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Information = 1,
|
HELP,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The connections category
|
/// The noop command
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Connections = 2,
|
NOOP,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The unspecified a category
|
/// The rset command
|
||||||
/// </summary>
|
/// </summary>
|
||||||
UnspecifiedA = 3,
|
RSET,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The unspecified b category
|
/// The mail command
|
||||||
/// </summary>
|
/// </summary>
|
||||||
UnspecifiedB = 4,
|
MAIL,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The system category
|
/// The data command
|
||||||
/// </summary>
|
/// </summary>
|
||||||
System = 5,
|
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,
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,388 +1,370 @@
|
|||||||
namespace Swan.Net.Smtp
|
#nullable enable
|
||||||
{
|
using System.Threading;
|
||||||
using System.Threading;
|
using System;
|
||||||
using System;
|
using System.Linq;
|
||||||
using System.Linq;
|
using System.Net;
|
||||||
using System.Net;
|
using System.Net.Sockets;
|
||||||
using System.Net.Sockets;
|
using System.Security;
|
||||||
using System.Security;
|
using System.Text;
|
||||||
using System.Text;
|
using System.Net.Security;
|
||||||
using System.Net.Security;
|
using System.Threading.Tasks;
|
||||||
using System.Threading.Tasks;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Generic;
|
using System.Net.Mail;
|
||||||
using System.Net.Mail;
|
|
||||||
|
namespace Swan.Net.Smtp {
|
||||||
|
/// <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, Int32 port) {
|
||||||
|
this.Host = host ?? throw new ArgumentNullException(nameof(host));
|
||||||
|
this.Port = port;
|
||||||
|
this.ClientHostname = Network.HostName;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a basic SMTP client that is capable of submitting messages to an SMTP server.
|
/// Gets or sets the credentials. No credentials will be used if set to null.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <example>
|
/// <value>
|
||||||
/// The following code explains how to send a simple e-mail.
|
/// The credentials.
|
||||||
/// <code>
|
/// </value>
|
||||||
/// using System.Net.Mail;
|
public NetworkCredential? Credentials {
|
||||||
///
|
get; set;
|
||||||
/// class Example
|
}
|
||||||
/// {
|
|
||||||
/// static void Main()
|
/// <summary>
|
||||||
/// {
|
/// Gets the host.
|
||||||
/// // create a new smtp client using google's smtp server
|
/// </summary>
|
||||||
/// var client = new Swan.Net.Smtp.SmtpClient("smtp.gmail.com", 587);
|
/// <value>
|
||||||
///
|
/// The host.
|
||||||
/// // send an email
|
/// </value>
|
||||||
/// client.SendMailAsync(
|
public String Host {
|
||||||
/// new MailMessage("sender@test.com", "recipient@test.cm", "Subject", "Body"));
|
get;
|
||||||
/// }
|
}
|
||||||
/// }
|
|
||||||
/// </code>
|
/// <summary>
|
||||||
///
|
/// Gets the port.
|
||||||
/// The following code demonstrates how to sent an e-mail using a SmtpSessionState:
|
/// </summary>
|
||||||
/// <code>
|
/// <value>
|
||||||
/// using Swan.Net.Smtp;
|
/// The port.
|
||||||
///
|
/// </value>
|
||||||
/// class Example
|
public Int32 Port {
|
||||||
/// {
|
get;
|
||||||
/// static void Main()
|
}
|
||||||
/// {
|
|
||||||
/// // create a new smtp client using google's smtp server
|
/// <summary>
|
||||||
/// var client = new SmtpClient("smtp.gmail.com", 587);
|
/// Gets or sets a value indicating whether the SSL is enabled.
|
||||||
///
|
/// If set to false, communication between client and server will not be secured.
|
||||||
/// // create a new session state with a sender address
|
/// </summary>
|
||||||
/// var session = new SmtpSessionState { SenderAddress = "sender@test.com" };
|
/// <value>
|
||||||
///
|
/// <c>true</c> if [enable SSL]; otherwise, <c>false</c>.
|
||||||
/// // add a recipient
|
/// </value>
|
||||||
/// session.Recipients.Add("recipient@test.cm");
|
public Boolean EnableSsl {
|
||||||
///
|
get; set;
|
||||||
/// // send
|
}
|
||||||
/// client.SendMailAsync(session);
|
|
||||||
/// }
|
/// <summary>
|
||||||
/// }
|
/// Gets or sets the name of the client that gets announced to the server.
|
||||||
/// </code>
|
/// </summary>
|
||||||
///
|
/// <value>
|
||||||
/// The following code shows how to send an e-mail with an attachment using MimeKit:
|
/// The client hostname.
|
||||||
/// <code>
|
/// </value>
|
||||||
/// using MimeKit;
|
public String ClientHostname {
|
||||||
/// using Swan.Net.Smtp;
|
get; set;
|
||||||
///
|
}
|
||||||
/// class Example
|
|
||||||
/// {
|
|
||||||
/// static void Main()
|
/// <summary>
|
||||||
/// {
|
/// Sends an email message asynchronously.
|
||||||
/// // create a new smtp client using google's smtp server
|
/// </summary>
|
||||||
/// var client = new SmtpClient("smtp.gmail.com", 587);
|
/// <param name="message">The message.</param>
|
||||||
///
|
/// <param name="sessionId">The session identifier.</param>
|
||||||
/// // create a new session state with a sender address
|
/// <param name="callback">The callback.</param>
|
||||||
/// var session = new SmtpSessionState { SenderAddress = "sender@test.com" };
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
///
|
/// <returns>
|
||||||
/// // add a recipient
|
/// A task that represents the asynchronous of send email operation.
|
||||||
/// session.Recipients.Add("recipient@test.cm");
|
/// </returns>
|
||||||
///
|
/// <exception cref="ArgumentNullException">message.</exception>
|
||||||
/// // load a file as an attachment
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("Codequalität", "IDE0067:Objekte verwerfen, bevor Bereich verloren geht", Justification = "<Ausstehend>")]
|
||||||
/// var attachment = new MimePart("image", "gif")
|
public Task SendMailAsync(MailMessage message, String? sessionId = null, RemoteCertificateValidationCallback? callback = null, CancellationToken cancellationToken = default) {
|
||||||
/// {
|
if(message == null) {
|
||||||
/// Content = new
|
throw new ArgumentNullException(nameof(message));
|
||||||
/// MimeContent(File.OpenRead("meme.gif"), ContentEncoding.Default),
|
}
|
||||||
/// ContentDisposition =
|
|
||||||
/// new ContentDisposition(ContentDisposition.Attachment),
|
SmtpSessionState state = new SmtpSessionState {
|
||||||
/// ContentTransferEncoding = ContentEncoding.Base64,
|
AuthMode = this.Credentials == null ? String.Empty : SmtpDefinitions.SmtpAuthMethods.Login,
|
||||||
/// FileName = Path.GetFileName("meme.gif")
|
ClientHostname = ClientHostname,
|
||||||
/// };
|
IsChannelSecure = EnableSsl,
|
||||||
///
|
SenderAddress = message.From.Address,
|
||||||
/// // send
|
};
|
||||||
/// client.SendMailAsync(session);
|
|
||||||
/// }
|
if(this.Credentials != null) {
|
||||||
/// }
|
state.Username = this.Credentials.UserName;
|
||||||
/// </code>
|
state.Password = this.Credentials.Password;
|
||||||
/// </example>
|
}
|
||||||
public class SmtpClient
|
|
||||||
{
|
foreach(MailAddress recipient in message.To) {
|
||||||
/// <summary>
|
state.Recipients.Add(recipient.Address);
|
||||||
/// Initializes a new instance of the <see cref="SmtpClient" /> class.
|
}
|
||||||
/// </summary>
|
|
||||||
/// <param name="host">The host.</param>
|
state.DataBuffer.AddRange(message.ToMimeMessage().ToArray());
|
||||||
/// <param name="port">The port.</param>
|
|
||||||
/// <exception cref="ArgumentNullException">host.</exception>
|
return this.SendMailAsync(state, sessionId, callback, cancellationToken);
|
||||||
public SmtpClient(string host, int port)
|
}
|
||||||
{
|
|
||||||
Host = host ?? throw new ArgumentNullException(nameof(host));
|
/// <summary>
|
||||||
Port = port;
|
/// Sends an email message using a session state object.
|
||||||
ClientHostname = Network.HostName;
|
/// 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 this.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 TcpClient tcpClient = new TcpClient();
|
||||||
|
await tcpClient.ConnectAsync(this.Host, this.Port).ConfigureAwait(false);
|
||||||
|
|
||||||
|
using Connection connection = new Connection(tcpClient, Encoding.UTF8, "\r\n", true, 1000);
|
||||||
|
SmtpSender sender = new SmtpSender(sessionId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Read the greeting message
|
||||||
|
sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// EHLO 1
|
||||||
|
await this.SendEhlo(sender, connection, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// STARTTLS
|
||||||
|
if(this.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.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
// EHLO 2
|
||||||
/// Gets or sets the credentials. No credentials will be used if set to null.
|
await this.SendEhlo(sender, connection, cancellationToken).ConfigureAwait(false);
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// The credentials.
|
|
||||||
/// </value>
|
|
||||||
public NetworkCredential Credentials { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
// AUTH
|
||||||
/// Gets the host.
|
if(this.Credentials != null) {
|
||||||
/// </summary>
|
ConnectionAuth auth = new ConnectionAuth(connection, sender, this.Credentials);
|
||||||
/// <value>
|
await auth.AuthenticateAsync(cancellationToken).ConfigureAwait(false);
|
||||||
/// 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>
|
foreach(SmtpSessionState sessionState in sessionStates) {
|
||||||
/// Sends an email message using a session state object.
|
{
|
||||||
/// Credentials, Enable SSL and Client Hostname are NOT taken from the state object but
|
// MAIL FROM
|
||||||
/// rather from the properties of this class.
|
sender.RequestText = $"{SmtpCommandNames.MAIL} FROM:<{sessionState.SenderAddress}>";
|
||||||
/// </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);
|
await connection.WriteLineAsync(sender.RequestText, cancellationToken).ConfigureAwait(false);
|
||||||
|
sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false);
|
||||||
do
|
|
||||||
{
|
|
||||||
sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
while (!sender.IsReplyOk);
|
|
||||||
|
|
||||||
sender.ValidateReply();
|
sender.ValidateReply();
|
||||||
|
}
|
||||||
|
|
||||||
|
// RCPT TO
|
||||||
|
foreach(String 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
|
||||||
|
String 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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ConnectionAuth
|
|
||||||
{
|
{
|
||||||
private readonly SmtpSender _sender;
|
// QUIT
|
||||||
private readonly Connection _connection;
|
sender.RequestText = $"{SmtpCommandNames.QUIT}";
|
||||||
private readonly NetworkCredential _credentials;
|
|
||||||
|
|
||||||
public ConnectionAuth(Connection connection, SmtpSender sender, NetworkCredential credentials)
|
await connection.WriteLineAsync(sender.RequestText, cancellationToken).ConfigureAwait(false);
|
||||||
{
|
sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false);
|
||||||
_connection = connection;
|
sender.ValidateReply();
|
||||||
_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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} 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} {this.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) {
|
||||||
|
this._connection = connection;
|
||||||
|
this._sender = sender;
|
||||||
|
this._credentials = credentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AuthenticateAsync(CancellationToken ct) {
|
||||||
|
this._sender.RequestText = $"{SmtpCommandNames.AUTH} {SmtpDefinitions.SmtpAuthMethods.Login} {Convert.ToBase64String(Encoding.UTF8.GetBytes(this._credentials.UserName))}";
|
||||||
|
|
||||||
|
await this._connection.WriteLineAsync(this._sender.RequestText, ct).ConfigureAwait(false);
|
||||||
|
this._sender.ReplyText = await this._connection.ReadLineAsync(ct).ConfigureAwait(false);
|
||||||
|
this._sender.ValidateReply();
|
||||||
|
this._sender.RequestText = Convert.ToBase64String(Encoding.UTF8.GetBytes(this._credentials.Password));
|
||||||
|
|
||||||
|
await this._connection.WriteLineAsync(this._sender.RequestText, ct).ConfigureAwait(false);
|
||||||
|
this._sender.ReplyText = await this._connection.ReadLineAsync(ct).ConfigureAwait(false);
|
||||||
|
this._sender.ValidateReply();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,29 +1,28 @@
|
|||||||
namespace Swan.Net.Smtp
|
using System;
|
||||||
{
|
|
||||||
|
namespace Swan.Net.Smtp {
|
||||||
|
/// <summary>
|
||||||
|
/// Contains useful constants and definitions.
|
||||||
|
/// </summary>
|
||||||
|
public static class SmtpDefinitions {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Contains useful constants and definitions.
|
/// The string sequence that delimits the end of the DATA command.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class SmtpDefinitions
|
public const String SmtpDataCommandTerminator = "\r\n.\r\n";
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The string sequence that delimits the end of the DATA command.
|
|
||||||
/// </summary>
|
|
||||||
public const string SmtpDataCommandTerminator = "\r\n.\r\n";
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Lists the AUTH methods supported by default.
|
/// Lists the AUTH methods supported by default.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class SmtpAuthMethods
|
public static class SmtpAuthMethods {
|
||||||
{
|
/// <summary>
|
||||||
/// <summary>
|
/// The plain method.
|
||||||
/// The plain method.
|
/// </summary>
|
||||||
/// </summary>
|
public const String Plain = "PLAIN";
|
||||||
public const string Plain = "PLAIN";
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The login method.
|
/// The login method.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string Login = "LOGIN";
|
public const String Login = "LOGIN";
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,60 +1,53 @@
|
|||||||
namespace Swan.Net.Smtp
|
using Swan.Logging;
|
||||||
{
|
using System;
|
||||||
using Logging;
|
using System.Linq;
|
||||||
using System;
|
using System.Net.Mail;
|
||||||
using System.Linq;
|
|
||||||
using System.Net.Mail;
|
|
||||||
|
|
||||||
/// <summary>
|
namespace Swan.Net.Smtp {
|
||||||
/// Use this class to store the sender session data.
|
/// <summary>
|
||||||
/// </summary>
|
/// Use this class to store the sender session data.
|
||||||
internal class SmtpSender
|
/// </summary>
|
||||||
{
|
internal class SmtpSender {
|
||||||
private readonly string _sessionId;
|
private readonly String _sessionId;
|
||||||
private string _requestText;
|
private String _requestText;
|
||||||
|
|
||||||
public SmtpSender(string sessionId)
|
public SmtpSender(String sessionId) => this._sessionId = sessionId;
|
||||||
{
|
|
||||||
_sessionId = sessionId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string RequestText
|
public String RequestText {
|
||||||
{
|
get => this._requestText;
|
||||||
get => _requestText;
|
set {
|
||||||
set
|
this._requestText = value;
|
||||||
{
|
$" TX {this._requestText}".Trace(typeof(SmtpClient), this._sessionId);
|
||||||
_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}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String ReplyText {
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean IsReplyOk => this.ReplyText.StartsWith("250 ", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public void ValidateReply() {
|
||||||
|
if(this.ReplyText == null) {
|
||||||
|
throw new SmtpException("There was no response from the server");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
SmtpServerReply response = SmtpServerReply.Parse(this.ReplyText);
|
||||||
|
$" RX {this.ReplyText} - {response.IsPositive}".Trace(typeof(SmtpClient), this._sessionId);
|
||||||
|
|
||||||
|
if(response.IsPositive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String 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: {this.ReplyText}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,243 +1,256 @@
|
|||||||
namespace Swan.Net.Smtp
|
using System;
|
||||||
{
|
using System.Collections.Generic;
|
||||||
using System;
|
using System.Globalization;
|
||||||
using System.Collections.Generic;
|
using System.Linq;
|
||||||
using System.Globalization;
|
using System.Text;
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
namespace Swan.Net.Smtp {
|
||||||
|
/// <summary>
|
||||||
|
/// Represents an SMTP server response object.
|
||||||
|
/// </summary>
|
||||||
|
public class SmtpServerReply {
|
||||||
|
#region Constructors
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents an SMTP server response object.
|
/// Initializes a new instance of the <see cref="SmtpServerReply"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SmtpServerReply
|
/// <param name="responseCode">The response code.</param>
|
||||||
{
|
/// <param name="statusCode">The status code.</param>
|
||||||
#region Constructors
|
/// <param name="content">The content.</param>
|
||||||
|
public SmtpServerReply(Int32 responseCode, String statusCode, params String[] content) {
|
||||||
|
this.Content = new List<String>();
|
||||||
|
this.ReplyCode = responseCode;
|
||||||
|
this.EnhancedStatusCode = statusCode;
|
||||||
|
this.Content.AddRange(content);
|
||||||
|
this.IsValid = responseCode >= 200 && responseCode < 600;
|
||||||
|
this.ReplyCodeSeverity = SmtpReplyCodeSeverities.Unknown;
|
||||||
|
this.ReplyCodeCategory = SmtpReplyCodeCategories.Unknown;
|
||||||
|
|
||||||
/// <summary>
|
if(!this.IsValid) {
|
||||||
/// Initializes a new instance of the <see cref="SmtpServerReply"/> class.
|
return;
|
||||||
/// </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) {
|
||||||
if (responseCode >= 200) ReplyCodeSeverity = SmtpReplyCodeSeverities.PositiveCompletion;
|
this.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(responseCode >= 300) {
|
||||||
{
|
this.ReplyCodeSeverity = SmtpReplyCodeSeverities.PositiveIntermediate;
|
||||||
if (middleDigit >= 0 && middleDigit <= 5)
|
}
|
||||||
ReplyCodeCategory = (SmtpReplyCodeCategories) middleDigit;
|
|
||||||
}
|
if(responseCode >= 400) {
|
||||||
|
this.ReplyCodeSeverity = SmtpReplyCodeSeverities.TransientNegative;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(responseCode >= 500) {
|
||||||
|
this.ReplyCodeSeverity = SmtpReplyCodeSeverities.PermanentNegative;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(responseCode >= 600) {
|
||||||
|
this.ReplyCodeSeverity = SmtpReplyCodeSeverities.Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(Int32.TryParse(responseCode.ToString(CultureInfo.InvariantCulture).Substring(1, 1), out Int32 middleDigit)) {
|
||||||
|
if(middleDigit >= 0 && middleDigit <= 5) {
|
||||||
|
this.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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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(Int32 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(Int32 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 Int32 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 Boolean IsValid {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether this instance is positive.
|
||||||
|
/// </summary>
|
||||||
|
public Boolean IsPositive => this.ReplyCode >= 200 && this.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) {
|
||||||
|
String[] lines = text.Split(new[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
if(lines.Length == 0) {
|
||||||
|
return new SmtpServerReply();
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] lastLineParts = lines.Last().Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
String enhancedStatusCode = String.Empty;
|
||||||
|
_ = Int32.TryParse(lastLineParts[0], out Int32 responseCode);
|
||||||
|
if(lastLineParts.Length > 1) {
|
||||||
|
if(lastLineParts[1].Split('.').Length == 3) {
|
||||||
|
enhancedStatusCode = lastLineParts[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> content = new List<String>();
|
||||||
|
|
||||||
|
for(Int32 i = 0; i < lines.Length; i++) {
|
||||||
|
String splitChar = i == lines.Length - 1 ? " " : "-";
|
||||||
|
|
||||||
|
String[] lineParts = lines[i].Split(new[] { splitChar }, 2, StringSplitOptions.None);
|
||||||
|
String 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() {
|
||||||
|
String responseCodeText = this.ReplyCode.ToString(CultureInfo.InvariantCulture);
|
||||||
|
String statusCodeText = String.IsNullOrWhiteSpace(this.EnhancedStatusCode) ? String.Empty : $" {this.EnhancedStatusCode.Trim()}";
|
||||||
|
if(this.Content.Count == 0) {
|
||||||
|
return $"{responseCodeText}{statusCodeText}";
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
|
||||||
|
for(Int32 i = 0; i < this.Content.Count; i++) {
|
||||||
|
Boolean isLastLine = i == this.Content.Count - 1;
|
||||||
|
|
||||||
|
_ = builder.Append(isLastLine ? $"{responseCodeText}{statusCodeText} {this.Content[i]}" : $"{responseCodeText}-{this.Content[i]}\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,158 +1,179 @@
|
|||||||
namespace Swan.Net.Smtp
|
using System.Collections.Generic;
|
||||||
{
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
|
namespace Swan.Net.Smtp {
|
||||||
|
/// <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() {
|
||||||
|
this.DataBuffer = new List<Byte>();
|
||||||
|
this.Reset(true);
|
||||||
|
this.ResetAuthentication();
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Properties
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the state of an SMTP session associated with a client.
|
/// Gets the contents of the data buffer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SmtpSessionState
|
public List<Byte> DataBuffer {
|
||||||
{
|
get; protected set;
|
||||||
/// <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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether this instance has initiated.
|
||||||
|
/// </summary>
|
||||||
|
public Boolean HasInitiated {
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether the current session supports extensions.
|
||||||
|
/// </summary>
|
||||||
|
public Boolean 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 Boolean 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 Boolean 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 Boolean HasProvidedUsername => String.IsNullOrWhiteSpace(this.Username) == false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether this instance is authenticated.
|
||||||
|
/// </summary>
|
||||||
|
public Boolean 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 Boolean IsChannelSecure {
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resets the authentication state.
|
||||||
|
/// </summary>
|
||||||
|
public void ResetAuthentication() {
|
||||||
|
this.Username = String.Empty;
|
||||||
|
this.Password = String.Empty;
|
||||||
|
this.AuthMode = String.Empty;
|
||||||
|
this.IsInAuthMode = false;
|
||||||
|
this.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() {
|
||||||
|
this.IsInDataMode = false;
|
||||||
|
this.Recipients.Clear();
|
||||||
|
this.SenderAddress = String.Empty;
|
||||||
|
this.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(Boolean clearExtensionData) {
|
||||||
|
this.HasInitiated = false;
|
||||||
|
this.SupportsExtensions = false;
|
||||||
|
this.ClientHostname = String.Empty;
|
||||||
|
this.ResetEmail();
|
||||||
|
|
||||||
|
if(clearExtensionData) {
|
||||||
|
this.ExtendedData = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new object that is a copy of the current instance.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A clone.</returns>
|
||||||
|
public virtual SmtpSessionState Clone() {
|
||||||
|
SmtpSessionState clonedState = this.CopyPropertiesToNew<SmtpSessionState>(new[] { nameof(this.DataBuffer) });
|
||||||
|
clonedState.DataBuffer.AddRange(this.DataBuffer);
|
||||||
|
clonedState.Recipients.AddRange(this.Recipients);
|
||||||
|
|
||||||
|
return clonedState;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,46 +1,51 @@
|
|||||||
namespace Swan
|
using System;
|
||||||
{
|
|
||||||
|
namespace Swan {
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the text of the standard output and standard error
|
||||||
|
/// of a process, including its exit code.
|
||||||
|
/// </summary>
|
||||||
|
public class ProcessResult {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the text of the standard output and standard error
|
/// Initializes a new instance of the <see cref="ProcessResult" /> class.
|
||||||
/// of a process, including its exit code.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ProcessResult
|
/// <param name="exitCode">The exit code.</param>
|
||||||
{
|
/// <param name="standardOutput">The standard output.</param>
|
||||||
/// <summary>
|
/// <param name="standardError">The standard error.</param>
|
||||||
/// Initializes a new instance of the <see cref="ProcessResult" /> class.
|
public ProcessResult(Int32 exitCode, String standardOutput, String standardError) {
|
||||||
/// </summary>
|
this.ExitCode = exitCode;
|
||||||
/// <param name="exitCode">The exit code.</param>
|
this.StandardOutput = standardOutput;
|
||||||
/// <param name="standardOutput">The standard output.</param>
|
this.StandardError = standardError;
|
||||||
/// <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; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the exit code.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// The exit code.
|
||||||
|
/// </value>
|
||||||
|
public Int32 ExitCode {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the text of the standard output.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// The standard output.
|
||||||
|
/// </value>
|
||||||
|
public String StandardOutput {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the text of the standard error.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// The standard error.
|
||||||
|
/// </value>
|
||||||
|
public String StandardError {
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,443 +1,353 @@
|
|||||||
namespace Swan
|
#nullable enable
|
||||||
{
|
using System;
|
||||||
using System;
|
using System.Diagnostics;
|
||||||
using System.Diagnostics;
|
using System.IO;
|
||||||
using System.IO;
|
using System.Linq;
|
||||||
using System.Linq;
|
using System.Text;
|
||||||
using System.Text;
|
using System.Threading;
|
||||||
using System.Threading;
|
using System.Threading.Tasks;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
namespace Swan {
|
||||||
|
/// <summary>
|
||||||
|
/// Provides methods to help create external processes, and efficiently capture the
|
||||||
|
/// standard error and standard output streams.
|
||||||
|
/// </summary>
|
||||||
|
public static class ProcessRunner {
|
||||||
|
/// <summary>
|
||||||
|
/// Defines a delegate to handle binary data reception from the standard
|
||||||
|
/// output or standard error streams from a process.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="processData">The process data.</param>
|
||||||
|
/// <param name="process">The process.</param>
|
||||||
|
public delegate void ProcessDataReceivedCallback(Byte[] processData, Process process);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides methods to help create external processes, and efficiently capture the
|
/// Runs the process asynchronously and if the exit code is 0,
|
||||||
/// standard error and standard output streams.
|
/// 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>
|
/// </summary>
|
||||||
public static class ProcessRunner
|
/// <param name="filename">The filename.</param>
|
||||||
{
|
/// <param name="arguments">The arguments.</param>
|
||||||
/// <summary>
|
/// <param name="workingDirectory">The working directory.</param>
|
||||||
/// Defines a delegate to handle binary data reception from the standard
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
/// output or standard error streams from a process.
|
/// <returns>The type of the result produced by this Task.</returns>
|
||||||
/// </summary>
|
/// <example>
|
||||||
/// <param name="processData">The process data.</param>
|
/// The following code explains how to run an external process using the
|
||||||
/// <param name="process">The process.</param>
|
/// <see cref="GetProcessOutputAsync(String, String, CancellationToken)"/> method.
|
||||||
public delegate void ProcessDataReceivedCallback(byte[] processData, Process process);
|
/// <code>
|
||||||
|
/// class Example
|
||||||
/// <summary>
|
/// {
|
||||||
/// Runs the process asynchronously and if the exit code is 0,
|
/// using System.Threading.Tasks;
|
||||||
/// returns all of the standard output text. If the exit code is something other than 0
|
/// using Swan;
|
||||||
/// it returns the contents of standard error.
|
///
|
||||||
/// This method is meant to be used for programs that output a relatively small amount of text.
|
/// static async Task Main()
|
||||||
/// </summary>
|
/// {
|
||||||
/// <param name="filename">The filename.</param>
|
/// // execute a process and save its output
|
||||||
/// <param name="arguments">The arguments.</param>
|
/// var data = await ProcessRunner.
|
||||||
/// <param name="workingDirectory">The working directory.</param>
|
/// GetProcessOutputAsync("dotnet", "--help");
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
///
|
||||||
/// <returns>The type of the result produced by this Task.</returns>
|
/// // print the output
|
||||||
/// <example>
|
/// data.WriteLine();
|
||||||
/// The following code explains how to run an external process using the
|
/// }
|
||||||
/// <see cref="GetProcessOutputAsync(string, string, CancellationToken)"/> method.
|
/// }
|
||||||
/// <code>
|
/// </code>
|
||||||
/// class Example
|
/// </example>
|
||||||
/// {
|
public static async Task<String> GetProcessOutputAsync(String filename, String arguments = "", String? workingDirectory = null, CancellationToken cancellationToken = default) {
|
||||||
/// using System.Threading.Tasks;
|
ProcessResult result = await GetProcessResultAsync(filename, arguments, workingDirectory, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||||
/// using Swan;
|
return result.ExitCode == 0 ? result.StandardOutput : result.StandardError;
|
||||||
///
|
|
||||||
/// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs the process asynchronously and if the exit code is 0,
|
||||||
|
/// returns all of the standard output text. If the exit code is something other than 0
|
||||||
|
/// it returns the contents of standard error.
|
||||||
|
/// This method is meant to be used for programs that output a relatively small amount
|
||||||
|
/// of text using a different encoder.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filename">The filename.</param>
|
||||||
|
/// <param name="arguments">The arguments.</param>
|
||||||
|
/// <param name="encoding">The encoding.</param>
|
||||||
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The type of the result produced by this Task.
|
||||||
|
/// </returns>
|
||||||
|
public static async Task<String> GetProcessEncodedOutputAsync(String filename, String arguments = "", Encoding? encoding = null, CancellationToken cancellationToken = default) {
|
||||||
|
ProcessResult result = await GetProcessResultAsync(filename, arguments, null, encoding, cancellationToken).ConfigureAwait(false);
|
||||||
|
return result.ExitCode == 0 ? result.StandardOutput : result.StandardError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes a process asynchronously and returns the text of the standard output and standard error streams
|
||||||
|
/// along with the exit code. This method is meant to be used for programs that output a relatively small
|
||||||
|
/// amount of text.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filename">The filename.</param>
|
||||||
|
/// <param name="arguments">The arguments.</param>
|
||||||
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// Text of the standard output and standard error streams along with the exit code as a <see cref="ProcessResult" /> instance.
|
||||||
|
/// </returns>
|
||||||
|
/// <exception cref="ArgumentNullException">filename.</exception>
|
||||||
|
public static Task<ProcessResult> GetProcessResultAsync(String filename, String arguments = "", CancellationToken cancellationToken = default) => GetProcessResultAsync(filename, arguments, null, Definitions.CurrentAnsiEncoding, cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes a process asynchronously and returns the text of the standard output and standard error streams
|
||||||
|
/// along with the exit code. This method is meant to be used for programs that output a relatively small
|
||||||
|
/// amount of text.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filename">The filename.</param>
|
||||||
|
/// <param name="arguments">The arguments.</param>
|
||||||
|
/// <param name="workingDirectory">The working directory.</param>
|
||||||
|
/// <param name="encoding">The encoding.</param>
|
||||||
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// Text of the standard output and standard error streams along with the exit code as a <see cref="ProcessResult" /> instance.
|
||||||
|
/// </returns>
|
||||||
|
/// <exception cref="ArgumentNullException">filename.</exception>
|
||||||
|
/// <example>
|
||||||
|
/// The following code describes how to run an external process using the <see cref="GetProcessResultAsync(String, String, String, Encoding, CancellationToken)" /> method.
|
||||||
|
/// <code>
|
||||||
|
/// class Example
|
||||||
|
/// {
|
||||||
|
/// using System.Threading.Tasks;
|
||||||
|
/// using Swan;
|
||||||
|
///
|
||||||
|
/// static async Task Main()
|
||||||
|
/// {
|
||||||
|
/// // Execute a process asynchronously
|
||||||
|
/// var data = await ProcessRunner.GetProcessResultAsync("dotnet", "--help");
|
||||||
|
///
|
||||||
|
/// // print out the exit code
|
||||||
|
/// $"{data.ExitCode}".WriteLine();
|
||||||
|
///
|
||||||
|
/// // print out the output
|
||||||
|
/// data.StandardOutput.WriteLine();
|
||||||
|
/// // and the error if exists
|
||||||
|
/// data.StandardError.Error();
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// </code></example>
|
||||||
|
public static async Task<ProcessResult> GetProcessResultAsync(String filename, String arguments, String? workingDirectory, Encoding? encoding = null, CancellationToken cancellationToken = default) {
|
||||||
|
if(filename == null) {
|
||||||
|
throw new ArgumentNullException(nameof(filename));
|
||||||
|
}
|
||||||
|
|
||||||
|
if(encoding == null) {
|
||||||
|
encoding = Definitions.CurrentAnsiEncoding;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder standardOutputBuilder = new StringBuilder();
|
||||||
|
StringBuilder standardErrorBuilder = new StringBuilder();
|
||||||
|
|
||||||
|
Int32 processReturn = await RunProcessAsync(filename, arguments, workingDirectory, (data, proc) => standardOutputBuilder.Append(encoding.GetString(data)), (data, proc) => standardErrorBuilder.Append(encoding.GetString(data)), encoding, true, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
return new ProcessResult(processReturn, standardOutputBuilder.ToString(), standardErrorBuilder.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs an external process asynchronously, providing callbacks to
|
||||||
|
/// capture binary data from the standard error and standard output streams.
|
||||||
|
/// The callbacks contain a reference to the process so you can respond to output or
|
||||||
|
/// error streams by writing to the process' input stream.
|
||||||
|
/// The exit code (return value) will be -1 for forceful termination of the process.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filename">The filename.</param>
|
||||||
|
/// <param name="arguments">The arguments.</param>
|
||||||
|
/// <param name="workingDirectory">The working directory.</param>
|
||||||
|
/// <param name="onOutputData">The on output data.</param>
|
||||||
|
/// <param name="onErrorData">The on error data.</param>
|
||||||
|
/// <param name="encoding">The encoding.</param>
|
||||||
|
/// <param name="syncEvents">if set to <c>true</c> the next data callback will wait until the current one completes.</param>
|
||||||
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// Value type will be -1 for forceful termination of the process.
|
||||||
|
/// </returns>
|
||||||
|
public static Task<Int32> RunProcessAsync(String filename, String arguments, String? workingDirectory, ProcessDataReceivedCallback onOutputData, ProcessDataReceivedCallback? onErrorData, Encoding encoding, Boolean syncEvents = true, CancellationToken cancellationToken = default) {
|
||||||
|
if(filename == null) {
|
||||||
|
throw new ArgumentNullException(nameof(filename));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.Run(() => {
|
||||||
|
// Setup the process and its corresponding start info
|
||||||
|
Process process = new Process {
|
||||||
|
EnableRaisingEvents = false,
|
||||||
|
StartInfo = new ProcessStartInfo {
|
||||||
|
Arguments = arguments,
|
||||||
|
CreateNoWindow = true,
|
||||||
|
FileName = filename,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
StandardErrorEncoding = encoding,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
StandardOutputEncoding = encoding,
|
||||||
|
UseShellExecute = false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if(!String.IsNullOrWhiteSpace(workingDirectory)) {
|
||||||
|
process.StartInfo.WorkingDirectory = workingDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Launch the process and discard any buffered data for standard error and standard output
|
||||||
|
_ = process.Start();
|
||||||
|
process.StandardError.DiscardBufferedData();
|
||||||
|
process.StandardOutput.DiscardBufferedData();
|
||||||
|
|
||||||
|
// Launch the asynchronous stream reading tasks
|
||||||
|
Task[] readTasks = new Task[2];
|
||||||
|
readTasks[0] = CopyStreamAsync(process, process.StandardOutput.BaseStream, onOutputData, syncEvents, cancellationToken);
|
||||||
|
readTasks[1] = CopyStreamAsync(process, process.StandardError.BaseStream, onErrorData, syncEvents, cancellationToken);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Wait for all tasks to complete
|
||||||
|
Task.WaitAll(readTasks, cancellationToken);
|
||||||
|
} catch(TaskCanceledException) {
|
||||||
|
// ignore
|
||||||
|
} finally {
|
||||||
|
// Wait for the process to exit
|
||||||
|
while(cancellationToken.IsCancellationRequested == false) {
|
||||||
|
if(process.HasExited || process.WaitForExit(5)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forcefully kill the process if it do not exit
|
||||||
|
try {
|
||||||
|
if(process.HasExited == false) {
|
||||||
|
process.Kill();
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// swallow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Retrieve and return the exit code.
|
||||||
|
// -1 signals error
|
||||||
|
return process.HasExited ? process.ExitCode : -1;
|
||||||
|
} catch {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs an external process asynchronously, providing callbacks to
|
||||||
|
/// capture binary data from the standard error and standard output streams.
|
||||||
|
/// The callbacks contain a reference to the process so you can respond to output or
|
||||||
|
/// error streams by writing to the process' input stream.
|
||||||
|
/// The exit code (return value) will be -1 for forceful termination of the process.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filename">The filename.</param>
|
||||||
|
/// <param name="arguments">The arguments.</param>
|
||||||
|
/// <param name="onOutputData">The on output data.</param>
|
||||||
|
/// <param name="onErrorData">The on error data.</param>
|
||||||
|
/// <param name="syncEvents">if set to <c>true</c> the next data callback will wait until the current one completes.</param>
|
||||||
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
|
/// <returns>Value type will be -1 for forceful termination of the process.</returns>
|
||||||
|
/// <example>
|
||||||
|
/// The following example illustrates how to run an external process using the
|
||||||
|
/// <see cref="RunProcessAsync(String, String, ProcessDataReceivedCallback, ProcessDataReceivedCallback, Boolean, CancellationToken)"/>
|
||||||
|
/// method.
|
||||||
|
/// <code>
|
||||||
|
/// class Example
|
||||||
|
/// {
|
||||||
|
/// using System.Diagnostics;
|
||||||
|
/// using System.Text;
|
||||||
|
/// using System.Threading.Tasks;
|
||||||
|
/// using Swan;
|
||||||
|
///
|
||||||
|
/// static async Task Main()
|
||||||
|
/// {
|
||||||
|
/// // Execute a process asynchronously
|
||||||
|
/// var data = await ProcessRunner
|
||||||
|
/// .RunProcessAsync("dotnet", "--help", Print, Print);
|
||||||
|
///
|
||||||
|
/// // flush all messages
|
||||||
|
/// Terminal.Flush();
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// // a callback to print both output or errors
|
||||||
|
/// static void Print(byte[] data, Process proc) =>
|
||||||
|
/// Encoding.GetEncoding(0).GetString(data).WriteLine();
|
||||||
|
/// }
|
||||||
|
/// </code>
|
||||||
|
/// </example>
|
||||||
|
public static Task<Int32> RunProcessAsync(String filename, String arguments, ProcessDataReceivedCallback onOutputData, ProcessDataReceivedCallback? onErrorData, Boolean syncEvents = true, CancellationToken cancellationToken = default) => RunProcessAsync(filename, arguments, null, onOutputData, onErrorData, Definitions.CurrentAnsiEncoding, syncEvents, cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Copies the stream asynchronously.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="process">The process.</param>
|
||||||
|
/// <param name="baseStream">The source stream.</param>
|
||||||
|
/// <param name="onDataCallback">The on data callback.</param>
|
||||||
|
/// <param name="syncEvents">if set to <c>true</c> [synchronize events].</param>
|
||||||
|
/// <param name="ct">The cancellation token.</param>
|
||||||
|
/// <returns>Total copies stream.</returns>
|
||||||
|
private static Task<UInt64> CopyStreamAsync(Process process, Stream baseStream, ProcessDataReceivedCallback? onDataCallback, Boolean syncEvents, CancellationToken ct) => Task.Run(async () => {
|
||||||
|
// define some state variables
|
||||||
|
Byte[] swapBuffer = new Byte[2048]; // the buffer to copy data from one stream to the next
|
||||||
|
UInt64 totalCount = 0; // the total amount of bytes read
|
||||||
|
Boolean hasExited = false;
|
||||||
|
|
||||||
|
while(ct.IsCancellationRequested == false) {
|
||||||
|
try {
|
||||||
|
// Check if process is no longer valid
|
||||||
|
// if this condition holds, simply read the last bits of data available.
|
||||||
|
Int32 readCount; // the bytes read in any given event
|
||||||
|
if(process.HasExited || process.WaitForExit(1)) {
|
||||||
|
while(true) {
|
||||||
|
try {
|
||||||
|
readCount = await baseStream.ReadAsync(swapBuffer, 0, swapBuffer.Length, ct);
|
||||||
|
|
||||||
|
if(readCount > 0) {
|
||||||
|
totalCount += (UInt64)readCount;
|
||||||
|
onDataCallback?.Invoke(swapBuffer.Skip(0).Take(readCount).ToArray(), process);
|
||||||
|
} else {
|
||||||
|
hasExited = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
hasExited = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(hasExited) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try reading from the stream. < 0 means no read occurred.
|
||||||
|
readCount = await baseStream.ReadAsync(swapBuffer, 0, swapBuffer.Length, ct).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// When no read is done, we need to let is rest for a bit
|
||||||
|
if(readCount <= 0) {
|
||||||
|
await Task.Delay(1, ct).ConfigureAwait(false); // do not hog CPU cycles doing nothing.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
totalCount += (UInt64)readCount;
|
||||||
|
if(onDataCallback == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the buffer to pass to the callback
|
||||||
|
Byte[] eventBuffer = swapBuffer.Skip(0).Take(readCount).ToArray();
|
||||||
|
|
||||||
|
// Create the data processing callback invocation
|
||||||
|
Task eventTask = Task.Run(() => onDataCallback.Invoke(eventBuffer, process), ct);
|
||||||
|
|
||||||
|
// wait for the event to process before the next read occurs
|
||||||
|
if(syncEvents) {
|
||||||
|
eventTask.Wait(ct);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalCount;
|
||||||
|
}, ct);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,92 +1,98 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
#if !NET461
|
|
||||||
namespace Swan.Services
|
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>
|
/// <summary>
|
||||||
/// Mimic a Windows ServiceBase class. Useful to keep compatibility with applications
|
/// Gets or sets a value indicating whether the service can be stopped once it has started.
|
||||||
/// running as services in OS different to Windows.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("This abstract class will be removed in version 3.0")]
|
/// <value>
|
||||||
public abstract class ServiceBase
|
/// <c>true</c> if this instance can stop; otherwise, <c>false</c>.
|
||||||
{
|
/// </value>
|
||||||
/// <summary>
|
public Boolean CanStop { get; set; } = true;
|
||||||
/// 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>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether the service should be notified when the system is shutting down.
|
/// Gets or sets a value indicating whether the service should be notified when the system is shutting down.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
/// <value>
|
||||||
/// <c>true</c> if this instance can shutdown; otherwise, <c>false</c>.
|
/// <c>true</c> if this instance can shutdown; otherwise, <c>false</c>.
|
||||||
/// </value>
|
/// </value>
|
||||||
public bool CanShutdown { get; set; }
|
public Boolean 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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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 Boolean CanPauseAndContinue {
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the exit code.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// The exit code.
|
||||||
|
/// </value>
|
||||||
|
public Int32 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 Boolean 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(!this.CanStop) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.CanStop = false;
|
||||||
|
this.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
|
|
||||||
|
@ -1,141 +1,136 @@
|
|||||||
namespace Swan.Threading
|
using System;
|
||||||
{
|
using System.Diagnostics;
|
||||||
using System;
|
using System.Threading;
|
||||||
using System.Diagnostics;
|
using System.Threading.Tasks;
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
namespace Swan.Threading {
|
||||||
|
/// <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 Boolean _isDisposed;
|
||||||
|
private IWaitEvent _delayEvent;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents logic providing several delay mechanisms.
|
/// Initializes a new instance of the <see cref="DelayProvider"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <example>
|
/// <param name="strategy">The strategy.</param>
|
||||||
/// The following example shows how to implement delay mechanisms.
|
public DelayProvider(DelayStrategy strategy = DelayStrategy.TaskDelay) => this.Strategy = strategy;
|
||||||
/// <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;
|
/// <summary>
|
||||||
private IWaitEvent _delayEvent;
|
/// Enumerates the different ways of providing delays.
|
||||||
|
/// </summary>
|
||||||
|
public enum DelayStrategy {
|
||||||
|
/// <summary>
|
||||||
|
/// Using the Thread.Sleep(15) mechanism.
|
||||||
|
/// </summary>
|
||||||
|
ThreadSleep,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="DelayProvider"/> class.
|
/// Using the Task.Delay(1).Wait mechanism.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="strategy">The strategy.</param>
|
TaskDelay,
|
||||||
public DelayProvider(DelayStrategy strategy = DelayStrategy.TaskDelay)
|
|
||||||
{
|
|
||||||
Strategy = strategy;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Enumerates the different ways of providing delays.
|
/// Using a wait event that completes in a background ThreadPool thread.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum DelayStrategy
|
ThreadPool,
|
||||||
{
|
|
||||||
/// <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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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(this._syncRoot) {
|
||||||
|
if(this._isDisposed) {
|
||||||
|
return TimeSpan.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._delayStopwatch.Restart();
|
||||||
|
|
||||||
|
switch(this.Strategy) {
|
||||||
|
case DelayStrategy.ThreadSleep:
|
||||||
|
DelaySleep();
|
||||||
|
break;
|
||||||
|
case DelayStrategy.TaskDelay:
|
||||||
|
DelayTask();
|
||||||
|
break;
|
||||||
|
case DelayStrategy.ThreadPool:
|
||||||
|
this.DelayThreadPool();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._delayStopwatch.Elapsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Dispose Pattern
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Dispose() {
|
||||||
|
lock(this._syncRoot) {
|
||||||
|
if(this._isDisposed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._isDisposed = true;
|
||||||
|
|
||||||
|
this._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(this._delayEvent == null) {
|
||||||
|
this._delayEvent = WaitEventFactory.Create(isCompleted: true, useSlim: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._delayEvent.Begin();
|
||||||
|
_ = ThreadPool.QueueUserWorkItem(s => {
|
||||||
|
DelaySleep();
|
||||||
|
this._delayEvent.Complete();
|
||||||
|
});
|
||||||
|
|
||||||
|
this._delayEvent.Wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,292 +1,251 @@
|
|||||||
namespace Swan.Threading
|
namespace Swan.Threading {
|
||||||
{
|
using System;
|
||||||
using System;
|
using System.Threading;
|
||||||
using System.Threading;
|
using System.Threading.Tasks;
|
||||||
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>
|
/// <summary>
|
||||||
/// Provides a base implementation for application workers
|
/// Initializes a new instance of the <see cref="ThreadWorkerBase"/> class.
|
||||||
/// that perform continuous, long-running tasks. This class
|
|
||||||
/// provides the ability to perform fine-grained control on these tasks.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <seealso cref="IWorker" />
|
/// <param name="name">The name.</param>
|
||||||
public abstract class ThreadWorkerBase : WorkerBase
|
/// <param name="priority">The thread priority.</param>
|
||||||
{
|
/// <param name="period">The interval of cycle execution.</param>
|
||||||
private readonly object _syncLock = new object();
|
/// <param name="delayProvider">The cycle delay provide implementation.</param>
|
||||||
private readonly Thread _thread;
|
protected ThreadWorkerBase(String name, ThreadPriority priority, TimeSpan period, IWorkerDelayProvider delayProvider) : base(name, period) {
|
||||||
|
this.DelayProvider = delayProvider;
|
||||||
|
this._thread = new Thread(this.RunWorkerLoop) {
|
||||||
|
IsBackground = true,
|
||||||
|
Priority = priority,
|
||||||
|
Name = name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ThreadWorkerBase"/> class.
|
/// Initializes a new instance of the <see cref="ThreadWorkerBase"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="name">The name.</param>
|
/// <param name="name">The name.</param>
|
||||||
/// <param name="priority">The thread priority.</param>
|
/// <param name="period">The execution interval.</param>
|
||||||
/// <param name="period">The interval of cycle execution.</param>
|
protected ThreadWorkerBase(String name, TimeSpan period) : this(name, ThreadPriority.Normal, period, WorkerDelayProvider.Default) {
|
||||||
/// <param name="delayProvider">The cycle delay provide implementation.</param>
|
// placeholder
|
||||||
protected ThreadWorkerBase(string name, ThreadPriority priority, TimeSpan period, IWorkerDelayProvider delayProvider)
|
}
|
||||||
: base(name, period)
|
|
||||||
{
|
/// <summary>
|
||||||
DelayProvider = delayProvider;
|
/// Provides an implementation on a cycle delay provider.
|
||||||
_thread = new Thread(RunWorkerLoop)
|
/// </summary>
|
||||||
{
|
protected IWorkerDelayProvider DelayProvider {
|
||||||
IsBackground = true,
|
get;
|
||||||
Priority = priority,
|
}
|
||||||
Name = name,
|
|
||||||
};
|
/// <inheritdoc />
|
||||||
|
public override Task<WorkerState> StartAsync() {
|
||||||
|
lock(this._syncLock) {
|
||||||
|
if(this.WorkerState == WorkerState.Paused || this.WorkerState == WorkerState.Waiting) {
|
||||||
|
return this.ResumeAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
if(this.WorkerState != WorkerState.Created) {
|
||||||
/// Initializes a new instance of the <see cref="ThreadWorkerBase"/> class.
|
return Task.FromResult(this.WorkerState);
|
||||||
/// </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>
|
if(this.IsStopRequested) {
|
||||||
/// Provides an implementation on a cycle delay provider.
|
return Task.FromResult(this.WorkerState);
|
||||||
/// </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 />
|
Task<WorkerState> task = this.QueueStateChange(StateChangeRequest.Start);
|
||||||
public override Task<WorkerState> PauseAsync()
|
this._thread.Start();
|
||||||
{
|
return task;
|
||||||
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> PauseAsync() {
|
||||||
|
lock(this._syncLock) {
|
||||||
|
return this.WorkerState != WorkerState.Running && this.WorkerState != WorkerState.Waiting ? Task.FromResult(this.WorkerState) : this.IsStopRequested ? Task.FromResult(this.WorkerState) : this.QueueStateChange(StateChangeRequest.Pause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Task<WorkerState> ResumeAsync() {
|
||||||
|
lock(this._syncLock) {
|
||||||
|
return this.WorkerState == WorkerState.Created ? this.StartAsync() : this.WorkerState != WorkerState.Paused && this.WorkerState != WorkerState.Waiting ? Task.FromResult(this.WorkerState) : this.IsStopRequested ? Task.FromResult(this.WorkerState) : this.QueueStateChange(StateChangeRequest.Resume);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Task<WorkerState> StopAsync() {
|
||||||
|
lock(this._syncLock) {
|
||||||
|
if(this.WorkerState == WorkerState.Stopped || this.WorkerState == WorkerState.Created) {
|
||||||
|
this.WorkerState = WorkerState.Stopped;
|
||||||
|
return Task.FromResult(this.WorkerState);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
return this.QueueStateChange(StateChangeRequest.Stop);
|
||||||
public override Task<WorkerState> ResumeAsync()
|
}
|
||||||
{
|
}
|
||||||
lock (_syncLock)
|
|
||||||
{
|
|
||||||
if (WorkerState == WorkerState.Created)
|
|
||||||
return StartAsync();
|
|
||||||
|
|
||||||
if (WorkerState != WorkerState.Paused && WorkerState != WorkerState.Waiting)
|
/// <summary>
|
||||||
return Task.FromResult(WorkerState);
|
/// 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(Int32 wantedDelay, Task delayTask, CancellationToken token) =>
|
||||||
|
this.DelayProvider?.ExecuteCycleDelay(wantedDelay, delayTask, token);
|
||||||
|
|
||||||
return IsStopRequested ? Task.FromResult(WorkerState) : QueueStateChange(StateChangeRequest.Resume);
|
/// <inheritdoc />
|
||||||
}
|
protected override void OnDisposing() {
|
||||||
|
lock(this._syncLock) {
|
||||||
|
if((this._thread.ThreadState & ThreadState.Unstarted) != ThreadState.Unstarted) {
|
||||||
|
this._thread.Join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Implements worker control, execution and delay logic in a loop.
|
||||||
|
/// </summary>
|
||||||
|
private void RunWorkerLoop() {
|
||||||
|
while(this.WorkerState != WorkerState.Stopped && !this.IsDisposing && !this.IsDisposed) {
|
||||||
|
this.CycleStopwatch.Restart();
|
||||||
|
CancellationToken interruptToken = this.CycleCancellation.Token;
|
||||||
|
Int32 period = this.Period.TotalMilliseconds >= Int32.MaxValue ? -1 : Convert.ToInt32(Math.Floor(this.Period.TotalMilliseconds));
|
||||||
|
Task delayTask = Task.Delay(period, interruptToken);
|
||||||
|
WorkerState initialWorkerState = this.WorkerState;
|
||||||
|
|
||||||
|
// Lock the cycle and capture relevant state valid for this cycle
|
||||||
|
this.CycleCompletedEvent.Reset();
|
||||||
|
|
||||||
|
// Process the tasks that are awaiting
|
||||||
|
if(this.ProcessStateChangeRequests()) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
try {
|
||||||
public override Task<WorkerState> StopAsync()
|
if(initialWorkerState == WorkerState.Waiting &&
|
||||||
{
|
!interruptToken.IsCancellationRequested) {
|
||||||
lock (_syncLock)
|
// Mark the state as Running
|
||||||
{
|
this.WorkerState = WorkerState.Running;
|
||||||
if (WorkerState == WorkerState.Stopped || WorkerState == WorkerState.Created)
|
|
||||||
{
|
|
||||||
WorkerState = WorkerState.Stopped;
|
|
||||||
return Task.FromResult(WorkerState);
|
|
||||||
}
|
|
||||||
|
|
||||||
return QueueStateChange(StateChangeRequest.Stop);
|
// Call the execution logic
|
||||||
}
|
this.ExecuteCycleLogic(interruptToken);
|
||||||
}
|
}
|
||||||
|
} catch(Exception ex) {
|
||||||
/// <summary>
|
this.OnCycleException(ex);
|
||||||
/// Suspends execution queues a new new cycle for execution. The delay is given in
|
} finally {
|
||||||
/// milliseconds. When overridden in a derived class the wait handle will be set
|
// Update the state
|
||||||
/// whenever an interrupt is received.
|
this.WorkerState = initialWorkerState == WorkerState.Paused
|
||||||
/// </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.Paused
|
||||||
: WorkerState.Waiting;
|
: WorkerState.Waiting;
|
||||||
|
|
||||||
// Signal the cycle has been completed so new cycles can be executed
|
// Signal the cycle has been completed so new cycles can be executed
|
||||||
CycleCompletedEvent.Set();
|
this.CycleCompletedEvent.Set();
|
||||||
|
|
||||||
if (!interruptToken.IsCancellationRequested)
|
if(!interruptToken.IsCancellationRequested) {
|
||||||
{
|
Int32 cycleDelay = this.ComputeCycleDelay(initialWorkerState);
|
||||||
var cycleDelay = ComputeCycleDelay(initialWorkerState);
|
if(cycleDelay == Timeout.Infinite) {
|
||||||
if (cycleDelay == Timeout.Infinite)
|
delayTask = Task.Delay(Timeout.Infinite, interruptToken);
|
||||||
delayTask = Task.Delay(Timeout.Infinite, interruptToken);
|
}
|
||||||
|
|
||||||
ExecuteCycleDelay(
|
this.ExecuteCycleDelay(
|
||||||
cycleDelay,
|
cycleDelay,
|
||||||
delayTask,
|
delayTask,
|
||||||
CycleCancellation.Token);
|
this.CycleCancellation.Token);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ClearStateChangeRequests();
|
|
||||||
WorkerState = WorkerState.Stopped;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
this.ClearStateChangeRequests();
|
||||||
/// Queues a transition in worker state for processing. Returns a task that can be awaited
|
this.WorkerState = WorkerState.Stopped;
|
||||||
/// 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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(this._syncLock) {
|
||||||
|
if(this.StateChangeTask != null) {
|
||||||
|
return this.StateChangeTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
Task<WorkerState> waitingTask = new Task<WorkerState>(() => {
|
||||||
|
this.StateChangedEvent.Wait();
|
||||||
|
lock(this._syncLock) {
|
||||||
|
this.StateChangeTask = null;
|
||||||
|
return this.WorkerState;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.StateChangeTask = waitingTask;
|
||||||
|
this.StateChangedEvent.Reset();
|
||||||
|
this.StateChangeRequests[request] = true;
|
||||||
|
waitingTask.Start();
|
||||||
|
this.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 Boolean ProcessStateChangeRequests() {
|
||||||
|
lock(this._syncLock) {
|
||||||
|
Boolean hasRequest = false;
|
||||||
|
WorkerState currentState = this.WorkerState;
|
||||||
|
|
||||||
|
// Update the state in the given priority
|
||||||
|
if(this.StateChangeRequests[StateChangeRequest.Stop] || this.IsDisposing || this.IsDisposed) {
|
||||||
|
hasRequest = true;
|
||||||
|
this.WorkerState = WorkerState.Stopped;
|
||||||
|
} else if(this.StateChangeRequests[StateChangeRequest.Pause]) {
|
||||||
|
hasRequest = true;
|
||||||
|
this.WorkerState = WorkerState.Paused;
|
||||||
|
} else if(this.StateChangeRequests[StateChangeRequest.Start] || this.StateChangeRequests[StateChangeRequest.Resume]) {
|
||||||
|
hasRequest = true;
|
||||||
|
this.WorkerState = WorkerState.Waiting;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signals all state changes to continue
|
||||||
|
// as a command has been handled.
|
||||||
|
if(hasRequest) {
|
||||||
|
this.ClearStateChangeRequests();
|
||||||
|
this.OnStateChangeProcessed(currentState, this.WorkerState);
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasRequest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Signals all state change requests to set.
|
||||||
|
/// </summary>
|
||||||
|
private void ClearStateChangeRequests() {
|
||||||
|
lock(this._syncLock) {
|
||||||
|
// Mark all events as completed
|
||||||
|
this.StateChangeRequests[StateChangeRequest.Start] = false;
|
||||||
|
this.StateChangeRequests[StateChangeRequest.Pause] = false;
|
||||||
|
this.StateChangeRequests[StateChangeRequest.Resume] = false;
|
||||||
|
this.StateChangeRequests[StateChangeRequest.Stop] = false;
|
||||||
|
|
||||||
|
this.StateChangedEvent.Set();
|
||||||
|
this.CycleCompletedEvent.Set();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,328 +1,300 @@
|
|||||||
namespace Swan.Threading
|
using System;
|
||||||
{
|
using System.Threading;
|
||||||
using System;
|
using System.Threading.Tasks;
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
namespace Swan.Threading {
|
||||||
|
/// <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 Boolean _isTimerAlive = true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides a base implementation for application workers.
|
/// Initializes a new instance of the <see cref="TimerWorkerBase"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <seealso cref="IWorker" />
|
/// <param name="name">The name.</param>
|
||||||
public abstract class TimerWorkerBase : WorkerBase
|
/// <param name="period">The execution interval.</param>
|
||||||
{
|
protected TimerWorkerBase(String name, TimeSpan period) : base(name, period) =>
|
||||||
private readonly object _syncLock = new object();
|
// Instantiate the timer that will be used to schedule cycles
|
||||||
private readonly Timer _timer;
|
this._timer = new Timer(this.ExecuteTimerCallback, this, Timeout.Infinite, Timeout.Infinite);
|
||||||
private bool _isTimerAlive = true;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc />
|
||||||
/// Initializes a new instance of the <see cref="TimerWorkerBase"/> class.
|
public override Task<WorkerState> StartAsync() {
|
||||||
/// </summary>
|
lock(this._syncLock) {
|
||||||
/// <param name="name">The name.</param>
|
if(this.WorkerState == WorkerState.Paused || this.WorkerState == WorkerState.Waiting) {
|
||||||
/// <param name="period">The execution interval.</param>
|
return this.ResumeAsync();
|
||||||
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 />
|
if(this.WorkerState != WorkerState.Created) {
|
||||||
public override Task<WorkerState> StartAsync()
|
return Task.FromResult(this.WorkerState);
|
||||||
{
|
|
||||||
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 />
|
if(this.IsStopRequested) {
|
||||||
public override Task<WorkerState> PauseAsync()
|
return Task.FromResult(this.WorkerState);
|
||||||
{
|
|
||||||
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 />
|
Task<WorkerState> task = this.QueueStateChange(StateChangeRequest.Start);
|
||||||
public override Task<WorkerState> ResumeAsync()
|
this.Interrupt();
|
||||||
{
|
return task;
|
||||||
lock (_syncLock)
|
}
|
||||||
{
|
}
|
||||||
if (WorkerState == WorkerState.Created)
|
|
||||||
return StartAsync();
|
|
||||||
|
|
||||||
if (WorkerState != WorkerState.Paused && WorkerState != WorkerState.Waiting)
|
/// <inheritdoc />
|
||||||
return Task.FromResult(WorkerState);
|
public override Task<WorkerState> PauseAsync() {
|
||||||
|
lock(this._syncLock) {
|
||||||
if (IsStopRequested)
|
if(this.WorkerState != WorkerState.Running && this.WorkerState != WorkerState.Waiting) {
|
||||||
return Task.FromResult(WorkerState);
|
return Task.FromResult(this.WorkerState);
|
||||||
|
|
||||||
var task = QueueStateChange(StateChangeRequest.Resume);
|
|
||||||
Interrupt();
|
|
||||||
return task;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
if(this.IsStopRequested) {
|
||||||
public override Task<WorkerState> StopAsync()
|
return Task.FromResult(this.WorkerState);
|
||||||
{
|
|
||||||
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>
|
Task<WorkerState> task = this.QueueStateChange(StateChangeRequest.Pause);
|
||||||
/// Schedules a new cycle for execution. The delay is given in
|
this.Interrupt();
|
||||||
/// milliseconds. Passing a delay of 0 means a new cycle should be executed
|
return task;
|
||||||
/// immediately.
|
}
|
||||||
/// </summary>
|
}
|
||||||
/// <param name="delay">The delay.</param>
|
|
||||||
protected void ScheduleCycle(int delay)
|
/// <inheritdoc />
|
||||||
{
|
public override Task<WorkerState> ResumeAsync() {
|
||||||
lock (_syncLock)
|
lock(this._syncLock) {
|
||||||
{
|
if(this.WorkerState == WorkerState.Created) {
|
||||||
if (!_isTimerAlive) return;
|
return this.StartAsync();
|
||||||
_timer.Change(delay, Timeout.Infinite);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
if(this.WorkerState != WorkerState.Paused && this.WorkerState != WorkerState.Waiting) {
|
||||||
protected override void Dispose(bool disposing)
|
return Task.FromResult(this.WorkerState);
|
||||||
{
|
|
||||||
base.Dispose(disposing);
|
|
||||||
|
|
||||||
lock (_syncLock)
|
|
||||||
{
|
|
||||||
if (!_isTimerAlive) return;
|
|
||||||
_isTimerAlive = false;
|
|
||||||
_timer.Dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
if(this.IsStopRequested) {
|
||||||
/// Cancels the current token and schedules a new cycle immediately.
|
return Task.FromResult(this.WorkerState);
|
||||||
/// </summary>
|
|
||||||
private void Interrupt()
|
|
||||||
{
|
|
||||||
lock (_syncLock)
|
|
||||||
{
|
|
||||||
if (WorkerState == WorkerState.Stopped)
|
|
||||||
return;
|
|
||||||
|
|
||||||
CycleCancellation.Cancel();
|
|
||||||
ScheduleCycle(0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
Task<WorkerState> task = this.QueueStateChange(StateChangeRequest.Resume);
|
||||||
/// Executes the worker cycle control logic.
|
this.Interrupt();
|
||||||
/// This includes processing state change requests,
|
return task;
|
||||||
/// the execution of use cycle code,
|
}
|
||||||
/// and the scheduling of new cycles.
|
}
|
||||||
/// </summary>
|
|
||||||
private void ExecuteWorkerCycle()
|
|
||||||
{
|
|
||||||
CycleStopwatch.Restart();
|
|
||||||
|
|
||||||
lock (_syncLock)
|
/// <inheritdoc />
|
||||||
{
|
public override Task<WorkerState> StopAsync() {
|
||||||
if (IsDisposing || IsDisposed)
|
lock(this._syncLock) {
|
||||||
{
|
if(this.WorkerState == WorkerState.Stopped || this.WorkerState == WorkerState.Created) {
|
||||||
WorkerState = WorkerState.Stopped;
|
this.WorkerState = WorkerState.Stopped;
|
||||||
|
return Task.FromResult(this.WorkerState);
|
||||||
|
}
|
||||||
|
|
||||||
// Cancel any awaiters
|
Task<WorkerState> task = this.QueueStateChange(StateChangeRequest.Stop);
|
||||||
try { StateChangedEvent.Set(); }
|
this.Interrupt();
|
||||||
catch { /* Ignore */ }
|
return task;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
/// <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(Int32 delay) {
|
||||||
|
lock(this._syncLock) {
|
||||||
|
if(!this._isTimerAlive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Prevent running another instance of the cycle
|
_ = this._timer.Change(delay, Timeout.Infinite);
|
||||||
if (CycleCompletedEvent.IsSet == false) return;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Lock the cycle and capture relevant state valid for this cycle
|
/// <inheritdoc />
|
||||||
CycleCompletedEvent.Reset();
|
protected override void Dispose(Boolean disposing) {
|
||||||
}
|
base.Dispose(disposing);
|
||||||
|
|
||||||
var interruptToken = CycleCancellation.Token;
|
lock(this._syncLock) {
|
||||||
var initialWorkerState = WorkerState;
|
if(!this._isTimerAlive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Process the tasks that are awaiting
|
this._isTimerAlive = false;
|
||||||
if (ProcessStateChangeRequests())
|
this._timer.Dispose();
|
||||||
return;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try
|
/// <summary>
|
||||||
{
|
/// Cancels the current token and schedules a new cycle immediately.
|
||||||
if (initialWorkerState == WorkerState.Waiting &&
|
/// </summary>
|
||||||
!interruptToken.IsCancellationRequested)
|
private void Interrupt() {
|
||||||
{
|
lock(this._syncLock) {
|
||||||
// Mark the state as Running
|
if(this.WorkerState == WorkerState.Stopped) {
|
||||||
WorkerState = WorkerState.Running;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Call the execution logic
|
this.CycleCancellation.Cancel();
|
||||||
ExecuteCycleLogic(interruptToken);
|
this.ScheduleCycle(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
{
|
/// <summary>
|
||||||
OnCycleException(ex);
|
/// Executes the worker cycle control logic.
|
||||||
}
|
/// This includes processing state change requests,
|
||||||
finally
|
/// the execution of use cycle code,
|
||||||
{
|
/// and the scheduling of new cycles.
|
||||||
// Update the state
|
/// </summary>
|
||||||
WorkerState = initialWorkerState == WorkerState.Paused
|
private void ExecuteWorkerCycle() {
|
||||||
|
this.CycleStopwatch.Restart();
|
||||||
|
|
||||||
|
lock(this._syncLock) {
|
||||||
|
if(this.IsDisposing || this.IsDisposed) {
|
||||||
|
this.WorkerState = WorkerState.Stopped;
|
||||||
|
|
||||||
|
// Cancel any awaiters
|
||||||
|
try {
|
||||||
|
this.StateChangedEvent.Set();
|
||||||
|
} catch { /* Ignore */ }
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent running another instance of the cycle
|
||||||
|
if(this.CycleCompletedEvent.IsSet == false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lock the cycle and capture relevant state valid for this cycle
|
||||||
|
this.CycleCompletedEvent.Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
CancellationToken interruptToken = this.CycleCancellation.Token;
|
||||||
|
WorkerState initialWorkerState = this.WorkerState;
|
||||||
|
|
||||||
|
// Process the tasks that are awaiting
|
||||||
|
if(this.ProcessStateChangeRequests()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if(initialWorkerState == WorkerState.Waiting &&
|
||||||
|
!interruptToken.IsCancellationRequested) {
|
||||||
|
// Mark the state as Running
|
||||||
|
this.WorkerState = WorkerState.Running;
|
||||||
|
|
||||||
|
// Call the execution logic
|
||||||
|
this.ExecuteCycleLogic(interruptToken);
|
||||||
|
}
|
||||||
|
} catch(Exception ex) {
|
||||||
|
this.OnCycleException(ex);
|
||||||
|
} finally {
|
||||||
|
// Update the state
|
||||||
|
this.WorkerState = initialWorkerState == WorkerState.Paused
|
||||||
? WorkerState.Paused
|
? WorkerState.Paused
|
||||||
: WorkerState.Waiting;
|
: WorkerState.Waiting;
|
||||||
|
|
||||||
lock (_syncLock)
|
lock(this._syncLock) {
|
||||||
{
|
// Signal the cycle has been completed so new cycles can be executed
|
||||||
// Signal the cycle has been completed so new cycles can be executed
|
this.CycleCompletedEvent.Set();
|
||||||
CycleCompletedEvent.Set();
|
|
||||||
|
|
||||||
// Schedule a new cycle
|
// Schedule a new cycle
|
||||||
ScheduleCycle(!interruptToken.IsCancellationRequested
|
this.ScheduleCycle(!interruptToken.IsCancellationRequested
|
||||||
? ComputeCycleDelay(initialWorkerState)
|
? this.ComputeCycleDelay(initialWorkerState)
|
||||||
: 0);
|
: 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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) => this.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(this._syncLock) {
|
||||||
|
if(this.StateChangeTask != null) {
|
||||||
|
return this.StateChangeTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
Task<WorkerState> waitingTask = new Task<WorkerState>(() => {
|
||||||
|
this.StateChangedEvent.Wait();
|
||||||
|
lock(this._syncLock) {
|
||||||
|
this.StateChangeTask = null;
|
||||||
|
return this.WorkerState;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.StateChangeTask = waitingTask;
|
||||||
|
this.StateChangedEvent.Reset();
|
||||||
|
this.StateChangeRequests[request] = true;
|
||||||
|
waitingTask.Start();
|
||||||
|
this.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 Boolean ProcessStateChangeRequests() {
|
||||||
|
lock(this._syncLock) {
|
||||||
|
WorkerState currentState = this.WorkerState;
|
||||||
|
Boolean hasRequest = false;
|
||||||
|
Int32 schedule = 0;
|
||||||
|
|
||||||
|
// Update the state according to request priority
|
||||||
|
if(this.StateChangeRequests[StateChangeRequest.Stop] || this.IsDisposing || this.IsDisposed) {
|
||||||
|
hasRequest = true;
|
||||||
|
this.WorkerState = WorkerState.Stopped;
|
||||||
|
schedule = this.StateChangeRequests[StateChangeRequest.Stop] ? Timeout.Infinite : 0;
|
||||||
|
} else if(this.StateChangeRequests[StateChangeRequest.Pause]) {
|
||||||
|
hasRequest = true;
|
||||||
|
this.WorkerState = WorkerState.Paused;
|
||||||
|
schedule = Timeout.Infinite;
|
||||||
|
} else if(this.StateChangeRequests[StateChangeRequest.Start] || this.StateChangeRequests[StateChangeRequest.Resume]) {
|
||||||
|
hasRequest = true;
|
||||||
|
this.WorkerState = WorkerState.Waiting;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signals all state changes to continue
|
||||||
|
// as a command has been handled.
|
||||||
|
if(hasRequest) {
|
||||||
|
this.ClearStateChangeRequests(schedule, currentState, this.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(Int32 schedule, WorkerState oldState, WorkerState newState) {
|
||||||
|
lock(this._syncLock) {
|
||||||
|
// Mark all events as completed
|
||||||
|
this.StateChangeRequests[StateChangeRequest.Start] = false;
|
||||||
|
this.StateChangeRequests[StateChangeRequest.Pause] = false;
|
||||||
|
this.StateChangeRequests[StateChangeRequest.Resume] = false;
|
||||||
|
this.StateChangeRequests[StateChangeRequest.Stop] = false;
|
||||||
|
|
||||||
|
this.StateChangedEvent.Set();
|
||||||
|
this.CycleCompletedEvent.Set();
|
||||||
|
this.OnStateChangeProcessed(oldState, newState);
|
||||||
|
this.ScheduleCycle(schedule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,240 +1,233 @@
|
|||||||
namespace Swan.Threading
|
#nullable enable
|
||||||
{
|
using System;
|
||||||
using System;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Generic;
|
using System.Diagnostics;
|
||||||
using System.Diagnostics;
|
using System.Threading;
|
||||||
using System.Threading;
|
using System.Threading.Tasks;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
namespace Swan.Threading {
|
||||||
|
/// <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>
|
/// <summary>
|
||||||
/// Provides base infrastructure for Timer and Thread workers.
|
/// Initializes a new instance of the <see cref="WorkerBase"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <seealso cref="IWorker" />
|
/// <param name="name">The name.</param>
|
||||||
public abstract class WorkerBase : IWorker, IDisposable
|
/// <param name="period">The execution interval.</param>
|
||||||
{
|
protected WorkerBase(String name, TimeSpan period) {
|
||||||
// Since these are API property backers, we use interlocked to read from them
|
this.Name = name;
|
||||||
// to avoid deadlocked reads
|
this._timeSpan = new AtomicTimeSpan(period);
|
||||||
private readonly object _syncLock = new object();
|
|
||||||
|
|
||||||
private readonly AtomicBoolean _isDisposed = new AtomicBoolean();
|
this.StateChangeRequests = new Dictionary<StateChangeRequest, Boolean>(5) {
|
||||||
private readonly AtomicBoolean _isDisposing = new AtomicBoolean();
|
[StateChangeRequest.Start] = false,
|
||||||
private readonly AtomicEnum<WorkerState> _workerState = new AtomicEnum<WorkerState>(WorkerState.Created);
|
[StateChangeRequest.Pause] = false,
|
||||||
private readonly AtomicTimeSpan _timeSpan;
|
[StateChangeRequest.Resume] = false,
|
||||||
|
[StateChangeRequest.Stop] = false,
|
||||||
/// <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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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 => this._timeSpan.Value;
|
||||||
|
set => this._timeSpan.Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public WorkerState WorkerState {
|
||||||
|
get => this._workerState.Value;
|
||||||
|
protected set => this._workerState.Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Boolean IsDisposed {
|
||||||
|
get => this._isDisposed.Value;
|
||||||
|
protected set => this._isDisposed.Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Boolean IsDisposing {
|
||||||
|
get => this._isDisposing.Value;
|
||||||
|
protected set => this._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 Boolean IsStopRequested => this.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, Boolean> 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() {
|
||||||
|
this.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(Boolean disposing) {
|
||||||
|
lock(this._syncLock) {
|
||||||
|
if(this.IsDisposed || this.IsDisposing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.IsDisposing = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This also ensures the state change queue gets cleared
|
||||||
|
this.StopAsync().Wait();
|
||||||
|
this.StateChangedEvent.Set();
|
||||||
|
this.CycleCompletedEvent.Set();
|
||||||
|
|
||||||
|
this.OnDisposing();
|
||||||
|
|
||||||
|
this.CycleStopwatch.Stop();
|
||||||
|
this.StateChangedEvent.Dispose();
|
||||||
|
this.CycleCompletedEvent.Dispose();
|
||||||
|
this.CycleCancellation.Dispose();
|
||||||
|
|
||||||
|
this.IsDisposed = true;
|
||||||
|
this.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 Int32 ComputeCycleDelay(WorkerState initialWorkerState) {
|
||||||
|
Int64 elapsedMillis = this.CycleStopwatch.ElapsedMilliseconds;
|
||||||
|
TimeSpan period = this.Period;
|
||||||
|
Double periodMillis = period.TotalMilliseconds;
|
||||||
|
Double delayMillis = periodMillis - elapsedMillis;
|
||||||
|
|
||||||
|
return initialWorkerState == WorkerState.Paused || period == TimeSpan.MaxValue || delayMillis >= Int32.MaxValue ? Timeout.Infinite : elapsedMillis >= periodMillis ? 0 : Convert.ToInt32(Math.Floor(delayMillis));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,151 +1,146 @@
|
|||||||
namespace Swan.Threading
|
using System;
|
||||||
{
|
using System.Diagnostics;
|
||||||
using System;
|
using System.Threading;
|
||||||
using System.Diagnostics;
|
using System.Threading.Tasks;
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
namespace Swan.Threading {
|
||||||
|
/// <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>
|
/// <summary>
|
||||||
/// Represents a class that implements delay logic for thread workers.
|
/// Provides a delay implementation which simply waits on the task and cancels on
|
||||||
|
/// the cancellation token.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class WorkerDelayProvider
|
public static IWorkerDelayProvider Token => new TokenCancellableDelay();
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the default delay provider.
|
|
||||||
/// </summary>
|
|
||||||
public static IWorkerDelayProvider Default => TokenTimeout;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides a delay implementation which simply waits on the task and cancels on
|
/// Provides a delay implementation which waits on the task and cancels on both,
|
||||||
/// the cancellation token.
|
/// the cancellation token and a wanted delay timeout.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static IWorkerDelayProvider Token => new TokenCancellableDelay();
|
public static IWorkerDelayProvider TokenTimeout => new TokenTimeoutCancellableDelay();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides a delay implementation which waits on the task and cancels on both,
|
/// Provides a delay implementation which uses short sleep intervals of 5ms.
|
||||||
/// the cancellation token and a wanted delay timeout.
|
/// </summary>
|
||||||
/// </summary>
|
public static IWorkerDelayProvider TokenSleep => new TokenSleepDelay();
|
||||||
public static IWorkerDelayProvider TokenTimeout => new TokenTimeoutCancellableDelay();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides a delay implementation which uses short sleep intervals of 5ms.
|
/// Provides a delay implementation which uses short delay intervals of 5ms and
|
||||||
/// </summary>
|
/// a wait on the delay task in the final loop.
|
||||||
public static IWorkerDelayProvider TokenSleep => new TokenSleepDelay();
|
/// </summary>
|
||||||
|
public static IWorkerDelayProvider SteppedToken => new SteppedTokenDelay();
|
||||||
|
|
||||||
/// <summary>
|
private class TokenCancellableDelay : IWorkerDelayProvider {
|
||||||
/// Provides a delay implementation which uses short delay intervals of 5ms and
|
public void ExecuteCycleDelay(Int32 wantedDelay, Task delayTask, CancellationToken token) {
|
||||||
/// a wait on the delay task in the final loop.
|
if(wantedDelay == 0 || wantedDelay < -1) {
|
||||||
/// </summary>
|
return;
|
||||||
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
|
// for wanted delays of less than 30ms it is not worth
|
||||||
{
|
// passing a timeout or a token as it only adds unnecessary
|
||||||
public void ExecuteCycleDelay(int wantedDelay, Task delayTask, CancellationToken token)
|
// overhead.
|
||||||
{
|
if(wantedDelay <= 30) {
|
||||||
if (wantedDelay == 0 || wantedDelay < -1)
|
try {
|
||||||
return;
|
delayTask.Wait(token);
|
||||||
|
} catch { /* ignore */ }
|
||||||
// for wanted delays of less than 30ms it is not worth
|
return;
|
||||||
// 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
|
// only wait on the cancellation token
|
||||||
{
|
// or until the task completes normally
|
||||||
private readonly Stopwatch _elapsedWait = new Stopwatch();
|
try {
|
||||||
|
delayTask.Wait(token);
|
||||||
public void ExecuteCycleDelay(int wantedDelay, Task delayTask, CancellationToken token)
|
} catch { /* ignore */ }
|
||||||
{
|
}
|
||||||
_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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class TokenTimeoutCancellableDelay : IWorkerDelayProvider {
|
||||||
|
public void ExecuteCycleDelay(Int32 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(Int32 wantedDelay, Task delayTask, CancellationToken token) {
|
||||||
|
this._elapsedWait.Restart();
|
||||||
|
|
||||||
|
if(wantedDelay == 0 || wantedDelay < -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while(!token.IsCancellationRequested) {
|
||||||
|
Thread.Sleep(5);
|
||||||
|
|
||||||
|
if(wantedDelay != Timeout.Infinite && this._elapsedWait.ElapsedMilliseconds >= wantedDelay) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SteppedTokenDelay : IWorkerDelayProvider {
|
||||||
|
private const Int32 StepMilliseconds = 15;
|
||||||
|
private readonly Stopwatch _elapsedWait = new Stopwatch();
|
||||||
|
|
||||||
|
public void ExecuteCycleDelay(Int32 wantedDelay, Task delayTask, CancellationToken token) {
|
||||||
|
this._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) {
|
||||||
|
Int32 remainingWaitTime = wantedDelay - Convert.ToInt32(this._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(this._elapsedWait.ElapsedMilliseconds >= wantedDelay) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,124 +1,118 @@
|
|||||||
using System.Collections.Concurrent;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Swan
|
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, Boolean> _queuedNotifications = new ConcurrentDictionary<String, Boolean>();
|
||||||
|
private readonly Boolean _useDeferredNotifications;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A base class for implementing models that fire notifications when their properties change.
|
/// Initializes a new instance of the <see cref="ViewModelBase"/> class.
|
||||||
/// This class is ideal for implementing MVVM driven UIs.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <seealso cref="INotifyPropertyChanged" />
|
/// <param name="useDeferredNotifications">Set to <c>true</c> to use deferred notifications in the background.</param>
|
||||||
public abstract class ViewModelBase : INotifyPropertyChanged
|
protected ViewModelBase(Boolean useDeferredNotifications) => this._useDeferredNotifications = useDeferredNotifications;
|
||||||
{
|
|
||||||
private readonly ConcurrentDictionary<string, bool> _queuedNotifications = new ConcurrentDictionary<string, bool>();
|
|
||||||
private readonly bool _useDeferredNotifications;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ViewModelBase"/> class.
|
/// Initializes a new instance of the <see cref="ViewModelBase"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="useDeferredNotifications">Set to <c>true</c> to use deferred notifications in the background.</param>
|
protected ViewModelBase() : this(false) {
|
||||||
protected ViewModelBase(bool useDeferredNotifications)
|
// placeholder
|
||||||
{
|
|
||||||
_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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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 Boolean SetProperty<T>(ref T storage, T value, [CallerMemberName] String propertyName = "", String[] notifyAlso = null) {
|
||||||
|
if(EqualityComparer<T>.Default.Equals(storage, value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
storage = value;
|
||||||
|
this.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) => this.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) {
|
||||||
|
this._queuedNotifications[mainProperty] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the state for notification properties
|
||||||
|
if(auxiliaryProperties != null) {
|
||||||
|
foreach(String property in auxiliaryProperties) {
|
||||||
|
if(String.IsNullOrWhiteSpace(property) == false) {
|
||||||
|
this._queuedNotifications[property] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Depending on operation mode, either fire the notifications in the background
|
||||||
|
// or fire them immediately
|
||||||
|
if(this._useDeferredNotifications) {
|
||||||
|
_ = Task.Run(this.NotifyQueuedProperties);
|
||||||
|
} else {
|
||||||
|
this.NotifyQueuedProperties();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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.
|
||||||
|
String[] propertyNames = this._queuedNotifications.Keys.ToArray();
|
||||||
|
|
||||||
|
// Iterate through the properties
|
||||||
|
foreach(String property in propertyNames) {
|
||||||
|
// don't notify if we don't have a change
|
||||||
|
if(!this._queuedNotifications[property]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// notify and reset queued state to false
|
||||||
|
try {
|
||||||
|
this.OnPropertyChanged(property);
|
||||||
|
} finally { this._queuedNotifications[property] = false; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user