NET Core Web APIの基礎を学ぶ

NET Core Web APIの基礎を学ぶ

この記事では、. NET Core 3.1を通じてWeb APIの基本を共有し、他のアップデートも同様です。

最后更新 2022/05/04 14:43
白云任去留
预计阅读 25 分钟
分类
ASP.NET Core
标签
.NET C# ASP.NET Core Web API

この記事では、. NET Core 3.1を通じてWeb APIの基本を共有し、他のアップデートも同様です。

I.序文

近年のフロントエンド分離、マイクロサービスなどのモデルの台頭に伴い、. NET Coreも本格化しているようで、16 年の最初のリリースから19年末の3.1 LTSリリースまで、. NET 5のリリースまで、. NET Coreはすべての方法で変更され、デプロイメントと開発ツールの両方でクロスプラットフォームアプリケーションをサポートしています。常に. NET Coreに注意を払っていましたが、実用的なアプリケーションはあまりなく、いくつかの学習と理解の後、共有しました。この記事では、主に. NET Core Web APIを例に、. NET Coreの基本的なアプリケーションと注意事項について説明し、Web APIを介してインターフェースアプリケーションを構築したい開発者のために、システムの概要と理解を提供すると同時に、より多くの. NET Core開発者と交流し、エラーを探索し、知識の理解を強化し、より多くの人々を助けることができるはずです。この記事は、基本的な実用的な操作に近く、概念や基本的なステップの一部はもはや詳述されず、省略された場合は、また、慎重になりたいです。

SwaggerのWeb APIデバッグ

開発環境:Visual Studio 2018

バックエンドとバックエンドのインターフェイスドキュメントと実際の矛盾、ドキュメントの維持と更新に時間と労力を要する問題を解決するために、swaggerが登場し、インターフェイステストの問題も解決しました。言うことはあまりなく、アプリケーションの手順を直接説明します。

  1. ASP.NET Core Web APIアプリケーションを作成します。バージョンを選択します。. ASP.NET Core 3.1;
  2. NuGet経由でパッケージをインストールする:Swashbuckle.AspNetCore、現在のサンプルバージョン5.5.0;
  3. StartupクラスのConfigureServicesメソッド内に次のインジェクションを追加します。
services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo
    {
        Title = "My API",
        Version = "v1",
        Description = "API文档描述",
        Contact = new OpenApiContact
        {
            Email = "5007032@qq.com",
            Name = "测试项目",
            //Url = new Uri("http://t.abc.com/")
        },
        License = new OpenApiLicense
        {
            Name = "BROOKE许可证",
            //Url = new Uri("http://t.abc.com/")
        }
    });

});

StartupクラスのConfigureメソッドには、次のコードが追加されます。

//配置Swagger
            app.UseSwagger();
            app.UseSwaggerUI(c =>
            {
                c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
                c.RoutePrefix = "api";// 如果设为空,访问路径就是根域名/index.html,设置为空,表示直接在根域名访问;想换一个路径,直接写名字即可,比如直接写c.RoutePrefix = "swagger"; 则访问路径为 根域名/swagger/index.html

            });

Ctrl+F5を押しながらブラウズを開始し、上記の構成でhttp//localhost***/api/index.htmlに変更すると、Swaggerページが表示されます。

しかし、ここではまだ終わっていません。関連するインターフェイスのコメントは、XMLファイルを構成することでコードを調整し続け、追加コードは太字部分を参照してください。

services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo
    {
        Title = "My API",
        Version = "v1",
        Description = "API文档描述",
        Contact = new OpenApiContact
        {
            Email = "5007032@qq.com",
            Name = "测试项目",
            //Url = new Uri("http://t.abc.com/")
        },
        License = new OpenApiLicense
        {
            Name = "BROOKE许可证",
            //Url = new Uri("http://t.abc.com/")
        }
    });

    var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
    c.IncludeXmlComments(xmlPath);
});

上記のコードは、リフレクションを介してWeb APIプロジェクトと一致するXMLファイル名を生成し、AppContext.BaseDirectory属性はXMLファイルのパスを構築するために使用され、OpenApiInfo 内の構成パラメータがドキュメントに使用されるいくつかの説明については、ここではあまり説明しない。

次に、Web APIプロジェクト、プロパティ、ビルドを右クリックし、XMLドキュメントへの出力パスを設定し、不要なXMLコメントを削除する警告アラート(159 1追加):

このようにして、クラスメソッドプロパティなどの関連コードにスラッシュ(///)でコメントを追加した後、Swaggerページをリフレッシュするとコメントの説明が表示されます。

XMLファイルをデバッグディレクトリとして出力したくない場合、例えばプロジェクトルートディレクトリに配置したい場合(ただし、ディスクの絶対パスに変更しないでください)、関連するコードを以下のように調整し、xmlファイルの名前を変更することができます。

var basePath = Path.GetDirectoryName(typeof(Program).Assembly.Location);//获取应用程序所在目录
var xmlPath = Path.Combine(basePath, "CoreAPI_Demo.xml");
c.IncludeXmlComments(xmlPath, true);

同時に、チューニングプロジェクトによって生成されるXMLドキュメントファイルパスは、.\ CoreAPI_Demo\CoreAPI_Demo.xml

** 関連インターフェイスを非表示 **

Swaggerに公開したくないインターフェイスの場合は、関連するControllerまたはActionヘッダーに[ApiExplorerSettings IgnoreApi = true]を追加します。

** システムのデフォルト出力パスを調整する **

プロジェクトが開始された後、デフォルトは独自のweatherforecastにアクセスします。他のパスに調整したい場合は、Swaggerドキュメントに直接アクセスした後、PropertiesディレクトリのlaunchSettings.jsonファイルを調整し、launchUrl値をapi(前述のRoutePrefix値の設定)に変更します。

{
  "$schema": "http://json.schemastore.org/launchsettings.json",
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:7864",
      "sslPort": 0
    }
  },
  "profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "launchUrl": "api",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "CoreApi_Demo": {
      "commandName": "Project",
      "launchBrowser": true,
      "launchUrl": "api",
      "applicationUrl": "http://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

III.プロファイル

appsettings.jsonファイルを読み込む場合、もちろん、他の名前の.jsonファイルも定義して、同じように読み込みます。例を簡単にするために、appsettings.jsonファイルの内容を次のように定義します。

{
  "ConnString": "Data Source=(local);Initial Catalog=Demo;Persist Security Info=True;User ID=DemoUser;Password=123456;MultipleActiveResultSets=True;",
  "ConnectionStrings": {
    "MySQLConnection": "server=127.0.0.1;database=mydemo;uid=root;pwd=123456;charset=utf8;SslMode=None;"
  },
  "SystemConfig": {
    "UploadFile": "/Files",
    "Domain": "http://localhost:7864"
  },
  "JwtTokenConfig": {
    "Secret": "fcbfc8df1ee52ba127ab",
    "Issuer": "abc.com",
    "Audience": "Brooke.WebApi",
    "AccessExpiration": 30,
    "RefreshExpiration": 60
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}
  1. プロファイルの基本読み取り
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();

        //读取方式一
        var ConnString = Configuration["ConnString"];
        var MySQLConnection = Configuration.GetSection("ConnectionStrings")["MySQLConnection"];
        var UploadPath = Configuration.GetSection("SystemConfig")["UploadPath"];
        var LogDefault = Configuration.GetSection("Logging").GetSection("LogLevel")["Default"];

        //读取方式二
        var ConnString2 = Configuration["ConnString"];
        var MySQLConnection2 = Configuration["ConnectionStrings:MySQLConnection"];
        var UploadPath2 = Configuration["SystemConfig:UploadPath"];
        var LogDefault2 = Configuration["Logging:LogLevel:Default"];

    }

}

設定情報を読み込む2つの方法を紹介しました。コントローラ内で使用する場合は、同様にインジェクションを実行して呼び出します。

public class ValuesController : ControllerBase
{
    private IConfiguration _configuration;

    public ValuesController(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    // GET: api/<ValuesController>
    [HttpGet]
    public IEnumerable<string> Get()
    {
        var ConnString = _configuration["ConnString"];
        var MySQLConnection = _configuration.GetSection("ConnectionStrings")["MySQLConnection"];
        var UploadPath = _configuration.GetSection("SystemConfig")["UploadPath"];
        var LogDefault = _configuration.GetSection("Logging").GetSection("LogLevel")["Default"];
        return new string[] { "value1", "value2" };
    }
}
  1. プロファイルをカスタムオブジェクトに読み取る

SystemConfigノードを例にとると、クラスは次のように定義されます。

public class SystemConfig
{
    public string UploadPath { get; set; }
    public string Domain { get; set; }

}

調整コードは以下の通り。

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.Configure<SystemConfig>(Configuration.GetSection("SystemConfig"));
    }

}

コントローラ内での注入呼び出し:

[Route("api/[controller]/[action]")]
[ApiController]
public class ValuesController : ControllerBase
{
    private SystemConfig _sysConfig;
    public ValuesController(IOptions<SystemConfig> sysConfig)
    {
        _sysConfig = sysConfig.Value;
    }

    [HttpGet]
    public IEnumerable<string> GetSetting()
    {
        var UploadPath = _sysConfig.UploadPath;
        var Domain = _sysConfig.Domain;
        return new string[] { "value1", "value2" };
    }
}
  1. 静的クラスへのバインド方法読み込み

関連する静的クラスを次のように定義します。

public static class MySettings
{
    public static SystemConfig Setting { get; set; } = new SystemConfig();
}

Startupクラスコンストラクタを次のように調整します。

public Startup(IConfiguration configuration, IWebHostEnvironment env)
{
    var builder = new ConfigurationBuilder()
        .SetBasePath(env.ContentRootPath)
        .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);

    Configuration = builder.Build();
    //Configuration = configuration;

    configuration.GetSection("SystemConfig").Bind(MySettings.Setting);//绑定静态配置类
}

次に、MySetting. Setting.UploadPathなどを使用して呼び出します。

IV.ドキュメントのアップロード

バイト配列オブジェクトなどの複雑な方法を介してファイルアップロードを行う. NET FrameworkフレームワークのWeb APIと比較して、. NET Core WebAPIは大幅に変更され、コントローラコードに直接アップロードファイルを受信するための新しいIFormFileオブジェクトを定義します。

** バックエンドコード ***

[Route("api/[controller]/[action]")]
[ApiController]
public class UploadController : ControllerBase
{
    private readonly IWebHostEnvironment _env;

    public UploadController(IWebHostEnvironment env)
    {
        _env = env;
    }

    public ApiResult UploadFile(List<IFormFile> files)
    {
        ApiResult result = new ApiResult();

       //注:参数files对象去也可以通过换成: var files = Request.Form.Files;来获取

        if (files.Count <= 0)
        {
            result.Message = "上传文件不能为空";
            return result;
        }

        #region 上传

        List<string> filenames = new List<string>();

        var webRootPath = _env.WebRootPath;
        var rootFolder = MySettings.Setting.UploadPath;

        var physicalPath = $"{webRootPath}/{rootFolder}/";

        if (!Directory.Exists(physicalPath))
        {
            Directory.CreateDirectory(physicalPath);
        }

        foreach (var file in files)
        {
            var fileExtension = Path.GetExtension(file.FileName);//获取文件格式,拓展名

            var saveName = $"{rootFolder}/{Path.GetRandomFileName()}{fileExtension}";
            filenames.Add(saveName);//相对路径

            var fileName = webRootPath + saveName;

            using FileStream fs = System.IO.File.Create(fileName);
            file.CopyTo(fs);
            fs.Flush();

        }
        #endregion


        result.IsSuccess = true;
        result.Data["files"] = filenames;

        return result;
    }
}

** フロントコール ***

次に、フロントエンド経由で上記のアップロードインターフェイスを呼び出し、プロジェクトルートディレクトリにw w wrootディレクトリ(. net core webapi組み込みディレクトリ)を作成し、関連するjsパッケージを追加し、次のような新しいindex.htmlファイルを作成します。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title></title>
    <style type="text/css"></style>
    <script src="res/scripts/jquery-1.10.2.min.js"></script>
    <script src="res/scripts/jquery.form.js"></script>
    <script type="text/javascript">
      //方法1
      function AjaxUploadfile() {
        var upload = $("#files").get(0);
        var files = upload.files;
        var data = new FormData();
        for (var i = 0; i < files.length; i++) {
          data.append("files", files[i]);
        }

        //此处data的构建也可以换成:var data = new FormData(document.getElementById("myform"));

        $.ajax({
          type: "POST",
          url: "/api/upload/uploadfile",
          contentType: false,
          processData: false,
          data: data,
          success: function (result) {
            alert("success");
            $.each(result.data.files, function (i, filename) {
              $("#filePanel").append("<p>" + filename + "</p>");
            });
          },
          error: function () {
            alert("上传文件错误");
          },
        });
      }

      //方法2
      function AjaxUploadfile2() {
        $("#myform").ajaxSubmit({
          success: function (result) {
            if (result.isSuccess) {
              $.each(result.data.files, function (i, filename) {
                $("#filePanel").append("<p>" + filename + "</p>");
              });
            } else {
              alert(result.message);
            }
          },
        });
      }
    </script>
  </head>
  <body>
    <form
      id="myform"
      method="post"
      action="/api/upload/uploadfile"
      enctype="multipart/form-data"
    >
      <input type="file" id="files" name="files" multiple /> <br /><br />
      <input
        type="button"
        value="FormData Upload"
        onclick="AjaxUploadfile();"
      /><br /><br />
      <input
        type="button"
        value="ajaxSubmit Upload"
        onclick="AjaxUploadfile2();"
      /><br /><br />
      <div id="filePanel"></div>
    </form>

    <script type="text/javascript">
      $(function () {});
    </script>
  </body>
</html>

FormDataとajaxSubmitの2つの方法を構築してアップロードするには、contentTypeとprocessDataの2つのパラメータの設定に注意する必要があります。また、一度に複数のファイルをアップロードするには、multipartプロパティを設定する必要があります。

wwwrootの静的ファイルにアクセスする前に、StartupクラスのConfigureメソッドに登録する必要があります。

public void Configure(IApplicationBuilder app)
{
    app.UseStaticFiles();//用于访问wwwroot下的文件
}

プロジェクトを起動して、パスhttp//localhost***/index.htmlにアクセスして、アップロードテストを行います。成功すると、wwwrootの下のFilesディレクトリにアップロードされたファイルが表示されます。

Web APIデータリターンフォーマットの統合

** 統一リターンフォーマットの定義 **

バックエンドとバックエンドで合意されたデータ形式を使用しやすくするために、通常、成功、リターンステータス、具体的なデータなどを含む統一データリターンを定義します。説明を容易にするために、データリターンクラスを定義します。

public class ApiResult
{
    public bool IsSuccess { get; set; }
    public string Message { get; set; }
    public string Code { get; set; }
    public Dictionary<string, object> Data { get; set; } = new Dictionary<string, object>();
}

このようにして、各アクションインターフェイス操作をApiResult形式でカプセル化して返します。ProductControllerの新規サンプルを作成します。

[Produces("application/json")]
[Route("api/[controller]")]
[ApiController]
public class ProductController : ControllerBase
{
    [HttpGet]
    public ApiResult Get()
    {
        var result = new ApiResult();

        var rd = new Random();

        result.Data["dataList"] = Enumerable.Range(1, 5).Select(index => new
        {
            Name = $"商品-{index}",
            Price = rd.Next(100, 9999)
        });

        result.IsSuccess = true;
        return result;
    }
}
  • Producesデータの戻り方を定義し、各Controllerに[Produces "application/json"]を付けます。つまり、データ出力をjson 方式で行うことを示します。
  • ApiController:各コントローラにApiController IDがあることを確認します。通常、BaseControllerのようなベースクラスを定義します。これはControllerBaseから継承し、[ApiController] IDを付けます。
  • Routeルートアクセス方法。RESTfulな方法が気に入らない場合は、アクションを追加します。つまり、[Route "api/[controller]/[action]"];
  • HTTPリクエスト:前に設定したSwaggerと組み合わせると、各アクションが特定のリクエストメソッドを持つことを確認する必要があります。つまり、HttpGet、HttpPost、HttpPut、HttpDeleteのいずれかでなければなりません。通常、HttpGet、HttpPostを使用するだけで十分です。

これにより、完了したデータが統一されます。

*** 時間形式を解く **

NET Core Web APIはデフォルトでは、最初の文字が小文字のCampleという名前で返されますが、DateTime型のデータに遭遇すると、T形式の時刻を返します。T形式の時刻を解決するには、次のように時刻変換クラスを定義します。

public class DatetimeJsonConverter : JsonConverter<DateTime>
{
    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();
    }

    public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value.ToString("yyyy-MM-dd HH:mm:ss"));
    }
}

次に、StartupクラスのConfigureServices内のservice.AddControllersコードを次のように調整します。

services.AddControllers()
    .AddJsonOptions(configure =>
    {
        configure.JsonSerializerOptions.Converters.Add(new DatetimeJsonConverter());
    });

VI.モデル検証

モデル検証はASP.NET MVCに存在し、基本的に一貫した方法で使用されます。必須項目、データ形式、文字長、範囲など、インターフェイスに送信されたデータのパラメータ検証を指します。一般的には、POSTによって送信されたオブジェクトを受信するエンティティークラスとして定義します。例えば、登録クラスを定義します。

public class RegisterEntity
{
    /// <summary>
    /// 手机号
    /// </summary>
    [Display(Name = "手机号")]
    [Required(ErrorMessage = "{0}不能为空")]
    [StringLength(11, ErrorMessage = "{0}最多{1}个字符")]
    public string Mobile { get; set; }

    /// <summary>
    /// 验证码
    /// </summary>
    [Display(Name = "验证码")]
    [Required(ErrorMessage = "{0}不能为空")]
    [StringLength(6, ErrorMessage = "{0}最多{1}个字符")]
    public string Code { get; set; }

    /// <summary>
    /// 密码
    /// </summary>
    [Display(Name = "密码")]
    [Required(ErrorMessage = "{0}不能为空")]
    [StringLength(16, ErrorMessage = "{0}最多{1}个字符")]
    public string Pwd { get; set; }
}

表示識別プロンプトフィールドの名前、Requiredは必須を意味し、StringLengthはフィールドの長さを制限します。もちろん、他の組み込み機能は、公式ドキュメントを参照することができます。

  • [CreditCard]属性にクレジットカード形式があることを確認します。jQueryは他のメソッドを検証する必要があります。
  • [比較]モデル内の2つのプロパティが一致することを検証します。
  • [EmailAddress]:属性に電子メール形式があることを確認します。
  • [Phone]プロパティに電話番号形式があることを確認します。
  • [Range]プロパティ値が指定した範囲内にあることを確認します。
  • [RegularExpression]プロパティの値が指定した正規表現と一致することを確認します。
  • [必須]:フィールドがnullでないことを確認します。このプロパティーの動作の詳細については、[必須]プロパティーを参照してください。
  • [StringLength]:文字列プロパティの値が指定した長さ制限を超えていないことを確認します。
  • [Url]属性がURL形式であることを確認します。
  • [Remote]:サーバ上でアクションメソッドを呼び出して、クライアント上の入力を検証します。

上記は基本的なモデル検証使用方法を説明し、この方式では、同時にT4テンプレートと結合し、表オブジェクトによってモデル検証実体を生成し、actionに大量の検証コードを書く作業を省くことができる。もちろん、必要なより複雑な検証、またはデータベース操作と組み合わせた検証の一部は、アクションまたは他のアプリケーションモジュールに個別に記述されます。

Web APIでのモデル検証はどのように機能するのでしょうか?StartupクラスのConfigureServicesに次のコードを追加します。

//模型参数验证
services.Configure<ApiBehaviorOptions>(options =>
{
options.InvalidModelStateResponseFactory = (context) =>
{
    var error = context.ModelState.FirstOrDefault().Value;
    var message = error.Errors.FirstOrDefault(p => !string.IsNullOrWhiteSpace(p.ErrorMessage))?.ErrorMessage;

    return new JsonResult(new ApiResult { Message = message });
};
});

登録サンプルのアクションコードを追加するには、次の手順に従います。

/// <summary>
/// 注册
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
[HttpPost]
public async Task<ApiResult> Register(RegisterEntity model)
{
    ApiResult result = new ApiResult();

    var _code = CacheHelper.GetCache(model.Mobile);
    if (_code == null)
    {
        result.Message = "验证码过期或不存在";
        return result;
    }
    if (!model.Code.Equals(_code.ToString()))
    {
        result.Message = "验证码错误";
        return result;
    }

    /**
    相关逻辑代码
    **/
    return result;
}

したがって、ApiBe haviorOptionsの方法を設定し、検証エラーメッセージの最初のメッセージを読んで返すことで、Web APIのActionによるリクエストパラメータの検証が完了し、エラーメッセージの戻りについても少しカプセル化することができます。

7.ジャーナルの利用

NET Core WebApiには独自のログ管理機能がありますが、必ずしも私たちのニーズを満たすのが容易ではなく、通常、NLog、Log4Netなどのサードパーティ製のロギングフレームワークを使用します。

**NLogの使い方 ***

NuGet経由でパッケージをインストールする:NLog. Web.AspNetCore、現在のプロジェクトバージョン4.9.2;

2プロジェクトルートディレクトリ新しいNLog.configファイル、キー NLog.configのその他の詳細な設定は、公式ドキュメントを参照することができます。ここでは簡単な設定を行います。

<?xml version="1.0" encoding="utf-8"?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      autoReload="true"
      throwExceptions="false"
      internalLogLevel="Off"
      internalLogFile="NlogRecords.log">
  <!--Nlog内部日志记录为Off关闭-->
  <extensions>
    <add assembly="NLog.Web.AspNetCore" />
  </extensions>
  <targets>
    <target name="log_file" xsi:type="File" fileName="${basedir}/logs/${shortdate}.log"
            layout="${longdate} | ${level:uppercase=false} | ${message} ${onexception:${exception:format=tostring} ${newline} ${stacktrace} ${newline}" />
  </targets>

  <rules>
    <!--跳过所有级别的Microsoft组件的日志记录-->
    <logger name="Microsoft.*" final="true" />
    <!--<logger name="logdb" writeTo="log_database" />-->
    <logger name="*" minlevel="Trace" writeTo="log_file" />

  </rules>
</nlog>

<!--https://github.com/NLog/NLog/wiki/Getting-started-with-ASP.NET-Core-3-->

プログラム. csファイルを以下のように調整します。

public class Program
{
    public static void Main(string[] args)
    {
        //CreateHostBuilder(args).Build().Run();
       
        var logger = NLog.Web.NLogBuilder.ConfigureNLog("nlog.config").GetCurrentClassLogger();
        try
        {
            logger.Debug("init main");
            CreateHostBuilder(args).Build().Run();
        }
        catch (Exception exception)
        {
            //NLog: catch setup errors
            logger.Error(exception, "Stopped program because of exception");
            throw;
        }
        finally
        {
            // Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux)
            NLog.LogManager.Shutdown();
        }
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            }).ConfigureLogging(logging => {
                logging.ClearProviders();
                logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace);
            }).UseNLog();//依赖注入Nlog;
}

Main関数内の例外キャッチコード設定の省略も可能で、CreateHostBuilderのUseNLogは必須です。

コントローラは注入により次のように呼び出されます。

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

    private readonly ILogger<WeatherForecastController> _logger;

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

    [HttpGet]
    public IEnumerable<WeatherForecast> Get()
    {
        _logger.LogInformation("测试一条日志");

        var rng = new Random();
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = rng.Next(-20, 55),
            Summary = Summaries[rng.Next(Summaries.Length)]
        })
        .ToArray();
    }

ローカルテスト后、logsディレクトリで生成されたログファイルをできます。

8.依存注入

NET Coreを使用することは、. NET Coreの設計アイデアの1つでもある依存注入に対処する必要があります。依存注入(DI)とは何か、なぜ依存注入を使用するのかについては、ここでは繰り返さず、最初に依存注入の簡単な例を見てみましょう。

public interface IProductRepository
{
    IEnumerable<Product> GetAll();
}

public class ProductRepository : IProductRepository
{
    public IEnumerable<Product> GetAll()
    {

    }
}

Startupクラスを登録する場合:

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<IProductRepository, ProductRepository>();
}

IProductRepositoryサービスを要求し、GetAllメソッドを呼び出すために使用するには:

public class ProductController : ControllerBase
{
    private readonly IProductRepository _productRepository;
   public ProductController(IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }

    public IEnumerable<Product> Get()
    {
        return _productRepository.GetAll();
    }
}

DIパターンを使用して、IProductRepositoryインターフェイスを実装します。実際、コンストラクタを介した注入呼び出しの例は何度もあります。

** ライフサイクル **

services.AddScoped<IMyDependency, MyDependency>();
services.AddTransient<IMyDependency, MyDependency>();
services.AddSingleton<IMyDependency, MyDependency>();
  • Transient:リクエストごとに新しいインスタンスが作成されます。
  • Scoped:各スコープ生成サイクルでインスタンスを作成します。
  • シングルトン:アプリケーションのライフサイクル全体で1つのインスタンスのみを作成するシングルトンパターン。

ここでは、特定のビジネスロジックシナリオのニーズに応じて、適切なライフサイクルサービスを選択して注入する必要があります。

実際のアプリケーションでは、ConfigureServicesに登録する必要があるサービスがたくさんあります。書き込みは明らかに面倒で、書き込みを忘れやすいです。一般的に、リフレクションを使用してバッチインジェクションを行い、拡張方法でインジェクションを行うことを考えるかもしれません。

public static class AppServiceExtensions
{
    /// <summary>
    /// 注册应用程序域中的服务
    /// </summary>
    /// <param name="services"></param>
    public static void AddAppServices(this IServiceCollection services)
    {
        var ts = System.Reflection.Assembly.Load("CoreAPI.Data").GetTypes().Where(s => s.Name.EndsWith("Repository") || s.Name.EndsWith("Service")).ToArray();
        foreach (var item in ts.Where(s => !s.IsInterface))
        {
            var interfaceType = item.GetInterfaces();
            foreach (var typeArray in interfaceType)
            {
                services.AddTransient(typeArray, item);
            }
        }
    }
}
public void ConfigureServices(IServiceCollection services)
{
    services.AddAppServices();//批量注册服务
}

確かに、このシステムはDI 注入を内蔵しており、バッチ注入のニーズを満たすことができます。しかし、他のサードパーティ製コンポーネント(Scrutor、Autofac)を選択するなど、DI登録を簡素化するのに役立つオプションがあります。

  1. Scrutorの使い方

ScrutorはMicrosoft Injectionコンポーネントに基づく拡張ライブラリで、簡単な例は次のとおりです。

services.Scan(scan => scan
    .FromAssemblyOf<Startup>()
        .AddClasses(classes => classes.Where(s => s.Name.EndsWith("Repository") || s.Name.EndsWith("Service")))
        .AsImplementedInterfaces()
        .WithTransientLifetime()
    );

上記のコードは、リポジトリとサービスで終わるインターフェイスサービスをスキャンによって一括登録し、そのライフサイクルはTransientであり、前述の反射的な一括登録サービスと同等である。

Scrutorのその他の用途については、公式ドキュメントを参照してください。ここでは紹介のみです。

  1. Autofac

一般的に、MS固有のDIまたはスクラーターの使用は、属性注入の要件、さらにはMS固有のDIの引き継ぎまたは交換など、より高いアプリケーション要件がある場合は、実際のニーズを満たすことができます。Autofacの特定の使用については、ここでは詳述しません。

IX.キャッシュバック

** メモリキャッシュ **

公式には、開発者はキャッシュを合理的に使用し、キャッシュサイズを制限する必要があります。Coreランタイムはコンテンツ圧力に基づいてキャッシュサイズを制限しません。使用方法については、まだ最初に登録し、コントローラが呼び出します。

public void ConfigureServices(IServiceCollection services)
{
    services.AddMemoryCache();//缓存中间件
}
public class ProductController : ControllerBase
    {
        private IMemoryCache _cache;

        public ProductController(IMemoryCache memoryCache)
        {
            _cache = memoryCache;
        }

        [HttpGet]
        public DateTime GetTime()
        {
            string key = "_timeKey";

            // Look for cache key.
            if (!_cache.TryGetValue(key, out DateTime cacheEntry))
            {
                // Key not in cache, so get data.
                cacheEntry = DateTime.Now;

                // Set cache options.
                var cacheEntryOptions = new MemoryCacheEntryOptions()
                    // Keep in cache for this time, reset time if accessed.
                    .SetSlidingExpiration(TimeSpan.FromSeconds(3));

                // Save data in cache.
                _cache.Set(key, cacheEntry, cacheEntryOptions);
            }

            return cacheEntry;
        }
    }

上記のコードは、時間をキャッシュし、スライド有効期限(最後のアクセスからの有効期限)を3 秒に設定します。絶対有効期限を設定する必要がある場合は、SetSliding ExpirationをSetAbsoluteExpirationに変更します。ブラウザは更新され、3 秒ごとに時間が更新されます。

パッケージ化されたキャッシュクラスは以下の通りです。

public class CacheHelper
{
    public static IMemoryCache _memoryCache = new MemoryCache(new MemoryCacheOptions());

    /// <summary>
    /// 缓存绝对过期时间
    /// </summary>
    ///<param name="key">Cache键</param>
    ///<param name="value">缓存的值</param>
    ///<param name="minute">minute分钟后绝对过期</param>
    public static void SetChache(string key, object value, int minute)
    {
        if (value == null) return;
        _memoryCache.Set(key, value, new MemoryCacheEntryOptions()
                .SetAbsoluteExpiration(TimeSpan.FromMinutes(minute)));
    }

    /// <summary>
    /// 缓存相对过期,最后一次访问后minute分钟后过期
    /// </summary>
    ///<param name="key">Cache键</param>
    ///<param name="value">缓存的值</param>
    ///<param name="minute">滑动过期分钟</param>
    public static void SetChacheSliding(string key, object value, int minute)
    {
        if (value == null) return;
        _memoryCache.Set(key, value, new MemoryCacheEntryOptions()
                .SetSlidingExpiration(TimeSpan.FromMinutes(minute)));
    }

    /// <summary>
    ///设置缓存,如果不主动清空,会一直保存在内存中.
    /// </summary>
    ///<param name="key">Cache键值</param>
    ///<param name="value">给Cache[key]赋的值</param>
    public static void SetChache(string key, object value)
    {
        _memoryCache.Set(key, value);
    }

    /// <summary>
    ///清除缓存
    /// </summary>
    ///<param name="key">cache键</param>
    public static void RemoveCache(string key)
    {
        _memoryCache.Remove(key);
    }

    /// <summary>
    ///根据key值,返回Cache[key]的值
    /// </summary>
    ///<param name="key"></param>
    public static object GetCache(string key)
    {
        //return _memoryCache.Get(key);
        if (key != null && _memoryCache.TryGetValue(key, out object val))
        {
            return val;
        }
        else
        {
            return default;
        }
    }

    /// <summary>
    /// 通过Key值返回泛型对象
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="key"></param>
    /// <returns></returns>
    public static T GetCache<T>(string key)
    {
        if (key != null && _memoryCache.TryGetValue<T>(key, out T val))
        {
            return val;
        }
        else
        {
            return default;
        }
    }

}

X.異常な処理

** 例外処理ミドルウェアを定義する **

ここでは、主にグローバル例外をキャプチャしてログを記録し、統一されたjson形式でインターフェイス呼び出し元に戻ります。例外処理の前にミドルウェアを言及すると、ミドルウェアとは何かについて、ここでは詳述しませんが、ミドルウェアの基本的な構造は次のとおりです。

public class CustomMiddleware
{
    private readonly RequestDelegate _next;

    public CustomMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext httpContext)
    {
        await _next(httpContext);
    }
}

以下のコードで独自のグローバル例外処理ミドルウェアを定義します。

public class CustomExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<CustomExceptionMiddleware> _logger;

    public CustomExceptionMiddleware(RequestDelegate next, ILogger<CustomExceptionMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task Invoke(HttpContext httpContext)
    {
        try
        {
            await _next(httpContext);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex,"Unhandled exception...");
            await HandleExceptionAsync(httpContext, ex);
        }
    }

    private Task HandleExceptionAsync(HttpContext httpContext, Exception ex)
    {
        var result = JsonConvert.SerializeObject(new { isSuccess = false, message = ex.Message });
        httpContext.Response.ContentType = "application/json;charset=utf-8";
        return httpContext.Response.WriteAsync(result);
    }
}

/// <summary>
/// 以扩展方式添加中间件
/// </summary>
public static class CustomExceptionMiddlewareExtensions
{
    public static IApplicationBuilder UseCustomExceptionMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<CustomExceptionMiddleware>();
    }
}

次に、StartupクラスのConfigureメソッドに上記の拡張ミドルウェアを追加します。太字の部分を参照してください。

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

    //全局异常处理
    app.UseCustomExceptionMiddleware();
}

HandleExceptionAsyncメソッドでは、開発とテストを容易にするために、システムエラーがインターフェイス呼び出し元に返され、実際の本番環境では固定エラーメッセージを統一的に返すことができます。

*** 例外の処理 ***

关于 http 状态码,常见的如正常返回的 200,其他 401、403、404、502 等等等等,因为系统有时候并不总是返回 200 成功,对于返回非 200 的异常状态码,WebApi 也要做到相应的处理,以便接口调用者能正确接收,譬如紧接下来的JWT认证,当认证令牌过期或没有权限时,系统实际会返回 401、403,但接口并不提供有效的可接收的返回,因此,这里列举一些常见的异常状态码,并以 200 方式提供给接口调用者,在 Startup 类的 Configure 方法里添加代码如下:

app.UseStatusCodePages(async context =>
{
    //context.HttpContext.Response.ContentType = "text/plain";
    context.HttpContext.Response.ContentType = "application/json;charset=utf-8";

    int code = context.HttpContext.Response.StatusCode;
    string message =
        code switch
        {
            401 => "未登录",
            403 => "访问拒绝",
            404 => "未找到",
            _ => "未知错误",
        };

    context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
    await context.HttpContext.Response.WriteAsync(Newtonsoft.Json.JsonConvert.SerializeObject(new
    {
        isSuccess = false,
        code,
        message
    }));

});

コードは非常にシンプルで、ここではシステム固有の例外処理ミドルウェアUseStatus Code Pagesを使用して、もちろん、例外を処理するためのフィルタをカスタマイズすることもできますが、推奨されず、シンプルで効率的な直接が必要です。

NET Coreの例外処理ミドルウェアに関しては、UseExceptionHandler、UseStatus Code PagesWithRedirectsなどの他のミドルウェアがあり、異なるミドルウェアには適切な環境があり、MVCや他のアプリケーションシナリオに適したものがあります。

余談ですが、UseStatus Code Pagesの例外ステータスコード処理を、前述のグローバル例外処理ミドルウェアにカプセル化することもできます。

アプリケーションセキュリティとJWT認定

JWTとは何かはここでは触れない。実際のアプリケーションでは、アクセスするために認証が必要なインターフェイスリソースなど、一部のインターフェイスのセキュリティのために、Web APIの場合、認証にトークントークンを使用し、サーバーはキャッシュと組み合わせて実装されています。

JWT認証を選ぶ理由は?その理由は次のとおりです。サーバーは保存されていない、ステートレス、モバイルに適している、分散に適している、標準化に適しています。JWTの使い方は以下の通りです。

NuGget経由でパッケージをインストールする:Microsoft.AspNetCore.Authentication.JwtBearer、現在のサンプルバージョン3.1.5;

ConfigureServices注入は、デフォルトでBearerという名前です。ここでは、一貫性を保つために、他の名前に変更することもできます。太字部分に注意してください。

appsettings.json JWT設定ノードを追加し(前述の設定ファイルを参照)、JWT関連の認証クラスを追加します。

public static class JwtSetting
{
    public static JwtConfig Setting { get; set; } = new JwtConfig();
}

public class JwtConfig
{
    public string Secret { get; set; }
    public string Issuer { get; set; }
    public string Audience { get; set; }
    public int AccessExpiration { get; set; }
    public int RefreshExpiration { get; set; }
}

前述の静的クラスバインド方法でJWT設定を読み込み、注入します。

public Startup(IConfiguration configuration, IWebHostEnvironment env)
{
    //Configuration = configuration;

    var builder = new ConfigurationBuilder()
        .SetBasePath(env.ContentRootPath)
        .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);

    Configuration = builder.Build();

    configuration.GetSection("SystemConfig").Bind(MySettings.Setting);//绑定静态配置类
    configuration.GetSection("JwtTokenConfig").Bind(JwtSetting.Setting);//同上

}

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)
{

    #region JWT认证注入

    JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
    services.AddAuthentication("Bearer")
        .AddJwtBearer("Bearer", options =>
        {
            options.RequireHttpsMetadata = false;

            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidateAudience = true,
                ValidateLifetime = true,
                ValidateIssuerSigningKey = true,
                ValidIssuer = JwtSetting.Setting.Issuer,
                ValidAudience = JwtSetting.Setting.Audience,
                IssuerSigningKey = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(JwtSetting.Setting.Secret))
            };
        });

    #endregion

}

SwaggerにJWT認証サポートを追加します。完了すると、SwaggerページにロックIDが表示され、トークンを取得した後、値(ベアートークン形式)エントリを入力してログインを許可します。

services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo
    {
        Title = "My API",
        Version = "v1",
        Description = "API文档描述",
        Contact = new OpenApiContact
        {
            Email = "5007032@qq.com",
            Name = "测试项目",
            //Url = new Uri("http://t.abc.com/")
        },
        License = new OpenApiLicense
        {
            Name = "BROOKE许可证",
            //Url = new Uri("http://t.abc.com/")
        }
    });


    // 为 Swagger JSON and UI设置xml文档注释路径
    //var basePath = Path.GetDirectoryName(typeof(Program).Assembly.Location);//获取应用程序所在目录(不受工作目录影响)
    //var xmlPath = Path.Combine(basePath, "CoreAPI_Demo.xml");
    //c.IncludeXmlComments(xmlPath, true);

    var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
    c.IncludeXmlComments(xmlPath);

    #region JWT认证Swagger授权

    c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        Description = "JWT授权(数据将在请求头header中进行传输) 直接在下框中输入Bearer {token}(中间是空格)",
        Name = "Authorization",
        In = ParameterLocation.Header,
        Type = SecuritySchemeType.ApiKey,
        BearerFormat = "JWT",
        Scheme = "Bearer"
    });

    c.AddSecurityRequirement(new OpenApiSecurityRequirement()
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference {
                    Type = ReferenceType.SecurityScheme,
                    Id = "Bearer"
                }
            },
            new string[] { }
        }
    });

    #endregion

});

Starupクラスは、app.UseAuthorization;の前にConfigureレジストリを追加します。

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseAuthentication();//jwt认证

    app.UseAuthorization();

}

これにより、JWTは基本的に構成され、認証ログインと承認を実装し、以下の操作をシミュレートします:

[HttpPost]
public async Task<ApiResult> Login(LoginEntity model)
{
    ApiResult result = new ApiResult();

    //验证用户名和密码
    var userInfo = await _memberService.CheckUserAndPwd(model.User, model.Pwd);
    if (userInfo == null)
    {
        result.Message = "用户名或密码不正确";
        return result;
    }
    var claims = new Claim[]
    {
        new Claim(ClaimTypes.Name,model.User),
        new Claim(ClaimTypes.Role,"User"),
        new Claim(JwtRegisteredClaimNames.Sub,userInfo.MemberID.ToString()),

    };

    var key = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(JwtSetting.Setting.Secret));
    var expires = DateTime.Now.AddDays(1);
    var token = new JwtSecurityToken(
                issuer: JwtSetting.Setting.Issuer,
                audience: JwtSetting.Setting.Audience,
                claims: claims,
                notBefore: DateTime.Now,
                expires: expires,
                signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256));

    //生成Token
    string jwtToken = new JwtSecurityTokenHandler().WriteToken(token);

    //更新最后登录时间
    await _memberService.UpdateLastLoginTime(userInfo.MemberID);

    result.IsSuccess= 1;
    result.ResultData["token"] = jwtToken;
    result.Message = "授权成功!";
    return result;

}

上記のコードは、ログイン操作(アカウントパスワードログイン、有効期限の設定後1日)をシミュレートし、トークンを生成して戻り、フロントエンド呼び出し元はローカルストレージなどの方法でトークンを取得し、認証インターフェイスを呼び出し、インターフェイス要求のためのヘッダ(ベアラトークン)にトークンを追加します。次に、認証が必要なコントローラーまたはアクションにAuthorize IDを割り当てます。

[Authorize]
[Route("api/[controller]/[action]")]
public class UserController : ControllerBase
{
}

ロールベースの認可を追加する場合は、次のようにアクションを制限できます。

[Authorize(Roles = "user")]
[Route("api/[controller]/[action]")]
public class UserController : ControllerBase
{
}

//多个角色也可以逗号分隔
[Authorize(Roles = "Administrator,Finance")]
[Route("api/[controller]/[action]")]
public class UserController : ControllerBase
{
}

さまざまなロール情報は、ログイン設定ClaimTypes.Roleによって設定できます。もちろん、ここではロールサービスのアプリケーションの簡単な例であり、複雑なものは登録ポリシーによって提供され、データベースと組み合わせて動的に設定できます。

これにより、JWT認証に基づく単純な認可作業が完了します。

XII.クロスフィールド

フロントとリアの分離は、クロスドメインの問題を含み、クロスドメイン操作の簡単なサポートは次のとおりです。

拡張サポートの追加

public static class CrosExtensions
{
    public static void ConfigureCors(this IServiceCollection services)
    {

        services.AddCors(options => options.AddPolicy("CorsPolicy",
            builder =>
            {
                builder.AllowAnyMethod()
                    .SetIsOriginAllowed(_ => true)
                    .AllowAnyHeader()
                    .AllowCredentials();
            }));

        //services.AddCors(options => options.AddPolicy("CorsPolicy",
        //builder =>
        //{
        //    builder.WithOrigins(new string[] { "http://localhost:13210" })
        //        .AllowAnyMethod()
        //        .AllowAnyHeader()
        //        .AllowCredentials();
        //}));


    }
}

Startupクラスに関連する登録を次のように追加します。

public void ConfigureServices(IServiceCollection services)
{
    services.ConfigureCors();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseCors("CorsPolicy");//跨域
}

これにより、単純なクロスドメイン操作が完了し、WithOriginsやWithMethodsなどのメソッドを設定して、リクエスト元とリクエスト方法を制限することもできます。

至此,全篇结束,本篇涉及到的源码地址:https://github.com/Brooke181/CoreAPI_Demo

NET CoreでのDapperの使い方、サポートありがとうございます!

Keep Exploring

延伸阅读

更多文章
同分类 / 同标签 2022/06/22

ASP.NET Core Web APIのローカライズ(単一リソースファイル)

マイクロソフトのデフォルトは、複数のリソースファイルに対応するクラスであり、使用はより面倒ですが、この記事では、単一のリソースファイルの使用方法、すなわち、プロジェクト全体のすべてのクラスが多言語リソースファイルのセットに対応しています。

继续阅读
同分类 / 同标签 2022/04/13

ASP.NET Core WebApiが結果を返す統一パッケージングプラクティス

WebApiの統一結果のリターンについては、より多くのステップを考えさせられました。第一に、統一形式のリターンをより良く制限する方法、第二に、結果のパッケージングがよりシンプルで強力でなければなりません。絶え間ない思考と改善の中で、最終的に予備的な結果があり、それを共有し、無限の思考を学ぶことは無限であり、あなたと一緒に働くことを願っています。

继续阅读