Modern mobile applications increasingly depend on dynamic content delivered via CMS platforms. However, tightly coupling an application to a single content source can limit flexibility, increase maintenance overhead, and slow down the ability to evolve backend systems.

To explore a more adaptable approach, a Proof of Concept (PoC) was developed within an existing .NET for iOS / Android application using the MVVMCross framework. The goal was simple in principle: enable the app to consume the same content from a completely different CMS—Umbraco—without requiring changes to the UI layer or core application logic.

By leveraging the application’s existing architectural patterns, particularly the use of facades and repositories, a new Umbraco-backed data source was introduced alongside the existing implementation. This allowed content retrieval to be redirected with minimal disruption, demonstrating how abstraction layers can decouple the application from its underlying data providers. Crucially, because these abstraction layers absorb the difference between sources, the change carried very little risk — almost no business logic above the data layer had to move.

Alongside this, the PoC also explored the creation of a reusable UmbracoRepository framework, aimed at simplifying integration with Umbraco’s content delivery APIs across mobile applications.

This case study details the approach taken, the architectural decisions involved, and the outcomes of introducing a flexible, CMS-agnostic content delivery strategy. It also shows how the application’s existing unit-test coverage — spanning view models and the facades themselves — makes that switch demonstrably safe rather than merely plausible.

shoutdigital_Create_an_editorial-style_image_that_conveys_the_aed182fb-c272-41b3-88eb-a8fa2a69be9b_3.png

The challenge: changing where the data comes from

Our application has always sourced its dynamic content from its own specific API, reached through a generated client package that in turn talks to an internal administrative portal. That arrangement works well, but it ties the app to a single provider.

With conversations underway about moving some, or all, of those endpoints to a different provider, one question dominates: how risky is it to change where a live app gets its data? Re-platforming a content source sounds like the kind of change that ripples through every screen. This proof of concept set out to answer that question with evidence rather than assertion — by repointing real features at an Umbraco headless, content delivery API and measuring exactly how much of the app had to change.

The architecture that made it cheap

The app is built in layers, each with a single job:

  • View — renders the screen and forwards user intent. Knows nothing about where data comes from.

  • ViewModel — exposes bindable state and commands. Talks only to a facade.

  • Facade — the “sense-maker”. Owns a data domain (for example InformationFacade for general content) and returns clean domain models, irrespective of source.

  • Repository — talks to exactly one data source. ApiRepository wraps the legacy client; UmbracoRepository calls Umbraco’s content delivery API.

The important boundary is the facade. A view model asks InformationFacade for a list of article objects; it neither knows nor cares which repository produced them. That single rule — view models depend on domain models, never on data sources — is what turns a re-platforming exercise into a localised change.

The switch in practice

Adding Umbraco meant introducing a second repository alongside the existing one, and giving the facade a way to choose between them. The choice is expressed as a simple enum:

public enum ApiSource
{
    App,
    Umbraco
}

The facade takes the source as a parameter, calls the relevant repository, and — the key step — maps whatever comes back into the same domain model the app already uses:

public async Task<ResponseWithError<List<Article>, FacadeError>>
    GetArticles(string region, ApiSource source =
ApiSource.App)
{
    if (source ==
ApiSource.App)
    {
        response.Content = await
apiRepository.GetArticles(region);
    }
    else
    {
        var umbracoData = await
umbracoRepository.GetArticles(region);
        response.Content = umbracoData.items
            .Select(x => _
mapper.Map<Article>(x))
            .ToList();
    }
    ...
}

The Umbraco branch decodes Umbraco’s own response shape and hands it to AutoMapper, which normalises it into the very same Article the App branch returns:

CreateMap<UmbracoPageResponse<UmbracoArticleResponse>, Article>()
    .ForMember(d =>
d.Name,             o => o.MapFrom(s => s.properties.title))
    .ForMember(d => d.ShortDescription, o => o.MapFrom(s =>
s.properties.summary))
    .ForMember(d => d.Description,      o => o.MapFrom(s =>
s.properties.introduction))
    .ForMember(d => d.ImageUrl,         o => o.MapFrom(s =>
s.properties.image.FirstOrDefault().url));

Because both branches return List<Article>, everything above the facade is untouched: no view model changed, no binding changed, no screen changed. The current source is remembered in a lightweight in-memory store and can even be flipped at runtime from the settings screen — handy for side-by-side comparison during the PoC.

Proven safe by the test suite

'Very little changed' is only reassuring if you can prove it, and the application’s test suite is what turns that claim into a guarantee. The proof rests on the same boundaries that made the switch cheap: every repository is an interface, so a test can stand a mock in its place, while the facade and the view model are constructed for real and exercised exactly as they run in production.

The shared test base wires this up once. The legacy App repository is mocked outright, the low-level network client is mocked, and an in-memory repository stands in for the setting that selects the active source. The facade — and, crucially, the real Umbraco repository — are then constructed on top of those mocks:

// data sources are interfaces, so the tests mock them
ApiRepositoryMock      = new Mock<IApiRepository>();
NetworkClientMock      = new Mock<INetworkClient>();
InMemoryRepositoryMock = new Mock<IInMemoryRepository>();
Ioc.RegisterSingleton(ApiRepositoryMock.Object);
Ioc.RegisterSingleton(NetworkClientMock.Object);
Ioc.RegisterSingleton(InMemoryRepositoryMock.Object);

// the Umbraco repository and the facade are the REAL objects,
// wired on top of the mocks above
UmbracoRepository = Ioc.IoCConstruct<UmbracoRepository>();
InformationFacade = Ioc.IoCConstruct<InformationFacade>();

With that harness in place, one screen — the content list the PoC repointed — is driven twice through a single, unchanged view model. The only difference between the two tests is the value returned by GetApiSource().

In the first test the selector returns App, so the facade calls the legacy repository, which already hands back finished domain objects. The test stubs that response and asserts on the mapped output the UI will bind to:

InMemoryRepositoryMock.Setup(x => x.GetApiSource()).Returns(0); // App

ApiRepositoryMock
    .Setup(x => x.GetArticles(It.IsAny<string>()))
    .ReturnsAsync(articles);

await LoadViewModel(null);

ViewModel.Articles.Should().HaveCount(2);

The second test flips the selector to Umbraco. Nothing about the view model changes — but the mock is placed one layer deeper, at the network client, so the real Umbraco repository and the real AutoMapper profile run against a raw Delivery API payload. That payload looks nothing like a domain article: a nested envelope of items, each wrapping its fields in a properties object, with images and links as arrays.

InMemoryRepositoryMock.Setup(x => x.GetApiSource()).Returns(1); // Umbraco

NetworkClientMock
    .Setup(x => x.Get<UmbracoContentListResponse<UmbracoArticleResponse>, UmbracoError>(
        It.IsAny<string>(), It.IsAny<object>(), It.IsAny<Dictionary<string, string>>()))
    .ReturnsAsync(new NetworkResponseWithError<
        UmbracoContentListResponse<UmbracoArticleResponse>,
        NetworkClientError<UmbracoError>>
    {
        Status  = Status.Ok,
        Content = umbracoArticles
    });

await LoadViewModel(null);

ViewModel.Articles.Should().HaveCount(3);

Both tests assert against the same property, ViewModel.Articles, populated as a List<Article>. One run is fed a flat list of domain records; the other is fed a deeply nested CMS envelope and forced through the full deserialise-and-map pipeline. They arrive at the same shape. The view model neither knows nor cares which happened — and because the Umbraco mapping genuinely executes, the test proves the reconciliation works rather than merely assuming it.

App source test

Umbraco source test

Source selector

GetApiSource() → App

GetApiSource() →  Umbraco

What is mocked

The App repository

The network client (one layer deeper)

What runs for real

Facade + view model

Facade + view model + Umbraco repository + AutoMapper

Payload shape

Flat List<Article>

Nested Umbraco envelope (Items → properties, arrays)

Asserted result

ViewModel.Articles has 2

ViewModel.Articles has 3

The significance is in what did not need writing. No new test pattern, no change to the view model itself, no parallel set of assertions for ‘the Umbraco version’ of the screen. Because the facade contract and the domain model are fixed points, adding a data source is just another mock standing in the same place — and the existing suite keeps its meaning whichever source sits underneath.

Why this matters

For a change that sounds daunting on paper, the PoC turned out to be low-drama — and that is the headline, not an aside:

  •  Safe — business logic didn’t move, so the surface area for regressions is tiny and the existing tests still describe the app’s behaviour.

  • Incremental — content can move one endpoint, one domain at a time, behind a flag. There is no big-bang cut-over.

  • Reversible — the source is a parameter; if anything misbehaves, the app falls straight back to the App API.

  • Reassuring for the client — ‘here is your app, running on the new source, today’ is a very different conversation from ‘trust us, the migration will be fine’.

None of this came from heroics during the PoC. It came from architectural decisions made long before — clean layering, dependency injection, and mapping at the facade. Investing in those boundaries early is what turned a re-platforming into something closer to a configuration change.

Towards a reusable Umbraco framework for mobile

The second strand of the PoC was a small, reusable foundation for talking to any Umbraco instance from a mobile app. Umbraco’s Delivery API is structurally consistent: content comes back as pages, each page has a typed set of properties, and rich content is composed from blocks and block lists. That regularity can be captured once, generically:

public class UmbracoPageResponse<TContentType>
{
    public string id { get; set; }
    public string name { get; set; }
    public TContentType properties { get; set; }
}

public class UmbracoContentListResponse<TContentType>
{
    public int total { get; set; }
    public List<UmbracoPageResponse<TContentType>> items { get; set; }
}

public class UmbracoBlockListResponse<TContentType> { ... }
public class UmbracoBlockResponse<TContentType>     { ... }

To consume a new content type you only define its properties class — the title, summary and image fields for an article, say — and let the generic wrappers handle the envelope. A thin repository built on the app’s existing network client issues the request, the wrappers decode the response, and AutoMapper projects it onto your own domain model.

The pieces are deliberately app-agnostic. Drop the generic models and a slim repository into any mobile project, point them at any Umbraco instance, supply the property classes for the content you care about, and you can pull structured headless content with almost no bespoke plumbing — whatever that content happens to be.

In short

  • Good architecture is risk reduction. Layering and mapping made ‘change the data source’ a contained change instead of a rewrite.

  • A headless CMS becomes a swappable backend. When the app depends on domain models rather than providers, the provider can change underneath it.

  • The facade is the unsung hero. By making sense of data regardless of origin, it kept every layer above it perfectly still.