mirror of https://github.com/abpframework/abp.git
195 changed files with 2793 additions and 1984 deletions
@ -0,0 +1,164 @@ |
|||||
|
```json |
||||
|
//[doc-seo] |
||||
|
{ |
||||
|
"Description": "Configure cross-application URLs in ABP with AppUrlOptions and IAppUrlProvider, including multi-tenant subdomain templates and redirect URL validation." |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
# Application URLs |
||||
|
|
||||
|
ABP provides the `AppUrlOptions` options class and the `IAppUrlProvider` service to centrally configure and resolve URLs that point to **other applications** in your solution (for example, an MVC/Razor Pages UI, an Auth Server, an HTTP API host, etc.). They are typically used when code in one application needs to build a link that targets another — like the Account module putting a **password reset link** into an email. |
||||
|
|
||||
|
* Defines `AppUrlOptions` to register the **root URL** and named relative URLs of each application. |
||||
|
* Provides `IAppUrlProvider` to **resolve** those URLs at runtime, with optional **tenant-aware** placeholder substitution. |
||||
|
* Supports **subdomain-style templates** (e.g. `https://{0}.example.com`) that produce per-tenant URLs without extra code. |
||||
|
* Maintains a `RedirectAllowedUrls` list used by `IAppUrlProvider.IsRedirectAllowedUrlAsync` to validate redirect targets. |
||||
|
|
||||
|
> `AppUrlOptions` is defined in the `Volo.Abp.UI.Navigation` package, which comes pre-installed with the [application startup template](../../solution-templates/layered-web-application). |
||||
|
|
||||
|
## Configuring Application URLs |
||||
|
|
||||
|
`AppUrlOptions` exposes a dictionary of **applications**, each with a `RootUrl` and a set of named `Urls`. |
||||
|
|
||||
|
**Example: Set the root URL and a named URL for the MVC application** |
||||
|
|
||||
|
```csharp |
||||
|
Configure<AppUrlOptions>(options => |
||||
|
{ |
||||
|
options.Applications["MVC"].RootUrl = "https://my-app.com"; |
||||
|
options.Applications["MVC"].Urls["MyPage"] = "my-page"; |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
* `"MVC"` is the **application key**. Some modules (such as Account) register their URLs under a known key — `"MVC"` is the default for the **server-side UI**. You can use any key you want for your own applications. |
||||
|
* `RootUrl` is the **base URL** of that application. |
||||
|
* `Urls[urlName]` is a **relative path** appended to `RootUrl`. The final URL is built as `RootUrl.EnsureEndsWith('/') + Urls[urlName]`, so the relative path should **not** start with a `/`. When `RootUrl` is `null`, the value of `Urls[urlName]` is returned as-is. |
||||
|
|
||||
|
The Account module, for example, **pre-registers** its URLs in its application module: |
||||
|
|
||||
|
**Example: How the Account module registers the password reset URL** |
||||
|
|
||||
|
```csharp |
||||
|
Configure<AppUrlOptions>(options => |
||||
|
{ |
||||
|
options.Applications["MVC"].Urls[AccountUrlNames.PasswordReset] = "Account/ResetPassword"; |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
> So configuring `Applications["MVC"].RootUrl` in your own module is usually enough to make password reset and similar Account email links point to the right host. |
||||
|
|
||||
|
### Defaults in the application startup template |
||||
|
|
||||
|
The ABP **application startup template** wires `Applications["MVC"].RootUrl` to the `App:SelfUrl` setting and seeds `RedirectAllowedUrls` from `App:RedirectAllowedUrls`: |
||||
|
|
||||
|
```csharp |
||||
|
Configure<AppUrlOptions>(options => |
||||
|
{ |
||||
|
options.Applications["MVC"].RootUrl = configuration["App:SelfUrl"]; |
||||
|
options.RedirectAllowedUrls.AddRange( |
||||
|
configuration["App:RedirectAllowedUrls"]?.Split(',') ?? Array.Empty<string>()); |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
> This is why Account email links point to your **host URL** out of the box: they reuse `App:SelfUrl`. If that default isn't what you want — for example, in a subdomain-based **multi-tenant** setup — override `Applications["MVC"].RootUrl` with the template you need (see [Multi-Tenant Aware URLs](#multi-tenant-aware-urls)). |
||||
|
|
||||
|
## Using `IAppUrlProvider` |
||||
|
|
||||
|
[Inject](../fundamentals/dependency-injection.md) the `IAppUrlProvider` service into any class that needs to build a cross-application URL. |
||||
|
|
||||
|
**Example: Resolve a root URL and a named URL of the MVC application** |
||||
|
|
||||
|
```csharp |
||||
|
public class MyNotificationSender : ITransientDependency |
||||
|
{ |
||||
|
private readonly IAppUrlProvider _appUrlProvider; |
||||
|
|
||||
|
public MyNotificationSender(IAppUrlProvider appUrlProvider) |
||||
|
{ |
||||
|
_appUrlProvider = appUrlProvider; |
||||
|
} |
||||
|
|
||||
|
public async Task SendAsync() |
||||
|
{ |
||||
|
var rootUrl = await _appUrlProvider.GetUrlAsync("MVC"); |
||||
|
var pageUrl = await _appUrlProvider.GetUrlAsync("MVC", "MyPage"); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
* `GetUrlAsync(appName)` returns the configured `RootUrl` for the given application. |
||||
|
* `GetUrlAsync(appName, urlName)` returns the **combined URL** described above. |
||||
|
* `GetUrlAsync(...)` throws an `AbpException` when the resolved URL is `null` or empty (e.g. both `RootUrl` and `Urls[urlName]` are unset). Use `GetUrlOrNullAsync(...)` if you'd rather get `null` and decide what to do yourself. |
||||
|
* `NormalizeUrlAsync(url)` applies tenant placeholder substitution to a URL string that you already have. Useful when the URL doesn't come from `AppUrlOptions`. |
||||
|
|
||||
|
## Multi-Tenant Aware URLs |
||||
|
|
||||
|
If your solution uses **subdomain-based** multi-tenancy (see the [Domain/Subdomain Tenant Resolver](../architecture/multi-tenancy/index.md#domainsubdomain-tenant-resolver)), you'll usually want the **outbound URLs** you generate (email links, redirects) to also be tenant-aware — otherwise the link in a password reset email won't point to the tenant's subdomain. |
||||
|
|
||||
|
`AppUrlOptions` supports the following **placeholders** in any URL value. They are substituted by `IAppUrlProvider` based on the **current tenant**: |
||||
|
|
||||
|
| Placeholder | Replaced with | |
||||
|
| --- | --- | |
||||
|
| `{0}` | Current tenant **name** | |
||||
|
| `{%{{{ {{tenantName}} }}}%}` | Current tenant **name** | |
||||
|
| `{%{{{ {{tenantId}} }}}%}` | Current tenant **id** | |
||||
|
|
||||
|
The `{0}` placeholder uses the **same convention** as `AddDomainTenantResolver("{0}.example.com")`, so a typical subdomain-tenant setup looks like this: |
||||
|
|
||||
|
**Example: Tenant-aware Account email links via a subdomain template** |
||||
|
|
||||
|
```csharp |
||||
|
Configure<AbpTenantResolveOptions>(options => |
||||
|
{ |
||||
|
options.AddDomainTenantResolver("{0}.example.com"); |
||||
|
}); |
||||
|
|
||||
|
Configure<AppUrlOptions>(options => |
||||
|
{ |
||||
|
options.Applications["MVC"].RootUrl = "https://{0}.example.com"; |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
With this configuration, password reset emails sent to a tenant whose name is `acme` will contain a link starting with `https://acme.example.com/`, matching the tenant's subdomain. |
||||
|
|
||||
|
### Host (no tenant) Fallback |
||||
|
|
||||
|
When there is **no current tenant** (host-side request), the placeholder **and the dot following it** are removed together: |
||||
|
|
||||
|
| Template | Tenant `acme` | Host (no tenant) | |
||||
|
| --- | --- | --- | |
||||
|
| `https://{0}.example.com` | `https://acme.example.com` | `https://example.com` | |
||||
|
| `https://{%{{{ {{tenantId}} }}}%}.example.com` | `https://3a21....example.com` | `https://example.com` | |
||||
|
|
||||
|
A single subdomain-style template like the ones above therefore works for **both** tenant and host scenarios without extra configuration. |
||||
|
|
||||
|
> If your subdomain is based on the tenant **id** rather than the name, use `https://{%{{{ {{tenantId}} }}}%}.example.com`. The resolver's `{0}` placeholder accepts both name and id when finding a tenant, but `AppUrlOptions` substitutes `{0}` with the tenant **name**; if those two don't match, switch to the explicit `{%{{{ {{tenantId}} }}}%}` form on the `AppUrlOptions` side. |
||||
|
|
||||
|
## Redirect Allowed URLs |
||||
|
|
||||
|
`AppUrlOptions.RedirectAllowedUrls` is a list of URL entries used by `IAppUrlProvider.IsRedirectAllowedUrlAsync(url)` to decide whether a redirect target is allowed. A URL is allowed when it satisfies **either** of: |
||||
|
|
||||
|
* **Prefix match**: the URL string **starts with** a configured entry (case-insensitive). |
||||
|
* **Subdomain match**: the URL and the entry have the **same scheme** and **port**, and the URL's host **ends with** `.{entry-host}`. |
||||
|
|
||||
|
**Example: Register allowed redirect URLs (including a wildcard)** |
||||
|
|
||||
|
```csharp |
||||
|
Configure<AppUrlOptions>(options => |
||||
|
{ |
||||
|
options.RedirectAllowedUrls.Add("https://my-app.com"); |
||||
|
options.RedirectAllowedUrls.Add("https://admin.my-app.com"); |
||||
|
|
||||
|
options.RedirectAllowedUrls.Add("https://*.my-app.com"); |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
* A **plain entry** like `https://my-app.com` allows any URL that starts with that prefix, plus any subdomain of `my-app.com`. |
||||
|
* A **wildcard entry** like `https://*.my-app.com` allows any subdomain of `my-app.com`; the `*.` is stripped before the subdomain check. |
||||
|
* Entries also go through **tenant placeholder substitution**, so `https://{0}.my-app.com` is resolved to the current tenant's URL first (e.g. `https://acme.my-app.com`) and then compared. Use the wildcard form when you need to allow *any* tenant subdomain regardless of the current tenant. |
||||
|
|
||||
|
## See Also |
||||
|
|
||||
|
* [Multi-Tenancy](../architecture/multi-tenancy/index.md) |
||||
|
* [Account Module](../../modules/account.md) |
||||
|
* [Emailing](emailing.md) |
||||
@ -1,133 +0,0 @@ |
|||||
```json |
|
||||
//[doc-seo] |
|
||||
{ |
|
||||
"Description": "Learn how to connect AI tools like Cursor, Claude Desktop, and VS Code to ABP Studio using the Model Context Protocol (MCP)." |
|
||||
} |
|
||||
``` |
|
||||
|
|
||||
# ABP Studio: Model Context Protocol (MCP) |
|
||||
|
|
||||
````json |
|
||||
//[doc-nav] |
|
||||
{ |
|
||||
"Next": { |
|
||||
"Name": "Working with Kubernetes", |
|
||||
"Path": "studio/kubernetes" |
|
||||
} |
|
||||
} |
|
||||
```` |
|
||||
|
|
||||
ABP Studio includes built-in [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) support so AI tools can query runtime telemetry and control solution runner operations. |
|
||||
|
|
||||
## How It Works |
|
||||
|
|
||||
ABP Studio runs a local MCP server in the background. The `abp mcp-studio` CLI command acts as a stdio bridge that AI clients connect to. The bridge forwards requests to ABP Studio and returns responses. |
|
||||
|
|
||||
```text |
|
||||
MCP Client (Cursor / Claude Desktop / VS Code) |
|
||||
──stdio──▶ abp mcp-studio ──HTTP──▶ ABP Studio |
|
||||
``` |
|
||||
|
|
||||
> ABP Studio must be running while MCP is used. If ABP Studio is not running (or its MCP endpoint is unavailable), `abp mcp-studio` returns an error to the AI client. |
|
||||
|
|
||||
## Configuration |
|
||||
|
|
||||
### Cursor (`.cursor/mcp.json`) |
|
||||
|
|
||||
```json |
|
||||
{ |
|
||||
"mcpServers": { |
|
||||
"abp-studio": { |
|
||||
"command": "abp", |
|
||||
"args": ["mcp-studio"] |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
``` |
|
||||
|
|
||||
### Claude Desktop (`claude_desktop_config.json`) |
|
||||
|
|
||||
```json |
|
||||
{ |
|
||||
"mcpServers": { |
|
||||
"abp-studio": { |
|
||||
"command": "abp", |
|
||||
"args": ["mcp-studio"] |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
``` |
|
||||
|
|
||||
Claude Desktop config file locations: |
|
||||
|
|
||||
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` |
|
||||
- Windows: `%APPDATA%\Claude\claude_desktop_config.json` |
|
||||
- Linux: `~/.config/Claude/claude_desktop_config.json` |
|
||||
|
|
||||
### VS Code (`.vscode/mcp.json`) |
|
||||
|
|
||||
```json |
|
||||
{ |
|
||||
"servers": { |
|
||||
"abp-studio": { |
|
||||
"command": "abp", |
|
||||
"args": ["mcp-studio"] |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
``` |
|
||||
|
|
||||
### Quick Reference |
|
||||
|
|
||||
You can run `abp help mcp-studio` at any time to see the available options and example configuration snippets for each supported IDE directly in your terminal. |
|
||||
|
|
||||
### Generating Config Files from ABP Studio |
|
||||
|
|
||||
When creating a new solution, ABP Studio can generate MCP configuration files for Cursor and VS Code automatically. |
|
||||
|
|
||||
## Available Tools |
|
||||
|
|
||||
ABP Studio exposes the following tools to MCP clients. All tools operate on the currently open solution and selected run profile in ABP Studio. |
|
||||
|
|
||||
### Monitoring |
|
||||
|
|
||||
| Tool | Description | |
|
||||
|------|-------------| |
|
||||
| `list_applications` | Lists all running ABP applications connected to ABP Studio. | |
|
||||
| `get_exceptions` | Gets recent exceptions including stack traces and error messages. | |
|
||||
| `get_logs` | Gets log entries. Can be filtered by log level. | |
|
||||
| `get_requests` | Gets HTTP request information. Can be filtered by status code. | |
|
||||
| `get_events` | Gets distributed events for debugging inter-service communication. | |
|
||||
| `clear_monitor` | Clears collected monitor data. | |
|
||||
|
|
||||
### Application Control |
|
||||
|
|
||||
| Tool | Description | |
|
||||
|------|-------------| |
|
||||
| `list_runnable_applications` | Lists all applications in the current run profile with their state. | |
|
||||
| `start_application` | Starts a stopped application. | |
|
||||
| `stop_application` | Stops a running application. | |
|
||||
| `restart_application` | Restarts a running application. | |
|
||||
| `build_application` | Builds a .NET application using `dotnet build`. | |
|
||||
|
|
||||
### Container Control |
|
||||
|
|
||||
| Tool | Description | |
|
||||
|------|-------------| |
|
||||
| `list_containers` | Lists Docker containers in the current run profile with their state. | |
|
||||
| `start_containers` | Starts Docker containers (docker-compose up). | |
|
||||
| `stop_containers` | Stops Docker containers (docker-compose down). | |
|
||||
|
|
||||
### Solution Structure |
|
||||
|
|
||||
| Tool | Description | |
|
||||
|------|-------------| |
|
||||
| `get_solution_info` | Gets solution name, path, template, and run profile information. | |
|
||||
| `list_modules` | Lists all modules in the solution. | |
|
||||
| `list_packages` | Lists packages (projects) in the solution. Can be filtered by module. | |
|
||||
| `get_module_dependencies` | Gets module dependency/import information. | |
|
||||
|
|
||||
## Notes |
|
||||
|
|
||||
- Monitor data (exceptions, logs, requests, events) is kept in memory and is cleared when the solution is closed. |
|
||||
- The `abp mcp-studio` command connects to the local ABP Studio instance. This is separate from the `abp mcp` command, which connects to the ABP.IO cloud MCP service and requires an active license. |
|
||||
@ -0,0 +1,157 @@ |
|||||
|
using System; |
||||
|
using System.IO; |
||||
|
using System.Net; |
||||
|
using System.Net.Http; |
||||
|
using System.Text; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Microsoft.Extensions.DependencyInjection.Extensions; |
||||
|
using Shouldly; |
||||
|
using Volo.Abp.AspNetCore.TestBase; |
||||
|
using Volo.Abp.Http.Client; |
||||
|
using Volo.Abp.Http.Client.Proxying; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Volo.Abp.Http.DynamicProxying; |
||||
|
|
||||
|
public class ClientProxyResponseDisposal_Tests : AbpHttpClientTestBase |
||||
|
{ |
||||
|
private readonly IRegularTestController _controller; |
||||
|
private readonly StubProxyHttpClientFactory _factory; |
||||
|
|
||||
|
public ClientProxyResponseDisposal_Tests() |
||||
|
{ |
||||
|
_controller = ServiceProvider.GetRequiredService<IRegularTestController>(); |
||||
|
_factory = (StubProxyHttpClientFactory)ServiceProvider.GetRequiredService<IProxyHttpClientFactory>(); |
||||
|
} |
||||
|
|
||||
|
protected override void ConfigureServices(IServiceCollection services) |
||||
|
{ |
||||
|
services.Replace(ServiceDescriptor.Singleton<IProxyHttpClientFactory, StubProxyHttpClientFactory>()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Non_Abp_Error_Response_Should_Dispose_HttpContent() |
||||
|
{ |
||||
|
var content = new TrackingHttpContent("Bad Gateway from upstream"); |
||||
|
_factory.SetStubResponse(() => new HttpResponseMessage(HttpStatusCode.BadGateway) |
||||
|
{ |
||||
|
Content = content |
||||
|
}); |
||||
|
|
||||
|
var exception = await Assert.ThrowsAsync<AbpRemoteCallException>( |
||||
|
() => _controller.IncrementValueAsync(1)); |
||||
|
|
||||
|
exception.HttpStatusCode.ShouldBe((int)HttpStatusCode.BadGateway); |
||||
|
content.Disposed.ShouldBeTrue(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Successful_Response_With_Json_Return_Should_Dispose_HttpContent() |
||||
|
{ |
||||
|
var content = new TrackingHttpContent("42"); |
||||
|
_factory.SetStubResponse(() => new HttpResponseMessage(HttpStatusCode.OK) |
||||
|
{ |
||||
|
Content = content |
||||
|
}); |
||||
|
|
||||
|
var result = await _controller.IncrementValueAsync(0); |
||||
|
|
||||
|
result.ShouldBe(42); |
||||
|
content.Disposed.ShouldBeTrue(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Successful_Response_With_Void_Return_Should_Dispose_HttpContent() |
||||
|
{ |
||||
|
var content = new TrackingHttpContent(string.Empty); |
||||
|
_factory.SetStubResponse(() => new HttpResponseMessage(HttpStatusCode.NoContent) |
||||
|
{ |
||||
|
Content = content |
||||
|
}); |
||||
|
|
||||
|
await _controller.GetException1Async(); |
||||
|
|
||||
|
content.Disposed.ShouldBeTrue(); |
||||
|
} |
||||
|
|
||||
|
class StubProxyHttpClientFactory : IProxyHttpClientFactory |
||||
|
{ |
||||
|
private readonly ITestServerAccessor _testServerAccessor; |
||||
|
|
||||
|
private int _count; |
||||
|
private Func<HttpResponseMessage> _responseFactory; |
||||
|
|
||||
|
public StubProxyHttpClientFactory(ITestServerAccessor testServerAccessor) |
||||
|
{ |
||||
|
_testServerAccessor = testServerAccessor; |
||||
|
} |
||||
|
|
||||
|
public void SetStubResponse(Func<HttpResponseMessage> responseFactory) |
||||
|
{ |
||||
|
_responseFactory = responseFactory; |
||||
|
} |
||||
|
|
||||
|
public HttpClient Create(string name) => Create(); |
||||
|
|
||||
|
public HttpClient Create() |
||||
|
{ |
||||
|
if (_count++ == 0) |
||||
|
{ |
||||
|
return _testServerAccessor.Server.CreateClient(); |
||||
|
} |
||||
|
|
||||
|
return new HttpClient(new StubHttpMessageHandler(() => |
||||
|
_responseFactory?.Invoke() |
||||
|
?? throw new InvalidOperationException("Stub response is not configured."))) |
||||
|
{ |
||||
|
BaseAddress = new Uri("http://localhost/") |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
class StubHttpMessageHandler : HttpMessageHandler |
||||
|
{ |
||||
|
private readonly Func<HttpResponseMessage> _responseFactory; |
||||
|
|
||||
|
public StubHttpMessageHandler(Func<HttpResponseMessage> responseFactory) |
||||
|
{ |
||||
|
_responseFactory = responseFactory; |
||||
|
} |
||||
|
|
||||
|
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
return Task.FromResult(_responseFactory()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
class TrackingHttpContent : HttpContent |
||||
|
{ |
||||
|
private readonly byte[] _data; |
||||
|
|
||||
|
public bool Disposed { get; private set; } |
||||
|
|
||||
|
public TrackingHttpContent(string text) |
||||
|
{ |
||||
|
_data = Encoding.UTF8.GetBytes(text); |
||||
|
} |
||||
|
|
||||
|
protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) |
||||
|
{ |
||||
|
return stream.WriteAsync(_data, 0, _data.Length); |
||||
|
} |
||||
|
|
||||
|
protected override bool TryComputeLength(out long length) |
||||
|
{ |
||||
|
length = _data.Length; |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
protected override void Dispose(bool disposing) |
||||
|
{ |
||||
|
Disposed = true; |
||||
|
base.Dispose(disposing); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue