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

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

示例代码:https://download.youkuaiyun.com/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<T> where T : class
{
    Task<IReadOnlyList<T>> GetAllAsync();
    Task<T> GetByIdAsync(long id);
    Task<string> AddAsync(T entity);
    Task<string> UpdateAsync(T entity);
    Task<string> DeleteAsync(long id);
}

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

// IContactRepository.cs file
public interface IContactRepository : IRepository<Contact>
{
}

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

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

/// <summary>
/// Logger class contract.
/// </summary>
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<Logger> _loggerInstance = new Lazy<Logger>(() => 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 ]==================================================================

    /// <summary>
    /// Gets the Logger instance.
    /// </summary>
    public static Logger Instance
    {
        get { return _loggerInstance.Value; }
    }

    #endregion

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

    /// <summary>
    /// Logs a message object with the log4net.Core.Level.Debug level.
    /// </summary>
    /// <param name="message"></param>
    public void Debug(object message)
    {
        if (_logger.IsDebugEnabled)
            _logger.Debug(message);
    }

    /// <summary>
    /// Logs a message object with the log4net.Core.Level.Info level.
    /// </summary>
    /// <param name="message"></param>
    public void Info(object message)
    {
        if (_logger.IsInfoEnabled)
            _logger.Info(message);
    }

    /// <summary>
    /// Logs a message object with the log4net.Core.Level.Info Warning.
    /// </summary>
    /// <param name="message"></param>
    public void Warn(object message)
    {
        if (_logger.IsWarnEnabled)
            _logger.Warn(message);
    }

    /// <summary>
    /// Logs a message object with the log4net.Core.Level.Error level.
    /// </summary>
    /// <param name="message"></param>
    public void Error(object message)
    {
        _logger.Error(message);
    }

    /// <summary>
    /// Logs a message object with the log4net.Core.Level.Fatal level.
    /// </summary>
    /// <param name="message"></param>
    public void Fatal(object message)
    {
        _logger.Fatal(message);
    }

    /// <summary>
    /// Logs a message object with the log4net.Core.Level.Debug level including the exception.
    /// </summary>
    /// <param name="message"></param>
    /// <param name="exception"></param>
    public void Debug(object message, Exception exception)
    {
        if (_logger.IsDebugEnabled)
            _logger.Debug(message, exception);
    }

    /// <summary>
    /// Logs a message object with the log4net.Core.Level.Info level including the exception.
    /// </summary>
    /// <param name="message"></param>
    /// <param name="exception"></param>
    public void Info(object message, Exception exception)
    {
        if (_logger.IsInfoEnabled)
            _logger.Info(message, exception);
    }

    /// <summary>
    /// Logs a message object with the log4net.Core.Level.Warn level including the exception.
    /// </summary>
    /// <param name="message"></param>
    /// <param name="exception"></param>
    public void Warn(object message, Exception exception)
    {
        if (_logger.IsWarnEnabled)
            _logger.Info(message, exception);
    }

    /// <summary>
    /// Logs a message object with the log4net.Core.Level.Error level including the exception.
    /// </summary>
    /// <param name="message"></param>
    /// <param name="exception"></param>
    public void Error(object message, Exception exception)
    {
        _logger.Error(message, exception);
    }

    /// <summary>
    /// Logs a message object with the log4net.Core.Level.Fatal level including the exception.
    /// </summary>
    /// <param name="message"></param>
    /// <param name="exception"></param>
    public void Fatal(object message, Exception exception)
    {
        _logger.Fatal(message, exception);
    }

    /// <summary>
    /// Log an exception with the log4net.Core.Level.Error level including the stack trace of the System.Exception passed as a parameter.
    /// </summary>
    /// <param name="exception"></param>
    public void Error(Exception exception)
    {
        _logger.Error(SerializeException(exception, ExceptionName));
    }

    /// <summary>
    /// Log an exception with the log4net.Core.Level.Fatal level including the stack trace of the System.Exception passed as a parameter.
    /// </summary>
    /// <param name="exception"></param>
    public void Fatal(Exception exception)
    {
        _logger.Fatal(SerializeException(exception, ExceptionName));
    }

    #endregion

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

    /// <summary>
    /// Serialize Exception to get the complete message and stack trace.
    /// </summary>
    /// <param name="exception"></param>
    /// <returns></returns>
    public static string SerializeException(Exception exception)
    {
        return SerializeException(exception, string.Empty);
    }

    #endregion

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

    /// <summary>
    /// Serialize Exception to get the complete message and stack trace.
    /// </summary>
    /// <param name="ex"></param>
    /// <param name="exceptionMessage"></param>
    /// <returns></returns>
    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<Contact>> GetAllAsync()
    {
        using (IDbConnection connection = new SqlConnection(configuration.GetConnectionString("DBConnection")))
        {
            var result = await connection.QueryAsync<Contact>(ContactQueries.AllContact);
            
            return result.ToList();
        }
    }

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

    public async Task<string> 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<string> 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<string> 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<IContactRepository, ContactRepository>();
        services.AddTransient<IUnitOfWork, UnitOfWork>();
    }

设置 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=<server-name>; Initial Catalog=<db-name>; User ID=<user-name>; PWD=<password>"
  },
  "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。

<?xml version="1.0" encoding="utf-8" ?>
<log4net>
    <appender name="Console" type="log4net.Appender.ConsoleAppender">
        <layout type="log4net.Layout.PatternLayout">
            <!-- Pattern to output the caller's file name and line number -->
            <conversionPattern value="%date %5level %logger.%method [%line] - MESSAGE: %message%newline %exception" />
        </layout>
    </appender>
    <appender name="RollingFile" type="log4net.Appender.RollingFileAppender">
        <file type="log4net.Util.PatternString" value="C:\Logs\CleanArch_API\Log.%property{log4net:HostName}.log" />
        <!-- If you don't have permission to write logs on C drive, then you can test by writing in api bin folder. -->
        <!--<file type="log4net.Util.PatternString" value="bin\Logs\Log.%property{log4net:HostName}.log" />-->
        <appendToFile value="true" />
        <rollingStyle value="Size" />
        <maxSizeRollBackups value="5" />
        <maximumFileSize value="10MB" />
        <layout type="log4net.Layout.PatternLayout">
            <conversionPattern value="%date %5level %logger.%method [%line] - MESSAGE: %message%newline %exception" />
        </layout>
    </appender>
    <appender name="TraceAppender" type="log4net.Appender.TraceAppender">
        <layout type="log4net.Layout.PatternLayout">
            <conversionPattern value="%date %5level %logger.%method [%line] - MESSAGE: %message%newline %exception" />
        </layout>
    </appender>
    <appender name="ConsoleAppender" type="log4net.Appender.ManagedColoredConsoleAppender">
        <mapping>
            <level value="ERROR" />
            <foreColor value="Red" />
        </mapping>
        <mapping>
            <level value="WARN" />
            <foreColor value="Yellow" />
        </mapping>
        <mapping>
            <level value="INFO" />
            <foreColor value="White" />
        </mapping>
        <mapping>
            <level value="DEBUG" />
            <foreColor value="Green" />
        </mapping>
        <layout type="log4net.Layout.PatternLayout">
            <conversionPattern value="%date %5level %logger.%method [%line] - MESSAGE: %message%newline %exception" />
        </layout>
    </appender>
    <root>
        <level value="WARN" />
        <appender-ref ref="RollingFile" />
        <appender-ref ref="TraceAppender" />
        <appender-ref ref="ConsoleAppender" />
        <appender-ref ref="JSONFileAppender" />
    </root>
</log4net>

• 配置启动设置,例如 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<string>()
        }
    });
});

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<T>
{
    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<string>
            {
                _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 ]=================================================================

    /// <summary>
    /// Initialize ContactController by injecting an object type of IUnitOfWork
    /// </summary>
    public ContactController(IUnitOfWork unitOfWork)
    {
        this._unitOfWork = unitOfWork;
    }

    #endregion

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

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

        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<Contact>> GetById(int id)
    {

        var apiResponse = new ApiResponse<Contact>();

        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<string>> Add(Contact contact)
    {
        var apiResponse = new ApiResponse<string>();

        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<string>> Update(Contact contact)
    {
        var apiResponse = new ApiResponse<string>();

        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<string>> Delete(int id)
    {
        var apiResponse = new ApiResponse<string>();

        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——获取单条记录。

删除——删除现有记录。

好了,本文到此结束。

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

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hefeng_aspnet

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值