Introduction
In Angular 7 with .NET Core 2.2 - Global Weather (Part 1), we talked about how to build an Angular 7 app with .NET Core 2.2 step by step. In this article, we’ll create .NET Core API to save the location user selected and populate the latest location when the user visits again.
API Controller
Compared with ASP.NET, ASP.NET Core gives the developer much better performance and is architected for cross-platform execution. With ASP.NET Core, your solutions will work as well on Linux as they do on Windows.
In Web API, a controller
is an object that handles HTTP requests. We'll add a controller
that can return and save the latest accessed city.
Add CitiesController
First, remove ValuesController
, which is created automatically with the project template. In Solution Explorer, right click ValuesController.cs, and delete it.
Then in Solution Explorer, right-click the Controllers folder. Select Add and then select Controller.
In the Add Scaffold dialog, select Web API Controller - Empty. Click Add.
In the Add Controller dialog, name the controller "CitiesController
". Click Add.
The scaffolding creates a file named CitiesController.cs in the Controllers folder.
Leave the controller for the time being, and come back later.
Add Database Persistence with EntiftyFrameworkCore
Entity Framework (EF) Core is a lightweight, extensible, and cross-platform version of the popular Entity Framework data access technology.
EF Core can serve as an object-relational mapper (O/RM), enabling .NET developers to work with a database using .NET objects, and eliminating the need for most of the data-access code they usually need to write.
In Solution Explorer, add new project.
Select “Class Library (.NET Core)" template and name the project “Weather.Persistence
”. Click “OK”. Weather.Persistence
project is created under GlobalWeather
solution.
Delete Class1.cs. Right click Weather.Persistence
project to select “Manage Nuget Packages”.
In Nuget Window, install dependant packages for Entity Framework Core. They are Microsoft.EntityFrameworkCore
, Microsoft.EntityFrameworkCore.Design
, Microsoft.EntityFrameworkCore.Relational
and Microsoft.EntityFrameworkCore.SqlServer
.
Also, we install some extra packages for dependency injection, application config and logging. They are Microsoft.Extensions.DependencyInjection
, Microsoft.Extensions.Options.ConfigurationExtensions
and Serilog.
Create Database Context
With EF Core, data access is performed using a model. A model is made up of entity classes and a derived context that represents a session with the database, allowing you to query and save data.
You can generate a model from an existing database, hand code a model to match your database, or use EF Migrations to create a database from your model.
Here we use Database First, generate a model from an existing database.
Create Weather
database in Microsoft SQL Server Management Studio. Then, run the below script:
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
Now, it’s ready to create Entity Framework data context and data models. The below is dbcontext scaffold
command, it will create dbContext
class and data model classes automatically.
dotnet ef dbcontext scaffold "Server=.\sqlexpress;Database=Weather;
Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -o Models -c "WeatherDbContext" -f
Before we run dbcontext scaffold
command, we need to think abut data model plural and single naming problem.
Normally, we create table with plural name like "Cities
". As a dataset, naming Cities
makes sense, but it doesn't make any sense if we name the model class as "Cities
". The expected model class name should be "City
". If we don't do anything, just run the scaffold command straight away. The data context and model classes generated are like this:
You can see, it generated Cities
model class. Then have a look at the WeatherDbContext
class.
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:
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);
});
}
}
}
The DbSet
is called Cities
as well. How ugly it is!
But actually, Entity Framework Core 2 supports Pluralization and Singularization.
There is a new IPluralizer interface
. It can be used to pluralize table names when EF is generating the database (dotnet ef database update) or entities when generating classes from it (Scaffold-DbContext
). The way to use it is somewhat tricky, as we need to have a class implementing IDesignTimeServices
, and this class will be discovered automatically by these tools.
There is a Nuget package Inflector to implement IPluralizer interface
.
For me, I add Pluaralizer.cs from Inflector to our persistence project.
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('_', '-');
}
}
Then in Powershell, to run the below command, go to GlobalWeather\Weather.Persistence folder, run the below command:
dotnet ef dbcontext scaffold "Server=.\sqlexpress;Database=Weather;
Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -o Models -c "WeatherDbContext" -f
After running this command, City.cs and WeatherDbContext.cs are generated under Models folder.
Please note the city
model is “City
” not “Cities
”. It's much better than before.
Adding Serilog to .NET Core App
Unlike other logging libraries, Serilog is built with powerful structured event data. Serilog has a good story for adding logging to ASP.NET Core apps with the Serilog.AspNetCore
library, as well as an extensive list of available sinks.
Installing the Library
You can install the Serilog NuGet packages into your app using the package manager. You'll also need to add at least one "sink" - this is where Serilog will write the log messages. For example, Serilog.Sinks.Console
writes messages to the console.
Right click GlobalWeather
project to select “Manage Nuget Packages”.
Configuring Serilog in Your Application
Once you've restored the packages, you can configure your app to use Serilog
. The recommended approach is to configure Serilog
's static Log.Logger
object first, before configuring your ASP.NET Core application.
First in appsettings.json, using the below Serilog
configuration to replace the default logging.
"Serilog": {
"MinimumLevel": "Debug",
"WriteTo": [
{
"Name": "File",
"Args": {
"path": "log\\log.txt",
"rollingInterval": "Day"
}
}
]
}
Then, make some changes in the default Startup.cs class.
Add the below line in Startup
method, which configured Log.Logger
.
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(Configuration)
.CreateLogger();
Add the below line in ConfigureServices
method to inject Log.Logger
.
services.AddSingleton(Log.Logger);
Adding Connection String for .NET Core App
Configuration in ASP.NET Core
In .NET Core, the configuration system is very flexible, and the connection string could be stored in appsettings.json, an environment variable, the user secret store, or another configuration source. Now we show the connection string stored in appsettings.json.
Open appsettings.json under GlobalWeather project folder, and add the below line:
"DbConnectionString": "Server=.\\sqlexpress;Database=Weather;Trusted_Connection=True;"
App configuration in ASP.NET Core is based on key-value pairs established by configuration providers. Configuration providers read configuration data into key-value pairs from a variety of configuration sources.
The options pattern is an extension of the configuration concepts. Options uses classes to represent groups of related settings.
An options
class must be non-abstract with a public
parameterless constructor.
Create DbContextSettings
class for Weather.Persistence
project.
public class DbContextSettings
{
public string DbConnectionString { get; set; }
}
Binding the Configuration to Your Classes
Setup the ConfigurationBuilder
to load your file. When you create a new ASP.NET Core application from the default templates, the ConfigurationBuilder
is already configured in Startup.cs to load settings from environment variables, appsettings.json.
In order to bind a settings
class to your configuration, you need to configure this in the ConfigureServices
method of Startup.cs.
Setup the ConfigurationBuilder
to load your file. When you create a new ASP.NET Core application from the default templates, the ConfigurationBuilder
is already configured in Startup.cs to load settings from environment variables, appsettings.json.
In order to bind a settings
class to your configuration, you need to configure this in the ConfigureServices
method of Startup.cs.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<DbContextSettings>(Configuration);
}
Because DBContextSettings
is defined in Weather.Persistence
project, you have to add Weather.Persistence
project reference to GlobalWeather
project.
Right click Dependencies of GlobalWeather
Project, and select “Add reference”. In Reference Manager window, select “Weather.Persistence” and click “OK”.
After adding Weather.Persistence.Config namespace
, the compile error is gone.
Also, because we read connection string from appsettings.json, we can remove the hardcoded connection string from OnConfiguring
of WeatherDbContext.cs.
Delete the below line:
optionsBuilder.UseSqlServer("Server=.\\sqlexpress;Database=Weather;Trusted_Connection=True;");
Adding DbContextFactory Class
Now, we can use connection string in application configuration file to create DBContextFactory
.
DbContextFactory
class is a factory
class to create Db context, here is WeatherDbContext
.
Right click Weather.Persistence
project, add Repositories folder. Then add IDbContextFactory interface
and DBContextFactory
class.
IDbContextFactory Interface
public interface IDbContextFactory
{
WeatherDbContext DbContext { get; }
}
DbContextFactory Class
public class DbContextFactory : IDbContextFactory, IDisposable
{
public DbContextFactory(IOptions<DbContextSettings> settings)
{
var options = new DbContextOptionsBuilder<WeatherDbContext>().UseSqlServer
(settings.Value.DbConnectionString).Options;
DbContext = new WeatherDbContext(options);
}
~DbContextFactory()
{
Dispose();
}
public WeatherDbContext DbContext { get; private set; }
public void Dispose()
{
DbContext?.Dispose();
}
}
Adding Generic Repository Class
The Repository Pattern is one of the most popular patterns to create an enterprise level application. It restricts us to work directly with the data in the application and creates new layers for database operations, business logic, and the application’s UI.
Using the Repository Pattern has many advantages:
- Your business logic can be unit tested without data access logic.
- The database access code can be reused.
- Your database access code is centrally managed so it is easy to implement any database access policies, like caching.
- It’s easy to implement domain logic.
- Your domain entities or business entities are strongly typed with annotations; and more.
In generic speaking, one repository
class for each data set class. If we use generic repository
, it will reuse all common codes, and reduce most duplicate code.
We add IRepository interface
and Repository class
in Repositories folder of Weather.Persistence
project.
IRepository Interface
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 Class
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;
public async Task<TEntity> GetEntity(object id)
{
var entity = await DbContext.FindAsync<TEntity>(id);
return entity;
}
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;
}
}
public async Task<TEntity> UpdateEntity(TEntity entity)
{
DbContext.Update<TEntity>(entity);
await DbContext.SaveChangesAsync();
return entity;
}
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 and Await
We use async
and await
pattern in Entity Framework Core query and save. You can avoid performance bottlenecks and enhance the overall responsiveness of your application by using asynchronous programming.
Asynchrony is essential for activities that are potentially blocking, such as web access. Access to a web resource sometimes is slow or delayed. If such an activity is blocked in a synchronous process, the entire application must wait. In an asynchronous process, the application can continue with other work that doesn't depend on the web resource until the potentially blocking task finishes.
Asynchrony proves especially valuable for applications that access the UI thread because all UI-related activity usually shares one thread. If any process is blocked in a synchronous application, all are blocked. Your application stops responding, and you might conclude that it has failed when instead, it's just waiting.
Adding Specific CityRepository Class
Generic Repository
class only has the common methods and properties for entity dataset
. Sometimes, some dataset
needs some more specific methods and properties. For these entities, we need to create the repository
subclass which derives from the generic repository
class.
The task we need do is getting and saving the last accessed city. So we need to add InsertOrUpdateCityAsync
and GetLastAccessedCityAsync
methods to CityRepository
class
.
We add ICityRepository interface
and CityRepository class
in Repositories folder of Weather.Persistence
project.
ICityRepository Interface
public interface ICityRepository : IRepository<City>
{
Task<City> GetLastAccessedCityAsync();
Task InsertOrUpdateCityAsync(City city);
}
CityRepository Class
public class CityRepository : Repository<City>, ICityRepository
{
public CityRepository(IDbContextFactory dbContextFactory, ILogger logger) :
base(dbContextFactory, logger)
{
}
public async Task<City> GetLastAccessedCityAsync()
{
var city = await DbContext.Cities.OrderByDescending(x=>x.AccessedDate).FirstOrDefaultAsync();
return city;
}
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);
}
}
}
Dependency Injection With .NET Core
To solve the problem of hardcoding a reference to the service implementation, Dependency Injection provides a level of indirection such that rather than instantiating the service directly with the new operator, the client (or application) will instead ask a service collection or “factory” for the instance. Furthermore, rather than asking the service collection for a specific type (thus creating a tightly coupled reference), you ask for an interface
with the expectation that the service provider will implement the interface
.
The result is that while the client will directly reference the abstract
assembly, defining the service interface
, no references to the direct implementation will be needed.
Dependency Injection registers an association between the type requested by the client (generally an interface
) and the type that will be returned. Furthermore, Dependency Injection generally determines the lifetime of the type returned, specifically, whether there will be a single instance shared between all requests for the type, a new instance for every request, or something in between.
One especially common need for Dependency Injection is in unit tests. All that’s needed is for the unit test to “configure” the DI framework to return a mock service.
Providing an instance of the “service” rather than having the client directly instantiating it is the fundamental principle of Dependency Injection.
To leverage the .NET Core DI framework, all you need is a reference to the Microsoft.Extensions.DependencyInjection.Abstractions
NuGet package. This provides access to the IServiceCollection interface
, which exposes a System.IServiceProvider
from which you can call GetService<TService>
. The type parameter, TService
, identifies the type of the service to retrieve (generally an interface
), thus the application code obtains an instance.
Inject DbContextFactory and CityRepository
Add RespositoryInjectionModule static class
in Repositories folder of Weather.Persistence
project. This static class
adds an extension method for IServiceCollection
.
public static class RepositoryInjectionModule
{
public static IServiceCollection InjectPersistence(this IServiceCollection services)
{
services.AddScoped<IDbContextFactory, DbContextFactory>();
services.AddTransient<ICityRepository, CityRepository>();
return services;
}
}
Then add services.InjectPersistence()
to ConfigureService
of Startup.cs.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<DbContextSettings>(Configuration);
services.AddSingleton(Log.Logger);
services.InjectPersistence();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "WeatherClient/dist";
});
}
Difference between services.AddTransient, service.AddScoped and service.AddSingleton
Choose an appropriate lifetime for each registered service. ASP.NET Core services can be configured with the following lifetimes:
Transient objects are always different; a new instance is provided to every controller and every service.
Scoped objects are the same within a request, but different across different requests.
Singleton objects are the same for every object and every request.
Adding CityService Class
I don’t want to call repository directly from API controller, the best practice is adding a service. Then from service to call repository.
Right click GlobalWeather
project to add a new folder, “Services”. Add ICityService interface
and CityService class
to this folder.
ICityService Interface
public interface ICityService
{
Task<City> GetLastAccessedCityAsync();
Task UpdateLastAccessedCityAsync(City city);
}
CityService Class
public class CityService : ICityService
{
private readonly ICityRepository _repository;
private readonly ILogger _logger;
public CityService(ICityRepository repository, ILogger logger)
{
_repository = repository;
_logger = logger;
}
public async Task<City> GetLastAccessedCityAsync()
{
var city = await _repository.GetLastAccessedCityAsync();
return city;
}
public async Task UpdateLastAccessedCityAsync(City city)
{
city.AccessedDate = DateTimeOffset.UtcNow;
await _repository.InsertOrUpdateCityAsync(city);
}
}
Dependency Inject CityService
Add ServiceInjectionModule static class
to Services folder. Same as before, this static class
adds another extension method for IServiceCollection
.
public static class ServiceInjectionModule
{
public static IServiceCollection InjectServices(this IServiceCollection services)
{
services.AddTransient<ICityService, CityService>();
return services;
}
}
Then add services.InjectServices ()
to ConfigureService
of Startup.cs.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<DbContextSettings>(Configuration);
services.AddSingleton(Log.Logger);
services.InjectPersistence();
services.InjectServices();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "WeatherClient/dist";
});
}
Calling CityService in CitiesController
Now it’s the time to update CitiesController
.
First, inject CityService
and Logger
instance in constructor.
public CitiesController(ICityService service, ILogger logger)
{
_service = service;
_logger = logger;
}
Add HttpGet
to get the last accessed city
.
[HttpGet]
public async Task<ActionResult<City>> Get()
{
var city = await _service.GetLastAccessedCityAsync();
return city;
}
Add HttpPost
to save the city
.
[HttpPost]
public async Task Post([FromBody] City city)
{
await _service.UpdateLastAccessedCityAsync(city);
}
Call API From Angular Front End
Now, we need get go back to Angular front end to call City
API to save and get the last accessed city
.
First, we need to create a model
class to map the json.
Create a file called city-meta-data
under src/app/shared/models/ folder. Define a CityMetaData class
and export it. The file should look like this:
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;
}
}
Open app.constants.ts under src/app/app.constants.ts. Add a new constant, which is the City API url. You should know, this url is a relative url. Relative url ensures it works on any environment.
static cityAPIUrl = '/api/cities';
Create a service
called city
in the src/app/shared/services/ folder.
ng generate service city
The command generates skeleton CityService class
in src/app/city.service.ts.
Then add getLastAccessedCity
and updateLastAccessedCity
method in 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
Open weather.component.ts under src/app/weather/weather.component.ts.
Import CityServer and Inject in Constructor
constructor(
private fb: FormBuilder,
private locationService: LocationService,
private currentConditionService: CurrentConditionsService,
private cityService: CityService) {
}
Save the City Which User Selected
Add UpdateLastAccessedCity
method.
async updateLastAccessedCity(city: City) {
const promise = new Promise((resolve, reject) => {
this.cityService.updateLastAccessedCity(city)
.toPromise()
.then(
_ => {
resolve();
},
err => {
console.error(err);
resolve();
}
);
});
await promise;
}
Call it after get 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();
}
Get the Last Accessed City from ngOnInit
Add getLastAccessedCity
method.
async getLastAccessedCity() {
const promise = new Promise((resolve, reject) => {
this.cityService.getLastAccessedCity()
.toPromise()
.then(
res => {
const data = res as City;
if (data) {
this.city = data;
}
resolve();
},
err => {
console.error(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
}
});
}
}
After getting the last accessed city
, patch the reactive form fields.
Call getLastAccessedCity
from ngOnInit
.
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.";
}
}
Debugging from Front End to Back End
Ok. Now I show the whole workflow from end to end.
In Chrome, we put the break points at WeatherComponent
. One is at line 43, getLastAccessedCity
of ngOnInit
. The other is line 231, updateLastAccessedCity
of Search
.
In Visual Studio, put the break points at CitiesController.cs. One is at Get
, the other is at Post
.
In Country field, select Australia, and input Geelong. Then click Go button, you can see updateLastAccessedCity
in Search
function is hit.
Click “Continue”.
Then Post
method in CitiesController
gets hit.
Click “Continue” or press F5. Geelong
is saved to the database.
Refresh Chrome. getLastAccessedCity
in ngOnInit
is hit.
Click “Continue”, Http Get
method in CitiesContoller
gets hit.
Conclusion
Building a great API depends on great architecture. In this article, we built a .NET Core 2.2 Web API, and introduced .NET Core fundamentals, like Entity Framework Core, Dependency Injection, and the full integration of Angular and .NET Core. Now you know how easy to build a Web API from ASP.Net Core.
In the next article, we’ll start to look at unit tests. I'll show you how to use BDDfy in xunit for .NET Core. Also, I'll show you how to create and debug unit test for Angular.