aspnetcore插件开发dll热加载 二

这一篇文章应该是个总结。

投简历的时候是不是有人问我有没有abp的开发经历,汗颜!

在各位大神的尝试及自己的总结下,还是实现了业务和主机服务分离,通过dll动态的加载卸载,控制器动态的删除添加。

项目如下:

演示效果:

下面就是代码部分:

重点

1.IActionDescriptorChangeProvider接口,(关于添加删除可以通过后台任务检测刷新,移除控制器操作)

2.builder.Services.AddControllers().ConfigureApplicationPartManager和AssemblyLoadContext搭配加载业务的dll(动态链接库)。

我的业务代码很简单,可能有人要说了,那复杂的业务,有很多业务类,注入这块怎么办,怎么实现整个的调用链。

关于业务和主服务之间的关联代码就在这了

复制代码
namespace ModuleLib
{
    //可以给个抽象类,默认实现。否则各个服务每次实现接口会多做一步删除为实现接口的动作
    public interface IModule
    {
        void ConfigureService(IServiceCollection services, IConfiguration configuration=null);
        void Configure(IApplicationBuilder app, IWebHostEnvironment env = null);
    } 
}

看下面的项目,有没有一点模块化开发的感觉,但是这次分离的很彻底,只需要dll就行,不需要程序集引用。

复制代码
{
  "Modules": [
    {
      "id": "FirstWeb",
      "version": "1.0.0",
      "path": "C:\\Users\\victor.liu\\Documents\\GitHub\\AspNetCoreSimpleAop\\LastModule\\FirstWeb\\bin\\Debug\\net8.0"
    },
    {
      "id": "SecondService",
      "version": "1.0.0",
      "path": "C:\\Users\\victor.liu\\Documents\\GitHub\\AspNetCoreSimpleAop\\LastModule\\SecondService\\bin\\Debug\\net8.0"   //����csproj�ļ�����ָ�����з������ɵ�ָ����һ��Ŀ¼���������
    }
  ]
}

以Assembly为单位做存储

复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace Common
{
    public class ModuleInfo
    {
        public string Id { get; set; }
        public string Name { get; set; }
        public Version Version { get; set; }
        public string Path { get; set; } = "lib";
        public Assembly Assembly { get; set; }
    }
}

在初次加载的时候注入Imodule,并且缓存起来,这样避免了反射的操作,之前的做法是通过反射来拿IModule

复制代码
using Common;
using ModuleLib;
using System.Reflection;

namespace MainHost.ServiceExtensions
{
    public static class InitModuleExt
    {
        public static void InitModule(this IServiceCollection services,IConfiguration configuration)
        {
            var modules = configuration.GetSection("Modules").Get<List<ModuleInfo>>();
            foreach (var module in modules)
            {
                GolbalConfiguration.Modules.Add(module);
                module.Assembly = Assembly.LoadFrom($"{module.Path}\\{module.Id}.dll"); //测试才这么写

                var moduleType = module.Assembly.GetTypes().FirstOrDefault(t => typeof(IModule).IsAssignableFrom(t));
                if ((moduleType != null) && (moduleType != typeof(IModule)))
                {
                    services.AddSingleton(typeof(IModule), moduleType);
                }
            }
        }
    }
}

再看看Program是怎么写的,等等,为什么注释掉了重要的代码呢

复制代码
using BigHost;
using BigHost.AssemblyExtensions;
using Common;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Configuration;
using ModuleLib;
using System.Xml.Linq;
using DependencyInjectionAttribute;

var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
builder.Configuration.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: true);
builder.Configuration.AddJsonFile("appsettings.Modules.json", optional: false, reloadOnChange: true);
//builder.Services.InitModule(builder.Configuration);
//var sp = builder.Services.BuildServiceProvider();
//var modules = sp.GetServices<IModule>();
// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

//最新dotnet没有这些
builder.Services.AddControllers().ConfigureApplicationPartManager(apm =>
{
    var context = new CollectibleAssemblyLoadContext();
    DirectoryInfo DirInfo = new DirectoryInfo(Path.Combine(Directory.GetCurrentDirectory(), "lib"));
    foreach (var file in DirInfo.GetFiles("*.dll"))
    {
        //if(!(file.Name.Contains("Test001Controller") || file.Name.Contains("Test002Controller")))
        //{
        //    continue;
        //}//不能屏蔽掉依赖引用
        var assembly = context.LoadFromAssemblyPath(file.FullName);
        var controllerAssemblyPart = new AssemblyPart(assembly);
        apm.ApplicationParts.Add(controllerAssemblyPart);
        ExternalContexts.Add(file.Name, context);
    }
});
    //builder.Services.AddTransient<IProductBusiness, ProductBusiness>();
    //foreach (var module in modules)
    //{
    //    module.ConfigureService(builder.Services, builder.Configuration);
    //}
    //GolbalConfiguration.Modules.Select(x => x.Assembly).ToList().ForEach(x =>
    //{
    //    builder.Services.ReisterServiceFromAssembly(x);
    //    var controllerAssemblyPart = new AssemblyPart(x);
    //    apm.ApplicationParts.Add(controllerAssemblyPart);
    //    ExternalContexts.Add(x.GetName().Name, context);
    //});
//});
//GolbalConfiguration.Modules.Select(x => x.Assembly).ToList().ForEach(x => builder.Services.ReisterServiceFromAssembly(x));
builder.Services.AddSingleton<IActionDescriptorChangeProvider>(ActionDescriptorChangeProvider.Instance);
builder.Services.AddSingleton(ActionDescriptorChangeProvider.Instance);



var app = builder.Build();
ServiceLocator.Instance = app.Services;
//foreach (var module in modules)
//{
//    module.Configure(app, app.Environment);
//}

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}
app.UseHttpsRedirection();


app.MapGet("/Add", ([FromServices] ApplicationPartManager _partManager, string name) =>
{

    FileInfo FileInfo = new FileInfo(Path.Combine(Directory.GetCurrentDirectory(), "lib/" + name + ".dll"));
    using (FileStream fs = new FileStream(FileInfo.FullName, FileMode.Open))
    {
        var context = new CollectibleAssemblyLoadContext();
        var assembly = context.LoadFromStream(fs);
        var controllerAssemblyPart = new AssemblyPart(assembly);

        _partManager.ApplicationParts.Add(controllerAssemblyPart);

        //ExternalContexts.Add(name + ".dll", context);
        ExternalContexts.Add(name, context);

        //更新Controllers
        ActionDescriptorChangeProvider.Instance.HasChanged = true;
        ActionDescriptorChangeProvider.Instance.TokenSource!.Cancel();
    }
    return "添加{name}controller成功";
})
.WithTags("Main")
.WithOpenApi();

app.MapGet("/Remove", ([FromServices] ApplicationPartManager _partManager, string name) =>
{
    //if (ExternalContexts.Any(
    //    $"{name}.dll"))
    if (ExternalContexts.Any(
   $"{name}"))
    {
        var matcheditem = _partManager.ApplicationParts.FirstOrDefault(x => x.Name == name);
        if (matcheditem != null)
        {
            _partManager.ApplicationParts.Remove(matcheditem);
            matcheditem = null;
        }
        ActionDescriptorChangeProvider.Instance.HasChanged = true;
        ActionDescriptorChangeProvider.Instance.TokenSource!.Cancel();
        //ExternalContexts.Remove(name + ".dll");
        ExternalContexts.Remove(name);
        return $"成功移除{name}controller";
    }
    else
    {
        return "$没有{name}controller";
    }
});
app.UseRouting(); //最新dotnet没有这些
app.MapControllers();  //最新dotnet没有这些
app.Run();

这里先对上面的尝试做个总结:

模块化开发通过IModule分离各个模块解耦,通过dll把接口加入到主程序,很nice,但是,我还想更深入一层,把这个接口也一并做成可拔可插,这样就不得不考虑如何动态的重载controller,这也没问题。重中之重来了,上面的都做到了,但是我要的不仅仅是增加删除一个controller,关联的业务代码发生了改变如何重载刷新,依赖注入这一块绕不过去。并没有好的解决办法,就这样项目戛然而止。

目前有两种解决办法:

1.加个中间层,通过反射去动态获取业务实现

2.业务实现通过new对象来拿。

下面是代码:

复制代码
using IOrder.Repository;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using System.Reflection;
using System.Runtime.Loader;

namespace AutofacRegister
{
    public interface IRepositoryProvider
    {
        IRepository GetRepository(string serviceeName);
    }
    public class RepositoryProvider : IRepositoryProvider
    {
        private readonly Dictionary<string, (Assembly assembly, DateTime lastModified)> _assemblyCache = new Dictionary<string, (Assembly assembly, DateTime lastModified)>();
        private readonly Dictionary<string, IRepository> _typeCache = new Dictionary<string, IRepository>();

        public IRepository GetRepository(string serviceName)
        {
            var path = $"{Directory.GetCurrentDirectory()}\\lib\\{serviceName}.Repository.dll";
            var lastModified = File.GetLastWriteTimeUtc(path);
            if (_assemblyCache.TryGetValue(path, out var cachedEntry) && cachedEntry.lastModified == lastModified)
            {
                // 使用缓存中的 Assembly 对象
                return CreateInstanceFromAssembly(cachedEntry.assembly,serviceName);
            }
            else
            {
                // 加载并缓存新的 Assembly 对象
                var assembly = LoadAssemblyFromFile(path);
                _assemblyCache[path] = (assembly, lastModified);
                return CreateInstanceFromAssembly(assembly,serviceName);
            }
        }

        private Assembly LoadAssemblyFromFile(string path)
        {
            var _AssemblyLoadContext = new AssemblyLoadContext(Guid.NewGuid().ToString("N"), true);
            using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read))
            {
                return _AssemblyLoadContext.LoadFromStream(fs);
            }
        }
        private IRepository CreateInstanceFromAssembly(Assembly assembly,string serviceName)
        {
            var  type_key = $"{assembly.FullName}_{serviceName}";
            if(_typeCache.TryGetValue(type_key, out var cachedType))
            {
                return _typeCache[type_key];
            }
            var type = assembly.GetTypes()
                .Where(t => typeof(IRepository).IsAssignableFrom(t) && !t.IsInterface)
                .FirstOrDefault();

            if (type != null)
            {
                var instance= (IRepository)Activator.CreateInstance(type);
                _typeCache[type_key] = instance;
                return instance;
            }
            else
            {
                throw new InvalidOperationException("No suitable type found in the assembly.");
            }
        }
    }
}

所有的注入业务放到单独的注入文件中,

复制代码
using Autofac;
using IOrder.Repository;
using Order.Repository;

namespace AutofacRegister
{
    public class RepositoryModule:Module
    {
        protected override void Load(ContainerBuilder builder)
        {
            //builder.RegisterType<Repository>().As<IRepository>().SingleInstance();
            builder.RegisterType<RepositoryProvider>().As<IRepositoryProvider>().InstancePerLifetimeScope();
        }
    }
}

上面的代码可以再加一层代理,类似这样

复制代码
using CustomAttribute;
using System.Reflection;
using ZURU_ERP.Base.Common.UnitOfWork;
using ZURU_ERP.Base.Common;
using ZURU_ERP.Base.Model;
using System.Collections.Concurrent;

namespace ZURU_ERP.Base.Reflect
{
    public class MethodInfoCache
    {
        public string Name { get; set; }
        public Type ClassType { get; set; }
        public CusTransAttribute TransAttribute { get; set; }

        public List<CusActionAttribute> ActionAttributes { get; set; }
        public bool UseTrans => (TransAttribute == null);
        public bool UseAop => ActionAttributes.Any();
    }
    public class CusProxyGenerator<T> : DispatchProxy where T:class
    {

        private readonly ConcurrentDictionary<string, MethodInfoCache> _cache = new ConcurrentDictionary<string, MethodInfoCache>();
        private IBusiness<T> business;
        private  List<ICusAop> cusAop;

        protected override object? Invoke(MethodInfo? targetMethod, object?[]? args)
        {
            #region 缓存优化 未经过测试
            string methodKey = targetMethod.Name;
            if (!_cache.ContainsKey(methodKey))
            {
                var classType = business.GetType();
                var transAttribute = classType.GetMethod(targetMethod.Name).GetCustomAttributes<CusTransAttribute>().FirstOrDefault();
                var actionAttributes = classType.GetMethod(targetMethod.Name).GetCustomAttributes<CusActionAttribute>().ToList();
                _cache[methodKey] = new MethodInfoCache()
                {
                    Name = methodKey,
                    ClassType = classType,
                    TransAttribute = transAttribute,
                    ActionAttributes = actionAttributes
                };
            }
            var methodInfoCache = _cache[methodKey];
            object result;
            if (methodInfoCache.UseAop)
            {
                var actionnames = methodInfoCache.ActionAttributes.Select(x => x.Name).ToList();
                var waitInvokes = cusAop.Where(x => actionnames.Contains(x.GetType().Name)).OrderBy(x => actionnames.IndexOf(x.GetType().Name)).ToList(); //排序
                foreach (var item in waitInvokes)
                {
                    item.Before(args);
                }

                result = methodInfoCache.UseTrans ? Trans(targetMethod, args, out result) : targetMethod.Invoke(business, args);
                foreach (var item in waitInvokes)
                {
                    item.After(new object[] { result });
                }
                return result;
            }
            else
            {
                return methodInfoCache.UseTrans ? Trans(targetMethod, args, out result) : targetMethod.Invoke(business, args);
            } 
            #endregion

            #region 没缓存原代码 经过测试

            //bool useTran = false;
            //var classType = business.GetType();
            //var useClassTrans = classType.GetCustomAttributes<CusTransAttribute>();
            //if (useClassTrans.Any())
            //{
            //    useTran = true;
            //}
            //else
            //{
            //    useTran = classType.GetMethod(targetMethod.Name).GetCustomAttributes<CusTransAttribute>().Any();  //是否使用事务
            //}

            //var actionnames = classType.GetCustomAttributes<CusActionAttribute>().Select(x => x.Name).ToList();

            //var waitInvokes = cusAop.Where(x => actionnames.Contains(x.GetType().Name)).OrderBy(x => actionnames.IndexOf(x.GetType().Name)).ToList(); //排序

            //foreach (var item in waitInvokes)
            //{
            //    item.Before(args);
            //}

            //object result;
            //if (useTran)
            //{
            //    return Trans(targetMethod, args, out result);
            //}
            //else
            //{
            //    result = targetMethod.Invoke(business, args);
            //}

            //foreach (var item in waitInvokes)
            //{
            //    item.After(new object[] { result });
            //}

            //return result;
            #endregion
        }

        private object? Trans(MethodInfo? targetMethod, object?[]? args, out object result)
        {
            var _unitOfWorkManage = App.GetService<IUnitOfWorkManage>();

            Console.WriteLine($"{targetMethod.Name} transaction started.");

            try
            {
                if (_unitOfWorkManage.TranCount <= 0)
                {
                    Console.WriteLine($"Begin Transaction");
                    _unitOfWorkManage.BeginTran();
                }
                result = targetMethod.Invoke(business, args);
                if (result is ApiResult apiResult && !apiResult.success)
                {
                    Console.WriteLine("apiResult return false Transaction rollback.");
                    _unitOfWorkManage.RollbackTran();
                    return apiResult;
                }
                if (_unitOfWorkManage.TranCount > 0)
                    _unitOfWorkManage.CommitTran();
                Console.WriteLine("Transaction Commit.");
                Console.WriteLine($"{targetMethod.Name}  transaction succeeded.");

                return result;
            }
            catch (Exception e)
            {
                _unitOfWorkManage.RollbackTran();
                Console.WriteLine("Transaction Rollback.");
                Console.WriteLine($"{targetMethod.Name}  transaction failed: " + e.Message);
                throw;
            }
        }

        public static IBusiness<T> Create(IBusiness<T> business, List<ICusAop> cusAop)
        {
            object proxy = Create<IBusiness<T>, CusProxyGenerator<T>>();
            ((CusProxyGenerator<T>)proxy).SetParameters(business, cusAop);
            return (IBusiness<T>)proxy;
        }

        private void SetParameters(IBusiness<T> business, List<ICusAop> cusAop)
        {
            this.business = business;
            this.cusAop = cusAop;
        }
    }
}

由于这层代码没有走依赖注入,想用各种aop组件,灵活性稍微低了一点点。

下面第二种直接在业务代码中new对象也不是不可,这一层的前后需要的都可以注入到容器里面去。只不过这一层就想到包装类一层不要在使用这个类的时候做过多的职责承担

复制代码
using IBusiness;

namespace Business
{
    public class ProductBusiness : IDisposable// : IProductBusiness
    {
        public static readonly ProductBusiness Instance;
        private bool _disposed = false; 
        static ProductBusiness()
        {
            Instance = new ProductBusiness();
        }
      
        private ProductBusiness()
        {
            // 初始化资源
        }
        public async Task<int> AddProduct(string name, decimal price)
        {
            await Task.CompletedTask;
            return 1;
        }

        // 实现IDisposable接口
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (_disposed)
                return;

            if (disposing)
            {
                // 释放托管资源
            }

            // 释放非托管资源
            _disposed = true;
        }

        // 析构函数
        ~ProductBusiness()
        {
            Dispose(false);
        }
    }
}

使用的时候就直接拿实例:

复制代码
  [HttpPost]
  public async Task<int> Add()
  {
      //using var scope = ServiceLocator.Instance.CreateScope();
      //var business = scope.ServiceProvider.GetRequiredService<IProductBusiness>();
      using var business = ProductBusiness.Instance;
      return await business.AddProduct("product1",12.1m);
  }

demo源代码:

liuzhixin405/AspNetCoreSimpleAop (github.com)