Angular 7和 .NET Core 2.2——全球天气(第2部分)

目录

介绍

API控制器

添加CitiesController

使用EntiftyFrameworkCore添加数据库持久性

创建数据库上下文

将Serilog添加到.NET Core应用程序

安装库

在您的应用程序中配置Serilog

为.NET Core App添加连接字符串

ASP.NET Core中的配置

将配置绑定到您的类

添加DbContextFactory类

IDbContextFactory接口

DbContextFactory类

添加泛型存储库类

IRepository接口

Repository 类

 Async和Await 

添加特定的CityRepository类

ICityRepository接口

CityRepository类

使用.NET Core进行依赖注入

注入DbContextFactory和CityRepository

services.AddTransient,service.AddScoped和service.AddSingleton之间的区别

添加CityService类

ICityService接口

CityService类

依赖注入CityService

在CitiesController中调用CityService

从Angular前端调用API

天气组件

在构造函数中导入CityServer和Inject 

保存用户选择的城市

从ngOnInit获取最后访问的城市

从前端到后端的调试

结论


https://www.codeproject.com/KB/api/1276248/weather-app-icon.png

介绍

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

https://www.codeproject.com/KB/api/1276248/1-1.png

Add Scaffold对话框中,选择Web API Controller - Empty。单击添加

https://www.codeproject.com/KB/api/1276248/1-2.png

添加控制器对话框中,将控制器命名为“ CitiesController。单击添加

https://www.codeproject.com/KB/api/1276248/1-3.png

脚手架在Controllers文件夹中创建名为CitiesController.cs的文件。

https://www.codeproject.com/KB/api/1276248/1-4.png

暂时离开控制器,稍后再回来。

使用EntiftyFrameworkCore添加数据库持久性

实体框架(EFCor​​e是流行的实体框架数据访问技术的轻量级、可扩展和跨平台版本。

EF Core可以作为对象关系映射器(O / RM),使.NET开发人员能够使用.NET对象使用数据库,并且无需他们通常需要编写的大多数数据访问代码。

在解决方案管理器中,添加新项目。

https://www.codeproject.com/KB/api/1276248/2-1.png

选择类库(.NET Core模板并将项目命名为“ Weather.Persistence。单击确定Weather.Persistence项目在GlobalWeather解决方案下创建。

https://www.codeproject.com/KB/api/1276248/2-2.png

删除Class1.cs。右键单击Weather.Persistence项目以选择管理Nuget

https://www.codeproject.com/KB/api/1276248/2-3.png

Nuget Window中,为Entity Framework Core安装依赖包。它们是Microsoft.EntityFrameworkCoreMicrosoft.EntityFrameworkCore.DesignMicrosoft.EntityFrameworkCore.RelationalMicrosoft.EntityFrameworkCore.SqlServer

此外,我们还为依赖注入,应用程序配置和日志记录安装了一些额外的包。它们是Microsoft.Extensions.DependencyInjectionMicrosoft.Extensions.Options.ConfigurationExtensionsSerilog

https://www.codeproject.com/KB/api/1276248/2-4.png

创建数据库上下文

使用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命令。生成的数据上下文和模型类如下所示:

https://www.codeproject.com/KB/api/1276248/2-7.png

你可以看到,它生成了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,这些类将由这些工具自动发现。

有一个NugetInflector实现IPluralizer interface

对我来说,我将InflectorPluaralizer.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.csWeatherDbContext.cs

https://www.codeproject.com/KB/api/1276248/2-5.png

请注意city的模板是City不是Cities。它比以前好多了。

Serilog添加到.NET Core应用程序

与其他日志库不同,Serilog使用强大的结构化事件数据构建。Serilog有一个很好的故事,可以使用Serilog.AspNetCore库添加日志记录到ASP.NET核心应用程序,以及大量可用的接收器列表。

安装库

您可以使用包管理器将Serilog NuGet包安装到您的应用程序中。您还需要添加至少一个接收器——这是Serilog将写入日志消息的位置。例如,Serilog.Sinks.Console将消息写入控制台。

右键单击GlobalWeather项目以选择管理Nuget ”

https://www.codeproject.com/KB/api/1276248/3-1.png

在您的应用程序中配置Serilog

还原软件包后,您可以配置要使用Serilog的应用程序。推荐的方法是首先配置Serilogstatic 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.csConfigureServices方法中配置它。

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<DbContextSettings>(Configuration);
}

因为DBContextSettings是在Weather.Persistence项目中定义的,所以必须向Weather.Persistence项目添加GlobalWeather项目引用。

右键单击GlobalWeather项目的依赖,然后选择Add reference。在参考管理器窗口中,选择 Weather.Persistence,然后单击确定 ”

https://www.codeproject.com/KB/api/1276248/4-1.png

添加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 interfaceDBContextFactory类。

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 interfaceRepository 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查询中使用asyncawait模式查询和保存数据。通过使用异步编程,您可以避免性能瓶颈并提高应用程序的整体响应能力。

异步对于可能阻塞的活动(例如Web访问)至关重要。访问Web资源有时会很慢或延迟。如果在同步过程中阻止此类活动,则整个应用程序必须等待。在异步过程中,在潜在阻塞任务完成之前,应用程序可以继续执行不依赖于Web资源的其他工作。

异步对于访问UI线程的应用程序尤其有用,因为所有与UI相关的活动通常共享一个线程。如果在同步应用程序中阻止了任何进程,则会阻止所有进程。您的应用程序停止响应,您可能会认为它失败了,而不是等待。

添加特定的CityRepository

泛型Repository类仅具有实体dataset的通用方法和属性。有时,某些dataset需要一些更具体的方法和属性。对于这些实体,我们需要创建派生自泛型repository类的repository子类。

我们需要做的任务是获取并保存最后访问的城市。所以我们需要向CityRepository class中添加InsertOrUpdateCityAsyncGetLastAccessedCityAsync方法。

我们在Weather.Persistence项目的Repositories 文件夹中增加ICityRepository interfaceCityRepository 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程序集,定义服务inter­face,但不需要引用直接实现。

依赖注入注册客户端请求的类型(通常为interface)与将返回的类型之间的关联。此外,依赖注入通常确定返回类型的生存期,具体而言,是否将在所有类型请求之间共享单个实例,每个请求的新实例或其间的内容。

对依赖注入的一个特别常见的需求是在单元测试中。所需要的只是单元测试配置”DI框架以返回模拟服务。

提供服务的实例而不是让客户端直接实例化是依赖注入的基本原则。

要利用.NET Core DI框架,您只需要引用Microsoft.Extensions.DependencyInjection.AbstractionsNuGet包。这提供访问IServiceCollection interface,其暴露了System.IService­Provider,在其中你可以调用GetService<TService>type参数,TService,标识要检索的服务的类型(通常为interface),因此应用程序代码获取实例。

注入DbContextFactoryCityRepository

Weather.Persistence项目的Repositories 文件夹中添加RespositoryInjectionModule static class。这个 static classIServiceCollection添加了扩展方法。

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.csConfigureService中。

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.AddTransientservice.AddScopedservice.AddSingleton之间的区别

为每个注册的服务选择适当的生命周期。可以使用以下生命周期配置ASP.NET Core服务:

瞬态对象总是不同的为每个控制器和每个服务提供一个新实例。

范围内的对象在请求中是相同的,但在不同的请求中是不同的。

单例对象对于每个对象和每个请求都是相同的。

添加CityService

我不想直接从API控制器调用存储库,最佳做法是添加服务。然后从服务调用存储库。

右键单击GlobalWeather项目以添加新文件夹Services。添加ICityService interfaceCityService 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 classServices 文件夹。与之前相同,这个static classIServiceCollection添加了另一种扩展方法。

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.csConfigureService中。

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了。

首先,在构造函数中注入CityServiceLogger实例。

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中添加getLastAccessedCityupdateLastAccessedCity方法。

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.tsweather.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行,ngOnInitgetLastAccessedCity。另一个是行231 SearchupdateLastAccessedCity

https://www.codeproject.com/KB/api/1276248/6-1.png

Visual Studio中,将断点放在CitiesController.cs。一个在Get,另一个在Post

https://www.codeproject.com/KB/api/1276248/6-2.png

Country字段中,选择Australia,然后输入Geelong。然后点击转到(Go)按钮,你可以看到在Search函数中的updateLastAccessedCity被中断。

https://www.codeproject.com/KB/api/1276248/6-3.png

点击继续

然后在CitiesController中的Post方法被中断。

https://www.codeproject.com/KB/api/1276248/6-4.png

单击继续或按F5Geelong保存到数据库中。

https://www.codeproject.com/KB/api/1276248/6-5.png

刷新Chrome。在ngOnInit中的getLastAccessedCity被中断。

https://www.codeproject.com/KB/api/1276248/6-6.png

点击继续 ”,在CitiesContoller中的Http Get方法被中断。

https://www.codeproject.com/KB/api/1276248/6-7.png

结论

构建一个出色的API依赖于伟大的架构。在本文中,我们构建了一个.NET Core 2.2 Web API,并介绍了.NET Core基础知识,如Entity Framework Core,依赖注入以及Angular.NET Core的完全集成。现在您知道从ASP.Net Core构建Web API有多容易。

 

原文地址:https://www.codeproject.com/Articles/1276248/Angular-7-with-NET-Core-2-2-Global-Weather-Part-2

猜你喜欢

转载自blog.csdn.net/mzl87/article/details/88081088