> 技术文档 > .NET 8.0 - 使用存储库模式和 Dapper 进行日志记录和单元测试的清洁架构

.NET 8.0 - 使用存储库模式和 Dapper 进行日志记录和单元测试的清洁架构

在本文中,我们将了解清洁架构并引导您了解 .NET 8.0 中的示例 CRUD API。

示例代码:https://download.csdn.net/download/hefeng_aspnet/91474246 

我们将在此示例中使用以下工具、技术和框架:

        • Visual Studio 2022和.NET 8.0
        • C#
        • MS SQL 数据库
        • Clean Architecture
        • Dapper(mini ORM)
        • Repository Pattern (存储库模式)
        • Unit of Work (工作单元)
        • Swagger UI
        • API Authentication (Key Based) [API 身份验证(基于密钥)]
        • Logging (using log4net) [日志记录(使用 log4net)]
        • Unit Testing (MSTest Project)[单元测试(MSTest 项目)]

在开始示例应用程序之前,让我们了解清晰的架构及其好处。

软件架构的目标是尽量减少构建和维护所需系统所需的人力资源。——罗伯特·C·马丁,《清洁架构》

清洁架构解释:

清洁架构 (Clean Architecture) 是由罗伯特·C·马丁 (Robert C. Martin)(又名鲍勃大叔)提出的系统架构指南。它衍生自许多架构指南,例如六边形架构、洋葱架构等。

        • 清洁架构的主要概念是应用程序的核心逻辑很少改变,因此它将是独立的并被视为核心。
        • 使这种架构发挥作用的首要规则是依赖规则。该规则规定,源代码依赖关系只能指向内部,并且内圈中的任何事物都无法知晓外圈中的任何事物。
        • 通过将软件分层并遵循依赖规则,您将创建一个本质上可测试的系统,并享受其带来的所有好处。当系统的任何外部组件(例如数据库或 Web 框架)过时时,您可以轻松替换这些过时的元素。
        • 在清晰架构中,领域层和应用层仍然处于设计的中心,被称为应用程序的核心。
                • 领域层包含企业逻辑,应用层包含业务逻辑。
                • 企业逻辑可以在许多相关系统之间共享,但业务逻辑不可共享,因为它是为特定的业务需求而设计的。
                • 如果您没有企业而只是编写单个应用程序,那么这些实体就是该应用程序的业务对象。

清洁架构的优点:

        • 框架独立——该架构不依赖于某些功能丰富的软件库。这使得您可以将这些框架用作工具。
        • UI 独立 - 它与 UI 层松散耦合。因此,您可以在不改变核心业务的情况下更改 UI。
        • 独立于数据库 - 您可以将 SQL Server 或 Oracle 替换为 MongoDB、Bigtable、CouchDB 或其他数据库。您的业务规则不受数据库的约束。
        • 高度可维护——遵循关注点分离。
        • 高度可测试 - 使用这种方法构建的应用程序,尤其是核心域模型及其业务规则,极易测试。
现在我们已经了解了简洁架构。在开始示例 API 之前,让我们简单回顾一下 Dapper。

Dapper 解释道:

        • Dapper 是一个简单的对象映射器或微型 ORM,负责数据库和编程语言之间的映射。
        • Dapper 由 Stack Overflow 团队创建,旨在解决他们的问题并将其开源。Dapper 在 Stack Overflow 上的使用本身就展现了它的强大功能。
        • 它大大减少了数据库访问代码,并专注于完成数据库任务,而不是完全依赖 ORM。
        • 它可以与任何数据库集成,例如 SQL Server、Oracle、SQLite、MySQL、PostgreSQL 等。
        • 如果DB已经设计好了,那么使用Dapper是一个最佳且高效的选择。
        • 性能:与 Entity Framework 相比,Dapper 的数据查询速度更快。这是因为 Dapper 直接使用 RAW SQL,因此时间延迟相对较小。

在本文中,我们将与 Dapper 一起使用存储库模式和工作单元,并向您展示如何按照存储库模式和工作单元在 ASP.NET 8.0 API 中使用 Dapper。

解决方案和项目设置:

首先,创建一个用于执行 CRUD 操作的新表。您可以使用CleanArch.Sql/Scripts代码示例文件夹下共享的脚本。

一旦我们的后端准备就绪,打开 Visual Studio 2022 并创建一个空白解决方案项目,并将其命名为CleanArch。

设置核心层:在解决方案下,创建一个新的类库项目,并将其命名为CleanArch.Core。

• 添加一个新文件夹Entities并添加一个名为 的新实体类Contact。

public class Contact
{
    public int? ContactId { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }

    public string Email { get; set; }

    public string PhoneNumber { get; set; }
}

这里需要注意的是,核心层不应该依赖于任何其他项目或层。这在使用清洁架构时非常重要。

设置应用层:添加另一个类库项目并将其命名为CleanArch.Application。

• 添加一个新文件夹Application,在此文件夹下,我们将定义将在另一层实现的接口。
• 创建一个通用IRepository接口并定义 CRUD 方法。

public interface IRepository where T : class
{
    Task<IReadOnlyList> GetAllAsync();
    Task GetByIdAsync(long id);
    Task AddAsync(T entity);
    Task UpdateAsync(T entity);
    Task DeleteAsync(long id);
}

• 添加对项目的引用Core,应用程序项目始终仅依赖于该Core项目。
• 之后添加一个联系人特定存储库(IContactRepository),并从中继承IRepository
• 另外,创建一个新的接口并命名它,IUnitOfWork因为我们将在实现中使用工作单元。

// IContactRepository.cs file
public interface IContactRepository : IRepository
{
}

// IUnitOfWork.cs file
public interface IUnitOfWork
{
    IContactRepository Contacts { get; }
}

• 由于我们也在实现日志记录,因此添加一个ILogger接口并添加不同日志级别的方法。

///

/// Logger class contract.
///

public interface ILogger
{
    /* Log a message object */
    void Debug(object message);
    void Info(object message);
    void Warn(object message);
    void Error(object message);
    void Fatal(object message);

    /* Log a message object and exception */
    void Debug(object message, Exception exception);
    void Info(object message, Exception exception);
    void Warn(object message, Exception exception);
    void Error(object message, Exception exception);
    void Fatal(object message, Exception exception);

    /* Log an exception including the stack trace of exception. */
    void Error(Exception exception);
    void Fatal(Exception exception);
}

设置日志记录:添加新的类库项目(CleanArch.Logging)

• 我们将使用 Log4Net 库进行日志记录,因此log4net从 NuGet 包管理器安装包。
• 添加对项目的引用Application,然后添加新类Logger并实现ILogger接口。

public sealed class Logger : Application.Interfaces.ILogger
{
    #region ===[ Private Members ]=============================================================

    private static readonly ILog _logger = LogManager.GetLogger(MethodBase.GetCurrentMethod()?.DeclaringType);
    private static readonly Lazy _loggerInstance = new Lazy(() => new Logger());

    private const string ExceptionName = \"Exception\";
    private const string InnerExceptionName = \"Inner Exception\";
    private const string ExceptionMessageWithoutInnerException = \"{0}{1}: {2}Message: {3}{4}StackTrace: {5}.\";
    private const string ExceptionMessageWithInnerException = \"{0}{1}{2}\";

    #endregion
        
    #region ===[ Properties ]==================================================================

    ///

    /// Gets the Logger instance.
    ///

    public static Logger Instance
    {
        get { return _loggerInstance.Value; }
    }

    #endregion

    #region ===[ ILogger Members ]=============================================================

    ///

    /// Logs a message object with the log4net.Core.Level.Debug level.
    ///

    ///
    public void Debug(object message)
    {
        if (_logger.IsDebugEnabled)
            _logger.Debug(message);
    }

    ///

    /// Logs a message object with the log4net.Core.Level.Info level.
    ///

    ///
    public void Info(object message)
    {
        if (_logger.IsInfoEnabled)
            _logger.Info(message);
    }

    ///

    /// Logs a message object with the log4net.Core.Level.Info Warning.
    ///

    ///
    public void Warn(object message)
    {
        if (_logger.IsWarnEnabled)
            _logger.Warn(message);
    }

    ///

    /// Logs a message object with the log4net.Core.Level.Error level.
    ///

    ///
    public void Error(object message)
    {
        _logger.Error(message);
    }

    ///

    /// Logs a message object with the log4net.Core.Level.Fatal level.
    ///

    ///
    public void Fatal(object message)
    {
        _logger.Fatal(message);
    }

    ///

    /// Logs a message object with the log4net.Core.Level.Debug level including the exception.
    ///

    ///
    ///
    public void Debug(object message, Exception exception)
    {
        if (_logger.IsDebugEnabled)
            _logger.Debug(message, exception);
    }

    ///

    /// Logs a message object with the log4net.Core.Level.Info level including the exception.
    ///

    ///
    ///
    public void Info(object message, Exception exception)
    {
        if (_logger.IsInfoEnabled)
            _logger.Info(message, exception);
    }

    ///

    /// Logs a message object with the log4net.Core.Level.Warn level including the exception.
    ///

    ///
    ///
    public void Warn(object message, Exception exception)
    {
        if (_logger.IsWarnEnabled)
            _logger.Info(message, exception);
    }

    ///

    /// Logs a message object with the log4net.Core.Level.Error level including the exception.
    ///

    ///
    ///
    public void Error(object message, Exception exception)
    {
        _logger.Error(message, exception);
    }

    ///

    /// Logs a message object with the log4net.Core.Level.Fatal level including the exception.
    ///

    ///
    ///
    public void Fatal(object message, Exception exception)
    {
        _logger.Fatal(message, exception);
    }

    ///

    /// Log an exception with the log4net.Core.Level.Error level including the stack trace of the System.Exception passed as a parameter.
    ///

    ///
    public void Error(Exception exception)
    {
        _logger.Error(SerializeException(exception, ExceptionName));
    }

    ///

    /// Log an exception with the log4net.Core.Level.Fatal level including the stack trace of the System.Exception passed as a parameter.
    ///

    ///
    public void Fatal(Exception exception)
    {
        _logger.Fatal(SerializeException(exception, ExceptionName));
    }

    #endregion

    #region ===[ Public Methods ]==============================================================

    ///

    /// Serialize Exception to get the complete message and stack trace.
    ///

    ///
    ///
    public static string SerializeException(Exception exception)
    {
        return SerializeException(exception, string.Empty);
    }

    #endregion

    #region ===[ Private Methods ]=============================================================

    ///

    /// Serialize Exception to get the complete message and stack trace.
    ///

    ///
    ///
    ///
    private static string SerializeException(Exception ex, string exceptionMessage)
    {
        var mesgAndStackTrace = string.Format(ExceptionMessageWithoutInnerException, Environment.NewLine,
            exceptionMessage, Environment.NewLine, ex.Message, Environment.NewLine, ex.StackTrace);

        if (ex.InnerException != null)
        {
            mesgAndStackTrace = string.Format(ExceptionMessageWithInnerException, mesgAndStackTrace,
                Environment.NewLine,
                SerializeException(ex.InnerException, InnerExceptionName));
        }

        return mesgAndStackTrace + Environment.NewLine;
    }

    #endregion
}

设置 SQL 项目:添加一个新的类库项目 ( CleanArch.Sql)。我们将使用此项目来管理 Dapper 查询。

• 添加一个新文件夹Queries并在其下添加一个新类ContactQueries(以管理对象的简洁查询Contact)。

public static class ContactQueries
{
    public static string AllContact => \"SELECT * FROM [Contact] (NOLOCK)\";

    public static string ContactById => \"SELECT * FROM [Contact] (NOLOCK) WHERE [ContactId] = @ContactId\";

    public static string AddContact =>
        @\"INSERT INTO [Contact] ([FirstName], [LastName], [Email], [PhoneNumber]) 
            VALUES (@FirstName, @LastName, @Email, @PhoneNumber)\";

    public static string UpdateContact =>
        @\"UPDATE [Contact] 
        SET [FirstName] = @FirstName, 
            [LastName] = @LastName, 
            [Email] = @Email, 
            [PhoneNumber] = @PhoneNumber
        WHERE [ContactId] = @ContactId\";

    public static string DeleteContact => \"DELETE FROM [Contact] WHERE [ContactId] = @ContactId\";
}

• 除此之外,Scripts还添加了包含示例中使用的表的先决条件脚本的文件夹。

设置基础设施层:由于我们的基础代码已经准备好,现在添加一个新的类库项目并将其命名为CleanArch.Infrastructure。

• 添加本项目需要使用的包。

Install-Package Dapper
Install-Package Microsoft.Extensions.Configuration
Install-Package Microsoft.Extensions.DependencyInjection.Abstractions
Install-Package System.Data.SqlClient 

• 添加对项目的引用(Application、、Core和Sql),并添加一个新文件夹Repository。
• 之后IContactRepository,让我们通过创建一个新类ContactRepository并注入来IConfiguration获取连接字符串来实现接口appsettings.json

public class ContactRepository : IContactRepository
{
    #region ===[ Private Members ]=============================================================

    private readonly IConfiguration configuration;

    #endregion

    #region ===[ Constructor ]=================================================================

    public ContactRepository(IConfiguration configuration)
    {
        this.configuration = configuration;
    }

    #endregion

    #region ===[ IContactRepository Methods ]==================================================

    public async Task<IReadOnlyList> GetAllAsync()
    {
        using (IDbConnection connection = new SqlConnection(configuration.GetConnectionString(\"DBConnection\")))
        {
            var result = await connection.QueryAsync(ContactQueries.AllContact);
            
            return result.ToList();
        }
    }

    public async Task GetByIdAsync(long id)
    {
        using (IDbConnection connection = new SqlConnection(configuration.GetConnectionString(\"DBConnection\")))
        {
            var result = await connection.QuerySingleOrDefaultAsync(ContactQueries.ContactById, new { ContactId = id });
            
            return result;
        }
    }

    public async Task AddAsync(Contact entity)
    {
        using (IDbConnection connection = new SqlConnection(configuration.GetConnectionString(\"DBConnection\")))
        {
            var result = await connection.ExecuteAsync(ContactQueries.AddContact, entity);
            
            return result.ToString();
        }
    }

    public async Task UpdateAsync(Contact entity)
    {
        using (IDbConnection connection = new SqlConnection(configuration.GetConnectionString(\"DBConnection\")))
        {
            var result = await connection.ExecuteAsync(ContactQueries.UpdateContact, entity);
            
            return result.ToString();
        }
    }

    public async Task DeleteAsync(long id)
    {
        using (IDbConnection connection = new SqlConnection(configuration.GetConnectionString(\"DBConnection\")))
        {
            var result = await connection.ExecuteAsync(ContactQueries.DeleteContact, new { ContactId = id });
            
            return result.ToString();
        }
    }

    #endregion
}

• 另外,IUnitOfWork通过创建新类来实现接口UnitOfWork

public class UnitOfWork : IUnitOfWork
{
    public UnitOfWork(IContactRepository contactRepository)
    {
        Contacts = contactRepository;
    }

    public IContactRepository Contacts { get; set; }
}

• 最后,将接口及其实现注册到 .NET Core 服务容器。添加一个新的静态类ServiceCollectionExtension,并通过注入的方式在其下添加 RegisterServices 方法IServiceCollection。
• 稍后,我们将在 API 的ConfigureService 方法下注册它。

public static class ServiceCollectionExtension
{
    public static void RegisterServices(this IServiceCollection services)
    {
        services.AddTransient();
        services.AddTransient();
    }

设置 API 项目: 添加一个新的 .NET 8.0 Web API 项目并将其命名为CleanArch.Api。 

• 添加对项目的引用(Application、Infrastructure和Logging),并添加Swashbuckle.AspNetCore包。
• 设置appsettings.json文件来管理 API 设置并替换ConnectionStrings部分下的 DB 连接字符串。

{
  \"Environment\": \"Development\",
  \"EnvironmentVersion\": \"1.0.0\",
  \"ConnectionStrings\": {
    \"DBConnection\": \"Data Source=; Initial Catalog=; User ID=; PWD=\"
  },
  \"SecretKeys\": {
    \"ApiKey\": \"04577BA6-3E32-456C-B528-E41E20D28D79\",
    \"ApiKeySecondary\": \"6D5D1ABA-4F78-4DD3-A69D-C2D15F2E259A,709C95E7-F59D-4CC4-9638-4CDE30B2FCFD\",
    \"UseSecondaryKey\": true
  },
  \"Logging\": {
    \"LogLevel\": {
      \"Default\": \"Information\",
      \"Microsoft.AspNetCore\": \"Warning\"
    }
  },
  \"AllowedHosts\": \"*\"
}

• 添加 log4net.config 并在其下添加与日志相关的设置。确保将其Copy to Output Directory属性设置为Copy Always。

   
       
           
           
       
   
   
       
       
        <!---->
       
       
       
       
       
           
       
   
   
       
           
       
   
   
       
           
           
       
       
           
           
       
       
           
           
       
       
           
           
       
       
           
       
   
   
       
       
       
       
       
   

• 配置启动设置,例如 RegisterServices(在CleanArch.Infrastructure项目下定义)、配置 log4net 以及添加 Swagger UI(带有身份验证方案)。

using log4net.Config;
using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateBuilder(args);

//Configure Log4net.
XmlConfigurator.Configure(new FileInfo(\"log4net.config\"));

//Injecting services -> defined under CleanArch.Infrastructure project. 
builder.Services.RegisterServices();

// Add services to the container.
builder.Services.AddControllers();

// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();

builder.Services.AddSwaggerGen(c =>
{
    c.AddSecurityDefinition(\"basic\", new OpenApiSecurityScheme
    {
        Description = \"api key.\",
        Name = \"Authorization\",
        In = ParameterLocation.Header,
        Type = SecuritySchemeType.ApiKey,
        Scheme = \"basic\"
    });

    c.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Type = ReferenceType.SecurityScheme,
                    Id = \"basic\"
                },
                In = ParameterLocation.Header
            },
            new List()
        }
    });
});

var app = builder.Build();

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

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

• 删除默认的控制器/模型类并在模型下添加一个新类(ApiResponse),以管理 API 响应的通用响应格式。

public class ApiResponse
{
    public bool Success { get; set; }
    public string? Message { get; set; }
    public T? Result { get; set; }

• 添加一个新的控制器并将其命名为AuthController,以实现未授权的实现,因为我们将使用基于密钥的身份验证。

[Produces(\"application/json\")]
[Route(\"api/[controller]\")]
[ApiExplorerSettings(IgnoreApi = true)]
public class AuthController : Controller
{
    [HttpGet]
    public IActionResult NotAuthorized()
    {
        return Unauthorized();
    }

• 添加AuthorizationFilter,如下所示,以管理基于 API 密钥的身份验证。
        • 这允许基于主密钥和辅助密钥进行身份验证。
        • 我们可以添加多个辅助键,并且可以打开或关闭它们的使用appsettings。
        • 这将有助于保证我们的主密钥安全,并根据需要将辅助密钥分发给不同的客户端。

public class AuthorizationFilterAttribute : Attribute, IAuthorizationFilter
{
    private readonly string _apiKey;
    private readonly string _apiKeySecondary;
    private readonly bool _canUseSecondaryApiKey;
    
    public AuthorizationFilterAttribute(IConfiguration configuration)
    {
        _apiKey = configuration[\"SecretKeys:ApiKey\"];
        _apiKeySecondary = configuration[\"SecretKeys:ApiKeySecondary\"];
        _canUseSecondaryApiKey = configuration[\"SecretKeys:UseSecondaryKey\"] == \"True\";
    }

    public void OnAuthorization(AuthorizationFilterContext context)
    {
        var apiKeyHeader = context.HttpContext.Request.Headers[\"Authorization\"].ToString();
        var authController = new Controllers.AuthController();

        if (apiKeyHeader.Any())
        {
            var keys = new List
            {
                _apiKey
            };

            if (_canUseSecondaryApiKey)
            {
                keys.AddRange(_apiKeySecondary.Split(\',\'));
            }

            if (keys.FindIndex(x => x.Equals(apiKeyHeader, StringComparison.OrdinalIgnoreCase)) == -1)
            {
                context.Result = authController.NotAuthorized();
            }
        }
        else
        {
            context.Result = authController.NotAuthorized();
        }
    }
}

• 添加一个新的控制器并将其命名为BaseApiController,该控制器将包含通用实现并将作为所有其他 API 控制器的基础控制器。

[Route(\"api/[controller]\")]
[TypeFilter(typeof(AuthorizationFilterAttribute))]
[ApiController]
public class BaseApiController : ControllerBase
{
}

• 最后,通过注入对象类型IUnitOfWork并添加所有 CRUD 操作来添加一个新的 API 控制器来公开联系人 API。

public class ContactController : BaseApiController
{
    #region ===[ Private Members ]=============================================================

    private readonly IUnitOfWork _unitOfWork;

    #endregion

    #region ===[ Constructor ]=================================================================

    ///

    /// Initialize ContactController by injecting an object type of IUnitOfWork
    ///

    public ContactController(IUnitOfWork unitOfWork)
    {
        this._unitOfWork = unitOfWork;
    }

    #endregion

    #region ===[ Public Methods ]==============================================================

    [HttpGet]
    public async Task<ApiResponse<List>> GetAll()
    {
        var apiResponse = new ApiResponse<List>();

        try
        {
            var data = await _unitOfWork.Contacts.GetAllAsync();
            apiResponse.Success = true;
            apiResponse.Result = data.ToList();
        }
        catch (SqlException ex)
        {
            apiResponse.Success = false;
            apiResponse.Message = ex.Message;
            Logger.Instance.Error(\"SQL Exception:\", ex);
        }
        catch (Exception ex)
        {
            apiResponse.Success = false;
            apiResponse.Message = ex.Message;
            Logger.Instance.Error(\"Exception:\", ex);
        }

        return apiResponse;
    }

    [HttpGet(\"{id}\")]
    public async Task<ApiResponse> GetById(int id)
    {

        var apiResponse = new ApiResponse();

        try
        {
            var data = await _unitOfWork.Contacts.GetByIdAsync(id);
            apiResponse.Success = true;
            apiResponse.Result = data;
        }
        catch (SqlException ex)
        {
            apiResponse.Success = false;
            apiResponse.Message = ex.Message;
            Logger.Instance.Error(\"SQL Exception:\", ex);
        }
        catch (Exception ex)
        {
            apiResponse.Success = false;
            apiResponse.Message = ex.Message;
            Logger.Instance.Error(\"Exception:\", ex);
        }

        return apiResponse;
    }

    [HttpPost]
    public async Task<ApiResponse> Add(Contact contact)
    {
        var apiResponse = new ApiResponse();

        try
        {
            var data = await _unitOfWork.Contacts.AddAsync(contact);
            apiResponse.Success = true;
            apiResponse.Result = data;
        }
        catch (SqlException ex)
        {
            apiResponse.Success = false;
            apiResponse.Message = ex.Message;
            Logger.Instance.Error(\"SQL Exception:\", ex);
        }
        catch (Exception ex)
        {
            apiResponse.Success = false;
            apiResponse.Message = ex.Message;
            Logger.Instance.Error(\"Exception:\", ex);
        }

        return apiResponse;
    }

    [HttpPut]
    public async Task<ApiResponse> Update(Contact contact)
    {
        var apiResponse = new ApiResponse();

        try
        {
            var data = await _unitOfWork.Contacts.UpdateAsync(contact);
            apiResponse.Success = true;
            apiResponse.Result = data;
        }
        catch (SqlException ex)
        {
            apiResponse.Success = false;
            apiResponse.Message = ex.Message;
            Logger.Instance.Error(\"SQL Exception:\", ex);
        }
        catch (Exception ex)
        {
            apiResponse.Success = false;
            apiResponse.Message = ex.Message;
            Logger.Instance.Error(\"Exception:\", ex);
        }

        return apiResponse;
    }

    [HttpDelete]
    public async Task<ApiResponse> Delete(int id)
    {
        var apiResponse = new ApiResponse();

        try
        {
            var data = await _unitOfWork.Contacts.DeleteAsync(id);
            apiResponse.Success = true;
            apiResponse.Result = data;
        }
        catch (SqlException ex)
        {
            apiResponse.Success = false;
            apiResponse.Message = ex.Message;
            Logger.Instance.Error(\"SQL Exception:\", ex);
        }
        catch (Exception ex)
        {
            apiResponse.Success = false;
            apiResponse.Message = ex.Message;
            Logger.Instance.Error(\"Exception:\", ex);
        }

        return apiResponse;
    }

    #endregion
}

设置测试项目: 添加一个新的 MSTest 测试项目并命名它CleanArch.Test并添加以下包。

Install-Package Microsoft.Extensions.Configuration
Install-Package MSTest.TestFramework
Install-Package MSTest.TestAdapter
Install-Package Moq

• 之后创建一个新类ContactControllerShould并设置所有可能的测试用例,检查CleanArch.Test项目的代码以进一步了解。

• 在解决方案资源管理器中查看项目结构。

构建并运行测试用例:

构建解决方案并运行代码覆盖率,这将运行所有测试用例并向您显示测试代码覆盖率。

运行并测试 API:

运行项目并测试所有 CRUD API 方法。(确保CleanArch.Api将其设置为启动项目)

Swagger 用户界面

未经身份验证运行 API 会引发错误。

添加 API 授权。

POST——添加新记录。

GET——获取所有记录。

PUT——更新现有记录。

GET——获取单条记录。

删除——删除现有记录。

好了,本文到此结束。

如果您喜欢此文章,请收藏、点赞、评论,谢谢,祝您快乐每一天。