Сохранение одинаковой конечной точки отдыха для сложных типов в API ядра asp.net - PullRequest
1 голос
/ 30 апреля 2019

У меня есть конечная точка отдыха, давайте называть ее тегами

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 # для создания нового Dto на API моего контроллера и передачи его в качестве другого параметра, потому что C # допускает перегрузки методов для этого:

void CreateTags(TagForCreateDto1 dto){…}

void CreateTags(TagForCreateDto2 dto){…}

Но когда мне нужно поддерживать в одном контроллере оба метода с запросом POST для создания тегов, mvc не позволяет одному и тому же маршруту иметь оба.

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

Произошло необработанное исключение при обработке запроса. AmbiguousActionException: несколько действий совпадают. Следующие действия соответствовали данным маршрута и были выполнены все ограничения.

Пожалуйста, сообщите

Ответы [ 2 ]

3 голосов
/ 01 мая 2019

Один из способов добиться того, что вы хотите, имея один 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 классов.

2 голосов
/ 01 мая 2019

Вы можете воспользоваться шаблоном Factory, который будет возвращать теги, которые вы хотите создать, основываясь на входных данных JSON. Создайте фабрику, назовите ее TagsFactory, которая реализует следующий интерфейс:

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

Создайте фабрику тегов, как показано ниже:

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]

Ваш контроллер будет выглядеть как показано ниже, используя ITagsFactory, введенный через DI

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

Зарегистрируйте форматер и TagsFactory в автозагрузке, как показано ниже:

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

Таким образом, ваша конечная точка останется прежней. Если вам нужно добавить больше TagTypes, вам просто нужно добавить этот случай в TagsFactory. Вы, вероятно, можете подумать, что это нарушение OCP. Однако Фабрике нужно знать, какой объект ей нужно создать. Если вам нравится больше абстрагироваться, вы можете использовать AbstractFactory, но я думаю, это было бы излишним.

...