Browse Source

1. Caching for app provider

2. Paging fix
3. Removing content in references-editor fix.
pull/282/head
Sebastian 8 years ago
parent
commit
7e3ea07718
  1. 168
      src/Squidex.Domain.Apps.Entities/AppProvider.cs
  2. 68
      src/Squidex.Infrastructure/Caching/HttpRequestCache.cs
  3. 18
      src/Squidex.Infrastructure/Caching/IRequestCache.cs
  4. 43
      src/Squidex.Infrastructure/Caching/RequestCacheExtensions.cs
  5. 1
      src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
  6. 4
      src/Squidex/Config/Domain/InfrastructureServices.cs
  7. 2
      src/Squidex/app/features/content/shared/contents-selector.component.html
  8. 2
      src/Squidex/app/features/content/shared/references-editor.component.html
  9. 133
      tests/Squidex.Infrastructure.Tests/Caching/HttpRequestCacheTests.cs

168
src/Squidex.Domain.Apps.Entities/AppProvider.cs

@ -17,6 +17,7 @@ using Squidex.Domain.Apps.Entities.Rules.Repositories;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.Schemas.Repositories; using Squidex.Domain.Apps.Entities.Schemas.Repositories;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Orleans;
@ -27,139 +28,164 @@ namespace Squidex.Domain.Apps.Entities
private readonly IGrainFactory grainFactory; private readonly IGrainFactory grainFactory;
private readonly IAppRepository appRepository; private readonly IAppRepository appRepository;
private readonly IRuleRepository ruleRepository; private readonly IRuleRepository ruleRepository;
private readonly IRequestCache requestCache;
private readonly ISchemaRepository schemaRepository; private readonly ISchemaRepository schemaRepository;
public AppProvider( public AppProvider(
IGrainFactory grainFactory, IGrainFactory grainFactory,
IAppRepository appRepository, IAppRepository appRepository,
ISchemaRepository schemaRepository, ISchemaRepository schemaRepository,
IRuleRepository ruleRepository) IRuleRepository ruleRepository,
IRequestCache requestCache)
{ {
Guard.NotNull(grainFactory, nameof(grainFactory)); Guard.NotNull(grainFactory, nameof(grainFactory));
Guard.NotNull(appRepository, nameof(appRepository)); Guard.NotNull(appRepository, nameof(appRepository));
Guard.NotNull(schemaRepository, nameof(schemaRepository)); Guard.NotNull(schemaRepository, nameof(schemaRepository));
Guard.NotNull(requestCache, nameof(requestCache));
Guard.NotNull(ruleRepository, nameof(ruleRepository)); Guard.NotNull(ruleRepository, nameof(ruleRepository));
this.grainFactory = grainFactory; this.grainFactory = grainFactory;
this.appRepository = appRepository; this.appRepository = appRepository;
this.schemaRepository = schemaRepository; this.schemaRepository = schemaRepository;
this.requestCache = requestCache;
this.ruleRepository = ruleRepository; this.ruleRepository = ruleRepository;
} }
public async Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(Guid appId, Guid id) public Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(Guid appId, Guid id)
{ {
using (Profile.Method<AppProvider>()) return requestCache.GetOrCreateAsync($"GetAppWithSchemaAsync({appId}, {id})", async () =>
{ {
var app = await grainFactory.GetGrain<IAppGrain>(appId).GetStateAsync(); using (Profile.Method<AppProvider>())
if (!IsExisting(app))
{ {
return (null, null); var app = await grainFactory.GetGrain<IAppGrain>(appId).GetStateAsync();
}
var schema = await grainFactory.GetGrain<ISchemaGrain>(id).GetStateAsync(); if (!IsExisting(app))
{
return (null, null);
}
if (!IsExisting(schema, false)) var schema = await grainFactory.GetGrain<ISchemaGrain>(id).GetStateAsync();
{
return (null, null);
}
return (app.Value, schema.Value); if (!IsExisting(schema, false))
} {
return (null, null);
}
return (app.Value, schema.Value);
}
});
} }
public async Task<IAppEntity> GetAppAsync(string appName) public Task<IAppEntity> GetAppAsync(string appName)
{ {
using (Profile.Method<AppProvider>()) return requestCache.GetOrCreateAsync($"GetAppAsync({appName})", async () =>
{ {
var appId = await GetAppIdAsync(appName); using (Profile.Method<AppProvider>())
if (appId == Guid.Empty)
{ {
return null; var appId = await GetAppIdAsync(appName);
}
var app = await grainFactory.GetGrain<IAppGrain>(appId).GetStateAsync(); if (appId == Guid.Empty)
{
return null;
}
if (!IsExisting(app)) var app = await grainFactory.GetGrain<IAppGrain>(appId).GetStateAsync();
{
return null;
}
return app.Value; if (!IsExisting(app))
} {
return null;
}
return app.Value;
}
});
} }
public async Task<ISchemaEntity> GetSchemaAsync(Guid appId, string name) public Task<ISchemaEntity> GetSchemaAsync(Guid appId, string name)
{ {
using (Profile.Method<AppProvider>()) return requestCache.GetOrCreateAsync($"GetSchemaAsync({appId}, {name})", async () =>
{ {
var schemaId = await GetSchemaIdAsync(appId, name); using (Profile.Method<AppProvider>())
if (schemaId == Guid.Empty)
{ {
return null; var schemaId = await GetSchemaIdAsync(appId, name);
}
return await GetSchemaAsync(appId, schemaId, false); if (schemaId == Guid.Empty)
} {
return null;
}
return await GetSchemaAsync(appId, schemaId, false);
}
});
} }
public async Task<ISchemaEntity> GetSchemaAsync(Guid appId, Guid id, bool allowDeleted = false) public Task<ISchemaEntity> GetSchemaAsync(Guid appId, Guid id, bool allowDeleted = false)
{ {
using (Profile.Method<AppProvider>()) return requestCache.GetOrCreateAsync($"GetSchemaAsync({appId}, {id}, {allowDeleted})", async () =>
{ {
var schema = await grainFactory.GetGrain<ISchemaGrain>(id).GetStateAsync(); using (Profile.Method<AppProvider>())
if (!IsExisting(schema, allowDeleted) || schema.Value.AppId.Id != appId)
{ {
return null; var schema = await grainFactory.GetGrain<ISchemaGrain>(id).GetStateAsync();
}
return schema.Value; if (!IsExisting(schema, allowDeleted) || schema.Value.AppId.Id != appId)
} {
return null;
}
return schema.Value;
}
});
} }
public async Task<List<ISchemaEntity>> GetSchemasAsync(Guid appId) public Task<List<ISchemaEntity>> GetSchemasAsync(Guid appId)
{ {
using (Profile.Method<AppProvider>()) return requestCache.GetOrCreateAsync($"GetSchemasAsync({appId})", async () =>
{ {
var ids = await schemaRepository.QuerySchemaIdsAsync(appId); using (Profile.Method<AppProvider>())
{
var ids = await schemaRepository.QuerySchemaIdsAsync(appId);
var schemas = var schemas =
await Task.WhenAll( await Task.WhenAll(
ids.Select(id => grainFactory.GetGrain<ISchemaGrain>(id).GetStateAsync())); ids.Select(id => grainFactory.GetGrain<ISchemaGrain>(id).GetStateAsync()));
return schemas.Where(s => IsFound(s.Value)).Select(s => s.Value).ToList(); return schemas.Where(s => IsFound(s.Value)).Select(s => s.Value).ToList();
} }
});
} }
public async Task<List<IRuleEntity>> GetRulesAsync(Guid appId) public Task<List<IRuleEntity>> GetRulesAsync(Guid appId)
{ {
using (Profile.Method<AppProvider>()) return requestCache.GetOrCreateAsync($"GetRulesAsync({appId})", async () =>
{ {
var ids = await ruleRepository.QueryRuleIdsAsync(appId); using (Profile.Method<AppProvider>())
{
var ids = await ruleRepository.QueryRuleIdsAsync(appId);
var rules = var rules =
await Task.WhenAll( await Task.WhenAll(
ids.Select(id => grainFactory.GetGrain<IRuleGrain>(id).GetStateAsync())); ids.Select(id => grainFactory.GetGrain<IRuleGrain>(id).GetStateAsync()));
return rules.Where(r => IsFound(r.Value)).Select(r => r.Value).ToList(); return rules.Where(r => IsFound(r.Value)).Select(r => r.Value).ToList();
} }
});
} }
public async Task<List<IAppEntity>> GetUserApps(string userId) public Task<List<IAppEntity>> GetUserApps(string userId)
{ {
using (Profile.Method<AppProvider>()) return requestCache.GetOrCreateAsync($"GetUserApps({userId})", async () =>
{ {
var ids = await appRepository.QueryUserAppIdsAsync(userId); using (Profile.Method<AppProvider>())
{
var ids = await appRepository.QueryUserAppIdsAsync(userId);
var apps = var apps =
await Task.WhenAll( await Task.WhenAll(
ids.Select(id => grainFactory.GetGrain<IAppGrain>(id).GetStateAsync())); ids.Select(id => grainFactory.GetGrain<IAppGrain>(id).GetStateAsync()));
return apps.Where(a => IsFound(a.Value)).Select(a => a.Value).ToList(); return apps.Where(a => IsFound(a.Value)).Select(a => a.Value).ToList();
} }
});
} }
private async Task<Guid> GetAppIdAsync(string name) private async Task<Guid> GetAppIdAsync(string name)

68
src/Squidex.Infrastructure/Caching/HttpRequestCache.cs

@ -0,0 +1,68 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.AspNetCore.Http;
namespace Squidex.Infrastructure.Caching
{
public sealed class HttpRequestCache : IRequestCache
{
private readonly IHttpContextAccessor httpContextAccessor;
public HttpRequestCache(IHttpContextAccessor httpContextAccessor)
{
Guard.NotNull(httpContextAccessor, nameof(httpContextAccessor));
this.httpContextAccessor = httpContextAccessor;
}
public void Add(object key, object value)
{
var cacheKey = GetCacheKey(key);
var items = httpContextAccessor.HttpContext?.Items;
if (items != null)
{
items[cacheKey] = value;
}
}
public void Remove(object key)
{
var cacheKey = GetCacheKey(key);
var items = httpContextAccessor.HttpContext?.Items;
if (items != null)
{
items?.Remove(cacheKey);
}
}
public bool TryGetValue(object key, out object value)
{
var cacheKey = GetCacheKey(key);
var items = httpContextAccessor.HttpContext?.Items;
if (items != null)
{
return items.TryGetValue(cacheKey, out value);
}
value = null;
return false;
}
private static string GetCacheKey(object key)
{
return $"CACHE_{key}";
}
}
}

18
src/Squidex.Infrastructure/Caching/IRequestCache.cs

@ -0,0 +1,18 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Infrastructure.Caching
{
public interface IRequestCache
{
void Add(object key, object value);
void Remove(object key);
bool TryGetValue(object key, out object value);
}
}

43
src/Squidex.Infrastructure/Caching/RequestCacheExtensions.cs

@ -0,0 +1,43 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
namespace Squidex.Infrastructure.Caching
{
public static class RequestCacheExtensions
{
public static async Task<T> GetOrCreateAsync<T>(this IRequestCache cache, object key, Func<Task<T>> task)
{
if (cache.TryGetValue(key, out var value) && value is T typedValue)
{
return typedValue;
}
typedValue = await task();
cache.Add(key, typedValue);
return typedValue;
}
public static T GetOrCreate<T>(this IRequestCache cache, object key, Func<T> task)
{
if (cache.TryGetValue(key, out var value) && value is T typedValue)
{
return typedValue;
}
typedValue = task();
cache.Add(key, typedValue);
return typedValue;
}
}
}

1
src/Squidex.Infrastructure/Squidex.Infrastructure.csproj

@ -8,6 +8,7 @@
<DebugSymbols>True</DebugSymbols> <DebugSymbols>True</DebugSymbols>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.0.2" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="2.0.1" /> <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="2.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.0.1" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="2.0.1" />
<PackageReference Include="Microsoft.Orleans.Core" Version="2.0.0" /> <PackageReference Include="Microsoft.Orleans.Core" Version="2.0.0" />

4
src/Squidex/Config/Domain/InfrastructureServices.cs

@ -12,6 +12,7 @@ using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using NodaTime; using NodaTime;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.UsageTracking; using Squidex.Infrastructure.UsageTracking;
#pragma warning disable RECS0092 // Convert field to readonly #pragma warning disable RECS0092 // Convert field to readonly
@ -31,6 +32,9 @@ namespace Squidex.Config.Domain
services.AddSingletonAs(c => new CachingUsageTracker(c.GetRequiredService<BackgroundUsageTracker>(), c.GetRequiredService<IMemoryCache>())) services.AddSingletonAs(c => new CachingUsageTracker(c.GetRequiredService<BackgroundUsageTracker>(), c.GetRequiredService<IMemoryCache>()))
.As<IUsageTracker>(); .As<IUsageTracker>();
services.AddSingletonAs<HttpRequestCache>()
.As<IRequestCache>();
services.AddSingletonAs<HttpContextAccessor>() services.AddSingletonAs<HttpContextAccessor>()
.As<IHttpContextAccessor>(); .As<IHttpContextAccessor>();

2
src/Squidex/app/features/content/shared/contents-selector.component.html

@ -62,7 +62,7 @@
</div> </div>
<div class="grid-footer"> <div class="grid-footer">
<sqx-pager [pager]="contentsState.contentsPager | async"></sqx-pager> <sqx-pager [pager]="contentsState.contentsPager | async" (prev)="goPrev()" (next)="goNext()"></sqx-pager>
</div> </div>
</ng-container> </ng-container>

2
src/Squidex/app/features/content/shared/references-editor.component.html

@ -12,7 +12,7 @@
<tr [sqxContent]="content" dnd-sortable [sortableIndex]="i" (sqxSorted)="sort($event)" <tr [sqxContent]="content" dnd-sortable [sortableIndex]="i" (sqxSorted)="sort($event)"
[language]="language" [language]="language"
[schema]="schema" [schema]="schema"
(removing)="remove(content)" (deleting)="remove(content)"
isReadOnly="true" isReadOnly="true"
isReference="true"></tr> isReference="true"></tr>
<tr class="spacer"></tr> <tr class="spacer"></tr>

133
tests/Squidex.Infrastructure.Tests/Caching/HttpRequestCacheTests.cs

@ -0,0 +1,133 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading.Tasks;
using FakeItEasy;
using Microsoft.AspNetCore.Http;
using Xunit;
namespace Squidex.Infrastructure.Caching
{
public sealed class HttpRequestCacheTests
{
private readonly IHttpContextAccessor httpContextAccessor = A.Fake<IHttpContextAccessor>();
private readonly IRequestCache sut;
private int called;
public HttpRequestCacheTests()
{
sut = new HttpRequestCache(httpContextAccessor);
}
[Fact]
public void Should_add_item_to_cache_when_context_exists()
{
SetupContext();
sut.Add("Key", 1);
var found = sut.TryGetValue("Key", out var value);
Assert.True(found);
Assert.Equal(1, value);
sut.Remove("Key");
var foundAfterRemove = sut.TryGetValue("Key", out value);
Assert.False(foundAfterRemove);
Assert.Null(value);
}
[Fact]
public void Should_not_add_item_to_cache_when_context_not_exists()
{
SetupNoContext();
sut.Add("Key", 1);
var found = sut.TryGetValue("Key", out var value);
Assert.False(found);
Assert.Null(value);
sut.Remove("Key");
var foundAfterRemove = sut.TryGetValue("Key", out value);
Assert.False(foundAfterRemove);
Assert.Null(value);
}
[Fact]
public void Should_call_creator_once_when_context_exists()
{
SetupContext();
var value1 = sut.GetOrCreate("Key", () => ++called);
var value2 = sut.GetOrCreate("Key", () => ++called);
Assert.Equal(1, called);
Assert.Equal(1, value1);
Assert.Equal(1, value2);
}
[Fact]
public void Should_call_creator_twice_when_context_not_exists()
{
SetupNoContext();
var value1 = sut.GetOrCreate("Key", () => ++called);
var value2 = sut.GetOrCreate("Key", () => ++called);
Assert.Equal(2, called);
Assert.Equal(1, value1);
Assert.Equal(2, value2);
}
[Fact]
public async Task Should_call_async_creator_once_when_context_exists()
{
SetupContext();
var value1 = await sut.GetOrCreateAsync("Key", () => Task.FromResult(++called));
var value2 = await sut.GetOrCreateAsync("Key", () => Task.FromResult(++called));
Assert.Equal(1, called);
Assert.Equal(1, value1);
Assert.Equal(1, value2);
}
[Fact]
public async Task Should_call_async_creator_twice_when_context_not_exists()
{
SetupNoContext();
var value1 = await sut.GetOrCreateAsync("Key", () => Task.FromResult(++called));
var value2 = await sut.GetOrCreateAsync("Key", () => Task.FromResult(++called));
Assert.Equal(2, called);
Assert.Equal(1, value1);
Assert.Equal(2, value2);
}
private void SetupNoContext()
{
A.CallTo(() => httpContextAccessor.HttpContext).Returns(null);
}
private void SetupContext()
{
var httpItems = new Dictionary<object, object>();
var httpContext = A.Fake<HttpContext>();
A.CallTo(() => httpContext.Items).Returns(httpItems);
A.CallTo(() => httpContextAccessor.HttpContext).Returns(httpContext);
}
}
}
Loading…
Cancel
Save