Blog posts by Alex Boesen2020-07-07T18:49:36.0000000Z/blogs/alex-boesen/Optimizely WorldGenerate Typescript interfaces for pages and blocks/blogs/alex-boesen/dates/2020/7/generate-typescript-interfaces-for-pages-and-blocks-models/2020-07-07T18:49:36.0000000Z<p>At <a href="https://www.almbrand.dk">Alm Brand</a> we are using Vue.js together with the content delivery api for our Single Page Applications(SPA) that powers parts of our website and App, when we starting doing this we took a lot of inspiration from <a href="https://github.com/episerver/musicfestival-vue-template">episerver musicfestival vue template</a> and we ended up with what we call the "Vue-Epi Framework" (yea the name needs work).</p>
<p>One of our key differences with the template, and the focus for this blog post, is we are using <a href="https://www.typescriptlang.org/">TypeScript</a> which leads to the question:<br />How do we get that sweet typing for our episerver models when using the content delivery api? read on and find out!</p>
<h2>Typescript all the things</h2>
<p>The first thing to do is find or create the folder where your typescript definitions lives and create a file named content.d.ts that maps all the base/common types/properties like the short sample below</p>
<pre class="language-javascript"><code>/** interface for ContentLanguage coming from episerver content delivery api */
export interface ContentLanguage {
/** the link to the this langauge version of the current content */
link: string;
/** the localized displayName for the language */
displayName: string;
/** the ISO name for the language */
name: string;
}
/** interface for ContentReference coming from episerver content delivery api */
export interface ContentReference {
/** the episerver content id */
id: number;
/** the episerver content work id */
workId: number;
/** the guid id of the content */
guidValue: string;
/** the content providerName */
providerName?: string;
/** url to the content (block points to the frontpage) */
url: string;
}</code></pre>
<p>(<a href="https://gist.github.com/Hangsolow/f59e6c089ff48aee5b0559360723d7f4">full file here</a>) </p>
<p>That is used by the ContentTypeCodeGenerator, a simple mapper that scans our assemblies for content, enums and "other things" to turn them into typescript interfaces and saving those to a file.</p>
<pre class="language-csharp"><code>private void GenerateTypescriptInterfaces()
{
IEnumerable<Assembly> assemblies = GetAssemblies();
IEnumerable<Type> types = assemblies.SelectMany(a => GetTypesFromAssembly(a)).ToList();
var contentTypes = types.Where(t => t.GetCustomAttribute<ContentTypeAttribute>() != null && !typeof(IContentMedia).IsAssignableFrom(t))
StringBuilder builder = new StringBuilder();
builder.AppendLine("import { IContent, ContentLanguage, ContentReference } from './content'");
GenerateTypescriptEnums(types, builder)
foreach (var contentType in contentTypes)
{
Logger.Information("Adding {ContentType} as typescript interface", contentType.Name);
builder.AppendLine($"export interface {contentType.Name} extends IContent {{");
AddProperties(contentType);
builder.AppendLine("}")
var fileText = builder.ToString();
if (HasFileContentChanged(fileText))
{
File.WriteAllText(FilePath, fileText);
}
}</code></pre>
<p>(<a href="https://gist.github.com/Hangsolow/83a63cbb1c6d45a4a18fcf530cc5893f">full class here</a>)</p>
<p>most of the secret sauce is in the GetDataType method that maps an property to an typescript type</p>
<pre class="language-csharp"><code>string GetDataType(Type contentType, PropertyInfo property)
{
if (TypeMappings.TryGetValue(property.PropertyType, out var func))
{
return func(contentType, property);
}
else
{
if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType))
{
return FindListType(contentType, property);
}
return property.PropertyType.Name;
}
}</code></pre>
<p>As you can see we either lookup the type in TypeMappings or return the name of the property type(mostly used with Enum and Block properties on models)<br />the TypeMappings is a simple Dictionary with mappings as can seen below </p>
<pre class="language-csharp"><code>private static IDictionary<Type, Func<Type, PropertyInfo, string>> CreateTypeMapping()
{
var mappingDictionary = new Dictionary<Type, Func<Type, PropertyInfo, string>>();
mappingDictionary.Add(typeof(string), (t, p) => "string");
mappingDictionary.Add(typeof(int), (t, p) => "number");
mappingDictionary.Add(typeof(int?), (t, p) => "number");
mappingDictionary.Add(typeof(decimal), (t, p) => "number");
mappingDictionary.Add(typeof(decimal?), (t, p) => "number");
mappingDictionary.Add(typeof(float), (t, p) => "number");
mappingDictionary.Add(typeof(float?), (t, p) => "number");
mappingDictionary.Add(typeof(double), (t, p) => "number");
mappingDictionary.Add(typeof(double?), (t, p) => "number");
mappingDictionary.Add(typeof(bool), (t, p) => "boolean");
mappingDictionary.Add(typeof(bool?), (t, p) => "boolean");
mappingDictionary.Add(typeof(DateTime), (t, p) => "string");
mappingDictionary.Add(typeof(DateTime?), (t, p) => "string");
mappingDictionary.Add(typeof(ContentReference), (t, p) => GetContentReferenceType(t, p));
mappingDictionary.Add(typeof(PageReference), (t, p) => "ContentReference");
mappingDictionary.Add(typeof(ContentArea), (t, p) => "Array<IContent>");
mappingDictionary.Add(typeof(LinkItemCollection), (t, p) => "Array<string>");
mappingDictionary.Add(typeof(PropertyContentReferenceList), (t, p) => "Array<IContent>");
mappingDictionary.Add(typeof(Url), (t, p) => "string");
mappingDictionary.Add(typeof(XhtmlString), (t, p) => "string");
return mappingDictionary;
string GetContentReferenceType(Type contentType, PropertyInfo property)
{
//we convert ContentReferences that point to Images to an url in content delivery api
var uiHint = property.GetCustomAttribute<UIHintAttribute>();
if (uiHint?.UIHint == UIHint.Image)
{
return "string";
}
return "ContentReference";
}
}</code></pre>
<p>the resulting file is like this (only with much more data of course)</p>
<pre class="language-javascript"><code>export enum TileVariantEnum {
Navigation=1,
Call=2,
}
export interface TileBlock extends IContent {
tileVariant: TileVariantEnum;
title: string;
icon: string;
link: string;
phoneNumber: string;
renderHtml: boolean;
hide: boolean;
}</code></pre>
<p>You can now import your models in your typescript code and get that sweet, sweet typing experience.</p>
<p> </p>
<pre class="language-javascript"><code>import { Vue, Component } from "vue-property-decorator";
import { PropType } from "vue";
import { mapState } from "vuex";
import template from './TileBlock.vue';
import { TileBlock, TileVariantEnum } from "@scripts/definitions/episerver/content-types";
import { AbLink } from "@scripts/app/components/sharedcomponents/baseComponents/components";
@Component({
name: 'TileBlockComponent',
mixins:[template],
components: {
AbLink
},
computed: mapState<any>({
isEditable: state => state.epiContext.isEditable,
parentModel: state => state.epiContent.model
}),
props: {
model: Object as PropType<TileBlock>
},
})
export default class TileBlockComponent extends Vue {
model: TileBlock;
path: string = "";
mounted() {
this.path = this.model.link;
if (this.model.tileVariant === TileVariantEnum.Call) {
this.path = this.model.phoneNumber;
}
}
}</code></pre>
<p>There is some small cavats with this, it assumes you are using <code>SetFlattenPropertyModel(true)</code> and <code>SetExtendedContentTypeModel(true)</code> with content delivery api and some of the mappings like ContentReference to string when it have the UIHint.Image attribute also requires you to expand the content delivery api with an custom IPropertyModelConverter to actually make the conversion but that another blog post ;) </p>
<p>we call this code from both an InitializableModule when on localhost for devs to always have an up to date version of the file and for our deployments we have an CI/CD pipeline that does the same thing to make sure the frontend can compile with the code that are currently building.</p>
<p>Play around with <a href="https://gist.github.com/Hangsolow/83a63cbb1c6d45a4a18fcf530cc5893f"><span>ContentTypeCodeGenerator</span></a> and <a href="https://gist.github.com/Hangsolow/83a63cbb1c6d45a4a18fcf530cc5893f">content.d.ts</a> and see if it is something you can use :) </p>Creating your own NullCleaningContentSerializer using pattern matching/blogs/alex-boesen/dates/2020/4/creating-your-own-nullcleaningcontentserializer-using-pattern-matching/2020-04-25T15:49:06.0000000Z<p>At <a href="https://www.almbrand.dk/">Alm. Brand</a> we are big users of the Content Delivery Api, using it to power most of our customer facing solutions with data be it SPA or mobile app and when we can reduce the amount of data we send over the wire is a big win.</p>
<p>So when the SetIncludeNullValues option was included to strip away null values we started using it right away, but we had to stop using it as the NullCleaningContentSerializer had a heavy impact on the Content Api performance in our setup especially after an deploy.</p>
<p>The reason for this performance impact is that NullCleaningContentSerializer uses a lot of reflection that gets the job done but with a cost(a high one in our case).</p>
<p> <br />That got me to thinking, what if I created a version of the NullCleaningContentSerializer that instead of reflection only used pattern matching, could that give is the size reduction we wanted without the performance cost? Yes as it turns out! </p>
<p>So first up is to make sure that IncludeNullValues is set to true and jsonSerializer settings NullValueHandling is set to ignore</p>
<pre class="language-csharp"><code>public void ConfigureContainer(ServiceConfigurationContext context)
{
context.Services.Configure<ContentApiConfiguration>(config =>
{
//config to content delivery api goes here
config.Default()
.SetFlattenPropertyModel(true)
.SetIncludeNullValues(true);
});
public void Initialize(InitializationEngine context)
{
var jsonSerializer = context.Locate.Advanced.GetInstance<JsonSerializer>();
jsonSerializer.Settings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore
}</code></pre>
<p> NullValueHandling only effect properties, not dictionaries so that where our new CleaningContentSerializer comes into the picture!</p>
<pre class="language-csharp"><code>public class CleaningContentSerializer : IContentApiSerializer
{
public CleaningContentSerializer(IContentApiSerializer defaultSerializer)
{
DefaultSerializer = defaultSerializer;
}
public string MediaType => DefaultSerializer.MediaType;
public Encoding Encoding => DefaultSerializer.Encoding;
private IContentApiSerializer DefaultSerializer { get; }
public string Serialize(object value)
{
CleanObject(value);
return DefaultSerializer.Serialize(value);
}
...</code></pre>
<p>we use the default IContentApiSerializer to do the <span>actual </span>serialize part, our magic is in the CleanObject method</p>
<pre class="language-csharp"><code>private void CleanObject(object value)
{
switch (value)
{
case ContentApiModel model:
CleanContentApiModel(model);
break;
case IEnumerable<ContentApiModel> models:
CleanContentApiModels(models);
break;
void CleanContentApiModel(ContentApiModel model)
{
//we have no need for these
model.Changed = null;
model.Created = null;
model.ExistingLanguages = null;
model.MasterLanguage = null;
model.Saved = null;
model.StartPublish = null;
model.StopPublish = null;
model.Status = null;
model.ParentLink = null;
var properties = model.Properties;
//only used serverside
RemoveProperty(properties, "PreScripts");
RemoveProperty(properties, "PostScripts");
RemoveProperty(properties, "BodyScripts");
RemoveProperty(properties, "SiteSettings");
RemoveProperty(properties, "PersonalizedSiteSettings");
var removeList = new List<string>();
foreach (var property in properties)
{
CheckProperty(property, removeList);
}
foreach (var removeKey in removeList)
{
properties.Remove(removeKey);
}
void CheckProperty(KeyValuePair<string, object> property, IList<string> removalList)
{
switch (property.Value)
{
case null:
removalList.Add(property.Key);
break;
case string str when string.IsNullOrEmpty(str):
removalList.Add(property.Key);
break;
case ICollection collection when collection.Count == 0:
removalList.Add(property.Key);
break;
case IEnumerable<ContentApiModel> models when models.Any():
CleanContentApiModels(models);
break;
case IEnumerable<ContentApiModel> models:
removalList.Add(property.Key);
break;
}
}
}</code></pre>
<p>(full class <a href="https://gist.github.com/Hangsolow/c8496b562eb300eb2af8fb28f3264a5c">here</a>)</p>
<p>all that is left is to register the CleaningContentSerializer in the DI container: <br /><code>services.Intercept<IContentApiSerializer>((locator, defaultApiSerializer) => new CleaningContentSerializer(defaultApiSerializer))</code> </p>
<p>As you can see we not only remove null/empty properties but also uses this opportunity to do a litte spring clearing on the ContentApiModel where we remove properties that is not used by our spa or app which result in a size reduction of over 60% in some of our content api requests!</p>
<p>There are likely some cases that this will not handle that the default NullCleaningContentSerializer can but atleast for us this is a better compromise between getting the size reduction and performance</p>