65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.70/5 (26投票s)

2019年2月5日

CPOL

13分钟阅读

viewsIcon

46089

downloadIcon

922

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.IService­Provider`,您可以从中调用 `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 的单元测试。

© . All rights reserved.