c# - 当实体匹配用户定义的查询/过滤器/规则时执行操作

标签 c# .net notifications rule-engine

通常,您编写一个查询并获取与其匹配的所有记录(实体)。我需要做相反的事情。

假设我有 100 万客户,他们有几十个非规范化属性:

public class Customer {
  public string Name {get;set;}
  public string Email {get;set;}
  public string Phone {get;set;}
  public DateTime Birthday {get;set;}
  public DateTime LastEmailed {get;set;}
  public DateTime LastCalled {get;set;}
  public int AgeInYears {get { return DateTime.UtcNow.Year - birthdate.Year;}}
  public int SalesTerritoryId {get;set;}
  // etc.

}

我有 1 万名用户想要设置自定义过滤器,并在任何新客户符合他们定义的规则时收到通知。

其中一些规则在创建/更新客户时进行评估(例如)

  • 有电话号码并且在我的销售区域内的客户。
  • 具有电子邮件地址且 LastEmailed 为 NULL 且销售区域在 (1, 7, 11) 中的客户

其他规则将定期运行(例如)

  • 今天生日的客户。

每天都会为客户节省数百万笔费用,并针对每个新/更新的客户检查 5-10,000 个自定义过滤器。

我意识到我可以使用 Expression Trees对于用户的过滤器,但最终会做这样的事情:

public class CustomerRule : IRule {

  public bool IsMatch() {
    // Expression Tree Stuff
  }

  public bool DoAction() {
    // Notification Stuff
  }
}

public class CustomerService {

  public void SaveOrUpdate {
    IList<IRule> rules = GetRules();

    // this isn't going to handle 1M save/updates * 10k rules very well
    foreach (var rule in rules){
      if(rule.IsMatch()) {
        rule.DoAction();
      }          
    }      
  }
}

我知道其他人已经解决了这个问题,但我很难弄清楚到底要寻找什么。赞赏一般指导,具体模式、代码、工具等更好。我们主要使用 C#,但如果需要,也可以走出 .NET 世界。

最佳答案

我会提到与其他答案不同的观点。您在代码中声称

// this isn't going to handle 1M save/updates * 10k rules very well

但是你真的验证了吗?考虑这段代码:

public class Program {
    static List<Func<Customer, bool>> _rules = new List<Func<Customer, bool>>();
    static void Main(string[] args) {
        foreach (var i in Enumerable.Range(0, 10000)) {
            // generate simple expression, but joined with OR conditions because 
            // in this case (on random data) it will have to check them all
            // c => c.Name == ".." || c.Email == Y || c.LastEmailed > Z || territories.Contains(c.TerritoryID)

            var customer = Expression.Parameter(typeof(Customer), "c");
            var name = Expression.Constant(RandomString(10));
            var email = Expression.Constant(RandomString(12));
            var lastEmailed = Expression.Constant(DateTime.Now.AddYears(-20));
            var salesTerritories = Expression.Constant(Enumerable.Range(0, 5).Select(c => random.Next()).ToArray());
            var exp = Expression.OrElse(Expression.OrElse(Expression.OrElse(
            Expression.Equal(Expression.PropertyOrField(customer, "Name"), name),
            Expression.Equal(Expression.PropertyOrField(customer, "Email"), email)),
            Expression.GreaterThan(Expression.PropertyOrField(customer, "LastEmailed"), lastEmailed)),
            Expression.Call(typeof(Enumerable), "Contains", new Type[] {typeof(int)}, salesTerritories, Expression.PropertyOrField(customer, "SalesTerritoryId")));
            // compile
            var l = Expression.Lambda<Func<Customer, bool>>(exp, customer).Compile();
            _rules.Add(l);
        }

        var customers = new List<Customer>();
        // generate 1M customers
        foreach (var i in Enumerable.Range(0, 1_000_000)) {
            var cust = new Customer();
            cust.Name = RandomString(10);
            cust.Email = RandomString(10);
            cust.Phone = RandomString(10);
            cust.Birthday = DateTime.Now.AddYears(random.Next(-70, -10));
            cust.LastEmailed = DateTime.Now.AddDays(random.Next(-70, -10));
            cust.LastCalled = DateTime.Now.AddYears(random.Next(-70, -10));
            cust.SalesTerritoryId = random.Next();
            customers.Add(cust);
        }
        Console.WriteLine($"Started. Customers {customers.Count}, rules: {_rules.Count}");
        int matches = 0;
        var w = Stopwatch.StartNew();
        // just loop
        Parallel.ForEach(customers, c => {
            foreach (var rule in _rules) {
                if (rule(c))
                    Interlocked.Increment(ref matches);
            }
        });
        w.Stop();
        Console.WriteLine($"matches {matches}, elapsed {w.ElapsedMilliseconds}ms");
        Console.ReadKey();
    }

    private static readonly Random random = new Random();
    public static string RandomString(int length)
    {
        const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        return new string(Enumerable.Repeat(chars, length)
          .Select(s => s[random.Next(s.Length)]).ToArray());
    }
}

public class Customer {
    public string Name { get; set; }
    public string Email { get; set; }
    public string Phone { get; set; }
    public DateTime Birthday { get; set; }
    public DateTime LastEmailed { get; set; }
    public DateTime LastCalled { get; set; }

    public int AgeInYears
    {
        get { return DateTime.UtcNow.Year - Birthday.Year; }
    }

    public int SalesTerritoryId { get; set; }
}

这里我以表达式的形式生成了 10K 条规则。它们很简单,但并非微不足道 - 4 个条件与 OR、字符串、日期、包含相结合。然后我生成 100 万个客户更新(您数据库中的客户数量无关紧要 - 我们只处理更新)并运行一个循环。猜猜我的常规(非服务器)PC 需要多长时间? 4 分钟。

因此,您可以在短短 4 分钟内检查一整天所有客户更新的所有规则(在适当的服务器上,它应该至少比这快 2 倍,可能更多)。根据 10K 规则检查单个更新需要几毫秒。鉴于此 - 你很可能会在任何其他地方遇到瓶颈,而不是在这里。如果您愿意,您可以在此基础上应用一些简单的优化:

  • 折叠相同的规则。无需为每个用户检查“今天是生日”规则。

  • 存储规则中使用的属性,并注意在客户中更新了哪些列。不要运行不使用客户中更新的列的规则。

但实际上这甚至可能会减慢你的速度,而不是加快速度,所以一切都应该衡量。

不要从进行规则检查的同一代码发送通知。将它们放入队列,让其他进程\线程处理它们。检查规则是严格的 CPU 绑定(bind)工作,发送通知(我假设,在你的情况下)是 IO 绑定(bind)的,所以你实际上可以在一台机器上,在一个进程中做到这一点。您也不希望以这种速度向给定用户发送垃圾邮件通知,您很可能会分批发送它们,我认为最多每分钟一批,所以这不会太昂贵。

至于客户更新本身——您可以将它们存储在某个队列中(如 rabbitMQ),使用数据库通知(如 postgresql pg_notify)或每分钟轮询一次数据库以获取该期间的所有更新。同样,应该衡量不同方法的性能。

除此之外,这种任务很容易在多台机器上并行化,因此如果您要达到 1 亿客户 - 没问题,您可以再添加一台服务器(或者可能一台仍然没问题)。

关于c# - 当实体匹配用户定义的查询/过滤器/规则时执行操作,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/43325292/

相关文章:

c# - 无法掌握 Freeze/Inject/Register 之间的区别

c# - 如何将运行时已知的类型转换为给定类型的列表?

.NET 国际奥委会 : Preconfiguring library components for easier use

c# - 为什么 'http://dd ' 是一个有效的 URL?

iphone - iPhone应用程序在屏幕上时是否可以弹出本地通知?

c# - 如何找到日期字符串的确切格式?

c# - CQRS:在没有事件源的情况下更新读取模型

c# - 对实现接口(interface)的对象进行 LINQ 查询

android向用户发送语音通知

objective-c - 在哪里将对象注册为通知监听器? - 最大化性能!