《Asp.Net Core3 + Vue3入坑教程》 – 6.异常处理与UserFriendlyException

简介

《Asp.Net Core3 + Vue3入坑教程》 此教程适合新手入门或者前后端分离尝试者。可以根据图文一步一步进操作编码也可以选择直接查看源码。每一篇文章都有对应的源码

目录

《Asp.Net Core3 + Vue3入坑教程》系列教程目录

Asp.Net Core后端项目

  1. 后端项目搭建与Swagger配置步骤
  2. 配置CROS策略解决跨域问题
  3. AutoMapper & Restful API & DI
  4. EF Core & Postgresql
  5. .Net Core 3升级成 .Net 5 & JWT
  6. (本文)异常处理与UserFriendlyException

Vue3 前端项目

  1. 使用vue-cli创建vue项目
  2. (暂未发表敬请期待…)使用Ant Design of Vue编写页面 & vue-router 初试
  3. (暂未发表敬请期待…)将Antd导航菜单与vue-router绑定
  4. (暂未发表敬请期待…) 保存用户登入状态vuex初试

本文简介

本文为《Asp.Net Core3 + Vue3入坑教程》系列教程的后端第六篇 – 异常处理与UserFriendlyException上文已经为Simple项目升级了SDK并且应用了JWT,本文继续为Simple项目增加异常处理与使用友好异常(UserFriendlyException)。

为什么需要使用友好异常的方式进行开发呢?

在很多情况下,我们在一个方法中往往包含着校验参数返回结果两个动作,这时候我们的返回结果就需要考虑用对象来包裹校验结果返回结果。 如果我们使用友好异常,默认方法能顺利通过校验并返回正确的结果,如果校验出现失败的情况则将失败原因通过友好异常的方式返回给调用者,可以让方法的返回内容不需要考虑校验的结果,代码更简洁明了!

用户友好参照了开源项目ABP项目 //docs.abp.io/zh-Hans/abp/latest/Exception-Handling

异常处理与UserFriendlyException

第一步先增加测试代码,修改SqlCommanderRepo.cs

代码调整如下:

using Simple_Asp.Net_Core.Models;
using Simple_Asp.Net_Core.ServiceProvider;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Simple_Asp.Net_Core.Data
{
    public class SqlCommanderRepo : ICommanderRepo
    {
        private readonly CommanderContext _context;

        public SqlCommanderRepo(CommanderContext context)
        {
            _context = context;
        }

        public void CreateCommand(Command cmd)
        {
            if (cmd == null)
            {
                throw new ArgumentNullException(nameof(cmd));
            }

            _context.Commands.Add(cmd);
        }

        public void DeleteCommand(Command cmd)
        {
            if (cmd == null)
            {
                throw new ArgumentNullException(nameof(cmd));
            }
            _context.Commands.Remove(cmd);
        }

        public IEnumerable<Command> GetAllCommands()
        {
            return _context.Commands.ToList();
        }

        public Command GetCommandById(int id)
        {
            if (id == 0)
                throw new Exception("id不能为0!");

            return _context.Commands.First(p => p.Id == id);
        }

        public bool SaveChanges()
        {
            return (_context.SaveChanges() >= 0);
        }

        public void UpdateCommand(Command cmd)
        {
            //Nothing
        }
    }
}

运行项目,调用接口api/commands/{id}接口,当请求参数id设置为0时,后端会抛出异常信息。

当前的异常信息将程序内部内容都暴露出来,并且返回信息也不清晰,调用者难以处理。

接着在ServiceProvider文件夹下增加自定义异常类UserFriendlyException.cs

代码如下:

using System;

namespace Simple_Asp.Net_Core.ServiceProvider
{
    public class UserFriendlyException : Exception
    {
        public UserFriendlyException(string message) : base(message)
        {
        }

        public UserFriendlyException(string message, Exception inner) : base(message, inner)
        {
        }
    }
}

在ServiceProvider文件夹下增加类ExceptionHandler.cs,用来处理异常

在捕捉到程序异常的时候需要写入日志方便问题追踪

代码如下:

using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
using System.Text;
using System.Threading.Tasks;

namespace Simple_Asp.Net_Core.ServiceProvider
{
    public class ExceptionHandler
    {
        public static Task ErrorEvent(HttpContext context)
        {
            var feature = context.Features.Get<IExceptionHandlerFeature>();
            var error = feature?.Error;

            if (error.GetType() == typeof(UserFriendlyException))
            {
                SetResponse(context);
                var content = GetApiResponse(error.Message);

                return context.Response.WriteAsync(JsonConvert.SerializeObject(content), Encoding.UTF8);
            }
            else
            {
                // 写入日志
                // error.Message
                // error.StackTrace

                SetResponse(context);
                var content = GetApiResponse("程序发生错误,请联系客服!");
                return context.Response.WriteAsync(JsonConvert.SerializeObject(content), Encoding.UTF8);
            }
        }

        /// <summary>
        /// 解决异常消息返回跨域问题
        /// </summary>
        private static void SetResponse(HttpContext context)
        {
            context.Response.Clear();
            context.Response.Headers.Add("Access-Control-Allow-Origin", "*");
            context.Response.Headers.Add("Access-Control-Allow-Methods", "POST,GET");
            context.Response.ContentType = "application/json";
        }

        /// <summary>
        /// 响应Response
        /// </summary>
        private static ErrorResponse GetApiResponse(string message)
        {
            return new ErrorResponse() { success = false, message = message };
        }

        private class ErrorResponse
        {
            public bool success { get; set; }
            public bool Success { get { return success; } }
            public string message { get; set; }
            public string Message { get { return message; } }
        }
    }
}

调整Startup.cs,增加异常捕捉

代码如下:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Newtonsoft.Json.Serialization;
using Simple_Asp.Net_Core.Data;
using Simple_Asp.Net_Core.ServiceProvider;
using System;

namespace Simple_Asp.Net_Core
{
    public class Startup
    {
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit //go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddJWT();

            services.AddDbContext<CommanderContext>(options =>
                options.UseNpgsql("Host=localhost;Database=postgres;Username=postgres;Password=123456"));

            services.AddCORS();
            services.AddMvc();
            services.AddSwagger();

            services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());

            services.AddScoped<ICommanderRepo, SqlCommanderRepo>();

            services.AddControllers().AddNewtonsoftJson(s =>
            {
                s.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
            });
        }

        // 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.UseSwagger();
                app.UseSwaggerUI(c =>
                {
                    c.SwaggerEndpoint("/swagger/v1/swagger.json", "ApiHelp V1");
                });
            }
            app.UseExceptionHandler(builder => builder.Run(async context => await ExceptionHandler.ErrorEvent(context)));
            app.UseCors("CorsTest");
            app.UseAuthentication();
            app.UseRouting();
            app.UseAuthorization();
            app.UseEndpoints(endpoints => endpoints.MapDefaultControllerRoute());
        }
    }
}

启动项目,调用api/commands/{id}接口,可以看出后端的接口发生了异常,此时的异常比较清晰。

最后我们将SqlCommanderRepo.cs里的异常改为友好异常

再次修改SqlCommanderRepo.cs

代码调整如下:

using Simple_Asp.Net_Core.Models;
using Simple_Asp.Net_Core.ServiceProvider;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Simple_Asp.Net_Core.Data
{
    public class SqlCommanderRepo : ICommanderRepo
    {
        private readonly CommanderContext _context;

        public SqlCommanderRepo(CommanderContext context)
        {
            _context = context;
        }

        public void CreateCommand(Command cmd)
        {
            if (cmd == null)
            {
                throw new ArgumentNullException(nameof(cmd));
            }

            _context.Commands.Add(cmd);
        }

        public void DeleteCommand(Command cmd)
        {
            if (cmd == null)
            {
                throw new ArgumentNullException(nameof(cmd));
            }
            _context.Commands.Remove(cmd);
        }

        public IEnumerable<Command> GetAllCommands()
        {
            return _context.Commands.ToList();
        }

        public Command GetCommandById(int id)
        {
            if (id == 0)
                throw new Exception("id不能为0!");

            return _context.Commands.First(p => p.Id == id);
        }

        public bool SaveChanges()
        {
            return (_context.SaveChanges() >= 0);
        }

        public void UpdateCommand(Command cmd)
        {
            //Nothing
        }
    }
}

最后启动项目,调用api/commands/{id}接口,这时候我们可以得到友好的提示!

总结

本文为Simple项目增加异常处理与使用友好异常(UserFriendlyException),在捕捉到程序异常的时候需要写入日志方便问题追踪!

目前Simple项目还未使用日志组件,后续会补上

异常捕捉为了能够将异常内容进行收集,并且能以统一的方式返回给客户端,保证服务器的安全、帮助我们追踪问题并且客户端的体验也能有所保证。

异常捕捉结合友好异常的方式能够为我们减少代码量,并且让代码更直观明了,推荐大家一试

GitHub源码

注意:源码调试过程中如果出现xml文件路径错误,需要参照第一章(后端项目搭建与Swagger配置步骤)Swagger配置“配置XML 文档文件”步骤,取消勾选然后再选中 ,将XML路径设置成与你的电脑路径匹配!

//github.com/Impartsoft/Simple_Asp.Net_Core/tree/master/Simple_Asp.Net_Core 6.Exception Handling %26 UserFriendlyException

参考资料

ABP开源项目异常处理 //docs.abp.io/zh-Hans/abp/latest/Exception-Handling