Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Build

on:
push:
branches: [main, master]
pull_request:

jobs:
core:
name: ASP.NET Core sample (Linux)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- run: dotnet build src/CastleDemo/CastleDemo.csproj -c Release

framework:
name: .NET Framework sample (Windows)
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- run: dotnet build src/CastleDemo.Framework/CastleDemo.Framework.csproj -c Release
14 changes: 14 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Build and run the ASP.NET Core CastleDemo sample.
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY src/CastleDemo/CastleDemo.csproj CastleDemo/
RUN dotnet restore CastleDemo/CastleDemo.csproj
COPY src/CastleDemo/ CastleDemo/
RUN dotnet publish CastleDemo/CastleDemo.csproj -c Release -o /app/publish

FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
COPY --from=build /app/publish .
ENV ASPNETCORE_URLS=http://+:8080
EXPOSE 8080
ENTRYPOINT ["dotnet", "CastleDemo.dll"]
75 changes: 53 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,39 +1,70 @@
# Castle .NET Example

This is an example of integrating Castle with a standard ASP.NET Core Razor Pages application.
Examples of integrating Castle into .NET applications. The repository contains two samples:

## Foundation
| Sample | Path | Target | Notes |
| --- | --- | --- | --- |
| ASP.NET Core Razor Pages | `src/CastleDemo` | `net8.0` | The main, cross-platform sample. |
| .NET Framework console | `src/CastleDemo.Framework` | `net48` | Exercises the `System.Web` code path; builds and runs on Windows. |

The example is almost fully from the default Visual Studio template for a Razor Pages app with _Invididual user accounts_ for authentication.
## ASP.NET Core sample (`src/CastleDemo`)

### Framework
A Razor Pages app based on the default template with _Individual user accounts_ for
authentication. The home page lists the available demos (see `Demos/DemoCatalog.cs`),
each of which triggers a Castle API call.

NET Core 6.0
### Highlights

### Template modifications

- The database runs in-memory
- Targets `net8.0` and uses minimal hosting (`Program.cs`); there is no `Startup.cs`.
- The database runs in-memory:

```csharp
services.AddDbContext<ApplicationDbContext>(options =>
options.UseInMemoryDatabase("CastleDemo")
);
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseInMemoryDatabase("CastleDemo"));
```

- Visual Studio scaffolding has been used to create a Login page we can alter, as described by Microsoft [here](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/scaffold-identity).
- We use [Microsoft.VisualStudio.Threading](https://www.nuget.org/packages/Microsoft.VisualStudio.Threading/) to get access to the `Forget()` extension method, which is useful for fire-and-forget calls to async methods, like **Track** or **Authenticate** in Monitor mode.
- Fire-and-forget Castle calls (e.g. **Track** / **Authenticate** in monitor mode) use a
discard (`_ = client.Track(...)`) rather than an external dependency.
- Client-side fingerprinting and secure mode are wired up in `Pages/Shared/_Layout.cshtml`.

All Castle-related changes are marked with comments containing the word _Castle_ for easy
searching, and primarily affect:

- `Program.cs` — service registration
- `Areas/Identity/Pages/Account/Login.cshtml.cs` and the `Risk` / `Filter` / `Log` pages — SDK calls
- `Pages/Shared/_Layout.cshtml` — client-side Castle
- `appsettings.json` — your Castle API secret and App ID

## The integration
### Run

The example application applies the steps described for the [Castle Baseline Integration](https://castle.io/docs/baseline), with the addition of [secure requests](https://castle.io/docs/securing_requests). All Castle-related changes are marked with comments containing the word _Castle_ for easy searching, and affect the following files:
```bash
dotnet run --project src/CastleDemo/CastleDemo.csproj
```

Set your credentials in `appsettings.json` (or via environment / user secrets):

- `Startup.cs` Ioc
- `Areas/Identity/Pages/Account/Login.cshtml.cs` Castle SDK calls
- `Pages/Shared/_Layout.cshtml` Client-side Castle
- `appsettings.json` Your Castle API secret and App ID
```json
"Castle": {
"ApiSecret": "YOUR API SECRET",
"AppId": "YOUR APP ID"
}
```

## Development testing
### Docker

# nuget add pathtonugetpackage.nupkg -source sourceDir
```bash
docker build -t castle-dotnet-example .
docker run -p 8080:8080 -e Castle__ApiSecret=YOUR_API_SECRET castle-dotnet-example
```

## .NET Framework sample (`src/CastleDemo.Framework`)

A minimal `net48` console app that adapts a `System.Web` request to
`Castle.Context.FromHttpRequest(HttpRequestBase)` and sends a Risk request (with
`DoNotTrack` enabled so it runs without a real secret). It builds and runs on Windows:

```bash
dotnet run --project src/CastleDemo.Framework/CastleDemo.Framework.csproj
```

# dotnet add package Castle.Sdk -s sourceDir
This sample requires `Castle.Sdk` **2.4.0** or newer (the first version with a `net48` target).
14 changes: 14 additions & 0 deletions src/CastleDemo.Framework/CastleDemo.Framework.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net48</TargetFramework>
<LangVersion>latest</LangVersion>
<RootNamespace>CastleDemo.Framework</RootNamespace>
<AssemblyName>CastleDemo.Framework</AssemblyName>
</PropertyGroup>
<ItemGroup>
<!-- net48 support is available from Castle.Sdk 2.4.0 onwards. -->
<PackageReference Include="Castle.Sdk" Version="2.4.0" />
<Reference Include="System.Web" />
</ItemGroup>
</Project>
29 changes: 29 additions & 0 deletions src/CastleDemo.Framework/DemoHttpRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System.Collections.Specialized;
using System.Web;

namespace CastleDemo.Framework
{
/// <summary>
/// A tiny <see cref="HttpRequestBase"/> stand-in so the console sample can build a
/// request context the same way an ASP.NET (System.Web) application would.
/// </summary>
internal sealed class DemoHttpRequest : HttpRequestBase
{
private readonly NameValueCollection _headers;
private readonly HttpCookieCollection _cookies;
private readonly string _userHostAddress;

public DemoHttpRequest(NameValueCollection headers, HttpCookieCollection cookies, string userHostAddress)
{
_headers = headers;
_cookies = cookies;
_userHostAddress = userHostAddress;
}

public override NameValueCollection Headers => _headers;

public override HttpCookieCollection Cookies => _cookies;

public override string UserHostAddress => _userHostAddress;
}
}
59 changes: 59 additions & 0 deletions src/CastleDemo.Framework/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Threading.Tasks;
using System.Web;
using Castle;
using Castle.Config;
using Castle.Messages.Requests;

namespace CastleDemo.Framework
{
/// <summary>
/// Minimal .NET Framework 4.8 sample. It exercises the System.Web code path of the
/// SDK — <see cref="Context.FromHttpRequest(HttpRequestBase)"/> — which only builds and
/// runs on Windows. The Castle call uses DoNotTrack so the sample runs without a
/// real API secret or network access.
/// </summary>
internal static class Program
{
private static async Task Main()
{
var apiSecret = Environment.GetEnvironmentVariable("CASTLE_API_SECRET") ?? "YOUR API SECRET";

var client = new CastleClient(new CastleConfiguration(apiSecret)
{
DoNotTrack = true
});

var headers = new NameValueCollection
{
["X-Castle-Client-ID"] = "the-client-id-from-castle-js",
["X-Forwarded-For"] = "1.2.3.4"
};

// Adapt a System.Web request to the SDK's .NET Framework context overload.
HttpRequestBase request = new DemoHttpRequest(headers, new HttpCookieCollection(), "1.2.3.4");
var context = Context.FromHttpRequest(request);

var actionRequest = new ActionRequest
{
Event = "$login.succeeded",
UserId = "user-123",
UserTraits = new Dictionary<string, string>
{
["email"] = "user@example.com"
},
Context = context
};

var verdict = await client.Risk(actionRequest);

Console.WriteLine("Castle .NET Framework sample");
Console.WriteLine($" Resolved client id: {context.ClientId}");
Console.WriteLine($" Resolved ip: {context.Ip}");
Console.WriteLine($" Verdict action: {verdict?.Action}");
Console.WriteLine($" Failover: {verdict?.Failover}");
}
}
}
6 changes: 6 additions & 0 deletions src/CastleDemo.sln
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ VisualStudioVersion = 15.0.28307.438
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CastleDemo", "CastleDemo\CastleDemo.csproj", "{155A2766-3EE6-42EC-9A73-A835C190E89E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CastleDemo.Framework", "CastleDemo.Framework\CastleDemo.Framework.csproj", "{A9116005-AC8D-4F84-8375-0C41D9F5EF3D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -15,6 +17,10 @@ Global
{155A2766-3EE6-42EC-9A73-A835C190E89E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{155A2766-3EE6-42EC-9A73-A835C190E89E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{155A2766-3EE6-42EC-9A73-A835C190E89E}.Release|Any CPU.Build.0 = Release|Any CPU
{A9116005-AC8D-4F84-8375-0C41D9F5EF3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A9116005-AC8D-4F84-8375-0C41D9F5EF3D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A9116005-AC8D-4F84-8375-0C41D9F5EF3D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A9116005-AC8D-4F84-8375-0C41D9F5EF3D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
9 changes: 4 additions & 5 deletions src/CastleDemo/Areas/Identity/Pages/Account/Login.cshtml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.Threading;

namespace CastleDemo.Areas.Identity.Pages.Account
{
Expand Down Expand Up @@ -90,8 +89,8 @@ public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
_logger.LogInformation("User logged in.");

// Castle Authenticate $login.succeeded
_castleClient.Authenticate(CreateCastleActionRequest("$login.succeeded")).Forget();
// Castle Authenticate $login.succeeded (fire-and-forget)
_ = _castleClient.Authenticate(CreateCastleActionRequest("$login.succeeded"));

return LocalRedirect(returnUrl);
}
Expand All @@ -106,8 +105,8 @@ public async Task<IActionResult> OnPostAsync(string returnUrl = null)
}
else
{
// Castle Track $login.failed
_castleClient.Track(CreateCastleActionRequest("$login.failed")).Forget();
// Castle Track $login.failed (fire-and-forget)
_ = _castleClient.Track(CreateCastleActionRequest("$login.failed"));

ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return Page();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
using Microsoft.AspNetCore.Mvc.RazorPages;
using Castle.Messages.Requests;
using System.Collections.Generic;
using Microsoft.VisualStudio.Threading;
using System.Threading.Tasks;
using System;
using Castle.Infrastructure.Exceptions;
Expand Down
1 change: 0 additions & 1 deletion src/CastleDemo/Areas/Identity/Pages/Log/Log.cshtml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
using Microsoft.AspNetCore.Mvc.RazorPages;
using Castle.Messages.Requests;
using System.Collections.Generic;
using Microsoft.VisualStudio.Threading;
using System.Threading.Tasks;
using System;

Expand Down
1 change: 0 additions & 1 deletion src/CastleDemo/Areas/Identity/Pages/Risk/Risk.cshtml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
using Microsoft.AspNetCore.Mvc.RazorPages;
using Castle.Messages.Requests;
using System.Collections.Generic;
using Microsoft.VisualStudio.Threading;
using System.Threading.Tasks;
using System;
using Castle.Infrastructure.Exceptions;
Expand Down
1 change: 0 additions & 1 deletion src/CastleDemo/Areas/Identity/Pages/_ViewImports.cshtml
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
@using Microsoft.AspNetCore.Identity
@using CastleDemo.Areas.Identity
@using Microsoft.AspNetCore.Identity
@namespace CastleDemo.Areas.Identity.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
23 changes: 9 additions & 14 deletions src/CastleDemo/CastleDemo.csproj
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
<UserSecretsId>aspnet-CastleDemo-708B0CB0-0EAA-415A-87AA-60C6A98D774C</UserSecretsId>
</PropertyGroup>
<ItemGroup>

<PackageReference Include="Castle.Sdk" Version="2.3.0"/>
<PackageReference Include="Microsoft.VisualStudio.Threading" Version="17.12.19"/>
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="9.0.0"/>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0"/>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.0"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.0"/>
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="9.0.0"/>
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="9.0.0"/>
<PackageReference Include="Castle.Sdk" Version="2.3.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.11" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.11" />
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="8.0.11" />
</ItemGroup>
</Project>
</Project>
Loading
Loading