在 asp.net core api 中为复杂类型保持相同的休息端点

Hei*_*ich 2 c# rest restful-url asp.net-core asp.net-core-webapi

我有一个 Rest 端点,我们称之为标签

\n\n

http://api/标签

\n\n

它创建传递此 json 格式的标签对象:

\n\n
[{\n   "TagName" : "IntegerTag",\n   "DataType" : 1,\n   "IsRequired" : true\n}]\n
Run Code Online (Sandbox Code Playgroud)\n\n

如果我想维护相同的端点来创建新标签但具有不同的 json 格式。假设我想创建一个 ListTag

\n\n
[{\n   "TagName" : "ListTag",\n   "DataType" : 5,\n   "Values" : ["Value1", "Value2", "Value3"]\n   "IsRequired" : true\n}]]\n
Run Code Online (Sandbox Code Playgroud)\n\n

或范围标签

\n\n
[{\n   "TagName" : "RangeTag",\n   "DataType" : 6,\n   "Min": 1,\n   "Max": 10,\n   "IsRequired" : true\n}]\n
Run Code Online (Sandbox Code Playgroud)\n\n

我在 C# 上在控制器 api 上创建一个新的 Dto 并将其作为不同的参数传递时没有任何问题,因为 C# 允许方法重载:

\n\n
void CreateTags(TagForCreateDto1 dto){\xe2\x80\xa6}\n\nvoid CreateTags(TagForCreateDto2 dto){\xe2\x80\xa6}\n
Run Code Online (Sandbox Code Playgroud)\n\n

但是,当我需要在同一个控制器中维护两种带有 POST 请求的方法来创建标签时,mvc 不允许同一路由同时拥有这两种方法。

\n\n
[HttpPost]\nvoid CreateTags(TagForCreateDto1 dto){\xe2\x80\xa6}\n[HttpPost]\nvoid CreateTags(TagForCreateDto2 dto){\xe2\x80\xa6}\n
Run Code Online (Sandbox Code Playgroud)\n\n
\n

处理请求时发生未处理的异常。\n AmbigouslyActionException: 多个操作匹配。以下操作与路线数据匹配并满足所有约束。

\n
\n\n

请指教

\n

jpg*_*ssi 5

POST endpoint实现您想要的目标的一种方法Tags是创建一个自定义JsonConverter.

基本上,由于您已经拥有一个DataType可用于识别Tag其类型的属性,因此很容易将其序列化为正确的类型。所以,在代码中它看起来像这样:

BaseTag> ListTag,RangeTag

public class BaseTag
{
    public string TagName { get; set; }

    public int DataType { get; set; }

    public bool IsRequired { get; set; }
}

public sealed class ListTag : BaseTag
{
    public ICollection<string> Values { get; set; }
}

public sealed class RangeTag: BaseTag
{
    public int Min { get; set; }

    public int Max { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

那么,习俗PolymorphicTagJsonConverter

public class PolymorphicTagJsonConverter : JsonConverter
{
    public override bool CanWrite => false;

    public override bool CanConvert(Type objectType) 
        => typeof(BaseTag).IsAssignableFrom(objectType);

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        => throw new NotImplementedException();

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader == null) throw new ArgumentNullException("reader");
        if (serializer == null) throw new ArgumentNullException("serializer");
        if (reader.TokenType == JsonToken.Null)
            return null;

        var jObject = JObject.Load(reader);

        var target = CreateTag(jObject);
        serializer.Populate(jObject.CreateReader(), target);
        return target;
    }       

    private BaseTag CreateTag(JObject jObject)
    {
        if (jObject == null) throw new ArgumentNullException("jObject");
        if (jObject["DataType"] == null) throw new ArgumentNullException("DataType");

        switch ((int)jObject["DataType"])
        {
            case 5:
                return new ListTag();
            case 6:
                return new RangeTag();
            default:
                return new BaseTag();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

繁重的工作是通过方法来完成ReadJsonCreateCreate接收一个JObject并在其内部检查该DataType属性以确定Tag其类型。然后,ReadJson只需继续调用Populate适当JsonSerializerType.

您需要告诉框架使用您的自定义转换器:

[JsonConverter(typeof(PolymorphicTagJsonConverter))]
public class BaseTag 
{ 
   // the same as before
}
Run Code Online (Sandbox Code Playgroud)

最后,您可以只拥有一个POST接受所有类型标签的端点:

[HttpPost]
public IActionResult Post(ICollection<BaseTag> tags)
{
    return Ok(tags);
}
Run Code Online (Sandbox Code Playgroud)

一个缺点是switch转换器。你可能同意或不接受它..你可以做一些聪明的工作,并尝试让标签类以某种方式实现一些接口,这样你就可以调用CreateBaseTag,它会在运行时将调用转发到正确的接口,但我猜您可以开始使用此方法,如果复杂性增加,那么您可以考虑采用更智能/更自动的方式来查找正确的Tag类。


Sha*_*san 5

您可以利用工厂模式,该模式将根据 JSON 输入返回您想要创建的标签。创建一个工厂,称之为TagsFactory,它实现以下接口:

public interface ITagsFactory
{
    string CreateTags(int dataType, string jsonInput);
}
Run Code Online (Sandbox Code Playgroud)

创建一个 TagsFactory 如下所示:

public class TagsFactory : ITagsFactory
{
    public string CreateTags(int dataType, string jsonInput)
    {
        switch(dataType)
        {
            case 1:
                var intTagsDto = JsonConvert.DeserializeObject<TagForCreateDto1(jsonInput);
                // your logic to create the tags below
                ...
                var tagsModel = GenerateTags();
                return the JsonConvert.SerializeObject(tagsModel);

            case 5:
                var ListTagsDto = JsonConvert.DeserializeObject<TagForCreateDto2>(jsonInput);
                // your logic to create the tags below
                ...
                var tagsModel = GenerateTags();
                return the JsonConvert.SerializeObject(tagsModel);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

为了更多地分离关注点,您可以将GenerateTags逻辑从工厂移到它自己的类中。

一旦上述内容到位,我建议对您的 TagsController. 将以下参数添加到CreateTags操作中

  • 数据类型或标记名称。使用更容易处理和阅读的内容[FromHeader]
  • jsonInput 并使用读取它[FromBody]

然后,您的控制器将如下所示,利用通过 DI 注入的 ITagsFactory

[Route("api")]
public class TagsController : Controller
{
    private readonly ITagsFactory _tagsFactory;

    public TagsController(ITagsFactory tagsFactory)
    {
        _tagsFactory= tagsFactory;
    }

    [HttpPost]
    [Route("tags")]
    public IActionResult CreateTags([FromHeader(Name = "data-type")] string dataType, [FromBody] string jsonInput)
    {
        var tags = _tagsFactory.CreateTags(dataType, jsonInput);
        
        return new ObjectResult(tags)
        {
            StatusCode = 200
        };
    }
}
Run Code Online (Sandbox Code Playgroud)

工作快完成了。但是,为了从正文中读取原始 JSON 输入,您需要添加CustomInputFormatter并在启动时注册它

public class RawRequestBodyInputFormatter : InputFormatter
{
    public RawRequestBodyInputFormatter()
    {
        SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json"));
    }
    public override bool CanRead(InputFormatterContext context)
    {
        return true;
    }
    public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
    {
        var request = context.HttpContext.Request;
        using (var reader = new StreamReader(request.Body))
        {
            var content = await reader.ReadToEndAsync();
            return await InputFormatterResult.SuccessAsync(content);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

TagsFactory在启动中注册格式化程序和 ,如下所示:

services.AddSingleton<ITagsFactory, TagsFactory>();
services.AddMvc(options =>
{
    options.InputFormatters.Insert(0, new RawRequestBodyInputFormatter());
}
Run Code Online (Sandbox Code Playgroud)

这样你的端点将保持不变。如果您需要添加更多 TagType,只需将该大小写添加到TagsFactory. 你可能会认为这是违反 OCP 的。然而,工厂需要知道它需要创建什么样的对象。如果你想更抽象,你可以使用 AbstractFactory,但我认为这有点矫枉过正了。