{"id":19099761,"url":"https://github.com/riskfirst/riskfirst.hateoas","last_synced_at":"2025-04-06T02:09:56.089Z","repository":{"id":19855035,"uuid":"86346288","full_name":"riskfirst/riskfirst.hateoas","owner":"riskfirst","description":"Powerful HATEOAS functionality for .NET web api","archived":false,"fork":false,"pushed_at":"2023-02-22T17:50:11.000Z","size":2821,"stargazers_count":78,"open_issues_count":10,"forks_count":25,"subscribers_count":17,"default_branch":"master","last_synced_at":"2024-05-28T14:47:07.206Z","etag":null,"topics":["aspnet","dotnetcore","hateoas"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/riskfirst.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-03-27T14:36:00.000Z","updated_at":"2024-06-18T15:18:40.627Z","dependencies_parsed_at":"2024-06-18T15:18:34.286Z","dependency_job_id":"b5247e49-1361-4111-b0df-9816b2290386","html_url":"https://github.com/riskfirst/riskfirst.hateoas","commit_stats":{"total_commits":83,"total_committers":16,"mean_commits":5.1875,"dds":0.7349397590361446,"last_synced_commit":"fe5bb78b3e8b17dc69ca59081dcc04a47e21cc40"},"previous_names":[],"tags_count":4,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/riskfirst%2Friskfirst.hateoas","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/riskfirst%2Friskfirst.hateoas/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/riskfirst%2Friskfirst.hateoas/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/riskfirst%2Friskfirst.hateoas/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/riskfirst","download_url":"https://codeload.github.com/riskfirst/riskfirst.hateoas/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247423515,"owners_count":20936626,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["aspnet","dotnetcore","hateoas"],"created_at":"2024-11-09T03:52:15.334Z","updated_at":"2025-04-06T02:09:56.058Z","avatar_url":"https://github.com/riskfirst.png","language":"C#","funding_links":[],"categories":["aspnet"],"sub_categories":[],"readme":"# RiskFirst.Hateoas\n\n[![CI Build](https://github.com/riskfirst/riskfirst.hateoas/actions/workflows/build.yml/badge.svg)](https://github.com/riskfirst/riskfirst.hateoas/actions/workflows/build.yml)\n\nAn implementation of [HATEOAS](https://en.wikipedia.org/wiki/HATEOAS) for aspnet core web api projects which gives full control of which links to apply to models returned from your api. In order to communicate varying state to the end-user, this library fully integrates with Authorization, and allows arbitrary conditions to determine whether to show or hide HATEOAS links between api resources.\n\n### Getting started\n\nInstall the package from [Nuget.org](https://www.nuget.org/packages/riskfirst.hateoas)\n\n```powershell\nPM\u003e Install-Package RiskFirst.Hateoas\n```\n\nThis will include the dependency RiskFirst.Hateoas.Models which was introduced in version 3.0.0 to remove the AspNetCore dependencies from assemblies referencing the LinkContainer base classes.\n\nConfigure the links to include for each of your models.\n\n```csharp\npublic class Startup\n{\n  public void ConfigureServices(IServicesCollection services)\n  {\n    services.AddLinks(config =\u003e\n    {\n      config.AddPolicy\u003cMyModel\u003e(policy =\u003e {\n          policy.RequireSelfLink()\n                .RequireRoutedLink(\"all\", \"GetAllModelsRoute\")\n                .RequireRoutedLink(\"delete\", \"DeleteModelRoute\", x =\u003e new { id = x.Id });\n      });\n    });\n  }\n}\n```\n\nInject `ILinksService` into any controller (or other class in your project) to add links to a model.\n\n```csharp\n[Route(\"api/[controller]\")]\npublic class MyController : Controller\n{\n    private readonly ILinksService linksService;\n\n    public MyController(ILinksService linksService)\n    {\n        this.linksService = linksService;\n    }\n\n    [HttpGet(\"{id}\",Name = \"GetModelRoute\")]\n    public async Task\u003cMyModel\u003e GetMyModel(int id)\n    {\n         var model = await myRepository.GetMyModel(id);\n         await linksService.AddLinksAsync(model);\n         return model;\n    }\n    [HttpGet(Name=\"GetAllModelsRoute\")]\n    public async Task\u003cIEnumerable\u003cMyModel\u003e\u003e GetAllModels()\n    {\n         //... snip .. //\n    }\n\n    [HttpDelete(\"{id}\",Name = \"DeleteModelRoute\")]\n    public async Task\u003cMyModel\u003e DeleteMyModel(int id)\n    {\n         //... snip .. //\n    }\n}\n```\n\nThe above code would produce a response as the example below\n\n```json\n{\n  \"id\": 1,\n  \"someOtherField\": \"foo\",\n  \"_links\": {\n    \"self\": {\n      \"rel\": \"MyController\\\\GetModelRoute\",\n      \"href\": \"https://api.example.com/my/1\",\n      \"method\": \"GET\"\n    },\n    \"all\": {\n      \"rel\": \"MyController\\\\GetAllModelsRoute\",\n      \"href\": \"https://api.example.com/my\",\n      \"method\": \"GET\"\n    },\n    \"delete\": {\n      \"rel\": \"MyController\\\\DeleteModelRoute\",\n      \"href\": \"https://api.example.com/my/1\",\n      \"method\": \"DELETE\"\n    }\n  }\n}\n```\n\nor if you're using XML\n\n```xml\n\u003c?xml version=\"1.0\"?\u003e\n\u003cMyModel xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\u003e\n  \u003clink href=\"https://api.example.com/my/1\" method=\"GET\" rel=\"self\"/\u003e\n  \u003clink href=\"https://api.example.com/my\" method=\"GET\" rel=\"all\"/\u003e\n  \u003clink href=\"https://api.example.com/my/1\" method=\"DELETE\" rel=\"delete\"/\u003e\n  \u003cId\u003e1\u003c/Id\u003e\n  \u003cSomeOtherField\u003efoo\u003c/SomeOtherField\u003e\n\u003c/MyModel\u003e\n```\n\n### Multiple policies for a model\n\nIt is possible to specify multiple named policies for a model during startup by providing a policy name to `AddPolicy`. For example, you could have the default (unnamed) policy give basic links when the model is part of a list, but more detailed information when a model is returned alone.\n\n```csharp\npublic class Startup\n{\n  public void ConfigureServices(IServicesCollection services)\n  {\n    services.AddLinks(config =\u003e\n    {\n      config.AddPolicy\u003cMyModel\u003e(policy =\u003e {\n          policy.RequireRoutedLink(\"self\",\"GetModelRoute\", x =\u003e new {id = x.Id })\n      });\n\n      config.AddPolicy\u003cMyModel\u003e(\"FullInfo\",policy =\u003e {\n          policy.RequireSelfLink()\n                .RequireRoutedLink(\"all\", \"GetAllModelsRoute\")\n                .RequireRoutedLink(\"parentModels\", \"GetParentModelRoute\", x =\u003e new { parentId = x.ParentId });\n                .RequireRoutedLink(\"subModels\", \"GetSubModelsRoute\", x =\u003e new { id = x.Id });\n                .RequireRoutedLink(\"delete\", \"DeleteModelRoute\", x =\u003e new { id = x.Id });\n      });\n    });\n  }\n}\n```\n\nWith a named policy, this can be applied at runtime using an overload of `AddLinksAsync` which takes a policy name:\n\n```csharp\nawait linksService.AddLinksAsync(model,\"FullInfo\");\n```\n\nYou can also markup your controller method with a `LinksAttribute` to override the default policy applied. The below code would apply the \"FullInfo\" profile to the returned model without having to specify the policy name in the call to `AddLinksAsync`.\n\n```csharp\n[Route(\"api/[controller]\")]\npublic class MyController : Controller\n{\n    private readonly ILinksService linksService;\n\n    public MyController(ILinksService linksService)\n    {\n        this.linksService = linksService;\n    }\n\n    [HttpGet(\"{id}\",Name = \"GetModelRoute\")]\n    [Links(Policy = \"FullInfo\")]\n    public async Task\u003cMyModel\u003e GetMyModel(int id)\n    {\n         var model = await myRepository.GetMyModel(id);\n         await linksService.AddLinksAsync(model);\n         return model;\n    }\n}\n```\n\nAnother way to achieve the same thing is to mark the actual object with the `LinksAttribute`:\n\n```csharp\n[Links(Policy=\"FullInfo\")]\npublic class MyModel : LinkContainer\n{ }\n\n[Route(\"api/[controller]\")]\npublic class MyController : Controller\n{\n    private readonly ILinksService linksService;\n\n    public MyController(ILinksService linksService)\n    {\n        this.linksService = linksService;\n    }\n\n    [HttpGet(\"{id}\",Name = \"GetModelRoute\")]\n    public async Task\u003cMyModel\u003e GetMyModel(int id)\n    {\n         MyModel model = await myRepository.GetMyModel(id);\n         await linksService.AddLinksAsync(model);\n         return model;\n    }\n}\n```\n\nThere are further overloads of `AddLinksAsync` which take an instance of [`ILinksPolicy`](src/RiskFirst.Hateoas/ILinksPolicy.cs) or an array of [`ILinksRequirement`](src/RiskFirst.Hateoas/ILinksRequirement.cs) which will be evaluated at runtime. This should give complete control of which links are applied at any point within your api code.\n\n### Configuring Href and Rel transformations\n\nThere should not have much need to change how the `Href` is transformed, however one common requirement is to output relative instead of absolute uris. This can be tried in the [Basic Sample](Samples/RiskFirst.Hateoas.BasicSample)\n\n```csharp\nservices.AddLinks(config =\u003e\n{\n  config.UseRelativeHrefs();\n  ...\n});\n```\n\nBoth Href and Rel transformations can be fully controlled by supplying a class or Type which implements [`ILinkTransformation`](src/RiskFirst.Hateoas/ILinkTransformation.cs).\n\n```csharp\nservices.AddLinks(config =\u003e\n{\n  // supply a type implementing ILinkTransformation\n  config.UseHrefTransformation\u003cMyHrefTransformation\u003e();\n  // or supply an instance\n  config.UseRelTransformation(new MyRelTransformation());\n});\n```\n\nAlternatively, transformations can be configured using a builder syntax\n\n```csharp\nservices.AddLinks(config =\u003e\n{\n  // output a uri for the rel values\n  config.ConfigureRelTransformation(transform =\u003e transform.AddProtocol()\n                                                          .AddHost()\n                                                          .AddVirtualPath(ctx =\u003e $\"/rel/{ctx.LinkSpec.ControllerName}/{ctx.LinkSpec.RouteName}\");\n});\n```\n\nBoth ways of customizaing transformations can be seen in the [LinkConfigurationSample](samples/RiskFirst.Hateoas.LinkConfigurationSample).\n\n### Authorization and Conditional links\n\nIt is likely that you wish to control which links are included with each model, and one common requirement is to only show links for which the current user is authorized. This library fully integrates into the authorization pipeline and will apply any authorization policy you have applied to the linked action.\n\nTo enable authorization on a link provide the `AuthorizeRoute` condition.\n\n```csharp\npublic class Startup\n{\n  public void ConfigureServices(IServicesCollection services)\n  {\n    services.AddLinks(config =\u003e\n    {\n      config.AddPolicy\u003cMyModel\u003e(\"FullInfo\",policy =\u003e {\n          policy.RequireSelfLink()\n                .RequireRoutedLink(\"all\", \"GetAllModelsRoute\")\n                .RequireRoutedLink(\"parentModels\", \"GetParentModelRoute\",\n                                      x =\u003e new { parentId = x.ParentId }, condition =\u003e condition.AuthorizeRoute());\n                .RequireRoutedLink(\"subModels\", \"GetSubModelsRoute\",\n                                      x =\u003e new { id = x.Id }, condition =\u003e condition.AuthorizeRoute());\n                .RequireRoutedLink(\"delete\", \"DeleteModelRoute\",\n                                      x =\u003e new { id = x.Id }, condition =\u003e condition.AuthorizeRoute());\n      });\n    });\n  }\n}\n```\n\nIn the above example, `GetParentModelRoute`, `GetSubModelsRoute` \u0026 `DeleteModelRoute` will not be shown to a user who does not have access to those routes as defined by their authorization policies. See the [Microsoft documentation](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/) for more information on authrization within an aspnet core webapi project.\n\nAs with the above examples, there are further condition methods which allow you to specifiy a policy name, an absolute policy or a set of requirements.\n\nYou can also conditionally show a link based on any boolean logic by using the `Assert` condition. For example, there is a method which allows you to add common paging links to paged results of objects. You may decide these are not worthwhile if there is a total of only one page of results.\n\n```csharp\noptions.AddPolicy\u003cIPageLinkContainer\u003e(policy =\u003e\n{\n    policy.RequireelfLink(\"all\")\n            .RequirePagingLinks(condition =\u003e condition.Assert(x =\u003e x.PageCount \u003e 1 ));\n});\n```\n\n### Further customization\n\nYou are free to add your own requirements using the generic `Requires` method on `LinksPolicyBuilder`. In addition, you must write an implementation of `ILinksHandler` to handle your requirement. For example, you may have a requirement on certain responses to provide a link back to your api root document. Define a simple requirement for this link.\n\n```csharp\nusing RiskFirst.Hateoas;\n\npublic class ApiRootLinkRequirement : ILinksRequirement\n{\n    public ApiRootLinkRequirement()\n    {\n    }\n    public string Id { get; set; } = \"root\";\n}\n```\n\nGiven this requirement, we need a class to handle it, which must implement `ILinkHandler` and handle your requirement.\n\n```csharp\nusing RiskFirst.Hateoas;\n\npublic class ApiRootLinkHandler : LinksHandler\u003cApiRootLinkRequirement\u003e\n{\n    protected override Task HandleRequirementAsync(LinksHandlerContext context, ApiRootLinkRequirement requirement)\n    {\n        var route = context.RouteMap.GetRoute(\"ApiRoot\"); // Assumes your API has a named route \"ApiRoot\".\n        context.Links.Add(new LinkSpec(requirement.Id, route));\n        context.Handled(requirement);\n        return Task.CompletedTask;\n    }\n}\n```\n\nFinally register your Handler with `IServicesCollection` and use the requirement within your link policy\n\n```csharp\npublic class Startup\n{\n  public void ConfigureServices(IServicesCollection services)\n  {\n    services.AddLinks(config =\u003e\n    {\n      config.AddPolicy\u003cMyModel\u003e(policy =\u003e\n      {\n          policy.RequireRoutedLink(\"self\",\"GetModelRoute\", x =\u003e new {id = x.Id })\n                .Requires\u003cApiRootLinkRequirement\u003e();\n      });\n    });\n\n    services.AddTransient\u003cILinksHandler,ApiRootLinkHandler\u003e();\n  }\n}\n```\n\nThis example is demonstrated in the [`CustomRequirementSample`](samples/RiskFirst.Hateoas.CustomRequirementSample)\n\nThere are many additional parts of the framework which can be extended by writing your own implementation of the appropriate interface and registering it with `IServicesCollection` for dependency injection. For example, you could change the way that links are evaluated and applied to your link container by implementing your own [`ILinksEvaluator`](src/RiskFirst.Hateoas/ILinksEvaluator.cs)\n\n```csharp\nusing RiskFirst.Hateoas;\n\npublic class Startup\n{\n  public void ConfigureServices(IServicesCollection services)\n  {\n    services.AddLinks(options =\u003e {\n        ...\n    });\n    services.AddTransient\u003cILinksEvaluator, MyLinksEvaluator\u003e();\n  }\n}\n```\n\nThe list of interfaces which have a default implementation, but which can be replaced is:\n\n- [`ILinkAuthorizationService`](src/RiskFirst.Hateoas/ILinkAuthorizationService.cs),\n  controls how links are authorized during link condition evaluation.\n- [`ILinksEvaluator`](src/RiskFirst.Hateoas/ILinksEvaluator.cs), controls how links are evaluated and transformed before being written to the returned model.\n- [`ILinksHandlerContextFactory`](src/RiskFirst.Hateoas/ILinksHandlerContextFactory.cs), controls how the context is created which is passed through the requirement handlers during processing.\n- [`ILinksPolicyProvider`](src/RiskFirst.Hateoas/ILinksPolicyProvider.cs), provides lookup for `ILinkPolicy` instances by resource type and name.\n- [`ILinksService`](src/RiskFirst.Hateoas/ILinksService.cs), the main entrypoint into the framework, this interface is injected into user code to apply links to api resources.\n- [`ILinkTransformationContextFactory`](src/RiskFirst.Hateoas/ILinkTransformationContextFactory.cs), controls how the transformation context is created during transformation for rel \u0026 href properies of links.\n- [`IRouteMap`](src/RiskFirst.Hateoas/IRouteMap.cs), controls how your API is indexed to allow links between routes.\n\n### Troubleshooting\n\n#### Upgrading from v1.0.x to v1.1.x\n\nThe change from version 1.0.x to 1.1.x was mostly non-breaking, however if you have implemented any custom requirement handlers as described in the example above the signature of the base class `LinksHandler` changed slightly to remove the duplicate declaration of the generic type `TResource`.\n\nIn v1.0.x your code may have looked like:\n\n```csharp\npublic class MyCustomHandler : ILinksHandler { ... }\n```\n\nIt should now inherit from `LinksHandler\u003cTRequirement\u003e` making implementation simpler, and giving a type-safe override of `HandleRequirementAsync` giving access to your correctly-typed requirement.\n\n```csharp\npublic class MyCustomHandler : LinksHandler\u003cMyCustomRequirement\u003e\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Friskfirst%2Friskfirst.hateoas","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Friskfirst%2Friskfirst.hateoas","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Friskfirst%2Friskfirst.hateoas/lists"}