RESTworld.Testing
5.0.2
See the version list below for details.
dotnet add package RESTworld.Testing --version 5.0.2
NuGet\Install-Package RESTworld.Testing -Version 5.0.2
<PackageReference Include="RESTworld.Testing" Version="5.0.2" />
paket add RESTworld.Testing --version 5.0.2
#r "nuget: RESTworld.Testing, 5.0.2"
// Install RESTworld.Testing as a Cake Addin #addin nuget:?package=RESTworld.Testing&version=5.0.2 // Install RESTworld.Testing as a Cake Tool #tool nuget:?package=RESTworld.Testing&version=5.0.2
RESTworld
RESTworld is a framework which utilizes other common frameworks and patterns alltogether to enable easy and fast creation of a truly RESTful API.
Used frameworks and patterns
- Entity Framework Core for data access
- ASP.Net Core for hosting
- HAL for providing hyperlinks between resources
- OData for query support on list endpoints
- AutoMapper for mapping between Entities and DTOs
- Resource based authorization
- API Versioning through media types
Pipeline
The most basic pipeline has the following data flow for a request on a list endpoint:
- Request
- Controller selection through ASP.Net Core (optionally with versioning)
- Query parsing through OData
- Controller method calls business service method
- Authorization validates and modifies the request (both optional)
- Service validates that all migrations have been applied to the database, to protect from locks during migration.
- Service gets the data through Entity Framework Core
- Entity Framework Core translates the query into SQL and gets the data from the database
- Business service translates Entities into DTOs through Automapper
- Authorization validates and modifies the response (both optional)
- Controller wraps the result in a HAL response
- Result
Usage as API developer
Example
You can find a complete example which leverages all the features offered by RESTworld at https://github.com/wertzui/RESTworld/tree/main/src/Example/ExampleBlog.
Solution structure
If your API gets the name MyApi, structure your Solution with the following Projects:
- MyApi (ASP.Net Core Web API)
- References RESTworld.AspNetCore, MyApi.Business
- Contains your startup logic and your custom controllers
- MyApi.Business
- References RESTworld.Business, MyApi.Data
- Contains your AutoMapperConfiguration and your custom services
- MyApi.Data
- References RESTworld.EntityFrameworkCore, MyApi.Common
- Contains your Entity Framework Core Database Model including Entities and Migrations
- MyApi.Common
- References RESTworld.Common
- Contains your DTOs and Enums
Startup configuration
Add the following to your appsettings.json
"RESTworld": {
"MaxNumberForListEndpoint": 10, // The maximum returned on a list endpoint
"Curie": "MyEx", // The curie used to reference all your actions
"CalculateTotalCountForListEndpoint": true, // If you set this to true, your clients may be able to get all pages faster as they can do more parallel requests by calculating everything themself
"DisableAuthorization": false, // For testing purposes you can set this to false
"Versioning": { // Add this is you want to opt into versioning
"AllowQueryParameterVersioning": false, // This will allow legacy clients who cannot version through the media-type to version through a query parameter. This is not considered REST, but here to also support such clients.
"DefaultVersion": "2.0", // Can either be a version or "latest"
"ParameterName": "v" // The name of the parameter in the media-type headers and the query "Accept: application/hal+json; v=2.0" or "http://localhost/Author/42?v=2.0"
}
}
Change your Program.cs to the following
namespace MyApi
{
public class Program
{
public static void Main(string[] args)
{
RESTworld.AspNetCore.Program<Startup>.Main(args);
}
}
}
Change or add to your Startup class
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using RESTworld.Business.Abstractions;
using MyApi.Common.Dtos;
using MyApi.Data;
using MyApi.Data.Models;
using MyApi.Business;
namespace MyApi
{
public class Startup : RESTworld.AspNetCore.StartupBase
{
public Startup(IConfiguration configuration)
: base(configuration)
{
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public override void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
base.Configure(app, env);
}
// This method gets called by the runtime. Use this method to add services to the container.
public override void ConfigureServices(IServiceCollection services)
{
// Database
services.AddDbContextFactoryWithDefaults<MyDatabase>(Configuration);
services.AddODataModelForDbContext<MyDatabase>();
// Optionally migrate your database to the latest version during startup
services.MigrateDatabaseDuringStartup<TDbContext>();
// Default pipeline
services.AddRestPipeline<TContext, TEntity, TCreateDto, TGetListDto, TGetFullDto, TUpdateDto>();
// Optionally you can also add versioned pipelines
services.AddRestPipeline<TContext, TEntity, TCreateDtoV1, TGetListDtoV1, TGetFullDtoV1, TUpdateDtoV1>(new ApiVersion(1, 0), true);
services.AddRestPipeline<TContext, TEntity, TCreateDto, TGetListDto, TGetFullDto, TUpdateDto>(new ApiVersion(2, 0));
// With custom service
services.AddRestPipelineWithCustomService<TContext, TEntity, TCreateDto, TGetListDto, TGetFullDto, TUpdateDto, TService>();
// Custom controllers will automatically be picked up by the pipeline so there is no need to register them.
base.ConfigureServices(services);
}
protected override void ConfigureAutomapper(IMapperConfigurationExpression config)
=> new AutoMapperConfiguration().ConfigureAutomapper(config);
}
}
Automapper
Add an AutoMapperConfiguration to your MyApi.Business project
using AutoMapper;
using MyApi.Common.Dtos;
using MyApi.Common.Enums;
using MyApi.Data.Models;
namespace MyApi.Business
{
public class AutoMapperConfiguration
{
public void ConfigureAutomapper(IMapperConfigurationExpression config)
{
// A simple mapping
config
.CreateMap<TEntity, TDto>()
.ReverseMap();
// If you are using versioning, you probably need to add different mappings for different DTO versions here
config
.CreateMap<MyEntity, MyDto>()
.ReverseMap();
config
.CreateMap<MyEntity, MyDtoV1>()
.ForMember(dst => dst.Name, opt => opt.MapFrom(src => src.FirstName + " " + src.LastName))
.ReverseMap()
.ForMember(dst => dst.FirstName, opt => opt.MapFrom(src => src.Name.Split(new[] { ' ' }, 2)[0]))
.ForMember(dst => dst.LastName, opt => opt.MapFrom(src => src.Name.Split(new[] { ' ' }, 2)[1]));
// Add more mappings
}
}
}
Authorization
If you want to use the inbuilt authorization logic, you must implement the interface ICrudAuthorizationHandler<TEntity, TCreateDto, TGetListDto, TGetFullDto, TUpdateDto>
in a class to handle your own authorization logic. You can then register it in the ConfigureServices
method in your startup class.
// Concrete implementation for one service
services.AddAuthorizationHandler<MyAuthorizationHandler, TEntity, TCreateDto, TGetListDto, TGetFullDto, TUpdateDto>();
// Generic implementation which can be used for all services
services.AddAuthorizationHandler<MyGenericAuthorizationHandler<TEntity, TCreateDto, TGetListDto, TGetFullDto, TUpdateDto>, TEntity, TCreateDto, TGetListDto, TGetFullDto, TUpdateDto>();
// Register a pipeline together with concrete authorization handler
services.AddRestPipelineWithAuthorization<TContext, TEntity, TCreateDto, TGetListDto, TGetFullDto, TUpdateDto, MyAuthorizationHandler>();
// Register a pipeline together with generic authorization handler
services.AddRestPipelineWithAuthorization<TContext, TEntity, TCreateDto, TGetListDto, TGetFullDto, TUpdateDto, MyGenericAuthorizationHandler<TEntity, TCreateDto, TGetListDto, TGetFullDto, TUpdateDto>>();
// With a custom service implementation and concrete authorization handler
services.AddRestPipelineWithCustomServiceAndAuthorization<TContext, TEntity, TCreateDto, TGetListDto, TGetFullDto, TUpdateDto, TService, MyAuthorizationHandler>();
// With a custom service implementation and generic authorization handler
services.AddRestPipelineWithCustomServiceAndAuthorization<TContext, TEntity, TCreateDto, TGetListDto, TGetFullDto, TUpdateDto, TService, MyGenericAuthorizationHandler<TEntity, TCreateDto, TGetListDto, TGetFullDto, TUpdateDto>>();
To get the current user, an IUserAccessor
is provided, which you may want to inject into your authorization handler implementation. It is automatically populated from the HttpContext
. However no method is provided to read the user from a token, a cookie, or something else as libraries for that are already existing. In addition no login functionality is provided, as RESTworld is meant to be a framework for APIs and the API itself should relay the login functionality to any login service (like an OAuth service or something else).
Versioning
If you want or need to version your API, this is done through the media-type headers Accept
and Content-Type
. The API will parse the version from the Accept
header and always return the used version in the Content-Type
header. It will also advertise supported versions in the api-supported-versions
header and advertise deprecated versions in the api-deprecated-versions
header. So if you implement a client for the API, you should always look at the api-deprecated-versions
header and give a warning to the user of the client if a deprecated version is used.
You can configure versioning in your appsettings as shown above. If possible, I suggest that you handle versioning in your AutomapperConfiguration
as this is the easiest place and does not require special service implementations.
However if you cannot do this, there is also the possibility to use a REST pipeline with a custom service. That way you will need to provide one service for each version, but can also do more complex logic and database access to support multiple versions.
Health checks
Three endpoints for health checks are provided:
/health/startup
- This one reports healthy once every database which you have added through
services.AddDbContextFactoryWithDefaults<TContext>(Configuration)
has migrated to the latest version.
- This one reports healthy once every database which you have added through
/health/live
- This one reports healthy as soon as the application has started. It has no checks configured upfront.
/health/ready
- This one reports healthy if a connection to every database which you have added through
services.AddDbContextFactoryWithDefaults<TContext>(Configuration)
can be established.
- This one reports healthy if a connection to every database which you have added through
These three endpoints have been choosen to play nicely with the three Kubernetes probes startupProbe
, livenessProbe
and readinessProbe
. For more information have a look at the Kubernetes documentation https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes.
You can add your own health checks to these endpoints by tagging them with either "startup"
, "live"
or "ready"
.
Done
That's it. Now you can start your API and use a HAL browser like https://chatty42.herokuapp.com/hal-explorer/index.html#uri=https://localhost:5001 to browse your API.
If you are using a launchSettings.json
, I suggest to use this as your "launchUrl"
.
Usage as client developer
When developing an Angular SPA, you can use the @wertzui/ngx-restworld-client
npm package for your Angular application and the RESTworld.Client.AspNetCore
NuGet package for hosting.
Here are some guidelines when developing your own client:
Make use of HAL
Write your client in such a way that it will always connect to the home-endpoint (/
) first to discover all the other endpoints. You can cache the routes to the controller enpoints for quite some time (maybe a day or for the duration of a session), but do not hardcode them! There are also a couple of libraries for different programming languages out there which support HAL. The specification can be found at https://stateless.group/hal_specification.html.
Link templating
Link templating is defined in the IETF RFC 6570 at https://datatracker.ietf.org/doc/html/rfc6570 and there are also libraries for that. Use it together with HAL.
OData queries for list endpoints
To filter data on the List-endpoint, you can use the OData syntax. It is very powerfull and you can find a basic tutorial on the sytax at https://www.odata.org/getting-started/basic-tutorial/#queryData.
Use the /new endpoint to get sensible default data
If you want you client to be able to create new objects, you might want to query the /new
endpoint and present that data to your user so he is guided a little bit.
Batch operations for POST and PUT
The POST (Create) and PUT (Update) endpoints both accept either a single object or an array of objects. If you need to create or modify a huge number of objects, you should send them as an array as this will ensure the atomicity of such an operation and might also give you a huge performance gain as it will save a lot of roundtrips.
Versioning
Always send your supported version(s) in the Accept
header to ensure you always get a response that you can handle and watch for the api-deprecated-versions
header to quickly get notified if you need to change your client before it breaks. The format of the Accept
header should always be application/hal+json; v=42
(or whatever version you need and is offered by the server).
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net7.0 is compatible. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. |
-
net7.0
- AutoFixture (>= 4.18.0)
- AutoMapper (>= 12.0.1)
- Microsoft.EntityFrameworkCore.InMemory (>= 7.0.5)
- Microsoft.Extensions.Logging.Console (>= 7.0.0)
- Moq (>= 4.18.4)
- RESTworld.Business (>= 12.1.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last updated |
---|---|---|
10.0.0 | 33 | 11/20/2024 |
9.1.2 | 82 | 11/7/2024 |
9.1.1 | 150 | 10/11/2024 |
9.1.0 | 130 | 9/27/2024 |
9.0.0 | 96 | 9/20/2024 |
8.0.1 | 206 | 7/10/2024 |
8.0.0 | 131 | 6/4/2024 |
7.2.0 | 311 | 1/9/2024 |
7.1.0 | 146 | 12/22/2023 |
7.0.0 | 230 | 11/15/2023 |
6.1.0 | 172 | 10/23/2023 |
6.0.0 | 154 | 9/27/2023 |
5.1.2 | 172 | 9/11/2023 |
5.1.1 | 158 | 7/17/2023 |
5.1.0 | 173 | 7/3/2023 |
5.0.4 | 170 | 6/28/2023 |
5.0.3 | 147 | 6/14/2023 |
5.0.2 | 155 | 5/25/2023 |
5.0.1 | 180 | 4/19/2023 |
5.0.0 | 229 | 3/12/2023 |
4.1.1 | 248 | 2/22/2023 |
4.1.0 | 272 | 2/9/2023 |
4.0.0 | 324 | 1/24/2023 |
3.0.1 | 318 | 12/21/2022 |
3.0.0 | 340 | 11/9/2022 |
2.0.1 | 410 | 10/20/2022 |
2.0.0 | 405 | 10/20/2022 |
1.3.0 | 440 | 9/27/2022 |
1.2.0 | 483 | 6/28/2022 |
1.1.0 | 468 | 6/23/2022 |
1.0.1 | 454 | 6/8/2022 |
1.0.0 | 489 | 5/27/2022 |