欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

ServiceStack 多租户的实现方案

程序员文章站 2022-04-08 19:12:02
以SqlServer为例子说明ServiceStack实现多租户,在SqlServer中创建4个Database:TMaster、T1,T2,T3,为了安全起见 每个Database不用sa账号,而是用独立的数据库的账号和密码,为了方便演示这密码设置成一样 租户TMaster Database:TM ......

以sqlserver为例子说明servicestack实现多租户,在sqlserver中创建4个database:tmaster、t1,t2,t3,为了安全起见

每个database不用sa账号,而是用独立的数据库的账号和密码,为了方便演示这密码设置成一样

租户tmaster database:tmaster  账号密码: user id=tmaster;password=t123

租户t1 database:t1  账号密码: user id=t1;password=t123

租户t2 database:t2  账号密码: user id=t2;password=t123

租户t3 database:t3  账号密码: user id=t3;password=t123

创建数据库的方法可以参见文章:  https://www.cnblogs.com/tonge/p/3791029.html

每个登陆用自己的账号和密码登陆,其它的数据库是没有访问权限的,这个各个租户是完全隔离的。

假设node和npm已经安装

npm install -g @servicestack/cli

执行命令dotnet-new selfhost sshost

这样就创建了servicestack的控制台程序,用vs2017解决方案,在servicemodel的types文件夹添加tenantconfig类文件

ServiceStack 多租户的实现方案

代码如下:

using system;
using system.collections.generic;
using system.text;

namespace sstest.servicemodel.types
{
    public interface ifortenant
    {
        string tenantid { get; }
    }

    public class tenantconfig
    {
        public string id { get; set; }

        public string company { get; set; }
    }
}

 

修改hello.cs文件,代码如下:

using servicestack;
using sstest.servicemodel.types;
using system;

namespace sstest.servicemodel
{
    [route("/hello")]
    [route("/hello/{name}")]
    public class hello : ifortenant, ireturn<helloresponse>
    {

        public string name { get; set; }

        // 实现接口ifortenant(租户标识id)
        public string tenantid { get; set; }
}

    public class helloresponse
    {
        public string result { get; set; }

        public datetime date { get; set; }

        // 返回租户公司信息
        public tenantconfig config { get; set; } 
    }
}

 

主程序的startup代码如下

    public class startup
    {
        public void configureservices(iservicecollection services)
        {
        }

        public void configure(iapplicationbuilder app, ihostingenvironment env)
        {
            // jsconfig.datehandler = datehandler.iso8601;
            // 保证时间类型的字段可以解析成js识别的时间类型
            jsconfig<datetime>.serializefn = time => new datetime(time.ticks, datetimekind.local).tostring("o");
            jsconfig<datetime?>.serializefn =
                time => time != null ? new datetime(time.value.ticks, datetimekind.local).tostring("o") : null;
            jsconfig.datehandler = datehandler.iso8601;

            app.useservicestack(new apphost());

            app.run(context =>
            {
                context.response.redirect("/metadata");
                return task.fromresult(0);
            });
        }
    }

 

下面就到核心代码了,在主程序中建立多租户db工程类,让程序可以自动的根据租户id访问自己的数据库

        public class multitenantdbfactory : idbconnectionfactory
        {
            private readonly idbconnectionfactory dbfactory;

            public multitenantdbfactory(idbconnectionfactory dbfactory)
            {
                this.dbfactory = dbfactory;
            }

            public idbconnection opendbconnection()
            {
                var tenantid = requestcontext.instance.items["tenantid"] as string;
                return opentenant(tenantid);
            }

            public idbconnection opentenant(string tenantid = null)
            {
                return tenantid != null
                    ? dbfactory.opendbconnectionstring($"data source=.; initial catalog={tenantid};user id={tenantid};password=t123;pooling=true;")
                    : dbfactory.opendbconnection();
            }

            public idbconnection createdbconnection()
            {
                return dbfactory.createdbconnection();
            }
        }

 

apphost中加入如下代码,globalrequestfilters的作用,根据传入的租户id来选择相应的数据库,如果租户id为null,系统自动使用tmaster数据库

initdb的作用就是初始化三个数据库,创建表tenantconfig并插入一条记录。

        public override void configure(container container)
        {
            coniguresqlserver(container);
        }

        private void coniguresqlserver(container container)
        {
            var dbfactory = new ormliteconnectionfactory(
                "data source=.; initial catalog=tmaster;user id=tmaster;password=t123;pooling=true;", sqlserverdialect.provider);

            const int nooftennants = 3;

            container.register<idbconnectionfactory>(c =>new multitenantdbfactory(dbfactory));

            var multidbfactory = (multitenantdbfactory)container.resolve<idbconnectionfactory>();

            using (var db = multidbfactory.opentenant())
                initdb(db, "tmaster", "masters inc.");

            for(int i=1; i<= nooftennants; i++)
            {
                var tenantid = $"t{i}";

                using (var db = multidbfactory.opentenant(tenantid))
                    initdb(db, tenantid,  $"acme {tenantid} inc.");
            }

            globalrequestfilters.add((req, res, dto) =>
            {
                var fortennant = dto as ifortenant;
                if (fortennant != null)
                    requestcontext.instance.items.add("tenantid", fortennant.tenantid);
            });
        }
        public void initdb(idbconnection db, string tenantid, string company)
        {
            db.dropandcreatetable<tenantconfig>();
            db.insert(new tenantconfig { id = tenantid, company = company });
        }

这样核心代码就完成了,我们用postman调用试试看,是不是达到了预期的效果

body为空,租户id没有设置,系统认为是默认的数据库tmaster,返回的是master数据库中的config表信息

ServiceStack 多租户的实现方案

body中设置json参数{"name":"joy", "tenantid":"t1"},可以看到查询返回的是数据库t1的信息

 ServiceStack 多租户的实现方案

我们再试验一下t2,body中设置json参数{"name":"peter", "tenantid":"t2"}

ServiceStack 多租户的实现方案

可以很惊喜的看到,查询的是数据库t2的信息

servicestack解决方案真是强大,本来一个复杂的多租户问题就这样轻易解决了,是不是很简单。这里例子用的都是sqlserver数据库,实际上每个租户可以使用不同的数据库。

最近一年很流行abp解决方案,我想说的是servicestack解决方案也很优秀,甚至更加优秀,当你了解越多你就会惊叹当初作者的设计思路是多么的优秀,有兴趣的小伙伴可以一起挖掘和分享啊!