在asp.net core api中为复杂类型保留相同的rest端点

问题描述 投票:1回答:2

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

http://api/tags

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

[{
   "TagName" : "IntegerTag",
   "DataType" : 1,
   "IsRequired" : true
}]

如果我想维护相同的端点来创建新的标签,但使用不同的json格式。让我们说我想创建一个ListTag

[{
   "TagName" : "ListTag",
   "DataType" : 5,
   "Values" : ["Value1", "Value2", "Value3"]
   "IsRequired" : true
}]]

或者RangeTag

[{
   "TagName" : "RangeTag",
   "DataType" : 6,
   "Min": 1,
   "Max": 10,
   "IsRequired" : true
}]

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

void CreateTags(TagForCreateDto1 dto){…}

void CreateTags(TagForCreateDto2 dto){…}

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

[HttpPost]
void CreateTags(TagForCreateDto1 dto){…}
[HttpPost]
void CreateTags(TagForCreateDto2 dto){…}

处理请求时发生未处理的异常。 AmbiguousActionException:匹配多个动作。以下操作匹配路由数据并满足所有约束。

请指教

c# rest asp.net-core asp.net-core-webapi restful-url
2个回答
3
投票

实现你想要的一种方法,拥有一个POST endpoint,同时能够发布Tags的不同“版本”是通过创建一个自定义JsonConverter

基本上,既然你已经有了一个属性DataType,它可以用来识别它是哪种类型的Tag,很容易将它序列化为正确的类型。所以,在代码中它看起来像这样:

BaseTag> ListTagRangeTag

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; }
}

然后,自定义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();
        }
    }
}

繁重的工作是用ReadJsonCreate方法完成的。 Create收到一个JObject,并在里面检查DataType财产,以确定它是哪种类型的Tag。然后,ReadJson继续在Populate上调用JsonSerializer为适当的Type

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

[JsonConverter(typeof(PolymorphicTagJsonConverter))]
public class BaseTag 
{ 
   // the same as before
}

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

[HttpPost]
public IActionResult Post(ICollection<BaseTag> tags)
{
    return Ok(tags);
}

一个缺点是转换器上的switch。你可能没关系。你可以做一些聪明的工作,并尝试让标签类以某种方式实现一些接口,这样你就可以在Create上调用BaseTag,它会在运行时将调用转发给正确的,但是我想你可以开始这个,如果复杂性增加,那么你可以考虑更聪明/更自动的方式找到正确的Tag类。


2
投票

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

public interface ITagsFactory
{
    string CreateTags(int dataType, string jsonInput);
}

创建一个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);
        }
    }
}

对于更多关注点的分离,您可以将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
        };
    }
}

工作差不多完成了。但是,为了从正文中读取原始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);
        }
    }
}

在Startup中注册formatter和TagsFactory,如下所示:

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

这样,您的终端将保持不变。如果您需要添加更多TagType,您只需要将该案例添加到TagsFactory。您可能认为这是违反OCP的。但是,工厂需要知道需要创建哪种对象。如果你想更多地抽象它,你可以使用AbstractFactory,但我认为这将是过度的。

© www.soinside.com 2019 - 2024. All rights reserved.