mirror of https://github.com/abpframework/abp.git
6 changed files with 175 additions and 0 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) |
|||
Loading…
Reference in new issue