ASP.NET Core MiniAPI中 EndPoint相关

发布时间 2023-12-30 17:49:26作者: JohnYang819

1.状态码返回之演化之路

1.1最基本的就是用Results或者TypedResults返回带有状态码的响应(可选Json响应体)

      app.MapGet("/fruit/{id}", (string id) =>
      {
          if (_fruit.TryGetValue(id, out Fruit fruit))
          {
              return Results.Ok(fruit);
          }
          else
          {
              return Results.NotFound();
          }
      });

            app.MapPost("/fruit/{id}", (string id, Fruit fruit) =>
            {
                if (_fruit.TryAdd(id, fruit))
                {
                    return TypedResults.Created($"/fruit/{id}", fruit);
                }
                else
                {
                    return Results.BadRequest(new { id = "A fruit with id already exists" });
                }
            });





可选响应体

app.MapGet("/fruit/{id}", (string id) =>
{
    if (_fruit.TryGetValue(id, out Fruit fruit))
    {
        return Results.Ok(fruit);
    }
    else
    {
        return Results.NotFound(new {id="暂无发现"});
    }
});

1.2 用Problem Details返回有用的错误。

1.1的方案有个小瑕疵,就是对于错误响应没有一个统一的格式,因此可以用**Problem Details"来有个统一的格式描述错误。
Problem Details有两个API:Results.Problem(TypedResults.Problem)Results.ValidationProblem(TypedResults.ValidationProblem)ProblemValidationProblem的区别就是前者是默认是500错误码,后者默认是400错误码,前者也可以填入参数statusCode来指明自定义错误码,如400,后者还需要填入Dictionary<string,string[]>类型的参数。

 app.MapGet("/fruit/{id}", (string id) =>
 {
     if (_fruit.TryGetValue(id, out Fruit fruit))
     {
         return Results.Ok(fruit);//或者TypedResults.Ok(fruit);
     }
     else
     {
         return Results.Problem("暂无",statusCode:404);
     }
 });

app.MapPost("/fruit/{id}", (string id, Fruit fruit) =>
{
    if (_fruit.TryAdd(id, fruit))
    {
        return TypedResults.Created($"/fruit/{id}", fruit);
    }
    else
    {
        return Results.ValidationProblem(new Dictionary<string, string[]>
        {
            ["id"] = new[] { "A fruit with this id already exists" },
        });
    }
});



可见,确实统一了格式!

1.3将所有错误转为Problem Details

在1.2中,我们仅仅是在我们可控的endpoint中使用ProblemValidationProblem,转为统一的Problem Details,问题是并不是所有的错误都仅发生在endpoint中,也可能发生在中间件中,也有可能是未知的异常。现在我们要将所有错误统一输出为Problem Detials。
错误分为异常和错误状态码两种:

1.3.1 将异常转为Problem Details

只需要builder.Services.AddProblemDetails()方法来注册服务,并app.UseExceptionHandler()即可.
以下是没有使用该方法及其反应:

app.MapGet("/error", void () => throw new NotImplementedException());

以下是使用该方法及其反应

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails();//注册服务
var app = builder.Build();
app.UseExceptionHandler();//使用异常处理中间件
app.MapGet("/error", void () => throw new NotImplementedException());

1.3.2 将错误状态码转为Problem Details

还是必须先注册builder.Services.AddProblemDetails(),然后使用app.UseStatusCodePages()
这样的话,如果任何进入该中间件的带有错误码的且无响应体的响应将会自动被加入Problem Details响应体.

builder.Services.AddProblemDetails();//注册服务
app.UseStatusCodePages();
app.MapGet("/fruit/{id}", (string id) =>
{
    if (_fruit.TryGetValue(id, out Fruit fruit))
    {
        return Results.Ok(fruit);
    }
    else
    {
        return Results.NotFound();
    }
});

2返回其他数据类型

  • Results.File()
  • Results.Byte()
  • Results.Stream()

3 endpoint filters

endpoint filters工作流程与中间件非常相似,都是流水线式请求进入,响应出来;都可以对请求进行短路;都可以进行logging,exception handle;
但也有非常明显的不同:中间件是对所有请求起作用的,endpoint filters,顾名思义,只对特定的请求起作用;endpoint filters可以访问到下一层级穿过来的result,而中间件不能。

class ValidationHelper
{
    internal static async ValueTask<object?> ValidateId(EndpointFilterInvocationContext context,EndpointFilterDelegate next)
    {
        var id = context.GetArgument<string>(0);
        if(String.IsNullOrEmpty(id) || !id.StartsWith("f"))
        {
            return Results.ValidationProblem(new Dictionary<string, string[]>
            {
                ["id"] = new[] { "Invalid format.Id must start with f" },
            });
        }
        return await next(context);
    }
}
app.MapGet("/fruit/{id}", (string id) => _fruit.TryGetValue(id, out var fruit) ?
TypedResults.Ok(fruit) : Results.Problem(statusCode: 404))
    .AddEndpointFilter(ValidationHelper.ValidateId)
    .AddEndpointFilter(async (context, next) =>
    {
        app.Logger.LogInformation("====Executing filter...");
        object? result = await next(context);
        app.Logger.LogInformation($"===Handler result:{result}");
        return result;
    });

直接短路:

f开头,不存在:

正常访问到:

将该Filter应用到post上

app.MapPost("/fruit/{id}", (string id, Fruit fruit) =>
{
    if (_fruit.TryAdd(id, fruit))
    {
        return TypedResults.Created($"/fruit/{id}", fruit);
    }
    else
    {
        return Results.BadRequest(new { id = "A fruit with id already exists" });
    }
}).AddEndpointFilter(ValidationHelper.ValidateId);

成功拦截!

3.1 Filter Factory

上面的Filter有个小瑕疵,就是严格依赖参数顺序,但是对于MapPost来说,参数顺序却是随意的,
比如app.MapPost("/fruit/{id}",(stirng id,Fruit fruit)=>{...}),和app.MapPost("/fruit/{id}",(Fruit fruit,string id)=>{...})都是一样的,
但对于Filter,就是致命的错误。
那么该怎么办呢?Filter Factory的产生就是为了解决这个困境。

class ValidationHelper
{
    internal static async ValueTask<object?> ValidateId(EndpointFilterInvocationContext context,EndpointFilterDelegate next)
    {
        var id = context.GetArgument<string>(0);
        if(String.IsNullOrEmpty(id) || !id.StartsWith("f"))
        {
            return Results.ValidationProblem(new Dictionary<string, string[]>
            {
                ["id"] = new[] { "Invalid format.Id must start with f" },
            });
        }
        return await next(context);
    }
    internal static EndpointFilterDelegate ValidateIdFactory(EndpointFilterFactoryContext context,EndpointFilterDelegate next)
    {
        ParameterInfo[] parameters = context.MethodInfo.GetParameters();
        int? idPosition = null;
        for(int i = 0; i < parameters.Length; i++)
        {
            if (parameters[i].Name=="id" && parameters[i].ParameterType == typeof(string))
            {
                idPosition = i;
                break;
            }
        }
        if (!idPosition.HasValue)
        {
            return next;
        }
        return async (invocationContext) =>
        {
            var id = invocationContext.GetArgument<string>(idPosition.Value);
            if(string.IsNullOrEmpty(id) || !id.StartsWith("f"))
            {
                return Results.ValidationProblem(new Dictionary<string, string[]>
                {
                    ["id"] = new[] {"Id must start with f"}, 
                });
            }
            return await next(invocationContext);
        };
    }
}

app.MapPost("/fruit/{id}", ( Fruit fruit, string id) =>
{
    if (_fruit.TryAdd(id, fruit))
    {
        return TypedResults.Created($"/fruit/{id}", fruit);
    }
    else
    {
        return Results.BadRequest(new { id = "A fruit with id already exists" });
    }
}).AddEndpointFilterFactory(ValidationHelper.ValidateIdFactory);

3.2 IEndpointFilter接口

每次手敲ValidationHelper中的方法,很恼火,那么用这个接口,可以充分利用vs的智能补全。

 class IdValidattionFilter : IEndpointFilter
 {
    //下面的async需要自己加上,不知道是不是vs智能补全的bug
     public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
     {
         var id = context.GetArgument<string>(0);
         if (String.IsNullOrEmpty(id) || !id.StartsWith("f"))
         {
             return Results.ValidationProblem(new Dictionary<string, string[]>
             {
                 ["id"] = new[] { "Invalid format.Id must start with f" },
             });
         }
         return await next(context);
     }
 }

app.MapPost("/fruit/{id}", ( Fruit fruit, string id) =>
{
    if (_fruit.TryAdd(id, fruit))
    {
        return TypedResults.Created($"/fruit/{id}", fruit);
    }
    else
    {
        return Results.BadRequest(new { id = "A fruit with id already exists" });
    }
}).AddEndpointFilter< IdValidattionFilter>();//不传实例,只传类型就行。

4.用路由组route group 来组织API

MapGet("/fruit/{id}",(string id)=>{...});
MapPost("/fruit/{id}",(string id)=>{...});
MapPut("/fruit/{id}",(string id)=>{...});
MapDelete("/fruit/{id}",(string id)=>{...});

对于以上4个endpoint,都需要验证id,我们可以用filter来验证,但是还要一个一个的添加,也还是非常繁琐,那么route group就是为了应对这种情况。

 var builder = WebApplication.CreateBuilder(args);
 builder.Services.AddProblemDetails();//注册服务
 var app = builder.Build();
 app.UseExceptionHandler();//使用异常处理中间件
 app.UseStatusCodePages();
 var _fruit=new ConcurrentDictionary<string , Fruit>()
 {
   
 };

RouteGroupBuilder fruitApi = app.MapGroup("/fruit");//MapGrtoup可以嵌套,即MapGroup(xxx).MapGroup(xxxx).MapGroup()...
fruitApi.MapGet("/",()=>_fruit);
RouteGroupBuilder fruiApiWithValidation = fruitApi.AddEndpointFilterFactory(ValidationHelper.ValidateIdFactory);
fruiApiWithValidation.MapGet("/{id}", (string id) => _fruit.TryGetValue(id, out var fruit) ?
TypedResults.Ok(fruit) : Results.Problem(statusCode: 404))
    //.AddEndpointFilter(ValidationHelper.ValidateId)
    .AddEndpointFilter(async (context, next) =>
    {
        app.Logger.LogInformation("====Executing filter...");
        object? result = await next(context);
        app.Logger.LogInformation($"===Handler result:{result}");
        return result;
    });

 fruiApiWithValidation.MapPost("/{id}", ( Fruit fruit, string id) =>
 {
     if (_fruit.TryAdd(id, fruit))
     {
         return TypedResults.Created($"/fruit/{id}", fruit);
     }
     else
     {
         return Results.BadRequest(new { id = "A fruit with id already exists" });
     }
 });

 fruiApiWithValidation.MapPut("/{id}", (string id, Fruit fruit) =>
 {
     _fruit[id] = fruit;
     return Results.NoContent();
 });
 fruiApiWithValidation.MapDelete("/{id}", (string id) =>
 {
     _fruit.TryRemove(id, out _);
     return Results.NoContent();
 });

 app.MapGet("/teapot", (HttpResponse res) =>
 {
     res.StatusCode = 428;
     res.ContentType = MediaTypeNames.Text.Plain;
     return res.WriteAsync("tea pot!");
 });
 app.MapGet("/error", void () => throw new NotImplementedException());
 app.Run();