Dynamics 365 portals: JavaScript as Web Files

With a lot of the previous posts about liquid there has been a lot of JavaScript involved as well. We have referenced JavaScript libraries on CDN’s (Content Distribution Networks) as well as wrote JavaScript inline in Web Templates. You may not want to use CDN’s and even package your own JavaScript into minified files so that they can easily be reused elsewhere. Out of the box Dynamics 365 portals is blocked from uploading JavaScript files as Web Files, but we can makes some changes to allow JavaScript files to be allowed as Web Files.

If you sign-in to the Dynamics 365 portal as an administrator and create a new child file with a .js file as the attachment it will seem that it will work as expected but when you browse to that new web file URL you will be presented with a blank screen. If you look at your browser tools you may notice that the portal actually returned a HTTP 404 (File Not Found) error. But in this case it does not redirect you to the standard Page Not Found portal page. So what happened? The portal seems to know the URL (or route) of the file I uploaded but isn’t showing any of the content. If you navigate to the back-end CRM and go to the Portals > Web Files entity, you will notice your Web File is infact there, but when you open it you may notice its missing your Note record with an attachment.

Web Files utilize notes (annotations) to store the actual file that will be surfaced with the Web File. If you try now creating a note with your attachment the issue will be revealed that the attachment was blocked due to “the attachment is not a valid file type”.

The standard Dynamics 365 configuration is set to block all file attachments with the file extension .js. This is due to security concerns that the JavaScript could be used to execute code within your CRM that causes unsafe actions or potentially even remotely accesses other services, all depending on the JavaScript code. The JavaScript code you likely want to use with the portal though is not a security risk to the CRM but we do need to be cautious as to not create a security hole for all the other CRM users.

First let’s change the allowed file types for CRM so that we can upload a JavaScript file. Navigate in CRM to Settings > Administration, then select System Settings. On the General tab find the Set blocked file extensions for attachments and within this long list locate js and remove it and the trialing semi-colon. Select OK to save the System Settings.

Now if you navigate back to Portals > Web Files, locate your previously created Web File without the note attachment and now try creating a note with an attached JavaScript file it will successfully create the note. If you now navigate on the portal to the Web File URL you should either see the JS content or the browser will prompt you to download the JS file. If you also now try to create a new Web File from the front-side editor tools in the portal this will also now operate as expected.

You can now reference this JavaScript Web File like any other JavaScript file in your Web Templates or elsewhere.

<script src="/path/name-of-file.js"></script>

Once you have uploaded your JavaScript files being your own JavaScript or libraries like chart.js or fullcalendar.js or the many other libraries, you should revert the System Settings to again block the .js file type to remove any security risk of your end users accidentally uploading malicious JavaScript code. Just navigate to Settings > Administration, open System Settings and within the Set blocked file extensions for attachments, add back the js extension with a semi-colon if not the last item.

Dynamics 365 portals Spring Roadmap and Portal Source Code

If you frequent the Dynamics 365 Roadmap site (built on portals 🙂 ) you may notice a new area has been added for the CRM based Applications (Sales, Customer Service, Field Service, Project Service) for portals features. A number of items for all areas of Dynamics 365 has been posted as we near the Spring release of Dynamics 365 (v8.3), but this is a first for the portal to have been posted which seems to be the makings of a Spring Roadmap for the product. There are some exciting items on there including a release of the portal source code, Azure AD B2C and more. Below is all the items posted to the roadmap as well as some comments on each.

Let’s get to the big one first, portal source code.

Source code for Portals
A one time release of Portals code will be released to the Microsoft Download Center under MIT license for developers to download.

This feature enables Portals to be deployed to Dynamics 365 on-premise environments, and allows developers to customize the code to suit their specific business needs.

This is a huge news, complete open source of all portals code! This should also be taken with some caution though. Let’s understand what this means.

  • One time release – it will never be updated, ever, ever again.
  • Under MIT License – free, and full open source software (OSS), no direct support from Microsoft.
  • Microsoft Download Center – not taking pull requests, no contributing back to a master branch

Some awesome news but a big caution for corporations looking to invest in a supported product. With it being a one time release it is basically a point in time and will not be updated with bug fixes, new features nor will it be supported by Microsoft. The future is very much still online with portals as a service which will continue to be updated with bug fixes, new features and fully supported by Microsoft. If you choose the OSS path then support could come from partners, or other corporations but they will all come with their own costs and potentially different versions of the portal. Being OSS MIT means that anyone can take the source change it and re-publish it, so we could end up with endless different versions of OSS portals. These are the pitfalls with any OSS product so just be very aware if you are someone interested in it.

If your an on premise customer that was still considering v7 of Adxstudio Portals then you should strongly consider this as the cost is a lot less 😉 (free vs $20k+ and even getting a license). However the same cautions apply and if your looking for a fully supported product then online with portal service is the future of the product. If you were looking at Adxstudio Portals v7 because of it’s event or retail functionality then that is still a gap in the OSS version as well.

For developers, this will be a gold mine for those to see the inner workings of the portals product. Look at how the Adxstudio and Microsoft product teams built features, implemented the CRM SDK, and gain a better understanding of how the product operates or why it does something a certain way or modify it to your liking.

Myself and Adoxio will have more to say on Open Source Portals shortly and the direction Adoxio will be taking.

The next big roadmap item…

Support Azure AD-B2C for Portal authentication using a single sign-on (SSO) configuration

For Portals that require a consumer based login, this feature will now support the ability to:

  • Configure your portal authentication to use a Single Sign-On configuration
  • Support Azure AD-B2C for customer authentication
  • Manage your Portal security in Azure

* depends on Azure AD-B2C availability in the region(s) Portals are deployed

I blogged about B2C working in CRM portals late last year, but at the time B2C while supported in portals was not exactly recommended by Microsoft due to some user experience elements not being as desired. It’s great to see that it will be natively supported and recommended with the user experience elements corrected and even more included features released. I can’t more strongly recommend using B2C as your authentication in the portal and moving your authentication outside of Dynamics 365 so that it can be used by all applications and not just the portal. One identity for all your external applications.

Some other interesting new functionality coming soon:

Post installation language add-ons
This feature makes it easier to manage new additions to the existing portal language support.

Great to see, so many portals start off in 1 language and later want to add additional support and this will make that painless now.

Admin wizard to add an entity to the Portal
Easily publish any entity on your Portal using our new administrative wizard. When data is updated in the entity, it will automatically be available to Portal users who have access to these data.

This is likely a tool that will automatically make entity lists, entity forms, and entity permissions with all the web pages setup for you. Will make exposing new entities in the system extremely easy for out of box functionality.

Portal interaction tracking
Track your customer’s interactions with your Portal and funnel it to Dynamics 365 Customer Intelligence to plot a 360 view.

Sort of like Google Analytics but with all the relationships to your other Dynamics 365 data!

Whew! That is a lot of awesome news of features that are going to be coming to the portals shortly. There are a couple more smaller features, you can checkout the full listing on the roadmap site. The exact release dates for all of these is not mentioned, the earliest would be with the Spring release of portals. Previous releases of portals from Microsoft have typically lagged approximately a month behind the Dynamics 365 release. They could also come in monthly updates between now and the fall release.

We will have to keep an eye on the roadmap for future features being announced!

Dynamics 365 portals: Use aggregate FetchXML queries and Chart.js

A frequently requested feature of all types of portals is to display Dynamics 365 data in different displays like charts. The Dynamics 365 portal has a pre-built chart function that utilizes the out of box CRM charting functionality so that you can take the charts you create in CRM and display then easily on the portal. This functionality is somewhat limited but using the techniques discussed in Using liquid to return JSON or XML we can easily build a services that uses FetchXML aggregate queries to build data returns to summarize or roll-up data. With the aggregated query results you can quickly work with many of the JavaScript charting libraries like d3.js, chart.js, flot.js or many others to display a rich interactive view of the data that the default chart liquid component can’t do. In this post is a simple implementation of Chart.js which is a powerful, but simple library that has a very small footprint to build some charts with the FetchXML aggregate data.

Let’s start by creating the web template that is going to return the JSON data for the chart to display. As in the previous examples we will use the fetchxml liquid tag to retrieve the data but using the aggregate function of FetchXML to summarize data. Our example is going to continue using the course schedules and instructors, for this chart we would like to get a count of schedules by instructor.

{% fetchxml feed %}
<fetch version="1.0" output-format="xml-platform" mapping="logical" aggregate="true">
  <entity name="dpx_courseschedule">
    <attribute name="dpx_coursescheduleid" alias="schedule_count" aggregate="countcolumn" />
    <attribute name="dpx_instructorid" alias="dpx_instructorid" groupby="true" />   
  </entity>
</fetch>
{% endfetchxml %}[
  {% for item in feed.results.entities %}
    {
      "count": "{{ item.schedule_count }}",
      "instructor": "{{ item.dpx_instructorid.name }}",
      "instructorid": "{{ item.dpx_instructorid.id }}"
    }{% unless forloop.last %},{% endunless %}
  {% endfor %}
]

You’ll notice this FetchXML is different from previous examples. Firstly in the opening fetch tag it removes the distinct property and adds aggregate="true". Within the entity tags the attributes are limited to one to count by with the aggregate="countcolumn" for the dpx_coursescheduleid and then because we want the count by instructor the attribute for dpx_instructorid has the property groupby="true". As in previous examples we take the results, iterate through them and create a JSON array. Also don’t forget to set the web template Mime Type to application/json and ensure that you give the return JSON template a URL using the instructions in Use liquid to return JSON or XML.

With aggregate FetchXML queries you can include filter criteria so if you want to dynamically filter the data in the aggregation you can easily add parameters that are then added as filters. You could do some cool combinations with other components that use the same technique like the previous post on FullCalendar so that as you change months or filter down your calendar data it also is filtering the chart with the same parameters.

Now that we have a JSON data return template we can build the web template that will process it with the chart.js library. Below is the basic scaffolding for our template which is a canvas HTML5 element, the chart.js Javascript library and then a document ready with the get of the canvas element.

<canvas id="myChart"></canvas>

<script src="//cdnjs.cloudflare.com/ajax/libs/Chart.js/2.5.0/Chart.min.js"></script>
<script>
  $(function(){  // document ready    
      var ctx = $("#myChart");
  });
</script>

Firstly within document ready we need to make a call to retrieve the data using a jQuery AJAX GET method to the JSON return template URL. With the returned JSON we will transform it into a chart.js data object so that it can be added easily to any chart type. To do so we are going to create a couple of arrays, one for labels and one for data values, then populate them by iterating through the resulting JSON array from the AJAX call.

$(function(){ // document ready
    $.ajax({
      method: "GET",
      url: "/courses-aggregate-json/"
    })
    .done(function( results ) {
      
      var labels = [], datavalues = [];
      
      for (i = 0; i < results.length; i++) { 
        labels.push(results[i].instructor);
        datavalues.push(parseFloat(results[i].count));
      }

      var dataobj = {
          labels: labels,
          datasets: [{
              data: countdata
          }]
      };

      var ctx = $("#myChart");

      // bind data to chart object
    });
});

With the returned data now formatted for chart.js data object we can initialize the chart element with a chart type and the data object. The chart.js options for the pie type are also set to animate its display and have the legend displayed at the bottom of the chart.

// inserted within the done of the ajax get
// after the transform of return JSON and creation of dataobj

var ctx = $("#myChart");

var myChart = new Chart(ctx, {
  type: 'pie',
  data: dataobj,
  options: {
    animation:{
        animateScale:true
    }, 
    legend: {
      display: true,
      position: "bottom"
    }
  }
});

After getting this web template a URL with a page template and web page you should end up with your pie chart. Although its probably looking a little gray as we didn’t assign any colors.

If you read the previous post on FullCalendar then we know that our instructors entity actually contains an attribute that holds a hex color code value which we could use to color the chart. With following code which should be located at the top of the script tag prior to the document ready method, we can get those color codes, as well as create a function that will find the color for a specific instructor.

{% fetchxml feed %}
  <fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false">
    <entity name="dpx_instructor">
      <attribute name="dpx_instructorid" />
      <attribute name="dpx_colorcode" />
    </entity>
  </fetch>
{% endfetchxml %}

var instructorData = [{% for item in feed.results.entities %} {
      "instructorid": "{{ item.id }}",
      "color": "{{ item.dpx_colorcode | default:"#ffff00" }}"
    }{% unless forloop.last %},{% endunless %}
  {% endfor %}];

function findInstructorColor(lookupId) {
  for(var a = 0; a < instructorData.length; a++) {
    if(instructorData[a].instructorid == lookupId)
    {
      return instructorData[a].color;
    }
  }
}

With this code we are doing similar to the previous JSON return template but within the JavaScript so on render of the template it dynamically renders an object called instructorData which is a JSON array with the instructorid and color.

In the transform into the data object section update it with a new array called color and populate the array in the result iteration (line 6) which uses the findInstructorColor method, then include that new array in the dataobj (line 13).

var labels = [], countdata = [], color = [];

for (i = 0; i < results.length; i++) { 
  labels.push(results[i].instructor);
  countdata.push(parseFloat(results[i].count));
  color.push(findInstructorColor(results[i].instructorid));
}

var dataobj = {
  labels: labels,
  datasets: [{
    data: countdata,
    backgroundColor: color
  }]
};

With these changes you should now have a much nicer looking pie chart with color!

Using JavaScript charting libraries in combination with the FetchXML aggregate queries can allow you to start creating some really cool looking dashboards that go beyond the out of box functionality of portals. You can even use some Bootstrap components to help you do some nice formatting in combination with charts.

If your interested in learning more about liquid techniques I will be giving a webinar with xRMVirtual on April 26th at 12pm EST – Advanced Liquid Templates for Dynamics 365 portals.

Dynamics 365 portals: Implement FullCalendar with Liquid

Using the techniques discussed in Using liquid to return JSON or XML we can easily build a services that work with various with JavaScript libraries. FullCalendar is one of the most popular libraries for displaying events in various views on the web and can easily be used in the portal and creating a JSON return template for the event data. With FullCalendar you can fully customize a calendar output in the portal to really get that user experience you might be looking for. In this post is a simple implementation of FullCalendar with a custom entity JSON return template that will help outline how you can use this library in your portal projects.

First we can start with creating the web template that will be the return JSON data. Within this template a fetchxml query that takes at least the parameters of start and end so that it will filter for the date range being displayed on the calendar by adding the necessary conditions in the fetch.

{% fetchxml feed %}
<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false" count="100" returntotalrecordcount="true" {% if request.params['page'] %} page="{{request.params['page']}}" {% else %}
page="1"
{% endif %}>
  <entity name="dpx_courseschedule">
    <attribute name="dpx_number" />
    <attribute name="createdon" />
    <attribute name="dpx_starttime" />
    <attribute name="dpx_endtime" />
    <attribute name="dpx_courselocationid" />
    <attribute name="dpx_courseid" />
    <attribute name="dpx_coursescheduleid" />
    <filter type="and">
      <condition attribute="statecode" operator="eq" value="0" />
      {% if request.params['location'] %}
        <condition attribute="dpx_courselocationid" operator="eq" value="{{ request.params['location'] | xml_escape }}" />
      {% endif %}
      {% if request.params['start'] %}
        <condition attribute="dpx_starttime" operator="ge" value="{{ request.params['start'] | xml_escape }}" />
      {% endif %}
      {% if request.params['end'] %}
        <condition attribute="dpx_endtime" operator="le" value="{{ request.params['end'] | xml_escape }}" />
      {% endif %}
    </filter>
    <link-entity name="dpx_instructor" from="dpx_instructorid" to="dpx_instructorid" visible="false" link-type="outer" alias="instructorlink">
      <attribute name="dpx_contactid" />
      <attribute name="dpx_number" />
    </link-entity>
    <link-entity name="dpx_course" from="dpx_courseid" to="dpx_courseid" alias="courselink">
      <attribute name="dpx_coursecategoryid" />
      {% if request.params['category'] %}
        <filter type="and">
          <condition attribute="dpx_coursecategoryid" operator="eq" uiname="Category 1" uitype="dpx_coursecategory" value="{{ request.params['category'] | xml_escape }}" />
        </filter>
      {% endif %}
    </link-entity>
  </entity>
</fetch>
{% endfetchxml %}[
  {% for item in feed.results.entities %}
    {
      "title": "{{ item.dpx_courseid.name }} - {{ item['instructorlink.dpx_contactid'].name }}",
      "start": "{{ item.dpx_starttime | date_to_iso8601 }}",
      "end": "{{ item.dpx_endtime | date_to_iso8601 }}"
    }{% unless forloop.last %},{% endunless %}
  {% endfor -%}
]

The JSON is formatted to follow the FullCalendar Event Object and return an Event Source Object. Providing the return format directly in the service allows us to now easily hook up this service to the FullCalendar event configuration. The sample above only includes a number of simple properties but you can add any number of properties from the Event Object and map them to data in the query results.

Ensure that you now give the return JSON template a URL using the instructions in Use liquid to return JSON or XML and the Mime Type is set to application/json.

Now create a new web template that is going to implement the FullCalendar library and consuming the event return JSON template.

The first thing that needs to be added is references to both the CSS as well as JavaScript for FullCalendar as well as a reference to Moment.js. FullCalendar utilizes Moment.js for all date functions and therefore the Moment.js library needs to be referenced prior to the FullCalendar JavaScript library. Below is a simple template that uses the CDN’s for FullCalendar as well as Moment.js.

<link href="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.3.1/fullcalendar.min.css" rel="stylesheet" />

<div id='calendar'></div>

<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.1/moment.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.3.1/fullcalendar.min.js"></script>

<script>
  $(document).ready(function() {
      $('#calendar').fullCalendar({
          // FullCalendar configuration properties
      });
  });
</script>

The JavaScript in the code above initializes FullCalendar on the HTML element with the id of calendar and takes a series of parameters. You can review all the parameters available for configuration on the FullCalendar documentation. There are configuration properties for just about every function and include event callbacks that can be configured for certain events which could be utilized to make further calls to return JSON based templates.

To display event data on the calendar there are a couple of parameters that can be used to inject event data. For the following configuration we will be using the previously created web template that returns the FullCalendar Event Object based on our custom entity, but you can retrieve the data in various ways with the options that FullCalendar provides including multiple event sources.

<script>
  $(document).ready(function() {  
      $('#calendar').fullCalendar({
        events: {
          url: '/fullcalendar-json/',
          type: 'GET',
          error: function() {
            alert('there was an error while fetching events!');
          },
          color: 'yellow',
          textColor: 'black'
        }
      });
  });
</script>

Here we have replaced the blank configuration properties with the Events property which uses a url to retrieve event data. The URL is configured to our previous templates URL and the GET HTTP method is configured as the type. The event properties color and textColor are also configured for the default display of events on the calendar. After hooking up this new web template to a page template and web page you can view it in the portal and should have a result similar to the following:

Now we can take this further and enhance the display of events as well as add event filtering options. For my custom entities, I extended the Instructor with 2 additional fields, color and text color which I put in HEX color code values into for each record. I extended the fetchxml query to include these fields as well as the JSON output to add these fields as Event Object properties, below is the new JSON output including as well a URL that will link to the event details using a site marker.

{% fetchxml feed %}
<!-- fetchxml statement removed for length -->
{% endfetchxml %}[
  {% assign urlMarker = sitemarker['Event Details'] %}
  {% for item in feed.results.entities %}
    {
      "title": "{{ item.dpx_courseid.name }} - {{ item['instructorlink.dpx_contactid'].name }}",
      "start": "{{ item.dpx_starttime | date_to_iso8601 }}",
      "end": "{{ item.dpx_endtime | date_to_iso8601 }}",
      "color": "{{ item['instructorlink.dpx_colorcode'] }}",
      "textColor": "{{ item['instructorlink.dpx_textcolor'] }}",
      "url": "/{{ urlMarker.Url }}?id={{ item.id }}"
    }{% unless forloop.last %},{% endunless %}
  {% endfor -%}
]

In my FullCalendar liquid template I have add a fetchxml query so I can get a list of locations which are displayed in a drop down so users can then filter events in the calendar by this property.

{% fetchxml locationfeed %}
<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false">
  <entity name="dpx_courselocation">
    <attribute name="dpx_name" />
  </entity>
</fetch>
{% endfetchxml %}

<p>
  <div class="input-group">
    <select id="course-location" class="form-control">
        <option value="" selected disabled>Select a location...</option>
      {% for item in locationfeed.results.entities %}
        <option value="{{ item.id }}">{{ item.dpx_name }}</option>
      {% endfor %}
    </select>
    <span class="input-group-btn">
      <button id="refresh-events" class="btn btn-primary">Refresh Events</button>
    </span>
    <span class="input-group-btn">
      <button id="clear-events" class="btn btn-danger">Reset/Clear</button>
    </span>
  </div>
</p>

Then the JavaScript I have added 2 events for the new refresh and clear buttons, as well I have added a data property to the events call that will inject the location value if it is selected.

$('#calendar').fullCalendar({
  events: {
    url: '/fullcalendar-json/',
    type: 'GET',
    data: function() {
      var locationId = $('#course-location option:selected').val();            
      if (locationId) {
        return {
          location: locationId
        };
      }            
      return null;
    },
    error: function() {
      alert('there was an error while fetching events!');
    },
    color: 'yellow',   // a non-ajax option
    textColor: 'black' // a non-ajax option
  }
});

$('#refresh-events').on('click', function(){
  $('#calendar').fullCalendar('refetchEvents');
});

$('#clear-events').on('click', function(){
  $('#course-location').prop('selectedIndex',0);
  $('#calendar').fullCalendar('refetchEvents');
});

The display and function of the calendar should now be enhanced with colors for each instructor and the ability to filter the course schedules by location!

Hopefully this has helped show an example of the power you can get out of liquid templates by creating your own JSON based service and utilizing a JavaScript library like FullCalendar.

If your interested in learning more about liquid techniques I will be giving a webinar with xRMVirtual on April 26th at 12pm EST – Advanced Liquid Templates for Dynamics 365 portals.

Dynamics 365 portals: Use liquid to return JSON or XML

For many business requirements to get a desired user experience you may push the limits that the entity list functionality provides. With liquid you can write your own service to return data in various formats using the web template Mime Type property. By returning your own data you can inject logic and specific formatting using liquid functionality, this will allow you to utilize new components or libraries to help you provide the specific experience your requirements demand. This post will look at how liquid can be used with web templates to return JSON or XML so that the data can be consumed and used to build a complex user experience.

With liquid there are many ways to query for data. There is the entities object which can be used to retrieve a single record by ID. To get lists of data you can use the entity list which behind is using one or many entity views and an entity view is a fetchxml query that defines the view. Entity list contains a feature, OData feed, that allows you to take an entity view and make it available as a service. The OData feed is a great way to get a RESTFul JSON return but it has many shortcomings. If your interested in trying out the OData feed functionality then check out the documentation still available on the Adxstudio Community site – Entity List OData Feeds.

If you want to directly write your own queries in web templates, perhaps dynamically constructing them, utilize entity permission relationship based data, then you can use the liquid fetchxml tag. Below is a little outline of the functions of this liquid object.

{% fetchxml my_query %}
  <fetch version="1.0" mapping="logical">
    <!-- Write FetchXML here, use Liquid in here if you want, to build XML dynamically. -->
  </fetch>
{% endfetchxml %}
 
{{ my_query.xml | escape }}
{{ my_query.results.total_record_count }}
{{ my_query.results.more_records }}
{{ my_query.results.paging_cookie | escape }}
{% for result in my_query.results.entities %}
  {{ result.id | escape }}
{% endfor %}

Reference: Adxstudio Community Forums

With Dynamics 365 portals entity permissions is required by default and does not need to be referenced in the liquid tag. This differs from Adxstudio Portals v7.x, so if you are getting blank results using the fetchxml liquid object then ensure to first validate your entity permissions.

The fetchxml liquid tag and web templates Mime Type functionality provide the ability to build a web template that returns custom JSON or XML objects. With this you can build endpoints that intake custom parameters, perform logic while constructing the query, logic in returning the results, formatting the results and doing related record queries, all the while adhering to the entity permissions in place for that entity.

Below is an example of a web template that queries a custom course schedule entity with joins to related entities, course and instructor. The liquid code looks for 2 parameters location and category and if they exist it adds the fetch conditions for those parameters.

{% fetchxml feed %}
<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false" count="10" returntotalrecordcount="true" {% if request.params['page'] %} page="{{request.params['page']}}" {% else %} page="1" {% endif %}>
  <entity name="dpx_courseschedule">
    <attribute name="dpx_number" />
    <attribute name="createdon" />
    <attribute name="dpx_starttime" />
    <attribute name="dpx_endtime" />
    <attribute name="dpx_courselocationid" />
    <attribute name="dpx_courseid" />
    <attribute name="dpx_accountid" />
    <attribute name="dpx_cost" />
    <attribute name="dpx_coursescheduleid" />
    <order attribute="createdon" descending="true" />
    <filter type="and">
      <condition attribute="statecode" operator="eq" value="0" />
      {% if request.params['location'] %}
        <condition attribute="dpx_courselocationid" operator="eq" value="{{ request.params['location'] | xml_escape }}" />
      {% endif %}
    </filter>
    <link-entity name="dpx_instructor" from="dpx_instructorid" to="dpx_instructorid" visible="false" link-type="outer" alias="instructorlink">
      <attribute name="dpx_contactid" />
      <attribute name="dpx_number" />
    </link-entity>
    <link-entity name="dpx_course" from="dpx_courseid" to="dpx_courseid" alias="courselink">
      <attribute name="dpx_level" />
      <attribute name="dpx_lengthunit" />
      <attribute name="dpx_length" />
      <attribute name="dpx_coursecategoryid" />
      {% if request.params['category'] %}
        <filter type="and">
          <condition attribute="dpx_coursecategoryid" operator="eq" value="{{ request.params['category'] | xml_escape }}" />
        </filter>
      {% endif %}
    </link-entity>
  </entity>
</fetch>
{% endfetchxml %}{
  "totalcount": {{ feed.results.total_record_count }},
  "morerecords": {{ feed.results.more_records }},
  "page": {{ request.params['page'] | default: 0 }},
  "results": [
    {% for item in feed.results.entities %}
      {
        "starttime": "{{ item.dpx_starttime | date_to_iso8601 }}",
        "endtime": "{{ item.dpx_endtime | date_to_iso8601 }}",
        "instructorname": "{{ item['instructorlink.dpx_contactid'].name }}",
        "courselevel": "{{ item['courselink.dpx_level'].label }}",
        "location": {
          "id" : "{{ item.dpx_courselocationid.id }}",
          "name": "{{ item.dpx_courselocationid.name }}"
        }
      }{% unless forloop.last %},{% endunless %}
    {% endfor -%}
  ]
}

The fetchxml result is then formatted into a JSON object using the forloop liquid object to iterate through each entity record. Linked entity attributes are easily accessed via the linked entity alias {{ entityRecord['alias.attribute'] }}. With the return being JSON you will want to set the Mime Type property to application/json.

Another example using Case (incident) where we also use a N:N relationship of all child cases in the custom data object returned with the referenced attribute to load the entity relationship.

{% fetchxml feed %}
<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false" count="10" returntotalrecordcount="true" {% if request.params['page'] %} page="{{request.params['page']}}" {% else %} page="1" {% endif %}>
  <entity name="incident">
    <attribute name="ticketnumber" />
    <attribute name="prioritycode" />
    <attribute name="title" />
    <attribute name="createdon" />
    <attribute name="customerid" />
    <attribute name="ownerid" />
    <attribute name="statecode" />
    <attribute name="incidentid" />
    <attribute name="caseorigincode" />
    <order attribute="title" descending="false" />
  </entity>
</fetch>
{% endfetchxml %}{
  "totalcount": {{ feed.results.total_record_count }},
  "morerecords": {{ feed.results.more_records }},
  "page": {{ request.params['page'] | default: 0 }},
  "results": [
    {% for item in feed.results.entities %}
      {
        "ticketnumber": "{{ item.ticketnumber }}",
        "title": "{{ item.title }}",
        "customer":  {
          "id" : "{{ item.customerid.id }}",
          "name": "{{ item.customerid.name }}"
        },
        "incident_parent_incident": [
            {% for parent in item.incident_parent_incident.referenced %}
              {
                "parentticketnumber": "{{ parent.ticketnumber }}"
              }{% unless forloop.last %},{% endunless %}
            {% endfor %}
          ]
      }{% unless forloop.last %},{% endunless %}
    {% endfor -%}
  ]
}

If you instead wanted to return XML then it is just a matter of updating the Mime Type of the web template to application/xml and the code to output XML instead of the JSON format. Below is a sample of the first example but returning XML.

<!--FETCHXML query -->
{% endfetchxml %}<?xml version="1.0" encoding="UTF-8" ?>
<fetchxmlquery>
  <totalcount>{{ feed.results.total_record_count }}</totalcount>
  <morerecords>{{ feed.results.more_records }}</morerecords>
  <page>{{ request.params['page'] | default: 0 }}</page>
  <results>
    {% for item in feed.results.entities %}
      <item>
        <starttime>{{ item.dpx_starttime | date_to_iso8601 }}</starttime>
        <endtime>{{ item.dpx_endtime | date_to_iso8601 }}</endtime>
        <instructorname>{{ item['instructorlink.dpx_contactid'].name }}</instructorname>
        <courselevel>{{ item['courselink.dpx_level'].label }}</courselevel>
        <location>
          <id>{{ item.dpx_courselocationid.id }}</id>
          <name>{{ item.dpx_courselocationid.name }}</name>
        </location>
      </item>
    {% endfor %}
  </results>
</fetchxmlquery>

Once you have setup your web template with your liquid logic and Mime Type you need to get a URL for it. Create a page template of the Type, Web Template, and the previous Web Template referenced. As well ensure that Use Website Header and Footer is unchecked so that all that is returned is the data formed by the web template.

Now using the portals front-end editor, create a new web page using the new page template that references the web template. This will give your web template a URL and you can now refer to this endpoint within other JavaScript on the site. Here is a small jQuery sample calling a JSON endpoint URL and logging the result to the browser console. You can also do testing of your endpoints with Postman.

<script>
  $(function(){
    $.ajax({
      method: "GET",
      url: "/cases-json/"
    })
    .done(function( msg ) {
      console.log(JSON.parse(msg));
    });
  });
</script>

Note for entity permissions to function beyond anonymous a cookie for authentication must be attached to the request. JavaScript on the site already making requests will include the necessary cookie by default.

If your interested in learning more about liquid techniques I will be giving a webinar with xRMVirtual on April 26th at 12pm EST – Advanced Liquid Templates for Dynamics 365 portals.