目录
使用EntiftyFrameworkCore添加数据库持久性
注入DbContextFactory和CityRepository
services.AddTransient,service.AddScoped和service.AddSingleton之间的区别
在CitiesController中调用CityService
介绍
在Angular 7 和 .Net Core 2.2——全球天气(第1部分)中,我们讨论了如何使用.NET Core 2.2逐步构建Angular 7应用程序。在本文中,我们将创建.NET Core API以保存用户选择的位置,并在用户再次访问时填充最新位置。
API控制器
与ASP.NET相比,ASP.NET Core为开发人员提供了更好的性能,并且是为跨平台执行而构建的。使用ASP.NET Core,您的解决方案在Linux上也可以像在Windows上一样工作。
在Web API中,controller是处理HTTP请求的对象。我们将添加一个controller,其可以返回并保存最新访问城市的内容。
添加CitiesController
首先,删除ValuesController,这是使用项目模板自动创建的。在解决方案资源管理器中,右键单击ValuesController.cs,然后将其删除。
然后在解决方案资源管理器中,右键单击Controllers文件夹。选择Add,然后选择Controller。
在Add Scaffold对话框中,选择Web API Controller - Empty。单击添加。
在“添加控制器”对话框中,将控制器命名为“ CitiesController”。单击添加。
脚手架在Controllers文件夹中创建名为CitiesController.cs的文件。
暂时离开控制器,稍后再回来。
使用EntiftyFrameworkCore添加数据库持久性
实体框架(EF)Core是流行的实体框架数据访问技术的轻量级、可扩展和跨平台版本。
EF Core可以作为对象关系映射器(O / RM),使.NET开发人员能够使用.NET对象使用数据库,并且无需他们通常需要编写的大多数数据访问代码。
在解决方案管理器中,添加新项目。
选择“类库(.NET Core)”模板并将项目命名为“ Weather.Persistence”。单击“确定”。Weather.Persistence项目在GlobalWeather解决方案下创建。
删除Class1.cs。右键单击Weather.Persistence项目以选择“管理Nuget包”。
在Nuget Window中,为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迁移从模型创建数据库。
这里我们使用Database First,从现有数据库生成模型。
在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”。如果我们不做任何事情,只需立即运行scaffold命令。生成的数据上下文和模型类如下所示:
你可以看到,它生成了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 interface。当EF生成数据库(dotnet ef数据库更新)或实体从中生成类(Scaffold-DbContext)时,它可用于复数表名。使用它的方法有点棘手,因为我们需要一个类实现IDesignTimeServices,这些类将由这些工具自动发现。
有一个Nuget包Inflector实现IPluralizer interface。
对我来说,我将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
运行此命令后,将在Models文件夹下生成City.cs和WeatherDbContext.cs。
请注意city的模板是“City”不是“Cities”。它比以前好多了。
将Serilog添加到.NET Core应用程序
与其他日志库不同,Serilog使用强大的结构化事件数据构建。Serilog有一个很好的故事,可以使用Serilog.AspNetCore库添加日志记录到ASP.NET核心应用程序,以及大量可用的接收器列表。
安装库
您可以使用包管理器将Serilog NuGet包安装到您的应用程序中。您还需要添加至少一个“接收器”——这是Serilog将写入日志消息的位置。例如,Serilog.Sinks.Console将消息写入控制台。
右键单击GlobalWeather项目以选择“管理Nuget包 ”。
在您的应用程序中配置Serilog
还原软件包后,您可以配置要使用Serilog的应用程序。推荐的方法是首先配置Serilog的static Log.Logger对象,请在配置ASP.NET的核心应用程序之前。
首先在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 App添加连接字符串
ASP.NET Core中的配置
在.NET Core中,配置系统非常灵活,连接字符串可以存储在appsettings.json中,环境变量,用户密码存储或其他配置源中。现在我们显示存储在appsettings.json中的连接字符串。
在GlobalWeather项目文件夹下打开appsettings.json,并添加以下行:
"DbConnectionString": "Server=.\\sqlexpress;Database=Weather;Trusted_Connection=True;"
ASP.NET Core中的应用程序配置基于配置提供程序建立的键值对。配置提供程序将配置数据从各种配置源读取为键值对。
选项模式是配置概念的扩展。选项使用类来表示相关设置组。
使用无public参数构造函数的options类必须是非抽象的。
为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方法中配置它。
public void ConfigureServices(IServiceCollection services)
{
services.Configure<DbContextSettings>(Configuration);
}
因为DBContextSettings是在Weather.Persistence项目中定义的,所以必须向Weather.Persistence项目添加GlobalWeather项目引用。
右键单击GlobalWeather项目的依赖,然后选择“Add reference”。在“参考管理器”窗口中,选择“ Weather.Persistence”,然后单击“确定 ”。
添加Weather.Persistence.Config namespace后,编译错误消失了。
此外,因为我们从appsettings.json中读取连接字符串,我们可以从WeatherDbContext.cs中的OnConfiguring里删除硬编码的连接字符串。
删除以下行:
optionsBuilder.UseSqlServer("Server=.\\sqlexpress;Database=Weather;Trusted_Connection=True;");
添加DbContextFactory类
现在,我们可以在应用程序配置文件中使用连接字符串来创建DBContextFactory。
DbContextFactory类是一个factory创建Db上下文的类,这里是WeatherDbContext。
右键单击Weather.Persistence项目,添加Repositories文件夹。然后添加IDbContextFactory interface和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();
}
}
添加泛型存储库类
存储库模式是创建企业级应用程序的最流行模式之一。它限制我们直接使用应用程序中的数据,并为数据库操作、业务逻辑和应用程序的UI创建新层。
使用存储库模式有许多优点:
- 您的业务逻辑可以在没有数据访问逻辑的情况下进行单元测试
- 可以重用数据库访问代码。
- 您的数据库访问代码是集中管理的,因此很容易实现任何数据库访问策略,如缓存。
- 实现域逻辑很容易。
- 您的域实体或业务实体使用注释进行强类型化; 还有更多。
一般来说,每个数据集类有一个repository类。如果我们使用泛型repository,它将重用所有公共代码,并减少大多数重复代码。
我们在Weather.Persistence的项目的Repositories 文件夹中增加IRepository interface和Repository class。
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子类。
我们需要做的任务是获取并保存最后访问的城市。所以我们需要向CityRepository class中添加InsertOrUpdateCityAsync和GetLastAccessedCityAsync方法。
我们在Weather.Persistence项目的Repositories 文件夹中增加ICityRepository interface和CityRepository class。
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框架以返回模拟服务。
提供“服务”的实例而不是让客户端直接实例化是依赖注入的基本原则。
要利用.NET Core DI框架,您只需要引用Microsoft.Extensions.DependencyInjection.AbstractionsNuGet包。这提供访问IServiceCollection interface,其暴露了System.IServiceProvider,在其中你可以调用GetService<TService>。type参数,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 interface和CityService class到这个文件夹。
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前端来调用CityAPI来保存并获取最后一次访问的city。
首先,我们需要创建一个model类来映射json。
在src/app/shared/models/ 文件夹下创建一个名为city-meta-data的文件。定义CityMetaData class并导出它。该文件应如下所示:
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确保它适用于任何环境。
static cityAPIUrl = '/api/cities';
在src/app/shared/services/文件夹中创建一个service调用city。
ng generate service city
在src/app/city.service.ts中的命令生成骨架CityService class。
然后在CityService class中添加getLastAccessedCity和updateLastAccessedCity方法。
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', []))
);
}
}
天气组件
打开src/app/weather/weather.component.ts下的weather.component.ts。
在构造函数中导入CityServer和Inject
constructor(
private fb: FormBuilder,
private locationService: LocationService,
private currentConditionService: CurrentConditionsService,
private cityService: CityService) {
}
保存用户选择的城市
添加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获取最后访问的城市
添加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有多容易。
下一篇:Angular 7和.NET Core 2.2——全球天气(第3部分)
原文地址:https://www.codeproject.com/Articles/1276248/Angular-7-with-NET-Core-2-2-Global-Weather-Part-2