2025-08-17 22:54:34
摘要:.Net生态有一个大名鼎鼎中介者模式实现库MediatR,今天介绍一个优雅的装饰器模式实现库DecoratR。
作为软件工程师,我们不断面临在应用程序中实现横切关注点的挑战,例如日志记录、缓存、验证、重试逻辑和安全性。传统方法通常会导致:
重复的样板代码散布在你的服务中。
业务逻辑与基础设施关注点之间紧密耦合。
由于职责混合而导致测试困难。
当需求变更时,可维护性差。
考虑这个典型的服务方法:
public async TaskOrder GetOrderAsync(int orderId)
{
_logger.LogInformation(Getting order {OrderId}, orderId); // 正在获取订单 {OrderId}
// 先检查缓存
var cacheKey = $order_{orderId};
if (_cache.TryGetValue(cacheKey, out Order cachedOrder))
{
_logger.LogInformation(Order {OrderId} found in cache, orderId); // 订单 {OrderId} 在缓存中找到
return cachedOrder;
}
try
{
// 验证输入
if (orderId = 0)
thrownew ArgumentException(Invalid order ID); // 无效的订单 ID
// 业务逻辑埋没在基础设施代码中
var order = await _repository.GetOrderAsync(orderId);
// 缓存结果
_cache.Set(cacheKey, order, TimeSpan.FromMinutes(5));
_logger.LogInformation(Order {OrderId} retrieved successfully, orderId); // 订单 {OrderId} 成功获取
return order;……
阅读全文
2019-11-13 23:06:01
摘要:参考
蒋金楠:书籍《ASP.NET Core 3框架揭秘》、博客园 :ASP.NET Core 3框架揭秘
官方文档:在 ASP.NET Core 依赖注入 在 ASP.NET Core 中将依赖项注入到控制器
学完这篇依赖注入,与面试官扯皮就没有问题了。
一个接口注入多次:【ASP.NET Core】依赖注入高级玩法——如何注入多个服务实现类 、在.NET Core中处理一个接口多个不同实现的依赖注入问题
spring依赖注入和控制反转的理解,写的太好了
ASP.NET Core应用的7种依赖注入方式
概念
控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫“依赖查找”(Dependency Lookup)。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中。
控制反转是一种思想,依赖注入是一种设计模式。
依赖抽象不依赖具体
谁控制谁,控制什么
传统程序设计,我们直接在对象内部通过new进行创建对象,是程序主动去创建依赖对象;而IoC是有专门一个容器来创建这些对象,即由Ioc容器来控制对 象的创建;谁控制谁?当然是IoC 容器控制了对象;控制什么?那就是主要控制了外部资源获取(不只是对象包括比如文件等)。
为何是反转,哪些方面反转了
有反转就有正转,传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象;为何是反转?因为由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转;哪些方面反转了?依赖对象的获取被反转了。
阅读全文
2018-08-21 22:37:30
摘要:1. 目标
如下代码:我们要实现缓存,但希望让使用者不用关心缓存的具体实现,只需要使用者在要操作缓存的方法上加上特性标注即可。
[Caching(CachingMethod.Remove, GetLinksQuery)]
public class CreateLinkCommand
{
}
[Caching(CachingMethod.Get)]
public class GetLinksQuery : IRequestListLinkViewModel
{
}
要实现我们的目标,我们把任务分成2部分,首先实现缓存逻辑,然后将缓存基于特性做AOP实现。
2. 缓存实现
首先我们定义一个缓存接口
public interface ICacheProvider
{
/// summary
/// 向缓存中添加一个对象。
/// /summary
/// param name=key缓存的键值,该值通常是使用缓存机制的方法的名称。/param
/// param name=valKey缓存值的键值,该值通常是由使用缓存机制的方法的参数值所产生。/param
/// param name=value需要缓存的对象。/param
void Add(string key, string valKey, object value);
void Put(string key, string valKey, object value);
object Get(string key, string valKey);
void Remove(string key);
bool Exists(string key);
bool Exists(string key, string valKey);
}
如上代码,为什么接口中key和valKey2个参数呢?这是因为我们可能会缓存同一个方法不同参数的结果,如在一个获取分页结果的方法中,我们可能会返回不同页的结果。如我们目标中GetLinksQuery方法缓存的值会是一个分页显示结果的字典。key是我们缓存的方法名,valKey这是缓存的字典结果中的字典key,value则是字典的结果。要进一步理解可以查看下面基于内存的默……
阅读全文
2016-05-04 11:00:21
摘要:参考
C#执行异步操作的几种方式比较和总结
异步与多线程的区别(异步是目的,多线程是实现它的一种方式,异步的优先级有时候比主线程还高) - 子福当自强 - 博客园
c#异步编程
JavaScript异步机制与异步原理
异步编程的本质:绑定 - zzfx - 博客园
异步编程的本质:怎么处理异步请求(事件)与响应的关系
进阶篇:以IL为剑,直指async/await
官方文档 使用 Async 和 Await 的异步编程
.NET 异步详解 --能说清楚异步与多线程的区别,涉及到操作系统原理
await,async 我要把它翻个底朝天,这回你总该明白了吧
异步开发是什么
异步开发,就是利用线程技术,当执行一个占用时间长的任务时,不会阻止用户其它工作,而且其完成时会通知用户。
虽然是通过多线程来实现异步,但是因为一般异步时耗时的操作都不是针对CPU的,所以对CPU压力影响不大。
异步本质是通过线程池提高线程的利用率。
异步采用IO的DMA模式,不会消耗CPU资源。计算密集的工作,采用多线程。IO密集的工作,采用异步
举例:网络爬虫爬数据,如果数据很庞大,这个时候就需要使用异步了。
DMA:Direct Memory Access是IO的操作模式,可以直接访问内存,不经过CPU,不消耗CPU资源。
异步和多线程区别就是,充分利用DMA释放CPU压力。
异步实现方式:.net中实现异步的方式有定时器和多线程,一般都是使用多线程。
**使用场景:**要执行耗时且不需要立刻返回结果的操作,如:I/O操作、网络操作
争议:
有种说法是:异步是不阻塞当前主线程执行,通过子线程去执行,还有总说法刚好相反,具体参考:异步与线程阻塞
同步、异步、并行区别
同步:代码执行顺序默认是一行一行执行的,要等当前行的代码执行完后,才会执行下一行的代码。
异步:不用等当前行代码执行完毕就会执行下一行的代码。异步编程是一项关键技术,可以直接处理多个核心上的阻塞 I/O 和并发操作
并行:许多个人计算机和工作站都有多个 CPU 内核,以便多个线程能够同时执行。 为了利用硬件,你可以对代码进行并行化,以将工作分摊在多个处理器上。
并行和多线程、异步和多线程有啥关系?
异步和多线程:多线程是创建多个线程,消耗的是CPU,异步呢?
异步方法返回类型:void、Task、Task
异步编程模式:
基于……
阅读全文
2015-05-18 22:19:39
摘要:逆变(contravariant)与协变(covariant)是C#4新增的概念,许多书籍和博客都有讲解,我觉得都没有把它们讲清楚,搞明白了它们,可以更准确地去定义泛型委托和接口,这里我尝试画图详细解析逆变与协变。
变的概念
我们都知道.Net里或者说在OO的世界里,可以安全地把子类的引用赋给父类引用,例如:
//父类 = 子类
string str = string;
object obj = str;//变了
而C#里又有泛型的概念,泛型是对类型系统的进一步抽象,比上面简单的类型高级,把上面的变化体现在泛型的参数上就是我们所说的逆变与协变的概念。通过在泛型参数上使用in或out关键字,可以得到逆变或协变的能力。下面是一些对比的例子:
协变(Foo父类 = Foo子类 ):
//泛型委托:
public delegate T MyFuncAT();//不支持逆变与协变
public delegate T MyFuncBout T();//支持协变
MyFuncAobject funcAObject = null;
MyFuncAstring funcAString = null;
MyFuncBobject funcBObject = null;
MyFuncBstring funcBString = null;
MyFuncBint funcBInt = null;
funcAObject = funcAString;//编译失败,MyFuncA不支持逆变与协变
funcBObject = funcBString;//变了,协变
funcBObject = funcBInt;//编译失败,值类型不参与协变或逆变
//泛型接口
public interface IFlyAT { }//不支持逆变与协变
public interface IFlyBout T { }//支持协变
IFlyAobject flyAObject = null;
IFlyAstring flyAString = null;
IFlyBobject flyBObject = null;
IFlyBstring flyBString = null;
IFlyBint flyBInt = null;
flyAObject = flyAString;//编译失败,IFly……
阅读全文
2015-02-20 15:00:38
摘要:反射是什么:
反射Reflection [rɪˈflekʃn] 提供描述程序集、模块和类型的对象(Type 类型)。 可以使用反射动态地创建类型的实例,将类型绑定到现有对象,或从现有对象中获取类型,然后调用其方法或访问其字段和属性。 如果代码中使用了特性,可以利用反射来访问它们。 有关更多信息,请参阅特性。
反射命名空间:System.Reflection
typeof 运算符:用于获取某个类型的 System.Type 实例
GetType():获取当前实例的 Type。
Type类常用属性
Assembly:获取在其中声明该类型的程序集。 对于泛型类型,则获取在其中定义该泛型类型的程序集。
Type类常用方法
GetMembers :获取当前 Type 的成员(包括属性、方法、字段、事件等)
GetConstructor :获取当前 Type 的特定构造函数
GetMethods:获取当前 Type 的方法
MethodInfo 类:发现方法的属性并提供对方法元数据的访问。
为什么要反射(优点):
需要访问程序元数据中的特性时。 有关详细信息,请参阅检索存储在特性中的信息。
检查和实例化程序集中的类型。
在运行时构建新类型。 使用 System.Reflection.Emit 中的类。
执行后期绑定,访问在运行时创建的类型上的方法。 请参阅主题 “动态加载和使用类型”。
反射提高了程序的灵活性和扩展性,降低耦合性,提高自适应能力。它允许程序创建和控制任何类的对象,无需提前硬编码目标类
反射的缺点
性能问题:使用反射基本上是一种解释操作,用于字段和方法接入时要远慢于直接代码。因此反射机制主要应用在对灵活性和扩展性要求很高的系统框架上,普通程序不建议使用
可以通过缓存优化提高性能
使用反射会模糊程序内内部逻辑:程序员希望在源代码中看到程序的逻辑,反射等绕过了源代码的技术,因而会带来维护问题。反射代码比相应的直接代码更复杂。 至于执行效率的话,还可以,因为它是一种强类型语言,执行效率不错。不过,建议将反射过后,保存进 cache中
反射怎么用
一般是用在反射配置以提高程序扩展、程序解耦
通过反调调用类的方法
框架中使用最多,例如MVC、WCF、WEBAPI、ORM、AOP等,这些框架就是通过反射设计,我们使用的时候可以灵活去配置文件、配置类、配置……
阅读全文
2015-02-16 11:26:53
摘要:泛型缓存字典是通过静态构造函数只被执行一次的调用机制,在首次执行时存好值,后面不再计算直接调用,以达到缓存下效果。
静态构造函数:静态构造函数用于初始化任何静态数据,或执行仅需执行一次的特定操作。 将在创建第一个实例或引用任何静态成员之前自动调用静态构造函数。
说人话就是:只有该类第一次被调用时执行一次静态构造函数,该类后面再继续被调用时都不会再去执行静态构造函数。(跟踪调试发现确实这样,编译器能识别)
public class GenericCacheTest
{
public static void Show()
{
for (int i = 0; i 5; i++)
{
Console.WriteLine(GenericCacheint.GetCache());
Thread.Sleep(10);
Console.WriteLine(GenericCachelong.GetCache());
Thread.Sleep(10);
Console.WriteLine(GenericCacheDateTime.GetCache());
Thread.Sleep(10);
Console.WriteLine(GenericCachestring.GetCache());
Thread.Sleep(10);
Console.WriteLine(GenericCacheGenericCacheTest.GetCache());
Thread.Sleep(10);
}
}
}
/// summary
/// 字典缓存:
/// 原理:静态属性常驻内存,是key-value的hash存储,每次调用时需要去内存器查找要进行哈希运算
/// /summary
public class DictionaryCache
{
private static DictionaryType, string _TypeTimeDictionary = null;
static DictionaryCache()//静态构造函数
{
Console.WriteLine(This is DictionaryCache 静态构造函数);
_TypeTimeDictionary = new Dictio……
阅读全文
2014-11-06 21:56:17
摘要:内容导读
概述
当你声明一个变量背后发生了什么?
堆和栈
值类型和引用类型
哪些是值类型,哪些是引用类型?
装箱和拆箱
装箱和拆箱的性能问题
一、概述
本文会阐述六个重要的概念:堆、栈、值类型、引用类型、装箱和拆箱。本文首先会通过阐述当你定义一个变量之后系统内部发生的改变开始讲解,然后将关注点转移到存储双雄:堆和栈。之后,我们会探讨一下值类型和引用类型,并对有关于这两种类型的重要基础内容做一个讲解。
本文会通过一个简单的代码来展示在装箱和拆箱过程中所带来的性能上的影响,请各位仔细阅读。
二、当你声明一个变量背后发生了什么?
当你在一个.NET应用程序中定义一个变量时,在RAM中会为其分配一些内存块。这块内存有三样东西:变量的名称、变量的数据类型以及变量的值。
上面简单阐述了内存中发生的事情,但是你的变量究竟会被分配到哪种类型的内存取决于数据类型。在.NET中有两种可分配的内存:栈和堆。在接下来的几个部分中,我们会试着详细地来理解这两种类型的存储。
三、存储双雄:堆和栈
为了理解栈和堆,让我们通过以下的代码来了解背后到底发生了什么。
public void Method1()
{
// Line 1
int i=4;
// Line 2
int y=2;
//Line 3
class1 cls1 = new class1();
}
代码只有三行,现在我们可以一行一行地来了解到底内部是怎么来执行的。
**Line 1:**当这一行被执行后,编译器会在栈上分配一小块内存。栈会在负责跟踪你的应用程序中是否有运行内存需要
**Line 2:**现在将会执行第二步。正如栈的名字一样,它会将此处的一小块内存分配叠加在刚刚第一步的内存分配的顶部。你可以认为栈就是一个一个叠加起来的房间或盒子。在栈中,数据的分配和解除都会通过LIFO (Last In First Out)即先进后出的逻辑规则进行。换句话说,也就是最先进入栈中的数据项有可能最后才会出栈。
**Line 3:**在第三行中,我们创建了一个对象。当这一行被执行后,.NET会在栈中创建一个指针,而实际的对象将会存储到一个叫做“堆”的内存区域中。“堆”不会监测运行内存,它只是能够被随时访问到的一堆对象而已。不同于栈,堆用于动态内存的分配。
这里需要注意的另一个……
阅读全文
2012-11-28 12:18:06
摘要:虽然散列表在关键字和存储位置之间建立了对应关系,理想情况是无须关键字的比较就可找到待查关键字。但是由于冲突的存在,散列表的查找过程仍是一个和关键字比较的过程,不过散列表的平均查找长度比顺序查找、二分查找等完全依赖于关键字比较的查找要小得多。
注意:
①由同一个散列函数、不同的解决冲突方法构造的散列表,其平均查找长度是不相同的。
②散列表的平均查找长度不是结点个数n的函数,而是装填因子α的函数。因此在设计散列表时可选择α以控制散列表的平均查找长度。
③ α的取值
α越小,产生冲突的机会就小,但α过小,空间的浪费就过多。只要α选择合适,散列表上的平均查找长度就是一个常数,即散列表上查找的平均时间为O(1)。
④ 散列法与其他查找方法的区别
除散列法外,其他查找方法有共同特征为:均是建立在比较关键字的基础上。其中顺序查找是对无序集合的查找,每次关键字的比较结果为=或!=两种可能,其平均时间为O(n);其余的查找均是对有序集合的查找,每次关键字的比较有=、和三种可能,且每次比较后均能缩小下次的查找范围,故查找速度更快,其平均时间为O(lgn)。而散列法是根据关键字直接求出地址的查找方法,其查找的期望时间为O(1)。
阅读全文
2012-11-20 22:17:51
摘要:在用线性查找和二分查找的过程中需要依据关键字进行若干次的比较判断,确定数据集合中是否存在关键字等于某个给定关键字的记录以及该记录在数据表中的位置,查找效率与比较的次数密切相关。在查找时需要不断进行比较的原因是建立数据表时,只考虑了各记录的关键字之间的相对大小,记录在表中的位置和其关键字无直接关系。而之前介绍的哈希表就记录了存储位置和其关键字之间的某种直接关系,那么使用哈希表进行查找时,就无须比较或只做很少的比较久能直接由关键字找到相应的记录。实际上哈希表也就是为解决查找问题提出的。具体哈希表的内容请参考之前的“散列表”相关文章。
下面我将通过实例来说明哈希算法的实现。
【例】数字序列{70,30,40,10,80,20,90,100,75,80,45}采用哈希表存放。哈希函数采用除13取余数,哈希冲突解决方法采用链表法。实现该实例的程序如下:
class chaintype
{
private int key;
private chaintype next;
public int Key
{
get
{
return key;
}
set
{
key = value;
}
}
public chaintype Next
{
get
{
return next;
}
set
{
next = value;
}
}
}
class SearchArithMetic
{
/* 除模取余法的哈希函数*/
public int Hash(int key, int Mod)
{
return key % Mod;
}
/*在哈希表中插入记录,用链表法解决冲突*/
public bool HashInsert(chaintype[] a, int Key, int Mod)
{
int i;
i = Hash(Key, Mod);
chaintype pre;
chaintype cur;
pre = a[i];
cur = a[i];
while (cur != null cur.Key != Key)
{
pre = cur;
cur = cur.Next;
}
/* 未查找到时插入该记录在对应的链表尾*/
……
阅读全文