利用dotnet命令行,快速创建简单WEB三层架构

我们要保证dotnet的开发环境和运行已经安装在本地,我们可以利用“
dotnet –version”命令进行检查,如果发现提示命令行不存在,说明环境还没安装,需要先安装,安装过程本文不在详细描述,参考微软的官方说明即可,本文重点在于如何用dotnet命令行,快速构建简单三层架构

一:首先,我们创建好项目的文件夹,然后进入文件夹,创建一个web的应用程序,具体用到的命令是“dotnet new”,dotnet new – 根据指定的模板,创建新的项目、配置文件或解决方案。常用的有类库“classlib”、控制台应用程序“console”、WebAPI应用“webapi”、ASP.NET Core Web 应用程序“webapp”

好了,我们创建一个名称叫Server的WebAPI程序、业务逻辑层的类库BLL、数据交互层DAL和工具层Common

dotnet new webapi -n Server
dotnet new classlib -n BLL
dotnet new classlib -n DAL
dotnet new classlib -n Common

此时,我们可以用visual code看下目录结构

二:我们要让这些类库至今添加引用关系,让Server引用BLL层和Common;BLL层引用DAL层和Common;DAL层引用Common层;这里,我们用到的命令是“dotnet add/list/remove reference”

dotnet add Server/Server.csproj reference BLL/BLL.csproj
dotnet add Server/Server.csproj reference Common/Common.csproj
dotnet add BLL/BLL.csproj reference DAL/DAL.csproj
dotnet add BLL/BLL.csproj reference Common/Common.csproj
dotnet add DAL/DAL.csproj reference Common/Common.csproj

三:接下来我们要在DAL层引入微软推荐的EntityFrameWork框架来进行数据库数据的访问,此处例子,我已经在“.Net Core + EntityFramework连接数据库”一文中有描述,此处只做简单说明,我用MySQL为例,我们需要引入

Microsoft.EntityFrameworkCore

Microsoft.EntityFrameworkCore.Design

Microsoft.EntityFrameworkCore.Tools

Pomelo.EntityFrameworkCore.MySql

Microsoft.Extensions.Configuration.Json

我们用dotnet add package – 添加或更新项目文件中的包引用。在引用之前,我们可以用dotnet package search 命令搜索 NuGet 包。

dotnet package search Microsoft.EntityFrameworkCore

然后,我们可以选择对应的版本,我这里使用的是8.0.2,进行添加,当然,也可以忽略版本号,用最新版本,前提是你的.net的框架也是最新的,可以兼容此版本

dotnet add DAL/DAL.csproj package Microsoft.EntityFrameworkCore --version 8.0.2
dotnet add DAL/DAL.csproj package Microsoft.EntityFrameworkCore.Design --version 8.0.2
dotnet add DAL/DAL.csproj package Microsoft.EntityFrameworkCore.Tools --version 8.0.2
dotnet add DAL/DAL.csproj package Pomelo.EntityFrameworkCore.MySql --version 8.0.2
dotnet add DAL/DAL.csproj package Microsoft.Extensions.Configuration.Json

三:下面框架和引用已经搭建完成了,我们需要把项目和数据库连接起来,让我们在DAL项目里面新建一个appsettings.json配置文件,把mysql的数据库连接字符串写上,里面填上你的数据库ip,用户名,密码,以及你想存放的数据库名称

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Port=3306;Database=数据库名称;User=root;Password=你的mysql密码;Charset=utf8mb4;"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

在DAL项目里创建Modles文件夹用来存放数据库对象,比如User类,对应数据库User表

namespace DAL.Models;
using System.ComponentModel.DataAnnotations.Schema;

public class User
{
  [Column("Id")]
  public int Id { get; set; }
  
  [Column("UserName")]
  public string? UserName { get; set; }
}

在DAL项目里创建Contexts文件用来存放上下文类,此类建议和数据库名称一一对应

namespace DAL.Contexts;
using Microsoft.EntityFrameworkCore;
using DAL.Models;
using Microsoft.Extensions.Configuration;

public class webapplicationContext : DbContext
{
  public webapplicationContext()
  {
    Configuration = new ConfigurationBuilder().SetBasePath(AppDomain.CurrentDomain.BaseDirectory).AddJsonFile("appsettings.json").Build();
  }
  public IConfiguration Configuration;
  public required DbSet<User> Users { get; set; }

  protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
  {
    var connectionString = Configuration.GetConnectionString("DefaultConnection");
    optionsBuilder.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString));
  }
}

四:初始化数据库,当我们完成了项目基本的搭建,这时候,我们需要用Entity Framework Core 的命令行工具,创建数据库,首先我们打开终端,定位到项目所在位置,然后,就可以用ef命令来初始化数据库,如图所示

cd DAL
dotnet ef migrations add InitialCreate
dotnet ef database update

完成后,发现我们对应的数据库就会有对应的库和表了

关于EntityFramWork的操作,请参考.Net Core+Entity Framework读写数据

下面,让我们回到Server文件夹下,运行dotnet run命令,找到对应端口,访问网页http://localhost:端口/swagger/index.html,看看是否正常

cd ..
cd Server
dotnet run

最后,我们可以给项目文件根里添加.gitignore文件,忽略bin文件,方便以后版本控制

*.log
temp/
.vs/
BLL/bin
BLL/obj
DAL/bin
DAL/obj
Server/bin
Server/obj
Common/bin
Common/obj

我们也可以在Server项目里新建Controllers文件夹,用来存放控制类

using Microsoft.AspNetCore.Mvc;

namespace Server.Controllers;

[ApiController]
[Route("api/[controller]")]
public class WeatherForcecastController : ControllerBase
{
    private readonly ILogger<WeatherForcecastController> _logger;

    public WeatherForcecastController(ILogger<WeatherForcecastController> logger)
    {
        _logger = logger;
    }

    private static readonly string[] Summaries = new[]
    {
      "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    [HttpGet(Name = "GetWeatherForecast")]
    public IEnumerable<WeatherForecast> GetWeatherForecast()
    {
        var forecast =  Enumerable.Range(1, 5).Select(index =>  new WeatherForecast
        (
          DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
          Random.Shared.Next(-20, 55),
          Summaries[Random.Shared.Next(Summaries.Length)]
        )).ToArray();
        return forecast;
    }

    public record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
    {
        public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
    }
}

最后把Server/Program.cs文件调整一下,加载控制类

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.MapControllers();
app.Run();

Linq多条件字段的多表连接

假设,我们有一个类StationCat,它的具体属性如下:

public class StationCat
{
    public string CustomerModel { get; set; }

    public string baj_cZZLINE { get; set; }

    public string IssuePosition { get; set; }

    public string fact { get; set; }

    public int InputCount { get; set; }

    public int RepCount { get; set; }

    public string S_NM { get; set; }
}

此时,我们有2个由这个类实例化出的实体数组SWAPMaterials和StationCats,当我们想要把SWAPMaterials左连接到StationCats时,条件为SWAPMaterials表中元素的CustomerModel、baj_cZZLINE、S_NM与StationCats表元素对应的属性相等,那我们应该按照如下方式写LINQ

var resSWAPMaterials = (from p in SWAPMaterials
join StationCat in StationCats on new { p.CustomerModel, p.baj_cZZLINE,p.S_NM } equals new { StationCat.CustomerModel,StationCat.baj_cZZLINE,StationCat.S_NM } into p_StationCat
from StationCat_join in p_StationCat.DefaultIfEmpty()
select new StationCat()
{
    CustomerModel = p.CustomerModel,
    baj_cZZLINE = p.baj_cZZLINE,
    IssuePosition = p.IssuePosition,
    fact = p.fact,
    RepCount = p.RepCount,
    S_NM = p.S_NM,
    InputCount = StationCat_join == null ? 0 : StationCat_join.InputCount
}).ToList();

到此,我们就实现了Linq多条件字段的多表连接

使用HTTP请求方式访问.Net的WebServer服务

.Net的WebServer服务,是一项已经过时的技术,随着技术迭代,当前跨平台的通讯,更多使用用JSON格式数据,发起HTTP请求来实现,但是,有时候,我们为了和老的.NET的WebServer框架通讯,不得不处理和面对的问题

WebServer服务,其实说白了,就是用XML的格式来处理通讯问题,我们可以试着写一个.Net的WebServer服务,文件名如下:WMI.asmx

/// <summary>
/// WMI 的摘要说明
/// </summary>
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.ComponentModel.ToolboxItem(false)]
// 若要允许使用 ASP.NET AJAX 从脚本中调用此 Web 服务,请取消注释以下行。 
[System.Web.Script.Services.ScriptService]
public class WMI : System.Web.Services.WebService
{
    [WebMethod(EnableSession = true)]
    public string WMI01(string Par)
    {
        try
        {
            //业务逻辑
            returnOK;
        }
        catch (Exception ex)
        {
            //处理异常信息
            return "存在异常信息";
        }
        finally
        {
            //如不记录就 就注释以下内容
        }
    }
}

此时,我们可以运行程序,预览页面效果,接下来,我们在向这个server地址提交http请求的时候,可以使用以下方法:

一、在HTTP头信息上,加上“Content-Type”,值为“text/xml; charset=utf-8”

二、撰写XML格式的请求对象文本,注意,<Par>节点就是WebServer的参数节点,<WMI01>节点就是WebServer的控制器中对应的方法函数

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <WMI01 xmlns="http://tempuri.org/">
      <Par>让我们试试看</Par>
    </WMI01>
  </soap:Body>
</soap:Envelope>

最后,我们可以在POSTMAN程序下试试,预览下请求结果,发现<WMI01Result>节点,就是我们程序返回的处理结果

C#路由方式调用RabbitMQ

RabbitMQ是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件)。RabbitMQ服务器是用Erlang语言编写的,而集群和故障转移是构建在开放电信平台框架上的。所有主要的编程语言均有与代理接口通讯的客户端库。

所谓RabbitMQ的路由模式(Routing),就是有选择地(Routing key)接收消息,发送消息到交换机并且要指定路由key ,消费者将队列绑定到交换机时需要指定路由key,仅消费指定路由key的消息

Exchange不再把消息交给每一个绑定的队列,而是根据消息的RoutingKey进行判断,只有队列的RoutingKey与消息的RoutingKey完全一致,才会接收消息,路由模式使用的Exchange类型为Direct类型

举个例子:如在商品库存中增加了1台iphone12,iphone12促销活动消费者指定routing key为iphone12,只有此促销活动会接收到消息,其它促销活动不关心也不会消费此routing key的消息

下面,我们用C#代码来具体实现一下,以下逻辑需要NuGet里的RabbitMQ.Client包支持,首先安装RabbitMQ.Client包

接下来,我们声明一个类库,用来存放消息对象ReceiverMsgData,为了通用,我特地申明了类型,操作,具体对象实体等内容,以下属性仅供参考,安装自己的具体业务需求设计即可

/// <summary>
/// 消息内容
/// </summary>
public class ReceiverMsg
{
    /// <summary>
    /// 消息类型
    /// 10001->定时任务(队列:calculation_remind)
    /// 23001->订单下单分佣计算(队列:calculation_orderaward)
    /// 23005->订单支付成功计算报表(队列:calculation_orderoperate)
    /// 23006->订单售后成功计算报表(队列:calculation_orderoperate)
    /// </summary>
    [Required(ErrorMessage = "{0} 必须填写")]
    public int msg_type { get; set; }

    /// <summary>
    /// 操作人
    /// </summary>
    [Required(ErrorMessage = "{0} 必须填写")]
    public long creator_id { get; set; }

    /// <summary>
    /// 单据id
    /// </summary>
    [Required(ErrorMessage = "{0} 必须填写")]
    public int source_id { get; set; }

    /// <summary>
    /// 单据来源,定时任务专用(1订单)
    /// </summary>
    public int source_type { get; set; }

    /// <summary>
    /// 单据操作,定时任务专用 (1添加,2发货,3签收)
    /// </summary>
    public int action_type { get; set; }

    /// <summary>
    /// 定时任务执行的时间(无需填写,定时任务自动生成)
    /// </summary>
    public DateTime next_remind_time { get; set; }

    /// <summary>
    /// 错误消息系统编码(无需填写,自动重试用)
    /// </summary>
    public int error_msg_id { get; set; }

    /// <summary>
    /// 错误消息所在的队列名称(无需填写,自动重试用)
    /// </summary>
    public string error_msg_domain { get; set; }

    /// <summary>
    /// 消息附加内容
    /// </summary>
    public object msg_body { get; set; }
}

/// <summary>
/// 消息对象
/// UserName:admin
/// Password:Myun@123jx
/// VirtualHost:Publish
/// Port:5672
/// ExchangeName:calculation
/// </summary>
[Serializable]
public class ReceiverMsgData
{
    /// <summary>
    /// VirtualHost
    /// </summary>
    [Required(ErrorMessage = "{0} 必须填写")]
    public string pattern { get; set; }

    /// <summary>
    /// Msgid
    /// </summary>
    public string id { get; set; }

    /// <summary>
    /// 消息内容
    /// </summary>
    [Required(ErrorMessage = "{0} 必须填写")]
    public ReceiverMsg data { get; set; }
}

然后,我们生命一个生产者,用来产生消息

public class RabbitMQHelp
{
    public static IConfigurationRoot configuration = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()).AddJsonFile("appsettings.json").Build();
    private static string HostName = EncryptHelp.Decrypt(configuration.GetSection("RabbitMQ")["HostName"]);  //RabbitMQ服务IP地址
    private static string UserName = EncryptHelp.Decrypt(configuration.GetSection("RabbitMQ")["UserName"]);  //RabbitMQ用户名
    private static string Password = EncryptHelp.Decrypt(configuration.GetSection("RabbitMQ")["Password"]);  //RabbitMQ密码
    private static int Port = Convert.ToInt32(EncryptHelp.Decrypt(configuration.GetSection("RabbitMQ")["Port"]));//RabbitMQ端口 默认5672
    private static string VirtualHost = EncryptHelp.Decrypt(configuration.GetSection("RabbitMQ")["VirtualHost"]);//Virtual Hosts名称
    private static string ExchangeName = "calculation";//交换机名称

    /// <summary>
    /// 连接配置
    /// </summary>
    private static readonly ConnectionFactory rabbitMqFactory = new ConnectionFactory()
    {
        HostName = HostName,
        UserName = UserName,
        Password = Password,
        Port = Port,
        VirtualHost = VirtualHost
    };

    /// <summary>
    /// 单点精确路由模式,重发消息
    /// </summary>
    public static void DirectExchangeSendMsg(string QueueName, string msgBody)
    {
        using (IConnection conn = rabbitMqFactory.CreateConnection())
        {
            using (IModel channel = conn.CreateModel())
            {
                channel.ExchangeDeclare(ExchangeName, ExchangeType.Direct, durable: true, autoDelete: false, arguments: null);
                channel.QueueDeclare(QueueName, durable: true, autoDelete: false, exclusive: false, arguments: null);
                channel.QueueBind(QueueName, ExchangeName, routingKey: QueueName);

                var props = channel.CreateBasicProperties();
                props.Persistent = true; //消息永久化硬盘上,打开会降低些许性能,但是可以防止服务器意外断电导致消息队列数据消失的情况
                var BytmsgBody = Encoding.UTF8.GetBytes(msgBody);
                channel.BasicPublish(exchange: ExchangeName, routingKey: QueueName, basicProperties: props, body: BytmsgBody);
            }
        }
    }
}

最后,我们声明一个消费者,用来接收消息

/// <summary>
/// 消费者
/// </summary>
/// <param name="QueueName"></param>
public static void ReceiveWeatherInfo(string QueueName)
{
    using (var connection = rabbitMqFactory.CreateConnection())
    {
        using (var channel = connection.CreateModel())
        {
            channel.ExchangeDeclare(ExchangeName, ExchangeType.Direct, durable: true, autoDelete: false, arguments: null);
            channel.QueueDeclare(QueueName, durable: true, autoDelete: false, exclusive: false, arguments: null);
            channel.QueueBind(QueueName, ExchangeName, routingKey: QueueName);
            channel.BasicQos(0, 1, false);
            var consumer = new EventingBasicConsumer(channel);

            consumer.Received += (model, ea) =>
            {
                var message = Encoding.UTF8.GetString(ea.Body.ToArray());
                Console.WriteLine($"队列{QueueName}收到的气象信息:{message}");
                channel.BasicAck(ea.DeliveryTag, false);
            };
            channel.BasicConsume(queue: QueueName,autoAck: false,consumer: consumer);
            Console.WriteLine(" Press [enter] to exit.");
            Console.ReadLine();
        }
    }
}

下面,我们演示一下消息发送

//自动处理售后消息
public static void doDealReturnOrder(int Return_Id)
{
    RabbitMQHelp.DirectExchangeSendMsg("calculation_orderoperate", JsonConvert.SerializeObject(new ReceiverMsgData
    {
        id = Guid.NewGuid().ToString(),
        pattern = "Publish",
        data = new ReceiverMsg()
        {
            source_id = Return_Id,
            msg_type = 23019,
        }
    }));//重发消息
}

最后,只要由控制台的Main函数指定队列名称,调度这个消费者方法,就能接收指定队列的消息了,注意我这里,应为发送的消息是序列化的JSON,所以,接收消息需要反序列化一下

.Net Core+Entity Framework读写数据

同步操作

查询数据

为了防止连接数过多,忘记及时释放,建议用using方式操作

using (var context = new MySqlTestDBContext())
{
    var blogs = context.Blogs
        .Where(b => b.Url.Contains("dotnet"))
        .ToList();
}

查询数据,使用的是大量的Linq语言,复杂查询也包括Join等多表连接,和Group By后,Sum(),Avg(),Max(),Min(),FirstOrDefault(),LastOrDefault()等聚合函数,具体可查询Linq的语法,这里不再展开

添加数据

using (var context = new MySqlTestDBContext())
{
	var blog = new Blog { Url = "http://example.com" };
	var Id = context.Blogs.Add(blog).Entity.Id;
	context.SaveChanges();
}

使用 DbSet.Add 方法添加实体类的新实例。 调用 SaveChanges 时,数据将插入到数据库中。

更新数据

找到Blogs表中,BlogId为1的数据,将它的Url字段进行更新

using (var context = new MySqlTestDBContext())
{
	var blog = context.Blogs.FirstOrDefault(i=>i.Id==1);
	blog.Url = "http://example.com/blog";
	context.SaveChanges();
}

删除数据

找到Blogs表中,BlogId为1的数据,将它物理删除

using (var context = new MySqlTestDBContext())
{
    var blog = context.Blogs.FirstOrDefault(i=>i.Id==1);
    context.Blogs.Remove(blog);
    context.SaveChanges();
}

事务

对于大多数数据库提供程序,“SaveChanges” 是事务性的。 这意味着所有操作将一起成功或一起失败,绝不会部分的应用这些操作。

using (var context = new MySqlTestDBContext())
{
    // seeding database
    context.Blogs.Add(new Blog { Url = "http://example.com/blog" });
    context.Blogs.Add(new Blog { Url = "http://example.com/another_blog" });
    context.SaveChanges();
}

using (var context = new MySqlTestDBContext())
{
    // add
    context.Blogs.Add(new Blog { Url = "http://example.com/blog_one" });
    context.Blogs.Add(new Blog { Url = "http://example.com/blog_two" });

    // update
    var firstBlog = context.Blogs.First();
    firstBlog.Url = "";

    // remove
    var lastBlog = context.Blogs.Last();
    context.Blogs.Remove(lastBlog);

    context.SaveChanges();
}

异步操作

当在数据库中执行查询时,异步查询可避免阻止线程。 异步查询对于在胖客户端应用程序中保持响应式 UI 非常重要。 异步查询还可以增加 Web 应用程序中的吞吐量,即通过释放线程,以处理 Web 应用程序中的其他请求

查询数据

public async Task<List<Blog>> GetBlogsAsync()
{
    using (var context = new MySqlTestDBContext())
    {
        return await context.Blogs.ToListAsync();
    }
}

数据集除了ToList方法外,还有一个ToListAsync方法,返回的是一个异步的Task对象

保存数据

同理,异步保存也有一个方法SaveChangesAsync(),返回的也是一个异步Task对象

public static async Task AddBlogAsync(string url)
{
    using (var context = new BloggingContext())
    {
        var blog = new Blog { Url = url };
        context.Blogs.Add(blog);
        await context.SaveChangesAsync();
    }
}

.Net Core + EntityFramework连接数据库

在本教程中,将创建一个 .NET Core 控制台应用,该应用使用 Entity Framework Core 对 SQLServer、MySQL数据库执行数据访问。

你可在 Windows 上使用 Visual Studio,或在MacOS上使用Visual Studio For Mac来学习本教程。

运行系统:Windows、MacOS、Linux

.Net Core环境:.Net Core 3.0+

开发架构搭建

首先创建Core控制台应用程序

目标框架选择.Net Core 3.0以上,起个项目名称,(以下用MySqlTest为例子)

创建项目完成后,选中项目,右键NuGet包管理,搜索entityframeworkcore

此时,我们要选择如下的几个包进行安装

Microsoft.EntityFrameworkCore

Microsoft.EntityFrameworkCore.Design

Microsoft.EntityFrameworkCore.Tools

然后,我们要根据我们项目的数据库。选应该使用的包,

如果是MySql数据库,选用

Pomelo.EntityFrameworkCore.MySql

如果是SQL Server数据库,选用

Microsoft.EntityFrameworkCore.SqlServer

最后,我们在搜索NuGet包,输入搜索Microsoft.Extensions.Configuration.Json,进行安装

Microsoft.Extensions.Configuration.Json

(以下用mysql数据库为例)

项目新建完毕后,我们再新建一个项目配置项,选中项目,右键->添加->新增文件->应用程序设置文件,然后把数据库连接字符串放上去

我们给数据库起名为MySqlTestDB,注意不同数据库使用的连接字符串不同

MySql数据库:

Server=localhost;Database=MySqlTestDB;Port=3306;charset=utf8;uid=用户;pwd=密码;

SqlServer数据库:

server=localhost,1433;database=MySqlTestDB;uid=用户;pwd=密码

创建模型和上下文

定义构成模型的上下文类和实体类。

我们在项目里新增“Modles”文件夹,用于存放数据库实体类

新增“Contexts”文件夹,用于存放上下文类

using MySqlTest.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using System.IO;
namespace MySqlTest.Contexts
{
    public class MySqlTestDBContext: DbContext
    {
        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }

        private IConfiguration configuration;

        public MySqlTestDBContext()
        {
            configuration = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()).AddJsonFile("appsettings.json").Build();
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseMySql(configuration.GetConnectionString("Default"));
        }
    }
}

Models对应着我们数据库里的表集合。

Entity Framework Core 中,基类默认主键名称为Id,那么在实际表设计中主键名称不一定为Id,那我们可以用Column来指定字段映射

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;

namespace MySqlTest.Models
{
    public class Blog
    {
        [Column("BlogId")]
        public int Id { get; set; }

        [Column("Url")]
        public string Url { get; set; }

        public List<Post> Posts { get; } = new List<Post>();
    }
}
using System;
using System.ComponentModel.DataAnnotations.Schema;

namespace MySqlTest.Models
{
    public class Post
    {
        [Column("PostId")]
        public int Id { get; set; }

        [Column("Title")]
        public string Title { get; set; }

        [Column("Content")]
        public string Content { get; set; }
    }
}

创建数据库

以上,我们完成了项目基本的搭建,这时候,我们需要用Entity Framework Core 的命令行工具,创建数据库,首先我们打开终端,定位到项目所在位置,如图所示

然后我们编译项目,输入如下命令(当然,也可以在Visual Studio里面直接编译)

dotnet build

然后我们输入以下命令

dotnet ef migrations add InitialCreate

这时,我们会看见项目里面多了一个文件夹,里面是升级的内容

然后我们在终端输入以下命令

dotnet ef database update

完成之后,我们就可以看见,我们的本地数据库已经有对应的数据库和数据表生成了

数据库添加字段

当然,我们在项目迭代过程中。回遇到数据库新增字段的情况,这时候,我们要先在对应的Modle实体类里面做调整,按Blog类为例,我们新增一个属性Title

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;

namespace MySqlTest.Models
{
    public class Blog
    {
        [Column("BlogId")]
        public int Id { get; set; }

        [Column("Url")]
        public string Url { get; set; }

        [Column("Title")]
        public string Title { get; set; }

        public List<Post> Posts { get; } = new List<Post>();
    }
}

然后,我们回到终端,重新编译(当然,也可以在Visual Studio里面直接编译)

dotnet build

然后我们输入以下命令

dotnet ef migrations add InitialUpdate_Title

然后我们在终端输入以下命令

dotnet ef database update

好了,这时候,我们看下数据库,已经有新的字段出来了

当然,除此之外,migrations 命令还可以回退sql,也可以指定时间范围进行升级和降级,更多功能请参考官方文档
此处不再累赘。

数据库新增表

当然,我们在项目迭代过程中。回遇到数据库新增表的情况,这时候,我们要先在对应的Modle实体类里面新增实体Person

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;

namespace MySqlTest.Models
{
    public class Person
    {
        [Column("PersonId")]
        public int Id { get; set; }

        [Column("CelPhoneNo")]
        public string CelPhoneNo { get; set; }

        [Column("Name")]
        public string Name { get; set; }
    }
}

然后,在上下文里面添加这个属性 public DbSet<Person> Persons { get; set; }

using MySqlTest.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using System.IO;
namespace MySqlTest.Contexts
{
    public class MySqlTestDBContext: DbContext
    {
        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }
        public DbSet<Person> Persons { get; set; }

        private IConfiguration configuration;

        public MySqlTestDBContext()
        {
            configuration = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()).AddJsonFile("appsettings.json").Build();
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseMySql(configuration.GetConnectionString("Default"));
        }
    }
}

然后,我们回到终端,重新编译(当然,也可以在Visual Studio里面直接编译)

dotnet bulid

然后我们输入以下命令

dotnet ef migrations add InitialAdd_TablePerson

然后我们在终端输入以下命令

dotnet ef database update

当我们有多个数据库DBContext的时候,命令后面带个参数即可

dotnet ef  migrations add xxxx --context XXXDBContext

dotnet ef database update --context XXXDBContext

搭建.Net Core WebAPI应用程序

在本章节,我们将着重介绍,如果搭建一个基于“.Net Core 3.1 WebAPI+EntityFrameWork+数据库”的应用程序

首先,我们需要.Net Core 3.1 WebAPI的应用程序

所需要的工具:

.Net Core 3.1运行开发环境

Visual Studio 2019

MySql 数据库或SqlServe数据库

创建空白解决方案

打开Visual Studio 2019 ->创建新项目,搜索“空白解决方案”,输入要搭建的服务名称,一般为“MYun.BPC.WebAPI.XXX”,如:

MYun.BPC.WebAPI.Basic

创建完成后,打开解决方案,选中,右键->添加->新建解决方案文件架。现阶段我们需要新增2个文件夹

01_Host 用于存放站点

02_Server 业务逻辑类库

完成后,我们就可以添加站点了

创建Action和Query站点

选中01_Host文件架,右键->添加->新建项目,搜索“ASP.NET Core Web”,选中,点击“下一步”,输入项目名称,名称一般为“MYun.BPC.WebAPI.XXX.Action”和“MYun.BPC.WebAPI.XXX.Query”如:

MYun.BPC.WebAPI.Basic.Action 用于处理端提交的操作请求

MYun.BPC.WebAPI.Basic.Query 用于处理端提交的查询请求

点击创建。这时,我们选中框架为“ASP.Net Core 3.1”,应用程序为“API”,Https配置关闭

创建完成后,效果如下:

到此,WabAPI站点创建完成,接下来,我们需要添加下面几个引用库,用来支持我们的站点

MYun.MY.Contract.Data.dll 契约类库,由类库Contract项目编译生成,用于承载返回的对象

Service.dll 框架类库,由类库Contract项目编译生成,用于承载返回对象的基类

此外,我们还需要在NuGet添加

Newtonsoft.Json

的包。用来支持Json对象的序列化

右键单击“解决方案资源管理器” -> “管理 NuGet 包”中的项目

将“包源”设置为“nuget.org”

在搜索框中输入“Newtonsoft.Json”

从“浏览”选项卡中选择“Newtonsoft.Json”包,然后单击“安装”

当前版本为“12.0.3”

为站点生成说明文档

在使用asp.net core 进行api开发完成后,书写api说明文档对于程序员来说想必是件很痛苦的事情吧,但文档又必须写,而且文档的格式如果没有具体要求的话,最终完成的文档则完全取决于开发者的心情。或者详细点,或者简单点。那么有没有一种快速有效的方法来构建api说明文档呢?答案是肯定的, Swagger就是最受欢迎的REST APIs文档生成工具之一!

为什么使用Swagger作为REST APIs文档生成工具

   Swagger 可以生成一个具有互动性的API控制台,开发者可以用来快速学习和尝试API。

   Swagger 可以生成客户端SDK代码用于各种不同的平台上的实现。

   Swagger 文件可以在许多不同的平台上从代码注释中自动生成。

   Swagger 有一个强大的社区,里面有许多强悍的贡献者。

asp.net core中如何使用Swagger生成api说明文档呢

   Swashbuckle.AspNetCore 是一个开源项目,用于生成 ASP.NET Core Web API 的 Swagger 文档。

   NSwag 是另一个用于将 Swagger UI 或 ReDoc 集成到 ASP.NET Core Web API 中的开源项目。 它提供了为 API 生成 C# 和 TypeScript 客户端代码的方法。

下面以Swashbuckle.AspNetCore为例为大家进行展示

右键单击选中站点 -> “管理 NuGet 包”中的项目

将“包源”设置为“nuget.org”

在搜索框中输入“Swashbuckle.AspNetCore”

从“浏览”选项卡中选择“Swashbuckle.AspNetCore”包,然后单击“安装”

当前版本为“5.6.1”

Action和Query两个站点都安装完毕后,我们再右键选中这些站点->属性->生成

XML文档文件,勾选,指定路径bin\Debug\netcoreapp3.1\MYun.BPC.WebAPI.Basic.XXX.xml

如:

bin\Debug\netcoreapp3.1\MYun.BPC.WebAPI.Basic.Query.xml

bin\Debug\netcoreapp3.1\MYun.BPC.WebAPI.Basic.Action.xml

最后,我们打开项目里的Startup.cs文件,新增引用命名空间

using Microsoft.OpenApi.Models;

using Swashbuckle.AspNetCore.Swagger;

using System.IO;

找到ConfigureServices方法。里面添加如下代码

services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo
    {
        Version = "v1", //接口文档版本
        Title = "Myun .NetCore BascQuery API",  //接口文档标题
        Description = "基础模块BaseQuery",   //描述,不同模块自己设计
    });

    // 为 Swagger JSON and UI设置xml文档注释路径
    var basePath = Path.GetDirectoryName(typeof(Program).Assembly.Location);//获取应用程序所在目录(绝对,不受工作目录影响,建议采用此方法获取路径)

    var ServicexmlPath = Path.Combine(basePath, "Service.xml"); //此为框架契约说明文档
    c.IncludeXmlComments(ServicexmlPath);

    var DataxmlPath = Path.Combine(basePath, "MYun.MY.Contract.Data.xml");  //此为返回对象说明文档
    c.IncludeXmlComments(DataxmlPath);

    var xmlPath = Path.Combine(basePath, "MYun.BPC.WebAPI.Basic.Query.xml");  //此为当前站点说明文档,Query或Action按不同站点具体配置
    c.IncludeXmlComments(xmlPath);
});

找到Configure方法。里面添加如下代码

//启用中间件服务生成Swagger作为JSON终结点
app.UseSwagger();
//启用中间件服务对swagger-ui,指定Swagger JSON终结点
app.UseSwaggerUI(c =>
{
    c.SwaggerEndpoint("/swagger/v1/swagger.json", "Myun .NetCore BascQuery API");
});

最终效果如下

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
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.Extensions.Logging;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.Swagger;
using System.IO;

namespace MYun.BPC.WebAPI.Basic.Action
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();

            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo
                {
                    Version = "v1", //接口文档版本
                    Title = "Myun .NetCore BascQuery API",  //接口文档标题
                    Description = "基础模块BaseQuery",   //描述,不同模块自己设计
                });

                // 为 Swagger JSON and UI设置xml文档注释路径
                var basePath = Path.GetDirectoryName(typeof(Program).Assembly.Location);//获取应用程序所在目录(绝对,不受工作目录影响,建议采用此方法获取路径)

                var ServicexmlPath = Path.Combine(basePath, "Service.xml"); //此为框架契约说明文档
                c.IncludeXmlComments(ServicexmlPath);

                var DataxmlPath = Path.Combine(basePath, "MYun.MY.Contract.Data.xml");  //此为返回对象说明文档
                c.IncludeXmlComments(DataxmlPath);

                var xmlPath = Path.Combine(basePath, "MYun.BPC.WebAPI.Basic.Query.xml");  //此为当前站点说明文档,Query或Action按不同站点具体配置
                c.IncludeXmlComments(xmlPath);
            });
        }

        // 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.UseRouting();

            app.UseAuthorization();

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

            //启用中间件服务生成Swagger作为JSON终结点
            app.UseSwagger();
            //启用中间件服务对swagger-ui,指定Swagger JSON终结点
            app.UseSwaggerUI(c =>
            {
                c.SwaggerEndpoint("/swagger/v1/swagger.json", "Myun .NetCore BascQuery API");
            });
        }
    }
}

到此,文档自动生成工具配置完成,我们现在试试效果。

我们打开项目自动生成的控制器WeatherForecastController文件和WeatherForecast文件,将各个方放加上备注说明,如下

接下来,我们右键站点->属性->调试,将“启动浏览器”修改为“swagger/index.html

保存后,点击调试“运行”。然后打开站点http://localhost:port/swagger/index.html看下效果。发现描述字段都已经出来了

注:为了方便后期维护和节约文档撰写时间,所有.NET Core项目的公共可见类和成员,必须要有描述,如果不描述,则编译的时候会产生警告。

统一时间格式

指定当前项目时间格式统一为文本。24小时制“yyyy-MM-dd HH:mm:ss”,选中站点->添加->新增项->类,添加一个DatetimeJsonConverter

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;

namespace MYun.BPC.WebAPI.Basic.Action
{
    public class DatetimeJsonConverter : JsonConverter<DateTime>
    {
        /// <summary>
        /// 读
        /// </summary>
        /// <param name="reader"></param>
        /// <param name="typeToConvert"></param>
        /// <param name="options"></param>
        /// <returns></returns>
        public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            if (reader.TokenType == JsonTokenType.String)
            {
                if (DateTime.TryParse(reader.GetString(), out DateTime date))
                    return date;
            }
            return reader.GetDateTime();
        }

        /// <summary>
        /// 写
        /// </summary>
        /// <param name="writer"></param>
        /// <param name="value"></param>
        /// <param name="options"></param>
        public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
        {
            writer.WriteStringValue(value.ToString("yyyy-MM-dd HH:mm:ss"));
        }
    }
}

打开Startup.cs找到ConfigureServices方法,添加一条记录

services.AddMvc().AddJsonOptions((options) =>
{
     options.JsonSerializerOptions.Converters.Add(new DatetimeJsonConverter());
     options.JsonSerializerOptions.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All);
     options.JsonSerializerOptions.PropertyNamingPolicy = null;
});

最终效果如下

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();

            services.AddMvc().AddJsonOptions((options) =>
            {
                options.JsonSerializerOptions.Converters.Add(new DatetimeJsonConverter());
                options.JsonSerializerOptions.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All);
                options.JsonSerializerOptions.PropertyNamingPolicy = null;
            });

            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo
                {
                    Version = "v1", //接口文档版本
                    Title = "Myun .NetCore BascQuery API",  //接口文档标题
                    Description = "基础模块BaseQuery",   //描述,不同模块自己设计
                });

                // 为 Swagger JSON and UI设置xml文档注释路径
                var basePath = Path.GetDirectoryName(typeof(Program).Assembly.Location);//获取应用程序所在目录(绝对,不受工作目录影响,建议采用此方法获取路径)

                var ServicexmlPath = Path.Combine(basePath, "Service.xml"); //此为框架契约说明文档
                c.IncludeXmlComments(ServicexmlPath);

                var DataxmlPath = Path.Combine(basePath, "MYun.MY.Contract.Data.xml");  //此为返回对象说明文档
                c.IncludeXmlComments(DataxmlPath);

                var xmlPath = Path.Combine(basePath, "MYun.BPC.WebAPI.Basic.Action.xml");  //此为当前站点说明文档,Query或Action按不同站点具体配置
                c.IncludeXmlComments(xmlPath);
            });
        }

这样,我们所有时间格式的输入输出,都统一为“yyyy-MM-dd HH:mm:ss”,我们用已有的WeatherForecastController控制器试试,发现时间结果都变了

指定标准接口和路由

查询请求

查询通用类库说明

QueryResponse 用来存放查询返回结果基类

QueryRequest 用来存放查询请求基类

查询路由指定”XXXQuery/[controller]/[action]”,如下:

[Route(“BasicQuery/[controller]/[action]”)]

创建一个新的查询控制器,举个例子

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Service;
using MYun.MY.Contract.Data.Order;

namespace MYun.BPC.WebAPI.Basic.Query.Controllers
{
    [ApiController]
    [Route("BasicQuery/[controller]/[action]")]
    public class BasicController : ControllerBase
    {
        /// <summary>
        /// 获取订单详情
        /// </summary>
        /// <param name="param"></param>
        /// <returns></returns>
        [HttpPost]
        public QueryResponse<CPSOrder> GetOrderBySysNo(QueryRequest<CPSOrderQuerySysNo> param)
        {
            QueryResponse<CPSOrder> result = new QueryResponse<CPSOrder>();
            #region 检测param
            if (param == null)
            {
                result.HasError = true;
                result.Fault = new MessageFault() { ErrorCode = 0, ErrorDescription = "GetLJChangeOrderBySysNo接口参数JSON对象不能为空" };
                return result;
            }
            #endregion
            try
            {
                #region 路由到Service
                QueryResponse<CPSOrder> temp = new QueryResponse<CPSOrder>() { Body=new CPSOrder() { OrderSysNo=111, CreateTime=DateTime.Now } };
                if (temp.HasError)
                {
                    result.HasError = true;
                    result.Fault = temp.Fault;
                }
                else
                {
                    result.Paging = temp.Paging;
                    result.Body = temp.Body;
                }
                return result;
                #endregion
            }
            catch (Exception ex)
            {
                result.HasError = true;
                result.Fault = new MessageFault() { ErrorCode = 0, ErrorDescription = ex.Message };
                return result;
            }
        }

        /// <summary>
        /// 获取订单列表
        /// </summary>
        /// <param name="param"></param>
        /// <returns></returns>
        [HttpPost]
        public QueryResponse<List<CPSOrder>> GetOrderList(QueryRequest<CPSOrderQuery> param)
        {
            QueryResponse<List<CPSOrder>> result = new QueryResponse<List<CPSOrder>>();
            #region 检测param
            if (param == null)
            {
                result.HasError = true;
                result.Fault = new MessageFault() { ErrorCode = 0, ErrorDescription = "GetLJOrderList接口参数JSON对象不能为空" };
                return result;
            }
            #endregion
            try
            {
                #region 路由到Service
                QueryResponse<List<CPSOrder>> temp = new QueryResponse<List<CPSOrder>>() { Body =new List<CPSOrder> { new CPSOrder() { OrderSysNo = 222, CreateTime = DateTime.Now } } };
                if (temp.HasError)
                {
                    result.HasError = true;
                    result.Fault = temp.Fault;
                }
                else
                {
                    result.Paging = temp.Paging;
                    result.Body = temp.Body;
                }
                return result;
                #endregion
            }
            catch (Exception ex)
            {
                result.HasError = true;
                result.Fault = new MessageFault() { ErrorCode = 0, ErrorDescription = ex.Message };
                return result;
            }
        }

    }
}

操作请求

操作通用类库说明

ActionResponse 用来存放操作返回结果基类

ActionRequest 用来存放操作请求基类

操作路由指定”XXXAction/[controller]/[action]”,如下:

[Route(“BasicAction/[controller]/[action]”)]

创建一个新的操作控制器,举个例子

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Service;
using MYun.MY.Contract.Data.Order;

namespace MYun.BPC.WebAPI.Basic.Action.Controllers
{
    [ApiController]
    [Route("BasicAction/[controller]/[action]")]
    public class BasicController : ControllerBase
    {
        /// <summary>
        /// 新增订单
        /// </summary>
        /// <param name="param"></param>
        /// <returns></returns>
        [HttpPost]
        public ActionResponse<int> AddOrderCPS(ActionRequest<OrderCPSAdd> param)
        {
            ActionResponse<int> result = new ActionResponse<int>();
            #region 检测param
            if (param == null)
            {
                result.HasError = true;
                result.Fault = new MessageFault() { ErrorCode = 0, ErrorDescription = "AddOrderRMA接口参数JSON对象不能为空" };
                return result;
            }
            #endregion
            try
            {
                #region 路由到Service
                return new ActionResponse<int> { Body=1 };
                #endregion
            }
            catch (Exception ex)
            {
                result.HasError = true;
                result.Fault = new MessageFault() { ErrorCode = 0, ErrorDescription = ex.Message };
                return result;
            }
        }

        /// <summary>
        /// 编辑订单
        /// </summary>
        /// <param name="param"></param>
        /// <returns></returns>
        [HttpPost]
        public ActionResponse EditOrderCPS(ActionRequest<OrderCPSEdit> param)
        {
            ActionResponse result = new ActionResponse();
            #region 检测param
            if (param == null)
            {
                result.HasError = true;
                result.Fault = new MessageFault() { ErrorCode = 0, ErrorDescription = "EditOrderRMA接口参数JSON对象不能为空" };
                return result;
            }
            #endregion
            try
            {
                #region 路由到Service
                return new ActionResponse();
                #endregion
            }
            catch (Exception ex)
            {
                result.HasError = true;
                result.Fault = new MessageFault() { ErrorCode = 0, ErrorDescription = ex.Message };
                return result;
            }
        }
    }
}

创建业务处理层

新增一个02_Server文件夹用来处理业务逻辑。然后在文件夹下,右键->新建项目

MYun.BPC.WebAPI.XXX.Server文件结构如下

Service 用于承接接口请求

ActionBP用于处理操作逻辑

QueryBP用于处理查询逻辑

Help用于存放帮助类

Transform用于存放翻译类

添加NuGet包,包内有EntityFrameworkCore等相关的引用,以及Newtonsoft.Json,EntityFrameworkCore等知识点,参考《.Net Core + EntityFramework连接数据库》

最后一步,添加相关项目程序集

然后将MYun.BPC.WebAPI.XXX.Query,MYun.BPC.WebAPI.XXX.Action这2个站点,引用MYun.BPC.WebAPI.XXX.Server这个类库

C#调用SmtpClient发送Hotmail邮件

以下是C#通过引用System.Net.Mail的SmtpClient,调用微软的Hotmail的Smtp服务器,去发送一封邮件

public static void SendEmail(string Email)
{
    var Emails = Email.Split(',').ToList(); //收件人,多个收件人用“,”来分割
    MailMessage mail = new MailMessage();
    mail.From = new MailAddress("xxxxx@hotmail.com");  //发送人,用自己的hotmail邮箱
    Emails.ForEach(i =>
    {
        mail.To.Add(i);
    });
    mail.Subject = "您好,这是一封测试邮件"; //邮件标题
    mail.Body =  "您好,这是一封测试邮件的具体内容"; //邮件详情 

    // 设置SMTP客户端
    using (SmtpClient client = new SmtpClient("smtp.office365.com", 587))
    {
        client.Credentials = new NetworkCredential("xxxxx@hotmail.com", "密码密码");  //发送人,自己的Hotmail用户名和密码
        client.EnableSsl = true; // 如果SMTP服务器需要安全连接,则启用

        // 发送邮件
        client.Send(mail);
    }
}

Emails:收件人,多个收件人用“,”来分割

mail.From:发送人,用自己的hotmail邮箱

SMTP服务端:用smtp.office365.com,端口587

Credentials:用的是自己的Hotmail用户名和密码

C#调用WindowsAPI模拟键盘鼠标事件

今天接到一个需求,要求开发一个定制的小工具,运行后,能自动化测试一个桌面应用,不能用按键精灵,于是,我想到了可以利用C#开发一个调用WindowsAPI的接口,来实现该需求,以下是具体步骤

首先我们先申明一个鼠标的事件类,用来标识鼠标的各个事件

    public static class Mouse
    {
        //移动鼠标 
        public const int MOUSEEVENTF_MOVE = 0x0001;
        //模拟鼠标左键按下 
        public const int MOUSEEVENTF_LEFTDOWN = 0x0002;
        //模拟鼠标左键抬起 
        public const int MOUSEEVENTF_LEFTUP = 0x0004;
        //模拟鼠标右键按下 
        public const int MOUSEEVENTF_RIGHTDOWN = 0x0008;
        //模拟鼠标右键抬起 
        public const int MOUSEEVENTF_RIGHTUP = 0x0010;
        //模拟鼠标中键按下 
        public const int MOUSEEVENTF_MIDDLEDOWN = 0x0020;
        //模拟鼠标中键抬起 
        public const int MOUSEEVENTF_MIDDLEUP = 0x0040;
        //标示是否采用绝对坐标 
        public const int MOUSEEVENTF_ABSOLUTE = 0x8000;
    }

其次,我们再申明一个键盘类,用来映射键盘的各个物理按键

    public static class Keyboard
    {
        #region bVk参数 常量定义

        public const int vbKeyLButton = 0x1;    // 鼠标左键
        public const int vbKeyRButton = 0x2;    // 鼠标右键
        public const int vbKeyCancel = 0x3;     // CANCEL 键
        public const int vbKeyMButton = 0x4;    // 鼠标中键
        public const int vbKeyBack = 0x8;       // BACKSPACE 键
        public const int vbKeyTab = 0x9;        // TAB 键
        public const int vbKeyClear = 0xC;      // CLEAR 键
        public const int vbKeyReturn = 0xD;     // ENTER 键
        public const int vbKeyShift = 0x10;     // SHIFT 键
        public const int vbKeyControl = 0x11;   // CTRL 键
        public const int vbKeyAlt = 18;         // Alt 键  (键码18)
        public const int vbKeyMenu = 0x12;      // MENU 键
        public const int vbKeyPause = 0x13;     // PAUSE 键
        public const int vbKeyCapital = 0x14;   // CAPS LOCK 键
        public const int vbKeyEscape = 0x1B;    // ESC 键
        public const int vbKeySpace = 0x20;     // SPACEBAR 键
        public const int vbKeyPageUp = 0x21;    // PAGE UP 键
        public const int vbKeyEnd = 0x23;       // End 键
        public const int vbKeyHome = 0x24;      // HOME 键
        public const int vbKeyLeft = 0x25;      // LEFT ARROW 键
        public const int vbKeyUp = 0x26;        // UP ARROW 键
        public const int vbKeyRight = 0x27;     // RIGHT ARROW 键
        public const int vbKeyDown = 0x28;      // DOWN ARROW 键
        public const int vbKeySelect = 0x29;    // Select 键
        public const int vbKeyPrint = 0x2A;     // PRINT SCREEN 键
        public const int vbKeyExecute = 0x2B;   // EXECUTE 键
        public const int vbKeySnapshot = 0x2C;  // SNAPSHOT 键
        public const int vbKeyDelete = 0x2E;    // Delete 键
        public const int vbKeyHelp = 0x2F;      // HELP 键
        public const int vbKeyNumlock = 0x90;   // NUM LOCK 键

        //常用键 字母键A到Z
        public const int vbKeyA = 65;
        public const int vbKeyB = 66;
        public const int vbKeyC = 67;
        public const int vbKeyD = 68;
        public const int vbKeyE = 69;
        public const int vbKeyF = 70;
        public const int vbKeyG = 71;
        public const int vbKeyH = 72;
        public const int vbKeyI = 73;
        public const int vbKeyJ = 74;
        public const int vbKeyK = 75;
        public const int vbKeyL = 76;
        public const int vbKeyM = 77;
        public const int vbKeyN = 78;
        public const int vbKeyO = 79;
        public const int vbKeyP = 80;
        public const int vbKeyQ = 81;
        public const int vbKeyR = 82;
        public const int vbKeyS = 83;
        public const int vbKeyT = 84;
        public const int vbKeyU = 85;
        public const int vbKeyV = 86;
        public const int vbKeyW = 87;
        public const int vbKeyX = 88;
        public const int vbKeyY = 89;
        public const int vbKeyZ = 90;

        //数字键盘0到9
        public const int vbKey0 = 48;    // 0 键
        public const int vbKey1 = 49;    // 1 键
        public const int vbKey2 = 50;    // 2 键
        public const int vbKey3 = 51;    // 3 键
        public const int vbKey4 = 52;    // 4 键
        public const int vbKey5 = 53;    // 5 键
        public const int vbKey6 = 54;    // 6 键
        public const int vbKey7 = 55;    // 7 键
        public const int vbKey8 = 56;    // 8 键
        public const int vbKey9 = 57;    // 9 键


        public const int vbKeyNumpad0 = 0x60;    //0 键
        public const int vbKeyNumpad1 = 0x61;    //1 键
        public const int vbKeyNumpad2 = 0x62;    //2 键
        public const int vbKeyNumpad3 = 0x63;    //3 键
        public const int vbKeyNumpad4 = 0x64;    //4 键
        public const int vbKeyNumpad5 = 0x65;    //5 键
        public const int vbKeyNumpad6 = 0x66;    //6 键
        public const int vbKeyNumpad7 = 0x67;    //7 键
        public const int vbKeyNumpad8 = 0x68;    //8 键
        public const int vbKeyNumpad9 = 0x69;    //9 键
        public const int vbKeyMultiply = 0x6A;   // MULTIPLICATIONSIGN(*)键
        public const int vbKeyAdd = 0x6B;        // PLUS SIGN(+) 键
        public const int vbKeySeparator = 0x6C;  // ENTER 键
        public const int vbKeySubtract = 0x6D;   // MINUS SIGN(-) 键
        public const int vbKeyDecimal = 0x6E;    // DECIMAL POINT(.) 键
        public const int vbKeyDivide = 0x6F;     // DIVISION SIGN(/) 键


        //F1到F12按键
        public const int vbKeyF1 = 0x70;   //F1 键
        public const int vbKeyF2 = 0x71;   //F2 键
        public const int vbKeyF3 = 0x72;   //F3 键
        public const int vbKeyF4 = 0x73;   //F4 键
        public const int vbKeyF5 = 0x74;   //F5 键
        public const int vbKeyF6 = 0x75;   //F6 键
        public const int vbKeyF7 = 0x76;   //F7 键
        public const int vbKeyF8 = 0x77;   //F8 键
        public const int vbKeyF9 = 0x78;   //F9 键
        public const int vbKeyF10 = 0x79;  //F10 键
        public const int vbKeyF11 = 0x7A;  //F11 键
        public const int vbKeyF12 = 0x7B;  //F12 键

        #endregion
    }

申明窗体坐标位置类

    [StructLayout(LayoutKind.Sequential)]
    public struct RECT
    {
        public int Left; //最左坐标
        public int Top; //最上坐标
        public int Right; //最右坐标
        public int Bottom; //最下坐标
    }

然后,申明Action类,用来引入WINDOWSAPI的接口

    public static class Action
    {
        public static int WM_CHAR = 0x102;   //键入事件
        public static int WM_CLICK = 0x00F5; //点击事件
        public static int WM_CLOSE = 0x0010; //窗体关闭事件

        public static int CB_SETCURSEL = 0x014E; //下拉框搜索
        public static int CB_FINDSTRINGEXACT = 0x0158; //下拉框选择

        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool GetWindowRect(IntPtr hWnd, ref RECT lpRect);

        [DllImport("User32.dll", EntryPoint = "SendMessage")]
        public static extern int SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, string lParam);

        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam);

        [DllImport("user32.dll")]
        public static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter,
            string lpszClass, string lpszWindow);

        [DllImport("user32.dll", SetLastError = true)]
        public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);

        [DllImport("user32.dll")]
        public static extern int AnyPopup();

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);

        [DllImport("user32.dll")]
        public static extern int EnumThreadWindows(IntPtr dwThreadId, CallBack lpfn, int lParam);

        [DllImport("user32.dll")]
        public static extern int EnumChildWindows(IntPtr hWndParent, CallBack lpfn, int lParam);

        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        public static extern IntPtr PostMessage(IntPtr hwnd, int wMsg, int wParam, int lParam);

        [DllImport("user32.dll", CharSet = CharSet.Unicode)]
        public static extern IntPtr SendMessageA(IntPtr hwnd, int wMsg, int wParam, int lParam);

        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        public static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);

        [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        public static extern int GetWindowTextLength(IntPtr hWnd);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)]
        public static extern IntPtr GetParent(IntPtr hWnd);

        [DllImport("user32.dll")]
        public static extern bool SetForegroundWindow(IntPtr hWnd);

        [DllImport("user32.dll")]
        public static extern int mouse_event(int dwFlags, int dx, int dy, int cButtons, int dwExtraInfo);
        [DllImport("user32.dll")]
        public static extern bool SetCursorPos(int X, int Y);
        [DllImport("user32.dll")]
        public static extern void keybd_event(int bVk, int bScan, int dwFlags, int dwExtraInfo);

        public delegate bool CallBack(IntPtr hwnd, int lParam);
    }

接下来,我们就可以封装方法使用这些接口,在此,我封装了一个MessageHelp方法,里面有一些常用的事件,比如可按照如下思想设计“按照窗体名称定位句柄”“定位到句柄后给句柄设置文字”;“把鼠标移动指定坐标位置并点击”“找到指定文字的按钮并点击”

   public static class MessageHelp
    {
        /// <summary>
        /// windows像句柄设置文字
        /// </summary>
        /// <param name="hand"></param>
        /// <param name="ch"></param>
        /// <param name="SleepTime"></param>
        public static void SendChar(IntPtr hand, char ch, int SleepTime = 10)
        {
            BaseHelp.Action.PostMessage(hand, BaseHelp.Action.WM_CHAR, ch, 0);
            Thread.Sleep(SleepTime);
        }

        /// <summary>
        /// windows关闭句柄窗体
        /// </summary>
        /// <param name="hwnd"></param>
        /// <param name="SleepTime"></param>
        public static void CloseWindow(IntPtr hwnd, int SleepTime = 100)
        {
            BaseHelp.Action.PostMessage(hwnd, BaseHelp.Action.WM_CLOSE, 0, 0);
            Thread.Sleep(SleepTime);
        }

        /// <summary>
        /// 移动鼠标位置(相对于句柄窗体位置)
        /// </summary>
        /// <param name="X"></param>
        /// <param name="Y"></param>
        /// <param name="WindowRect"></param>
        /// <param name="SleepTime"></param>
        public static void SetCursorPos(int X,int Y, BaseHelp.RECT WindowRect, int SleepTime = 10)
        {
            BaseHelp.Action.SetCursorPos(X + (WindowRect.Left), Y + (WindowRect.Top));
            Thread.Sleep(SleepTime);
        }

        /// <summary>
        /// 鼠标左键单击
        /// </summary>
        /// <param name="SleepTime"></param>
        public static void MouseLEFTDown(int SleepTime = 10)
        {
            //模拟鼠标按下操作
            BaseHelp.Action.mouse_event((int)(BaseHelp.Mouse.MOUSEEVENTF_LEFTDOWN | BaseHelp.Mouse.MOUSEEVENTF_LEFTDOWN), 0, 0, 0, 0);
            Thread.Sleep(SleepTime);
        }

        /// <summary>
        /// 查找窗体里句柄
        /// </summary>
        /// <param name="lpClassName"></param>
        /// <param name="lpWindowName"></param>
        /// <returns></returns>
        public static IntPtr FindWindow(string lpClassName, string lpWindowName)
        {
            return BaseHelp.Action.FindWindow(lpClassName, lpWindowName);
        }

        /// <summary>
        /// 查找窗体里的控件句柄
        /// </summary>
        /// <param name="hwndParent"></param>
        /// <param name="hwndChildAfter"></param>
        /// <param name="lpszClass"></param>
        /// <param name="lpszWindow"></param>
        /// <returns></returns>
        public static IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter,string lpszClass, string lpszWindow)
        {
            return BaseHelp.Action.FindWindowEx(hwndParent, hwndChildAfter, lpszClass, lpszWindow);
        }

        /// <summary>
        /// 遍历获取Windows的控件子句柄
        /// </summary>
        /// <param name="hWndParent"></param>
        /// <param name="lpfn"></param>
        /// <param name="lParam"></param>
        /// <returns></returns>
        public static int EnumChildWindows(IntPtr hWndParent, CallBack lpfn, int lParam)
        {
            return BaseHelp.Action.EnumChildWindows(hWndParent, lpfn, lParam);
        }

        /// <summary>
        /// 获取Windows窗体坐标
        /// </summary>
        /// <param name="hWnd"></param>
        /// <param name="lpRect"></param>
        /// <returns></returns>
        public static bool GetWindowRect(IntPtr hWnd, ref RECT lpRect)
        {
            return BaseHelp.Action.GetWindowRect(hWnd, ref lpRect);
        }

        /// <summary>
        /// 发送异步Windows消息
        /// </summary>
        /// <param name="hwnd"></param>
        /// <param name="wMsg"></param>
        /// <param name="wParam"></param>
        /// <param name="lParam"></param>
        /// <param name="SleepTime"></param>
        /// <returns></returns>
        public static IntPtr PostMessage(IntPtr hwnd, int wMsg, int wParam, int lParam)
        {
            return BaseHelp.Action.PostMessage(hwnd, wMsg, wParam, lParam);
        }

        /// <summary>
        /// 发送同步Windows消息
        /// </summary>
        /// <param name="hWnd"></param>
        /// <param name="Msg"></param>
        /// <param name="wParam"></param>
        /// <param name="lParam"></param>
        /// <returns></returns>
        public static IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam)
        {
            return BaseHelp.Action.SendMessage(hWnd, Msg, wParam, lParam);
        }

        /// <summary>
        /// 调用键盘事件
        /// </summary>
        /// <param name="bVk"></param>
        /// <param name="bScan"></param>
        /// <param name="dwFlags"></param>
        /// <param name="dwExtraInfo"></param>
        /// <param name="SleepTime"></param>
        public static void keybd_event(int bVk, int bScan, int dwFlags, int dwExtraInfo, int SleepTime = 10)
        {
            BaseHelp.Action.keybd_event(bVk, bScan, dwFlags, dwExtraInfo);
            Thread.Sleep(SleepTime);
        }

        /// <summary>
        /// 切换前台窗体
        /// </summary>
        /// <param name="hWnd"></param>
        /// <returns></returns>
        public static bool SetForegroundWindow(IntPtr hWnd)
        {
            return BaseHelp.Action.SetForegroundWindow(hWnd);
        }

        /// <summary>
        /// 查找定位窗体句柄
        /// </summary>
        /// <param name="WindowsIntPtr"></param>
        /// <param name="Action"></param>
        /// <returns></returns>
        public static List<MessageHelp.MatchIntPtr> FindControl(IntPtr WindowsIntPtr, MessageHelp.ActionDetail Action)
        {
            List<MessageHelp.MatchIntPtr> result = new List<MessageHelp.MatchIntPtr>();
            if(!string.IsNullOrEmpty(Action.SpyID))
            {
                return FindControlBySpyID(WindowsIntPtr, Action);
            }
            else if(Action.Points!=null && Action.Points.Count>0)
            {
                return FindControlByPoint(WindowsIntPtr, Action);
            }
            else
            {
                return FindControlByName(WindowsIntPtr, Action);
            }
        }

        /// <summary>
        /// 根据SPYID,查找窗体
        /// </summary>
        /// <param name="WindowsIntPtr"></param>
        /// <param name="SpyID"></param>
        /// <returns></returns>
        private static List<MessageHelp.MatchIntPtr> FindControlBySpyID(IntPtr WindowsIntPtr, MessageHelp.ActionDetail Action)
        {
            List<MessageHelp.MatchIntPtr> result = new List<MessageHelp.MatchIntPtr>();
            MessageHelp.EnumChildWindows(WindowsIntPtr, new BaseHelp.Action.CallBack(delegate (IntPtr hwnd, int lParam)
            {
                StringBuilder title = new StringBuilder(256);
                GetClassName(hwnd, title, title.Capacity);
                if (title.ToString() == Action.SpyID)
                {
                    result.Add(new MessageHelp.MatchIntPtr()
                    {
                        intPtr = hwnd,
                    });
                }
                return true;
            }), 0);
            return result;
        }

        /// <summary>
        /// 根据坐标点,查找窗体的WINFORM控件句柄
        /// </summary>
        /// <param name="WindowsIntPtr"></param>
        /// <param name="Action"></param>
        /// <returns></returns>
        private static List<MessageHelp.MatchIntPtr> FindControlByPoint(IntPtr WindowsIntPtr, MessageHelp.ActionDetail Action)
        {
            List<MessageHelp.MatchIntPtr> result = new List<MessageHelp.MatchIntPtr>();
            RECT WindowsRect = new RECT();
            #region 获取窗体的绝对坐标
            MessageHelp.GetWindowRect(WindowsIntPtr, ref WindowsRect);
            #endregion
            MessageHelp.EnumChildWindows(WindowsIntPtr, new BaseHelp.Action.CallBack(delegate (IntPtr hwnd, int lParam)
            {
                RECT clientRect = new RECT();

                #region 获取控件的绝对坐标
                MessageHelp.GetWindowRect(hwnd, ref clientRect);
                #endregion

                #region 计算相对坐标
                int Left = clientRect.Left - WindowsRect.Left;
                int Right = Left + (clientRect.Right - clientRect.Left);
                int Top = clientRect.Top - WindowsRect.Top;
                int Bottom = Top + (clientRect.Bottom - clientRect.Top);
                #endregion
                foreach (var Point in Action.Points)
                {
                    if (Top <= Point.PointY && Bottom >= Point.PointY && Left <= Point.PointX && Right >= Point.PointX)
                    {
                        result.Add(new MessageHelp.MatchIntPtr()
                        {
                            intPtr = hwnd,
                        });
                    }
                }
                return true;
            }), 0);
            return result;
        }

        /// <summary>
        /// 根据标题名称,查找窗体的WINFORM控件句柄
        /// </summary>
        /// <param name="WindowsIntPtr"></param>
        /// <param name="Action"></param>
        /// <returns></returns>
        private static List<MessageHelp.MatchIntPtr> FindControlByName(IntPtr WindowsIntPtr, MessageHelp.ActionDetail Action)
        {
            List<MessageHelp.MatchIntPtr> result = new List<MessageHelp.MatchIntPtr>();
            MessageHelp.EnumChildWindows(WindowsIntPtr, new BaseHelp.Action.CallBack(delegate (IntPtr hwnd, int lParam)
            {
                StringBuilder title = new StringBuilder();
                GetWindowText(hwnd, title, 200);
                if (title.ToString() == Action.CharMsg)
                {
                    result.Add(new MessageHelp.MatchIntPtr()
                    {
                        intPtr = hwnd,
                    });
                }
                return true;
            }), 0);
            return result;
        }

        /// <summary>
        /// 切图保存
        /// </summary>
        /// <param name="folderPath"></param>
        /// <param name="fileName"></param>
        public static void CaptureScreenAndSave(string folderPath, string fileName)
        {
            // 确保文件夹存在
            Directory.CreateDirectory(folderPath);

            // 创建完整的文件路径
            string filePath = Path.Combine(folderPath, fileName);

            // 获取整个屏幕的尺寸
            Rectangle bounds = Screen.PrimaryScreen.Bounds;

            // 创建一个bitmap
            using (Bitmap bmp = new Bitmap(bounds.Width, bounds.Height))
            {
                // 创建一个画布
                using (Graphics g = Graphics.FromImage(bmp))
                {
                    // 截取屏幕
                    g.CopyFromScreen(bounds.Location, Point.Empty, bounds.Size);
                }

                // 保存截图到文件
                bmp.Save(filePath, ImageFormat.Png);
            }
        }

        public class ActionDetail
        {
            public string Form { get; set; }

            public List<ActionPoint> Points { get; set; }

            public string CharMsg { get; set; }

            public string SpyID { get; set; }
        }

        public class ActionPoint
        {
            public int PointX { get; set; }

            public int PointY { get; set; }
        }

        public class MatchIntPtr
        {
            public IntPtr intPtr { get; set; }
        }
    }

其中SpyID的获取方法,可以使用VS自带的Spy++工具来进行选点

注意:以下部分是伪代码,旨意再说明上述方法的使用,其中Actions对象是一系列准备待操作鼠标和键盘事件的集合,将依次执行,Action.Job_Type是定义的枚举事件的方法

foreach (var Action in Actions)
{
    IntPtr hwnd_Form = MessageHelp.FindWindow(null, Action.Action.Form);
    if (hwnd_Form == IntPtr.Zero && Action.Job_Type != "009" && Action.Job_Type != "007" && Action.Job_Type != "006")
    {
        continue;
    }
    if (Action.Job_Type == "001") //文字键入
    {
        List<MessageHelp.MatchIntPtr> MatchsCos = new List<MessageHelp.MatchIntPtr>();
        MatchsCos = MessageHelp.FindControl(hwnd_Form, Action.Action);
        foreach (var MatchsCo in MatchsCos)
        {
            char[] UserChar = Action.Action.CharMsg.ToCharArray();
            foreach (char ch in UserChar)
            {
                MessageHelp.SendChar(MatchsCo.intPtr, ch);
            }
        }
    }
    else if (Action.Job_Type == "002") //鼠标移动
    {
        RECT rect = new RECT();
        MessageHelp.GetWindowRect(hwnd_Form, ref rect);
        MessageHelp.SetCursorPos(Action.Action.Points[0].PointX, Action.Action.Points[0].PointY, rect);
    }
    else if (Action.Job_Type == "003")//鼠标左击
    {
        MessageHelp.MouseLEFTDown();
    }
    else if (Action.Job_Type == "004")//按钮点击
    {
        var MatchsCos = MessageHelp.FindControl(hwnd_Form, Action.Action);
        foreach (var MatchsCo in MatchsCos)
        {
            MessageHelp.PostMessage(MatchsCo.intPtr, BaseHelp.Action.WM_CLICK, (int)hwnd_Form, 0);
        }
    }
    else if (Action.Job_Type == "005")//下拉选择
    {
        var MatchsCos = MessageHelp.FindControl(hwnd_Form, Action.Action);
        foreach (var MatchsCo in MatchsCos)
        {
            int index = (int)MessageHelp.SendMessage(MatchsCo.intPtr, BaseHelp.Action.CB_FINDSTRINGEXACT, IntPtr.Zero, Marshal.StringToHGlobalAuto(Action.Action.CharMsg));
            MessageHelp.SendMessage(MatchsCo.intPtr, BaseHelp.Action.CB_SETCURSEL, (IntPtr)index, (IntPtr)0);
        }
    }
    else if (Action.Job_Type == "006")//按下回车
    {
        MessageHelp.keybd_event(BaseHelp.Keyboard.vbKeyReturn, 0, 0, 0);
    }
    else if (Action.Job_Type == "007")//按下ESC
    {
        MessageHelp.keybd_event(BaseHelp.Keyboard.vbKeyEscape, 0, 0, 0);
    }
    else if(Action.Job_Type== "011")//按下TAB
    {
        MessageHelp.keybd_event(BaseHelp.Keyboard.vbKeyTab, 0, 0, 0);
    }
    else if (Action.Job_Type == "008")//关闭窗体
    {
        MessageHelp.SendMessage(hwnd_Form, BaseHelp.Action.WM_CLOSE, (IntPtr)0, (IntPtr)0);
    }
    else if (Action.Job_Type == "009")//截图保存
    {
        var Now = DateTime.Now;
        string currentDirectory = Application.StartupPath;
        MessageHelp.CaptureScreenAndSave($@"{currentDirectory}/{DataSets.Rows[0]["Job_Name"].ToString()}/{Now.ToString("yyyyMMdd")}", $@"{DataSets.Rows[0]["Job_Title"].ToString()}{Now.ToString("yyyyMMddHHmmssfff")}.png");
    }
    else if (Action.Job_Type == "010")//等待时间
    {
        Thread.Sleep(Convert.ToInt32(Action.Action.CharMsg));
    }
    Thread.Sleep(2000);
}

Docker部署.Net Core 3.1程序(超详细)

首先,我们把编译好的.Net Core 3.1程序上传到宿主机的特地目录,如下图,确保你的程序路径在netcoreapp3.1/publish/文件夹中,以下是以我的程序Web.dll为例

然后,在netcoreapp3.1文件同级,新建Dockerfile文件,并且为了防止DOCK镜像中可能不存在字体,我们在同级别目录放一个字体文件 simsun.ttc

Dockerfile的内容如下,以下我以运行我的Web.dll程序为例子,为了调试方便,看到错误信息,ASPNETCORE_ENVIRONMENT暂时设置为Development,容器内端口设置为80,容器内的工作目录设置为/app

# 使用官方.NET Core运行时镜像作为基础镜像
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1
 
# 设置工作目录
WORKDIR /app
 
# 复制项目发布文件到容器中的工作目录
COPY ./netcoreapp3.1/publish/ .

COPY simsun.ttc /usr/share/fonts/
 
# 设置环境变量,端口
ENV ASPNETCORE_ENVIRONMENT=Development
ENV ASPNETCORE_URLS=http://+:80
 
# 当容器启动时,运行应用程序
ENTRYPOINT ["dotnet", "Web.dll"]

接下来,在控制台,定位到指定目录,用以下命令生成镜像,然后就会新增一个mathapp的Image镜像

sudo docker build -t mathapp .

此时,我们可以试着用这个镜像部署一个容器mathapp,并且把宿主机的8080端口,映射到容器80

sudo docker run --name mathapp -d -p 8080:80 mathapp

这时,我们就可以在游览器地址栏输入:http://服务器IP:8080 来访问自己的.Net Core程序了

接下来,我们为了后续更新方便,也可以试着在宿主机做个文件映射,首先,把容器内刚刚的/app工作目录的文件,复制到宿主机

sudo docker cp mathapp:/app /home/ec2-user/www

此时,我们看到,宿主机的对应目录中多了一个app文件,里面就是我们容器的工作目录文件

此时,我们可以删除之前创建的测试容器,创建全新容器,然后把这个目录挂载到新容器上

sudo docker stop mathapp
sudo docker rm mathapp
sudo docker run -d -p 8080:80 --name mathapp --restart=always -v /home/ec2-user/www/app:/app mathapp

接下来,后续更新只要更新宿主机中的文件,然后重启对应的docker容器即可