我有一个Rest端点,我们称之为标签
它创建传递此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:匹配多个动作。以下操作匹配路由数据并满足所有约束。
请指教
实现你想要的一种方法,拥有一个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; }
}
然后,自定义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();
}
}
}
繁重的工作是用ReadJson
和Create
方法完成的。 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
类。
您可以利用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,但我认为这将是过度的。