Logo Fabasoft

Fabasoft

Established Company

Dynamically created OData Datasource

Description

Gerald Leitner von Fabasoft spricht in seinem devjobs.at TechTalk über das Thema „Dynamically created OData Datasource“. Anhand eines Beispiels in einer Live-Coding Session, werden verschiedene Aspekte demonstriert.

By playing the video, you agree to data transfer to YouTube and acknowledge the privacy policy.

Video Summary

In Dynamically created OData Datasource, Gerald Leitner (Fabasoft) first shows the standard way to enable OData in ASP.NET 5 (Microsoft.AspNetCore.OData 7.x, AddOData, ODataConventionModelBuilder, EnableQuery, and a model key) before taking it fully dynamic. A middleware parses the request, generates controllers and entities from JSON metadata, compiles them with Roslyn, loads them via Application Parts, rewrites routes, and rebuilds the OData EDM—so $metadata and options like $top work. The demo exposes localized German/English endpoints (e.g., Deutsch/Katze, Englisch/Cat), equipping viewers to build runtime-extensible OData APIs suitable for tools like Power BI.

Building a Dynamic OData Datasource in ASP.NET 5: Gerald Leitner (Fabasoft) Shows Runtime-Generated Models and Controllers

Overview: From static templates to runtime flexibility

In the session “Dynamically created OData Datasource,” Speaker Gerald Leitner (Fabasoft) set an ambitious target: take OData beyond the usual static models and compile-time controllers, and push it into fully dynamic territory. As he put it, the goal was to bring “the greatest possible flexibility, the dynamics” into OData.

We at DevJobs.at watched a live demo that moved step by step: starting with the familiar ASP.NET “WeatherForecast” template, switching on OData 7.x and query features, and then tackling the heart of the challenge. A custom middleware inspects incoming requests, derives language and resource from the URL, generates C# source code for models and controllers, compiles it at runtime, adds the resulting assembly to the running ASP.NET Core app, rewrites routes, and finally provides OData with a dynamically built EDM model. The result is language-specific endpoints like “/deutsch/katze” and “/englisch/cat,” each with its own model, controller, and OData metadata — created just-in-time.

OData basics and the starting point

Leitner began by situating OData: it’s an HTTP-based protocol for standardized data access — a fit when you need to expose larger data sets in a uniform way to tools such as Power BI. OData is maintained by OASIS, and OData 4.0 is the relevant standard referenced in the talk. In practice, clients shape their queries via URL options like “$top,” “$filter,” “$orderby,” and so forth.

The starting point in code was the stock ASP.NET template with a “WeatherForecast” model and controller that emits random JSON data. This baseline had no OData layer initially, which means no query features or metadata out of the box.

Step 1: Enable OData in the static project (7.x)

Before making OData dynamic, Leitner turned it on in the static example to make the moving parts explicit — the same parts that would later be assembled at runtime:

  • Package: “Microsoft.AspNetCore.OData” (version 7.x). He noted that 8.0 exists as a preview but differs in behavior; thus the demo sticks to 7.x.
  • Services: In Startup, register OData with “AddOData.”
  • Endpoints: In “UseEndpoints,” enable OData features such as “Select,” “Filter,” “Count,” and “Top,” and map an OData route (e.g., “/odata”).
  • EDM model: OData requires an explicit model. Leitner used “ODataConventionModelBuilder.” Importantly, “EntitySet” controls which entities are exposed to OData (e.g., “WeatherForecast”), allowing fine-grained selection from a potentially larger domain model.
  • Controller: Convert the existing controller to an OData controller. Key pieces include placing “EnableQuery” on the method and defining a primary key in the model via a “Key” attribute. In the demo, the date field served as the key.

Outcome: At “/odata/weatherforecast,” the controller serves data alongside OData metadata. Options like “$top=1” and “$orderby=Date” are available. The metadata endpoint reveals the entity sets and properties included in the model.

Understanding this static setup is crucial for the next step. The dynamic solution must still satisfy OData’s needs for an EDM model, route, controller endpoints, and query features — only now, all of those must be produced on demand.

Target architecture: everything dynamic — model, controller, route, and OData registration

Leitner summarized the aim: “move away from the static model and static controllers to an extremely dynamic application.” Conceptually:

  • The application starts with no predefined OData entities or controllers.
  • An incoming request (e.g., “/deutsch” or “/deutsch/bär”) triggers a middleware component.
  • The middleware parses language and resource, loads metadata (in the demo, from JSON files like “katze” and “bär”), generates C# code for both model and controller, compiles it at runtime, adds the assembly to the live ASP.NET Core app via Application Parts, informs routing about the new controllers, rewrites the route, and supplies OData with a dynamically built EDM model.
  • The result is language- and resource-specific endpoints created exactly when they are requested, including OData metadata and query processing.

The pipeline end to end: middleware as the orchestrator

A custom middleware (registered with “UseMiddleware” in Startup) orchestrates the entire workflow:

1) Request analysis

  • The middleware inspects “HTTPContext.Request.Path.Value.” The first path segment represents the language (e.g., “deutsch,” “englisch”), and the second optionally denotes the resource (e.g., “bär,” “katze”).

2) Code generation driver

  • A controller generator produces C# source code from metadata. Leitner emphasized that the source of metadata is unconstrained — a web service, a database, even search results — anything is possible. In the demo, the metadata lives in simple JSON files under “Resources.”
  • Example “katze.json”: It lists localized names (“Katze”/“Cat”) and the entity’s structure (e.g., “Name,” “Gewicht,” “Farbe,” “Geburtstag,” “Anzahl an Impfungen”). “bär.json” is more compact (e.g., “Name,” “Gewicht”).

3) Programmatic C# code generation

  • The generator constructs source code via a string builder: “using” directives, a namespace, and classes for both model and controller.
  • Controller naming convention: “{Language}_{Name}Controller,” such as “deutsch_bär” and “deutsch_katze.” Controllers expose a “Get” method with “EnableQuery.”
  • Model: For each resource (e.g., “Katze”), a class is generated with properties derived from the JSON description. The primary key is set using a data annotation.
  • The generated controller delegates to a base controller’s “Get” method — the concrete implementation isn’t the point so much as ensuring the OData controller pattern is followed and recognized by the framework.

4) Runtime compilation

  • The generator builds a syntax tree and assembles references. A practical technique is to reuse references from the current AppDomain — the running web app already has OData and ASP.NET Core available, and the generated assembly should target the same environment.
  • Compilation uses “CSharpCompilation.Create” (latest language version, “Release” optimizations). With “Emit,” the compiled DLL is written to disk.
  • The application then loads the assembly.

5) Hot-adding the assembly

  • Application Parts: Using the “ApplicationPartManager,” the new assembly is added as an application part at runtime.
  • Routing awareness: The framework does not dynamically scan for changes on every request for performance reasons. Therefore, a custom “IActionDescriptorChangeProvider” implementation sets “HasChanged = true” and cancels a token source, prompting ASP.NET Core to rebuild action descriptors and pick up the new controllers.

6) Dynamic route transformation

  • Users call intuitive URLs (“/deutsch/bär,” “/englisch/cat”), but the generated controller classes are named “deutsch_bär” and “englisch_cat.” Leitner maps a “Dynamic Controller Route” with a transformer:
  • The “Controller” route value is rewritten to “{language}_{animal}.”
  • The “Action” route value is set to “Get.”
  • As a result, requests land on the correctly named, dynamically generated controllers and actions.

7) Dynamic OData model registration

  • OData is unaware of the new entities by default. The middleware also registers the OData route just like in the static example — only now the EDM model is built dynamically:
  • It reflects the newly loaded assembly to find types that represent entities (in the demo, types derived from a base entity).
  • For each such type, it adds an “EntitySet” on the “ODataConventionModelBuilder.”
  • OData receives this newly created “IEDMModel,” which includes the just-added entities and properties.
  • Outcome: Even “$metadata” reflects the dynamically generated types.

The demo in action: German/English, Bear/Cat, and live metadata

The live demo showcased how the pieces click together:

  • Request “/deutsch/bär”: The first call generates “Deutsch.dll” — visible on disk during the demo. The controller “deutsch_bär” is registered, the route is rewritten, and an OData model is built. The response contains data and OData metadata.
  • Request “/englisch/cat”: Similarly, “Englisch.dll” is generated. Property names are localized: in English “Name,” “Weight,” “Color,” “Birthday,” “Vaccinations,” versus German “Name,” “Gewicht,” “Farbe,” “Geburtstag,” etc.
  • “$metadata”: The OData metadata endpoint shows exactly the dynamic entity sets and properties — mirroring the JSON-driven model definitions.
  • Query options: Standard OData options like “$top” are active; Leitner demonstrated “$top=1.” Features such as “$orderby” or “$filter” can be used depending on configuration and model.
  • Robustness: The demo was tolerant of case differences in the path (e.g., “deutsch,” “Deutsch,” “katze,” “Katze”).

Leitner’s takeaway: “The proof is delivered. We can build a completely dynamic OData solution.”

Key technical takeaways from the session

  • OData requires an explicit EDM model: Even in the dynamic approach, “ODataConventionModelBuilder” is essential to register entity sets. Clients like Power BI depend on the metadata these models expose.
  • “EnableQuery” and a primary key are required: The “EnableQuery” attribute activates OData’s query logic for controller methods; and a primary key (via “Key” attribute) ensures OData can address entities properly.
  • Middleware is the right place: Generation must happen before routing, so that the application can add assemblies, update routing information, and supply OData with the model before the request is ultimately handled.
  • Application Parts plus an action descriptor change provider: Add assemblies with “ApplicationPartManager,” then signal a change via “IActionDescriptorChangeProvider” so routing rescans and recognizes new controllers.
  • Route transformation decouples URLs from class names: Users can call readable URLs (“/deutsch/bär”), while a transformer maps them to controller/action names (“deutsch_bär”, “Get”).
  • Metadata sources are open-ended: The demo used JSON for simplicity. In a real system, the metadata could come from services, databases, or other repositories — the generator isn’t tied to one source.
  • OData 7.x vs. 8.0: The session focuses on 7.x. There is a preview of 8.0 with notable differences; the patterns shown align with the 7.x ecosystem.

A practical roadmap for engineers

If you want to apply the session’s approach, the following steps provide a compact roadmap:

1) Understand the static OData setup

  • Add the OData 7.x package.
  • Register “AddOData” and enable query features (“Select,” “Filter,” “Count,” “Top”) in “UseEndpoints.”
  • Map an OData route (e.g., “/odata”).
  • Use “ODataConventionModelBuilder” and add an “EntitySet” for the initial entity.
  • Mark the controller method with “EnableQuery”; define a primary key in the model.

2) Prepare a middleware

  • Insert a custom middleware that reads “Request.Path.”
  • Extract the language and, optionally, the resource.

3) Define a metadata source

  • For a demo: JSON structures like “katze”/“bär” (localized names, properties, types).
  • For real-world usage: web services, databases, or other metadata repositories.

4) Implement the code generator

  • Produce source code strings: “using” directives, namespace, model class(es), controller class(es).
  • Use the “{language}_{resource}Controller” naming scheme.
  • Put “EnableQuery” on “Get”; ensure a “Key” is present on the model.

5) Compile at runtime

  • Build a syntax tree; reuse assembly references from the current AppDomain.
  • Compile with “CSharpCompilation.Create” and “Emit” to disk (e.g., “Deutsch.dll,” “Englisch.dll”).
  • Load the resulting assembly.

6) Register with Application Parts and refresh routing

  • Add the new assembly via “ApplicationPartManager.”
  • Signal routing changes using an “IActionDescriptorChangeProvider” — set “HasChanged = true” and cancel the token to force a refresh.

7) Configure route transformation

  • Map a Dynamic Controller Route; in the transformer, set “Controller = {language}_{resource},” “Action = Get.”

8) Register the OData model dynamically

  • Reflect the newly loaded assembly to find entity types (e.g., derived from a base entity type).
  • Add each as an “EntitySet” on the “ODataConventionModelBuilder.”
  • Register the OData route with this runtime-built EDM model.

9) Verify end to end

  • Call endpoints (“/deutsch/katze,” “/englisch/cat”).
  • Inspect “$metadata” to confirm the dynamic entities are present.
  • Exercise query options (e.g., “$top=1”).

Notes, constraints, and opportunities

Observations from the session:

  • Error handling and authentication: Leitner intentionally kept these minimal in the demo. Authentication could be integrated in the middleware. For production, add validation, logging, and robust error flows.
  • Primary key discipline: OData expects a key; when pulling metadata from external sources, ensure a proper key is defined for each entity.
  • Model scoping: The builder lets you expose only the parts of a larger domain that you want per dynamic route — helpful for multi-language or multi-domain scenarios.
  • Performance considerations: The explicit “HasChanged” signal makes sense because the framework doesn’t rescan everything on every request. Consider caching and when to trigger dynamic compilation.
  • Client tooling compatibility: With metadata correctly registered, tools like Power BI can consume dynamically generated endpoints — this is exactly what OData is good at.

Conclusion: Proof delivered — OData as a runtime-composed API

The session “Dynamically created OData Datasource” by Gerald Leitner (Fabasoft) demonstrated convincingly that OData APIs don’t have to be static. With a well-placed middleware, a source generator, runtime compilation, Application Parts, a route transformer, and a dynamically built OData EDM model, you can assemble an API on demand — including localized names, varying property schemas, and complete OData metadata.

In the demo, both “/deutsch/bär” and “/englisch/cat” worked as advertised, producing “Deutsch.dll” and “Englisch.dll,” localized properties (“Gewicht” vs. “Weight”), and correct “$metadata” documents. As Leitner summed it up, the proof is there: a completely dynamic OData solution is feasible.

For teams using OData for reporting, analytics, or integration, this is a compelling blueprint. Rather than compiling every variant upfront, models and controllers can be created on demand — tailored by language, domain, or tenant. Leitner mentioned a GitHub repository containing the code shown. It’s a valuable starting point to replicate and adapt the building blocks for your own scenarios.

From our DevJobs.at editorial perspective, the session is a strong example of using ASP.NET Core and OData 7.x exactly as intended — leveraging their extension points to achieve high flexibility without hacks. The outcome is both elegant and practical: a dynamic OData solution that composes itself at request time.