aspnetcore外掛開發dll熱載入 二

星仔007發表於2024-05-25

這一篇文章應該是個總結。

投簡歷的時候是不是有人問我有沒有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)

相關文章