preface

I haven’t touched C# much in maybe three years. Currently working full-time on the front end, I’ve had some time recently to check out Asp.net Core. The following will record a development process using Asp.Net Core WebApi+Dapper+Mysql+Redis+Docker these days.

The project structure

The final structure of the project is as follows: CodeUin.Dapper data access layer and CodeUin.WebApi application layer. CodeUin.Helpers I will store some generic help classes for the project. If the help classes are only related to the current level, they will be stored directly in the Helpers folder of their level.

Installation environment

MySQL

Docker run -itd --name container name -p3306:3306-e MYSQL_ROOT_PASSWORD= your password mysqlCopy the code

If you are using the client tool to connect to MySQL, the error message 1251 is displayed because the client does not support the new encryption method.

#View the currently running container
docker ps 
#Into the containerDocker exec -it container name bash#Access to the MySQL
mysql -u root -p
#Viewing Encryption Rules
select host,user,plugin,authentication_string from mysql.user;
#Authorize remote connections
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION;
#Change password encryption rulesALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'your password ';#Refresh the permissions
flush privileges;
Copy the code

Finally, I used the MySQL client tool for connection testing, which I used was Navicat Premium.

Redis

#Download mirror
docker pull redis
#run
docker run -itd -p 6379:6379 redis
Copy the code

For connection testing, I used the Redis client tool, Another Redis DeskTop Manager.

The.net environment

I use CentOS 8 and NET SDK version 5.0 for the server. The following will record how I install it in CentOS 8. .net SDK and. NET runtime.

#Installing the SDKSudo DNF install dotnet SDK - 5.0#Install runtimeSudo DNF install aspnetcore - runtime - 5.0Copy the code

Check whether the installation is successful. Run the dotnet –info command to view the installation information

Create a project

The following will achieve a user login registration, and access to user information small functions.

Data Service layer

The design of this floor refers to the architecture of Jade Dragon Snow Mountain. I also like this structure, which is simple and clear.

First, create a new project named codeuin.dapper that only provides the interface to serve the business layer.

  • Entities
    • Store entity class
  • IRepository
    • Storage port
  • Repository
    • Store the warehouse interface implementation classes
  • BaseModel
    • The base class of the entity class that holds generic fields
  • DataBaseConfig
    • Data access configuration classes
  • IRepositoryBase
    • Storage of the most basic storage interface increase, delete, change, check, etc
  • RepositoryBase
    • The concrete realization of basic warehouse interface

Create the BaseModel base class

This class is stored in the root directory of the project and its main function is to isolate the fields that are present in the database entity classes.

using System;

namespace CodeUin.Dapper
{
    /// <summary>
    ///Base entity Class
    /// </summary>
    public class BaseModel
    {
        /// <summary>
        ///The primary key Id
        /// </summary>
        public int Id { get; set; }

        /// <summary>
        ///Creation time
        /// </summary>
        public DateTime CreateTime { get; set; }}}Copy the code

Create DataBaseConfig

This class is stored in the root directory of the project. I am using MySQL here, and the following dependency packages need to be installed. If you use other databases, you can install the corresponding dependency packages by yourself.

The code of this class is as follows:

using MySql.Data.MySqlClient;
using System.Data;

namespace CodeUin.Dapper
{
    public class DataBaseConfig
    {
        private static string MySqlConnectionString = @"Data Source= database address; Initial Catalog=codeuin; Charset=utf8mb4; User ID=root; Password= database Password;;
        
        public static IDbConnection GetMySqlConnection(string sqlConnectionString = null)
        {
            if (string.IsNullOrWhiteSpace(sqlConnectionString))
            {
                sqlConnectionString = MySqlConnectionString;
            }
            IDbConnection conn = new MySqlConnection(sqlConnectionString);
            conn.Open();
            returnconn; }}}Copy the code

Create IRepositoryBase class

This class is stored in the root directory of the project and holds commonly used warehouse interfaces.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace CodeUin.Dapper
{
    public interface IRepositoryBase<T>
    {
        Task<int> Insert(T entity, string insertSql);

        Task Update(T entity, string updateSql);

        Task Delete(int Id, string deleteSql);

        Task<List<T>> Select(string selectSql);

        Task<T> Detail(int Id, string detailSql); }}Copy the code

Create RepositoryBase class

This class is stored at the root of the project and is a concrete implementation of the IRepositoryBase class.

using Dapper;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Threading.Tasks;

namespace CodeUin.Dapper
{
    public class RepositoryBase<T> : IRepositoryBase<T>
    {
        public async Task Delete(int Id, string deleteSql)
        {
            using (IDbConnection conn = DataBaseConfig.GetMySqlConnection())
            {
                await conn.ExecuteAsync(deleteSql, new{ Id }); }}public async Task<T> Detail(int Id, string detailSql)
        {
            using (IDbConnection conn = DataBaseConfig.GetMySqlConnection())
            {
                return await conn.QueryFirstOrDefaultAsync<T>(detailSql, new{ Id }); }}public async Task<List<T>> ExecQuerySP(string SPName)
        {
            using (IDbConnection conn = DataBaseConfig.GetMySqlConnection())
            {
                return await Task.Run(() => conn.Query<T>(SPName, null.null.true.null, CommandType.StoredProcedure).ToList()); }}public async Task<int> Insert(T entity, string insertSql)
        {
            using (IDbConnection conn = DataBaseConfig.GetMySqlConnection())
            {
                return awaitconn.ExecuteAsync(insertSql, entity); }}public async Task<List<T>> Select(string selectSql)
        {
            using (IDbConnection conn = DataBaseConfig.GetMySqlConnection())
            {
                return awaitTask.Run(() => conn.Query<T>(selectSql).ToList()); }}public async Task Update(T entity, string updateSql)
        {
            using (IDbConnection conn = DataBaseConfig.GetMySqlConnection())
            {
                awaitconn.ExecuteAsync(updateSql, entity); }}}}Copy the code

Ok, the base class is almost defined. Let’s create a New Users class and define several commonly used interfaces.

Create the Users entity class

This class is stored in the Entities folder and inherits from BaseModel.

namespace CodeUin.Dapper.Entities
{
    /// <summary>
    ///The users table
    /// </summary>
    public class Users : BaseModel
    {
        /// <summary>
        ///The user name
        /// </summary>
        public string UserName { get; set; }

        /// <summary>
        ///password
        /// </summary>
        public string Password { get; set; }

        /// <summary>
        ///
        /// </summary>
        public string Salt { get; set; }

        /// <summary>
        ///email
        /// </summary>
        public string Email { get; set; }

        /// <summary>
        ///Mobile phone no.
        /// </summary>
        public string Mobile { get; set; }

        /// <summary>
        ///gender
        /// </summary>
        public int Gender { get; set; }

        /// <summary>
        ///age
        /// </summary>
        public int Age { get; set; }

        /// <summary>
        ///Head portrait
        /// </summary>
        public string Avatar { get; set; }

        /// <summary>
        ///Whether or not to delete
        /// </summary>
        public int IsDelete { get; set; }}}Copy the code

Create IUserRepository class

This class is stored in the IRepository folder, inherits IRepositoryBase, and defines additional interfaces.

using CodeUin.Dapper.Entities;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace CodeUin.Dapper.IRepository
{
    public interface IUserRepository : IRepositoryBase<Users>
    {
        Task<List<Users>> GetUsers();

        Task<int> AddUser(Users entity);

        Task DeleteUser(int d);

        Task<Users> GetUserDetail(int id);

        Task<Users> GetUserDetailByEmail(string email); }}Copy the code

Create UserRepository class

This class is stored in the Repository folder, inheriting RepositoryBase, IUserRepository, which is an implementation of the IUserRepository class.

using CodeUin.Dapper.Entities;
using CodeUin.Dapper.IRepository;
using Dapper;
using System.Collections.Generic;
using System.Data;
using System.Threading.Tasks;

namespace CodeUin.Dapper.Repository
{
    public class UserRepository : RepositoryBase<Users>, IUserRepository
    {
        public async Task DeleteUser(int id)
        {
            string deleteSql = "DELETE FROM [dbo].[Users] WHERE Id=@Id";
            await Delete(id, deleteSql);
        }


        public async Task<Users> GetUserDetail(int id)
        {
            string detailSql = @"SELECT Id, Email, UserName, Mobile, Password, Age, Gender, CreateTime,Salt, IsDelete FROM Users WHERE Id=@Id";
            return await Detail(id, detailSql);
        }

        public async Task<Users> GetUserDetailByEmail(string email)
        {
            string detailSql = @"SELECT Id, Email, UserName, Mobile, Password, Age, Gender, CreateTime, Salt, IsDelete FROM Users WHERE Email=@email";

            using (IDbConnection conn = DataBaseConfig.GetMySqlConnection())
            {
                return await conn.QueryFirstOrDefaultAsync<Users>(detailSql, new{ email }); }}public async Task<List<Users>> GetUsers()
        {
            string selectSql = @"SELECT * FROM Users";
            return await Select(selectSql);
        }

        public async Task<int> AddUser(Users entity)
        {
            string insertSql = @"INSERT INTO Users (UserName, Gender, Avatar, Mobile, CreateTime, Password, Salt, IsDelete, Email) VALUES (@UserName, @Gender, @Avatar, @Mobile, now(),@Password, @Salt, @IsDelete,@Email); SELECT @id= LAST_INSERT_ID();";
            return awaitInsert(entity, insertSql); }}}Copy the code

There you have to create the database and table structures manually, instead of creating them automatically as with EF. With Dapper you basically write SQL only, and if you want to use it like EF you need to install an additional extension, Dapper.Contrib.

The database table structure is simple as follows.

DROP TABLE IF EXISTS `Users`;
CREATE TABLE `Users` (
  `Id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'primary key',
  `Email` varchar(255) DEFAULT NULL COMMENT 'email',
  `UserName` varchar(20) DEFAULT NULL COMMENT 'User name',
  `Mobile` varchar(11) DEFAULT NULL COMMENT 'Mobile phone Number',
  `Age` int(11) DEFAULT NULL COMMENT 'age',
  `Gender` int(1) DEFAULT '0' COMMENT 'gender',
  `Avatar` varchar(255) DEFAULT NULL COMMENT 'avatar',
  `Salt` varchar(255) DEFAULT NULL COMMENT 'salt',
  `Password` varchar(255) DEFAULT NULL COMMENT 'password',
  `IsDelete` int(2) DEFAULT '0' COMMENT '0- Normal 1- Delete ',
  `CreateTime` datetime DEFAULT CURRENT_TIMESTAMP COMMENT 'Creation time'.PRIMARY KEY (`Id`),
  UNIQUE KEY `USER_MOBILE_INDEX` (`Mobile`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=10000 DEFAULT CHARSET=utf8mb4 COMMENT='User Information Table';
Copy the code

Ok, that’s about it. Let’s look at the implementation of the application layer.

Application layer

Create a WebApi project to provide EXTERNAL Api interface services. The structure is as follows.

  • Autofac
    • Stores IOC dependency injection configuration items
  • AutoMapper
    • Stores configuration items for entity object mapping
  • Controllers
    • The controller, the actual business logic will also be written here
  • Fliters
    • Store custom filters
  • Helpers
    • Store some helper classes used in this layer
  • Models
    • Store input/output /DTO entity classes

All right, so that’s the structure. Error first, handle exceptions first, and integrate logers.

Custom exception handling

Create an ErrorHandingMiddleware middleware in the Helpers folder and add the ErrorHandlingExtensions extension that will be used in Startup.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System;
using System.Threading.Tasks;

namespace CodeUin.WebApi.Helpers
{
    public class ErrorHandlingMiddleware
    {
        private readonly RequestDelegate next;
        private readonly ILogger<ErrorHandlingMiddleware> _logger;

        public ErrorHandlingMiddleware(RequestDelegate next, ILogger<ErrorHandlingMiddleware> logger)
        {
            this.next = next;
            _logger = logger;
        }

        public async Task Invoke(HttpContext context)
        {
            try
            {
                await next(context);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex.Message);

                var statusCode = 500;

                await HandleExceptionAsync(context, statusCode, ex.Message);
            }
            finally
            {
                var statusCode = context.Response.StatusCode;
                var msg = "";

                if (statusCode == 401)
                {
                    msg = "Not authorized";
                }
                else if (statusCode == 404)
                {
                    msg = "Service not found";
                }
                else if (statusCode == 502)
                {
                    msg = "Request error";
                }
                else if(statusCode ! =200)
                {
                    msg = "Unknown error";
                }
                if (!string.IsNullOrWhiteSpace(msg))
                {
                    awaitHandleExceptionAsync(context, statusCode, msg); }}}// Error information is captured and returned as Json
        private static Task HandleExceptionAsync(HttpContext context, int statusCode, string msg)
        {
            var result = JsonConvert.SerializeObject(new { Msg = msg, Code = statusCode });

            context.Response.ContentType = "application/json; charset=utf-8";

            returncontext.Response.WriteAsync(result); }}// Extend the method
    public static class ErrorHandlingExtensions
    {
        public static IApplicationBuilder UseErrorHandling(this IApplicationBuilder builder)
        {
            returnbuilder.UseMiddleware<ErrorHandlingMiddleware>(); }}}Copy the code

Then add app.useErrorHandling () to the Configure method of Startup. When the application sends an exception, our custom exception handling will follow.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseHttpsRedirection();

    // Request error configuration
    app.UseErrorHandling();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
  		endpoints.MapControllers();
    });
}
Copy the code

logger

I’m using NLog here and need to install the dependency packages in the project first.

Start by creating a configuration file for nlog.config in the project root directory as follows.


      
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      autoReload="true"
      internalLogLevel="Info"
      internalLogFile="c:\temp\internal-nlog.txt">

	<! -- enable asp.net core layout renderers -->
	<extensions>
		<add assembly="NLog.Web.AspNetCore"/>
	</extensions>

	<! -- the targets to write to -->
	<targets>

		<target xsi:type="File" name="allfile" fileName="${currentdir}\logs\nlog-all-${shortdate}.log"
				layout="${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${aspnet-request-ip}|${logger}|${message} ${exception:format=tostring}" />

		<target xsi:type="Console" name="ownFile-web"
				layout="${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${logger}|${aspnet-request-ip}|${message} ${exception:format=tostring}|url: ${aspnet-request-url}|action: ${aspnet-mvc-action}" />
	</targets>
	<! -- rules to map from logger name to target -->
	<rules>
		<! --All logs, including from Microsoft-->
		<logger name="*" minlevel="Info" writeTo="allfile" />

		<! --Skip non-critical Microsoft logs and so log only own logs-->
		<logger name="Microsoft.*" maxlevel="Info" final="true" />
		<! -- BlackHole without writeTo -->
		<logger name="*" minlevel="Info" writeTo="ownFile-web" />
	</rules>
</nlog>
Copy the code

For more configuration information, go to nlog-project.org

Finally, integrate Nlog into the Program entry file

using Autofac.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using NLog.Web;

namespace CodeUin.WebApi
{
    public class Program
    {
        public static void Main(string[] args)
        {
            NLogBuilder.ConfigureNLog("nlog.config");
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .UseServiceProviderFactory(newAutofacServiceProviderFactory()) .ConfigureLogging(logging => { logging.ClearProviders(); logging.AddConsole(); }) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); }) .UseNLog(); }}Copy the code

Now you can use NLog directly, using the ErrorHandlingMiddleware class above.

Dependency injection

You’ll use Autofac to manage dependencies between classes, and Autofac is awesome. NET IoC container. First we need to install the dependency packages.

Create a new CustomAutofacModule class in the Autofac folder of the project root directory to manage the dependencies between our classes.

using Autofac;
using CodeUin.Dapper.IRepository;
using CodeUin.Dapper.Repository;

namespace CodeUin.WebApi.Autofac
{
    public class CustomAutofacModule:Module
    {
        protected override void Load(ContainerBuilder builder){ builder.RegisterType<UserRepository>().As<IUserRepository>(); }}}Copy the code

Add methods to the Startup class

public void ConfigureContainer(ContainerBuilder builder)
{
    // dependency injection
    builder.RegisterModule(new CustomAutofacModule());
}
Copy the code

Entity mapping

We’ll use Automapper to help us map an object to another object, like this code.

// If there are dozens of attributes it is quite scary
var users = new Users
{
    Email = user.Email,
    Password = user.Password,
    UserName = user.UserName
};
// Using Automapper is much easier
var model = _mapper.Map<Users>(user);
Copy the code

Install the dependency packages first

Create a new AutoMapperConfig class in the AutoMapper folder of the project root directory to manage our mapping.

using AutoMapper;
using CodeUin.Dapper.Entities;
using CodeUin.WebApi.Models;

namespace CodeUin.WebApi.AutoMapper
{
    public class AutoMapperConfig : Profile
    {
        public AutoMapperConfig(){ CreateMap<UserRegisterModel, Users>().ReverseMap(); CreateMap<UserLoginModel, Users>().ReverseMap(); CreateMap<UserLoginModel, UserModel>().ReverseMap(); CreateMap<UserModel, Users>().ReverseMap(); }}}Copy the code

Services is added in the Startup file ConfigureServices method. The AddAutoMapper (AppDomain. CurrentDomain. GetAssemblies ()).

The use of JWT

JWT will be integrated to handle authorization and other information. First, you need to install the dependency packages.

Modify the appsttings. Json file to add Jwt configuration information.

{
    "Logging": {
        "LogLevel": {
            "Default": "Information"."Microsoft": "Warning"."Microsoft.Hosting.Lifetime": "Information"}},"AllowedHosts": "*"."Jwt": {
        "Key": "e816f4e9d7a7be785a".// This key must be larger than 16 digits
        "Issuer": "codeuin.com"}}Copy the code

Finally, add the use of Jwt in the ConfigureServices method of the Startup class.

     services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer(options =>
            {
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuer = true,
                    ValidateAudience = true,
                    ValidateLifetime = true,
                    ClockSkew = TimeSpan.FromMinutes(5),   // The buffer expires in 5 minutes by default
                    ValidateIssuerSigningKey = true,
                    ValidIssuer = Configuration["Jwt:Issuer"],
                    ValidAudience = Configuration["Jwt:Issuer"],
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"))}; });Copy the code

Ok, so finally our Startup class looks like this, and we’ll talk about custom parameter validation later.

using Autofac;
using AutoMapper;
using CodeUin.WebApi.Autofac;
using CodeUin.WebApi.Filters;
using CodeUin.WebApi.Helpers;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Text;

namespace CodeUin.WebApi
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureContainer(ContainerBuilder builder)
        {
            // dependency injection
            builder.RegisterModule(new CustomAutofacModule());
        }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer(options =>
            {
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuer = true,
                    ValidateAudience = true,
                    ValidateLifetime = true,
                    ClockSkew = TimeSpan.FromMinutes(5),   // The buffer expires in 5 minutes by default
                    ValidateIssuerSigningKey = true,
                    ValidIssuer = Configuration["Jwt:Issuer"],
                    ValidAudience = Configuration["Jwt:Issuer"],
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"))}; }); services.AddHttpContextAccessor();/ / use AutoMapper
            services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());

            // Disable automatic parameter verification
            services.Configure<ApiBehaviorOptions>((options) =>
            {
                options.SuppressModelStateInvalidFilter = true;
            });

            // Use custom validators
            services.AddControllers(options =>
            {
                options.Filters.Add<ValidateModelAttribute>();
            }).
            AddJsonOptions(options =>
            {
                // Ignore null values
                options.JsonSerializerOptions.IgnoreNullValues = true;
            });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseHttpsRedirection();

            // Request error configuration
            app.UseErrorHandling();

            / / authorizationapp.UseAuthentication(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); }}}Copy the code

Creating a New Entity Class

I will create three entity classes, namely UserLoginModel user login, UserRegisterModel user registration, UserModel user basic information.

UserLoginModel and UserRegisterModel will automatically validate validity based on the properties we configured in the properties, eliminating the need to write validation logic separately in the controller, which saves a lot of work.

using System;
using System.ComponentModel.DataAnnotations;

namespace CodeUin.WebApi.Models
{
    /// <summary>
    ///User entity class
    /// </summary>
    public class UserModel
    {
        public int Id { get; set; }

        public string Email { get; set; }
        public string UserName { get; set; }

        public string Mobile { get; set; }

        public int Gender { get; set; }

        public int Age { get; set; }

        public string Avatar { get; set; }}public class UserLoginModel{[Required(ErrorMessage = "Please enter email address")]
        public string Email { get; set; }

        [Required(ErrorMessage = "Please enter your password")]
        public string Password { get; set; }}public class UserRegisterModel{[Required(ErrorMessage = "Please enter email address")]
        [EmailAddress(ErrorMessage = "Please enter the correct email address")]
        public string Email { get; set; }

        [Required(ErrorMessage = "Please enter user name")]
        [MaxLength(length: 12, ErrorMessage = "Maximum username length cannot exceed 12")]
        [MinLength(length: 2, ErrorMessage = "Username length must not be less than 2")]
        public string UserName { get; set; }

        [Required(ErrorMessage = "Please enter your password")]
        [MaxLength(length: 20, ErrorMessage = "Maximum password length cannot exceed 20")]
        [MinLength(length: 6, ErrorMessage = "Password length must not be less than 6.")]
        public string Password { get; set; }}}Copy the code

The validator

Adding the ValidateModelAttribute folder to the Filters folder in the root of the project will enter our Filters first in the Action request and output error items that do not match our defined rules.

The specific code is as follows.

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using System.Linq;

namespace CodeUin.WebApi.Filters
{
    public class ValidateModelAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext context)
        {
            if(! context.ModelState.IsValid) {var item = context.ModelState.Keys.ToList().FirstOrDefault();

                // Returns the error message for the first validation argument
                context.Result = new BadRequestObjectResult(new
                {
                    Code = 400,
                    Msg = context.ModelState[item].Errors[0].ErrorMessage }); }}}}Copy the code

Add a custom authentication feature

Sometimes we need to extend our own rules by inheriting the ValidationAttribute class and implementing the IsValid method, for example, I validated the Chinese mobile phone number.

using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;

namespace CodeUin.WebApi.Filters
{
    public class ChineMobileAttribute : ValidationAttribute
    {
        public override bool IsValid(object value)
        {
            if(! (value is string)) return false;

            var val = (string)value;

            return Regex.IsMatch(val, ^ @ "[1] {1} {1},3,4,5,6,7,8,9 [2] \ d {9} $"); }}}Copy the code

Implement login registration

Let’s implement a simple business requirement, user registration, login, and access to user information, the rest of the function is much the same, nothing more than CRUD! .

We’ve already written the interfaces in the data services layer, so it’s time to deal with the business logic, which will be written directly in Controllers.

Create a new controller, UsersController. The service is very simple.

using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using AutoMapper;
using CodeUin.Dapper.Entities;
using CodeUin.Dapper.IRepository;
using CodeUin.Helpers;
using CodeUin.WebApi.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;

namespace CodeUin.WebApi.Controllers{[Route("api/[controller]/[action]")]
    [ApiController]
    [Authorize]
    public class UsersController : Controller
    {
        private readonly ILogger<UsersController> _logger;
        private readonly IUserRepository _userRepository;
        private readonly IMapper _mapper;
        private readonly IConfiguration _config;
        private readonly IHttpContextAccessor _httpContextAccessor;

        public UsersController(ILogger<UsersController> logger, IUserRepository userRepository, IMapper mapper, IConfiguration config, IHttpContextAccessor httpContextAccessor)
        {
            _logger = logger;
            _userRepository = userRepository;
            _mapper = mapper;
            _config = config;
            _httpContextAccessor = httpContextAccessor;
        }

        [HttpGet]
        public async Task<JsonResult> Get()
        {
            var userId = int.Parse(_httpContextAccessor.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value);

            var userInfo = await _userRepository.GetUserDetail(userId);

            if (userInfo == null)
            {
                return Json(new { Code = 200, Msg = "No information found for this user" });
            }

            var outputModel = _mapper.Map<UserModel>(userInfo);

            return Json(new { Code = 200, Data = outputModel }); ;
        }

        [HttpPost]
        [AllowAnonymous]
        public async Task<JsonResult> Login([FromBody] UserLoginModel user)
        {
            // Query user information
            var data = await _userRepository.GetUserDetailByEmail(user.Email);

            // The account does not exist
            if (data == null)
            {
                return Json(new { Code = 200, Msg = "Wrong account or password" });
            }

            user.Password = Encrypt.Md5(data.Salt + user.Password);

            // The password is inconsistent
            if(! user.Password.Equals(data.Password)) {return Json(new { Code = 200, Msg = "Wrong account or password" });
            }

            var userModel = _mapper.Map<UserModel>(data);

            / / token is generated
            var token = GenerateJwtToken(userModel);

            / / in the Redis
            await new RedisHelper().StringSetAsync($"token:{data.Id}", token);

            return Json(new
            {
                Code = 200,
                Msg = "Login successful",
                Data = userModel,
                Token = token
            });
        }

        [HttpPost]
        [AllowAnonymous]
        public async Task<JsonResult> Register([FromBody] UserRegisterModel user)
        {
            // Query user information
            var data = await _userRepository.GetUserDetailByEmail(user.Email);

            if(data ! =null)
            {
                return Json(new { Code = 200, Msg = "This email address has been registered" });
            }

            var salt = Guid.NewGuid().ToString("N");

            user.Password = Encrypt.Md5(salt + user.Password);

            var users = new Users
            {
                Email = user.Email,
                Password = user.Password,
                UserName = user.UserName
            };

            var model = _mapper.Map<Users>(user);

            model.Salt = salt;

            await _userRepository.AddUser(model);

            return Json(new { Code = 200, Msg = "Registration successful" });
        }

        /// <summary>
        ///To generate the Token
        /// </summary>
        /// <param name="user">The user information</param>
        /// <returns></returns>
        private string GenerateJwtToken(UserModel user)
        {
            var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
            var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);

            var claims = new[] {
                new Claim(JwtRegisteredClaimNames.Email, user.Email),
                new Claim(JwtRegisteredClaimNames.Gender, user.Gender.ToString()),
                new Claim(ClaimTypes.NameIdentifier,user.Id.ToString()),
                new Claim(ClaimTypes.Name,user.UserName),
                new Claim(ClaimTypes.MobilePhone,user.Mobile??""),};var token = new JwtSecurityToken(_config["Jwt:Issuer"],
                _config["Jwt:Issuer"],
                claims,
                expires: DateTime.Now.AddMinutes(120),
                signingCredentials: credentials);

            return newJwtSecurityTokenHandler().WriteToken(token); }}}Copy the code

Finally, let’s test our functionality, starting with registration.

Let’s first verify that our passed parameters conform to our defined rules.

Try typing a wrong email number!

Ok, no problem, and the result is the same as the validation feature we added in the UserRegisterModel. Finally, let’s test for full compliance.

Finally, the registration is successful, the query under the database also exists.

Let’s try the login interface. Before calling the login interface, let’s test whether our configured permission authentication is working, and directly access the user information interface without logging in.

Direct access will return unauthorized because we are not logged in and therefore have no Token. This is fine for now, but let’s see if we can pass in the correct Token.

Now, we need to call the login interface. After successful login, a Token will be returned, which will be used for all subsequent interface requests. Otherwise, there will be no access.

Let’s test the password error.

It returns the correct result that we expect. Now let’s try logging in with the correct password to see if we can return the result we want.

The login is successful and the interface returns the expected result. Finally, check whether the generated token is saved to Redis according to the logic we wrote.

No problem, just as we expected.

Let’s take the correct token request to the interface for user information and see if it returns correctly.

The interface that gets the user information does not carry any parameters and only adds Authorization to the Headers of the request header, passing our correct token into it.

We can get our user information correctly, that is to say, there is no problem with our permissions. Now we will use Docker package and deploy it to Linux server.

Packaged deployment

Add the Dockerfile file to the root directory of your project with the following contents.

#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for Faster - was debugging. The FROM McR.microsoft.com/dotnet/aspnet:5.0-buster-slim AS base WORKDIR/app EXPOSE EXPOSE 443 FROM 80 McR.microsoft.com/dotnet/sdk:5.0-buster-slim AS build WORKDIR/SRC COPY [" CodeUin. WebApi/CodeUin. WebApi. Csproj ", "CodeUin.WebApi/"] COPY ["CodeUin.Helpers/CodeUin.Helpers.csproj", "CodeUin.Helpers/"] COPY ["CodeUin.Dapper/CodeUin.Dapper.csproj", "CodeUin.Dapper/"] RUN dotnet restore "CodeUin.WebApi/CodeUin.WebApi.csproj" COPY . . WORKDIR "/src/CodeUin.WebApi" RUN dotnet build "CodeUin.WebApi.csproj" -c Release -o /app/build FROM build AS publish RUN dotnet publish "CodeUin.WebApi.csproj" -c Release -o /app/publish FROM base AS final WORKDIR /app COPY --from=publish /app/publish . ENTRYPOINT ["dotnet", "CodeUin.WebApi.dll"]Copy the code

Run the package command in the directory of the Dockerfile file

# Find Dockerfile docker build -t codeuin-api in current folder. # Save images to local Docker save-o codeuin-api.tar codeuin-apiCopy the code

Finally, we will save the image through the uploaded server after import.

Log in to the server using SSH and run the import command in the directory where the package was uploaded.

Docker load -i codeuin-api.tar # docker run -itd -p8888:80--name codeuin-api codeuin-apiCopy the code

At this point, we are done with the entire deployment, and finally we test the request server interface to see if it is ok.

The final result is ok, so far, all the basic work is done, all the code is stored at github.com/xiazanzhang… If it is helpful to you, you can refer to it.