RESTworld.EntityFrameworkCore 7.1.1

dotnet add package RESTworld.EntityFrameworkCore --version 7.1.1
NuGet\Install-Package RESTworld.EntityFrameworkCore -Version 7.1.1
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="RESTworld.EntityFrameworkCore" Version="7.1.1" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add RESTworld.EntityFrameworkCore --version 7.1.1
#r "nuget: RESTworld.EntityFrameworkCore, 7.1.1"
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install RESTworld.EntityFrameworkCore as a Cake Addin
#addin nuget:?package=RESTworld.EntityFrameworkCore&version=7.1.1

// Install RESTworld.EntityFrameworkCore as a Cake Tool
#tool nuget:?package=RESTworld.EntityFrameworkCore&version=7.1.1

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:

  1. Request
  2. Controller selection through ASP.Net Core (optionally with versioning)
  3. Query parsing through OData
  4. Controller method calls business service method
  5. Authorization validates and modifies the request (both optional)
  6. Service validates that all migrations have been applied to the database, to protect from locks during migration.
  7. Service gets the data through Entity Framework Core
  8. Entity Framework Core translates the query into SQL and gets the data from the database
  9. Business service translates Entities into DTOs through Automapper
  10. Authorization validates and modifies the response (both optional)
  11. Controller wraps the result in a HAL response
  12. 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:

  1. /health/startup
    1. This one reports healthy once every database which you have added through services.AddDbContextFactoryWithDefaults<TContext>(Configuration) has migrated to the latest version.
  2. /health/live
    1. This one reports healthy as soon as the application has started. It has no checks configured upfront.
  3. /health/ready
    1. This one reports healthy if a connection to every database which you have added through services.AddDbContextFactoryWithDefaults<TContext>(Configuration) can be established.

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 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 Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on RESTworld.EntityFrameworkCore:

Package Downloads
RESTworld.Business

Package Description

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
7.1.1 452 1/9/2024
7.1.0 163 12/22/2023
7.0.0 582 11/15/2023
6.1.0 307 10/23/2023
6.0.0 296 9/27/2023
5.3.2 234 9/11/2023
5.3.1 421 7/17/2023
5.3.0 248 7/3/2023
5.2.3 205 6/28/2023
5.2.2 840 6/14/2023
5.2.1 378 5/25/2023
5.2.0 357 4/30/2023
5.1.3 237 4/19/2023
5.1.2 283 3/12/2023
5.1.1 597 2/22/2023
5.1.0 317 2/9/2023
5.0.0 494 1/24/2023
4.0.1 718 12/21/2022
4.0.0 1,051 11/9/2022
3.4.2 893 10/20/2022
3.4.1 811 10/20/2022
3.4.0 937 9/27/2022
3.2.3 1,195 6/28/2022
3.2.2 1,777 6/8/2022
3.2.1 1,312 5/13/2022
3.2.0 1,155 4/1/2022
3.1.0 876 3/30/2022
3.0.2 1,731 3/10/2022
3.0.1 1,226 3/9/2022
3.0.0 3,127 2/21/2022
2.0.2 815 1/14/2022
2.0.1 434 1/6/2022
2.0.0 830 12/3/2021
1.3.1 977 11/4/2021
1.3.0 1,859 9/29/2021
1.2.0 588 9/27/2021
1.1.0 1,020 8/18/2021
1.0.3 835 6/29/2021
1.0.2 511 5/11/2021
1.0.1 535 4/22/2021
1.0.0 852 3/31/2021