A Breeze app typically retrieves metadata from the server asynchronously. But the typical way is not the only way.
This topic shows you how to capture the metadata in a JavaScript file and load it synchronously at runtime.
The code shown in this topic is adapted from the Breeze "DocCode" sample.
Pablo Romeo proposes an interesting approach to solving a related problem. He doesn't mind that the server provides the metadata and he doesn't want to serve a metadata.js file as described here. He does want the server-supplied metadata to be versioned and cached on the server. If that sounds more like your scenario, take a look at his StackOverflow question and answer.
We think a .NET web service should support Breeze clients by exposing a “metadata” endpoint. That’s what a BreezeJS client application expects out-of-the-box.
There is nothing wrong with this approach and it is all that many applications will ever need.
But retrieving metadata from the web service can be less than ideal for some applications and their developers. Because fetching metadata from the server is an asynchronous operation:
the app may have to wait for metadata to arrive before it is useful; delay can be really annoying.
we can’t create entities until we have metadata; if we want to create entities first thing, before issuing any queries, we have to fetch metadata explictily.
making the app wait for metadata adds complexity.
the app can’t function offline until it has metadata.
many unit tests could be both synchronous and fast if only metadata were already available.
We can ameliorate these problems if metadata are readily available on the client when the application begins.
We could load “launch data” (which includes metadata and lookup data) from browser local storage. That’s a fast operation and it is synchronous on most platforms.
While we heartily endorse this approach, it can’t stand on its own because
we have to stash the launch data in browser storage first which means the app must run at least once “the old way”
browser storage can be cleared any time.
updating the server metadata invalidates the local metadata.
local storage access is asynchronous on some platforms (e.g, Windows Store apps). It’s still faster than going remote. But the asynchronous protocol complicates production and test code.
Another option: we can write the metadata by hand in JavaScript and push it to the client in a script file. In fact, that’s exactly what you’ll do when your app talks to a non-.NET server or a server you can’t touch.
But we’d rather use generated metadata when available. Coding metadata by hand - especially for a large model - is tedious, error prone, and requires on-going maintenance. Generated metadata is almost carefree and automatically in-sync with the evolving service model.
We think the best approach is to capture and store the generated metadata in a variable in a JavaScript file. The client application loads this script file as it does all other scripts. On launch, it initializes a MetadataStore
from the metadata variable and proceeds without delay.
The metadata script is always current because we regenerate it automatically when the web server starts. We can use standard URL cache-busting to force the browser to replace an out-of-date cached script.
Best of all, it’s easy to set up. Let’s see how.
We want to see benefits right away so we’ll quickly assemble a metadata.js file by grabbing the metadata off the wire.
We’ll circle back and do this properly later in the story.
Here’s the procedure:
Add a new JavaScript file to your project in some convenient location next to other client assets; we’ll call ours metadata.js.
Launch the app to wake up the server and then set the browser address bar to the URL for the metadata endpoint. The browser tab might look a bit like this (with optional JSON formatting turned off):
Copy the contents of the browser window (ctrl-A, ctrl-C) and paste (ctrl-V) them into metadata.js. You’ve just pasted in a huge JSON object on a single line.
Open a couple of new lines at the top and assign the JSON.stringified
object to a variable in the global application namespace (pretend you have one if you don’t). In our example the variable is named window.app.metadata
.
Scroll to the end, add a new line, and enter “);” to terminate the JSON.stringified
expression.
Save the file.
Check it into source control (optional but well advised).
Here’s the resulting metadata.js:
window.app = window.app || {}; // define the "app" global namespace if it doesn't already exist
window.app.metadata = JSON.stringify(
{ "schema": { "namespace": "Northwind.Models", "alias": ... } }
);
Make sure the client application loads this script, perhaps by adding it to index.html as seen in this example.
...
<script src="app/app.js"></script>
<script src="app/metadata.js"></script>
...
Open the JavaScript application file that creates and initializes your EntityManager
. Modify it to look something like this annotated example:
var serviceName = 'breeze/northwind'; // your service root here
function createEntityManager() {
// define the Breeze `DataService` for this app
var dataService = new breeze.DataService({
serviceName: serviceName,
hasServerMetadata: false // don't ask the server for metadata
});
// create the metadataStore
var metadataStore = new breeze.MetadataStore({
namingConvention: camelCaseConvention // if you use this convention
});
// initialize it from the application's metadata variable
metadataStore.importMetadata(windows.app.metadata);
// create a new EntityManager that uses this metadataStore
return new breeze.EntityManager({
dataService: dataService,
metadataStore: metadataStore
});
}
That’s all there is to it!
Your new EntityManager
is primed with metadata and no longer requests metadata from the server … a fact you can confirm by running your application and checking the network traffic. You will not see a call to the “metadata” endpoint.
We’re a little uncomfortable with the way we created metadata.js.
We took a snapshot of the metadata as it is today. As the application evolves, the service model will evolve and the metadata will change. The metadata in our script file will be incorrect triggering a cascade of errors that may be hard to diagnose.
We can always recreate metadata.js by hand when we change the service model. That manual step is easily neglected. We’d sleep better if we automated the process, knowing that the client metadata are always in sync with our service model.
It’s not hard to do, so let’s do it. Our plan:
Notice that we (re)write metadata.js from the latest generated metadata before any client can download that script. Therefore every client will get the latest, greatest metadata.
And now for the details …
Study this example:
public static class MetadataScriptWriter {
public static void Write()
{
// get the metadata the same way we get it for the controller
var metadata = new NorthwindRepository().Metadata;
// construct the filename and runtime file location
var fileName = HostingEnvironment.MapPath("~/app/metadata.js");
// the same pre- and post-fix strings we used earlier
const string prefix = "window.app = window.app || {}; windows.app.metadata = JSON.stringify(";
const string postfix = ");";
// write to file
using (var writer = new StreamWriter(filename))
{
writer.WriteLine(prefix + metadata + postfix);
}
}
}
Notice that we generate the metadata the same way that the Web API controller does: by new-ing up an instance of the repository and calling its Metadata
method … which is likely implemented like this:
public string Metadata {
get {
return new EFContextProvider<NorthwindContext>().Metadata();
}
}
Also remarkable is the voodoo that determines the file location on the server
var fileName = HostingEnvironment.MapPath("~/app/metadata.js");
We want to write a new metadata.js file when the server launches, before it delivers any JavaScript files to clients.
We need to hook into the ASP.NET pipeline early. Almost any of the components in the App_Start folder will do. Let’s try WebApiConfig.cs.
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
MetadataScriptWriter.Write();
...
}
}
We can get metadata synchronously from a JavaScript file rather than asynchronously through a service call. That makes the client-side developer’s job a little easier and may help the application launch faster and run offline.
We can teach our app to load metadata from script in a few easy steps:
On the server
On the client
MetadataStore
EntityManager
that uses this metadataStore
.The newly created EntityManager
is ready to go and won’t ever ask the server for metadata.