aspnetcore插件开发dll热加载 二

aspnetcore,dll · 浏览次数 : 0

小编点评

**方法一:使用依赖注入** 1. 创建一个接口 `IBusiness`,并定义 `AddProduct` 方法。 2. 在 `ProductBusiness` 中实现 `IBusiness` 接口的 `AddProduct` 方法。 3. 在 `ProductBusiness` 中注册 `IBusiness` 接口的实现类。 4. 在调用 `AddProduct` 方法时,使用 `ServiceProvider` 获取 `IBusiness` 接口的实例并调用其 `AddProduct` 方法。 **方法二:直接在业务代码中创建对象** 1. 在 `ProductBusiness` 中定义 `AddProduct` 方法。 2. 在 `AddProduct` 方法中直接使用 `Create` 方法创建实例并调用 `AddProduct` 方法。 3. 使用 `ServiceProvider` 获取 `IBusiness` 接口的实例并调用其 `AddProduct` 方法。 **优点和缺点:** **方法一:使用依赖注入** * 更加安全,因为您不直接在业务代码中创建对象。 * 允许您轻松添加新的 AOP 组件。 * 减少了代码中的依赖关系。 **方法二:直接在业务代码中创建对象** * 更易于理解。 * 可以在运行时创建对象。 * 减少了代码中的依赖关系。 **选择方法:** * 如果您需要频繁创建并使用不同的 AOP 组件,则应该使用 **方法一**。 * 如果您的项目对代码可读性和易维护性更重要,则应该使用 **方法二**。

正文

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

投简历的时候是不是有人问我有没有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)

 

与aspnetcore插件开发dll热加载 二相似的内容:

aspnetcore插件开发dll热加载 二

这一篇文章应该是个总结。 投简历的时候是不是有人问我有没有abp的开发经历,汗颜! 在各位大神的尝试及自己的总结下,还是实现了业务和主机服务分离,通过dll动态的加载卸载,控制器动态的删除添加。 项目如下: 演示效果: 下面就是代码部分: 重点 1.IActionDescriptorChangePr

aspnetcore插件开发dll热加载

该项目比较简单,只是单纯的把业务的dll模块和controller的dll做了一个动态的添加删除处理,目的就是插件开发。由于该项目过于简单,请勿吐槽。复杂的后续可以通过泛型的实体、dto等做业务和接口的动态区分。 项目结构如下: 上面的两个模块是独立通过dll加载道项目中的 repository动态

NET9 AspnetCore将整合OpenAPI的文档生成功能而无需三方库

OpenAPI 规范是用于描述 HTTP API 的标准。该标准允许开发人员定义 API 的形状,这些 API 可以插入到客户端生成器、服务器生成器、测试工具、文档等中。尽管该标准具有普遍性和普遍性,但 ASP.NET Core 在框架内默认不提供对 OpenAPI 的支持。 当前 ASP.NET

记一次aspnetcore发布部署流程初次使用k8s

主题: aspnetcorewebapi项目,提交到gitlab,通过jenkins(gitlab的ci/cd)编译、发布、推送到k8s。 关于gitlab、jenkins、k8s安装,都是使用docker启动服务。 首先新建一个项目,为了方便浏览就把swaggerr非开发环境不展示去掉 下面就是需

解决aspnetcore-browser-refresh.js:234 WebSocket connection to 'wss://localhost:62356/Admin/' failed问题

前言 前段时间升级了Visual Studio到v17.1.1最新版本,然后今天来运行之前的一个.net5项目一直提示:aspnetcore-browser-refresh.js:234 WebSocket connection to 'wss://localhost:62356/Admin/' f

Asp-Net-Core学习笔记:单元测试和集成测试

## 前言 我在使用 AspNetCore 的这段时间内,看了很多开源项目和博客,发现各种 .Net 体系的新技术很多人都有关注和使用,但却很少有人关注测试。 测试是软件生命周期中的一个非常重要的阶段,对于保证软件的可靠性具有极其重要的意义。在应用程序的开发过程中,为了确保它的功能与预期一致,必须对

基于 .net core 8.0 的 swagger 文档优化分享-根据命名空间分组显示

之前也分享过 Swashbuckle.AspNetCore 的使用,不过版本比较老了,本次演示用的示例版本为 .net core 8.0,从安装使用开始,到根据命名空间分组显示,十分的有用

在FreeSQL中实现「触发器」和软删除功能

前言 最近做新项目,技术栈 AspNetCore + FreeSQL 这个ORM真的好用,文档也很完善,这里记录一下两个有关「触发器」的功能实现 修改实体时记录更新时间 模型代码 我的模型都是基于这个 ModelBase 派生的,自带三个属性字段 public abstract class Mode

Asp-Net-Core开发笔记:FrameworkDependent搭配docker部署

## 前言 之前我写过一篇使用 docker 部署 AspNetCore 应用的文章,这种方式搭配 CICD 非常方便, build 之后 push 到私有的 dockerhub ,在生产服务器上 pull 下来镜像就可以直接运行了。 然而,有时需要一种更传统的部署方式,比如在本地打包可执行文件之后

Asp-Net-Core开发笔记:使用原生的接口限流功能

前言 之前介绍过使用 AspNetCoreRateLimit 组件来实现接口限流 从 .Net7 开始,AspNetCore 开始内置限流组件,当时我们的项目还在 .Net6 所以只能用第三方的 现在都升级到 .Net8 了,当然是得来试试这个原生组件 体验后:配置使用都比较简单,不过功能也没有 A