Sample app on how to add Rhetos to ASP.NET Web API project.
Complete source code for this example is available at: https://github.com/Rhetos/Rhetos.Samples.AspNet
Contents:
- Prerequisites
- Setting up
- Build your first Rhetos App
- Connecting to ASP.NET pipeline
- Applying Rhetos model to database
- Use Rhetos components in ASP.NET controllers
- Additional integration/extension options
- Run
dotnet --version
to check if you have .NET 5 SDK installed. It should output 5.x.x. If not, install the latest version from https://dotnet.microsoft.com/download/dotnet/5.0.
-
Create a new folder for your project
-
Run
dotnet new webapi
-
Configure
.csproj
-
Prevent Rhetos auto deploy: Add
<RhetosDeploy>False</RhetosDeploy>
to<PropertyGroup>
tag. -
Add packages:
<ItemGroup> <PackageReference Include="Rhetos.Host" Version="5.0.0" /> <PackageReference Include="Rhetos.Host.AspNet" Version="5.0.0" /> <PackageReference Include="Rhetos.CommonConcepts" Version="5.0.0" /> <PackageReference Include="Rhetos.MSBuild" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration" Version="5.0.0" /> </ItemGroup>
-
Add Rhetos DSL script named DslScripts/Books.rhe
and add the following to it:
Module Bookstore
{
Entity Book
{
ShortString Code { AutoCode; }
ShortString Title;
Integer NumberOfPages;
ItemFilter CommonMisspelling 'book => book.Title.Contains("curiousity")';
InvalidData CommonMisspelling 'It is not allowed to enter misspelled word "curiousity".';
Logging;
}
}
This demo app has namespace Rhetos.Sample.AspNet
that starts with Rhetos.
, so we need to correct Host
conflict in Program.cs
by changing Host.CreateDefaultBuilder(...
reads Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder(...
.
Run dotnet build
to verify that everything compiles. Your DSL model from newly added script will be compiled and Rhetos classes are now available in your project.
To wire up Rhetos and ASP.NET dependency injection, modify Startup.cs
, add a static method (this is a useful convention but it is not required):
using Rhetos;
private void ConfigureRhetosHostBuilder(IServiceProvider serviceProvider, IRhetosHostBuilder rhetosHostBuilder)
{
rhetosHostBuilder
.ConfigureRhetosAppDefaults()
.UseBuilderLogProviderFromHost(serviceProvider)
.ConfigureConfiguration(cfg => cfg.MapNetCoreConfiguration(Configuration));
}
And register Rhetos in ConfigureServices
method:
services.AddRhetosHost(ConfigureRhetosHostBuilder)
.AddAspNetCoreIdentityUser()
.AddHostLogging();
Rhetos needs database to work with, create it and configure connection string in appsettings.json
file:
"ConnectionStrings": {
"RhetosConnectionString": "<YOURDBCONNECTIONSTRING>"
}
To apply model to database we need to use rhetos.exe
CLI tool. CLI tools need to be able to discover host application configuration and setup. We provide that via static method in Program.cs
.
rhetos.exe
will look for the class where the entry point method is located and will look for the method public static IHostBuilder CreateHostBuilder(string[] args)
inside that class and use this method to construct a Rhetos host.
Run dotnet build
Run ./rhetos.exe dbupdate Rhetos.Samples.AspNet.dll
in the binary output folder. This runs database update operation in the context of specified host DLL (in our case, our sample application).
This example shows how to use Rhetos components when developing a custom controller.
Add a new controller DemoController.cs
.
using Microsoft.AspNetCore.Mvc;
using Rhetos;
using Rhetos.Processing;
using Rhetos.Processing.DefaultCommands;
[Route("Demo/[action]")]
public class DemoController : ControllerBase
{
private readonly IProcessingEngine processingEngine;
private readonly IUnitOfWork unitOfWork;
public DemoController(IRhetosComponent<IProcessingEngine> processingEngine, IRhetosComponent<IUnitOfWork> unitOfWork)
{
this.processingEngine = processingEngine.Value;
this.unitOfWork = unitOfWork.Value;
}
[HttpGet]
public string ReadBooks()
{
var readCommandInfo = new ReadCommandInfo { DataSource = "Bookstore.Book", ReadTotalCount = true };
var result = processingEngine.Execute(readCommandInfo);
return $"{result.TotalCount} books.";
}
[HttpGet]
public string WriteBook()
{
var newBook = new Bookstore.Book { Title = "NewBook" };
var saveCommandInfo = new SaveEntityCommandInfo { Entity = "Bookstore.Book", DataToInsert = new[] { newBook } };
processingEngine.Execute(saveCommandInfo);
unitOfWork.CommitAndClose(); // Commits and closes database transaction.
return "1 book inserted.";
}
}
By default, Rhetos permissions will not allow anonymous users to read any data. Enable anonymous access by modifying appsettings.json
:
"Rhetos": {
"AppSecurity": {
"AllClaimsForAnonymous": true
}
}
Run dotnet run
and browse to http://localhost:5000/Demo/ReadBooks
.
You should receive a response value 0 books.
indicating there are 0 entries in the database.
In WriteBook
method, unitOfWork.CommitAndClose()
commits the database transaction
for the current unit of work (a web request).
Instead of manually committing the transaction, you can use a ServiceFilter ApiCommitOnSuccessFilter
from Rhetos.RestGenerator plugin,
see example.
Rhetos dashboard is a standard Rhetos "homepage" that includes basic system information and GUI for some plugins. It is intended for testing and administration, but it could also be used by end users if needed, since all official features are implemented with standard Rhetos security permissions.
Adding Rhetos dashboard to a Rhetos application:
- Extend the Rhetos services configuration (at
services.AddRhetosHost
) with the dashboard components:.AddDashboard()
- Extend the application with new endpoint: in the
Startup.Configure
method callapp.UseEndpoints(endpoints => { endpoints.MapRhetosDashboard(); });
To use it simply open /rhetos
web page in your Rhetos app,
for example http://localhos:5000/rhetos. The route is configurable in MapRhetosDashboard
.
Rhetos.RestGenerator package automatically maps all Rhetos data structures to REST endpoints.
Add package to .csproj
file:
<PackageReference Include="Rhetos.RestGenerator" Version="5.0.0" />
Modify lines which add Rhetos in Startup.cs
, method ConfigureServices
to read:
services.AddRhetosHost(ConfigureRhetosHostBuilder)
.AddAspNetCoreIdentityUser()
.AddRestApi(o => o.BaseRoute = "rest");
Add to Startup.cs
, method Configure
before line app.UseEndpoints(...
:
app.UseRhetosRestApi();
If you have not configured authentication yet, enable "AllClaimsForAnonymous" configuration option (see the example in section above).
Run dotnet run
. REST API is now available. Navigate to http://localhost:5000/rest/Bookstore/Book
to issue a GET and retrieve all Book entity records in the database.
For more info on usage and serialization configuration see Rhetos.RestGenerator
Since Swagger is already added to webapi project template, we can generate Open API specification for mapped Rhetos endpoints.
Modify lines which add Rhetos in Startup.cs
, method ConfigureServices
to read:
services.AddRhetosHost(ConfigureRhetosHostBuilder)
.AddAspNetCoreIdentityUser()
.AddRestApi(o =>
{
o.BaseRoute = "rest";
o.GroupNameMapper = (conceptInfo, controller, oldName) => "v1";
});
This addition maps all generated Rhetos API controllers to an existing Swagger document named 'v1'.
Run dotnet run Environment=Development
and navigate to http://localhost:5000/swagger/index.html
. You should see entire Rhetos REST API in interactive UI.
In larger applications, for improved Swagger load time, it is recommended to split each DSL Module into a separate Swagger document. See additional instructions in RestGenerator documentation in section Adding Swagger/OpenAPI.
In this example we will use the simplest possible authentication method, although ANY authentication method supported by ASP.NET may be used. For example Configure Windows Authentication
Add authentication to ASPNET application. Modify Services.cs
:
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
Add to ConfigureServices
:
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(o => o.Events.OnRedirectToLogin = context =>
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return Task.CompletedTask;
});
And in Configure
method after UseRouting()
add:
app.UseAuthentication();
Modify DemoController.cs
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using System.Threading.Tasks;
using System.Security.Claims;
and a new method to allow us to sign-in:
[HttpGet]
public async Task Login()
{
var claimsIdentity = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, "SampleUser") }, CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
new AuthenticationProperties() { IsPersistent = true });
}
This is simple stub code to sign-in SampleUser
so we have a valid user to work with.
In appsettings.json
set AllClaimsForAnonymous
to false
. This disables anonymous workaround we have been using so far.
If you run the app now and navigate to http://localhost:5000/Demo/Login
and then to http://localhost:5000/Demo/ReadBooks
, you will receive an error:
UserException: Your account 'SampleUser' is not registered in the system. Please contact the system administrator.
Since 'SampleUser' doesn't exist in Rhetos we will use a simple configuration feature to treat him as admin.
Add to appsettings.json
:
"Rhetos": {
"AppSecurity": {
"AllClaimsForUsers": "SampleUser"
}
}
http://localhost:5000/Demo/ReadBooks
should now correctly return 0
as we haven't added any Book
entities.
You can write additional controllers/actions and invoke Rhetos commands now.
- In Program.cs add
using NLog.Web;
- In
Program.CreateHostBuilder
method addhostBuilder.UseNLog();
- In
Startup.ConfigureServices
, atAddRhetosHost
, add.AddHostLogging()
(if it's not there already). - To configure NLog add the
nlog.config
file to the project. Make sure that the file properties are set to Copy to Output Directory: Copy if newer. To make logging compatible with Rhetos v3 and v4, enter the following text into the file.
<?xml version="1.0" encoding="utf-8"?>
<!-- THis configuration file is used by NLog to setup the logging if the hostBuilder.UseNLog() method is called inside the Program.CreateHostBuilder method-->
<nlog throwConfigExceptions="true" xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<targets>
<target name="MainLog" xsi:type="File" fileName="${basedir}\Logs\RhetosServer.log" encoding="utf-8" archiveFileName="${basedir}\Logs\Archives\RhetosServer {#####}.zip" enableArchiveFileCompression="true" archiveAboveSize="2000000" archiveNumbering="DateAndSequence" />
<target name="ConsoleLog" xsi:type="Console" />
<target name="TraceLog" xsi:type="AsyncWrapper" overflowAction="Block">
<target name="TraceLogBase" xsi:type="File" fileName="${basedir}\Logs\RhetosServerTrace.log" encoding="utf-8" archiveFileName="${basedir}\Logs\Archives\RhetosServerTrace {#####}.zip" enableArchiveFileCompression="true" archiveAboveSize="10000000" archiveNumbering="DateAndSequence" />
</target>
<target name="TraceCommandsXml" xsi:type="AsyncWrapper" overflowAction="Block">
<target name="TraceCommandsXmlBase" xsi:type="File" fileName="${basedir}\Logs\RhetosServerCommandsTrace.xml" encoding="utf-16" layout="<!--${longdate} ${logger}-->${newline}${message}" archiveFileName="${basedir}\Logs\Archives\RhetosServerCommandsTrace {#####}.zip" enableArchiveFileCompression="true" archiveAboveSize="10000000" archiveNumbering="DateAndSequence" />
</target>
<target name="PerformanceLog" xsi:type="AsyncWrapper" overflowAction="Block">
<target name="PerformanceLogBase" xsi:type="File" fileName="${basedir}\Logs\RhetosServerPerformance.log" encoding="utf-8" archiveFileName="${basedir}\Logs\Archives\RhetosServerPerformance {#####}.zip" enableArchiveFileCompression="true" archiveAboveSize="10000000" archiveNumbering="DateAndSequence" />
</target>
</targets>
<rules>
<logger name="*" minLevel="Info" writeTo="MainLog" />
<!-- <logger name="*" minLevel="Info" writeTo="ConsoleLog" /> -->
<!-- <logger name="*" minLevel="Trace" writeTo="TraceLog" /> -->
<!-- <logger name="ProcessingEngine Request" minLevel="Trace" writeTo="ConsoleLog" /> -->
<!-- <logger name="ProcessingEngine Request" minLevel="Trace" writeTo="TraceLog" /> -->
<!-- <logger name="ProcessingEngine Commands" minLevel="Trace" writeTo="TraceCommandsXml" /> -->
<!-- <logger name="ProcessingEngine CommandsResult" minLevel="Trace" writeTo="TraceCommandsXml" /> -->
<!-- <logger name="ProcessingEngine CommandsWithClientError" minLevel="Trace" writeTo="TraceCommandsXml" /> -->
<logger name="ProcessingEngine CommandsWithServerError" minLevel="Trace" writeTo="TraceCommandsXml" />
<!-- <logger name="ProcessingEngine CommandsWithServerError" minLevel="Trace" writeTo="MainLog" /> -->
<!-- <logger name="Performance*" minLevel="Trace" writeTo="PerformanceLog" /> -->
</rules>
</nlog>
Localization provides support for multiple languages, but it can also be very useful even if an application uses only one language (English, e.g.) to modify the messages to match the client requirements.
Localization in Rhetos app is automatically applied on translating the Rhetos response messages for end users. For example, a data validation error message (InvalidData), UserException, and other.
The following example adds GetText / PO localization support to the Rhetos app:
- Rhetos components are configured to use the host application's localization
(standard ASP.NET Core localization) by simply adding
AddHostLocalization()
in Rhetos setup. - Any ASP.NET Core localization plugin can be used. This example uses OrchardCore, a 3rd party library recommended by Microsoft, see Configure portable object localization in ASP.NET Core
Add localization to your Rhetos app:
-
In the
.csproj
file, add the following lines:<ItemGroup> <PackageReference Include="OrchardCore.Localization.Core" Version="1.1.0" /> <None Update="Localization\hr.po"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </None> </ItemGroup>
-
Create file
Localization\hr.po
with translations for language "hr" (see CultureInfo for language codes), with the following content:msgctxt "Rhetos" msgid "It is not allowed to enter {0} because the required property {1} is not set." msgstr "Nije dozvoljen unos zapisa {0} jer polje {1} nije zadano."
-
In
Startup.cs
file, add the following lines (note that DefaultRequestCulture is set to "hr"):using Microsoft.AspNetCore.Localization; using System.Collections.Generic; using System.Globalization; // ... in ConfigureServices method, after services.AddRhetosHost: .AddHostLocalization() // ... in ConfigureServices method: services.AddLocalization() .AddPortableObjectLocalization(options => options.ResourcesPath = "Localization") .AddMemoryCache(); // ... in Configure method: app.UseRequestLocalization(options => { var supportedCultures = new List<CultureInfo> { new CultureInfo("en"), new CultureInfo("hr") }; options.DefaultRequestCulture = new RequestCulture("hr"); options.SupportedCultures = supportedCultures; options.SupportedUICultures = supportedCultures; options.RequestCultureProviders = new List<IRequestCultureProvider> { //The culture will be resolved based on the query parameter. //For example if we want the validation message to be translated to Croatian //we can call the POST method rest/Bookstore/Book?culture=hr and insert a json object without the 'Title' property. //It can be configured so that the culture gets resolved based on cookies or headers. new QueryStringRequestCultureProvider() }; });
For example in a demo application, see Bookstore.Service/Startup.cs.
Complex applications with large number of entities may experience performance improvement
if Entity Framework's query cache size is increased from its default value.
This needs to be configured in App.config, since EF 6 still uses the ConfigurationManager
class to load its configuration.
- Add the App.config file as a plain text file in the project root, with the recommended EF configuration settings:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework" />
</configSections>
<entityFramework>
<queryCache size="10000" cleaningIntervalInSeconds="60" />
</entityFramework>
</configuration>