[{"data":1,"prerenderedAt":60},["ShallowReactive",2],{"blog-post-bringing-net-extensions-to-xamarin-forms":3,"blog-sidebar-tags":41,"blog-sidebar-popular":47},{"overview":4,"post":30},{"slug":5,"title":6,"subtitle":7,"opener":8,"tags":9,"date":13,"headerImage":14,"author":19,"images":7,"__typename":29},"bringing-net-extensions-to-xamarin-forms","Bringing .NET extensions to Xamarin.Forms",null,"This blog post will show you a way of integrating configuration, dependency injection, logging and localization of the .NET extensions stack into your Xamarin.Forms application.",[10,11,12],"Mobile","Xamarin.Forms","C#","2020-09-28T00:00:00Z",[15],{"fileName":16,"url":17,"__typename":18},"background.png","https:\u002F\u002Fcms.spatial-focus.net\u002Fapi\u002Fassets\u002Fspatialfocus\u002F9711d050-3ff9-4abe-8822-47d04dfe4d47\u002F","Asset",[20],{"id":21,"flatData":22,"__typename":28},"0152e82d-a848-44f8-b0b8-fd8aec4b874a",{"name":23,"image":24,"__typename":27},"Christopher Dresel",[25],{"url":26,"__typename":18},"https:\u002F\u002Fcms.spatial-focus.net\u002Fapi\u002Fassets\u002Fspatialfocus\u002F305bcd01-b811-4147-8947-e025b7966355\u002F","AuthorFlatDataDto","Author","PostsFlatDataDto",{"slug":5,"title":6,"subtitle":7,"headerImage":31,"opener":8,"text":33,"tags":34,"date":13,"repo":35,"author":36,"images":7,"__typename":29},[32],{"fileName":16,"url":17,"__typename":18},"With the rise and evolution of ASP.NET Core a lot of practical libraries were created and bundled as `.NET Extensions`. In the offical [.NET Extensions Github repository](https:\u002F\u002Fgithub.com\u002Fdotnet\u002Fextensions) (hint: select a corresponding release branch if you want to check it out) it is described as follows:\n\n>.NET Extensions is an open-source, cross-platform set of APIs for commonly used programming patterns and utilities, such as dependency injection, logging, and app configuration. Most of the API in this project is meant to work on many .NET platforms, such as .NET Core, .NET Framework, Xamarin, and others.\n\nThis blog posts will focus on the integration of .NET extensions into Xamarin.Forms.\n\n>Update: It seems that Microsoft will integrate some of the [.NET Extensions into .NET Multi-platform App UI](https:\u002F\u002Fgithub.com\u002Fdotnet\u002Fmaui\u002Fissues\u002F24) by default :sunglasses:\n\n# Make it configurable\n\nIf you need to store configuration data like connection strings, log settings, cloud services variables and so on `ConfigurationBuilder` comes to the rescue. At Microsoft Docs you'll find some [ASP.NET flavored Configuration documentation](https:\u002F\u002Fdocs.microsoft.com\u002Fen-us\u002Faspnet\u002Fcore\u002Ffundamentals\u002Fconfiguration\u002F).\n\nI won't go into too much detail here but the core essence is that you can specify one or many providers that can handle configuration from different sources.\n\nEvery provider added will override settings defined by a previous provider. This is great for the following use cases:\n\n* Use different values for debug \u002F release builds\n* Set platform specific configuration for Android \u002F iOS devices or emulators and simulators (e.g. Host IP)\n* Specify optional sources that could contain sensitive information added locally or during continuous deployments\n\n## Install NuGet packages\n\nI prefer json configuration files, so we add `Microsoft.Extensions.Configuration.Json` to the netstandard project:\n\n```ps\nInstall-Package Microsoft.Extensions.Configuration.Json\n```\n\nFor Xamarin.Forms embedded resources are the way to go, so we also add `Microsoft.Extensions.FileProviders.Embedded`:\n\n```ps\nInstall-Package Microsoft.Extensions.FileProviders.Embedded\n```\n\n## Configuration and setup\n\nIn your netstandard and platform specific projects, add an `appsettings.json` and set the Build Action to `Embedded Resource`.\n\nAdd a `Setup.cs` to your platform specific project (note that you have to specify the base namespace for Android projects):\n\n```csharp\npublic class Setup\n{\n    public static Action\u003CConfigurationBuilder> Configuration => (builder) =>\n    {\n        builder.AddJsonFile(new EmbeddedFileProvider(typeof(Setup).Assembly, typeof(Setup).Namespace),\n            \"appsettings.json\", false, false);\n    };\n}\n```\n\nPass this action when instantiating your `App`:\n\n```csharp\nLoadApplication(new App(Setup.Configuration));\n```\n\nAdd a `Setup.cs` to your netstandard project:\n\n```csharp\npublic static class Setup\n{\n    public static ConfigurationBuilder Configuration => new ConfigurationBuilder();\n\n    public static ConfigurationBuilder ConfigureNetStandardProject(this ConfigurationBuilder builder)\n    {\n        builder.AddJsonFile(new EmbeddedFileProvider(typeof(Setup).Assembly), \"appsettings.json\", false, false);\n\n        return builder;\n    }\n\n    public static ConfigurationBuilder ConfigurePlatformProject(this ConfigurationBuilder builder,\n        Action\u003CConfigurationBuilder> configure)\n    {\n        configure(builder);\n\n        return builder;\n    }\n}\n```\n\nModify your `App` constructor:\n\n```csharp\npublic App(Action\u003CConfigurationBuilder> configuration)\n{\n    InitializeComponent();\n\n    IConfigurationRoot configurationRoot = Setup.Configuration\n        .ConfigureNetStandardProject()\n        .ConfigurePlatformProject(configuration)\n        .Build();\n\n    \u002F\u002F TODO: Store configuration root, bind configuration to concrete classes, ...\n    string value = configurationRoot[\"Key2\"];\n\n    MainPage = new AppShell();\n}\n```\n\nThis is the foundation of how you can use .NET Configuration in your Xamarin.Forms app. Later on we'll focus on a more practical sample but let's continue with `Microsoft.Extensions.DependencyInjection`.\n\n# Replace the Xamarin.Forms Service Locator Anti-Pattern\n\nI'm sure you came across the Xamarin.Forms `DependencyService` class when creating platform specific services. This class is quite limited and also considered an Anti-Pattern. This is why frameworks like Prism add their own dependency injection mechanism.\n\nIn fact it's no rocket science to integrate the dependency injection framework of your choice. Since this blog post is focusing on .NET extensions we'll use `Microsoft.Extensions.DependencyInjection` as you can imagine. If you have never used the DI extension before, read the [ASP.NET flavored Dependency Injection documentation](https:\u002F\u002Fdocs.microsoft.com\u002Fen-us\u002Faspnet\u002Fcore\u002Ffundamentals\u002Fdependency-injection).\n\nWouldn't it be nice to design your view models as composable objects and let DI inject all your dependencies? Or even better let it create your pages and automatically bind your view models (yes this idea is inspired by Prism)?\n\n## Install NuGet packages\n\nAdd the package to your netstandard project:\n\n```ps\nInstall-Package Microsoft.Extensions.DependencyInjection\n```\n\n## Configuration and setup\n\nI'll go the same route as for configuration and define generic services in the netstandard project and platform specific one in the corresponding Android and iOS project.\n\nAdd this to your `Setup.cs` in your platform specific project (remove `IConfigurationRoot` if you do not need it here):\n\n```csharp\n    public static Action\u003CIServiceCollection, IConfigurationRoot> DependencyInjection =>\n        (serviceCollection, configurationRoot) =>\n        {\n            \u002F\u002F TODO: Add your platform services\n        };\n```\n\nAdd this to your `Setup.cs` in your netstandard project:\n\n```csharp\npublic static IServiceCollection ConfigureNetStandardProject(this IServiceCollection serviceCollection,\n    IConfigurationRoot configurationRoot)\n{\n    \u002F\u002F TODO: Add your services\n\n    return serviceCollection;\n}\n\npublic static IServiceCollection ConfigurePlatformProject(this IServiceCollection serviceCollection,\n    IConfigurationRoot configurationRoot, Action\u003CIServiceCollection, IConfigurationRoot> configure)\n{\n    configure(serviceCollection, configurationRoot);\n\n    return serviceCollection;\n}\n```\n\nExtend your `App` constructor again:\n\n```csharp\npublic App(Action\u003CConfigurationBuilder> configuration,\n    Action\u003CIServiceCollection, IConfigurationRoot> dependencyServiceConfiguration)\n{\n    InitializeComponent();\n\n    IConfigurationRoot configurationRoot = Setup.Configuration\n        .ConfigureNetStandardProject()\n        .ConfigurePlatformProject(configuration)\n        .Build();\n\n    IServiceProvider serviceProvider = Setup.DependencyInjection\n        .ConfigureNetStandardProject(configurationRoot)\n        .ConfigurePlatformProject(configurationRoot, dependencyServiceConfiguration)\n        .BuildServiceProvider();\n\n    MainPage = new AppShell(serviceProvider);\n}\n```\n\nAs you see I'm passing the constructed `IServiceProvider` to the AppShell. There it is stored as a Property and can be retrieved via `Shell.Current.ServiceProvider()` with the following extension method:\n\n```csharp\npublic static class ShellExtension\n{\n    public static IServiceProvider ServiceProvider(this Shell shell)\n    {\n        return (shell as AppShell)?.ServiceProvider;\n    }\n}\n```\n\nNow everything is setup and we can start defining our services:\n\nFor every view model do ```serviceCollection.AddTransient\u003CTViewModel>``` or even easier register all at once with the help of [Scrutor](https:\u002F\u002Fgithub.com\u002Fkhellang\u002FScrutor):\n\n```csharp\npublic static IServiceCollection AddViewModels\u003CT>(this IServiceCollection serviceCollection)\n{\n    return serviceCollection.Scan(selector => selector\n        .FromAssemblies(typeof(T).Assembly)\n        .AddClasses(filter => filter.InNamespaceOf(typeof(T)))\n        .AsSelf()\n        .WithTransientLifetime());\n}\n```\n\nDo the same for your views (pages). If you want to perform the mentioned auto binding, do something like this:\n\n```csharp\nprivate static IServiceCollection AddView\u003CTView, TViewModel>(this IServiceCollection serviceCollection) where TView : Page\n{\n    return serviceCollection.AddTransient\u003CTView>(serviceProvider =>\n    {\n        TView view = ActivatorUtilities.CreateInstance\u003CTView>(serviceProvider);\n\n        \u002F\u002F Automatically bind the view model\n        view.BindingContext = serviceProvider.GetRequiredService\u003CTViewModel>();\n\n        \u002F\u002F You could also forward Appearing and Disappearing page events to your view model (again inspired by Prism)\n        view.Appearing += (sender, args) => (((BindableObject)sender).BindingContext as IPageLifeCycleAware)?.OnAppearing();\n        view.Disappearing += (sender, args) => (((BindableObject)sender).BindingContext as IPageLifeCycleAware)?.OnDisappearing();\n\n        return view;\n    });\n}\n```\n\nI can imagine there is a lot more you could think of (especially when the [Shell Navigation and Structural Management](https:\u002F\u002Fgithub.com\u002Fxamarin\u002FXamarin.Forms\u002Fissues\u002F5166) enhancement gets integrated), but let's keep it simple.\n\nThe remaining question is how to construct your views:\n\n* Manually\n\n    You can always manually create your objects if it is desired. If you are using the classical `INavigation` mechanism for example, you have to pass the `Page` object. So you could either access the `IServiceProvider` via `Shell.Current.ServiceProvider()` or pass a factory to the creating object.\n\n* DataTemplateExtension\n\n    The `ShellContent` specifies a ContentTemplate property, where you can pass a DataTemplate. If specified in XAML it is used together with the `DataTemplateExtension`. This extension takes a string creates the DataTemplate with the matched Type:\n\n    ```csharp\n    public DataTemplate(Type type)\n    ```\n\n    Good thing though there is also an overloaded constructor which takes a factory:\n\n    ```csharp\n    public DataTemplate(Func\u003Cobject> loadTemplate)\n    ```\n\n    I guess you know what I'm up for? Just copy the DateTemplateExtension and replace one line:\n\n    ```csharp\n    [ContentProperty(nameof(DataTemplateExtension.TypeName))]\n    public sealed class MyDataTemplateExtension : IMarkupExtension\u003CDataTemplate>\n    {\n        public string TypeName { get; set; }\n\n        public DataTemplate ProvideValue(IServiceProvider serviceProvider)\n        {\n            if (serviceProvider == null)\n            {\n                throw new ArgumentNullException(nameof(serviceProvider));\n            }\n\n            if (!(serviceProvider.GetService(typeof(IXamlTypeResolver)) is IXamlTypeResolver typeResolver))\n            {\n                throw new ArgumentException(\"No IXamlTypeResolver in IServiceProvider\");\n            }\n\n            if (string.IsNullOrEmpty(TypeName))\n            {\n                IXmlLineInfo li = serviceProvider.GetService(typeof(IXmlLineInfoProvider)) is IXmlLineInfoProvider lip\n                    ? lip.XmlLineInfo\n                    : new XmlLineInfo();\n                throw new XamlParseException(\"TypeName isn't set.\", li);\n            }\n\n            if (typeResolver.TryResolve(TypeName, out Type type))\n            {\n                \u002F\u002F Change the DataTemplate creation and use the .NET extensions DependencyInjection\n                return new DataTemplate(() => Shell.Current.ServiceProvider().GetRequiredService(type));\n            }\n\n            IXmlLineInfo lineInfo = serviceProvider.GetService(typeof(IXmlLineInfoProvider)) is IXmlLineInfoProvider lineInfoProvider\n                ? lineInfoProvider.XmlLineInfo\n                : new XmlLineInfo();\n            throw new XamlParseException($\"MyDataTemplateExtension: Could not locate type for {TypeName}.\", lineInfo);\n        }\n\n        object IMarkupExtension.ProvideValue(IServiceProvider serviceProvider)\n        {\n            return (this as IMarkupExtension\u003CDataTemplate>).ProvideValue(serviceProvider);\n        }\n    }\n    ```\n\n* RegisterRoute\n\n    `Routing.RegisterRoute` also allows to pass a RouteFactory. It's not a big deal to provide your own implementation:\n\n    ```csharp\n    public class MyRouteFactory\u003CT> : RouteFactory where T : Element\n    {\n        protected IServiceProvider ServiceProvider { get; }\n\n        public MyRouteFactory(IServiceProvider serviceProvider)\n        {\n            ServiceProvider = serviceProvider;\n        }\n\n        public override Element GetOrCreate() => ServiceProvider.GetRequiredService\u003CT>();\n    }\n    ```\n\n    Now you can register your routes like this:\n\n    ```csharp\n    Routing.RegisterRoute(\"NewItem\", ServiceProvider.GetRequiredService\u003CMyRouteFactory\u003CNewItemPage>>());\n    ```\n\n    You could make this prettier with an extension method, but I think you get the point. When you now use the URI based shell navigation and call `Shell.Current.GoToAsync`, the pages will be created by the .NET extensions DependencyInjection.\n\n# Start logging with Serilog\n\nMicrosoft simplified logging a lot with the `Microsoft.Extensions.Logging` package. Program to the logging interface and choose from a variety of different logging solutions. This makes logging a real plug and play experience.\n\nI'm a big fan of [Serilog](https:\u002F\u002Fserilog.net\u002F), which plays nicely in combination with .NET extensions DependencyInjection & Configuration. It also provides some useful sinks here:\n\n* [Xamarin](https:\u002F\u002Fgithub.com\u002Fserilog\u002Fserilog-sinks-xamarin): The sink for classic NSLog and AndroidLog logging while debugging your application.\n* [File](https:\u002F\u002Fgithub.com\u002Fserilog\u002Fserilog-sinks-file): A file based sink. Useful if you want to collect logs locally and provide the possibility to share those logs on demand.\n* [Seq](https:\u002F\u002Fgithub.com\u002Fserilog\u002Fserilog-sinks-seq): A sink for convenient remote logging.\n\n## Install NuGet packages\n\nAdd this packages to your netstandard project:\n\n```ps\nInstall-Package Microsoft.Extensions.Logging\nInstall-Package Serilog\nInstall-Package Serilog.Extensions.Logging\nInstall-Package Serilog.Settings.Configuration\n```\n\nAdd the Xamarin (or whatever sink you prefer) to your platform specific projects:\n\n```ps\nInstall-Package Serilog.Sinks.Xamarin\n```\n\n## Configuration and setup\n\nAdd a logging section to your `appsettings.json` for the netstandard project:\n\n```json\n{\n    \"Logging\": {\n        \"LogLevel\": {\n            \"Default\": \"Debug\"\n        }\n    }\n}\n```\n\nThis is the `appsettings.json` for the Android project:\n\n```json\n{\n    \"Logging\": {\n        \"Serilog\": {\n            \"WriteTo\": [\n                {\n                    \"Name\": \"AndroidLog\"\n                }\n            ]\n        }\n    }\n}\n```\n\nThis is the `appsettings.json` for the iOS project:\n\n```json\n{\n    \"Logging\": {\n        \"Serilog\": {\n            \"WriteTo\": [\n                {\n                    \"Name\": \"NSLog\"\n                }\n            ]\n        }\n    }\n}\n```\n\nAnd finally the addition to your `Setup.cs`:\n\n```csharp\npublic static IServiceCollection ConfigureLogging(this IServiceCollection serviceCollection,\n    IConfigurationRoot configurationRoot)\n{\n    return serviceCollection.AddLogging(builder =>\n    {\n        builder.AddSerilog(new LoggerConfiguration().ReadFrom.Configuration(configurationRoot.GetSection(\"Logging\"))\n            .CreateLogger());\n    });\n}\n```\n\nDon't forget to call `ConfigureLogging` in your `App` constructor.\n\nAnd that's all we need to do. With dependency injection and configuration setup, this was a piece of cake. Now we can inject our logger where appropriate:\n\n```csharp\npublic class AboutViewModel : BaseViewModel\n{\n    public AboutViewModel(IDataStore\u003CItem> dataStore, ILogger\u003CAboutViewModel> logger) : base(dataStore)\n    {\n        Title = \"About\";\n        OpenWebCommand = new Command(async () => await Browser.OpenAsync(\"https:\u002F\u002Fxamarin.com\"));\n\n        \u002F\u002F Here we go (log)\n        logger.LogWarning($\"Hello from {nameof(AboutViewModel)}\");\n    }\n\n    public ICommand OpenWebCommand { get; }\n}\n```\n\n# ASP.NET flavored localization support\n\nI really like the [ASP.NET localization](https:\u002F\u002Fdocs.microsoft.com\u002Fen-us\u002Faspnet\u002Fcore\u002Ffundamentals\u002Flocalization) system. Especially the resource file naming schema used by the default `ResourceManagerStringLocalizerFactory`, which makes finding and managing localizations way more enjoyable. There are also blog posts about adding different localization sources, like databases which is also easy if you adhere to the `IStringLocalizer` interface.\n\nSo why not try to bring this into Xamarin.Forms?\n\n## Install NuGet packages\n\nAdd the package to your netstandard project:\n\n```ps\nInstall-Package Microsoft.Extensions.Localization\n```\n\n## Configuration and setup\n\nAdd a localization section to your `appsettings.json` for the netstandard project:\n\n```json\n{\n    \"Localization\": {\n        \"ResourcesPath\": \"Resources\"\n    }\n}\n```\n\nAdd this to your `Setup.cs` in your netstandard project:\n\n```csharp\npublic static IServiceCollection ConfigureLocalization(this IServiceCollection serviceCollection,\n    IConfigurationRoot configurationRoot)\n{\n    return serviceCollection.AddLocalization(options =>\n    {\n        options.ResourcesPath = configurationRoot.GetSection(\"Localization\")[\"ResourcesPath\"];\n    });\n}\n```\n\nDon't forget to call `ConfigureLocalization` in your `App` constructor.\n\nNow you can use the known `StringLocalizer` class where nedded:\n\n```csharp\npublic class AboutViewModel : BaseViewModel\n{\n    public AboutViewModel(IDataStore\u003CItem> dataStore, IStringLocalizer\u003CAboutViewModel> localizer) : base(dataStore)\n    {\n        Title = localizer[\"Title\"];\n        OpenWebCommand = new Command(async () => await Browser.OpenAsync(\"https:\u002F\u002Fxamarin.com\"));\n    }\n\n    public ICommand OpenWebCommand { get; }\n}\n```\n\nFor the above view model you would add an `AboutViewModel.resx` to the \"Resources\\ViewModels\" folder with the Title translation key.\n\nLocalizing xaml files is a bit trickier as you will need an `IMarkupExtension`.\n\n```csharp\n[ContentProperty(\"Text\")]\npublic class TranslateExtension : IMarkupExtension\n{\n    public string Text { get; set; }\n\n    public object ProvideValue(IServiceProvider serviceProvider)\n    {\n        if (serviceProvider == null)\n        {\n            throw new ArgumentNullException(nameof(serviceProvider));\n        }\n\n        return TranslateExtension.GetStringLocalizer(GetRootObjectType(serviceProvider))[Text];\n    }\n\n    protected static IStringLocalizer GetStringLocalizer(Type type)\n    {\n        Type stringLocalizerTypeOfT = typeof(IStringLocalizer\u003C>).MakeGenericType(type);\n\n        return (IStringLocalizer)Shell.Current.ServiceProvider().GetService(stringLocalizerTypeOfT);\n    }\n\n    \u002F\u002F See https:\u002F\u002Fstackoverflow.com\u002Fquestions\u002F55869794\u002Faccess-contentpage-from-imarkupextension\n    protected Type GetRootObjectType(IServiceProvider serviceProvider)\n    {\n        if (serviceProvider == null)\n        {\n            throw new ArgumentNullException(nameof(serviceProvider));\n        }\n\n        IProvideValueTarget valueProvider = serviceProvider.GetService\u003CIProvideValueTarget>() ??\n            throw new ArgumentException(\"serviceProvider does not provide an IProvideValueTarget\");\n\n        PropertyInfo cachedPropertyInfo = valueProvider.GetType()\n            .GetProperty(\"Xamarin.Forms.Xaml.IProvideParentValues.ParentObjects\", BindingFlags.NonPublic | BindingFlags.Instance);\n\n        if (cachedPropertyInfo != null)\n        {\n            IEnumerable\u003Cobject> parentObjects = cachedPropertyInfo.GetValue(valueProvider) as IEnumerable\u003Cobject>;\n\n            if (parentObjects == null)\n            {\n                throw new ArgumentException(\"Unable to access parent objects\");\n            }\n\n            IEnumerable\u003Cobject> enumerable = parentObjects as object[] ?? parentObjects.ToArray();\n\n            foreach (object target in enumerable)\n            {\n                if (target is Page page)\n                {\n                    return target.GetType();\n                }\n            }\n\n            return enumerable.Last().GetType();\n        }\n\n        throw new XamlParseException($\"Unable to access parent page\");\n    }\n}\n```\n\nThe following shows how you would localize the app name in the `AboutPage.xaml`:\n\n```xml\n\u003CFormattedString>\n    \u003CFormattedString.Spans>\n        \u003CSpan Text=\"{i18n:Translate AppName}\" FontAttributes=\"Bold\" FontSize=\"22\" \u002F>\n        \u003CSpan Text=\" \" \u002F>\n        \u003CSpan Text=\"1.0\" ForegroundColor=\"{StaticResource LightTextColor}\" \u002F>\n    \u003C\u002FFormattedString.Spans>\n\u003C\u002FFormattedString>\n```\n\n# Conclusion\n\nIn this blog post I showed you a way to integrate the .NET extensions into your Xamarin.Forms application. With `Microsoft.Extensions.Configuration` and `Microsoft.Extensions.DependencyInjection` a lot of configurability and flexibility is added to your app. `Microsoft.Extensions.Logging` will make your debugging life a lot easier. In the last section I demonstrated a way to use `Microsoft.Extensions.Localization` within your application, which might be valuable for you especially if you are used to ASP.NET. You might also want to check for other .NET extensions functionality, like `Microsoft.Extensions.Caching` or similar.\n\nDon't forget to take a look into the sample code repo. We hope you enjoyed these guides and please don't hesitate contacting us if you have any questions or feedback.",[10,11,12],"https:\u002F\u002Fgithub.com\u002FSpatialFocus\u002Fsample-bringing-net-extensions-to-xamarin-forms",[37],{"id":21,"flatData":38,"__typename":28},{"name":23,"image":39,"__typename":27},[40],{"url":26,"__typename":18},[12,42,43,44,45,46],"DDD","EF","Clean code","Geo","web",[48,54],{"slug":49,"title":50,"headerImage":51,"__typename":29},"radial-progress-css-animation","Radial progress with CSS",[52],{"url":53,"__typename":18},"https:\u002F\u002Fcms.spatial-focus.net\u002Fapi\u002Fassets\u002Fspatialfocus\u002Fb7cdc0ef-364e-4444-8dd3-b85f19e87820\u002F",{"slug":55,"title":56,"headerImage":57,"__typename":29},"load-testing-put-endpoints-with-apache-jmeter","Load testing PUT endpoints",[58],{"url":59,"__typename":18},"https:\u002F\u002Fcms.spatial-focus.net\u002Fapi\u002Fassets\u002Fspatialfocus\u002Fad44fc84-935c-4ab4-ab0e-9fbcb84fb2c1\u002F",1783525747560]