在正式进入主题之前我们来看下几个概念:
依赖倒置是编程五大原则之一,即:
1、上层模块不应该依赖于下层模块,它们共同依赖于一个抽象。
2、抽象不能依赖于具体,具体依赖于抽象。
其中上层就是指使用者,下层就是指被使用者。
控制反转(IoC,全称Inversion of Control
)是一种思想,所谓“控制反转”,就是反转获得依赖对象的过程。
依赖注入设计模式是一种在类及其依赖对象之间实现控制反转(IoC)思想的技术。
所谓依赖注入(DI,全称Dependency Injection
),就是由IoC容器在运行期间,动态地将某种依赖关系注入到对象之中。
依赖注入主要分为3种:构造函数注入、属性注入、方法注入。
这里就不做过多的描述,如果有机会会通过具体例子再和大家分享,下面我们正式进入本章主题。
PS:ASP.NET Core 内置的IoC容器目前只支持构造函数注入,以下我们也主要讲解构造函数注入的方式。
话不多说,直入主题看我们的解决方案结构:
分别对上面的工程进行简单的说明:
1、TianYa.DotNetShare.Model
:为demo的实体层
2、TianYa.DotNetShare.Repository
:为demo的仓储层即数据访问层
3、TianYa.DotNetShare.Service
:为demo的服务层即业务逻辑层
4、TianYa.DotNetShare.CommTool
:为demo的公共工具类库
5、TianYa.DotNetShare.SharpCore
:为demo的Sharp
核心类库
6、TianYa.DotNetShare.CoreMvcDemo
:为demo的web层项目,MVC框架
约定:
1、公共的类库,我们选择.NET Standard 2.0
作为目标框架,可与Framework
进行共享。
2、本demo的web
项目为ASP.NET Core Web
应用程序(.NET Core 2.2) MVC框架。
using System;
using System.Collections.Generic;
using System.Text;
namespace TianYa.DotNetShare.Model
{
///
/// 学生类
///
public class Student
{
///
/// 学号
///
public string StuNo { get; set; }
///
/// 姓名
///
public string Name { get; set; }
///
/// 年龄
///
public int Age { get; set; }
///
/// 性别
///
public string *** { get; set; }
}
}
demo中的实体就这样了
本demo的仓储层需要引用我们的实体层TianYa.DotNetShare.Model
为什么选择用仓储,原因很简单,方便我们进行个性化扩展。在数据操作的底层进行其他个性化逻辑处理。
约定:
1、接口的定义放在根目录下,接口的实现类,统一放到Impl
文件夹,表示实现类目录。
2、每个实体,对应一个仓储的接口和实现类,即有多少个实体,就对应创建多少个接口和实现类。
3、仓储层接口都以“I”开头,以“Repository
”结尾。仓储层实现都以“Repository
”结尾。
我们新建一个Student
的仓储接口IStudentRepository.cs
using System;
using System.Collections.Generic;
using System.Text;
using TianYa.DotNetShare.Model;
namespace TianYa.DotNetShare.Repository
{
///
/// 学生类仓储层接口
///
public interface IStudentRepository
{
///
/// 根据学号获取学生信息
///
/// 学号
/// 学生信息
Student GetStuInfo(string stuNo);
}
}
接着在Impl
中新建一个Student
的仓储实现StudentRepository.cs
using System;
using System.Collections.Generic;
using System.Text;
using TianYa.DotNetShare.Model;
namespace TianYa.DotNetShare.Repository.Impl
{
///
/// 学生类仓储层
///
public class StudentRepository : IStudentRepository
{
///
/// 根据学号获取学生信息
///
/// 学号
/// 学生信息
public Student GetStuInfo(string stuNo)
{
//数据访问逻辑,此处为了演示就简单些
var student = new Student();
switch (stuNo)
{
case "10000":
student = new Student() { StuNo = "10000", Name = "张三", *** = "男", Age = 20 };
break;
case "10001":
student = new Student() { StuNo = "10001", Name = "钱七七", *** = "女", Age = 18 };
break;
case "10002":
student = new Student() { StuNo = "10002", Name = "李四", *** = "男", Age = 21 };
break;
default:
student = new Student() { StuNo = "10003", Name = "王五", *** = "男", Age = 25 };
break;
}
return student;
}
}
}
该类实现了IStudentRepository
接口
本demo的服务层需要引用我们的实体层TianYa.DotNetShare.Model
和我们的仓储层TianYa.DotNetShare.Repository
服务层与仓储层类似,它属于仓储层的使用者。定义的方式也与仓储层类似,有接口和Impl
实现目录。
但服务层不需要一个实体对应一个,服务层更多的是按照功能模块进行划分,比如一个登录模块,创建一个LoginService
。
约定:
1、服务层接口都以“I”开头,以“Service
”结尾。服务层实现都以“Service
”结尾。
为了演示,我们新建一个Student
的服务层接口IStudentService.cs
using System;
using System.Collections.Generic;
using System.Text;
using TianYa.DotNetShare.Model;
namespace TianYa.DotNetShare.Service
{
///
/// 学生类服务层接口
///
public interface IStudentService
{
///
/// 根据学号获取学生信息
///
/// 学号
/// 学生信息
Student GetStuInfo(string stuNo);
}
}
接着我们同样在Impl
中新建一个Student
的服务层实现StudentService.cs
using System;
using System.Collections.Generic;
using System.Text;
using TianYa.DotNetShare.Model;
using TianYa.DotNetShare.Repository;
namespace TianYa.DotNetShare.Service.Impl
{
///
/// 学生类服务层
///
public class StudentService : IStudentService
{
///
/// 定义仓储层学生抽象类对象
///
protected IStudentRepository StuRepository;
///
/// 空构造函数
///
public StudentService() { }
///
/// 构造函数
///
/// 仓储层学生抽象类对象
public StudentService(IStudentRepository stuRepository)
{
this.StuRepository = stuRepository;
}
///
/// 根据学号获取学生信息
///
/// 学号
/// 学生信息
public Student GetStuInfo(string stuNo)
{
var stu = StuRepository.GetStuInfo(stuNo);
return stu;
}
}
}
该类实现了IStudentService
接口
公共工具类库就是将来我们要在里面写各种各样的帮助类以提高程序的可复用性,此处就不做赘述。
Sharp
核心类库
需要从NuGet上引用以下几个程序集:
Sharp
核心类库为公共的基础类,最底层。
其中Model
文件夹为实体目录,主要存放数据库连接相关的实体。Extensions
文件夹为扩展目录,主要存放最底层的扩展类,我们底层的批量依赖注入就放在这里面。
在Model
实体目录中我们新建一个用于数据库连接的接口IDataBaseSetting.cs
using System;
using System.Collections.Generic;
using System.Text;
namespace TianYa.DotNetShare.SharpCore.Model
{
public interface IDataBaseSetting
{
///
/// 访问数据库连接串
///
string ConnectionString { get; set; }
///
/// 数据库名称,当是关系型数据库时,DatabaseName属性没用到
///
string DatabaseName { get; set; }
}
}
接着添加一个用于数据库连接的实现类DataBaseSetting.cs
using System;
using System.Collections.Generic;
using System.Text;
namespace TianYa.DotNetShare.SharpCore.Model
{
public class DataBaseSetting : IDataBaseSetting
{
///
/// 访问数据库连接串
///
public string ConnectionString { get; set; }
///
/// 数据库名称,当是关系型数据库时,DatabaseName属性没用到
///
public string DatabaseName { get; set; }
}
}
该类实现了IDataBaseSetting.cs
接口
Model
实体目录主要用于以后涉及到数据库访问的时候使用,本demo主要为了简单介绍下如何使用ASP.NET Core内置的IoC容器DI进行批量依赖注入,故没有对该实体目录进行详细的讲解。
接下来就是重头戏了,我们在Extensions
扩展目录中添加一个用于批量依赖注入的扩展类ServiceCollectionExtensions.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyModel;
using TianYa.DotNetShare.SharpCore.Model;
namespace TianYa.DotNetShare.SharpCore.Extensions
{
///
/// ServiceCollection扩展
///
public static class ServiceCollectionExtensions
{
#region 通过反射批量注入指定的程序集
///
/// 通过反射批量注入指定的程序集
///
/// 服务
/// 程序集数组 如:["TianYa.DotNetShare.Repository","TianYa.DotNetShare.Service"],无需写dll
public static void RegisterTianYaSharpService(this IServiceCollection services, params string[] assemblyNames)
{
foreach (string assemblyName in assemblyNames)
{
foreach (var itemClass in GetClassInterfacePairs(assemblyName))
{
foreach (var itemInterface in itemClass.Value)
{
if (itemInterface != typeof(DataBaseSetting))
{
services.AddTransient(itemInterface, itemClass.Key); //DI依赖注入
}
}
}
}
}
#endregion
#region DI依赖注入辅助方法
///
/// 获取类以及类实现的接口键值对
///
/// 程序集名称
/// 类以及类实现的接口键值对
private static Dictionary<Type, List<Type>> GetClassInterfacePairs(string assemblyName)
{
//存储 实现类 以及 对应接口
Dictionary<Type, List<Type>> dic = new Dictionary<Type, List<Type>>();
Assembly assembly = GetAssembly(assemblyName);
if (assembly != null)
{
Type[] types = assembly.GetTypes();
foreach (var item in types.AsEnumerable().Where(x => !x.IsAbstract && !x.IsInterface && !x.IsGenericType))
{
dic.Add(item, item.GetInterfaces().Where(x => !x.IsGenericType).ToList());
}
}
return dic;
}
///
/// 获取所有的程序集
///
/// 程序集集合
private static List<Assembly> GetAllAssemblies()
{
var list = new List<Assembly>();
var deps = DependencyContext.Default;
var libs = deps.CompileLibraries.Where(lib => !lib.Serviceable && lib.Type != "package");//排除所有的系统程序集、Nuget下载包
foreach (var lib in libs)
{
try
{
var assembly = AssemblyLoadContext.Default.LoadFromAssemblyName(new AssemblyName(lib.Name));
list.Add(assembly);
}
catch (Exception)
{
// ignored
}
}
return list;
}
///
/// 获取指定的程序集
///
/// 程序集名称
/// 程序集
private static Assembly GetAssembly(string assemblyName)
{
return GetAllAssemblies().FirstOrDefault(assembly => assembly.FullName.Contains(assemblyName));
}
#endregion
}
}
并且添加一个Dynamic
的扩展类DynamicExtensions.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Dynamic;
using System.Linq;
using System.Xml;
namespace TianYa.DotNetShare.SharpCore.Extensions
{
///
/// Dynamic的扩展方法
///
public static class DynamicExtensions
{
#region 匿名对象处理
#region 将对象[主要是匿名对象]转换为dynamic
///
/// 将对象[主要是匿名对象]转换为dynamic
///
public static dynamic ToDynamic(this object value)
{
IDictionary<string, object> expando = new ExpandoObject();
var type = value.GetType();
var properties = TypeDescriptor.GetProperties(type);
foreach (PropertyDescriptor property in properties)
{
var val = property.GetValue(value);
if (property.PropertyType.FullName.StartsWith("<>f__AnonymousType"))
{
dynamic dval = val.ToDynamic();
expando.Add(property.Name, dval);
}
else
{
expando.Add(property.Name, val);
}
}
return expando as ExpandoObject;
}
#endregion
#region 将对象[主要是匿名对象]转换为List
///
/// 将对象[主要是匿名对象]转换为List
///
public static List<dynamic> ToDynamicList(this IEnumerable<dynamic> values)
{
var list = new List<dynamic>();
if (values != null)
{
if (values.Any())
{
list.AddRange(values.Select(v => ((object)v).ToDynamic()));
}
}
return list;
}
#endregion
#region 将匿名对象集合转换为XML
///
/// 将匿名对象集合转换为XML
///
public static XmlDocument ListObjertToXML(this IEnumerable<dynamic> values)
{
var xmlDoc = new XmlDocument();
var xmlElem = xmlDoc.CreateElement("DocumentElement");
xmlDoc.AppendChild(xmlElem);
if (values != null)
{
if (values.Any())
{
var node = xmlDoc.SelectSingleNode("DocumentElement");
foreach (var item in values)
{
var xmlRow = xmlDoc.CreateElement("Row");
ObjectToXML(item, xmlDoc, xmlRow);
node.AppendChild(xmlRow);
}
}
}
return xmlDoc;
}
#endregion
#region 将匿名对象填充XML节点
///
/// 将匿名对象填充XML节点
///
private static void ObjectToXML(object value, XmlDocument xmlDoc, XmlElement xmlRow)
{
IDictionary<string, object> expando = new ExpandoObject();
var type = value.GetType();
var properties = TypeDescriptor.GetProperties(type);
foreach (PropertyDescriptor property in properties)
{
var val = property.GetValue(value);
xmlRow.CloneNode(false);
var xmlTemp = xmlDoc.CreateElement(property.Name);
XmlText xmlText;
if (property.PropertyType.FullName.StartsWith("<>f__AnonymousType"))
{
dynamic dval = val.ToDynamic();
xmlText = xmlDoc.CreateTextNode(dval.ObjectToString());
}
else
{
xmlText = xmlDoc.CreateTextNode(val.ToString());
}
xmlTemp.AppendChild(xmlText);
xmlRow.AppendChild(xmlTemp);
}
}
#endregion
#endregion
}
}
该扩展类主要在我们的Action
向视图传递匿名类型值的时候使用
1、TianYa.DotNetShare.Model
我们的实体层
2、TianYa.DotNetShare.Service
我们的服务层
3、TianYa.DotNetShare.Repository
我们的仓储层,正常我们的web
项目是不应该使用仓储层的,此处我们引用是为了演示IoC依赖注入
4、TianYa.DotNetShare.CommTool
我们的公共工具类库
5、TianYa.DotNetShare.SharpCore
我们的Sharp
核心类库
到了这里我们所有的工作都已经准备好了,接下来就是开始做注入工作了。
打开我们的Startup.cs
文件进行注入工作:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using TianYa.DotNetShare.SharpCore.Extensions;
namespace TianYa.DotNetShare.CoreMvcDemo
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
//DI依赖注入,批量注入指定的程序集
services.RegisterTianYaSharpService(new string[] { "TianYa.DotNetShare.Repository", "TianYa.DotNetShare.Service" });
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
其中用来实现批量依赖注入的只要一句话就搞定了,如下所示:
//DI依赖注入,批量注入指定的程序集
services.RegisterTianYaSharpService(new string[] { "TianYa.DotNetShare.Repository", "TianYa.DotNetShare.Service" });
Sharp
核心类库在底层实现了批量注入的逻辑,程序集的注入必须按照先后顺序进行,先进行仓储层注入然后再进行服务层注入。
接下来我们来看看控制器里面怎么弄:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using TianYa.DotNetShare.CoreMvcDemo.Models;
using TianYa.DotNetShare.Service;
using TianYa.DotNetShare.Repository;
namespace TianYa.DotNetShare.CoreMvcDemo.Controllers
{
public class HomeController : Controller
{
///
/// 定义仓储层学生抽象类对象
///
protected IStudentRepository StuRepository;
///
/// 定义服务层学生抽象类对象
///
protected IStudentService StuService;
///
/// 通过构造函数进行注入
/// 注意:参数是抽象类,而非实现类,因为已经在Startup.cs中将实现类映射给了抽象类
///
/// 仓储层学生抽象类对象
/// 服务层学生抽象类对象
public HomeController(IStudentRepository stuRepository, IStudentService stuService)
{
this.StuRepository = stuRepository;
this.StuService = stuService;
}
public IActionResult Index()
{
var stu1 = StuRepository.GetStuInfo("10000");
var stu2 = StuService.GetStuInfo("10001");
string msg = $"学号:10000,姓名:{stu1.Name},性别:{stu1.***},年龄:{stu1.Age}
";
msg += $"学号:10001,姓名:{stu2.Name},性别:{stu2.***},年龄:{stu2.Age}";
return Content(msg, "text/html", System.Text.Encoding.UTF8);
}
public IActionResult Privacy()
{
return View();
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
}
至此,完成处理,接下来就是见证奇迹的时刻了,我们来访问一下/home/index
,看看是否能返回学生信息。
我们可以发现,返回了学生的信息,说明我们注入成功了。
另外通过这个例子我们可以发现在注入仓储层对象StudentRepository
时,不仅控制器中注入成功了,而且在服务层中也注入成功了,说明我们ASP.NET Core内置的IoC容器DI依赖注入是全局的。
1、采用的是构造函数注入的方式,在构造函数中初始化赋值。
2、ASP.NET Core内置的IoC容器DI依赖注入是全局的。
3、DI批量依赖注入的核心思想就是根据程序集的名称通过反射获取类以及类实现的接口键值对字典,然后通过循环进行批量注入。
扩展:DI生命周期
生命周期是依赖注入设计原则里一个非常重要的概念,ASP.NET Core 一共有3种生命周期。
1、暂时(Transient
):顾名思义,这种生命周期的对象是暂时的,每次请求都会创建一个新的实例。
services.AddTransient<IStudentRepository, StudentRepository>();
services.AddTransient<IStudentService, StudentService>();
2、作用域(Scoped
):每次请求使用的是同一个实例。
services.AddScoped<IStudentRepository, StudentRepository>();
services.AddScoped<IStudentService, StudentService>();
3、单例(Singleton
):第一次请求时就创建,以后每次请求都是使用相同的实例。
services.AddSingleton<IStudentRepository, StudentRepository>();
services.AddSingleton<IStudentService, StudentService>();
官方文档建议:依赖注入是静态/全局对象访问模式的替代方法,如果将其与静态对象访问混合使用,则可能无法实现依赖关系注入的优点。