2019-05-15 14:56:09 +00:00
/ *
*
* ( c ) Copyright Ascensio System Limited 2010 - 2018
*
* This program is freeware . You can redistribute it and / or modify it under the terms of the GNU
* General Public License ( GPL ) version 3 as published by the Free Software Foundation ( https : //www.gnu.org/copyleft/gpl.html).
* In accordance with Section 7 ( a ) of the GNU GPL its Section 15 shall be amended to the effect that
* Ascensio System SIA expressly excludes the warranty of non - infringement of any third - party rights .
*
* THIS PROGRAM IS DISTRIBUTED WITHOUT ANY WARRANTY ; WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR
* FITNESS FOR A PARTICULAR PURPOSE . For more details , see GNU GPL at https : //www.gnu.org/copyleft/gpl.html
*
* You can contact Ascensio System SIA by email at sales @onlyoffice . com
*
* The interactive user interfaces in modified source and object code versions of ONLYOFFICE must display
* Appropriate Legal Notices , as required under Section 5 of the GNU GPL version 3.
*
* Pursuant to Section 7 § 3 ( b ) of the GNU GPL you must retain the original ONLYOFFICE logo which contains
* relevant author attributions when distributing the software . If the display of the logo in its graphic
* form is not reasonably feasible for technical reasons , you must include the words "Powered by ONLYOFFICE"
* in every copy of the program you distribute .
* Pursuant to Section 7 § 3 ( e ) we decline to grant you any rights under trademark law for use of our trademarks .
*
* /
using System ;
using System.Collections.Generic ;
using System.Linq ;
using System.Threading ;
2019-11-12 13:23:15 +00:00
using System.Threading.Tasks ;
2019-05-15 14:56:09 +00:00
using ASC.Common.Logging ;
using ASC.Common.Notify.Patterns ;
using ASC.Notify.Channels ;
using ASC.Notify.Cron ;
using ASC.Notify.Messages ;
using ASC.Notify.Patterns ;
2019-11-12 13:23:15 +00:00
using ASC.Notify.Recipients ;
2019-11-06 15:03:09 +00:00
using Microsoft.Extensions.DependencyInjection ;
using Microsoft.Extensions.Options ;
2019-05-15 14:56:09 +00:00
namespace ASC.Notify.Engine
{
public class NotifyEngine : INotifyEngine
{
2019-10-17 15:55:35 +00:00
private readonly ILog log ;
2019-05-15 14:56:09 +00:00
private readonly Context context ;
private readonly List < SendMethodWrapper > sendMethods = new List < SendMethodWrapper > ( ) ;
private readonly Queue < NotifyRequest > requests = new Queue < NotifyRequest > ( 1000 ) ;
private readonly Thread notifyScheduler ;
private readonly Thread notifySender ;
private readonly AutoResetEvent requestsEvent = new AutoResetEvent ( false ) ;
private readonly AutoResetEvent methodsEvent = new AutoResetEvent ( false ) ;
private readonly Dictionary < string , IPatternStyler > stylers = new Dictionary < string , IPatternStyler > ( ) ;
private readonly IPatternFormatter sysTagFormatter = new ReplacePatternFormatter ( @"_#(?<tagName>[A-Z0-9_\-.]+)#_" , true ) ;
2019-11-06 15:03:09 +00:00
private readonly TimeSpan defaultSleep = TimeSpan . FromSeconds ( 10 ) ;
public IServiceProvider ServiceProvider { get ; }
2019-10-11 08:31:21 +00:00
public event Action < NotifyEngine , NotifyRequest , IServiceScope > BeforeTransferRequest ;
2019-05-15 14:56:09 +00:00
2019-10-11 08:31:21 +00:00
public event Action < NotifyEngine , NotifyRequest , IServiceScope > AfterTransferRequest ;
2019-05-15 14:56:09 +00:00
2019-10-11 08:31:21 +00:00
public NotifyEngine ( Context context , IServiceProvider serviceProvider )
2019-11-06 15:03:09 +00:00
{
this . context = context ? ? throw new ArgumentNullException ( "context" ) ;
log = serviceProvider . GetService < IOptionsMonitor < ILog > > ( ) . Get ( "ASC.Notify" ) ;
ServiceProvider = serviceProvider ;
2019-05-15 14:56:09 +00:00
notifyScheduler = new Thread ( NotifyScheduler ) { IsBackground = true , Name = "NotifyScheduler" } ;
notifySender = new Thread ( NotifySender ) { IsBackground = true , Name = "NotifySender" } ;
}
2019-10-11 08:31:21 +00:00
public virtual void QueueRequest ( NotifyRequest request , IServiceScope serviceScope )
2019-11-06 15:03:09 +00:00
{
2019-10-11 08:31:21 +00:00
BeforeTransferRequest ? . Invoke ( this , request , serviceScope ) ;
2019-05-15 14:56:09 +00:00
lock ( requests )
{
if ( ! notifySender . IsAlive )
{
notifySender . Start ( ) ;
}
requests . Enqueue ( request ) ;
}
requestsEvent . Set ( ) ;
}
internal void RegisterSendMethod ( Action < DateTime > method , string cron )
{
if ( method = = null ) throw new ArgumentNullException ( "method" ) ;
if ( string . IsNullOrEmpty ( cron ) ) throw new ArgumentNullException ( "cron" ) ;
2019-10-17 15:55:35 +00:00
var w = new SendMethodWrapper ( method , cron , log ) ;
2019-05-15 14:56:09 +00:00
lock ( sendMethods )
{
if ( ! notifyScheduler . IsAlive )
{
notifyScheduler . Start ( ) ;
}
sendMethods . Remove ( w ) ;
sendMethods . Add ( w ) ;
}
methodsEvent . Set ( ) ;
}
internal void UnregisterSendMethod ( Action < DateTime > method )
{
if ( method = = null ) throw new ArgumentNullException ( "method" ) ;
lock ( sendMethods )
{
2019-10-17 15:55:35 +00:00
sendMethods . Remove ( new SendMethodWrapper ( method , null , log ) ) ;
2019-05-15 14:56:09 +00:00
}
}
private void NotifyScheduler ( object state )
{
try
{
while ( true )
{
var min = DateTime . MaxValue ;
var now = DateTime . UtcNow ;
List < SendMethodWrapper > copy ;
lock ( sendMethods )
{
copy = sendMethods . ToList ( ) ;
}
foreach ( var w in copy )
{
if ( ! w . ScheduleDate . HasValue )
{
lock ( sendMethods )
{
sendMethods . Remove ( w ) ;
}
}
if ( w . ScheduleDate . Value < = now )
{
try
{
w . InvokeSendMethod ( now ) ;
}
catch ( Exception error )
{
log . Error ( error ) ;
}
w . UpdateScheduleDate ( now ) ;
}
if ( w . ScheduleDate . Value > now & & w . ScheduleDate . Value < min )
{
min = w . ScheduleDate . Value ;
}
}
var wait = min ! = DateTime . MaxValue ? min - DateTime . UtcNow : defaultSleep ;
if ( wait < defaultSleep )
{
wait = defaultSleep ;
}
2019-08-15 13:16:39 +00:00
else if ( wait . Ticks > int . MaxValue )
2019-05-15 14:56:09 +00:00
{
2019-08-15 13:16:39 +00:00
wait = TimeSpan . FromTicks ( int . MaxValue ) ;
2019-05-15 14:56:09 +00:00
}
methodsEvent . WaitOne ( wait , false ) ;
}
}
catch ( ThreadAbortException )
{
return ;
}
catch ( Exception e )
{
log . Error ( e ) ;
}
}
private void NotifySender ( object state )
{
try
{
while ( true )
{
NotifyRequest request = null ;
lock ( requests )
{
if ( requests . Any ( ) )
{
request = requests . Dequeue ( ) ;
}
}
if ( request ! = null )
2019-11-06 15:03:09 +00:00
{
using var scope = ServiceProvider . CreateScope ( ) ;
2019-10-11 08:31:21 +00:00
AfterTransferRequest ? . Invoke ( this , request , scope ) ;
2019-05-15 14:56:09 +00:00
try
{
2019-11-12 13:23:15 +00:00
SendNotify ( request , scope ) ;
2019-05-15 14:56:09 +00:00
}
catch ( Exception e )
{
log . Error ( e ) ;
}
}
else
{
requestsEvent . WaitOne ( ) ;
}
}
}
catch ( ThreadAbortException )
{
return ;
}
catch ( Exception e )
{
log . Error ( e ) ;
}
}
2019-11-12 13:23:15 +00:00
private NotifyResult SendNotify ( NotifyRequest request , IServiceScope serviceScope )
2019-05-15 14:56:09 +00:00
{
var sendResponces = new List < SendResponse > ( ) ;
2019-10-11 08:31:21 +00:00
var response = CheckPreventInterceptors ( request , InterceptorPlace . Prepare , serviceScope , null ) ;
2019-05-15 14:56:09 +00:00
if ( response ! = null )
{
sendResponces . Add ( response ) ;
}
else
{
2019-11-12 13:23:15 +00:00
sendResponces . AddRange ( SendGroupNotify ( request , serviceScope ) ) ;
2019-05-15 14:56:09 +00:00
}
NotifyResult result = null ;
if ( sendResponces = = null | | sendResponces . Count = = 0 )
{
result = new NotifyResult ( SendResult . OK , sendResponces ) ;
}
else
{
result = new NotifyResult ( sendResponces . Aggregate ( ( SendResult ) 0 , ( s , r ) = > s | = r . Result ) , sendResponces ) ;
}
log . Debug ( result ) ;
return result ;
}
2019-10-11 08:31:21 +00:00
private SendResponse CheckPreventInterceptors ( NotifyRequest request , InterceptorPlace place , IServiceScope serviceScope , string sender )
2019-05-15 14:56:09 +00:00
{
2019-10-11 08:31:21 +00:00
return request . Intercept ( place , serviceScope ) ? new SendResponse ( request . NotifyAction , sender , request . Recipient , SendResult . Prevented ) : null ;
2019-05-15 14:56:09 +00:00
}
2019-11-12 13:23:15 +00:00
private List < SendResponse > SendGroupNotify ( NotifyRequest request , IServiceScope serviceScope )
2019-05-15 14:56:09 +00:00
{
var responces = new List < SendResponse > ( ) ;
2019-11-12 13:23:15 +00:00
SendGroupNotify ( request , responces , serviceScope ) ;
2019-05-15 14:56:09 +00:00
return responces ;
}
2019-11-12 13:23:15 +00:00
private void SendGroupNotify ( NotifyRequest request , List < SendResponse > responces , IServiceScope serviceScope )
2019-05-15 14:56:09 +00:00
{
if ( request . Recipient is IDirectRecipient )
{
var subscriptionSource = request . NotifySource . GetSubscriptionProvider ( ) ;
if ( ! request . IsNeedCheckSubscriptions | | ! subscriptionSource . IsUnsubscribe ( request . Recipient as IDirectRecipient , request . NotifyAction , request . ObjectID ) )
{
var directresponses = new List < SendResponse > ( 1 ) ;
try
{
2019-11-12 13:23:15 +00:00
directresponses = SendDirectNotify ( request , serviceScope ) ;
2019-05-15 14:56:09 +00:00
}
catch ( Exception exc )
{
directresponses . Add ( new SendResponse ( request . NotifyAction , request . Recipient , exc ) ) ;
}
responces . AddRange ( directresponses ) ;
}
}
else
{
if ( request . Recipient is IRecipientsGroup )
{
2019-10-11 08:31:21 +00:00
var checkresp = CheckPreventInterceptors ( request , InterceptorPlace . GroupSend , serviceScope , null ) ;
2019-05-15 14:56:09 +00:00
if ( checkresp ! = null )
{
responces . Add ( checkresp ) ;
}
else
{
var recipientProvider = request . NotifySource . GetRecipientsProvider ( ) ;
try
{
2019-09-12 11:34:58 +00:00
var recipients = recipientProvider . GetGroupEntries ( request . Recipient as IRecipientsGroup ) ? ? new IRecipient [ 0 ] ;
2019-05-15 14:56:09 +00:00
foreach ( var recipient in recipients )
{
try
{
var newRequest = request . Split ( recipient ) ;
2019-11-12 13:23:15 +00:00
SendGroupNotify ( newRequest , responces , serviceScope ) ;
2019-05-15 14:56:09 +00:00
}
catch ( Exception exc )
{
responces . Add ( new SendResponse ( request . NotifyAction , request . Recipient , exc ) ) ;
}
}
}
catch ( Exception exc )
{
responces . Add ( new SendResponse ( request . NotifyAction , request . Recipient , exc ) { Result = SendResult . IncorrectRecipient } ) ;
}
}
}
else
{
responces . Add ( new SendResponse ( request . NotifyAction , request . Recipient , null )
{
Result = SendResult . IncorrectRecipient ,
Exception = new NotifyException ( "recipient may be IRecipientsGroup or IDirectRecipient" )
} ) ;
}
}
}
2019-11-12 13:23:15 +00:00
private List < SendResponse > SendDirectNotify ( NotifyRequest request , IServiceScope serviceScope )
2019-05-15 14:56:09 +00:00
{
if ( ! ( request . Recipient is IDirectRecipient ) ) throw new ArgumentException ( "request.Recipient not IDirectRecipient" , "request" ) ;
var responses = new List < SendResponse > ( ) ;
2019-10-11 08:31:21 +00:00
var response = CheckPreventInterceptors ( request , InterceptorPlace . DirectSend , serviceScope , null ) ;
2019-05-15 14:56:09 +00:00
if ( response ! = null )
{
responses . Add ( response ) ;
return responses ;
}
try
{
2019-09-12 11:34:58 +00:00
PrepareRequestFillSenders ( request ) ;
2019-05-15 14:56:09 +00:00
PrepareRequestFillPatterns ( request ) ;
PrepareRequestFillTags ( request ) ;
}
catch ( Exception )
{
responses . Add ( new SendResponse ( request . NotifyAction , null , request . Recipient , SendResult . Impossible ) ) ;
}
if ( request . SenderNames ! = null & & request . SenderNames . Length > 0 )
{
foreach ( var sendertag in request . SenderNames )
{
var channel = context . NotifyService . GetSender ( sendertag ) ;
if ( channel ! = null )
{
try
{
2019-11-12 13:23:15 +00:00
response = SendDirectNotify ( request , channel , serviceScope ) ;
2019-05-15 14:56:09 +00:00
}
catch ( Exception exc )
{
response = new SendResponse ( request . NotifyAction , channel . SenderName , request . Recipient , exc ) ;
}
}
else
{
2019-08-15 13:16:39 +00:00
response = new SendResponse ( request . NotifyAction , sendertag , request . Recipient , new NotifyException ( string . Format ( "Not registered sender \"{0}\"." , sendertag ) ) ) ;
2019-05-15 14:56:09 +00:00
}
responses . Add ( response ) ;
}
}
else
{
response = new SendResponse ( request . NotifyAction , request . Recipient , new NotifyException ( "Notice hasn't any senders." ) ) ;
responses . Add ( response ) ;
}
return responses ;
}
2019-11-12 13:23:15 +00:00
private SendResponse SendDirectNotify ( NotifyRequest request , ISenderChannel channel , IServiceScope serviceScope )
2019-11-06 15:03:09 +00:00
{
2019-08-16 09:08:46 +00:00
if ( ! ( request . Recipient is IDirectRecipient ) ) throw new ArgumentException ( "request.Recipient not IDirectRecipient" , "request" ) ;
2019-05-15 14:56:09 +00:00
request . CurrentSender = channel . SenderName ;
2019-11-12 13:23:15 +00:00
var oops = CreateNoticeMessageFromNotifyRequest ( request , channel . SenderName , serviceScope , out var noticeMessage ) ;
2019-05-15 14:56:09 +00:00
if ( oops ! = null ) return oops ;
request . CurrentMessage = noticeMessage ;
2019-10-11 08:31:21 +00:00
var preventresponse = CheckPreventInterceptors ( request , InterceptorPlace . MessageSend , serviceScope , channel . SenderName ) ;
2019-05-15 14:56:09 +00:00
if ( preventresponse ! = null ) return preventresponse ;
channel . SendAsync ( noticeMessage ) ;
return new SendResponse ( noticeMessage , channel . SenderName , SendResult . Inprogress ) ;
}
2019-11-12 13:23:15 +00:00
private SendResponse CreateNoticeMessageFromNotifyRequest ( NotifyRequest request , string sender , IServiceScope serviceScope , out NoticeMessage noticeMessage )
2019-05-15 14:56:09 +00:00
{
if ( request = = null ) throw new ArgumentNullException ( "request" ) ;
var recipientProvider = request . NotifySource . GetRecipientsProvider ( ) ;
var recipient = request . Recipient as IDirectRecipient ;
var addresses = recipient . Addresses ;
if ( addresses = = null | | ! addresses . Any ( ) )
{
2019-09-12 11:34:58 +00:00
addresses = recipientProvider . GetRecipientAddresses ( request . Recipient as IDirectRecipient , sender ) ;
2019-05-15 14:56:09 +00:00
recipient = new DirectRecipient ( request . Recipient . ID , request . Recipient . Name , addresses ) ;
}
2019-09-12 11:34:58 +00:00
recipient = recipientProvider . FilterRecipientAddresses ( recipient ) ;
2019-05-15 14:56:09 +00:00
noticeMessage = request . CreateMessage ( recipient ) ;
addresses = recipient . Addresses ;
if ( addresses = = null | | ! addresses . Any ( a = > ! string . IsNullOrEmpty ( a ) ) )
{
//checking addresses
return new SendResponse ( request . NotifyAction , sender , recipient , new NotifyException ( string . Format ( "For recipient {0} by sender {1} no one addresses getted." , recipient , sender ) ) ) ;
}
var pattern = request . GetSenderPattern ( sender ) ;
if ( pattern = = null )
{
2019-08-15 13:16:39 +00:00
return new SendResponse ( request . NotifyAction , sender , recipient , new NotifyException ( string . Format ( "For action \"{0}\" by sender \"{1}\" no one patterns getted." , request . NotifyAction , sender ) ) ) ;
2019-05-15 14:56:09 +00:00
}
noticeMessage . Pattern = pattern ;
noticeMessage . ContentType = pattern . ContentType ;
noticeMessage . AddArgument ( request . Arguments . ToArray ( ) ) ;
var patternProvider = request . NotifySource . GetPatternProvider ( ) ;
var formatter = patternProvider . GetFormatter ( pattern ) ;
try
{
if ( formatter ! = null )
{
formatter . FormatMessage ( noticeMessage , noticeMessage . Arguments ) ;
}
sysTagFormatter . FormatMessage (
noticeMessage , new [ ]
{
new TagValue ( Context . _SYS_RECIPIENT_ID , request . Recipient . ID ) ,
new TagValue ( Context . _SYS_RECIPIENT_NAME , request . Recipient . Name ) ,
new TagValue ( Context . _SYS_RECIPIENT_ADDRESS , addresses ! = null & & addresses . Length > 0 ? addresses [ 0 ] : null )
}
) ;
//Do styling here
if ( ! string . IsNullOrEmpty ( pattern . Styler ) )
{
//We need to run through styler before templating
2019-11-12 13:23:15 +00:00
StyleMessage ( serviceScope , noticeMessage ) ;
2019-05-15 14:56:09 +00:00
}
}
catch ( Exception exc )
{
return new SendResponse ( request . NotifyAction , sender , recipient , exc ) ;
}
return null ;
}
2019-11-12 13:23:15 +00:00
private void StyleMessage ( IServiceScope scope , NoticeMessage message )
2019-05-15 14:56:09 +00:00
{
try
{
if ( ! stylers . ContainsKey ( message . Pattern . Styler ) )
2019-09-20 14:04:06 +00:00
{
2019-11-12 13:23:15 +00:00
if ( scope . ServiceProvider . GetService ( Type . GetType ( message . Pattern . Styler , true ) ) is IPatternStyler styler )
2019-05-15 14:56:09 +00:00
{
stylers . Add ( message . Pattern . Styler , styler ) ;
}
}
stylers [ message . Pattern . Styler ] . ApplyFormating ( message ) ;
}
catch ( Exception exc )
{
log . Warn ( "error styling message" , exc ) ;
}
}
2019-09-27 15:12:13 +00:00
private void PrepareRequestFillSenders ( NotifyRequest request )
2019-05-15 14:56:09 +00:00
{
if ( request . SenderNames = = null )
{
var subscriptionProvider = request . NotifySource . GetSubscriptionProvider ( ) ;
var senderNames = new List < string > ( ) ;
2019-09-12 11:34:58 +00:00
senderNames . AddRange ( subscriptionProvider . GetSubscriptionMethod ( request . NotifyAction , request . Recipient ) ? ? new string [ 0 ] ) ;
2019-08-15 12:04:42 +00:00
senderNames . AddRange ( request . Arguments . OfType < AdditionalSenderTag > ( ) . Select ( tag = > ( string ) tag . Value ) ) ;
2019-05-15 14:56:09 +00:00
request . SenderNames = senderNames . ToArray ( ) ;
}
}
private void PrepareRequestFillPatterns ( NotifyRequest request )
{
if ( request . Patterns = = null )
{
request . Patterns = new IPattern [ request . SenderNames . Length ] ;
if ( request . Patterns . Length = = 0 ) return ;
var apProvider = request . NotifySource . GetPatternProvider ( ) ;
for ( var i = 0 ; i < request . SenderNames . Length ; i + + )
{
var senderName = request . SenderNames [ i ] ;
IPattern pattern = null ;
if ( apProvider . GetPatternMethod ! = null )
{
pattern = apProvider . GetPatternMethod ( request . NotifyAction , senderName , request ) ;
}
if ( pattern = = null )
{
pattern = apProvider . GetPattern ( request . NotifyAction , senderName ) ;
2019-11-06 15:03:09 +00:00
}
2019-08-22 15:59:49 +00:00
request . Patterns [ i ] = pattern ? ? throw new NotifyException ( string . Format ( "For action \"{0}\" by sender \"{1}\" no one patterns getted." , request . NotifyAction . ID , senderName ) ) ;
2019-05-15 14:56:09 +00:00
}
}
}
private void PrepareRequestFillTags ( NotifyRequest request )
{
var patternProvider = request . NotifySource . GetPatternProvider ( ) ;
foreach ( var pattern in request . Patterns )
{
IPatternFormatter formatter ;
try
{
formatter = patternProvider . GetFormatter ( pattern ) ;
}
catch ( Exception exc )
{
throw new NotifyException ( string . Format ( "For pattern \"{0}\" formatter not instanced." , pattern ) , exc ) ;
}
var tags = new string [ 0 ] ;
try
{
if ( formatter ! = null )
{
tags = formatter . GetTags ( pattern ) ? ? new string [ 0 ] ;
}
}
catch ( Exception exc )
{
throw new NotifyException ( string . Format ( "Get tags from formatter of pattern \"{0}\" failed." , pattern ) , exc ) ;
}
foreach ( var tag in tags . Where ( tag = > ! request . Arguments . Exists ( tagValue = > Equals ( tagValue . Tag , tag ) ) & & ! request . RequaredTags . Exists ( rtag = > Equals ( rtag , tag ) ) ) )
{
request . RequaredTags . Add ( tag ) ;
}
}
}
private class SendMethodWrapper
{
private readonly object locker = new object ( ) ;
private readonly CronExpression cronExpression ;
private readonly Action < DateTime > method ;
2019-11-06 15:03:09 +00:00
public DateTime ? ScheduleDate { get ; private set ; }
public ILog Log { get ; }
2019-10-17 15:55:35 +00:00
public SendMethodWrapper ( Action < DateTime > method , string cron , ILog log )
2019-05-15 14:56:09 +00:00
{
2019-11-06 15:03:09 +00:00
this . method = method ;
Log = log ;
2019-05-15 14:56:09 +00:00
if ( ! string . IsNullOrEmpty ( cron ) )
{
this . cronExpression = new CronExpression ( cron ) ;
}
UpdateScheduleDate ( DateTime . UtcNow ) ;
}
public void UpdateScheduleDate ( DateTime d )
{
try
{
if ( cronExpression ! = null )
{
ScheduleDate = cronExpression . GetTimeAfter ( d ) ;
}
}
catch ( Exception e )
{
2019-10-17 15:55:35 +00:00
Log . Error ( e ) ;
2019-05-15 14:56:09 +00:00
}
}
public void InvokeSendMethod ( DateTime d )
{
lock ( locker )
2019-11-06 15:03:09 +00:00
{
Task . Run ( ( ) = >
{
try
{
method ( d ) ;
}
catch ( Exception e )
{
Log . Error ( e ) ;
}
2019-08-01 08:47:15 +00:00
} ) . Wait ( ) ;
2019-05-15 14:56:09 +00:00
}
}
public override bool Equals ( object obj )
{
2019-08-01 08:47:15 +00:00
return obj is SendMethodWrapper w & & method . Equals ( w . method ) ;
2019-05-15 14:56:09 +00:00
}
public override int GetHashCode ( )
{
return method . GetHashCode ( ) ;
}
}
}
}