Giter Site home page Giter Site logo

hallo's Introduction

Hallo

NuGet NuGet NuGet

Hallo is an implementation of the Hypertext Application Language (HAL) format for ASP.NET Core.

Why Hallo?

The primary design goal of Hallo is to enable generation of HAL documents through content negotiation without requiring HAL-specific code in models or controllers.

Getting started with Hallo

Installing Hallo

Hallo is available on Nuget as three packages:

dotnet add package Hallo
dotnet add package Hallo.AspNetCore
dotnet add package Hallo.AspNetCore.Mvc

The Hallo package is the core library which provides types for writing HAL representation generators and serializing objects to HAL+JSON strings. The Hallo.AspNetCore provides basic support for serializing HAL representations to the HttpResponse body stream and the Hallo.AspNetCore.Mvc package provides an output formatter to leverage ASP.NET MVC content negotiation functionality.

The rest of this readme will assume you are using the Hallo.AspNetCore.MVC package.

Using Hallo

Hallo does not require any changes to existing models or controllers so it can easily be added to an existing project.

To get started using Hallo you need to first register the output formatter in ASP.NET Core to enable content negotiation for HAL responses:

services.AddControllers(options =>
{
    options.RespectBrowserAcceptHeader = true;
    options.OutputFormatters.Add(new HalJsonOutputFormatter());
})

For every resource you want to generate a HAL document for you need to derive a new class from Hal<T> and implement one or more of IHalState<T>, IHalEmbedded<T> or IHalLinks<T>:

public class PersonRepresentation : Hal<Person>, 
                                    IHalState<Person>,
                                    IHalLinks<Person>, 
                                    IHalEmbedded<Person>
{
    public object StateFor(Person resource)
    {
        return new
        {
            resource.FirstName,
            resource.LastName
        };
    }
    
    public IEnumerable<Link> LinksFor(Person resource)
    {
        yield return new Link(Link.Self, $"/people/{resource.Id}");
        yield return new Link("contacts", $"/people/{resource.Id}/contacts");
    }

    public object EmbeddedFor(Person resource)
    {
        return new
        {
            Contacts = new List<Person>()
        };
    }
}

Each resource representation needs to be registered in the ASP.NET Core IServiceCollection:

services.AddTransient<Hal<Person>, PersonRepresentation>();

Given the example above, a HTTP request such as:

GET http://localhost:5000/people/1
Accept: application/hal+json

will produce the result:

{
  "firstName": "Geoffrey",
  "lastName": "Merrill",
  "_embedded": {
    "contacts": []
  },
  "_links": {
    "self": {
      "href": "/people/1"
    },
    "contacts": {
      "href": "/people/1/contacts"
    }
  }
}

Dependency Injection

As resource representations are registered with and resolved through ASP.NET Core services, the standard approach to injecting dependencies can be applied.

Example

public class PersonRepresentation : Hal<Person>, 
                                    IHalEmbeddedAsync<Person>
{
    private readonly ContactsLookup _contacts;

    public PersonRepresentation(ContactsLookup contacts)
    {
        _contacts = contacts;
    }

    public async Task<object> EmbeddedForAsync(Person resource)
    {
        var contacts = await _contacts.GetFor(resource.Id);
        
        return new
        {
            Contacts = contacts
        };
    }
}

Async Support

Hallo provides the interfaces IHalStateAsync<T>, IHalEmbeddedAsync<T> and IHalLinksAsync<T>. These interfaces define asynchronous version of the StateFor, EmbeddedFor and LinksFor methods to enable the execution of asynchronous code as part of the HAL document generation process.

Example

public class PersonRepresentation : Hal<Person>, 
                                    IHalLinks<Person>, 
                                    IHalEmbeddedAsync<Person>
{
    public IEnumerable<Link> LinksFor(Person resource)
    {
        yield return new Link(Link.Self, $"/people/{resource.Id}");
        yield return new Link("contacts", $"/people/{resource.Id}/contacts");
    }

    public async Task<object> EmbeddedForAsync(Person resource)
    {
        var contacts = await FetchContactsAsync(resource.Id);
        
        return new
        {
            Contacts = contacts
        };
    }
}

Nested Representations

Sometimes it is necessary to produce "nested" HAL documents. For example it is common to generate _links for resources under the _embedded property in the root HAL document.

Hallo supports recursive generation of HAL documents by wrapping embedded resources in a HalRepresentation.

Example

public class PersonRepresentation : Hal<Person>, 
                                    IHalLinks<Person>, 
                                    IHalEmbedded<Person>
{
    public IEnumerable<Link> LinksFor(Person resource)
    {
        yield return new Link(Link.Self, $"/people/{resource.Id}");
        yield return new Link("contacts", $"/people/{resource.Id}/contacts");
    }

    public object EmbeddedFor(Person resource)
    {
        var spouse = new Person
        {
            Id = 321,
            FirstName = "A",
            LastName = "Spouse"
        };

        var links = LinksFor(spouse);
        
        return new
        {
            Spouse = new HalRepresentation(spouse, links)
        };
    }
}

The above example will produce a response of:

{
  "id": 1,
  "firstName": "Geoffrey",
  "lastName": "Merrill",
  "_embedded": {
    "spouse": {
      "id": 321,
      "firstName": "A",
      "lastName": "Spouse",
      "_links": {
        "self": {
          "href": "/people/321"
        },
        "contacts": {
          "href": "/people/321/contacts"
        }
      }
    }
  },
  "_links": {
    "self": {
      "href": "/people/1"
    },
    "contacts": {
      "href": "/people/1/contacts"
    }
  }
}

Prefixing Links With a Virtual Path

If a deployed API is available via a virtual path such as an IIS sub-application/virtual directory, API gateway or reverse proxy it may be necessary to prefix links with the virtual path. For example, an API may be developed locally with the URL http://localhost:5000/people/{id} however the API is deployed to production behind an API gateway with the URL http://my-app/address-book/people/{id}. In this scenario it may be preferable to generate links prefixed with /address-book.

This can be easily achieved by ensuring the PathBase property for the request is set and using the ASP.NET Core IUrlHelper to create links rather than the string building approach used in this README.

Example

The following ASP.NET Core services need to be registered on startup:

services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();
services.AddScoped(x => {
    var actionContext = x.GetRequiredService<IActionContextAccessor>().ActionContext;
    var factory = x.GetRequiredService<IUrlHelperFactory>();
    return factory.GetUrlHelper(actionContext);
});

The IUrlHelper can then be injected into representations and used to create links:

public class PersonRepresentation : Hal<Person>, 
                                    IHalLinks<Person>
{
    private readonly IUrlHelper _urlHelper;

    public PersonRepresentation(IUrlHelper urlHelper)
    {
        _urlHelper = urlHelper;
    }

    public IEnumerable<Link> LinksFor(Person resource)
    {
        var self = _urlHelper.Action("Get", "People", new {id = resource.Id});
        var contacts = _urlHelper.Action("List", "Contacts", new {personId = resource.Id});

        yield return new Link(Link.Self, self);
        yield return new Link("contacts", contacts);
    }
}

Assuming a PathBase value of /address-book, the above example will produce a response of:

{
  "id": 1,
  "firstName": "Geoffrey",
  "lastName": "Merrill",
  "_links": {
    "self": {
      "href": "/address-book/people/1"
    },
    "contacts": {
      "href": "/address-book/people/1/contacts"
    }
  }
}

hallo's People

Contributors

dependabot[bot] avatar j0nnyhughes avatar jasonmitchell avatar jchannon avatar roketworks avatar thefringeninja avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

hallo's Issues

Support for curies

This could probably be done pretty within Links by supporting the name and title properties. However it is also possible to use a curie name as part of a property name elsewhere in the representation so this will need to be taken into consideration.

{
    "_links": {
        "curies": [{ "name": "ea", "href": "http://example.com/docs/rels/{rel}", "templated": true }],
        "ea:find": {
            "href": "/orders{?id}",
            "templated": true
        }
    },
    "_embedded": {
        "ea:order": [{
            "_links": {
                "ea:basket": { "href": "/baskets/98712" },
                "ea:customer": { "href": "/customers/7809" }
            }
        }]
    }
}

Extensibility of representations

From https://tools.ietf.org/html/draft-kelly-json-hal-08#appendix-B.5

B.5.  Why does HAL have no forms?

   Omitting forms from HAL was an intentional design decision that was
   made to keep it focused on linking for APIs.  HAL is therefore a good
   candidate for use as a base media type on which to build more complex
   capabilities.  An additional media type is planned for the future
   which will add form-like controls on top of HAL.

Should Hallo be flexible enough to define additional concepts such as _forms? If so, how should this work?

Should links be non-nullable in HalRepresentation?

In general it wouldn't be expected for a consumer to implement LinksFor and return null however the default behaviour when LinksFor is not implemented is to return null. Perhaps this should return an empty collection?

Requires #11 to be resolved before links can be non-nullable.

How to support creating links for sub-collections in the PagedListRepresentation sample?

The Hallo.Sample project includes an example PagedListRepresentation<T> which accepts the baseUrl for the collection. Below is the snippet of how this is constructed.

public class PersonListRepresentation : PagedListRepresentation<Person>
{
public PersonListRepresentation(PersonRepresentation personRepresentation)
: base("/people", personRepresentation) { }
}

This works fine for constant base URLs like in this example but is more difficult when the base URL is dynamic. For example, if the URL to the collection was /race/123/cars (where 123 is the resource id) how do we support getting the race id into the base URL?

Empty Links collection returns null _links

Expected behaviour is that no "_links" property is included in the response body when a null or empty Links collection is returned from a LinksFor implementation.

For example, given the following method:

public IEnumerable<Link> LinksFor(Person resource)
{
    return new Link[0];
}

The response will include a null "_links" property like below:

{
  "id": 1,
  "firstName": "Geoffrey",
  "lastName": "Merrill",
  "_links": null
}

Align default JsonSerializerOptions with defaults of SystemTextJsonOutputFormatter

Currently if no JsonSerializerOptions are passed when instantiating HalJsonOutputFormatter, a new instance is created with mostly default settings. The defaults should align with the defaults for SystemTextJsonOutputFormatter to avoid surprises when switching between application\json and application\hal+json.

In particular, the defaults for SystemTextJsonOutputFormatter will use a camel case naming policy while HalJsonOutputFormatter does not. The screenshot below shows the defaults for SystemTextJsonOutputFormatter.

Screenshot 2021-02-24 at 21 11 25

Thoughts on having 2 packages, 1 for MVC, 1 for ASP.NET Core

The ouputformatter is actually the MVC specific part here but we could move that logic into a class which an outputformatter calls and we could publish a package just for MVC. That logic can then be used inside a ASP.NET Core package where we could provide extensions similar to

Use((next,ctx) => halHandler(next,ctx))

halHandler would also then call the class with the logic in it and write to the response stream directly

That way C# and F# devs could use the lib

What do you think?

Update build and readme to reflect new 3 project structure

PR #53 splits the project into 3 separate packages. The build will need updated to publish the 3 packages and the installation structures in the readme will need updated to cover this.

Since this is a bit of a breaking change it would be good to also take the opportunity to resovle #49 as well to minimise the major version updates.

Should state be non-nullable in HalRepresentation?

Can it be assumed that HAL responses will always include some state from the resource?

It would be possible for a consumer to implement StateFor and return null - seems like an uncommon use case but does it serve a purpose?

Multiple links with same relation

Example:

{
    "_links": {
      "items": [{
          "href": "/first_item"
      },{
          "href": "/second_item"
      }]
    }
}

Ideally this would be picked up from a flat list:

protected override IEnumerable<Link> LinksFor(AResource resource)
{
    yield return new Link("items", $"/first_item");
    yield return new Link("items", $"/second_item");
}

How to access route parameters for generating links?

Similar to the scenario in #42.

Assuming a route like /race/123/cars/456 and the following model for a car resource:

public class Car
{
    public int Id { get; set; }
    public string DriverName { get; set; }
    public string TeamName { get; set; }
}

How would do we support getting the race id 123 to generate links without needing to modify the model to support generating the state?

How to return collections

Hi. Is there a built-in way to output collections? If so, could you please give an example in the Readme of how? I see that you have a PagedRepresentation in the samples, but I wonder if a default Hal<IEnumerable<T>> exists as part of the framework? I'd expect the output to be similar to the following:

{
    "_links": {
        "self": {
            "href": "/people?searchParameter=foo&offset=30&limit=10"
        }
    },
    "count": 3,
    "_embedded": {
        "people": [
            {
                "firstName": "Geoffrey",
                "lastName": "Merrill",
                "_links": {
                    "self": {
                        "href": "/people/1"
                    },
                    "contacts": {
                        "href": "/people/1/contacts"
                    }
                }
            },
            {
                "firstName": "George",
                "lastName": "Murray",
                "_links": {
                    "self": {
                        "href": "/people/2"
                    },
                    "contacts": {
                        "href": "/people/2/contacts"
                    }
                }
            },
            {
                "firstName": "Giles",
                "lastName": "Munton",
                "_links": {
                    "self": {
                        "href": "/people/3"
                    },
                    "contacts": {
                        "href": "/people/3/contacts"
                    }
                }
            }
        ]
    }
}

Thanks!

Create sample or test demonstrating extending HAL representations

For example, can the library be extended to support _forms for example?

With the upcoming major release it would be a good opportunity to try this and make any core library changes necessary to avoid needing another major release in the future to support it.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.