Angular 7 配合 .NET Core 2.2 - 全球天气 (第二部分)






4.70/5 (26投票s)
Angular 7 配合 .NET Core 2.2 - 全球天气 (第二部分)
引言
在 Angular 7 配合 .NET Core 2.2 - 全球天气 (第一部分) 中,我们逐步讲解了如何构建一个 Angular 7 应用配合 .NET Core 2.2。在本文中,我们将创建一个 .NET Core API 来保存用户选择的地点,并在用户再次访问时加载最新的地点。
API 控制器
与 ASP.NET 相比,ASP.NET Core 为开发者提供了更好的性能,并且是为跨平台执行而设计的。借助 ASP.NET Core,您的解决方案在 Linux 和 Windows 上都能良好运行。
在 Web API 中,`controller` 是一个处理 HTTP 请求的对象。我们将添加一个 `controller` 来返回和保存最近访问的城市。
添加 CitiesController
首先,删除项目模板自动创建的 `ValuesController`。在解决方案资源管理器中,右键单击 `ValuesController.cs`,然后删除它。
然后,在解决方案资源管理器中,右键单击 `Controllers` 文件夹。选择 **添加**,然后选择 **控制器**。
在“添加脚手架”对话框中,选择 Web **API 控制器 - 空**。单击 **添加**。
在“添加控制器”对话框中,将控制器命名为“`CitiesController`”。单击 **添加**。
脚手架会在 `Controllers` 文件夹中创建一个名为 `CitiesController.cs` 的文件。
暂时保持控制器不变,稍后再回来处理。
使用 EntityFrameworkCore 添加数据库持久化
Entity Framework (EF) Core 是一个轻量级、可扩展且跨平台的版本,源自流行的 Entity Framework 数据访问技术。
EF Core 可以作为对象关系映射器 (O/RM),使 .NET 开发者能够使用 .NET 对象来操作数据库,从而无需编写大部分通常需要的数据访问代码。
在解决方案资源管理器中,添加新项目。
选择“**类库 (.NET Core)**”模板,并将项目命名为“`Weather.Persistence`”。单击“**确定**”。`Weather.Persistence` 项目会在 `GlobalWeather` 解决方案下创建。
删除 `Class1.cs`。右键单击 `Weather.Persistence` 项目,选择“**管理 Nuget 包**”。
在 Nuget 窗口中,安装 Entity Framework Core 的依赖包。它们是 `Microsoft.EntityFrameworkCore`、`Microsoft.EntityFrameworkCore.Design`、`Microsoft.EntityFrameworkCore.Relational` 和 `Microsoft.EntityFrameworkCore.SqlServer`。
此外,我们还安装了一些用于依赖注入、应用程序配置和日志记录的额外包。它们是 `Microsoft.Extensions.DependencyInjection`、`Microsoft.Extensions.Options.ConfigurationExtensions` 和 Serilog。
创建数据库上下文
使用 EF Core,数据访问是通过模型进行的。模型由实体类和一个派生上下文组成,该上下文代表与数据库的会话,允许您查询和保存数据。
您可以从现有数据库生成模型,手动编写模型以匹配您的数据库,或者使用 EF Migrations 从模型创建数据库。
在这里,我们使用数据库优先方法,从现有数据库生成模型。
在 Microsoft SQL Server Management Studio 中创建 `Weather` 数据库。然后,运行以下脚本
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Cities](
[Id] [nvarchar](255) NOT NULL,
[Name] [nvarchar](255) NOT NULL,
[CountryId] [nvarchar](255) NOT NULL,
[AccessedDate] [datetimeoffset](7) NOT NULL
PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
现在,就可以创建 Entity Framework 数据上下文和数据模型了。以下是 `dbcontext scaffold` 命令,它将自动创建 `dbContext` 类和数据模型类。
dotnet ef dbcontext scaffold "Server=.\sqlexpress;Database=Weather;
Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -o Models -c "WeatherDbContext" -f
在运行 `dbcontext scaffold` 命令之前,我们需要考虑数据模型的复数和单数命名问题。
通常,我们使用复数名称创建表,例如“`Cities`”。作为数据集,命名为 `Cities` 是有意义的,但如果我们将模型类命名为“`Cities`”则没有意义。期望的模型类名应该是“`City`”。如果我们不做任何处理,直接运行脚手架命令。生成的数据上下文和模型类如下
您可以看到,它生成了 `Cities` 模型类。然后查看 `WeatherDbContext` 类。
public partial class WeatherDbContext : DbContext
{
public WeatherDbContext()
{
}
public WeatherDbContext(DbContextOptions<WeatherDbContext> options)
: base(options)
{
}
public virtual DbSet<Cities> Cities { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
#warning To protect potentially sensitive information in your connection string,
you should move it out of source code. See http://go.microsoft.com/fwlink/?LinkId=723263
for guidance on storing connection strings.
optionsBuilder.UseSqlServer("Server=.\\sqlexpress;Database=Weather;
Trusted_Connection=True;");
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasAnnotation("ProductVersion", "2.2.1-servicing-10028");
modelBuilder.Entity<Cities>(entity =>
{
entity.Property(e => e.Id)
.HasMaxLength(255)
.ValueGeneratedNever();
entity.Property(e => e.CountryId)
.IsRequired()
.HasMaxLength(255);
entity.Property(e => e.Name)
.IsRequired()
.HasMaxLength(255);
});
}
}
}
`DbSet` 也被调用为 `Cities`。多难看啊!
但实际上,Entity Framework Core 2 支持复数和单数形式。
有一个新的 `IPluralizer 接口`。它可以在 EF 生成数据库(`dotnet ef database update`)或从数据库生成实体(`Scaffold-DbContext`)时用于复数化表名。它的用法有点棘手,因为我们需要有一个实现 `IDesignTimeServices` 的类,并且这些工具会自动发现该类。
有一个 Nuget 包 Inflector 用于实现 `IPluralizer 接口`。
我将 Inflector 中的 `Pluaralizer.cs` 添加到我们的持久化项目中。
public class MyDesignTimeServices : IDesignTimeServices
{
public void ConfigureDesignTimeServices(IServiceCollection services)
{
services.AddSingleton<IPluralizer, Pluralizer>();
}
}
public class Pluralizer : IPluralizer
{
public string Pluralize(string name)
{
return Inflector.Pluralize(name) ?? name;
}
public string Singularize(string name)
{
return Inflector.Singularize(name) ?? name;
}
}
public static class Inflector
{
#region Default Rules
static Inflector()
{
AddPlural("$", "s");
AddPlural("s$", "s");
AddPlural("(ax|test)is$", "$1es");
AddPlural("(octop|vir|alumn|fung)us$", "$1i");
AddPlural("(alias|status)$", "$1es");
AddPlural("(bu)s$", "$1ses");
AddPlural("(buffal|tomat|volcan)o$", "$1oes");
AddPlural("([ti])um$", "$1a");
AddPlural("sis$", "ses");
AddPlural("(?:([^f])fe|([lr])f)$", "$1$2ves");
AddPlural("(hive)$", "$1s");
AddPlural("([^aeiouy]|qu)y$", "$1ies");
AddPlural("(x|ch|ss|sh)$", "$1es");
AddPlural("(matr|vert|ind)ix|ex$", "$1ices");
AddPlural("([m|l])ouse$", "$1ice");
AddPlural("^(ox)$", "$1en");
AddPlural("(quiz)$", "$1zes");
AddSingular("s$", "");
AddSingular("(n)ews$", "$1ews");
AddSingular("([ti])a$", "$1um");
AddSingular("((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$", "$1$2sis");
AddSingular("(^analy)ses$", "$1sis");
AddSingular("([^f])ves$", "$1fe");
AddSingular("(hive)s$", "$1");
AddSingular("(tive)s$", "$1");
AddSingular("([lr])ves$", "$1f");
AddSingular("([^aeiouy]|qu)ies$", "$1y");
AddSingular("(s)eries$", "$1eries");
AddSingular("(m)ovies$", "$1ovie");
AddSingular("(x|ch|ss|sh)es$", "$1");
AddSingular("([m|l])ice$", "$1ouse");
AddSingular("(bus)es$", "$1");
AddSingular("(o)es$", "$1");
AddSingular("(shoe)s$", "$1");
AddSingular("(cris|ax|test)es$", "$1is");
AddSingular("(octop|vir|alumn|fung)i$", "$1us");
AddSingular("(alias|status)$", "$1");
AddSingular("(alias|status)es$", "$1");
AddSingular("^(ox)en", "$1");
AddSingular("(vert|ind)ices$", "$1ex");
AddSingular("(matr)ices$", "$1ix");
AddSingular("(quiz)zes$", "$1");
AddIrregular("person", "people");
AddIrregular("man", "men");
AddIrregular("child", "children");
AddIrregular("sex", "sexes");
AddIrregular("move", "moves");
AddIrregular("goose", "geese");
AddIrregular("alumna", "alumnae");
AddUncountable("equipment");
AddUncountable("information");
AddUncountable("rice");
AddUncountable("money");
AddUncountable("species");
AddUncountable("series");
AddUncountable("fish");
AddUncountable("sheep");
AddUncountable("deer");
AddUncountable("aircraft");
}
#endregion
private class Rule
{
private readonly Regex _regex;
private readonly string _replacement;
public Rule(string pattern, string replacement)
{
_regex = new Regex(pattern, RegexOptions.IgnoreCase);
_replacement = replacement;
}
public string Apply(string word)
{
if (!_regex.IsMatch(word))
{
return null;
}
return _regex.Replace(word, _replacement);
}
}
private static void AddIrregular(string singular, string plural)
{
AddPlural("(" + singular[0] + ")" +
singular.Substring(1) + "$", "$1" + plural.Substring(1));
AddSingular("(" + plural[0] + ")" +
plural.Substring(1) + "$", "$1" + singular.Substring(1));
}
private static void AddUncountable(string word)
{
_uncountables.Add(word.ToLower());
}
private static void AddPlural(string rule, string replacement)
{
_plurals.Add(new Rule(rule, replacement));
}
private static void AddSingular(string rule, string replacement)
{
_singulars.Add(new Rule(rule, replacement));
}
private static readonly List<Rule> _plurals = new List<Rule>();
private static readonly List<Rule> _singulars = new List<Rule>();
private static readonly List<string> _uncountables = new List<string>();
public static string Pluralize(this string word)
{
return ApplyRules(_plurals, word);
}
public static string Singularize(this string word)
{
return ApplyRules(_singulars, word);
}
#if NET45 || NETFX_CORE
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
private static string ApplyRules(List<Rule> rules, string word)
{
string result = word;
if (!_uncountables.Contains(word.ToLower()))
{
for (int i = rules.Count - 1; i >= 0; i--)
{
if ((result = rules[i].Apply(word)) != null)
{
break;
}
}
}
return result;
}
public static string Titleize(this string word)
{
return Regex.Replace(Humanize(Underscore(word)), @"\b([a-z])",
delegate(Match match) { return match.Captures[0].Value.ToUpper(); });
}
public static string Humanize(this string lowercaseAndUnderscoredWord)
{
return Capitalize(Regex.Replace(lowercaseAndUnderscoredWord, @"_", " "));
}
public static string Pascalize(this string lowercaseAndUnderscoredWord)
{
return Regex.Replace(lowercaseAndUnderscoredWord, "(?:^|_)(.)",
delegate(Match match) { return match.Groups[1].Value.ToUpper(); });
}
public static string Camelize(this string lowercaseAndUnderscoredWord)
{
return Uncapitalize(Pascalize(lowercaseAndUnderscoredWord));
}
public static string Underscore(this string pascalCasedWord)
{
return Regex.Replace(
Regex.Replace(
Regex.Replace(pascalCasedWord, @"([A-Z]+)([A-Z][a-z])", "$1_$2"), @"([a-z\d])([A-Z])",
"$1_$2"), @"[-\s]", "_").ToLower();
}
public static string Capitalize(this string word)
{
return word.Substring(0, 1).ToUpper() + word.Substring(1).ToLower();
}
public static string Uncapitalize(this string word)
{
return word.Substring(0, 1).ToLower() + word.Substring(1);
}
public static string Ordinalize(this string numberString)
{
return Ordanize(int.Parse(numberString), numberString);
}
public static string Ordinalize(this int number)
{
return Ordanize(number, number.ToString());
}
#if NET45 || NETFX_CORE
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
private static string Ordanize(int number, string numberString)
{
int nMod100 = number % 100;
if (nMod100 >= 11 && nMod100 <= 13)
{
return numberString + "th";
}
switch (number % 10)
{
case 1:
return numberString + "st";
case 2:
return numberString + "nd";
case 3:
return numberString + "rd";
default:
return numberString + "th";
}
}
public static string Dasherize(this string underscoredWord)
{
return underscoredWord.Replace('_', '-');
}
}
然后,在 PowerShell 中,转到 `GlobalWeather\Weather.Persistence` 文件夹,运行以下命令
dotnet ef dbcontext scaffold "Server=.\sqlexpress;Database=Weather;
Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -o Models -c "WeatherDbContext" -f
运行此命令后,`City.cs` 和 `WeatherDbContext.cs` 会在 `Models` 文件夹下生成。
请注意,`city` 模型是“`City`”而不是“`Cities`”。这比以前好多了。
向 .NET Core 应用添加 Serilog
与其他的日志库不同,Serilog 是基于强大的结构化事件数据构建的。Serilog 通过 `Serilog.AspNetCore` 库为 ASP.NET Core 应用添加日志提供了很好的支持,并且还有大量可用的接收器。
安装库
您可以使用程序包管理器将 Serilog NuGet 包安装到您的应用程序中。您还需要添加至少一个“接收器”——Serilog 将日志消息写入此处。例如,`Serilog.Sinks.Console` 将消息写入控制台。
右键单击 `GlobalWeather` 项目,选择“**管理 Nuget 包**”。
在应用程序中配置 Serilog
恢复包后,您可以配置应用程序以使用 `Serilog`。推荐的方法是首先配置 `Serilog` 的 `static Log.Logger` 对象,然后再配置 ASP.NET Core 应用程序。
首先,在 `appsettings.json` 中,使用以下 `Serilog` 配置替换默认日志记录。
"Serilog": {
"MinimumLevel": "Debug",
"WriteTo": [
{
"Name": "File",
"Args": {
"path": "log\\log.txt",
"rollingInterval": "Day"
}
}
]
}
然后,对默认的 `Startup.cs` 类进行一些更改。
在 `Startup` 方法中添加以下行,它配置了 `Log.Logger`。
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(Configuration)
.CreateLogger();
在 `ConfigureServices` 方法中添加以下行以注入 `Log.Logger`。
services.AddSingleton(Log.Logger);
为 .NET Core 应用添加连接字符串
ASP.NET Core 中的配置
在 .NET Core 中,配置系统非常灵活,连接字符串可以存储在 `appsettings.json`、环境变量、用户机密存储或其他配置源中。现在,我们将连接字符串存储在 `appsettings.json` 中。
打开 `GlobalWeather` 项目文件夹下的 `appsettings.json`,并添加以下行
"DbConnectionString": "Server=.\\sqlexpress;Database=Weather;Trusted_Connection=True;"
ASP.NET Core 中的应用程序配置基于配置提供程序建立的键值对。配置提供程序从各种配置源将配置数据读取为键值对。
Options 模式是配置概念的扩展。Options 使用类来表示一组相关的设置。
一个 `options` 类必须是非抽象的,并具有一个 `public` 参数化构造函数。
为 `Weather.Persistence` 项目创建一个 `DbContextSettings` 类。
public class DbContextSettings
{
/// <summary>
/// DbConnectingString from appsettings.json
/// </summary>
public string DbConnectionString { get; set; }
}
将配置绑定到您的类
设置 `ConfigurationBuilder` 来加载您的文件。当您从默认模板创建新的 ASP.NET Core 应用程序时,`ConfigurationBuilder` 已在 `Startup.cs` 中配置为从环境变量、`appsettings.json` 加载设置。
为了将 `settings` 类绑定到您的配置,您需要在 `Startup.cs` 的 `ConfigureServices` 方法中进行配置。
设置 `ConfigurationBuilder` 来加载您的文件。当您从默认模板创建新的 ASP.NET Core 应用程序时,`ConfigurationBuilder` 已在 `Startup.cs` 中配置为从环境变量、`appsettings.json` 加载设置。
为了将 `settings` 类绑定到您的配置,您需要在 `Startup.cs` 的 `ConfigureServices` 方法中进行配置。
public void ConfigureServices(IServiceCollection services)
{
services.Configure<DbContextSettings>(Configuration);
}
由于 `DBContextSettings` 定义在 `Weather.Persistence` 项目中,因此您必须将 `Weather.Persistence` 项目引用添加到 `GlobalWeather` 项目。
右键单击 `GlobalWeather` 项目的 Dependencies,然后选择“**添加引用**”。在“引用管理器”窗口中,选择“**Weather.Persistence**”并单击“**确定**”。
添加 `Weather.Persistence.Config` 命名空间后,编译错误消失。
另外,由于我们从 `appsettings.json` 读取连接字符串,因此可以从 `WeatherDbContext.cs` 的 `OnConfiguring` 中删除硬编码的连接字符串。
删除以下行
optionsBuilder.UseSqlServer("Server=.\\sqlexpress;Database=Weather;Trusted_Connection=True;");
添加 DbContextFactory 类
现在,我们可以在应用程序配置文件中使用连接字符串来创建 `DBContextFactory`。
`DbContextFactory` 类是一个 `factory` 类,用于创建 Db context,这里是 `WeatherDbContext`。
右键单击 `Weather.Persistence` 项目,添加 `Repositories` 文件夹。然后添加 `IDbContextFactory` 接口和 `DBContextFactory` 类。
IDbContextFactory 接口
public interface IDbContextFactory
{
WeatherDbContext DbContext { get; }
}
DbContextFactory 类
public class DbContextFactory : IDbContextFactory, IDisposable
{
/// <summary>
/// Create Db context with connection string
/// </summary>
/// <param name="settings"></param>
public DbContextFactory(IOptions<DbContextSettings> settings)
{
var options = new DbContextOptionsBuilder<WeatherDbContext>().UseSqlServer
(settings.Value.DbConnectionString).Options;
DbContext = new WeatherDbContext(options);
}
/// <summary>
/// Call Dispose to release DbContext
/// </summary>
~DbContextFactory()
{
Dispose();
}
public WeatherDbContext DbContext { get; private set; }
/// <summary>
/// Release DB context
/// </summary>
public void Dispose()
{
DbContext?.Dispose();
}
}
添加泛型 Repository 类
Repository 模式是创建企业级应用最流行的模式之一。它限制我们直接与应用程序中的数据交互,并为数据库操作、业务逻辑和应用程序的 UI 创建新的层。
使用 Repository 模式有许多优点
- 您的业务逻辑可以在不包含数据访问逻辑的情况下进行单元测试。
- 数据库访问代码可以被重用。
- 您的数据库访问代码是集中管理的,因此可以轻松实现任何数据库访问策略,例如缓存。
- 易于实现领域逻辑。
- 您的领域实体或业务实体带有注释,并且是强类型的;等等。
总的来说,每个数据集类都应该有一个 `repository` 类。如果我们使用泛型 `repository`,它将重用所有通用代码,并减少大部分重复代码。
我们在 `Weather.Persistence` 项目的 `Repositories` 文件夹中添加了 `IRepository` 接口和 `Repository` 类。
IRepository 接口
public interface IRepository<T> where T : class
{
Task<T> GetEntity(object id);
Task<T> AddEntity(T entity);
Task<T> UpdateEntity(T entity);
Task<bool> DeleteEntity(object id);
}
Repository 类
public class Repository<TEntity> : IRepository<TEntity>
where TEntity : class
{
private readonly IDbContextFactory _dbContextFactory;
protected ILogger Logger;
public Repository(IDbContextFactory dbContextFactory, ILogger logger)
{
_dbContextFactory = dbContextFactory;
Logger = logger;
}
protected WeatherDbContext DbContext => _dbContextFactory?.DbContext;
/// <summary>
/// Get Entity
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public async Task<TEntity> GetEntity(object id)
{
var entity = await DbContext.FindAsync<TEntity>(id);
return entity;
}
/// <summary>
/// Add Entity
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
public async Task<TEntity> AddEntity(TEntity entity)
{
try
{
var result = await DbContext.AddAsync<TEntity>(entity);
await DbContext.SaveChangesAsync();
return result.Entity;
}
catch (Exception ex)
{
Logger.Error(ex, "Unhandled Exception");
throw;
}
}
/// <summary>
/// Update Entity
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
public async Task<TEntity> UpdateEntity(TEntity entity)
{
DbContext.Update<TEntity>(entity);
await DbContext.SaveChangesAsync();
return entity;
}
/// <summary>
/// Delete Entity
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public async Task<bool> DeleteEntity(object id)
{
var entity = await DbContext.FindAsync<TEntity>(id);
if (entity != null)
{
DbContext.Remove<TEntity>(entity);
await DbContext.SaveChangesAsync();
}
return true;
}
}
Async 和 Await
我们在 Entity Framework Core 查询和保存中使用 `async` 和 `await` 模式。通过使用异步编程,您可以避免性能瓶颈并提高应用程序的整体响应能力。
对于潜在阻塞的活动(如 Web 访问),异步是必不可少的。访问 Web 资源有时会很慢或延迟。如果此类活动在同步进程中被阻塞,整个应用程序都必须等待。在异步进程中,应用程序可以继续执行其他不依赖于 Web 资源的工作,直到潜在的阻塞任务完成。
对于访问 UI 线程的应用程序,异步尤其有价值,因为所有 UI 相关活动通常共享一个线程。如果在同步应用程序中阻塞了任何进程,所有进程都会被阻塞。您的应用程序将停止响应,您可能会认为它已失败,而实际上它只是在等待。
添加特定的 CityRepository 类
泛型 `Repository` 类只包含实体 `dataset` 的通用方法和属性。有时,某些 `dataset` 需要一些更具体的方法和属性。对于这些实体,我们需要创建派生自泛型 `repository` 类的 `repository` 子类。
我们需要执行的任务是获取和保存最后访问的城市。因此,我们需要将 `InsertOrUpdateCityAsync` 和 `GetLastAccessedCityAsync` 方法添加到 `CityRepository` `class`。
我们在 `Weather.Persistence` 项目的 `Repositories` 文件夹中添加了 `ICityRepository` 接口和 `CityRepository` 类。
ICityRepository 接口
public interface ICityRepository : IRepository<City>
{
Task<City> GetLastAccessedCityAsync();
Task InsertOrUpdateCityAsync(City city);
}
CityRepository 类
public class CityRepository : Repository<City>, ICityRepository
{
public CityRepository(IDbContextFactory dbContextFactory, ILogger logger) :
base(dbContextFactory, logger)
{
}
/// <summary>
/// GetLastAccessedCityAsync
/// </summary>
/// <returns>City</returns>
public async Task<City> GetLastAccessedCityAsync()
{
var city = await DbContext.Cities.OrderByDescending(x=>x.AccessedDate).FirstOrDefaultAsync();
return city;
}
/// <summary>
/// InsertOrUpdateCityAsync
/// </summary>
/// <param name="city"></param>
/// <returns></returns>
public async Task InsertOrUpdateCityAsync(City city)
{
var entity = await GetEntity(city.Id);
if (entity != null)
{
entity.Name = city.Name;
entity.CountryId = city.CountryId;
entity.AccessedDate = city.AccessedDate;
await UpdateEntity(entity);
}
else
{
await AddEntity(city);
}
}
}
使用 .NET Core 进行依赖注入
为了解决硬编码对服务实现的引用问题,依赖注入提供了一种间接机制,客户端(或应用程序)不再使用 new 运算符直接实例化服务,而是向服务集合或“工厂”请求实例。此外,与其向服务集合请求特定类型(从而创建紧耦合的引用),不如请求一个 `interface`,并期望服务提供者实现该 `interface`。
结果是,虽然客户端将直接引用 `abstract` 程序集,定义服务 `interface`,但不需要直接引用实现。
依赖注入会注册客户端请求的类型(通常是 `interface`)与将返回的类型之间的关联。此外,依赖注入通常会确定返回类型的生命周期,特别是,在所有对该类型的请求之间是否会有一个共享的单个实例,每次请求是否会有一个新实例,或者介于两者之间。
依赖注入的一个特别常见的需求是在单元测试中。所需要做的就是“配置”DI 框架以返回一个 mock 服务。
提供“服务”的实例而不是让客户端直接实例化它是依赖注入的基本原则。
要利用 .NET Core DI 框架,您只需要一个对 `Microsoft.Extensions.DependencyInjection.Abstractions` NuGet 包的引用。这提供了对 `IServiceCollection interface` 的访问,它公开了一个 `System.IServiceProvider`,您可以从中调用 `GetService<TService>`。类型参数 `TService` 标识要检索的服务类型(通常是 `interface`),从而应用程序代码获取一个实例。
注入 DbContextFactory 和 CityRepository
在 `Weather.Persistence` 项目的 `Repositories` 文件夹中添加 `RespositoryInjectionModule` `static class`。这个 `static class` 为 `IServiceCollection` 添加了一个扩展方法。
public static class RepositoryInjectionModule
{
/// <summary>
/// Dependency inject DbContextFactory and CustomerRepository
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection InjectPersistence(this IServiceCollection services)
{
services.AddScoped<IDbContextFactory, DbContextFactory>();
services.AddTransient<ICityRepository, CityRepository>();
return services;
}
}
然后将 `services.InjectPersistence()` 添加到 `Startup.cs` 的 `ConfigureService` 中。
public void ConfigureServices(IServiceCollection services)
{
services.Configure<DbContextSettings>(Configuration);
//Inject logger
services.AddSingleton(Log.Logger);
services.InjectPersistence();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "WeatherClient/dist";
});
}
services.AddTransient、service.AddScoped 和 service.AddSingleton 之间的区别
为每个注册的服务选择合适的生命周期。ASP.NET Core 服务可以配置为以下生命周期:
瞬时对象始终是不同的;每个控制器和服务都会获得一个新实例。
作用域对象在请求内是相同的,但在不同请求之间是不同的。
单例对象对于每个对象和每个请求都是相同的。
添加 CityService 类
我不想直接从 API 控制器调用存储库,最佳实践是添加一个服务。然后从服务调用存储库。
右键单击 `GlobalWeather` 项目,添加一个新文件夹,“*Services*”。将 `ICityService` 接口和 `CityService` 类添加到此文件夹。
ICityService 接口
public interface ICityService
{
Task<City> GetLastAccessedCityAsync();
Task UpdateLastAccessedCityAsync(City city);
}
CityService 类
public class CityService : ICityService
{
private readonly ICityRepository _repository;
private readonly ILogger _logger;
public CityService(ICityRepository repository, ILogger logger)
{
_repository = repository;
_logger = logger;
}
/// <summary>
/// GetLastAccessedCityAsync
/// </summary>
/// <returns>City</returns>
public async Task<City> GetLastAccessedCityAsync()
{
var city = await _repository.GetLastAccessedCityAsync();
return city;
}
/// <summary>
/// UpdateLastAccessedCityAsync
/// </summary>
/// <param name="city"></param>
/// <returns></returns>
public async Task UpdateLastAccessedCityAsync(City city)
{
city.AccessedDate = DateTimeOffset.UtcNow;
await _repository.InsertOrUpdateCityAsync(city);
}
}
依赖注入 CityService
将 `ServiceInjectionModule` `static class` 添加到 `Services` 文件夹。与之前一样,这个 `static class` 为 `IServiceCollection` 添加了另一个扩展方法。
public static class ServiceInjectionModule
{
/// <summary>
/// Dependency inject services
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection InjectServices(this IServiceCollection services)
{
services.AddTransient<ICityService, CityService>();
return services;
}
}
然后将 `services.InjectServices ()` 添加到 `Startup.cs` 的 `ConfigureService` 中。
public void ConfigureServices(IServiceCollection services)
{
services.Configure<DbContextSettings>(Configuration);
//Inject logger
services.AddSingleton(Log.Logger);
services.InjectPersistence();
services.InjectServices();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "WeatherClient/dist";
});
}
在 CitiesController 中调用 CityService
现在是时候更新 `CitiesController` 了。
首先,在构造函数中注入 `CityService` 和 `Logger` 实例。
public CitiesController(ICityService service, ILogger logger)
{
_service = service;
_logger = logger;
}
添加 `HttpGet` 以获取最后访问的 `city`。
// GET api/cities
[HttpGet]
public async Task<ActionResult<City>> Get()
{
var city = await _service.GetLastAccessedCityAsync();
return city;
}
添加 `HttpPost` 以保存 `city`。
[HttpPost]
public async Task Post([FromBody] City city)
{
await _service.UpdateLastAccessedCityAsync(city);
}
从 Angular 前端调用 API
现在,我们需要回到 Angular 前端调用 `City` API 来保存和获取最后访问的 `city`。
首先,我们需要创建一个 `model` 类来映射 JSON。
在 `src/app/shared/models/` 文件夹下创建一个名为 `city-meta-data` 的文件。定义一个 `CityMetaData` 类并导出它。该文件应如下所示
import { City } from './city';
export class CityMetaData {
public id: string;
public name: string;
public countryId: string;
public constructor(city: City) {
this.id = city.Key;
this.name = city.EnglishName;
this.countryId = city.Country.ID;
}
}
打开 `src/app/app.constants.ts` 下的 `app.constants.ts`。添加一个新常量,它是 City API 的 URL。您应该知道,此 URL 是相对 URL。相对 URL 可确保它在任何环境中都能正常工作。
static cityAPIUrl = '/api/cities';
在 `src/app/shared/services/` 文件夹下创建一个名为 `city` 的 `service`。
ng generate service city
该命令将在 `src/app/city.service.ts` 中生成骨架 `CityService` 类。
然后将 `getLastAccessedCity` 和 `updateLastAccessedCity` 方法添加到 `CityService` `class`。
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { Constants } from '../../../app/app.constants';
import { City } from '../models/city';
import { CityMetaData } from '../models/city-meta-data';
import { catchError, map, tap } from 'rxjs/operators';
import { ErrorHandleService } from './error-handle.service';
@Injectable({
providedIn: 'root'
})
export class CityService {
constructor(
private http: HttpClient,
private errorHandleService: ErrorHandleService) { }
getLastAccessedCity(): Observable<City> {
const uri = decodeURIComponent(`${Constants.cityAPIUrl}`);
return this.http.get<CityMetaData>(uri)
.pipe(
map(res => {
const data = res as CityMetaData;
const city = {
Key: data.id,
EnglishName: data.name,
Type: 'City',
Country:
{
ID: data.countryId,
EnglishName: ''
}
};
return city;
}),
tap(_ => console.log('fetched the last accessed city')),
catchError(this.errorHandleService.handleError('getLastAccessedCity', null))
);
}
updateLastAccessedCity(city: City) {
const uri = decodeURIComponent(`${Constants.cityAPIUrl}`);
var data = new CityMetaData(city);
return this.http.post(uri, data)
.pipe(
catchError(this.errorHandleService.handleError('updateLastAccessedCity', []))
);
}
}
Weather Component
打开 `src/app/weather/weather.component.ts` 下的 `weather.component.ts`。
在构造函数中导入 CityServer 并注入
constructor(
private fb: FormBuilder,
private locationService: LocationService,
private currentConditionService: CurrentConditionsService,
private cityService: CityService) {
}
保存用户选择的 City
添加 `UpdateLastAccessedCity` 方法。
async updateLastAccessedCity(city: City) {
const promise = new Promise((resolve, reject) => {
this.cityService.updateLastAccessedCity(city)
.toPromise()
.then(
_ => { // Success
resolve();
},
err => {
console.error(err);
//reject(err);
resolve();
}
);
});
await promise;
}
在获取 `city` 后调用它。
async search() {
this.weather = null;
this.errorMessage = null;
const searchText = this.cityControl.value as string;
if (!this.city ||
this.city.EnglishName !== searchText ||
!this.city.Key ||
!this.city.Country ||
!this.city.Country.ID) {
await this.getCity();
await this.updateLastAccessedCity(this.city);
}
await this.getCurrentConditions();
}
从 ngOnInit 获取最后访问的 City
添加 `getLastAccessedCity` 方法。
async getLastAccessedCity() {
const promise = new Promise((resolve, reject) => {
this.cityService.getLastAccessedCity()
.toPromise()
.then(
res => { // Success
const data = res as City;
if (data) {
this.city = data;
}
resolve();
},
err => {
console.error(err);
//reject(err);
resolve();
}
);
});
await promise;
if (this.city) {
const country = this.countries.filter(x => x.ID === this.city.Country.ID)[0];
this.weatherForm.patchValue({
searchGroup: {
country: country,
city: this.city.EnglishName
}
});
}
}
获取最后访问的 `city` 后,修补响应式表单字段。
从 `ngOnInit` 调用 `getLastAccessedCity`。
async ngOnInit() {
this.weatherForm = this.buildForm();
await this.getCountries();
await this.getLastAccessedCity();
this.errorMessage = null;
if (this.weatherForm.valid)
await this.search();
else {
this.errorMessage = "Weather is not available. Please specify a location.";
}
}
从前端到后端的调试
好的。现在我将展示整个端到端的工作流程。
在 Chrome 中,我们在 `WeatherComponent` 中设置断点。一个在第 43 行,`ngOnInit` 的 `getLastAccessedCity`。另一个在第 231 行,`Search` 的 `updateLastAccessedCity`。
在 Visual Studio 中,在 `CitiesController.cs` 中设置断点。一个在 `Get`,另一个在 `Post`。
在 **Country** 字段中,选择 **Australia**,然后输入 **Geelong**。然后单击 **Go** 按钮,您可以看到 `Search` 函数中的 `updateLastAccessedCity` 被命中。
单击“**继续**”。
然后 `CitiesController` 中的 `Post` 方法被命中。
单击“**继续**”或按 **F5**。`Geelong` 已保存到数据库。
刷新 Chrome。`ngOnInit` 中的 `getLastAccessedCity` 被命中。
单击“**继续**”,`CitiesContoller` 中的 Http `Get` 方法被命中。
结论
构建一个优秀的 API 取决于优秀的架构。在本文中,我们构建了一个 .NET Core 2.2 Web API,并介绍了 .NET Core 的基础知识,例如 Entity Framework Core、依赖注入,以及 Angular 和 .NET Core 的完全集成。现在您知道从 ASP.Net Core 构建 Web API 是多么容易。
在 下一篇文章中,我们将开始研究单元测试。我将向您展示如何在 xunit 中为 .NET Core 使用 BDDfy。此外,我还会向您展示如何创建和调试 Angular 的单元测试。