Shashlik.Kernel - dotnet-shashlik/shashlik GitHub Wiki

Shashlik.Kernel

Features

  • 配置类自动装载
  • 约定式服务注册
  • 服务注册条件
  • 自动装配
  • 全局锁

AddShashlik

service.AddShashlik(Configuration);这一句代码就完成了整个 Shashlik 装配处理,但你也有必要了解其内部处理流程。

    /// <summary>
    /// AddShashlik,执行典型的shashlik服务配置
    /// </summary>
    /// <param name="services"></param>
    /// <param name="rootConfiguration">根配置</param>
    /// <param name="dependencyContext">依赖上下文,null使用默认配置</param>
    /// <param name="disableAutoOptionTypes">禁用自动装配的option类型</param>
    /// <returns></returns>
    public static IServiceCollection AddShashlik(
        this IServiceCollection services,
        IConfiguration rootConfiguration,
        DependencyContext? dependencyContext = null,
        params Type[] disableAutoOptionTypes)
    {
        return services.AddShashlikCore(rootConfiguration, dependencyContext)
            // 配置装载
            .AutowireOptions(disableAutoOptionTypes)
            // 注册约定的服务
            .RegistryConventionServices()
            // 自动服务装配
            .AutowireServices()
            // 执行服务过滤
            .DoFilter();
    }
  1. AddShashlikCore:注册 Shashlik 核心服务。dependencyContext:Shashlik 扫描的整个应用依赖上下文,建议使用默认 null 即默认的依赖上下文。
  2. AutowireOptions: 开始处理配置文件自动装载,该方法执行后就可以直接注入 IOptions 相关接口获取配置了。disableAutoOptionTypes:需要禁用自动装配的配置类,如果你想手动配置某个类型可以通过此参数禁用。
  3. RegistryConventionServices: 按约定方式注册所有的服务,即包含TransientSingletonScoped特性的类和接口。
  4. AutowireServices: 自动装配。即继承自IAutowire的类型。
  5. DoFilter: 执行服务注册条件过滤。

UseShashlik

启动 Shashlik。主要执行IServiceProviderAutowire的自动装配,以及为 Shashlik 提供扩展基础(如 EfCore 的自动迁移、AspNetCore 相关装配),并注册静态GlobalKernelServiceProvider全局根IServiceProvider

    public static IKernelServiceProvider UseShashlik(this IServiceProvider serviceProvider)
    {
        var kernelServiceProvider = new InnerKernelServiceProvider(serviceProvider);
        kernelServiceProvider.AutowireServiceProvider();
        GlobalKernelServiceProvider.InitServiceProvider(kernelServiceProvider);
        return kernelServiceProvider;
    }

配置类自动装载

使用 Shashlik 前:

    service.Configure<AMapOptions>(Configuration.GetSection("Shashlik.AMap"));

当你的应用中充满大量配置类时,你就会有大量的这种配置代码,现在使用AutoOptions吧,做到 0 代码配置装载。

使用 Shashlik 后:

    [AutoOptions("Shashlik.AMap")]
    public class AMapOptions{}

AutoOptions 特性

构造函数:public AutoOptionsAttribute(string section, bool supportDot = true)

  • string section: 配置阶段名称
  • bool supportDot:是否支持将.转换为:,一般.更符合配置习惯,如果配置节点名称本身包含:那你需要设为false

约定式服务注册

Shashlik 已定义TransientSingletonScoped三个特性来用于注册你的服务,如果还不满足你的骚操作,你可以自行实现ServiceAttribute类。

服务注册规则

两个重要参数

  • IgnoreServices:需要忽略的服务类型集合
  • RequireRegistryInheritedChain:是否需要注册整个继承链,仍然会忽略IgnoreServices定义的忽略类型。

重要规则

  1. 特性使用在接口或抽象类上:将注册接口和最终实现类(继承链没有子类)为服务。
  2. 特性使用在实现类上:注册该类所有的父类、接口为服务,不会注册子类
  3. 允许规则1 和规则2组合,即继承链上可以同时包含多个特性标签,Shashlik 将组合注册这些服务。
  4. 一个继承链上不允许包含多种生命周期类型:比如如下代码是不允许的:
        [Singleton]
        public interface IA {}
        [Transient]
        public class A : IA {}

下面通过实际例子来说明这些规则。

  • 注册自己
    [Singleton]
    public class A {}

    GetService<A>().ShouldBeOfType<A>();
  • 使用在接口上,注册接口和最终实现类
    [Singleton]
    public interface IA {}
    public class A : IA {}

    // 注册 IA服务,实现类为A
    GetService<IA>().ShouldBeOfType<A>();
    GetService<A>().ShouldBeOfType<A>();
  • 使用在抽象类上,注册接口和最终实现类
    [Singleton]
    public abstract class AA {}
    public class A : AA {}

    // 注册 IA服务,实现类为A
    GetService<AA>().ShouldBeOfType<A>();
    GetService<A>().ShouldBeOfType<A>();
  • 使用在接口上,仅注册接口和最终实现类
    [Singleton]
    public interface IA {}
    public abstract class AA : IA {}
    public class A : AA {}

    // 注册 IA服务,实现类为A
    GetService<IA>().ShouldBeOfType<A>();
    // 不会注册中间的类型和接口,如果需要同时注册AA,可以在AA上同时添加Singleton或者A类型上添加Singleton,再或者设置RequireRegistryInheritedChain=true
    GetService<AA>().ShouldBeNull();
    GetService<A>().ShouldBeOfType<A>();
  • 使用在实现类上,注册自身和所有父类、接口
    public interface IA {}
    [Singleton]
    public class A : IA {}

    // 注册IA和A为服务,实现类为A
    GetService<IA>().ShouldBeOfType<A>();
    GetService<A>().ShouldBeOfType<A>();
  • 使用在接口上,注册最终实现类。
    [Singleton]
    public interface IA {}
    public class A : IA {}
    public class B : A {}

    // 注册IA和B为服务,实现类为B
    GetService<IA>().ShouldBeOfType<B>();
    GetService<B>().ShouldBeOfType<B>();
    // 并不会注册A,因为A并不是最终实现类
    GetService<A>().ShouldBeNull();
  • 泛型,会注册匹配的泛型定义最终实现类,以及对应的泛型实现
    [Singleton]
    public interface IA<T> {}
    public class A<T> : IA<T> {}
    public class B : IA<int> {}

    // A<T>和IA<T>是匹配的泛型定义,等同于注册service.AddSinleton(typeof(IA<>), typeof(T<>));
    GetServices<IA<int>>().Any(r => r.GetType() == typeof(A<int>)).ShouldBeTrue();
    // B实现了IA<int>,等同于service.AddSinleton<IA<int>, B>();
    GetServices<IA<int>>().Any(r => r.GetType() == typeof(B)).ShouldBeTrue();
    GetServices<IA<string>>().Any(r => r.GetType() == typeof(A<int>)).ShouldBeTrue();
    GetServices<IA<string>>().Any(r => r.GetType() == typeof(B)).ShouldBeFalse();
    GetService<A<int>>().ShouldBeNotNull();
    GetService<A<string>>().ShouldBeNotNull();
    GetService<B>().ShouldBeNotNull();
  • 忽略服务类型,某些服务需要忽略时使用,必须继承链中有 IDispose 等完全没必要注册的接口
    [Singleton(typeof(IA))]
    public interface IA {}
    public class A : IA {}

    // 忽略IA类型服务
    GetService<IA>().ShouldBeNull();
    // 只会注册最终实现类
    GetService<A>().ShouldBeOfType<A>();
  • IgnoreServices忽略服务类型,某些服务需要忽略时使用,比如继承链中有 IDispose 等完全没必要注册的接口
    [Singleton(typeof(IA))]
    public interface IA {}
    public class A : IA {}

    // 忽略IA类型服务
    GetService<IA>().ShouldBeNull();
    // 只会注册最终实现类
    GetService<A>().ShouldBeOfType<A>();
  • RequireRegistryInheritedChain注册整个继承链
    public interface IA : IDispose{}
    public interface IB : IDispose{}
    // 注册整个继承链,但是忽略IDispose
    [Singleton(typeof(IDispose), RequireRegistryInheritedChain = true)]
    public abstract class A : IA {}
    public class B : A {}

    GetService<IDispose>().ShouldBeNull();
    GetService<IA>().ShouldBeOfType<B>();
    GetService<A>().ShouldBeOfType<B>();
    GetService<B>().ShouldBeOfType<B>();
    // IB和A并不是同一继承链,他们没任何关系,不会注册
    GetService<IB>().ShouldBeNull();

LatestImplementation

特性LatestImplementationAttribute用于标记实现类作为最后的实现类,一般用于某接口有多个实现时需要指定某个类为最后的实现,在依赖注入系统中,一个服务可能会有多个实现,但最后使用时GetService都是获取的最后一个实现类。那么这个特性就是满足这个需求。一个服务有多个实现类都定义了LatestImplementationAttribute将变得毫无意义且最后的结果可能不是预期的。例:

    [Singleton]
    public interface ICache {}

    public class MemoryCache : ICache {}

    // 将RedisCache标记为最后实现类
    [LatestImplementation]
    public class RedisCache : ICache {}

    public class MemrcachedCache : ICache {}

    GetService<ICache>().ShouldBeOfType<RedisCache>();

最佳实践

  1. 应该尽量把约定特性使用在实现类上,显示的通过IgnoreServices参数忽略掉不需要的注册的服务,因为接口和抽象类的实现类一般都是未知的,他们的继承链情况也是未知的。
  2. 如果你设计的接口需要实现自动注册,可以在接口上使用约定特性,那么通过 GetServices 方法可以获取到所有的注册服务,这经常用于自动化处理。例,设计一个付款接口,付款方式可以有很多种。
//
[Singleton]
public interface IPay{

    // 支付通道
    string PayChannel { get; }

    // 请求支付数据
    PayData GetPayData(PayInput input);
}
public class WxJsapiPay : IPay {}
public class WxAppPay : IPay {}
public class AliH5Pay : IPay {}
public class WxAppPay : IPay {}

这里就注册四种支付通道,用户在实际支付时,传入支付参数payChannel就可以获取到具体的支付方式。

var pay = GetServices<IPay>().First(r => r.PayChannel == payChannel);
pay.GetPayData(input);

服务注册条件

有些类型我们希望在满足一定条件下才进行服务注册,这个时候就可以使用条件注册。比如当系统中不存在 ICache 服务时,才注册 MemoryCache。

所有的条件注册特性都继承自ConditionBaseAttribute仅允许使用在实现类上,可以任意扩展,Shashlik 已实现如下条件特性:

  • ConditionDependsOn:当服务类型存在时
    • Type[] Types: 服务类型
    • ConditionType ConditionType:条件类型,ALL:所有服务类型都存在时才注册,ANY:只要某一个服务类型存在时就注册,默认值ALL
  • ConditionDependsOnMissing:当服务类型不存在时
    • Type[] Types: 服务类型
    • ConditionType ConditionType:条件类型,ALL:必须所有服务类型都不存在时才注册,ANY:只要某一个服务类型不存在时就注册,默认值ALL
  • ConditionOnHostEnvironment:当前运行环境满足条件时
    • string EnvName: 运行环境值,比如当前环境为Production
    • bool IgnoreCase: 是否忽略大小写,默认true
  • ConditionOnProperty:当配置数据的值满足条件时
    • string Property: 属性名称,例:Shashlik.Sms.Enable
    • Type ValueType: 条件值的类型
    • object? Value: 条件值
    • object? DefaultValue: 默认条件值,当不存在该配置数据时的值,默认为 null
    • bool IgnoreCase: 当条件值类型为string时,是否忽略大小写,默认true
    • bool SupportDot: 是否支持将.转换为:,一般.更符合配置习惯,如果配置节点名称本身包含:那你需要设为false,默认true

例:

// 当不存在ICache时,注册MemoryCache
[ConditionDependsOnMissing(typeof(ICache))]
[Singleton]
public class MemoryCache : ICache {}

// 当系统中存在CSRedisClient服务才注册RedisLock
[ConditionDependsOn(typeof(CSRedisClient))]
[Singleton]
public class RedisLock : ILock {}

// 当前环境为测试环境时注册EmptySms
[ConditionOnHostEnvironment("Test")]
[Singleton]
public class EmptySms : ISms {}

// 当属性值Shashlik.Sms.Enable为true时,注册AliyunSms
[ConditionOnProperty(typeof(bool), "Shashlik.Sms.Enable", true, DefaultValue = true)]
[Singleton]
public class AliyunSms : ISms {}

自动装配

Shashlik 的自动装配和 Spring 框架的自动不是一个概念,如果你熟悉 Abp 框架,你可以理解为 Abp 的 Module 类型,但却比 Abp 的更加强大、灵活。

下面通过 Shashlik.Redis 的自动装配例子来说明:

    [AutoOptions("Shashlik.Redis")]
    public class RedisOptions
    {
        /// <summary>
        /// 是否启用
        /// </summary>
        public bool Enable { get; set; } = true;

        /// <summary>
        /// 连接字符串
        /// </summary>
        public string? ConnectionString { get; set; }

        /// <summary>
        /// 哨兵配置
        /// </summary>
        public string[] Sentinels { get; set; } = new string[0];

        /// <summary>
        /// 只读
        /// </summary>
        public bool Readonly { get; set; }

        /// <summary>
        /// 自定义Client创建,高优先级
        /// </summary>
        public Func<CSRedisClient>? CSRedisClientFactory { get; set; }
    }

    /// <summary>
    /// redis自动装配,装配顺序200
    /// </summary>
    [Order(200)]
    public class RedisAutowire : IServiceAutowire
    {
        public RedisAutowire(IOptions<RedisOptions> options)
        {
            Options = options.Value;
        }

        private RedisOptions Options { get; }

        public void Configure(IKernelServices kernelService)
        {
            if (!Options.Enable)
                return;

            var csRedis = Options.CSRedisClientFactory?.Invoke() ?? new CSRedisClient(Options.ConnectionString, Options.Sentinels, Options.Readonly);

            RedisHelper.Initialization(csRedis);
            kernelService.Services.AddSingleton(csRedis);
            kernelService.Services.TryAddSingleton<IRedisSnowflakeId, DefaultRedisSnowflakeId>();
            kernelService.Services.AddSingleton<IDistributedCache>(
                new Microsoft.Extensions.Caching.Redis.CSRedisCache(RedisHelper.Instance));
        }
    }

首先约定式服务注册不是万能的,在引用一些三方框架时,就需要手动去注册一些服务,这里我们就需要手动注册CSRedisClientCSRedisCache。首先 Shashlik.Redis 设计必须是可复用,也不希望使用方自己来注册这些服务,只要引用了 Shashlik.Redis 并且没有关闭 Redis(Shashlik.Redis.Enable),那么这些服务的注册希望完全自动化,也就是使用者 0 代码,这就是IServiceAutowire服务自动装配的使用。我们可以在不同的场景定义、使用不同的自动装配。

自动装配类型结构:

  • IAutoWire:自动装配基础接口,已定义约定[Transient(typeof(IAutowire), RequireRegistryInheritedChain = true)],即所有的自动装配类型都将自动完成注册。
    • IServiceAutoWire:服务自动装配,在服务注册阶段,在 AddShashlik 的.AutowireServices()开始装配,一般用于模块的配置、服务注册。当约定式服务注册不能满足需求时,一般使用IServiceAutoWire来处理。
      • RedisAutowire: Redis 自动装配。
      • IdentityAutowire:AspNetCore.Identity 自动装配。
      • Ids4Autowire:IdentityServer4 自动装配。
      • 其它......
    • IServiceProviderAutowire:IServiceProvider 自动装配,服务配置已完成,在UseShashlik时装配。
    • IApplicationStartAutowire:Host 启动时装配。
    • IApplicationStopAutowire:Host 停止时装配。

自定义装配

简单的自定义扩展都可以使用以下两个扩展方法:

  • 基于 IKernelServices 的自动装配,主要用于服务注册阶段的装配。
    public static IKernelServices Autowire<T>(this IKernelServices kernelServices, Action<T> autowireAction) where T : IAutowire
  • 基于 IKernelServiceProvider 的自动装配,主要用于应用配置阶段的装配。
    public static IKernelServiceProvider Autowire<T>(this IKernelServiceProvider kernelServiceProvider, Action<T> autowireAction) where T : IAutowire

还不满足你的需求?你可以使用最基础的IAutowireProvider<T>接口来完成你的装配任务。例:

    public class ApplicationLifetimeHostedService : IHostedService
    {
        public ApplicationLifetimeHostedService(
            IAutowireProvider<IApplicationStartAutowire> applicationStartAutowireProvider,
            IAutowireProvider<IApplicationStopAutowire> applicationStopAutowireProvider,
            IKernelServices kernelServices,
            IServiceProvider serviceProvider)
        {
            ApplicationStartAutowireProvider = applicationStartAutowireProvider;
            ApplicationStopAutowireProvider = applicationStopAutowireProvider;
            KernelServices = kernelServices;
            ServiceProvider = serviceProvider;
        }

        private IServiceProvider ServiceProvider { get; }
        private IAutowireProvider<IApplicationStartAutowire> ApplicationStartAutowireProvider { get; }
        private IAutowireProvider<IApplicationStopAutowire> ApplicationStopAutowireProvider { get; }
        private IKernelServices KernelServices { get; }

        public Task StartAsync(CancellationToken cancellationToken)
        {
            // 自定义装配IApplicationStartAutowire
            var dic = ApplicationStartAutowireProvider.Load(KernelServices, ServiceProvider);
            ApplicationStartAutowireProvider.Autowire(dic,
                async r => await r.ServiceInstance.OnStart(cancellationToken));
            return Task.CompletedTask;
        }

        public Task StopAsync(CancellationToken cancellationToken)
        {
            // 自定义装配IApplicationStartAutowire
            var dic = ApplicationStopAutowireProvider.Load(KernelServices, ServiceProvider);
            ApplicationStopAutowireProvider.Autowire(dic,
                async r => await r.ServiceInstance.OnStop(cancellationToken));
            return Task.CompletedTask;
        }
    }

这里以 Shashlik.Cap 对 CAP 框架的集成来说吧,集成 CAP 需要配置消息存储和消息中间件等配置,常规用法我们需要再 ConfigureServices 中配置,如下:

service.AddCap(options => {
    options.Version = "Default";
    options.UseMySql("...");
    options.RabbitMQ("...");
});

Shashlik.Cap 是不存在对 MySql 和 RabbitMQ 引用的,最后用户会用什么技术是未知的,那么如何将这些配置下放呢?这里就可以使用自动装配。

  1. 定义装配接口ICapAutowire
    /// <summary>
    /// cap 自动装配,主要是配置cap
    /// </summary>
    public interface ICapAutowire : IAutowire
    {
        void Configure(CapOptions capOptions);
    }
  1. 在 CAP 的 service 装配类中完成ICapAutowire的装配。
 /// <summary>
    /// event bus自动装配,装配顺序600
    /// </summary>
    [Order(600)]
    public class CapAutowire : IServiceAutowire
    {
        public CapAutowire(IOptions<ShashlikCapOptions> capOptions)
        {
            CapOptions = capOptions.Value;
        }

        private ShashlikCapOptions CapOptions { get; }

        public void Configure(IKernelServices kernelService)
        {
            if (!CapOptions.Enable)
                return;

            kernelService.Services.TryAddSingleton<IEventPublisher, DefaultEventPublisher>();
            kernelService.Services.TryAddSingleton<INameRuler, DefaultNameRuler>();

            kernelService.Services.AddCap(r =>
            {
                CapOptions.CopyTo(r, true);
                // 装配ICapAutowire
                kernelService.Autowire<ICapAutowire>(a => a.Configure(r));
            });

            var exists = kernelService.Services.FirstOrDefault(r => r.ServiceType == typeof(IConsumerServiceSelector));
            if (exists != null)
                kernelService.Services.Remove(exists);

            kernelService.Services.Add(ServiceDescriptor.Describe(typeof(IConsumerServiceSelector),
                typeof(CapConsumerServiceSelector), ServiceLifetime.Singleton));
        }
    }

重点就是kernelService.Autowire<ICapAutowire>(a => a.Configure(r));这句代码,将自动装载所有的ICapAutowire实现类并调用Configure配置方法。

So easy,你也可以根据你的场景定制你的自动装配。

装配顺序

也许你已经注意到了Order特性了,这个特性就是用来处理装配顺序的,对应的还有AfterAtBeforeAt一个类型不要实现多个装配接口,这会影响装配逻辑。

举个例来说,我们设计一个 Redis 缓存库,比如在 Redis 程序集配置完成,也就是RedisAutowire执行完成后,否则就可能因为 Redis 未完成初始化而产生错误。

    [AfterAt(typeof(RedisAutowire))]
    pubic class RedisCacheAutowire : IServiceAutowire
    {
        // 配置代码
    }
  • Order:按照序号进行装配,从小达到排序。
  • AfterAt:在指定的装配类型之后,优先级比Order高。
  • BeforeAt:在指定的装配类型之前,优先级比Order高。

ILock

一个应用中通常都需要用到锁的场景,Shashlik提供通用的全局锁接口抽象,定义如下:

    /// <summary>
    /// 抽象的分布式锁,kernel内部内存锁实现
    /// </summary>
    [Singleton]
    public interface ILock
    {
        /// <summary>
        /// 开始锁定,如果在<paramref name="waitTimeoutSeconds"/>时间内未获取到锁,将抛出锁失败异常(<see cref="Shashlik.Kernel.Exceptions.LockFailureException"/>)
        /// </summary>
        /// <param name="key">锁key</param>
        /// <param name="lockSeconds">锁定时长, 秒</param>
        /// <param name="autoDelay">是否自动延期</param>
        /// <param name="waitTimeoutSeconds">等待锁的超时时间, 秒</param>
        /// <returns>锁释放实例</returns>
        /// <exception cref="Shashlik.Kernel.Exceptions.LockFailureException">锁失败异常</exception>
        IDisposable Lock(string key, int lockSeconds, bool autoDelay = true, int waitTimeoutSeconds = 60);
    }

Shashlik.Kernel内部已有基于内存的实现MemoryLock,同时Shashlik.Redis有Redis版本的实现,可以实现分布式锁。当然你可以自行实现该接口,然后什么都不用干,就会自动注册。

⚠️ **GitHub.com Fallback** ⚠️