先来看看最终效果吧
首先是正常情况下C#的linq代码:
var query = db.Users.Where(p => p.Name == "admin");
再看ruby调用linq的代码:
query = $db.Users.where { self.Name == 'admin' }
还原度是不是相当高?
下面来看看怎么实现的
首先来分析C#的Where调用参数: p=>p.Name=="admin"
这段代码经过编译器大概会经过以下的操作, 编译成一个ExpressionTree:
var param = Expression.Parameter(typeof(User));
var exp = Expression.PropertyOrField(param, "Name");
var exp2 = Expression.Equal(exp, Expression.Constant("admin"));
var exp3 = Expression.Lambda<Func<User, bool>>(exp2, param);
熟悉这个转换的人如果听到说要用ruby来实现相同的语法, 可能第一感觉就是: 不用想了, 做不到的, 你知道编译器做了多少工作么?
当然我们要抛弃这种僵化的思维来动手试试, 毕竟ruby的魔法是很厉害的
回头看看我们想要的最终效果: $db.Users.where, 这样我们需要一个小写的where方法, 这个可以直接使用c#的扩展方法来实现
当代码进入where的时候, 我们得到了db.Users, 它的类型是IQueryable<User>, 也就是说, 我们得到了User这个类型
这样, 4座大山的第一座也搞定了
如果对象调用了一个未定义的方法, 就会把方法名和参数被转发到method_missing中, 如下:
class User
def method_missing(method,*args)
puts "User类没有#{method}这个方法哦"
end
end
User.new.Name #输出: User类没有Name这个方法哦
Expression.PropertyOrField(param, "Name")
这样我们搞定了上面4座大山的第二座
再看: self.Name == 'admin', 我们需要让==运算符执行时来到自己的代码中, 理论上ruby和C#都可以做到
我们重载一个==运算符, 在这个重载里面, 我们能得到运算符左右2边以及运算符本身, 一共3个信息, 足以推翻第三座大山了
到了最后一座大山, 我们已经不需要更多的信息, 直接在where方法里面写出来就行了
已经有了充足的理论, 下面来看看最终的实现代码
由于IronRuby在实现上有一些限制, 导致只用上了ruby的一个魔法, 一开始是打算还要用上openclass和运算符重载的, 这2个功能都用c#来实现了
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using Microsoft.Scripting.Hosting;
using IronRuby.Runtime;
using IronRuby;
using Microsoft.Scripting.Hosting.Providers;
using Microsoft.Scripting;
using IronRuby.Builtins;
using System.Linq.Expressions;
namespace IronRubyTest
{
class Program
{
static void Main(string[] args)
{
var db = new Context();
Console.WriteLine("正常调用linq: ");
var query = db.Users.Where(p => p.Name == "admin");
Console.WriteLine(query);
Console.WriteLine();
Console.WriteLine("Ruby调用Linq: ");
var ctx = new ServiceScriptContext();
ctx.Context.SetGlobalVariable(null, "db", db);
ctx.Engine.Execute(@"
query = $db.Users.where { self.Name == 'admin' }
System::Console.WriteLine query
");
}
}
public class RubyExpressionTree
{
public ParameterExpression[] Param { get; set; }
public Expression Exp { get; set; }
public RubyExpressionTree GetField(string field) => Fork(p => Expression.PropertyOrField(p, field));
public RubyExpressionTree Fork(Func<Expression, Expression> newExp)
{
return new RubyExpressionTree
{
Param = Param,
Exp = newExp(Exp)
};
}
static object Ruby2CSObject(object obj, Type type)
{
return Convert.ChangeType(obj.ToString(), type);
}
public static RubyExpressionTree operator ==(RubyExpressionTree tree, object obj) => tree.Fork(p => Expression.Equal(p, Expression.Constant(Ruby2CSObject(obj, p.Type))));
public static RubyExpressionTree operator !=(RubyExpressionTree tree, object obj) => tree.Fork(p => Expression.NotEqual(p, Expression.Constant(Ruby2CSObject(obj, p.Type))));
public static RubyExpressionTree Create<T>()
{
var exp = Expression.Parameter(typeof(T));
return new RubyExpressionTree
{
Exp = exp,
Param = new ParameterExpression[] { exp }
};
}
}
public static class RubyExt
{
public static IQueryable<T> where<T>(this IQueryable<T> src, BlockParam block)
{
var exp = RubyOps.Yield0(block.Proc, RubyExpressionTree.Create<T>(), block) as RubyExpressionTree;
return src.Where(Expression.Lambda<Func<T, bool>>(exp.Exp, exp.Param));
}
}
public class Context : DbContext
{
public DbSet<User> Users { get; set; }
public Context()
{
Database.SetInitializer<Context>(null);
}
}
public class User
{
public Guid ID { get; set; } = Guid.NewGuid();
public string Name { get; set; }
public int Age { get; set; }
}
public class ServiceScriptContext
{
public ScriptRuntime Runtime { get; private set; }
public RubyContext Context { get; private set; }
public ScriptEngine Engine { get; private set; }
public static readonly string MainScriptPath = @"main.rb";
public ServiceScriptContext()
{
var setup = ScriptRuntimeSetup.ReadConfiguration();
setup.AddRubySetup();
setup.PrivateBinding = true;
Runtime = Ruby.CreateRuntime(setup);
Engine = Ruby.GetEngine(Runtime);
Context = (RubyContext)HostingHelpers.GetLanguageContext(Engine);
ExecuteFile(MainScriptPath);
}
public dynamic ExecuteFile(string file, ScriptScope scope = null)
{
return Engine.CreateScriptSourceFromFile(file, Encoding.UTF8, SourceCodeKind.File).Execute(scope ?? Runtime.Globals);
}
}
}
load_assembly 'IronRubyTest'
include IronRubyTest
using_clr_extensions IronRubyTest
class RubyExpressionTree
def method_missing(method,*args)
GetField method
end
end
正常调用linq:
SELECT
[Extent1].[ID] AS [ID],
[Extent1].[Name] AS [Name],
[Extent1].[Age] AS [Age]
FROM [dbo].[Users] AS [Extent1]
WHERE N'admin' = [Extent1].[Name]
Ruby调用Linq:
SELECT
[Extent1].[ID] AS [ID],
[Extent1].[Name] AS [Name],
[Extent1].[Age] AS [Age]
FROM [dbo].[Users] AS [Extent1]
WHERE N'admin' = [Extent1].[Name]
请按任意键继续. . .
一模一样, 对吧