Codingstyle nullable

This commit is contained in:
BlubbFish 2019-12-08 21:23:54 +01:00
parent aa9fcd4a36
commit d0b26111dd
50 changed files with 8669 additions and 9749 deletions

File diff suppressed because it is too large Load Diff

View File

@ -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}."; /// <summary>
private const string ErrorText = "Duplicate implementation of type {0} found ({1})."; /// Initializes a new instance of the <see cref="DependencyContainerRegistrationException" /> class.
/// </summary>
/// <summary> /// <param name="type">The type.</param>
/// Initializes a new instance of the <see cref="DependencyContainerRegistrationException"/> class. /// <param name="method">The method.</param>
/// </summary> /// <param name="isTypeFactory">if set to <c>true</c> [is type factory].</param>
/// <param name="registerType">Type of the register.</param> public DependencyContainerRegistrationException(Type type, String method, Boolean isTypeFactory = false) : base(isTypeFactory ? String.Format(RegisterErrorText, type.FullName, method) : String.Format(ConvertErrorText, type.FullName, method)) {
/// <param name="types">The types.</param> }
public DependencyContainerRegistrationException(Type registerType, IEnumerable<Type> types)
: base(string.Format(ErrorText, registerType, GetTypesString(types))) private static String GetTypesString(IEnumerable<Type> types) => 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, 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));
}
}
} }

View File

@ -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> /// <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] public DependencyContainerResolutionException(Type type) : base($"Unable to resolve type: {type.FullName}") {
public class DependencyContainerResolutionException : Exception }
{
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="DependencyContainerResolutionException"/> class. /// Initializes a new instance of the <see cref="DependencyContainerResolutionException"/> class.
/// </summary> /// </summary>
/// <param name="type">The type.</param> /// <param name="type">The type.</param>
public DependencyContainerResolutionException(Type type) /// <param name="innerException">The inner exception.</param>
: base($"Unable to resolve type: {type.FullName}") 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>
/// <param name="innerException">The inner exception.</param>
public DependencyContainerResolutionException(Type type, Exception innerException)
: base($"Unable to resolve type: {type.FullName}", innerException)
{
}
}
} }

View File

@ -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> /// <summary>
/// Resolution settings. /// Gets the default options (attempt resolution of unregistered types, fail on named resolution if name not found).
/// </summary> /// </summary>
public class DependencyContainerResolveOptions public static DependencyContainerResolveOptions Default { get; } = new DependencyContainerResolveOptions();
{
/// <summary>
/// Gets the default options (attempt resolution of unregistered types, fail on named resolution if name not found).
/// </summary>
public static DependencyContainerResolveOptions Default { get; } = new DependencyContainerResolveOptions();
/// <summary>
/// Gets or sets the unregistered resolution action.
/// </summary>
/// <value>
/// The unregistered resolution action.
/// </value>
public DependencyContainerUnregisteredResolutionAction UnregisteredResolutionAction { get; set; } =
DependencyContainerUnregisteredResolutionAction.AttemptResolve;
/// <summary>
/// Gets or sets the named resolution failure action.
/// </summary>
/// <value>
/// The named resolution failure action.
/// </value>
public DependencyContainerNamedResolutionFailureAction NamedResolutionFailureAction { get; set; } =
DependencyContainerNamedResolutionFailureAction.Fail;
/// <summary>
/// Gets the constructor parameters.
/// </summary>
/// <value>
/// The constructor parameters.
/// </value>
public Dictionary<string, object> ConstructorParameters { get; } = new Dictionary<string, object>();
/// <summary>
/// Clones this instance.
/// </summary>
/// <returns></returns>
public DependencyContainerResolveOptions Clone() => new DependencyContainerResolveOptions
{
NamedResolutionFailureAction = NamedResolutionFailureAction,
UnregisteredResolutionAction = UnregisteredResolutionAction,
};
}
/// <summary> /// <summary>
/// Defines Resolution actions. /// Gets or sets the unregistered resolution action.
/// </summary> /// </summary>
public enum DependencyContainerUnregisteredResolutionAction /// <value>
{ /// The unregistered resolution action.
/// <summary> /// </value>
/// Attempt to resolve type, even if the type isn't registered. public DependencyContainerUnregisteredResolutionAction UnregisteredResolutionAction { get; set; } = DependencyContainerUnregisteredResolutionAction.AttemptResolve;
///
/// 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 or sets the named resolution failure action.
/// </summary> /// </summary>
public enum DependencyContainerNamedResolutionFailureAction /// <value>
{ /// The named resolution failure action.
/// <summary> /// </value>
/// The attempt unnamed resolution public DependencyContainerNamedResolutionFailureAction NamedResolutionFailureAction { get; set; } = DependencyContainerNamedResolutionFailureAction.Fail;
/// </summary>
AttemptUnnamedResolution,
/// <summary>
/// The fail
/// </summary>
Fail,
}
/// <summary> /// <summary>
/// Enumerates duplicate definition actions. /// Gets the constructor parameters.
/// </summary> /// </summary>
public enum DependencyContainerDuplicateImplementationAction /// <value>
{ /// The constructor parameters.
/// <summary> /// </value>
/// The register single public Dictionary<System.String, System.Object> ConstructorParameters { get; } = new Dictionary<System.String, System.Object>();
/// </summary>
RegisterSingle, /// <summary>
/// Clones this instance.
/// <summary> /// </summary>
/// The register multiple /// <returns></returns>
/// </summary> public DependencyContainerResolveOptions Clone() => new DependencyContainerResolveOptions {
RegisterMultiple, NamedResolutionFailureAction = NamedResolutionFailureAction,
UnregisteredResolutionAction = UnregisteredResolutionAction,
/// <summary> };
/// The fail }
/// </summary>
Fail, /// <summary>
} /// Defines Resolution actions.
/// </summary>
public enum DependencyContainerUnregisteredResolutionAction {
/// <summary>
/// Attempt to resolve type, even if the type isn't registered.
///
/// Registered types/options will always take precedence.
/// </summary>
AttemptResolve,
/// <summary>
/// Fail resolution if type not explicitly registered
/// </summary>
Fail,
/// <summary>
/// Attempt to resolve unregistered type if requested type is generic
/// and no registration exists for the specific generic parameters used.
///
/// Registered types/options will always take precedence.
/// </summary>
GenericsOnly,
}
/// <summary>
/// Enumerates failure actions.
/// </summary>
public enum DependencyContainerNamedResolutionFailureAction {
/// <summary>
/// The attempt unnamed resolution
/// </summary>
AttemptUnnamedResolution,
/// <summary>
/// The fail
/// </summary>
Fail,
}
/// <summary>
/// Enumerates duplicate definition actions.
/// </summary>
public enum DependencyContainerDuplicateImplementationAction {
/// <summary>
/// The register single
/// </summary>
RegisterSingle,
/// <summary>
/// The register multiple
/// </summary>
RegisterMultiple,
/// <summary>
/// The fail
/// </summary>
Fail,
}
} }

View File

@ -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))
{
}
}
} }

View File

@ -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> /// <summary>
/// Represents an abstract class for Object Factory. /// 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> /// </summary>
public abstract class ObjectFactoryBase public virtual Boolean AssumeConstruction => false;
{
/// <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> /// <summary>
/// IObjectFactory that creates new instances of types for each resolution. /// The type the factory instantiates.
/// </summary> /// </summary>
internal class MultiInstanceFactory : ObjectFactoryBase public abstract Type CreatesType {
{ get;
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> /// <summary>
/// IObjectFactory that invokes a specified delegate to construct the object. /// Constructor to use, if specified.
/// </summary> /// </summary>
internal class DelegateFactory : ObjectFactoryBase public ConstructorInfo Constructor {
{ get; private set;
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> /// <summary>
/// IObjectFactory that invokes a specified delegate to construct the object /// Gets the singleton variant.
/// Holds the delegate using a weak reference.
/// </summary> /// </summary>
internal class WeakDelegateFactory : ObjectFactoryBase /// <value>
{ /// The singleton variant.
private readonly Type _registerType; /// </value>
/// <exception cref="DependencyContainerRegistrationException">singleton.</exception>
private readonly WeakReference _factory; public virtual ObjectFactoryBase SingletonVariant => throw new DependencyContainerRegistrationException(this.GetType(), "singleton");
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. /// Gets the multi instance variant.
/// </summary> /// </summary>
internal class InstanceFactory : 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 _instance; public virtual ObjectFactoryBase MultiInstanceVariant => throw new DependencyContainerRegistrationException(this.GetType(), "multi-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 strong reference variant.
/// </summary> /// </summary>
internal class WeakInstanceFactory : ObjectFactoryBase, IDisposable /// <value>
{ /// The strong reference variant.
private readonly Type _registerType; /// </value>
private readonly Type _registerImplementation; /// <exception cref="DependencyContainerRegistrationException">strong reference.</exception>
private readonly WeakReference _instance; public virtual ObjectFactoryBase StrongReferenceVariant => throw new DependencyContainerRegistrationException(this.GetType(), "strong reference");
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 weak reference variant.
/// </summary> /// </summary>
internal class SingletonFactory : ObjectFactoryBase, IDisposable /// <value>
{ /// The weak reference variant.
private readonly Type _registerType; /// </value>
private readonly Type _registerImplementation; /// <exception cref="DependencyContainerRegistrationException">weak reference.</exception>
private readonly object _singletonLock = new object(); public virtual ObjectFactoryBase WeakReferenceVariant => throw new DependencyContainerRegistrationException(this.GetType(), "weak reference");
private object _current;
/// <summary>
public SingletonFactory(Type registerType, Type registerImplementation) /// Create the type.
{ /// </summary>
if (registerImplementation.IsAbstract || registerImplementation.IsInterface) /// <param name="requestedType">Type user requested to be resolved.</param>
{ /// <param name="container">Container that requested the creation.</param>
throw new DependencyContainerRegistrationException(registerImplementation, nameof(SingletonFactory), true); /// <param name="options">The options.</param>
} /// <returns> Instance of type. </returns>
public abstract Object GetObject(Type requestedType, DependencyContainer container, DependencyContainerResolveOptions options);
if (!DependencyContainer.IsValidAssignment(registerType, registerImplementation))
{ /// <summary>
throw new DependencyContainerRegistrationException(registerImplementation, nameof(SingletonFactory), true); /// Gets the factory for child container.
} /// </summary>
/// <param name="type">The type.</param>
_registerType = registerType; /// <param name="parent">The parent.</param>
_registerImplementation = registerImplementation; /// <param name="child">The child.</param>
} /// <returns></returns>
public virtual ObjectFactoryBase GetFactoryForChildContainer(Type type, DependencyContainer parent, DependencyContainer child) => this;
public override Type CreatesType => _registerImplementation; }
public override ObjectFactoryBase SingletonVariant => this; /// <inheritdoc />
/// <summary>
public override ObjectFactoryBase MultiInstanceVariant => /// IObjectFactory that creates new instances of types for each resolution.
new MultiInstanceFactory(_registerType, _registerImplementation); /// </summary>
internal class MultiInstanceFactory : ObjectFactoryBase {
public override object GetObject( private readonly Type _registerType;
Type requestedType, private readonly Type _registerImplementation;
DependencyContainer container,
DependencyContainerResolveOptions options) public MultiInstanceFactory(Type registerType, Type registerImplementation) {
{ if(registerImplementation.IsAbstract || registerImplementation.IsInterface) {
if (options.ConstructorParameters.Count != 0) throw new DependencyContainerRegistrationException(registerImplementation, "MultiInstanceFactory", true);
throw new ArgumentException("Cannot specify parameters for singleton types"); }
lock (_singletonLock) if(!DependencyContainer.IsValidAssignment(registerType, registerImplementation)) {
{ throw new DependencyContainerRegistrationException(registerImplementation, "MultiInstanceFactory", true);
if (_current == null) }
_current = container.RegisteredTypes.ConstructType(_registerImplementation, Constructor, options);
} this._registerType = registerType;
this._registerImplementation = registerImplementation;
return _current; }
}
public override Type CreatesType => this._registerImplementation;
public override ObjectFactoryBase GetFactoryForChildContainer(
Type type, public override ObjectFactoryBase SingletonVariant =>
DependencyContainer parent, new SingletonFactory(this._registerType, this._registerImplementation);
DependencyContainer child)
{ public override ObjectFactoryBase MultiInstanceVariant => this;
// 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 public override Object GetObject(Type requestedType, DependencyContainer container, DependencyContainerResolveOptions options) {
// the type before the child container does. try {
GetObject(type, parent, DependencyContainerResolveOptions.Default); return container.RegisteredTypes.ConstructType(this._registerImplementation, this.Constructor, options);
return this; } catch(DependencyContainerResolutionException ex) {
} throw new DependencyContainerResolutionException(this._registerType, ex);
}
public void Dispose() => (_current as IDisposable)?.Dispose(); }
} }
/// <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();
}
} }

View File

@ -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>
/// Initializes a new instance of the <see cref="MultiRegisterOptions"/> class. if(currentFactory == null) {
/// </summary> throw new DependencyContainerRegistrationException(this._registration.Type, "singleton");
/// <param name="registerOptions">The register options.</param> }
public MultiRegisterOptions(IEnumerable<RegisterOptions> registerOptions)
{ return this._registeredTypes.AddUpdateRegistration(this._registration, currentFactory.SingletonVariant);
_registerOptions = registerOptions; }
}
/// <summary>
/// <summary> /// Make registration multi-instance if possible.
/// Make registration a singleton (single instance) if possible. /// </summary>
/// </summary> /// <returns>A registration options for fluent API.</returns>
/// <returns>A registration multi-instance for fluent API.</returns> /// <exception cref="DependencyContainerRegistrationException">Generic constraint registration exception.</exception>
/// <exception cref="DependencyContainerRegistrationException">Generic Constraint Registration Exception.</exception> public RegisterOptions AsMultiInstance() {
public MultiRegisterOptions AsSingleton() ObjectFactoryBase currentFactory = this._registeredTypes.GetCurrentFactory(this._registration);
{
_registerOptions = ExecuteOnAllRegisterOptions(ro => ro.AsSingleton()); if(currentFactory == null) {
return this; throw new DependencyContainerRegistrationException(this._registration.Type, "multi-instance");
} }
/// <summary> return this._registeredTypes.AddUpdateRegistration(this._registration, currentFactory.MultiInstanceVariant);
/// Make registration multi-instance if possible. }
/// </summary>
/// <returns>A registration multi-instance for fluent API.</returns> /// <summary>
/// <exception cref="DependencyContainerRegistrationException">Generic Constraint Registration Exception.</exception> /// Make registration hold a weak reference if possible.
public MultiRegisterOptions AsMultiInstance() /// </summary>
{ /// <returns>A registration options for fluent API.</returns>
_registerOptions = ExecuteOnAllRegisterOptions(ro => ro.AsMultiInstance()); /// <exception cref="DependencyContainerRegistrationException">Generic constraint registration exception.</exception>
return this; public RegisterOptions WithWeakReference() {
} ObjectFactoryBase currentFactory = this._registeredTypes.GetCurrentFactory(this._registration);
private IEnumerable<RegisterOptions> ExecuteOnAllRegisterOptions( if(currentFactory == null) {
Func<RegisterOptions, RegisterOptions> action) throw new DependencyContainerRegistrationException(this._registration.Type, "weak reference");
{ }
return _registerOptions.Select(action).ToList();
} 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();
}
} }

View File

@ -1,67 +1,61 @@
namespace Swan.DependencyInjection using System;
{
using System; namespace Swan.DependencyInjection {
public partial class DependencyContainer {
public partial class DependencyContainer /// <summary>
{ /// Represents a Type Registration within the IoC Container.
/// <summary> /// </summary>
/// Represents a Type Registration within the IoC Container. public sealed class TypeRegistration {
/// </summary> private readonly Int32 _hashCode;
public sealed class TypeRegistration
{ /// <summary>
private readonly int _hashCode; /// Initializes a new instance of the <see cref="TypeRegistration"/> class.
/// </summary>
/// <summary> /// <param name="type">The type.</param>
/// Initializes a new instance of the <see cref="TypeRegistration"/> class. /// <param name="name">The name.</param>
/// </summary> public TypeRegistration(Type type, String name = null) {
/// <param name="type">The type.</param> this.Type = type;
/// <param name="name">The name.</param> this.Name = name ?? String.Empty;
public TypeRegistration(Type type, string name = null)
{ this._hashCode = String.Concat(this.Type.FullName, "|", this.Name).GetHashCode();
Type = type; }
Name = name ?? string.Empty;
/// <summary>
_hashCode = string.Concat(Type.FullName, "|", Name).GetHashCode(); /// Gets the type.
} /// </summary>
/// <value>
/// <summary> /// The type.
/// Gets the type. /// </value>
/// </summary> public Type Type {
/// <value> get;
/// The type. }
/// </value>
public Type Type { get; } /// <summary>
/// Gets the name.
/// <summary> /// </summary>
/// Gets the name. /// <value>
/// </summary> /// The name.
/// <value> /// </value>
/// The name. public String Name {
/// </value> 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) /// <summary>
return false; /// Returns a hash code for this instance.
/// </summary>
return string.Compare(Name, typeRegistration.Name, StringComparison.Ordinal) == 0; /// <returns>
} /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.
/// </returns>
/// <summary> public override Int32 GetHashCode() => this._hashCode;
/// Returns a hash code for this instance. }
/// </summary> }
/// <returns>
/// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.
/// </returns>
public override int GetHashCode() => _hashCode;
}
}
} }

View File

@ -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>();
internal IEnumerable<Object> Resolve(Type resolveType, Boolean includeUnnamed) {
private readonly DependencyContainer _dependencyContainer; 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; }
}
return registrations.Select(registration => this.ResolveInternal(registration, DependencyContainerResolveOptions.Default));
/// <summary> }
/// Represents a delegate to build an object with the parameters.
/// </summary> internal ObjectFactoryBase GetCurrentFactory(DependencyContainer.TypeRegistration registration) {
/// <param name="parameters">The parameters.</param> _ = this.TryGetValue(registration, out ObjectFactoryBase? current);
/// <returns>The built object.</returns>
public delegate object ObjectConstructor(params object[] parameters); return current!;
}
internal IEnumerable<object> Resolve(Type resolveType, bool includeUnnamed)
{ internal RegisterOptions Register(Type registerType, String name, ObjectFactoryBase factory) => this.AddUpdateRegistration(new DependencyContainer.TypeRegistration(registerType, name), factory);
var registrations = Keys.Where(tr => tr.Type == resolveType)
.Concat(GetParentRegistrationsForType(resolveType)).Distinct(); internal RegisterOptions AddUpdateRegistration(DependencyContainer.TypeRegistration typeRegistration, ObjectFactoryBase factory) {
this[typeRegistration] = factory;
if (!includeUnnamed)
registrations = registrations.Where(tr => !string.IsNullOrEmpty(tr.Name)); return new RegisterOptions(this, typeRegistration);
}
return registrations.Select(registration =>
ResolveInternal(registration, DependencyContainerResolveOptions.Default)); internal Boolean RemoveRegistration(DependencyContainer.TypeRegistration typeRegistration) => this.TryRemove(typeRegistration, out _);
}
internal Object ResolveInternal(DependencyContainer.TypeRegistration registration, DependencyContainerResolveOptions? options = null) {
internal ObjectFactoryBase GetCurrentFactory(DependencyContainer.TypeRegistration registration) if(options == null) {
{ options = DependencyContainerResolveOptions.Default;
TryGetValue(registration, out var current); }
return current; // Attempt container resolution
} if(this.TryGetValue(registration, out ObjectFactoryBase? factory)) {
try {
internal RegisterOptions Register(Type registerType, string name, ObjectFactoryBase factory) return factory.GetObject(registration.Type, this._dependencyContainer, options);
=> AddUpdateRegistration(new DependencyContainer.TypeRegistration(registerType, name), factory); } catch(DependencyContainerResolutionException) {
throw;
internal RegisterOptions AddUpdateRegistration(DependencyContainer.TypeRegistration typeRegistration, ObjectFactoryBase factory) } catch(Exception ex) {
{ throw new DependencyContainerResolutionException(registration.Type, ex);
this[typeRegistration] = factory; }
}
return new RegisterOptions(this, typeRegistration);
} // Attempt to get a factory from parent if we can
ObjectFactoryBase? bubbledObjectFactory = this.GetParentObjectFactory(registration);
internal bool RemoveRegistration(DependencyContainer.TypeRegistration typeRegistration) if(bubbledObjectFactory != null) {
=> TryRemove(typeRegistration, out _); try {
return bubbledObjectFactory.GetObject(registration.Type, this._dependencyContainer, options);
internal object ResolveInternal( } catch(DependencyContainerResolutionException) {
DependencyContainer.TypeRegistration registration, throw;
DependencyContainerResolveOptions? options = null) } catch(Exception ex) {
{ throw new DependencyContainerResolutionException(registration.Type, ex);
if (options == null) }
options = DependencyContainerResolveOptions.Default; }
// Attempt container resolution // Fail if requesting named resolution and settings set to fail if unresolved
if (TryGetValue(registration, out var factory)) if(!String.IsNullOrEmpty(registration.Name) && options.NamedResolutionFailureAction == DependencyContainerNamedResolutionFailureAction.Fail) {
{ throw new DependencyContainerResolutionException(registration.Type);
try }
{
return factory.GetObject(registration.Type, _dependencyContainer, options); // Attempted unnamed fallback container resolution if relevant and requested
} if(!String.IsNullOrEmpty(registration.Name) && options.NamedResolutionFailureAction == DependencyContainerNamedResolutionFailureAction.AttemptUnnamedResolution) {
catch (DependencyContainerResolutionException) if(this.TryGetValue(new DependencyContainer.TypeRegistration(registration.Type, String.Empty), out factory)) {
{ try {
throw; return factory.GetObject(registration.Type, this._dependencyContainer, options);
} } catch(DependencyContainerResolutionException) {
catch (Exception ex) throw;
{ } catch(Exception ex) {
throw new DependencyContainerResolutionException(registration.Type, ex); throw new DependencyContainerResolutionException(registration.Type, ex);
} }
} }
}
// Attempt to get a factory from parent if we can
var bubbledObjectFactory = GetParentObjectFactory(registration); // Attempt unregistered construction if possible and requested
if (bubbledObjectFactory != null) Boolean isValid = options.UnregisteredResolutionAction == DependencyContainerUnregisteredResolutionAction.AttemptResolve || registration.Type.IsGenericType && options.UnregisteredResolutionAction == DependencyContainerUnregisteredResolutionAction.GenericsOnly;
{
try return isValid && !registration.Type.IsAbstract && !registration.Type.IsInterface ? this.ConstructType(registration.Type, null, options) : throw new DependencyContainerResolutionException(registration.Type);
{ }
return bubbledObjectFactory.GetObject(registration.Type, _dependencyContainer, options);
} internal Boolean CanResolve(DependencyContainer.TypeRegistration registration, DependencyContainerResolveOptions? options = null) {
catch (DependencyContainerResolutionException) if(options == null) {
{ options = DependencyContainerResolveOptions.Default;
throw; }
}
catch (Exception ex) Type checkType = registration.Type;
{ String name = registration.Name;
throw new DependencyContainerResolutionException(registration.Type, ex);
} 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
if (!string.IsNullOrEmpty(registration.Name) && options.NamedResolutionFailureAction == // Fail if requesting named resolution and settings set to fail if unresolved
DependencyContainerNamedResolutionFailureAction.Fail) // Or bubble up if we have a parent
throw new DependencyContainerResolutionException(registration.Type); 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(registration.Name) && options.NamedResolutionFailureAction ==
DependencyContainerNamedResolutionFailureAction.AttemptUnnamedResolution) // Attempted unnamed fallback container resolution if relevant and requested
{ if(!String.IsNullOrEmpty(name) && options.NamedResolutionFailureAction == DependencyContainerNamedResolutionFailureAction.AttemptUnnamedResolution) {
if (TryGetValue(new DependencyContainer.TypeRegistration(registration.Type, string.Empty), out factory)) if(this.TryGetValue(new DependencyContainer.TypeRegistration(checkType), out factory)) {
{ return factory.AssumeConstruction ? true : this.GetBestConstructor(factory.CreatesType, options) != null;
try }
{ }
return factory.GetObject(registration.Type, _dependencyContainer, options);
} // Check if type is an automatic lazy factory request or an IEnumerable<ResolveType>
catch (DependencyContainerResolutionException) if(IsAutomaticLazyFactoryRequest(checkType) || registration.Type.IsIEnumerable()) {
{ return true;
throw; }
}
catch (Exception ex) // Attempt unregistered construction if possible and requested
{ // If we cant', bubble if we have a parent
throw new DependencyContainerResolutionException(registration.Type, ex); 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
// Attempt unregistered construction if possible and requested return this._dependencyContainer.Parent != null && this._dependencyContainer.Parent.RegisteredTypes.CanResolve(registration, options.Clone());
var isValid = (options.UnregisteredResolutionAction == }
DependencyContainerUnregisteredResolutionAction.AttemptResolve) ||
(registration.Type.IsGenericType && options.UnregisteredResolutionAction == internal Object ConstructType(Type implementationType, ConstructorInfo? constructor, DependencyContainerResolveOptions? options = null) {
DependencyContainerUnregisteredResolutionAction.GenericsOnly); Type typeToConstruct = implementationType;
return isValid && !registration.Type.IsAbstract && !registration.Type.IsInterface if(constructor == null) {
? ConstructType(registration.Type, null, options) // Try and get the best constructor that we can construct
: throw new DependencyContainerResolutionException(registration.Type); // if we can't construct any then get the constructor
} // with the least number of parameters so we can throw a meaningful
// resolve exception
internal bool CanResolve( constructor = this.GetBestConstructor(typeToConstruct, options) ?? GetTypeConstructors(typeToConstruct).LastOrDefault();
DependencyContainer.TypeRegistration registration, }
DependencyContainerResolveOptions? options = null)
{ if(constructor == null) {
if (options == null) throw new DependencyContainerResolutionException(typeToConstruct);
options = DependencyContainerResolveOptions.Default; }
var checkType = registration.Type; ParameterInfo[] ctorParams = constructor.GetParameters();
var name = registration.Name; Object?[] args = new Object?[ctorParams.Length];
if (TryGetValue(new DependencyContainer.TypeRegistration(checkType, name), out var factory)) for(Int32 parameterIndex = 0; parameterIndex < ctorParams.Length; parameterIndex++) {
{ ParameterInfo currentParam = ctorParams[parameterIndex];
if (factory.AssumeConstruction)
return true; try {
args[parameterIndex] = options?.ConstructorParameters.GetValueOrDefault(currentParam.Name, this.ResolveInternal(new DependencyContainer.TypeRegistration(currentParam.ParameterType), options.Clone()));
if (factory.Constructor == null) } catch(DependencyContainerResolutionException ex) {
return GetBestConstructor(factory.CreatesType, options) != null; // If a constructor parameter can't be resolved
// it will throw, so wrap it and throw that this can't
return CanConstruct(factory.Constructor, options); // be resolved.
} throw new DependencyContainerResolutionException(typeToConstruct, ex);
} catch(Exception ex) {
// Fail if requesting named resolution and settings set to fail if unresolved throw new DependencyContainerResolutionException(typeToConstruct, ex);
// 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; try {
return CreateObjectConstructionDelegateWithCache(constructor).Invoke(args);
// Attempted unnamed fallback container resolution if relevant and requested } catch(Exception ex) {
if (!string.IsNullOrEmpty(name) && options.NamedResolutionFailureAction == throw new DependencyContainerResolutionException(typeToConstruct, ex);
DependencyContainerNamedResolutionFailureAction.AttemptUnnamedResolution) }
{ }
if (TryGetValue(new DependencyContainer.TypeRegistration(checkType), out factory))
{ private static ObjectConstructor CreateObjectConstructionDelegateWithCache(ConstructorInfo constructor) {
if (factory.AssumeConstruction) if(ObjectConstructorCache.TryGetValue(constructor, out ObjectConstructor? objectConstructor)) {
return true; return objectConstructor;
}
return GetBestConstructor(factory.CreatesType, options) != null;
} // 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
// Check if type is an automatic lazy factory request or an IEnumerable<ResolveType> // every creation.
if (IsAutomaticLazyFactoryRequest(checkType) || registration.Type.IsIEnumerable()) ParameterInfo[] constructorParams = constructor.GetParameters();
return true; ParameterExpression lambdaParams = Expression.Parameter(typeof(Object[]), "parameters");
Expression[] newParams = new Expression[constructorParams.Length];
// Attempt unregistered construction if possible and requested
// If we cant', bubble if we have a parent for(Int32 i = 0; i < constructorParams.Length; i++) {
if ((options.UnregisteredResolutionAction == BinaryExpression paramsParameter = Expression.ArrayIndex(lambdaParams, Expression.Constant(i));
DependencyContainerUnregisteredResolutionAction.AttemptResolve) ||
(checkType.IsGenericType && options.UnregisteredResolutionAction == newParams[i] = Expression.Convert(paramsParameter, constructorParams[i].ParameterType);
DependencyContainerUnregisteredResolutionAction.GenericsOnly)) }
{
return (GetBestConstructor(checkType, options) != null) || NewExpression newExpression = Expression.New(constructor, newParams);
(_dependencyContainer.Parent?.RegisteredTypes.CanResolve(registration, options.Clone()) ?? false);
} LambdaExpression constructionLambda = Expression.Lambda(typeof(ObjectConstructor), newExpression, lambdaParams);
// Bubble resolution up the container tree if we have a parent objectConstructor = (ObjectConstructor)constructionLambda.Compile();
return _dependencyContainer.Parent != null && _dependencyContainer.Parent.RegisteredTypes.CanResolve(registration, options.Clone());
} ObjectConstructorCache[constructor] = objectConstructor;
return objectConstructor;
internal object ConstructType( }
Type implementationType,
ConstructorInfo constructor, private static IEnumerable<ConstructorInfo> GetTypeConstructors(Type type) => type.GetConstructors().OrderByDescending(ctor => ctor.GetParameters().Length);
DependencyContainerResolveOptions? options = null)
{ private static Boolean IsAutomaticLazyFactoryRequest(Type type) {
var typeToConstruct = implementationType; if(!type.IsGenericType) {
return false;
if (constructor == null) }
{
// Try and get the best constructor that we can construct Type genericType = type.GetGenericTypeDefinition();
// if we can't construct any then get the constructor
// with the least number of parameters so we can throw a meaningful // Just a func
// resolve exception if(genericType == typeof(Func<>)) {
constructor = GetBestConstructor(typeToConstruct, options) ?? return true;
GetTypeConstructors(typeToConstruct).LastOrDefault(); }
}
// 2 parameter func with string as first parameter (name)
if (constructor == null) if(genericType == typeof(Func<,>) && type.GetGenericArguments()[0] == typeof(String)) {
throw new DependencyContainerResolutionException(typeToConstruct); return true;
}
var ctorParams = constructor.GetParameters();
var args = new object?[ctorParams.Length]; // 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>);
for (var parameterIndex = 0; parameterIndex < ctorParams.Length; parameterIndex++) }
{
var currentParam = ctorParams[parameterIndex]; private ObjectFactoryBase? GetParentObjectFactory(DependencyContainer.TypeRegistration registration) => this._dependencyContainer.Parent == null
? null
try : this._dependencyContainer.Parent.RegisteredTypes.TryGetValue(registration, out ObjectFactoryBase? factory) ? factory.GetFactoryForChildContainer(registration.Type, this._dependencyContainer.Parent, this._dependencyContainer) : this._dependencyContainer.Parent.RegisteredTypes.GetParentObjectFactory(registration);
{
args[parameterIndex] = options?.ConstructorParameters.GetValueOrDefault(currentParam.Name, ResolveInternal(new DependencyContainer.TypeRegistration(currentParam.ParameterType), options.Clone())); private ConstructorInfo? GetBestConstructor(Type type, DependencyContainerResolveOptions? options) => type.IsValueType ? null : GetTypeConstructors(type).FirstOrDefault(ctor => this.CanConstruct(ctor, options));
}
catch (DependencyContainerResolutionException ex) private Boolean CanConstruct(MethodBase ctor, DependencyContainerResolveOptions? options) {
{ foreach(ParameterInfo parameter in ctor.GetParameters()) {
// If a constructor parameter can't be resolved if(String.IsNullOrEmpty(parameter.Name)) {
// it will throw, so wrap it and throw that this can't return false;
// be resolved. }
throw new DependencyContainerResolutionException(typeToConstruct, ex);
} Boolean isParameterOverload = options!.ConstructorParameters.ContainsKey(parameter.Name);
catch (Exception ex)
{ if(parameter.ParameterType.IsPrimitive && !isParameterOverload) {
throw new DependencyContainerResolutionException(typeToConstruct, ex); return false;
} }
}
if(!isParameterOverload && !this.CanResolve(new DependencyContainer.TypeRegistration(parameter.ParameterType), options.Clone())) {
try return false;
{ }
return CreateObjectConstructionDelegateWithCache(constructor).Invoke(args); }
}
catch (Exception ex) return true;
{ }
throw new DependencyContainerResolutionException(typeToConstruct, ex);
} 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));
} }
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));
}
} }

View File

@ -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(); /// <summary>
private ISyncLocker? _locker = SyncLockerFactory.Create(useSlim: true); /// Gets or sets the clock position.
private long _offsetTicks; /// </summary>
private double _speedRatio = 1.0d; public TimeSpan Position {
private bool _isDisposed; get {
using(this._locker?.AcquireReaderLock()) {
/// <summary> return TimeSpan.FromTicks(this._offsetTicks + Convert.ToInt64(this._chrono.Elapsed.Ticks * this.SpeedRatio));
/// Initializes a new instance of the <see cref="RealTimeClock"/> class. }
/// The clock starts paused and at the 0 position. }
/// </summary> }
public RealTimeClock()
{ /// <summary>
Reset(); /// Gets a value indicating whether the clock is running.
} /// </summary>
public Boolean IsRunning {
/// <summary> get {
/// Gets or sets the clock position. using(this._locker?.AcquireReaderLock()) {
/// </summary> return this._chrono.IsRunning;
public TimeSpan Position }
{ }
get }
{
using (_locker?.AcquireReaderLock()) /// <summary>
{ /// Gets or sets the speed ratio at which the clock runs.
return TimeSpan.FromTicks( /// </summary>
_offsetTicks + Convert.ToInt64(_chrono.Elapsed.Ticks * SpeedRatio)); public Double SpeedRatio {
} get {
} using(this._locker?.AcquireReaderLock()) {
} return this._speedRatio;
}
/// <summary> }
/// Gets a value indicating whether the clock is running. set {
/// </summary> using(this._locker?.AcquireWriterLock()) {
public bool IsRunning if(value < 0d) {
{ value = 0d;
get }
{
using (_locker?.AcquireReaderLock()) // Capture the initial position se we set it even after the Speed Ratio has changed
{ // this ensures a smooth position transition
return _chrono.IsRunning; TimeSpan initialPosition = this.Position;
} this._speedRatio = value;
} this.Update(initialPosition);
} }
}
/// <summary> }
/// Gets or sets the speed ratio at which the clock runs.
/// </summary> /// <summary>
public double SpeedRatio /// Sets a new position value atomically.
{ /// </summary>
get /// <param name="value">The new value that the position property will hold.</param>
{ public void Update(TimeSpan value) {
using (_locker?.AcquireReaderLock()) using(this._locker?.AcquireWriterLock()) {
{ Boolean resume = this._chrono.IsRunning;
return _speedRatio; this._chrono.Reset();
} this._offsetTicks = value.Ticks;
} if(resume) {
set this._chrono.Start();
{ }
using (_locker?.AcquireWriterLock()) }
{ }
if (value < 0d) value = 0d;
/// <summary>
// Capture the initial position se we set it even after the Speed Ratio has changed /// Starts or resumes the clock.
// this ensures a smooth position transition /// </summary>
var initialPosition = Position; public void Play() {
_speedRatio = value; using(this._locker?.AcquireWriterLock()) {
Update(initialPosition); if(this._chrono.IsRunning) {
} return;
} }
}
this._chrono.Start();
/// <summary> }
/// Sets a new position value atomically. }
/// </summary>
/// <param name="value">The new value that the position property will hold.</param> /// <summary>
public void Update(TimeSpan value) /// Pauses the clock.
{ /// </summary>
using (_locker?.AcquireWriterLock()) public void Pause() {
{ using(this._locker?.AcquireWriterLock()) {
var resume = _chrono.IsRunning; this._chrono.Stop();
_chrono.Reset(); }
_offsetTicks = value.Ticks; }
if (resume) _chrono.Start();
} /// <summary>
} /// Sets the clock position to 0 and stops it.
/// The speed ratio is not modified.
/// <summary> /// </summary>
/// Starts or resumes the clock. public void Reset() {
/// </summary> using(this._locker?.AcquireWriterLock()) {
public void Play() this._offsetTicks = 0;
{ this._chrono.Reset();
using (_locker?.AcquireWriterLock()) }
{ }
if (_chrono.IsRunning) return;
_chrono.Start(); /// <inheritdoc />
} public void Dispose() {
} if(this._isDisposed) {
return;
/// <summary> }
/// Pauses the clock.
/// </summary> this._isDisposed = true;
public void Pause() this._locker?.Dispose();
{ this._locker = null;
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;
}
}
} }

View File

@ -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) {
/// <summary> throw new ArgumentNullException(nameof(@this));
/// The raw contents of this MailMessage as a MemoryStream. }
/// </summary>
/// <param name="this">The caller.</param> MemoryStream result = new MemoryStream();
/// <returns>A MemoryStream with the raw contents of this MailMessage.</returns> Object mailWriter = MimeMessageConstants.MailWriterConstructor.Invoke(new Object[] { result });
public static MemoryStream ToMimeMessage(this MailMessage @this) _ = MimeMessageConstants.SendMethod.Invoke(@this, PrivateInstanceFlags, null, MimeMessageConstants.IsRunningInDotNetFourPointFive ? new[] { mailWriter, true, true } : new[] { mailWriter, true }, null);
{
if (@this == null) result = new MemoryStream(result.ToArray());
throw new ArgumentNullException(nameof(@this)); _ = MimeMessageConstants.CloseMethod.Invoke(mailWriter, PrivateInstanceFlags, null, Array.Empty<Object>(), null);
result.Position = 0;
var result = new MemoryStream(); return result;
var mailWriter = MimeMessageConstants.MailWriterConstructor.Invoke(new object[] { result }); }
MimeMessageConstants.SendMethod.Invoke(
@this, internal static class MimeMessageConstants {
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 #pragma warning disable DE0005 // API is deprecated
public static readonly Type MailWriter = typeof(SmtpClient).Assembly.GetType("System.Net.Mail.MailWriter"); public static readonly Type MailWriter = typeof(SmtpClient).Assembly.GetType("System.Net.Mail.MailWriter");
#pragma warning restore DE0005 // API is deprecated #pragma warning restore DE0005 // API is deprecated
public static readonly ConstructorInfo MailWriterConstructor = MailWriter.GetConstructor(PrivateInstanceFlags, null, new[] { typeof(Stream) }, null); 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 CloseMethod = MailWriter.GetMethod("Close", PrivateInstanceFlags);
public static readonly MethodInfo SendMethod = typeof(MailMessage).GetMethod("Send", PrivateInstanceFlags); public static readonly MethodInfo SendMethod = typeof(MailMessage).GetMethod("Send", PrivateInstanceFlags);
public static readonly bool IsRunningInDotNetFourPointFive = SendMethod.GetParameters().Length == 3; public static readonly Boolean IsRunningInDotNetFourPointFive = SendMethod.GetParameters().Length == 3;
} }
} }
} }

View File

@ -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> /// <summary>
/// Provides various extension methods for networking-related tasks. /// Determines whether the IP address is private.
/// </summary> /// </summary>
public static class NetworkExtensions /// <param name="this">The IP address.</param>
{ /// <returns>
/// <summary> /// True if the IP Address is private; otherwise, false.
/// Determines whether the IP address is private. /// </returns>
/// </summary> /// <exception cref="ArgumentNullException">address.</exception>
/// <param name="this">The IP address.</param> public static Boolean IsPrivateAddress(this IPAddress @this) {
/// <returns> if(@this == null) {
/// True if the IP Address is private; otherwise, false. throw new ArgumentNullException(nameof(@this));
/// </returns> }
/// <exception cref="ArgumentNullException">address.</exception>
public static bool IsPrivateAddress(this IPAddress @this) Byte[] octets = @this.ToString().Split(new[] { "." }, StringSplitOptions.RemoveEmptyEntries).Select(Byte.Parse).ToArray();
{ Boolean is24Bit = octets[0] == 10;
if (@this == null) Boolean is20Bit = octets[0] == 172 && octets[1] >= 16 && octets[1] <= 31;
throw new ArgumentNullException(nameof(@this)); Boolean is16Bit = octets[0] == 192 && octets[1] == 168;
var octets = @this.ToString().Split(new[] { "." }, StringSplitOptions.RemoveEmptyEntries).Select(byte.Parse).ToArray(); return is24Bit || is20Bit || is16Bit;
var is24Bit = octets[0] == 10; }
var is20Bit = octets[0] == 172 && (octets[1] >= 16 && octets[1] <= 31);
var is16Bit = octets[0] == 192 && octets[1] == 168; /// <summary>
/// Converts an IPv4 Address to its Unsigned, 32-bit integer representation.
return is24Bit || is20Bit || is16Bit; /// </summary>
} /// <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.
/// Converts an IPv4 Address to its Unsigned, 32-bit integer representation. /// </returns>
/// </summary> /// <exception cref="ArgumentNullException">address.</exception>
/// <param name="this">The address.</param> /// <exception cref="ArgumentException">InterNetwork - address.</exception>
/// <returns> public static UInt32 ToUInt32(this IPAddress @this) {
/// A 32-bit unsigned integer converted from four bytes at a specified position in a byte array. if(@this == null) {
/// </returns> throw new ArgumentNullException(nameof(@this));
/// <exception cref="ArgumentNullException">address.</exception> }
/// <exception cref="ArgumentException">InterNetwork - address.</exception>
public static uint ToUInt32(this IPAddress @this) if(@this.AddressFamily != AddressFamily.InterNetwork) {
{ throw new ArgumentException($"Address has to be of family '{nameof(AddressFamily.InterNetwork)}'", nameof(@this));
if (@this == null) }
throw new ArgumentNullException(nameof(@this));
Byte[] addressBytes = @this.GetAddressBytes();
if (@this.AddressFamily != AddressFamily.InterNetwork) if(BitConverter.IsLittleEndian) {
throw new ArgumentException($"Address has to be of family '{nameof(AddressFamily.InterNetwork)}'", nameof(@this)); Array.Reverse(addressBytes);
}
var addressBytes = @this.GetAddressBytes();
if (BitConverter.IsLittleEndian) return BitConverter.ToUInt32(addressBytes, 0);
Array.Reverse(addressBytes); }
}
return BitConverter.ToUInt32(addressBytes, 0);
}
}
} }

View File

@ -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> /// <summary>
/// Extension methods. /// Runs a service in console mode.
/// </summary> /// </summary>
public static class WindowsServicesExtensions /// <param name="this">The service to run.</param>
{ /// <param name="loggerSource">The logger source.</param>
/// <summary> /// <exception cref="ArgumentNullException">this.</exception>
/// Runs a service in console mode. [Obsolete("This extension method will be removed in version 3.0")]
/// </summary> public static void RunInConsoleMode(this ServiceBase @this, String loggerSource = null) {
/// <param name="this">The service to run.</param> if(@this == null) {
/// <param name="loggerSource">The logger source.</param> throw new ArgumentNullException(nameof(@this));
/// <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) RunInConsoleMode(new[] { @this }, loggerSource);
{ }
if (@this == null)
throw new ArgumentNullException(nameof(@this)); /// <summary>
/// Runs a set of services in console mode.
RunInConsoleMode(new[] { @this }, loggerSource); /// </summary>
} /// <param name="this">The services to run.</param>
/// <param name="loggerSource">The logger source.</param>
/// <summary> /// <exception cref="ArgumentNullException">this.</exception>
/// Runs a set of services 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 services 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));
/// <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) const String onStartMethodName = "OnStart";
{ const String onStopMethodName = "OnStop";
if (@this == null)
throw new ArgumentNullException(nameof(@this)); MethodInfo onStartMethod = typeof(ServiceBase).GetMethod(onStartMethodName, BindingFlags.Instance | BindingFlags.NonPublic);
MethodInfo onStopMethod = typeof(ServiceBase).GetMethod(onStopMethodName, BindingFlags.Instance | BindingFlags.NonPublic);
const string onStartMethodName = "OnStart";
const string onStopMethodName = "OnStop"; if(onStartMethod == null || onStopMethod == null) {
throw new InvalidOperationException("The ServiceBase class isn't available.");
var onStartMethod = typeof(ServiceBase).GetMethod(onStartMethodName, }
BindingFlags.Instance | BindingFlags.NonPublic);
var onStopMethod = typeof(ServiceBase).GetMethod(onStopMethodName, List<Thread> serviceThreads = new List<Thread>();
BindingFlags.Instance | BindingFlags.NonPublic); "Starting services . . .".Info(loggerSource ?? SwanRuntime.EntryAssemblyName.Name);
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>() });
var serviceThreads = new List<Thread>(); $"Started service '{service.GetType().Name}'".Info(loggerSource ?? service.GetType().Name);
"Starting services . . .".Info(loggerSource ?? SwanRuntime.EntryAssemblyName.Name); });
foreach (var service in @this) serviceThreads.Add(thread);
{ thread.Start();
var thread = new Thread(() => }
{
onStartMethod.Invoke(service, new object[] { Array.Empty<string>() }); "Press any key to stop all services.".Info(loggerSource ?? SwanRuntime.EntryAssemblyName.Name);
$"Started service '{service.GetType().Name}'".Info(loggerSource ?? service.GetType().Name); _ = Terminal.ReadKey(true, true);
}); "Stopping services . . .".Info(SwanRuntime.EntryAssemblyName.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);
Terminal.ReadKey(true, true); foreach(Thread thread in serviceThreads) {
"Stopping services . . .".Info(SwanRuntime.EntryAssemblyName.Name); thread.Join();
}
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);
}
}
} }

View File

@ -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; }
}
} }

View File

@ -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> /// <summary>
MessageHubSubscriptionToken SubscriptionToken { get; } /// Whether delivery should be attempted.
/// </summary>
/// <summary> /// <param name="message">Message that may potentially be delivered.</param>
/// Whether delivery should be attempted. /// <returns><c>true</c> - ok to send, <c>false</c> - should not attempt to send.</returns>
/// </summary> Boolean ShouldAttemptDelivery(IMessageHubMessage message);
/// <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> /// <summary>
bool ShouldAttemptDelivery(IMessageHubMessage message); /// Deliver the message.
/// </summary>
/// <summary> /// <param name="message">Message to deliver.</param>
/// Deliver the message. void Deliver(IMessageHubMessage message);
/// </summary> }
/// <param name="message">Message to deliver.</param>
void Deliver(IMessageHubMessage message);
}
} }

View File

@ -1,442 +1,371 @@
// =============================================================================== // ===============================================================================
// TinyIoC - TinyMessenger // TinyIoC - TinyMessenger
// //
// A simple messenger/event aggregator. // A simple messenger/event aggregator.
// //
// https://github.com/grumpydev/TinyIoC/blob/master/src/TinyIoC/TinyMessenger.cs // https://github.com/grumpydev/TinyIoC/blob/master/src/TinyIoC/TinyMessenger.cs
// =============================================================================== // ===============================================================================
// Copyright © Steven Robbins. All rights reserved. // Copyright © Steven Robbins. All rights reserved.
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY // THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY
// OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT // OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT
// 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
namespace Swan.Messaging using System.Threading.Tasks;
{ using System;
using System.Threading.Tasks; using System.Collections.Generic;
using System; using System.Linq;
using System.Collections.Generic;
using System.Linq; namespace Swan.Messaging {
#region Message Types / Interfaces
#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.
/// </summary>
/// <param name="message">The message.</param>
/// <param name="subscription">The subscription.</param>
void Deliver(IMessageHubMessage message, IMessageHubSubscription subscription);
}
/// <summary>
/// Default "pass through" proxy.
///
/// Does nothing other than deliver the message.
/// </summary>
public sealed class MessageHubDefaultProxy : IMessageHubProxy {
private MessageHubDefaultProxy() {
// placeholder
}
/// <summary>
/// Singleton instance of the proxy.
/// </summary>
public static MessageHubDefaultProxy Instance { get; } = new MessageHubDefaultProxy();
/// <summary>
/// Delivers the specified message.
/// </summary>
/// <param name="message">The message.</param>
/// <param name="subscription">The subscription.</param>
public void Deliver(IMessageHubMessage message, IMessageHubSubscription subscription) => subscription.Deliver(message);
}
#endregion
#region Hub Interface
/// <summary>
/// Messenger hub responsible for taking subscriptions/publications and delivering of messages.
/// </summary>
public interface IMessageHub {
/// <summary>
/// Subscribe to a message type with the given destination and delivery action.
/// Messages will be delivered via the specified proxy.
/// ///
/// A message proxy can be used to intercept/alter messages and/or /// All messages of this type will be delivered.
/// marshal delivery actions onto a particular thread.
/// </summary> /// </summary>
public interface IMessageHubProxy /// <typeparam name="TMessage">Type of message.</typeparam>
{ /// <param name="deliveryAction">Action to invoke when message is delivered.</param>
/// <summary> /// <param name="useStrongReferences">Use strong references to destination and deliveryAction.</param>
/// Delivers the specified message. /// <param name="proxy">Proxy to use when delivering the messages.</param>
/// </summary> /// <returns>MessageSubscription used to unsubscribing.</returns>
/// <param name="message">The message.</param> MessageHubSubscriptionToken Subscribe<TMessage>(Action<TMessage> deliveryAction, Boolean useStrongReferences, IMessageHubProxy proxy) where TMessage : class, IMessageHubMessage;
/// <param name="subscription">The subscription.</param>
void Deliver(IMessageHubMessage message, IMessageHubSubscription subscription);
}
/// <summary> /// <summary>
/// Default "pass through" proxy. /// 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 nothing other than deliver the message. /// Does not throw an exception if the subscription is not found.
/// </summary> /// </summary>
public sealed class MessageHubDefaultProxy : IMessageHubProxy /// <typeparam name="TMessage">Type of message.</typeparam>
{ /// <param name="subscriptionToken">Subscription token received from Subscribe.</param>
private MessageHubDefaultProxy() void Unsubscribe<TMessage>(MessageHubSubscriptionToken subscriptionToken) where TMessage : class, IMessageHubMessage;
{
// placeholder
}
/// <summary>
/// Singleton instance of the proxy.
/// </summary>
public static MessageHubDefaultProxy Instance { get; } = new MessageHubDefaultProxy();
/// <summary>
/// Delivers the specified message.
/// </summary>
/// <param name="message">The message.</param>
/// <param name="subscription">The subscription.</param>
public void Deliver(IMessageHubMessage message, IMessageHubSubscription subscription)
=> subscription.Deliver(message);
}
#endregion
#region Hub Interface
/// <summary> /// <summary>
/// Messenger hub responsible for taking subscriptions/publications and delivering of messages. /// Publish a message to any subscribers.
/// </summary> /// </summary>
public interface IMessageHub /// <typeparam name="TMessage">Type of message.</typeparam>
{ /// <param name="message">Message to deliver.</param>
/// <summary> void Publish<TMessage>(TMessage message) where TMessage : class, IMessageHubMessage;
/// Subscribe to a message type with the given destination and delivery action.
/// Messages will be delivered via the specified proxy. /// <summary>
/// /// Publish a message to any subscribers asynchronously.
/// All messages of this type will be delivered. /// </summary>
/// </summary> /// <typeparam name="TMessage">Type of message.</typeparam>
/// <typeparam name="TMessage">Type of message.</typeparam> /// <param name="message">Message to deliver.</param>
/// <param name="deliveryAction">Action to invoke when message is delivered.</param> /// <returns>A task from Publish action.</returns>
/// <param name="useStrongReferences">Use strong references to destination and deliveryAction.</param> Task PublishAsync<TMessage>(TMessage message) where TMessage : class, IMessageHubMessage;
/// <param name="proxy">Proxy to use when delivering the messages.</param> }
/// <returns>MessageSubscription used to unsubscribing.</returns>
MessageHubSubscriptionToken Subscribe<TMessage>( #endregion
Action<TMessage> deliveryAction,
bool useStrongReferences, #region Hub Implementation
IMessageHubProxy proxy)
where TMessage : class, IMessageHubMessage; /// <inheritdoc />
/// <example>
/// <summary> /// The following code describes how to use a MessageHub. Both the
/// Subscribe to a message type with the given destination and delivery action with the given filter. /// subscription and the message sending are done in the same place but this is only for explanatory purposes.
/// Messages will be delivered via the specified proxy. /// <code>
/// All references are held with WeakReferences /// class Example
/// Only messages that "pass" the filter will be delivered. /// {
/// </summary> /// using Swan;
/// <typeparam name="TMessage">Type of message.</typeparam> /// using Swan.Components;
/// <param name="deliveryAction">Action to invoke when message is delivered.</param> ///
/// <param name="messageFilter">The message filter.</param> /// static void Main()
/// <param name="useStrongReferences">Use strong references to destination and deliveryAction.</param> /// {
/// <param name="proxy">Proxy to use when delivering the messages.</param> /// // using DependencyContainer to create an instance of MessageHub
/// <returns> /// var messageHub = DependencyContainer
/// MessageSubscription used to unsubscribing. /// .Current
/// </returns> /// .Resolve&lt;IMessageHub&gt;() as MessageHub;
MessageHubSubscriptionToken Subscribe<TMessage>( ///
Action<TMessage> deliveryAction, /// // create an instance of the publisher class
Func<TMessage, bool> messageFilter, /// // which has a string as its content
bool useStrongReferences, /// var message = new MessageHubGenericMessage&lt;string&gt;(new object(), "SWAN");
IMessageHubProxy proxy) ///
where TMessage : class, IMessageHubMessage; /// // subscribe to the publisher's event
/// // and just print out the content which is a string
/// <summary> /// // a token is returned which can be used to unsubscribe later on
/// Unsubscribe from a particular message type. /// var token = messageHub
/// /// .Subscribe&lt;MessageHubGenericMessage&lt;string&gt;&gt;(m =&gt; m.Content.Info());
/// Does not throw an exception if the subscription is not found. ///
/// </summary> /// // publish the message described above which is
/// <typeparam name="TMessage">Type of message.</typeparam> /// // the string 'SWAN'
/// <param name="subscriptionToken">Subscription token received from Subscribe.</param> /// messageHub.Publish(message);
void Unsubscribe<TMessage>(MessageHubSubscriptionToken subscriptionToken) ///
where TMessage : class, IMessageHubMessage; /// // unsuscribe, we will no longer receive any messages
/// messageHub.Unsubscribe&lt;MessageHubGenericMessage&lt;string&gt;&gt;(token);
/// <summary> ///
/// Publish a message to any subscribers. /// Terminal.Flush();
/// </summary> /// }
/// <typeparam name="TMessage">Type of message.</typeparam> ///
/// <param name="message">Message to deliver.</param> /// }
void Publish<TMessage>(TMessage message) /// </code>
where TMessage : class, IMessageHubMessage; /// </example>
public sealed class MessageHub : IMessageHub {
/// <summary> #region Private Types and Interfaces
/// Publish a message to any subscribers asynchronously.
/// </summary> private readonly Object _subscriptionsPadlock = new Object();
/// <typeparam name="TMessage">Type of message.</typeparam>
/// <param name="message">Message to deliver.</param> private readonly Dictionary<Type, List<SubscriptionItem>> _subscriptions = new Dictionary<Type, List<SubscriptionItem>>();
/// <returns>A task from Publish action.</returns>
Task PublishAsync<TMessage>(TMessage message) private class WeakMessageSubscription<TMessage> : IMessageHubSubscription where TMessage : class, IMessageHubMessage {
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 #endregion
#region Hub Implementation #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;
}
MessageHubSubscriptionToken subscriptionToken = new MessageHubSubscriptionToken(this, typeof(TMessage));
IMessageHubSubscription subscription = useStrongReferences ? new StrongMessageSubscription<TMessage>(subscriptionToken, deliveryAction, messageFilter) : (IMessageHubSubscription)new WeakMessageSubscription<TMessage>(subscriptionToken, deliveryAction, messageFilter);
currentSubscriptions.Add(new SubscriptionItem(proxy ?? MessageHubDefaultProxy.Instance, subscription));
return subscriptionToken;
}
}
/// <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
/// { lock(this._subscriptionsPadlock) {
/// using Swan; if(!this._subscriptions.TryGetValue(typeof(TMessage), out List<SubscriptionItem>? currentSubscriptions)) {
/// using Swan.Components; return;
/// }
/// static void Main()
/// { List<SubscriptionItem> currentlySubscribed = currentSubscriptions.Where(sub => ReferenceEquals(sub.Subscription.SubscriptionToken, subscriptionToken)).ToList();
/// // using DependencyContainer to create an instance of MessageHub
/// var messageHub = DependencyContainer currentlySubscribed.ForEach(sub => currentSubscriptions.Remove(sub));
/// .Current }
/// .Resolve&lt;IMessageHub&gt;() as MessageHub; }
///
/// // create an instance of the publisher class /// <summary>
/// // which has a string as its content /// Publish a message to any subscribers.
/// var message = new MessageHubGenericMessage&lt;string&gt;(new object(), "SWAN"); /// </summary>
/// /// <typeparam name="TMessage">Type of message.</typeparam>
/// // subscribe to the publisher's event /// <param name="message">Message to deliver.</param>
/// // and just print out the content which is a string public void Publish<TMessage>(TMessage message) where TMessage : class, IMessageHubMessage {
/// // a token is returned which can be used to unsubscribe later on if(message == null) {
/// var token = messageHub throw new ArgumentNullException(nameof(message));
/// .Subscribe&lt;MessageHubGenericMessage&lt;string&gt;&gt;(m =&gt; m.Content.Info()); }
///
/// // publish the message described above which is List<SubscriptionItem> currentlySubscribed;
/// // the string 'SWAN' lock(this._subscriptionsPadlock) {
/// messageHub.Publish(message); if(!this._subscriptions.TryGetValue(typeof(TMessage), out List<SubscriptionItem>? currentSubscriptions)) {
/// return;
/// // unsuscribe, we will no longer receive any messages }
/// messageHub.Unsubscribe&lt;MessageHubGenericMessage&lt;string&gt;&gt;(token);
/// currentlySubscribed = currentSubscriptions.Where(sub => sub.Subscription.ShouldAttemptDelivery(message)).ToList();
/// Terminal.Flush(); }
/// }
/// currentlySubscribed.ForEach(sub => {
/// } try {
/// </code> sub.Proxy.Deliver(message, sub.Subscription);
/// </example> } catch {
public sealed class MessageHub : IMessageHub // Ignore any errors and carry on
{ }
#region Private Types and Interfaces });
}
private readonly object _subscriptionsPadlock = new object();
/// <summary>
private readonly Dictionary<Type, List<SubscriptionItem>> _subscriptions = /// Publish a message to any subscribers asynchronously.
new Dictionary<Type, List<SubscriptionItem>>(); /// </summary>
/// <typeparam name="TMessage">Type of message.</typeparam>
private class WeakMessageSubscription<TMessage> : IMessageHubSubscription /// <param name="message">Message to deliver.</param>
where TMessage : class, IMessageHubMessage /// <returns>A task with the publish.</returns>
{ public Task PublishAsync<TMessage>(TMessage message) where TMessage : class, IMessageHubMessage => Task.Run(() => this.Publish(message));
private readonly WeakReference _deliveryAction;
private readonly WeakReference _messageFilter;
/// <summary>
/// Initializes a new instance of the <see cref="WeakMessageSubscription{TMessage}" /> class.
/// </summary>
/// <param name="subscriptionToken">The subscription token.</param>
/// <param name="deliveryAction">The delivery action.</param>
/// <param name="messageFilter">The message filter.</param>
/// <exception cref="ArgumentNullException">subscriptionToken
/// or
/// deliveryAction
/// or
/// messageFilter.</exception>
public WeakMessageSubscription(
MessageHubSubscriptionToken subscriptionToken,
Action<TMessage> deliveryAction,
Func<TMessage, bool> messageFilter)
{
SubscriptionToken = subscriptionToken ?? throw new ArgumentNullException(nameof(subscriptionToken));
_deliveryAction = new WeakReference(deliveryAction);
_messageFilter = new WeakReference(messageFilter);
}
public MessageHubSubscriptionToken SubscriptionToken { get; }
public bool ShouldAttemptDelivery(IMessageHubMessage message)
{
return _deliveryAction.IsAlive && _messageFilter.IsAlive &&
((Func<TMessage, bool>) _messageFilter.Target).Invoke((TMessage) message);
}
public void Deliver(IMessageHubMessage message)
{
if (_deliveryAction.IsAlive)
{
((Action<TMessage>) _deliveryAction.Target).Invoke((TMessage) message);
}
}
}
private class StrongMessageSubscription<TMessage> : IMessageHubSubscription
where TMessage : class, IMessageHubMessage
{
private readonly Action<TMessage> _deliveryAction;
private readonly Func<TMessage, bool> _messageFilter;
/// <summary>
/// Initializes a new instance of the <see cref="StrongMessageSubscription{TMessage}" /> class.
/// </summary>
/// <param name="subscriptionToken">The subscription token.</param>
/// <param name="deliveryAction">The delivery action.</param>
/// <param name="messageFilter">The message filter.</param>
/// <exception cref="ArgumentNullException">subscriptionToken
/// or
/// deliveryAction
/// or
/// messageFilter.</exception>
public StrongMessageSubscription(
MessageHubSubscriptionToken subscriptionToken,
Action<TMessage> deliveryAction,
Func<TMessage, bool> messageFilter)
{
SubscriptionToken = subscriptionToken ?? throw new ArgumentNullException(nameof(subscriptionToken));
_deliveryAction = deliveryAction;
_messageFilter = messageFilter;
}
public MessageHubSubscriptionToken SubscriptionToken { get; }
public bool ShouldAttemptDelivery(IMessageHubMessage message) => _messageFilter.Invoke((TMessage) message);
public void Deliver(IMessageHubMessage message) => _deliveryAction.Invoke((TMessage) message);
}
#endregion
#region Subscription dictionary
private class SubscriptionItem
{
public SubscriptionItem(IMessageHubProxy proxy, IMessageHubSubscription subscription)
{
Proxy = proxy;
Subscription = subscription;
}
public IMessageHubProxy Proxy { get; }
public IMessageHubSubscription Subscription { get; }
}
#endregion
#region Public API
/// <summary>
/// Subscribe to a message type with the given destination and delivery action.
/// Messages will be delivered via the specified proxy.
///
/// All messages of this type will be delivered.
/// </summary>
/// <typeparam name="TMessage">Type of message.</typeparam>
/// <param name="deliveryAction">Action to invoke when message is delivered.</param>
/// <param name="useStrongReferences">Use strong references to destination and deliveryAction. </param>
/// <param name="proxy">Proxy to use when delivering the messages.</param>
/// <returns>MessageSubscription used to unsubscribing.</returns>
public MessageHubSubscriptionToken Subscribe<TMessage>(
Action<TMessage> deliveryAction,
bool useStrongReferences = true,
IMessageHubProxy? proxy = null)
where TMessage : class, IMessageHubMessage
{
return Subscribe(deliveryAction, m => true, useStrongReferences, proxy);
}
/// <summary>
/// Subscribe to a message type with the given destination and delivery action with the given filter.
/// Messages will be delivered via the specified proxy.
/// All references are held with WeakReferences
/// Only messages that "pass" the filter will be delivered.
/// </summary>
/// <typeparam name="TMessage">Type of message.</typeparam>
/// <param name="deliveryAction">Action to invoke when message is delivered.</param>
/// <param name="messageFilter">The message filter.</param>
/// <param name="useStrongReferences">Use strong references to destination and deliveryAction.</param>
/// <param name="proxy">Proxy to use when delivering the messages.</param>
/// <returns>
/// MessageSubscription used to unsubscribing.
/// </returns>
public MessageHubSubscriptionToken Subscribe<TMessage>(
Action<TMessage> deliveryAction,
Func<TMessage, bool> messageFilter,
bool useStrongReferences = true,
IMessageHubProxy? proxy = null)
where TMessage : class, IMessageHubMessage
{
if (deliveryAction == null)
throw new ArgumentNullException(nameof(deliveryAction));
if (messageFilter == null)
throw new ArgumentNullException(nameof(messageFilter));
lock (_subscriptionsPadlock)
{
if (!_subscriptions.TryGetValue(typeof(TMessage), out var currentSubscriptions))
{
currentSubscriptions = new List<SubscriptionItem>();
_subscriptions[typeof(TMessage)] = currentSubscriptions;
}
var subscriptionToken = new MessageHubSubscriptionToken(this, typeof(TMessage));
IMessageHubSubscription subscription;
if (useStrongReferences)
{
subscription = new StrongMessageSubscription<TMessage>(
subscriptionToken,
deliveryAction,
messageFilter);
}
else
{
subscription = new WeakMessageSubscription<TMessage>(
subscriptionToken,
deliveryAction,
messageFilter);
}
currentSubscriptions.Add(new SubscriptionItem(proxy ?? MessageHubDefaultProxy.Instance, subscription));
return subscriptionToken;
}
}
/// <inheritdoc />
public void Unsubscribe<TMessage>(MessageHubSubscriptionToken subscriptionToken)
where TMessage : class, IMessageHubMessage
{
if (subscriptionToken == null)
throw new ArgumentNullException(nameof(subscriptionToken));
lock (_subscriptionsPadlock)
{
if (!_subscriptions.TryGetValue(typeof(TMessage), out var currentSubscriptions))
return;
var currentlySubscribed = currentSubscriptions
.Where(sub => ReferenceEquals(sub.Subscription.SubscriptionToken, subscriptionToken))
.ToList();
currentlySubscribed.ForEach(sub => currentSubscriptions.Remove(sub));
}
}
/// <summary>
/// Publish a message to any subscribers.
/// </summary>
/// <typeparam name="TMessage">Type of message.</typeparam>
/// <param name="message">Message to deliver.</param>
public void Publish<TMessage>(TMessage message)
where TMessage : class, IMessageHubMessage
{
if (message == null)
throw new ArgumentNullException(nameof(message));
List<SubscriptionItem> currentlySubscribed;
lock (_subscriptionsPadlock)
{
if (!_subscriptions.TryGetValue(typeof(TMessage), out var currentSubscriptions))
return;
currentlySubscribed = currentSubscriptions
.Where(sub => sub.Subscription.ShouldAttemptDelivery(message))
.ToList();
}
currentlySubscribed.ForEach(sub =>
{
try
{
sub.Proxy.Deliver(message, sub.Subscription);
}
catch
{
// Ignore any errors and carry on
}
});
}
/// <summary>
/// Publish a message to any subscribers asynchronously.
/// </summary>
/// <typeparam name="TMessage">Type of message.</typeparam>
/// <param name="message">Message to deliver.</param>
/// <returns>A task with the publish.</returns>
public Task PublishAsync<TMessage>(TMessage message)
where TMessage : class, IMessageHubMessage
{
return Task.Run(() => Publish(message));
}
#endregion
}
#endregion #endregion
}
#endregion
} }

View File

@ -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> /// <summary>
/// Base class for messages that provides weak reference storage of the sender. /// 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> /// </summary>
public abstract class MessageHubMessageBase private readonly WeakReference _sender;
: IMessageHubMessage
{
/// <summary>
/// Store a WeakReference to the sender just in case anyone is daft enough to
/// keep the message around and prevent the sender from being collected.
/// </summary>
private readonly WeakReference _sender;
/// <summary>
/// Initializes a new instance of the <see cref="MessageHubMessageBase"/> class.
/// </summary>
/// <param name="sender">The sender.</param>
/// <exception cref="System.ArgumentNullException">sender.</exception>
protected MessageHubMessageBase(object sender)
{
if (sender == null)
throw new ArgumentNullException(nameof(sender));
_sender = new WeakReference(sender);
}
/// <inheritdoc />
public object Sender => _sender.Target;
}
/// <summary> /// <summary>
/// Generic message with user specified content. /// Initializes a new instance of the <see cref="MessageHubMessageBase"/> class.
/// </summary> /// </summary>
/// <typeparam name="TContent">Content type to store.</typeparam> /// <param name="sender">The sender.</param>
public class MessageHubGenericMessage<TContent> /// <exception cref="System.ArgumentNullException">sender.</exception>
: MessageHubMessageBase protected MessageHubMessageBase(Object sender) {
{ if(sender == null) {
/// <summary> throw new ArgumentNullException(nameof(sender));
/// Initializes a new instance of the <see cref="MessageHubGenericMessage{TContent}"/> class. }
/// </summary>
/// <param name="sender">The sender.</param> this._sender = new WeakReference(sender);
/// <param name="content">The content.</param> }
public MessageHubGenericMessage(object sender, TContent content)
: base(sender) /// <inheritdoc />
{ public Object Sender => this._sender.Target;
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 { get; protected set; } 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>
/// Contents of the message.
/// </summary>
public TContent Content {
get; protected set;
}
}
} }

View File

@ -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) {
/// <summary> throw new ArgumentNullException(nameof(hub));
/// Initializes a new instance of the <see cref="MessageHubSubscriptionToken"/> class. }
/// </summary>
/// <param name="hub">The hub.</param> if(!typeof(IMessageHubMessage).IsAssignableFrom(messageType)) {
/// <param name="messageType">Type of the message.</param> throw new ArgumentOutOfRangeException(nameof(messageType));
/// <exception cref="System.ArgumentNullException">hub.</exception> }
/// <exception cref="System.ArgumentOutOfRangeException">messageType.</exception>
public MessageHubSubscriptionToken(IMessageHub hub, Type messageType) this._hub = new WeakReference(hub);
{ this._messageType = messageType;
if (hub == null) }
{
throw new ArgumentNullException(nameof(hub)); /// <inheritdoc />
} public void Dispose() {
if(this._hub.IsAlive && this._hub.Target is IMessageHub hub) {
if (!typeof(IMessageHubMessage).IsAssignableFrom(messageType)) MethodInfo unsubscribeMethod = typeof(IMessageHub).GetMethod(nameof(IMessageHub.Unsubscribe), new[] { typeof(MessageHubSubscriptionToken) });
{ unsubscribeMethod = unsubscribeMethod.MakeGenericMethod(this._messageType);
throw new ArgumentOutOfRangeException(nameof(messageType)); _ = unsubscribeMethod.Invoke(hub, new Object[] { this });
} }
_hub = new WeakReference(hub); GC.SuppressFinalize(this);
_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);
}
}
} }

File diff suppressed because it is too large Load Diff

View File

@ -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> /// <summary>
/// The trigger was a forceful flush of the buffer /// The new line sequence bytes were received
/// </summary> /// </summary>
Flush, NewLineSequenceEncountered,
/// <summary> /// <summary>
/// The new line sequence bytes were received /// The buffer was full
/// </summary> /// </summary>
NewLineSequenceEncountered, BufferFull,
/// <summary> /// <summary>
/// The buffer was full /// The block size reached
/// </summary> /// </summary>
BufferFull, BlockSizeReached,
}
/// <summary>
/// The block size reached
/// </summary>
BlockSizeReached,
}
} }

View File

@ -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
{ /// <summary>
private readonly object _stateLock = new object(); /// Occurs when a new connection is accepted.
private TcpListener _listenerSocket; /// </summary>
private bool _cancellationPending; public event EventHandler<ConnectionAcceptedEventArgs> OnConnectionAccepted = (s, e) => { };
private CancellationTokenSource _cancelListening;
private Task? _backgroundWorkerTask; /// <summary>
private bool _hasDisposed; /// Occurs when a connection fails to get accepted
/// </summary>
public event EventHandler<ConnectionFailureEventArgs> OnConnectionFailure = (s, e) => { };
/// <summary>
/// Occurs when the listener stops.
/// </summary>
public event EventHandler<ConnectionListenerStoppedEventArgs> OnListenerStopped = (s, e) => { };
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="ConnectionListener"/> class.
/// </summary>
/// <param name="listenEndPoint">The listen end point.</param>
public ConnectionListener(IPEndPoint listenEndPoint) {
this.Id = Guid.NewGuid();
this.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(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();
#region Events continue;
}
/// <summary>
/// Occurs when a new connection requests a socket from the listener. OnConnectionAccepted(this, new ConnectionAcceptedEventArgs(client));
/// Set Cancel = true to prevent the TCP client from being accepted. } catch(Exception ex) {
/// </summary> OnConnectionFailure(this, new ConnectionFailureEventArgs(ex));
public event EventHandler<ConnectionAcceptingEventArgs> OnConnectionAccepting = (s, e) => { }; }
}
/// <summary>
/// Occurs when a new connection is accepted. OnListenerStopped(this, new ConnectionListenerStoppedEventArgs(this.LocalEndPoint));
/// </summary> } catch(ObjectDisposedException) {
public event EventHandler<ConnectionAcceptedEventArgs> OnConnectionAccepted = (s, e) => { }; OnListenerStopped(this, new ConnectionListenerStoppedEventArgs(this.LocalEndPoint));
} catch(Exception ex) {
/// <summary> OnListenerStopped(this,
/// Occurs when a connection fails to get accepted new ConnectionListenerStoppedEventArgs(this.LocalEndPoint, this._cancellationPending ? null : ex));
/// </summary> } finally {
public event EventHandler<ConnectionFailureEventArgs> OnConnectionFailure = (s, e) => { }; this._backgroundWorkerTask = null;
this._cancellationPending = false;
/// <summary> }
/// Occurs when the listener stops. }
/// </summary>
public event EventHandler<ConnectionListenerStoppedEventArgs> OnListenerStopped = (s, e) => { }; #endregion
}
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="ConnectionListener"/> class.
/// </summary>
/// <param name="listenEndPoint">The listen end point.</param>
public ConnectionListener(IPEndPoint listenEndPoint)
{
Id = Guid.NewGuid();
LocalEndPoint = listenEndPoint ?? throw new ArgumentNullException(nameof(listenEndPoint));
}
/// <summary>
/// Initializes a new instance of the <see cref="ConnectionListener"/> class.
/// It uses the loopback address for listening.
/// </summary>
/// <param name="listenPort">The listen port.</param>
public ConnectionListener(int listenPort)
: this(new IPEndPoint(IPAddress.Loopback, listenPort))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ConnectionListener"/> class.
/// </summary>
/// <param name="listenAddress">The listen address.</param>
/// <param name="listenPort">The listen port.</param>
public ConnectionListener(IPAddress listenAddress, int listenPort)
: this(new IPEndPoint(listenAddress, listenPort))
{
}
/// <summary>
/// Finalizes an instance of the <see cref="ConnectionListener"/> class.
/// </summary>
~ConnectionListener()
{
Dispose(false);
}
#endregion
#region Public Properties
/// <summary>
/// Gets the local end point on which we are listening.
/// </summary>
/// <value>
/// The local end point.
/// </value>
public IPEndPoint LocalEndPoint { get; }
/// <summary>
/// Gets a value indicating whether this listener is active.
/// </summary>
/// <value>
/// <c>true</c> if this instance is listening; otherwise, <c>false</c>.
/// </value>
public bool IsListening => _backgroundWorkerTask != null;
/// <summary>
/// Gets a unique identifier that gets automatically assigned upon instantiation of this class.
/// </summary>
/// <value>
/// The unique identifier.
/// </value>
public Guid Id { get; }
#endregion
#region Start and Stop
/// <summary>
/// Starts the listener in an asynchronous, non-blocking fashion.
/// Subscribe to the events of this class to gain access to connected client sockets.
/// </summary>
/// <exception cref="System.InvalidOperationException">Cancellation has already been requested. This listener is not reusable.</exception>
public void Start()
{
lock (_stateLock)
{
if (_backgroundWorkerTask != null)
{
return;
}
if (_cancellationPending)
{
throw new InvalidOperationException(
"Cancellation has already been requested. This listener is not reusable.");
}
_backgroundWorkerTask = DoWorkAsync();
}
}
/// <summary>
/// Stops the listener from receiving new connections.
/// This does not prevent the listener from .
/// </summary>
public void Stop()
{
lock (_stateLock)
{
_cancellationPending = true;
_listenerSocket?.Stop();
_cancelListening?.Cancel();
_backgroundWorkerTask?.Wait();
_backgroundWorkerTask = null;
_cancellationPending = false;
}
}
/// <summary>
/// Returns a <see cref="System.String" /> that represents this instance.
/// </summary>
/// <returns>
/// A <see cref="System.String" /> that represents this instance.
/// </returns>
public override string ToString() => LocalEndPoint.ToString();
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
private void Dispose(bool disposing)
{
if (_hasDisposed)
return;
if (disposing)
{
// Release managed resources
Stop();
}
_hasDisposed = true;
}
/// <summary>
/// Continuously checks for client connections until the Close method has been called.
/// </summary>
/// <returns>A task that represents the asynchronous connection operation.</returns>
private async Task DoWorkAsync()
{
_cancellationPending = false;
_listenerSocket = new TcpListener(LocalEndPoint);
_listenerSocket.Start();
_cancelListening = new CancellationTokenSource();
try
{
while (_cancellationPending == false)
{
try
{
var client = await Task.Run(() => _listenerSocket.AcceptTcpClientAsync(), _cancelListening.Token).ConfigureAwait(false);
var acceptingArgs = new ConnectionAcceptingEventArgs(client);
OnConnectionAccepting(this, acceptingArgs);
if (acceptingArgs.Cancel)
{
#if !NET461
client.Dispose();
#else
client.Close();
#endif
continue;
}
OnConnectionAccepted(this, new ConnectionAcceptedEventArgs(client));
}
catch (Exception ex)
{
OnConnectionFailure(this, new ConnectionFailureEventArgs(ex));
}
}
OnListenerStopped(this, new ConnectionListenerStoppedEventArgs(LocalEndPoint));
}
catch (ObjectDisposedException)
{
OnListenerStopped(this, new ConnectionListenerStoppedEventArgs(LocalEndPoint));
}
catch (Exception ex)
{
OnListenerStopped(this,
new ConnectionListenerStoppedEventArgs(LocalEndPoint, _cancellationPending ? null : ex));
}
finally
{
_backgroundWorkerTask = null;
_cancellationPending = false;
}
}
#endregion
}
} }

View File

@ -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; namespace Swan.Net.Dns {
/// <summary>
/// <summary> /// DnsClient public interfaces.
/// DnsClient public interfaces. /// </summary>
/// </summary> internal partial class DnsClient {
internal partial class DnsClient public interface IDnsMessage {
{ IList<DnsQuestion> Questions {
public interface IDnsMessage get;
{ }
IList<DnsQuestion> Questions { get; }
Int32 Size {
int Size { get; } get;
byte[] ToArray(); }
} Byte[] ToArray();
}
public interface IDnsMessageEntry
{ public interface IDnsMessageEntry {
DnsDomain Name { get; } DnsDomain Name {
DnsRecordType Type { get; } get;
DnsRecordClass Class { get; } }
DnsRecordType Type {
int Size { get; } get;
byte[] ToArray(); }
} DnsRecordClass Class {
get;
public interface IDnsResourceRecord : IDnsMessageEntry }
{
TimeSpan TimeToLive { get; } Int32 Size {
int DataLength { get; } get;
byte[] Data { get; } }
} Byte[] ToArray();
}
public interface IDnsRequest : IDnsMessage
{ public interface IDnsResourceRecord : IDnsMessageEntry {
int Id { get; set; } TimeSpan TimeToLive {
DnsOperationCode OperationCode { get; set; } get;
bool RecursionDesired { get; set; } }
} Int32 DataLength {
get;
public interface IDnsResponse : IDnsMessage }
{ Byte[] Data {
int Id { get; set; } get;
IList<IDnsResourceRecord> AnswerRecords { get; } }
IList<IDnsResourceRecord> AuthorityRecords { get; } }
IList<IDnsResourceRecord> AdditionalRecords { get; }
bool IsRecursionAvailable { get; set; } public interface IDnsRequest : IDnsMessage {
bool IsAuthorativeServer { get; set; } Int32 Id {
bool IsTruncated { get; set; } get; set;
DnsOperationCode OperationCode { get; set; } }
DnsResponseCode ResponseCode { get; set; } DnsOperationCode OperationCode {
} get; set;
}
public interface IDnsRequestResolver Boolean RecursionDesired {
{ get; set;
Task<DnsClientResponse> Request(DnsClientRequest request); }
} }
}
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

View File

@ -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; namespace Swan.Net.Dns {
/// <summary>
/// <summary> /// DnsClient public methods.
/// DnsClient public methods. /// </summary>
/// </summary> internal partial class DnsClient {
internal partial class DnsClient public abstract class DnsResourceRecordBase : IDnsResourceRecord {
{ private readonly IDnsResourceRecord _record;
public abstract class DnsResourceRecordBase : IDnsResourceRecord
{ protected DnsResourceRecordBase(IDnsResourceRecord record) => this._record = record;
private readonly IDnsResourceRecord _record;
public DnsDomain Name => this._record.Name;
protected DnsResourceRecordBase(IDnsResourceRecord record)
{ public DnsRecordType Type => this._record.Type;
_record = record;
} public DnsRecordClass Class => this._record.Class;
public DnsDomain Name => _record.Name; public TimeSpan TimeToLive => this._record.TimeToLive;
public DnsRecordType Type => _record.Type; public Int32 DataLength => this._record.DataLength;
public DnsRecordClass Class => _record.Class; public Byte[] Data => this._record.Data;
public TimeSpan TimeToLive => _record.TimeToLive; public Int32 Size => this._record.Size;
public int DataLength => _record.DataLength; protected virtual String[] IncludedProperties => new[] { nameof(this.Name), nameof(this.Type), nameof(this.Class), nameof(this.TimeToLive), nameof(this.DataLength) };
public byte[] Data => _record.Data; public Byte[] ToArray() => this._record.ToArray();
public int Size => _record.Size; public override String ToString() => Json.SerializeOnly(this, true, this.IncludedProperties);
}
protected virtual string[] IncludedProperties
=> new[] {nameof(Name), nameof(Type), nameof(Class), nameof(TimeToLive), nameof(DataLength)}; public class DnsResourceRecord : IDnsResourceRecord {
public DnsResourceRecord(DnsDomain domain, Byte[] data, DnsRecordType type, DnsRecordClass klass = DnsRecordClass.IN, TimeSpan ttl = default) {
public byte[] ToArray() => _record.ToArray(); this.Name = domain;
this.Type = type;
public override string ToString() this.Class = klass;
=> Json.SerializeOnly(this, true, IncludedProperties); this.TimeToLive = ttl;
} this.Data = data;
}
public class DnsResourceRecord : IDnsResourceRecord
{ public DnsDomain Name {
public DnsResourceRecord( get;
DnsDomain domain, }
byte[] data,
DnsRecordType type, public DnsRecordType Type {
DnsRecordClass klass = DnsRecordClass.IN, get;
TimeSpan ttl = default) }
{
Name = domain; public DnsRecordClass Class {
Type = type; get;
Class = klass; }
TimeToLive = ttl;
Data = data; public TimeSpan TimeToLive {
} get;
}
public DnsDomain Name { get; }
public Int32 DataLength => this.Data.Length;
public DnsRecordType Type { get; }
public Byte[] Data {
public DnsRecordClass Class { get; } get;
}
public TimeSpan TimeToLive { get; }
public Int32 Size => this.Name.Size + Tail.SIZE + this.Data.Length;
public int DataLength => Data.Length;
public static DnsResourceRecord FromArray(Byte[] message, Int32 offset, out Int32 endOffset) {
public byte[] Data { get; } DnsDomain domain = DnsDomain.FromArray(message, offset, out offset);
Tail tail = message.ToStruct<Tail>(offset, Tail.SIZE);
public int Size => Name.Size + Tail.SIZE + Data.Length;
Byte[] data = new Byte[tail.DataLength];
public static DnsResourceRecord FromArray(byte[] message, int offset, out int endOffset)
{ offset += Tail.SIZE;
var domain = DnsDomain.FromArray(message, offset, out offset); Array.Copy(message, offset, data, 0, data.Length);
var tail = message.ToStruct<Tail>(offset, Tail.SIZE);
endOffset = offset + data.Length;
var data = new byte[tail.DataLength];
return new DnsResourceRecord(domain, data, tail.Type, tail.Class, tail.TimeToLive);
offset += Tail.SIZE; }
Array.Copy(message, offset, data, 0, data.Length);
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();
endOffset = offset + data.Length;
public override String ToString() => Json.SerializeOnly(this, true, nameof(this.Name), nameof(this.Type), nameof(this.Class), nameof(this.TimeToLive), nameof(this.DataLength));
return new DnsResourceRecord(domain, data, tail.Type, tail.Class, tail.TimeToLive);
} [StructEndianness(Endianness.Big)]
[StructLayout(LayoutKind.Sequential, Pack = 2)]
public byte[] ToArray() => private struct Tail {
new MemoryStream(Size) public const Int32 SIZE = 10;
.Append(Name.ToArray())
.Append(new Tail() private UInt16 type;
{ private UInt16 klass;
Type = Type, private UInt32 ttl;
Class = Class, private UInt16 dataLength;
TimeToLive = TimeToLive,
DataLength = Data.Length, public DnsRecordType Type {
}.ToBytes()) get => (DnsRecordType)this.type;
.Append(Data) set => this.type = (UInt16)value;
.ToArray(); }
public override string ToString() public DnsRecordClass Class {
{ get => (DnsRecordClass)this.klass;
return Json.SerializeOnly( set => this.klass = (UInt16)value;
this, }
true,
nameof(Name), public TimeSpan TimeToLive {
nameof(Type), get => TimeSpan.FromSeconds(this.ttl);
nameof(Class), set => this.ttl = (UInt32)value.TotalSeconds;
nameof(TimeToLive), }
nameof(DataLength));
} public Int32 DataLength {
get => this.dataLength;
[StructEndianness(Endianness.Big)] set => this.dataLength = (UInt16)value;
[StructLayout(LayoutKind.Sequential, Pack = 2)] }
private struct Tail }
{ }
public const int SIZE = 10;
public class DnsPointerResourceRecord : DnsResourceRecordBase {
private ushort type; public DnsPointerResourceRecord(IDnsResourceRecord record, Byte[] message, Int32 dataOffset) : base(record) => this.PointerDomainName = DnsDomain.FromArray(message, dataOffset);
private ushort klass;
private uint ttl; public DnsDomain PointerDomainName {
private ushort dataLength; get;
}
public DnsRecordType Type
{ protected override String[] IncludedProperties {
get => (DnsRecordType) type; get {
set => type = (ushort) value; List<String> temp = new List<String>(base.IncludedProperties) { nameof(this.PointerDomainName) };
} return temp.ToArray();
}
public DnsRecordClass Class }
{ }
get => (DnsRecordClass) klass;
set => klass = (ushort) value; public class DnsIPAddressResourceRecord : DnsResourceRecordBase {
} public DnsIPAddressResourceRecord(IDnsResourceRecord record) : base(record) => this.IPAddress = new IPAddress(this.Data);
public TimeSpan TimeToLive public IPAddress IPAddress {
{ get;
get => TimeSpan.FromSeconds(ttl); }
set => ttl = (uint) value.TotalSeconds;
} protected override String[] IncludedProperties => new List<String>(base.IncludedProperties) { nameof(this.IPAddress) }.ToArray();
}
public int DataLength
{ public class DnsNameServerResourceRecord : DnsResourceRecordBase {
get => dataLength; public DnsNameServerResourceRecord(IDnsResourceRecord record, Byte[] message, Int32 dataOffset) : base(record) => this.NSDomainName = DnsDomain.FromArray(message, dataOffset);
set => dataLength = (ushort) value;
} public DnsDomain NSDomainName {
} get;
} }
public class DnsPointerResourceRecord : DnsResourceRecordBase protected override String[] IncludedProperties => new List<String>(base.IncludedProperties) { nameof(this.NSDomainName) }.ToArray();
{ }
public DnsPointerResourceRecord(IDnsResourceRecord record, byte[] message, int dataOffset)
: base(record) public class DnsCanonicalNameResourceRecord : DnsResourceRecordBase {
{ public DnsCanonicalNameResourceRecord(IDnsResourceRecord record, Byte[] message, Int32 dataOffset) : base(record) => this.CanonicalDomainName = DnsDomain.FromArray(message, dataOffset);
PointerDomainName = DnsDomain.FromArray(message, dataOffset);
} public DnsDomain CanonicalDomainName {
get;
public DnsDomain PointerDomainName { get; } }
protected override string[] IncludedProperties protected override String[] IncludedProperties => new List<String>(base.IncludedProperties) { nameof(this.CanonicalDomainName) }.ToArray();
{ }
get
{ public class DnsMailExchangeResourceRecord : DnsResourceRecordBase {
var temp = new List<string>(base.IncludedProperties) {nameof(PointerDomainName)}; private const Int32 PreferenceSize = 2;
return temp.ToArray();
} public DnsMailExchangeResourceRecord(IDnsResourceRecord record, Byte[] message, Int32 dataOffset)
} : base(record) {
} Byte[] preference = new Byte[PreferenceSize];
Array.Copy(message, dataOffset, preference, 0, preference.Length);
public class DnsIPAddressResourceRecord : DnsResourceRecordBase
{ if(BitConverter.IsLittleEndian) {
public DnsIPAddressResourceRecord(IDnsResourceRecord record) Array.Reverse(preference);
: base(record) }
{
IPAddress = new IPAddress(Data); dataOffset += PreferenceSize;
}
this.Preference = BitConverter.ToUInt16(preference, 0);
public IPAddress IPAddress { get; } this.ExchangeDomainName = DnsDomain.FromArray(message, dataOffset);
}
protected override string[] IncludedProperties
=> new List<string>(base.IncludedProperties) {nameof(IPAddress)}.ToArray(); public Int32 Preference {
} get;
}
public class DnsNameServerResourceRecord : DnsResourceRecordBase
{ public DnsDomain ExchangeDomainName {
public DnsNameServerResourceRecord(IDnsResourceRecord record, byte[] message, int dataOffset) get;
: base(record) }
{
NSDomainName = DnsDomain.FromArray(message, dataOffset); protected override String[] IncludedProperties => new List<String>(base.IncludedProperties)
} {
nameof(this.Preference),
public DnsDomain NSDomainName { get; } nameof(this.ExchangeDomainName),
}.ToArray();
protected override string[] IncludedProperties }
=> new List<string>(base.IncludedProperties) {nameof(NSDomainName)}.ToArray();
} public class DnsStartOfAuthorityResourceRecord : DnsResourceRecordBase {
public DnsStartOfAuthorityResourceRecord(IDnsResourceRecord record, Byte[] message, Int32 dataOffset) : base(record) {
public class DnsCanonicalNameResourceRecord : DnsResourceRecordBase this.MasterDomainName = DnsDomain.FromArray(message, dataOffset, out dataOffset);
{ this.ResponsibleDomainName = DnsDomain.FromArray(message, dataOffset, out dataOffset);
public DnsCanonicalNameResourceRecord(IDnsResourceRecord record, byte[] message, int dataOffset)
: base(record) Options tail = message.ToStruct<Options>(dataOffset, Options.SIZE);
{
CanonicalDomainName = DnsDomain.FromArray(message, dataOffset); this.SerialNumber = tail.SerialNumber;
} this.RefreshInterval = tail.RefreshInterval;
this.RetryInterval = tail.RetryInterval;
public DnsDomain CanonicalDomainName { get; } this.ExpireInterval = tail.ExpireInterval;
this.MinimumTimeToLive = tail.MinimumTimeToLive;
protected override string[] IncludedProperties }
=> new List<string>(base.IncludedProperties) {nameof(CanonicalDomainName)}.ToArray();
} 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)) {
public class DnsMailExchangeResourceRecord : DnsResourceRecordBase this.MasterDomainName = master;
{ this.ResponsibleDomainName = responsible;
private const int PreferenceSize = 2;
this.SerialNumber = serial;
public DnsMailExchangeResourceRecord( this.RefreshInterval = refresh;
IDnsResourceRecord record, this.RetryInterval = retry;
byte[] message, this.ExpireInterval = expire;
int dataOffset) this.MinimumTimeToLive = minTtl;
: base(record) }
{
var preference = new byte[PreferenceSize]; public DnsDomain MasterDomainName {
Array.Copy(message, dataOffset, preference, 0, preference.Length); get;
}
if (BitConverter.IsLittleEndian)
{ public DnsDomain ResponsibleDomainName {
Array.Reverse(preference); get;
} }
dataOffset += PreferenceSize; public Int64 SerialNumber {
get;
Preference = BitConverter.ToUInt16(preference, 0); }
ExchangeDomainName = DnsDomain.FromArray(message, dataOffset);
} public TimeSpan RefreshInterval {
get;
public int Preference { get; } }
public DnsDomain ExchangeDomainName { get; } public TimeSpan RetryInterval {
get;
protected override string[] IncludedProperties => new List<string>(base.IncludedProperties) }
{
nameof(Preference), public TimeSpan ExpireInterval {
nameof(ExchangeDomainName), get;
}.ToArray(); }
}
public TimeSpan MinimumTimeToLive {
public class DnsStartOfAuthorityResourceRecord : DnsResourceRecordBase get;
{ }
public DnsStartOfAuthorityResourceRecord(IDnsResourceRecord record, byte[] message, int dataOffset)
: base(record) protected override String[] IncludedProperties => new List<String>(base.IncludedProperties)
{ {
MasterDomainName = DnsDomain.FromArray(message, dataOffset, out dataOffset); nameof(this.MasterDomainName),
ResponsibleDomainName = DnsDomain.FromArray(message, dataOffset, out dataOffset); nameof(this.ResponsibleDomainName),
nameof(this.SerialNumber),
var tail = message.ToStruct<Options>(dataOffset, Options.SIZE); }.ToArray();
SerialNumber = tail.SerialNumber; private static IDnsResourceRecord Create(DnsDomain domain, DnsDomain master, DnsDomain responsible, Int64 serial, TimeSpan refresh, TimeSpan retry, TimeSpan expire, TimeSpan minTtl, TimeSpan ttl) {
RefreshInterval = tail.RefreshInterval; MemoryStream data = new MemoryStream(Options.SIZE + master.Size + responsible.Size);
RetryInterval = tail.RetryInterval; Options tail = new Options {
ExpireInterval = tail.ExpireInterval; SerialNumber = serial,
MinimumTimeToLive = tail.MinimumTimeToLive; RefreshInterval = refresh,
} RetryInterval = retry,
ExpireInterval = expire,
public DnsStartOfAuthorityResourceRecord( MinimumTimeToLive = minTtl,
DnsDomain domain, };
DnsDomain master,
DnsDomain responsible, _ = data.Append(master.ToArray()).Append(responsible.ToArray()).Append(tail.ToBytes());
long serial,
TimeSpan refresh, return new DnsResourceRecord(domain, data.ToArray(), DnsRecordType.SOA, DnsRecordClass.IN, ttl);
TimeSpan retry, }
TimeSpan expire,
TimeSpan minTtl, [StructEndianness(Endianness.Big)]
TimeSpan ttl = default) [StructLayout(LayoutKind.Sequential, Pack = 4)]
: base(Create(domain, master, responsible, serial, refresh, retry, expire, minTtl, ttl)) public struct Options {
{ public const Int32 SIZE = 20;
MasterDomainName = master;
ResponsibleDomainName = responsible; private UInt32 serialNumber;
private UInt32 refreshInterval;
SerialNumber = serial; private UInt32 retryInterval;
RefreshInterval = refresh; private UInt32 expireInterval;
RetryInterval = retry; private UInt32 ttl;
ExpireInterval = expire;
MinimumTimeToLive = minTtl; public Int64 SerialNumber {
} get => this.serialNumber;
set => this.serialNumber = (UInt32)value;
public DnsDomain MasterDomainName { get; } }
public DnsDomain ResponsibleDomainName { get; } public TimeSpan RefreshInterval {
get => TimeSpan.FromSeconds(this.refreshInterval);
public long SerialNumber { get; } set => this.refreshInterval = (UInt32)value.TotalSeconds;
}
public TimeSpan RefreshInterval { get; }
public TimeSpan RetryInterval {
public TimeSpan RetryInterval { get; } get => TimeSpan.FromSeconds(this.retryInterval);
set => this.retryInterval = (UInt32)value.TotalSeconds;
public TimeSpan ExpireInterval { get; } }
public TimeSpan MinimumTimeToLive { get; } public TimeSpan ExpireInterval {
get => TimeSpan.FromSeconds(this.expireInterval);
protected override string[] IncludedProperties => new List<string>(base.IncludedProperties) set => this.expireInterval = (UInt32)value.TotalSeconds;
{ }
nameof(MasterDomainName),
nameof(ResponsibleDomainName), public TimeSpan MinimumTimeToLive {
nameof(SerialNumber), get => TimeSpan.FromSeconds(this.ttl);
}.ToArray(); set => this.ttl = (UInt32)value.TotalSeconds;
}
private static IDnsResourceRecord Create( }
DnsDomain domain, }
DnsDomain master,
DnsDomain responsible, private static class DnsResourceRecordFactory {
long serial, public static IList<IDnsResourceRecord> GetAllFromArray(Byte[] message, Int32 offset, Int32 count, out Int32 endOffset) {
TimeSpan refresh, List<IDnsResourceRecord> result = new List<IDnsResourceRecord>(count);
TimeSpan retry,
TimeSpan expire, for(Int32 i = 0; i < count; i++) {
TimeSpan minTtl, result.Add(GetFromArray(message, offset, out offset));
TimeSpan ttl) }
{
var data = new MemoryStream(Options.SIZE + master.Size + responsible.Size); endOffset = offset;
var tail = new Options return result;
{ }
SerialNumber = serial,
RefreshInterval = refresh, private static IDnsResourceRecord GetFromArray(Byte[] message, Int32 offset, out Int32 endOffset) {
RetryInterval = retry, DnsResourceRecord record = DnsResourceRecord.FromArray(message, offset, out endOffset);
ExpireInterval = expire, Int32 dataOffset = endOffset - record.DataLength;
MinimumTimeToLive = minTtl,
}; return record.Type switch
{
data.Append(master.ToArray()).Append(responsible.ToArray()).Append(tail.ToBytes()); DnsRecordType.A => (new DnsIPAddressResourceRecord(record)),
DnsRecordType.AAAA => new DnsIPAddressResourceRecord(record),
return new DnsResourceRecord(domain, data.ToArray(), DnsRecordType.SOA, DnsRecordClass.IN, ttl); DnsRecordType.NS => new DnsNameServerResourceRecord(record, message, dataOffset),
} DnsRecordType.CNAME => new DnsCanonicalNameResourceRecord(record, message, dataOffset),
DnsRecordType.SOA => new DnsStartOfAuthorityResourceRecord(record, message, dataOffset),
[StructEndianness(Endianness.Big)] DnsRecordType.PTR => new DnsPointerResourceRecord(record, message, dataOffset),
[StructLayout(LayoutKind.Sequential, Pack = 4)] DnsRecordType.MX => new DnsMailExchangeResourceRecord(record, message, dataOffset),
public struct Options _ => record
{ };
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
};
}
}
}
} }

View File

@ -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; namespace Swan.Net.Dns {
/// <summary>
/// <summary> /// DnsClient Response inner class.
/// DnsClient Response inner class. /// </summary>
/// </summary> internal partial class DnsClient {
internal partial class DnsClient public class DnsClientResponse : IDnsResponse {
{ private readonly DnsResponse _response;
public class DnsClientResponse : IDnsResponse private readonly Byte[] _message;
{
private readonly DnsResponse _response; internal DnsClientResponse(DnsClientRequest request, DnsResponse response, Byte[] message) {
private readonly byte[] _message; this.Request = request;
internal DnsClientResponse(DnsClientRequest request, DnsResponse response, byte[] message) this._message = message;
{ this._response = response;
Request = request; }
_message = message; public DnsClientRequest Request {
_response = response; get;
} }
public DnsClientRequest Request { get; } public Int32 Id {
get => this._response.Id;
public int Id set {
{ }
get { return _response.Id; } }
set { }
} public IList<IDnsResourceRecord> AnswerRecords => this._response.AnswerRecords;
public IList<IDnsResourceRecord> AnswerRecords => _response.AnswerRecords; public IList<IDnsResourceRecord> AuthorityRecords => new ReadOnlyCollection<IDnsResourceRecord>(this._response.AuthorityRecords);
public IList<IDnsResourceRecord> AuthorityRecords => public IList<IDnsResourceRecord> AdditionalRecords => new ReadOnlyCollection<IDnsResourceRecord>(this._response.AdditionalRecords);
new ReadOnlyCollection<IDnsResourceRecord>(_response.AuthorityRecords);
public Boolean IsRecursionAvailable {
public IList<IDnsResourceRecord> AdditionalRecords => get => this._response.IsRecursionAvailable;
new ReadOnlyCollection<IDnsResourceRecord>(_response.AdditionalRecords); set {
}
public bool IsRecursionAvailable }
{
get { return _response.IsRecursionAvailable; } public Boolean IsAuthorativeServer {
set { } get => this._response.IsAuthorativeServer;
} set {
}
public bool IsAuthorativeServer }
{
get { return _response.IsAuthorativeServer; } public Boolean IsTruncated {
set { } get => this._response.IsTruncated;
} set {
}
public bool IsTruncated }
{
get { return _response.IsTruncated; } public DnsOperationCode OperationCode {
set { } get => this._response.OperationCode;
} set {
}
public DnsOperationCode OperationCode }
{
get { return _response.OperationCode; } public DnsResponseCode ResponseCode {
set { } get => this._response.ResponseCode;
} set {
}
public DnsResponseCode ResponseCode }
{
get { return _response.ResponseCode; } public IList<DnsQuestion> Questions => new ReadOnlyCollection<DnsQuestion>(this._response.Questions);
set { }
} public Int32 Size => this._message.Length;
public IList<DnsQuestion> Questions => new ReadOnlyCollection<DnsQuestion>(_response.Questions); public Byte[] ToArray() => this._message;
public int Size => _message.Length; public override String ToString() => this._response.ToString();
}
public byte[] ToArray() => _message;
public class DnsResponse : IDnsResponse {
public override string ToString() => _response.ToString(); private DnsHeader _header;
}
public DnsResponse(DnsHeader header, IList<DnsQuestion> questions, IList<IDnsResourceRecord> answers, IList<IDnsResourceRecord> authority, IList<IDnsResourceRecord> additional) {
public class DnsResponse : IDnsResponse this._header = header;
{ this.Questions = questions;
private DnsHeader _header; this.AnswerRecords = answers;
this.AuthorityRecords = authority;
public DnsResponse( this.AdditionalRecords = additional;
DnsHeader header, }
IList<DnsQuestion> questions,
IList<IDnsResourceRecord> answers, public IList<DnsQuestion> Questions {
IList<IDnsResourceRecord> authority, get;
IList<IDnsResourceRecord> additional) }
{
_header = header; public IList<IDnsResourceRecord> AnswerRecords {
Questions = questions; get;
AnswerRecords = answers; }
AuthorityRecords = authority;
AdditionalRecords = additional; public IList<IDnsResourceRecord> AuthorityRecords {
} get;
}
public IList<DnsQuestion> Questions { get; }
public IList<IDnsResourceRecord> AdditionalRecords {
public IList<IDnsResourceRecord> AnswerRecords { get; } get;
}
public IList<IDnsResourceRecord> AuthorityRecords { get; }
public Int32 Id {
public IList<IDnsResourceRecord> AdditionalRecords { get; } get => this._header.Id;
set => this._header.Id = value;
public int Id }
{
get => _header.Id; public Boolean IsRecursionAvailable {
set => _header.Id = value; get => this._header.RecursionAvailable;
} set => this._header.RecursionAvailable = value;
}
public bool IsRecursionAvailable
{ public Boolean IsAuthorativeServer {
get => _header.RecursionAvailable; get => this._header.AuthorativeServer;
set => _header.RecursionAvailable = value; set => this._header.AuthorativeServer = value;
} }
public bool IsAuthorativeServer public Boolean IsTruncated {
{ get => this._header.Truncated;
get => _header.AuthorativeServer; set => this._header.Truncated = value;
set => _header.AuthorativeServer = value; }
}
public DnsOperationCode OperationCode {
public bool IsTruncated get => this._header.OperationCode;
{ set => this._header.OperationCode = value;
get => _header.Truncated; }
set => _header.Truncated = value;
} public DnsResponseCode ResponseCode {
get => this._header.ResponseCode;
public DnsOperationCode OperationCode set => this._header.ResponseCode = value;
{ }
get => _header.OperationCode;
set => _header.OperationCode = 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) {
public DnsResponseCode ResponseCode DnsHeader header = DnsHeader.FromArray(message);
{ Int32 offset = header.Size;
get => _header.ResponseCode;
set => _header.ResponseCode = value; if(!header.Response || header.QuestionCount == 0) {
} throw new ArgumentException("Invalid response message");
}
public int Size
=> _header.Size + return header.Truncated
Questions.Sum(q => q.Size) + ? new DnsResponse(header, DnsQuestion.GetAllFromArray(message, offset, header.QuestionCount), new List<IDnsResourceRecord>(), new List<IDnsResourceRecord>(), new List<IDnsResourceRecord>())
AnswerRecords.Sum(a => a.Size) + : 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 _));
AuthorityRecords.Sum(a => a.Size) + }
AdditionalRecords.Sum(a => a.Size);
public Byte[] ToArray() {
public static DnsResponse FromArray(byte[] message) this.UpdateHeader();
{ MemoryStream result = new MemoryStream(this.Size);
var header = DnsHeader.FromArray(message);
var offset = header.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()));
if (!header.Response || header.QuestionCount == 0) return result.ToArray();
{ }
throw new ArgumentException("Invalid response message");
} public override String ToString() {
this.UpdateHeader();
if (header.Truncated)
{ return Json.SerializeOnly(this, true, nameof(this.Questions), nameof(this.AnswerRecords), nameof(this.AuthorityRecords), nameof(this.AdditionalRecords));
return new DnsResponse(header, }
DnsQuestion.GetAllFromArray(message, offset, header.QuestionCount),
new List<IDnsResourceRecord>(), private void UpdateHeader() {
new List<IDnsResourceRecord>(), this._header.QuestionCount = this.Questions.Count;
new List<IDnsResourceRecord>()); this._header.AnswerRecordCount = this.AnswerRecords.Count;
} this._header.AuthorityRecordCount = this.AuthorityRecords.Count;
this._header.AdditionalRecordCount = this.AdditionalRecords.Count;
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;
}
}
}
} }

View File

@ -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;
namespace Swan.Net.Dns {
/// <summary> /// <summary>
/// DnsClient public methods. /// DnsClient public methods.
/// </summary> /// </summary>
internal partial class DnsClient 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;
{ this._resolver = resolver ?? new DnsUdpRequestResolver(new DnsTcpRequestResolver());
_dns = dns; }
_resolver = resolver ?? new DnsUdpRequestResolver(new DnsTcpRequestResolver());
} public DnsClient(IPAddress ip, Int32 port = Network.DnsDefaultPort, IDnsRequestResolver? resolver = null) : this(new IPEndPoint(ip, port), resolver) {
}
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(this._dns, request, this._resolver);
{
} public async Task<IList<IPAddress>> Lookup(String domain, DnsRecordType type = DnsRecordType.A) {
if(String.IsNullOrWhiteSpace(domain)) {
public DnsClientRequest Create(IDnsRequest? request = null) throw new ArgumentNullException(nameof(domain));
=> new DnsClientRequest(_dns, request, _resolver); }
public async Task<IList<IPAddress>> Lookup(string domain, DnsRecordType type = DnsRecordType.A) if(type != DnsRecordType.A && type != DnsRecordType.AAAA) {
{ throw new ArgumentException("Invalid record type " + type);
if (string.IsNullOrWhiteSpace(domain)) }
throw new ArgumentNullException(nameof(domain));
DnsClientResponse response = await this.Resolve(domain, type).ConfigureAwait(false);
if (type != DnsRecordType.A && type != DnsRecordType.AAAA) List<IPAddress> ips = response.AnswerRecords.Where(r => r.Type == type).Cast<DnsIPAddressResourceRecord>().Select(r => r.IPAddress).ToList();
{
throw new ArgumentException("Invalid record type " + type); return ips.Count == 0 ? throw new DnsQueryException(response, "No matching records") : ips;
} }
var response = await Resolve(domain, type).ConfigureAwait(false); public async Task<String> Reverse(IPAddress ip) {
var ips = response.AnswerRecords if(ip == null) {
.Where(r => r.Type == type) throw new ArgumentNullException(nameof(ip));
.Cast<DnsIPAddressResourceRecord>() }
.Select(r => r.IPAddress)
.ToList(); DnsClientResponse response = await this.Resolve(DnsDomain.PointerName(ip), DnsRecordType.PTR);
IDnsResourceRecord ptr = response.AnswerRecords.FirstOrDefault(r => r.Type == DnsRecordType.PTR);
return ips.Count == 0 ? throw new DnsQueryException(response, "No matching records") : ips;
} return ptr == null ? throw new DnsQueryException(response, "No matching records") : ((DnsPointerResourceRecord)ptr).PointerDomainName.ToString();
}
public async Task<string> Reverse(IPAddress ip)
{ public Task<DnsClientResponse> Resolve(String domain, DnsRecordType type) => this.Resolve(new DnsDomain(domain), type);
if (ip == null)
throw new ArgumentNullException(nameof(ip)); public Task<DnsClientResponse> Resolve(DnsDomain domain, DnsRecordType type) {
DnsClientRequest request = this.Create();
var response = await Resolve(DnsDomain.PointerName(ip), DnsRecordType.PTR); DnsQuestion question = new DnsQuestion(domain, type);
var ptr = response.AnswerRecords.FirstOrDefault(r => r.Type == DnsRecordType.PTR);
request.Questions.Add(question);
return ptr == null request.OperationCode = DnsOperationCode.Query;
? throw new DnsQueryException(response, "No matching records") request.RecursionDesired = true;
: ((DnsPointerResourceRecord) ptr).PointerDomainName.ToString();
} return request.Resolve();
}
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();
}
}
} }

View File

@ -1,37 +1,28 @@
namespace Swan.Net.Dns #nullable enable
{ using System;
using System;
namespace Swan.Net.Dns {
/// <summary> /// <summary>
/// An exception thrown when the DNS query fails. /// An exception thrown when the DNS query fails.
/// </summary> /// </summary>
/// <seealso cref="Exception" /> /// <seealso cref="Exception" />
[Serializable] [Serializable]
public class DnsQueryException : Exception public class DnsQueryException : Exception {
{ internal DnsQueryException(String message) : base(message) {
internal DnsQueryException(string message) }
: base(message)
{ internal DnsQueryException(String message, Exception e) : base(message, e) {
} }
internal DnsQueryException(string message, Exception e) internal DnsQueryException(DnsClient.IDnsResponse response) : this(response, Format(response)) {
: base(message, e) }
{
} internal DnsQueryException(DnsClient.IDnsResponse response, String message) : base(message) => this.Response = response;
internal DnsQueryException(DnsClient.IDnsResponse response) internal DnsClient.IDnsResponse? Response {
: this(response, Format(response)) get;
{ }
}
private static String Format(DnsClient.IDnsResponse response) => $"Invalid response received with code {response.ResponseCode}";
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}";
}
} }

View File

@ -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;
/// <summary> this.OperationCode = response.OperationCode;
/// Initializes a new instance of the <see cref="DnsQueryResult"/> class. this.ResponseCode = response.ResponseCode;
/// </summary>
/// <param name="response">The response.</param> if(response.AnswerRecords != null) {
internal DnsQueryResult(DnsClient.IDnsResponse response) foreach(DnsClient.IDnsResourceRecord record in response.AnswerRecords) {
: this() this.AnswerRecords.Add(new DnsRecord(record));
{ }
Id = response.Id; }
IsAuthoritativeServer = response.IsAuthorativeServer;
IsRecursionAvailable = response.IsRecursionAvailable; if(response.AuthorityRecords != null) {
IsTruncated = response.IsTruncated; foreach(DnsClient.IDnsResourceRecord record in response.AuthorityRecords) {
OperationCode = response.OperationCode; this.AuthorityRecords.Add(new DnsRecord(record));
ResponseCode = response.ResponseCode; }
}
if (response.AnswerRecords != null)
{ if(response.AdditionalRecords != null) {
foreach (var record in response.AnswerRecords) foreach(DnsClient.IDnsResourceRecord record in response.AdditionalRecords) {
AnswerRecords.Add(new DnsRecord(record)); this.AdditionalRecords.Add(new DnsRecord(record));
} }
}
if (response.AuthorityRecords != null) }
{
foreach (var record in response.AuthorityRecords) private DnsQueryResult() {
AuthorityRecords.Add(new DnsRecord(record)); }
}
/// <summary>
if (response.AdditionalRecords != null) /// Gets the identifier.
{ /// </summary>
foreach (var record in response.AdditionalRecords) /// <value>
AdditionalRecords.Add(new DnsRecord(record)); /// The identifier.
} /// </value>
} public System.Int32 Id {
get;
private DnsQueryResult() }
{
} /// <summary>
/// Gets a value indicating whether this instance is authoritative server.
/// <summary> /// </summary>
/// Gets the identifier. /// <value>
/// </summary> /// <c>true</c> if this instance is authoritative server; otherwise, <c>false</c>.
/// <value> /// </value>
/// The identifier. public System.Boolean IsAuthoritativeServer {
/// </value> get;
public int Id { get; } }
/// <summary> /// <summary>
/// Gets a value indicating whether this instance is authoritative server. /// Gets a value indicating whether this instance is truncated.
/// </summary> /// </summary>
/// <value> /// <value>
/// <c>true</c> if this instance is authoritative server; otherwise, <c>false</c>. /// <c>true</c> if this instance is truncated; otherwise, <c>false</c>.
/// </value> /// </value>
public bool IsAuthoritativeServer { get; } public System.Boolean IsTruncated {
get;
/// <summary> }
/// Gets a value indicating whether this instance is truncated.
/// </summary> /// <summary>
/// <value> /// Gets a value indicating whether this instance is recursion available.
/// <c>true</c> if this instance is truncated; otherwise, <c>false</c>. /// </summary>
/// </value> /// <value>
public bool IsTruncated { get; } /// <c>true</c> if this instance is recursion available; otherwise, <c>false</c>.
/// </value>
/// <summary> public System.Boolean IsRecursionAvailable {
/// Gets a value indicating whether this instance is recursion available. get;
/// </summary> }
/// <value>
/// <c>true</c> if this instance is recursion available; otherwise, <c>false</c>. /// <summary>
/// </value> /// Gets the operation code.
public bool IsRecursionAvailable { get; } /// </summary>
/// <value>
/// <summary> /// The operation code.
/// Gets the operation code. /// </value>
/// </summary> public DnsOperationCode OperationCode {
/// <value> get;
/// The operation code. }
/// </value>
public DnsOperationCode OperationCode { get; } /// <summary>
/// Gets the response code.
/// <summary> /// </summary>
/// Gets the response code. /// <value>
/// </summary> /// The response code.
/// <value> /// </value>
/// The response code. public DnsResponseCode ResponseCode {
/// </value> get;
public DnsResponseCode ResponseCode { get; } }
/// <summary> /// <summary>
/// Gets the answer records. /// Gets the answer records.
/// </summary> /// </summary>
/// <value> /// <value>
/// The answer records. /// The answer records.
/// </value> /// </value>
public IList<DnsRecord> AnswerRecords => _mAnswerRecords; public IList<DnsRecord> AnswerRecords => this._mAnswerRecords;
/// <summary> /// <summary>
/// Gets the additional records. /// Gets the additional records.
/// </summary> /// </summary>
/// <value> /// <value>
/// The additional records. /// The additional records.
/// </value> /// </value>
public IList<DnsRecord> AdditionalRecords => _mAdditionalRecords; public IList<DnsRecord> AdditionalRecords => this._mAdditionalRecords;
/// <summary> /// <summary>
/// Gets the authority records. /// Gets the authority records.
/// </summary> /// </summary>
/// <value> /// <value>
/// The authority records. /// The authority records.
/// </value> /// </value>
public IList<DnsRecord> AuthorityRecords => _mAuthorityRecords; public IList<DnsRecord> AuthorityRecords => this._mAuthorityRecords;
} }
} }

View File

@ -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> /// <summary>
/// Represents a DNS record entry. /// Initializes a new instance of the <see cref="DnsRecord"/> class.
/// </summary> /// </summary>
public class DnsRecord /// <param name="record">The record.</param>
{ internal DnsRecord(DnsClient.IDnsResourceRecord record) : this() {
/// <summary> this.Name = record.Name.ToString();
/// Initializes a new instance of the <see cref="DnsRecord"/> class. this.Type = record.Type;
/// </summary> this.Class = record.Class;
/// <param name="record">The record.</param> this.TimeToLive = record.TimeToLive;
internal DnsRecord(DnsClient.IDnsResourceRecord record) this.Data = record.Data;
: this()
{ // PTR
Name = record.Name.ToString(); this.PointerDomainName = (record as DnsClient.DnsPointerResourceRecord)?.PointerDomainName?.ToString();
Type = record.Type;
Class = record.Class; // A
TimeToLive = record.TimeToLive; this.IPAddress = (record as DnsClient.DnsIPAddressResourceRecord)?.IPAddress;
Data = record.Data;
// NS
// PTR this.NameServerDomainName = (record as DnsClient.DnsNameServerResourceRecord)?.NSDomainName?.ToString();
PointerDomainName = (record as DnsClient.DnsPointerResourceRecord)?.PointerDomainName?.ToString();
// CNAME
// A this.CanonicalDomainName = (record as DnsClient.DnsCanonicalNameResourceRecord)?.CanonicalDomainName.ToString();
IPAddress = (record as DnsClient.DnsIPAddressResourceRecord)?.IPAddress;
// MX
// NS this.MailExchangerDomainName = (record as DnsClient.DnsMailExchangeResourceRecord)?.ExchangeDomainName.ToString();
NameServerDomainName = (record as DnsClient.DnsNameServerResourceRecord)?.NSDomainName?.ToString(); this.MailExchangerPreference = (record as DnsClient.DnsMailExchangeResourceRecord)?.Preference;
// CNAME // SOA
CanonicalDomainName = (record as DnsClient.DnsCanonicalNameResourceRecord)?.CanonicalDomainName.ToString(); this.SoaMasterDomainName = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.MasterDomainName.ToString();
this.SoaResponsibleDomainName = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.ResponsibleDomainName.ToString();
// MX this.SoaSerialNumber = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.SerialNumber;
MailExchangerDomainName = (record as DnsClient.DnsMailExchangeResourceRecord)?.ExchangeDomainName.ToString(); this.SoaRefreshInterval = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.RefreshInterval;
MailExchangerPreference = (record as DnsClient.DnsMailExchangeResourceRecord)?.Preference; this.SoaRetryInterval = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.RetryInterval;
this.SoaExpireInterval = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.ExpireInterval;
// SOA this.SoaMinimumTimeToLive = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.MinimumTimeToLive;
SoaMasterDomainName = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.MasterDomainName.ToString(); }
SoaResponsibleDomainName = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.ResponsibleDomainName.ToString();
SoaSerialNumber = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.SerialNumber; private DnsRecord() {
SoaRefreshInterval = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.RefreshInterval; // placeholder
SoaRetryInterval = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.RetryInterval; }
SoaExpireInterval = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.ExpireInterval;
SoaMinimumTimeToLive = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.MinimumTimeToLive; /// <summary>
} /// Gets the name.
/// </summary>
private DnsRecord() /// <value>
{ /// The name.
// placeholder /// </value>
} public String Name {
get;
/// <summary> }
/// Gets the name.
/// </summary> /// <summary>
/// <value> /// Gets the type.
/// The name. /// </summary>
/// </value> /// <value>
public string Name { get; } /// The type.
/// </value>
/// <summary> public DnsRecordType Type {
/// Gets the type. get;
/// </summary> }
/// <value>
/// The type. /// <summary>
/// </value> /// Gets the class.
public DnsRecordType Type { get; } /// </summary>
/// <value>
/// <summary> /// The class.
/// Gets the class. /// </value>
/// </summary> public DnsRecordClass Class {
/// <value> get;
/// The class. }
/// </value>
public DnsRecordClass Class { get; } /// <summary>
/// Gets the time to live.
/// <summary> /// </summary>
/// Gets the time to live. /// <value>
/// </summary> /// The time to live.
/// <value> /// </value>
/// The time to live. public TimeSpan TimeToLive {
/// </value> get;
public TimeSpan TimeToLive { get; } }
/// <summary> /// <summary>
/// Gets the raw data of the record. /// Gets the raw data of the record.
/// </summary> /// </summary>
/// <value> /// <value>
/// The data. /// The data.
/// </value> /// </value>
public byte[] Data { get; } public Byte[] Data {
get;
/// <summary> }
/// Gets the data text bytes in ASCII encoding.
/// </summary> /// <summary>
/// <value> /// Gets the data text bytes in ASCII encoding.
/// The data text. /// </summary>
/// </value> /// <value>
public string DataText => Data == null ? string.Empty : Encoding.ASCII.GetString(Data); /// The data text.
/// </value>
/// <summary> public String DataText => this.Data == null ? String.Empty : Encoding.ASCII.GetString(this.Data);
/// Gets the name of the pointer domain.
/// </summary> /// <summary>
/// <value> /// Gets the name of the pointer domain.
/// The name of the pointer domain. /// </summary>
/// </value> /// <value>
public string PointerDomainName { get; } /// The name of the pointer domain.
/// </value>
/// <summary> public String PointerDomainName {
/// Gets the ip address. get;
/// </summary> }
/// <value>
/// The ip address. /// <summary>
/// </value> /// Gets the ip address.
public IPAddress IPAddress { get; } /// </summary>
/// <value>
/// <summary> /// The ip address.
/// Gets the name of the name server domain. /// </value>
/// </summary> public IPAddress IPAddress {
/// <value> get;
/// The name of the name server domain. }
/// </value>
public string NameServerDomainName { get; } /// <summary>
/// Gets the name of the name server domain.
/// <summary> /// </summary>
/// Gets the name of the canonical domain. /// <value>
/// </summary> /// The name of the name server domain.
/// <value> /// </value>
/// The name of the canonical domain. public String NameServerDomainName {
/// </value> get;
public string CanonicalDomainName { get; } }
/// <summary> /// <summary>
/// Gets the mail exchanger preference. /// Gets the name of the canonical domain.
/// </summary> /// </summary>
/// <value> /// <value>
/// The mail exchanger preference. /// The name of the canonical domain.
/// </value> /// </value>
public int? MailExchangerPreference { get; } public String CanonicalDomainName {
get;
/// <summary> }
/// Gets the name of the mail exchanger domain.
/// </summary> /// <summary>
/// <value> /// Gets the mail exchanger preference.
/// The name of the mail exchanger domain. /// </summary>
/// </value> /// <value>
public string MailExchangerDomainName { get; } /// The mail exchanger preference.
/// </value>
/// <summary> public Int32? MailExchangerPreference {
/// Gets the name of the soa master domain. get;
/// </summary> }
/// <value>
/// The name of the soa master domain. /// <summary>
/// </value> /// Gets the name of the mail exchanger domain.
public string SoaMasterDomainName { get; } /// </summary>
/// <value>
/// <summary> /// The name of the mail exchanger domain.
/// Gets the name of the soa responsible domain. /// </value>
/// </summary> public String MailExchangerDomainName {
/// <value> get;
/// The name of the soa responsible domain. }
/// </value>
public string SoaResponsibleDomainName { get; } /// <summary>
/// Gets the name of the soa master domain.
/// <summary> /// </summary>
/// Gets the soa serial number. /// <value>
/// </summary> /// The name of the soa master domain.
/// <value> /// </value>
/// The soa serial number. public String SoaMasterDomainName {
/// </value> get;
public long? SoaSerialNumber { get; } }
/// <summary> /// <summary>
/// Gets the soa refresh interval. /// Gets the name of the soa responsible domain.
/// </summary> /// </summary>
/// <value> /// <value>
/// The soa refresh interval. /// The name of the soa responsible domain.
/// </value> /// </value>
public TimeSpan? SoaRefreshInterval { get; } public String SoaResponsibleDomainName {
get;
/// <summary> }
/// Gets the soa retry interval.
/// </summary> /// <summary>
/// <value> /// Gets the soa serial number.
/// The soa retry interval. /// </summary>
/// </value> /// <value>
public TimeSpan? SoaRetryInterval { get; } /// The soa serial number.
/// </value>
/// <summary> public Int64? SoaSerialNumber {
/// Gets the soa expire interval. get;
/// </summary> }
/// <value>
/// The soa expire interval. /// <summary>
/// </value> /// Gets the soa refresh interval.
public TimeSpan? SoaExpireInterval { get; } /// </summary>
/// <value>
/// <summary> /// The soa refresh interval.
/// Gets the soa minimum time to live. /// </value>
/// </summary> public TimeSpan? SoaRefreshInterval {
/// <value> get;
/// The soa minimum time to live. }
/// </value>
public TimeSpan? SoaMinimumTimeToLive { 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;
}
}
} }

View File

@ -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> /// <summary>
/// No error /// WKS records
/// </summary> /// </summary>
NoError = 0, WKS = 11,
/// <summary> /// <summary>
/// No error /// PTR records
/// </summary> /// </summary>
FormatError, PTR = 12,
/// <summary> /// <summary>
/// Format error /// MX records
/// </summary> /// </summary>
ServerFailure, MX = 15,
/// <summary> /// <summary>
/// Server failure error /// TXT records
/// </summary> /// </summary>
NameError, TXT = 16,
/// <summary> /// <summary>
/// Name error /// A records fot IPv6
/// </summary> /// </summary>
NotImplemented, AAAA = 28,
/// <summary> /// <summary>
/// Not implemented error /// SRV records
/// </summary> /// </summary>
Refused, SRV = 33,
/// <summary> /// <summary>
/// Refused error /// ANY records
/// </summary> /// </summary>
YXDomain, ANY = 255,
}
/// <summary>
/// YXRR error /// <summary>
/// </summary> /// Enumerates the different DNS record classes.
YXRRSet, /// </summary>
public enum DnsRecordClass {
/// <summary> /// <summary>
/// NXRR Set error /// IN records
/// </summary> /// </summary>
NXRRSet, IN = 1,
/// <summary> /// <summary>
/// Not authorized error /// ANY records
/// </summary> /// </summary>
NotAuth, ANY = 255,
}
/// <summary>
/// Not zone error /// <summary>
/// </summary> /// Enumerates the different DNS operation codes.
NotZone, /// </summary>
} public enum DnsOperationCode {
/// <summary>
/// Query operation
/// </summary>
Query = 0,
/// <summary>
/// IQuery operation
/// </summary>
IQuery,
/// <summary>
/// Status operation
/// </summary>
Status,
/// <summary>
/// Notify operation
/// </summary>
Notify = 4,
/// <summary>
/// Update operation
/// </summary>
Update,
}
/// <summary>
/// Enumerates the different DNS query response codes.
/// </summary>
public enum DnsResponseCode {
/// <summary>
/// No error
/// </summary>
NoError = 0,
/// <summary>
/// No error
/// </summary>
FormatError,
/// <summary>
/// Format error
/// </summary>
ServerFailure,
/// <summary>
/// Server failure error
/// </summary>
NameError,
/// <summary>
/// Name error
/// </summary>
NotImplemented,
/// <summary>
/// Not implemented error
/// </summary>
Refused,
/// <summary>
/// Refused error
/// </summary>
YXDomain,
/// <summary>
/// YXRR error
/// </summary>
YXRRSet,
/// <summary>
/// NXRR Set error
/// </summary>
NXRRSet,
/// <summary>
/// Not authorized error
/// </summary>
NotAuth,
/// <summary>
/// Not zone error
/// </summary>
NotZone,
}
} }

View File

@ -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> /// <summary>
/// The event arguments for when connections are accepted. /// Initializes a new instance of the <see cref="ConnectionAcceptedEventArgs" /> class.
/// </summary> /// </summary>
/// <seealso cref="System.EventArgs" /> /// <param name="client">The client.</param>
public class ConnectionAcceptedEventArgs : EventArgs /// <exception cref="ArgumentNullException">client.</exception>
{ public ConnectionAcceptedEventArgs(TcpClient client) => this.Client = client ?? throw new ArgumentNullException(nameof(client));
/// <summary>
/// Initializes a new instance of the <see cref="ConnectionAcceptedEventArgs" /> class.
/// </summary>
/// <param name="client">The client.</param>
/// <exception cref="ArgumentNullException">client.</exception>
public ConnectionAcceptedEventArgs(TcpClient client)
{
Client = client ?? throw new ArgumentNullException(nameof(client));
}
/// <summary>
/// Gets the client.
/// </summary>
/// <value>
/// The client.
/// </value>
public TcpClient Client { get; }
}
/// <summary> /// <summary>
/// Occurs before a connection is accepted. Set the Cancel property to true to prevent the connection from being accepted. /// Gets the client.
/// </summary> /// </summary>
/// <seealso cref="ConnectionAcceptedEventArgs" /> /// <value>
public class ConnectionAcceptingEventArgs : ConnectionAcceptedEventArgs /// The client.
{ /// </value>
/// <summary> public TcpClient Client {
/// Initializes a new instance of the <see cref="ConnectionAcceptingEventArgs"/> class. get;
/// </summary> }
/// <param name="client">The client.</param> }
public ConnectionAcceptingEventArgs(TcpClient client)
: base(client) /// <summary>
{ /// Occurs before a connection is accepted. Set the Cancel property to true to prevent the connection from being accepted.
} /// </summary>
/// <seealso cref="ConnectionAcceptedEventArgs" />
/// <summary> public class ConnectionAcceptingEventArgs : ConnectionAcceptedEventArgs {
/// Setting Cancel to true rejects the new TcpClient.
/// </summary>
/// <value>
/// <c>true</c> if cancel; otherwise, <c>false</c>.
/// </value>
public bool Cancel { get; set; }
}
/// <summary> /// <summary>
/// Event arguments for when a server listener is started. /// Initializes a new instance of the <see cref="ConnectionAcceptingEventArgs"/> class.
/// </summary> /// </summary>
/// <seealso cref="System.EventArgs" /> /// <param name="client">The client.</param>
public class ConnectionListenerStartedEventArgs : EventArgs public ConnectionAcceptingEventArgs(TcpClient client) : base(client) {
{ }
/// <summary>
/// Initializes a new instance of the <see cref="ConnectionListenerStartedEventArgs" /> class.
/// </summary>
/// <param name="listenerEndPoint">The listener end point.</param>
/// <exception cref="ArgumentNullException">listenerEndPoint.</exception>
public ConnectionListenerStartedEventArgs(IPEndPoint listenerEndPoint)
{
EndPoint = listenerEndPoint ?? throw new ArgumentNullException(nameof(listenerEndPoint));
}
/// <summary>
/// Gets the end point.
/// </summary>
/// <value>
/// The end point.
/// </value>
public IPEndPoint EndPoint { get; }
}
/// <summary> /// <summary>
/// Event arguments for when a server listener fails to start. /// Setting Cancel to true rejects the new TcpClient.
/// </summary> /// </summary>
/// <seealso cref="System.EventArgs" /> /// <value>
public class ConnectionListenerFailedEventArgs : EventArgs /// <c>true</c> if cancel; otherwise, <c>false</c>.
{ /// </value>
/// <summary> public Boolean Cancel {
/// Initializes a new instance of the <see cref="ConnectionListenerFailedEventArgs" /> class. get; set;
/// </summary> }
/// <param name="listenerEndPoint">The listener end point.</param> }
/// <param name="ex">The ex.</param>
/// <exception cref="ArgumentNullException"> /// <summary>
/// listenerEndPoint /// Event arguments for when a server listener is started.
/// or /// </summary>
/// ex. /// <seealso cref="System.EventArgs" />
/// </exception> public class ConnectionListenerStartedEventArgs : EventArgs {
public ConnectionListenerFailedEventArgs(IPEndPoint listenerEndPoint, Exception ex)
{
EndPoint = listenerEndPoint ?? throw new ArgumentNullException(nameof(listenerEndPoint));
Error = ex ?? throw new ArgumentNullException(nameof(ex));
}
/// <summary>
/// Gets the end point.
/// </summary>
/// <value>
/// The end point.
/// </value>
public IPEndPoint EndPoint { get; }
/// <summary>
/// Gets the error.
/// </summary>
/// <value>
/// The error.
/// </value>
public Exception Error { get; }
}
/// <summary> /// <summary>
/// Event arguments for when a server listener stopped. /// Initializes a new instance of the <see cref="ConnectionListenerStartedEventArgs" /> class.
/// </summary> /// </summary>
/// <seealso cref="System.EventArgs" /> /// <param name="listenerEndPoint">The listener end point.</param>
public class ConnectionListenerStoppedEventArgs : EventArgs /// <exception cref="ArgumentNullException">listenerEndPoint.</exception>
{ public ConnectionListenerStartedEventArgs(IPEndPoint listenerEndPoint) => this.EndPoint = listenerEndPoint ?? throw new ArgumentNullException(nameof(listenerEndPoint));
/// <summary>
/// Initializes a new instance of the <see cref="ConnectionListenerStoppedEventArgs" /> class. /// <summary>
/// </summary> /// Gets the end point.
/// <param name="listenerEndPoint">The listener end point.</param> /// </summary>
/// <param name="ex">The ex.</param> /// <value>
/// <exception cref="ArgumentNullException"> /// The end point.
/// listenerEndPoint /// </value>
/// or public IPEndPoint EndPoint {
/// ex. get;
/// </exception> }
public ConnectionListenerStoppedEventArgs(IPEndPoint listenerEndPoint, Exception? ex = null) }
{
EndPoint = listenerEndPoint ?? throw new ArgumentNullException(nameof(listenerEndPoint)); /// <summary>
Error = ex; /// Event arguments for when a server listener fails to start.
} /// </summary>
/// <seealso cref="System.EventArgs" />
/// <summary> public class ConnectionListenerFailedEventArgs : EventArgs {
/// Gets the end point. /// <summary>
/// </summary> /// Initializes a new instance of the <see cref="ConnectionListenerFailedEventArgs" /> class.
/// <value> /// </summary>
/// The end point. /// <param name="listenerEndPoint">The listener end point.</param>
/// </value> /// <param name="ex">The ex.</param>
public IPEndPoint EndPoint { get; } /// <exception cref="ArgumentNullException">
/// listenerEndPoint
/// <summary> /// or
/// Gets the error. /// ex.
/// </summary> /// </exception>
/// <value> public ConnectionListenerFailedEventArgs(IPEndPoint listenerEndPoint, Exception ex) {
/// The error. this.EndPoint = listenerEndPoint ?? throw new ArgumentNullException(nameof(listenerEndPoint));
/// </value> this.Error = ex ?? throw new ArgumentNullException(nameof(ex));
public Exception? Error { get; } }
}
/// <summary>
/// Gets the end point.
/// </summary>
/// <value>
/// The end point.
/// </value>
public IPEndPoint EndPoint {
get;
}
/// <summary>
/// Gets the error.
/// </summary>
/// <value>
/// The error.
/// </value>
public Exception Error {
get;
}
}
/// <summary>
/// Event arguments for when a server listener stopped.
/// </summary>
/// <seealso cref="System.EventArgs" />
public class ConnectionListenerStoppedEventArgs : EventArgs {
/// <summary>
/// Initializes a new instance of the <see cref="ConnectionListenerStoppedEventArgs" /> class.
/// </summary>
/// <param name="listenerEndPoint">The listener end point.</param>
/// <param name="ex">The ex.</param>
/// <exception cref="ArgumentNullException">
/// listenerEndPoint
/// or
/// ex.
/// </exception>
public ConnectionListenerStoppedEventArgs(IPEndPoint listenerEndPoint, Exception? ex = null) {
this.EndPoint = listenerEndPoint ?? throw new ArgumentNullException(nameof(listenerEndPoint));
this.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;
}
}
} }

View File

@ -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> /// <summary>
/// The event arguments for connection failure events. /// Initializes a new instance of the <see cref="ConnectionFailureEventArgs"/> class.
/// </summary> /// </summary>
/// <seealso cref="System.EventArgs" /> /// <param name="ex">The ex.</param>
public class ConnectionFailureEventArgs : EventArgs public ConnectionFailureEventArgs(Exception ex) => this.Error = ex;
{
/// <summary>
/// Initializes a new instance of the <see cref="ConnectionFailureEventArgs"/> class.
/// </summary>
/// <param name="ex">The ex.</param>
public ConnectionFailureEventArgs(Exception ex)
{
Error = ex;
}
/// <summary>
/// Gets the error.
/// </summary>
/// <value>
/// The error.
/// </value>
public Exception Error { get; }
}
/// <summary> /// <summary>
/// Event arguments for when data is received. /// Gets the error.
/// </summary> /// </summary>
/// <seealso cref="System.EventArgs" /> /// <value>
public class ConnectionDataReceivedEventArgs : EventArgs /// The error.
{ /// </value>
/// <summary> public Exception Error {
/// 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> /// <summary>
public ConnectionDataReceivedEventArgs(byte[] buffer, ConnectionDataReceivedTrigger trigger, bool moreAvailable) /// Event arguments for when data is received.
{ /// </summary>
Buffer = buffer ?? throw new ArgumentNullException(nameof(buffer)); /// <seealso cref="System.EventArgs" />
Trigger = trigger; public class ConnectionDataReceivedEventArgs : EventArgs {
HasMoreAvailable = moreAvailable; /// <summary>
} /// Initializes a new instance of the <see cref="ConnectionDataReceivedEventArgs"/> class.
/// </summary>
/// <summary> /// <param name="buffer">The buffer.</param>
/// Gets the buffer. /// <param name="trigger">The trigger.</param>
/// </summary> /// <param name="moreAvailable">if set to <c>true</c> [more available].</param>
/// <value> public ConnectionDataReceivedEventArgs(Byte[] buffer, ConnectionDataReceivedTrigger trigger, Boolean moreAvailable) {
/// The buffer. this.Buffer = buffer ?? throw new ArgumentNullException(nameof(buffer));
/// </value> this.Trigger = trigger;
public byte[] Buffer { get; } this.HasMoreAvailable = moreAvailable;
}
/// <summary>
/// Gets the cause as to why this event was thrown. /// <summary>
/// </summary> /// Gets the buffer.
/// <value> /// </summary>
/// The trigger. /// <value>
/// </value> /// The buffer.
public ConnectionDataReceivedTrigger Trigger { get; } /// </value>
public Byte[] Buffer {
/// <summary> get;
/// Gets a value indicating whether the receive buffer has more bytes available. }
/// </summary>
/// <value> /// <summary>
/// <c>true</c> if this instance has more available; otherwise, <c>false</c>. /// Gets the cause as to why this event was thrown.
/// </value> /// </summary>
public bool HasMoreAvailable { get; } /// <value>
/// The trigger.
/// <summary> /// </value>
/// Gets the string from buffer. public ConnectionDataReceivedTrigger Trigger {
/// </summary> get;
/// <param name="encoding">The encoding.</param> }
/// <returns>
/// A <see cref="System.String" /> that contains the results of decoding the specified sequence of bytes. /// <summary>
/// </returns> /// Gets a value indicating whether the receive buffer has more bytes available.
/// <exception cref="ArgumentNullException">encoding</exception> /// </summary>
public string GetStringFromBuffer(Encoding encoding) /// <value>
=> encoding?.GetString(Buffer).TrimEnd('\r', '\n') ?? throw new ArgumentNullException(nameof(encoding)); /// <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));
}
} }

View File

@ -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>
private static readonly HttpClient HttpClient = new HttpClient(); /// <returns>
/// A task with a result of the requested type.
/// <summary> /// </returns>
/// Post a object as JSON with optional authorization token. public static async Task<T> Post<T>(Uri requestUri, Object payload, String? authorization = null, CancellationToken cancellationToken = default) where T : notnull {
/// </summary> String jsonString = await PostString(requestUri, payload, authorization, cancellationToken).ConfigureAwait(false);
/// <typeparam name="T">The type of response object.</typeparam>
/// <param name="requestUri">The request URI.</param> return !String.IsNullOrEmpty(jsonString) ? Json.Deserialize<T>(jsonString) : default;
/// <param name="payload">The payload.</param> }
/// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <summary>
/// <returns> /// Posts the specified URL.
/// A task with a result of the requested type. /// </summary>
/// </returns> /// <param name="requestUri">The request URI.</param>
public static async Task<T> Post<T>( /// <param name="payload">The payload.</param>
Uri requestUri, /// <param name="authorization">The authorization.</param>
object payload, /// <param name="cancellationToken">The cancellation token.</param>
string? authorization = null, /// <returns>
CancellationToken cancellationToken = default) /// A task with a result as a collection of key/value pairs.
{ /// </returns>
var jsonString = await PostString(requestUri, payload, authorization, cancellationToken) public static async Task<IDictionary<String, Object>?> Post(Uri requestUri, Object payload, String? authorization = null, CancellationToken cancellationToken = default) {
.ConfigureAwait(false); String jsonString = await PostString(requestUri, payload, authorization, cancellationToken).ConfigureAwait(false);
return !string.IsNullOrEmpty(jsonString) ? Json.Deserialize<T>(jsonString) : default; return String.IsNullOrWhiteSpace(jsonString) ? default : Json.Deserialize(jsonString) as IDictionary<String, Object>;
} }
/// <summary> /// <summary>
/// Posts the specified URL. /// Posts the specified URL.
/// </summary> /// </summary>
/// <param name="requestUri">The request URI.</param> /// <param name="requestUri">The request URI.</param>
/// <param name="payload">The payload.</param> /// <param name="payload">The payload.</param>
/// <param name="authorization">The authorization.</param> /// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns> /// <returns>
/// A task with a result as a collection of key/value pairs. /// A task with a result of the requested string.
/// </returns> /// </returns>
public static async Task<IDictionary<string, object>?> Post( /// <exception cref="ArgumentNullException">url.</exception>
Uri requestUri, /// <exception cref="JsonRequestException">Error POST JSON.</exception>
object payload, public static Task<String> PostString(Uri requestUri, Object payload, String? authorization = null, CancellationToken cancellationToken = default) => SendAsync(HttpMethod.Post, requestUri, payload, authorization, cancellationToken);
string? authorization = null,
CancellationToken cancellationToken = default) /// <summary>
{ /// Puts the specified URL.
var jsonString = await PostString(requestUri, payload, authorization, cancellationToken) /// </summary>
.ConfigureAwait(false); /// <typeparam name="T">The type of response object.</typeparam>
/// <param name="requestUri">The request URI.</param>
return string.IsNullOrWhiteSpace(jsonString) /// <param name="payload">The payload.</param>
? default /// <param name="authorization">The authorization.</param>
: Json.Deserialize(jsonString) as IDictionary<string, object>; /// <param name="ct">The cancellation token.</param>
} /// <returns>
/// A task with a result of the requested type.
/// <summary> /// </returns>
/// Posts the specified URL. public static async Task<T> Put<T>(Uri requestUri, Object payload, String? authorization = null, CancellationToken ct = default) where T : notnull {
/// </summary> String jsonString = await PutString(requestUri, payload, authorization, ct).ConfigureAwait(false);
/// <param name="requestUri">The request URI.</param>
/// <param name="payload">The payload.</param> return !String.IsNullOrEmpty(jsonString) ? Json.Deserialize<T>(jsonString) : default;
/// <param name="authorization">The authorization.</param> }
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns> /// <summary>
/// A task with a result of the requested string. /// Puts the specified URL.
/// </returns> /// </summary>
/// <exception cref="ArgumentNullException">url.</exception> /// <param name="requestUri">The request URI.</param>
/// <exception cref="JsonRequestException">Error POST JSON.</exception> /// <param name="payload">The payload.</param>
public static Task<string> PostString( /// <param name="authorization">The authorization.</param>
Uri requestUri, /// <param name="cancellationToken">The cancellation token.</param>
object payload, /// <returns>
string? authorization = null, /// A task with a result of the requested collection of key/value pairs.
CancellationToken cancellationToken = default) /// </returns>
=> SendAsync(HttpMethod.Post, requestUri, payload, authorization, cancellationToken); 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);
/// <summary>
/// Puts the specified URL. return response as IDictionary<String, Object>;
/// </summary> }
/// <typeparam name="T">The type of response object.</typeparam>
/// <param name="requestUri">The request URI.</param> /// <summary>
/// <param name="payload">The payload.</param> /// Puts as string.
/// <param name="authorization">The authorization.</param> /// </summary>
/// <param name="ct">The cancellation token.</param> /// <param name="requestUri">The request URI.</param>
/// <returns> /// <param name="payload">The payload.</param>
/// A task with a result of the requested type. /// <param name="authorization">The authorization.</param>
/// </returns> /// <param name="ct">The cancellation token.</param>
public static async Task<T> Put<T>( /// <returns>
Uri requestUri, /// A task with a result of the requested string.
object payload, /// </returns>
string? authorization = null, /// <exception cref="ArgumentNullException">url.</exception>
CancellationToken ct = default) /// <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);
var jsonString = await PutString(requestUri, payload, authorization, ct)
.ConfigureAwait(false); /// <summary>
/// Gets as string.
return !string.IsNullOrEmpty(jsonString) ? Json.Deserialize<T>(jsonString) : default; /// </summary>
} /// <param name="requestUri">The request URI.</param>
/// <param name="authorization">The authorization.</param>
/// <summary> /// <param name="ct">The cancellation token.</param>
/// Puts the specified URL. /// <returns>
/// </summary> /// A task with a result of the requested string.
/// <param name="requestUri">The request URI.</param> /// </returns>
/// <param name="payload">The payload.</param> /// <exception cref="ArgumentNullException">url.</exception>
/// <param name="authorization">The authorization.</param> /// <exception cref="JsonRequestException">Error GET JSON.</exception>
/// <param name="cancellationToken">The cancellation token.</param> public static Task<String> GetString(Uri requestUri, String? authorization = null, CancellationToken ct = default) => GetString(requestUri, null, authorization, ct);
/// <returns>
/// A task with a result of the requested collection of key/value pairs. /// <summary>
/// </returns> /// Gets the string.
public static async Task<IDictionary<string, object>?> Put( /// </summary>
Uri requestUri, /// <param name="uri">The URI.</param>
object payload, /// <param name="headers">The headers.</param>
string? authorization = null, /// <param name="authorization">The authorization.</param>
CancellationToken cancellationToken = default) /// <param name="ct">The ct.</param>
{ /// <returns>
var response = await Put<object>(requestUri, payload, authorization, cancellationToken) /// A task with a result of the requested string.
.ConfigureAwait(false); /// </returns>
public static async Task<String> GetString(Uri uri, IDictionary<String, IEnumerable<String>>? headers, String? authorization = null, CancellationToken ct = default) {
return response as IDictionary<string, object>; HttpContent response = await GetHttpContent(uri, ct, authorization, headers).ConfigureAwait(false);
}
return await response.ReadAsStringAsync().ConfigureAwait(false);
/// <summary> }
/// Puts as string.
/// </summary> /// <summary>
/// <param name="requestUri">The request URI.</param> /// Gets the specified URL and return the JSON data as object
/// <param name="payload">The payload.</param> /// with optional authorization token.
/// <param name="authorization">The authorization.</param> /// </summary>
/// <param name="ct">The cancellation token.</param> /// <typeparam name="T">The response type.</typeparam>
/// <returns> /// <param name="requestUri">The request URI.</param>
/// A task with a result of the requested string. /// <param name="authorization">The authorization.</param>
/// </returns> /// <param name="ct">The cancellation token.</param>
/// <exception cref="ArgumentNullException">url.</exception> /// <returns>
/// <exception cref="JsonRequestException">Error PUT JSON.</exception> /// A task with a result of the requested type.
public static Task<string> PutString( /// </returns>
Uri requestUri, public static async Task<T> Get<T>(Uri requestUri, String? authorization = null, CancellationToken ct = default) where T : notnull {
object payload, String jsonString = await GetString(requestUri, authorization, ct).ConfigureAwait(false);
string? authorization = null,
CancellationToken ct = default) => SendAsync(HttpMethod.Put, requestUri, payload, authorization, ct); return !String.IsNullOrEmpty(jsonString) ? Json.Deserialize<T>(jsonString) : default;
}
/// <summary>
/// Gets as string. /// <summary>
/// </summary> /// Gets the specified URL and return the JSON data as object
/// <param name="requestUri">The request URI.</param> /// with optional authorization token.
/// <param name="authorization">The authorization.</param> /// </summary>
/// <param name="ct">The cancellation token.</param> /// <typeparam name="T">The response type.</typeparam>
/// <returns> /// <param name="requestUri">The request URI.</param>
/// A task with a result of the requested string. /// <param name="headers">The headers.</param>
/// </returns> /// <param name="authorization">The authorization.</param>
/// <exception cref="ArgumentNullException">url.</exception> /// <param name="ct">The cancellation token.</param>
/// <exception cref="JsonRequestException">Error GET JSON.</exception> /// <returns>
public static Task<string> GetString( /// A task with a result of the requested type.
Uri requestUri, /// </returns>
string? authorization = null, public static async Task<T> Get<T>(Uri requestUri, IDictionary<String, IEnumerable<String>>? headers, String? authorization = null, CancellationToken ct = default) where T : notnull {
CancellationToken ct = default) String jsonString = await GetString(requestUri, headers, authorization, ct).ConfigureAwait(false);
=> GetString(requestUri, null, authorization, ct);
return !String.IsNullOrEmpty(jsonString) ? Json.Deserialize<T>(jsonString) : default;
/// <summary> }
/// Gets the string.
/// </summary> /// <summary>
/// <param name="uri">The URI.</param> /// Gets the binary.
/// <param name="headers">The headers.</param> /// </summary>
/// <param name="authorization">The authorization.</param> /// <param name="requestUri">The request URI.</param>
/// <param name="ct">The ct.</param> /// <param name="authorization">The authorization.</param>
/// <returns> /// <param name="ct">The cancellation token.</param>
/// A task with a result of the requested string. /// <returns>
/// </returns> /// A task with a result of the requested byte array.
public static async Task<string> GetString( /// </returns>
Uri uri, /// <exception cref="ArgumentNullException">url.</exception>
IDictionary<string, IEnumerable<string>>? headers, /// <exception cref="JsonRequestException">Error GET Binary.</exception>
string? authorization = null, public static async Task<Byte[]> GetBinary(Uri requestUri, String? authorization = null, CancellationToken ct = default) {
CancellationToken ct = default) HttpContent response = await GetHttpContent(requestUri, ct, authorization).ConfigureAwait(false);
{
var response = await GetHttpContent(uri, ct, authorization, headers) return await response.ReadAsByteArrayAsync().ConfigureAwait(false);
.ConfigureAwait(false); }
return await response.ReadAsStringAsync() /// <summary>
.ConfigureAwait(false); /// Authenticate against a web server using Bearer Token.
} /// </summary>
/// <param name="requestUri">The request URI.</param>
/// <summary> /// <param name="username">The username.</param>
/// Gets the specified URL and return the JSON data as object /// <param name="password">The password.</param>
/// with optional authorization token. /// <param name="ct">The cancellation token.</param>
/// </summary> /// <returns>
/// <typeparam name="T">The response type.</typeparam> /// A task with a Dictionary with authentication data.
/// <param name="requestUri">The request URI.</param> /// </returns>
/// <param name="authorization">The authorization.</param> /// <exception cref="ArgumentNullException">url
/// <param name="ct">The cancellation token.</param> /// or
/// <returns> /// username.</exception>
/// A task with a result of the requested type. /// <exception cref="SecurityException">Error Authenticating.</exception>
/// </returns> public static async Task<IDictionary<String, Object>?> Authenticate(Uri requestUri, String username, String password, CancellationToken ct = default) {
public static async Task<T> Get<T>( if(String.IsNullOrWhiteSpace(username)) {
Uri requestUri, throw new ArgumentNullException(nameof(username));
string? authorization = null, }
CancellationToken ct = default)
{ // ignore empty password for now
var jsonString = await GetString(requestUri, authorization, ct) String content = $"grant_type=password&username={username}&password={password}";
.ConfigureAwait(false); using StringContent requestContent = new StringContent(content, Encoding.UTF8, FormType);
HttpResponseMessage response = await HttpClient.PostAsync(requestUri, requestContent, ct).ConfigureAwait(false);
return !string.IsNullOrEmpty(jsonString) ? Json.Deserialize<T>(jsonString) : default;
} if(!response.IsSuccessStatusCode) {
throw new SecurityException($"Error Authenticating. Status code: {response.StatusCode}.");
/// <summary> }
/// Gets the specified URL and return the JSON data as object
/// with optional authorization token. String jsonPayload = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
/// </summary>
/// <typeparam name="T">The response type.</typeparam> return Json.Deserialize(jsonPayload) as IDictionary<String, Object>;
/// <param name="requestUri">The request URI.</param> }
/// <param name="headers">The headers.</param>
/// <param name="authorization">The authorization.</param> /// <summary>
/// <param name="ct">The cancellation token.</param> /// Posts the file.
/// <returns> /// </summary>
/// A task with a result of the requested type. /// <param name="requestUri">The request URI.</param>
/// </returns> /// <param name="buffer">The buffer.</param>
public static async Task<T> Get<T>( /// <param name="fileName">Name of the file.</param>
Uri requestUri, /// <param name="authorization">The authorization.</param>
IDictionary<string, IEnumerable<string>>? headers, /// <param name="ct">The cancellation token.</param>
string? authorization = null, /// <returns>
CancellationToken ct = default) /// A task with a result of the requested string.
{ /// </returns>
var jsonString = await GetString(requestUri, headers, authorization, ct) 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);
.ConfigureAwait(false);
/// <summary>
return !string.IsNullOrEmpty(jsonString) ? Json.Deserialize<T>(jsonString) : default; /// Posts the file.
} /// </summary>
/// <typeparam name="T">The response type.</typeparam>
/// <summary> /// <param name="requestUri">The request URI.</param>
/// Gets the binary. /// <param name="buffer">The buffer.</param>
/// </summary> /// <param name="fileName">Name of the file.</param>
/// <param name="requestUri">The request URI.</param> /// <param name="authorization">The authorization.</param>
/// <param name="authorization">The authorization.</param> /// <param name="ct">The cancellation token.</param>
/// <param name="ct">The cancellation token.</param> /// <returns>
/// <returns> /// A task with a result of the requested string.
/// A task with a result of the requested byte array. /// </returns>
/// </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);
/// <exception cref="ArgumentNullException">url.</exception>
/// <exception cref="JsonRequestException">Error GET Binary.</exception> /// <summary>
public static async Task<byte[]> GetBinary( /// Sends the asynchronous request.
Uri requestUri, /// </summary>
string? authorization = null, /// <param name="method">The method.</param>
CancellationToken ct = default) /// <param name="requestUri">The request URI.</param>
{ /// <param name="payload">The payload.</param>
var response = await GetHttpContent(requestUri, ct, authorization) /// <param name="authorization">The authorization.</param>
.ConfigureAwait(false); /// <param name="ct">The cancellation token.</param>
/// <returns>
return await response.ReadAsByteArrayAsync() /// A task with a result of the requested string.
.ConfigureAwait(false); /// </returns>
} /// <exception cref="ArgumentNullException">requestUri.</exception>
/// <exception cref="JsonRequestException">Error {method} JSON.</exception>
/// <summary> public static async Task<String> SendAsync(HttpMethod method, Uri requestUri, Object payload, String? authorization = null, CancellationToken ct = default) {
/// Authenticate against a web server using Bearer Token. using HttpResponseMessage response = await GetResponse(requestUri, authorization, null, payload, method, ct).ConfigureAwait(false);
/// </summary> if(!response.IsSuccessStatusCode) {
/// <param name="requestUri">The request URI.</param> throw new JsonRequestException(
/// <param name="username">The username.</param> $"Error {method} JSON",
/// <param name="password">The password.</param> (Int32)response.StatusCode,
/// <param name="ct">The cancellation token.</param> await response.Content.ReadAsStringAsync().ConfigureAwait(false));
/// <returns> }
/// A task with a Dictionary with authentication data.
/// </returns> return await response.Content.ReadAsStringAsync().ConfigureAwait(false);
/// <exception cref="ArgumentNullException">url }
/// or
/// username.</exception> private static async Task<HttpContent> GetHttpContent(Uri uri, CancellationToken ct, String? authorization = null, IDictionary<String, IEnumerable<String>>? headers = null) {
/// <exception cref="SecurityException">Error Authenticating.</exception> HttpResponseMessage response = await GetResponse(uri, authorization, headers, ct: ct).ConfigureAwait(false);
public static async Task<IDictionary<string, object>?> Authenticate(
Uri requestUri, return response.IsSuccessStatusCode ? response.Content : throw new JsonRequestException("Error GET", (Int32)response.StatusCode);
string username, }
string password,
CancellationToken ct = default) 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) {
if (string.IsNullOrWhiteSpace(username)) throw new ArgumentNullException(nameof(uri));
throw new ArgumentNullException(nameof(username)); }
// ignore empty password for now using HttpRequestMessage requestMessage = new HttpRequestMessage(method ?? HttpMethod.Get, uri);
var content = $"grant_type=password&username={username}&password={password}";
using var requestContent = new StringContent(content, Encoding.UTF8, FormType); if(!String.IsNullOrWhiteSpace(authorization)) {
var response = await HttpClient.PostAsync(requestUri, requestContent, ct).ConfigureAwait(false); requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authorization);
}
if (!response.IsSuccessStatusCode)
throw new SecurityException($"Error Authenticating. Status code: {response.StatusCode}."); if(headers != null) {
foreach(KeyValuePair<String, IEnumerable<String>> header in headers) {
var jsonPayload = await response.Content.ReadAsStringAsync().ConfigureAwait(false); requestMessage.Headers.Add(header.Key, header.Value);
}
return Json.Deserialize(jsonPayload) as IDictionary<string, object>; }
}
if(payload != null && requestMessage.Method != HttpMethod.Get) {
/// <summary> requestMessage.Content = new StringContent(Json.Serialize(payload), Encoding.UTF8, JsonMimeType);
/// Posts the file. }
/// </summary>
/// <param name="requestUri">The request URI.</param> return await HttpClient.SendAsync(requestMessage, ct).ConfigureAwait(false);
/// <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);
}
}
} }

View File

@ -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> /// <summary>
/// Represents errors that occurs requesting a JSON file through HTTP. /// Initializes a new instance of the <see cref="JsonRequestException"/> class.
/// </summary> /// </summary>
/// <seealso cref="System.Exception" /> /// <param name="message">The message.</param>
[Serializable] /// <param name="httpErrorCode">The HTTP error code.</param>
public class JsonRequestException /// <param name="errorContent">Content of the error.</param>
: Exception public JsonRequestException(String message, Int32 httpErrorCode = 500, String errorContent = null) : base(message) {
{ this.HttpErrorCode = httpErrorCode;
/// <summary> this.HttpErrorContent = errorContent;
/// Initializes a new instance of the <see cref="JsonRequestException"/> class. }
/// </summary>
/// <param name="message">The message.</param> /// <summary>
/// <param name="httpErrorCode">The HTTP error code.</param> /// Gets the HTTP error code.
/// <param name="errorContent">Content of the error.</param> /// </summary>
public JsonRequestException(string message, int httpErrorCode = 500, string errorContent = null) /// <value>
: base(message) /// The HTTP error code.
{ /// </value>
HttpErrorCode = httpErrorCode; public Int32 HttpErrorCode {
HttpErrorContent = errorContent; get;
} }
/// <summary> /// <summary>
/// Gets the HTTP error code. /// Gets the content of the HTTP error.
/// </summary> /// </summary>
/// <value> /// <value>
/// The HTTP error code. /// The content of the HTTP error.
/// </value> /// </value>
public int HttpErrorCode { get; } public String HttpErrorContent {
get;
/// <summary> }
/// Gets the content of the HTTP error.
/// </summary> /// <inheritdoc />
/// <value> public override String ToString() => String.IsNullOrEmpty(this.HttpErrorContent) ? $"HTTP Response Status Code {this.HttpErrorCode} Error Message: {this.HttpErrorContent}" : base.ToString();
/// 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();
}
} }

View File

@ -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> /// <summary>
/// Provides miscellaneous network utilities such as a Public IP finder, /// The DNS 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 DnsDefaultPort = 53;
{
/// <summary> /// <summary>
/// The DNS default port. /// The NTP default port.
/// </summary> /// </summary>
public const int DnsDefaultPort = 53; public const Int32 NtpDefaultPort = 123;
/// <summary>
/// Gets the name of the host.
/// </summary>
/// <value>
/// The name of the host.
/// </value>
public static String HostName => IPGlobalProperties.GetIPGlobalProperties().HostName;
/// <summary>
/// Gets the name of the network domain.
/// </summary>
/// <value>
/// The name of the network domain.
/// </value>
public static String DomainName => IPGlobalProperties.GetIPGlobalProperties().DomainName;
#region IP Addresses and Adapters Information Methods
/// <summary>
/// Gets the active IPv4 interfaces.
/// Only those interfaces with a valid unicast address and a valid gateway will be returned in the collection.
/// </summary>
/// <returns>
/// A collection of NetworkInterface/IPInterfaceProperties pairs
/// that represents the active IPv4 interfaces.
/// </returns>
public static Dictionary<NetworkInterface, IPInterfaceProperties> GetIPv4Interfaces() {
// zero conf ip address
IPAddress zeroConf = new IPAddress(0);
NetworkInterface[] adapters = NetworkInterface.GetAllNetworkInterfaces().Where(network => network.OperationalStatus == OperationalStatus.Up && network.NetworkInterfaceType != NetworkInterfaceType.Unknown && network.NetworkInterfaceType != NetworkInterfaceType.Loopback).ToArray();
Dictionary<NetworkInterface, IPInterfaceProperties> result = new Dictionary<NetworkInterface, IPInterfaceProperties>();
foreach(NetworkInterface adapter in adapters) {
IPInterfaceProperties properties = adapter.GetIPProperties();
if(properties == null || properties.GatewayAddresses.Count == 0 || properties.GatewayAddresses.All(gateway => Equals(gateway.Address, zeroConf)) || properties.UnicastAddresses.Count == 0 || properties.GatewayAddresses.All(address => Equals(address.Address, zeroConf)) || properties.UnicastAddresses.Any(a => a.Address.AddressFamily == AddressFamily.InterNetwork) == false) {
continue;
}
result[adapter] = properties;
}
return result;
}
/// <summary>
/// Retrieves the local ip addresses.
/// </summary>
/// <param name="includeLoopback">if set to <c>true</c> [include loopback].</param>
/// <returns>An array of local ip addresses.</returns>
public static IPAddress[] GetIPv4Addresses(Boolean includeLoopback = true) => GetIPv4Addresses(NetworkInterfaceType.Unknown, true, includeLoopback);
/// <summary>
/// Retrieves the local ip addresses.
/// </summary>
/// <param name="interfaceType">Type of the interface.</param>
/// <param name="skipTypeFilter">if set to <c>true</c> [skip type filter].</param>
/// <param name="includeLoopback">if set to <c>true</c> [include loopback].</param>
/// <returns>An array of local ip addresses.</returns>
public static IPAddress[] GetIPv4Addresses(NetworkInterfaceType interfaceType, Boolean skipTypeFilter = false, Boolean includeLoopback = false) {
List<IPAddress> addressList = new List<IPAddress>();
NetworkInterface[] interfaces = NetworkInterface.GetAllNetworkInterfaces()
.Where(ni => (skipTypeFilter || ni.NetworkInterfaceType == interfaceType) && ni.OperationalStatus == OperationalStatus.Up).ToArray();
foreach(NetworkInterface networkInterface in interfaces) {
IPInterfaceProperties properties = networkInterface.GetIPProperties();
if(properties.GatewayAddresses.All(g => g.Address.AddressFamily != AddressFamily.InterNetwork)) {
continue;
}
addressList.AddRange(properties.UnicastAddresses.Where(i => i.Address.AddressFamily == AddressFamily.InterNetwork).Select(i => i.Address));
}
if(includeLoopback || interfaceType == NetworkInterfaceType.Loopback) {
addressList.Add(IPAddress.Loopback);
}
return addressList.ToArray();
}
/// <summary>
/// Gets the public IP address using ipify.org.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A public IP address of the result produced by this Task.</returns>
public static async Task<IPAddress> GetPublicIPAddressAsync(CancellationToken cancellationToken = default) {
using HttpClient client = new HttpClient();
HttpResponseMessage response = await client.GetAsync("https://api.ipify.org", cancellationToken).ConfigureAwait(false);
return IPAddress.Parse(await response.Content.ReadAsStringAsync().ConfigureAwait(false));
}
/// <summary>
/// Gets the configured IPv4 DNS servers for the active network interfaces.
/// </summary>
/// <returns>
/// A collection of NetworkInterface/IPInterfaceProperties pairs
/// that represents the active IPv4 interfaces.
/// </returns>
public static IPAddress[] GetIPv4DnsServers() => GetIPv4Interfaces().Select(a => a.Value.DnsAddresses.Where(d => d.AddressFamily == AddressFamily.InterNetwork)).SelectMany(d => d).ToArray();
#endregion
#region DNS and NTP Clients
/// <summary>
/// Gets the DNS host entry (a list of IP addresses) for the domain name.
/// </summary>
/// <param name="fqdn">The FQDN.</param>
/// <returns>An array of local ip addresses of the result produced by this task.</returns>
public static Task<IPAddress[]> GetDnsHostEntryAsync(String fqdn) {
IPAddress dnsServer = GetIPv4DnsServers().FirstOrDefault() ?? IPAddress.Parse("8.8.8.8");
return GetDnsHostEntryAsync(fqdn, dnsServer, DnsDefaultPort);
}
/// <summary>
/// Gets the DNS host entry (a list of IP addresses) for the domain name.
/// </summary>
/// <param name="fqdn">The FQDN.</param>
/// <param name="dnsServer">The DNS server.</param>
/// <param name="port">The port.</param>
/// <returns>
/// An array of local ip addresses of the result produced by this task.
/// </returns>
/// <exception cref="ArgumentNullException">fqdn.</exception>
public static async Task<IPAddress[]> GetDnsHostEntryAsync(String fqdn, IPAddress dnsServer, Int32 port) {
if(fqdn == null) {
throw new ArgumentNullException(nameof(fqdn));
}
if(fqdn.IndexOf(".", StringComparison.Ordinal) == -1) {
fqdn += "." + IPGlobalProperties.GetIPGlobalProperties().DomainName;
}
while(true) {
if(!fqdn.EndsWith(".", StringComparison.OrdinalIgnoreCase)) {
break;
}
fqdn = fqdn[0..^1];
}
DnsClient client = new DnsClient(dnsServer, port);
IList<IPAddress> result = await client.Lookup(fqdn).ConfigureAwait(false);
return result.ToArray();
}
/// <summary>
/// Gets the reverse lookup FQDN of the given IP Address.
/// </summary>
/// <param name="query">The query.</param>
/// <param name="dnsServer">The DNS server.</param>
/// <param name="port">The port.</param>
/// <returns>A <see cref="System.String" /> that represents the current object.</returns>
public static Task<String> GetDnsPointerEntryAsync(IPAddress query, IPAddress dnsServer, Int32 port) {
DnsClient client = new DnsClient(dnsServer, port);
return client.Reverse(query);
}
/// <summary>
/// Gets the reverse lookup FQDN of the given IP Address.
/// </summary>
/// <param name="query">The query.</param>
/// <returns>A <see cref="System.String" /> that represents the current object.</returns>
public static Task<String> GetDnsPointerEntryAsync(IPAddress query) {
DnsClient client = new DnsClient(GetIPv4DnsServers().FirstOrDefault());
return client.Reverse(query);
}
/// <summary>
/// Queries the DNS server for the specified record type.
/// </summary>
/// <param name="query">The query.</param>
/// <param name="recordType">Type of the record.</param>
/// <param name="dnsServer">The DNS server.</param>
/// <param name="port">The port.</param>
/// <returns>Queries the DNS server for the specified record type of the result produced by this Task.</returns>
public static async Task<DnsQueryResult> QueryDnsAsync(String query, DnsRecordType recordType, IPAddress dnsServer, Int32 port) {
if(query == null) {
throw new ArgumentNullException(nameof(query));
}
DnsClient client = new DnsClient(dnsServer, port);
DnsClient.DnsClientResponse response = await client.Resolve(query, recordType).ConfigureAwait(false);
return new DnsQueryResult(response);
}
/// <summary>
/// Queries the DNS server for the specified record type.
/// </summary>
/// <param name="query">The query.</param>
/// <param name="recordType">Type of the record.</param>
/// <returns>Queries the DNS server for the specified record type of the result produced by this Task.</returns>
public static Task<DnsQueryResult> QueryDnsAsync(String query, DnsRecordType recordType) => QueryDnsAsync(query, recordType, GetIPv4DnsServers().FirstOrDefault(), DnsDefaultPort);
/// <summary>
/// Gets the UTC time by querying from an NTP server.
/// </summary>
/// <param name="ntpServerAddress">The NTP server address.</param>
/// <param name="port">The port.</param>
/// <returns>The UTC time by querying from an NTP server of the result produced by this Task.</returns>
public static async Task<DateTime> GetNetworkTimeUtcAsync(IPAddress ntpServerAddress, Int32 port = NtpDefaultPort) {
if(ntpServerAddress == null) {
throw new ArgumentNullException(nameof(ntpServerAddress));
}
// NTP message size - 16 bytes of the digest (RFC 2030)
Byte[] ntpData = new Byte[48];
// Setting the Leap Indicator, Version Number and Mode values
ntpData[0] = 0x1B; // LI = 0 (no warning), VN = 3 (IPv4 only), Mode = 3 (Client Mode)
// The UDP port number assigned to NTP is 123
IPEndPoint endPoint = new IPEndPoint(ntpServerAddress, port);
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
/// <summary> await socket.ConnectAsync(endPoint).ConfigureAwait(false);
/// The NTP default port.
/// </summary>
public const int NtpDefaultPort = 123;
/// <summary>
/// Gets the name of the host. socket.ReceiveTimeout = 3000; // Stops code hang if NTP is blocked
/// </summary> _ = socket.Send(ntpData);
/// <value> _ = socket.Receive(ntpData);
/// The name of the host. socket.Dispose();
/// </value>
public static string HostName => IPGlobalProperties.GetIPGlobalProperties().HostName; // Offset to get to the "Transmit Timestamp" field (time at which the reply
// departed the server for the client, in 64-bit timestamp format."
/// <summary> const Byte serverReplyTime = 40;
/// Gets the name of the network domain.
/// </summary> // Get the seconds part
/// <value> UInt64 intPart = BitConverter.ToUInt32(ntpData, serverReplyTime);
/// The name of the network domain.
/// </value> // Get the seconds fraction
public static string DomainName => IPGlobalProperties.GetIPGlobalProperties().DomainName; UInt64 fractPart = BitConverter.ToUInt32(ntpData, serverReplyTime + 4);
#region IP Addresses and Adapters Information Methods // Convert From big-endian to little-endian to match the platform
if(BitConverter.IsLittleEndian) {
/// <summary> intPart = intPart.SwapEndianness();
/// Gets the active IPv4 interfaces. fractPart = intPart.SwapEndianness();
/// Only those interfaces with a valid unicast address and a valid gateway will be returned in the collection. }
/// </summary>
/// <returns> UInt64 milliseconds = intPart * 1000 + fractPart * 1000 / 0x100000000L;
/// A collection of NetworkInterface/IPInterfaceProperties pairs
/// that represents the active IPv4 interfaces. // The time is given in UTC
/// </returns> return new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds((Int64)milliseconds);
public static Dictionary<NetworkInterface, IPInterfaceProperties> GetIPv4Interfaces() }
{
// zero conf ip address /// <summary>
var zeroConf = new IPAddress(0); /// Gets the UTC time by querying from an NTP server.
/// </summary>
var adapters = NetworkInterface.GetAllNetworkInterfaces() /// <param name="ntpServerName">The NTP server, by default pool.ntp.org.</param>
.Where(network => /// <param name="port">The port, by default NTP 123.</param>
network.OperationalStatus == OperationalStatus.Up /// <returns>The UTC time by querying from an NTP server of the result produced by this Task.</returns>
&& network.NetworkInterfaceType != NetworkInterfaceType.Unknown public static async Task<DateTime> GetNetworkTimeUtcAsync(String ntpServerName = "pool.ntp.org", Int32 port = NtpDefaultPort) {
&& network.NetworkInterfaceType != NetworkInterfaceType.Loopback) IPAddress[] addresses = await GetDnsHostEntryAsync(ntpServerName).ConfigureAwait(false);
.ToArray(); return await GetNetworkTimeUtcAsync(addresses.First(), port).ConfigureAwait(false);
}
var result = new Dictionary<NetworkInterface, IPInterfaceProperties>();
#endregion
foreach (var adapter in adapters) }
{
var properties = adapter.GetIPProperties();
if (properties == null
|| properties.GatewayAddresses.Count == 0
|| properties.GatewayAddresses.All(gateway => Equals(gateway.Address, zeroConf))
|| properties.UnicastAddresses.Count == 0
|| properties.GatewayAddresses.All(address => Equals(address.Address, zeroConf))
|| properties.UnicastAddresses.Any(a => a.Address.AddressFamily == AddressFamily.InterNetwork) ==
false)
continue;
result[adapter] = properties;
}
return result;
}
/// <summary>
/// Retrieves the local ip addresses.
/// </summary>
/// <param name="includeLoopback">if set to <c>true</c> [include loopback].</param>
/// <returns>An array of local ip addresses.</returns>
public static IPAddress[] GetIPv4Addresses(bool includeLoopback = true) =>
GetIPv4Addresses(NetworkInterfaceType.Unknown, true, includeLoopback);
/// <summary>
/// Retrieves the local ip addresses.
/// </summary>
/// <param name="interfaceType">Type of the interface.</param>
/// <param name="skipTypeFilter">if set to <c>true</c> [skip type filter].</param>
/// <param name="includeLoopback">if set to <c>true</c> [include loopback].</param>
/// <returns>An array of local ip addresses.</returns>
public static IPAddress[] GetIPv4Addresses(
NetworkInterfaceType interfaceType,
bool skipTypeFilter = false,
bool includeLoopback = false)
{
var addressList = new List<IPAddress>();
var interfaces = NetworkInterface.GetAllNetworkInterfaces()
.Where(ni =>
#if NET461
ni.IsReceiveOnly == false &&
#endif
(skipTypeFilter || ni.NetworkInterfaceType == interfaceType) &&
ni.OperationalStatus == OperationalStatus.Up)
.ToArray();
foreach (var networkInterface in interfaces)
{
var properties = networkInterface.GetIPProperties();
if (properties.GatewayAddresses.All(g => g.Address.AddressFamily != AddressFamily.InterNetwork))
continue;
addressList.AddRange(properties.UnicastAddresses
.Where(i => i.Address.AddressFamily == AddressFamily.InterNetwork)
.Select(i => i.Address));
}
if (includeLoopback || interfaceType == NetworkInterfaceType.Loopback)
addressList.Add(IPAddress.Loopback);
return addressList.ToArray();
}
/// <summary>
/// Gets the public IP address using ipify.org.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A public IP address of the result produced by this Task.</returns>
public static async Task<IPAddress> GetPublicIPAddressAsync(CancellationToken cancellationToken = default)
{
using var client = new HttpClient();
var response = await client.GetAsync("https://api.ipify.org", cancellationToken).ConfigureAwait(false);
return IPAddress.Parse(await response.Content.ReadAsStringAsync().ConfigureAwait(false));
}
/// <summary>
/// Gets the configured IPv4 DNS servers for the active network interfaces.
/// </summary>
/// <returns>
/// A collection of NetworkInterface/IPInterfaceProperties pairs
/// that represents the active IPv4 interfaces.
/// </returns>
public static IPAddress[] GetIPv4DnsServers()
=> GetIPv4Interfaces()
.Select(a => a.Value.DnsAddresses.Where(d => d.AddressFamily == AddressFamily.InterNetwork))
.SelectMany(d => d)
.ToArray();
#endregion
#region DNS and NTP Clients
/// <summary>
/// Gets the DNS host entry (a list of IP addresses) for the domain name.
/// </summary>
/// <param name="fqdn">The FQDN.</param>
/// <returns>An array of local ip addresses of the result produced by this task.</returns>
public static Task<IPAddress[]> GetDnsHostEntryAsync(string fqdn)
{
var dnsServer = GetIPv4DnsServers().FirstOrDefault() ?? IPAddress.Parse("8.8.8.8");
return GetDnsHostEntryAsync(fqdn, dnsServer, DnsDefaultPort);
}
/// <summary>
/// Gets the DNS host entry (a list of IP addresses) for the domain name.
/// </summary>
/// <param name="fqdn">The FQDN.</param>
/// <param name="dnsServer">The DNS server.</param>
/// <param name="port">The port.</param>
/// <returns>
/// An array of local ip addresses of the result produced by this task.
/// </returns>
/// <exception cref="ArgumentNullException">fqdn.</exception>
public static async Task<IPAddress[]> GetDnsHostEntryAsync(string fqdn, IPAddress dnsServer, int port)
{
if (fqdn == null)
throw new ArgumentNullException(nameof(fqdn));
if (fqdn.IndexOf(".", StringComparison.Ordinal) == -1)
{
fqdn += "." + IPGlobalProperties.GetIPGlobalProperties().DomainName;
}
while (true)
{
if (!fqdn.EndsWith(".", StringComparison.OrdinalIgnoreCase)) break;
fqdn = fqdn.Substring(0, fqdn.Length - 1);
}
var client = new DnsClient(dnsServer, port);
var result = await client.Lookup(fqdn).ConfigureAwait(false);
return result.ToArray();
}
/// <summary>
/// Gets the reverse lookup FQDN of the given IP Address.
/// </summary>
/// <param name="query">The query.</param>
/// <param name="dnsServer">The DNS server.</param>
/// <param name="port">The port.</param>
/// <returns>A <see cref="System.String" /> that represents the current object.</returns>
public static Task<string> GetDnsPointerEntryAsync(IPAddress query, IPAddress dnsServer, int port)
{
var client = new DnsClient(dnsServer, port);
return client.Reverse(query);
}
/// <summary>
/// Gets the reverse lookup FQDN of the given IP Address.
/// </summary>
/// <param name="query">The query.</param>
/// <returns>A <see cref="System.String" /> that represents the current object.</returns>
public static Task<string> GetDnsPointerEntryAsync(IPAddress query)
{
var client = new DnsClient(GetIPv4DnsServers().FirstOrDefault());
return client.Reverse(query);
}
/// <summary>
/// Queries the DNS server for the specified record type.
/// </summary>
/// <param name="query">The query.</param>
/// <param name="recordType">Type of the record.</param>
/// <param name="dnsServer">The DNS server.</param>
/// <param name="port">The port.</param>
/// <returns>Queries the DNS server for the specified record type of the result produced by this Task.</returns>
public static async Task<DnsQueryResult> QueryDnsAsync(string query, DnsRecordType recordType, IPAddress dnsServer, int port)
{
if (query == null)
throw new ArgumentNullException(nameof(query));
var client = new DnsClient(dnsServer, port);
var response = await client.Resolve(query, recordType).ConfigureAwait(false);
return new DnsQueryResult(response);
}
/// <summary>
/// Queries the DNS server for the specified record type.
/// </summary>
/// <param name="query">The query.</param>
/// <param name="recordType">Type of the record.</param>
/// <returns>Queries the DNS server for the specified record type of the result produced by this Task.</returns>
public static Task<DnsQueryResult> QueryDnsAsync(string query, DnsRecordType recordType) => QueryDnsAsync(query, recordType, GetIPv4DnsServers().FirstOrDefault(), DnsDefaultPort);
/// <summary>
/// Gets the UTC time by querying from an NTP server.
/// </summary>
/// <param name="ntpServerAddress">The NTP server address.</param>
/// <param name="port">The port.</param>
/// <returns>The UTC time by querying from an NTP server of the result produced by this Task.</returns>
public static async Task<DateTime> GetNetworkTimeUtcAsync(IPAddress ntpServerAddress, int port = NtpDefaultPort)
{
if (ntpServerAddress == null)
throw new ArgumentNullException(nameof(ntpServerAddress));
// NTP message size - 16 bytes of the digest (RFC 2030)
var ntpData = new byte[48];
// Setting the Leap Indicator, Version Number and Mode values
ntpData[0] = 0x1B; // LI = 0 (no warning), VN = 3 (IPv4 only), Mode = 3 (Client Mode)
// The UDP port number assigned to NTP is 123
var endPoint = new IPEndPoint(ntpServerAddress, port);
var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
#if !NET461
await socket.ConnectAsync(endPoint).ConfigureAwait(false);
#else
socket.Connect(endPoint);
#endif
socket.ReceiveTimeout = 3000; // Stops code hang if NTP is blocked
socket.Send(ntpData);
socket.Receive(ntpData);
socket.Dispose();
// Offset to get to the "Transmit Timestamp" field (time at which the reply
// departed the server for the client, in 64-bit timestamp format."
const byte serverReplyTime = 40;
// Get the seconds part
ulong intPart = BitConverter.ToUInt32(ntpData, serverReplyTime);
// Get the seconds fraction
ulong fractPart = BitConverter.ToUInt32(ntpData, serverReplyTime + 4);
// Convert From big-endian to little-endian to match the platform
if (BitConverter.IsLittleEndian)
{
intPart = intPart.SwapEndianness();
fractPart = intPart.SwapEndianness();
}
var milliseconds = (intPart * 1000) + ((fractPart * 1000) / 0x100000000L);
// The time is given in UTC
return new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds((long) milliseconds);
}
/// <summary>
/// Gets the UTC time by querying from an NTP server.
/// </summary>
/// <param name="ntpServerName">The NTP server, by default pool.ntp.org.</param>
/// <param name="port">The port, by default NTP 123.</param>
/// <returns>The UTC time by querying from an NTP server of the result produced by this Task.</returns>
public static async Task<DateTime> GetNetworkTimeUtcAsync(string ntpServerName = "pool.ntp.org",
int port = NtpDefaultPort)
{
var addresses = await GetDnsHostEntryAsync(ntpServerName).ConfigureAwait(false);
return await GetNetworkTimeUtcAsync(addresses.First(), port).ConfigureAwait(false);
}
#endregion
}
} }

View File

@ -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> /// <summary>
/// The unknown category /// The quit command
/// </summary> /// </summary>
Unknown = -1, QUIT,
/// <summary> /// <summary>
/// The syntax category /// The help command
/// </summary> /// </summary>
Syntax = 0, HELP,
/// <summary> /// <summary>
/// The information category /// The noop command
/// </summary> /// </summary>
Information = 1, NOOP,
/// <summary> /// <summary>
/// The connections category /// The rset command
/// </summary> /// </summary>
Connections = 2, RSET,
/// <summary> /// <summary>
/// The unspecified a category /// The mail command
/// </summary> /// </summary>
UnspecifiedA = 3, MAIL,
/// <summary> /// <summary>
/// The unspecified b category /// The data command
/// </summary> /// </summary>
UnspecifiedB = 4, DATA,
/// <summary> /// <summary>
/// The system category /// The send command
/// </summary> /// </summary>
System = 5, 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,
}
} }

View File

@ -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> /// <summary>
/// Represents a basic SMTP client that is capable of submitting messages to an SMTP server. /// Initializes a new instance of the <see cref="SmtpClient" /> class.
/// </summary> /// </summary>
/// <example> /// <param name="host">The host.</param>
/// The following code explains how to send a simple e-mail. /// <param name="port">The port.</param>
/// <code> /// <exception cref="ArgumentNullException">host.</exception>
/// using System.Net.Mail; public SmtpClient(String host, Int32 port) {
/// this.Host = host ?? throw new ArgumentNullException(nameof(host));
/// class Example this.Port = port;
/// { this.ClientHostname = Network.HostName;
/// static void Main() }
/// {
/// // create a new smtp client using google's smtp server /// <summary>
/// var client = new Swan.Net.Smtp.SmtpClient("smtp.gmail.com", 587); /// Gets or sets the credentials. No credentials will be used if set to null.
/// /// </summary>
/// // send an email /// <value>
/// client.SendMailAsync( /// The credentials.
/// new MailMessage("sender@test.com", "recipient@test.cm", "Subject", "Body")); /// </value>
/// } public NetworkCredential? Credentials {
/// } get; set;
/// </code> }
///
/// The following code demonstrates how to sent an e-mail using a SmtpSessionState: /// <summary>
/// <code> /// Gets the host.
/// using Swan.Net.Smtp; /// </summary>
/// /// <value>
/// class Example /// The host.
/// { /// </value>
/// static void Main() public String Host {
/// { get;
/// // create a new smtp client using google's smtp server }
/// var client = new SmtpClient("smtp.gmail.com", 587);
/// /// <summary>
/// // create a new session state with a sender address /// Gets the port.
/// var session = new SmtpSessionState { SenderAddress = "sender@test.com" }; /// </summary>
/// /// <value>
/// // add a recipient /// The port.
/// session.Recipients.Add("recipient@test.cm"); /// </value>
/// public Int32 Port {
/// // send get;
/// client.SendMailAsync(session); }
/// }
/// } /// <summary>
/// </code> /// Gets or sets a value indicating whether the SSL is enabled.
/// /// If set to false, communication between client and server will not be secured.
/// The following code shows how to send an e-mail with an attachment using MimeKit: /// </summary>
/// <code> /// <value>
/// using MimeKit; /// <c>true</c> if [enable SSL]; otherwise, <c>false</c>.
/// using Swan.Net.Smtp; /// </value>
/// public Boolean EnableSsl {
/// class Example get; set;
/// { }
/// static void Main()
/// { /// <summary>
/// // create a new smtp client using google's smtp server /// Gets or sets the name of the client that gets announced to the server.
/// var client = new SmtpClient("smtp.gmail.com", 587); /// </summary>
/// /// <value>
/// // create a new session state with a sender address /// The client hostname.
/// var session = new SmtpSessionState { SenderAddress = "sender@test.com" }; /// </value>
/// public String ClientHostname {
/// // add a recipient get; set;
/// session.Recipients.Add("recipient@test.cm"); }
///
/// // load a file as an attachment
/// var attachment = new MimePart("image", "gif") /// <summary>
/// { /// Sends an email message asynchronously.
/// Content = new /// </summary>
/// MimeContent(File.OpenRead("meme.gif"), ContentEncoding.Default), /// <param name="message">The message.</param>
/// ContentDisposition = /// <param name="sessionId">The session identifier.</param>
/// new ContentDisposition(ContentDisposition.Attachment), /// <param name="callback">The callback.</param>
/// ContentTransferEncoding = ContentEncoding.Base64, /// <param name="cancellationToken">The cancellation token.</param>
/// FileName = Path.GetFileName("meme.gif") /// <returns>
/// }; /// A task that represents the asynchronous of send email operation.
/// /// </returns>
/// // send /// <exception cref="ArgumentNullException">message.</exception>
/// client.SendMailAsync(session); [System.Diagnostics.CodeAnalysis.SuppressMessage("Codequalität", "IDE0067:Objekte verwerfen, bevor Bereich verloren geht", Justification = "<Ausstehend>")]
/// } public Task SendMailAsync(MailMessage message, String? sessionId = null, RemoteCertificateValidationCallback? callback = null, CancellationToken cancellationToken = default) {
/// } if(message == null) {
/// </code> throw new ArgumentNullException(nameof(message));
/// </example> }
public class SmtpClient
{ SmtpSessionState state = new SmtpSessionState {
/// <summary> AuthMode = this.Credentials == null ? String.Empty : SmtpDefinitions.SmtpAuthMethods.Login,
/// Initializes a new instance of the <see cref="SmtpClient" /> class. ClientHostname = ClientHostname,
/// </summary> IsChannelSecure = EnableSsl,
/// <param name="host">The host.</param> SenderAddress = message.From.Address,
/// <param name="port">The port.</param> };
/// <exception cref="ArgumentNullException">host.</exception>
public SmtpClient(string host, int port) if(this.Credentials != null) {
{ state.Username = this.Credentials.UserName;
Host = host ?? throw new ArgumentNullException(nameof(host)); state.Password = this.Credentials.Password;
Port = port; }
ClientHostname = Network.HostName;
} foreach(MailAddress recipient in message.To) {
state.Recipients.Add(recipient.Address);
/// <summary> }
/// Gets or sets the credentials. No credentials will be used if set to null.
/// </summary> state.DataBuffer.AddRange(message.ToMimeMessage().ToArray());
/// <value>
/// The credentials. return this.SendMailAsync(state, sessionId, callback, cancellationToken);
/// </value> }
public NetworkCredential Credentials { get; set; }
/// <summary>
/// <summary> /// Sends an email message using a session state object.
/// Gets the host. /// Credentials, Enable SSL and Client Hostname are NOT taken from the state object but
/// </summary> /// rather from the properties of this class.
/// <value> /// </summary>
/// The host. /// <param name="sessionState">The state.</param>
/// </value> /// <param name="sessionId">The session identifier.</param>
public string Host { get; } /// <param name="callback">The callback.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <summary> /// <returns>
/// Gets the port. /// A task that represents the asynchronous of send email operation.
/// </summary> /// </returns>
/// <value> /// <exception cref="ArgumentNullException">sessionState.</exception>
/// The port. public Task SendMailAsync(SmtpSessionState sessionState, String? sessionId = null, RemoteCertificateValidationCallback? callback = null, CancellationToken cancellationToken = default) {
/// </value> if(sessionState == null) {
public int Port { get; } throw new ArgumentNullException(nameof(sessionState));
}
/// <summary>
/// Gets or sets a value indicating whether the SSL is enabled. return this.SendMailAsync(new[] { sessionState }, sessionId, callback, cancellationToken);
/// If set to false, communication between client and server will not be secured. }
/// </summary>
/// <value> /// <summary>
/// <c>true</c> if [enable SSL]; otherwise, <c>false</c>. /// Sends an array of email messages using a session state object.
/// </value> /// Credentials, Enable SSL and Client Hostname are NOT taken from the state object but
public bool EnableSsl { get; set; } /// rather from the properties of this class.
/// </summary>
/// <summary> /// <param name="sessionStates">The session states.</param>
/// Gets or sets the name of the client that gets announced to the server. /// <param name="sessionId">The session identifier.</param>
/// </summary> /// <param name="callback">The callback.</param>
/// <value> /// <param name="cancellationToken">The cancellation token.</param>
/// The client hostname. /// <returns>
/// </value> /// A task that represents the asynchronous of send email operation.
public string ClientHostname { get; set; } /// </returns>
/// <exception cref="ArgumentNullException">sessionStates.</exception>
/// <summary> /// <exception cref="SecurityException">Could not upgrade the channel to SSL.</exception>
/// Sends an email message asynchronously. /// <exception cref="SmtpException">Defines an SMTP Exceptions class.</exception>
/// </summary> public async Task SendMailAsync(IEnumerable<SmtpSessionState> sessionStates, String? sessionId = null, RemoteCertificateValidationCallback? callback = null, CancellationToken cancellationToken = default) {
/// <param name="message">The message.</param> if(sessionStates == null) {
/// <param name="sessionId">The session identifier.</param> throw new ArgumentNullException(nameof(sessionStates));
/// <param name="callback">The callback.</param> }
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns> using TcpClient tcpClient = new TcpClient();
/// A task that represents the asynchronous of send email operation. await tcpClient.ConnectAsync(this.Host, this.Port).ConfigureAwait(false);
/// </returns>
/// <exception cref="ArgumentNullException">message.</exception> using Connection connection = new Connection(tcpClient, Encoding.UTF8, "\r\n", true, 1000);
public Task SendMailAsync( SmtpSender sender = new SmtpSender(sessionId);
MailMessage message,
string? sessionId = null, try {
RemoteCertificateValidationCallback? callback = null, // Read the greeting message
CancellationToken cancellationToken = default) sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false);
{
if (message == null) // EHLO 1
throw new ArgumentNullException(nameof(message)); await this.SendEhlo(sender, connection, cancellationToken).ConfigureAwait(false);
var state = new SmtpSessionState // STARTTLS
{ if(this.EnableSsl) {
AuthMode = Credentials == null ? string.Empty : SmtpDefinitions.SmtpAuthMethods.Login, sender.RequestText = $"{SmtpCommandNames.STARTTLS}";
ClientHostname = ClientHostname,
IsChannelSecure = EnableSsl, await connection.WriteLineAsync(sender.RequestText, cancellationToken).ConfigureAwait(false);
SenderAddress = message.From.Address, sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false);
}; sender.ValidateReply();
if (Credentials != null) if(await connection.UpgradeToSecureAsClientAsync(callback: callback).ConfigureAwait(false) == false) {
{ throw new SecurityException("Could not upgrade the channel to SSL.");
state.Username = Credentials.UserName; }
state.Password = Credentials.Password; }
}
// EHLO 2
foreach (var recipient in message.To) await this.SendEhlo(sender, connection, cancellationToken).ConfigureAwait(false);
{
state.Recipients.Add(recipient.Address); // AUTH
} if(this.Credentials != null) {
ConnectionAuth auth = new ConnectionAuth(connection, sender, this.Credentials);
state.DataBuffer.AddRange(message.ToMimeMessage().ToArray()); await auth.AuthenticateAsync(cancellationToken).ConfigureAwait(false);
}
return SendMailAsync(state, sessionId, callback, cancellationToken);
} foreach(SmtpSessionState sessionState in sessionStates) {
{
/// <summary> // MAIL FROM
/// Sends an email message using a session state object. sender.RequestText = $"{SmtpCommandNames.MAIL} FROM:<{sessionState.SenderAddress}>";
/// Credentials, Enable SSL and Client Hostname are NOT taken from the state object but
/// rather from the properties of this class. await connection.WriteLineAsync(sender.RequestText, cancellationToken).ConfigureAwait(false);
/// </summary> sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false);
/// <param name="sessionState">The state.</param> sender.ValidateReply();
/// <param name="sessionId">The session identifier.</param> }
/// <param name="callback">The callback.</param>
/// <param name="cancellationToken">The cancellation token.</param> // RCPT TO
/// <returns> foreach(String recipient in sessionState.Recipients) {
/// A task that represents the asynchronous of send email operation. sender.RequestText = $"{SmtpCommandNames.RCPT} TO:<{recipient}>";
/// </returns>
/// <exception cref="ArgumentNullException">sessionState.</exception> await connection.WriteLineAsync(sender.RequestText, cancellationToken).ConfigureAwait(false);
public Task SendMailAsync( sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false);
SmtpSessionState sessionState, sender.ValidateReply();
string? sessionId = null, }
RemoteCertificateValidationCallback? callback = null,
CancellationToken cancellationToken = default) {
{ // DATA
if (sessionState == null) sender.RequestText = $"{SmtpCommandNames.DATA}";
throw new ArgumentNullException(nameof(sessionState));
await connection.WriteLineAsync(sender.RequestText, cancellationToken).ConfigureAwait(false);
return SendMailAsync(new[] { sessionState }, sessionId, callback, cancellationToken); sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false);
} sender.ValidateReply();
}
/// <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 // CONTENT
/// rather from the properties of this class. String dataTerminator = sessionState.DataBuffer.Skip(sessionState.DataBuffer.Count - 5).ToText();
/// </summary>
/// <param name="sessionStates">The session states.</param> sender.RequestText = $"Buffer ({sessionState.DataBuffer.Count} bytes)";
/// <param name="sessionId">The session identifier.</param>
/// <param name="callback">The callback.</param> await connection.WriteDataAsync(sessionState.DataBuffer.ToArray(), true, cancellationToken).ConfigureAwait(false);
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns> if(!dataTerminator.EndsWith(SmtpDefinitions.SmtpDataCommandTerminator)) {
/// A task that represents the asynchronous of send email operation. await connection.WriteTextAsync(SmtpDefinitions.SmtpDataCommandTerminator, cancellationToken).ConfigureAwait(false);
/// </returns> }
/// <exception cref="ArgumentNullException">sessionStates.</exception>
/// <exception cref="SecurityException">Could not upgrade the channel to SSL.</exception> sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false);
/// <exception cref="SmtpException">Defines an SMTP Exceptions class.</exception> sender.ValidateReply();
public async Task SendMailAsync( }
IEnumerable<SmtpSessionState> sessionStates, }
string? sessionId = null,
RemoteCertificateValidationCallback? callback = null, {
CancellationToken cancellationToken = default) // QUIT
{ sender.RequestText = $"{SmtpCommandNames.QUIT}";
if (sessionStates == null)
throw new ArgumentNullException(nameof(sessionStates)); await connection.WriteLineAsync(sender.RequestText, cancellationToken).ConfigureAwait(false);
sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false);
using var tcpClient = new TcpClient(); sender.ValidateReply();
await tcpClient.ConnectAsync(Host, Port).ConfigureAwait(false); }
} catch(Exception ex) {
using var connection = new Connection(tcpClient, Encoding.UTF8, "\r\n", true, 1000); throw new SmtpException($"Could not send email - Session ID {sessionId}. {ex.Message}\r\n Last Request: {sender.RequestText}\r\n Last Reply: {sender.ReplyText}");
var sender = new SmtpSender(sessionId); }
}
try
{ private async Task SendEhlo(SmtpSender sender, Connection connection, CancellationToken cancellationToken) {
// Read the greeting message sender.RequestText = $"{SmtpCommandNames.EHLO} {this.ClientHostname}";
sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false);
await connection.WriteLineAsync(sender.RequestText, cancellationToken).ConfigureAwait(false);
// EHLO 1
await SendEhlo(sender, connection, cancellationToken).ConfigureAwait(false); do {
sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false);
// STARTTLS }
if (EnableSsl) while(!sender.IsReplyOk);
{
sender.RequestText = $"{SmtpCommandNames.STARTTLS}"; sender.ValidateReply();
}
await connection.WriteLineAsync(sender.RequestText, cancellationToken).ConfigureAwait(false);
sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false); private class ConnectionAuth {
sender.ValidateReply(); private readonly SmtpSender _sender;
private readonly Connection _connection;
if (await connection.UpgradeToSecureAsClientAsync(callback: callback).ConfigureAwait(false) == false) private readonly NetworkCredential _credentials;
throw new SecurityException("Could not upgrade the channel to SSL.");
} public ConnectionAuth(Connection connection, SmtpSender sender, NetworkCredential credentials) {
this._connection = connection;
// EHLO 2 this._sender = sender;
await SendEhlo(sender, connection, cancellationToken).ConfigureAwait(false); this._credentials = credentials;
}
// AUTH
if (Credentials != null) public async Task AuthenticateAsync(CancellationToken ct) {
{ this._sender.RequestText = $"{SmtpCommandNames.AUTH} {SmtpDefinitions.SmtpAuthMethods.Login} {Convert.ToBase64String(Encoding.UTF8.GetBytes(this._credentials.UserName))}";
var auth = new ConnectionAuth(connection, sender, Credentials);
await auth.AuthenticateAsync(cancellationToken).ConfigureAwait(false); await this._connection.WriteLineAsync(this._sender.RequestText, ct).ConfigureAwait(false);
} this._sender.ReplyText = await this._connection.ReadLineAsync(ct).ConfigureAwait(false);
this._sender.ValidateReply();
foreach (var sessionState in sessionStates) this._sender.RequestText = Convert.ToBase64String(Encoding.UTF8.GetBytes(this._credentials.Password));
{
{ await this._connection.WriteLineAsync(this._sender.RequestText, ct).ConfigureAwait(false);
// MAIL FROM this._sender.ReplyText = await this._connection.ReadLineAsync(ct).ConfigureAwait(false);
sender.RequestText = $"{SmtpCommandNames.MAIL} FROM:<{sessionState.SenderAddress}>"; this._sender.ValidateReply();
}
await connection.WriteLineAsync(sender.RequestText, cancellationToken).ConfigureAwait(false); }
sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false); }
sender.ValidateReply();
}
// RCPT TO
foreach (var recipient in sessionState.Recipients)
{
sender.RequestText = $"{SmtpCommandNames.RCPT} TO:<{recipient}>";
await connection.WriteLineAsync(sender.RequestText, cancellationToken).ConfigureAwait(false);
sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false);
sender.ValidateReply();
}
{
// DATA
sender.RequestText = $"{SmtpCommandNames.DATA}";
await connection.WriteLineAsync(sender.RequestText, cancellationToken).ConfigureAwait(false);
sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false);
sender.ValidateReply();
}
{
// CONTENT
var dataTerminator = sessionState.DataBuffer
.Skip(sessionState.DataBuffer.Count - 5)
.ToText();
sender.RequestText = $"Buffer ({sessionState.DataBuffer.Count} bytes)";
await connection.WriteDataAsync(sessionState.DataBuffer.ToArray(), true, cancellationToken).ConfigureAwait(false);
if (!dataTerminator.EndsWith(SmtpDefinitions.SmtpDataCommandTerminator))
await connection.WriteTextAsync(SmtpDefinitions.SmtpDataCommandTerminator, cancellationToken).ConfigureAwait(false);
sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false);
sender.ValidateReply();
}
}
{
// QUIT
sender.RequestText = $"{SmtpCommandNames.QUIT}";
await connection.WriteLineAsync(sender.RequestText, cancellationToken).ConfigureAwait(false);
sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false);
sender.ValidateReply();
}
}
catch (Exception ex)
{
throw new SmtpException($"Could not send email - Session ID {sessionId}. {ex.Message}\r\n Last Request: {sender.RequestText}\r\n Last Reply: {sender.ReplyText}");
}
}
private async Task SendEhlo(SmtpSender sender, Connection connection, CancellationToken cancellationToken)
{
sender.RequestText = $"{SmtpCommandNames.EHLO} {ClientHostname}";
await connection.WriteLineAsync(sender.RequestText, cancellationToken).ConfigureAwait(false);
do
{
sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false);
}
while (!sender.IsReplyOk);
sender.ValidateReply();
}
private class ConnectionAuth
{
private readonly SmtpSender _sender;
private readonly Connection _connection;
private readonly NetworkCredential _credentials;
public ConnectionAuth(Connection connection, SmtpSender sender, NetworkCredential credentials)
{
_connection = connection;
_sender = sender;
_credentials = credentials;
}
public async Task AuthenticateAsync(CancellationToken ct)
{
_sender.RequestText =
$"{SmtpCommandNames.AUTH} {SmtpDefinitions.SmtpAuthMethods.Login} {Convert.ToBase64String(Encoding.UTF8.GetBytes(_credentials.UserName))}";
await _connection.WriteLineAsync(_sender.RequestText, ct).ConfigureAwait(false);
_sender.ReplyText = await _connection.ReadLineAsync(ct).ConfigureAwait(false);
_sender.ValidateReply();
_sender.RequestText = Convert.ToBase64String(Encoding.UTF8.GetBytes(_credentials.Password));
await _connection.WriteLineAsync(_sender.RequestText, ct).ConfigureAwait(false);
_sender.ReplyText = await _connection.ReadLineAsync(ct).ConfigureAwait(false);
_sender.ValidateReply();
}
}
}
} }

View File

@ -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> /// <summary>
/// The string sequence that delimits the end of the DATA command. /// Lists the AUTH methods supported by default.
/// </summary> /// </summary>
public const string SmtpDataCommandTerminator = "\r\n.\r\n"; public static class SmtpAuthMethods {
/// <summary>
/// <summary> /// The plain method.
/// Lists the AUTH methods supported by default. /// </summary>
/// </summary> public const String Plain = "PLAIN";
public static class SmtpAuthMethods
{ /// <summary>
/// <summary> /// The login method.
/// The plain method. /// </summary>
/// </summary> public const String Login = "LOGIN";
public const string Plain = "PLAIN"; }
}
/// <summary>
/// The login method.
/// </summary>
public const string Login = "LOGIN";
}
}
} }

View File

@ -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; namespace Swan.Net.Smtp {
/// <summary>
/// <summary> /// Use this class to store the sender session data.
/// Use this class to store the sender session data. /// </summary>
/// </summary> internal class SmtpSender {
internal class SmtpSender private readonly String _sessionId;
{ private String _requestText;
private readonly string _sessionId;
private string _requestText; public SmtpSender(String sessionId) => this._sessionId = sessionId;
public SmtpSender(string sessionId) public String RequestText {
{ get => this._requestText;
_sessionId = sessionId; set {
} this._requestText = value;
$" TX {this._requestText}".Trace(typeof(SmtpClient), this._sessionId);
public string RequestText }
{ }
get => _requestText;
set public String ReplyText {
{ get; set;
_requestText = value; }
$" TX {_requestText}".Trace(typeof(SmtpClient), _sessionId);
} public Boolean IsReplyOk => this.ReplyText.StartsWith("250 ", StringComparison.OrdinalIgnoreCase);
}
public void ValidateReply() {
public string ReplyText { get; set; } if(this.ReplyText == null) {
throw new SmtpException("There was no response from the server");
public bool IsReplyOk => ReplyText.StartsWith("250 ", StringComparison.OrdinalIgnoreCase); }
public void ValidateReply() try {
{ SmtpServerReply response = SmtpServerReply.Parse(this.ReplyText);
if (ReplyText == null) $" RX {this.ReplyText} - {response.IsPositive}".Trace(typeof(SmtpClient), this._sessionId);
throw new SmtpException("There was no response from the server");
if(response.IsPositive) {
try return;
{ }
var response = SmtpServerReply.Parse(ReplyText);
$" RX {ReplyText} - {response.IsPositive}".Trace(typeof(SmtpClient), _sessionId); String responseContent = response.Content.Any() ? String.Join(";", response.Content.ToArray()) : String.Empty;
if (response.IsPositive) return; throw new SmtpException((SmtpStatusCode)response.ReplyCode, responseContent);
} catch(Exception ex) {
var responseContent = response.Content.Any() if(!(ex is SmtpException)) {
? string.Join(";", response.Content.ToArray()) throw new SmtpException($"Could not parse server response: {this.ReplyText}");
: 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}");
}
}
}
} }

View File

@ -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) {
/// <summary> this.Content = new List<String>();
/// Initializes a new instance of the <see cref="SmtpServerReply"/> class. this.ReplyCode = responseCode;
/// </summary> this.EnhancedStatusCode = statusCode;
/// <param name="responseCode">The response code.</param> this.Content.AddRange(content);
/// <param name="statusCode">The status code.</param> this.IsValid = responseCode >= 200 && responseCode < 600;
/// <param name="content">The content.</param> this.ReplyCodeSeverity = SmtpReplyCodeSeverities.Unknown;
public SmtpServerReply(int responseCode, string statusCode, params string[] content) this.ReplyCodeCategory = SmtpReplyCodeCategories.Unknown;
{
Content = new List<string>(); if(!this.IsValid) {
ReplyCode = responseCode; return;
EnhancedStatusCode = statusCode; }
Content.AddRange(content);
IsValid = responseCode >= 200 && responseCode < 600; if(responseCode >= 200) {
ReplyCodeSeverity = SmtpReplyCodeSeverities.Unknown; this.ReplyCodeSeverity = SmtpReplyCodeSeverities.PositiveCompletion;
ReplyCodeCategory = SmtpReplyCodeCategories.Unknown; }
if (!IsValid) return; if(responseCode >= 300) {
if (responseCode >= 200) ReplyCodeSeverity = SmtpReplyCodeSeverities.PositiveCompletion; this.ReplyCodeSeverity = SmtpReplyCodeSeverities.PositiveIntermediate;
if (responseCode >= 300) ReplyCodeSeverity = SmtpReplyCodeSeverities.PositiveIntermediate; }
if (responseCode >= 400) ReplyCodeSeverity = SmtpReplyCodeSeverities.TransientNegative;
if (responseCode >= 500) ReplyCodeSeverity = SmtpReplyCodeSeverities.PermanentNegative; if(responseCode >= 400) {
if (responseCode >= 600) ReplyCodeSeverity = SmtpReplyCodeSeverities.Unknown; this.ReplyCodeSeverity = SmtpReplyCodeSeverities.TransientNegative;
}
if (int.TryParse(responseCode.ToString(CultureInfo.InvariantCulture).Substring(1, 1), out var middleDigit))
{ if(responseCode >= 500) {
if (middleDigit >= 0 && middleDigit <= 5) this.ReplyCodeSeverity = SmtpReplyCodeSeverities.PermanentNegative;
ReplyCodeCategory = (SmtpReplyCodeCategories) middleDigit; }
}
} if(responseCode >= 600) {
this.ReplyCodeSeverity = SmtpReplyCodeSeverities.Unknown;
/// <summary> }
/// Initializes a new instance of the <see cref="SmtpServerReply"/> class.
/// </summary> if(Int32.TryParse(responseCode.ToString(CultureInfo.InvariantCulture).Substring(1, 1), out Int32 middleDigit)) {
public SmtpServerReply() if(middleDigit >= 0 && middleDigit <= 5) {
: this(0, string.Empty, string.Empty) this.ReplyCodeCategory = (SmtpReplyCodeCategories)middleDigit;
{ }
// placeholder }
} }
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="SmtpServerReply"/> class. /// Initializes a new instance of the <see cref="SmtpServerReply"/> class.
/// </summary> /// </summary>
/// <param name="responseCode">The response code.</param> public SmtpServerReply() : this(0, String.Empty, String.Empty) {
/// <param name="statusCode">The status code.</param> // placeholder
/// <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>
/// <summary> /// <param name="statusCode">The status code.</param>
/// Initializes a new instance of the <see cref="SmtpServerReply"/> class. /// <param name="content">The content.</param>
/// </summary> public SmtpServerReply(Int32 responseCode, String statusCode, String content) : this(responseCode, statusCode, new[] { content }) {
/// <param name="responseCode">The response code.</param> }
/// <param name="content">The content.</param>
public SmtpServerReply(int responseCode, string content) /// <summary>
: this(responseCode, string.Empty, content) /// Initializes a new instance of the <see cref="SmtpServerReply"/> class.
{ /// </summary>
} /// <param name="responseCode">The response code.</param>
/// <param name="content">The content.</param>
#endregion public SmtpServerReply(Int32 responseCode, String content) : this(responseCode, String.Empty, content) {
}
#region Pre-built responses (https://tools.ietf.org/html/rfc5321#section-4.2.2)
#endregion
/// <summary>
/// Gets the command unrecognized reply. #region Pre-built responses (https://tools.ietf.org/html/rfc5321#section-4.2.2)
/// </summary>
public static SmtpServerReply CommandUnrecognized => /// <summary>
new SmtpServerReply(500, "Syntax error, command unrecognized"); /// Gets the command unrecognized reply.
/// </summary>
/// <summary> public static SmtpServerReply CommandUnrecognized => new SmtpServerReply(500, "Syntax error, command unrecognized");
/// Gets the syntax error arguments reply.
/// </summary> /// <summary>
public static SmtpServerReply SyntaxErrorArguments => /// Gets the syntax error arguments reply.
new SmtpServerReply(501, "Syntax error in parameters or arguments"); /// </summary>
public static SmtpServerReply SyntaxErrorArguments => new SmtpServerReply(501, "Syntax error in parameters or arguments");
/// <summary>
/// Gets the command not implemented reply. /// <summary>
/// </summary> /// Gets the command not implemented reply.
public static SmtpServerReply CommandNotImplemented => new SmtpServerReply(502, "Command not implemented"); /// </summary>
public static SmtpServerReply CommandNotImplemented => new SmtpServerReply(502, "Command not implemented");
/// <summary>
/// Gets the bad sequence of commands reply. /// <summary>
/// </summary> /// Gets the bad sequence of commands reply.
public static SmtpServerReply BadSequenceOfCommands => new SmtpServerReply(503, "Bad sequence of commands"); /// </summary>
public static SmtpServerReply BadSequenceOfCommands => new SmtpServerReply(503, "Bad sequence of commands");
/// <summary>
/// Gets the protocol violation reply. /// <summary>
/// </summary>= /// Gets the protocol violation reply.
public static SmtpServerReply ProtocolViolation => /// </summary>=
new SmtpServerReply(451, "Requested action aborted: error in processing"); public static SmtpServerReply ProtocolViolation => new SmtpServerReply(451, "Requested action aborted: error in processing");
/// <summary> /// <summary>
/// Gets the system status bye reply. /// Gets the system status bye reply.
/// </summary> /// </summary>
public static SmtpServerReply SystemStatusBye => public static SmtpServerReply SystemStatusBye => new SmtpServerReply(221, "Service closing transmission channel");
new SmtpServerReply(221, "Service closing transmission channel");
/// <summary>
/// <summary> /// Gets the system status help reply.
/// Gets the system status help reply. /// </summary>=
/// </summary>= public static SmtpServerReply SystemStatusHelp => new SmtpServerReply(221, "Refer to RFC 5321");
public static SmtpServerReply SystemStatusHelp => new SmtpServerReply(221, "Refer to RFC 5321");
/// <summary>
/// <summary> /// Gets the bad syntax command empty reply.
/// Gets the bad syntax command empty reply. /// </summary>
/// </summary> public static SmtpServerReply BadSyntaxCommandEmpty => new SmtpServerReply(400, "Error: bad syntax");
public static SmtpServerReply BadSyntaxCommandEmpty => new SmtpServerReply(400, "Error: bad syntax");
/// <summary>
/// <summary> /// Gets the OK reply.
/// Gets the OK reply. /// </summary>
/// </summary> public static SmtpServerReply Ok => new SmtpServerReply(250, "OK");
public static SmtpServerReply Ok => new SmtpServerReply(250, "OK");
/// <summary>
/// <summary> /// Gets the authorization required reply.
/// Gets the authorization required reply. /// </summary>
/// </summary> public static SmtpServerReply AuthorizationRequired => new SmtpServerReply(530, "Authorization Required");
public static SmtpServerReply AuthorizationRequired => new SmtpServerReply(530, "Authorization Required");
#endregion
#endregion
#region Properties
#region Properties
/// <summary>
/// <summary> /// Gets the response severity.
/// Gets the response severity. /// </summary>
/// </summary> public SmtpReplyCodeSeverities ReplyCodeSeverity {
public SmtpReplyCodeSeverities ReplyCodeSeverity { get; } get;
}
/// <summary>
/// Gets the response category. /// <summary>
/// </summary> /// Gets the response category.
public SmtpReplyCodeCategories ReplyCodeCategory { get; } /// </summary>
public SmtpReplyCodeCategories ReplyCodeCategory {
/// <summary> get;
/// Gets the numeric response code. }
/// </summary>
public int ReplyCode { get; } /// <summary>
/// Gets the numeric response code.
/// <summary> /// </summary>
/// Gets the enhanced status code. public Int32 ReplyCode {
/// </summary> get;
public string EnhancedStatusCode { get; } }
/// <summary> /// <summary>
/// Gets the content. /// Gets the enhanced status code.
/// </summary> /// </summary>
public List<string> Content { get; } public String EnhancedStatusCode {
get;
/// <summary> }
/// Returns true if the response code is between 200 and 599.
/// </summary> /// <summary>
public bool IsValid { get; } /// Gets the content.
/// </summary>
/// <summary> public List<String> Content {
/// Gets a value indicating whether this instance is positive. get;
/// </summary> }
public bool IsPositive => ReplyCode >= 200 && ReplyCode <= 399;
/// <summary>
#endregion /// Returns true if the response code is between 200 and 599.
/// </summary>
#region Methods public Boolean IsValid {
get;
/// <summary> }
/// Parses the specified text into a Server Reply for thorough analysis.
/// </summary> /// <summary>
/// <param name="text">The text.</param> /// Gets a value indicating whether this instance is positive.
/// <returns>A new instance of SMTP server response object.</returns> /// </summary>
public static SmtpServerReply Parse(string text) public Boolean IsPositive => this.ReplyCode >= 200 && this.ReplyCode <= 399;
{
var lines = text.Split(new[] {"\r\n"}, StringSplitOptions.RemoveEmptyEntries); #endregion
if (lines.Length == 0) return new SmtpServerReply();
#region Methods
var lastLineParts = lines.Last().Split(new[] {" "}, StringSplitOptions.RemoveEmptyEntries);
var enhancedStatusCode = string.Empty; /// <summary>
int.TryParse(lastLineParts[0], out var responseCode); /// Parses the specified text into a Server Reply for thorough analysis.
if (lastLineParts.Length > 1) /// </summary>
{ /// <param name="text">The text.</param>
if (lastLineParts[1].Split('.').Length == 3) /// <returns>A new instance of SMTP server response object.</returns>
enhancedStatusCode = lastLineParts[1]; public static SmtpServerReply Parse(String text) {
} String[] lines = text.Split(new[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries);
if(lines.Length == 0) {
var content = new List<string>(); return new SmtpServerReply();
}
for (var i = 0; i < lines.Length; i++)
{ String[] lastLineParts = lines.Last().Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries);
var splitChar = i == lines.Length - 1 ? " " : "-"; String enhancedStatusCode = String.Empty;
_ = Int32.TryParse(lastLineParts[0], out Int32 responseCode);
var lineParts = lines[i].Split(new[] {splitChar}, 2, StringSplitOptions.None); if(lastLineParts.Length > 1) {
var lineContent = lineParts.Last(); if(lastLineParts[1].Split('.').Length == 3) {
if (string.IsNullOrWhiteSpace(enhancedStatusCode) == false) enhancedStatusCode = lastLineParts[1];
lineContent = lineContent.Replace(enhancedStatusCode, string.Empty).Trim(); }
}
content.Add(lineContent);
} List<String> content = new List<String>();
return new SmtpServerReply(responseCode, enhancedStatusCode, content.ToArray()); for(Int32 i = 0; i < lines.Length; i++) {
} String splitChar = i == lines.Length - 1 ? " " : "-";
/// <summary> String[] lineParts = lines[i].Split(new[] { splitChar }, 2, StringSplitOptions.None);
/// Returns a <see cref="System.String" /> that represents this instance. String lineContent = lineParts.Last();
/// </summary> if(String.IsNullOrWhiteSpace(enhancedStatusCode) == false) {
/// <returns> lineContent = lineContent.Replace(enhancedStatusCode, String.Empty).Trim();
/// A <see cref="System.String" /> that represents this instance. }
/// </returns>
public override string ToString() content.Add(lineContent);
{ }
var responseCodeText = ReplyCode.ToString(CultureInfo.InvariantCulture);
var statusCodeText = string.IsNullOrWhiteSpace(EnhancedStatusCode) return new SmtpServerReply(responseCode, enhancedStatusCode, content.ToArray());
? string.Empty }
: $" {EnhancedStatusCode.Trim()}";
if (Content.Count == 0) return $"{responseCodeText}{statusCodeText}"; /// <summary>
/// Returns a <see cref="System.String" /> that represents this instance.
var builder = new StringBuilder(); /// </summary>
/// <returns>
for (var i = 0; i < Content.Count; i++) /// A <see cref="System.String" /> that represents this instance.
{ /// </returns>
var isLastLine = i == Content.Count - 1; public override String ToString() {
String responseCodeText = this.ReplyCode.ToString(CultureInfo.InvariantCulture);
builder.Append(isLastLine String statusCodeText = String.IsNullOrWhiteSpace(this.EnhancedStatusCode) ? String.Empty : $" {this.EnhancedStatusCode.Trim()}";
? $"{responseCodeText}{statusCodeText} {Content[i]}" if(this.Content.Count == 0) {
: $"{responseCodeText}-{Content[i]}\r\n"); return $"{responseCodeText}{statusCodeText}";
} }
return builder.ToString(); StringBuilder builder = new StringBuilder();
}
for(Int32 i = 0; i < this.Content.Count; i++) {
#endregion 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
}
} }

View File

@ -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> /// <summary>
/// Represents the state of an SMTP session associated with a client. /// Initializes a new instance of the <see cref="SmtpSessionState"/> class.
/// </summary> /// </summary>
public class SmtpSessionState public SmtpSessionState() {
{ this.DataBuffer = new List<Byte>();
/// <summary> this.Reset(true);
/// Initializes a new instance of the <see cref="SmtpSessionState"/> class. this.ResetAuthentication();
/// </summary> }
public SmtpSessionState()
{ #region Properties
DataBuffer = new List<byte>();
Reset(true); /// <summary>
ResetAuthentication(); /// Gets the contents of the data buffer.
} /// </summary>
public List<Byte> DataBuffer {
#region Properties get; protected set;
}
/// <summary>
/// Gets the contents of the data buffer. /// <summary>
/// </summary> /// Gets or sets a value indicating whether this instance has initiated.
public List<byte> DataBuffer { get; protected set; } /// </summary>
public Boolean HasInitiated {
/// <summary> get; set;
/// 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> /// </summary>
/// Gets or sets a value indicating whether the current session supports extensions. public Boolean SupportsExtensions {
/// </summary> get; set;
public bool SupportsExtensions { get; set; } }
/// <summary> /// <summary>
/// Gets or sets the client hostname. /// Gets or sets the client hostname.
/// </summary> /// </summary>
public string ClientHostname { get; set; } public String ClientHostname {
get; set;
/// <summary> }
/// Gets or sets a value indicating whether the session is currently receiving DATA.
/// </summary> /// <summary>
public bool IsInDataMode { get; set; } /// Gets or sets a value indicating whether the session is currently receiving DATA.
/// </summary>
/// <summary> public Boolean IsInDataMode {
/// Gets or sets the sender address. get; set;
/// </summary> }
public string SenderAddress { get; set; }
/// <summary>
/// <summary> /// Gets or sets the sender address.
/// Gets the recipients. /// </summary>
/// </summary> public String SenderAddress {
public List<string> Recipients { get; } = new List<string>(); get; set;
}
/// <summary>
/// Gets or sets the extended data supporting any additional field for storage by a responder implementation. /// <summary>
/// </summary> /// Gets the recipients.
public object ExtendedData { get; set; } /// </summary>
public List<String> Recipients { get; } = new List<String>();
#endregion
/// <summary>
#region AUTH State /// Gets or sets the extended data supporting any additional field for storage by a responder implementation.
/// </summary>
/// <summary> public Object ExtendedData {
/// Gets or sets a value indicating whether this instance is in authentication mode. get; set;
/// </summary> }
public bool IsInAuthMode { get; set; }
#endregion
/// <summary>
/// Gets or sets the username. #region AUTH State
/// </summary>
public string Username { get; set; } /// <summary>
/// Gets or sets a value indicating whether this instance is in authentication mode.
/// <summary> /// </summary>
/// Gets or sets the password. public Boolean IsInAuthMode {
/// </summary> get; set;
public string Password { get; set; } }
/// <summary> /// <summary>
/// Gets a value indicating whether this instance has provided username. /// Gets or sets the username.
/// </summary> /// </summary>
public bool HasProvidedUsername => string.IsNullOrWhiteSpace(Username) == false; public String Username {
get; set;
/// <summary> }
/// Gets or sets a value indicating whether this instance is authenticated.
/// </summary> /// <summary>
public bool IsAuthenticated { get; set; } /// Gets or sets the password.
/// </summary>
/// <summary> public String Password {
/// Gets or sets the authentication mode. get; set;
/// </summary> }
public string AuthMode { get; set; }
/// <summary>
/// <summary> /// Gets a value indicating whether this instance has provided username.
/// Gets or sets a value indicating whether this instance is channel secure. /// </summary>
/// </summary> public Boolean HasProvidedUsername => String.IsNullOrWhiteSpace(this.Username) == false;
public bool IsChannelSecure { get; set; }
/// <summary>
/// <summary> /// Gets or sets a value indicating whether this instance is authenticated.
/// Resets the authentication state. /// </summary>
/// </summary> public Boolean IsAuthenticated {
public void ResetAuthentication() get; set;
{ }
Username = string.Empty;
Password = string.Empty; /// <summary>
AuthMode = string.Empty; /// Gets or sets the authentication mode.
IsInAuthMode = false; /// </summary>
IsAuthenticated = false; public String AuthMode {
} get; set;
}
#endregion
/// <summary>
#region Methods /// Gets or sets a value indicating whether this instance is channel secure.
/// </summary>
/// <summary> public Boolean IsChannelSecure {
/// Resets the data mode to false, clears the recipients, the sender address and the data buffer. get; set;
/// </summary> }
public void ResetEmail()
{ /// <summary>
IsInDataMode = false; /// Resets the authentication state.
Recipients.Clear(); /// </summary>
SenderAddress = string.Empty; public void ResetAuthentication() {
DataBuffer.Clear(); this.Username = String.Empty;
} this.Password = String.Empty;
this.AuthMode = String.Empty;
/// <summary> this.IsInAuthMode = false;
/// Resets the state table entirely. this.IsAuthenticated = false;
/// </summary> }
/// <param name="clearExtensionData">if set to <c>true</c> [clear extension data].</param>
public void Reset(bool clearExtensionData) #endregion
{
HasInitiated = false; #region Methods
SupportsExtensions = false;
ClientHostname = string.Empty; /// <summary>
ResetEmail(); /// Resets the data mode to false, clears the recipients, the sender address and the data buffer.
/// </summary>
if (clearExtensionData) public void ResetEmail() {
ExtendedData = null; this.IsInDataMode = false;
} this.Recipients.Clear();
this.SenderAddress = String.Empty;
/// <summary> this.DataBuffer.Clear();
/// Creates a new object that is a copy of the current instance. }
/// </summary>
/// <returns>A clone.</returns> /// <summary>
public virtual SmtpSessionState Clone() /// Resets the state table entirely.
{ /// </summary>
var clonedState = this.CopyPropertiesToNew<SmtpSessionState>(new[] {nameof(DataBuffer)}); /// <param name="clearExtensionData">if set to <c>true</c> [clear extension data].</param>
clonedState.DataBuffer.AddRange(DataBuffer); public void Reset(Boolean clearExtensionData) {
clonedState.Recipients.AddRange(Recipients); this.HasInitiated = false;
this.SupportsExtensions = false;
return clonedState; this.ClientHostname = String.Empty;
} this.ResetEmail();
#endregion 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
}
} }

View File

@ -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)
{ /// <summary>
ExitCode = exitCode; /// Gets the exit code.
StandardOutput = standardOutput; /// </summary>
StandardError = standardError; /// <value>
} /// The exit code.
/// </value>
/// <summary> public Int32 ExitCode {
/// Gets the exit code. get;
/// </summary> }
/// <value>
/// The exit code. /// <summary>
/// </value> /// Gets the text of the standard output.
public int ExitCode { get; } /// </summary>
/// <value>
/// <summary> /// The standard output.
/// Gets the text of the standard output. /// </value>
/// </summary> public String StandardOutput {
/// <value> get;
/// The standard output. }
/// </value>
public string StandardOutput { get; } /// <summary>
/// Gets the text of the standard error.
/// <summary> /// </summary>
/// Gets the text of the standard error. /// <value>
/// </summary> /// The standard error.
/// <value> /// </value>
/// The standard error. public String StandardError {
/// </value> get;
public string StandardError { get; } }
} }
} }

View File

@ -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> /// <summary>
/// Provides methods to help create external processes, and efficiently capture the /// Defines a delegate to handle binary data reception from the standard
/// standard error and standard output streams. /// output or standard error streams from a process.
/// </summary> /// </summary>
public static class ProcessRunner /// <param name="processData">The process data.</param>
{ /// <param name="process">The process.</param>
/// <summary> public delegate void ProcessDataReceivedCallback(Byte[] processData, Process process);
/// Defines a delegate to handle binary data reception from the standard
/// output or standard error streams from a process. /// <summary>
/// </summary> /// Runs the process asynchronously and if the exit code is 0,
/// <param name="processData">The process data.</param> /// returns all of the standard output text. If the exit code is something other than 0
/// <param name="process">The process.</param> /// it returns the contents of standard error.
public delegate void ProcessDataReceivedCallback(byte[] processData, Process process); /// This method is meant to be used for programs that output a relatively small amount of text.
/// </summary>
/// <summary> /// <param name="filename">The filename.</param>
/// Runs the process asynchronously and if the exit code is 0, /// <param name="arguments">The arguments.</param>
/// returns all of the standard output text. If the exit code is something other than 0 /// <param name="workingDirectory">The working directory.</param>
/// it returns the contents of standard error. /// <param name="cancellationToken">The cancellation token.</param>
/// This method is meant to be used for programs that output a relatively small amount of text. /// <returns>The type of the result produced by this Task.</returns>
/// </summary> /// <example>
/// <param name="filename">The filename.</param> /// The following code explains how to run an external process using the
/// <param name="arguments">The arguments.</param> /// <see cref="GetProcessOutputAsync(String, String, CancellationToken)"/> method.
/// <param name="workingDirectory">The working directory.</param> /// <code>
/// <param name="cancellationToken">The cancellation token.</param> /// class Example
/// <returns>The type of the result produced by this Task.</returns> /// {
/// <example> /// using System.Threading.Tasks;
/// The following code explains how to run an external process using the /// using Swan;
/// <see cref="GetProcessOutputAsync(string, string, CancellationToken)"/> method. ///
/// <code> /// static async Task Main()
/// class Example /// {
/// { /// // execute a process and save its output
/// using System.Threading.Tasks; /// var data = await ProcessRunner.
/// using Swan; /// GetProcessOutputAsync("dotnet", "--help");
/// ///
/// static async Task Main() /// // print the output
/// { /// data.WriteLine();
/// // execute a process and save its output /// }
/// var data = await ProcessRunner. /// }
/// GetProcessOutputAsync("dotnet", "--help"); /// </code>
/// /// </example>
/// // print the output public static async Task<String> GetProcessOutputAsync(String filename, String arguments = "", String? workingDirectory = null, CancellationToken cancellationToken = default) {
/// data.WriteLine(); ProcessResult result = await GetProcessResultAsync(filename, arguments, workingDirectory, cancellationToken: cancellationToken).ConfigureAwait(false);
/// } return result.ExitCode == 0 ? result.StandardOutput : result.StandardError;
/// } }
/// </code>
/// </example> /// <summary>
public static async Task<string> GetProcessOutputAsync( /// Runs the process asynchronously and if the exit code is 0,
string filename, /// returns all of the standard output text. If the exit code is something other than 0
string arguments = "", /// it returns the contents of standard error.
string? workingDirectory = null, /// This method is meant to be used for programs that output a relatively small amount
CancellationToken cancellationToken = default) /// of text using a different encoder.
{ /// </summary>
var result = await GetProcessResultAsync(filename, arguments, workingDirectory, cancellationToken: cancellationToken).ConfigureAwait(false); /// <param name="filename">The filename.</param>
return result.ExitCode == 0 ? result.StandardOutput : result.StandardError; /// <param name="arguments">The arguments.</param>
} /// <param name="encoding">The encoding.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <summary> /// <returns>
/// Runs the process asynchronously and if the exit code is 0, /// The type of the result produced by this Task.
/// returns all of the standard output text. If the exit code is something other than 0 /// </returns>
/// it returns the contents of standard error. public static async Task<String> GetProcessEncodedOutputAsync(String filename, String arguments = "", Encoding? encoding = null, CancellationToken cancellationToken = default) {
/// This method is meant to be used for programs that output a relatively small amount ProcessResult result = await GetProcessResultAsync(filename, arguments, null, encoding, cancellationToken).ConfigureAwait(false);
/// of text using a different encoder. return result.ExitCode == 0 ? result.StandardOutput : result.StandardError;
/// </summary> }
/// <param name="filename">The filename.</param>
/// <param name="arguments">The arguments.</param> /// <summary>
/// <param name="encoding">The encoding.</param> /// Executes a process asynchronously and returns the text of the standard output and standard error streams
/// <param name="cancellationToken">The cancellation token.</param> /// along with the exit code. This method is meant to be used for programs that output a relatively small
/// <returns> /// amount of text.
/// The type of the result produced by this Task. /// </summary>
/// </returns> /// <param name="filename">The filename.</param>
public static async Task<string> GetProcessEncodedOutputAsync( /// <param name="arguments">The arguments.</param>
string filename, /// <param name="cancellationToken">The cancellation token.</param>
string arguments = "", /// <returns>
Encoding? encoding = null, /// Text of the standard output and standard error streams along with the exit code as a <see cref="ProcessResult" /> instance.
CancellationToken cancellationToken = default) /// </returns>
{ /// <exception cref="ArgumentNullException">filename.</exception>
var result = await GetProcessResultAsync(filename, arguments, null, encoding, cancellationToken).ConfigureAwait(false); public static Task<ProcessResult> GetProcessResultAsync(String filename, String arguments = "", CancellationToken cancellationToken = default) => GetProcessResultAsync(filename, arguments, null, Definitions.CurrentAnsiEncoding, cancellationToken);
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
/// <summary> /// along with the exit code. This method is meant to be used for programs that output a relatively small
/// Executes a process asynchronously and returns the text of the standard output and standard error streams /// amount of text.
/// along with the exit code. This method is meant to be used for programs that output a relatively small /// </summary>
/// amount of text. /// <param name="filename">The filename.</param>
/// </summary> /// <param name="arguments">The arguments.</param>
/// <param name="filename">The filename.</param> /// <param name="workingDirectory">The working directory.</param>
/// <param name="arguments">The arguments.</param> /// <param name="encoding">The encoding.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns> /// <returns>
/// Text of the standard output and standard error streams along with the exit code as a <see cref="ProcessResult" /> instance. /// Text of the standard output and standard error streams along with the exit code as a <see cref="ProcessResult" /> instance.
/// </returns> /// </returns>
/// <exception cref="ArgumentNullException">filename.</exception> /// <exception cref="ArgumentNullException">filename.</exception>
public static Task<ProcessResult> GetProcessResultAsync( /// <example>
string filename, /// The following code describes how to run an external process using the <see cref="GetProcessResultAsync(String, String, String, Encoding, CancellationToken)" /> method.
string arguments = "", /// <code>
CancellationToken cancellationToken = default) => /// class Example
GetProcessResultAsync(filename, arguments, null, Definitions.CurrentAnsiEncoding, cancellationToken); /// {
/// using System.Threading.Tasks;
/// <summary> /// using Swan;
/// 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 /// static async Task Main()
/// amount of text. /// {
/// </summary> /// // Execute a process asynchronously
/// <param name="filename">The filename.</param> /// var data = await ProcessRunner.GetProcessResultAsync("dotnet", "--help");
/// <param name="arguments">The arguments.</param> ///
/// <param name="workingDirectory">The working directory.</param> /// // print out the exit code
/// <param name="encoding">The encoding.</param> /// $"{data.ExitCode}".WriteLine();
/// <param name="cancellationToken">The cancellation token.</param> ///
/// <returns> /// // print out the output
/// Text of the standard output and standard error streams along with the exit code as a <see cref="ProcessResult" /> instance. /// data.StandardOutput.WriteLine();
/// </returns> /// // and the error if exists
/// <exception cref="ArgumentNullException">filename.</exception> /// data.StandardError.Error();
/// <example> /// }
/// The following code describes how to run an external process using the <see cref="GetProcessResultAsync(string, string, string, Encoding, CancellationToken)" /> method. /// }
/// <code> /// </code></example>
/// class Example public static async Task<ProcessResult> GetProcessResultAsync(String filename, String arguments, String? workingDirectory, Encoding? encoding = null, CancellationToken cancellationToken = default) {
/// { if(filename == null) {
/// using System.Threading.Tasks; throw new ArgumentNullException(nameof(filename));
/// using Swan; }
///
/// static async Task Main() if(encoding == null) {
/// { encoding = Definitions.CurrentAnsiEncoding;
/// // Execute a process asynchronously }
/// var data = await ProcessRunner.GetProcessResultAsync("dotnet", "--help");
/// StringBuilder standardOutputBuilder = new StringBuilder();
/// // print out the exit code StringBuilder standardErrorBuilder = new StringBuilder();
/// $"{data.ExitCode}".WriteLine();
/// 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);
/// // print out the output
/// data.StandardOutput.WriteLine(); return new ProcessResult(processReturn, standardOutputBuilder.ToString(), standardErrorBuilder.ToString());
/// // and the error if exists }
/// data.StandardError.Error();
/// } /// <summary>
/// } /// Runs an external process asynchronously, providing callbacks to
/// </code></example> /// capture binary data from the standard error and standard output streams.
public static async Task<ProcessResult> GetProcessResultAsync( /// The callbacks contain a reference to the process so you can respond to output or
string filename, /// error streams by writing to the process' input stream.
string arguments, /// The exit code (return value) will be -1 for forceful termination of the process.
string? workingDirectory, /// </summary>
Encoding? encoding = null, /// <param name="filename">The filename.</param>
CancellationToken cancellationToken = default) /// <param name="arguments">The arguments.</param>
{ /// <param name="workingDirectory">The working directory.</param>
if (filename == null) /// <param name="onOutputData">The on output data.</param>
throw new ArgumentNullException(nameof(filename)); /// <param name="onErrorData">The on error data.</param>
/// <param name="encoding">The encoding.</param>
if (encoding == null) /// <param name="syncEvents">if set to <c>true</c> the next data callback will wait until the current one completes.</param>
encoding = Definitions.CurrentAnsiEncoding; /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
var standardOutputBuilder = new StringBuilder(); /// Value type will be -1 for forceful termination of the process.
var standardErrorBuilder = new StringBuilder(); /// </returns>
public static Task<Int32> RunProcessAsync(String filename, String arguments, String? workingDirectory, ProcessDataReceivedCallback onOutputData, ProcessDataReceivedCallback? onErrorData, Encoding encoding, Boolean syncEvents = true, CancellationToken cancellationToken = default) {
var processReturn = await RunProcessAsync( if(filename == null) {
filename, throw new ArgumentNullException(nameof(filename));
arguments, }
workingDirectory,
(data, proc) => standardOutputBuilder.Append(encoding.GetString(data)), return Task.Run(() => {
(data, proc) => standardErrorBuilder.Append(encoding.GetString(data)), // Setup the process and its corresponding start info
encoding, Process process = new Process {
true, EnableRaisingEvents = false,
cancellationToken) StartInfo = new ProcessStartInfo {
.ConfigureAwait(false); Arguments = arguments,
CreateNoWindow = true,
return new ProcessResult(processReturn, standardOutputBuilder.ToString(), standardErrorBuilder.ToString()); FileName = filename,
} RedirectStandardError = true,
StandardErrorEncoding = encoding,
/// <summary> RedirectStandardOutput = true,
/// Runs an external process asynchronously, providing callbacks to StandardOutputEncoding = encoding,
/// capture binary data from the standard error and standard output streams. UseShellExecute = false,
/// 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> if(!String.IsNullOrWhiteSpace(workingDirectory)) {
/// <param name="filename">The filename.</param> process.StartInfo.WorkingDirectory = workingDirectory;
/// <param name="arguments">The arguments.</param> }
/// <param name="workingDirectory">The working directory.</param>
/// <param name="onOutputData">The on output data.</param> // Launch the process and discard any buffered data for standard error and standard output
/// <param name="onErrorData">The on error data.</param> _ = process.Start();
/// <param name="encoding">The encoding.</param> process.StandardError.DiscardBufferedData();
/// <param name="syncEvents">if set to <c>true</c> the next data callback will wait until the current one completes.</param> process.StandardOutput.DiscardBufferedData();
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns> // Launch the asynchronous stream reading tasks
/// Value type will be -1 for forceful termination of the process. Task[] readTasks = new Task[2];
/// </returns> readTasks[0] = CopyStreamAsync(process, process.StandardOutput.BaseStream, onOutputData, syncEvents, cancellationToken);
public static Task<int> RunProcessAsync( readTasks[1] = CopyStreamAsync(process, process.StandardError.BaseStream, onErrorData, syncEvents, cancellationToken);
string filename,
string arguments, try {
string? workingDirectory, // Wait for all tasks to complete
ProcessDataReceivedCallback onOutputData, Task.WaitAll(readTasks, cancellationToken);
ProcessDataReceivedCallback onErrorData, } catch(TaskCanceledException) {
Encoding encoding, // ignore
bool syncEvents = true, } finally {
CancellationToken cancellationToken = default) // Wait for the process to exit
{ while(cancellationToken.IsCancellationRequested == false) {
if (filename == null) if(process.HasExited || process.WaitForExit(5)) {
throw new ArgumentNullException(nameof(filename)); break;
}
return Task.Run(() => }
{
// Setup the process and its corresponding start info // Forcefully kill the process if it do not exit
var process = new Process try {
{ if(process.HasExited == false) {
EnableRaisingEvents = false, process.Kill();
StartInfo = new ProcessStartInfo }
{ } catch {
Arguments = arguments, // swallow
CreateNoWindow = true, }
FileName = filename, }
RedirectStandardError = true,
StandardErrorEncoding = encoding, try {
RedirectStandardOutput = true, // Retrieve and return the exit code.
StandardOutputEncoding = encoding, // -1 signals error
UseShellExecute = false, return process.HasExited ? process.ExitCode : -1;
#if NET461 } catch {
WindowStyle = ProcessWindowStyle.Hidden, return -1;
#endif }
}, }, cancellationToken);
}; }
if (!string.IsNullOrWhiteSpace(workingDirectory)) /// <summary>
process.StartInfo.WorkingDirectory = workingDirectory; /// Runs an external process asynchronously, providing callbacks to
/// capture binary data from the standard error and standard output streams.
// Launch the process and discard any buffered data for standard error and standard output /// The callbacks contain a reference to the process so you can respond to output or
process.Start(); /// error streams by writing to the process' input stream.
process.StandardError.DiscardBufferedData(); /// The exit code (return value) will be -1 for forceful termination of the process.
process.StandardOutput.DiscardBufferedData(); /// </summary>
/// <param name="filename">The filename.</param>
// Launch the asynchronous stream reading tasks /// <param name="arguments">The arguments.</param>
var readTasks = new Task[2]; /// <param name="onOutputData">The on output data.</param>
readTasks[0] = CopyStreamAsync( /// <param name="onErrorData">The on error data.</param>
process, /// <param name="syncEvents">if set to <c>true</c> the next data callback will wait until the current one completes.</param>
process.StandardOutput.BaseStream, /// <param name="cancellationToken">The cancellation token.</param>
onOutputData, /// <returns>Value type will be -1 for forceful termination of the process.</returns>
syncEvents, /// <example>
cancellationToken); /// The following example illustrates how to run an external process using the
readTasks[1] = CopyStreamAsync( /// <see cref="RunProcessAsync(String, String, ProcessDataReceivedCallback, ProcessDataReceivedCallback, Boolean, CancellationToken)"/>
process, /// method.
process.StandardError.BaseStream, /// <code>
onErrorData, /// class Example
syncEvents, /// {
cancellationToken); /// using System.Diagnostics;
/// using System.Text;
try /// using System.Threading.Tasks;
{ /// using Swan;
// Wait for all tasks to complete ///
Task.WaitAll(readTasks, cancellationToken); /// static async Task Main()
} /// {
catch (TaskCanceledException) /// // Execute a process asynchronously
{ /// var data = await ProcessRunner
// ignore /// .RunProcessAsync("dotnet", "--help", Print, Print);
} ///
finally /// // flush all messages
{ /// Terminal.Flush();
// Wait for the process to exit /// }
while (cancellationToken.IsCancellationRequested == false) ///
{ /// // a callback to print both output or errors
if (process.HasExited || process.WaitForExit(5)) /// static void Print(byte[] data, Process proc) =>
break; /// Encoding.GetEncoding(0).GetString(data).WriteLine();
} /// }
/// </code>
// Forcefully kill the process if it do not exit /// </example>
try 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);
{
if (process.HasExited == false) /// <summary>
process.Kill(); /// Copies the stream asynchronously.
} /// </summary>
catch /// <param name="process">The process.</param>
{ /// <param name="baseStream">The source stream.</param>
// swallow /// <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>
try private static Task<UInt64> CopyStreamAsync(Process process, Stream baseStream, ProcessDataReceivedCallback? onDataCallback, Boolean syncEvents, CancellationToken ct) => Task.Run(async () => {
{ // define some state variables
// Retrieve and return the exit code. Byte[] swapBuffer = new Byte[2048]; // the buffer to copy data from one stream to the next
// -1 signals error UInt64 totalCount = 0; // the total amount of bytes read
return process.HasExited ? process.ExitCode : -1; Boolean hasExited = false;
}
catch while(ct.IsCancellationRequested == false) {
{ try {
return -1; // Check if process is no longer valid
} // if this condition holds, simply read the last bits of data available.
}, cancellationToken); Int32 readCount; // the bytes read in any given event
} if(process.HasExited || process.WaitForExit(1)) {
while(true) {
/// <summary> try {
/// Runs an external process asynchronously, providing callbacks to readCount = await baseStream.ReadAsync(swapBuffer, 0, swapBuffer.Length, ct);
/// 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 if(readCount > 0) {
/// error streams by writing to the process' input stream. totalCount += (UInt64)readCount;
/// The exit code (return value) will be -1 for forceful termination of the process. onDataCallback?.Invoke(swapBuffer.Skip(0).Take(readCount).ToArray(), process);
/// </summary> } else {
/// <param name="filename">The filename.</param> hasExited = true;
/// <param name="arguments">The arguments.</param> break;
/// <param name="onOutputData">The on output data.</param> }
/// <param name="onErrorData">The on error data.</param> } catch {
/// <param name="syncEvents">if set to <c>true</c> the next data callback will wait until the current one completes.</param> hasExited = true;
/// <param name="cancellationToken">The cancellation token.</param> break;
/// <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. if(hasExited) {
/// <code> break;
/// class Example }
/// {
/// using System.Diagnostics; // Try reading from the stream. < 0 means no read occurred.
/// using System.Text; readCount = await baseStream.ReadAsync(swapBuffer, 0, swapBuffer.Length, ct).ConfigureAwait(false);
/// using System.Threading.Tasks;
/// using Swan; // When no read is done, we need to let is rest for a bit
/// if(readCount <= 0) {
/// static async Task Main() await Task.Delay(1, ct).ConfigureAwait(false); // do not hog CPU cycles doing nothing.
/// { continue;
/// // Execute a process asynchronously }
/// var data = await ProcessRunner
/// .RunProcessAsync("dotnet", "--help", Print, Print); totalCount += (UInt64)readCount;
/// if(onDataCallback == null) {
/// // flush all messages continue;
/// Terminal.Flush(); }
/// }
/// // Create the buffer to pass to the callback
/// // a callback to print both output or errors Byte[] eventBuffer = swapBuffer.Skip(0).Take(readCount).ToArray();
/// static void Print(byte[] data, Process proc) =>
/// Encoding.GetEncoding(0).GetString(data).WriteLine(); // Create the data processing callback invocation
/// } Task eventTask = Task.Run(() => onDataCallback.Invoke(eventBuffer, process), ct);
/// </code>
/// </example> // wait for the event to process before the next read occurs
public static Task<int> RunProcessAsync( if(syncEvents) {
string filename, eventTask.Wait(ct);
string arguments, }
ProcessDataReceivedCallback onOutputData, } catch {
ProcessDataReceivedCallback onErrorData, break;
bool syncEvents = true, }
CancellationToken cancellationToken = default) }
=> RunProcessAsync(
filename, return totalCount;
arguments, }, ct);
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);
}
} }

View File

@ -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> /// <summary>
/// <value> /// Gets or sets a value indicating whether the service should be notified when the system is shutting down.
/// <c>true</c> if this instance can stop; otherwise, <c>false</c>. /// </summary>
/// </value> /// <value>
public bool CanStop { get; set; } = true; /// <c>true</c> if this instance can shutdown; otherwise, <c>false</c>.
/// </value>
/// <summary> public Boolean CanShutdown {
/// Gets or sets a value indicating whether the service should be notified when the system is shutting down. get; set;
/// </summary> }
/// <value>
/// <c>true</c> if this instance can shutdown; otherwise, <c>false</c>. /// <summary>
/// </value> /// Gets or sets a value indicating whether the service can be paused and resumed.
public bool CanShutdown { get; set; } /// </summary>
/// <value>
/// <summary> /// <c>true</c> if this instance can pause and continue; otherwise, <c>false</c>.
/// Gets or sets a value indicating whether the service can be paused and resumed. /// </value>
/// </summary> public Boolean CanPauseAndContinue {
/// <value> get; set;
/// <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> /// </summary>
/// Gets or sets the exit code. /// <value>
/// </summary> /// The exit code.
/// <value> /// </value>
/// The exit code. public Int32 ExitCode {
/// </value> get; set;
public int ExitCode { get; set; } }
/// <summary> /// <summary>
/// Indicates whether to report Start, Stop, Pause, and Continue commands in the event log. /// Indicates whether to report Start, Stop, Pause, and Continue commands in the event log.
/// </summary> /// </summary>
/// <value> /// <value>
/// <c>true</c> if [automatic log]; otherwise, <c>false</c>. /// <c>true</c> if [automatic log]; otherwise, <c>false</c>.
/// </value> /// </value>
public bool AutoLog { get; set; } public Boolean AutoLog {
get; set;
/// <summary> }
/// Gets or sets the name of the service.
/// </summary> /// <summary>
/// <value> /// Gets or sets the name of the service.
/// The name of the service. /// </summary>
/// </value> /// <value>
public string ServiceName { get; set; } /// The name of the service.
/// </value>
/// <summary> public String ServiceName {
/// Stops the executing service. get; set;
/// </summary> }
public void Stop()
{ /// <summary>
if (!CanStop) return; /// Stops the executing service.
/// </summary>
CanStop = false; public void Stop() {
OnStop(); if(!this.CanStop) {
} return;
}
/// <summary>
/// When implemented in a derived class, executes when a Start command is sent to the service by the Service Control Manager (SCM) this.CanStop = false;
/// or when the operating system starts (for a service that starts automatically). Specifies actions to take when the service starts. this.OnStop();
/// </summary> }
/// <param name="args">The arguments.</param>
protected virtual void OnStart(string[] args) /// <summary>
{ /// When implemented in a derived class, executes when a Start command is sent to the service by the Service Control Manager (SCM)
// do nothing /// 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>
/// <summary> protected virtual void OnStart(String[] args) {
/// When implemented in a derived class, executes when a Stop command is sent to the service by the Service Control Manager (SCM). // do nothing
/// Specifies actions to take when a service stops running. }
/// </summary>
protected virtual void OnStop() /// <summary>
{ /// When implemented in a derived class, executes when a Stop command is sent to the service by the Service Control Manager (SCM).
// do nothing /// Specifies actions to take when a service stops running.
} /// </summary>
} protected virtual void OnStop() {
// do nothing
}
}
} }
#endif

View File

@ -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; /// <summary>
/// /// Enumerates the different ways of providing delays.
/// public class Example /// </summary>
/// { public enum DelayStrategy {
/// public static void Main() /// <summary>
/// { /// Using the Thread.Sleep(15) mechanism.
/// // using the ThreadSleep strategy /// </summary>
/// using (var delay = new DelayProvider(DelayProvider.DelayStrategy.ThreadSleep)) ThreadSleep,
/// {
/// // retrieve how much time was delayed /// <summary>
/// var time = delay.WaitOne(); /// Using the Task.Delay(1).Wait mechanism.
/// } /// </summary>
/// } TaskDelay,
/// }
/// </code> /// <summary>
/// </example> /// Using a wait event that completes in a background ThreadPool thread.
public sealed class DelayProvider : IDisposable /// </summary>
{ ThreadPool,
private readonly object _syncRoot = new object(); }
private readonly Stopwatch _delayStopwatch = new Stopwatch();
/// <summary>
private bool _isDisposed; /// Gets the selected delay strategy.
private IWaitEvent _delayEvent; /// </summary>
public DelayStrategy Strategy {
/// <summary> get;
/// Initializes a new instance of the <see cref="DelayProvider"/> class. }
/// </summary>
/// <param name="strategy">The strategy.</param> /// <summary>
public DelayProvider(DelayStrategy strategy = DelayStrategy.TaskDelay) /// Creates the smallest possible, synchronous delay based on the selected strategy.
{ /// </summary>
Strategy = strategy; /// <returns>The elapsed time of the delay.</returns>
} public TimeSpan WaitOne() {
lock(this._syncRoot) {
/// <summary> if(this._isDisposed) {
/// Enumerates the different ways of providing delays. return TimeSpan.Zero;
/// </summary> }
public enum DelayStrategy
{ this._delayStopwatch.Restart();
/// <summary>
/// Using the Thread.Sleep(15) mechanism. switch(this.Strategy) {
/// </summary> case DelayStrategy.ThreadSleep:
ThreadSleep, DelaySleep();
break;
/// <summary> case DelayStrategy.TaskDelay:
/// Using the Task.Delay(1).Wait mechanism. DelayTask();
/// </summary> break;
TaskDelay, case DelayStrategy.ThreadPool:
this.DelayThreadPool();
/// <summary> break;
/// Using a wait event that completes in a background ThreadPool thread. }
/// </summary>
ThreadPool, return this._delayStopwatch.Elapsed;
} }
}
/// <summary>
/// Gets the selected delay strategy. #region Dispose Pattern
/// </summary>
public DelayStrategy Strategy { get; } /// <inheritdoc />
public void Dispose() {
/// <summary> lock(this._syncRoot) {
/// Creates the smallest possible, synchronous delay based on the selected strategy. if(this._isDisposed) {
/// </summary> return;
/// <returns>The elapsed time of the delay.</returns> }
public TimeSpan WaitOne()
{ this._isDisposed = true;
lock (_syncRoot)
{ this._delayEvent?.Dispose();
if (_isDisposed) return TimeSpan.Zero; }
}
_delayStopwatch.Restart();
#endregion
switch (Strategy)
{ #region Private Delay Mechanisms
case DelayStrategy.ThreadSleep:
DelaySleep(); private static void DelaySleep() => Thread.Sleep(15);
break;
case DelayStrategy.TaskDelay: private static void DelayTask() => Task.Delay(1).Wait();
DelayTask();
break; private void DelayThreadPool() {
case DelayStrategy.ThreadPool: if(this._delayEvent == null) {
DelayThreadPool(); this._delayEvent = WaitEventFactory.Create(isCompleted: true, useSlim: true);
break; }
}
this._delayEvent.Begin();
return _delayStopwatch.Elapsed; _ = ThreadPool.QueueUserWorkItem(s => {
} DelaySleep();
} this._delayEvent.Complete();
});
#region Dispose Pattern
this._delayEvent.Wait();
/// <inheritdoc /> }
public void Dispose()
{ #endregion
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
}
} }

View File

@ -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;
/// <summary> this._thread = new Thread(this.RunWorkerLoop) {
/// Initializes a new instance of the <see cref="ThreadWorkerBase"/> class. IsBackground = true,
/// </summary> Priority = priority,
/// <param name="name">The name.</param> Name = name,
/// <param name="priority">The thread priority.</param> };
/// <param name="period">The interval of cycle execution.</param> }
/// <param name="delayProvider">The cycle delay provide implementation.</param>
protected ThreadWorkerBase(string name, ThreadPriority priority, TimeSpan period, IWorkerDelayProvider delayProvider) /// <summary>
: base(name, period) /// Initializes a new instance of the <see cref="ThreadWorkerBase"/> class.
{ /// </summary>
DelayProvider = delayProvider; /// <param name="name">The name.</param>
_thread = new Thread(RunWorkerLoop) /// <param name="period">The execution interval.</param>
{ protected ThreadWorkerBase(String name, TimeSpan period) : this(name, ThreadPriority.Normal, period, WorkerDelayProvider.Default) {
IsBackground = true, // placeholder
Priority = priority, }
Name = name,
}; /// <summary>
} /// Provides an implementation on a cycle delay provider.
/// </summary>
/// <summary> protected IWorkerDelayProvider DelayProvider {
/// Initializes a new instance of the <see cref="ThreadWorkerBase"/> class. get;
/// </summary> }
/// <param name="name">The name.</param>
/// <param name="period">The execution interval.</param> /// <inheritdoc />
protected ThreadWorkerBase(string name, TimeSpan period) public override Task<WorkerState> StartAsync() {
: this(name, ThreadPriority.Normal, period, WorkerDelayProvider.Default) lock(this._syncLock) {
{ if(this.WorkerState == WorkerState.Paused || this.WorkerState == WorkerState.Waiting) {
// placeholder return this.ResumeAsync();
} }
/// <summary> if(this.WorkerState != WorkerState.Created) {
/// Provides an implementation on a cycle delay provider. return Task.FromResult(this.WorkerState);
/// </summary> }
protected IWorkerDelayProvider DelayProvider { get; }
if(this.IsStopRequested) {
/// <inheritdoc /> return Task.FromResult(this.WorkerState);
public override Task<WorkerState> StartAsync() }
{
lock (_syncLock) Task<WorkerState> task = this.QueueStateChange(StateChangeRequest.Start);
{ this._thread.Start();
if (WorkerState == WorkerState.Paused || WorkerState == WorkerState.Waiting) return task;
return ResumeAsync(); }
}
if (WorkerState != WorkerState.Created)
return Task.FromResult(WorkerState); /// <inheritdoc />
public override Task<WorkerState> PauseAsync() {
if (IsStopRequested) lock(this._syncLock) {
return Task.FromResult(WorkerState); return this.WorkerState != WorkerState.Running && this.WorkerState != WorkerState.Waiting ? Task.FromResult(this.WorkerState) : this.IsStopRequested ? Task.FromResult(this.WorkerState) : this.QueueStateChange(StateChangeRequest.Pause);
}
var task = QueueStateChange(StateChangeRequest.Start); }
_thread.Start();
return task; /// <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> PauseAsync() }
{
lock (_syncLock) /// <inheritdoc />
{ public override Task<WorkerState> StopAsync() {
if (WorkerState != WorkerState.Running && WorkerState != WorkerState.Waiting) lock(this._syncLock) {
return Task.FromResult(WorkerState); if(this.WorkerState == WorkerState.Stopped || this.WorkerState == WorkerState.Created) {
this.WorkerState = WorkerState.Stopped;
return IsStopRequested ? Task.FromResult(WorkerState) : QueueStateChange(StateChangeRequest.Pause); return Task.FromResult(this.WorkerState);
} }
}
return this.QueueStateChange(StateChangeRequest.Stop);
/// <inheritdoc /> }
public override Task<WorkerState> ResumeAsync() }
{
lock (_syncLock) /// <summary>
{ /// Suspends execution queues a new new cycle for execution. The delay is given in
if (WorkerState == WorkerState.Created) /// milliseconds. When overridden in a derived class the wait handle will be set
return StartAsync(); /// whenever an interrupt is received.
/// </summary>
if (WorkerState != WorkerState.Paused && WorkerState != WorkerState.Waiting) /// <param name="wantedDelay">The remaining delay to wait for in the cycle.</param>
return Task.FromResult(WorkerState); /// <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>
return IsStopRequested ? Task.FromResult(WorkerState) : QueueStateChange(StateChangeRequest.Resume); protected virtual void ExecuteCycleDelay(Int32 wantedDelay, Task delayTask, CancellationToken token) =>
} this.DelayProvider?.ExecuteCycleDelay(wantedDelay, delayTask, token);
}
/// <inheritdoc />
/// <inheritdoc /> protected override void OnDisposing() {
public override Task<WorkerState> StopAsync() lock(this._syncLock) {
{ if((this._thread.ThreadState & ThreadState.Unstarted) != ThreadState.Unstarted) {
lock (_syncLock) this._thread.Join();
{ }
if (WorkerState == WorkerState.Stopped || WorkerState == WorkerState.Created) }
{ }
WorkerState = WorkerState.Stopped;
return Task.FromResult(WorkerState); /// <summary>
} /// Implements worker control, execution and delay logic in a loop.
/// </summary>
return QueueStateChange(StateChangeRequest.Stop); private void RunWorkerLoop() {
} while(this.WorkerState != WorkerState.Stopped && !this.IsDisposing && !this.IsDisposed) {
} this.CycleStopwatch.Restart();
CancellationToken interruptToken = this.CycleCancellation.Token;
/// <summary> Int32 period = this.Period.TotalMilliseconds >= Int32.MaxValue ? -1 : Convert.ToInt32(Math.Floor(this.Period.TotalMilliseconds));
/// Suspends execution queues a new new cycle for execution. The delay is given in Task delayTask = Task.Delay(period, interruptToken);
/// milliseconds. When overridden in a derived class the wait handle will be set WorkerState initialWorkerState = this.WorkerState;
/// whenever an interrupt is received.
/// </summary> // Lock the cycle and capture relevant state valid for this cycle
/// <param name="wantedDelay">The remaining delay to wait for in the cycle.</param> this.CycleCompletedEvent.Reset();
/// <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> // Process the tasks that are awaiting
protected virtual void ExecuteCycleDelay(int wantedDelay, Task delayTask, CancellationToken token) => if(this.ProcessStateChangeRequests()) {
DelayProvider?.ExecuteCycleDelay(wantedDelay, delayTask, token); continue;
}
/// <inheritdoc />
protected override void OnDisposing() try {
{ if(initialWorkerState == WorkerState.Waiting &&
lock (_syncLock) !interruptToken.IsCancellationRequested) {
{ // Mark the state as Running
if ((_thread.ThreadState & ThreadState.Unstarted) != ThreadState.Unstarted) this.WorkerState = WorkerState.Running;
_thread.Join();
} // Call the execution logic
} this.ExecuteCycleLogic(interruptToken);
}
/// <summary> } catch(Exception ex) {
/// Implements worker control, execution and delay logic in a loop. this.OnCycleException(ex);
/// </summary> } finally {
private void RunWorkerLoop() // Update the state
{ this.WorkerState = initialWorkerState == WorkerState.Paused
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(); this.ClearStateChangeRequests();
WorkerState = WorkerState.Stopped; this.WorkerState = WorkerState.Stopped;
} }
/// <summary> /// <summary>
/// Queues a transition in worker state for processing. Returns a task that can be awaited /// Queues a transition in worker state for processing. Returns a task that can be awaited
/// when the operation completes. /// when the operation completes.
/// </summary> /// </summary>
/// <param name="request">The request.</param> /// <param name="request">The request.</param>
/// <returns>The awaitable task.</returns> /// <returns>The awaitable task.</returns>
private Task<WorkerState> QueueStateChange(StateChangeRequest request) private Task<WorkerState> QueueStateChange(StateChangeRequest request) {
{ lock(this._syncLock) {
lock (_syncLock) if(this.StateChangeTask != null) {
{ return this.StateChangeTask;
if (StateChangeTask != null) }
return StateChangeTask;
Task<WorkerState> waitingTask = new Task<WorkerState>(() => {
var waitingTask = new Task<WorkerState>(() => this.StateChangedEvent.Wait();
{ lock(this._syncLock) {
StateChangedEvent.Wait(); this.StateChangeTask = null;
lock (_syncLock) return this.WorkerState;
{ }
StateChangeTask = null; });
return WorkerState;
} this.StateChangeTask = waitingTask;
}); this.StateChangedEvent.Reset();
this.StateChangeRequests[request] = true;
StateChangeTask = waitingTask; waitingTask.Start();
StateChangedEvent.Reset(); this.CycleCancellation.Cancel();
StateChangeRequests[request] = true;
waitingTask.Start(); return waitingTask;
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> /// </summary>
/// Processes the state change request by checking pending events and scheduling /// <returns>Returns <c>true</c> if the execution should be terminated. <c>false</c> otherwise.</returns>
/// cycle execution accordingly. The <see cref="WorkerState"/> is also updated. private Boolean ProcessStateChangeRequests() {
/// </summary> lock(this._syncLock) {
/// <returns>Returns <c>true</c> if the execution should be terminated. <c>false</c> otherwise.</returns> Boolean hasRequest = false;
private bool ProcessStateChangeRequests() WorkerState currentState = this.WorkerState;
{
lock (_syncLock) // Update the state in the given priority
{ if(this.StateChangeRequests[StateChangeRequest.Stop] || this.IsDisposing || this.IsDisposed) {
var hasRequest = false; hasRequest = true;
var currentState = WorkerState; this.WorkerState = WorkerState.Stopped;
} else if(this.StateChangeRequests[StateChangeRequest.Pause]) {
// Update the state in the given priority hasRequest = true;
if (StateChangeRequests[StateChangeRequest.Stop] || IsDisposing || IsDisposed) this.WorkerState = WorkerState.Paused;
{ } else if(this.StateChangeRequests[StateChangeRequest.Start] || this.StateChangeRequests[StateChangeRequest.Resume]) {
hasRequest = true; hasRequest = true;
WorkerState = WorkerState.Stopped; this.WorkerState = WorkerState.Waiting;
} }
else if (StateChangeRequests[StateChangeRequest.Pause])
{ // Signals all state changes to continue
hasRequest = true; // as a command has been handled.
WorkerState = WorkerState.Paused; if(hasRequest) {
} this.ClearStateChangeRequests();
else if (StateChangeRequests[StateChangeRequest.Start] || StateChangeRequests[StateChangeRequest.Resume]) this.OnStateChangeProcessed(currentState, this.WorkerState);
{ }
hasRequest = true;
WorkerState = WorkerState.Waiting; return hasRequest;
} }
}
// Signals all state changes to continue
// as a command has been handled. /// <summary>
if (hasRequest) /// Signals all state change requests to set.
{ /// </summary>
ClearStateChangeRequests(); private void ClearStateChangeRequests() {
OnStateChangeProcessed(currentState, WorkerState); lock(this._syncLock) {
} // Mark all events as completed
this.StateChangeRequests[StateChangeRequest.Start] = false;
return hasRequest; this.StateChangeRequests[StateChangeRequest.Pause] = false;
} this.StateChangeRequests[StateChangeRequest.Resume] = false;
} this.StateChangeRequests[StateChangeRequest.Stop] = false;
/// <summary> this.StateChangedEvent.Set();
/// Signals all state change requests to set. this.CycleCompletedEvent.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();
}
}
}
} }

View File

@ -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;
/// <inheritdoc />
/// <summary> public override Task<WorkerState> StartAsync() {
/// Initializes a new instance of the <see cref="TimerWorkerBase"/> class. lock(this._syncLock) {
/// </summary> if(this.WorkerState == WorkerState.Paused || this.WorkerState == WorkerState.Waiting) {
/// <param name="name">The name.</param> return this.ResumeAsync();
/// <param name="period">The execution interval.</param> }
protected TimerWorkerBase(string name, TimeSpan period)
: base(name, period) if(this.WorkerState != WorkerState.Created) {
{ return Task.FromResult(this.WorkerState);
// Instantiate the timer that will be used to schedule cycles }
_timer = new Timer(
ExecuteTimerCallback, if(this.IsStopRequested) {
this, return Task.FromResult(this.WorkerState);
Timeout.Infinite, }
Timeout.Infinite);
} Task<WorkerState> task = this.QueueStateChange(StateChangeRequest.Start);
this.Interrupt();
/// <inheritdoc /> return task;
public override Task<WorkerState> StartAsync() }
{ }
lock (_syncLock)
{ /// <inheritdoc />
if (WorkerState == WorkerState.Paused || WorkerState == WorkerState.Waiting) public override Task<WorkerState> PauseAsync() {
return ResumeAsync(); lock(this._syncLock) {
if(this.WorkerState != WorkerState.Running && this.WorkerState != WorkerState.Waiting) {
if (WorkerState != WorkerState.Created) return Task.FromResult(this.WorkerState);
return Task.FromResult(WorkerState); }
if (IsStopRequested) if(this.IsStopRequested) {
return Task.FromResult(WorkerState); return Task.FromResult(this.WorkerState);
}
var task = QueueStateChange(StateChangeRequest.Start);
Interrupt(); Task<WorkerState> task = this.QueueStateChange(StateChangeRequest.Pause);
return task; this.Interrupt();
} return task;
} }
}
/// <inheritdoc />
public override Task<WorkerState> PauseAsync() /// <inheritdoc />
{ public override Task<WorkerState> ResumeAsync() {
lock (_syncLock) lock(this._syncLock) {
{ if(this.WorkerState == WorkerState.Created) {
if (WorkerState != WorkerState.Running && WorkerState != WorkerState.Waiting) return this.StartAsync();
return Task.FromResult(WorkerState); }
if (IsStopRequested) if(this.WorkerState != WorkerState.Paused && this.WorkerState != WorkerState.Waiting) {
return Task.FromResult(WorkerState); return Task.FromResult(this.WorkerState);
}
var task = QueueStateChange(StateChangeRequest.Pause);
Interrupt(); if(this.IsStopRequested) {
return task; return Task.FromResult(this.WorkerState);
} }
}
Task<WorkerState> task = this.QueueStateChange(StateChangeRequest.Resume);
/// <inheritdoc /> this.Interrupt();
public override Task<WorkerState> ResumeAsync() return task;
{ }
lock (_syncLock) }
{
if (WorkerState == WorkerState.Created) /// <inheritdoc />
return StartAsync(); public override Task<WorkerState> StopAsync() {
lock(this._syncLock) {
if (WorkerState != WorkerState.Paused && WorkerState != WorkerState.Waiting) if(this.WorkerState == WorkerState.Stopped || this.WorkerState == WorkerState.Created) {
return Task.FromResult(WorkerState); this.WorkerState = WorkerState.Stopped;
return Task.FromResult(this.WorkerState);
if (IsStopRequested) }
return Task.FromResult(WorkerState);
Task<WorkerState> task = this.QueueStateChange(StateChangeRequest.Stop);
var task = QueueStateChange(StateChangeRequest.Resume); this.Interrupt();
Interrupt(); return task;
return task; }
} }
}
/// <summary>
/// <inheritdoc /> /// Schedules a new cycle for execution. The delay is given in
public override Task<WorkerState> StopAsync() /// milliseconds. Passing a delay of 0 means a new cycle should be executed
{ /// immediately.
lock (_syncLock) /// </summary>
{ /// <param name="delay">The delay.</param>
if (WorkerState == WorkerState.Stopped || WorkerState == WorkerState.Created) protected void ScheduleCycle(Int32 delay) {
{ lock(this._syncLock) {
WorkerState = WorkerState.Stopped; if(!this._isTimerAlive) {
return Task.FromResult(WorkerState); return;
} }
var task = QueueStateChange(StateChangeRequest.Stop); _ = this._timer.Change(delay, Timeout.Infinite);
Interrupt(); }
return task; }
}
} /// <inheritdoc />
protected override void Dispose(Boolean disposing) {
/// <summary> base.Dispose(disposing);
/// Schedules a new cycle for execution. The delay is given in
/// milliseconds. Passing a delay of 0 means a new cycle should be executed lock(this._syncLock) {
/// immediately. if(!this._isTimerAlive) {
/// </summary> return;
/// <param name="delay">The delay.</param> }
protected void ScheduleCycle(int delay)
{ this._isTimerAlive = false;
lock (_syncLock) this._timer.Dispose();
{ }
if (!_isTimerAlive) return; }
_timer.Change(delay, Timeout.Infinite);
} /// <summary>
} /// Cancels the current token and schedules a new cycle immediately.
/// </summary>
/// <inheritdoc /> private void Interrupt() {
protected override void Dispose(bool disposing) lock(this._syncLock) {
{ if(this.WorkerState == WorkerState.Stopped) {
base.Dispose(disposing); return;
}
lock (_syncLock)
{ this.CycleCancellation.Cancel();
if (!_isTimerAlive) return; this.ScheduleCycle(0);
_isTimerAlive = false; }
_timer.Dispose(); }
}
} /// <summary>
/// Executes the worker cycle control logic.
/// <summary> /// This includes processing state change requests,
/// Cancels the current token and schedules a new cycle immediately. /// the execution of use cycle code,
/// </summary> /// and the scheduling of new cycles.
private void Interrupt() /// </summary>
{ private void ExecuteWorkerCycle() {
lock (_syncLock) this.CycleStopwatch.Restart();
{
if (WorkerState == WorkerState.Stopped) lock(this._syncLock) {
return; if(this.IsDisposing || this.IsDisposed) {
this.WorkerState = WorkerState.Stopped;
CycleCancellation.Cancel();
ScheduleCycle(0); // Cancel any awaiters
} try {
} this.StateChangedEvent.Set();
} catch { /* Ignore */ }
/// <summary>
/// Executes the worker cycle control logic. return;
/// This includes processing state change requests, }
/// the execution of use cycle code,
/// and the scheduling of new cycles. // Prevent running another instance of the cycle
/// </summary> if(this.CycleCompletedEvent.IsSet == false) {
private void ExecuteWorkerCycle() return;
{ }
CycleStopwatch.Restart();
// Lock the cycle and capture relevant state valid for this cycle
lock (_syncLock) this.CycleCompletedEvent.Reset();
{ }
if (IsDisposing || IsDisposed)
{ CancellationToken interruptToken = this.CycleCancellation.Token;
WorkerState = WorkerState.Stopped; WorkerState initialWorkerState = this.WorkerState;
// Cancel any awaiters // Process the tasks that are awaiting
try { StateChangedEvent.Set(); } if(this.ProcessStateChangeRequests()) {
catch { /* Ignore */ } return;
}
return;
} try {
if(initialWorkerState == WorkerState.Waiting &&
// Prevent running another instance of the cycle !interruptToken.IsCancellationRequested) {
if (CycleCompletedEvent.IsSet == false) return; // Mark the state as Running
this.WorkerState = WorkerState.Running;
// Lock the cycle and capture relevant state valid for this cycle
CycleCompletedEvent.Reset(); // Call the execution logic
} this.ExecuteCycleLogic(interruptToken);
}
var interruptToken = CycleCancellation.Token; } catch(Exception ex) {
var initialWorkerState = WorkerState; this.OnCycleException(ex);
} finally {
// Process the tasks that are awaiting // Update the state
if (ProcessStateChangeRequests()) this.WorkerState = initialWorkerState == WorkerState.Paused
return;
try
{
if (initialWorkerState == WorkerState.Waiting &&
!interruptToken.IsCancellationRequested)
{
// Mark the state as Running
WorkerState = WorkerState.Running;
// Call the execution logic
ExecuteCycleLogic(interruptToken);
}
}
catch (Exception ex)
{
OnCycleException(ex);
}
finally
{
// Update the state
WorkerState = initialWorkerState == WorkerState.Paused
? WorkerState.Paused ? WorkerState.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 this.ScheduleCycle(!interruptToken.IsCancellationRequested
ScheduleCycle(!interruptToken.IsCancellationRequested ? this.ComputeCycleDelay(initialWorkerState)
? ComputeCycleDelay(initialWorkerState) : 0);
: 0); }
} }
} }
}
/// <summary>
/// <summary> /// Represents the callback that is executed when the <see cref="_timer"/> ticks.
/// Represents the callback that is executed when the <see cref="_timer"/> ticks. /// </summary>
/// </summary> /// <param name="state">The state -- this contains the worker.</param>
/// <param name="state">The state -- this contains the worker.</param> private void ExecuteTimerCallback(Object state) => this.ExecuteWorkerCycle();
private void ExecuteTimerCallback(object state) => ExecuteWorkerCycle();
/// <summary>
/// <summary> /// Queues a transition in worker state for processing. Returns a task that can be awaited
/// Queues a transition in worker state for processing. Returns a task that can be awaited /// when the operation completes.
/// when the operation completes. /// </summary>
/// </summary> /// <param name="request">The request.</param>
/// <param name="request">The request.</param> /// <returns>The awaitable task.</returns>
/// <returns>The awaitable task.</returns> private Task<WorkerState> QueueStateChange(StateChangeRequest request) {
private Task<WorkerState> QueueStateChange(StateChangeRequest request) lock(this._syncLock) {
{ if(this.StateChangeTask != null) {
lock (_syncLock) return this.StateChangeTask;
{ }
if (StateChangeTask != null)
return StateChangeTask; Task<WorkerState> waitingTask = new Task<WorkerState>(() => {
this.StateChangedEvent.Wait();
var waitingTask = new Task<WorkerState>(() => lock(this._syncLock) {
{ this.StateChangeTask = null;
StateChangedEvent.Wait(); return this.WorkerState;
lock (_syncLock) }
{ });
StateChangeTask = null;
return WorkerState; this.StateChangeTask = waitingTask;
} this.StateChangedEvent.Reset();
}); this.StateChangeRequests[request] = true;
waitingTask.Start();
StateChangeTask = waitingTask; this.CycleCancellation.Cancel();
StateChangedEvent.Reset();
StateChangeRequests[request] = true; return waitingTask;
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>
/// <summary> /// <returns>Returns <c>true</c> if the execution should be terminated. <c>false</c> otherwise.</returns>
/// Processes the state change queue by checking pending events and scheduling private Boolean ProcessStateChangeRequests() {
/// cycle execution accordingly. The <see cref="WorkerState"/> is also updated. lock(this._syncLock) {
/// </summary> WorkerState currentState = this.WorkerState;
/// <returns>Returns <c>true</c> if the execution should be terminated. <c>false</c> otherwise.</returns> Boolean hasRequest = false;
private bool ProcessStateChangeRequests() Int32 schedule = 0;
{
lock (_syncLock) // Update the state according to request priority
{ if(this.StateChangeRequests[StateChangeRequest.Stop] || this.IsDisposing || this.IsDisposed) {
var currentState = WorkerState; hasRequest = true;
var hasRequest = false; this.WorkerState = WorkerState.Stopped;
var schedule = 0; schedule = this.StateChangeRequests[StateChangeRequest.Stop] ? Timeout.Infinite : 0;
} else if(this.StateChangeRequests[StateChangeRequest.Pause]) {
// Update the state according to request priority hasRequest = true;
if (StateChangeRequests[StateChangeRequest.Stop] || IsDisposing || IsDisposed) this.WorkerState = WorkerState.Paused;
{ schedule = Timeout.Infinite;
hasRequest = true; } else if(this.StateChangeRequests[StateChangeRequest.Start] || this.StateChangeRequests[StateChangeRequest.Resume]) {
WorkerState = WorkerState.Stopped; hasRequest = true;
schedule = StateChangeRequests[StateChangeRequest.Stop] ? Timeout.Infinite : 0; this.WorkerState = WorkerState.Waiting;
} }
else if (StateChangeRequests[StateChangeRequest.Pause])
{ // Signals all state changes to continue
hasRequest = true; // as a command has been handled.
WorkerState = WorkerState.Paused; if(hasRequest) {
schedule = Timeout.Infinite; this.ClearStateChangeRequests(schedule, currentState, this.WorkerState);
} }
else if (StateChangeRequests[StateChangeRequest.Start] || StateChangeRequests[StateChangeRequest.Resume])
{ return hasRequest;
hasRequest = true; }
WorkerState = WorkerState.Waiting; }
}
/// <summary>
// Signals all state changes to continue /// Signals all state change requests to set.
// as a command has been handled. /// </summary>
if (hasRequest) /// <param name="schedule">The cycle schedule.</param>
{ /// <param name="oldState">The previous worker state.</param>
ClearStateChangeRequests(schedule, currentState, WorkerState); /// <param name="newState">The new worker state.</param>
} private void ClearStateChangeRequests(Int32 schedule, WorkerState oldState, WorkerState newState) {
lock(this._syncLock) {
return hasRequest; // Mark all events as completed
} this.StateChangeRequests[StateChangeRequest.Start] = false;
} this.StateChangeRequests[StateChangeRequest.Pause] = false;
this.StateChangeRequests[StateChangeRequest.Resume] = false;
/// <summary> this.StateChangeRequests[StateChangeRequest.Stop] = false;
/// Signals all state change requests to set.
/// </summary> this.StateChangedEvent.Set();
/// <param name="schedule">The cycle schedule.</param> this.CycleCompletedEvent.Set();
/// <param name="oldState">The previous worker state.</param> this.OnStateChangeProcessed(oldState, newState);
/// <param name="newState">The new worker state.</param> this.ScheduleCycle(schedule);
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);
}
}
}
} }

View File

@ -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();
this.StateChangeRequests = new Dictionary<StateChangeRequest, Boolean>(5) {
private readonly AtomicBoolean _isDisposed = new AtomicBoolean(); [StateChangeRequest.Start] = false,
private readonly AtomicBoolean _isDisposing = new AtomicBoolean(); [StateChangeRequest.Pause] = false,
private readonly AtomicEnum<WorkerState> _workerState = new AtomicEnum<WorkerState>(WorkerState.Created); [StateChangeRequest.Resume] = false,
private readonly AtomicTimeSpan _timeSpan; [StateChangeRequest.Stop] = false,
};
/// <summary> }
/// Initializes a new instance of the <see cref="WorkerBase"/> class.
/// </summary> /// <summary>
/// <param name="name">The name.</param> /// Enumerates all the different state change requests.
/// <param name="period">The execution interval.</param> /// </summary>
protected WorkerBase(string name, TimeSpan period) protected enum StateChangeRequest {
{ /// <summary>
Name = name; /// No state change request.
_timeSpan = new AtomicTimeSpan(period); /// </summary>
None,
StateChangeRequests = new Dictionary<StateChangeRequest, bool>(5)
{ /// <summary>
[StateChangeRequest.Start] = false, /// Start state change request
[StateChangeRequest.Pause] = false, /// </summary>
[StateChangeRequest.Resume] = false, Start,
[StateChangeRequest.Stop] = false,
}; /// <summary>
} /// Pause state change request
/// </summary>
/// <summary> Pause,
/// Enumerates all the different state change requests.
/// </summary> /// <summary>
protected enum StateChangeRequest /// Resume state change request
{ /// </summary>
/// <summary> Resume,
/// No state change request.
/// </summary> /// <summary>
None, /// Stop state change request
/// </summary>
/// <summary> Stop,
/// Start state change request }
/// </summary>
Start, /// <inheritdoc />
public String Name {
/// <summary> get;
/// Pause state change request }
/// </summary>
Pause, /// <inheritdoc />
public TimeSpan Period {
/// <summary> get => this._timeSpan.Value;
/// Resume state change request set => this._timeSpan.Value = value;
/// </summary> }
Resume,
/// <inheritdoc />
/// <summary> public WorkerState WorkerState {
/// Stop state change request get => this._workerState.Value;
/// </summary> protected set => this._workerState.Value = value;
Stop, }
}
/// <inheritdoc />
/// <inheritdoc /> public Boolean IsDisposed {
public string Name { get; } get => this._isDisposed.Value;
protected set => this._isDisposed.Value = value;
/// <inheritdoc /> }
public TimeSpan Period
{ /// <inheritdoc />
get => _timeSpan.Value; public Boolean IsDisposing {
set => _timeSpan.Value = value; get => this._isDisposing.Value;
} protected set => this._isDisposing.Value = value;
}
/// <inheritdoc />
public WorkerState WorkerState /// <summary>
{ /// Gets the default period of 15 milliseconds which is the default precision for timers.
get => _workerState.Value; /// </summary>
protected set => _workerState.Value = value; protected static TimeSpan DefaultPeriod { get; } = TimeSpan.FromMilliseconds(15);
}
/// <summary>
/// <inheritdoc /> /// Gets a value indicating whether stop has been requested.
public bool IsDisposed /// This is useful to prevent more requests from being issued.
{ /// </summary>
get => _isDisposed.Value; protected Boolean IsStopRequested => this.StateChangeRequests[StateChangeRequest.Stop];
protected set => _isDisposed.Value = value;
} /// <summary>
/// Gets the cycle stopwatch.
/// <inheritdoc /> /// </summary>
public bool IsDisposing protected Stopwatch CycleStopwatch { get; } = new Stopwatch();
{
get => _isDisposing.Value; /// <summary>
protected set => _isDisposing.Value = value; /// Gets the state change requests.
} /// </summary>
protected Dictionary<StateChangeRequest, Boolean> StateChangeRequests {
/// <summary> get;
/// 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 the cycle completed event.
/// <summary> /// </summary>
/// Gets a value indicating whether stop has been requested. protected ManualResetEventSlim CycleCompletedEvent { get; } = new ManualResetEventSlim(true);
/// This is useful to prevent more requests from being issued.
/// </summary> /// <summary>
protected bool IsStopRequested => StateChangeRequests[StateChangeRequest.Stop]; /// Gets the state changed event.
/// </summary>
/// <summary> protected ManualResetEventSlim StateChangedEvent { get; } = new ManualResetEventSlim(true);
/// Gets the cycle stopwatch.
/// </summary> /// <summary>
protected Stopwatch CycleStopwatch { get; } = new Stopwatch(); /// Gets the cycle logic cancellation owner.
/// </summary>
/// <summary> protected CancellationTokenOwner CycleCancellation { get; } = new CancellationTokenOwner();
/// Gets the state change requests.
/// </summary> /// <summary>
protected Dictionary<StateChangeRequest, bool> StateChangeRequests { get; } /// Gets or sets the state change task.
/// </summary>
/// <summary> protected Task<WorkerState>? StateChangeTask {
/// Gets the cycle completed event. get; set;
/// </summary> }
protected ManualResetEventSlim CycleCompletedEvent { get; } = new ManualResetEventSlim(true);
/// <inheritdoc />
/// <summary> public abstract Task<WorkerState> StartAsync();
/// Gets the state changed event.
/// </summary> /// <inheritdoc />
protected ManualResetEventSlim StateChangedEvent { get; } = new ManualResetEventSlim(true); public abstract Task<WorkerState> PauseAsync();
/// <summary> /// <inheritdoc />
/// Gets the cycle logic cancellation owner. public abstract Task<WorkerState> ResumeAsync();
/// </summary>
protected CancellationTokenOwner CycleCancellation { get; } = new CancellationTokenOwner(); /// <inheritdoc />
public abstract Task<WorkerState> StopAsync();
/// <summary>
/// Gets or sets the state change task. /// <inheritdoc />
/// </summary> public void Dispose() {
protected Task<WorkerState>? StateChangeTask { get; set; } this.Dispose(true);
GC.SuppressFinalize(this);
/// <inheritdoc /> }
public abstract Task<WorkerState> StartAsync();
/// <summary>
/// <inheritdoc /> /// Releases unmanaged and - optionally - managed resources.
public abstract Task<WorkerState> PauseAsync(); /// </summary>
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
/// <inheritdoc /> protected virtual void Dispose(Boolean disposing) {
public abstract Task<WorkerState> ResumeAsync(); lock(this._syncLock) {
if(this.IsDisposed || this.IsDisposing) {
/// <inheritdoc /> return;
public abstract Task<WorkerState> StopAsync(); }
/// <inheritdoc /> this.IsDisposing = true;
public void Dispose() }
{
Dispose(true); // This also ensures the state change queue gets cleared
GC.SuppressFinalize(this); this.StopAsync().Wait();
} this.StateChangedEvent.Set();
this.CycleCompletedEvent.Set();
/// <summary>
/// Releases unmanaged and - optionally - managed resources. this.OnDisposing();
/// </summary>
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> this.CycleStopwatch.Stop();
protected virtual void Dispose(bool disposing) this.StateChangedEvent.Dispose();
{ this.CycleCompletedEvent.Dispose();
lock (_syncLock) this.CycleCancellation.Dispose();
{
if (IsDisposed || IsDisposing) return; this.IsDisposed = true;
IsDisposing = true; this.IsDisposing = false;
} }
// This also ensures the state change queue gets cleared /// <summary>
StopAsync().Wait(); /// Handles the cycle logic exceptions.
StateChangedEvent.Set(); /// </summary>
CycleCompletedEvent.Set(); /// <param name="ex">The exception that was thrown.</param>
protected abstract void OnCycleException(Exception ex);
OnDisposing();
/// <summary>
CycleStopwatch.Stop(); /// Represents the user defined logic to be executed on a single worker cycle.
StateChangedEvent.Dispose(); /// Check the cancellation token continuously if you need responsive interrupts.
CycleCompletedEvent.Dispose(); /// </summary>
CycleCancellation.Dispose(); /// <param name="cancellationToken">The cancellation token.</param>
protected abstract void ExecuteCycleLogic(CancellationToken cancellationToken);
IsDisposed = true;
IsDisposing = false; /// <summary>
} /// This method is called automatically when <see cref="Dispose()"/> is called.
/// Makes sure you release all resources within this call.
/// <summary> /// </summary>
/// Handles the cycle logic exceptions. protected abstract void OnDisposing();
/// </summary>
/// <param name="ex">The exception that was thrown.</param> /// <summary>
protected abstract void OnCycleException(Exception ex); /// Called when a state change request is processed.
/// </summary>
/// <summary> /// <param name="previousState">The state before the change.</param>
/// Represents the user defined logic to be executed on a single worker cycle. /// <param name="newState">The new state.</param>
/// Check the cancellation token continuously if you need responsive interrupts. protected virtual void OnStateChangeProcessed(WorkerState previousState, WorkerState newState) {
/// </summary> // placeholder
/// <param name="cancellationToken">The cancellation token.</param> }
protected abstract void ExecuteCycleLogic(CancellationToken cancellationToken);
/// <summary>
/// <summary> /// Computes the cycle delay.
/// This method is called automatically when <see cref="Dispose()"/> is called. /// </summary>
/// Makes sure you release all resources within this call. /// <param name="initialWorkerState">Initial state of the worker.</param>
/// </summary> /// <returns>The number of milliseconds to delay for.</returns>
protected abstract void OnDisposing(); protected Int32 ComputeCycleDelay(WorkerState initialWorkerState) {
Int64 elapsedMillis = this.CycleStopwatch.ElapsedMilliseconds;
/// <summary> TimeSpan period = this.Period;
/// Called when a state change request is processed. Double periodMillis = period.TotalMilliseconds;
/// </summary> Double delayMillis = periodMillis - elapsedMillis;
/// <param name="previousState">The state before the change.</param>
/// <param name="newState">The new state.</param> return initialWorkerState == WorkerState.Paused || period == TimeSpan.MaxValue || delayMillis >= Int32.MaxValue ? Timeout.Infinite : elapsedMillis >= periodMillis ? 0 : Convert.ToInt32(Math.Floor(delayMillis));
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));
}
}
} }

View File

@ -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> /// <summary>
/// Represents a class that implements delay logic for thread workers. /// Gets the default delay provider.
/// </summary> /// </summary>
public static class WorkerDelayProvider public static IWorkerDelayProvider Default => TokenTimeout;
{
/// <summary> /// <summary>
/// Gets the default delay provider. /// Provides a delay implementation which simply waits on the task and cancels on
/// </summary> /// the cancellation token.
public static IWorkerDelayProvider Default => TokenTimeout; /// </summary>
public static IWorkerDelayProvider Token => new TokenCancellableDelay();
/// <summary>
/// Provides a delay implementation which simply waits on the task and cancels on /// <summary>
/// the cancellation token. /// Provides a delay implementation which waits on the task and cancels on both,
/// </summary> /// the cancellation token and a wanted delay timeout.
public static IWorkerDelayProvider Token => new TokenCancellableDelay(); /// </summary>
public static IWorkerDelayProvider TokenTimeout => new TokenTimeoutCancellableDelay();
/// <summary>
/// Provides a delay implementation which waits on the task and cancels on both, /// <summary>
/// the cancellation token and a wanted delay timeout. /// Provides a delay implementation which uses short sleep intervals of 5ms.
/// </summary> /// </summary>
public static IWorkerDelayProvider TokenTimeout => new TokenTimeoutCancellableDelay(); public static IWorkerDelayProvider TokenSleep => new TokenSleepDelay();
/// <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>
/// Provides a delay implementation which uses short delay intervals of 5ms and private class TokenCancellableDelay : IWorkerDelayProvider {
/// a wait on the delay task in the final loop. public void ExecuteCycleDelay(Int32 wantedDelay, Task delayTask, CancellationToken token) {
/// </summary> if(wantedDelay == 0 || wantedDelay < -1) {
public static IWorkerDelayProvider SteppedToken => new SteppedTokenDelay(); return;
}
private class TokenCancellableDelay : IWorkerDelayProvider
{ // for wanted delays of less than 30ms it is not worth
public void ExecuteCycleDelay(int wantedDelay, Task delayTask, CancellationToken token) // passing a timeout or a token as it only adds unnecessary
{ // overhead.
if (wantedDelay == 0 || wantedDelay < -1) if(wantedDelay <= 30) {
return; try {
delayTask.Wait(token);
// for wanted delays of less than 30ms it is not worth } catch { /* ignore */ }
// passing a timeout or a token as it only adds unnecessary return;
// overhead. }
if (wantedDelay <= 30)
{ // only wait on the cancellation token
try { delayTask.Wait(token); } // or until the task completes normally
catch { /* ignore */ } try {
return; delayTask.Wait(token);
} } catch { /* ignore */ }
}
// only wait on the cancellation token }
// or until the task completes normally
try { delayTask.Wait(token); } private class TokenTimeoutCancellableDelay : IWorkerDelayProvider {
catch { /* ignore */ } public void ExecuteCycleDelay(Int32 wantedDelay, Task delayTask, CancellationToken token) {
} if(wantedDelay == 0 || wantedDelay < -1) {
} return;
}
private class TokenTimeoutCancellableDelay : IWorkerDelayProvider
{ // for wanted delays of less than 30ms it is not worth
public void ExecuteCycleDelay(int wantedDelay, Task delayTask, CancellationToken token) // passing a timeout or a token as it only adds unnecessary
{ // overhead.
if (wantedDelay == 0 || wantedDelay < -1) if(wantedDelay <= 30) {
return; try {
delayTask.Wait(token);
// for wanted delays of less than 30ms it is not worth } catch { /* ignore */ }
// passing a timeout or a token as it only adds unnecessary return;
// overhead. }
if (wantedDelay <= 30)
{ try {
try { delayTask.Wait(token); } _ = delayTask.Wait(wantedDelay, token);
catch { /* ignore */ } } catch { /* ignore */ }
return; }
} }
try { delayTask.Wait(wantedDelay, token); } private class TokenSleepDelay : IWorkerDelayProvider {
catch { /* ignore */ } private readonly Stopwatch _elapsedWait = new Stopwatch();
}
} public void ExecuteCycleDelay(Int32 wantedDelay, Task delayTask, CancellationToken token) {
this._elapsedWait.Restart();
private class TokenSleepDelay : IWorkerDelayProvider
{ if(wantedDelay == 0 || wantedDelay < -1) {
private readonly Stopwatch _elapsedWait = new Stopwatch(); return;
}
public void ExecuteCycleDelay(int wantedDelay, Task delayTask, CancellationToken token)
{ while(!token.IsCancellationRequested) {
_elapsedWait.Restart(); Thread.Sleep(5);
if (wantedDelay == 0 || wantedDelay < -1) if(wantedDelay != Timeout.Infinite && this._elapsedWait.ElapsedMilliseconds >= wantedDelay) {
return; break;
}
while (!token.IsCancellationRequested) }
{ }
Thread.Sleep(5); }
if (wantedDelay != Timeout.Infinite && _elapsedWait.ElapsedMilliseconds >= wantedDelay) private class SteppedTokenDelay : IWorkerDelayProvider {
break; private const Int32 StepMilliseconds = 15;
} private readonly Stopwatch _elapsedWait = new Stopwatch();
}
} public void ExecuteCycleDelay(Int32 wantedDelay, Task delayTask, CancellationToken token) {
this._elapsedWait.Restart();
private class SteppedTokenDelay : IWorkerDelayProvider
{ if(wantedDelay == 0 || wantedDelay < -1) {
private const int StepMilliseconds = 15; return;
private readonly Stopwatch _elapsedWait = new Stopwatch(); }
public void ExecuteCycleDelay(int wantedDelay, Task delayTask, CancellationToken token) if(wantedDelay == Timeout.Infinite) {
{ try {
_elapsedWait.Restart(); _ = delayTask.Wait(wantedDelay, token);
} catch { /* Ignore cancelled tasks */ }
if (wantedDelay == 0 || wantedDelay < -1) return;
return; }
if (wantedDelay == Timeout.Infinite) while(!token.IsCancellationRequested) {
{ Int32 remainingWaitTime = wantedDelay - Convert.ToInt32(this._elapsedWait.ElapsedMilliseconds);
try { delayTask.Wait(wantedDelay, token); }
catch { /* Ignore cancelled tasks */ } // Exit for no remaining wait time
return; if(remainingWaitTime <= 0) {
} break;
}
while (!token.IsCancellationRequested)
{ if(remainingWaitTime >= StepMilliseconds) {
var remainingWaitTime = wantedDelay - Convert.ToInt32(_elapsedWait.ElapsedMilliseconds); Task.Delay(StepMilliseconds, token).Wait(token);
} else {
// Exit for no remaining wait time try {
if (remainingWaitTime <= 0) _ = delayTask.Wait(remainingWaitTime);
break; } catch { /* ignore cancellation of task exception */ }
}
if (remainingWaitTime >= StepMilliseconds)
{ if(this._elapsedWait.ElapsedMilliseconds >= wantedDelay) {
Task.Delay(StepMilliseconds, token).Wait(token); break;
} }
else }
{ }
try { delayTask.Wait(remainingWaitTime); } }
catch { /* ignore cancellation of task exception */ } }
}
if (_elapsedWait.ElapsedMilliseconds >= wantedDelay)
break;
}
}
}
}
} }

View File

@ -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>(); /// <summary>
private readonly bool _useDeferredNotifications; /// Initializes a new instance of the <see cref="ViewModelBase"/> class.
/// </summary>
/// <summary> protected ViewModelBase() : this(false) {
/// Initializes a new instance of the <see cref="ViewModelBase"/> class. // placeholder
/// </summary> }
/// <param name="useDeferredNotifications">Set to <c>true</c> to use deferred notifications in the background.</param>
protected ViewModelBase(bool useDeferredNotifications) /// <inheritdoc />
{ public event PropertyChangedEventHandler PropertyChanged;
_useDeferredNotifications = useDeferredNotifications;
} /// <summary>Checks if a property already matches a desired value. Sets the property and
/// notifies listeners only when necessary.</summary>
/// <summary> /// <typeparam name="T">Type of the property.</typeparam>
/// Initializes a new instance of the <see cref="ViewModelBase"/> class. /// <param name="storage">Reference to a property with both getter and setter.</param>
/// </summary> /// <param name="value">Desired value for the property.</param>
protected ViewModelBase() /// <param name="propertyName">Name of the property used to notify listeners. This
: this(false) /// value is optional and can be provided automatically when invoked from compilers that
{ /// support CallerMemberName.</param>
// placeholder /// <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>
/// <inheritdoc /> protected Boolean SetProperty<T>(ref T storage, T value, [CallerMemberName] String propertyName = "", String[] notifyAlso = null) {
public event PropertyChangedEventHandler PropertyChanged; if(EqualityComparer<T>.Default.Equals(storage, value)) {
return false;
/// <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> storage = value;
/// <param name="storage">Reference to a property with both getter and setter.</param> this.NotifyPropertyChanged(propertyName, notifyAlso);
/// <param name="value">Desired value for the property.</param> return true;
/// <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> /// <summary>
/// <param name="notifyAlso">An array of property names to notify in addition to notifying the changes on the current property name.</param> /// Notifies one or more properties changed.
/// <returns>True if the value was changed, false if the existing value matched the /// </summary>
/// desired value.</returns> /// <param name="propertyNames">The property names.</param>
protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = "", string[] notifyAlso = null) protected void NotifyPropertyChanged(params String[] propertyNames) => this.NotifyPropertyChanged(null, propertyNames);
{
if (EqualityComparer<T>.Default.Equals(storage, value)) /// <summary>
return false; /// Notifies one or more properties changed.
/// </summary>
storage = value; /// <param name="mainProperty">The main property.</param>
NotifyPropertyChanged(propertyName, notifyAlso); /// <param name="auxiliaryProperties">The auxiliary properties.</param>
return true; private void NotifyPropertyChanged(String mainProperty, String[] auxiliaryProperties) {
} // Queue property notification
if(String.IsNullOrWhiteSpace(mainProperty) == false) {
/// <summary> this._queuedNotifications[mainProperty] = true;
/// Notifies one or more properties changed. }
/// </summary>
/// <param name="propertyNames">The property names.</param> // Set the state for notification properties
protected void NotifyPropertyChanged(params string[] propertyNames) => NotifyPropertyChanged(null, propertyNames); if(auxiliaryProperties != null) {
foreach(String property in auxiliaryProperties) {
/// <summary> if(String.IsNullOrWhiteSpace(property) == false) {
/// Notifies one or more properties changed. this._queuedNotifications[property] = true;
/// </summary> }
/// <param name="mainProperty">The main property.</param> }
/// <param name="auxiliaryProperties">The auxiliary properties.</param> }
private void NotifyPropertyChanged(string mainProperty, string[] auxiliaryProperties)
{ // Depending on operation mode, either fire the notifications in the background
// Queue property notification // or fire them immediately
if (string.IsNullOrWhiteSpace(mainProperty) == false) if(this._useDeferredNotifications) {
_queuedNotifications[mainProperty] = true; _ = Task.Run(this.NotifyQueuedProperties);
} else {
// Set the state for notification properties this.NotifyQueuedProperties();
if (auxiliaryProperties != null) }
{ }
foreach (var property in auxiliaryProperties)
{ /// <summary>
if (string.IsNullOrWhiteSpace(property) == false) /// Notifies the queued properties and resets the property name to a non-queued stated.
_queuedNotifications[property] = true; /// </summary>
} private void NotifyQueuedProperties() {
} // get a snapshot of property names.
String[] propertyNames = this._queuedNotifications.Keys.ToArray();
// Depending on operation mode, either fire the notifications in the background
// or fire them immediately // Iterate through the properties
if (_useDeferredNotifications) foreach(String property in propertyNames) {
Task.Run(NotifyQueuedProperties); // don't notify if we don't have a change
else if(!this._queuedNotifications[property]) {
NotifyQueuedProperties(); continue;
} }
/// <summary> // notify and reset queued state to false
/// Notifies the queued properties and resets the property name to a non-queued stated. try {
/// </summary> this.OnPropertyChanged(property);
private void NotifyQueuedProperties() } finally { this._queuedNotifications[property] = false; }
{ }
// get a snapshot of property names. }
var propertyNames = _queuedNotifications.Keys.ToArray();
/// <summary>
// Iterate through the properties /// Called when a property changes its backing value.
foreach (var property in propertyNames) /// </summary>
{ /// <param name="propertyName">Name of the property.</param>
// don't notify if we don't have a change private void OnPropertyChanged(String propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName ?? String.Empty));
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));
}
} }