在ASP.NET Core中使用AOP来简化缓存操作

前言

关于缓存的使用,相信大家都是熟悉的不能再熟悉了,简单来说就是下面一句话。

优先从缓存中取数据,缓存中取不到再去数据库中取,取到了在扔进缓存中去。

然后我们就会看到项目中有类似这样的代码了。

public Product Get(int productId){  
 var product = _cache.Get($"Product_{productId}");    if(product == null){product = Query(productId);_cache.Set($"Product_{productId}",product ,10);}    return product; }

然而在初期,没有缓存的时候,可能这个方法就一行代码。

public Product Get(int productId){    return Query(productId);
}

随着业务的不断发展,可能会出现越来越多类似第一段的示例代码。这样就会出现大量“重复的代码”了!

显然,我们不想让这样的代码到处都是!

基于这样的情景下,我们完全可以使用AOP去简化缓存这一部分的代码。

大致的思路如下 :

在某个有返回值的方法执行前去判断缓存中有没有数据,有就直接返回了;

如果缓存中没有的话,就是去执行这个方法,拿到返回值,执行完成之后,把对应的数据写到缓存中去,

下面就根据这个思路来实现。

本文分别使用了Castle和AspectCore来进行演示。

这里主要是做了做了两件事

  1. 自动处理缓存的key,避免硬编码带来的坑

  2. 通过Attribute来简化缓存操作

下面就先从Castle开始吧!

使用Castle来实现

一般情况下,我都会配合Autofac来实现,所以这里也不例外。

我们先新建一个ASP.NET Core 2.0的项目,通过Nuget添加下面几个包(当然也可以直接编辑csproj来完成的)。

<PackageReference Include="Autofac" Version="4.6.2" /><PackageReference Include="Autofac.Extensions.DependencyInjection" Version="4.2.0" /><PackageReference Include="Autofac.Extras.DynamicProxy" Version="4.2.1" /><PackageReference Include="Castle.Core" Version="4.2.1" />

然后做一下前期准备工作

1.缓存的使用

定义一个ICachingProvider和其对应的实现类MemoryCachingProvider

简化了一下定义,就留下读和取的操作。

public interface ICachingProvider{   

 object Get(string cacheKey);  
  void Set(string cacheKey, object cacheValue, TimeSpan absoluteExpirationRelativeToNow); }
  public class MemoryCachingProvider : ICachingProvider{  
   private IMemoryCache _cache;  
  
   public MemoryCachingProvider(IMemoryCache cache)    {_cache = cache;}  
   
    public object Get(string cacheKey)    {        return _cache.Get(cacheKey);}  
    
    public void Set(string cacheKey, object cacheValue, TimeSpan absoluteExpirationRelativeToNow)    {_cache.Set(cacheKey, cacheValue, absoluteExpirationRelativeToNow);} }

2.定义一个Attribute

这个Attribute就是我们使用时候的关键了,把它添加到要缓存数据的方法中,即可完成缓存的操作。

这里只用了一个绝对过期时间(单位是秒)来作为演示。如果有其他缓存的配置,也是可以往这里加的。

[AttributeUsage(AttributeTargets.Method, Inherited = true)]
public class QCachingAttribute : Attribute{  
    public int AbsoluteExpiration { get; set; } = 30;    //add other settings ...}

3.定义一个空接口

这个空接口只是为了做一个标识的作用,为了后面注册类型而专门定义的。

public interface IQCaching{
}

4.定义一个与缓存键相关的接口

定义这个接口是针对在方法中使用了自定义类的时候,识别出这个类对应的缓存键。

public interface IQCachable{    string CacheKey { get; }
}

准备工作就这4步(AspectCore中也是要用到的),

下面我们就是要去做方法的拦截了(拦截器)。

拦截器首先要继承并实现IInterceptor这个接口。

public class QCachingInterceptor : IInterceptor{  

 private ICachingProvider _cacheProvider;  
 
   public QCachingInterceptor(ICachingProvider cacheProvider)    {_cacheProvider = cacheProvider;}  
   
   public void Intercept(IInvocation invocation)    {    
       var qCachingAttribute = this.GetQCachingAttributeInfo(invocation.MethodInvocationTarget ?? invocation.Method);      
       if (qCachingAttribute != null){ProceedCaching(invocation, qCachingAttribute);}      
       else{invocation.Proceed();}} }

有两点要注意:

  1. 因为要使用缓存,所以这里需要我们前面定义的缓存操作接口,并且在构造函数中进行注入。

  2. Intercept方法是拦截的关键所在,也是IInterceptor接口中的唯一定义。

Intercept方法其实很简单,获取一下当前执行方法是不是有我们前面自定义的QCachingAttribute,有的话就去处理缓存,没有的话就是仅执行这个方法而已。

下面揭开ProceedCaching方法的面纱。

private void ProceedCaching(IInvocation invocation, QCachingAttribute attribute){    var cacheKey = GenerateCacheKey(invocation);   
 var cacheValue = _cacheProvider.Get(cacheKey);  
   if (cacheValue != null){invocation.ReturnValue = cacheValue;    
       return;}invocation.Proceed();  
    if (!string.IsNullOrWhiteSpace(cacheKey)){_cacheProvider.Set(cacheKey, invocation.ReturnValue, TimeSpan.FromSeconds(attribute.AbsoluteExpiration));} }

这个方法,就是和大部分操作缓存的代码一样的写法了!

注意下面几个地方

  1. invocation.Proceed()表示执行当前的方法

  2. invocation.ReturnValue是要执行后才会有值的。

  3. 在每次执行前,都会依据当前执行的方法去生成一个缓存的键。

下面来看看生成缓存键的操作。

这里生成的依据是当前执行方法的名称,参数以及该方法所在的类名。

生成的代码如下:

private string GenerateCacheKey(IInvocation invocation){   
 var typeName = invocation.TargetType.Name;  
  var methodName = invocation.Method.Name;  
   var methodArguments = this.FormatArgumentsToPartOfCacheKey(invocation.Arguments);  
     return this.GenerateCacheKey(typeName, methodName, methodArguments); }
     //拼接缓存的键

private string GenerateCacheKey(string typeName, string methodName, IList<string> parameters){  
      var builder = new StringBuilder();builder.Append(typeName);builder.Append(_linkChar);builder.Append(methodName);builder.Append(_linkChar);  
 foreach (var param in parameters){builder.Append(param);builder.Append(_linkChar);}    return builder.ToString().TrimEnd(_linkChar); }
 
 private IList<string> FormatArgumentsToPartOfCacheKey(IList<object> methodArguments, int maxCount = 5){    
 return methodArguments.Select(this.GetArgumentValue).Take(maxCount).ToList(); }//处理方法的参数,可根据情况自行调整private string GetArgumentValue(object arg){    if (arg is int || arg is long || arg is string)    
     return arg.ToString();  
       if (arg is DateTime)    
          return ((DateTime)arg).ToString("yyyyMMddHHmmss");
              if (arg is IQCachable)    
                  return ((IQCachable)arg).CacheKey;  
                   return null; }

这里要注意的是GetArgumentValue这个方法,因为一个方法的参数有可能是基本的数据类型,也有可能是自己定义的类。

对于自己定义的类,必须要去实现IQCachable这个接口,并且要定义好键要取的值!

如果说,在一个方法的参数中,有一个自定义的类,但是这个类却没有实现IQCachable这个接口,那么生成的缓存键将不会包含这个参数的信息。

举个生成的例子:

MyClass:MyMethod:100:abc:999

到这里,我们缓存的拦截器就已经完成了。

下面是删除了注释的代码(可去github上查看完整的代码)

public class QCachingInterceptor : IInterceptor{    

private ICachingProvider _cacheProvider;  

 private char _linkChar = ':';  
 
  public QCachingInterceptor(ICachingProvider cacheProvider)    {_cacheProvider = cacheProvider;}  
  
   public void Intercept(IInvocation invocation)    {    
      var qCachingAttribute = this.GetQCachingAttributeInfo(invocation.MethodInvocationTarget ?? invocation.Method);    
         if (qCachingAttribute != null){ProceedCaching(invocation, qCachingAttribute);}      
        else{invocation.Proceed();}}    
        private QCachingAttribute GetQCachingAttributeInfo(MethodInfo method)    {      
          return method.GetCustomAttributes(true).FirstOrDefault(x => x.GetType() == typeof(QCachingAttribute)) as QCachingAttribute;}    private void ProceedCaching(IInvocation invocation, QCachingAttribute attribute)    {      
           var cacheKey = GenerateCacheKey(invocation);  
           var cacheValue = _cacheProvider.Get(cacheKey);    
           if (cacheValue != null){invocation.ReturnValue = cacheValue;      
                return;}invocation.Proceed();    
        if (!string.IsNullOrWhiteSpace(cacheKey)){_cacheProvider.Set(cacheKey, invocation.ReturnValue, TimeSpan.FromSeconds(attribute.AbsoluteExpiration));}}  
    
     private string GenerateCacheKey(IInvocation invocation)    {        var typeName = invocation.TargetType.Name;    
         var methodName = invocation.Method.Name;    
             var methodArguments = this.FormatArgumentsToPartOfCacheKey(invocation.Arguments);    
                return this.GenerateCacheKey(typeName, methodName, methodArguments);}  
    
     private string GenerateCacheKey(string typeName, string methodName, IList<string> parameters)    {      
      var builder = new StringBuilder();builder.Append(typeName);builder.Append(_linkChar);builder.Append(methodName);builder.Append(_linkChar);      
        foreach (var param in parameters){builder.Append(param);builder.Append(_linkChar);}        return builder.ToString().TrimEnd(_linkChar);}    
        private IList<string> FormatArgumentsToPartOfCacheKey(IList<object> methodArguments, int maxCount = 5)    {    
           return methodArguments.Select(this.GetArgumentValue).Take(maxCount).ToList();}  
            private string GetArgumentValue(object arg)    {    
               if (arg is int || arg is long || arg is string)            return arg.ToString();      
                 if (arg is DateTime)        
                    return ((DateTime)arg).ToString("yyyyMMddHHmmss");      
                     if (arg is IQCachable)      
                         return ((IQCachable)arg).CacheKey;    
                             return null;} }  

下面就是怎么用的问题了。

这里考虑了两种用法:

  • 一种是面向接口的用法,也是目前比较流行的用法

  • 一种是传统的,类似通过实例化一个BLL层对象的方法。

先来看看面向接口的用法

public interface IDateTimeService{        string GetCurrentUtcTime();
}

public class DateTimeService : IDateTimeService, QCaching.IQCaching{[QCaching.QCaching(AbsoluteExpiration = 10)]  
 public string GetCurrentUtcTime()    {  
      return System.DateTime.UtcNow.ToString();} }

简单起见,就返回当前时间了,也是看缓存是否生效最简单有效的办法。

在控制器中,我们只需要通过构造函数的方式去注入我们上面定义的Service就可以了。

public class HomeController : Controller{   

 private IDateTimeService _dateTimeService;  
 
  public HomeController(IDateTimeService dateTimeService)    {_dateTimeService = dateTimeService;}    
  
  public IActionResult Index()    {    
     return Content(_dateTimeService.GetCurrentUtcTime());} }

如果这个时候运行,肯定是会出错的,因为我们还没有配置!

去Starpup中修改一下ConfigureServices方法,完成我们的注入和启用拦截操作。

public class Startup{  

 public IServiceProvider ConfigureServices(IServiceCollection services)    {services.AddMvc();services.AddScoped<ICachingProvider, MemoryCachingProvider>();        return this.GetAutofacServiceProvider(services);}    
 
 private IServiceProvider GetAutofacServiceProvider(IServiceCollection services)    {      
  var builder = new ContainerBuilder();builder.Populate(services);    
      var assembly = this.GetType().GetTypeInfo().Assembly;builder.RegisterType<QCachingInterceptor>();        //scenario 1builder.RegisterAssemblyTypes(assembly).Where(type => typeof(IQCaching).IsAssignableFrom(type) && !type.GetTypeInfo().IsAbstract).AsImplementedInterfaces().InstancePerLifetimeScope().EnableInterfaceInterceptors().InterceptedBy(typeof(QCachingInterceptor));       return new AutofacServiceProvider(builder.Build());}    //other ...}

要注意的是这个方法原来是没有返回值的,现在需要调整为返回IServiceProvider

这段代码,网上其实有很多解释,这里就不再细说了,主要是EnableInterfaceInterceptorsInterceptedBy

下面是运行的效果:

再来看看通过实例化的方法

先定义一个BLL层的方法,同样是返回当前时间。这里我们直接把Attribute放到这个方法中即可,同时还要注意是virtual的。

public class DateTimeBLL : QCaching.IQCaching{[QCaching.QCaching(AbsoluteExpiration = 10)]  
 public virtual string GetCurrentUtcTime()    {    
     return System.DateTime.UtcNow.ToString();} }

在控制器中,就不是简单的实例化一下这个BLL的对象就行了,还需要借肋ILifetimeScope去Resolve。如果是直接实例化的话,是没办法拦截到的。

public class BllController : Controller{ 

   private ILifetimeScope _scope;  
    private DateTimeBLL _dateTimeBLL;  
    
     public BllController(ILifetimeScope scope)    {      
     this._scope = scope;_dateTimeBLL = _scope.Resolve<DateTimeBLL>();}  
     
      public IActionResult Index()    {  
           return Content(_dateTimeBLL.GetCurrentUtcTime());} }

同时还要在builder中启用类的拦截EnableClassInterceptors

//scenario 2builder.RegisterAssemblyTypes(assembly).Where(type => type.Name.EndsWith("BLL", StringComparison.OrdinalIgnoreCase)).EnableClassInterceptors().InterceptedBy(typeof(QCachingInterceptor));

效果如下:

到这里已经通过Castle和Autofac完成了简化缓存的操作了。

下面再来看看用AspectCore该如何来实现。

使用AspectCore来实现

AspectCore是由Lemon丶写的一个基于AOP的框架。

首先还是要通过Nuget添加一下相应的包。这里只需要添加两个就可以了。

<PackageReference Include="AspectCore.Core" Version="0.2.2" /><PackageReference Include="AspectCore.Extensions.DependencyInjection" Version="0.2.2" />

用法大同小异,所以后面只讲述一下使用上面的不同点。

注:我也是下午看了一下作者的博客和一些单元测试代码写的下面的示例代码,希望没有对大家造成误导。

首先,第一个不同点就是我们的拦截器。这里需要去继承AbstractInterceptor这个抽象类并且要去重写Invoke方法。

public class QCachingInterceptor : AbstractInterceptor{[FromContainer]   
 public ICachingProvider CacheProvider { get; set; }  
 
  public async override Task Invoke(AspectContext context, AspectDelegate next)    {        var qCachingAttribute = GetQCachingAttributeInfo(context.ServiceMethod);    
      if (qCachingAttribute != null){          
       await ProceedCaching(context, next, qCachingAttribute);}        
       else{          
        await next(context);}} }

细心的读者会发现,两者并没有太大的区别!

缓存的接口,这里是用FromContainer的形式的处理的。

接下来是Service的不同。

这里主要就是把Attribute放到了接口的方法中,而不是其实现类上面。

public interface IDateTimeService : QCaching.IQCaching{     [QCaching.QCaching(AbsoluteExpiration = 10)]  
 string GetCurrentUtcTime(); }
 public class DateTimeService : IDateTimeService{    //[QCaching.QCaching(AbsoluteExpiration = 10)]public string GetCurrentUtcTime()    {  
       return System.DateTime.UtcNow.ToString();} }

然后是使用实例化方式时的控制器也略有不同,主要是替换了一下相关的接口,这里用的是IServiceResolver

public class BllController : Controller{   

 private IServiceResolver _scope;    
 private DateTimeBLL _dateTimeBLL;  
   public BllController(IServiceResolver scope)    {    
       this._scope = scope;_dateTimeBLL = _scope.Resolve<DateTimeBLL>();}  
        public IActionResult Index()    {    
            return Content(_dateTimeBLL.GetCurrentUtcTime());}

最后,也是至关重要的Stratup。

public class Startup{   

 public IServiceProvider ConfigureServices(IServiceCollection services)    {services.AddMvc();services.AddScoped<ICachingProvider, MemoryCachingProvider>();services.AddScoped<IDateTimeService, DateTimeService>();        //handle BLL classvar assembly = this.GetType().GetTypeInfo().Assembly;        this.AddBLLClassToServices(assembly, services);      
  var container = services.ToServiceContainer();container.AddType<QCachingInterceptor>();container.Configure(config =>{config.Interceptors.AddTyped<QCachingInterceptor>(method => typeof(IQCaching).IsAssignableFrom(method.DeclaringType));});        return container.Build();}    
  
  public void AddBLLClassToServices(Assembly assembly, IServiceCollection services)    {    
     var types = assembly.GetTypes().ToList();      
      foreach (var item in types.Where(x => x.Name.EndsWith("BLL", StringComparison.OrdinalIgnoreCase) && x.IsClass)){services.AddSingleton(item);}}    //other code...}

我这里是先用自带的DependencyInjection完成了一些操作,然后才去用ToServiceContainer()得到AspectCore内置容器。

得到这个容器后,就去配置拦截了。

最终的效果是和前面一样的,就不再放图了。

总结

AOP在某些方面的作用确实很明显,也很方便,能做的事情也很多。

对比Castle和AspectCore的话,两者各有优点!

就我个人使用而言,对Castle略微熟悉一下,资料也比较多。

对AspectCore的话,我比较喜欢它的配置,比较简单,依赖也少。

原文地址:http://www.cnblogs.com/catcher1994/p/7788890.html


.NET社区新闻,深度好文,欢迎访问公众号文章汇总 http://www.csharpkit.com

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/323003.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Hadoop入门(七)Mapreduce高级Shuffle

一、Shuffle概述 Reduce阶段三个步骤&#xff0c;Shuffle就是一个随机、洗牌操作 Shuffle是什么 针对多个map任务的输出按照不同的分区&#xff08;Partition&#xff09;通过网络复制到不同的reduce任务节点上&#xff0c;这个过程就称作为Shuffle。 二、Shuffle过程 &#…

methods中axios里的数据无法渲染到页面

最近在研究axios聊天室室遇到一个问题 将axios获取到的数据传递给data&#xff0c;从而改变页面中的数值&#xff0c;但是结果令人失望 这是data里的数据 原想将data中的items数组换成axios里的response.data&#xff0c;后来发现items一直为空&#xff0c;就拿字符串做实验了…

jzoj4229-学习神技【逆元,费马小定理】

正题 题目大意 求 (∑i1na∗qi)mod(1097)(\sum_{i1}^na*q^i)\ mod\ (10^97)(i1∑n​a∗qi) mod (1097) 解题思路 题目里都给出公式 ∑i1na∗qia∗(1−qn)1−q\sum_{i1}^na*q_i\frac{a*(1-q^n)}{1-q}i1∑n​a∗qi​1−qa∗(1−qn)​ 其实就是 a∗(qn−1)q−1\frac{a*(q^n-1)}{…

.NET Core跨平台的奥秘[上篇]:历史的枷锁

微软推出的第一个版本的.NET Framework是一个面向Windows桌面和服务器的基础框架&#xff0c;在此之后&#xff0c;为此微软根据设备自身的需求对.NET Framework进行裁剪&#xff0c;不断推出了针对具体设备类型的.NET Framework版本以实现针对移动、平板和嵌入式设备提供支持。…

Hadoop入门(十)Mapreduce高级shuffle之Sort和Group

一、排序分组概述 MapReduce中排序和分组在哪里被执行 第3步中需要对不同分区中的数据进行排序和分组&#xff0c;默认情况按照key进行排序和分组 二、排序 在Hadoop默认的排序算法中&#xff0c;只会针对key值进行排序 任务&#xff1a; 数据文件中&#xff0c;如果按照第一…

Js使滑轮到最底部

在做ajax的聊天室页面时&#xff0c;新数据总是不能显示出来&#xff0c;需要下翻&#xff0c;所以必须在setInterval(“app.aaa()”, 1000)中aaa函数里添加一个可以使滑轮在底部的代码 加入 div1 document.getElementById("div1") div1.scrollTop div1.scrollHe…

jzoj4230-淬炼神体【0/1分数规划】

正题 题目大意 nnn个东西&#xff0c;有ai,bia_i,b_iai​,bi​。选择kkk个&#xff0c;使得∑ai/∑bi\sum a_i/\sum b_i∑ai​/∑bi​最大。 解题思路 ∑ai/∑bik\sum a_i/\sum b_ik∑ai​/∑bi​k ∑ai/∑bi/k1\sum a_i/\sum b_i/k1∑ai​/∑bi​/k1 ∑ai/k∑bi\sum a_i/k\sum…

使用Identity Server 4建立Authorization Server (3)

预备知识: 学习Identity Server 4的预备知识 第一部分: 使用Identity Server 4建立Authorization Server (1) 第二部分: 使用Identity Server 4建立Authorization Server (2) 上一部分简单的弄了个web api 并通过Client_Credentials和ResourceOwnerPassword两种方式获取token然…

php接口跨域问题

报错是因为接口跨域&#xff0c;不允许访问 只需在php头部加入此行代码就行了 header(Access-Control-Allow-Origin:*);

jzoj4231-寻找神格【线段树,数学】

正题 题目大意 4个操作 单点修改&#xff0c;区间修改&#xff0c;区间求和&#xff0c;区间求方差 方差为:∑(xi−ave)2n\frac{\sum(x_i-ave)^2}{n}n∑(xi​−ave)2​ aveaveave为平均值 解题思路 我们将方差的式子分解一下 ∑(xi−ave)2n\frac{\sum(x_i-ave)^2}{n}n∑(xi​…

Hadoop入门(十四)Mapreduce的数据去重程序

1 实例描述 对数据文件中的数据进行去重。数据文件中的每行都是一个数据 样例输入如下所示&#xff1a; 1&#xff09;file1 2012-3-1 a 2012-3-2 b 2012-3-3 c 2012-3-4 d 2012-3-5 a 2012-3-6 b 2012-3-7 c 2012-3-3 c 2&#xff09;file2 2012-3-1 b 2012-3-2 a 2012…

spring boot输出hello world几种方法

1、手动配置&#xff0c;三个文件 打开创建maven,创建这三个文件从上到下依次复制即可 配置文件&#xff08;重要&#xff09;&#xff08;否则后面会报错&#xff09; pom.xml <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w…

欢乐纪中某B组赛【2019.1.20】

前言 有回来做BBB组了&#xff0c;话说第3道题就是AAA组第一道。 成绩 RankRankRank是有算别人的 今天XJQXJQXJQ不在 RankRankRankPersonPersonPersonScoreScoreScoreAAABBBCCC1112017wyc2017wyc2017wyc2702702701001001001001001007070701010102017hjq2017hjq2017hjq13013013…

向ASP.NET Core迁移

我们首先来看看ASP.NET Core有哪些优势&#xff1f; 跨平台&#xff1a;可以部署到Linux服务器上 内置一套对云和部署环境非常友好的配置模块 内置依赖注入 IIS或者Kestrel&#xff08;或者其它自定义&#xff09; 轻量级、高性能、模块化的Http处理管线 .NET Core 是开源…

Hadoop入门(十五)Mapreduce的数据排序程序

"数据排序"是许多实际任务执行时要完成的第一项工作&#xff0c;比如学生成绩评比、数据建立索引等。这个实例和数据去重类似&#xff0c;都是先对原始数据进行初步处理&#xff0c;为进一步的数据操作打好基础 1 实例描述 对输入文件中数据进行排序。输入文件中的…

Java我来了

引言 原本我也是个计算机的小白&#xff0c;从今年的二月份开始学习java&#xff0c;接触eclipse&#xff0c;终于算是开启了我的编程之旅。 在此之前我也不算是零基础&#xff0c;学校在上学期还未开设c语言课程的时候&#xff0c;我就自学了c语言&#xff0c;学的不深&#x…

jzoj4208-线段树什么的最讨厌了【dfs】

正题 题目大意 一个0∼n0\sim n0∼n的线段树包含l∼rl\sim rl∼r的区间&#xff0c;求最小的nnn 解题思路 dfsdfsdfs一下&#xff0c;从下面开始往上扩展区间&#xff0c;知道变成0∼x0\sim x0∼x的格式就就可以去最小值了。 记得剪枝 codecodecode #include<cstdio> …

Hadoop入门(十六)Mapreduce的单表关联程序

"单表关联"要求从给出的数据中寻找所关心的数据&#xff0c;它是对原始数据所包含信息的挖掘 1 实例描述 给出child-parent&#xff08;孩子——父母&#xff09;表&#xff0c;要求输出grandchild-grandparent&#xff08;孙子——祖父母&#xff09;表 样例输入&…

ASP.NET Core集成现有系统认证

我们现在大多数转向ASP.NET Core来使用开发的团队&#xff0c;应该都不是从0开始搭建系统&#xff0c;而是老的业务系统已经在运行&#xff0c;ASP.NET Core用来开发新模块。那么解决用户认证的问题&#xff0c;成为我们的第一个拦路虎。 认证与授权 什么是认证&#xff1f; …

Hadoop入门(十七)Mapreduce的多表关联程序

多表关联和单表关联类似&#xff0c;它也是通过对原始数据进行一定的处理&#xff0c;从其中挖掘出关心的信息 1 实例描述 输入是两个文件&#xff0c;一个代表工厂表&#xff0c;包含工厂名列和地址编号列&#xff1b;另一个代表地址表&#xff0c;包含地址名列和地址编号列…