mirror of https://github.com/Squidex/squidex.git
19 changed files with 703 additions and 32 deletions
@ -0,0 +1,65 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using Lucene.Net.Analysis; |
||||
|
using Lucene.Net.Analysis.Standard; |
||||
|
using Lucene.Net.Util; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.Text |
||||
|
{ |
||||
|
public sealed class MultiLanguageAnalyzer : AnalyzerWrapper |
||||
|
{ |
||||
|
private readonly StandardAnalyzer fallbackAnalyzer; |
||||
|
private readonly Dictionary<string, Analyzer> analyzers = new Dictionary<string, Analyzer>(StringComparer.OrdinalIgnoreCase); |
||||
|
|
||||
|
public MultiLanguageAnalyzer(LuceneVersion version) |
||||
|
: base(PER_FIELD_REUSE_STRATEGY) |
||||
|
{ |
||||
|
fallbackAnalyzer = new StandardAnalyzer(version); |
||||
|
|
||||
|
foreach (var type in typeof(StandardAnalyzer).Assembly.GetTypes()) |
||||
|
{ |
||||
|
if (typeof(Analyzer).IsAssignableFrom(type)) |
||||
|
{ |
||||
|
var language = type.Namespace.Split('.').Last(); |
||||
|
|
||||
|
if (language.Length == 2) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var analyzer = Activator.CreateInstance(type, version); |
||||
|
|
||||
|
analyzers[language] = (Analyzer)analyzer; |
||||
|
} |
||||
|
catch (MissingMethodException) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected override Analyzer GetWrappedAnalyzer(string fieldName) |
||||
|
{ |
||||
|
if (fieldName.Length > 0) |
||||
|
{ |
||||
|
var analyzer = analyzers.GetOrDefault(fieldName.Substring(0, 2)) ?? fallbackAnalyzer; |
||||
|
|
||||
|
return analyzer; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
return fallbackAnalyzer; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,90 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.IO; |
||||
|
using System.IO.Compression; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Infrastructure.Assets; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.Text |
||||
|
{ |
||||
|
public static class PersistenceHelper |
||||
|
{ |
||||
|
private const string ArchiveFile = "Archive.zip"; |
||||
|
private const string LockFile = "write.lock"; |
||||
|
|
||||
|
public static async Task UploadDirectoryAsync(this IAssetStore assetStore, DirectoryInfo directory) |
||||
|
{ |
||||
|
using (var fileStream = new FileStream( |
||||
|
Path.Combine(directory.FullName, ArchiveFile), |
||||
|
FileMode.Create, |
||||
|
FileAccess.ReadWrite, |
||||
|
FileShare.None, |
||||
|
4096, |
||||
|
FileOptions.DeleteOnClose)) |
||||
|
{ |
||||
|
using (var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Create, true)) |
||||
|
{ |
||||
|
foreach (var file in directory.GetFiles()) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
if (!file.Name.Equals(ArchiveFile, StringComparison.OrdinalIgnoreCase) && !file.Name.Equals(LockFile, StringComparison.OrdinalIgnoreCase)) |
||||
|
{ |
||||
|
zipArchive.CreateEntryFromFile(file.FullName, file.Name); |
||||
|
} |
||||
|
} |
||||
|
catch (IOException) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
fileStream.Position = 0; |
||||
|
|
||||
|
await assetStore.UploadAsync(directory.Name, 0, string.Empty, fileStream, true); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static async Task DownloadAsync(this IAssetStore assetStore, DirectoryInfo directory) |
||||
|
{ |
||||
|
if (directory.Exists) |
||||
|
{ |
||||
|
directory.Delete(true); |
||||
|
} |
||||
|
|
||||
|
directory.Create(); |
||||
|
|
||||
|
using (var fileStream = new FileStream( |
||||
|
Path.Combine(directory.FullName, ArchiveFile), |
||||
|
FileMode.Create, |
||||
|
FileAccess.ReadWrite, |
||||
|
FileShare.None, |
||||
|
4096, |
||||
|
FileOptions.DeleteOnClose)) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
await assetStore.DownloadAsync(directory.Name, 0, string.Empty, fileStream); |
||||
|
|
||||
|
fileStream.Position = 0; |
||||
|
|
||||
|
using (var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Read, true)) |
||||
|
{ |
||||
|
zipArchive.ExtractToDirectory(directory.FullName); |
||||
|
} |
||||
|
} |
||||
|
catch (AssetNotFoundException) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,47 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Domain.Apps.Events.Contents; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
using Squidex.Infrastructure.Tasks; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.Text |
||||
|
{ |
||||
|
public sealed class TextIndexer : IEventConsumer |
||||
|
{ |
||||
|
public string Name |
||||
|
{ |
||||
|
get { return GetType().Name; } |
||||
|
} |
||||
|
|
||||
|
public string EventsFilter |
||||
|
{ |
||||
|
get { return "^content-"; } |
||||
|
} |
||||
|
|
||||
|
public Task ClearAsync() |
||||
|
{ |
||||
|
return TaskHelper.Done; |
||||
|
} |
||||
|
|
||||
|
public Task On(Envelope<IEvent> @event) |
||||
|
{ |
||||
|
switch (@event.Payload) |
||||
|
{ |
||||
|
case ContentCreated contentCreated: |
||||
|
break; |
||||
|
case ContentUpdated contentUpdated: |
||||
|
break; |
||||
|
case ContentUpdateProposed contentUpdateProposed: |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,268 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.IO; |
||||
|
using System.Threading.Tasks; |
||||
|
using Lucene.Net.Analysis; |
||||
|
using Lucene.Net.Analysis.Standard; |
||||
|
using Lucene.Net.Documents; |
||||
|
using Lucene.Net.Index; |
||||
|
using Lucene.Net.QueryParsers.Classic; |
||||
|
using Lucene.Net.Search; |
||||
|
using Lucene.Net.Store; |
||||
|
using Lucene.Net.Util; |
||||
|
using Squidex.Domain.Apps.Core; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
using Squidex.Domain.Apps.Core.Schemas; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Assets; |
||||
|
using Squidex.Infrastructure.Json.Objects; |
||||
|
using Squidex.Infrastructure.Orleans; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.Text |
||||
|
{ |
||||
|
public sealed class TextIndexerGrain : GrainOfGuid |
||||
|
{ |
||||
|
private const LuceneVersion Version = LuceneVersion.LUCENE_48; |
||||
|
private const int MaxResults = 2000; |
||||
|
private const int MaxUpdates = 100; |
||||
|
private static readonly HashSet<string> IdFields = new HashSet<string>(); |
||||
|
private static readonly Analyzer Analyzer = new MultiLanguageAnalyzer(Version); |
||||
|
private readonly IAssetStore assetStore; |
||||
|
private DirectoryInfo directory; |
||||
|
private IndexWriter indexWriter; |
||||
|
private IndexReader indexReader; |
||||
|
private QueryParser queryParser; |
||||
|
private int currentAppVersion; |
||||
|
private int currentSchemaVersion; |
||||
|
private int updates; |
||||
|
|
||||
|
public TextIndexerGrain(IAssetStore assetStore) |
||||
|
{ |
||||
|
Guard.NotNull(assetStore, nameof(assetStore)); |
||||
|
|
||||
|
this.assetStore = assetStore; |
||||
|
} |
||||
|
|
||||
|
public override Task OnActivateAsync() |
||||
|
{ |
||||
|
RegisterTimer(_ => FlushAsync(), null, TimeSpan.Zero, TimeSpan.FromMinutes(10)); |
||||
|
|
||||
|
return base.OnActivateAsync(); |
||||
|
} |
||||
|
|
||||
|
public override async Task OnDeactivateAsync() |
||||
|
{ |
||||
|
await DeactivateAsync(true); |
||||
|
} |
||||
|
|
||||
|
protected override async Task OnActivateAsync(Guid key) |
||||
|
{ |
||||
|
directory = new DirectoryInfo(Path.Combine(Path.GetTempPath(), $"Index_{key}")); |
||||
|
|
||||
|
await assetStore.DownloadAsync(directory); |
||||
|
|
||||
|
indexWriter = new IndexWriter(FSDirectory.Open(directory), new IndexWriterConfig(Version, Analyzer)); |
||||
|
indexReader = indexWriter.GetReader(true); |
||||
|
} |
||||
|
|
||||
|
public Task DeleteContentAsync(Guid id) |
||||
|
{ |
||||
|
indexWriter.DeleteDocuments(new Term("id", id.ToString())); |
||||
|
|
||||
|
return TryFlushAsync(); |
||||
|
} |
||||
|
|
||||
|
public Task AddContentAsync(Guid id, NamedContentData data, bool isUpdate, bool isDraft) |
||||
|
{ |
||||
|
var idString = id.ToString(); |
||||
|
|
||||
|
if (isUpdate) |
||||
|
{ |
||||
|
indexWriter.DeleteDocuments(new Term("id", idString)); |
||||
|
} |
||||
|
|
||||
|
var document = new Document(); |
||||
|
|
||||
|
document.AddStringField("id", idString, Field.Store.YES); |
||||
|
document.AddInt32Field("draft", isDraft ? 1 : 0, Field.Store.YES); |
||||
|
|
||||
|
foreach (var field in data) |
||||
|
{ |
||||
|
foreach (var fieldValue in field.Value) |
||||
|
{ |
||||
|
var value = fieldValue.Value; |
||||
|
|
||||
|
if (value.Type == JsonValueType.String) |
||||
|
{ |
||||
|
var fieldName = BuildFieldName(fieldValue.Key, field.Key); |
||||
|
|
||||
|
document.AddTextField(fieldName, fieldValue.Value.ToString(), Field.Store.YES); |
||||
|
} |
||||
|
else if (value.Type == JsonValueType.Object) |
||||
|
{ |
||||
|
foreach (var property in (JsonObject)value) |
||||
|
{ |
||||
|
if (property.Value.Type == JsonValueType.String) |
||||
|
{ |
||||
|
var fieldName = BuildFieldName(fieldValue.Key, field.Key, property.Key); |
||||
|
|
||||
|
document.AddTextField(fieldName, property.Value.ToString(), Field.Store.YES); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
indexWriter.AddDocument(document); |
||||
|
|
||||
|
return TryFlushAsync(); |
||||
|
} |
||||
|
|
||||
|
public Task<List<Guid>> SearchAsync(string term, int appVersion, int schemaVersion, J<Schema> schema, List<string> languages) |
||||
|
{ |
||||
|
var query = BuildQuery(term, appVersion, schemaVersion, schema, languages); |
||||
|
|
||||
|
var result = new List<Guid>(); |
||||
|
|
||||
|
if (indexReader != null) |
||||
|
{ |
||||
|
var hits = new IndexSearcher(indexReader).Search(query, MaxResults).ScoreDocs; |
||||
|
|
||||
|
foreach (var hit in hits) |
||||
|
{ |
||||
|
var document = indexReader.Document(hit.Doc); |
||||
|
|
||||
|
var idField = document.GetField("id")?.GetStringValue(); |
||||
|
|
||||
|
if (idField != null && Guid.TryParse(idField, out var guid)) |
||||
|
{ |
||||
|
result.Add(guid); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return Task.FromResult(result); |
||||
|
} |
||||
|
|
||||
|
private Query BuildQuery(string query, int appVersion, int schemaVersion, J<Schema> schema, List<string> language) |
||||
|
{ |
||||
|
if (queryParser == null || currentAppVersion != appVersion || currentSchemaVersion != schemaVersion) |
||||
|
{ |
||||
|
var fields = BuildFields(schema, language); |
||||
|
|
||||
|
queryParser = new MultiFieldQueryParser(Version, fields, Analyzer); |
||||
|
|
||||
|
currentAppVersion = appVersion; |
||||
|
currentSchemaVersion = schemaVersion; |
||||
|
} |
||||
|
|
||||
|
return queryParser.Parse(query); |
||||
|
} |
||||
|
|
||||
|
private string[] BuildFields(Schema schema, IEnumerable<string> languages) |
||||
|
{ |
||||
|
var fieldNames = new List<string>(); |
||||
|
|
||||
|
var iv = InvariantPartitioning.Instance.Master.Key; |
||||
|
|
||||
|
foreach (var field in schema.Fields) |
||||
|
{ |
||||
|
if (field.RawProperties is StringFieldProperties) |
||||
|
{ |
||||
|
if (field.Partitioning.Equals(Partitioning.Invariant)) |
||||
|
{ |
||||
|
fieldNames.Add(BuildFieldName(iv, field.Name)); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
foreach (var language in languages) |
||||
|
{ |
||||
|
fieldNames.Add(BuildFieldName(language, field.Name)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
else if (field is IArrayField arrayField) |
||||
|
{ |
||||
|
foreach (var nested in arrayField.Fields) |
||||
|
{ |
||||
|
if (nested.RawProperties is StringFieldProperties) |
||||
|
{ |
||||
|
if (field.Partitioning.Equals(Partitioning.Invariant)) |
||||
|
{ |
||||
|
fieldNames.Add(BuildFieldName(iv, field.Name, nested.Name)); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
foreach (var language in languages) |
||||
|
{ |
||||
|
fieldNames.Add(BuildFieldName(language, field.Name, nested.Name)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return fieldNames.ToArray(); |
||||
|
} |
||||
|
|
||||
|
private async Task TryFlushAsync() |
||||
|
{ |
||||
|
updates++; |
||||
|
|
||||
|
if (updates >= MaxUpdates) |
||||
|
{ |
||||
|
await FlushAsync(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task FlushAsync() |
||||
|
{ |
||||
|
if (updates > 0 && indexWriter != null) |
||||
|
{ |
||||
|
indexWriter.Flush(true, true); |
||||
|
indexWriter.Commit(); |
||||
|
|
||||
|
indexReader?.Dispose(); |
||||
|
indexReader = indexWriter.GetReader(true); |
||||
|
|
||||
|
await assetStore.UploadDirectoryAsync(directory); |
||||
|
|
||||
|
updates = 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task DeactivateAsync(bool deleteFolder = false) |
||||
|
{ |
||||
|
await TryFlushAsync(); |
||||
|
|
||||
|
indexWriter?.Dispose(); |
||||
|
indexWriter = null; |
||||
|
|
||||
|
indexReader?.Dispose(); |
||||
|
indexReader = null; |
||||
|
|
||||
|
if (deleteFolder && directory.Exists) |
||||
|
{ |
||||
|
directory.Delete(true); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static string BuildFieldName(string language, string name) |
||||
|
{ |
||||
|
return $"{language}_{name}"; |
||||
|
} |
||||
|
|
||||
|
private static string BuildFieldName(string language, string name, string nested) |
||||
|
{ |
||||
|
return $"{language}_{name}_{nested}"; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,165 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Domain.Apps.Core; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
using Squidex.Domain.Apps.Core.Schemas; |
||||
|
using Squidex.Infrastructure.Assets; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.Text |
||||
|
{ |
||||
|
public class TextIndexerGrainTests : IDisposable |
||||
|
{ |
||||
|
private readonly Schema schema = |
||||
|
new Schema("test") |
||||
|
.AddString(1, "test", Partitioning.Invariant) |
||||
|
.AddString(2, "localized", Partitioning.Language); |
||||
|
private readonly List<string> languages = new List<string> { "de", "en" }; |
||||
|
private readonly Guid schemaId = Guid.NewGuid(); |
||||
|
private readonly List<Guid> ids1 = new List<Guid> { Guid.NewGuid() }; |
||||
|
private readonly List<Guid> ids2 = new List<Guid> { Guid.NewGuid() }; |
||||
|
private readonly IAssetStore assetStore = new MemoryAssetStore(); |
||||
|
private readonly TextIndexerGrain sut; |
||||
|
|
||||
|
public TextIndexerGrainTests() |
||||
|
{ |
||||
|
sut = new TextIndexerGrain(assetStore); |
||||
|
sut.ActivateAsync(schemaId).Wait(); |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
sut.OnDeactivateAsync().Wait(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_read_index_and_retrieve() |
||||
|
{ |
||||
|
await AddInvariantContent(); |
||||
|
|
||||
|
await sut.DeactivateAsync(); |
||||
|
|
||||
|
var other = new TextIndexerGrain(assetStore); |
||||
|
try |
||||
|
{ |
||||
|
await other.ActivateAsync(schemaId); |
||||
|
|
||||
|
var helloIds = await other.SearchAsync("Hello", 0, 0, schema, languages); |
||||
|
|
||||
|
Assert.Equal(ids1, helloIds); |
||||
|
|
||||
|
var worldIds = await other.SearchAsync("World", 0, 0, schema, languages); |
||||
|
|
||||
|
Assert.Equal(ids2, worldIds); |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
await other.OnDeactivateAsync(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_index_invariant_content_and_retrieve() |
||||
|
{ |
||||
|
await AddInvariantContent(); |
||||
|
|
||||
|
var helloIds = await sut.SearchAsync("Hello", 0, 0, schema, languages); |
||||
|
|
||||
|
Assert.Equal(ids1, helloIds); |
||||
|
|
||||
|
var worldIds = await sut.SearchAsync("World", 0, 0, schema, languages); |
||||
|
|
||||
|
Assert.Equal(ids2, worldIds); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_delete_documents_from_index() |
||||
|
{ |
||||
|
await AddInvariantContent(); |
||||
|
|
||||
|
await sut.DeleteContentAsync(ids1[0]); |
||||
|
await sut.FlushAsync(); |
||||
|
|
||||
|
var helloIds = await sut.SearchAsync("Hello", 0, 0, schema, languages); |
||||
|
|
||||
|
Assert.Empty(helloIds); |
||||
|
|
||||
|
var worldIds = await sut.SearchAsync("World", 0, 0, schema, languages); |
||||
|
|
||||
|
Assert.Equal(ids2, worldIds); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_index_localized_content_and_retrieve() |
||||
|
{ |
||||
|
await AddLocalizedContent(); |
||||
|
|
||||
|
var german1 = await sut.SearchAsync("Stadt", 0, 0, schema, languages); |
||||
|
var german2 = await sut.SearchAsync("and", 0, 0, schema, languages); |
||||
|
|
||||
|
var germanStopwordsIds = await sut.SearchAsync("und", 0, 0, schema, languages); |
||||
|
|
||||
|
Assert.Equal(ids1, german1); |
||||
|
Assert.Equal(ids1, german2); |
||||
|
|
||||
|
Assert.Equal(ids2, germanStopwordsIds); |
||||
|
|
||||
|
var english1 = await sut.SearchAsync("City", 0, 0, schema, languages); |
||||
|
var english2 = await sut.SearchAsync("und", 0, 0, schema, languages); |
||||
|
|
||||
|
var englishStopwordsIds = await sut.SearchAsync("and", 0, 0, schema, languages); |
||||
|
|
||||
|
Assert.Equal(ids2, english1); |
||||
|
Assert.Equal(ids2, english2); |
||||
|
|
||||
|
Assert.Equal(ids1, englishStopwordsIds); |
||||
|
} |
||||
|
|
||||
|
private async Task AddLocalizedContent() |
||||
|
{ |
||||
|
var germanData = |
||||
|
new NamedContentData() |
||||
|
.AddField("localized", |
||||
|
new ContentFieldData() |
||||
|
.AddValue("de", "Stadt und Umgebung and whatever")); |
||||
|
|
||||
|
var englishData = |
||||
|
new NamedContentData() |
||||
|
.AddField("localized", |
||||
|
new ContentFieldData() |
||||
|
.AddValue("en", "City and Surroundings und sonstiges")); |
||||
|
|
||||
|
await sut.AddContentAsync(ids1[0], germanData, false, false); |
||||
|
await sut.AddContentAsync(ids2[0], englishData, false, false); |
||||
|
await sut.FlushAsync(); |
||||
|
} |
||||
|
|
||||
|
private async Task AddInvariantContent() |
||||
|
{ |
||||
|
var data1 = |
||||
|
new NamedContentData() |
||||
|
.AddField("test", |
||||
|
new ContentFieldData() |
||||
|
.AddValue("iv", "Hello")); |
||||
|
|
||||
|
var data2 = |
||||
|
new NamedContentData() |
||||
|
.AddField("test", |
||||
|
new ContentFieldData() |
||||
|
.AddValue("iv", "World")); |
||||
|
|
||||
|
await sut.AddContentAsync(ids1[0], data1, false, false); |
||||
|
await sut.AddContentAsync(ids2[0], data2, false, false); |
||||
|
|
||||
|
await sut.FlushAsync(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue