mirror of https://github.com/abpframework/abp.git
87 changed files with 1329 additions and 292 deletions
@ -0,0 +1,44 @@ |
|||||
|
name: Create Release |
||||
|
|
||||
|
on: |
||||
|
workflow_dispatch: |
||||
|
inputs: |
||||
|
tag_name: |
||||
|
description: 'Tag Name' |
||||
|
required: true |
||||
|
prerelease: |
||||
|
description: 'Pre-release?' |
||||
|
required: true |
||||
|
branchName: |
||||
|
description: 'Branch Name' |
||||
|
required: true |
||||
|
|
||||
|
jobs: |
||||
|
build: |
||||
|
runs-on: ubuntu-latest |
||||
|
steps: |
||||
|
- name: Checkout code |
||||
|
uses: actions/checkout@v2 |
||||
|
with: |
||||
|
ref: ${{ github.event.inputs.branchName }} |
||||
|
|
||||
|
- name: Create Release |
||||
|
id: create_release |
||||
|
uses: actions/create-release@v1 |
||||
|
env: |
||||
|
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} |
||||
|
with: |
||||
|
tag_name: ${{ github.event.inputs.tag_name }} |
||||
|
release_name: ${{ github.event.inputs.tag_name }} |
||||
|
draft: false |
||||
|
prerelease: ${{ github.event.inputs.prerelease }} |
||||
|
|
||||
|
- name: Checkout code at tag |
||||
|
uses: actions/checkout@v2 |
||||
|
with: |
||||
|
ref: ${{ github.event.inputs.tag_name }} |
||||
|
|
||||
|
- name: Build Project |
||||
|
run: | |
||||
|
# add your build commands here, depending on your project's requirements. |
||||
|
echo "Build project here" |
||||
@ -0,0 +1,8 @@ |
|||||
|
# ABP Version 7.4 Migration Guide |
||||
|
|
||||
|
This document is a guide for upgrading ABP v7.3 solutions to ABP v7.4. There are a few changes in this version that may affect your applications, please read it carefully and apply the necessary changes to your application. |
||||
|
|
||||
|
## TemplateDefinition |
||||
|
|
||||
|
The `LocalizationResource(Type)` of `TemplateDefinition` class is changed to `LocalizationResourceName(string)`. |
||||
|
|
||||
@ -1,6 +1,11 @@ |
|||||
namespace Volo.Abp.AspNetCore.Components.Web.Theming.Layout; |
using System; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Components.Web.Theming.Layout; |
||||
|
|
||||
public static class StandardLayouts |
public static class StandardLayouts |
||||
{ |
{ |
||||
public const string Application = "Application"; |
public const string Application = "Application"; |
||||
|
public const string Account = "Account"; |
||||
|
public const string Public = "Public"; |
||||
|
public const string Empty = "Empty"; |
||||
} |
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
namespace Volo.Abp.AspNetCore.Components.Web.Theming.Theming; |
||||
|
|
||||
|
public class AbpThemingOptions |
||||
|
{ |
||||
|
public ThemeDictionary Themes { get; } |
||||
|
|
||||
|
public string DefaultThemeName { get; set; } |
||||
|
|
||||
|
public AbpThemingOptions() |
||||
|
{ |
||||
|
Themes = new ThemeDictionary(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,34 @@ |
|||||
|
using System; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Components.Web.Theming.Theming; |
||||
|
|
||||
|
public class DefaultThemeManager : IThemeManager, IScopedDependency, IServiceProviderAccessor |
||||
|
{ |
||||
|
public IServiceProvider ServiceProvider { get; } |
||||
|
public ITheme CurrentTheme => GetCurrentTheme(); |
||||
|
|
||||
|
private ITheme _currentTheme; |
||||
|
|
||||
|
protected IThemeSelector ThemeSelector { get; } |
||||
|
|
||||
|
public DefaultThemeManager( |
||||
|
IServiceProvider serviceProvider, |
||||
|
IThemeSelector themeSelector) |
||||
|
{ |
||||
|
ServiceProvider = serviceProvider; |
||||
|
ThemeSelector = themeSelector; |
||||
|
} |
||||
|
|
||||
|
protected virtual ITheme GetCurrentTheme() |
||||
|
{ |
||||
|
if (_currentTheme != null) |
||||
|
{ |
||||
|
return _currentTheme; |
||||
|
} |
||||
|
|
||||
|
_currentTheme = (ITheme)ServiceProvider.GetRequiredService(ThemeSelector.GetCurrentThemeInfo().ThemeType); |
||||
|
return CurrentTheme; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,36 @@ |
|||||
|
using System.Linq; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Components.Web.Theming.Theming; |
||||
|
|
||||
|
public class DefaultThemeSelector : IThemeSelector, ITransientDependency |
||||
|
{ |
||||
|
protected AbpThemingOptions Options { get; } |
||||
|
|
||||
|
public DefaultThemeSelector(IOptions<AbpThemingOptions> options) |
||||
|
{ |
||||
|
Options = options.Value; |
||||
|
} |
||||
|
|
||||
|
public virtual ThemeInfo GetCurrentThemeInfo() |
||||
|
{ |
||||
|
if (!Options.Themes.Any()) |
||||
|
{ |
||||
|
throw new AbpException($"No theme registered! Use {nameof(AbpThemingOptions)} to register themes."); |
||||
|
} |
||||
|
|
||||
|
if (Options.DefaultThemeName == null) |
||||
|
{ |
||||
|
return Options.Themes.Values.First(); |
||||
|
} |
||||
|
|
||||
|
var themeInfo = Options.Themes.Values.FirstOrDefault(t => t.Name == Options.DefaultThemeName); |
||||
|
if (themeInfo == null) |
||||
|
{ |
||||
|
throw new AbpException("Default theme is configured but it's not found in the registered themes: " + Options.DefaultThemeName); |
||||
|
} |
||||
|
|
||||
|
return themeInfo; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,8 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Components.Web.Theming.Theming; |
||||
|
|
||||
|
public interface ITheme |
||||
|
{ |
||||
|
Type GetLayout(string name, bool fallbackToDefault = true); |
||||
|
} |
||||
@ -0,0 +1,6 @@ |
|||||
|
namespace Volo.Abp.AspNetCore.Components.Web.Theming.Theming; |
||||
|
|
||||
|
public interface IThemeManager |
||||
|
{ |
||||
|
ITheme CurrentTheme { get; } |
||||
|
} |
||||
@ -0,0 +1,6 @@ |
|||||
|
namespace Volo.Abp.AspNetCore.Components.Web.Theming.Theming; |
||||
|
|
||||
|
public interface IThemeSelector |
||||
|
{ |
||||
|
ThemeInfo GetCurrentThemeInfo(); |
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Components.Web.Theming.Theming; |
||||
|
|
||||
|
public class ThemeDictionary : Dictionary<Type, ThemeInfo> |
||||
|
{ |
||||
|
public ThemeInfo Add<TTheme>() |
||||
|
where TTheme : ITheme |
||||
|
{ |
||||
|
return Add(typeof(TTheme)); |
||||
|
} |
||||
|
|
||||
|
public ThemeInfo Add(Type themeType) |
||||
|
{ |
||||
|
if (ContainsKey(themeType)) |
||||
|
{ |
||||
|
throw new AbpException("This theme is already added before: " + themeType.AssemblyQualifiedName); |
||||
|
} |
||||
|
|
||||
|
return this[themeType] = new ThemeInfo(themeType); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
using System; |
||||
|
using Volo.Abp.AspNetCore.Components.Web.Theming.Layout; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Components.Web.Theming.Theming; |
||||
|
|
||||
|
public static class ThemeExtensions |
||||
|
{ |
||||
|
public static Type GetApplicationLayout(this ITheme theme, bool fallbackToDefault = true) |
||||
|
{ |
||||
|
return theme.GetLayout(StandardLayouts.Application, fallbackToDefault); |
||||
|
} |
||||
|
|
||||
|
public static Type GetAccountLayout(this ITheme theme, bool fallbackToDefault = true) |
||||
|
{ |
||||
|
return theme.GetLayout(StandardLayouts.Account, fallbackToDefault); |
||||
|
} |
||||
|
|
||||
|
public static Type GetPublicLayout(this ITheme theme, bool fallbackToDefault = true) |
||||
|
{ |
||||
|
return theme.GetLayout(StandardLayouts.Public, fallbackToDefault); |
||||
|
} |
||||
|
|
||||
|
public static Type GetEmptyLayout(this ITheme theme, bool fallbackToDefault = true) |
||||
|
{ |
||||
|
return theme.GetLayout(StandardLayouts.Empty, fallbackToDefault); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
using System; |
||||
|
using JetBrains.Annotations; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Components.Web.Theming.Theming; |
||||
|
|
||||
|
public class ThemeInfo |
||||
|
{ |
||||
|
public Type ThemeType { get; } |
||||
|
|
||||
|
public string Name { get; } |
||||
|
|
||||
|
public ThemeInfo([NotNull] Type themeType) |
||||
|
{ |
||||
|
Check.NotNull(themeType, nameof(themeType)); |
||||
|
|
||||
|
if (!typeof(ITheme).IsAssignableFrom(themeType)) |
||||
|
{ |
||||
|
throw new AbpException($"Given {nameof(themeType)} ({themeType.AssemblyQualifiedName}) should be type of {typeof(ITheme).AssemblyQualifiedName}"); |
||||
|
} |
||||
|
|
||||
|
ThemeType = themeType; |
||||
|
Name = ThemeNameAttribute.GetName(themeType); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
using System; |
||||
|
using System.Linq; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Components.Web.Theming.Theming; |
||||
|
|
||||
|
[AttributeUsage(AttributeTargets.Class)] |
||||
|
public class ThemeNameAttribute : Attribute |
||||
|
{ |
||||
|
public string Name { get; set; } |
||||
|
|
||||
|
public ThemeNameAttribute(string name) |
||||
|
{ |
||||
|
Name = name; |
||||
|
} |
||||
|
|
||||
|
public static string GetName(Type themeType) |
||||
|
{ |
||||
|
return themeType |
||||
|
.GetCustomAttributes(true) |
||||
|
.OfType<ThemeNameAttribute>() |
||||
|
.FirstOrDefault()?.Name ?? themeType.Name; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,12 @@ |
|||||
|
using System; |
||||
|
using Volo.Abp.EventBus; |
||||
|
|
||||
|
namespace Volo.Abp.Data; |
||||
|
|
||||
|
[Serializable] |
||||
|
[EventName("abp.data.applied_database_migrations")] |
||||
|
public class AppliedDatabaseMigrationsEto |
||||
|
{ |
||||
|
public string DatabaseName { get; set; } |
||||
|
public Guid? TenantId { get; set; } |
||||
|
} |
||||
@ -0,0 +1,318 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.EntityFrameworkCore; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
using Volo.Abp.Data; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Volo.Abp.Domain.Entities.Events.Distributed; |
||||
|
using Volo.Abp.EventBus.Distributed; |
||||
|
using Volo.Abp.MultiTenancy; |
||||
|
using Volo.Abp.Uow; |
||||
|
|
||||
|
namespace Volo.Abp.EntityFrameworkCore.Migrations; |
||||
|
|
||||
|
public abstract class DatabaseMigrationEventHandlerBase<TDbContext> : |
||||
|
IDistributedEventHandler<TenantCreatedEto>, |
||||
|
IDistributedEventHandler<TenantConnectionStringUpdatedEto>, |
||||
|
IDistributedEventHandler<ApplyDatabaseMigrationsEto>, |
||||
|
ITransientDependency |
||||
|
where TDbContext : DbContext, IEfCoreDbContext |
||||
|
{ |
||||
|
protected string DatabaseName { get; } |
||||
|
|
||||
|
protected const string TryCountPropertyName = "__TryCount"; |
||||
|
|
||||
|
protected int MaxEventTryCount { get; set; } = 3; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// As milliseconds.
|
||||
|
/// </summary>
|
||||
|
protected int MinValueToWaitOnFailure { get; set; } = 5000; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// As milliseconds.
|
||||
|
/// </summary>
|
||||
|
protected int MaxValueToWaitOnFailure { get; set; } = 15000; |
||||
|
|
||||
|
protected ICurrentTenant CurrentTenant { get; } |
||||
|
protected IUnitOfWorkManager UnitOfWorkManager { get; } |
||||
|
protected ITenantStore TenantStore { get; } |
||||
|
protected IDistributedEventBus DistributedEventBus { get; } |
||||
|
protected ILogger<DatabaseMigrationEventHandlerBase<TDbContext>> Logger { get; } |
||||
|
|
||||
|
protected DatabaseMigrationEventHandlerBase( |
||||
|
string databaseName, |
||||
|
ICurrentTenant currentTenant, |
||||
|
IUnitOfWorkManager unitOfWorkManager, |
||||
|
ITenantStore tenantStore, |
||||
|
IDistributedEventBus distributedEventBus, |
||||
|
ILoggerFactory loggerFactory) |
||||
|
{ |
||||
|
CurrentTenant = currentTenant; |
||||
|
UnitOfWorkManager = unitOfWorkManager; |
||||
|
TenantStore = tenantStore; |
||||
|
DatabaseName = databaseName; |
||||
|
DistributedEventBus = distributedEventBus; |
||||
|
|
||||
|
Logger = loggerFactory.CreateLogger<DatabaseMigrationEventHandlerBase<TDbContext>>(); |
||||
|
} |
||||
|
|
||||
|
public virtual async Task HandleEventAsync(ApplyDatabaseMigrationsEto eventData) |
||||
|
{ |
||||
|
if (eventData.DatabaseName != DatabaseName) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
var schemaMigrated = false; |
||||
|
try |
||||
|
{ |
||||
|
schemaMigrated = await MigrateDatabaseSchemaAsync(eventData.TenantId); |
||||
|
await SeedAsync(eventData.TenantId); |
||||
|
|
||||
|
if (schemaMigrated) |
||||
|
{ |
||||
|
await DistributedEventBus.PublishAsync( |
||||
|
new AppliedDatabaseMigrationsEto |
||||
|
{ |
||||
|
DatabaseName = DatabaseName, |
||||
|
TenantId = eventData.TenantId |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
await HandleErrorOnApplyDatabaseMigrationAsync(eventData, ex); |
||||
|
} |
||||
|
|
||||
|
await AfterApplyDatabaseMigrations(eventData, schemaMigrated); |
||||
|
} |
||||
|
|
||||
|
protected virtual Task AfterApplyDatabaseMigrations(ApplyDatabaseMigrationsEto eventData, bool schemaMigrated) |
||||
|
{ |
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
public virtual async Task HandleEventAsync(TenantCreatedEto eventData) |
||||
|
{ |
||||
|
var schemaMigrated = false; |
||||
|
try |
||||
|
{ |
||||
|
schemaMigrated = await MigrateDatabaseSchemaAsync(eventData.Id); |
||||
|
await SeedAsync(eventData.Id); |
||||
|
|
||||
|
if (schemaMigrated) |
||||
|
{ |
||||
|
await DistributedEventBus.PublishAsync( |
||||
|
new AppliedDatabaseMigrationsEto |
||||
|
{ |
||||
|
DatabaseName = DatabaseName, |
||||
|
TenantId = eventData.Id |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
await HandleErrorTenantCreatedAsync(eventData, ex); |
||||
|
} |
||||
|
|
||||
|
await AfterTenantCreated(eventData, schemaMigrated); |
||||
|
} |
||||
|
|
||||
|
protected virtual Task AfterTenantCreated(TenantCreatedEto eventData, bool schemaMigrated) |
||||
|
{ |
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
public virtual async Task HandleEventAsync(TenantConnectionStringUpdatedEto eventData) |
||||
|
{ |
||||
|
if (eventData.ConnectionStringName != DatabaseName && |
||||
|
eventData.ConnectionStringName != Volo.Abp.Data.ConnectionStrings.DefaultConnectionStringName || |
||||
|
eventData.NewValue.IsNullOrWhiteSpace()) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
var schemaMigrated = false; |
||||
|
try |
||||
|
{ |
||||
|
schemaMigrated = await MigrateDatabaseSchemaAsync(eventData.Id); |
||||
|
await SeedAsync(eventData.Id); |
||||
|
|
||||
|
if (schemaMigrated) |
||||
|
{ |
||||
|
await DistributedEventBus.PublishAsync( |
||||
|
new AppliedDatabaseMigrationsEto |
||||
|
{ |
||||
|
DatabaseName = DatabaseName, |
||||
|
TenantId = eventData.Id |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
await HandleErrorTenantConnectionStringUpdatedAsync(eventData, ex); |
||||
|
} |
||||
|
|
||||
|
await AfterTenantConnectionStringUpdated(eventData, schemaMigrated); |
||||
|
} |
||||
|
|
||||
|
protected virtual Task AfterTenantConnectionStringUpdated(TenantConnectionStringUpdatedEto eventData, |
||||
|
bool schemaMigrated) |
||||
|
{ |
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
protected virtual Task SeedAsync(Guid? tenantId) |
||||
|
{ |
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Apply pending EF Core schema migrations to the database.
|
||||
|
/// Returns true if any migration has applied.
|
||||
|
/// </summary>
|
||||
|
protected virtual async Task<bool> MigrateDatabaseSchemaAsync(Guid? tenantId) |
||||
|
{ |
||||
|
var result = false; |
||||
|
|
||||
|
using (CurrentTenant.Change(tenantId)) |
||||
|
{ |
||||
|
using (var uow = UnitOfWorkManager.Begin(requiresNew: true, isTransactional: false)) |
||||
|
{ |
||||
|
async Task<bool> MigrateDatabaseSchemaWithDbContextAsync() |
||||
|
{ |
||||
|
var dbContext = await uow.ServiceProvider |
||||
|
.GetRequiredService<IDbContextProvider<TDbContext>>() |
||||
|
.GetDbContextAsync(); |
||||
|
|
||||
|
if ((await dbContext.Database.GetPendingMigrationsAsync()).Any()) |
||||
|
{ |
||||
|
await dbContext.Database.MigrateAsync(); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
if (tenantId == null) |
||||
|
{ |
||||
|
//Migrating the host database
|
||||
|
Logger.LogInformation($"Migrating database of host. Database Name = {DatabaseName}"); |
||||
|
result = await MigrateDatabaseSchemaWithDbContextAsync(); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
var tenantConfiguration = await TenantStore.FindAsync(tenantId.Value); |
||||
|
if (!tenantConfiguration.ConnectionStrings.Default.IsNullOrWhiteSpace() || |
||||
|
!tenantConfiguration.ConnectionStrings.GetOrDefault(DatabaseName).IsNullOrWhiteSpace()) |
||||
|
{ |
||||
|
//Migrating the tenant database (only if tenant has a separate database)
|
||||
|
Logger.LogInformation( |
||||
|
$"Migrating separate database of tenant. Database Name = {DatabaseName}, TenantId = {tenantId}"); |
||||
|
result = await MigrateDatabaseSchemaWithDbContextAsync(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
await uow.CompleteAsync(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
protected virtual async Task HandleErrorOnApplyDatabaseMigrationAsync( |
||||
|
ApplyDatabaseMigrationsEto eventData, |
||||
|
Exception exception) |
||||
|
{ |
||||
|
var tryCount = IncrementEventTryCount(eventData); |
||||
|
if (tryCount <= MaxEventTryCount) |
||||
|
{ |
||||
|
Logger.LogWarning( |
||||
|
$"Could not apply database migrations. Re-queueing the operation. TenantId = {eventData.TenantId}, Database Name = {eventData.DatabaseName}."); |
||||
|
Logger.LogException(exception, LogLevel.Warning); |
||||
|
|
||||
|
await Task.Delay(RandomHelper.GetRandom(MinValueToWaitOnFailure, MaxValueToWaitOnFailure)); |
||||
|
await DistributedEventBus.PublishAsync(eventData); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
Logger.LogError( |
||||
|
$"Could not apply database migrations. Canceling the operation. TenantId = {eventData.TenantId}, DatabaseName = {eventData.DatabaseName}."); |
||||
|
Logger.LogException(exception); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected virtual async Task HandleErrorTenantCreatedAsync( |
||||
|
TenantCreatedEto eventData, |
||||
|
Exception exception) |
||||
|
{ |
||||
|
var tryCount = IncrementEventTryCount(eventData); |
||||
|
if (tryCount <= MaxEventTryCount) |
||||
|
{ |
||||
|
Logger.LogWarning( |
||||
|
$"Could not perform tenant created event. Re-queueing the operation. TenantId = {eventData.Id}, TenantName = {eventData.Name}."); |
||||
|
Logger.LogException(exception, LogLevel.Warning); |
||||
|
|
||||
|
await Task.Delay(RandomHelper.GetRandom(5000, 15000)); |
||||
|
await DistributedEventBus.PublishAsync(eventData); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
Logger.LogError( |
||||
|
$"Could not perform tenant created event. Canceling the operation. TenantId = {eventData.Id}, TenantName = {eventData.Name}."); |
||||
|
Logger.LogException(exception); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected virtual async Task HandleErrorTenantConnectionStringUpdatedAsync( |
||||
|
TenantConnectionStringUpdatedEto eventData, |
||||
|
Exception exception) |
||||
|
{ |
||||
|
var tryCount = IncrementEventTryCount(eventData); |
||||
|
if (tryCount <= MaxEventTryCount) |
||||
|
{ |
||||
|
Logger.LogWarning( |
||||
|
$"Could not perform tenant connection string updated event. Re-queueing the operation. TenantId = {eventData.Id}, TenantName = {eventData.Name}."); |
||||
|
Logger.LogException(exception, LogLevel.Warning); |
||||
|
|
||||
|
await Task.Delay(RandomHelper.GetRandom(5000, 15000)); |
||||
|
await DistributedEventBus.PublishAsync(eventData); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
Logger.LogError( |
||||
|
$"Could not perform tenant connection string updated event. Canceling the operation. TenantId = {eventData.Id}, TenantName = {eventData.Name}."); |
||||
|
Logger.LogException(exception); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static int GetEventTryCount(EtoBase eventData) |
||||
|
{ |
||||
|
var tryCountAsString = eventData.Properties.GetOrDefault(TryCountPropertyName); |
||||
|
if (tryCountAsString.IsNullOrEmpty()) |
||||
|
{ |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
return int.Parse(tryCountAsString); |
||||
|
} |
||||
|
|
||||
|
private static void SetEventTryCount(EtoBase eventData, int count) |
||||
|
{ |
||||
|
eventData.Properties[TryCountPropertyName] = count.ToString(); |
||||
|
} |
||||
|
|
||||
|
private static int IncrementEventTryCount(EtoBase eventData) |
||||
|
{ |
||||
|
var count = GetEventTryCount(eventData) + 1; |
||||
|
SetEventTryCount(eventData, count); |
||||
|
return count; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,145 @@ |
|||||
|
using System; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.EntityFrameworkCore; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
using Volo.Abp.Data; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Volo.Abp.DistributedLocking; |
||||
|
using Volo.Abp.EventBus.Distributed; |
||||
|
using Volo.Abp.MultiTenancy; |
||||
|
using Volo.Abp.Uow; |
||||
|
|
||||
|
namespace Volo.Abp.EntityFrameworkCore.Migrations; |
||||
|
|
||||
|
public abstract class EfCoreRuntimeDatabaseMigratorBase<TDbContext> : ITransientDependency |
||||
|
where TDbContext : DbContext, IEfCoreDbContext |
||||
|
{ |
||||
|
protected int MinValueToWaitOnFailure { get; set; } = 5000; |
||||
|
protected int MaxValueToWaitOnFailure { get; set; } = 15000; |
||||
|
|
||||
|
protected string DatabaseName { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Enabling this might be inefficient if you have many tenants!
|
||||
|
/// If disabled (default), tenant databases will be seeded only
|
||||
|
/// if there is a schema migration applied to the host database.
|
||||
|
/// If enabled, tenant databases will be seeded always on every service startup.
|
||||
|
/// </summary>
|
||||
|
protected bool AlwaysSeedTenantDatabases { get; set; } = false; |
||||
|
|
||||
|
protected IUnitOfWorkManager UnitOfWorkManager { get; } |
||||
|
protected IServiceProvider ServiceProvider { get; } |
||||
|
protected ICurrentTenant CurrentTenant { get; } |
||||
|
protected IAbpDistributedLock DistributedLock { get; } |
||||
|
protected IDistributedEventBus DistributedEventBus { get; } |
||||
|
protected ILogger<EfCoreRuntimeDatabaseMigratorBase<TDbContext>> Logger { get; } |
||||
|
|
||||
|
protected EfCoreRuntimeDatabaseMigratorBase( |
||||
|
string databaseName, |
||||
|
IUnitOfWorkManager unitOfWorkManager, |
||||
|
IServiceProvider serviceProvider, |
||||
|
ICurrentTenant currentTenant, |
||||
|
IAbpDistributedLock abpDistributedLock, |
||||
|
IDistributedEventBus distributedEventBus, |
||||
|
ILoggerFactory loggerFactory) |
||||
|
{ |
||||
|
DatabaseName = databaseName; |
||||
|
UnitOfWorkManager = unitOfWorkManager; |
||||
|
ServiceProvider = serviceProvider; |
||||
|
CurrentTenant = currentTenant; |
||||
|
DistributedLock = abpDistributedLock; |
||||
|
DistributedEventBus = distributedEventBus; |
||||
|
Logger = loggerFactory.CreateLogger<EfCoreRuntimeDatabaseMigratorBase<TDbContext>>(); |
||||
|
} |
||||
|
|
||||
|
public virtual async Task CheckAndApplyDatabaseMigrationsAsync() |
||||
|
{ |
||||
|
await TryAsync(LockAndApplyDatabaseMigrationsAsync); |
||||
|
} |
||||
|
|
||||
|
protected virtual async Task LockAndApplyDatabaseMigrationsAsync() |
||||
|
{ |
||||
|
Logger.LogInformation($"Trying to acquire the distributed lock for database migration: {DatabaseName}."); |
||||
|
|
||||
|
var schemaMigrated = false; |
||||
|
|
||||
|
await using (var handle = await DistributedLock.TryAcquireAsync("DatabaseMigration_" + DatabaseName)) |
||||
|
{ |
||||
|
if (handle is null) |
||||
|
{ |
||||
|
Logger.LogInformation($"Distributed lock could not be acquired for database migration: {DatabaseName}. Operation cancelled."); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
Logger.LogInformation($"Distributed lock is acquired for database migration: {DatabaseName}..."); |
||||
|
|
||||
|
using (CurrentTenant.Change(null)) |
||||
|
{ |
||||
|
// Create database tables if needed
|
||||
|
using (var uow = UnitOfWorkManager.Begin(requiresNew: true, isTransactional: false)) |
||||
|
{ |
||||
|
var dbContext = await ServiceProvider |
||||
|
.GetRequiredService<IDbContextProvider<TDbContext>>() |
||||
|
.GetDbContextAsync(); |
||||
|
|
||||
|
var pendingMigrations = await dbContext |
||||
|
.Database |
||||
|
.GetPendingMigrationsAsync(); |
||||
|
|
||||
|
if (pendingMigrations.Any()) |
||||
|
{ |
||||
|
await dbContext.Database.MigrateAsync(); |
||||
|
schemaMigrated = true; |
||||
|
} |
||||
|
|
||||
|
await uow.CompleteAsync(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
await SeedAsync(); |
||||
|
|
||||
|
if (schemaMigrated || AlwaysSeedTenantDatabases) |
||||
|
{ |
||||
|
await DistributedEventBus.PublishAsync( |
||||
|
new AppliedDatabaseMigrationsEto |
||||
|
{ |
||||
|
DatabaseName = DatabaseName, |
||||
|
TenantId = null |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Logger.LogInformation($"Distributed lock has been released for database migration: {DatabaseName}..."); |
||||
|
} |
||||
|
|
||||
|
protected virtual Task SeedAsync() |
||||
|
{ |
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
protected virtual async Task TryAsync(Func<Task> task, int maxTryCount = 3) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
await task(); |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
maxTryCount--; |
||||
|
|
||||
|
if (maxTryCount <= 0) |
||||
|
{ |
||||
|
throw; |
||||
|
} |
||||
|
|
||||
|
Logger.LogWarning($"{ex.GetType().Name} has been thrown. The operation will be tried {maxTryCount} times more. Exception:\n{ex.Message}. Stack Trace:\n{ex.StackTrace}"); |
||||
|
|
||||
|
await Task.Delay(RandomHelper.GetRandom(MinValueToWaitOnFailure, MaxValueToWaitOnFailure)); |
||||
|
|
||||
|
await TryAsync(task, maxTryCount); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
using JetBrains.Annotations; |
||||
|
|
||||
|
namespace Volo.Abp.TextTemplating; |
||||
|
|
||||
|
public interface IDynamicTemplateDefinitionStore |
||||
|
{ |
||||
|
Task<TemplateDefinition> GetAsync([NotNull] string name); |
||||
|
|
||||
|
Task<IReadOnlyList<TemplateDefinition>> GetAllAsync(); |
||||
|
|
||||
|
Task<TemplateDefinition> GetOrNullAsync(string name); |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
using JetBrains.Annotations; |
||||
|
|
||||
|
namespace Volo.Abp.TextTemplating; |
||||
|
|
||||
|
public interface IStaticTemplateDefinitionStore |
||||
|
{ |
||||
|
Task<TemplateDefinition> GetAsync([NotNull] string name); |
||||
|
|
||||
|
Task<IReadOnlyList<TemplateDefinition>> GetAllAsync(); |
||||
|
|
||||
|
Task<TemplateDefinition> GetOrNullAsync(string name); |
||||
|
} |
||||
@ -1,16 +1,17 @@ |
|||||
using System.Collections.Generic; |
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
using JetBrains.Annotations; |
using JetBrains.Annotations; |
||||
|
|
||||
namespace Volo.Abp.TextTemplating; |
namespace Volo.Abp.TextTemplating; |
||||
|
|
||||
public interface ITemplateDefinitionManager |
public interface ITemplateDefinitionManager |
||||
{ |
{ |
||||
[NotNull] |
[ItemNotNull] |
||||
TemplateDefinition Get([NotNull] string name); |
Task<TemplateDefinition> GetAsync([NotNull] string name); |
||||
|
|
||||
[NotNull] |
[ItemNotNull] |
||||
IReadOnlyList<TemplateDefinition> GetAll(); |
Task<IReadOnlyList<TemplateDefinition>> GetAllAsync(); |
||||
|
|
||||
[CanBeNull] |
[ItemCanBeNull] |
||||
TemplateDefinition GetOrNull(string name); |
Task<TemplateDefinition> GetOrNullAsync(string name); |
||||
} |
} |
||||
|
|||||
@ -0,0 +1,29 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Collections.Immutable; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
|
||||
|
namespace Volo.Abp.TextTemplating; |
||||
|
|
||||
|
public class NullIDynamicTemplateDefinitionStore : IDynamicTemplateDefinitionStore, ISingletonDependency |
||||
|
{ |
||||
|
private readonly static Task<TemplateDefinition> CachedTemplateResult = Task.FromResult((TemplateDefinition)null); |
||||
|
|
||||
|
private readonly static Task<IReadOnlyList<TemplateDefinition>> CachedFeaturesResult = Task.FromResult((IReadOnlyList<TemplateDefinition>)Array.Empty<TemplateDefinition>().ToImmutableList()); |
||||
|
|
||||
|
public Task<TemplateDefinition> GetAsync(string name) |
||||
|
{ |
||||
|
return CachedTemplateResult; |
||||
|
} |
||||
|
|
||||
|
public Task<IReadOnlyList<TemplateDefinition>> GetAllAsync() |
||||
|
{ |
||||
|
return CachedFeaturesResult; |
||||
|
} |
||||
|
|
||||
|
public Task<TemplateDefinition> GetOrNullAsync(string name) |
||||
|
{ |
||||
|
return CachedTemplateResult; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,83 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Collections.Immutable; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
|
||||
|
namespace Volo.Abp.TextTemplating; |
||||
|
|
||||
|
public class StaticTemplateDefinitionStore : IStaticTemplateDefinitionStore, ISingletonDependency |
||||
|
{ |
||||
|
protected Lazy<IDictionary<string, TemplateDefinition>> TemplateDefinitions { get; } |
||||
|
|
||||
|
protected AbpTextTemplatingOptions Options { get; } |
||||
|
|
||||
|
protected IServiceProvider ServiceProvider { get; } |
||||
|
|
||||
|
public StaticTemplateDefinitionStore(IOptions<AbpTextTemplatingOptions> options, IServiceProvider serviceProvider) |
||||
|
{ |
||||
|
ServiceProvider = serviceProvider; |
||||
|
Options = options.Value; |
||||
|
|
||||
|
TemplateDefinitions = new Lazy<IDictionary<string, TemplateDefinition>>(CreateTextTemplateDefinitions, true); |
||||
|
} |
||||
|
|
||||
|
public virtual Task<TemplateDefinition> GetAsync(string name) |
||||
|
{ |
||||
|
Check.NotNull(name, nameof(name)); |
||||
|
|
||||
|
var template = GetOrNullAsync(name); |
||||
|
|
||||
|
if (template == null) |
||||
|
{ |
||||
|
throw new AbpException("Undefined template: " + name); |
||||
|
} |
||||
|
|
||||
|
return template; |
||||
|
} |
||||
|
|
||||
|
public virtual Task<IReadOnlyList<TemplateDefinition>> GetAllAsync() |
||||
|
{ |
||||
|
return Task.FromResult<IReadOnlyList<TemplateDefinition>>(TemplateDefinitions.Value.Values.ToImmutableList()); |
||||
|
} |
||||
|
|
||||
|
public virtual Task<TemplateDefinition> GetOrNullAsync(string name) |
||||
|
{ |
||||
|
return Task.FromResult(TemplateDefinitions.Value.GetOrDefault(name)); |
||||
|
} |
||||
|
|
||||
|
protected virtual IDictionary<string, TemplateDefinition> CreateTextTemplateDefinitions() |
||||
|
{ |
||||
|
var templates = new Dictionary<string, TemplateDefinition>(); |
||||
|
|
||||
|
using (var scope = ServiceProvider.CreateScope()) |
||||
|
{ |
||||
|
var providers = Options |
||||
|
.DefinitionProviders |
||||
|
.Select(p => scope.ServiceProvider.GetRequiredService(p) as ITemplateDefinitionProvider) |
||||
|
.ToList(); |
||||
|
|
||||
|
var context = new TemplateDefinitionContext(templates); |
||||
|
|
||||
|
foreach (var provider in providers) |
||||
|
{ |
||||
|
provider.PreDefine(context); |
||||
|
} |
||||
|
|
||||
|
foreach (var provider in providers) |
||||
|
{ |
||||
|
provider.Define(context); |
||||
|
} |
||||
|
|
||||
|
foreach (var provider in providers) |
||||
|
{ |
||||
|
provider.PostDefine(context); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return templates; |
||||
|
} |
||||
|
} |
||||
@ -1,85 +1,50 @@ |
|||||
using System; |
using System.Collections.Generic; |
||||
using System.Collections.Generic; |
|
||||
using System.Collections.Immutable; |
using System.Collections.Immutable; |
||||
using System.Linq; |
using System.Linq; |
||||
using Microsoft.Extensions.DependencyInjection; |
using System.Threading.Tasks; |
||||
using Microsoft.Extensions.Options; |
|
||||
using Volo.Abp.DependencyInjection; |
using Volo.Abp.DependencyInjection; |
||||
|
|
||||
namespace Volo.Abp.TextTemplating; |
namespace Volo.Abp.TextTemplating; |
||||
|
|
||||
public class TemplateDefinitionManager : ITemplateDefinitionManager, ISingletonDependency |
public class TemplateDefinitionManager : ITemplateDefinitionManager, ISingletonDependency |
||||
{ |
{ |
||||
protected Lazy<IDictionary<string, TemplateDefinition>> TemplateDefinitions { get; } |
protected readonly IStaticTemplateDefinitionStore StaticStore; |
||||
|
protected readonly IDynamicTemplateDefinitionStore DynamicStore; |
||||
|
|
||||
protected AbpTextTemplatingOptions Options { get; } |
public TemplateDefinitionManager(IStaticTemplateDefinitionStore staticStore, IDynamicTemplateDefinitionStore dynamicStore) |
||||
|
|
||||
protected IServiceProvider ServiceProvider { get; } |
|
||||
|
|
||||
public TemplateDefinitionManager( |
|
||||
IOptions<AbpTextTemplatingOptions> options, |
|
||||
IServiceProvider serviceProvider) |
|
||||
{ |
{ |
||||
ServiceProvider = serviceProvider; |
StaticStore = staticStore; |
||||
Options = options.Value; |
DynamicStore = dynamicStore; |
||||
|
|
||||
TemplateDefinitions = |
|
||||
new Lazy<IDictionary<string, TemplateDefinition>>(CreateTextTemplateDefinitions, true); |
|
||||
} |
} |
||||
|
|
||||
public virtual TemplateDefinition Get(string name) |
public virtual async Task<TemplateDefinition> GetAsync(string name) |
||||
{ |
{ |
||||
Check.NotNull(name, nameof(name)); |
var permission = await GetOrNullAsync(name); |
||||
|
if (permission == null) |
||||
var template = GetOrNull(name); |
|
||||
|
|
||||
if (template == null) |
|
||||
{ |
{ |
||||
throw new AbpException("Undefined template: " + name); |
throw new AbpException("Undefined Template: " + name); |
||||
} |
} |
||||
|
|
||||
return template; |
return permission; |
||||
} |
} |
||||
|
|
||||
public virtual IReadOnlyList<TemplateDefinition> GetAll() |
public virtual async Task<TemplateDefinition> GetOrNullAsync(string name) |
||||
{ |
{ |
||||
return TemplateDefinitions.Value.Values.ToImmutableList(); |
Check.NotNull(name, nameof(name)); |
||||
} |
|
||||
|
|
||||
public virtual TemplateDefinition GetOrNull(string name) |
return await StaticStore.GetOrNullAsync(name) ?? await DynamicStore.GetOrNullAsync(name); |
||||
{ |
|
||||
return TemplateDefinitions.Value.GetOrDefault(name); |
|
||||
} |
} |
||||
|
|
||||
protected virtual IDictionary<string, TemplateDefinition> CreateTextTemplateDefinitions() |
public virtual async Task<IReadOnlyList<TemplateDefinition>> GetAllAsync() |
||||
{ |
{ |
||||
var templates = new Dictionary<string, TemplateDefinition>(); |
var staticTemplates = await StaticStore.GetAllAsync(); |
||||
|
var staticTemplateNames = staticTemplates |
||||
using (var scope = ServiceProvider.CreateScope()) |
.Select(p => p.Name) |
||||
{ |
.ToImmutableHashSet(); |
||||
var providers = Options |
|
||||
.DefinitionProviders |
|
||||
.Select(p => scope.ServiceProvider.GetRequiredService(p) as ITemplateDefinitionProvider) |
|
||||
.ToList(); |
|
||||
|
|
||||
var context = new TemplateDefinitionContext(templates); |
|
||||
|
|
||||
foreach (var provider in providers) |
var dynamicTemplates = await DynamicStore.GetAllAsync(); |
||||
{ |
|
||||
provider.PreDefine(context); |
|
||||
} |
|
||||
|
|
||||
foreach (var provider in providers) |
|
||||
{ |
|
||||
provider.Define(context); |
|
||||
} |
|
||||
|
|
||||
foreach (var provider in providers) |
|
||||
{ |
|
||||
provider.PostDefine(context); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return templates; |
/* We prefer static Templates over dynamics */ |
||||
|
return staticTemplates.Concat(dynamicTemplates.Where(d => !staticTemplateNames.Contains(d.Name))).ToImmutableList(); |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -0,0 +1,8 @@ |
|||||
|
namespace Volo.Abp.TextTemplating.VirtualFiles; |
||||
|
|
||||
|
public class TemplateContentFileInfo |
||||
|
{ |
||||
|
public string FileName { get; set; } |
||||
|
|
||||
|
public string FileContent { get; set; } |
||||
|
} |
||||
@ -0,0 +1,56 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Volo.Abp.VirtualFileSystem; |
||||
|
|
||||
|
namespace Volo.Abp.TextTemplating.VirtualFiles; |
||||
|
|
||||
|
public class TemplateContentFileProvider : ITransientDependency |
||||
|
{ |
||||
|
protected IVirtualFileProvider VirtualFileProvider { get; } |
||||
|
|
||||
|
public TemplateContentFileProvider(IVirtualFileProvider virtualFileProvider) |
||||
|
{ |
||||
|
VirtualFileProvider = virtualFileProvider; |
||||
|
} |
||||
|
|
||||
|
public async Task<List<TemplateContentFileInfo>> GetFilesAsync(TemplateDefinition templateDefinition) |
||||
|
{ |
||||
|
var files = new List<TemplateContentFileInfo>(); |
||||
|
|
||||
|
var virtualPath = templateDefinition.GetVirtualFilePathOrNull(); |
||||
|
if (virtualPath == null) |
||||
|
{ |
||||
|
return files; |
||||
|
} |
||||
|
|
||||
|
var fileInfo = VirtualFileProvider.GetFileInfo(virtualPath); |
||||
|
if (!fileInfo.Exists) |
||||
|
{ |
||||
|
var directoryContents = VirtualFileProvider.GetDirectoryContents(virtualPath); |
||||
|
if (!directoryContents.Exists) |
||||
|
{ |
||||
|
throw new AbpException("Could not find a file/folder at the location: " + virtualPath); |
||||
|
} |
||||
|
|
||||
|
fileInfo = new VirtualDirectoryFileInfo(virtualPath, virtualPath, DateTimeOffset.UtcNow); |
||||
|
} |
||||
|
|
||||
|
if (fileInfo.IsDirectory) |
||||
|
{ |
||||
|
//TODO: Configure file extensions.
|
||||
|
var folderReader = new VirtualFolderLocalizedTemplateContentReader(new[] { ".tpl", ".cshtml" }); |
||||
|
await folderReader.ReadContentsAsync(VirtualFileProvider, virtualPath); |
||||
|
files.AddRange(folderReader.GetFiles()); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
var singleFileReader = new FileInfoLocalizedTemplateContentReader(); |
||||
|
await singleFileReader.ReadContentsAsync(fileInfo); |
||||
|
files.Add(singleFileReader.GetFile()); |
||||
|
} |
||||
|
|
||||
|
return files; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
using Shouldly; |
||||
|
using Volo.Abp.TextTemplating.VirtualFiles; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Volo.Abp.TextTemplating.Razor; |
||||
|
|
||||
|
public class RazorTemplateContentFileProvider_Tests : TemplateContentFileProvider_Tests<RazorTextTemplatingTestModule> |
||||
|
{ |
||||
|
[Fact] |
||||
|
public async Task GetRazorFilesAsync() |
||||
|
{ |
||||
|
var definition = await TemplateDefinitionManager.GetAsync(TestTemplates.WelcomeEmail); |
||||
|
var files = await TemplateContentFileProvider.GetFilesAsync(definition); |
||||
|
files.Count.ShouldBe(2); |
||||
|
files.ShouldContain(x => x.FileName == "en.cshtml" && x.FileContent.Contains("Welcome @Model.Name to the abp.io!")); |
||||
|
files.ShouldContain(x => x.FileName == "tr.cshtml" && x.FileContent.Contains("Merhaba @Model.Name, abp.io'ya hoşgeldiniz!")); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
using Shouldly; |
||||
|
using Volo.Abp.TextTemplating.VirtualFiles; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Volo.Abp.TextTemplating.Scriban.SampleTemplates; |
||||
|
|
||||
|
public class ScribanTemplateContentFileProvider_Tests : TemplateContentFileProvider_Tests<ScribanTextTemplatingTestModule> |
||||
|
{ |
||||
|
[Fact] |
||||
|
public async Task GetScribanFilesAsync() |
||||
|
{ |
||||
|
var definition = await TemplateDefinitionManager.GetAsync(TestTemplates.WelcomeEmail); |
||||
|
var files = await TemplateContentFileProvider.GetFilesAsync(definition); |
||||
|
files.Count.ShouldBe(2); |
||||
|
files.ShouldContain(x => x.FileName == "en.tpl" && x.FileContent.Contains("Welcome {{model.name}} to the abp.io!")); |
||||
|
files.ShouldContain(x => x.FileName == "tr.tpl" && x.FileContent.Contains("Merhaba {{model.name}}, abp.io'ya hoşgeldiniz!")); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
using Shouldly; |
||||
|
using Volo.Abp.Modularity; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Volo.Abp.TextTemplating.VirtualFiles; |
||||
|
|
||||
|
public abstract class TemplateContentFileProvider_Tests<TStartupModule> : AbpTextTemplatingTestBase<TStartupModule> |
||||
|
where TStartupModule : IAbpModule |
||||
|
{ |
||||
|
protected readonly TemplateContentFileProvider TemplateContentFileProvider; |
||||
|
protected readonly ITemplateDefinitionManager TemplateDefinitionManager; |
||||
|
|
||||
|
protected TemplateContentFileProvider_Tests() |
||||
|
{ |
||||
|
TemplateContentFileProvider = GetRequiredService<TemplateContentFileProvider>(); |
||||
|
TemplateDefinitionManager = GetRequiredService<ITemplateDefinitionManager>(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetFilesAsync() |
||||
|
{ |
||||
|
var definition = await TemplateDefinitionManager.GetAsync(TestTemplates.HybridTemplateScriban); |
||||
|
|
||||
|
var files = await TemplateContentFileProvider.GetFilesAsync(definition); |
||||
|
files.Count.ShouldBe(1); |
||||
|
files.ShouldContain(x => x.FileName == "TestScribanTemplate.tpl" && x.FileContent == "Hello {{model.name}}, {{L \"HowAreYou\" }}"); |
||||
|
} |
||||
|
} |
||||
@ -1,12 +1,24 @@ |
|||||
using Volo.Abp.AspNetCore.Components.Web.Theming; |
using Volo.Abp.AspNetCore.Components.Web.Theming; |
||||
|
using Volo.Abp.AspNetCore.Components.Web.Theming.Theming; |
||||
using Volo.Abp.Modularity; |
using Volo.Abp.Modularity; |
||||
|
|
||||
namespace Volo.Abp.AspNetCore.Components.Web.BasicTheme; |
namespace Volo.Abp.AspNetCore.Components.Web.BasicTheme; |
||||
|
|
||||
[DependsOn( |
[DependsOn( |
||||
typeof(AbpAspNetCoreComponentsWebThemingModule) |
typeof(AbpAspNetCoreComponentsWebThemingModule) |
||||
)] |
)] |
||||
public class AbpAspNetCoreComponentsWebBasicThemeModule : AbpModule |
public class AbpAspNetCoreComponentsWebBasicThemeModule : AbpModule |
||||
{ |
{ |
||||
|
public override void ConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
Configure<AbpThemingOptions>(options => |
||||
|
{ |
||||
|
options.Themes.Add<BasicTheme>(); |
||||
|
|
||||
} |
if (options.DefaultThemeName == null) |
||||
|
{ |
||||
|
options.DefaultThemeName = BasicTheme.Name; |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
using System; |
||||
|
using Volo.Abp.AspNetCore.Components.Web.BasicTheme.Themes.Basic; |
||||
|
using Volo.Abp.AspNetCore.Components.Web.Theming.Layout; |
||||
|
using Volo.Abp.AspNetCore.Components.Web.Theming.Theming; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Components.Web.BasicTheme; |
||||
|
|
||||
|
[ThemeName(Name)] |
||||
|
public class BasicTheme : ITheme, ITransientDependency |
||||
|
{ |
||||
|
public const string Name = "Basic"; |
||||
|
|
||||
|
public virtual Type GetLayout(string name, bool fallbackToDefault = true) |
||||
|
{ |
||||
|
switch (name) |
||||
|
{ |
||||
|
case StandardLayouts.Application: |
||||
|
case StandardLayouts.Account: |
||||
|
case StandardLayouts.Empty: |
||||
|
return typeof(MainLayout); |
||||
|
default: |
||||
|
return fallbackToDefault ? typeof(MainLayout) : typeof(NullLayout); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
@inherits LayoutComponentBase |
||||
|
|
||||
|
@Body |
||||
@ -0,0 +1,30 @@ |
|||||
|
import { Directive, EventEmitter, HostListener, Output } from '@angular/core'; |
||||
|
|
||||
|
@Directive({ |
||||
|
standalone: true, |
||||
|
selector: '[abpCapsLock]', |
||||
|
}) |
||||
|
export class TrackCapsLockDirective { |
||||
|
@Output('abpCapsLock') capsLock = new EventEmitter<Boolean>(); |
||||
|
|
||||
|
@HostListener('window:keydown', ['$event']) |
||||
|
onKeyDown(event: KeyboardEvent): void { |
||||
|
this.capsLock.emit(this.isCapsLockOpen(event)); |
||||
|
} |
||||
|
@HostListener('window:keyup', ['$event']) |
||||
|
onKeyUp(event: KeyboardEvent): void { |
||||
|
this.capsLock.emit(this.isCapsLockOpen(event)); |
||||
|
} |
||||
|
|
||||
|
isCapsLockOpen(e): boolean { |
||||
|
var s = String.fromCharCode(e.which); |
||||
|
if ( |
||||
|
(s.toUpperCase() === s && s.toLowerCase() !== s && e.shiftKey) || |
||||
|
(s.toUpperCase() !== s && s.toLowerCase() === s && e.shiftKey) || |
||||
|
e.getModifierState('CapsLock') |
||||
|
) { |
||||
|
return true; |
||||
|
} |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
import { Directive, ElementRef, Input, inject } from '@angular/core'; |
||||
|
|
||||
|
@Directive({ |
||||
|
standalone: true, |
||||
|
selector: '[abpShowPassword]', |
||||
|
}) |
||||
|
export class ShowPasswordDirective { |
||||
|
protected readonly elementRef = inject(ElementRef); |
||||
|
|
||||
|
@Input() set abpShowPassword(visible: boolean) { |
||||
|
const element = this.elementRef.nativeElement as HTMLInputElement; |
||||
|
if (!element) return; |
||||
|
|
||||
|
element.type = visible ? 'text' : 'password'; |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue