kontent-ai / management-sdk-net

Kontent.ai Management .NET SDK
https://www.nuget.org/packages/Kontent.Ai.Management
MIT License
7 stars 31 forks source link

Using strong-typed element in Language Variants #172

Closed mattnield closed 2 years ago

mattnield commented 2 years ago

Motivation

Why is this feature required? What problems does it solve? Being able to retrieve a strong-typed language variant, and/or its elements when processing content items

Proposed solution

Add an extension method to LanguageVariantModel that makes use of the functionality in Kentico.Kontent.Management.Modules.ModelBuilders.ElementModelProvider to return all of the boxed element in an IEnumerable<BaseElement>. Also, possibly exposing the ToElement(...).

Additional context

The reason I came across this issue is that I'm writing an integration with a 3rd party translation service. The service uses a mixture of machine and human translation, so providing to a readable structure is a must. T do this I currently use the Devliery API (DAPI) rather than the Management API (MAPI) and recusrively create a JSON document that presents a hierarchy of a content item and it 'children', focussing on Text, RichText element. This looks similar to the below:

{
  "ItemType": "faq_section",
  "ItemCodename": "test_faq_5c6875b",
  "ItemElements": [
    {
      "ElementCodename": "title",
      "ElementType": "text",
      "ElementValue": "This is a test FAQ"
    },
    {
      "ElementCodename": "questions",
      "ElementType": "modular_content",
      "ElementItems": [
        {
          "ItemType": "faq",
          "ItemCodename": "test_faq__03",
          "ItemElements": [
            {
              "ElementCodename": "answer",
              "ElementType": "rich_text",
              "ElementValue": "\u003Cp\u003E42\u003C/p\u003E"
            },
            {
              "ElementCodename": "question",
              "ElementType": "rich_text",
              "ElementValue": "\u003Cp\u003EWhat is the meaning of life?\u003C/p\u003E"
            }
          ]
        }
      ]
    }
  ]
}

RichText however obviously provides some complications due to modular content and component etc. These cannot be resolved for translation, so I want to use the MAPI to deal with exporting the RichText fields. Possibly if things get easier I can also use MAPI for the entire export, though the rate limit on the MAPI may be prohibitive. Due to how this is working, I don't know the Conent Item's type at design time and dependon on what can be determined at runtime.

I'm using the following code to work things through:

var mapiLanguageVariant = await mapiClient.GetLanguageVariantAsync(
  new LanguageVariantIdentifier(
    Reference.ByCodename(ITEM_CODENAME),
    Reference.ByCodename(LANGUAGE_CODENAME)
  ));
var mapiContentItem = await mapiClient.GetContentItemAsync(
  mapiLanguageVariant.Item
  );
var mapiContentType = await mapiClient.GetContentTypeAsync(
  mapiContentItem.Type
  );

foreach (var mapiTypeElement in mapiContentType.Elements)
{
  switch (mapiTypeElement.Type)
  {
    case ElementMetadataType.RichText:
      // TODO : Something amazing
      var mapiItemElement = (dynamic) mapiLanguageVariant.Elements.FirstOrDefault(element => Guid.Parse(element.element.id) == mapiTypeElement.Id);
      break;
  }
}

As it stands, mapiItemElement is an ExpandoObject, so needs to be cast using (dynamic)mapiItemElement before the components, element, and value properties can be accessed. Copying the ToElement method as mentioned above provides a much smoother and more readable codebase:

var typedElement = CustomElementModelProvider.ToElement((dynamic) mapiItemElement, typeof(RichTextElement));

We now have typedElement.Components 👌 However, being able to do that with the mapiLanguageVariant as an extension method would make this a simpler task. I.e.

IEnumerable<BaseElement> typedElements = mapiLanguageVariant.GetTypedElements(mapiContentType.Elements);

(if we can do it without passing the type in, that's even better!)

gormal commented 2 years ago

Hi, thank you for submitting this issue!

Unfortunately, we need to know the type of the element (it can't be deduced from its shape as it is not unique) in order to create a strongly typed object. One way how we it can be done:

    public async static Task<IEnumerable<BaseElement>> GetStronglyTypedElements(this IManagementClient client, LanguageVariantModel variant)
    {
        var item = await client.GetContentItemAsync(variant.Item);
        var type = await client.GetContentTypeAsync(item.Type);

        var results = new List<BaseElement>();

        foreach (dynamic element in variant.Elements)
        {
            var typeElement = type.Elements.FirstOrDefault(x => x.Id == Guid.Parse(element.element.id));
            if (typeElement != null)
            {
                var strongyTypedElement = typeElement.Type switch
                {
                    ElementMetadataType.Text => ElementModelProvider.ToElement(element, typeof(TextElement)),
                    ElementMetadataType.RichText => ElementModelProvider.ToElement(element, typeof(RichTextElement)),
                    ElementMetadataType.Number => ElementModelProvider.ToElement(element, typeof(NumberElement)),
                    ElementMetadataType.MultipleChoice => ElementModelProvider.ToElement(element, typeof(MultipleChoiceElement)),
                    ElementMetadataType.DateTime => ElementModelProvider.ToElement(element, typeof(DateTimeElement)),
                    ElementMetadataType.Asset => ElementModelProvider.ToElement(element, typeof(AssetElement)),
                    ElementMetadataType.LinkedItems => ElementModelProvider.ToElement(element, typeof(LinkedItemsElement)),
                    ElementMetadataType.Taxonomy => ElementModelProvider.ToElement(element, typeof(TaxonomyElement)),
                    ElementMetadataType.UrlSlug => ElementModelProvider.ToElement(element, typeof(UrlSlugElement)),
                    ElementMetadataType.Custom => ElementModelProvider.ToElement(element, typeof(CustomElement)),
                    ElementMetadataType.Subpages => ElementModelProvider.ToElement(element, typeof(SubpagesElement)),
                    _ => throw new InvalidOperationException("Unknow element type"),
                };

                results.Add(strongyTypedElement);
            }
        }

        return results;
    }

What do you think? Another option is to take ContentTypeModel as parameter...

mattnield commented 2 years ago

Hi @gormal ,

With the help of @Simply007, I've got something similar to the above for the time being by copying out the ToElement function to m own code for now. Exposing that would certainly help. I know the type of the element in what I'm doing, but only at run time, not design time. So being able to pass the type model in might be a good additional solution.

Simply007 commented 2 years ago

Yes, we can either do

IManagementClient .GetStronglyTypedElements(LanguageVariantModel variant, ContentTypeModel type = null) to provide a way to pass the type.

Or we can do

IManagementClient .GetStronglyTypedElement(dynamic element, ElementMetadataType elementType) which might be more flexible, because you might want to use projection and gen only some elements.

What do you think @mattnield @gormal - and maybe @mcbeev?

mattnield commented 2 years ago

So - being greedy - could we do both? 🫢

I can think of places where both would be really useful. In my case right now, it's the second case, but equally being able to do the first is really helpful.

Simply007 commented 2 years ago

Let's start with the IManagementClient.GetStronglyTypedElement(dynamic element, ElementMetadataType elementType) extension method which might is more flexible and "low-level". The wrapper is basically loading a content type and iteration through the element and calling this method.

Simply007 commented 2 years ago

@mattnield - it is out -> https://github.com/Kentico/kontent-management-sdk-net/releases/tag/3.0.4

Could you test it out?