web.rest.server.examples - grecosoft/NetFusion GitHub Wiki

REST/HAL Server - Examples

The following sections provide the steps for configuring and returning resources with links using a variety of mapping methods.

nuget NetFusion.Rest.Server, NetFusion.Rest.Resources
register services.CompositeAppBuilder().AddRest().Compose();
types HalResourceMap, GroupMeta, ActionMeta, HalResource
sample code NetFusion-Examples/Examples/Source/REST

Setup

The NuGet packages and configuration of the NetFusion.Rest.Server plug-in can be found in the overview section. This must be completed before following completing the examples.

Resource Mapping

The examples will add all of the resource mappings to a single derived HalResourceMap class. For a production applications, multiple mapping files should be used for better organization. These mapping classes can also be located as child classes within their corresponding WebApi controllers. Add a mapping file to the Demo.WebApi project as follows:

mkdir ./src/Demo.WebApi/HalMappings
nano ./src/Demo.WebApi/HalMappings/ResourceMappings.cs
using NetFusion.Rest.Server.Hal;

namespace Demo.WebApi.HalMappings
{
    #pragma warning disable CS4014
    namespace ApiHost.Relations
    {
        public class ResourceMappings : HalResourceMap
        {
            protected override void OnBuildResourceMap()
            {
                // Your mappings will go here...
            }
        }
    }
}

Supporting Example Code

The following sections illustrate the different mapping methods used to specify the links associated with a given resource.

Create the following resources within the Demo.WebApi project as follows:

nano ./src/Demo.WebApi/Models/ListingModel.cs
using System;

namespace Demo.WebApi.Models
{
    public class ListingModel
    {
        public int ListingId { get; set; }
        public DateTime DateListed { get; set; }
        public int NumberBeds { get; set; }
        public int NumberFullBaths { get; set; }
        public int NumberHalfBaths { get; set; }
        public int SquareFeet { get; set; }
        public decimal AcresLot { get; set; }
        public string Address { get; set; }
        public string City { get; set; }
        public string State { get; set; }
        public string ZipCode { get; set; }
        public decimal ListPrice { get; set; }
        public int YearBuild { get; set; }
    }
}
nano ./src/Demo.WebApi/Models/PriceHistoryModel.cs
using System;

namespace Demo.WebApi.Models
{
    public class PriceHistoryModel
    {
        public int PriceHistoryId { get; set; }
        public int ListingId { get; set; }
        public DateTime DateOfEvent { get; set; }
        public string Event { get; set; }
        public decimal Price { get; set; }
        public string Source { get; set; }
    }
}

Create the following controllers:

nano ./src/Demo.WebApi/Controllers/ListingController.cs
using Microsoft.AspNetCore.Mvc;
using System;
using System.Threading.Tasks;
using Demo.WebApi.Models;
using NetFusion.Rest.Resources;
using NetFusion.Rest.Server.Hal;

namespace Demo.WebApi.Controllers
{
    [ApiController]
    [Route("api/listing")]
    public class ListingController : ControllerBase
    {
        [HttpGet("{id}")]
        public Task<IActionResult> GetListing(int id)
        {
            var listingModel = new ListingModel
            {
                ListingId = id,
                AcresLot = 3,
                Address = "112 Main Avenue",
                City = "Cheshire",
                State = "CT",
                ZipCode = "06410",
                DateListed = DateTime.Parse("5/5/2016"),
                ListPrice = 300_000M,
                NumberBeds = 5,
                NumberFullBaths = 3,
                NumberHalfBaths = 2,
                SquareFeet = 2500,
                YearBuild = 1986
            };

            var resource = listingModel.AsResource();
            return Task.FromResult<IActionResult>(Ok(resource));

        }

        [HttpPut("{id}")]
        public IActionResult UpdateListing(int id, ListingModel listing)
        {
            listing.ListingId = id;
            return Ok(listing.AsResource());
        }

        [HttpDelete("{id}")]
        public string DeleteListing(int id)
        {
            return $"DELETED: {id}";
        }
    }
}

nano ./src/Demo.WebApi/Controllers/PriceHistoryController.cs
using Microsoft.AspNetCore.Mvc;
using System;
using System.Linq;
using Demo.WebApi.Models;
using NetFusion.Rest.Resources;
using NetFusion.Rest.Server.Hal;

namespace Demo.WebApi.Controllers
{
    [ApiController]
    [Route("api/listing/price-history")]
    public class PriceHistoryController : ControllerBase
    {
        [HttpGet("{id}")]
        public IActionResult GetPriceHistory(int id)
        {
            var history = GetPricingHistory().FirstOrDefault(h => h.PriceHistoryId == id);
            var resource = history.AsResource();
            return Ok(resource);
        }

        [HttpGet("{listingId}/events")]
        public IActionResult GetPriceHistoryEvents(int listingId)
        {
            var historyResources = GetPricingHistory().Where(h => h.ListingId == listingId)
                .Select(h => h.AsResource());
            
            var resource = HalResource.New(r => r.EmbedResources(historyResources, "price-history"));
             
            return Ok(resource);
        }

        private static PriceHistoryModel[] GetPricingHistory()
        {
            return new[] {
                new PriceHistoryModel {
                    ListingId = 1000,
                    DateOfEvent = DateTime.Parse("5/5/2016"),
                    PriceHistoryId = 2000,
                    Event = "Listed",
                    Price = 300_000,
                    Source = "SMARTMLS" },

                new PriceHistoryModel {
                    ListingId = 1000,
                    DateOfEvent = DateTime.Parse("7/6/2016"),
                    PriceHistoryId = 2001,
                    Event = "Price Changed",
                    Price = 285_000,
                    Source = "SMARTMLS" }
            };
        }
    }
}

Resource Linking Examples

With the above example models and controllers in place, the following will illustrate each of the mapping methods for specifying resource links.

The specified links contained within derived HalResourceMap classes are read and cached when the NetFusion.Rest.Server plug-in bootstraps. When the client makes a request with an Accept header value of application/json+hal, the HalJsonOutputFormatter checks if any link meta-data exists for the model of the resource being returned. If present, the link meta-data and the state of the model are used to generate the link URLs and associated with the resource.

Controller/Model Lambda Expression

Add the following expression based link mappings to the ResourceMappings file:

nano ./src/Demo.WebApi/HalMappings/ResourceMappings.cs
using Demo.WebApi.Controllers;
using Demo.WebApi.Models;
using NetFusion.Rest.Common;
using NetFusion.Rest.Server.Hal;

namespace Demo.WebApi.HalMappings
{
#pragma warning disable CS4014
    namespace ApiHost.Relations
    {
        public class ResourceMappings : HalResourceMap
        {
            protected override void OnBuildResourceMap()
            {
                
                Map<ListingModel>()    
                    .LinkMeta<ListingController>(meta =>
                    {
                        meta.Url(RelationTypes.Self, (c, r) => c.GetListing(r.ListingId));
                        meta.Url("listing:update", (c, r) => c.UpdateListing(r.ListingId, default));
                        meta.Url("listing:delete", (c, r) => c.DeleteListing(r.ListingId));
                    })
                
                    .LinkMeta<PriceHistoryController>(meta => {
                        meta.Url(RelationTypes.History.Archives, (c, r) => c.GetPriceHistoryEvents(r.ListingId));
                    });
            }
        }
    }
}

IMAGEThis type of link specification should be used most often. If the controller's action method or model changes, a compile error will result. If hard-coded URLs where used, the developer would have to remember to update the URL. Also, when the HalJsonOutputFormatter adds this type of link to the resource, it knows exactly which model properties should be used for the route parameters. The resulting URL is generated by delegating to ASP.NET UrlHelper class. This method should be used when specifying URLs that are handled by controllers defined in the same WebApi application as the mapping.


Test the above mappings by running the service and using your HTTP Client of choice:

cd ./src/Demo.WebApi
dotnet run

IMAGE

Hard-Coding URL String

This example shows how to add a hard-coded relation link. This is best used when referencing an external related resource not managed by the application. Comment out current mappings and add the following:

nano ./src/Demo.WebApi/HalMappings/ResourceMappings.cs
using System.Net.Http;
using Demo.WebApi.Models;
using NetFusion.Rest.Server.Hal;

namespace Demo.WebApi.HalMappings
{
#pragma warning disable CS4014
    namespace ApiHost.Relations
    {
        public class ResourceMappings : HalResourceMap
        {
            protected override void OnBuildResourceMap()
            {
                Map<ListingModel>()
                    .LinkMeta(meta => meta.Href("conn", HttpMethod.Get, 
                    		"https://www.realtor.com/propertyrecord-search/Connecticut"))
                    .LinkMeta(meta => meta.Href("conn-cheshire", HttpMethod.Get, 
                        "https://www.realtor.com/realestateandhomes-search/Cheshire_CT"));
            }
        }
    }
}

Test the above mappings by running the service and using your HTTP Client of choice:

cd ./src/Demo.WebApi
dotnet run

IMAGE

Resource String Interpolated URL String

Allows the mapping to specify an URL based on the state of the returned model using C# string interpolation. This example shows how to add an URL string containing values from properties of the model. This mapping should be used when referencing an external URL based on properties of the model. Comment out the current mappings and add the following:

nano ./src/Demo.WebApi/HalMappings/ResourceMappings.cs
using System.Net.Http;
using Demo.WebApi.Models;
using NetFusion.Rest.Common;
using NetFusion.Rest.Server.Hal;

namespace Demo.WebApi.HalMappings
{
#pragma warning disable CS4014
    namespace ApiHost.Relations
    {
        public class ResourceMappings : HalResourceMap
        {
            protected override void OnBuildResourceMap()
            {
                Map<ListingModel>()
                    .LinkMeta(meta => meta.Href(RelationTypes.Alternate, HttpMethod.Get, 
                        r => $"http://www.homes.com/for/sale/{r.ListingId}"));
            }
        }
    }
}

Test the above mappings by running the service and using your HTTP Client of choice:

cd ./src/Demo.WebApi
dotnet run

IMAGE

URL Templates

The following example shows how to return a resource link consisting of a template. This type of URL can be used to reduce the number of returned resource links to the client. However, this type of URL depends on the client to replace the URL tokens with values. Comment out the current mappings and add the following:

nano ./src/Demo.WebApi/HalMappings/ResourceMappings.cs
using System.Threading.Tasks;
using Demo.WebApi.Controllers;
using Demo.WebApi.Models;
using Microsoft.AspNetCore.Mvc;
using NetFusion.Rest.Server.Hal;

namespace Demo.WebApi.HalMappings
{
#pragma warning disable CS4014
    namespace ApiHost.Relations
    {
        public class ResourceMappings : HalResourceMap
        {
            protected override void OnBuildResourceMap()
            {
                Map<ListingModel>()
                    .LinkMeta<PriceHistoryController>(meta => meta.UrlTemplate<int, IActionResult>(
                        "listing:prices", c => c.GetPriceHistoryEvents));
            }
        }
    }
}

Test the above mappings by running the service and using your HTTP Client of choice:

cd ./src/Demo.WebApi
dotnet run

IMAGE

Embedded Resources and Models

A returned resource can also have named resources and or models embedded. A resource's embedded resources is just a key/value pair of related items. The embedded resources can be either single or a collection of resources/models. When a resource with embedded resources is returned, the REST/HAL links are applied recursively.

The example will be changed so the price history is returned as an embedded resource.

Create a new controller as follows:

nano ./src/Demo.WebApi/Controllers/ListingEmbeddedController.cs
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using Demo.WebApi.Models;
using NetFusion.Rest.Server.Hal;

namespace Demo.WebApi.Controllers
{
    [Route("api/[controller]")]
    public class ListingEmbeddedController : Controller
    {
        [HttpGet("{id}")]
        public IActionResult GetListing(int id)
        {
            var listingModel = new ListingModel
            {
                ListingId = id,
                AcresLot = 3,
                Address = "112 Main Avenue",
                City = "Cheshire",
                State = "CT",
                ZipCode = "06410",
                DateListed = DateTime.Parse("5/5/2016"),
                ListPrice = 300_000M,
                NumberBeds = 5,
                NumberFullBaths = 3,
                NumberHalfBaths = 2,
                SquareFeet = 2500,
                YearBuild = 1986,
            };

            var listingResource = listingModel.AsResource();
            if (id == 1000)
            {
                var pricingResources = GetPricingHistory().Select(m => m.AsResource()).ToArray();
                listingResource.EmbedResources(pricingResources, "price-history");
            }
            else
            {
                var pricingResources = GetPricingHistory().ToArray();
                listingResource.EmbedModels(pricingResources, "price-history");
            }

            return Ok(listingResource);
        }

        private static IEnumerable<PriceHistoryModel> GetPricingHistory()
        {
            return new[] {
                new PriceHistoryModel {
                    ListingId = 1000,
                    DateOfEvent = DateTime.Parse("5/5/2016"),
                    PriceHistoryId = 2000,
                    Event = "Listed",
                    Price = 300_000,
                    Source = "SMARTMLS" },

                new PriceHistoryModel {
                    ListingId = 1000,
                    DateOfEvent = DateTime.Parse("7/6/2016"),
                    PriceHistoryId = 2001,
                    Event = "Price Changed",
                    Price = 285_000,
                    Source = "SMARTMLS" }
            };
        }
    }
}

Next, update the ResourceMapping class to define the following mappings. All other mappings should be commented out.

nano ./src/Demo.WebApi/HalMappings/ResourceMappings.cs
using Demo.WebApi.Controllers;
using Demo.WebApi.Models;
using NetFusion.Rest.Common;
using NetFusion.Rest.Server.Hal;

namespace Demo.WebApi.HalMappings
{
#pragma warning disable CS4014
    namespace ApiHost.Relations
    {
        public class ResourceMappings : HalResourceMap
        {
            protected override void OnBuildResourceMap()
            {
                Map<ListingModel>()
                    .LinkMeta<ListingController>(meta =>
                    {
                        meta.Url(RelationTypes.Self, (c, r) => c.GetListing(r.ListingId));
                        meta.Url("listing:update", (c, r) => c.UpdateListing(r.ListingId, default));
                        meta.Url("listing:delete", (c, r) => c.DeleteListing(r.ListingId));
                    });
                
                Map<PriceHistoryModel>()
                    .LinkMeta<PriceHistoryController>(meta =>
                    {
                        meta.Url(RelationTypes.Self, (c, r) => c.GetPriceHistory(r.PriceHistoryId));
                        meta.Url("events", (c, r) => c.GetPriceHistoryEvents(r.ListingId));
                    });
            }
        }
    }
}

Test the above mappings by running the service and using your HTTP Client of choice:

cd ./src/Demo.WebApi
dotnet run

IMAGE

Non resource models can also be embedded into a parent resource. If a returned model does not have any associated links or embedded items, it can be directly embedded. The only real difference is the model is not wrapped within a resource. If the same call is made passing a value other than 1000 for the ID, models not wrapped in a resource will be returned as shown by the following example.

IMAG

Notice how the embedded items no longer have associated links.

Defining Server API Entry Point

This step is not necessary for invoking REST API services but provides the client with a known entry point used to lookup the entry routes provided by the service. These root links are most often template based and are used to load root resources. Once a root resource is retrieved, links directly specified on the resource can be used.

Add an entry point resource to the Demo.WebApi project as follows:

nano ./src/Demo.WebApi/HalMappings/EntryPointMappings.cs
using System.Threading.Tasks;
using Demo.WebApi.Controllers;
using Microsoft.AspNetCore.Mvc;
using NetFusion.Rest.Resources;
using NetFusion.Rest.Server.Hal;

#pragma warning disable CS4014
namespace Demo.WebApi.HalMappings
{
    public class EntityPointMappings : HalResourceMap
    {
        protected override void OnBuildResourceMap()
        {
            Map<EntryPointModel>()
                .LinkMeta<ListingController>(meta =>
                {
                    meta.UrlTemplate<int, Task<IActionResult>>("listing:entry", c => c.GetListing);
                })
                .LinkMeta<PriceHistoryController>(meta =>
                {
                    meta.UrlTemplate<int, IActionResult>("history:entry", c => c.GetPriceHistory);
                });
        }
    }
}
nano ./src/Demo.WebApi/Controllers/ListingController.cs

Lastly, add the following method to the ListingController to return the entry model as a resource:

[HttpGet("entry")]
public IActionResult GetEntryPoint()
{
		var model = new EntryPointModel
  	{
    		Version = GetType().Assembly.GetName().Version?.ToString() ?? ""
  	};

  	return Ok(model.AsResource());
}

Test the retrieval of the entry resource:

cd ./src/Demo.WebApi
dotnet run

IMAGE